mirror of https://github.com/go-co-op/gocron.git
1173 lines
26 KiB
Go
1173 lines
26 KiB
Go
package gocron
|
|
|
|
import (
|
|
"math/rand"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jonboulle/clockwork"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestDurationJob_next(t *testing.T) {
|
|
tests := []time.Duration{
|
|
time.Millisecond,
|
|
time.Second,
|
|
100 * time.Second,
|
|
1000 * time.Second,
|
|
5 * time.Second,
|
|
50 * time.Second,
|
|
time.Minute,
|
|
5 * time.Minute,
|
|
100 * time.Minute,
|
|
time.Hour,
|
|
2 * time.Hour,
|
|
100 * time.Hour,
|
|
1000 * time.Hour,
|
|
}
|
|
|
|
lastRun := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
|
|
|
|
for _, duration := range tests {
|
|
t.Run(duration.String(), func(t *testing.T) {
|
|
d := durationJob{duration: duration}
|
|
next := d.next(lastRun)
|
|
expected := lastRun.Add(duration)
|
|
|
|
assert.Equal(t, expected, next)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDailyJob_next(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
interval uint
|
|
atTimes []time.Time
|
|
lastRun time.Time
|
|
expectedNextRun time.Time
|
|
expectedDurationToNextRun time.Duration
|
|
}{
|
|
{
|
|
"daily at midnight",
|
|
1,
|
|
[]time.Time{
|
|
time.Date(0, 0, 0, 0, 0, 0, 0, time.UTC),
|
|
},
|
|
time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
|
|
time.Date(2000, 1, 2, 0, 0, 0, 0, time.UTC),
|
|
24 * time.Hour,
|
|
},
|
|
{
|
|
"daily multiple at times",
|
|
1,
|
|
[]time.Time{
|
|
time.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),
|
|
time.Date(0, 0, 0, 12, 30, 0, 0, time.UTC),
|
|
},
|
|
time.Date(2000, 1, 1, 5, 30, 0, 0, time.UTC),
|
|
time.Date(2000, 1, 1, 12, 30, 0, 0, time.UTC),
|
|
7 * time.Hour,
|
|
},
|
|
{
|
|
"every 2 days multiple at times",
|
|
2,
|
|
[]time.Time{
|
|
time.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),
|
|
time.Date(0, 0, 0, 12, 30, 0, 0, time.UTC),
|
|
},
|
|
time.Date(2000, 1, 1, 12, 30, 0, 0, time.UTC),
|
|
time.Date(2000, 1, 3, 5, 30, 0, 0, time.UTC),
|
|
41 * time.Hour,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
d := dailyJob{
|
|
interval: tt.interval,
|
|
atTimes: tt.atTimes,
|
|
}
|
|
|
|
next := d.next(tt.lastRun)
|
|
assert.Equal(t, tt.expectedNextRun, next)
|
|
assert.Equal(t, tt.expectedDurationToNextRun, next.Sub(tt.lastRun))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWeeklyJob_next(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
interval uint
|
|
daysOfWeek []time.Weekday
|
|
atTimes []time.Time
|
|
lastRun time.Time
|
|
expectedNextRun time.Time
|
|
expectedDurationToNextRun time.Duration
|
|
}{
|
|
{
|
|
"last run Monday, next run is Thursday",
|
|
1,
|
|
[]time.Weekday{time.Monday, time.Thursday},
|
|
[]time.Time{
|
|
time.Date(0, 0, 0, 0, 0, 0, 0, time.UTC),
|
|
},
|
|
time.Date(2000, 1, 3, 0, 0, 0, 0, time.UTC),
|
|
time.Date(2000, 1, 6, 0, 0, 0, 0, time.UTC),
|
|
3 * 24 * time.Hour,
|
|
},
|
|
{
|
|
"last run Thursday, next run is Monday",
|
|
1,
|
|
[]time.Weekday{time.Monday, time.Thursday},
|
|
[]time.Time{
|
|
time.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),
|
|
},
|
|
time.Date(2000, 1, 6, 5, 30, 0, 0, time.UTC),
|
|
time.Date(2000, 1, 10, 5, 30, 0, 0, time.UTC),
|
|
4 * 24 * time.Hour,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
w := weeklyJob{
|
|
interval: tt.interval,
|
|
daysOfWeek: tt.daysOfWeek,
|
|
atTimes: tt.atTimes,
|
|
}
|
|
|
|
next := w.next(tt.lastRun)
|
|
assert.Equal(t, tt.expectedNextRun, next)
|
|
assert.Equal(t, tt.expectedDurationToNextRun, next.Sub(tt.lastRun))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMonthlyJob_next(t *testing.T) {
|
|
americaChicago, err := time.LoadLocation("America/Chicago")
|
|
require.NoError(t, err)
|
|
|
|
tests := []struct {
|
|
name string
|
|
interval uint
|
|
days []int
|
|
daysFromEnd []int
|
|
atTimes []time.Time
|
|
lastRun time.Time
|
|
expectedNextRun time.Time
|
|
expectedDurationToNextRun time.Duration
|
|
}{
|
|
{
|
|
"same day - before at time",
|
|
1,
|
|
[]int{1},
|
|
nil,
|
|
[]time.Time{
|
|
time.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),
|
|
},
|
|
time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
|
|
time.Date(2000, 1, 1, 5, 30, 0, 0, time.UTC),
|
|
5*time.Hour + 30*time.Minute,
|
|
},
|
|
{
|
|
"same day - after at time, runs next available date",
|
|
1,
|
|
[]int{1, 10},
|
|
nil,
|
|
[]time.Time{
|
|
time.Date(0, 0, 0, 0, 0, 0, 0, time.UTC),
|
|
},
|
|
time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
|
|
time.Date(2000, 1, 10, 0, 0, 0, 0, time.UTC),
|
|
9 * 24 * time.Hour,
|
|
},
|
|
{
|
|
"same day - after at time, runs next available date, following interval month",
|
|
2,
|
|
[]int{1},
|
|
nil,
|
|
[]time.Time{
|
|
time.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),
|
|
},
|
|
time.Date(2000, 1, 1, 5, 30, 0, 0, time.UTC),
|
|
time.Date(2000, 3, 1, 5, 30, 0, 0, time.UTC),
|
|
60 * 24 * time.Hour,
|
|
},
|
|
{
|
|
"daylight savings time",
|
|
1,
|
|
[]int{5},
|
|
nil,
|
|
[]time.Time{
|
|
time.Date(0, 0, 0, 5, 30, 0, 0, americaChicago),
|
|
},
|
|
time.Date(2023, 11, 1, 0, 0, 0, 0, americaChicago),
|
|
time.Date(2023, 11, 5, 5, 30, 0, 0, americaChicago),
|
|
4*24*time.Hour + 6*time.Hour + 30*time.Minute,
|
|
},
|
|
{
|
|
"negative days",
|
|
1,
|
|
nil,
|
|
[]int{-1, -3, -5},
|
|
[]time.Time{
|
|
time.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),
|
|
},
|
|
time.Date(2000, 1, 29, 5, 30, 0, 0, time.UTC),
|
|
time.Date(2000, 1, 31, 5, 30, 0, 0, time.UTC),
|
|
2 * 24 * time.Hour,
|
|
},
|
|
{
|
|
"day not in current month, runs next month (leap year)",
|
|
1,
|
|
[]int{31},
|
|
nil,
|
|
[]time.Time{
|
|
time.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),
|
|
},
|
|
time.Date(2000, 1, 31, 5, 30, 0, 0, time.UTC),
|
|
time.Date(2000, 3, 31, 5, 30, 0, 0, time.UTC),
|
|
29*24*time.Hour + 31*24*time.Hour,
|
|
},
|
|
{
|
|
"multiple days not in order",
|
|
1,
|
|
[]int{10, 7, 19, 2},
|
|
nil,
|
|
[]time.Time{
|
|
time.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),
|
|
},
|
|
time.Date(2000, 1, 2, 5, 30, 0, 0, time.UTC),
|
|
time.Date(2000, 1, 7, 5, 30, 0, 0, time.UTC),
|
|
5 * 24 * time.Hour,
|
|
},
|
|
{
|
|
"day not in next interval month, selects next available option, skips Feb, April & June",
|
|
2,
|
|
[]int{31},
|
|
nil,
|
|
[]time.Time{
|
|
time.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),
|
|
},
|
|
time.Date(1999, 12, 31, 5, 30, 0, 0, time.UTC),
|
|
time.Date(2000, 8, 31, 5, 30, 0, 0, time.UTC),
|
|
244 * 24 * time.Hour,
|
|
},
|
|
{
|
|
"handle -1 with differing month's day count",
|
|
1,
|
|
nil,
|
|
[]int{-1},
|
|
[]time.Time{
|
|
time.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),
|
|
},
|
|
time.Date(2024, 1, 31, 5, 30, 0, 0, time.UTC),
|
|
time.Date(2024, 2, 29, 5, 30, 0, 0, time.UTC),
|
|
29 * 24 * time.Hour,
|
|
},
|
|
{
|
|
"handle -1 with another differing month's day count",
|
|
1,
|
|
nil,
|
|
[]int{-1},
|
|
[]time.Time{
|
|
time.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),
|
|
},
|
|
time.Date(2024, 2, 29, 5, 30, 0, 0, time.UTC),
|
|
time.Date(2024, 3, 31, 5, 30, 0, 0, time.UTC),
|
|
31 * 24 * time.Hour,
|
|
},
|
|
{
|
|
"handle -1 every 3 months next run in February",
|
|
3,
|
|
nil,
|
|
[]int{-1},
|
|
[]time.Time{
|
|
time.Date(0, 0, 0, 5, 30, 0, 0, time.UTC),
|
|
},
|
|
time.Date(2023, 11, 30, 5, 30, 0, 0, time.UTC),
|
|
time.Date(2024, 2, 29, 5, 30, 0, 0, time.UTC),
|
|
91 * 24 * time.Hour,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
m := monthlyJob{
|
|
interval: tt.interval,
|
|
days: tt.days,
|
|
daysFromEnd: tt.daysFromEnd,
|
|
atTimes: tt.atTimes,
|
|
}
|
|
|
|
next := m.next(tt.lastRun)
|
|
assert.Equal(t, tt.expectedNextRun, next)
|
|
assert.Equal(t, tt.expectedDurationToNextRun, next.Sub(tt.lastRun))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDurationRandomJob_next(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
min time.Duration
|
|
max time.Duration
|
|
lastRun time.Time
|
|
expectedMin time.Time
|
|
expectedMax time.Time
|
|
}{
|
|
{
|
|
"min 1s, max 5s",
|
|
time.Second,
|
|
5 * time.Second,
|
|
time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
|
|
time.Date(2000, 1, 1, 0, 0, 1, 0, time.UTC),
|
|
time.Date(2000, 1, 1, 0, 0, 5, 0, time.UTC),
|
|
},
|
|
{
|
|
"min 100ms, max 1s",
|
|
100 * time.Millisecond,
|
|
1 * time.Second,
|
|
time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
|
|
time.Date(2000, 1, 1, 0, 0, 0, 100000000, time.UTC),
|
|
time.Date(2000, 1, 1, 0, 0, 1, 0, time.UTC),
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
rj := durationRandomJob{
|
|
min: tt.min,
|
|
max: tt.max,
|
|
rand: rand.New(rand.NewSource(time.Now().UnixNano())), // nolint:gosec
|
|
}
|
|
|
|
for i := 0; i < 100; i++ {
|
|
next := rj.next(tt.lastRun)
|
|
assert.GreaterOrEqual(t, next, tt.expectedMin)
|
|
assert.LessOrEqual(t, next, tt.expectedMax)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOneTimeJob_next(t *testing.T) {
|
|
otj := oneTimeJob{}
|
|
assert.Zero(t, otj.next(time.Time{}))
|
|
}
|
|
|
|
func TestJob_RunNow_Error(t *testing.T) {
|
|
s := newTestScheduler(t)
|
|
|
|
j, err := s.NewJob(
|
|
DurationJob(time.Second),
|
|
NewTask(func() {}),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, s.Shutdown())
|
|
|
|
assert.EqualError(t, j.RunNow(), ErrJobRunNowFailed.Error())
|
|
}
|
|
|
|
func TestJob_LastRun(t *testing.T) {
|
|
testTime := time.Date(2000, 1, 1, 0, 0, 0, 0, time.Local)
|
|
fakeClock := clockwork.NewFakeClockAt(testTime)
|
|
|
|
s := newTestScheduler(t,
|
|
WithClock(fakeClock),
|
|
)
|
|
|
|
j, err := s.NewJob(
|
|
DurationJob(
|
|
time.Second,
|
|
),
|
|
NewTask(
|
|
func() {},
|
|
),
|
|
WithStartAt(WithStartImmediately()),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
s.Start()
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
lastRun, err := j.LastRun()
|
|
assert.NoError(t, err)
|
|
|
|
err = s.Shutdown()
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, testTime, lastRun)
|
|
}
|
|
|
|
func TestWithEventListeners(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
eventListeners []EventListener
|
|
err error
|
|
}{
|
|
{
|
|
"no event listeners",
|
|
nil,
|
|
nil,
|
|
},
|
|
{
|
|
"beforeJobRuns",
|
|
[]EventListener{
|
|
BeforeJobRuns(func(_ uuid.UUID, _ string) {}),
|
|
},
|
|
nil,
|
|
},
|
|
{
|
|
"afterJobRuns",
|
|
[]EventListener{
|
|
AfterJobRuns(func(_ uuid.UUID, _ string) {}),
|
|
},
|
|
nil,
|
|
},
|
|
{
|
|
"afterJobRunsWithError",
|
|
[]EventListener{
|
|
AfterJobRunsWithError(func(_ uuid.UUID, _ string, _ error) {}),
|
|
},
|
|
nil,
|
|
},
|
|
{
|
|
"afterJobRunsWithPanic",
|
|
[]EventListener{
|
|
AfterJobRunsWithPanic(func(_ uuid.UUID, _ string, _ any) {}),
|
|
},
|
|
nil,
|
|
},
|
|
{
|
|
"afterLockError",
|
|
[]EventListener{
|
|
AfterLockError(func(_ uuid.UUID, _ string, _ error) {}),
|
|
},
|
|
nil,
|
|
},
|
|
{
|
|
"multiple event listeners",
|
|
[]EventListener{
|
|
AfterJobRuns(func(_ uuid.UUID, _ string) {}),
|
|
AfterJobRunsWithError(func(_ uuid.UUID, _ string, _ error) {}),
|
|
BeforeJobRuns(func(_ uuid.UUID, _ string) {}),
|
|
AfterLockError(func(_ uuid.UUID, _ string, _ error) {}),
|
|
},
|
|
nil,
|
|
},
|
|
{
|
|
"nil after job runs listener",
|
|
[]EventListener{
|
|
AfterJobRuns(nil),
|
|
},
|
|
ErrEventListenerFuncNil,
|
|
},
|
|
{
|
|
"nil after job runs with error listener",
|
|
[]EventListener{
|
|
AfterJobRunsWithError(nil),
|
|
},
|
|
ErrEventListenerFuncNil,
|
|
},
|
|
{
|
|
"nil before job runs listener",
|
|
[]EventListener{
|
|
BeforeJobRuns(nil),
|
|
},
|
|
ErrEventListenerFuncNil,
|
|
},
|
|
{
|
|
"nil before job runs error listener",
|
|
[]EventListener{
|
|
BeforeJobRunsSkipIfBeforeFuncErrors(nil),
|
|
},
|
|
ErrEventListenerFuncNil,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
var ij internalJob
|
|
err := WithEventListeners(tt.eventListeners...)(&ij, time.Now())
|
|
assert.Equal(t, tt.err, err)
|
|
|
|
if err != nil {
|
|
return
|
|
}
|
|
var count int
|
|
if ij.beforeJobRuns != nil {
|
|
count++
|
|
}
|
|
if ij.afterJobRuns != nil {
|
|
count++
|
|
}
|
|
if ij.afterJobRunsWithError != nil {
|
|
count++
|
|
}
|
|
if ij.afterJobRunsWithPanic != nil {
|
|
count++
|
|
}
|
|
if ij.afterLockError != nil {
|
|
count++
|
|
}
|
|
assert.Equal(t, len(tt.eventListeners), count)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestJob_NextRun(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
f func()
|
|
}{
|
|
{
|
|
"simple",
|
|
func() {},
|
|
},
|
|
{
|
|
"sleep 3 seconds",
|
|
func() {
|
|
time.Sleep(300 * time.Millisecond)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
testTime := time.Now()
|
|
|
|
s := newTestScheduler(t)
|
|
|
|
j, err := s.NewJob(
|
|
DurationJob(
|
|
100*time.Millisecond,
|
|
),
|
|
NewTask(
|
|
func() {},
|
|
),
|
|
WithStartAt(WithStartDateTime(testTime.Add(100*time.Millisecond))),
|
|
WithSingletonMode(LimitModeReschedule),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
s.Start()
|
|
nextRun, err := j.NextRun()
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, testTime.Add(100*time.Millisecond), nextRun)
|
|
|
|
time.Sleep(150 * time.Millisecond)
|
|
|
|
nextRun, err = j.NextRun()
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, testTime.Add(200*time.Millisecond), nextRun)
|
|
assert.Equal(t, 200*time.Millisecond, nextRun.Sub(testTime))
|
|
|
|
err = s.Shutdown()
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestJob_NextRuns(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
jd JobDefinition
|
|
assertion func(t *testing.T, iteration int, previousRun, nextRun time.Time)
|
|
}{
|
|
{
|
|
"simple - milliseconds",
|
|
DurationJob(
|
|
100 * time.Millisecond,
|
|
),
|
|
func(t *testing.T, _ int, previousRun, nextRun time.Time) {
|
|
assert.Equal(t, previousRun.UnixMilli()+100, nextRun.UnixMilli())
|
|
},
|
|
},
|
|
{
|
|
"weekly",
|
|
WeeklyJob(
|
|
2,
|
|
NewWeekdays(time.Tuesday),
|
|
NewAtTimes(
|
|
NewAtTime(0, 0, 0),
|
|
),
|
|
),
|
|
func(t *testing.T, iteration int, previousRun, nextRun time.Time) {
|
|
diff := time.Hour * 14 * 24
|
|
if iteration == 1 {
|
|
// because the job is run immediately, the first run is on
|
|
// Saturday 1/1/2000. The following run is then on Tuesday 1/11/2000
|
|
diff = time.Hour * 10 * 24
|
|
}
|
|
assert.Equal(t, previousRun.Add(diff).Day(), nextRun.Day())
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
testTime := time.Date(2000, 1, 1, 0, 0, 0, 0, time.Local)
|
|
fakeClock := clockwork.NewFakeClockAt(testTime)
|
|
|
|
s := newTestScheduler(t,
|
|
WithClock(fakeClock),
|
|
)
|
|
|
|
j, err := s.NewJob(
|
|
tt.jd,
|
|
NewTask(
|
|
func() {},
|
|
),
|
|
WithStartAt(WithStartImmediately()),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
s.Start()
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
nextRuns, err := j.NextRuns(5)
|
|
require.NoError(t, err)
|
|
|
|
assert.Len(t, nextRuns, 5)
|
|
|
|
for i := range nextRuns {
|
|
if i == 0 {
|
|
// skipping because there is no previous run
|
|
continue
|
|
}
|
|
tt.assertion(t, i, nextRuns[i-1], nextRuns[i])
|
|
}
|
|
|
|
assert.NoError(t, s.Shutdown())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestJob_PanicOccurred(t *testing.T) {
|
|
gotCh := make(chan any)
|
|
errCh := make(chan error)
|
|
s := newTestScheduler(t)
|
|
_, err := s.NewJob(
|
|
DurationJob(10*time.Millisecond),
|
|
NewTask(func() {
|
|
a := 0
|
|
_ = 1 / a
|
|
}),
|
|
WithEventListeners(
|
|
AfterJobRunsWithPanic(func(_ uuid.UUID, _ string, recoverData any) {
|
|
gotCh <- recoverData
|
|
}), AfterJobRunsWithError(func(_ uuid.UUID, _ string, err error) {
|
|
errCh <- err
|
|
}),
|
|
),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
s.Start()
|
|
got := <-gotCh
|
|
require.EqualError(t, got.(error), "runtime error: integer divide by zero")
|
|
|
|
err = <-errCh
|
|
require.ErrorIs(t, err, ErrPanicRecovered)
|
|
require.EqualError(t, err, "gocron: panic recovered from runtime error: integer divide by zero")
|
|
|
|
require.NoError(t, s.Shutdown())
|
|
close(gotCh)
|
|
close(errCh)
|
|
}
|
|
|
|
func TestTimeFromAtTime(t *testing.T) {
|
|
testTimeUTC := time.Date(0, 0, 0, 1, 1, 1, 0, time.UTC)
|
|
cst, err := time.LoadLocation("America/Chicago")
|
|
require.NoError(t, err)
|
|
testTimeCST := time.Date(0, 0, 0, 1, 1, 1, 0, cst)
|
|
|
|
tests := []struct {
|
|
name string
|
|
at AtTime
|
|
loc *time.Location
|
|
expectedTime time.Time
|
|
expectedStr string
|
|
}{
|
|
{
|
|
"UTC",
|
|
NewAtTime(
|
|
uint(testTimeUTC.Hour()),
|
|
uint(testTimeUTC.Minute()),
|
|
uint(testTimeUTC.Second()),
|
|
),
|
|
time.UTC,
|
|
testTimeUTC,
|
|
"01:01:01",
|
|
},
|
|
{
|
|
"CST",
|
|
NewAtTime(
|
|
uint(testTimeCST.Hour()),
|
|
uint(testTimeCST.Minute()),
|
|
uint(testTimeCST.Second()),
|
|
),
|
|
cst,
|
|
testTimeCST,
|
|
"01:01:01",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := TimeFromAtTime(tt.at, tt.loc)
|
|
assert.Equal(t, tt.expectedTime, result)
|
|
|
|
resultFmt := result.Format("15:04:05")
|
|
assert.Equal(t, tt.expectedStr, resultFmt)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNewAtTimes(t *testing.T) {
|
|
at := NewAtTimes(
|
|
NewAtTime(1, 1, 1),
|
|
NewAtTime(2, 2, 2),
|
|
)
|
|
|
|
var times []string
|
|
for _, att := range at() {
|
|
timeStr := TimeFromAtTime(att, time.UTC).Format("15:04")
|
|
times = append(times, timeStr)
|
|
}
|
|
|
|
var timesAgain []string
|
|
for _, att := range at() {
|
|
timeStr := TimeFromAtTime(att, time.UTC).Format("15:04")
|
|
timesAgain = append(timesAgain, timeStr)
|
|
}
|
|
|
|
assert.Equal(t, times, timesAgain)
|
|
}
|
|
|
|
func TestNewWeekdays(t *testing.T) {
|
|
wd := NewWeekdays(
|
|
time.Monday,
|
|
time.Tuesday,
|
|
)
|
|
|
|
var dayStrings []string
|
|
for _, w := range wd() {
|
|
dayStrings = append(dayStrings, w.String())
|
|
}
|
|
|
|
var dayStringsAgain []string
|
|
for _, w := range wd() {
|
|
dayStringsAgain = append(dayStringsAgain, w.String())
|
|
}
|
|
|
|
assert.Equal(t, dayStrings, dayStringsAgain)
|
|
}
|
|
|
|
func TestNewDaysOfTheMonth(t *testing.T) {
|
|
dom := NewDaysOfTheMonth(1, 2, 3)
|
|
|
|
var domInts []int
|
|
for _, d := range dom() {
|
|
domInts = append(domInts, d)
|
|
}
|
|
|
|
var domIntsAgain []int
|
|
for _, d := range dom() {
|
|
domIntsAgain = append(domIntsAgain, d)
|
|
}
|
|
|
|
assert.Equal(t, domInts, domIntsAgain)
|
|
}
|
|
|
|
func TestWithIntervalFromCompletion_BasicFunctionality(t *testing.T) {
|
|
t.Run("interval calculated from completion time", func(t *testing.T) {
|
|
s, err := NewScheduler()
|
|
require.NoError(t, err)
|
|
defer func() { _ = s.Shutdown() }()
|
|
|
|
var mu sync.Mutex
|
|
executions := []struct {
|
|
startTime time.Time
|
|
completeTime time.Time
|
|
}{}
|
|
|
|
jobExecutionTime := 2 * time.Second
|
|
scheduledInterval := 5 * time.Second
|
|
|
|
_, err = s.NewJob(
|
|
DurationJob(scheduledInterval),
|
|
NewTask(func() {
|
|
start := time.Now()
|
|
time.Sleep(jobExecutionTime)
|
|
complete := time.Now()
|
|
|
|
mu.Lock()
|
|
executions = append(executions, struct {
|
|
startTime time.Time
|
|
completeTime time.Time
|
|
}{start, complete})
|
|
mu.Unlock()
|
|
}),
|
|
WithIntervalFromCompletion(),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
s.Start()
|
|
|
|
// Wait for at least 3 executions
|
|
// With intervalFromCompletion:
|
|
// Execution 1: 0s-2s
|
|
// Wait: 5s (from 2s to 7s)
|
|
// Execution 2: 7s-9s
|
|
// Wait: 5s (from 9s to 14s)
|
|
// Execution 3: 14s-16s
|
|
time.Sleep(18 * time.Second)
|
|
|
|
mu.Lock()
|
|
executionCount := len(executions)
|
|
mu.Unlock()
|
|
|
|
require.GreaterOrEqual(t, executionCount, 2,
|
|
"Expected at least 2 executions")
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
for i := 1; i < len(executions); i++ {
|
|
prev := executions[i-1]
|
|
curr := executions[i]
|
|
|
|
completionToStartGap := curr.startTime.Sub(prev.completeTime)
|
|
|
|
assert.InDelta(t, scheduledInterval.Seconds(), completionToStartGap.Seconds(), 0.5,
|
|
"Gap from completion to start should match the interval")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestWithIntervalFromCompletion_VariableExecutionTime(t *testing.T) {
|
|
s, err := NewScheduler()
|
|
require.NoError(t, err)
|
|
defer func() { _ = s.Shutdown() }()
|
|
|
|
var mu sync.Mutex
|
|
executions := []struct {
|
|
startTime time.Time
|
|
completeTime time.Time
|
|
executionDur time.Duration
|
|
}{}
|
|
|
|
executionTimes := []time.Duration{
|
|
1 * time.Second,
|
|
3 * time.Second,
|
|
500 * time.Millisecond,
|
|
}
|
|
currentExecution := atomic.Int32{}
|
|
scheduledInterval := 4 * time.Second
|
|
|
|
_, err = s.NewJob(
|
|
DurationJob(scheduledInterval),
|
|
NewTask(func() {
|
|
idx := int(currentExecution.Add(1)) - 1
|
|
if idx >= len(executionTimes) {
|
|
return
|
|
}
|
|
|
|
start := time.Now()
|
|
executionTime := executionTimes[idx]
|
|
time.Sleep(executionTime)
|
|
complete := time.Now()
|
|
|
|
mu.Lock()
|
|
executions = append(executions, struct {
|
|
startTime time.Time
|
|
completeTime time.Time
|
|
executionDur time.Duration
|
|
}{start, complete, executionTime})
|
|
mu.Unlock()
|
|
}),
|
|
WithIntervalFromCompletion(),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
s.Start()
|
|
|
|
// Wait for all 3 executions
|
|
// Execution 1: 0s-1s, wait 4s → next at 5s
|
|
// Execution 2: 5s-8s, wait 4s → next at 12s
|
|
// Execution 3: 12s-12.5s
|
|
time.Sleep(15 * time.Second)
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
require.GreaterOrEqual(t, len(executions), 2, "Expected at least 2 executions")
|
|
|
|
for i := 1; i < len(executions); i++ {
|
|
prev := executions[i-1]
|
|
curr := executions[i]
|
|
|
|
restPeriod := curr.startTime.Sub(prev.completeTime)
|
|
|
|
assert.InDelta(t, scheduledInterval.Seconds(), restPeriod.Seconds(), 0.5,
|
|
"Rest period should be consistent regardless of execution time")
|
|
}
|
|
}
|
|
|
|
func TestWithIntervalFromCompletion_LongRunningJob(t *testing.T) {
|
|
s, err := NewScheduler()
|
|
require.NoError(t, err)
|
|
defer func() { _ = s.Shutdown() }()
|
|
|
|
var mu sync.Mutex
|
|
executions := []struct {
|
|
startTime time.Time
|
|
completeTime time.Time
|
|
}{}
|
|
|
|
jobExecutionTime := 6 * time.Second
|
|
scheduledInterval := 3 * time.Second
|
|
|
|
_, err = s.NewJob(
|
|
DurationJob(scheduledInterval),
|
|
NewTask(func() {
|
|
start := time.Now()
|
|
time.Sleep(jobExecutionTime)
|
|
complete := time.Now()
|
|
|
|
mu.Lock()
|
|
executions = append(executions, struct {
|
|
startTime time.Time
|
|
completeTime time.Time
|
|
}{start, complete})
|
|
mu.Unlock()
|
|
}),
|
|
WithIntervalFromCompletion(),
|
|
WithSingletonMode(LimitModeReschedule),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
s.Start()
|
|
|
|
// Wait for 2 executions
|
|
// Execution 1: 0s-6s, wait 3s → next at 9s
|
|
// Execution 2: 9s-15s, wait 3s → next at 18s
|
|
// Need to wait at least 16 seconds for 2 executions + buffer
|
|
time.Sleep(22 * time.Second)
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
require.GreaterOrEqual(t, len(executions), 2, "Expected at least 2 executions")
|
|
|
|
if len(executions) < 2 {
|
|
t.Logf("Only got %d execution(s), skipping gap assertion", len(executions))
|
|
return
|
|
}
|
|
|
|
prev := executions[0]
|
|
curr := executions[1]
|
|
|
|
completionGap := curr.startTime.Sub(prev.completeTime)
|
|
|
|
assert.InDelta(t, scheduledInterval.Seconds(), completionGap.Seconds(), 0.5,
|
|
"Gap should be the full interval even when execution time exceeds interval")
|
|
}
|
|
|
|
func TestWithIntervalFromCompletion_ComparedToDefault(t *testing.T) {
|
|
jobExecutionTime := 2 * time.Second
|
|
scheduledInterval := 5 * time.Second
|
|
|
|
t.Run("default behavior - interval from scheduled time", func(t *testing.T) {
|
|
s, err := NewScheduler()
|
|
require.NoError(t, err)
|
|
defer func() { _ = s.Shutdown() }()
|
|
|
|
var mu sync.Mutex
|
|
executions := []struct {
|
|
startTime time.Time
|
|
completeTime time.Time
|
|
}{}
|
|
|
|
_, err = s.NewJob(
|
|
DurationJob(scheduledInterval),
|
|
NewTask(func() {
|
|
start := time.Now()
|
|
time.Sleep(jobExecutionTime)
|
|
complete := time.Now()
|
|
|
|
mu.Lock()
|
|
executions = append(executions, struct {
|
|
startTime time.Time
|
|
completeTime time.Time
|
|
}{start, complete})
|
|
mu.Unlock()
|
|
}),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
s.Start()
|
|
time.Sleep(13 * time.Second)
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
require.GreaterOrEqual(t, len(executions), 2, "Expected at least 2 executions")
|
|
|
|
prev := executions[0]
|
|
curr := executions[1]
|
|
completionGap := curr.startTime.Sub(prev.completeTime)
|
|
|
|
expectedGap := scheduledInterval - jobExecutionTime
|
|
assert.InDelta(t, expectedGap.Seconds(), completionGap.Seconds(), 0.5,
|
|
"Default behavior: gap should be interval minus execution time")
|
|
})
|
|
|
|
t.Run("with intervalFromCompletion - interval from completion time", func(t *testing.T) {
|
|
s, err := NewScheduler()
|
|
require.NoError(t, err)
|
|
defer func() { _ = s.Shutdown() }()
|
|
|
|
var mu sync.Mutex
|
|
executions := []struct {
|
|
startTime time.Time
|
|
completeTime time.Time
|
|
}{}
|
|
|
|
_, err = s.NewJob(
|
|
DurationJob(scheduledInterval),
|
|
NewTask(func() {
|
|
start := time.Now()
|
|
time.Sleep(jobExecutionTime)
|
|
complete := time.Now()
|
|
|
|
mu.Lock()
|
|
executions = append(executions, struct {
|
|
startTime time.Time
|
|
completeTime time.Time
|
|
}{start, complete})
|
|
mu.Unlock()
|
|
}),
|
|
WithIntervalFromCompletion(),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
s.Start()
|
|
time.Sleep(15 * time.Second)
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
require.GreaterOrEqual(t, len(executions), 2, "Expected at least 2 executions")
|
|
|
|
prev := executions[0]
|
|
curr := executions[1]
|
|
completionGap := curr.startTime.Sub(prev.completeTime)
|
|
|
|
assert.InDelta(t, scheduledInterval.Seconds(), completionGap.Seconds(), 0.5,
|
|
"With intervalFromCompletion: gap should be the full interval")
|
|
})
|
|
}
|
|
|
|
func TestWithIntervalFromCompletion_DurationRandomJob(t *testing.T) {
|
|
s, err := NewScheduler()
|
|
require.NoError(t, err)
|
|
defer func() { _ = s.Shutdown() }()
|
|
|
|
var mu sync.Mutex
|
|
executions := []struct {
|
|
startTime time.Time
|
|
completeTime time.Time
|
|
}{}
|
|
|
|
jobExecutionTime := 1 * time.Second
|
|
minInterval := 3 * time.Second
|
|
maxInterval := 4 * time.Second
|
|
|
|
_, err = s.NewJob(
|
|
DurationRandomJob(minInterval, maxInterval),
|
|
NewTask(func() {
|
|
start := time.Now()
|
|
time.Sleep(jobExecutionTime)
|
|
complete := time.Now()
|
|
|
|
mu.Lock()
|
|
executions = append(executions, struct {
|
|
startTime time.Time
|
|
completeTime time.Time
|
|
}{start, complete})
|
|
mu.Unlock()
|
|
}),
|
|
WithIntervalFromCompletion(),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
s.Start()
|
|
|
|
time.Sleep(15 * time.Second)
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
require.GreaterOrEqual(t, len(executions), 2, "Expected at least 2 executions")
|
|
|
|
for i := 1; i < len(executions); i++ {
|
|
prev := executions[i-1]
|
|
curr := executions[i]
|
|
|
|
restPeriod := curr.startTime.Sub(prev.completeTime)
|
|
assert.GreaterOrEqual(t, restPeriod.Seconds(), minInterval.Seconds()-0.5,
|
|
"Rest period should be at least minInterval")
|
|
assert.LessOrEqual(t, restPeriod.Seconds(), maxInterval.Seconds()+0.5,
|
|
"Rest period should be at most maxInterval")
|
|
}
|
|
}
|
|
|
|
func TestWithIntervalFromCompletion_FirstRun(t *testing.T) {
|
|
s, err := NewScheduler()
|
|
require.NoError(t, err)
|
|
defer func() { _ = s.Shutdown() }()
|
|
|
|
var mu sync.Mutex
|
|
var firstRunTime time.Time
|
|
|
|
_, err = s.NewJob(
|
|
DurationJob(5*time.Second),
|
|
NewTask(func() {
|
|
mu.Lock()
|
|
if firstRunTime.IsZero() {
|
|
firstRunTime = time.Now()
|
|
}
|
|
mu.Unlock()
|
|
}),
|
|
WithIntervalFromCompletion(),
|
|
WithStartAt(WithStartImmediately()),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
startTime := time.Now()
|
|
s.Start()
|
|
|
|
time.Sleep(1 * time.Second)
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
require.False(t, firstRunTime.IsZero(), "Job should have run at least once")
|
|
|
|
timeSinceStart := firstRunTime.Sub(startTime)
|
|
assert.Less(t, timeSinceStart.Seconds(), 1.0,
|
|
"First run should happen quickly with WithStartImmediately")
|
|
}
|