timecop

package module
v0.0.0-...-f619efc Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 17, 2026 License: BSD-3-Clause Imports: 1 Imported by: 0

README

go-ruby-timecop/timecop

timecop — go-ruby-timecop

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of the deterministic core of Ruby's timecop gem — a controllable clock that lets code freeze, travel through, and scale the passage of time, with block-scoped nesting and a return-to-baseline mechanism. It reproduces the gem's TimeStackItem mock-time formulas and its stack semantics exactly — without any Ruby runtime.

It is the timecop engine for go-embedded-ruby, but a standalone, reusable module: a host (rbgo) binds Ruby's Timecop module methods to a Clock and routes Time.now / Date.today / DateTime.now through Clock.Current.

The real-clock seam. timecop mocks time relative to the real system clock: a travelled or scaled clock keeps advancing as wall-clock time passes. To stay deterministic and testable, the "real now" is an injectable seam — the Clock.Now field. When it is nil the clock reads time.Now; a test (or a host) sets it to a controlled function so no reading of the wall clock ever leaks in. This mirrors the gem's Time.now_without_mock_time.

Mock-time formulas

A Clock holds a stack of mock-time frames; Clock.Current reports the time according to the top frame (or the real clock when the stack is empty). Each frame captures the real "now" at the moment it was pushed (timeWas) and a target time, and reports — exactly as timecop's TimeStackItem#time:

mode formula behaviour
freeze target time is pinned; it does not move
travel realNow + (target − timeWas) jump, then advance at 1× real
scale target + (realNow − timeWas) × factor jump, then advance at factor×

Features

