NAME

Test::Mockingbird::TimeTravel - Deterministic, controllable time for Perl tests

VERSION

Version 0.12

SYNOPSIS

use Test::Mockingbird::TimeTravel qw(
    now freeze_time travel_to advance_time rewind_time
    restore_all with_frozen_time
);

freeze_time('2025-01-01T00:00:00Z');
is now(), 1735689600, 'time is frozen';

advance_time(2 => 'minutes');
is now(), 1735689720, 'time advanced deterministically';

with_frozen_time '2025-01-02T12:00:00Z' => sub {
    is now(), 1735819200, 'block sees overridden time';
};

is now(), 1735689720, 'outer time restored after block';

restore_all();
isnt now(), 1735689720, 'real time restored';

DESCRIPTION

Test::Mockingbird::TimeTravel provides a lightweight, deterministic time-control layer for Perl tests. It does not monkey-patch CORE::time(). Instead it provides a dedicated now() function and a small set of declarative operations that manipulate an internal frozen clock.

LIMITATIONS

now() is not a drop-in for time()

Production code must call now() for time control to take effect. Code that calls CORE::time(), POSIX::time(), or Time::HiRes::time() directly is not affected.

Fractional seconds not supported

All timestamps are integer epoch seconds. Sub-second resolution is not available.

Thread safety

$ACTIVE, $CURRENT_EPOCH, and $BASE_EPOCH are package globals. Concurrent threads that manipulate time state will race.

METHODS

now

Return the current time.

Returns $CURRENT_EPOCH when frozen, CORE::time() otherwise.

API SPECIFICATION

Input (Params::Validate::Strict schema)

none

Output (Returns::Set schema)

returns: Int  -- epoch seconds

FORMAL SPECIFICATION

now ≙ ($ACTIVE = 1 ⇒ returns $CURRENT_EPOCH) ∧ ($ACTIVE = 0 ⇒ returns CORE::time())

freeze_time

Freeze the clock at a specific timestamp.

my $epoch = freeze_time('2025-01-01T00:00:00Z');

API SPECIFICATION

Input (Params::Validate::Strict schema)

$timestamp -- Str|Int (see _parse_timestamp for accepted formats)

Output (Returns::Set schema)

returns: Int  -- frozen epoch

MESSAGES

"Invalid timestamp format for TimeTravel: ..." -- unrecognised format

FORMAL SPECIFICATION

freeze_time ≙
  ∀ ts : Str|Int •
    post $ACTIVE' = 1 ∧ $CURRENT_EPOCH' = parse(ts) ∧ $BASE_EPOCH' = parse(ts)

travel_to

Move the frozen clock to a new timestamp without unfreezing.

travel_to('2025-06-01T00:00:00Z');

Croaks if called while TimeTravel is inactive.

API SPECIFICATION

Input (Params::Validate::Strict schema)

$timestamp -- Str|Int

Output (Returns::Set schema)

returns: Int  -- new epoch

MESSAGES

"travel_to() called while TimeTravel is inactive" -- freeze_time() not called first

FORMAL SPECIFICATION

travel_to ≙
  pre  $ACTIVE = 1
  post $CURRENT_EPOCH' = parse(ts) ∧ $BASE_EPOCH unchanged ∧ $ACTIVE unchanged

advance_time

Advance the frozen clock by a duration.

advance_time(30);              # 30 seconds
advance_time(2 => 'minutes');  # 2 minutes

Croaks if called while TimeTravel is inactive.

API SPECIFICATION

Input (Params::Validate::Strict schema)

$amount -- Int
$unit   -- Str, optional (second|minute|hour|day, plural forms ok)

Output (Returns::Set schema)

returns: Int  -- new epoch

MESSAGES

"advance_time() called while TimeTravel is inactive" -- not frozen
"Unknown time unit '...'"                            -- unrecognised unit string

FORMAL SPECIFICATION

advance_time ≙
  pre  $ACTIVE = 1
  post $CURRENT_EPOCH' = $CURRENT_EPOCH + to_seconds(amount, unit)

rewind_time

Rewind the frozen clock by a duration.

rewind_time(30);           # 30 seconds
rewind_time(1 => 'hour');  # 1 hour

Croaks if called while TimeTravel is inactive.

API SPECIFICATION

Input (Params::Validate::Strict schema)

$amount -- Int
$unit   -- Str, optional

Output (Returns::Set schema)

returns: Int  -- new epoch

MESSAGES

"rewind_time() called while TimeTravel is inactive" -- not frozen

FORMAL SPECIFICATION

rewind_time ≙
  pre  $ACTIVE = 1
  post $CURRENT_EPOCH' = $CURRENT_EPOCH - to_seconds(amount, unit)

restore_all

Return to real time and clear all frozen state.

restore_all();

Idempotent. Called automatically by Test::Mockingbird::DeepMock.

API SPECIFICATION

Input (Params::Validate::Strict schema)

none

Output (Returns::Set schema)

returns: undef

FORMAL SPECIFICATION

restore_all ≙
  post $ACTIVE' = 0 ∧ $CURRENT_EPOCH' = undef ∧ $BASE_EPOCH' = undef

with_frozen_time

Temporarily override time inside a code block, restoring previous state afterward even if the block throws.

with_frozen_time '2025-01-02T12:00:00Z' => sub {
    is now(), 1735819200, 'block sees overridden time';
};

Fully nestable.

API SPECIFICATION

Input (Params::Validate::Strict schema)

$timestamp -- Str|Int
$code      -- CodeRef

Output (Returns::Set schema)

returns: Any  -- whatever $code returns

MESSAGES

"with_frozen_time() requires a coderef"    -- second arg not a CodeRef
"with_frozen_time() requires a timestamp"  -- first arg undefined

FORMAL SPECIFICATION

with_frozen_time ≙
  ∀ ts : Str|Int; code : CodeRef •
    pre  defined(ts) ∧ ref(code) = 'CODE'
    post (save prev_state; freeze(ts); run code; restore prev_state)
         ∧ exceptions rethrown

SUPPORT

Please report bugs at https://github.com/nigelhorne/Test-Mockingbird/issues.

AUTHOR

Nigel Horne, <njh at nigelhorne.com>

SEE ALSO

REPOSITORY

https://github.com/nigelhorne/Test-Mockingbird

LICENCE AND COPYRIGHT

Copyright 2026 Nigel Horne.

Usage is subject to GPL2 licence terms.

1 POD Error

The following errors were encountered while parsing the POD:

Around line 122:

Non-ASCII character seen before =encoding in '≙'. Assuming UTF-8