diff --git a/job.go b/job.go index 792811ad..e4734e8a 100644 --- a/job.go +++ b/job.go @@ -9,14 +9,13 @@ import ( type Job struct { interval uint64 // pause interval * unit between runs unit timeUnit // time units, ,e.g. 'minutes', 'hours'... - periodDuration time.Duration // interval * unit startsImmediately bool // if the Job should run upon scheduler start jobFunc string // the Job jobFunc to run, func[jobFunc] atTime time.Duration // optional time at which this Job runs err error // error related to Job lastRun time.Time // datetime of last run nextRun time.Time // datetime of next run - startDay time.Weekday // Specific day of the week to start on + scheduledWeekday *time.Weekday // Specific day of the week to start on dayOfTheMonth int // Specific day of the month to run the job funcs map[string]interface{} // Map for the function task store fparams map[string][]interface{} // Map for function and params of function @@ -26,12 +25,10 @@ type Job struct { // NewJob creates a new Job with the provided interval func NewJob(interval uint64) *Job { - th := newTimeWrapper() return &Job{ interval: interval, - lastRun: th.Unix(0, 0), - nextRun: th.Unix(0, 0), - startDay: time.Sunday, + lastRun: time.Time{}, + nextRun: time.Time{}, funcs: make(map[string]interface{}), fparams: make(map[string][]interface{}), tags: []string{}, @@ -43,6 +40,10 @@ func (j *Job) run() { callJobFuncWithParams(j.funcs[j.jobFunc], j.fparams[j.jobFunc]) } +func (j Job) neverRan() bool { + return j.lastRun.IsZero() +} + // Err returns an error if one ocurred while creating the Job func (j *Job) Err() error { return j.err @@ -74,28 +75,6 @@ func (j *Job) Tags() []string { return j.tags } -func (j *Job) setPeriodDuration() error { - interval := time.Duration(j.interval) - - switch j.unit { - case seconds: - j.periodDuration = interval * time.Second - case minutes: - j.periodDuration = interval * time.Minute - case hours: - j.periodDuration = interval * time.Hour - case days: - j.periodDuration = interval * time.Hour * 24 - case weeks: - j.periodDuration = interval * time.Hour * 24 * 7 - case months: - // periodDuration doesn't apply here - default: - return ErrPeriodNotSpecified - } - return nil -} - // ScheduledTime returns the time of the Job's next scheduled run func (j *Job) ScheduledTime() time.Time { return j.nextRun @@ -109,8 +88,8 @@ func (j *Job) ScheduledAtTime() string { // Weekday returns which day of the week the Job will run on and // will return an error if the Job is not scheduled weekly func (j *Job) Weekday() (time.Weekday, error) { - if j.unit == weeks { - return j.startDay, nil + if j.scheduledWeekday == nil { + return time.Sunday, ErrNotScheduledWeekday } - return time.Sunday, ErrNotScheduledWeekday + return *j.scheduledWeekday, nil } diff --git a/job_test.go b/job_test.go index 5d4e3bc8..da2faed4 100644 --- a/job_test.go +++ b/job_test.go @@ -27,7 +27,8 @@ func TestGetScheduledTime(t *testing.T) { func TestGetWeekday(t *testing.T) { s := NewScheduler(time.UTC) - weedayJob, _ := s.Every(1).Weekday(time.Wednesday).Do(task) + wednesday := time.Wednesday + weedayJob, _ := s.Every(1).Weekday(wednesday).Do(task) nonWeekdayJob, _ := s.Every(1).Minute().Do(task) testCases := []struct { @@ -36,7 +37,7 @@ func TestGetWeekday(t *testing.T) { expectedWeekday time.Weekday expectedError error }{ - {"success", weedayJob, time.Wednesday, nil}, + {"success", weedayJob, wednesday, nil}, {"fail - not set for weekday", nonWeekdayJob, time.Sunday, ErrNotScheduledWeekday}, } @@ -46,36 +47,9 @@ func TestGetWeekday(t *testing.T) { if tc.expectedError != nil { assert.Error(t, tc.expectedError, err) } else { + assert.Equal(t, tc.expectedWeekday, weekday) assert.Nil(t, err) } - - assert.Equal(t, tc.expectedWeekday, weekday) }) } } - -func TestSetPeriodDuration(t *testing.T) { - - testCases := []struct { - desc string - job *Job - expectedDuration time.Duration - expectedError error - }{ - {"seconds", &Job{interval: 1, unit: seconds}, time.Duration(1) * time.Second, nil}, - {"minutes", &Job{interval: 1, unit: minutes}, time.Duration(1) * time.Minute, nil}, - {"hours", &Job{interval: 1, unit: hours}, time.Duration(1) * time.Hour, nil}, - {"days", &Job{interval: 1, unit: days}, time.Duration(1) * time.Hour * 24, nil}, - {"weeks", &Job{interval: 1, unit: weeks}, time.Duration(1) * time.Hour * 24 * 7, nil}, - {"none", &Job{interval: 1}, 0, ErrPeriodNotSpecified}, - } - - for _, tc := range testCases { - t.Run(tc.desc, func(t *testing.T) { - err := tc.job.setPeriodDuration() - assert.Equal(t, tc.expectedError, err) - assert.Equal(t, tc.expectedDuration, tc.job.periodDuration) - }) - } - -} diff --git a/scheduler.go b/scheduler.go index 20d9103d..c25d2db6 100644 --- a/scheduler.go +++ b/scheduler.go @@ -2,6 +2,7 @@ package gocron import ( "fmt" + "math" "reflect" "sort" "strings" @@ -27,7 +28,7 @@ func NewScheduler(loc *time.Location) *Scheduler { loc: loc, running: false, stopChan: make(chan struct{}), - time: newTimeWrapper(), + time: &trueTime{}, } } @@ -43,6 +44,7 @@ func (s *Scheduler) StartAsync() chan struct{} { } s.running = true + s.scheduleAllJobs() ticker := s.time.NewTicker(1 * time.Second) go func() { for { @@ -85,41 +87,135 @@ func (s *Scheduler) ChangeLocation(newLocation *time.Location) { } // scheduleNextRun Compute the instant when this Job should run next -func (s *Scheduler) scheduleNextRun(j *Job) error { +func (s *Scheduler) scheduleNextRun(j *Job) { now := s.time.Now(s.loc) + if j.startsImmediately { + j.nextRun = now + j.startsImmediately = false + return + } + + // delta represent the time slice used to calculate the next run + // it can be the last time ran by a job, or time.Now() if the job never ran + var delta time.Time + if j.neverRan() { + if !j.nextRun.IsZero() { // scheduled for future run, wait to run at least once + return + } + delta = now + } else { + delta = j.lastRun + } + switch j.unit { case seconds, minutes, hours: - j.nextRun = j.lastRun.Add(j.periodDuration) + j.nextRun = s.rescheduleDuration(j, delta) case days: - j.nextRun = s.roundToMidnight(j.lastRun) - j.nextRun = j.nextRun.Add(j.atTime).Add(j.periodDuration) + j.nextRun = s.rescheduleDay(j, delta) case weeks: - j.nextRun = s.roundToMidnight(j.lastRun) - dayDiff := int(j.startDay) - dayDiff -= int(j.nextRun.Weekday()) - if dayDiff != 0 { - j.nextRun = j.nextRun.Add(time.Duration(dayDiff) * 24 * time.Hour) - } - j.nextRun = j.nextRun.Add(j.atTime) + j.nextRun = s.rescheduleWeek(j, delta) case months: - increment := j.lastRun.Month() + time.Month(j.interval) - nextMonth := increment % 12 - year := j.lastRun.Year() + int(increment/12) - j.nextRun = time.Date(year, nextMonth, j.dayOfTheMonth, 0, 0, 0, 0, s.loc).Add(j.atTime) + j.nextRun = s.rescheduleMonth(j, delta) } +} + +func (s *Scheduler) rescheduleMonth(j *Job, delta time.Time) time.Time { + if j.neverRan() { // calculate days to j.dayOfTheMonth + jobDay := time.Date(delta.Year(), delta.Month(), j.dayOfTheMonth, 0, 0, 0, 0, s.loc).Add(j.atTime) + daysDifference := int(math.Abs(delta.Sub(jobDay).Hours()) / 24) + nextRun := s.roundToMidnight(delta) + if jobDay.Before(delta) { // shouldn't run this month; schedule for next interval minus day difference - // advance to next possible Schedule - for j.nextRun.Before(now) || j.nextRun.Before(j.lastRun) { - j.nextRun = j.nextRun.Add(j.periodDuration) + nextRun = nextRun.AddDate(0, int(j.interval), -daysDifference) + } else { + if j.interval == 1 { // every month counts current month + nextRun = nextRun.AddDate(0, int(j.interval)-1, daysDifference) + } else { // should run next month interval + nextRun = nextRun.AddDate(0, int(j.interval), daysDifference) + } + } + return nextRun.Add(j.atTime) } + return s.roundToMidnight(delta).AddDate(0, int(j.interval), 0).Add(j.atTime) +} - return nil +func (s *Scheduler) rescheduleWeek(j *Job, delta time.Time) time.Time { + var days int + if j.scheduledWeekday != nil { // weekday selected, Every().Monday(), for example + days = s.calculateWeekdayDifference(delta, j) + } else { + days = int(j.interval) * 7 + } + delta = s.roundToMidnight(delta) + return delta.AddDate(0, 0, days).Add(j.atTime) } -// roundToMidnight truncate time to midnight +func (s *Scheduler) rescheduleDay(j *Job, delta time.Time) time.Time { + if j.interval == 1 { + atTime := time.Date(delta.Year(), delta.Month(), delta.Day(), 0, 0, 0, 0, s.loc).Add(j.atTime) + if delta.Before(atTime) { // should run today + return s.roundToMidnight(delta).Add(j.atTime) + } + } + return s.roundToMidnight(delta).AddDate(0, 0, int(j.interval)).Add(j.atTime) +} + +func (s *Scheduler) rescheduleDuration(j *Job, delta time.Time) time.Time { + if j.neverRan() && j.atTime != 0 { // ugly. in order to avoid this we could prohibit setting .At() and allowing only .StartAt() when dealing with Duration types + atTime := time.Date(delta.Year(), delta.Month(), delta.Day(), 0, 0, 0, 0, s.loc).Add(j.atTime) + if delta.Before(atTime) || delta.Equal(atTime) { + return s.roundToMidnight(delta).Add(j.atTime) + } + } + + var periodDuration time.Duration + switch j.unit { + case seconds: + periodDuration = time.Duration(j.interval) * time.Second + case minutes: + periodDuration = time.Duration(j.interval) * time.Minute + case hours: + periodDuration = time.Duration(j.interval) * time.Hour + } + return delta.Add(periodDuration) +} + +func (s *Scheduler) calculateWeekdayDifference(delta time.Time, j *Job) int { + daysToWeekday := remainingDaysToWeekday(delta.Weekday(), *j.scheduledWeekday) + + if j.interval > 1 { + return daysToWeekday + int(j.interval-1)*7 // minus a week since to compensate daysToWeekday + } + + if daysToWeekday > 0 { // within the next following days, but not today + return daysToWeekday + } + + // following paths are on same day + if j.atTime.Seconds() == 0 && j.neverRan() { // .At() not set, run today + return 0 + } + + atJobTime := time.Date(delta.Year(), delta.Month(), delta.Day(), 0, 0, 0, 0, s.loc).Add(j.atTime) + if delta.Before(atJobTime) || delta.Equal(atJobTime) { // .At() set and should run today + return 0 + } + + return 7 +} + +func remainingDaysToWeekday(from time.Weekday, to time.Weekday) int { + daysUntilScheduledDay := int(to) - int(from) + if daysUntilScheduledDay < 0 { + daysUntilScheduledDay += 7 + } + return daysUntilScheduledDay +} + +// roundToMidnight truncates time to midnight func (s *Scheduler) roundToMidnight(t time.Time) time.Time { - return s.time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, s.loc) + return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, s.loc) } // Get the current runnable Jobs, which shouldRun is True @@ -154,8 +250,7 @@ func (s *Scheduler) Every(interval uint64) *Scheduler { // RunPending runs all the Jobs that are scheduled to run. func (s *Scheduler) RunPending() { - runnableJobs := s.runnableJobs() - for _, job := range runnableJobs { + for _, job := range s.runnableJobs() { s.runAndReschedule(job) // we should handle this error somehow } } @@ -164,9 +259,7 @@ func (s *Scheduler) runAndReschedule(job *Job) error { if err := s.run(job); err != nil { return err } - if err := s.scheduleNextRun(job); err != nil { - return err - } + s.scheduleNextRun(job) return nil } @@ -299,30 +392,6 @@ func (s *Scheduler) Do(jobFun interface{}, params ...interface{}) (*Job, error) j.fparams[fname] = params j.jobFunc = fname - if j.periodDuration == 0 { - err := j.setPeriodDuration() - if err != nil { - return nil, err - } - } - - if !j.startsImmediately { - - if j.lastRun == s.time.Unix(0, 0) { - j.lastRun = s.time.Now(s.loc) - - if j.atTime != 0 { - j.lastRun = j.lastRun.Add(-j.periodDuration) - } - } - - if j.nextRun == s.time.Unix(0, 0) { - if err := s.scheduleNextRun(j); err != nil { - return nil, err - } - } - } - return j, nil } @@ -355,7 +424,6 @@ func (s *Scheduler) StartAt(t time.Time) *Scheduler { // StartImmediately sets the Jobs next run as soon as the scheduler starts func (s *Scheduler) StartImmediately() *Scheduler { job := s.getCurrentJob() - job.nextRun = s.time.Now(s.loc) job.startsImmediately = true return s } @@ -447,7 +515,7 @@ func (s *Scheduler) Months(dayOfTheMonth int) *Scheduler { // Weekday sets the start with a specific weekday weekday func (s *Scheduler) Weekday(startDay time.Weekday) *Scheduler { - s.getCurrentJob().startDay = startDay + s.getCurrentJob().scheduledWeekday = &startDay s.setUnit(weeks) return s } @@ -496,3 +564,9 @@ func (s *Scheduler) Lock() *Scheduler { s.getCurrentJob().lock = true return s } + +func (s *Scheduler) scheduleAllJobs() { + for _, j := range s.jobs { + s.scheduleNextRun(j) + } +} diff --git a/scheduler_test.go b/scheduler_test.go index 4a0f84db..4e779c9a 100644 --- a/scheduler_test.go +++ b/scheduler_test.go @@ -73,6 +73,7 @@ func TestStartImmediately(t *testing.T) { now := time.Now().UTC() job, _ := sched.Every(1).Hour().StartImmediately().Do(task) + sched.scheduleAllJobs() next := job.ScheduledTime() nextRounded := time.Date(next.Year(), next.Month(), next.Day(), next.Hour(), next.Minute(), next.Second(), 0, time.UTC) @@ -81,51 +82,21 @@ func TestStartImmediately(t *testing.T) { assert.Exactly(t, expected, nextRounded) } -func TestAt(t *testing.T) { - s := NewScheduler(time.UTC) - - // Schedule to run in next 2 seconds - now := time.Now().UTC() - dayJobDone := make(chan bool, 1) - - // Schedule every day At - startAt := fmt.Sprintf("%02d:%02d:%02d", now.Hour(), now.Minute(), now.Add(time.Second*2).Second()) - dayJob, _ := s.Every(1).Day().At(startAt).Do(func() { - dayJobDone <- true - }) - - // Expected start time - expectedStartTime := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute(), now.Add(time.Second*2).Second(), 0, time.UTC) - nextRun := dayJob.ScheduledTime() - assert.Equal(t, expectedStartTime, nextRun) - - stop := s.StartAsync() - <-dayJobDone // Wait job done - close(stop) - time.Sleep(time.Second) // wait for scheduler to reschedule job - - // Expected next start time 1 day after - expectedNextRun := expectedStartTime.AddDate(0, 0, 1) - nextRun = dayJob.ScheduledTime() - assert.Equal(t, expectedNextRun, nextRun) -} - func TestAtFuture(t *testing.T) { - // Create new scheduler to have clean test env s := NewScheduler(time.UTC) - now := time.Now().UTC() // Schedule to run in next minute - nextMinuteTime := now.Add(time.Duration(1 * time.Minute)) - startAt := fmt.Sprintf("%02d:%02d", nextMinuteTime.Hour(), nextMinuteTime.Minute()) + nextMinuteTime := now.Add(1 * time.Minute) + startAt := fmt.Sprintf("%02d:%02d:%02d", nextMinuteTime.Hour(), nextMinuteTime.Minute(), nextMinuteTime.Second()) shouldBeFalse := false dayJob, _ := s.Every(1).Day().At(startAt).Do(func() { shouldBeFalse = true }) + s.scheduleAllJobs() // Check first run - expectedStartTime := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), now.Add(time.Minute).Minute(), 0, 0, time.UTC) + expectedStartTime := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), now.Add(time.Minute).Minute(), now.Second(), 0, time.UTC) nextRun := dayJob.ScheduledTime() assert.Equal(t, expectedStartTime, nextRun) @@ -178,116 +149,28 @@ func TestDay(t *testing.T) { assert.Equal(t, expectedTime, dayJob.nextRun) } -// utility function for testing the weekday functions *on* the current weekday. -func getWeekday(s *Scheduler, loc *time.Location) *Scheduler { - switch time.Now().In(loc).Weekday() { - case 0: - s.Sunday() - case 1: - s.Monday() - case 2: - s.Tuesday() - case 3: - s.Wednesday() - case 4: - s.Thursday() - case 5: - s.Friday() - case 6: - s.Saturday() - } - return s -} - -// This is a basic test for the issue described here: https://github.com/jasonlvhit/gocron/issues/23 -func TestWeekdays(t *testing.T) { - scheduler := NewScheduler(time.UTC) - - job1, _ := scheduler.Every(1).Monday().At("23:59").Do(task) - job2, _ := scheduler.Every(1).Wednesday().At("23:59").Do(task) - t.Logf("job1 scheduled for %s", job1.ScheduledTime()) - t.Logf("job2 scheduled for %s", job2.ScheduledTime()) - assert.NotEqual(t, job1.ScheduledTime(), job2.ScheduledTime(), "Two jobs scheduled at the same time on two different weekdays should never run at the same time") -} - -// This ensures that if you schedule a job for today's weekday, but the time is already passed, it will be scheduled for -// next week at the requested time. -func TestWeekdaysTodayAfter(t *testing.T) { - scheduler := NewScheduler(time.UTC) - now := time.Now().UTC() - timeToSchedule := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute()-1, 0, 0, time.UTC) - - runTime := fmt.Sprintf("%02d:%02d", timeToSchedule.Hour(), timeToSchedule.Minute()) - job, _ := getWeekday(scheduler.Every(1), time.UTC).At(runTime).Do(task) - t.Logf("job is scheduled for %s", job.ScheduledTime()) - if job.ScheduledTime().Weekday() != timeToSchedule.Weekday() { - t.Errorf("Job scheduled for current weekday for earlier time, should still be scheduled for current weekday (but next week)") - } - nextWeek := time.Date(now.Year(), now.Month(), now.Day()+7, now.Hour(), now.Minute()-1, 0, 0, time.UTC) - if !job.ScheduledTime().Equal(nextWeek) { - t.Errorf("Job should be scheduled for the correct time next week.\nGot %+v, expected %+v", job.ScheduledTime(), nextWeek) - } -} - -// This is to ensure that if you schedule a job for today's weekday, and the time hasn't yet passed, the next run time -// will be scheduled for today. -func TestWeekdaysTodayBefore(t *testing.T) { - scheduler := NewScheduler(time.UTC) - - now := time.Now().UTC() - timeToSchedule := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), now.Minute()+1, 0, 0, time.UTC) - - runTime := fmt.Sprintf("%02d:%02d", timeToSchedule.Hour(), timeToSchedule.Minute()) - job, _ := getWeekday(scheduler.Every(1), time.UTC).At(runTime).Do(task) - t.Logf("job is scheduled for %s", job.ScheduledTime()) - if !job.ScheduledTime().Equal(timeToSchedule) { - t.Error("Job should be run today, at the set time.") - } -} - -func TestWeekdayAfterToday(t *testing.T) { - now := time.Now().UTC() - - // Create new scheduler to have clean test env - s := NewScheduler(time.UTC) - - // Schedule job at next week day - switch now.Weekday() { +func schedulerForNextWeekdayEveryNTimes(weekday time.Weekday, n uint64, s *Scheduler) *Scheduler { + switch weekday { case time.Monday: - s = s.Every(1).Tuesday() + s = s.Every(n).Tuesday() case time.Tuesday: - s = s.Every(1).Wednesday() + s = s.Every(n).Wednesday() case time.Wednesday: - s = s.Every(1).Thursday() + s = s.Every(n).Thursday() case time.Thursday: - s = s.Every(1).Friday() + s = s.Every(n).Friday() case time.Friday: - s = s.Every(1).Saturday() + s = s.Every(n).Saturday() case time.Saturday: - s = s.Every(1).Sunday() + s = s.Every(n).Sunday() case time.Sunday: - s = s.Every(1).Monday() + s = s.Every(n).Monday() } - - weekJob, _ := s.Do(task) - - // First run - s.scheduleNextRun(weekJob) - exp := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, time.UTC) - assert.Equal(t, exp, weekJob.nextRun) - - // Simulate job run 7 days before - weekJob.lastRun = weekJob.nextRun.AddDate(0, 0, -7) - // Next run - s.scheduleNextRun(weekJob) - exp = time.Date(now.Year(), now.Month(), now.Day()+1, 0, 0, 0, 0, time.UTC) - assert.Equal(t, exp, weekJob.nextRun) + return s } func TestWeekdayBeforeToday(t *testing.T) { now := time.Now().In(time.UTC) - - // Create new scheduler to have clean test env s := NewScheduler(time.UTC) // Schedule job at day before @@ -313,56 +196,23 @@ func TestWeekdayBeforeToday(t *testing.T) { exp := time.Date(sixDaysFromNow.Year(), sixDaysFromNow.Month(), sixDaysFromNow.Day(), 0, 0, 0, 0, time.UTC) assert.Equal(t, exp, weekJob.nextRun) - - // Simulate job run 7 days before - weekJob.lastRun = weekJob.nextRun.AddDate(0, 0, -7) - // Next run - s.scheduleNextRun(weekJob) - exp = time.Date(sixDaysFromNow.Year(), sixDaysFromNow.Month(), sixDaysFromNow.Day(), 0, 0, 0, 0, time.UTC) - assert.Equal(t, exp, weekJob.nextRun) } func TestWeekdayAt(t *testing.T) { - now := time.Now() - - hour := now.Hour() - minute := now.Minute() - startAt := fmt.Sprintf("%02d:%02d", hour, minute) - - // Create new scheduler to have clean test env - s := NewScheduler(time.UTC) - - // Schedule job at next week day - switch now.Weekday() { - case time.Monday: - s = s.Every(1).Tuesday().At(startAt) - case time.Tuesday: - s = s.Every(1).Wednesday().At(startAt) - case time.Wednesday: - s = s.Every(1).Thursday().At(startAt) - case time.Thursday: - s = s.Every(1).Friday().At(startAt) - case time.Friday: - s = s.Every(1).Saturday().At(startAt) - case time.Saturday: - s = s.Every(1).Sunday().At(startAt) - case time.Sunday: - s = s.Every(1).Monday().At(startAt) - } + t.Run("asserts weekday scheduling starts at the current week", func(t *testing.T) { + s := NewScheduler(time.UTC) + now := time.Now().UTC() + s = schedulerForNextWeekdayEveryNTimes(now.Weekday(), 1, s) + weekdayJob, _ := s.Do(task) - weekJob, _ := s.Do(task) + s.scheduleNextRun(weekdayJob) - // First run - s.scheduleNextRun(weekJob) - exp := time.Date(now.Year(), now.Month(), now.AddDate(0, 0, 1).Day(), hour, minute, 0, 0, time.UTC) - assert.Equal(t, exp, weekJob.nextRun) - - // Simulate job run 7 days before - weekJob.lastRun = weekJob.nextRun.AddDate(0, 0, -7) - // Next run - s.scheduleNextRun(weekJob) - exp = time.Date(now.Year(), now.Month(), now.AddDate(0, 0, 1).Day(), hour, minute, 0, 0, time.UTC) - assert.Equal(t, exp, weekJob.nextRun) + tomorrow := now.AddDate(0, 0, 1) + exp := time.Date(tomorrow.Year(), tomorrow.Month(), tomorrow.Day(), 0, 0, 0, 0, time.UTC) + nextRun := weekdayJob.nextRun + nextRunDate := time.Date(nextRun.Year(), nextRun.Month(), nextRun.Day(), 0, 0, 0, 0, time.UTC) + assert.Equal(t, exp, nextRunDate) + }) } func TestRemove(t *testing.T) { @@ -535,38 +385,249 @@ func TestScheduler_Stop(t *testing.T) { }) } -func TestMonths(t *testing.T) { - scheduler := NewScheduler(time.Local) - now := time.Now() - scheduler.Every(1).Month(now.Day()).Do(func() {}) - _, nextRun := scheduler.NextRun() - assert.Equal( - t, - time.Date(now.Year(), (now.Month()+time.Month(1))%12, now.Day(), 0, 0, 0, 0, time.Local), - nextRun, - ) -} - func TestScheduler_StartAt(t *testing.T) { scheduler := NewScheduler(time.Local) now := time.Now() // With StartAt job, _ := scheduler.Every(3).Seconds().StartAt(now.Add(time.Second * 5)).Do(func() {}) + scheduler.scheduleAllJobs() _, nextRun := scheduler.NextRun() - assert.Equal( - t, - now.Add(time.Second*5), - nextRun, - ) + assert.Equal(t, now.Add(time.Second*5), nextRun) scheduler.Remove(job) // Without StartAt - scheduler.Every(3).Seconds().Do(func() {}) + job, _ = scheduler.Every(3).Seconds().Do(func() {}) + scheduler.scheduleNextRun(job) _, nextRun = scheduler.NextRun() - assert.Equal( - t, - now.Add(time.Second*3).Second(), - nextRun.Second(), - ) + assert.Equal(t, now.Add(time.Second*3).Second(), nextRun.Second()) +} + +func TestScheduler_FirstSchedule(t *testing.T) { + day := time.Hour * 24 + janFirst2020 := time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC) + monday := time.Date(2020, time.January, 6, 0, 0, 0, 0, time.UTC) + tuesday := monday.AddDate(0, 0, 1) + wednesday := monday.AddDate(0, 0, 2) + + fakeTime := fakeTime{} + sched := NewScheduler(time.UTC) + sched.time = &fakeTime + var tests = []struct { + name string + job *Job + startRunningTime time.Time + wantNextSchedule time.Time + }{ + // SECONDS + { + name: "every second test", + job: getJob(sched.Every(1).Second().Do), + startRunningTime: janFirst2020, + wantNextSchedule: janFirst2020.Add(time.Second), + }, + { + name: "every 62 seconds test", + job: getJob(sched.Every(62).Seconds().Do), + startRunningTime: janFirst2020, + wantNextSchedule: janFirst2020.Add(62 * time.Second), + }, + // MINUTES + { + name: "every minute test", + job: getJob(sched.Every(1).Minute().Do), + startRunningTime: janFirst2020, + wantNextSchedule: janFirst2020.Add(1 * time.Minute), + }, + { + name: "every 62 minutes test", + job: getJob(sched.Every(62).Minutes().Do), + startRunningTime: janFirst2020, + wantNextSchedule: janFirst2020.Add(62 * time.Minute), + }, + // HOURS + { + name: "every hour test", + job: getJob(sched.Every(1).Hour().Do), + startRunningTime: janFirst2020, + wantNextSchedule: janFirst2020.Add(1 * time.Hour), + }, + { + name: "every 25 hours test", + job: getJob(sched.Every(25).Hours().Do), + startRunningTime: janFirst2020, + wantNextSchedule: janFirst2020.Add(25 * time.Hour), + }, + // DAYS + { + name: "every day at midnight", + job: getJob(sched.Every(1).Day().Do), + startRunningTime: janFirst2020, + wantNextSchedule: janFirst2020.Add(1 * day), + }, + { + name: "every day at 09:30AM with scheduler starting before 09:30AM should run at same day at time", + job: getJob(sched.Every(1).Day().At("09:30").Do), + startRunningTime: janFirst2020, + wantNextSchedule: time.Date(2020, time.January, 1, 9, 30, 0, 0, time.UTC), + }, + { + name: "every day at 09:30AM with scheduler starting after 09:30AM should run tomorrow at time", + job: getJob(sched.Every(1).Day().At("09:30").Do), + startRunningTime: janFirst2020.Add(10 * time.Hour), + wantNextSchedule: time.Date(2020, time.January, 2, 9, 30, 0, 0, time.UTC), + }, + { + name: "every 31 days at midnight should run 31 days later", + job: getJob(sched.Every(31).Days().Do), + startRunningTime: janFirst2020, + wantNextSchedule: time.Date(2020, time.February, 1, 0, 0, 0, 0, time.UTC), + }, + // WEEKS + { + name: "every week should run in 7 days", + job: getJob(sched.Every(1).Week().Do), + startRunningTime: janFirst2020, + wantNextSchedule: time.Date(2020, time.January, 8, 0, 0, 0, 0, time.UTC), + }, + { + name: "every week at 09:30AM should run in 7 days at 09:30AM", + job: getJob(sched.Every(1).Week().At("09:30").Do), + startRunningTime: janFirst2020, + wantNextSchedule: time.Date(2020, time.January, 8, 9, 30, 0, 0, time.UTC), + }, + { + name: "every two weeks at 09:30AM should run in 14 days at 09:30AM", + job: getJob(sched.Every(2).Weeks().At("09:30").Do), + startRunningTime: janFirst2020, + wantNextSchedule: time.Date(2020, time.January, 15, 9, 30, 0, 0, time.UTC), + }, + { + name: "every 31 weeks at midnight should run in 217 days (2020 was a leap year)", + job: getJob(sched.Every(31).Weeks().Do), + startRunningTime: janFirst2020, + wantNextSchedule: time.Date(2020, time.August, 5, 0, 0, 0, 0, time.UTC), + }, + // MONTHS + { + name: "every month at first day starting at first day should run at same day", + job: getJob(sched.Every(1).Month(1).Do), + startRunningTime: janFirst2020, + wantNextSchedule: time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC), + }, + { + name: "every month at first day at time, but started late, should run next month", + job: getJob(sched.Every(1).Month(1).At("09:30").Do), + startRunningTime: janFirst2020.Add(10 * time.Hour), + wantNextSchedule: time.Date(2020, time.February, 1, 9, 30, 0, 0, time.UTC), + }, + { + name: "every month at day 2, and started in day 1, day should run day 2 same month", + job: getJob(sched.Every(1).Month(2).Do), + startRunningTime: janFirst2020, + wantNextSchedule: time.Date(2020, time.January, 2, 0, 0, 0, 0, time.UTC), + }, + { + name: "every month at day 1, but started on the day 8, should run next month at day 1", + job: getJob(sched.Every(1).Month(1).Do), + startRunningTime: time.Date(2020, time.January, 8, 0, 0, 0, 0, time.UTC), + wantNextSchedule: time.Date(2020, time.February, 1, 0, 0, 0, 0, time.UTC), + }, + { + name: "every 2 months at day 1, starting at day 1, should run in 2 months", + job: getJob(sched.Every(2).Months(1).Do), + startRunningTime: janFirst2020, + wantNextSchedule: time.Date(2020, time.March, 1, 0, 0, 0, 0, time.UTC), + }, + { + name: "every 2 months at day 2, starting at day 1, should run in 2 months", + job: getJob(sched.Every(2).Months(2).Do), + startRunningTime: janFirst2020, + wantNextSchedule: time.Date(2020, time.March, 2, 0, 0, 0, 0, time.UTC), + }, + { + name: "every 2 months at day 1, starting at day 2, should run 2 months later at day 1", + job: getJob(sched.Every(2).Months(1).Do), + startRunningTime: time.Date(2020, time.January, 2, 0, 0, 0, 0, time.UTC), + wantNextSchedule: time.Date(2020, time.March, 1, 0, 0, 0, 0, time.UTC), + }, + { + name: "every 13 months at day 1, starting at day 2 run in 13 months", + job: getJob(sched.Every(13).Months(1).Do), + startRunningTime: time.Date(2020, time.January, 2, 0, 0, 0, 0, time.UTC), + wantNextSchedule: time.Date(2021, time.February, 1, 0, 0, 0, 0, time.UTC), + }, + // WEEKDAYS + { + name: "every tuesday starting on a monday should run this tuesday", + job: getJob(sched.Every(1).Tuesday().Do), + startRunningTime: monday, + wantNextSchedule: tuesday, + }, + { + name: "every tuesday starting on tuesday a should run on same tuesday", + job: getJob(sched.Every(1).Tuesday().Do), + startRunningTime: tuesday, + wantNextSchedule: tuesday, + }, + { + name: "every 2 tuesdays starting on a tuesday should run next tuesday", + job: getJob(sched.Every(2).Tuesday().Do), + startRunningTime: tuesday, + wantNextSchedule: tuesday.AddDate(0, 0, 7), + }, + { + name: "every tuesday starting on a wednesday should run next tuesday", + job: getJob(sched.Every(1).Tuesday().Do), + startRunningTime: wednesday, + wantNextSchedule: tuesday.AddDate(0, 0, 7), + }, + { + name: "starting on a monday, every monday at time to happen should run at same day at time", + job: getJob(sched.Every(1).Monday().At("09:00").Do), + startRunningTime: monday, + wantNextSchedule: monday.Add(9 * time.Hour), + }, + { + name: "starting on a monday, every monday at time that already passed should run at next week at time", + job: getJob(sched.Every(1).Monday().At("09:00").Do), + startRunningTime: monday.Add(10 * time.Hour), + wantNextSchedule: monday.AddDate(0, 0, 7).Add(9 * time.Hour), + }, + } + + for i, tt := range tests { + fakeTime.onNow = func(location *time.Location) time.Time { // scheduler started + return tests[i].startRunningTime + } + + job := tt.job + sched.scheduleNextRun(job) + assert.Equal(t, tt.wantNextSchedule, job.nextRun, tt.name) + } +} + +func getJob(fn func(interface{}, ...interface{}) (*Job, error)) *Job { + j, _ := fn(func() {}) + return j +} + +type fakeTime struct { + onNow func(location *time.Location) time.Time +} + +func (f fakeTime) Now(loc *time.Location) time.Time { + return f.onNow(loc) +} + +func (f fakeTime) Unix(i int64, i2 int64) time.Time { + panic("implement me") +} + +func (f fakeTime) Sleep(duration time.Duration) { + panic("implement me") +} + +func (f fakeTime) NewTicker(duration time.Duration) *time.Ticker { + panic("implement me") } diff --git a/timeHelper.go b/timeHelper.go index 50665ce4..ef5d45ab 100644 --- a/timeHelper.go +++ b/timeHelper.go @@ -6,19 +6,13 @@ type timeWrapper interface { Now(*time.Location) time.Time Unix(int64, int64) time.Time Sleep(time.Duration) - Date(int, time.Month, int, int, int, int, int, *time.Location) time.Time NewTicker(time.Duration) *time.Ticker } -func newTimeWrapper() timeWrapper { - return &trueTime{} -} - type trueTime struct{} func (t *trueTime) Now(location *time.Location) time.Time { - n := time.Now().In(location) - return t.Date(n.Year(), n.Month(), n.Day(), n.Hour(), n.Minute(), n.Second(), 0, location) + return time.Now().In(location) } func (t *trueTime) Unix(sec int64, nsec int64) time.Time { @@ -29,10 +23,6 @@ func (t *trueTime) Sleep(d time.Duration) { time.Sleep(d) } -func (t *trueTime) Date(year int, month time.Month, day, hour, min, sec, nsec int, loc *time.Location) time.Time { - return time.Date(year, month, day, hour, min, sec, nsec, loc) -} - func (t *trueTime) NewTicker(d time.Duration) *time.Ticker { return time.NewTicker(d) }