Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Convert FakeClock to a struct. #71

Merged
merged 1 commit into from
Jul 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 48 additions & 57 deletions clockwork.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,44 +21,12 @@ type Clock interface {
AfterFunc(d time.Duration, f func()) Timer
}

// FakeClock provides an interface for a clock which can be manually advanced
// through time.
//
// FakeClock maintains a list of "waiters," which consists of all callers
// waiting on the underlying clock (i.e. Tickers and Timers including callers of
// Sleep or After). Users can call BlockUntil to block until the clock has an
// expected number of waiters.
type FakeClock interface {
Clock
// Advance advances the FakeClock to a new point in time, ensuring any existing
// waiters are notified appropriately before returning.
Advance(d time.Duration)
// BlockUntil blocks until the FakeClock has the given number of waiters.
BlockUntil(waiters int)
}

// NewRealClock returns a Clock which simply delegates calls to the actual time
// package; it should be used by packages in production.
func NewRealClock() Clock {
return &realClock{}
}

// NewFakeClock returns a FakeClock implementation which can be
// manually advanced through time for testing. The initial time of the
// FakeClock will be the current system time.
//
// Tests that require a deterministic time must use NewFakeClockAt.
func NewFakeClock() FakeClock {
return NewFakeClockAt(time.Now())
}

// NewFakeClockAt returns a FakeClock initialised at the given time.Time.
func NewFakeClockAt(t time.Time) FakeClock {
return &fakeClock{
time: t,
}
}

type realClock struct{}

