-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
510 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,40 @@ | ||
# clock-go | ||
Provides a "Clock" interface for facilitating dependency injection in time-related Go code. | ||
|
||
## Why? | ||
|
||
Golang's "time" package does not facilitate dependency injection by itself, because it relies on package-level functions (e.g. time.Now()). It can be very tricky to properly unit test code like that, because it may require carefully choreographed sleep statements and may end up non-deterministic/flaky. Using a dependency-injected clock allows you to use a real clock in production and a deterministic simulated clock in unit tests. | ||
|
||
## Example | ||
|
||
```go | ||
package main | ||
|
||
import ( | ||
"github.com/karagog/clock-go" | ||
"github.com/karagog/clock-go/real" | ||
"github.com/karagog/clock-go/simulated" | ||
) | ||
|
||
// Given a function that needs the current time, inject a clock | ||
// object instead of calling "time.Now()". | ||
func DoIt(c clock.Clock) { | ||
fmt.Printf("The time is %v", c.Now()) | ||
} | ||
|
||
func main() { | ||
// You can inject a real clock, which delegates to time.Now(). | ||
DoIt(&real.Clock{}) | ||
|
||
// ...or you can use a simulated clock, whose output is | ||
// deterministic and controlled by you. | ||
// | ||
// For example, this simulated clock is one hour in the future: | ||
s := simulated.NewClock(time.Now().Add(time.Hour)) | ||
DoIt(s) | ||
|
||
// You can advance the time in any increments you want. | ||
s.Advance(time.Second) | ||
DoIt(s) | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
// Package clock implements a clock interface. | ||
// | ||
// Your application code should use the clock interface to interact with time wherever possible, | ||
// and then it can be easily unit-tested by swapping out the "real" clock with a "simulated" | ||
// one during unit tests via dependency injection techniques. | ||
package clock | ||
|
||
import ( | ||
"time" | ||
) | ||
|
||
// An abstract clock interface to facilitate dependency injection of time. | ||
// | ||
// Implementations must provide thread-safe access to interface methods, to allow | ||
// clocks to be used in background goroutines. | ||
type Clock interface { | ||
// Returns the current time. | ||
Now() time.Time | ||
|
||
// Returns a timer that will fire after the given duration from Now(). | ||
NewTimer(time.Duration) Timer | ||
} | ||
|
||
// This has essentially the same interface as a time.Timer, except C() is a function | ||
// in order to make it a pure interface that we can mock. | ||
type Timer interface { | ||
Reset(time.Duration) bool | ||
Stop() bool | ||
C() <-chan time.Time | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
package clock | ||
|
||
// There are no tests for an abstract interface. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
module github.com/karagog/clock-go | ||
|
||
go 1.17 | ||
|
||
require github.com/golang/glog v1.0.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= | ||
github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
// Package real implements a real clock, which delegates to the "time" package | ||
// methods to access the time. | ||
package real | ||
|
||
import ( | ||
"time" | ||
|
||
"github.com/karagog/clock-go" | ||
) | ||
|
||
// A real clock which calls `time.Now()` to get the time. | ||
type Clock struct{} | ||
|
||
func (*Clock) Now() time.Time { return time.Now() } | ||
func (*Clock) NewTimer(d time.Duration) clock.Timer { | ||
return &realTimer{ | ||
timer: time.NewTimer(d), | ||
} | ||
} | ||
|
||
// This implementation of the timer interface is backed by a real timer. | ||
type realTimer struct { | ||
timer *time.Timer | ||
} | ||
|
||
func (t *realTimer) Reset(d time.Duration) bool { | ||
return t.timer.Reset(d) | ||
} | ||
|
||
func (t *realTimer) Stop() bool { | ||
return t.timer.Stop() | ||
} | ||
|
||
func (t *realTimer) C() <-chan time.Time { | ||
return t.timer.C | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
package real | ||
|
||
import ( | ||
"testing" | ||
"time" | ||
|
||
"github.com/karagog/clock-go/testutil" | ||
) | ||
|
||
func TestRealClock(t *testing.T) { | ||
c := Clock{} | ||
n := c.Now() | ||
now := time.Now() | ||
diff := now.Sub(n) | ||
tolerance := time.Millisecond | ||
if diff > tolerance { | ||
t.Fatalf("Got time %v, want %v to within %v tolerance. Diff was %v.", | ||
n, now, tolerance, diff) | ||
} | ||
} | ||
|
||
// No need to test this thoroughly, since it's backed by Golang's real timer. | ||
// We'll just test it enough to show that it doesn't crash. | ||
func TestRealClockTimer(t *testing.T) { | ||
c := Clock{} | ||
d := time.Millisecond | ||
tmr := c.NewTimer(d) | ||
if _, fired := testutil.TryRead(tmr.C(), time.Second); !fired { | ||
t.Fatalf("Timer did not fire after %v, wanted it to fire", time.Second) | ||
} | ||
if tmr.Stop() { | ||
<-tmr.C() | ||
} | ||
tmr.Reset(time.Second) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
// Package simulated implements a simulated clock for unit tests. | ||
package simulated | ||
|
||
import ( | ||
"sync" | ||
"time" | ||
|
||
"github.com/golang/glog" | ||
"github.com/karagog/clock-go" | ||
) | ||
|
||
// Clock is a fake clock that you can manipulate through the accessor methods. | ||
// | ||
// Create with NewClock(). Advance time with Advance(). | ||
type Clock struct { | ||
mu sync.RWMutex // guards all members below | ||
now time.Time | ||
activeTimers map[*simulatedTimer]bool | ||
} | ||
|
||
func NewClock(now time.Time) *Clock { | ||
return &Clock{ | ||
now: now, | ||
activeTimers: make(map[*simulatedTimer]bool), | ||
} | ||
} | ||
|
||
func (s *Clock) Advance(d time.Duration) { | ||
if d < 0 { | ||
panic("Time can only advance in the positive direction") | ||
} | ||
s.mu.Lock() | ||
defer s.mu.Unlock() | ||
newTime := s.now.Add(d) | ||
glog.V(1).Infof("Advancing the time %v from %v to %v", newTime.Sub(s.now), s.now, newTime) | ||
s.now = newTime | ||
for t := range s.activeTimers { | ||
if t.shouldFire(s.now) { | ||
t.fire() // timer fired! | ||
} | ||
} | ||
} | ||
|
||
func (s *Clock) Now() time.Time { | ||
s.mu.RLock() | ||
defer s.mu.RUnlock() | ||
return s.now | ||
} | ||
|
||
func (s *Clock) NewTimer(d time.Duration) clock.Timer { | ||
t := &simulatedTimer{ | ||
c: make(chan time.Time, 1), // small buffer to avoid blocking | ||
clock: s, | ||
} | ||
t.Reset(d) | ||
return t | ||
} | ||
|
||
// The timer object may run on a separate goroutine from the clock object, but | ||
// it should never be used from multiple goroutines simultaneously. | ||
type simulatedTimer struct { | ||
clock *Clock | ||
c chan time.Time | ||
deadline time.Time // when the timer should fire | ||
active bool // true if the timer has not yet fired | ||
} | ||
|
||
func (t *simulatedTimer) Reset(d time.Duration) bool { | ||
t.clock.mu.Lock() | ||
defer t.clock.mu.Unlock() | ||
wasActive := t.active | ||
t.active = true | ||
now := t.clock.now | ||
newDeadline := now.Add(d) | ||
glog.V(1).Infof("Resetting timer from deadline %v to %v", t.deadline, newDeadline) | ||
t.deadline = newDeadline | ||
if t.shouldFire(now) { | ||
t.fire() | ||
return wasActive | ||
} | ||
if !wasActive { | ||
glog.V(1).Infof("Timer became active") | ||
t.clock.activeTimers[t] = true | ||
} | ||
return wasActive | ||
} | ||
|
||
func (t *simulatedTimer) shouldFire(now time.Time) bool { | ||
return !now.Before(t.deadline) | ||
} | ||
|
||
func (t *simulatedTimer) fire() { | ||
glog.V(1).Infof("Timer fired at %v", t.deadline) | ||
t.c <- t.deadline | ||
t.stopNoLock() | ||
} | ||
|
||
func (t *simulatedTimer) Stop() bool { | ||
t.clock.mu.Lock() | ||
defer t.clock.mu.Unlock() | ||
return t.stopNoLock() | ||
} | ||
|
||
// LOCK_REQUIRED t.clock.mu | ||
func (t *simulatedTimer) stopNoLock() bool { | ||
wasActive := t.active | ||
t.active = false | ||
delete(t.clock.activeTimers, t) | ||
return wasActive | ||
} | ||
|
||
func (t *simulatedTimer) C() <-chan time.Time { | ||
return t.c | ||
} |
Oops, something went wrong.