Skip to content

Commit abe75f0

Browse files
authored
fix: fix mock TickerFunc to act like real (#7)
fixes #5 fixes #6 Fixes the mock TickerFunc to behave like the real one in that it won't run more than one callback at a time and will wait until the callback completes to return from a `Wait()` call.
1 parent a32b54a commit abe75f0

File tree

2 files changed

+114
-1
lines changed

2 files changed

+114
-1
lines changed

mock.go

+9-1
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,8 @@ type mockTickerFunc struct {
466466

467467
// cond is a condition Locked on the main Mock.mu
468468
cond *sync.Cond
469+
// inProgress is true when we are actively calling f
470+
inProgress bool
469471
// done is true when the ticker exits
470472
done bool
471473
// err holds the error when the ticker exits
@@ -479,15 +481,18 @@ func (m *mockTickerFunc) next() time.Time {
479481
func (m *mockTickerFunc) fire(_ time.Time) {
480482
m.mock.mu.Lock()
481483
defer m.mock.mu.Unlock()
482-
if m.done {
484+
if m.done || m.inProgress {
483485
return
484486
}
485487
m.nxt = m.nxt.Add(m.d)
486488
m.mock.recomputeNextLocked()
487489

490+
m.inProgress = true
488491
m.mock.mu.Unlock()
489492
err := m.f()
490493
m.mock.mu.Lock()
494+
m.inProgress = false
495+
m.cond.Broadcast() // wake up anything waiting for f to finish
491496
if err != nil {
492497
m.exitLocked(err)
493498
}
@@ -507,6 +512,9 @@ func (m *mockTickerFunc) waitForCtx() {
507512
<-m.ctx.Done()
508513
m.mock.mu.Lock()
509514
defer m.mock.mu.Unlock()
515+
for m.inProgress {
516+
m.cond.Wait()
517+
}
510518
m.exitLocked(m.ctx.Err())
511519
}
512520

mock_test.go

+105
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package quartz_test
22

33
import (
44
"context"
5+
"errors"
56
"testing"
67
"time"
78

@@ -210,3 +211,107 @@ func TestPeek(t *testing.T) {
210211
t.Fatal("expected Peek() to return false")
211212
}
212213
}
214+
215+
// TestTickerFunc_ContextDoneDuringTick tests that TickerFunc.Wait() can't return while the tick
216+
// function callback is in progress.
217+
func TestTickerFunc_ContextDoneDuringTick(t *testing.T) {
218+
t.Parallel()
219+
testCtx, testCancel := context.WithTimeout(context.Background(), 10*time.Second)
220+
defer testCancel()
221+
mClock := quartz.NewMock(t)
222+
223+
tickStart := make(chan struct{})
224+
tickDone := make(chan struct{})
225+
ctx, cancel := context.WithCancel(testCtx)
226+
defer cancel()
227+
tkr := mClock.TickerFunc(ctx, time.Second, func() error {
228+
close(tickStart)
229+
select {
230+
case <-tickDone:
231+
case <-testCtx.Done():
232+
t.Error("timeout waiting for tickDone")
233+
}
234+
return nil
235+
})
236+
w := mClock.Advance(time.Second)
237+
select {
238+
case <-tickStart:
239+
// OK
240+
case <-testCtx.Done():
241+
t.Fatal("timeout waiting for tickStart")
242+
}
243+
waitErr := make(chan error, 1)
244+
go func() {
245+
waitErr <- tkr.Wait()
246+
}()
247+
cancel()
248+
select {
249+
case <-waitErr:
250+
t.Fatal("wait should not return while tick callback in progress")
251+
case <-time.After(time.Millisecond * 100):
252+
// OK
253+
}
254+
close(tickDone)
255+
select {
256+
case err := <-waitErr:
257+
if !errors.Is(err, context.Canceled) {
258+
t.Fatal("expected context.Canceled error")
259+
}
260+
case <-testCtx.Done():
261+
t.Fatal("timed out waiting for wait to finish")
262+
}
263+
w.MustWait(testCtx)
264+
}
265+
266+
// TestTickerFunc_LongCallback tests that we don't call the ticker func a second time while the
267+
// first is still executing.
268+
func TestTickerFunc_LongCallback(t *testing.T) {
269+
t.Parallel()
270+
testCtx, testCancel := context.WithTimeout(context.Background(), 10*time.Second)
271+
defer testCancel()
272+
mClock := quartz.NewMock(t)
273+
274+
expectedErr := errors.New("callback error")
275+
tickStart := make(chan struct{})
276+
tickDone := make(chan struct{})
277+
ctx, cancel := context.WithCancel(testCtx)
278+
defer cancel()
279+
tkr := mClock.TickerFunc(ctx, time.Second, func() error {
280+
close(tickStart)
281+
select {
282+
case <-tickDone:
283+
case <-testCtx.Done():
284+
t.Error("timeout waiting for tickDone")
285+
}
286+
return expectedErr
287+
})
288+
w := mClock.Advance(time.Second)
289+
select {
290+
case <-tickStart:
291+
// OK
292+
case <-testCtx.Done():
293+
t.Fatal("timeout waiting for tickStart")
294+
}
295+
// second tick completes immediately, since it doesn't actually call the
296+
// ticker function.
297+
mClock.Advance(time.Second).MustWait(testCtx)
298+
299+
waitErr := make(chan error, 1)
300+
go func() {
301+
waitErr <- tkr.Wait()
302+
}()
303+
cancel()
304+
close(tickDone)
305+
306+
select {
307+
case err := <-waitErr:
308+
// we should get the function error, not the context error, since context was canceled while
309+
// we were calling the function, and it returned an error.
310+
if !errors.Is(err, expectedErr) {
311+
t.Fatalf("wrong error: %s", err)
312+
}
313+
case <-testCtx.Done():
314+
t.Fatal("timed out waiting for wait to finish")
315+
}
316+
w.MustWait(testCtx)
317+
}

0 commit comments

Comments
 (0)