Use case · Testing

Drive virtual time in a test

Run the same engine on a VirtualClock. Advancing time wakes sleepers in deadline order, each observing now equal to its own deadline — and advancing before a sleeper registers still wakes it, so tests are race-free with no real waiting.

A race-free wait

Construct a VirtualClock, spawn work that sleeps, then advance. The sleeper wakes when now reaches its deadline and observes exactly that value.

use std::sync::Arc;
use ferro_replay::{Clock, VirtualClock};

#[tokio::test]
async fn wakes_at_its_deadline() {
    let clock = Arc::new(VirtualClock::new(1_000));
    let c = clock.clone();
    let task = tokio::spawn(async move {
        c.sleep_until(2_000).await;
        c.now_ns()
    });

    // Even if this runs before the task registers, the wait still fires.
    clock.advance_to(2_000).await;
    assert_eq!(task.await.unwrap(), 2_000);
}

Deadline-ordered wakes

Multiple sleepers wake in (deadline, registration-order) order, and each observes now equal to its own deadline — never a later one. Time never runs ahead of the timers it fires.

let clock = Arc::new(VirtualClock::new(0));
let mut handles = Vec::new();
for deadline in [3_000i64, 1_000, 2_000] {
    let c = clock.clone();
    handles.push(tokio::spawn(async move {
        c.sleep_until(deadline).await;
        (deadline, c.now_ns()) // (d, d) — observed == scheduled
    }));
}

clock.advance_to(3_000).await;
// Fires in deadline order: (1000,1000), (2000,2000), (3000,3000).
Runtime

The strict ordering guarantee holds on a current-thread runtime — the default #[tokio::test] flavor. A multi-thread runtime would reintroduce wake-order races.

Cancelled sleeps cost nothing

A sleep cancelled by a select! arm (its receiver dropped) is discarded on pop without a wake or a yield — O(1) each, even when dead waiters accumulate one-per-event on a busy timeline. A live sleeper behind a pile of dead ones still observes its own deadline.