From 92fb80d55f18b7a8341403abaa57c000b32ae586 Mon Sep 17 00:00:00 2001 From: Adrian Dewhurst Date: Fri, 23 Jun 2023 13:45:59 -0400 Subject: [PATCH] tstest, tstime: mockable timers and tickers This change introduces tstime.Clock which is the start of a mockable interface for use with testing other upcoming code changes. Fixes #8463 Change-Id: I59eabc797828809194575736615535d918242ec4 Signed-off-by: Adrian Dewhurst --- tstest/clock.go | 663 +++++++++++- tstest/clock_test.go | 2439 ++++++++++++++++++++++++++++++++++++++++++ tstime/tstime.go | 76 ++ 3 files changed, 3124 insertions(+), 54 deletions(-) create mode 100644 tstest/clock_test.go diff --git a/tstest/clock.go b/tstest/clock.go index a5b4af55c..bd63278c8 100644 --- a/tstest/clock.go +++ b/tstest/clock.go @@ -4,8 +4,12 @@ package tstest import ( + "container/heap" "sync" "time" + + "tailscale.com/tstime" + "tailscale.com/util/mak" ) // ClockOpts is used to configure the initial settings for a Clock. Once the @@ -43,92 +47,643 @@ type ClockOpts struct { // Clock with only the default settings, new(Clock) is equivalent, except that // the start time will not be computed until one of the receivers is called. func NewClock(co ClockOpts) *Clock { - if co.TimerChannelSize != 0 || co.FollowRealTime { - panic("TimerChannelSize and FollowRealTime are not implemented yet") + if co.FollowRealTime && co.Step != 0 { + panic("only one of FollowRealTime and Step are allowed in NewClock") + } + + return newClockInternal(co, nil) +} + +// newClockInternal creates a Clock with the specified settings and allows +// specifying a non-standard realTimeClock. +func newClockInternal(co ClockOpts, rtClock tstime.Clock) *Clock { + if !co.FollowRealTime && rtClock != nil { + panic("rtClock can only be set with FollowRealTime enabled") } - clock := &Clock{ - Start: co.Start, - Step: co.Step, + if co.FollowRealTime && rtClock == nil { + rtClock = new(tstime.StdClock) } - clock.Lock() - defer clock.Unlock() - clock.initLocked() - return clock + c := &Clock{ + start: co.Start, + realTimeClock: rtClock, + step: co.Step, + timerChannelSize: co.TimerChannelSize, + } + c.init() // init now to capture the current time when co.Start.IsZero() + return c } // Clock is a testing clock that advances every time its Now method is -// called, beginning at Start. -// -// The zero value starts virtual time at an arbitrary value recorded -// in Start on the first call to Now, and time never advances. +// called, beginning at its start time. If no start time is specified using +// ClockBuilder, an arbitrary start time will be selected when the Clock is +// created and can be retrieved by calling Clock.Start(). type Clock struct { - // Start is the first value returned by Now. - Start time.Time - // Step is how much to advance with each Now call. - Step time.Duration - // Present is the time that the next Now call will receive. - Present time.Time + // start is the first value returned by Now. It must not be modified after + // init is called. + start time.Time + + // realTimeClock, if not nil, indicates that the Clock shall move forward + // according to realTimeClock + the accumulated calls to Advance. This can + // make writing tests easier that require some control over the clock but do + // not need exact control over the clock. While step can also be used for + // this purpose, it is harder to control how quickly time moves using step. + realTimeClock tstime.Clock + + initOnce sync.Once + mu sync.Mutex - sync.Mutex + // step is how much to advance with each Now call. + step time.Duration + // present is the last value returned by Now (and will be returned again by + // PeekNow). + present time.Time + // realTime is the time from realTimeClock corresponding to the current + // value of present. + realTime time.Time + // skipStep indicates that the next call to Now should not add step to + // present. This occurs after initialization and after Advance. + skipStep bool + // timerChannelSize is the buffer size to use for channels created by + // NewTimer and NewTicker. + timerChannelSize int + + events eventManager +} + +func (c *Clock) init() { + c.initOnce.Do(func() { + if c.realTimeClock != nil { + c.realTime = c.realTimeClock.Now() + } + if c.start.IsZero() { + if c.realTime.IsZero() { + c.start = time.Now() + } else { + c.start = c.realTime + } + } + if c.timerChannelSize == 0 { + c.timerChannelSize = 1 + } + c.present = c.start + c.skipStep = true + c.events.AdvanceTo(c.present) + }) } // Now returns the virtual clock's current time, and advances it // according to its step configuration. func (c *Clock) Now() time.Time { - c.Lock() - defer c.Unlock() - c.initLocked() - step := c.Step - ret := c.Present - c.Present = c.Present.Add(step) - return ret + c.init() + rt := c.maybeGetRealTime() + + c.mu.Lock() + defer c.mu.Unlock() + + step := c.step + if c.skipStep { + step = 0 + c.skipStep = false + } + c.advanceLocked(rt, step) + + return c.present } -func (c *Clock) Advance(d time.Duration) { - c.Lock() - defer c.Unlock() - c.initLocked() - c.Present = c.Present.Add(d) +func (c *Clock) maybeGetRealTime() time.Time { + if c.realTimeClock == nil { + return time.Time{} + } + return c.realTimeClock.Now() } -func (c *Clock) initLocked() { - if c.Start.IsZero() { - c.Start = time.Now() +func (c *Clock) advanceLocked(now time.Time, add time.Duration) { + if !now.IsZero() { + add += now.Sub(c.realTime) + c.realTime = now } - if c.Present.Before(c.Start) { - c.Present = c.Start + if add == 0 { + return } + c.present = c.present.Add(add) + c.events.AdvanceTo(c.present) } -// Reset rewinds the virtual clock to its start time. -func (c *Clock) Reset() { - c.Lock() - defer c.Unlock() - c.Present = c.Start +// PeekNow returns the last time reported by Now. If Now has never been called, +// PeekNow returns the same value as GetStart. +func (c *Clock) PeekNow() time.Time { + c.init() + c.mu.Lock() + defer c.mu.Unlock() + return c.present +} + +// Advance moves simulated time forward or backwards by a relative amount. Any +// Timer or Ticker that is waiting will fire at the requested point in simulated +// time. Advance returns the new simulated time. If this Clock follows real time +// then the next call to Now will equal the return value of Advance + the +// elapsed time since calling Advance. Otherwise, the next call to Now will +// equal the return value of Advance, regardless of the current step. +func (c *Clock) Advance(d time.Duration) time.Time { + c.init() + rt := c.maybeGetRealTime() + + c.mu.Lock() + defer c.mu.Unlock() + c.skipStep = true + + c.advanceLocked(rt, d) + return c.present +} + +// AdvanceTo moves simulated time to a new absolute value. Any Timer or Ticker +// that is waiting will fire at the requested point in simulated time. If this +// Clock follows real time then the next call to Now will equal t + the elapsed +// time since calling Advance. Otherwise, the next call to Now will equal t, +// regardless of the configured step. +func (c *Clock) AdvanceTo(t time.Time) { + c.init() + rt := c.maybeGetRealTime() + + c.mu.Lock() + defer c.mu.Unlock() + c.skipStep = true + c.realTime = rt + c.present = t + c.events.AdvanceTo(c.present) } // GetStart returns the initial simulated time when this Clock was created. func (c *Clock) GetStart() time.Time { - c.Lock() - defer c.Unlock() - c.initLocked() - return c.Start + c.init() + c.mu.Lock() + defer c.mu.Unlock() + return c.start } // GetStep returns the amount that simulated time advances on every call to Now. func (c *Clock) GetStep() time.Duration { - c.Lock() - defer c.Unlock() - c.initLocked() - return c.Step + c.init() + c.mu.Lock() + defer c.mu.Unlock() + return c.step } // SetStep updates the amount that simulated time advances on every call to Now. func (c *Clock) SetStep(d time.Duration) { - c.Lock() - defer c.Unlock() - c.initLocked() - c.Step = d + c.init() + c.mu.Lock() + defer c.mu.Unlock() + c.step = d +} + +// SetTimerChannelSize changes the channel size for any Timer or Ticker created +// in the future. It does not affect those that were already created. +func (c *Clock) SetTimerChannelSize(n int) { + c.init() + c.mu.Lock() + defer c.mu.Unlock() + c.timerChannelSize = n +} + +// NewTicker returns a Ticker that uses this Clock for accessing the current +// time. +func (c *Clock) NewTicker(d time.Duration) (tstime.TickerController, <-chan time.Time) { + c.init() + rt := c.maybeGetRealTime() + + c.mu.Lock() + defer c.mu.Unlock() + + c.advanceLocked(rt, 0) + t := &Ticker{ + nextTrigger: c.present.Add(d), + period: d, + em: &c.events, + } + t.init(c.timerChannelSize) + return t, t.C +} + +// NewTimer returns a Timer that uses this Clock for accessing the current +// time. +func (c *Clock) NewTimer(d time.Duration) (tstime.TimerController, <-chan time.Time) { + c.init() + rt := c.maybeGetRealTime() + + c.mu.Lock() + defer c.mu.Unlock() + + c.advanceLocked(rt, 0) + t := &Timer{ + nextTrigger: c.present.Add(d), + em: &c.events, + } + t.init(c.timerChannelSize, nil) + return t, t.C +} + +// AfterFunc returns a Timer that calls f when it fires, using this Clock for +// accessing the current time. +func (c *Clock) AfterFunc(d time.Duration, f func()) tstime.TimerController { + c.init() + rt := c.maybeGetRealTime() + + c.mu.Lock() + defer c.mu.Unlock() + + c.advanceLocked(rt, 0) + t := &Timer{ + nextTrigger: c.present.Add(d), + em: &c.events, + } + t.init(c.timerChannelSize, f) + return t +} + +// eventHandler offers a common interface for Timer and Ticker events to avoid +// code duplication in eventManager. +type eventHandler interface { + // Fire signals the event. The provided time is written to the event's + // channel as the current time. The return value is the next time this event + // should fire, otherwise if it is zero then the event will be removed from + // the eventManager. + Fire(time.Time) time.Time +} + +// event tracks details about an upcoming Timer or Ticker firing. +type event struct { + position int // The current index in the heap, needed for heap.Fix and heap.Remove. + when time.Time // A cache of the next time the event triggers to avoid locking issues if we were to get it from eh. + eh eventHandler +} + +// eventManager tracks pending events created by Timer and Ticker. eventManager +// implements heap.Interface for efficient lookups of the next event. +type eventManager struct { + // clock is a real time clock for scheduling events with. When clock is nil, + // events only fire when AdvanceTo is called by the simulated clock that + // this eventManager belongs to. When clock is not nil, events may fire when + // timer triggers. + clock tstime.Clock + + mu sync.Mutex + now time.Time + heap []*event + reverseLookup map[eventHandler]*event + + // timer is an AfterFunc that triggers at heap[0].when.Sub(now) relative to + // the time represented by clock. In other words, if clock is real world + // time, then if an event is scheduled 1 second into the future in the + // simulated time, then the event will trigger after 1 second of actual test + // execution time (unless the test advances simulated time, in which case + // the timer is updated accordingly). This makes tests easier to write in + // situations where the simulated time only needs to be partially + // controlled, and the test writer wishes for simulated time to pass with an + // offset but still synchronized with the real world. + // + // In the future, this could be extended to allow simulated time to run at a + // multiple of real world time. + timer tstime.TimerController +} + +func (em *eventManager) handleTimer() { + rt := em.clock.Now() + em.AdvanceTo(rt) +} + +// Push implements heap.Interface.Push and must only be called by heap funcs +// with em.mu already held. +func (em *eventManager) Push(x any) { + e, ok := x.(*event) + if !ok { + panic("incorrect event type") + } + if e == nil { + panic("nil event") + } + + mak.Set(&em.reverseLookup, e.eh, e) + e.position = len(em.heap) + em.heap = append(em.heap, e) +} + +// Pop implements heap.Interface.Pop and must only be called by heap funcs with +// em.mu already held. +func (em *eventManager) Pop() any { + e := em.heap[len(em.heap)-1] + em.heap = em.heap[:len(em.heap)-1] + delete(em.reverseLookup, e.eh) + return e +} + +// Len implements sort.Interface.Len and must only be called by heap funcs with +// em.mu already held. +func (em *eventManager) Len() int { + return len(em.heap) +} + +// Less implements sort.Interface.Less and must only be called by heap funcs +// with em.mu already held. +func (em *eventManager) Less(i, j int) bool { + return em.heap[i].when.Before(em.heap[j].when) +} + +// Swap implements sort.Interface.Swap and must only be called by heap funcs +// with em.mu already held. +func (em *eventManager) Swap(i, j int) { + em.heap[i], em.heap[j] = em.heap[j], em.heap[i] + em.heap[i].position = i + em.heap[j].position = j +} + +// Reschedule adds/updates/deletes an event in the heap, whichever +// operation is applicable (use a zero time to delete). +func (em *eventManager) Reschedule(eh eventHandler, t time.Time) { + em.mu.Lock() + defer em.mu.Unlock() + defer em.updateTimerLocked() + + e, ok := em.reverseLookup[eh] + if !ok { + if t.IsZero() { + // eh is not scheduled and also not active, so do nothing. + return + } + // eh is not scheduled but is active, so add it. + heap.Push(em, &event{ + when: t, + eh: eh, + }) + em.processEventsLocked(em.now) // This is always safe and required when !t.After(em.now). + return + } + + if t.IsZero() { + // e is scheduled but not active, so remove it. + heap.Remove(em, e.position) + return + } + + // e is scheduled and active, so update it. + e.when = t + heap.Fix(em, e.position) + em.processEventsLocked(em.now) // This is always safe and required when !t.After(em.now). +} + +// AdvanceTo updates the current time to tm and fires all events scheduled +// before or equal to tm. When an event fires, it may request rescheduling and +// the rescheduled events will be combined with the other existing events that +// are waiting, and will be run in the unified ordering. A poorly behaved event +// may theoretically prevent this from ever completing, but both Timer and +// Ticker require positive steps into the future. +func (em *eventManager) AdvanceTo(tm time.Time) { + em.mu.Lock() + defer em.mu.Unlock() + defer em.updateTimerLocked() + + em.processEventsLocked(tm) + em.now = tm +} + +// Now returns the cached current time. It is intended for use by a Timer or +// Ticker that needs to convert a relative time to an absolute time. +func (em *eventManager) Now() time.Time { + em.mu.Lock() + defer em.mu.Unlock() + return em.now +} + +func (em *eventManager) processEventsLocked(tm time.Time) { + for len(em.heap) > 0 && !em.heap[0].when.After(tm) { + // Ideally some jitter would be added here but it's difficult to do so + // in a deterministic fashion. + em.now = em.heap[0].when + + if nextFire := em.heap[0].eh.Fire(em.now); !nextFire.IsZero() { + em.heap[0].when = nextFire + heap.Fix(em, 0) + } else { + heap.Pop(em) + } + } +} + +func (em *eventManager) updateTimerLocked() { + if em.clock == nil { + return + } + if len(em.heap) == 0 { + if em.timer != nil { + em.timer.Stop() + } + return + } + + timeToEvent := em.heap[0].when.Sub(em.now) + if em.timer == nil { + em.timer = em.clock.AfterFunc(timeToEvent, em.handleTimer) + return + } + em.timer.Reset(timeToEvent) +} + +// Ticker is a time.Ticker lookalike for use in tests that need to control when +// events fire. Ticker could be made standalone in future but for now is +// expected to be paired with a Clock and created by Clock.NewTicker. +type Ticker struct { + C <-chan time.Time // The channel on which ticks are delivered. + + // em is the eventManager to be notified when nextTrigger changes. + // eventManager has its own mutex, and the pointer is immutable, therefore + // em can be accessed without holding mu. + em *eventManager + + c chan<- time.Time // The writer side of C. + + mu sync.Mutex + + // nextTrigger is the time of the ticker's next scheduled activation. When + // Fire activates the ticker, nextTrigger is the timestamp written to the + // channel. + nextTrigger time.Time + + // period is the duration that is added to nextTrigger when the ticker + // fires. + period time.Duration +} + +func (t *Ticker) init(channelSize int) { + if channelSize <= 0 { + panic("ticker channel size must be non-negative") + } + c := make(chan time.Time, channelSize) + t.c = c + t.C = c + t.em.Reschedule(t, t.nextTrigger) +} + +// Fire triggers the ticker. curTime is the timestamp to write to the channel. +// The next trigger time for the ticker is updated to the last computed trigger +// time + the ticker period (set at creation or using Reset). The next trigger +// time is computed this way to match standard time.Ticker behavior, which +// prevents accumulation of long term drift caused by delays in event execution. +func (t *Ticker) Fire(curTime time.Time) time.Time { + t.mu.Lock() + defer t.mu.Unlock() + + if t.nextTrigger.IsZero() { + return time.Time{} + } + select { + case t.c <- curTime: + default: + } + t.nextTrigger = t.nextTrigger.Add(t.period) + + return t.nextTrigger +} + +// Reset adjusts the Ticker's period to d and reschedules the next fire time to +// the current simulated time + d. +func (t *Ticker) Reset(d time.Duration) { + if d <= 0 { + // The standard time.Ticker requires a positive period. + panic("non-positive period for Ticker.Reset") + } + + now := t.em.Now() + + t.mu.Lock() + t.resetLocked(now.Add(d), d) + t.mu.Unlock() + + t.em.Reschedule(t, t.nextTrigger) +} + +// ResetAbsolute adjusts the Ticker's period to d and reschedules the next fire +// time to nextTrigger. +func (t *Ticker) ResetAbsolute(nextTrigger time.Time, d time.Duration) { + if nextTrigger.IsZero() { + panic("zero nextTrigger time for ResetAbsolute") + } + if d <= 0 { + panic("non-positive period for ResetAbsolute") + } + + t.mu.Lock() + t.resetLocked(nextTrigger, d) + t.mu.Unlock() + + t.em.Reschedule(t, t.nextTrigger) +} + +func (t *Ticker) resetLocked(nextTrigger time.Time, d time.Duration) { + t.nextTrigger = nextTrigger + t.period = d +} + +// Stop deactivates the Ticker. +func (t *Ticker) Stop() { + t.mu.Lock() + t.nextTrigger = time.Time{} + t.mu.Unlock() + + t.em.Reschedule(t, t.nextTrigger) +} + +// Timer is a time.Timer lookalike for use in tests that need to control when +// events fire. Timer could be made standalone in future but for now must be +// paired with a Clock and created by Clock.NewTimer. +type Timer struct { + C <-chan time.Time // The channel on which ticks are delivered. + + // em is the eventManager to be notified when nextTrigger changes. + // eventManager has its own mutex, and the pointer is immutable, therefore + // em can be accessed without holding mu. + em *eventManager + + f func(time.Time) // The function to call when the timer expires. + + mu sync.Mutex + + // nextTrigger is the time of the ticker's next scheduled activation. When + // Fire activates the ticker, nextTrigger is the timestamp written to the + // channel. + nextTrigger time.Time +} + +func (t *Timer) init(channelSize int, afterFunc func()) { + if channelSize <= 0 { + panic("ticker channel size must be non-negative") + } + c := make(chan time.Time, channelSize) + t.C = c + if afterFunc == nil { + t.f = func(curTime time.Time) { + select { + case c <- curTime: + default: + } + } + } else { + t.f = func(_ time.Time) { afterFunc() } + } + t.em.Reschedule(t, t.nextTrigger) +} + +// Fire triggers the ticker. curTime is the timestamp to write to the channel. +// The next trigger time for the ticker is updated to the last computed trigger +// time + the ticker period (set at creation or using Reset). The next trigger +// time is computed this way to match standard time.Ticker behavior, which +// prevents accumulation of long term drift caused by delays in event execution. +func (t *Timer) Fire(curTime time.Time) time.Time { + t.mu.Lock() + defer t.mu.Unlock() + + if t.nextTrigger.IsZero() { + return time.Time{} + } + t.nextTrigger = time.Time{} + t.f(curTime) + return time.Time{} +} + +// Reset reschedules the next fire time to the current simulated time + d. +// Reset reports whether the timer was still active before the reset. +func (t *Timer) Reset(d time.Duration) bool { + if d <= 0 { + // The standard time.Timer requires a positive delay. + panic("non-positive delay for Timer.Reset") + } + + return t.reset(t.em.Now().Add(d)) +} + +// ResetAbsolute reschedules the next fire time to nextTrigger. +// ResetAbsolute reports whether the timer was still active before the reset. +func (t *Timer) ResetAbsolute(nextTrigger time.Time) bool { + if nextTrigger.IsZero() { + panic("zero nextTrigger time for ResetAbsolute") + } + + return t.reset(nextTrigger) +} + +// Stop deactivates the Timer. Stop reports whether the timer was active before +// stopping. +func (t *Timer) Stop() bool { + return t.reset(time.Time{}) +} + +func (t *Timer) reset(nextTrigger time.Time) bool { + t.mu.Lock() + wasActive := !t.nextTrigger.IsZero() + t.nextTrigger = nextTrigger + t.mu.Unlock() + + t.em.Reschedule(t, t.nextTrigger) + return wasActive } diff --git a/tstest/clock_test.go b/tstest/clock_test.go new file mode 100644 index 000000000..42572827b --- /dev/null +++ b/tstest/clock_test.go @@ -0,0 +1,2439 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package tstest + +import ( + "sync/atomic" + "testing" + "time" + + "golang.org/x/exp/slices" + "tailscale.com/tstime" +) + +func TestClockWithDefinedStartTime(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + start time.Time + step time.Duration + wants []time.Time // The return values of sequential calls to Now(). + }{ + { + name: "increment ms", + start: time.Unix(12345, 1000), + step: 1000, + wants: []time.Time{ + time.Unix(12345, 1000), + time.Unix(12345, 2000), + time.Unix(12345, 3000), + time.Unix(12345, 4000), + }, + }, + { + name: "increment second", + start: time.Unix(12345, 1000), + step: time.Second, + wants: []time.Time{ + time.Unix(12345, 1000), + time.Unix(12346, 1000), + time.Unix(12347, 1000), + time.Unix(12348, 1000), + }, + }, + { + name: "no increment", + start: time.Unix(12345, 1000), + wants: []time.Time{ + time.Unix(12345, 1000), + time.Unix(12345, 1000), + time.Unix(12345, 1000), + time.Unix(12345, 1000), + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + clock := NewClock(ClockOpts{ + Start: tt.start, + Step: tt.step, + }) + + if start := clock.GetStart(); !start.Equal(tt.start) { + t.Errorf("clock has start %v, want %v", start, tt.start) + } + if step := clock.GetStep(); step != tt.step { + t.Errorf("clock has step %v, want %v", step, tt.step) + } + + for i := range tt.wants { + if got := clock.Now(); !got.Equal(tt.wants[i]) { + t.Errorf("step %v: clock.Now() = %v, want %v", i, got, tt.wants[i]) + } + if got := clock.PeekNow(); !got.Equal(tt.wants[i]) { + t.Errorf("step %v: clock.PeekNow() = %v, want %v", i, got, tt.wants[i]) + } + } + }) + } +} + +func TestClockWithDefaultStartTime(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + step time.Duration + wants []time.Duration // The return values of sequential calls to Now() after added to Start() + }{ + { + name: "increment ms", + step: 1000, + wants: []time.Duration{ + 0, + 1000, + 2000, + 3000, + }, + }, + { + name: "increment second", + step: time.Second, + wants: []time.Duration{ + 0 * time.Second, + 1 * time.Second, + 2 * time.Second, + 3 * time.Second, + }, + }, + { + name: "no increment", + wants: []time.Duration{0, 0, 0, 0}, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + clock := NewClock(ClockOpts{ + Step: tt.step, + }) + start := clock.GetStart() + + if step := clock.GetStep(); step != tt.step { + t.Errorf("clock has step %v, want %v", step, tt.step) + } + + for i := range tt.wants { + want := start.Add(tt.wants[i]) + if got := clock.Now(); !got.Equal(want) { + t.Errorf("step %v: clock.Now() = %v, want %v", i, got, tt.wants[i]) + } + if got := clock.PeekNow(); !got.Equal(want) { + t.Errorf("step %v: clock.PeekNow() = %v, want %v", i, got, tt.wants[i]) + } + } + }) + } +} + +func TestZeroInitClock(t *testing.T) { + t.Parallel() + + var clock Clock + start := clock.GetStart() + + if step := clock.GetStep(); step != 0 { + t.Errorf("clock has step %v, want 0", step) + } + + for i := 0; i < 10; i++ { + if got := clock.Now(); !got.Equal(start) { + t.Errorf("step %v: clock.Now() = %v, want %v", i, got, start) + } + if got := clock.PeekNow(); !got.Equal(start) { + t.Errorf("step %v: clock.PeekNow() = %v, want %v", i, got, start) + } + } +} + +func TestClockSetStep(t *testing.T) { + t.Parallel() + + type stepInfo struct { + when int + step time.Duration + } + + tests := []struct { + name string + start time.Time + step time.Duration + stepChanges []stepInfo + wants []time.Time // The return values of sequential calls to Now(). + }{ + { + name: "increment ms then s", + start: time.Unix(12345, 1000), + step: 1000, + stepChanges: []stepInfo{ + { + when: 4, + step: time.Second, + }, + }, + wants: []time.Time{ + time.Unix(12345, 1000), + time.Unix(12345, 2000), + time.Unix(12345, 3000), + time.Unix(12345, 4000), + time.Unix(12346, 4000), + time.Unix(12347, 4000), + time.Unix(12348, 4000), + time.Unix(12349, 4000), + }, + }, + { + name: "multiple changes over time", + start: time.Unix(12345, 1000), + step: 1, + stepChanges: []stepInfo{ + { + when: 2, + step: time.Second, + }, + { + when: 4, + step: 0, + }, + { + when: 6, + step: 1000, + }, + }, + wants: []time.Time{ + time.Unix(12345, 1000), + time.Unix(12345, 1001), + time.Unix(12346, 1001), + time.Unix(12347, 1001), + time.Unix(12347, 1001), + time.Unix(12347, 1001), + time.Unix(12347, 2001), + time.Unix(12347, 3001), + }, + }, + { + name: "multiple changes at once", + start: time.Unix(12345, 1000), + step: 1, + stepChanges: []stepInfo{ + { + when: 2, + step: time.Second, + }, + { + when: 2, + step: 0, + }, + { + when: 2, + step: 1000, + }, + }, + wants: []time.Time{ + time.Unix(12345, 1000), + time.Unix(12345, 1001), + time.Unix(12345, 2001), + time.Unix(12345, 3001), + }, + }, + { + name: "changes at start", + start: time.Unix(12345, 1000), + step: 0, + stepChanges: []stepInfo{ + { + when: 0, + step: time.Second, + }, + { + when: 0, + step: 1000, + }, + }, + wants: []time.Time{ + time.Unix(12345, 1000), + time.Unix(12345, 2000), + time.Unix(12345, 3000), + time.Unix(12345, 4000), + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + clock := NewClock(ClockOpts{ + Start: tt.start, + Step: tt.step, + }) + wantStep := tt.step + changeIndex := 0 + + for i := range tt.wants { + for len(tt.stepChanges) > changeIndex && tt.stepChanges[changeIndex].when == i { + wantStep = tt.stepChanges[changeIndex].step + clock.SetStep(wantStep) + changeIndex++ + } + + if start := clock.GetStart(); !start.Equal(tt.start) { + t.Errorf("clock has start %v, want %v", start, tt.start) + } + if step := clock.GetStep(); step != wantStep { + t.Errorf("clock has step %v, want %v", step, tt.step) + } + + if got := clock.Now(); !got.Equal(tt.wants[i]) { + t.Errorf("step %v: clock.Now() = %v, want %v", i, got, tt.wants[i]) + } + if got := clock.PeekNow(); !got.Equal(tt.wants[i]) { + t.Errorf("step %v: clock.PeekNow() = %v, want %v", i, got, tt.wants[i]) + } + } + }) + } +} + +func TestClockAdvance(t *testing.T) { + t.Parallel() + + type advanceInfo struct { + when int + advance time.Duration + } + + tests := []struct { + name string + start time.Time + step time.Duration + advances []advanceInfo + wants []time.Time // The return values of sequential calls to Now(). + }{ + { + name: "increment ms then advance 1s", + start: time.Unix(12345, 1000), + step: 1000, + advances: []advanceInfo{ + { + when: 4, + advance: time.Second, + }, + }, + wants: []time.Time{ + time.Unix(12345, 1000), + time.Unix(12345, 2000), + time.Unix(12345, 3000), + time.Unix(12345, 4000), + time.Unix(12346, 4000), + time.Unix(12346, 5000), + time.Unix(12346, 6000), + time.Unix(12346, 7000), + }, + }, + { + name: "multiple advances over time", + start: time.Unix(12345, 1000), + step: 1, + advances: []advanceInfo{ + { + when: 2, + advance: time.Second, + }, + { + when: 4, + advance: 0, + }, + { + when: 6, + advance: 1000, + }, + }, + wants: []time.Time{ + time.Unix(12345, 1000), + time.Unix(12345, 1001), + time.Unix(12346, 1001), + time.Unix(12346, 1002), + time.Unix(12346, 1002), + time.Unix(12346, 1003), + time.Unix(12346, 2003), + time.Unix(12346, 2004), + }, + }, + { + name: "multiple advances at once", + start: time.Unix(12345, 1000), + step: 1, + advances: []advanceInfo{ + { + when: 2, + advance: time.Second, + }, + { + when: 2, + advance: 0, + }, + { + when: 2, + advance: 1000, + }, + }, + wants: []time.Time{ + time.Unix(12345, 1000), + time.Unix(12345, 1001), + time.Unix(12346, 2001), + time.Unix(12346, 2002), + }, + }, + { + name: "changes at start", + start: time.Unix(12345, 1000), + step: 5, + advances: []advanceInfo{ + { + when: 0, + advance: time.Second, + }, + { + when: 0, + advance: 1000, + }, + }, + wants: []time.Time{ + time.Unix(12346, 2000), + time.Unix(12346, 2005), + time.Unix(12346, 2010), + time.Unix(12346, 2015), + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + clock := NewClock(ClockOpts{ + Start: tt.start, + Step: tt.step, + }) + wantStep := tt.step + changeIndex := 0 + + for i := range tt.wants { + for len(tt.advances) > changeIndex && tt.advances[changeIndex].when == i { + clock.Advance(tt.advances[changeIndex].advance) + changeIndex++ + } + + if start := clock.GetStart(); !start.Equal(tt.start) { + t.Errorf("clock has start %v, want %v", start, tt.start) + } + if step := clock.GetStep(); step != wantStep { + t.Errorf("clock has step %v, want %v", step, tt.step) + } + + if got := clock.Now(); !got.Equal(tt.wants[i]) { + t.Errorf("step %v: clock.Now() = %v, want %v", i, got, tt.wants[i]) + } + if got := clock.PeekNow(); !got.Equal(tt.wants[i]) { + t.Errorf("step %v: clock.PeekNow() = %v, want %v", i, got, tt.wants[i]) + } + } + }) + } +} + +func expectNoTicks(t *testing.T, tickC <-chan time.Time) { + t.Helper() + select { + case tick := <-tickC: + t.Errorf("wanted no ticks, got %v", tick) + default: + } +} + +func TestSingleTicker(t *testing.T) { + t.Parallel() + + type testStep struct { + stop bool + reset time.Duration + resetAbsolute time.Time + setStep time.Duration + advance time.Duration + advanceRealTime time.Duration + wantTime time.Time + wantTicks []time.Time + } + + tests := []struct { + name string + realTimeOpts *ClockOpts + start time.Time + step time.Duration + period time.Duration + channelSize int + steps []testStep + }{ + { + name: "no tick advance", + start: time.Unix(12345, 0), + period: time.Second, + steps: []testStep{ + { + advance: time.Second - 1, + wantTime: time.Unix(12345, 999_999_999), + }, + }, + }, + { + name: "no tick step", + start: time.Unix(12345, 0), + step: time.Second - 1, + period: time.Second, + steps: []testStep{ + { + wantTime: time.Unix(12345, 0), + }, + { + wantTime: time.Unix(12345, 999_999_999), + }, + }, + }, + { + name: "single tick advance exact", + start: time.Unix(12345, 0), + period: time.Second, + steps: []testStep{ + { + advance: time.Second, + wantTime: time.Unix(12346, 0), + wantTicks: []time.Time{time.Unix(12346, 0)}, + }, + }, + }, + { + name: "single tick advance extra", + start: time.Unix(12345, 0), + period: time.Second, + steps: []testStep{ + { + advance: time.Second + 1, + wantTime: time.Unix(12346, 1), + wantTicks: []time.Time{time.Unix(12346, 0)}, + }, + }, + }, + { + name: "single tick step exact", + start: time.Unix(12345, 0), + step: time.Second, + period: time.Second, + steps: []testStep{ + { + wantTime: time.Unix(12345, 0), + }, + { + wantTime: time.Unix(12346, 0), + wantTicks: []time.Time{time.Unix(12346, 0)}, + }, + }, + }, + { + name: "single tick step extra", + start: time.Unix(12345, 0), + step: time.Second + 1, + period: time.Second, + steps: []testStep{ + { + wantTime: time.Unix(12345, 0), + }, + { + wantTime: time.Unix(12346, 1), + wantTicks: []time.Time{time.Unix(12346, 0)}, + }, + }, + }, + { + name: "single tick per advance", + start: time.Unix(12345, 0), + period: 3 * time.Second, + steps: []testStep{ + { + wantTime: time.Unix(12345, 0), + }, + { + advance: 4 * time.Second, + wantTime: time.Unix(12349, 0), + wantTicks: []time.Time{time.Unix(12348, 0)}, + }, + { + advance: 2 * time.Second, + wantTime: time.Unix(12351, 0), + wantTicks: []time.Time{time.Unix(12351, 0)}, + }, + { + advance: 2 * time.Second, + wantTime: time.Unix(12353, 0), + }, + { + advance: 2 * time.Second, + wantTime: time.Unix(12355, 0), + wantTicks: []time.Time{time.Unix(12354, 0)}, + }, + }, + }, + { + name: "single tick per step", + start: time.Unix(12345, 0), + step: 2 * time.Second, + period: 3 * time.Second, + steps: []testStep{ + { + wantTime: time.Unix(12345, 0), + }, + { + wantTime: time.Unix(12347, 0), + }, + { + wantTime: time.Unix(12349, 0), + wantTicks: []time.Time{time.Unix(12348, 0)}, + }, + { + wantTime: time.Unix(12351, 0), + wantTicks: []time.Time{time.Unix(12351, 0)}, + }, + { + wantTime: time.Unix(12353, 0), + }, + { + wantTime: time.Unix(12355, 0), + wantTicks: []time.Time{time.Unix(12354, 0)}, + }, + }, + }, + { + name: "multiple tick per advance", + start: time.Unix(12345, 0), + period: time.Second, + channelSize: 3, + steps: []testStep{ + { + wantTime: time.Unix(12345, 0), + }, + { + advance: 2 * time.Second, + wantTime: time.Unix(12347, 0), + wantTicks: []time.Time{ + time.Unix(12346, 0), + time.Unix(12347, 0), + }, + }, + { + advance: 4 * time.Second, + wantTime: time.Unix(12351, 0), + wantTicks: []time.Time{ + time.Unix(12348, 0), + time.Unix(12349, 0), + time.Unix(12350, 0), + // fourth tick dropped due to channel size + }, + }, + }, + }, + { + name: "multiple tick per step", + start: time.Unix(12345, 0), + step: 3 * time.Second, + period: 2 * time.Second, + channelSize: 3, + steps: []testStep{ + { + wantTime: time.Unix(12345, 0), + }, + { + wantTime: time.Unix(12348, 0), + wantTicks: []time.Time{ + time.Unix(12347, 0), + }, + }, + { + wantTime: time.Unix(12351, 0), + wantTicks: []time.Time{ + time.Unix(12349, 0), + time.Unix(12351, 0), + }, + }, + { + wantTime: time.Unix(12354, 0), + wantTicks: []time.Time{ + time.Unix(12353, 0), + }, + }, + { + wantTime: time.Unix(12357, 0), + wantTicks: []time.Time{ + time.Unix(12355, 0), + time.Unix(12357, 0), + }, + }, + }, + }, + { + name: "stop", + start: time.Unix(12345, 0), + step: 2 * time.Second, + period: time.Second, + channelSize: 3, + steps: []testStep{ + { + wantTime: time.Unix(12345, 0), + }, + { + wantTime: time.Unix(12347, 0), + wantTicks: []time.Time{ + time.Unix(12346, 0), + time.Unix(12347, 0), + }, + }, + { + stop: true, + wantTime: time.Unix(12349, 0), + }, + { + wantTime: time.Unix(12351, 0), + }, + { + advance: 10 * time.Second, + wantTime: time.Unix(12361, 0), + }, + }, + }, + { + name: "reset while running", + start: time.Unix(12345, 0), + period: 2 * time.Second, + steps: []testStep{ + { + wantTime: time.Unix(12345, 0), + }, + { + advance: time.Second, + wantTime: time.Unix(12346, 0), + }, + { + advance: time.Second, + wantTime: time.Unix(12347, 0), + wantTicks: []time.Time{ + time.Unix(12347, 0), + }, + }, + { + advance: time.Second, + reset: time.Second, + wantTime: time.Unix(12348, 0), + wantTicks: []time.Time{ + time.Unix(12348, 0), + }, + }, + { + setStep: 5 * time.Second, + reset: 10 * time.Second, + wantTime: time.Unix(12353, 0), + }, + { + wantTime: time.Unix(12358, 0), + wantTicks: []time.Time{ + time.Unix(12358, 0), + }, + }, + }, + }, + { + name: "reset while stopped", + start: time.Unix(12345, 0), + step: time.Second, + period: 2 * time.Second, + steps: []testStep{ + { + wantTime: time.Unix(12345, 0), + }, + { + wantTime: time.Unix(12346, 0), + }, + { + wantTime: time.Unix(12347, 0), + wantTicks: []time.Time{ + time.Unix(12347, 0), + }, + }, + { + stop: true, + wantTime: time.Unix(12348, 0), + }, + { + wantTime: time.Unix(12349, 0), + }, + { + reset: time.Second, + wantTime: time.Unix(12350, 0), + wantTicks: []time.Time{ + time.Unix(12350, 0), + }, + }, + { + wantTime: time.Unix(12351, 0), + wantTicks: []time.Time{ + time.Unix(12351, 0), + }, + }, + }, + }, + { + name: "reset absolute", + start: time.Unix(12345, 0), + step: time.Second, + period: 2 * time.Second, + steps: []testStep{ + { + wantTime: time.Unix(12345, 0), + }, + { + wantTime: time.Unix(12346, 0), + }, + { + wantTime: time.Unix(12347, 0), + wantTicks: []time.Time{ + time.Unix(12347, 0), + }, + }, + { + reset: time.Second, + resetAbsolute: time.Unix(12354, 50), + advance: 7 * time.Second, + wantTime: time.Unix(12354, 0), + }, + { + wantTime: time.Unix(12355, 0), + wantTicks: []time.Time{ + time.Unix(12354, 50), + }, + }, + { + wantTime: time.Unix(12356, 0), + wantTicks: []time.Time{ + time.Unix(12355, 50), + }, + }, + }, + }, + { + name: "follow real time", + realTimeOpts: new(ClockOpts), + start: time.Unix(12345, 0), + period: 2 * time.Second, + channelSize: 3, + steps: []testStep{ + { + wantTime: time.Unix(12345, 0), + }, + { + advanceRealTime: 5 * time.Second, + wantTime: time.Unix(12350, 0), + wantTicks: []time.Time{ + time.Unix(12347, 0), + time.Unix(12349, 0), + }, + }, + { + advance: 5 * time.Second, + wantTime: time.Unix(12355, 0), + wantTicks: []time.Time{ + time.Unix(12351, 0), + time.Unix(12353, 0), + time.Unix(12355, 0), + }, + }, + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var realTimeClockForTestClock tstime.Clock + var realTimeClock *Clock + if tt.realTimeOpts != nil { + realTimeClock = NewClock(*tt.realTimeOpts) + // Passing realTimeClock into newClockInternal results in a + // non-nil interface with a nil pointer, so this is necessary. + realTimeClockForTestClock = realTimeClock + } + + clock := newClockInternal(ClockOpts{ + Start: tt.start, + Step: tt.step, + TimerChannelSize: tt.channelSize, + FollowRealTime: realTimeClock != nil, + }, realTimeClockForTestClock) + tc, tickC := clock.NewTicker(tt.period) + tickControl := tc.(*Ticker) + + t.Cleanup(tickControl.Stop) + + expectNoTicks(t, tickC) + + for i, step := range tt.steps { + if step.stop { + tickControl.Stop() + } + + if !step.resetAbsolute.IsZero() { + tickControl.ResetAbsolute(step.resetAbsolute, step.reset) + } else if step.reset > 0 { + tickControl.Reset(step.reset) + } + + if step.setStep > 0 { + clock.SetStep(step.setStep) + } + + if step.advance > 0 { + clock.Advance(step.advance) + } + if step.advanceRealTime > 0 { + realTimeClock.Advance(step.advanceRealTime) + } + + if now := clock.Now(); !step.wantTime.IsZero() && !now.Equal(step.wantTime) { + t.Errorf("step %v now = %v, want %v", i, now, step.wantTime) + } + + for j, want := range step.wantTicks { + select { + case tick := <-tickC: + if tick.Equal(want) { + continue + } + t.Errorf("step %v tick %v = %v, want %v", i, j, tick, want) + default: + t.Errorf("step %v tick %v missing", i, j) + } + } + + expectNoTicks(t, tickC) + } + }) + } +} + +func TestSingleTimer(t *testing.T) { + t.Parallel() + + type testStep struct { + stop bool + stopReturn bool // The expected return value for Stop() if stop is true. + reset time.Duration + resetAbsolute time.Time + resetReturn bool // The expected return value for Reset() or ResetAbsolute(). + setStep time.Duration + advance time.Duration + advanceRealTime time.Duration + wantTime time.Time + wantTicks []time.Time + } + + tests := []struct { + name string + realTimeOpts *ClockOpts + start time.Time + step time.Duration + delay time.Duration + steps []testStep + }{ + { + name: "no tick advance", + start: time.Unix(12345, 0), + delay: time.Second, + steps: []testStep{ + { + advance: time.Second - 1, + wantTime: time.Unix(12345, 999_999_999), + }, + }, + }, + { + name: "no tick step", + start: time.Unix(12345, 0), + step: time.Second - 1, + delay: time.Second, + steps: []testStep{ + { + wantTime: time.Unix(12345, 0), + }, + { + wantTime: time.Unix(12345, 999_999_999), + }, + }, + }, + { + name: "single tick advance exact", + start: time.Unix(12345, 0), + delay: time.Second, + steps: []testStep{ + { + advance: time.Second, + wantTime: time.Unix(12346, 0), + wantTicks: []time.Time{time.Unix(12346, 0)}, + }, + { + advance: time.Second, + wantTime: time.Unix(12347, 0), + }, + }, + }, + { + name: "single tick advance extra", + start: time.Unix(12345, 0), + delay: time.Second, + steps: []testStep{ + { + advance: time.Second + 1, + wantTime: time.Unix(12346, 1), + wantTicks: []time.Time{time.Unix(12346, 0)}, + }, + { + advance: time.Second, + wantTime: time.Unix(12347, 1), + }, + }, + }, + { + name: "single tick step exact", + start: time.Unix(12345, 0), + step: time.Second, + delay: time.Second, + steps: []testStep{ + { + wantTime: time.Unix(12345, 0), + }, + { + wantTime: time.Unix(12346, 0), + wantTicks: []time.Time{time.Unix(12346, 0)}, + }, + { + wantTime: time.Unix(12347, 0), + }, + }, + }, + { + name: "single tick step extra", + start: time.Unix(12345, 0), + step: time.Second + 1, + delay: time.Second, + steps: []testStep{ + { + wantTime: time.Unix(12345, 0), + }, + { + wantTime: time.Unix(12346, 1), + wantTicks: []time.Time{time.Unix(12346, 0)}, + }, + { + wantTime: time.Unix(12347, 2), + }, + }, + }, + { + name: "reset for single tick per advance", + start: time.Unix(12345, 0), + delay: 3 * time.Second, + steps: []testStep{ + { + wantTime: time.Unix(12345, 0), + }, + { + advance: 4 * time.Second, + wantTime: time.Unix(12349, 0), + wantTicks: []time.Time{time.Unix(12348, 0)}, + }, + { + resetAbsolute: time.Unix(12351, 0), + advance: 2 * time.Second, + wantTime: time.Unix(12351, 0), + wantTicks: []time.Time{time.Unix(12351, 0)}, + }, + { + reset: 3 * time.Second, + advance: 2 * time.Second, + wantTime: time.Unix(12353, 0), + }, + { + advance: 2 * time.Second, + wantTime: time.Unix(12355, 0), + wantTicks: []time.Time{time.Unix(12354, 0)}, + }, + { + advance: 10 * time.Second, + wantTime: time.Unix(12365, 0), + }, + }, + }, + { + name: "reset for single tick per step", + start: time.Unix(12345, 0), + step: 2 * time.Second, + delay: 3 * time.Second, + steps: []testStep{ + { + wantTime: time.Unix(12345, 0), + }, + { + wantTime: time.Unix(12347, 0), + }, + { + wantTime: time.Unix(12349, 0), + wantTicks: []time.Time{time.Unix(12348, 0)}, + }, + { + reset: time.Second, + wantTime: time.Unix(12351, 0), + wantTicks: []time.Time{time.Unix(12350, 0)}, + }, + { + resetAbsolute: time.Unix(12354, 0), + wantTime: time.Unix(12353, 0), + }, + { + wantTime: time.Unix(12355, 0), + wantTicks: []time.Time{time.Unix(12354, 0)}, + }, + }, + }, + { + name: "reset while active", + start: time.Unix(12345, 0), + step: 2 * time.Second, + delay: 3 * time.Second, + steps: []testStep{ + { + wantTime: time.Unix(12345, 0), + }, + { + wantTime: time.Unix(12347, 0), + }, + { + reset: 3 * time.Second, + resetReturn: true, + wantTime: time.Unix(12349, 0), + }, + { + resetAbsolute: time.Unix(12354, 0), + resetReturn: true, + wantTime: time.Unix(12351, 0), + }, + { + wantTime: time.Unix(12353, 0), + }, + { + wantTime: time.Unix(12355, 0), + wantTicks: []time.Time{time.Unix(12354, 0)}, + }, + }, + }, + { + name: "stop after fire", + start: time.Unix(12345, 0), + step: 2 * time.Second, + delay: time.Second, + steps: []testStep{ + { + wantTime: time.Unix(12345, 0), + }, + { + wantTime: time.Unix(12347, 0), + wantTicks: []time.Time{time.Unix(12346, 0)}, + }, + { + stop: true, + wantTime: time.Unix(12349, 0), + }, + { + wantTime: time.Unix(12351, 0), + }, + { + advance: 10 * time.Second, + wantTime: time.Unix(12361, 0), + }, + }, + }, + { + name: "stop before fire", + start: time.Unix(12345, 0), + step: 2 * time.Second, + delay: time.Second, + steps: []testStep{ + { + stop: true, + stopReturn: true, + wantTime: time.Unix(12345, 0), + }, + { + wantTime: time.Unix(12347, 0), + }, + { + wantTime: time.Unix(12349, 0), + }, + { + wantTime: time.Unix(12351, 0), + }, + { + advance: 10 * time.Second, + wantTime: time.Unix(12361, 0), + }, + }, + }, + { + name: "stop after reset", + start: time.Unix(12345, 0), + step: 2 * time.Second, + delay: time.Second, + steps: []testStep{ + { + wantTime: time.Unix(12345, 0), + }, + { + wantTime: time.Unix(12347, 0), + wantTicks: []time.Time{time.Unix(12346, 0)}, + }, + { + reset: 10 * time.Second, + wantTime: time.Unix(12349, 0), + }, + { + stop: true, + stopReturn: true, + wantTime: time.Unix(12351, 0), + }, + { + advance: 10 * time.Second, + wantTime: time.Unix(12361, 0), + }, + }, + }, + { + name: "reset while running", + start: time.Unix(12345, 0), + delay: 2 * time.Second, + steps: []testStep{ + { + wantTime: time.Unix(12345, 0), + }, + { + advance: time.Second, + wantTime: time.Unix(12346, 0), + }, + { + advance: time.Second, + wantTime: time.Unix(12347, 0), + wantTicks: []time.Time{ + time.Unix(12347, 0), + }, + }, + { + advance: time.Second, + reset: time.Second, + wantTime: time.Unix(12348, 0), + wantTicks: []time.Time{ + time.Unix(12348, 0), + }, + }, + { + setStep: 5 * time.Second, + reset: 10 * time.Second, + wantTime: time.Unix(12353, 0), + }, + { + wantTime: time.Unix(12358, 0), + wantTicks: []time.Time{ + time.Unix(12358, 0), + }, + }, + }, + }, + { + name: "reset while stopped", + start: time.Unix(12345, 0), + step: time.Second, + delay: 2 * time.Second, + steps: []testStep{ + { + wantTime: time.Unix(12345, 0), + }, + { + wantTime: time.Unix(12346, 0), + }, + { + stop: true, + stopReturn: true, + wantTime: time.Unix(12347, 0), + }, + { + wantTime: time.Unix(12348, 0), + }, + { + wantTime: time.Unix(12349, 0), + }, + { + reset: time.Second, + wantTime: time.Unix(12350, 0), + wantTicks: []time.Time{ + time.Unix(12350, 0), + }, + }, + { + wantTime: time.Unix(12351, 0), + }, + }, + }, + { + name: "reset absolute", + start: time.Unix(12345, 0), + step: time.Second, + delay: 2 * time.Second, + steps: []testStep{ + { + wantTime: time.Unix(12345, 0), + }, + { + wantTime: time.Unix(12346, 0), + }, + { + wantTime: time.Unix(12347, 0), + wantTicks: []time.Time{ + time.Unix(12347, 0), + }, + }, + { + resetAbsolute: time.Unix(12354, 50), + advance: 7 * time.Second, + wantTime: time.Unix(12354, 0), + }, + { + wantTime: time.Unix(12355, 0), + wantTicks: []time.Time{ + time.Unix(12354, 50), + }, + }, + { + wantTime: time.Unix(12356, 0), + }, + }, + }, + { + name: "follow real time", + realTimeOpts: new(ClockOpts), + start: time.Unix(12345, 0), + delay: 2 * time.Second, + steps: []testStep{ + { + wantTime: time.Unix(12345, 0), + }, + { + advanceRealTime: 5 * time.Second, + wantTime: time.Unix(12350, 0), + wantTicks: []time.Time{ + time.Unix(12347, 0), + }, + }, + { + reset: 2 * time.Second, + advance: 5 * time.Second, + wantTime: time.Unix(12355, 0), + wantTicks: []time.Time{ + time.Unix(12352, 0), + }, + }, + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var realTimeClockForTestClock tstime.Clock + var realTimeClock *Clock + if tt.realTimeOpts != nil { + realTimeClock = NewClock(*tt.realTimeOpts) + // Passing realTimeClock into newClockInternal results in a + // non-nil interface with a nil pointer, so this is necessary. + realTimeClockForTestClock = realTimeClock + } + + clock := newClockInternal(ClockOpts{ + Start: tt.start, + Step: tt.step, + FollowRealTime: realTimeClock != nil, + }, realTimeClockForTestClock) + tc, tickC := clock.NewTimer(tt.delay) + timerControl := tc.(*Timer) + + t.Cleanup(func() { timerControl.Stop() }) + + expectNoTicks(t, tickC) + + for i, step := range tt.steps { + if step.stop { + if got := timerControl.Stop(); got != step.stopReturn { + t.Errorf("step %v Stop returned %v, want %v", i, got, step.stopReturn) + } + } + + if !step.resetAbsolute.IsZero() { + if got := timerControl.ResetAbsolute(step.resetAbsolute); got != step.resetReturn { + t.Errorf("step %v Reset returned %v, want %v", i, got, step.resetReturn) + } + } + + if step.reset > 0 { + if got := timerControl.Reset(step.reset); got != step.resetReturn { + t.Errorf("step %v Reset returned %v, want %v", i, got, step.resetReturn) + } + } + + if step.setStep > 0 { + clock.SetStep(step.setStep) + } + + if step.advance > 0 { + clock.Advance(step.advance) + } + if step.advanceRealTime > 0 { + realTimeClock.Advance(step.advanceRealTime) + } + + if now := clock.Now(); !step.wantTime.IsZero() && !now.Equal(step.wantTime) { + t.Errorf("step %v now = %v, want %v", i, now, step.wantTime) + } + + for j, want := range step.wantTicks { + select { + case tick := <-tickC: + if tick.Equal(want) { + continue + } + t.Errorf("step %v tick %v = %v, want %v", i, j, tick, want) + default: + t.Errorf("step %v tick %v missing", i, j) + } + } + + expectNoTicks(t, tickC) + } + }) + } +} + +type testEvent struct { + fireTimes []time.Time + scheduleTimes []time.Time +} + +func (te *testEvent) Fire(t time.Time) time.Time { + var ret time.Time + + te.fireTimes = append(te.fireTimes, t) + if len(te.scheduleTimes) > 0 { + ret = te.scheduleTimes[0] + te.scheduleTimes = te.scheduleTimes[1:] + } + return ret +} + +func TestEventManager(t *testing.T) { + t.Parallel() + + var em eventManager + + testEvents := []testEvent{ + { + scheduleTimes: []time.Time{ + time.Unix(12300, 0), // step 1 + time.Unix(12340, 0), // step 1 + time.Unix(12345, 0), // step 1 + time.Unix(12346, 0), // step 1 + time.Unix(12347, 0), // step 3 + time.Unix(12348, 0), // step 4 + time.Unix(12349, 0), // step 4 + }, + }, + { + scheduleTimes: []time.Time{ + time.Unix(12350, 0), // step 4 + time.Unix(12360, 0), // step 5 + time.Unix(12370, 0), // rescheduled + time.Unix(12380, 0), // step 6 + time.Unix(12381, 0), // step 6 + time.Unix(12382, 0), // step 6 + time.Unix(12393, 0), // stopped + }, + }, + { + scheduleTimes: []time.Time{ + time.Unix(12350, 1), // step 4 + time.Unix(12360, 1), // rescheduled + time.Unix(12370, 1), // step 6 + time.Unix(12380, 1), // step 6 + time.Unix(12381, 1), // step 6 + time.Unix(12382, 1), // step 6 + time.Unix(12383, 1), // step 6 + }, + }, + { + scheduleTimes: []time.Time{ + time.Unix(12355, 0), // step 5 + time.Unix(12365, 0), // step 5 + time.Unix(12370, 0), // step 6 + time.Unix(12390, 0), // step 6 + time.Unix(12391, 0), // step 7 + time.Unix(12392, 0), // step 7 + time.Unix(12393, 0), // step 7 + }, + }, + { + scheduleTimes: []time.Time{ + time.Unix(100000, 0), // step 7 + }, + }, + { + scheduleTimes: []time.Time{ + time.Unix(12346, 0), // step 1 + }, + }, + { + scheduleTimes: []time.Time{ + time.Unix(12305, 0), // step 5 + }, + }, + { + scheduleTimes: []time.Time{ + time.Unix(12372, 0), // step 6 + time.Unix(12374, 0), // step 6 + time.Unix(12376, 0), // step 6 + time.Unix(12386, 0), // step 6 + time.Unix(12396, 0), // step 7 + }, + }, + } + + steps := []struct { + reschedule []int + stop []int + advanceTo time.Time + want map[int][]time.Time + waitingEvents int + }{ + { + advanceTo: time.Unix(12345, 0), + }, + { + reschedule: []int{0, 1, 2, 3, 4, 5}, // add 0, 1, 2, 3, 4, 5 + advanceTo: time.Unix(12346, 0), + want: map[int][]time.Time{ + 0: { + time.Unix(12300, 0), + time.Unix(12340, 0), + time.Unix(12345, 0), + time.Unix(12346, 0), + }, + 5: { + time.Unix(12346, 0), + }, + }, + waitingEvents: 5, // scheduled 0, 1, 2, 3, 4, 5; retired 5 + }, + { + advanceTo: time.Unix(12346, 50), + waitingEvents: 5, // no change + }, + { + advanceTo: time.Unix(12347, 50), + want: map[int][]time.Time{ + 0: { + time.Unix(12347, 0), + }, + }, + waitingEvents: 5, // no change + }, + { + advanceTo: time.Unix(12350, 50), + want: map[int][]time.Time{ + 0: { + time.Unix(12348, 0), + time.Unix(12349, 0), + }, + 1: { + time.Unix(12350, 0), + }, + 2: { + time.Unix(12350, 1), + }, + }, + waitingEvents: 4, // retired 0 + }, + { + reschedule: []int{6, 7}, // add 6, 7 + stop: []int{2}, + advanceTo: time.Unix(12365, 0), + want: map[int][]time.Time{ + 1: { + time.Unix(12360, 0), + }, + 3: { + time.Unix(12355, 0), + time.Unix(12365, 0), + }, + 6: { + time.Unix(12305, 0), + }, + }, + waitingEvents: 4, // scheduled 6, 7; retired 2, 5 + }, + { + reschedule: []int{1, 2}, // update 1; add 2 + stop: []int{6}, + advanceTo: time.Unix(12390, 0), + want: map[int][]time.Time{ + 1: { + time.Unix(12380, 0), + time.Unix(12381, 0), + time.Unix(12382, 0), + }, + 2: { + time.Unix(12370, 1), + time.Unix(12380, 1), + time.Unix(12381, 1), + time.Unix(12382, 1), + time.Unix(12383, 1), + }, + 3: { + time.Unix(12370, 0), + time.Unix(12390, 0), + }, + 7: { + time.Unix(12372, 0), + time.Unix(12374, 0), + time.Unix(12376, 0), + time.Unix(12386, 0), + }, + }, + waitingEvents: 3, // scheduled 2, retired 2, stopped 6 + }, + { + stop: []int{1}, // no-op: already stopped + advanceTo: time.Unix(200000, 0), + want: map[int][]time.Time{ + 3: { + time.Unix(12391, 0), + time.Unix(12392, 0), + time.Unix(12393, 0), + }, + 4: { + time.Unix(100000, 0), + }, + 7: { + time.Unix(12396, 0), + }, + }, + waitingEvents: 0, // retired 3, 4, 7 + }, + { + advanceTo: time.Unix(300000, 0), + }, + } + + for i, step := range steps { + for _, idx := range step.reschedule { + ev := &testEvents[idx] + t := ev.scheduleTimes[0] + ev.scheduleTimes = ev.scheduleTimes[1:] + em.Reschedule(ev, t) + } + for _, idx := range step.stop { + ev := &testEvents[idx] + em.Reschedule(ev, time.Time{}) + } + em.AdvanceTo(step.advanceTo) + for j := range testEvents { + if !slices.Equal(testEvents[j].fireTimes, step.want[j]) { + t.Errorf("step %v event %v fire times = %v, want %v", i, j, testEvents[j].fireTimes, step.want[j]) + } + testEvents[j].fireTimes = nil + } + } +} + +func TestClockFollowRealTime(t *testing.T) { + t.Parallel() + + type advanceInfo struct { + when int + advanceTestClock time.Duration + advanceTestClockTo time.Time + advanceRealTimeClock time.Duration + } + + tests := []struct { + name string + start time.Time + wantStart time.Time // This may differ from start when start.IsZero(). + realTimeClockOpts ClockOpts + advances []advanceInfo + wants []time.Time // The return values of sequential calls to Now(). + }{ + { + name: "increment ms then advance 1s", + start: time.Unix(12345, 1000), + wantStart: time.Unix(12345, 1000), + advances: []advanceInfo{ + { + when: 1, + advanceRealTimeClock: 1000, + }, + { + when: 2, + advanceRealTimeClock: 1000, + }, + { + when: 3, + advanceRealTimeClock: 1000, + }, + { + when: 4, + advanceTestClock: time.Second, + }, + { + when: 5, + advanceRealTimeClock: 1000, + }, + { + when: 6, + advanceRealTimeClock: 1000, + }, + { + when: 7, + advanceRealTimeClock: 1000, + }, + }, + wants: []time.Time{ + time.Unix(12345, 1000), + time.Unix(12345, 2000), + time.Unix(12345, 3000), + time.Unix(12345, 4000), + time.Unix(12346, 4000), + time.Unix(12346, 5000), + time.Unix(12346, 6000), + time.Unix(12346, 7000), + }, + }, + { + name: "multiple advances over time", + start: time.Unix(12345, 1000), + wantStart: time.Unix(12345, 1000), + advances: []advanceInfo{ + { + when: 1, + advanceRealTimeClock: 1, + }, + { + when: 2, + advanceTestClock: time.Second, + }, + { + when: 3, + advanceRealTimeClock: 1, + }, + { + when: 4, + advanceTestClock: 0, + }, + { + when: 5, + advanceRealTimeClock: 1, + }, + { + when: 6, + advanceTestClock: 1000, + }, + { + when: 7, + advanceRealTimeClock: 1, + }, + }, + wants: []time.Time{ + time.Unix(12345, 1000), + time.Unix(12345, 1001), + time.Unix(12346, 1001), + time.Unix(12346, 1002), + time.Unix(12346, 1002), + time.Unix(12346, 1003), + time.Unix(12346, 2003), + time.Unix(12346, 2004), + }, + }, + { + name: "multiple advances at once", + start: time.Unix(12345, 1000), + wantStart: time.Unix(12345, 1000), + advances: []advanceInfo{ + { + when: 1, + advanceRealTimeClock: 1, + }, + { + when: 2, + advanceTestClock: time.Second, + }, + { + when: 2, + advanceTestClock: 0, + }, + { + when: 2, + advanceTestClock: 1000, + }, + { + when: 3, + advanceRealTimeClock: 1, + }, + }, + wants: []time.Time{ + time.Unix(12345, 1000), + time.Unix(12345, 1001), + time.Unix(12346, 2001), + time.Unix(12346, 2002), + }, + }, + { + name: "changes at start", + start: time.Unix(12345, 1000), + wantStart: time.Unix(12345, 1000), + advances: []advanceInfo{ + { + when: 0, + advanceTestClock: time.Second, + }, + { + when: 0, + advanceTestClock: 1000, + }, + { + when: 1, + advanceRealTimeClock: 5, + }, + { + when: 2, + advanceRealTimeClock: 5, + }, + { + when: 3, + advanceRealTimeClock: 5, + }, + }, + wants: []time.Time{ + time.Unix(12346, 2000), + time.Unix(12346, 2005), + time.Unix(12346, 2010), + time.Unix(12346, 2015), + }, + }, + { + name: "start from current time", + realTimeClockOpts: ClockOpts{ + Start: time.Unix(12345, 0), + }, + wantStart: time.Unix(12345, 0), + advances: []advanceInfo{ + { + when: 1, + advanceTestClock: time.Second, + }, + { + when: 2, + advanceRealTimeClock: 10 * time.Second, + }, + { + when: 3, + advanceTestClock: time.Minute, + }, + { + when: 4, + advanceRealTimeClock: time.Hour, + }, + { + when: 5, + advanceTestClockTo: time.Unix(100, 0), + }, + { + when: 6, + advanceRealTimeClock: time.Hour, + }, + }, + wants: []time.Time{ + time.Unix(12345, 0), + time.Unix(12346, 0), + time.Unix(12356, 0), + time.Unix(12416, 0), + time.Unix(16016, 0), + time.Unix(100, 0), + time.Unix(3700, 0), + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + realTimeClock := NewClock(tt.realTimeClockOpts) + clock := newClockInternal(ClockOpts{ + Start: tt.start, + FollowRealTime: true, + }, realTimeClock) + changeIndex := 0 + + for i := range tt.wants { + for len(tt.advances) > changeIndex && tt.advances[changeIndex].when == i { + advance := tt.advances[changeIndex] + if advance.advanceTestClockTo.IsZero() { + clock.Advance(advance.advanceTestClock) + } else { + clock.AdvanceTo(advance.advanceTestClockTo) + } + realTimeClock.Advance(advance.advanceRealTimeClock) + changeIndex++ + } + + if start := clock.GetStart(); !start.Equal(tt.wantStart) { + t.Errorf("clock has start %v, want %v", start, tt.wantStart) + } + + if got := clock.Now(); !got.Equal(tt.wants[i]) { + t.Errorf("step %v: clock.Now() = %v, want %v", i, got, tt.wants[i]) + } + if got := clock.PeekNow(); !got.Equal(tt.wants[i]) { + t.Errorf("step %v: clock.PeekNow() = %v, want %v", i, got, tt.wants[i]) + } + } + }) + } +} + +func TestAfterFunc(t *testing.T) { + t.Parallel() + + type testStep struct { + stop bool + stopReturn bool // The expected return value for Stop() if stop is true. + reset time.Duration + resetAbsolute time.Time + resetReturn bool // The expected return value for Reset() or ResetAbsolute(). + setStep time.Duration + advance time.Duration + advanceRealTime time.Duration + wantTime time.Time + wantTick bool + } + + tests := []struct { + name string + realTimeOpts *ClockOpts + start time.Time + step time.Duration + delay time.Duration + steps []testStep + }{ + { + name: "no tick advance", + start: time.Unix(12345, 0), + delay: time.Second, + steps: []testStep{ + { + advance: time.Second - 1, + wantTime: time.Unix(12345, 999_999_999), + }, + }, + }, + { + name: "no tick step", + start: time.Unix(12345, 0), + step: time.Second - 1, + delay: time.Second, + steps: []testStep{ + { + wantTime: time.Unix(12345, 0), + }, + { + wantTime: time.Unix(12345, 999_999_999), + }, + }, + }, + { + name: "single tick advance exact", + start: time.Unix(12345, 0), + delay: time.Second, + steps: []testStep{ + { + advance: time.Second, + wantTime: time.Unix(12346, 0), + wantTick: true, + }, + { + advance: time.Second, + wantTime: time.Unix(12347, 0), + }, + }, + }, + { + name: "single tick advance extra", + start: time.Unix(12345, 0), + delay: time.Second, + steps: []testStep{ + { + advance: time.Second + 1, + wantTime: time.Unix(12346, 1), + wantTick: true, + }, + { + advance: time.Second, + wantTime: time.Unix(12347, 1), + }, + }, + }, + { + name: "single tick step exact", + start: time.Unix(12345, 0), + step: time.Second, + delay: time.Second, + steps: []testStep{ + { + wantTime: time.Unix(12345, 0), + }, + { + wantTime: time.Unix(12346, 0), + wantTick: true, + }, + { + wantTime: time.Unix(12347, 0), + }, + }, + }, + { + name: "single tick step extra", + start: time.Unix(12345, 0), + step: time.Second + 1, + delay: time.Second, + steps: []testStep{ + { + wantTime: time.Unix(12345, 0), + }, + { + wantTime: time.Unix(12346, 1), + wantTick: true, + }, + { + wantTime: time.Unix(12347, 2), + }, + }, + }, + { + name: "reset for single tick per advance", + start: time.Unix(12345, 0), + delay: 3 * time.Second, + steps: []testStep{ + { + wantTime: time.Unix(12345, 0), + }, + { + advance: 4 * time.Second, + wantTime: time.Unix(12349, 0), + wantTick: true, + }, + { + resetAbsolute: time.Unix(12351, 0), + advance: 2 * time.Second, + wantTime: time.Unix(12351, 0), + wantTick: true, + }, + { + reset: 3 * time.Second, + advance: 2 * time.Second, + wantTime: time.Unix(12353, 0), + }, + { + advance: 2 * time.Second, + wantTime: time.Unix(12355, 0), + wantTick: true, + }, + { + advance: 10 * time.Second, + wantTime: time.Unix(12365, 0), + }, + }, + }, + { + name: "reset for single tick per step", + start: time.Unix(12345, 0), + step: 2 * time.Second, + delay: 3 * time.Second, + steps: []testStep{ + { + wantTime: time.Unix(12345, 0), + }, + { + wantTime: time.Unix(12347, 0), + }, + { + wantTime: time.Unix(12349, 0), + wantTick: true, + }, + { + reset: time.Second, + wantTime: time.Unix(12351, 0), + wantTick: true, + }, + { + resetAbsolute: time.Unix(12354, 0), + wantTime: time.Unix(12353, 0), + }, + { + wantTime: time.Unix(12355, 0), + wantTick: true, + }, + }, + }, + { + name: "reset while active", + start: time.Unix(12345, 0), + step: 2 * time.Second, + delay: 3 * time.Second, + steps: []testStep{ + { + wantTime: time.Unix(12345, 0), + }, + { + wantTime: time.Unix(12347, 0), + }, + { + reset: 3 * time.Second, + resetReturn: true, + wantTime: time.Unix(12349, 0), + }, + { + resetAbsolute: time.Unix(12354, 0), + resetReturn: true, + wantTime: time.Unix(12351, 0), + }, + { + wantTime: time.Unix(12353, 0), + }, + { + wantTime: time.Unix(12355, 0), + wantTick: true, + }, + }, + }, + { + name: "stop after fire", + start: time.Unix(12345, 0), + step: 2 * time.Second, + delay: time.Second, + steps: []testStep{ + { + wantTime: time.Unix(12345, 0), + }, + { + wantTime: time.Unix(12347, 0), + wantTick: true, + }, + { + stop: true, + wantTime: time.Unix(12349, 0), + }, + { + wantTime: time.Unix(12351, 0), + }, + { + advance: 10 * time.Second, + wantTime: time.Unix(12361, 0), + }, + }, + }, + { + name: "stop before fire", + start: time.Unix(12345, 0), + step: 2 * time.Second, + delay: time.Second, + steps: []testStep{ + { + stop: true, + stopReturn: true, + wantTime: time.Unix(12345, 0), + }, + { + wantTime: time.Unix(12347, 0), + }, + { + wantTime: time.Unix(12349, 0), + }, + { + wantTime: time.Unix(12351, 0), + }, + { + advance: 10 * time.Second, + wantTime: time.Unix(12361, 0), + }, + }, + }, + { + name: "stop after reset", + start: time.Unix(12345, 0), + step: 2 * time.Second, + delay: time.Second, + steps: []testStep{ + { + wantTime: time.Unix(12345, 0), + }, + { + wantTime: time.Unix(12347, 0), + wantTick: true, + }, + { + reset: 10 * time.Second, + wantTime: time.Unix(12349, 0), + }, + { + stop: true, + stopReturn: true, + wantTime: time.Unix(12351, 0), + }, + { + advance: 10 * time.Second, + wantTime: time.Unix(12361, 0), + }, + }, + }, + { + name: "reset while running", + start: time.Unix(12345, 0), + delay: 2 * time.Second, + steps: []testStep{ + { + wantTime: time.Unix(12345, 0), + }, + { + advance: time.Second, + wantTime: time.Unix(12346, 0), + }, + { + advance: time.Second, + wantTime: time.Unix(12347, 0), + wantTick: true, + }, + { + advance: time.Second, + reset: time.Second, + wantTime: time.Unix(12348, 0), + wantTick: true, + }, + { + setStep: 5 * time.Second, + reset: 10 * time.Second, + wantTime: time.Unix(12353, 0), + }, + { + wantTime: time.Unix(12358, 0), + wantTick: true, + }, + }, + }, + { + name: "reset while stopped", + start: time.Unix(12345, 0), + step: time.Second, + delay: 2 * time.Second, + steps: []testStep{ + { + wantTime: time.Unix(12345, 0), + }, + { + wantTime: time.Unix(12346, 0), + }, + { + stop: true, + stopReturn: true, + wantTime: time.Unix(12347, 0), + }, + { + wantTime: time.Unix(12348, 0), + }, + { + wantTime: time.Unix(12349, 0), + }, + { + reset: time.Second, + wantTime: time.Unix(12350, 0), + wantTick: true, + }, + { + wantTime: time.Unix(12351, 0), + }, + }, + }, + { + name: "reset absolute", + start: time.Unix(12345, 0), + step: time.Second, + delay: 2 * time.Second, + steps: []testStep{ + { + wantTime: time.Unix(12345, 0), + }, + { + wantTime: time.Unix(12346, 0), + }, + { + wantTime: time.Unix(12347, 0), + wantTick: true, + }, + { + resetAbsolute: time.Unix(12354, 50), + advance: 7 * time.Second, + wantTime: time.Unix(12354, 0), + }, + { + wantTime: time.Unix(12355, 0), + wantTick: true, + }, + { + wantTime: time.Unix(12356, 0), + }, + }, + }, + { + name: "follow real time", + realTimeOpts: new(ClockOpts), + start: time.Unix(12345, 0), + delay: 2 * time.Second, + steps: []testStep{ + { + wantTime: time.Unix(12345, 0), + }, + { + advanceRealTime: 5 * time.Second, + wantTime: time.Unix(12350, 0), + wantTick: true, + }, + { + reset: 2 * time.Second, + advance: 5 * time.Second, + wantTime: time.Unix(12355, 0), + wantTick: true, + }, + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + var realTimeClockForTestClock tstime.Clock + var realTimeClock *Clock + if tt.realTimeOpts != nil { + realTimeClock = NewClock(*tt.realTimeOpts) + // Passing realTimeClock into newClockInternal results in a + // non-nil interface with a nil pointer, so this is necessary. + realTimeClockForTestClock = realTimeClock + } + + var gotTick atomic.Bool + + clock := newClockInternal(ClockOpts{ + Start: tt.start, + Step: tt.step, + FollowRealTime: realTimeClock != nil, + }, realTimeClockForTestClock) + tc := clock.AfterFunc(tt.delay, func() { + if gotTick.Swap(true) == true { + t.Error("multiple ticks detected") + } + }) + timerControl := tc.(*Timer) + + t.Cleanup(func() { timerControl.Stop() }) + + if gotTick.Load() { + t.Error("initial tick detected, want none") + } + + for i, step := range tt.steps { + if step.stop { + if got := timerControl.Stop(); got != step.stopReturn { + t.Errorf("step %v Stop returned %v, want %v", i, got, step.stopReturn) + } + } + + if !step.resetAbsolute.IsZero() { + if got := timerControl.ResetAbsolute(step.resetAbsolute); got != step.resetReturn { + t.Errorf("step %v Reset returned %v, want %v", i, got, step.resetReturn) + } + } + + if step.reset > 0 { + if got := timerControl.Reset(step.reset); got != step.resetReturn { + t.Errorf("step %v Reset returned %v, want %v", i, got, step.resetReturn) + } + } + + if step.setStep > 0 { + clock.SetStep(step.setStep) + } + + if step.advance > 0 { + clock.Advance(step.advance) + } + if step.advanceRealTime > 0 { + realTimeClock.Advance(step.advanceRealTime) + } + + if now := clock.Now(); !step.wantTime.IsZero() && !now.Equal(step.wantTime) { + t.Errorf("step %v now = %v, want %v", i, now, step.wantTime) + } + + if got := gotTick.Swap(false); got != step.wantTick { + t.Errorf("step %v tick %v, want %v", i, got, step.wantTick) + } + } + }) + } +} diff --git a/tstime/tstime.go b/tstime/tstime.go index cc76755cc..82a6f53da 100644 --- a/tstime/tstime.go +++ b/tstime/tstime.go @@ -59,3 +59,79 @@ func Sleep(ctx context.Context, d time.Duration) bool { return true } } + +// Clock offers a subset of the functionality from the std/time package. +// Normally, applications will use the StdClock implementation that calls the +// appropriate std/time exported funcs. The advantage of using Clock is that +// tests can substitute a different implementation, allowing the test to control +// time precisely, something required for certain types of tests to be possible +// at all, speeds up execution by not needing to sleep, and can dramatically +// reduce the risk of flakes due to tests executing too slowly or quickly. +type Clock interface { + // Now returns the current time, as in time.Now. + Now() time.Time + // NewTimer returns a timer whose notion of the current time is controlled + // by this Clock. It follows the semantics of time.NewTimer as closely as + // possible but is adapted to return an interface, so the channel needs to + // be returned as well. + NewTimer(d time.Duration) (TimerController, <-chan time.Time) + // NewTicker returns a ticker whose notion of the current time is controlled + // by this Clock. It follows the semantics of time.NewTicker as closely as + // possible but is adapted to return an interface, so the channel needs to + // be returned as well. + NewTicker(d time.Duration) (TickerController, <-chan time.Time) + // AfterFunc returns a ticker whose notion of the current time is controlled + // by this Clock. When the ticker expires, it will call the provided func. + // It follows the semantics of time.AfterFunc. + AfterFunc(d time.Duration, f func()) TimerController +} + +// TickerController offers the receivers of a time.Ticker to ensure +// compatibility with standard timers, but allows for the option of substituting +// a standard timer with something else for testing purposes. +type TickerController interface { + // Reset follows the same semantics as with time.Ticker.Reset. + Reset(d time.Duration) + // Stop follows the same semantics as with time.Ticker.Stop. + Stop() +} + +// TimerController offers the receivers of a time.Timer to ensure +// compatibility with standard timers, but allows for the option of substituting +// a standard timer with something else for testing purposes. +type TimerController interface { + // Reset follows the same semantics as with time.Timer.Reset. + Reset(d time.Duration) bool + // Stop follows the same semantics as with time.Timer.Stop. + Stop() bool +} + +// StdClock is a simple implementation of Clock using the relevant funcs in the +// std/time package. +type StdClock struct{} + +// Now calls time.Now. +func (StdClock) Now() time.Time { + return time.Now() +} + +// NewTimer calls time.NewTimer. As an interface does not allow for struct +// members and other packages cannot add receivers to another package, the +// channel is also returned because it would be otherwise inaccessible. +func (StdClock) NewTimer(d time.Duration) (TimerController, <-chan time.Time) { + t := time.NewTimer(d) + return t, t.C +} + +// NewTicker calls time.NewTicker. As an interface does not allow for struct +// members and other packages cannot add receivers to another package, the +// channel is also returned because it would be otherwise inaccessible. +func (StdClock) NewTicker(d time.Duration) (TickerController, <-chan time.Time) { + t := time.NewTicker(d) + return t, t.C +} + +// AfterFunc calls time.AfterFunc. +func (StdClock) AfterFunc(d time.Duration, f func()) TimerController { + return time.AfterFunc(d, f) +}