Skip to content

Commit 39aa737

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

File tree

6 files changed

+214
-16
lines changed

6 files changed

+214
-16
lines changed

clock/clock.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ 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
1521
// wait until this happens and obtain the error.

clock/mock.go

Lines changed: 47 additions & 15 deletions
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

Lines changed: 87 additions & 0 deletions
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

Lines changed: 5 additions & 0 deletions
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

Lines changed: 68 additions & 0 deletions
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

Lines changed: 1 addition & 1 deletion
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy