Documentation
¶
Overview ¶
Package clock provides an injectable source of time so components that stamp, pace, or schedule work can be tested deterministically.
The Clock interface covers the three ways services consume time: reading it (Now, Since), pacing against it (Sleep, which is context-aware and never strands a goroutine past cancellation), and ticking on it (NewTicker). NewClock returns the production implementation backed by the time package.
Components should accept a Clock rather than calling time.Now or time.Sleep directly. Scheduling (cron, distributed coordination) is out of scope; in a multi-process system the shared database's clock, not this one, is the arbiter of ordering.
Testing time-dependent logic ¶
There is no fake Clock, because testing/synctest makes one unnecessary. The wall Clock delegates to the time package on every call and caches nothing, so inside a bubble it rides the bubble's fake clock: a component under test keeps its production Clock, and the test moves time with time.Sleep. TTL expiry, backoff pacing, and periodic sweeps run in nanoseconds of wall time. See synctest_test.go, which pins this contract.
The bubble replaces the two things a hand-rolled fake provided. Advancing is time.Sleep in the test goroutine, or nothing at all — when every goroutine is durably blocked, time jumps to the next deadline on its own. Waiting for a goroutine to reach its sleep or ticker before advancing is synctest.Wait, which needs no count of registered waiters and returns without moving time.
That auto-advance is worth respecting, because it will happily rescue a broken test. A blocking receive on the result of the code under test — <-done, <-ticked — durably blocks the bubble, so time skips to whatever deadline comes next and the receive succeeds no matter how wrong the interval was. Such a test passes with the cadence set to an hour. Assert timing the other way instead: sleep to just short of the deadline, synctest.Wait (which parks everything without moving the clock), and check that nothing has happened; then step across the deadline, Wait again, and check with a non-blocking receive or a counter that exactly one thing did. Pair it with a deferred cancel or Close so a failed assertion unwinds the goroutines under test and reports itself instead of tripping the bubble's deadlock panic.
Two limits are worth knowing. A bubble's clock always starts at midnight UTC 2000-01-01, so a test wanting a particular timestamp derives it from time.Now inside the bubble. And Example functions cannot open a bubble — synctest.Test needs a *testing.T — so an example that must not be interrupted by a tick sets an interval longer than the example instead.
Tests that genuinely cannot run in a bubble — those blocking on real network or container I/O, which never counts as durably blocked — should use real durations against the real clock, as the integration suites do.
Example ¶
Example wires a component to the production clock. Tests need no double: inside a testing/synctest bubble this same Clock reads the bubble's fake time, so time.Sleep moves the token to expiry in nanoseconds of wall time.
package main
import (
"context"
"fmt"
"time"
"github.com/primandproper/platform-go/v8/clock"
)
// expiringToken is the shape components take: it stamps and checks against an
// injected Clock rather than calling time.Now directly, which is what lets a
// test drive it under testing/synctest without waiting on real time.
type expiringToken struct {
clock clock.Clock
expiresAt time.Time
}
func newExpiringToken(c clock.Clock, ttl time.Duration) *expiringToken {
return &expiringToken{clock: c, expiresAt: c.Now().Add(ttl)}
}
func (t *expiringToken) expired() bool {
return !t.clock.Now().Before(t.expiresAt)
}
// Example wires a component to the production clock. Tests need no double:
// inside a testing/synctest bubble this same Clock reads the bubble's fake
// time, so time.Sleep moves the token to expiry in nanoseconds of wall time.
func main() {
c := clock.NewClock()
tok := newExpiringToken(c, time.Hour)
fmt.Println("expired at issue:", tok.expired())
// Sleep is context-aware: a canceled context ends the pause immediately
// rather than stranding the goroutine for the full duration.
ctx, cancel := context.WithCancel(context.Background())
cancel()
fmt.Println("sleep:", c.Sleep(ctx, time.Hour))
}
Output: expired at issue: false sleep: context canceled
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func RegisterClock ¶
RegisterClock registers the wall Clock with the injector.
Types ¶
type Clock ¶
type Clock interface {
// Now returns the current time.
Now() time.Time
// Since returns the time elapsed since t, per this clock's Now.
Since(t time.Time) time.Duration
// Sleep pauses the calling goroutine for d or until ctx is done,
// whichever comes first, returning ctx.Err in the latter case. A
// non-positive d does not sleep but still reports a done context, so a
// pacing loop's cancellation check cannot be skipped by a zero delay.
Sleep(ctx context.Context, d time.Duration) error
// NewTicker returns a Ticker that delivers ticks every d. Like
// time.NewTicker it panics if d is not positive, and slow receivers see
// coalesced (dropped) ticks rather than a backlog. Callers must Stop the
// ticker to release its resources.
NewTicker(d time.Duration) Ticker
}
Clock is an injectable source of time. Production and test code alike receive the wall clock from NewClock: inside a testing/synctest bubble it reads the bubble's fake time, so no test double is needed. The interface deliberately covers only reading time, sleeping against it, and ticking on it — anything more (timers with resets, cron scheduling) belongs to the caller.
type Ticker ¶
type Ticker interface {
// Chan returns the channel on which ticks are delivered.
Chan() <-chan time.Time
// Stop turns off the ticker. As with *time.Ticker, Stop does not close
// the channel.
Stop()
}
Ticker delivers periodic ticks on a channel. It is the injectable counterpart of *time.Ticker, narrowed to the two members loops actually use.