├── .tool-versions ├── go.mod ├── pkg ├── ticker.go ├── timer.go ├── mock_clock.go ├── clock.go └── clock_test.go ├── internal ├── clock.go ├── mock │ ├── clock_timer.go │ ├── clock_timers.go │ ├── mock_context.go │ ├── ticker.go │ ├── timer.go │ ├── mock_context_test.go │ ├── mock_example_test.go │ ├── mock.go │ └── mock_test.go └── impl │ ├── ticker.go │ ├── timer.go │ └── clock.go ├── .gitignore ├── .github ├── dependabot.yml └── workflows │ ├── gosec.yml │ ├── dependency-review.yml │ └── test.yml ├── clock.go ├── .editorconfig ├── context.go ├── context_test.go ├── LICENSE ├── Makefile ├── .golangci.yml └── README.md /.tool-versions: -------------------------------------------------------------------------------- 1 | golang 1.21.6 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/itbasis/go-clock/v2 2 | 3 | go 1.21 4 | -------------------------------------------------------------------------------- /pkg/ticker.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import "time" 4 | 5 | type Ticker interface { 6 | Chan() <-chan time.Time 7 | 8 | Stop() 9 | Reset(duration time.Duration) 10 | } 11 | -------------------------------------------------------------------------------- /pkg/timer.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import "time" 4 | 5 | type Timer interface { 6 | Chan() <-chan time.Time 7 | 8 | Stop() bool 9 | Reset(duration time.Duration) bool 10 | } 11 | -------------------------------------------------------------------------------- /internal/clock.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Sleep momentarily so that other goroutines can process. 8 | func Gosched() { time.Sleep(1 * time.Millisecond) } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | tmp.* 4 | 5 | # Go 6 | bin/ 7 | junit-report.xml 8 | .coverage.out 9 | .coverage-*.out 10 | 11 | # JetBrains 12 | .idea/* 13 | !.idea/externalDependencies.xml 14 | -------------------------------------------------------------------------------- /pkg/mock_clock.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import "time" 4 | 5 | type Mock interface { 6 | Clock 7 | 8 | Add(duration time.Duration) 9 | Set(time time.Time) 10 | 11 | WaitForAllTimers() time.Time 12 | } 13 | -------------------------------------------------------------------------------- /internal/mock/clock_timer.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // clockTimer represents an object with an associated start time. 8 | type clockTicker interface { 9 | Next() time.Time 10 | Tick(time.Time) 11 | } 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: / 5 | schedule: 6 | interval: daily 7 | - package-ecosystem: github-actions 8 | directory: / 9 | schedule: 10 | interval: daily 11 | -------------------------------------------------------------------------------- /internal/mock/clock_timers.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | // ClockTimers represents a list of sortable timers. 4 | type clockTickers []clockTicker 5 | 6 | func (a clockTickers) Len() int { return len(a) } 7 | func (a clockTickers) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 8 | func (a clockTickers) Less(i, j int) bool { return a[i].Next().Before(a[j].Next()) } 9 | -------------------------------------------------------------------------------- /clock.go: -------------------------------------------------------------------------------- 1 | package clock 2 | 3 | import ( 4 | "github.com/itbasis/go-clock/v2/internal/impl" 5 | "github.com/itbasis/go-clock/v2/internal/mock" 6 | "github.com/itbasis/go-clock/v2/pkg" 7 | ) 8 | 9 | // New returns an instance of a real-time clock. 10 | func New() pkg.Clock { 11 | return impl.NewClock() 12 | } 13 | 14 | // NewMock returns an instance of a mock clock. 15 | // The current time of the mock clock on initialization is the Unix epoch. 16 | func NewMock() pkg.Mock { 17 | return mock.NewMock() 18 | } 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | max_line_length = 150 8 | indent_size = tab 9 | indent_style = tab 10 | tab_width = 4 11 | ij_smart_tabs = true 12 | 13 | [*.go] 14 | ij_go_run_go_fmt_on_reformat = true 15 | ij_go_group_stdlib_imports = true 16 | ij_go_move_all_imports_in_one_declaration = true 17 | ij_go_remove_redundant_import_aliases = true 18 | ij_go_use_back_quotes_for_imports = false 19 | ij_go_import_sorting = gofmt 20 | 21 | [*.yml] 22 | tab_width = 2 -------------------------------------------------------------------------------- /internal/impl/ticker.go: -------------------------------------------------------------------------------- 1 | package impl 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/itbasis/go-clock/v2/pkg" 7 | ) 8 | 9 | // Ticker holds a channel that receives "ticks" at regular intervals. 10 | type Ticker struct { 11 | ticker *time.Ticker // realtime impl, if set 12 | } 13 | 14 | func NewTicker(d time.Duration) pkg.Ticker { 15 | t := time.NewTicker(d) 16 | 17 | return &Ticker{ticker: t} 18 | } 19 | 20 | func (t *Ticker) Chan() <-chan time.Time { return t.ticker.C } 21 | 22 | // Stop turns off the ticker. 23 | func (t *Ticker) Stop() { t.ticker.Stop() } 24 | 25 | // Reset resets the ticker to a new duration. 26 | func (t *Ticker) Reset(dur time.Duration) { t.ticker.Reset(dur) } 27 | -------------------------------------------------------------------------------- /.github/workflows/gosec.yml: -------------------------------------------------------------------------------- 1 | name: Gosec 2 | on: 3 | push: 4 | pull_request: 5 | schedule: 6 | - cron: '0 0 * * 0' 7 | 8 | jobs: 9 | tests: 10 | runs-on: ubuntu-latest 11 | env: 12 | GO111MODULE: on 13 | steps: 14 | - name: Checkout Source 15 | uses: actions/checkout@v4 16 | - name: Run Gosec Security Scanner 17 | uses: securego/gosec@master 18 | with: 19 | args: '-no-fail -fmt sarif -out results.sarif ./...' 20 | - name: Upload SARIF file 21 | uses: github/codeql-action/upload-sarif@v3 22 | with: 23 | # Path to SARIF file relative to the root of the repository 24 | sarif_file: results.sarif 25 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package clock 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/itbasis/go-clock/v2/pkg" 7 | ) 8 | 9 | // Default holds real clock implementation 10 | var Default = New() 11 | 12 | // Used as a context key which holds clock value 13 | type ctxClock struct{} 14 | 15 | // WithContext creates child context with embedded clock implementation 16 | func WithContext(ctx context.Context, clock pkg.Clock) context.Context { 17 | return context.WithValue(ctx, ctxClock{}, clock) 18 | } 19 | 20 | // FromContext returns the implementation of clock associated with provided context. 21 | // It returns default implementation if not present. 22 | func FromContext(ctx context.Context) pkg.Clock { 23 | if ctx == nil { 24 | panic("nil context passed to Clock") 25 | } 26 | if clock, ok := ctx.Value(ctxClock{}).(pkg.Clock); ok { 27 | return clock 28 | } 29 | 30 | return Default 31 | } 32 | -------------------------------------------------------------------------------- /internal/impl/timer.go: -------------------------------------------------------------------------------- 1 | package impl 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/itbasis/go-clock/v2/pkg" 7 | ) 8 | 9 | // Timer represents a single event. 10 | // The current time will be sent on C, unless the timer was created by AfterFunc. 11 | type Timer struct { 12 | timer *time.Timer // realtime impl, if set 13 | } 14 | 15 | func NewTimer(d time.Duration) pkg.Timer { 16 | t := time.NewTimer(d) 17 | 18 | return &Timer{timer: t} 19 | } 20 | 21 | func NewTimerFunc(d time.Duration, f func()) pkg.Timer { 22 | t := time.AfterFunc(d, f) 23 | 24 | return &Timer{timer: t} 25 | } 26 | 27 | func (t *Timer) Chan() <-chan time.Time { return t.timer.C } 28 | 29 | // Stop turns off the ticker. 30 | func (t *Timer) Stop() bool { return t.timer.Stop() } 31 | 32 | // Reset changes the expiry time of the timer 33 | func (t *Timer) Reset(duration time.Duration) bool { return t.timer.Reset(duration) } 34 | -------------------------------------------------------------------------------- /context_test.go: -------------------------------------------------------------------------------- 1 | package clock_test 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/itbasis/go-clock/v2" 9 | "github.com/itbasis/go-clock/v2/pkg" 10 | ) 11 | 12 | func TestFromContext(t *testing.T) { 13 | tests := []struct { 14 | name string 15 | ctx context.Context 16 | want pkg.Clock 17 | }{ 18 | { 19 | name: "empty", 20 | ctx: context.Background(), 21 | want: clock.New(), 22 | }, 23 | { 24 | name: "real", 25 | ctx: clock.WithContext(context.Background(), clock.New()), 26 | want: clock.New(), 27 | }, 28 | { 29 | name: "mock", 30 | ctx: clock.WithContext(context.Background(), clock.NewMock()), 31 | want: clock.NewMock(), 32 | }, 33 | } 34 | for _, tt := range tests { 35 | t.Run(tt.name, func(t *testing.T) { 36 | if got := clock.FromContext(tt.ctx); !reflect.DeepEqual(got, tt.want) { 37 | t.Errorf("FromContext() = %v, want %v", got, tt.want) 38 | } 39 | }) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # This Action will scan dependency manifest files that change as part of a Pull Request, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging. 4 | # 5 | # Source repository: https://github.com/actions/dependency-review-action 6 | # Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement 7 | name: 'Dependency Review' 8 | on: [pull_request] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | dependency-review: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: 'Checkout Repository' 18 | uses: actions/checkout@v4 19 | - name: 'Dependency Review' 20 | uses: actions/dependency-review-action@v4 21 | -------------------------------------------------------------------------------- /pkg/clock.go: -------------------------------------------------------------------------------- 1 | package pkg 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | // Re-export of time.Duration 9 | type Duration = time.Duration 10 | 11 | // Clock represents an interface to the functions in the standard library time 12 | // package. Two implementations are available in the clock package. The first 13 | // is a real-time clock which simply wraps the time package's functions. The 14 | // second is a mock clock which will only change when 15 | // programmatically adjusted. 16 | type Clock interface { //nolint:interfacebloat 17 | After(d time.Duration) <-chan time.Time 18 | AfterFunc(d time.Duration, f func()) Timer 19 | Now() time.Time 20 | Since(t time.Time) time.Duration 21 | Until(t time.Time) time.Duration 22 | Sleep(d time.Duration) 23 | Tick(d time.Duration) <-chan time.Time 24 | Ticker(d time.Duration) Ticker 25 | Timer(d time.Duration) Timer 26 | WithDeadline(parent context.Context, d time.Time) (context.Context, context.CancelFunc) 27 | WithTimeout(parent context.Context, t time.Duration) (context.Context, context.CancelFunc) 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Ben Johnson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | go-dependencies: 2 | # https://asdf-vm.com/ 3 | asdf install golang || : 4 | # 5 | # https://github.com/securego/gosec 6 | go install github.com/securego/gosec/v2/cmd/gosec@latest 7 | go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest 8 | # 9 | go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest 10 | go install github.com/nunnatsa/ginkgolinter/cmd/ginkgolinter@latest 11 | # 12 | go install github.com/onsi/ginkgo/v2/ginkgo@latest 13 | # 14 | #go install github.com/vektra/mockery/v2@latest 15 | # 16 | asdf reshim golang || : 17 | # 18 | go get -u -t -v ./... || : 19 | 20 | go-generate: go-dependencies 21 | #mockery 22 | go generate ./... 23 | 24 | go-lint: go-dependencies 25 | #golangci-lint run 26 | ginkgolinter ./... 27 | go vet -vettool=$$(go env GOPATH)/bin/shadow ./... 28 | 29 | go-test: go-lint 30 | gosec ./... 31 | ginkgo -r -race --cover --coverprofile=.coverage-ginkgo.out --junit-report=junit-report.xml ./... 32 | go tool cover -func=.coverage-ginkgo.out -o=.coverage.out 33 | cat .coverage.out 34 | 35 | go-all-tests: go-dependencies go-generate go-lint go-test 36 | 37 | go-all: go-all-tests 38 | go mod tidy || : 39 | -------------------------------------------------------------------------------- /internal/impl/clock.go: -------------------------------------------------------------------------------- 1 | package impl 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/itbasis/go-clock/v2/pkg" 8 | ) 9 | 10 | // Clock implements a real-time clock by simply wrapping the time package functions. 11 | type Clock struct{} 12 | 13 | func NewClock() pkg.Clock { 14 | return &Clock{} 15 | } 16 | 17 | func (c *Clock) After(d time.Duration) <-chan time.Time { return time.After(d) } 18 | 19 | func (c *Clock) AfterFunc(d time.Duration, f func()) pkg.Timer { return NewTimerFunc(d, f) } 20 | 21 | func (c *Clock) Now() time.Time { return time.Now() } 22 | 23 | func (c *Clock) Since(t time.Time) time.Duration { return time.Since(t) } 24 | 25 | func (c *Clock) Until(t time.Time) time.Duration { return time.Until(t) } 26 | 27 | func (c *Clock) Sleep(d time.Duration) { time.Sleep(d) } 28 | 29 | func (c *Clock) Tick(d time.Duration) <-chan time.Time { 30 | return c.Ticker(d).Chan() 31 | } 32 | 33 | func (c *Clock) Ticker(d time.Duration) pkg.Ticker { return NewTicker(d) } 34 | 35 | func (c *Clock) Timer(d time.Duration) pkg.Timer { return NewTimer(d) } 36 | 37 | func (c *Clock) WithDeadline(parent context.Context, d time.Time) (context.Context, context.CancelFunc) { 38 | return context.WithDeadline(parent, d) 39 | } 40 | 41 | func (c *Clock) WithTimeout(parent context.Context, t time.Duration) (context.Context, context.CancelFunc) { 42 | return context.WithTimeout(parent, t) 43 | } 44 | -------------------------------------------------------------------------------- /internal/mock/mock_context.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | "time" 8 | 9 | "github.com/itbasis/go-clock/v2/pkg" 10 | ) 11 | 12 | // propagateCancel arranges for child to be canceled when parent is. 13 | func propagateCancel(parent context.Context, child *timerCtx) { 14 | if parent.Done() == nil { 15 | return // parent is never canceled 16 | } 17 | 18 | go func() { 19 | select { 20 | case <-parent.Done(): 21 | child.cancel(parent.Err()) 22 | case <-child.Done(): 23 | } 24 | }() 25 | } 26 | 27 | type timerCtx struct { 28 | sync.Mutex 29 | 30 | clock pkg.Clock 31 | parent context.Context //nolint:containedctx 32 | deadline time.Time 33 | done chan struct{} 34 | 35 | err error 36 | timer pkg.Timer 37 | } 38 | 39 | func (c *timerCtx) cancel(err error) { 40 | c.Lock() 41 | 42 | defer c.Unlock() 43 | 44 | if c.err != nil { 45 | return // already canceled 46 | } 47 | 48 | c.err = err 49 | close(c.done) 50 | 51 | if c.timer != nil { 52 | c.timer.Stop() 53 | c.timer = nil 54 | } 55 | } 56 | 57 | func (c *timerCtx) Deadline() (deadline time.Time, ok bool) { return c.deadline, true } 58 | 59 | func (c *timerCtx) Done() <-chan struct{} { return c.done } 60 | 61 | func (c *timerCtx) Err() error { return c.err } 62 | 63 | func (c *timerCtx) Value(key interface{}) interface{} { return c.parent.Value(key) } 64 | 65 | func (c *timerCtx) String() string { 66 | return fmt.Sprintf("clock.WithDeadline(%s [%s])", c.deadline, c.deadline.Sub(c.clock.Now())) 67 | } 68 | -------------------------------------------------------------------------------- /internal/mock/ticker.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/itbasis/go-clock/v2/internal" 7 | ) 8 | 9 | // Ticker holds a channel that receives "ticks" at regular intervals. 10 | type Ticker struct { 11 | c chan time.Time 12 | next time.Time // next tick time 13 | mock *Mock // mock clock, if set 14 | d time.Duration // time between ticks 15 | stopped bool // True if stopped, false if running 16 | } 17 | 18 | func NewTicker(c chan time.Time, m *Mock, duration time.Duration) *Ticker { 19 | return &Ticker{ 20 | c: c, 21 | mock: m, 22 | d: duration, 23 | next: m.now.Add(duration), 24 | } 25 | } 26 | 27 | func (t *Ticker) Chan() <-chan time.Time { return t.c } 28 | 29 | // Stop turns off the ticker. 30 | func (t *Ticker) Stop() { 31 | t.mock.mu.Lock() 32 | t.mock.removeClockTimer(t) 33 | t.stopped = true 34 | t.mock.mu.Unlock() 35 | } 36 | 37 | // Reset resets the ticker to a new duration. 38 | func (t *Ticker) Reset(duration time.Duration) { 39 | t.mock.mu.Lock() 40 | defer t.mock.mu.Unlock() 41 | 42 | if t.stopped { 43 | t.mock.timers = append(t.mock.timers, t) 44 | t.stopped = false 45 | } 46 | 47 | t.d = duration 48 | t.next = t.mock.now.Add(duration) 49 | } 50 | 51 | func (t *Ticker) Next() time.Time { return t.next } 52 | 53 | func (t *Ticker) Tick(now time.Time) { 54 | select { 55 | case t.c <- now: 56 | default: 57 | } 58 | 59 | t.mock.mu.Lock() 60 | t.next = now.Add(t.d) 61 | t.mock.mu.Unlock() 62 | 63 | internal.Gosched() 64 | } 65 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | test-gomod: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: asdf-vm/actions/install@v3 14 | # - run: go mod tidy && git diff --exit-code go.mod go.sum 15 | - run: go mod tidy && git diff --exit-code go.mod 16 | 17 | test-asdf: 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - name: Install asdf & tools 24 | uses: asdf-vm/actions/install@v3 25 | 26 | - run: make go-all-tests 27 | 28 | - name: Test Summary 29 | uses: test-summary/action@v2 30 | with: 31 | paths: "junit-report.xml" 32 | if: always() 33 | 34 | - name: Upload coverage reports to Codecov 35 | uses: codecov/codecov-action@v4 36 | with: 37 | token: ${{ secrets.CODECOV_TOKEN }} 38 | files: ./.coverage.out 39 | 40 | test-matrix: 41 | runs-on: ubuntu-latest 42 | 43 | strategy: 44 | matrix: 45 | go: ["1.21.x", ">=1.22.0-rc.1"] 46 | 47 | steps: 48 | - uses: actions/checkout@v4 49 | 50 | - name: Set up Go 51 | uses: actions/setup-go@v5 52 | with: 53 | go-version: ${{ matrix.go }} 54 | check-latest: true 55 | 56 | - run: make go-all-tests 57 | 58 | - name: Test Summary 59 | uses: test-summary/action@v2 60 | with: 61 | paths: "junit-report.xml" 62 | if: always() 63 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | #file: noinspection SpellCheckingInspection 2 | run: 3 | concurrency: 4 4 | 5 | # https://golangci-lint.run/usage/configuration/#linters-configuration 6 | linters: 7 | disable-all: true 8 | enable: 9 | - errcheck 10 | - gosimple 11 | - govet 12 | - ineffassign 13 | - staticcheck 14 | - unused 15 | - asasalint 16 | - bodyclose 17 | - containedctx 18 | - contextcheck 19 | - cyclop 20 | - dogsled 21 | - errname 22 | - errorlint 23 | - exhaustive 24 | - exportloopref 25 | - forbidigo 26 | - funlen 27 | - gocritic 28 | - gocyclo 29 | - goerr113 30 | - goimports 31 | - gomnd 32 | - gomoddirectives 33 | - gosec 34 | - govet 35 | - grouper 36 | - misspell 37 | - nilerr 38 | - nlreturn 39 | - noctx 40 | - prealloc 41 | - predeclared 42 | - promlinter 43 | - revive 44 | - wastedassign 45 | - tagliatelle 46 | - tenv 47 | - testpackage 48 | - typecheck 49 | - unconvert 50 | - unparam 51 | - usestdlibvars 52 | - varnamelen 53 | - whitespace 54 | - wrapcheck 55 | - wsl 56 | - ginkgolinter 57 | - interfacebloat 58 | 59 | issues: 60 | exclude-rules: 61 | - path: '(.+)_test\.go' 62 | linters: 63 | - dupl 64 | - goconst 65 | - funlen 66 | - varnamelen 67 | - revive 68 | 69 | linters-settings: 70 | varnamelen: 71 | ignore-type-assert-ok: true 72 | ignore-map-index-ok: true 73 | ignore-names: 74 | - t 75 | - f 76 | -------------------------------------------------------------------------------- /internal/mock/timer.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/itbasis/go-clock/v2/internal" 7 | ) 8 | 9 | // Timer represents a single event. 10 | // The current time will be sent on C, unless the timer was created by AfterFunc. 11 | type Timer struct { 12 | c chan time.Time 13 | next time.Time // next tick time 14 | mock *Mock // mock clock, if set 15 | fn func() // AfterFunc function, if set 16 | stopped bool // True if stopped, false if running 17 | } 18 | 19 | func NewTimer(c chan time.Time, f func(), m *Mock, d time.Duration) *Timer { 20 | return &Timer{ 21 | c: c, 22 | fn: f, 23 | mock: m, 24 | next: m.now.Add(d), 25 | } 26 | } 27 | 28 | func (t *Timer) Chan() <-chan time.Time { return t.c } 29 | 30 | // Stop turns off the ticker. 31 | func (t *Timer) Stop() bool { 32 | t.mock.mu.Lock() 33 | registered := !t.stopped 34 | t.mock.removeClockTimer(t) 35 | t.stopped = true 36 | t.mock.mu.Unlock() 37 | 38 | return registered 39 | } 40 | 41 | // Reset changes the expiry time of the timer 42 | func (t *Timer) Reset(duration time.Duration) bool { 43 | t.mock.mu.Lock() 44 | t.next = t.mock.now.Add(duration) 45 | defer t.mock.mu.Unlock() 46 | 47 | registered := !t.stopped 48 | 49 | if t.stopped { 50 | t.mock.timers = append(t.mock.timers, t) 51 | } 52 | 53 | t.stopped = false 54 | 55 | return registered 56 | } 57 | 58 | func (t *Timer) Next() time.Time { return t.next } 59 | 60 | func (t *Timer) Tick(now time.Time) { 61 | // a gosched() after ticking, to allow any consequences of the 62 | // tick to complete 63 | defer internal.Gosched() 64 | 65 | t.mock.mu.Lock() 66 | 67 | if t.fn != nil { 68 | // defer function execution until the lock is released, and 69 | defer func() { go t.fn() }() 70 | } else { 71 | t.c <- now 72 | } 73 | 74 | t.mock.removeClockTimer(t) 75 | t.stopped = true 76 | t.mock.mu.Unlock() 77 | } 78 | -------------------------------------------------------------------------------- /internal/mock/mock_context_test.go: -------------------------------------------------------------------------------- 1 | package mock_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | "time" 8 | 9 | "github.com/itbasis/go-clock/v2/internal/mock" 10 | ) 11 | 12 | // Ensure that WithDeadline is cancelled when deadline exceeded. 13 | func TestMock_WithDeadline(t *testing.T) { 14 | m := mock.NewMock() 15 | ctx, _ := m.WithDeadline(context.Background(), m.Now().Add(time.Second)) 16 | m.Add(time.Second) 17 | select { 18 | case <-ctx.Done(): 19 | if !errors.Is(ctx.Err(), context.DeadlineExceeded) { 20 | t.Error("invalid type of error returned when deadline exceeded") 21 | } 22 | default: 23 | t.Error("context is not cancelled when deadline exceeded") 24 | } 25 | } 26 | 27 | // Ensure that WithDeadline does nothing when the deadline is later than the current deadline. 28 | func TestMock_WithDeadlineLaterThanCurrent(t *testing.T) { 29 | m := mock.NewMock() 30 | ctx, _ := m.WithDeadline(context.Background(), m.Now().Add(time.Second)) 31 | ctx, _ = m.WithDeadline(ctx, m.Now().Add(10*time.Second)) 32 | m.Add(time.Second) 33 | select { 34 | case <-ctx.Done(): 35 | if !errors.Is(ctx.Err(), context.DeadlineExceeded) { 36 | t.Error("invalid type of error returned when deadline exceeded") 37 | } 38 | default: 39 | t.Error("context is not cancelled when deadline exceeded") 40 | } 41 | } 42 | 43 | // Ensure that WithDeadline cancel closes Done channel with context.Canceled error. 44 | func TestMock_WithDeadlineCancel(t *testing.T) { 45 | m := mock.NewMock() 46 | ctx, cancel := m.WithDeadline(context.Background(), m.Now().Add(time.Second)) 47 | cancel() 48 | select { 49 | case <-ctx.Done(): 50 | if !errors.Is(ctx.Err(), context.Canceled) { 51 | t.Error("invalid type of error returned after cancellation") 52 | } 53 | case <-time.After(time.Second): 54 | t.Error("context is not cancelled after cancel was called") 55 | } 56 | } 57 | 58 | // Ensure that WithDeadline closes child contexts after it was closed. 59 | func TestMock_WithDeadlineCancelledWithParent(t *testing.T) { 60 | m := mock.NewMock() 61 | parent, cancel := context.WithCancel(context.Background()) 62 | ctx, _ := m.WithDeadline(parent, m.Now().Add(time.Second)) 63 | 64 | cancel() 65 | 66 | select { 67 | case <-ctx.Done(): 68 | if !errors.Is(ctx.Err(), context.Canceled) { 69 | t.Error("invalid type of error returned after cancellation") 70 | } 71 | case <-time.After(time.Second): 72 | t.Error("context is not cancelled when parent context is cancelled") 73 | } 74 | } 75 | 76 | // Ensure that WithDeadline cancelled immediately when deadline has already passed. 77 | func TestMock_WithDeadlineImmediate(t *testing.T) { 78 | m := mock.NewMock() 79 | ctx, _ := m.WithDeadline(context.Background(), m.Now().Add(-time.Second)) 80 | select { 81 | case <-ctx.Done(): 82 | if !errors.Is(ctx.Err(), context.DeadlineExceeded) { 83 | t.Error("invalid type of error returned when deadline has already passed") 84 | } 85 | default: 86 | t.Error("context is not cancelled when deadline has already passed") 87 | } 88 | } 89 | 90 | // Ensure that WithTimeout is cancelled when deadline exceeded. 91 | func TestMock_WithTimeout(t *testing.T) { 92 | m := mock.NewMock() 93 | ctx, _ := m.WithTimeout(context.Background(), time.Second) 94 | m.Add(time.Second) 95 | select { 96 | case <-ctx.Done(): 97 | if !errors.Is(ctx.Err(), context.DeadlineExceeded) { 98 | t.Error("invalid type of error returned when time is over") 99 | } 100 | default: 101 | t.Error("context is not cancelled when time is over") 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | clock 2 | ===== 3 | 4 | [![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/mod/github.com/itbasis/go-clock) 5 | ![GitHub Release](https://img.shields.io/github/v/release/itbasis/go-clock) 6 | [![codecov](https://codecov.io/gh/itbasis/go-clock/graph/badge.svg?token=NgqYRaNbNb)](https://codecov.io/gh/itbasis/go-clock) 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/itbasis/go-clock)](https://goreportcard.com/report/github.com/itbasis/go-clock) 8 | 9 | > [!IMPORTANT] 10 | > This repository will no longer be developed - please use [clockwork](https://github.com/jonboulle/clockwork), which has similar functionality but a larger community 11 | 12 | Clock is a small library for mocking time in Go. It provides an interface 13 | around the standard library's [`time`][time] package so that the application 14 | can use the realtime clock while tests can use the mock clock. 15 | 16 | This module is no longer maintained. 17 | 18 | [time]: https://pkg.go.dev/github.com/itbasis/go-clock 19 | 20 | ## Usage 21 | 22 | ### Realtime Clock 23 | 24 | Your application can maintain a `Clock` variable that will allow realtime and 25 | mock clocks to be interchangeable. For example, if you had an `Application` type: 26 | 27 | ```go 28 | import "github.com/itbasis/go-clock/v2" 29 | 30 | type Application struct { 31 | Clock clock.Clock 32 | } 33 | ``` 34 | 35 | You could initialize it to use the realtime clock like this: 36 | 37 | ```go 38 | var app Application 39 | app.Clock = clock.New() 40 | ... 41 | ``` 42 | 43 | Then all timers and time-related functionality should be performed from the 44 | `Clock` variable. 45 | 46 | 47 | ### Mocking time 48 | 49 | In your tests, you will want to use a `Mock` clock: 50 | 51 | ```go 52 | import ( 53 | "testing" 54 | 55 | "github.com/itbasis/go-clock/v2" 56 | ) 57 | 58 | func TestApplication_DoSomething(t *testing.T) { 59 | mockClock := clock.NewMock() 60 | app := Application{Clock: mockClock} 61 | ... 62 | } 63 | ``` 64 | 65 | Now that you've initialized your application to use the mock clock, you can 66 | adjust the time programmatically. The mock clock always starts from the Unix 67 | epoch (midnight UTC on Jan 1, 1970). 68 | 69 | 70 | ### Controlling time 71 | 72 | The mock clock provides the same functions that the standard library's `time` 73 | package provides. For example, to find the current time, you use the `Now()` 74 | function: 75 | 76 | ```go 77 | mock := clock.NewMock() 78 | 79 | // Find the current time. 80 | mock.Now().UTC() // 1970-01-01 00:00:00 +0000 UTC 81 | 82 | // Move the clock forward. 83 | mock.Add(2 * time.Hour) 84 | 85 | // Check the time again. It's 2 hours later! 86 | mock.Now().UTC() // 1970-01-01 02:00:00 +0000 UTC 87 | ``` 88 | 89 | Timers and Tickers are also controlled by this same mock clock. They will only 90 | execute when the clock is moved forward: 91 | 92 | ```go 93 | mock := clock.NewMock() 94 | count := 0 95 | 96 | // Kick off a timer to increment every 1 mock second. 97 | go func() { 98 | ticker := mock.Ticker(1 * time.Second) 99 | for { 100 | <-ticker.C 101 | count++ 102 | } 103 | }() 104 | runtime.Gosched() 105 | 106 | // Move the clock forward 10 seconds. 107 | mock.Add(10 * time.Second) 108 | 109 | // This prints 10. 110 | fmt.Println(count) 111 | ``` 112 | 113 | ### Working with context 114 | 115 | It is possible to put clock into context without passing it directly to the function: 116 | 117 | ```go 118 | clock := clock.New() 119 | ctx := clock.WithContext(context.Background(), clock) 120 | do(ctx) 121 | 122 | func do(ctx context.Context) { 123 | clock := clock.FromContext(ctx) 124 | // do some staff.. 125 | } 126 | 127 | ``` 128 | -------------------------------------------------------------------------------- /internal/mock/mock_example_test.go: -------------------------------------------------------------------------------- 1 | package mock_test 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/itbasis/go-clock/v2/internal" 8 | "github.com/itbasis/go-clock/v2/internal/mock" 9 | ) 10 | 11 | func ExampleMock_After() { 12 | var ( // Create a new mock clock. 13 | clock = mock.NewMock() 14 | count counter 15 | ) 16 | 17 | ready := make(chan struct{}) 18 | // Create a channel to execute after 10 mock seconds. 19 | go func() { 20 | ch := clock.After(10 * time.Second) 21 | 22 | close(ready) 23 | 24 | <-ch 25 | count.incr() 26 | }() 27 | <-ready 28 | 29 | // Print the starting value. 30 | fmt.Printf("%s: %d\n", clock.Now().UTC(), count.get()) 31 | 32 | // Move the clock forward 5 seconds and print the value again. 33 | clock.Add(5 * time.Second) 34 | fmt.Printf("%s: %d\n", clock.Now().UTC(), count.get()) 35 | 36 | // Move the clock forward 5 seconds to the tick time and check the value. 37 | clock.Add(5 * time.Second) 38 | fmt.Printf("%s: %d\n", clock.Now().UTC(), count.get()) 39 | 40 | // Output: 41 | // 1970-01-01 00:00:00 +0000 UTC: 0 42 | // 1970-01-01 00:00:05 +0000 UTC: 0 43 | // 1970-01-01 00:00:10 +0000 UTC: 1 44 | } 45 | 46 | func ExampleMock_AfterFunc() { 47 | var ( 48 | // Create a new mock clock. 49 | clock = mock.NewMock() 50 | count counter 51 | ) 52 | 53 | count.incr() 54 | 55 | // Execute a function after 10 mock seconds. 56 | clock.AfterFunc( 57 | 10*time.Second, func() { 58 | count.incr() 59 | }, 60 | ) 61 | internal.Gosched() 62 | 63 | // Print the starting value. 64 | fmt.Printf("%s: %d\n", clock.Now().UTC(), count.get()) 65 | 66 | // Move the clock forward 10 seconds and print the new value. 67 | clock.Add(10 * time.Second) 68 | fmt.Printf("%s: %d\n", clock.Now().UTC(), count.get()) 69 | 70 | // Output: 71 | // 1970-01-01 00:00:00 +0000 UTC: 1 72 | // 1970-01-01 00:00:10 +0000 UTC: 2 73 | } 74 | 75 | func ExampleMock_Sleep() { 76 | var ( 77 | // Create a new mock clock. 78 | clock = mock.NewMock() 79 | count counter 80 | ) 81 | 82 | // Execute a function after 10 mock seconds. 83 | go func() { 84 | clock.Sleep(10 * time.Second) 85 | count.incr() 86 | }() 87 | 88 | internal.Gosched() 89 | 90 | // Print the starting value. 91 | fmt.Printf("%s: %d\n", clock.Now().UTC(), count.get()) 92 | 93 | // Move the clock forward 10 seconds and print the new value. 94 | clock.Add(10 * time.Second) 95 | fmt.Printf("%s: %d\n", clock.Now().UTC(), count.get()) 96 | 97 | // Output: 98 | // 1970-01-01 00:00:00 +0000 UTC: 0 99 | // 1970-01-01 00:00:10 +0000 UTC: 1 100 | } 101 | 102 | func ExampleMock_Ticker() { 103 | var ( 104 | // Create a new mock clock. 105 | clock = mock.NewMock() 106 | count counter 107 | ) 108 | 109 | ready := make(chan struct{}) 110 | // Increment count every mock second. 111 | go func() { 112 | ticker := clock.Ticker(1 * time.Second) 113 | 114 | close(ready) 115 | 116 | for { 117 | <-ticker.Chan() 118 | count.incr() 119 | } 120 | }() 121 | <-ready 122 | 123 | // Move the clock forward 10 seconds and print the new value. 124 | clock.Add(10 * time.Second) 125 | fmt.Printf("Count is %d after 10 seconds\n", count.get()) 126 | 127 | // Move the clock forward 5 more seconds and print the new value. 128 | clock.Add(5 * time.Second) 129 | fmt.Printf("Count is %d after 15 seconds\n", count.get()) 130 | 131 | // Output: 132 | // Count is 10 after 10 seconds 133 | // Count is 15 after 15 seconds 134 | } 135 | 136 | func ExampleMock_Timer() { 137 | var ( // Create a new mock clock. 138 | clock = mock.NewMock() 139 | count counter 140 | ) 141 | 142 | ready := make(chan struct{}) 143 | // Increment count after a mock second. 144 | go func() { 145 | timer := clock.Timer(1 * time.Second) 146 | 147 | close(ready) 148 | 149 | <-timer.Chan() 150 | count.incr() 151 | }() 152 | <-ready 153 | 154 | // Move the clock forward 10 seconds and print the new value. 155 | clock.Add(10 * time.Second) 156 | fmt.Printf("Count is %d after 10 seconds\n", count.get()) 157 | 158 | // Output: 159 | // Count is 1 after 10 seconds 160 | } 161 | -------------------------------------------------------------------------------- /pkg/clock_test.go: -------------------------------------------------------------------------------- 1 | package pkg_test 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | "time" 7 | 8 | "github.com/itbasis/go-clock/v2/internal/impl" 9 | "github.com/itbasis/go-clock/v2/internal/mock" 10 | ) 11 | 12 | // Ensure that the clock's After channel sends at the correct time. 13 | func TestClock_After(t *testing.T) { 14 | start := time.Now() 15 | <-impl.NewClock().After(20 * time.Millisecond) 16 | dur := time.Since(start) 17 | 18 | if dur < 20*time.Millisecond || dur > 40*time.Millisecond { 19 | t.Fatalf("Bad duration: %s", dur) 20 | } 21 | } 22 | 23 | // Ensure that the clock's AfterFunc executes at the correct time. 24 | func TestClock_AfterFunc(t *testing.T) { 25 | var ( 26 | ok bool 27 | wg sync.WaitGroup 28 | ) 29 | 30 | wg.Add(1) 31 | 32 | start := time.Now() 33 | 34 | impl.NewClock().AfterFunc( 35 | 20*time.Millisecond, func() { 36 | ok = true 37 | wg.Done() 38 | }, 39 | ) 40 | wg.Wait() 41 | 42 | dur := time.Since(start) 43 | 44 | if dur < 20*time.Millisecond || dur > 40*time.Millisecond { 45 | t.Fatalf("Bad duration: %s", dur) 46 | } 47 | 48 | if !ok { 49 | t.Fatal("Function did not run") 50 | } 51 | } 52 | 53 | // Ensure that the clock's time matches the standary library. 54 | func TestClock_Now(t *testing.T) { 55 | a := time.Now().Round(time.Second) 56 | b := impl.NewClock().Now().Round(time.Second) 57 | 58 | if !a.Equal(b) { 59 | t.Errorf("not equal: %s != %s", a, b) 60 | } 61 | } 62 | 63 | // Ensure that the clock sleeps for the appropriate amount of time. 64 | func TestClock_Sleep(t *testing.T) { 65 | start := time.Now() 66 | impl.NewClock().Sleep(20 * time.Millisecond) 67 | dur := time.Since(start) 68 | 69 | if dur < 20*time.Millisecond || dur > 40*time.Millisecond { 70 | t.Fatalf("Bad duration: %s", dur) 71 | } 72 | } 73 | 74 | // Ensure that the clock ticks correctly. 75 | func TestClock_Tick(t *testing.T) { 76 | start := time.Now() 77 | c := impl.NewClock().Tick(20 * time.Millisecond) 78 | <-c 79 | <-c 80 | 81 | dur := time.Since(start) 82 | 83 | if dur < 20*time.Millisecond || dur > 50*time.Millisecond { 84 | t.Fatalf("Bad duration: %s", dur) 85 | } 86 | } 87 | 88 | // Ensure that the clock's ticker ticks correctly. 89 | func TestClock_Ticker(t *testing.T) { 90 | start := time.Now() 91 | ticker := impl.NewClock().Ticker(50 * time.Millisecond) 92 | <-ticker.Chan() 93 | <-ticker.Chan() 94 | 95 | dur := time.Since(start) 96 | 97 | if dur < 100*time.Millisecond || dur > 200*time.Millisecond { 98 | t.Fatalf("Bad duration: %s", dur) 99 | } 100 | } 101 | 102 | // Ensure that the clock's ticker can stop correctly. 103 | func TestClock_Ticker_Stp(t *testing.T) { 104 | ticker := impl.NewClock().Ticker(20 * time.Millisecond) 105 | <-ticker.Chan() 106 | ticker.Stop() 107 | select { 108 | case <-ticker.Chan(): 109 | t.Fatal("unexpected send") 110 | case <-time.After(30 * time.Millisecond): 111 | } 112 | } 113 | 114 | // Ensure that the clock's ticker can reset correctly. 115 | func TestClock_Ticker_Rst(t *testing.T) { 116 | start := time.Now() 117 | ticker := impl.NewClock().Ticker(20 * time.Millisecond) 118 | <-ticker.Chan() 119 | ticker.Reset(5 * time.Millisecond) 120 | <-ticker.Chan() 121 | 122 | dur := time.Since(start) 123 | 124 | if dur >= 30*time.Millisecond { 125 | t.Fatal("took more than 30ms") 126 | } 127 | 128 | ticker.Stop() 129 | } 130 | 131 | // Ensure that the clock's ticker can stop and then be reset correctly. 132 | func TestClock_Ticker_Stop_Rst(t *testing.T) { 133 | start := time.Now() 134 | ticker := impl.NewClock().Ticker(20 * time.Millisecond) 135 | <-ticker.Chan() 136 | ticker.Stop() 137 | 138 | select { 139 | case <-ticker.Chan(): 140 | t.Fatal("unexpected send") 141 | case <-time.After(30 * time.Millisecond): 142 | } 143 | ticker.Reset(5 * time.Millisecond) 144 | <-ticker.Chan() 145 | 146 | dur := time.Since(start) 147 | 148 | if dur >= 60*time.Millisecond { 149 | t.Fatal("took more than 60ms") 150 | } 151 | 152 | ticker.Stop() 153 | } 154 | 155 | // Ensure that the clock's timer waits correctly. 156 | func TestClock_Timer(t *testing.T) { 157 | start := time.Now() 158 | timer := impl.NewClock().Timer(20 * time.Millisecond) 159 | <-timer.Chan() 160 | 161 | dur := time.Since(start) 162 | 163 | if dur < 20*time.Millisecond || dur > 40*time.Millisecond { 164 | t.Fatalf("Bad duration: %s", dur) 165 | } 166 | 167 | if timer.Stop() { 168 | t.Fatal("timer still running") 169 | } 170 | } 171 | 172 | // Ensure that the clock's timer can be stopped. 173 | func TestClock_Timer_Stop(t *testing.T) { 174 | timer := impl.NewClock().Timer(20 * time.Millisecond) 175 | 176 | if !timer.Stop() { 177 | t.Fatal("timer not running") 178 | } 179 | 180 | if timer.Stop() { 181 | t.Fatal("timer wasn't cancelled") 182 | } 183 | 184 | select { 185 | case <-timer.Chan(): 186 | t.Fatal("unexpected send") 187 | case <-time.After(30 * time.Millisecond): 188 | } 189 | } 190 | 191 | // Ensure that the clock's timer can be reset. 192 | func TestClock_Timer_Reset(t *testing.T) { 193 | start := time.Now() 194 | timer := impl.NewClock().Timer(10 * time.Millisecond) 195 | 196 | if !timer.Reset(20 * time.Millisecond) { 197 | t.Fatal("timer not running") 198 | } 199 | 200 | <-timer.Chan() 201 | 202 | dur := time.Since(start) 203 | 204 | if dur < 20*time.Millisecond || dur > 40*time.Millisecond { 205 | t.Fatalf("Bad duration: %s", dur) 206 | } 207 | } 208 | 209 | func TestClock_NegativeDuration(t *testing.T) { 210 | clock := mock.NewMock() 211 | timer := clock.Timer(-time.Second) 212 | select { 213 | case <-timer.Chan(): 214 | default: 215 | t.Fatal("timer should have fired immediately") 216 | } 217 | } 218 | 219 | // Ensure reset can be called immediately after reading channel 220 | func TestClock_Timer_Reset_Unlock(t *testing.T) { 221 | var ( 222 | clock = mock.NewMock() 223 | timer = clock.Timer(1 * time.Second) 224 | wg sync.WaitGroup 225 | ) 226 | 227 | wg.Add(1) 228 | 229 | go func() { 230 | defer wg.Done() 231 | 232 | select { //nolint:gosimple 233 | case <-timer.Chan(): 234 | println("case reset") 235 | timer.Reset(1 * time.Second) 236 | } 237 | 238 | select { //nolint:gosimple 239 | case <-timer.Chan(): 240 | println("case read") 241 | } 242 | }() 243 | 244 | clock.Add(2 * time.Second) 245 | wg.Wait() 246 | } 247 | -------------------------------------------------------------------------------- /internal/mock/mock.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "context" 5 | "sort" 6 | "sync" 7 | "time" 8 | 9 | "github.com/itbasis/go-clock/v2/internal" 10 | "github.com/itbasis/go-clock/v2/pkg" 11 | ) 12 | 13 | // Mock represents a mock clock that only moves forward programmically. 14 | // It can be preferable to a real-time clock when testing time-based functionality. 15 | type Mock struct { 16 | // mu protects all other fields in this struct, and the data that they 17 | // point to. 18 | mu sync.Mutex 19 | 20 | now time.Time // current time 21 | timers clockTickers // tickers & timers 22 | } 23 | 24 | // NewMock returns an instance of a mock clock. 25 | // The current time of the mock clock on initialization is the Unix epoch. 26 | func NewMock() *Mock { 27 | return &Mock{now: time.Unix(0, 0)} 28 | } 29 | 30 | // After waits for the duration to elapse and then sends the current time on the returned channel. 31 | func (m *Mock) After(d time.Duration) <-chan time.Time { 32 | return m.Timer(d).Chan() 33 | } 34 | 35 | // AfterFunc waits for the duration to elapse and then executes a function in its own goroutine. 36 | // A Timer is returned that can be stopped. 37 | func (m *Mock) AfterFunc(duration time.Duration, f func()) pkg.Timer { 38 | m.mu.Lock() 39 | 40 | defer m.mu.Unlock() 41 | 42 | ch := make(chan time.Time, 1) 43 | 44 | timer := NewTimer(ch, f, m, duration) 45 | 46 | m.timers = append(m.timers, timer) 47 | 48 | return timer 49 | } 50 | 51 | // Now returns the current wall time on the mock clock. 52 | func (m *Mock) Now() time.Time { 53 | m.mu.Lock() 54 | defer m.mu.Unlock() 55 | 56 | return m.now 57 | } 58 | 59 | // Since returns time since `t` using the mock clock's wall time. 60 | func (m *Mock) Since(t time.Time) time.Duration { 61 | return m.Now().Sub(t) 62 | } 63 | 64 | // Until returns time until `t` using the mock clock's wall time. 65 | func (m *Mock) Until(t time.Time) time.Duration { 66 | return t.Sub(m.Now()) 67 | } 68 | 69 | // Sleep pauses the goroutine for the given duration on the mock clock. 70 | // The clock must be moved forward in a separate goroutine. 71 | func (m *Mock) Sleep(d time.Duration) { 72 | <-m.After(d) 73 | } 74 | 75 | // Tick is a convenience function for Ticker(). 76 | // It will return a ticker channel that cannot be stopped. 77 | func (m *Mock) Tick(d time.Duration) <-chan time.Time { return m.Ticker(d).Chan() } 78 | 79 | // Ticker creates a new instance of Ticker. 80 | func (m *Mock) Ticker(duration time.Duration) pkg.Ticker { 81 | m.mu.Lock() 82 | 83 | defer m.mu.Unlock() 84 | 85 | ch := make(chan time.Time, 1) 86 | 87 | ticker := NewTicker(ch, m, duration) 88 | 89 | m.timers = append(m.timers, ticker) 90 | 91 | return ticker 92 | } 93 | 94 | // Timer creates a new instance of Timer. 95 | func (m *Mock) Timer(duration time.Duration) pkg.Timer { 96 | m.mu.Lock() 97 | ch := make(chan time.Time, 1) 98 | 99 | timer := NewTimer(ch, nil, m, duration) 100 | 101 | m.timers = append(m.timers, timer) 102 | now := m.now 103 | m.mu.Unlock() 104 | m.runNextTimer(now) 105 | 106 | return timer 107 | } 108 | 109 | func (m *Mock) WithTimeout(parent context.Context, timeout time.Duration) (context.Context, context.CancelFunc) { 110 | return m.WithDeadline(parent, m.Now().Add(timeout)) 111 | } 112 | 113 | func (m *Mock) WithDeadline(parent context.Context, deadline time.Time) (context.Context, context.CancelFunc) { 114 | if cur, ok := parent.Deadline(); ok && cur.Before(deadline) { 115 | // The current deadline is already sooner than the new one. 116 | return context.WithCancel(parent) 117 | } 118 | 119 | ctx := &timerCtx{clock: m, parent: parent, deadline: deadline, done: make(chan struct{})} 120 | propagateCancel(parent, ctx) 121 | 122 | dur := m.Until(deadline) 123 | 124 | if dur <= 0 { 125 | ctx.cancel(context.DeadlineExceeded) // deadline has already passed 126 | 127 | return ctx, func() {} 128 | } 129 | 130 | ctx.Lock() 131 | defer ctx.Unlock() 132 | 133 | if ctx.err == nil { 134 | ctx.timer = m.AfterFunc( 135 | dur, func() { 136 | ctx.cancel(context.DeadlineExceeded) 137 | }, 138 | ) 139 | } 140 | 141 | return ctx, func() { ctx.cancel(context.Canceled) } 142 | } 143 | 144 | // Add moves the current time of the mock clock forward by the specified duration. 145 | // This should only be called from a single goroutine at a time. 146 | func (m *Mock) Add(duration time.Duration) { 147 | // Calculate the final current time. 148 | m.mu.Lock() 149 | t := m.now.Add(duration) 150 | m.mu.Unlock() 151 | 152 | // Continue to execute timers until there are no more before the new time. 153 | for { 154 | if !m.runNextTimer(t) { 155 | break 156 | } 157 | } 158 | 159 | // Ensure that we end with the new time. 160 | m.mu.Lock() 161 | m.now = t 162 | m.mu.Unlock() 163 | 164 | // Give a small buffer to make sure that other goroutines get handled. 165 | internal.Gosched() 166 | } 167 | 168 | // Set sets the current time of the mock clock to a specific one. 169 | // This should only be called from a single goroutine at a time. 170 | func (m *Mock) Set(time time.Time) { 171 | // Continue to execute timers until there are no more before the new time. 172 | for { 173 | if !m.runNextTimer(time) { 174 | break 175 | } 176 | } 177 | 178 | // Ensure that we end with the new time. 179 | m.mu.Lock() 180 | m.now = time 181 | m.mu.Unlock() 182 | 183 | // Give a small buffer to make sure that other goroutines get handled. 184 | internal.Gosched() 185 | } 186 | 187 | // WaitForAllTimers sets the clock until all timers are expired 188 | func (m *Mock) WaitForAllTimers() time.Time { 189 | // Continue to execute timers until there are no more 190 | for { 191 | m.mu.Lock() 192 | if len(m.timers) == 0 { 193 | m.mu.Unlock() 194 | 195 | return m.Now() 196 | } 197 | 198 | sort.Sort(m.timers) 199 | next := m.timers[len(m.timers)-1].Next() 200 | m.mu.Unlock() 201 | m.Set(next) 202 | } 203 | } 204 | 205 | // runNextTimer executes the next timer in chronological order and moves the 206 | // current time to the timer's next tick time. The next time is not executed if 207 | // its next time is after the max time. Returns true if a timer was executed. 208 | func (m *Mock) runNextTimer(max time.Time) bool { 209 | m.mu.Lock() 210 | 211 | // Sort timers by time. 212 | sort.Sort(m.timers) 213 | 214 | // If we have no more timers then exit. 215 | if len(m.timers) == 0 { 216 | m.mu.Unlock() 217 | 218 | return false 219 | } 220 | 221 | // Retrieve next timer. Exit if next tick is after new time. 222 | t := m.timers[0] 223 | if t.Next().After(max) { 224 | m.mu.Unlock() 225 | 226 | return false 227 | } 228 | 229 | // Move "now" forward and unlock clock. 230 | m.now = t.Next() 231 | now := m.now 232 | m.mu.Unlock() 233 | 234 | // Execute timer. 235 | t.Tick(now) 236 | 237 | return true 238 | } 239 | 240 | // removeClockTimer removes a timer from m.timers. m.mu MUST be held 241 | // when this method is called. 242 | func (m *Mock) removeClockTimer(t clockTicker) { 243 | for i, timer := range m.timers { 244 | if timer == t { 245 | copy(m.timers[i:], m.timers[i+1:]) 246 | m.timers[len(m.timers)-1] = nil 247 | m.timers = m.timers[:len(m.timers)-1] 248 | 249 | break 250 | } 251 | } 252 | 253 | sort.Sort(m.timers) 254 | } 255 | -------------------------------------------------------------------------------- /internal/mock/mock_test.go: -------------------------------------------------------------------------------- 1 | package mock_test 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "sync/atomic" 7 | "testing" 8 | "time" 9 | 10 | "github.com/itbasis/go-clock/v2/internal" 11 | "github.com/itbasis/go-clock/v2/internal/mock" 12 | ) 13 | 14 | // Counter is an atomic uint32 that can be incremented easily. It's 15 | // useful for asserting things have happened in tests. 16 | type counter struct { 17 | count uint32 18 | } 19 | 20 | func (c *counter) incr() { 21 | atomic.AddUint32(&c.count, 1) 22 | } 23 | 24 | func (c *counter) get() uint32 { 25 | return atomic.LoadUint32(&c.count) 26 | } 27 | 28 | // Ensure that the mock's After channel sends at the correct time. 29 | func TestMock_After(t *testing.T) { 30 | var ( 31 | ok int32 32 | clock = mock.NewMock() 33 | ) 34 | 35 | // Create a channel to execute after 10 mock seconds. 36 | ch := clock.After(10 * time.Second) 37 | 38 | go func(ch <-chan time.Time) { 39 | <-ch 40 | atomic.StoreInt32(&ok, 1) 41 | }(ch) 42 | 43 | // Move clock forward to just before the time. 44 | clock.Add(9 * time.Second) 45 | 46 | if atomic.LoadInt32(&ok) == 1 { 47 | t.Fatal("too early") 48 | } 49 | 50 | // Move clock forward to the after channel's time. 51 | clock.Add(1 * time.Second) 52 | 53 | if atomic.LoadInt32(&ok) == 0 { 54 | t.Fatal("too late") 55 | } 56 | } 57 | 58 | // Ensure that the mock's After channel doesn't block on write. 59 | func TestMock_UnusedAfter(t *testing.T) { 60 | mock := mock.NewMock() 61 | mock.After(1 * time.Millisecond) 62 | 63 | done := make(chan bool, 1) 64 | go func() { 65 | mock.Add(1 * time.Second) 66 | done <- true 67 | }() 68 | 69 | select { 70 | case <-done: 71 | case <-time.After(1 * time.Second): 72 | t.Fatal("mock.Add hung") 73 | } 74 | } 75 | 76 | // Ensure that the mock's AfterFunc executes at the correct time. 77 | func TestMock_AfterFunc(t *testing.T) { 78 | var ( 79 | ok int32 80 | clock = mock.NewMock() 81 | ) 82 | 83 | // Execute function after duration. 84 | clock.AfterFunc( 85 | 10*time.Second, func() { 86 | atomic.StoreInt32(&ok, 1) 87 | }, 88 | ) 89 | 90 | // Move clock forward to just before the time. 91 | clock.Add(9 * time.Second) 92 | 93 | if atomic.LoadInt32(&ok) == 1 { 94 | t.Fatal("too early") 95 | } 96 | 97 | // Move clock forward to the after channel's time. 98 | clock.Add(1 * time.Second) 99 | 100 | if atomic.LoadInt32(&ok) == 0 { 101 | t.Fatal("too late") 102 | } 103 | } 104 | 105 | // Ensure that the mock's AfterFunc doesn't execute if stopped. 106 | func TestMock_AfterFunc_Stop(t *testing.T) { 107 | // Execute function after duration. 108 | clock := mock.NewMock() 109 | timer := clock.AfterFunc( 110 | 10*time.Second, func() { 111 | t.Fatal("unexpected function execution") 112 | }, 113 | ) 114 | 115 | internal.Gosched() 116 | 117 | // Stop timer & move clock forward. 118 | timer.Stop() 119 | clock.Add(10 * time.Second) 120 | internal.Gosched() 121 | } 122 | 123 | // Ensure that the mock's current time can be changed. 124 | func TestMock_Now(t *testing.T) { 125 | clock := mock.NewMock() 126 | 127 | if now := clock.Now(); !now.Equal(time.Unix(0, 0)) { 128 | t.Fatalf("expected epoch, got: %v", now) 129 | } 130 | 131 | // Add 10 seconds and check the time. 132 | clock.Add(10 * time.Second) 133 | 134 | if now := clock.Now(); !now.Equal(time.Unix(10, 0)) { 135 | t.Fatalf("expected epoch, got: %v", now) 136 | } 137 | } 138 | 139 | func TestMock_Since(t *testing.T) { 140 | clock := mock.NewMock() 141 | 142 | beginning := clock.Now() 143 | clock.Add(500 * time.Second) 144 | 145 | if since := clock.Since(beginning); since.Seconds() != 500 { 146 | t.Fatalf("expected 500 since beginning, actually: %v", since.Seconds()) 147 | } 148 | } 149 | 150 | func TestMock_Until(t *testing.T) { 151 | clock := mock.NewMock() 152 | 153 | end := clock.Now().Add(500 * time.Second) 154 | if dur := clock.Until(end); dur.Seconds() != 500 { 155 | t.Fatalf("expected 500s duration between `clock` and `end`, actually: %v", dur.Seconds()) 156 | } 157 | 158 | clock.Add(100 * time.Second) 159 | 160 | if dur := clock.Until(end); dur.Seconds() != 400 { 161 | t.Fatalf("expected 400s duration between `clock` and `end`, actually: %v", dur.Seconds()) 162 | } 163 | } 164 | 165 | // Ensure that the mock can sleep for the correct time. 166 | func TestMock_Sleep(t *testing.T) { 167 | var ( 168 | ok int32 169 | clock = mock.NewMock() 170 | ) 171 | 172 | // Create a channel to execute after 10 mock seconds. 173 | go func() { 174 | clock.Sleep(10 * time.Second) 175 | atomic.StoreInt32(&ok, 1) 176 | }() 177 | 178 | internal.Gosched() 179 | 180 | // Move clock forward to just before the sleep duration. 181 | clock.Add(9 * time.Second) 182 | 183 | if atomic.LoadInt32(&ok) == 1 { 184 | t.Fatal("too early") 185 | } 186 | 187 | // Move clock forward to after the sleep duration. 188 | clock.Add(1 * time.Second) 189 | 190 | if atomic.LoadInt32(&ok) == 0 { 191 | t.Fatal("too late") 192 | } 193 | } 194 | 195 | // Ensure that the mock's Tick channel sends at the correct time. 196 | func TestMock_Tick(t *testing.T) { 197 | var ( 198 | n int32 199 | clock = mock.NewMock() 200 | ) 201 | 202 | // Create a channel to increment every 10 seconds. 203 | go func() { 204 | tick := clock.Tick(10 * time.Second) 205 | 206 | for { 207 | <-tick 208 | atomic.AddInt32(&n, 1) 209 | } 210 | }() 211 | 212 | internal.Gosched() 213 | 214 | // Move clock forward to just before the first tick. 215 | clock.Add(9 * time.Second) 216 | 217 | if atomic.LoadInt32(&n) != 0 { 218 | t.Fatalf("expected 0, got %d", n) 219 | } 220 | 221 | // Move clock forward to the start of the first tick. 222 | clock.Add(1 * time.Second) 223 | 224 | if atomic.LoadInt32(&n) != 1 { 225 | t.Fatalf("expected 1, got %d", n) 226 | } 227 | 228 | // Move clock forward over several ticks. 229 | clock.Add(30 * time.Second) 230 | 231 | if atomic.LoadInt32(&n) != 4 { 232 | t.Fatalf("expected 4, got %d", n) 233 | } 234 | } 235 | 236 | // Ensure that the mock's Ticker channel sends at the correct time. 237 | func TestMock_Ticker(t *testing.T) { 238 | var ( 239 | n int32 240 | clock = mock.NewMock() 241 | ) 242 | 243 | // Create a channel to increment every microsecond. 244 | go func() { 245 | ticker := clock.Ticker(1 * time.Microsecond) 246 | 247 | for { 248 | <-ticker.Chan() 249 | atomic.AddInt32(&n, 1) 250 | } 251 | }() 252 | 253 | internal.Gosched() 254 | 255 | // Move clock forward. 256 | clock.Add(10 * time.Microsecond) 257 | 258 | if atomic.LoadInt32(&n) != 10 { 259 | t.Fatalf("unexpected: %d", n) 260 | } 261 | } 262 | 263 | // Ensure that the mock's Ticker channel won't block if not read from. 264 | func TestMock_Ticker_Overflow(t *testing.T) { 265 | clock := mock.NewMock() 266 | ticker := clock.Ticker(1 * time.Microsecond) 267 | 268 | clock.Add(10 * time.Microsecond) 269 | ticker.Stop() 270 | } 271 | 272 | // Ensure that the mock's Ticker can be stopped. 273 | func TestMock_Ticker_Stop(t *testing.T) { 274 | var ( 275 | n int32 276 | clock = mock.NewMock() 277 | ) 278 | 279 | // Create a channel to increment every second. 280 | ticker := clock.Ticker(1 * time.Second) 281 | 282 | go func() { 283 | for { 284 | <-ticker.Chan() 285 | atomic.AddInt32(&n, 1) 286 | } 287 | }() 288 | 289 | internal.Gosched() 290 | 291 | // Move clock forward. 292 | clock.Add(5 * time.Second) 293 | 294 | if atomic.LoadInt32(&n) != 5 { 295 | t.Fatalf("expected 5, got: %d", n) 296 | } 297 | 298 | ticker.Stop() 299 | 300 | // Move clock forward again. 301 | clock.Add(5 * time.Second) 302 | 303 | if atomic.LoadInt32(&n) != 5 { 304 | t.Fatalf("still expected 5, got: %d", n) 305 | } 306 | } 307 | 308 | func TestMock_Ticker_Reset(t *testing.T) { 309 | var ( 310 | n int32 311 | clock = mock.NewMock() 312 | ) 313 | 314 | ticker := clock.Ticker(5 * time.Second) 315 | defer ticker.Stop() 316 | 317 | go func() { 318 | for { 319 | <-ticker.Chan() 320 | atomic.AddInt32(&n, 1) 321 | } 322 | }() 323 | internal.Gosched() 324 | 325 | // Move clock forward. 326 | clock.Add(10 * time.Second) 327 | 328 | if atomic.LoadInt32(&n) != 2 { 329 | t.Fatalf("expected 2, got: %d", n) 330 | } 331 | 332 | clock.Add(4 * time.Second) 333 | ticker.Reset(5 * time.Second) 334 | 335 | // Advance the remaining second 336 | clock.Add(1 * time.Second) 337 | 338 | if atomic.LoadInt32(&n) != 2 { 339 | t.Fatalf("expected 2, got: %d", n) 340 | } 341 | 342 | // Advance the remaining 4 seconds from the previous tick 343 | clock.Add(4 * time.Second) 344 | 345 | if atomic.LoadInt32(&n) != 3 { 346 | t.Fatalf("expected 3, got: %d", n) 347 | } 348 | } 349 | 350 | func TestMock_Ticker_Stop_Reset(t *testing.T) { 351 | var ( 352 | n int32 353 | clock = mock.NewMock() 354 | ) 355 | 356 | ticker := clock.Ticker(5 * time.Second) 357 | defer ticker.Stop() 358 | 359 | go func() { 360 | for { 361 | <-ticker.Chan() 362 | atomic.AddInt32(&n, 1) 363 | } 364 | }() 365 | internal.Gosched() 366 | 367 | // Move clock forward. 368 | clock.Add(10 * time.Second) 369 | 370 | if atomic.LoadInt32(&n) != 2 { 371 | t.Fatalf("expected 2, got: %d", n) 372 | } 373 | 374 | ticker.Stop() 375 | 376 | // Move clock forward again. 377 | clock.Add(5 * time.Second) 378 | 379 | if atomic.LoadInt32(&n) != 2 { 380 | t.Fatalf("still expected 2, got: %d", n) 381 | } 382 | 383 | ticker.Reset(2 * time.Second) 384 | 385 | // Advance the remaining 2 seconds 386 | clock.Add(2 * time.Second) 387 | 388 | if atomic.LoadInt32(&n) != 3 { 389 | t.Fatalf("expected 3, got: %d", n) 390 | } 391 | 392 | // Advance another 2 seconds 393 | clock.Add(2 * time.Second) 394 | 395 | if atomic.LoadInt32(&n) != 4 { 396 | t.Fatalf("expected 4, got: %d", n) 397 | } 398 | } 399 | 400 | // Ensure that multiple tickers can be used together. 401 | func TestMock_Ticker_Multi(t *testing.T) { 402 | var ( 403 | n int32 404 | clock = mock.NewMock() 405 | ) 406 | 407 | go func() { 408 | a := clock.Ticker(1 * time.Microsecond) 409 | b := clock.Ticker(3 * time.Microsecond) 410 | 411 | for { 412 | select { 413 | case <-a.Chan(): 414 | atomic.AddInt32(&n, 1) 415 | case <-b.Chan(): 416 | atomic.AddInt32(&n, 100) 417 | } 418 | } 419 | }() 420 | 421 | internal.Gosched() 422 | 423 | // Move clock forward. 424 | clock.Add(10 * time.Microsecond) 425 | internal.Gosched() 426 | 427 | if atomic.LoadInt32(&n) != 310 { 428 | t.Fatalf("unexpected: %d", n) 429 | } 430 | } 431 | 432 | func TestMock_ReentrantDeadlock(t *testing.T) { 433 | mockedClock := mock.NewMock() 434 | timer20 := mockedClock.Timer(20 * time.Second) 435 | 436 | go func() { 437 | v := <-timer20.Chan() 438 | panic(fmt.Sprintf("timer should not have ticked: %v", v)) 439 | }() 440 | 441 | mockedClock.AfterFunc( 442 | 10*time.Second, func() { 443 | timer20.Stop() 444 | }, 445 | ) 446 | 447 | mockedClock.Add(15 * time.Second) 448 | mockedClock.Add(15 * time.Second) 449 | } 450 | 451 | func TestMock_AddAfterFuncRace(t *testing.T) { 452 | // start blocks the goroutines in this test 453 | // until we're ready for them to race. 454 | start := make(chan struct{}) 455 | 456 | var wg sync.WaitGroup 457 | 458 | mockedClock := mock.NewMock() 459 | 460 | var calls counter 461 | defer func() { 462 | if calls.get() == 0 { 463 | t.Errorf("AfterFunc did not call the function") 464 | } 465 | }() 466 | 467 | wg.Add(1) 468 | 469 | go func() { 470 | defer wg.Done() 471 | <-start 472 | 473 | mockedClock.AfterFunc( 474 | time.Millisecond, func() { 475 | calls.incr() 476 | }, 477 | ) 478 | }() 479 | 480 | wg.Add(1) 481 | 482 | go func() { 483 | defer wg.Done() 484 | <-start 485 | 486 | mockedClock.Add(time.Millisecond) 487 | mockedClock.Add(time.Millisecond) 488 | }() 489 | 490 | close(start) // unblock the goroutines 491 | wg.Wait() // and wait for them 492 | } 493 | 494 | func TestMock_AfterRace(t *testing.T) { 495 | const num = 10 496 | 497 | var ( 498 | mock = mock.NewMock() 499 | finished atomic.Int32 500 | ) 501 | 502 | for i := 0; i < num; i++ { 503 | go func() { 504 | <-mock.After(1 * time.Millisecond) 505 | finished.Add(1) 506 | }() 507 | } 508 | 509 | for finished.Load() < num { 510 | mock.Add(time.Second) 511 | internal.Gosched() 512 | } 513 | } 514 | --------------------------------------------------------------------------------