Files
tailscale/tstest/clock_test.go
Brad Fitzpatrick c13a1f9c55 tstest: coalesce dropped Ticker ticks when advancing the clock far ahead
Clock.Advance previously fired each Ticker once per period between the
current time and the advance target, even though all but the first few
sends were dropped on the Ticker's full channel. A test that advances
the clock 180 days with a 5 second ticker registered did 3.1 million
fires, each taking a mutex, attempting a channel send, and fixing the
event heap. Under the race detector, where synchronization operations
are 20-30x more expensive, this dominated test runtime.

Now, when a tick is dropped because the channel is full, skip the
remaining missed ticks in one step, staying aligned to the original
schedule and still firing the final tick at or before the advance
target. This matches time.Ticker, which drops ticks it cannot deliver,
and is observably identical behavior. Ticks that fit in the channel are
still all delivered.

In the internal corp repo, this takes controlclient.TestExpiry under
race from 6.0s to 2.2s (its no-race time is 0.5s), and the new
benchmark improves from 449µs to 90ns per 24h advance:

  BenchmarkTickerLargeAdvance-16  100       449313 ns/op    (before)
  BenchmarkTickerLargeAdvance-16  13311910      90.22 ns/op (after)

Updates tailscale/corp#47035

Change-Id: I4826df212415eb2ea7ceb5bc46848e816f8ccde1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2026-08-24 08:32:12 -07:00

2516 lines
52 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package tstest
import (
"slices"
"sync/atomic"
"testing"
"time"
"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 {
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 {
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 := range 10 {
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 {
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 {
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
},
},
},
},
{
// A large advance drops ticks that don't fit in the channel,
// but later ticks must stay aligned to the original schedule.
name: "dropped-ticks-remain-aligned",
start: time.Unix(12345, 0),
period: time.Second,
channelSize: 1,
steps: []testStep{
{
advance: 5 * time.Second,
wantTime: time.Unix(12350, 0),
wantTicks: []time.Time{
time.Unix(12346, 0),
// Remaining ticks dropped due to channel size.
},
},
{
advance: 2 * time.Second,
wantTime: time.Unix(12352, 0),
wantTicks: []time.Time{
time.Unix(12351, 0),
// Second 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 {
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 {
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 {
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 {
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)
}
}
})
}
}
func TestSince(t *testing.T) {
t.Parallel()
tests := []struct {
name string
start time.Time
since time.Time
want time.Duration
}{
{
name: "positive",
start: time.Unix(12345, 1000),
since: time.Unix(11111, 1000),
want: 1234 * time.Second,
},
{
name: "negative",
start: time.Unix(12345, 1000),
since: time.Unix(15436, 1000),
want: -3091 * time.Second,
},
{
name: "zero",
start: time.Unix(12345, 1000),
since: time.Unix(12345, 1000),
want: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
clock := NewClock(ClockOpts{
Start: tt.start,
})
got := clock.Since(tt.since)
if got != tt.want {
t.Errorf("Since duration %v, want %v", got, tt.want)
}
})
}
}
// BenchmarkTickerLargeAdvance measures advancing the clock far past a
// short-period ticker's next trigger, as happens when a test using an
// injected clock jumps days into the future while background loops hold
// tickers with periods of a few seconds. Most of the intermediate ticks
// are dropped because the channel is full, so they should be cheap.
func BenchmarkTickerLargeAdvance(b *testing.B) {
clock := NewClock(ClockOpts{Start: time.Unix(12345, 0)})
tick, _ := clock.NewTicker(5 * time.Second)
defer tick.Stop()
b.ReportAllocs()
for b.Loop() {
clock.Advance(24 * time.Hour)
}
}