Skip to content
Open
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
52 changes: 30 additions & 22 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,40 @@ on:
pull_request:
branches: [master]

permissions:
contents: read

jobs:

build:
lint:
runs-on: ubuntu-latest

strategy:
matrix:
# Test on one "earliest" Go as well as the latest two major Go
# versions. If some change requires bumping the "earliest" Go versiion,
# that's fine - just include that in the commit description so that
# users are aware.
go: ["1.16.x", "1.19.x", "1.20.x"]

steps:
- uses: actions/checkout@v2

- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v3.5.2
- name: Set up Go
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0
with:
go-version-file: './go.mod'
cache-dependency-path: './go.sum'
check-latest: true
- name: golangci-lint
uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
with:
version: latest
args: -v -c .golangci.yaml

tests:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v3.5.2
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go }}

- name: Load cached dependencies
uses: actions/cache@v2
uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
go-version-file: './go.mod'
cache-dependency-path: './go.sum'
check-latest: true

- name: Test
run: go test -v -race ./...
- name: Tests
run: make test
31 changes: 31 additions & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
version: "2"
run:
modules-download-mode: readonly
allow-parallel-runners: true
linters:
default: all

disable: # all these produce failures right now
- containedctx
- exhaustruct
- funcorder
- interfacebloat
- ireturn
- nonamedreturns
- paralleltest
- testpackage
- varnamelen

settings:
gocyclo:
min-complexity: 58
govet:
enable-all: true
staticcheck:
checks:
- all
exclusions:
generated: lax
rules:
- path: (.+)\.go$
text: unused-parameter
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
test:
go test -v -race -count=1 ./...
lint:
golangci-lint run -v --fix -c .golangci.yaml ./...
63 changes: 39 additions & 24 deletions clock.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Package clock provides a mock clock for testing purposes.
package clock

