Skip to content

Commit d314742

Browse files
committed
feat: add NewTicker to clock testing library
1 parent 7049d7a commit d314742

File tree

6 files changed

+216
-17
lines changed

6 files changed

+216
-17
lines changed

clock/clock.go

+8-1
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,16 @@ import (
1010
)
1111

1212
type Clock interface {
13+
// NewTicker returns a new Ticker containing a channel that will send the current time on the
14+
// channel after each tick. The period of the ticks is specified by the duration argument. The
15+
// ticker will adjust the time interval or drop ticks to make up for slow receivers. The
16+
// duration d must be greater than zero; if not, NewTicker will panic. Stop the ticker to
17+
// release associated resources.
18+
NewTicker(d time.Duration, tags ...string) *Ticker
1319
// TickerFunc is a convenience function that calls f on the interval d until either the given
1420
// context expires or f returns an error. Callers may call Wait() on the returned Waiter to
15-
// wait until this happens and obtain the error.
21+
// wait until this happens and obtain the error. The duration d must be greater than zero; if
22+
// not, TickerFunc will panic.
1623
TickerFunc(ctx context.Context, d time.Duration, f func() error, tags ...string) Waiter
1724
// NewTimer creates a new Timer that will send the current time on its channel after at least
1825
// duration d.

clock/mock.go

+47-15
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ type event interface {
3232
}
3333

3434
func (m *Mock) TickerFunc(ctx context.Context, d time.Duration, f func() error, tags ...string) Waiter {
35+
if d <= 0 {
36+
panic("TickerFunc called with negative or zero duration")
37+
}
3538
m.mu.Lock()
3639
defer m.mu.Unlock()
3740
c := newCall(clockFunctionTickerFunc, tags, withDuration(d))
@@ -51,6 +54,28 @@ func (m *Mock) TickerFunc(ctx context.Context, d time.Duration, f func() error,
5154
return t
5255
}
5356

57+
func (m *Mock) NewTicker(d time.Duration, tags ...string) *Ticker {
58+
if d <= 0 {
59+
panic("NewTicker called with negative or zero duration")
60+
}
61+
m.mu.Lock()
62+
defer m.mu.Unlock()
63+
c := newCall(clockFunctionNewTicker, tags, withDuration(d))
64+
m.matchCallLocked(c)
65+
defer close(c.complete)
66+
// 1 element buffer follows standard library implementation
67+
ticks := make(chan time.Time, 1)
68+
t := &Ticker{
69+
C: ticks,
70+
c: ticks,
71+
d: d,
72+
nxt: m.cur.Add(d),
73+
mock: m,
74+
}
75+
m.addEventLocked(t)
76+
return t
77+
}
78+
5479
func (m *Mock) NewTimer(d time.Duration, tags ...string) *Timer {
5580
m.mu.Lock()
5681
defer m.mu.Unlock()
@@ -70,7 +95,7 @@ func (m *Mock) NewTimer(d time.Duration, tags ...string) *Timer {
7095
go t.fire(t.mock.cur)
7196
return t
7297
}
73-
m.addTimerLocked(t)
98+
m.addEventLocked(t)
7499
return t
75100
}
76101

@@ -91,7 +116,7 @@ func (m *Mock) AfterFunc(d time.Duration, f func(), tags ...string) *Timer {
91116
go t.fire(t.mock.cur)
92117
return t
93118
}
94-
m.addTimerLocked(t)
119+
m.addEventLocked(t)
95120
return t
96121
}
97122

@@ -122,8 +147,8 @@ func (m *Mock) Until(t time.Time, tags ...string) time.Duration {
122147
return t.Sub(m.cur)
123148
}
124149

125-
func (m *Mock) addTimerLocked(t *Timer) {
126-
m.all = append(m.all, t)
150+
func (m *Mock) addEventLocked(e event) {
151+
m.all = append(m.all, e)
127152
m.recomputeNextLocked()
128153
}
129154

@@ -152,20 +177,12 @@ func (m *Mock) removeTimer(t *Timer) {
152177
}
153178

154179
func (m *Mock) removeTimerLocked(t *Timer) {
155-
defer m.recomputeNextLocked()
156180
t.stopped = true
157-
var e event = t
158-
for i := range m.all {
159-
if m.all[i] == e {
160-
m.all = append(m.all[:i], m.all[i+1:]...)
161-
return
162-
}
163-
}
181+
m.removeEventLocked(t)
164182
}
165183

166-
func (m *Mock) removeTickerFuncLocked(ct *mockTickerFunc) {
184+
func (m *Mock) removeEventLocked(e event) {
167185
defer m.recomputeNextLocked()
168-
var e event = ct
169186
for i := range m.all {
170187
if m.all[i] == e {
171188
m.all = append(m.all[:i], m.all[i+1:]...)
@@ -371,6 +388,18 @@ func (t Trapper) TickerFuncWait(tags ...string) *Trap {
371388
return t.mock.newTrap(clockFunctionTickerFuncWait, tags)
372389
}
373390

391+
func (t Trapper) NewTicker(tags ...string) *Trap {
392+
return t.mock.newTrap(clockFunctionNewTicker, tags)
393+
}
394+
395+
func (t Trapper) TickerStop(tags ...string) *Trap {
396+
return t.mock.newTrap(clockFunctionTickerStop, tags)
397+
}
398+
399+
func (t Trapper) TickerReset(tags ...string) *Trap {
400+
return t.mock.newTrap(clockFunctionTickerReset, tags)
401+
}
402+
374403
func (t Trapper) Now(tags ...string) *Trap {
375404
return t.mock.newTrap(clockFunctionNow, tags)
376405
}
@@ -459,7 +488,7 @@ func (m *mockTickerFunc) exitLocked(err error) {
459488
}
460489
m.done = true
461490
m.err = err
462-
m.mock.removeTickerFuncLocked(m)
491+
m.mock.removeEventLocked(m)
463492
m.cond.Broadcast()
464493
}
465494

@@ -493,6 +522,9 @@ const (
493522
clockFunctionTimerReset
494523
clockFunctionTickerFunc
495524
clockFunctionTickerFuncWait
525+
clockFunctionNewTicker
526+
clockFunctionTickerReset
527+
clockFunctionTickerStop
496528
clockFunctionNow
497529
clockFunctionSince
498530
clockFunctionUntil

clock/mock_test.go

+87
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,90 @@ func TestAfterFunc_NegativeDuration(t *testing.T) {
8080
t.Fatal("timer still running")
8181
}
8282
}
83+
84+
func TestNewTicker(t *testing.T) {
85+
t.Parallel()
86+
// nolint:gocritic // trying to avoid Coder-specific stuff with an eye toward spinning this out
87+
ctx, cancel := context.WithTimeout(context.Background(), 1000*time.Second)
88+
defer cancel()
89+
90+
mClock := clock.NewMock(t)
91+
start := mClock.Now()
92+
trapNT := mClock.Trap().NewTicker("new")
93+
defer trapNT.Close()
94+
trapStop := mClock.Trap().TickerStop("stop")
95+
defer trapStop.Close()
96+
trapReset := mClock.Trap().TickerReset("reset")
97+
defer trapReset.Close()
98+
99+
tickers := make(chan *clock.Ticker, 1)
100+
go func() {
101+
tickers <- mClock.NewTicker(time.Hour, "new")
102+
}()
103+
c := trapNT.MustWait(ctx)
104+
c.Release()
105+
if c.Duration != time.Hour {
106+
t.Fatalf("expected time.Hour, got: %v", c.Duration)
107+
}
108+
tkr := <-tickers
109+
110+
for i := 0; i < 3; i++ {
111+
mClock.Advance(time.Hour).MustWait(ctx)
112+
}
113+
114+
// should get first tick, rest dropped
115+
tTime := start.Add(time.Hour)
116+
select {
117+
case <-ctx.Done():
118+
t.Fatal("timeout waiting for ticker")
119+
case tick := <-tkr.C:
120+
if !tick.Equal(tTime) {
121+
t.Fatalf("expected time %v, got %v", tTime, tick)
122+
}
123+
}
124+
125+
go tkr.Reset(time.Minute, "reset")
126+
c = trapReset.MustWait(ctx)
127+
mClock.Advance(time.Second).MustWait(ctx)
128+
c.Release()
129+
if c.Duration != time.Minute {
130+
t.Fatalf("expected time.Minute, got: %v", c.Duration)
131+
}
132+
mClock.Advance(time.Minute).MustWait(ctx)
133+
134+
// tick should show present time, ensuring the 2 hour ticks got dropped when
135+
// we didn't read from the channel.
136+
tTime = mClock.Now()
137+
select {
138+
case <-ctx.Done():
139+
t.Fatal("timeout waiting for ticker")
140+
case tick := <-tkr.C:
141+
if !tick.Equal(tTime) {
142+
t.Fatalf("expected time %v, got %v", tTime, tick)
143+
}
144+
}
145+
146+
go tkr.Stop("stop")
147+
trapStop.MustWait(ctx).Release()
148+
mClock.Advance(time.Hour).MustWait(ctx)
149+
select {
150+
case <-tkr.C:
151+
t.Fatal("ticker still running")
152+
default:
153+
// OK
154+
}
155+
156+
// Resetting after stop
157+
go tkr.Reset(time.Minute, "reset")
158+
trapReset.MustWait(ctx).Release()
159+
mClock.Advance(time.Minute).MustWait(ctx)
160+
tTime = mClock.Now()
161+
select {
162+
case <-ctx.Done():
163+
t.Fatal("timeout waiting for ticker")
164+
case tick := <-tkr.C:
165+
if !tick.Equal(tTime) {
166+
t.Fatalf("expected time %v, got %v", tTime, tick)
167+
}
168+
}
169+
}

clock/real.go

+5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ func NewReal() Clock {
1111
return realClock{}
1212
}
1313

14+
func (realClock) NewTicker(d time.Duration, _ ...string) *Ticker {
15+
tkr := time.NewTicker(d)
16+
return &Ticker{ticker: tkr, C: tkr.C}
17+
}
18+
1419
func (realClock) TickerFunc(ctx context.Context, d time.Duration, f func() error, _ ...string) Waiter {
1520
ct := &realContextTicker{
1621
ctx: ctx,

clock/ticker.go

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package clock
2+
3+
import "time"
4+
5+
type Ticker struct {
6+
C <-chan time.Time
7+
//nolint: revive
8+
c chan time.Time
9+
ticker *time.Ticker // realtime impl, if set
10+
d time.Duration // period, if set
11+
nxt time.Time // next tick time
12+
mock *Mock // mock clock, if set
13+
stopped bool // true if the ticker is not running
14+
}
15+
16+
func (t *Ticker) fire(tt time.Time) {
17+
t.mock.mu.Lock()
18+
defer t.mock.mu.Unlock()
19+
if t.stopped {
20+
return
21+
}
22+
for !t.nxt.After(t.mock.cur) {
23+
t.nxt = t.nxt.Add(t.d)
24+
}
25+
t.mock.recomputeNextLocked()
26+
select {
27+
case t.c <- tt:
28+
default:
29+
}
30+
}
31+
32+
func (t *Ticker) next() time.Time {
33+
return t.nxt
34+
}
35+
36+
func (t *Ticker) Stop(tags ...string) {
37+
if t.ticker != nil {
38+
t.ticker.Stop()
39+
return
40+
}
41+
t.mock.mu.Lock()
42+
defer t.mock.mu.Unlock()
43+
c := newCall(clockFunctionTickerStop, tags)
44+
t.mock.matchCallLocked(c)
45+
defer close(c.complete)
46+
t.mock.removeEventLocked(t)
47+
t.stopped = true
48+
}
49+
50+
func (t *Ticker) Reset(d time.Duration, tags ...string) {
51+
if t.ticker != nil {
52+
t.ticker.Reset(d)
53+
return
54+
}
55+
t.mock.mu.Lock()
56+
defer t.mock.mu.Unlock()
57+
c := newCall(clockFunctionTickerReset, tags, withDuration(d))
58+
t.mock.matchCallLocked(c)
59+
defer close(c.complete)
60+
t.nxt = t.mock.cur.Add(d)
61+
t.d = d
62+
if t.stopped {
63+
t.stopped = false
64+
t.mock.addEventLocked(t)
65+
} else {
66+
t.mock.recomputeNextLocked()
67+
}
68+
}

clock/timer.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,6 @@ func (t *Timer) Reset(d time.Duration, tags ...string) bool {
6464
t.mock.removeTimerLocked(t)
6565
t.stopped = false
6666
t.nxt = t.mock.cur.Add(d)
67-
t.mock.addTimerLocked(t)
67+
t.mock.addEventLocked(t)
6868
return result
6969
}

0 commit comments

Comments
 (0)