Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Time Mocking #327

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"runtime"
"sync"
"time"

"github.com/mixer/clock"
)

// JobWrapper decorates the given Job with some behavior.
Expand Down Expand Up @@ -59,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 := time.Now()
start := clk.Now()
mu.Lock()
defer mu.Unlock()
if dur := time.Since(start); dur > time.Minute {
if dur := clk.Since(start); dur > time.Minute {
logger.Info("delay", "duration", dur)
}
j.Run()
Expand Down
14 changes: 9 additions & 5 deletions cron.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -24,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
Expand Down Expand Up @@ -123,6 +126,7 @@ func New(opts ...Option) *Cron {
logger: DefaultLogger,
location: time.Local,
parser: standardParser,
clk: clock.C,
}
for _, opt := range opts {
opt(c)
Expand Down Expand Up @@ -250,18 +254,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 = c.clk.NewTimer(100000 * time.Hour)
} else {
timer = time.NewTimer(c.entries[0].Next.Sub(now))
timer = c.clk.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)

Expand Down Expand Up @@ -315,7 +319,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 c.clk.Now().In(c.location)
}

// Stop stops the cron scheduler if it is running; otherwise it does nothing.
Expand Down
61 changes: 61 additions & 0 deletions cron_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
module github.com/robfig/cron/v3

go 1.12

require github.com/mixer/clock v0.0.0-20190507173039-c311c17adb1f
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
9 changes: 9 additions & 0 deletions option.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package cron

import (
"time"

"github.com/mixer/clock"
)

// Option represents a modification to the default behavior of a Cron.
Expand Down Expand Up @@ -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
}
}