Skip to content

Commit d0b2f61

Browse files
authored
fix: allow mock clock Timers to accept negative duration (coder#13592)
The standard library `NewTimer`, `AfterFunc` and `Reset` allow negative durations, so our mock clock library should as well.
1 parent 1de023a commit d0b2f61

File tree

3 files changed

+97
-12
lines changed

3 files changed

+97
-12
lines changed

clock/mock.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,6 @@ func (m *Mock) TickerFunc(ctx context.Context, d time.Duration, f func() error,
5252
}
5353

5454
func (m *Mock) NewTimer(d time.Duration, tags ...string) *Timer {
55-
if d < 0 {
56-
panic("duration must be positive or zero")
57-
}
5855
m.mu.Lock()
5956
defer m.mu.Unlock()
6057
c := newCall(clockFunctionNewTimer, tags, withDuration(d))
@@ -67,14 +64,17 @@ func (m *Mock) NewTimer(d time.Duration, tags ...string) *Timer {
6764
nxt: m.cur.Add(d),
6865
mock: m,
6966
}
67+
if d <= 0 {
68+
// zero or negative duration timer means we should immediately fire
69+
// it, rather than add it.
70+
go t.fire(t.mock.cur)
71+
return t
72+
}
7073
m.addTimerLocked(t)
7174
return t
7275
}
7376

7477
func (m *Mock) AfterFunc(d time.Duration, f func(), tags ...string) *Timer {
75-
if d < 0 {
76-
panic("duration must be positive or zero")
77-
}
7878
m.mu.Lock()
7979
defer m.mu.Unlock()
8080
c := newCall(clockFunctionAfterFunc, tags, withDuration(d))
@@ -85,6 +85,12 @@ func (m *Mock) AfterFunc(d time.Duration, f func(), tags ...string) *Timer {
8585
fn: f,
8686
mock: m,
8787
}
88+
if d <= 0 {
89+
// zero or negative duration timer means we should immediately fire
90+
// it, rather than add it.
91+
go t.fire(t.mock.cur)
92+
return t
93+
}
8894
m.addTimerLocked(t)
8995
return t
9096
}

clock/mock_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package clock_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
8+
"github.com/coder/coder/v2/clock"
9+
)
10+
11+
func TestTimer_NegativeDuration(t *testing.T) {
12+
t.Parallel()
13+
// nolint:gocritic // trying to avoid Coder-specific stuff with an eye toward spinning this out
14+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
15+
defer cancel()
16+
17+
mClock := clock.NewMock(t)
18+
start := mClock.Now()
19+
trap := mClock.Trap().NewTimer()
20+
defer trap.Close()
21+
22+
timers := make(chan *clock.Timer, 1)
23+
go func() {
24+
timers <- mClock.NewTimer(-time.Second)
25+
}()
26+
c := trap.MustWait(ctx)
27+
c.Release()
28+
// trap returns the actual passed value
29+
if c.Duration != -time.Second {
30+
t.Fatalf("expected -time.Second, got: %v", c.Duration)
31+
}
32+
33+
tmr := <-timers
34+
select {
35+
case <-ctx.Done():
36+
t.Fatal("timeout waiting for timer")
37+
case tme := <-tmr.C:
38+
// the tick is the current time, not the past
39+
if !tme.Equal(start) {
40+
t.Fatalf("expected time %v, got %v", start, tme)
41+
}
42+
}
43+
if tmr.Stop() {
44+
t.Fatal("timer still running")
45+
}
46+
}
47+
48+
func TestAfterFunc_NegativeDuration(t *testing.T) {
49+
t.Parallel()
50+
// nolint:gocritic // trying to avoid Coder-specific stuff with an eye toward spinning this out
51+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
52+
defer cancel()
53+
54+
mClock := clock.NewMock(t)
55+
trap := mClock.Trap().AfterFunc()
56+
defer trap.Close()
57+
58+
timers := make(chan *clock.Timer, 1)
59+
done := make(chan struct{})
60+
go func() {
61+
timers <- mClock.AfterFunc(-time.Second, func() {
62+
close(done)
63+
})
64+
}()
65+
c := trap.MustWait(ctx)
66+
c.Release()
67+
// trap returns the actual passed value
68+
if c.Duration != -time.Second {
69+
t.Fatalf("expected -time.Second, got: %v", c.Duration)
70+
}
71+
72+
tmr := <-timers
73+
select {
74+
case <-ctx.Done():
75+
t.Fatal("timeout waiting for timer")
76+
case <-done:
77+
// OK!
78+
}
79+
if tmr.Stop() {
80+
t.Fatal("timer still running")
81+
}
82+
}

clock/timer.go

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,6 @@ func (t *Timer) Reset(d time.Duration, tags ...string) bool {
4444
if t.timer != nil {
4545
return t.timer.Reset(d)
4646
}
47-
if d < 0 {
48-
panic("duration must be positive or zero")
49-
}
5047
t.mock.mu.Lock()
5148
defer t.mock.mu.Unlock()
5249
c := newCall(clockFunctionTimerReset, tags, withDuration(d))
@@ -57,9 +54,9 @@ func (t *Timer) Reset(d time.Duration, tags ...string) bool {
5754
case <-t.c:
5855
default:
5956
}
60-
if d == 0 {
61-
// zero duration timer means we should immediately re-fire it, rather
62-
// than remove and re-add it.
57+
if d <= 0 {
58+
// zero or negative duration timer means we should immediately re-fire
59+
// it, rather than remove and re-add it.
6360
t.stopped = false
6461
go t.fire(t.mock.cur)
6562
return result

0 commit comments

Comments
 (0)