Faithful port of the timecop gem's engine:

  • ClockNew() / NewWith(now); Current() reports the mocked now.
  • freeze / travel / scaleFreeze(t), Travel(t), Scale(factor, t) push a frame and return the resulting mocked time.
  • Block formsWithFrozen(t, fn), WithTravel(t, fn), WithScale(factor, t, fn) push a frame for the duration of fn and pop it afterwards — even if fn panics (timecop's ensure-scoped block).
  • Nesting — an arbitrarily deep stack of frames; the top frame wins, mirroring Timecop.freeze { Timecop.travel { … } }.
  • returnReturn() unwinds all frames and clears the baseline; WithReturn(fn) reverts to real time for fn then restores (panic-safe).
  • baselineSetBaseline(t) records a baseline (pushing a travel frame at it); ReturnToBaseline() unwinds every nested frame back down to the baseline; Baseline() reads it.
  • predicatesFrozen(), Travelled(), Scaled(), Mocked(), Depth(), ScalingFactor(), mirroring Timecop.frozen?/travelled?/scaled?.
  • Package surface — package-level Freeze/Travel/Scale/Return/… operate on a process-wide default Clock (the analogue of timecop's singleton); SetNow(func) injects the seam for deterministic tests.

CGO-free, dependency-free (stdlib only), 100% test coverage, gofmt + go vet clean, and green across the six 64-bit Go targets (amd64, arm64, riscv64, loong64, ppc64le, s390x — big-endian) plus js/wasm and wasip1/wasm.

Install

go get github.com/go-ruby-timecop/timecop

Usage

package main

import (
	"fmt"
	"time"

	"github.com/go-ruby-timecop/timecop"
)

func main() {
	c := timecop.New()

	t := time.Date(2008, 10, 5, 0, 0, 0, 0, time.UTC)

	c.Freeze(t)                     // Time.now == t, pinned
	fmt.Println(c.Current())        // 2008-10-05 00:00:00 +0000 UTC
	c.Return()

	c.Travel(t)                     // jump to t, then keep ticking at 1×
	c.Scale(4, t)                   // ... nested: now runs 4× real time

	c.WithFrozen(t, func() {        // block scope: pushed, then popped
		// ... time frozen at t inside here ...
	})                              // popped even if fn panics

	c.Return()                      // unwind everything, back to real time
}
Injecting the real-clock seam (tests / hosts)
now := time.Date(2008, 10, 5, 12, 0, 0, 0, time.UTC)
c := timecop.NewWith(func() time.Time { return now })

c.Travel(time.Date(1999, 1, 1, 0, 0, 0, 0, time.UTC))
now = now.Add(90 * time.Minute)          // advance the *real* clock
fmt.Println(c.Current())                 // 1999-01-01 01:30:00 (travelled + advanced)

Value model

gem this package
Timecop.freeze(t) Clock.Freeze(t) / timecop.Freeze(t)
Timecop.travel(t) Clock.Travel(t) / timecop.Travel(t)
Timecop.scale(factor, t) Clock.Scale(factor, t)
Timecop.freeze(t) { … } Clock.WithFrozen(t, fn)
Timecop.travel(t) { … } Clock.WithTravel(t, fn)
Timecop.scale(factor, t) { … } Clock.WithScale(factor, t, fn)
Timecop.return Clock.Return()
Timecop.return { … } Clock.WithReturn(fn)
Timecop.baseline = t Clock.SetBaseline(t)
Timecop.return_to_baseline Clock.ReturnToBaseline()
mocked Time.now Clock.Current() / timecop.Now()
Timecop.frozen?/travelled?/scaled? Clock.Frozen()/Travelled()/Scaled()
Time.now_without_mock_time Clock.Now (injectable real-clock seam)

Tests & coverage

The suite is fully deterministic: every test injects a fake real-clock seam and advances it explicitly, so no wall-clock reading leaks into an assertion. Every branch — nested frames, block pop-on-panic, scale math, baseline unwinding — is covered, holding coverage at 100% across every OS and arch lane.

COVERPKG=$(go list ./... | paste -sd, -)
go test -race -coverpkg="$COVERPKG" -coverprofile=cover.out ./...
go tool cover -func=cover.out | tail -1   # 100.0%

WebAssembly

Being pure Go (CGO=0), this library also compiles to WebAssembly — both GOOS=js GOARCH=wasm (browser / Node.js) and GOOS=wasip1 GOARCH=wasm (WASI). CI builds both targets on every push, alongside the six 64-bit native/qemu arches.

GOOS=js     GOARCH=wasm go build ./...   # browser / Node
GOOS=wasip1 GOARCH=wasm go build ./...   # WASI (wasmtime, wasmer, wasmedge, …)

License

BSD-3-Clause — see LICENSE. Copyright the go-ruby-timecop/timecop authors.

Documentation

Overview

Package timecop is a pure-Go (CGO-free) reimplementation of the deterministic core of Ruby's timecop gem — a controllable clock that lets code "freeze", "travel" through, and "scale" the passage of time, with block-scoped nesting and a return-to-baseline mechanism. It reproduces the gem's TimeStackItem mock-time formulas and its stack semantics exactly, without any Ruby runtime.

It is the timecop engine for [go-embedded-ruby](https://github.com/go-embedded-ruby/ruby), but a standalone, reusable module: a host (rbgo) binds Ruby's Timecop module methods (Timecop.freeze/travel/scale/return/return_to_baseline plus their block forms) to a Clock and routes Time.now / Date.today / DateTime.now through Clock.Current.

The real-clock seam

timecop mocks time relative to the real system clock: a travelled or scaled clock keeps advancing as wall-clock time passes. To stay deterministic and testable, the "real now" is an injectable seam — the Clock.Now field. When it is nil the clock reads time.Now; a test (or a host) sets it to a controlled function so no reading of the wall clock ever leaks in. This mirrors the gem's Time.now_without_mock_time.

Mock-time formulas

A Clock holds a stack of mock-time frames; Clock.Current reports the time according to the top frame (or the real clock when the stack is empty). Each frame captures the real "now" at the moment it was pushed (timeWas) and a target time, and reports:

  • freeze: the fixed target — time does not move.
  • travel: realNow + (target - timeWas) — offset that keeps advancing at 1×.
  • scale: target + (realNow - timeWas) * factor — advances at factor×.

Flow

c := timecop.New()
c.Freeze(t)                 // Time.now == t until Return
c.Travel(t)                 // jump to t, then keep ticking
c.Scale(4, t)               // from t, time runs 4× real
c.WithFrozen(t, func() {    // block scope: pushed, then popped
	// ... time frozen at t here ...
})                          // popped even if fn panics
c.Return()                  // unwind everything, back to real time

Baseline

Clock.SetBaseline records a baseline time (pushing a travel frame at it); Clock.ReturnToBaseline unwinds every nested frame back down to that baseline, mirroring Timecop.return_to_baseline.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Baseline

func Baseline() (time.Time, bool)

Baseline returns the default clock's baseline. Mirrors Timecop.baseline.

func Freeze

func Freeze(t time.Time) time.Time

Freeze freezes the default clock at t. Mirrors Timecop.freeze(t).

func Frozen

func Frozen() bool

Frozen reports whether the default clock is frozen. Mirrors Timecop.frozen?.

func Mocked

func Mocked() bool

Mocked reports whether the default clock is mocking time.

func Now

func Now() time.Time

Now reports the default clock's current mock time — the analogue of Ruby's mocked Time.now.

func Return

func Return()

Return unwinds the default clock to real time. Mirrors Timecop.return.

func ReturnToBaseline

func ReturnToBaseline() time.Time

ReturnToBaseline unwinds the default clock to its baseline. Mirrors Timecop.return_to_baseline.

func Scale

func Scale(factor float64, t time.Time) time.Time

Scale scales the default clock by factor from t. Mirrors Timecop.scale(factor, t).

func Scaled

func Scaled() bool

Scaled reports whether the default clock is scaled. Mirrors Timecop.scaled?.

func SetBaseline

func SetBaseline(t time.Time)

SetBaseline sets the default clock's baseline. Mirrors Timecop's `baseline=`.

func SetNow

func SetNow(now func() time.Time)

SetNow installs now as the real-clock seam of the default clock. Passing nil restores the real system clock (time.Now). Tests use this to make the default clock deterministic.

func Travel

func Travel(t time.Time) time.Time

Travel travels the default clock to t. Mirrors Timecop.travel(t).

func Travelled

func Travelled() bool

Travelled reports whether the default clock is travelled. Mirrors Timecop.travelled?.

func WithFrozen

func WithFrozen(t time.Time, fn func())

WithFrozen runs fn with the default clock frozen at t. Mirrors Timecop.freeze(t) { }.

func WithReturn

func WithReturn(fn func())

WithReturn runs fn with the default clock reverted to real time, then restores it. Mirrors Timecop.return { }.

func WithScale

func WithScale(factor float64, t time.Time, fn func())

WithScale runs fn with the default clock scaled by factor from t. Mirrors Timecop.scale(factor, t) { }.

func WithTravel

func WithTravel(t time.Time, fn func())

WithTravel runs fn with the default clock travelled to t. Mirrors Timecop.travel(t) { }.

Types

type Clock

type Clock struct {
	// Now is the real-clock seam. When nil the clock reads [time.Now]; set it
	// to a deterministic function in tests or to a host-provided clock. It is
	// the analogue of timecop's Time.now_without_mock_time.
	Now func() time.Time
	// contains filtered or unexported fields
}

Clock is a controllable clock: the pure-Go engine behind Ruby's Timecop module. It maintains a stack of mock-time frames (freeze/travel/scale) plus an optional baseline, and reports the current mock time through Clock.Current.

A Clock is not safe for concurrent use; guard it externally if shared. This matches the gem, whose per-thread stacks the host manages.

func Default

func Default() *Clock

Default returns the process-wide default Clock backing the package-level functions.

func New

func New() *Clock

New returns a Clock backed by the real system clock (time.Now).

func NewWith

func NewWith(now func() time.Time) *Clock

NewWith returns a Clock whose real-clock seam is now. A nil now falls back to time.Now at read time.

func (*Clock) Baseline

func (c *Clock) Baseline() (time.Time, bool)

Baseline returns the current baseline and whether one is set.

func (*Clock) Current

func (c *Clock) Current() time.Time

Current reports the current mock time: the top stack frame's time, or the real now when the stack is empty. It is the analogue of Ruby's mocked Time.now.

func (*Clock) Depth

func (c *Clock) Depth() int

Depth reports the number of active mock-time frames on the stack.

func (*Clock) Freeze

func (c *Clock) Freeze(t time.Time) time.Time

Freeze pushes a freeze frame pinning time at t and returns the resulting current mock time (which equals t). Mirrors Timecop.freeze(t) without a block.

func (*Clock) Frozen

func (c *Clock) Frozen() bool

Frozen reports whether the top frame is a freeze. Mirrors Timecop.frozen?.

func (*Clock) Mocked

func (c *Clock) Mocked() bool

Mocked reports whether any mock-time frame is active (Time.now is faked).

func (*Clock) Return

func (c *Clock) Return()

Return unwinds all mock-time frames and clears any baseline, reverting to the real clock. Mirrors Timecop.return / Timecop.unfreeze without a block.

func (*Clock) ReturnToBaseline

func (c *Clock) ReturnToBaseline() time.Time

ReturnToBaseline unwinds every nested frame back down to the baseline frame (keeping only the bottom-most frame), or reverts entirely to the real clock when no baseline is set. It returns the resulting current mock time. Mirrors Timecop.return_to_baseline.

func (*Clock) Scale

func (c *Clock) Scale(factor float64, t time.Time) time.Time

Scale pushes a scale frame: time jumps to t and then advances at factor× the real rate. Returns the resulting current mock time (≈ t). Mirrors Timecop.scale(factor, t) without a block.

func (*Clock) Scaled

func (c *Clock) Scaled() bool

Scaled reports whether the top frame is a scale. Mirrors Timecop.scaled?.

func (*Clock) ScalingFactor

func (c *Clock) ScalingFactor() (float64, bool)

ScalingFactor returns the top frame's scaling factor and true when the top frame is a scale, or 0 and false otherwise. Mirrors Timecop.scaling_factor.

func (*Clock) SetBaseline

func (c *Clock) SetBaseline(t time.Time)

SetBaseline records t as the baseline and pushes a travel frame at it, so the clock travels to the baseline. Clock.ReturnToBaseline later unwinds back to this frame. Mirrors Timecop's `baseline=`.

func (*Clock) Travel

func (c *Clock) Travel(t time.Time) time.Time

Travel pushes a travel frame jumping time to t; time then keeps advancing at the real rate. Returns the resulting current mock time (≈ t). Mirrors Timecop.travel(t) without a block.

func (*Clock) Travelled

func (c *Clock) Travelled() bool

Travelled reports whether the top frame is a travel. Mirrors Timecop.travelled?.

func (*Clock) WithFrozen

func (c *Clock) WithFrozen(t time.Time, fn func())

WithFrozen runs fn with time frozen at t, then pops the frame (even on panic). Mirrors Timecop.freeze(t) { ... }.

func (*Clock) WithReturn

func (c *Clock) WithReturn(fn func())

WithReturn reverts to the real clock for the duration of fn, then restores the previous stack and baseline — even if fn panics. Mirrors Timecop.return { ... }.

func (*Clock) WithScale

func (c *Clock) WithScale(factor float64, t time.Time, fn func())

WithScale runs fn with time scaled by factor from t, then pops the frame (even on panic). Mirrors Timecop.scale(factor, t) { ... }.

func (*Clock) WithTravel

func (c *Clock) WithTravel(t time.Time, fn func())

WithTravel runs fn with time travelled to t, then pops the frame (even on panic). Mirrors Timecop.travel(t) { ... }.

type Mode

type Mode int

Mode is the kind of mock-time frame on a Clock's stack — the Go analogue of timecop's TimeStackItem mock_type (:freeze, :travel, :scale).

const (
	// ModeFreeze pins time at a fixed instant; it does not advance.
	ModeFreeze Mode = iota
	// ModeTravel jumps time to an instant, after which it keeps advancing at
	// real (1×) rate.
	ModeTravel
	// ModeScale jumps time to an instant, after which it advances at a
	// configurable factor of the real rate.
	ModeScale
)

func (Mode) String

func (m Mode) String() string

String returns the timecop mock_type name of the mode: "freeze", "travel", or "scale".

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL