testlib/clock.go (98 lines of code) (raw):
// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package testlib
import (
"sync"
"time"
"github.com/GoogleCloudPlatform/ubbagent/clock"
)
// MockClock is an extension of Clock that adds the ability to set the current time. Now returns
// the value passed to SetNow until a new value is set.
//
// Timers created by a MockClock will fire once the clock's time is set to or after the calculated
// fire time. This helps enable deterministic tests involving timers. However, because a MockClock's
// time doesn't continuously increase, a couple of considerations should be followed to avoid racy
// conditions in tests.
//
// 1. Avoid calls to Clock.Now() outside of the test thread. For example, when creating timers
// asynchronously, avoid calls to clock.Now() and instead get the base time from the test thread.
// e.g.,
//
// func NewComponent(c clock.Clock) *Component {
// c = &Component{clock: c}
// go c.run(c.Now()) // Retrieve the initial time from this thread.
// }
//
// func (c *Component) run(now time.Time) {
// fireAt := now.Add(someDelay)
// for {
// tmr := c.clock.NewTimerAt(fireAt)
// select {
// case <-c.someChan:
// c.handleEvent()
//
// case n := <-tmr.GetC():
// c.somePeriodicOperation()
// // Compute next fire time based on value received from timer.
// fireAt = n.Add(someDelay)
// }
// tmr.Cancel()
// }
//
// 2. When creating a new timer, use Clock.NewTimerAt(time.Time) to specify a point in time at which
// the timer should fire, and calculate that time based on a known base time (#1). Using
// Clock.NewTimer(time.Duration) can lead to race conditions in tests:
//
// d := someDelay - c.clock.Now().Sub(c.lastFireTime)
// // <-- if the MockClock's time is advanced here, the new timer may not fire when expected.
// tmr := c.clock.NewTimer(d)
//
// Instead:
//
// now := c.clock.Now()
// nextFire := now.Add(someDelay - now.Sub(c.lastFireTime))
// tmr := c.clock.NewTimerAt(nextFire)
type MockClock interface {
clock.Clock
SetNow(time.Time)
// GetNextFireTime returns the time that the next Timer will fire, or the zero value if no timers
// are set.
GetNextFireTime() time.Time
}
// NewMockClock creates a new MockClock instance that initially returns time zero.
func NewMockClock() MockClock {
return &mockClock{
timers: make(map[*mockTimer]bool),
}
}
type mockClock struct {
mutex sync.Mutex
now time.Time
timers map[*mockTimer]bool
}
func (mc *mockClock) Now() time.Time {
mc.mutex.Lock()
defer mc.mutex.Unlock()
return mc.now
}
func (mc *mockClock) SetNow(now time.Time) {
mc.mutex.Lock()
defer mc.mutex.Unlock()
mc.now = now
for mt := range mc.timers {
// this call might result in the timer being removed from the set.
mt.maybeFire(now)
}
}
func (mc *mockClock) GetNextFireTime() time.Time {
mc.mutex.Lock()
defer mc.mutex.Unlock()
var earliest time.Time
for mt := range mc.timers {
if !mt.done && (earliest.IsZero() || mt.fireAt.Before(earliest)) {
earliest = mt.fireAt
}
}
return earliest
}
func (mc *mockClock) NewTimer(d time.Duration) clock.Timer {
mc.mutex.Lock()
defer mc.mutex.Unlock()
at := mc.now.Add(d)
return mc.newTimer(at)
}
func (mc *mockClock) NewTimerAt(at time.Time) clock.Timer {
mc.mutex.Lock()
defer mc.mutex.Unlock()
return mc.newTimer(at)
}
// Assumes mc.mutex is held.
func (mc *mockClock) newTimer(at time.Time) clock.Timer {
c := make(chan time.Time, 1)
mt := &mockTimer{
c: c,
owner: mc,
fireAt: at,
}
mc.timers[mt] = true
// Call maybeFire to handle cases where the given duration is 0 or negative.
mt.maybeFire(mc.now)
return mt
}
type mockTimer struct {
c chan time.Time
num int
owner *mockClock
fireAt time.Time
done bool
}
func (mt *mockTimer) GetC() <-chan time.Time {
return mt.c
}
func (mt *mockTimer) Stop() bool {
mt.owner.mutex.Lock()
defer mt.owner.mutex.Unlock()
if mt.done {
return false
}
mt.done = true
mt.remove()
return true
}
// maybeFire fires a timer event into the channel if appropriate mock time has elapsed and the timer
// hasn't already fired or been stopped. Assumes that mt.owner.mutex is held.
func (mt *mockTimer) maybeFire(t time.Time) {
if mt.done || mt.fireAt.After(t) {
return
}
mt.c <- t
mt.done = true
mt.remove()
}
// remove removes this mockTimer from the owner mockClock. Assumes that mt.owner.mutex is held.
func (mt *mockTimer) remove() {
delete(mt.owner.timers, mt)
}