Skip to content

Commit

Permalink
Implement AfterFunc for Clock, FakeClock and IntervalClock and add un…
Browse files Browse the repository at this point in the history
…it tests for the same

Signed-off-by: Madhav Jivrajani <madhav.jiv@gmail.com>
  • Loading branch information
MadhavJivrajani committed Sep 23, 2021
1 parent d34e5cb commit 21bec34
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 2 deletions.
10 changes: 10 additions & 0 deletions clock/clock.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ type Clock interface {
After(d time.Duration) <-chan time.Time
// NewTimer returns a new Timer.
NewTimer(d time.Duration) Timer
// AfterFunc executes f in its own goroutine after waiting for d duration and returns
// a Timer whose channel can be closed by calling Stop() on the Timer.
AfterFunc(d time.Duration, f func()) Timer
// Sleep sleeps for the provided duration d.
// Consider making the sleep interruptible by using 'select' on a context channel and a timer channel.
Sleep(d time.Duration)
Expand Down Expand Up @@ -88,6 +91,13 @@ func (RealClock) NewTimer(d time.Duration) Timer {
}
}

// AfterFunc is the same as time.AfterFunc(d, f).
func (RealClock) AfterFunc(d time.Duration, f func()) Timer {
return &realTimer{
timer: time.AfterFunc(d, f),
}
}

// Tick is the same as time.Tick(d)
// This method does not allow to free/GC the backing ticker. Use
// NewTicker instead.
Expand Down
33 changes: 31 additions & 2 deletions clock/testing/fake_clock.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ type fakeClockWaiter struct {
skipIfBlocked bool
destChan chan time.Time
fired bool
afterFunc func()
}

// NewFakePassiveClock returns a new FakePassiveClock.
Expand Down Expand Up @@ -116,6 +117,25 @@ func (f *FakeClock) NewTimer(d time.Duration) clock.Timer {
return timer
}

// AfterFunc is the Fake version of time.AfterFunc(d, cb).
func (f *FakeClock) AfterFunc(d time.Duration, cb func()) clock.Timer {
f.lock.Lock()
defer f.lock.Unlock()
stopTime := f.time.Add(d)
ch := make(chan time.Time, 1) // Don't block!

timer := &fakeTimer{
fakeClock: f,
waiter: fakeClockWaiter{
targetTime: stopTime,
destChan: ch,
afterFunc: cb,
},
}
f.waiters = append(f.waiters, &timer.waiter)
return timer
}

// Tick constructs a fake ticker, akin to time.Tick
func (f *FakeClock) Tick(d time.Duration) <-chan time.Time {
if d <= 0 {
Expand Down Expand Up @@ -175,7 +195,6 @@ func (f *FakeClock) setTimeLocked(t time.Time) {
for i := range f.waiters {
w := f.waiters[i]
if !w.targetTime.After(t) {

if w.skipIfBlocked {
select {
case w.destChan <- t:
Expand All @@ -187,6 +206,10 @@ func (f *FakeClock) setTimeLocked(t time.Time) {
w.fired = true
}

if w.afterFunc != nil {
w.afterFunc()
}

if w.stepInterval > 0 {
for !w.targetTime.After(t) {
w.targetTime = w.targetTime.Add(w.stepInterval)
Expand All @@ -201,7 +224,7 @@ func (f *FakeClock) setTimeLocked(t time.Time) {
f.waiters = newWaiters
}

// HasWaiters returns true if After has been called on f but not yet satisfied (so you can
// HasWaiters returns true if After or AfterFunc has been called on f but not yet satisfied (so you can
// write race-free tests).
func (f *FakeClock) HasWaiters() bool {
f.lock.RLock()
Expand Down Expand Up @@ -245,6 +268,12 @@ func (*IntervalClock) NewTimer(d time.Duration) clock.Timer {
panic("IntervalClock doesn't implement NewTimer")
}

// AfterFunc is unimplemented, will panic.
// TODO: make interval clock use FakeClock so this can be implemented.
func (*IntervalClock) AfterFunc(d time.Duration, f func()) clock.Timer {
panic("IntervalClock doesn't implement AfterFunc")
}

// Tick is unimplemented, will panic.
// TODO: make interval clock use FakeClock so this can be implemented.
func (*IntervalClock) Tick(d time.Duration) <-chan time.Time {
Expand Down
60 changes: 60 additions & 0 deletions clock/testing/fake_clock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,66 @@ func TestFakeAfter(t *testing.T) {
}
}

func TestFakeAfterFunc(t *testing.T) {
tc := NewFakeClock(time.Now())
if tc.HasWaiters() {
t.Errorf("unexpected waiter?")
}
expectOneSecTimerFire := false
oneSecTimerFire := 0
tc.AfterFunc(time.Second, func() {
if !expectOneSecTimerFire {
t.Errorf("oneSecTimer func fired")
} else {
oneSecTimerFire++
}
})
if !tc.HasWaiters() {
t.Errorf("unexpected lack of waiter?")
}

expectOneOhOneSecTimerFire := false
oneOhOneSecTimerFire := 0
tc.AfterFunc(time.Second+time.Millisecond, func() {
if !expectOneOhOneSecTimerFire {
t.Errorf("oneOhOneSecTimer func fired")
} else {
oneOhOneSecTimerFire++
}
})

expectTwoSecTimerFire := false
twoSecTimerFire := 0
twoSecTimer := tc.AfterFunc(2*time.Second, func() {
if !expectTwoSecTimerFire {
t.Errorf("twoSecTimer func fired")
} else {
twoSecTimerFire++
}
})

tc.Step(999 * time.Millisecond)

expectOneSecTimerFire = true
tc.Step(time.Millisecond)
if oneSecTimerFire != 1 {
t.Errorf("expected oneSecTimerFire=1, got %d", oneSecTimerFire)
}
expectOneSecTimerFire = false

expectOneOhOneSecTimerFire = true
tc.Step(time.Millisecond)
if oneOhOneSecTimerFire != 1 {
// should not double-trigger!
t.Errorf("expected oneOhOneSecTimerFire=1, got %d", oneOhOneSecTimerFire)
}
expectOneOhOneSecTimerFire = false

// ensure a canceled timer doesn't fire
twoSecTimer.Stop()
tc.Step(time.Second)
}

func TestFakeTick(t *testing.T) {
tc := NewFakeClock(time.Now())
if tc.HasWaiters() {
Expand Down

0 comments on commit 21bec34

Please sign in to comment.