Skip to content
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
3 changes: 1 addition & 2 deletions .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,7 @@ linters-settings:

gocognit:
# Minimal code complexity to report.
# Default: 30 (but we recommend 10-20)
min-complexity: 20
min-complexity: 30

gocritic:
# Settings passed to gocritic.
Expand Down
13 changes: 13 additions & 0 deletions clock/clock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package clock

import (
"time"
)

// Clock is a time abstraction which can be used to if you need to mock time in tests.
type Clock interface {
Now() time.Time
NewTicker(d time.Duration) (<-chan time.Time, func())
NewTimer(d time.Duration) (<-chan time.Time, func() bool)
Since(t time.Time) time.Duration
}
76 changes: 30 additions & 46 deletions server/clock.go → clock/mock.go
Original file line number Diff line number Diff line change
@@ -1,67 +1,42 @@
package server
package clock

import (
"sync"
"sync/atomic"
"time"
)

// Clock is a simple abstraction to allow for time based assertions in tests.
type Clock interface {
Now() time.Time
NewTicker(d time.Duration) (<-chan time.Time, func())
NewTimer(d time.Duration) (<-chan time.Time, func() bool)
}

type RealClock struct{}

func NewClock() *RealClock {
return &RealClock{}
}

func (c *RealClock) Now() time.Time {
return time.Now()
}

func (c *RealClock) NewTicker(d time.Duration) (<-chan time.Time, func()) {
t := time.NewTicker(d)
return t.C, t.Stop
}

func (c *RealClock) NewTimer(d time.Duration) (<-chan time.Time, func() bool) {
t := time.NewTimer(d)
return t.C, t.Stop
}

type testTimer struct {
type mockTimer struct {
deadline time.Time
ch chan time.Time
stopped *atomic.Bool
}

type testTicker struct {
type mockTicker struct {
nextTick time.Time
interval time.Duration
ch chan time.Time
stopped *atomic.Bool
}

type TestClock struct {
// MockClock is a mock implementation of the Clock interface.
type MockClock struct {
mu sync.Mutex
time time.Time
timers []*testTimer
tickers []*testTicker
timers []*mockTimer
tickers []*mockTicker
}

func NewTestClock(time time.Time) *TestClock {
var c TestClock
func NewMock(time time.Time) *MockClock {
var c MockClock
c.time = time
c.timers = make([]*testTimer, 0)
c.tickers = make([]*testTicker, 0)
c.timers = make([]*mockTimer, 0)
c.tickers = make([]*mockTicker, 0)
return &c
}

func (c *TestClock) Set(t time.Time) {
// Set sets the time of the clock and triggers any timers or tickers that should fire.
func (c *MockClock) Set(t time.Time) {
c.mu.Lock()
defer c.mu.Unlock()

Expand All @@ -72,7 +47,7 @@ func (c *TestClock) Set(t time.Time) {
c.time = t
for _, ticker := range c.tickers {
if !ticker.stopped.Load() && !ticker.nextTick.Add(ticker.interval).After(c.time) {
//nolint: durationcheck // This is a test clock where we can ignore overflows.
//nolint: durationcheck // This is a test clock, we don't care about overflows.
nextTick := (c.time.Sub(ticker.nextTick) / ticker.interval) * ticker.interval
ticker.nextTick = ticker.nextTick.Add(nextTick)
select {
Expand All @@ -82,7 +57,7 @@ func (c *TestClock) Set(t time.Time) {
}
}

unfiredTimers := make([]*testTimer, 0)
unfiredTimers := make([]*mockTimer, 0)
for i, timer := range c.timers {
if timer.deadline.After(c.time) && !timer.stopped.Load() {
unfiredTimers = append(unfiredTimers, c.timers[i])
Expand All @@ -94,23 +69,26 @@ func (c *TestClock) Set(t time.Time) {
c.timers = unfiredTimers
}

func (c *TestClock) Add(d time.Duration) {
// Add advances the clock by the duration and triggers any timers or tickers that should fire.
func (c *MockClock) Add(d time.Duration) {
c.Set(c.time.Add(d))
}

func (c *TestClock) Now() time.Time {
// Now returns the current time of the clock.
func (c *MockClock) Now() time.Time {
c.mu.Lock()
defer c.mu.Unlock()
return c.time
}

func (c *TestClock) NewTicker(d time.Duration) (<-chan time.Time, func()) {
// NewTicker creates a new ticker that will fire once you advance the clock by the duration.
func (c *MockClock) NewTicker(d time.Duration) (<-chan time.Time, func()) {
c.mu.Lock()
defer c.mu.Unlock()

ch := make(chan time.Time, 1)
stopped := &atomic.Bool{}
ticker := &testTicker{nextTick: c.time, interval: d, ch: ch, stopped: stopped}
ticker := &mockTicker{nextTick: c.time, interval: d, ch: ch, stopped: stopped}
c.tickers = append(c.tickers, ticker)
stop := func() {
stopped.Store(true)
Expand All @@ -119,7 +97,8 @@ func (c *TestClock) NewTicker(d time.Duration) (<-chan time.Time, func()) {
return ch, stop
}

func (c *TestClock) NewTimer(d time.Duration) (<-chan time.Time, func() bool) {
// NewTimer creates a new timer that will fire once you advance the clock by the duration.
func (c *MockClock) NewTimer(d time.Duration) (<-chan time.Time, func() bool) {
c.mu.Lock()
defer c.mu.Unlock()

Expand All @@ -132,11 +111,16 @@ func (c *TestClock) NewTimer(d time.Duration) (<-chan time.Time, func() bool) {
return ch, func() bool { return false }
}

timer := &testTimer{deadline: c.time.Add(d), ch: ch, stopped: stopped}
timer := &mockTimer{deadline: c.time.Add(d), ch: ch, stopped: stopped}
c.timers = append(c.timers, timer)
stop := func() bool {
return stopped.CompareAndSwap(false, true)
}

return ch, stop
}

// Since calculates the time since the given time t.
func (c *MockClock) Since(t time.Time) time.Duration {
return c.Now().Sub(t)
}
32 changes: 32 additions & 0 deletions clock/real.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package clock

import "time"

// RealClock imlements the Clock interface using the standard libraries time package.
type RealClock struct{}

func New() *RealClock {
return &RealClock{}
}

// Now is a wrapper around time.Now().
func (c *RealClock) Now() time.Time {
return time.Now()
}

// NewTicker is a wrapper around time.NewTicker().
func (c *RealClock) NewTicker(d time.Duration) (<-chan time.Time, func()) {
t := time.NewTicker(d)
return t.C, t.Stop
}

// NewTimer is a wrapper around time.NewTimer().
func (c *RealClock) NewTimer(d time.Duration) (<-chan time.Time, func() bool) {
t := time.NewTimer(d)
return t.C, t.Stop
}

// Since is a wrapper around time.Since().
func (c *RealClock) Since(t time.Time) time.Duration {
return time.Since(t)
}
Loading