import (
Expand All @@ -7,7 +8,7 @@ import (
"time"
)

// Re-export of time.Duration
// Duration is a re-export of time.Duration.
type Duration = time.Duration

// Clock represents an interface to the functions in the standard library time
Expand Down Expand Up @@ -58,11 +59,13 @@ func (c *clock) Tick(d time.Duration) <-chan time.Time {

func (c *clock) Ticker(d time.Duration) *Ticker {
t := time.NewTicker(d)

return &Ticker{C: t.C, ticker: t}
}

func (c *clock) Timer(d time.Duration) *Timer {
t := time.NewTimer(d)

return &Timer{C: t.C, timer: t}
}

Expand All @@ -74,15 +77,12 @@ func (c *clock) WithTimeout(parent context.Context, t time.Duration) (context.Co
return context.WithTimeout(parent, t)
}

// Mock represents a mock clock that only moves forward programmically.
// Mock represents a mock clock that only moves forward programmatically.
// It can be preferable to a real-time clock when testing time-based functionality.
type Mock struct {
// mu protects all other fields in this struct, and the data that they
// point to.
mu sync.Mutex

now time.Time // current time
timers clockTimers // tickers & timers
now time.Time // Current time.GUARDED_BY(mu)
timers clockTimers // tickers & timers. GUARDED_BY(mu)
mu sync.Mutex
}

// NewMock returns an instance of a mock clock.
Expand All @@ -100,10 +100,8 @@ func (m *Mock) Add(d time.Duration) {
m.mu.Unlock()

// Continue to execute timers until there are no more before the new time.
for {
if !m.runNextTimer(t) {
break
}
for m.runNextTimer(t) {

}

// Ensure that we end with the new time.
Expand All @@ -119,10 +117,8 @@ func (m *Mock) Add(d time.Duration) {
// This should only be called from a single goroutine at a time.
func (m *Mock) Set(t time.Time) {
// Continue to execute timers until there are no more before the new time.
for {
if !m.runNextTimer(t) {
break
}
for m.runNextTimer(t) {

}

// Ensure that we end with the new time.
Expand All @@ -134,13 +130,14 @@ func (m *Mock) Set(t time.Time) {
gosched()
}

// WaitForAllTimers sets the clock until all timers are expired
// WaitForAllTimers sets the clock until all timers are expired.
func (m *Mock) WaitForAllTimers() time.Time {
// Continue to execute timers until there are no more
for {
m.mu.Lock()
if len(m.timers) == 0 {
m.mu.Unlock()

return m.Now()
}

Expand All @@ -154,22 +151,24 @@ func (m *Mock) WaitForAllTimers() time.Time {
// runNextTimer executes the next timer in chronological order and moves the
// current time to the timer's next tick time. The next time is not executed if
// its next time is after the max time. Returns true if a timer was executed.
func (m *Mock) runNextTimer(max time.Time) bool {
func (m *Mock) runNextTimer(maxTime time.Time) bool {
m.mu.Lock()

// Sort timers by time.
sort.Sort(m.timers)

// If we have no more timers then exit.
// If we have no more timers, then exit.
if len(m.timers) == 0 {
m.mu.Unlock()

return false
}

// Retrieve next timer. Exit if next tick is after new time.
t := m.timers[0]
if t.Next().After(max) {
if t.Next().After(maxTime) {
m.mu.Unlock()

return false
}

Expand All @@ -180,6 +179,7 @@ func (m *Mock) runNextTimer(max time.Time) bool {

// Execute timer.
t.Tick(now)

return true
}

Expand All @@ -193,6 +193,7 @@ func (m *Mock) After(d time.Duration) <-chan time.Time {
func (m *Mock) AfterFunc(d time.Duration, f func()) *Timer {
m.mu.Lock()
defer m.mu.Unlock()

ch := make(chan time.Time, 1)
t := &Timer{
c: ch,
Expand All @@ -202,13 +203,15 @@ func (m *Mock) AfterFunc(d time.Duration, f func()) *Timer {
stopped: false,
}
m.timers = append(m.timers, (*internalTimer)(t))

return t
}

// Now returns the current wall time on the mock clock.
func (m *Mock) Now() time.Time {
m.mu.Lock()
defer m.mu.Unlock()

return m.now
}

Expand Down Expand Up @@ -238,6 +241,7 @@ func (m *Mock) Tick(d time.Duration) <-chan time.Time {
func (m *Mock) Ticker(d time.Duration) *Ticker {
m.mu.Lock()
defer m.mu.Unlock()

ch := make(chan time.Time, 1)
t := &Ticker{
C: ch,
Expand All @@ -247,6 +251,7 @@ func (m *Mock) Ticker(d time.Duration) *Ticker {
next: m.now.Add(d),
}
m.timers = append(m.timers, (*internalTicker)(t))

return t
}

Expand All @@ -265,6 +270,7 @@ func (m *Mock) Timer(d time.Duration) *Timer {
now := m.now
m.mu.Unlock()
m.runNextTimer(now)

return t
}

Expand All @@ -276,16 +282,18 @@ func (m *Mock) removeClockTimer(t clockTimer) {
copy(m.timers[i:], m.timers[i+1:])
m.timers[len(m.timers)-1] = nil
m.timers = m.timers[:len(m.timers)-1]

break
}
}

sort.Sort(m.timers)
}

// clockTimer represents an object with an associated start time.
type clockTimer interface {
Next() time.Time
Tick(time.Time)
Tick(now time.Time)
}

// clockTimers represents a list of sortable timers.
Expand Down Expand Up @@ -318,10 +326,11 @@ func (t *Timer) Stop() bool {
t.mock.removeClockTimer((*internalTimer)(t))
t.stopped = true
t.mock.mu.Unlock()

return registered
}

// Reset changes the expiry time of the timer
// Reset changes the expiry time of the timer.
func (t *Timer) Reset(d time.Duration) bool {
if t.timer != nil {
return t.timer.Reset(d)
Expand All @@ -333,11 +342,13 @@ func (t *Timer) Reset(d time.Duration) bool {
t.next = t.mock.now.Add(d)

registered := !t.stopped

if t.stopped {
t.mock.timers = append(t.mock.timers, (*internalTimer)(t))
}

t.stopped = false

return registered
}

Expand All @@ -357,6 +368,7 @@ func (t *internalTimer) Tick(now time.Time) {
if t.stopped {
return
}

if t.fn != nil {
// defer function execution until the lock is released, and
defer func() { go t.fn() }()
Expand All @@ -368,8 +380,9 @@ func (t *internalTimer) Tick(now time.Time) {
default:
}
}

t.stopped = true
t.mock.removeClockTimer((*internalTimer)(t))
t.mock.removeClockTimer(t)
}

// Ticker holds a channel that receives "ticks" at regular intervals.
Expand Down Expand Up @@ -399,6 +412,7 @@ func (t *Ticker) Stop() {
func (t *Ticker) Reset(dur time.Duration) {
if t.ticker != nil {
t.ticker.Reset(dur)

return
}

Expand Down Expand Up @@ -433,13 +447,14 @@ func (t *internalTicker) Tick(now time.Time) {
case t.c <- now:
default:
}

t.next = now.Add(t.d)
}

// Sleep momentarily so that other goroutines can process.
func gosched() { time.Sleep(1 * time.Millisecond) }

var (
// type checking
// type checking.
_ Clock = &Mock{}
)
Loading