From 3425821cd644380d70d76fffb4a9aa546c2f47c9 Mon Sep 17 00:00:00 2001 From: MattMc <4984708+tooolbox@users.noreply.github.com> Date: Mon, 13 Jul 2020 11:37:09 -0700 Subject: [PATCH 1/2] Time mocking support. --- chain.go | 6 ++++-- cron.go | 12 +++++++----- go.mod | 2 ++ go.sum | 2 ++ 4 files changed, 15 insertions(+), 7 deletions(-) create mode 100644 go.sum diff --git a/chain.go b/chain.go index 9565b418..a4e71cbb 100644 --- a/chain.go +++ b/chain.go @@ -5,6 +5,8 @@ import ( "runtime" "sync" "time" + + "github.com/mixer/clock" ) // JobWrapper decorates the given Job with some behavior. @@ -62,10 +64,10 @@ func DelayIfStillRunning(logger Logger) JobWrapper { return func(j Job) Job { var mu sync.Mutex return FuncJob(func() { - start := time.Now() + start := clock.C.Now() mu.Lock() defer mu.Unlock() - if dur := time.Since(start); dur > time.Minute { + if dur := clock.C.Since(start); dur > time.Minute { logger.Info("delay", "duration", dur) } j.Run() diff --git a/cron.go b/cron.go index c7e91766..b4a26ca9 100644 --- a/cron.go +++ b/cron.go @@ -5,6 +5,8 @@ import ( "sort" "sync" "time" + + "github.com/mixer/clock" ) // Cron keeps track of any number of entries, invoking the associated func as @@ -250,18 +252,18 @@ func (c *Cron) run() { // Determine the next entry to run. sort.Sort(byTime(c.entries)) - var timer *time.Timer + var timer clock.Timer if len(c.entries) == 0 || c.entries[0].Next.IsZero() { // If there are no entries yet, just sleep - it still handles new entries // and stop requests. - timer = time.NewTimer(100000 * time.Hour) + timer = clock.C.NewTimer(100000 * time.Hour) } else { - timer = time.NewTimer(c.entries[0].Next.Sub(now)) + timer = clock.C.NewTimer(c.entries[0].Next.Sub(now)) } for { select { - case now = <-timer.C: + case now = <-timer.Chan(): now = now.In(c.location) c.logger.Info("wake", "now", now) @@ -315,7 +317,7 @@ func (c *Cron) startJob(j Job) { // now returns current time in c location func (c *Cron) now() time.Time { - return time.Now().In(c.location) + return clock.C.Now().In(c.location) } // Stop stops the cron scheduler if it is running; otherwise it does nothing. diff --git a/go.mod b/go.mod index 8c95bf47..930e3137 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/robfig/cron/v3 go 1.12 + +require github.com/mixer/clock v0.0.0-20190507173039-c311c17adb1f diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..7f7075a1 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/mixer/clock v0.0.0-20190507173039-c311c17adb1f h1:GVMwQJIugRbOBgPK5RvPdvKPCxFex4bx+MUj2oG70XI= +github.com/mixer/clock v0.0.0-20190507173039-c311c17adb1f/go.mod h1:U8TDygO2XZh1RtBCgX7oRbJ7gmSH4C6FROsBdQ6QyCc= From 0ef26ab6babdcb0da403d1d4da95fdb371ec6f66 Mon Sep 17 00:00:00 2001 From: MattMc <4984708+tooolbox@users.noreply.github.com> Date: Mon, 13 Jul 2020 13:23:20 -0700 Subject: [PATCH 2/2] Clock per cron, and working test. --- chain.go | 10 +++++++-- cron.go | 8 ++++--- cron_test.go | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++ option.go | 9 ++++++++ 4 files changed, 83 insertions(+), 5 deletions(-) diff --git a/chain.go b/chain.go index a4e71cbb..ffc1a7bf 100644 --- a/chain.go +++ b/chain.go @@ -61,13 +61,19 @@ func Recover(logger Logger) JobWrapper { // previous one is complete. Jobs running after a delay of more than a minute // have the delay logged at Info. func DelayIfStillRunning(logger Logger) JobWrapper { + return DelayIfStillRunningWithClock(logger, clock.C) +} + +// DelayIfStillRunningWithClock behaves identically to DelayIfStillRunning but +// uses the provided Clock for measuring the delay, for use in testing. +func DelayIfStillRunningWithClock(logger Logger, clk clock.Clock) JobWrapper { return func(j Job) Job { var mu sync.Mutex return FuncJob(func() { - start := clock.C.Now() + start := clk.Now() mu.Lock() defer mu.Unlock() - if dur := clock.C.Since(start); dur > time.Minute { + if dur := clk.Since(start); dur > time.Minute { logger.Info("delay", "duration", dur) } j.Run() diff --git a/cron.go b/cron.go index b4a26ca9..753835d7 100644 --- a/cron.go +++ b/cron.go @@ -26,6 +26,7 @@ type Cron struct { parser ScheduleParser nextID EntryID jobWaiter sync.WaitGroup + clk clock.Clock } // ScheduleParser is an interface for schedule spec parsers that return a Schedule @@ -125,6 +126,7 @@ func New(opts ...Option) *Cron { logger: DefaultLogger, location: time.Local, parser: standardParser, + clk: clock.C, } for _, opt := range opts { opt(c) @@ -256,9 +258,9 @@ func (c *Cron) run() { if len(c.entries) == 0 || c.entries[0].Next.IsZero() { // If there are no entries yet, just sleep - it still handles new entries // and stop requests. - timer = clock.C.NewTimer(100000 * time.Hour) + timer = c.clk.NewTimer(100000 * time.Hour) } else { - timer = clock.C.NewTimer(c.entries[0].Next.Sub(now)) + timer = c.clk.NewTimer(c.entries[0].Next.Sub(now)) } for { @@ -317,7 +319,7 @@ func (c *Cron) startJob(j Job) { // now returns current time in c location func (c *Cron) now() time.Time { - return clock.C.Now().In(c.location) + return c.clk.Now().In(c.location) } // Stop stops the cron scheduler if it is running; otherwise it does nothing. diff --git a/cron_test.go b/cron_test.go index 36f06bf7..f9d33fcb 100644 --- a/cron_test.go +++ b/cron_test.go @@ -9,6 +9,8 @@ import ( "sync/atomic" "testing" "time" + + "github.com/mixer/clock" ) // Many tests schedule a job for every second, and then wait at most a second @@ -671,6 +673,65 @@ func TestStopAndWait(t *testing.T) { }) } +func TestScheduleBehavior(t *testing.T) { + + loc := time.FixedZone("America/Los_Angeles", -7*60*60) + start := time.Date(2020, 7, 13, 11, 50, 0, 0, loc) + clk := clock.NewMockClock(start) + cron := New( + WithClock(clk), + WithChain(), + WithLocation(loc), + ) + + ch := make(chan bool) + cron.AddFunc("50 11 31 * *", func() { + ch <- true + }) + cron.Start() + defer cron.Stop() + + expectations := []struct { + month string + shouldFire bool + }{ + {month: "Jul", shouldFire: true}, + {month: "Aug", shouldFire: true}, + {month: "Sep", shouldFire: false}, + {month: "Oct", shouldFire: true}, + {month: "Nov", shouldFire: false}, + {month: "Dec", shouldFire: true}, + {month: "Jan", shouldFire: true}, + {month: "Feb", shouldFire: false}, + {month: "Mar", shouldFire: true}, + {month: "Apr", shouldFire: false}, + {month: "May", shouldFire: true}, + {month: "Jun", shouldFire: false}, + } + + t.Logf("Start date: %s", clk.Now().Format(time.RFC3339)) + for _, exp := range expectations { + + time.Sleep(time.Millisecond) + clk.AddTime(clk.Now().AddDate(0, 1, 0).Sub(clk.Now())) + t.Logf("New date: %s", clk.Now().Format(time.RFC3339)) + time.Sleep(time.Millisecond) + + select { + case <-ch: + if !exp.shouldFire { + t.Fatalf("job unexpectedly fired in %s", exp.month) + } + t.Logf("job fired in %s", exp.month) + case <-time.After(time.Second): + if exp.shouldFire { + t.Fatalf("job should have fired in %s", exp.month) + } + } + } + +} + func TestMultiThreadedStartAndStop(t *testing.T) { cron := New() go cron.Run() diff --git a/option.go b/option.go index 09e4278e..84ec93a8 100644 --- a/option.go +++ b/option.go @@ -2,6 +2,8 @@ package cron import ( "time" + + "github.com/mixer/clock" ) // Option represents a modification to the default behavior of a Cron. @@ -43,3 +45,10 @@ func WithLogger(logger Logger) Option { c.logger = logger } } + +// WithClock uses the provided clock to track time. +func WithClock(clk clock.Clock) Option { + return func(c *Cron) { + c.clk = clk + } +}