Skip to content
Merged
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
11 changes: 10 additions & 1 deletion cron.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import (
"time"
)

// Clock is a function that returns the current time.
// It can be overridden using WithClock for testing purposes.
type Clock func() time.Time

// Cron keeps track of any number of entries, invoking the associated func as
// specified by the schedule. It may be started, stopped, and the entries may
// be inspected while running.
Expand All @@ -24,6 +28,7 @@ type Cron struct {
parser ScheduleParser
nextID EntryID
jobWaiter sync.WaitGroup
clock Clock
}

// ScheduleParser is an interface for schedule spec parsers that return a Schedule
Expand Down Expand Up @@ -335,8 +340,12 @@ func (c *Cron) startJob(j Job) {
}()
}

// now returns current time in c location
// now returns current time in c location.
// If a custom clock is configured via WithClock, it will be used instead of time.Now.
func (c *Cron) now() time.Time {
if c.clock != nil {
return c.clock().In(c.location)
}
return time.Now().In(c.location)
}

Expand Down
19 changes: 19 additions & 0 deletions option.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,22 @@ func WithLogger(logger Logger) Option {
c.logger = logger
}
}

// WithClock uses the provided clock function instead of time.Now.
// This is useful for testing time-dependent behavior without waiting.
//
// Example usage:
//
// // For testing, use a fixed time
// fixedTime := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
// c := cron.New(cron.WithClock(func() time.Time { return fixedTime }))
//
// // Or advance time manually
// var currentTime atomic.Value
// currentTime.Store(time.Now())
// c := cron.New(cron.WithClock(func() time.Time { return currentTime.Load().(time.Time) }))
func WithClock(clock Clock) Option {
return func(c *Cron) {
c.clock = clock
}
}
51 changes: 51 additions & 0 deletions option_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,54 @@ func TestWithVerboseLogger(t *testing.T) {
t.Error("expected to see some actions, got:", out)
}
}

func TestWithClock(t *testing.T) {
fixedTime := time.Date(2024, 6, 15, 12, 0, 0, 0, time.UTC)
c := New(WithClock(func() time.Time { return fixedTime }))
if c.clock == nil {
t.Error("expected clock to be set")
}

// Verify now() uses the custom clock
now := c.now()
if !now.Equal(fixedTime) {
t.Errorf("expected %v, got %v", fixedTime, now)
}
}

func TestWithClockAndLocation(t *testing.T) {
fixedTime := time.Date(2024, 6, 15, 12, 0, 0, 0, time.UTC)
loc, _ := time.LoadLocation("America/New_York")

c := New(
WithClock(func() time.Time { return fixedTime }),
WithLocation(loc),
)

// Verify now() converts to the specified location
now := c.now()
if now.Location() != loc {
t.Errorf("expected location %v, got %v", loc, now.Location())
}

// The time should be the same instant, just in a different location
if !now.Equal(fixedTime) {
t.Errorf("times should represent the same instant: %v vs %v", now, fixedTime)
}
}

func TestWithClockNilFallback(t *testing.T) {
// When no clock is set, now() should use time.Now()
c := New()
if c.clock != nil {
t.Error("expected clock to be nil by default")
}

before := time.Now()
now := c.now()
after := time.Now()

if now.Before(before) || now.After(after) {
t.Errorf("now() should return current time, got %v (expected between %v and %v)", now, before, after)
}
}
Loading