func (rc *realClock) After(d time.Duration) <-chan time.Time {
Expand Down Expand Up @@ -89,7 +57,14 @@ func (rc *realClock) AfterFunc(d time.Duration, f func()) Timer {
return realTimer{time.AfterFunc(d, f)}
}

type fakeClock struct {
// FakeClock provides an interface for a clock which can be manually advanced
// through time.
//
// FakeClock maintains a list of "waiters," which consists of all callers
// waiting on the underlying clock (i.e. Tickers and Timers including callers of
// Sleep or After). Users can call BlockUntil to block until the clock has an
// expected number of waiters.
type FakeClock struct {
// l protects all attributes of the clock, including all attributes of all
// waiters and blockers.
l sync.RWMutex
Expand All @@ -98,11 +73,27 @@ type fakeClock struct {
time time.Time
}

// NewFakeClock returns a FakeClock implementation which can be
// manually advanced through time for testing. The initial time of the
// FakeClock will be the current system time.
//
// Tests that require a deterministic time must use NewFakeClockAt.
func NewFakeClock() *FakeClock {
return NewFakeClockAt(time.Now())
}

// NewFakeClockAt returns a FakeClock initialised at the given time.Time.
func NewFakeClockAt(t time.Time) *FakeClock {
return &FakeClock{
time: t,
}
}

// blocker is a caller of BlockUntil.
type blocker struct {
count int

// ch is closed when the underlying clock has the specificed number of blockers.
// ch is closed when the underlying clock has the specified number of blockers.
ch chan struct{}
}

Expand All @@ -119,33 +110,33 @@ type expirer interface {

// After mimics [time.After]; it waits for the given duration to elapse on the
// fakeClock, then sends the current time on the returned channel.
func (fc *fakeClock) After(d time.Duration) <-chan time.Time {
func (fc *FakeClock) After(d time.Duration) <-chan time.Time {
return fc.NewTimer(d).Chan()
}

// Sleep blocks until the given duration has passed on the fakeClock.
func (fc *fakeClock) Sleep(d time.Duration) {
func (fc *FakeClock) Sleep(d time.Duration) {
<-fc.After(d)
}

// Now returns the current time of the fakeClock
func (fc *fakeClock) Now() time.Time {
func (fc *FakeClock) Now() time.Time {
fc.l.RLock()
defer fc.l.RUnlock()
return fc.time
}

// Since returns the duration that has passed since the given time on the
// fakeClock.
func (fc *fakeClock) Since(t time.Time) time.Duration {
func (fc *FakeClock) Since(t time.Time) time.Duration {
return fc.Now().Sub(t)
}

// NewTicker returns a Ticker that will expire only after calls to
// fakeClock.Advance() have moved the clock past the given duration.
// FakeClock.Advance() have moved the clock past the given duration.
//
// The duration d must be greater than zero; if not, NewTicker will panic.
func (fc *fakeClock) NewTicker(d time.Duration) Ticker {
func (fc *FakeClock) NewTicker(d time.Duration) Ticker {
// Maintain parity with
// https://cs.opensource.google/go/go/+/refs/tags/go1.20.3:src/time/tick.go;l=23-25
if d <= 0 {
Expand All @@ -164,19 +155,19 @@ func (fc *fakeClock) NewTicker(d time.Duration) Ticker {

// NewTimer returns a Timer that will fire only after calls to
// fakeClock.Advance() have moved the clock past the given duration.
func (fc *fakeClock) NewTimer(d time.Duration) Timer {
func (fc *FakeClock) NewTimer(d time.Duration) Timer {
return fc.newTimer(d, nil)
}

// AfterFunc mimics [time.AfterFunc]; it returns a Timer that will invoke the
// given function only after calls to fakeClock.Advance() have moved the clock
// past the given duration.
func (fc *fakeClock) AfterFunc(d time.Duration, f func()) Timer {
func (fc *FakeClock) AfterFunc(d time.Duration, f func()) Timer {
return fc.newTimer(d, f)
}

// newTimer returns a new timer, using an optional afterFunc.
func (fc *fakeClock) newTimer(d time.Duration, afterfunc func()) *fakeTimer {
func (fc *FakeClock) newTimer(d time.Duration, afterfunc func()) *fakeTimer {
var ft *fakeTimer
ft = &fakeTimer{
firer: newFirer(),
Expand All @@ -198,7 +189,7 @@ func (fc *fakeClock) newTimer(d time.Duration, afterfunc func()) *fakeTimer {

// Advance advances fakeClock to a new point in time, ensuring waiters and
// blockers are notified appropriately before returning.
func (fc *fakeClock) Advance(d time.Duration) {
func (fc *FakeClock) Advance(d time.Duration) {
fc.l.Lock()
defer fc.l.Unlock()
end := fc.time.Add(d)
Expand All @@ -211,24 +202,24 @@ func (fc *fakeClock) Advance(d time.Duration) {
w := fc.waiters[0]
fc.waiters = fc.waiters[1:]

// Use the waiter's expriation as the current time for this expiration.
// Use the waiter's expiration as the current time for this expiration.
now := w.expiry()
fc.time = now
if d := w.expire(now); d != nil {
// Set the new exipration if needed.
// Set the new expiration if needed.
fc.setExpirer(w, *d)
}
}
fc.time = end
}

// BlockUntil blocks until the fakeClock has the given number of waiters.
// BlockUntil blocks until the FakeClock has the given number of waiters.
//
// Prefer BlockUntilContext, which offers context cancellation to prevent
// deadlock.
// Prefer BlockUntilContext in new code, which offers context cancellation to
// prevent deadlock.
//
// Deprecation warning: This function might be deprecated in later versions.
func (fc *fakeClock) BlockUntil(n int) {
// Deprecated: New code should prefer BlockUntilContext.
func (fc *FakeClock) BlockUntil(n int) {
b := fc.newBlocker(n)
if b == nil {
return
Expand All @@ -238,7 +229,7 @@ func (fc *fakeClock) BlockUntil(n int) {

// BlockUntilContext blocks until the fakeClock has the given number of waiters
// or the context is cancelled.
func (fc *fakeClock) BlockUntilContext(ctx context.Context, n int) error {
func (fc *FakeClock) BlockUntilContext(ctx context.Context, n int) error {
b := fc.newBlocker(n)
if b == nil {
return nil
Expand All @@ -252,7 +243,7 @@ func (fc *fakeClock) BlockUntilContext(ctx context.Context, n int) error {
}
}

func (fc *fakeClock) newBlocker(n int) *blocker {
func (fc *FakeClock) newBlocker(n int) *blocker {
fc.l.Lock()
defer fc.l.Unlock()
// Fast path: we already have >= n waiters.
Expand All @@ -269,7 +260,7 @@ func (fc *fakeClock) newBlocker(n int) *blocker {
}

// stop stops an expirer, returning true if the expirer was stopped.
func (fc *fakeClock) stop(e expirer) bool {
func (fc *FakeClock) stop(e expirer) bool {
fc.l.Lock()
defer fc.l.Unlock()
return fc.stopExpirer(e)
Expand All @@ -278,7 +269,7 @@ func (fc *fakeClock) stop(e expirer) bool {
// stopExpirer stops an expirer, returning true if the expirer was stopped.
//
// The caller must hold fc.l.
func (fc *fakeClock) stopExpirer(e expirer) bool {
func (fc *FakeClock) stopExpirer(e expirer) bool {
for i, t := range fc.waiters {
if t == e {
// Remove element, maintaining order.
Expand All @@ -292,7 +283,7 @@ func (fc *fakeClock) stopExpirer(e expirer) bool {
}

// set sets an expirer to expire at a future point in time.
func (fc *fakeClock) set(e expirer, d time.Duration) {
func (fc *FakeClock) set(e expirer, d time.Duration) {
fc.l.Lock()
defer fc.l.Unlock()
fc.setExpirer(e, d)
Expand All @@ -301,7 +292,7 @@ func (fc *fakeClock) set(e expirer, d time.Duration) {
// setExpirer sets an expirer to expire at a future point in time.
//
// The caller must hold fc.l.
func (fc *fakeClock) setExpirer(e expirer, d time.Duration) {
func (fc *FakeClock) setExpirer(e expirer, d time.Duration) {
if d.Nanoseconds() <= 0 {
// Special case for timers with duration <= 0: trigger immediately, never
// reset.
Expand Down
12 changes: 6 additions & 6 deletions clockwork_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const timeout = time.Minute

func TestFakeClockAfter(t *testing.T) {
t.Parallel()
fc := &fakeClock{}
fc := &FakeClock{}

neg := fc.After(-1)
select {
Expand Down Expand Up @@ -128,7 +128,7 @@ func TestFakeClockSince(t *testing.T) {
// https://github.com/jonboulle/clockwork/issues/35
func TestTwoBlockersOneBlock(t *testing.T) {
t.Parallel()
fc := &fakeClock{}
fc := &FakeClock{}

ft1 := fc.NewTicker(time.Second)
ft2 := fc.NewTicker(time.Second)
Expand All @@ -141,7 +141,7 @@ func TestTwoBlockersOneBlock(t *testing.T) {

func TestBlockUntilContext(t *testing.T) {
t.Parallel()
fc := &fakeClock{}
fc := &FakeClock{}

ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
Expand All @@ -163,13 +163,13 @@ func TestBlockUntilContext(t *testing.T) {
t.Errorf("BlockUntilContext returned %v, want context.Canceled.", err)
}
case <-ctx.Done():
t.Errorf("Never receved error on context cancellation.")
t.Errorf("Never received error on context cancellation.")
}
}

func TestAfterDeliveryInOrder(t *testing.T) {
t.Parallel()
fc := &fakeClock{}
fc := &FakeClock{}
for i := 0; i < 1000; i++ {
three := fc.After(3 * time.Second)
for j := 0; j < 100; j++ {
Expand All @@ -192,7 +192,7 @@ func TestAfterDeliveryInOrder(t *testing.T) {
// There are no failure conditions when invoked without the -race flag.
func TestFakeClockRace(t *testing.T) {
t.Parallel()
fc := &fakeClock{}
fc := &FakeClock{}
d := time.Second
go func() { fc.Advance(d) }()
go func() { fc.NewTicker(d) }()
Expand Down
4 changes: 2 additions & 2 deletions ticker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (

func TestFakeTickerStop(t *testing.T) {
t.Parallel()
fc := &fakeClock{}
fc := &FakeClock{}

ft := fc.NewTicker(1)
ft.Stop()
Expand All @@ -24,7 +24,7 @@ func TestFakeTickerTick(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()

fc := &fakeClock{}
fc := &FakeClock{}
now := fc.Now()

// The tick at now.Add(2) should not get through since we advance time by
Expand Down
4 changes: 2 additions & 2 deletions timer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (

func TestFakeClockTimerStop(t *testing.T) {
t.Parallel()
fc := &fakeClock{}
fc := &FakeClock{}

ft := fc.NewTimer(1)
ft.Stop()
Expand All @@ -24,7 +24,7 @@ func TestFakeClockTimers(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()

fc := &fakeClock{}
fc := &FakeClock{}

zero := fc.NewTimer(0)

Expand Down
Loading