Skip to content

Commit 445f142

Browse files
Copilotvcastellm
andcommitted
Add comprehensive integration tests for @after schedule feature
Co-authored-by: vcastellm <47026+vcastellm@users.noreply.github.com>
1 parent bd54e10 commit 445f142

File tree

1 file changed

+162
-0
lines changed

1 file changed

+162
-0
lines changed

extcron/integration_test.go

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
package extcron
2+
3+
import (
4+
"testing"
5+
"time"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
// TestAfterScheduleEndToEnd tests the complete @after schedule flow
12+
// from parsing to execution time calculation
13+
func TestAfterScheduleEndToEnd(t *testing.T) {
14+
tests := []struct {
15+
name string
16+
schedule string
17+
currentTime time.Time
18+
expectNextAt time.Time
19+
expectImmediate bool
20+
expectNever bool
21+
description string
22+
}{
23+
{
24+
name: "future job should run at scheduled time",
25+
schedule: "@after 2025-01-01T00:00:00Z <PT2H",
26+
currentTime: time.Date(2024, 12, 31, 23, 0, 0, 0, time.UTC),
27+
expectNextAt: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
28+
description: "Job created before scheduled time should run at that time",
29+
},
30+
{
31+
name: "past job within grace period should run immediately",
32+
schedule: "@after 2025-01-01T00:00:00Z <PT2H",
33+
currentTime: time.Date(2025, 1, 1, 1, 0, 0, 0, time.UTC),
34+
expectImmediate: true,
35+
description: "Job created 1 hour after scheduled time (within 2h grace) should run immediately",
36+
},
37+
{
38+
name: "past job at end of grace period should run immediately",
39+
schedule: "@after 2025-01-01T00:00:00Z <PT2H",
40+
currentTime: time.Date(2025, 1, 1, 1, 59, 59, 0, time.UTC),
41+
expectImmediate: true,
42+
description: "Job created just before end of grace period should run immediately",
43+
},
44+
{
45+
name: "past job beyond grace period should never run",
46+
schedule: "@after 2025-01-01T00:00:00Z <PT2H",
47+
currentTime: time.Date(2025, 1, 1, 2, 0, 1, 0, time.UTC),
48+
expectNever: true,
49+
description: "Job created 1 second after grace period should never run",
50+
},
51+
{
52+
name: "exactly at scheduled time should run immediately",
53+
schedule: "@after 2025-01-01T00:00:00Z <PT2H",
54+
currentTime: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC),
55+
expectImmediate: true,
56+
description: "Job created exactly at scheduled time should run immediately",
57+
},
58+
{
59+
name: "small grace period example",
60+
schedule: "@after 2025-01-01T12:00:00Z <PT5M",
61+
currentTime: time.Date(2025, 1, 1, 12, 3, 0, 0, time.UTC),
62+
expectImmediate: true,
63+
description: "Job with 5 minute grace period created 3 minutes late should run immediately",
64+
},
65+
{
66+
name: "small grace period expired",
67+
schedule: "@after 2025-01-01T12:00:00Z <PT5M",
68+
currentTime: time.Date(2025, 1, 1, 12, 6, 0, 0, time.UTC),
69+
expectNever: true,
70+
description: "Job with 5 minute grace period created 6 minutes late should never run",
71+
},
72+
{
73+
name: "large grace period example",
74+
schedule: "@after 2025-01-01T00:00:00Z <P30D",
75+
currentTime: time.Date(2025, 1, 15, 0, 0, 0, 0, time.UTC),
76+
expectImmediate: true,
77+
description: "Job with 30 day grace period created 15 days late should run immediately",
78+
},
79+
}
80+
81+
for _, tt := range tests {
82+
t.Run(tt.name, func(t *testing.T) {
83+
// Parse the schedule
84+
schedule, err := Parse(tt.schedule)
85+
require.NoError(t, err, "Failed to parse schedule: %s", tt.schedule)
86+
87+
// Calculate the next execution time
88+
nextRun := schedule.Next(tt.currentTime)
89+
90+
if tt.expectImmediate {
91+
// Should run immediately (next run == current time)
92+
assert.Equal(t, tt.currentTime, nextRun, tt.description)
93+
} else if tt.expectNever {
94+
// Should never run (zero time)
95+
assert.True(t, nextRun.IsZero(), "Expected zero time (never run), got: %v. %s", nextRun, tt.description)
96+
} else {
97+
// Should run at the expected time
98+
assert.Equal(t, tt.expectNextAt, nextRun, tt.description)
99+
}
100+
})
101+
}
102+
}
103+
104+
// TestAfterScheduleUseCase demonstrates the primary use case from the issue:
105+
// Jobs that are created slightly after their scheduled time due to network latency
106+
func TestAfterScheduleUseCase(t *testing.T) {
107+
// Scenario: User wants to create a job that runs at a specific time,
108+
// but due to network latency, the job is created a few seconds after that time.
109+
110+
// Job should run at 2025-01-01 12:00:00
111+
scheduledTime := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC)
112+
113+
// But job is created 30 seconds late due to network latency
114+
creationTime := scheduledTime.Add(30 * time.Second)
115+
116+
// With a 5-minute grace period, the job should still run immediately
117+
schedule := "@after " + scheduledTime.Format(time.RFC3339) + " <PT5M"
118+
119+
sched, err := Parse(schedule)
120+
require.NoError(t, err)
121+
122+
nextRun := sched.Next(creationTime)
123+
124+
// The job should run immediately (at creation time)
125+
assert.Equal(t, creationTime, nextRun,
126+
"Job created 30s late with 5min grace period should run immediately")
127+
128+
// If the job is created much later (10 minutes), it should not run
129+
lateCreationTime := scheduledTime.Add(10 * time.Minute)
130+
nextRun = sched.Next(lateCreationTime)
131+
assert.True(t, nextRun.IsZero(),
132+
"Job created 10 minutes late with 5min grace period should never run")
133+
}
134+
135+
// TestAfterScheduleVsAtSchedule compares @after with @at behavior
136+
func TestAfterScheduleVsAtSchedule(t *testing.T) {
137+
scheduledTime := time.Date(2025, 1, 1, 12, 0, 0, 0, time.UTC)
138+
pastTime := scheduledTime.Add(-30 * time.Minute) // 30 minutes before scheduled time
139+
futureTime := scheduledTime.Add(30 * time.Minute) // 30 minutes after scheduled time
140+
141+
// Parse @at schedule
142+
atSchedule, err := Parse("@at " + scheduledTime.Format(time.RFC3339))
143+
require.NoError(t, err)
144+
145+
// Parse @after schedule with 1 hour grace period
146+
afterSchedule, err := Parse("@after " + scheduledTime.Format(time.RFC3339) + " <PT1H")
147+
require.NoError(t, err)
148+
149+
// Before scheduled time: both should return scheduled time
150+
atNext := atSchedule.Next(pastTime)
151+
afterNext := afterSchedule.Next(pastTime)
152+
assert.Equal(t, scheduledTime, atNext, "@at should return scheduled time when before")
153+
assert.Equal(t, scheduledTime, afterNext, "@after should return scheduled time when before")
154+
155+
// After scheduled time (within grace period for @after):
156+
// @at returns zero time (never runs)
157+
// @after returns current time (runs immediately)
158+
atNext = atSchedule.Next(futureTime)
159+
afterNext = afterSchedule.Next(futureTime)
160+
assert.True(t, atNext.IsZero(), "@at should return zero time (never run) when after")
161+
assert.Equal(t, futureTime, afterNext, "@after should return current time (run immediately) when within grace")
162+
}

0 commit comments

Comments
 (0)