diff --git a/README.md b/README.md index 1f9dc0f..5d3d44b 100644 --- a/README.md +++ b/README.md @@ -65,8 +65,10 @@ mClock.Set(time.Date(2021, 6, 18, 12, 0, 0, 0, time.UTC)) // June 18, 2021 @ 12p #### Advancing the clock -Once you begin setting timers or tickers, you cannot change the time backward, only advance it -forward. You may continue to use `Set()`, but it is often easier and clearer to use `Advance()`. +While a timer, ticker, or [mock context](#contexts) is scheduled, you cannot change the time +backward, only advance it forward. A timer is scheduled until it fires or is stopped, a ticker until +it is stopped, and a mock context until it expires or is canceled. You may continue to use `Set()`, +but it is often easier and clearer to use `Advance()`. For example, with a timer: @@ -80,8 +82,8 @@ mClock.Advance(time.Second) ``` When you call `Advance()` it immediately moves the clock forward the given amount, and triggers any -tickers or timers that are scheduled to happen at that time. Any triggered events happen on separate -goroutines, so _do not_ immediately assert the results: +tickers, timers, or mock contexts that are scheduled to happen at that time. Any triggered events +happen on separate goroutines, so _do not_ immediately assert the results: ```go fired := false @@ -146,7 +148,8 @@ mClock.Advance(time.Second).MustWait(ctx) ### Advance only to the next event One important restriction on advancing the clock is that you may only advance forward to the next -timer or ticker event and no further. The following will result in a test failure: +event and no further. Timers, tickers, and pending [mock contexts](#contexts) are events. The +following will result in a test failure: ```go func TestAdvanceTooFar(t *testing.T) { @@ -161,6 +164,12 @@ func TestAdvanceTooFar(t *testing.T) { } ``` +The failure names the events that stop the clock and the calls that created them: + +``` +cannot advance 2s which is beyond next event in 1s: timer from AfterFunc(..., []) +``` + This is a deliberate design decision to allow `Advance()` to immediately and synchronously move the clock forward (even without calling `Wait()` on returned waiter). This helps meet Quartz's design goals of writing deterministic and easy to understand unit tests. It also allows the clock to be @@ -186,7 +195,7 @@ w.MustWait(ctx) ``` `d, ok := Peek()` returns the duration until the next event, if any (`ok` is `true`). You can use -this to advance a specific time, regardless of the tickers and timer events: +this to advance a specific time, regardless of the scheduled events: ```go desired := time.Minute // time to advance @@ -315,8 +324,8 @@ by the real clock. They also appear on all methods on returned timers and ticker ### Contexts `Clock` provides `WithDeadline`, `WithDeadlineCause`, `WithTimeout`, and `WithTimeoutCause` methods -that mirror the standard `context` package. With a `*Mock`, contexts created through the clock -expire on mock time: +that mirror the standard `context` package. With a `*Mock`, contexts created through the clock, +called mock contexts, expire on mock time: ```go ctx, cancel := mClock.WithTimeout(context.Background(), 30*time.Second) @@ -367,6 +376,9 @@ contexts created after the test ended are returned already canceled with that ca Call the returned cancel function as you would for `context.WithTimeout`. It is idempotent and removes the pending mock event. Cause variants preserve the cause for `context.Cause`. +A pending mock context also limits how far `Advance()` can move the clock; see [Advance only to the +next event](#advance-only-to-the-next-event). + #### `Deadline()` does not report mock time With a `*Mock`, `ctx.Done()` and `ctx.Err()` follow mock time, but `ctx.Deadline()` reports the diff --git a/mock.go b/mock.go index 5e9a4bb..a40f842 100644 --- a/mock.go +++ b/mock.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "slices" + "strings" "sync" "time" ) @@ -31,7 +32,9 @@ type TestingT interface { var ErrMockClockTestEnded = errors.New("quartz: mock clock test ended") // Mock is the testing implementation of Clock. It tracks a time that monotonically increases -// during a test, triggering any timers or tickers automatically. +// during a test, triggering scheduled events automatically. Timers, tickers, and mock contexts are +// events. A mock context is a context from WithDeadline, WithTimeout, or their Cause variants, and +// it is scheduled until it expires or is canceled. type Mock struct { tb TestingT logger Logger @@ -50,6 +53,26 @@ type Mock struct { type event interface { next() time.Time fire(t time.Time) + // describe names the event for failure messages. + describe() string +} + +// eventSource is the Clock call that created an event. +type eventSource struct { + fn clockFunction + tags []string +} + +// describe formats the call like Trap.String, so the user can find the call or trap it by function +// and tags. +func (s eventSource) describe(kind string) string { + return fmt.Sprintf("%s from %s(..., %v)", kind, s.fn, s.tags) +} + +// newEventSource copies the tags, which may alias the caller's variadic slice. The caller may reuse +// that slice once the call returns, while describe reads the tags later. +func newEventSource(c *apiCall) eventSource { + return eventSource{fn: c.fn, tags: slices.Clone(c.Tags)} } func (m *Mock) TickerFunc(ctx context.Context, d time.Duration, f func() error, tags ...string) Waiter { @@ -68,6 +91,7 @@ func (m *Mock) TickerFunc(ctx context.Context, d time.Duration, f func() error, nxt: m.cur.Add(d), mock: m, cond: sync.NewCond(&m.mu), + src: newEventSource(c), } m.all = append(m.all, t) m.recomputeNextLocked() @@ -87,7 +111,7 @@ func (m *Mock) NewTicker(d time.Duration, tags ...string) *Ticker { c := newCall(clockFunctionNewTicker, tags, withDuration(d)) m.matchCallLocked(c) defer close(c.complete) - return newMockTickerLocked(m, d) + return newMockTickerLocked(m, d, newEventSource(c)) } func (m *Mock) NewTimer(d time.Duration, tags ...string) *Timer { @@ -102,6 +126,7 @@ func (m *Mock) NewTimer(d time.Duration, tags ...string) *Timer { c: ch, nxt: m.cur.Add(d), mock: m, + src: newEventSource(c), } if d <= 0 { // zero or negative duration timer means we should immediately fire @@ -123,6 +148,7 @@ func (m *Mock) AfterFunc(d time.Duration, f func(), tags ...string) *Timer { nxt: m.cur.Add(d), fn: f, mock: m, + src: newEventSource(c), } if d <= 0 { // zero or negative duration timer means we should immediately fire @@ -199,6 +225,7 @@ func (m *Mock) newMockCtx(parent context.Context, call *apiCall, cause error, de finished: make(chan struct{}), cause: cause, mock: m, + src: newEventSource(call), done: make(chan struct{}), } @@ -300,6 +327,15 @@ func (m *Mock) recomputeNextLocked() { m.nextEvents = events } +// describeNextEventsLocked lists the events scheduled at nextTime for failure messages. +func (m *Mock) describeNextEventsLocked() string { + descs := make([]string, 0, len(m.nextEvents)) + for _, e := range m.nextEvents { + descs = append(descs, e.describe()) + } + return strings.Join(descs, ", ") +} + func (m *Mock) removeTimer(t *Timer) { m.mu.Lock() defer m.mu.Unlock() @@ -343,19 +379,19 @@ func (m *Mock) matchCallLocked(c *apiCall) { m.mu.Lock() } -// AdvanceWaiter is returned from Advance and Set calls and allows you to wait for ticks and timers -// to complete. In the case of functions passed to AfterFunc or TickerFunc, it waits for the -// functions to return. For other ticks & timers, it just waits for the tick to be delivered to -// the channel. +// AdvanceWaiter is returned from Advance and Set calls and allows you to wait for the triggered +// events to complete. In the case of functions passed to AfterFunc or TickerFunc, it waits for the +// functions to return. For other ticks & timers, it just waits for the tick to be delivered to the +// channel. For a mock context, it waits until the context is done, along with the derived contexts +// that Clock.WithDeadline lists. // -// If multiple timers or tickers trigger simultaneously, they are all run on separate -// go routines. +// If multiple events trigger simultaneously, they are all run on separate go routines. type AdvanceWaiter struct { tb TestingT ch chan struct{} } -// Wait for all timers and ticks to complete, or until context expires. +// Wait for all triggered events to complete, or until ctx expires. func (w AdvanceWaiter) Wait(ctx context.Context) error { select { case <-w.ch: @@ -365,7 +401,7 @@ func (w AdvanceWaiter) Wait(ctx context.Context) error { } } -// MustWait waits for all timers and ticks to complete, and fails the test immediately if the +// MustWait waits for all triggered events to complete, and fails the test immediately if the // context completes first. MustWait must be called from the goroutine running the test or // benchmark, similar to `t.FailNow()`. func (w AdvanceWaiter) MustWait(ctx context.Context) { @@ -378,15 +414,15 @@ func (w AdvanceWaiter) MustWait(ctx context.Context) { } } -// Done returns a channel that is closed when all timers and ticks complete. +// Done returns a channel that is closed when all triggered events complete. func (w AdvanceWaiter) Done() <-chan struct{} { return w.ch } -// Advance moves the clock forward by d, triggering any timers or tickers. The returned value can -// be used to wait for all timers and ticks to complete. Advance sets the clock forward before -// returning, and can only advance up to the next timer or tick event. It will fail the test if you -// attempt to advance beyond. +// Advance moves the clock forward by d, triggering any events scheduled at that time. The returned +// value can be used to wait for the triggered events to complete. Advance sets the clock forward +// before returning, and can only advance up to the next scheduled event. It will fail the test if +// you attempt to advance beyond. // // If you need to advance exactly to the next event, and don't know or don't wish to calculate it, // consider AdvanceNext(). @@ -407,8 +443,8 @@ func (m *Mock) Advance(d time.Duration) AdvanceWaiter { return w } if fin.After(m.nextTime) { - m.tb.Errorf("cannot advance %s which is beyond next timer/ticker event in %s", - d.String(), m.nextTime.Sub(m.cur)) + m.tb.Errorf("cannot advance %s which is beyond next event in %s: %s", + d.String(), m.nextTime.Sub(m.cur), m.describeNextEventsLocked()) m.mu.Unlock() close(w.ch) return w @@ -439,8 +475,8 @@ func (m *Mock) advanceLocked(w AdvanceWaiter) { } // Set the time to t. If the time is after the current mocked time, then this is equivalent to -// Advance() with the difference. You may only Set the time earlier than the current time before -// starting tickers and timers (e.g. at the start of your test case). +// Advance() with the difference. You may only Set the time earlier than the current time while no +// events are scheduled (e.g. at the start of your test case). func (m *Mock) Set(t time.Time) AdvanceWaiter { m.tb.Helper() w := AdvanceWaiter{tb: m.tb, ch: make(chan struct{})} @@ -454,7 +490,8 @@ func (m *Mock) Set(t time.Time) AdvanceWaiter { defer m.mu.Unlock() // past if !m.nextTime.IsZero() { - m.tb.Error("Set mock clock to the past after timers/tickers started") + m.tb.Errorf("Set mock clock to the past while events are scheduled, next at %s: %s", + m.nextTime, m.describeNextEventsLocked()) } m.cur = t return w @@ -470,8 +507,8 @@ func (m *Mock) Set(t time.Time) AdvanceWaiter { if t.After(m.nextTime) { defer close(w.ch) defer m.mu.Unlock() - m.tb.Errorf("cannot Set time to %s which is beyond next timer/ticker event at %s", - t.String(), m.nextTime) + m.tb.Errorf("cannot Set time to %s which is beyond next event at %s: %s", + t.String(), m.nextTime, m.describeNextEventsLocked()) return w } @@ -480,9 +517,9 @@ func (m *Mock) Set(t time.Time) AdvanceWaiter { return w } -// AdvanceNext advances the clock to the next timer or tick event. It fails the test if there are -// none scheduled. It returns the duration the clock was advanced and a waiter that can be used to -// wait for the timer/tick event(s) to finish. +// AdvanceNext advances the clock to the next scheduled event. It fails the test if there are none +// scheduled. It returns the duration the clock was advanced and a waiter that can be used to wait +// for the event(s) to finish. func (m *Mock) AdvanceNext() (time.Duration, AdvanceWaiter) { m.checkDoneContexts() m.mu.Lock() @@ -494,7 +531,7 @@ func (m *Mock) AdvanceNext() (time.Duration, AdvanceWaiter) { if m.nextTime.IsZero() { defer close(w.ch) defer m.mu.Unlock() - m.tb.Error("cannot AdvanceNext because there are no timers or tickers running") + m.tb.Error("cannot AdvanceNext because no timers, tickers, or mock contexts are scheduled") return 0, w } d := m.nextTime.Sub(m.cur) @@ -503,9 +540,8 @@ func (m *Mock) AdvanceNext() (time.Duration, AdvanceWaiter) { return d, w } -// Peek returns the duration until the next ticker or timer event and the value -// true, or, if there are no running tickers or timers, it returns zero and -// false. +// Peek returns the duration until the next scheduled event and the value true, or, if there are no +// scheduled events, it returns zero and false. func (m *Mock) Peek() (d time.Duration, ok bool) { m.checkDoneContexts() m.mu.Lock() @@ -620,9 +656,8 @@ func (m *Mock) WithLogger(l Logger) *Mock { return m } -// NewMock creates a new Mock with the time set to midnight UTC on Jan 1, 2024. -// You may re-set the time earlier than this, but only before timers or tickers -// are created. +// NewMock creates a new Mock with the time set to midnight UTC on Jan 1, 2024. You may re-set the +// time earlier than this, but only while no events are scheduled. func NewMock(tb TestingT) *Mock { cur, err := time.Parse(time.RFC3339, "2024-01-01T00:00:00Z") if err != nil { @@ -659,12 +694,18 @@ type mockTickerFunc struct { done bool // err holds the error when the ticker exits err error + // src is the mock call that created the ticker + src eventSource } func (m *mockTickerFunc) next() time.Time { return m.nxt } +func (m *mockTickerFunc) describe() string { + return m.src.describe("ticker") +} + func (m *mockTickerFunc) fire(_ time.Time) { m.mock.mu.Lock() if m.done { @@ -731,6 +772,7 @@ var _ Waiter = &mockTickerFunc{} type mockCtx struct { mock *Mock parent context.Context + src eventSource // inner stores the cancellation cause so context.Cause() can find it via Value(&cancelCtxKey). // Done() must NOT return inner.Done(): stdlib's parentCancelCtx fast-path would then attach @@ -908,6 +950,10 @@ func (c *mockCtx) next() time.Time { return c.deadline } +func (c *mockCtx) describe() string { + return c.src.describe("mock context") +} + func (c *mockCtx) fire(_ time.Time) { c.cancel(context.DeadlineExceeded, c.cause) // Advance().MustWait() waits for fire, so wait for mock descendants that other goroutines are diff --git a/mock_test.go b/mock_test.go index 37a3fe1..780dbef 100644 --- a/mock_test.go +++ b/mock_test.go @@ -7,6 +7,7 @@ import ( "fmt" "os" "runtime/pprof" + "slices" "strings" "sync" "testing" @@ -340,28 +341,46 @@ func TestTickerFunc_MockContextCanceled(t *testing.T) { } } -// A mock context expires when Advance reaches its deadline. By the time MustWait returns, it and -// the contexts derived from it report DeadlineExceeded and the deadline's cause. +// A mock context expires when Advance or AdvanceNext reaches its deadline. By the time MustWait +// returns, it and the contexts derived from it report DeadlineExceeded and the deadline's cause. func TestWithTimeout_FiresOnAdvance(t *testing.T) { t.Parallel() deadlineCause := errors.New("deadline cause") + withTimeout := func(mClock *quartz.Mock) (context.Context, context.CancelFunc) { + return mClock.WithTimeout(context.Background(), time.Second) + } + advance := func(_ *testing.T, mClock *quartz.Mock) quartz.AdvanceWaiter { + return mClock.Advance(time.Second) + } tests := map[string]struct { newCtx func(mClock *quartz.Mock) (context.Context, context.CancelFunc) + move func(t *testing.T, mClock *quartz.Mock) quartz.AdvanceWaiter wantCause error }{ "WithTimeout": { - newCtx: func(mClock *quartz.Mock) (context.Context, context.CancelFunc) { - return mClock.WithTimeout(context.Background(), time.Second) - }, + newCtx: withTimeout, + move: advance, wantCause: context.DeadlineExceeded, }, "WithTimeoutCause": { newCtx: func(mClock *quartz.Mock) (context.Context, context.CancelFunc) { return mClock.WithTimeoutCause(context.Background(), time.Second, deadlineCause) }, + move: advance, wantCause: deadlineCause, }, + "AdvanceNext": { + newCtx: withTimeout, + move: func(t *testing.T, mClock *quartz.Mock) quartz.AdvanceWaiter { + d, w := mClock.AdvanceNext() + if d != time.Second { + t.Errorf("expected AdvanceNext to advance 1s, got %s", d) + } + return w + }, + wantCause: context.DeadlineExceeded, + }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { @@ -375,7 +394,7 @@ func TestWithTimeout_FiresOnAdvance(t *testing.T) { ctxs := descendants(t, mClock, ctx) ctxs["Self"] = ctx - mClock.Advance(time.Second).MustWait(testCtx) + tt.move(t, mClock).MustWait(testCtx) for name, c := range ctxs { if !errors.Is(c.Err(), context.DeadlineExceeded) { @@ -1208,6 +1227,107 @@ func TestWithTimeout_Trap(t *testing.T) { } } +// Failures to move the clock name the events that block it, and the AdvanceNext failure lists the +// kinds of event it looks for, so a pending mock context is not mistaken for a timer or ticker. +func TestMock_FailureNamesBlockingEvents(t *testing.T) { + t.Parallel() + + advanceHour := func(mClock *quartz.Mock) { mClock.Advance(time.Hour) } + tests := map[string]struct { + schedule func(t *testing.T, mClock *quartz.Mock, tags []string) + move func(mClock *quartz.Mock) + want string + }{ + "AdvancePastWithTimeout": { + schedule: func(t *testing.T, mClock *quartz.Mock, tags []string) { + _, cancel := mClock.WithTimeout(context.Background(), 30*time.Second, tags...) + t.Cleanup(cancel) + }, + move: advanceHour, + want: "cannot advance 1h0m0s which is beyond next event in 30s: " + + "mock context from WithTimeout(..., [a])", + }, + "AdvancePastNewTimer": { + schedule: func(_ *testing.T, mClock *quartz.Mock, tags []string) { + mClock.NewTimer(30*time.Second, tags...) + }, + move: advanceHour, + want: "cannot advance 1h0m0s which is beyond next event in 30s: " + + "timer from NewTimer(..., [a])", + }, + "AdvancePastAfterFunc": { + schedule: func(_ *testing.T, mClock *quartz.Mock, tags []string) { + mClock.AfterFunc(30*time.Second, func() {}, tags...) + }, + move: advanceHour, + want: "cannot advance 1h0m0s which is beyond next event in 30s: " + + "timer from AfterFunc(..., [a])", + }, + "AdvancePastNewTicker": { + schedule: func(_ *testing.T, mClock *quartz.Mock, tags []string) { + mClock.NewTicker(30*time.Second, tags...) + }, + move: advanceHour, + want: "cannot advance 1h0m0s which is beyond next event in 30s: " + + "ticker from NewTicker(..., [a])", + }, + "AdvancePastTickerFunc": { + schedule: func(t *testing.T, mClock *quartz.Mock, tags []string) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + mClock.TickerFunc(ctx, 30*time.Second, func() error { return nil }, tags...) + }, + move: advanceHour, + want: "cannot advance 1h0m0s which is beyond next event in 30s: " + + "ticker from TickerFunc(..., [a])", + }, + "SetPastTwoEvents": { + schedule: func(t *testing.T, mClock *quartz.Mock, tags []string) { + mClock.NewTimer(30 * time.Second) + deadline := mClock.Now().Add(30 * time.Second) + _, cancel := mClock.WithDeadline(context.Background(), deadline, tags...) + t.Cleanup(cancel) + }, + move: func(mClock *quartz.Mock) { mClock.Set(mClock.Now().Add(time.Hour)) }, + want: "cannot Set time to 2024-01-01 01:00:00 +0000 UTC " + + "which is beyond next event at 2024-01-01 00:00:30 +0000 UTC: " + + "timer from NewTimer(..., []), mock context from WithDeadline(..., [a])", + }, + "SetBackward": { + schedule: func(t *testing.T, mClock *quartz.Mock, tags []string) { + _, cancel := mClock.WithTimeout(context.Background(), 30*time.Second, tags...) + t.Cleanup(cancel) + }, + move: func(mClock *quartz.Mock) { mClock.Set(mClock.Now().Add(-time.Hour)) }, + want: "Set mock clock to the past while events are scheduled, next at " + + "2024-01-01 00:00:30 +0000 UTC: mock context from WithTimeout(..., [a])", + }, + "AdvanceNextWithNothingScheduled": { + schedule: func(*testing.T, *quartz.Mock, []string) {}, + move: func(mClock *quartz.Mock) { mClock.AdvanceNext() }, + want: "cannot AdvanceNext because no timers, tickers, or mock contexts are " + + "scheduled", + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + t.Parallel() + tb := &captureFailTB{TB: t} + mClock := quartz.NewMock(tb) + tags := []string{"a"} + tt.schedule(t, mClock, tags) + // The caller may reuse its tags slice once the call returns. The failure still reports + // the tags the event was created with. + tags[0] = "reused" + + tt.move(mClock) + if want := []string{tt.want}; !slices.Equal(tb.errs, want) { + t.Fatalf("expected errors %q, got %q", want, tb.errs) + } + }) + } +} + func Test_MultipleTraps(t *testing.T) { t.Parallel() testCtx, testCancel := context.WithTimeout(context.Background(), 10*time.Second) @@ -1332,18 +1452,22 @@ func Test_WithLogger(t *testing.T) { type captureFailTB struct { failed bool + // errs records Error and Errorf messages. + errs []string testing.TB } func (t *captureFailTB) Errorf(format string, args ...any) { t.Helper() t.Logf(format, args...) + t.errs = append(t.errs, fmt.Sprintf(format, args...)) t.failed = true } func (t *captureFailTB) Error(args ...any) { t.Helper() t.Log(args...) + t.errs = append(t.errs, fmt.Sprint(args...)) t.failed = true } @@ -1458,9 +1582,8 @@ func TestTimerReset_NegativeDuration(t *testing.T) { t.Fatal("Reset should report the timer was active") } - // The timer's old 1h schedule must not remain in the mock's event list. - // If it does, Advance rejects with "cannot advance beyond next - // timer/ticker event". + // The timer's old 1h schedule must not remain in the mock's event list. If it does, Advance + // rejects with "cannot advance 2h0m0s which is beyond next event". if d, ok := mClock.Peek(); ok { t.Fatalf("expected no pending events, got Peek()=%s", d) } diff --git a/ticker.go b/ticker.go index f4a4b06..6e3fa14 100644 --- a/ticker.go +++ b/ticker.go @@ -13,6 +13,7 @@ type Ticker struct { mock *Mock // mock clock, if set stopped bool // true if the ticker is not running internalTicks chan time.Time // used to deliver ticks to the runLoop goroutine + src eventSource // mock call that created the ticker // As of Go 1.23, ticker channels are unbuffered and guaranteed to block forever after a call to stop. // @@ -46,6 +47,10 @@ func (t *Ticker) next() time.Time { return t.nxt } +func (t *Ticker) describe() string { + return t.src.describe("ticker") +} + // Stop turns off a ticker. After Stop, no more ticks will be sent. Stop does // not close the channel, to prevent a concurrent goroutine reading from the // channel from seeing an erroneous "tick". @@ -126,7 +131,7 @@ func (t *Ticker) startRunLoopLocked() { go t.runLoop(interrupt) } -func newMockTickerLocked(m *Mock, d time.Duration) *Ticker { +func newMockTickerLocked(m *Mock, d time.Duration, src eventSource) *Ticker { // no buffer follows Go 1.23+ behavior ticks := make(chan time.Time) t := &Ticker{ @@ -136,6 +141,7 @@ func newMockTickerLocked(m *Mock, d time.Duration) *Ticker { nxt: m.cur.Add(d), mock: m, internalTicks: make(chan time.Time), + src: src, } m.addEventLocked(t) m.tb.Cleanup(func() { diff --git a/timer.go b/timer.go index 0f5f733..fe2fc13 100644 --- a/timer.go +++ b/timer.go @@ -16,6 +16,7 @@ type Timer struct { mock *Mock // mock clock, if set fn func() // AfterFunc function, if set stopped bool // True if stopped, false if running + src eventSource // mock call that created the timer // As of Go 1.23, timer channels are unbuffered and guaranteed to block forever after a call to stop. // @@ -60,6 +61,10 @@ func (t *Timer) next() time.Time { return t.nxt } +func (t *Timer) describe() string { + return t.src.describe("timer") +} + // Stop prevents the Timer from firing. It returns true if the call stops the timer, false if the // timer has already expired or been stopped. Stop does not close the channel, to prevent a read // from the channel succeeding incorrectly.