Skip to main content

dialcache/
testing.rs

1//! Deterministic test doubles: a virtual clock and a single-threaded runtime
2//! that runs the cache's detached work to quiescence on demand (feature `test-util`).
3//!
4//! The cache's detached work and timers go through [`Runtime`], so a
5//! single-threaded pool can run every task to quiescence after each command
6//! (the `causally-ready-v1` settlement of the formal replay) and deliver timers
7//! only when a test advances the clock.
8
9use std::future::Future;
10use std::pin::Pin;
11use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
12use std::sync::Arc;
13use std::task::{Context, Poll, Waker};
14use std::time::Duration;
15
16use crate::clock::{Clock, SystemClock};
17use crate::runtime::Runtime;
18use futures::executor::{LocalPool, LocalSpawner};
19use futures::future::BoxFuture;
20use futures::task::LocalSpawnExt;
21use parking_lot::Mutex;
22
23/// Epoch of every controlled history: 2026-09-08T12:00:00.000Z.
24pub const WALL_EPOCH_MS: i64 = 1_788_868_800_000;
25
26/// The name [`Absent`] serializes as a unit struct.
27pub const ABSENT_MARKER: &str = "$dialcache::testing::Absent";
28
29/// The test-only absent value of confirmed-mismatch warnings.
30///
31/// Rust's default codec stores no distinct absent value, unlike TypeScript's
32/// top-level `undefined`. A test value type whose `Serialize` delegates to
33/// this marker at the top level stands for one: a warning treats it as an
34/// absent side, not as a value that failed to render, so its value field is
35/// unavailable without a `loggingOutcome` and the built-in difference
36/// reports the other side whole at the root pointer. `serde_json` renders the
37/// marker as `null`.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
39pub struct Absent;
40
41impl serde::Serialize for Absent {
42    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
43        serializer.serialize_unit_struct(ABSENT_MARKER)
44    }
45}
46
47impl std::fmt::Debug for VirtualClock {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        f.debug_struct("VirtualClock")
50            .field("wall_ms", &self.wall_ms())
51            .field("elapsed", &self.elapsed())
52            .finish()
53    }
54}
55
56struct TimerState {
57    at_ns: u128,
58    id: u64,
59    fired: AtomicBool,
60    cancelled: AtomicBool,
61    waker: Mutex<Option<Waker>>,
62}
63
64struct ClockState {
65    wall_ns: i128,
66    elapsed_ns: u128,
67    scheduler_ns: u128,
68    next_timer: u64,
69    timers: Vec<Arc<TimerState>>,
70}
71
72/// Virtual wall, elapsed and scheduler clocks. Scheduler time is what timers
73/// are registered against; it moves only when a history delivers timers, so a
74/// silent shift never fires a deliberately held timer.
75pub struct VirtualClock {
76    state: Mutex<ClockState>,
77}
78
79impl VirtualClock {
80    /// A clock at `wall_ms` epoch milliseconds with zero elapsed and
81    /// scheduler time.
82    pub fn new(wall_ms: i64) -> Arc<Self> {
83        Arc::new(VirtualClock {
84            state: Mutex::new(ClockState {
85                wall_ns: wall_ms as i128 * 1_000_000,
86                elapsed_ns: 0,
87                scheduler_ns: 0,
88                next_timer: 0,
89                timers: Vec::new(),
90            }),
91        })
92    }
93
94    /// Move wall (and unless `wall_only`, elapsed) time without delivering timers.
95    pub fn shift(&self, delta_ms: i64, wall_only: bool) {
96        self.shift_ns(delta_ms as i128 * 1_000_000, wall_only);
97    }
98
99    /// [`shift`](Self::shift) in nanoseconds; elapsed time saturates at zero.
100    pub fn shift_ns(&self, delta_ns: i128, wall_only: bool) {
101        let mut state = self.state.lock();
102        state.wall_ns += delta_ns;
103        if !wall_only {
104            state.elapsed_ns = (state.elapsed_ns as i128 + delta_ns).max(0) as u128;
105        }
106    }
107
108    /// The earliest live timer due at or before `target_ns` scheduler time.
109    fn pop_due(&self, target_ns: u128) -> Option<Arc<TimerState>> {
110        let mut state = self.state.lock();
111        state
112            .timers
113            .retain(|t| !t.cancelled.load(Ordering::SeqCst) && !t.fired.load(Ordering::SeqCst));
114        let mut best: Option<usize> = None;
115        for (index, timer) in state.timers.iter().enumerate() {
116            if timer.at_ns <= target_ns {
117                let better = match best {
118                    None => true,
119                    Some(current) => {
120                        let c = &state.timers[current];
121                        timer.at_ns < c.at_ns || (timer.at_ns == c.at_ns && timer.id < c.id)
122                    }
123                };
124                if better {
125                    best = Some(index);
126                }
127            }
128        }
129        best.map(|index| state.timers.remove(index))
130    }
131
132    fn set_scheduler(&self, at_ns: u128) {
133        let mut state = self.state.lock();
134        let delta = at_ns.saturating_sub(state.scheduler_ns);
135        state.wall_ns += delta as i128;
136        state.elapsed_ns += delta;
137        state.scheduler_ns = at_ns;
138    }
139
140    fn scheduler_ns(&self) -> u128 {
141        self.state.lock().scheduler_ns
142    }
143
144    /// Timers registered and neither fired nor dropped.
145    pub fn pending_timers(&self) -> usize {
146        let state = self.state.lock();
147        state
148            .timers
149            .iter()
150            .filter(|t| !t.cancelled.load(Ordering::SeqCst) && !t.fired.load(Ordering::SeqCst))
151            .count()
152    }
153}
154
155impl Clock for VirtualClock {
156    fn wall_ms(&self) -> i64 {
157        (self.state.lock().wall_ns.div_euclid(1_000_000)) as i64
158    }
159
160    fn elapsed(&self) -> Duration {
161        let ns = self.state.lock().elapsed_ns;
162        Duration::from_nanos(ns.min(u64::MAX as u128) as u64)
163    }
164}
165
166impl VirtualClock {
167    /// Register a timer against scheduler time.
168    pub fn sleep(&self, duration: Duration) -> BoxFuture<'static, ()> {
169        // A millisecond timer must never shorten a fractional remaining budget:
170        // round the delay up to whole milliseconds, as the reference ports do.
171        let delay_ns = duration.as_nanos();
172        let delay_ms = delay_ns.div_ceil(1_000_000);
173        let mut state = self.state.lock();
174        state.next_timer += 1;
175        let timer = Arc::new(TimerState {
176            at_ns: state.scheduler_ns + delay_ms * 1_000_000,
177            id: state.next_timer,
178            fired: AtomicBool::new(false),
179            cancelled: AtomicBool::new(false),
180            waker: Mutex::new(None),
181        });
182        state.timers.push(timer.clone());
183        Box::pin(Sleep { timer })
184    }
185}
186
187struct Sleep {
188    timer: Arc<TimerState>,
189}
190
191impl Future for Sleep {
192    type Output = ();
193
194    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
195        if self.timer.fired.load(Ordering::SeqCst) {
196            return Poll::Ready(());
197        }
198        *self.timer.waker.lock() = Some(cx.waker().clone());
199        Poll::Pending
200    }
201}
202
203impl Drop for Sleep {
204    fn drop(&mut self) {
205        self.timer.cancelled.store(true, Ordering::SeqCst);
206    }
207}
208
209/// Tasks handed to the spawner. Immediate tasks run on the next drain;
210/// deferred tasks run only once nothing else is runnable.
211#[derive(Default)]
212struct Queues {
213    immediate: Vec<BoxFuture<'static, ()>>,
214    deferred: Vec<BoxFuture<'static, ()>>,
215}
216
217/// The test runtime handle: collects tasks for the pool and registers timers
218/// on the virtual clock.
219pub struct StepRuntime {
220    queues: Mutex<Queues>,
221    spawned: AtomicU64,
222    clock: Arc<VirtualClock>,
223}
224
225impl StepRuntime {
226    /// A runtime whose timers register on `clock` and whose tasks wait for
227    /// a [`TestExecutor`] to drain them.
228    pub fn new(clock: Arc<VirtualClock>) -> Arc<Self> {
229        Arc::new(StepRuntime {
230            queues: Mutex::new(Queues::default()),
231            spawned: AtomicU64::new(0),
232            clock,
233        })
234    }
235
236    /// Tasks handed over so far, immediate and deferred.
237    pub fn spawned(&self) -> u64 {
238        self.spawned.load(Ordering::Relaxed)
239    }
240}
241
242impl std::fmt::Debug for StepRuntime {
243    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
244        f.debug_struct("StepRuntime")
245            .field("spawned", &self.spawned())
246            .finish()
247    }
248}
249
250impl Runtime for StepRuntime {
251    fn spawn(&self, task: BoxFuture<'static, ()>) {
252        self.spawned.fetch_add(1, Ordering::Relaxed);
253        self.queues.lock().immediate.push(task);
254    }
255
256    fn defer(&self, task: BoxFuture<'static, ()>) {
257        self.spawned.fetch_add(1, Ordering::Relaxed);
258        self.queues.lock().deferred.push(task);
259    }
260
261    fn spawn_blocking(
262        &self,
263        task: Box<dyn FnOnce() + Send + 'static>,
264    ) -> Result<(), crate::BoxError> {
265        self.spawn(Box::pin(async move { task() }));
266        Ok(())
267    }
268
269    fn sleep(&self, duration: Duration) -> BoxFuture<'static, ()> {
270        self.clock.sleep(duration)
271    }
272}
273
274/// The controlled executor of one test: a single-threaded pool plus the
275/// clock and runtime handle every cache instance under test shares.
276pub struct TestExecutor {
277    pool: LocalPool,
278    local: LocalSpawner,
279    polls: Arc<AtomicU64>,
280    /// The virtual clock to share with every instance under test.
281    pub clock: Arc<VirtualClock>,
282    /// The runtime handle to pass to every instance under test.
283    pub runtime: Arc<StepRuntime>,
284}
285
286impl std::fmt::Debug for TestExecutor {
287    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
288        f.debug_struct("TestExecutor")
289            .field("wall_ms", &self.clock.wall_ms())
290            .finish()
291    }
292}
293
294impl TestExecutor {
295    /// A fresh pool with its clock at `wall_ms`; replayed histories use
296    /// [`WALL_EPOCH_MS`].
297    pub fn new(wall_ms: i64) -> Self {
298        let pool = LocalPool::new();
299        let local = pool.spawner();
300        let clock = VirtualClock::new(wall_ms);
301        let runtime = StepRuntime::new(clock.clone());
302        TestExecutor {
303            pool,
304            local,
305            polls: Arc::new(AtomicU64::new(0)),
306            clock,
307            runtime,
308        }
309    }
310
311    /// Actual task polls performed by this executor. A zero-time verification
312    /// drain must leave this count unchanged when every task is already blocked.
313    pub fn poll_count(&self) -> u64 {
314        self.polls.load(Ordering::Relaxed)
315    }
316
317    fn tracked<F: Future>(&self, future: F) -> impl Future<Output = F::Output> + use<F> {
318        let polls = self.polls.clone();
319        let mut future = Box::pin(future);
320        futures::future::poll_fn(move |cx| {
321            polls.fetch_add(1, Ordering::Relaxed);
322            future.as_mut().poll(cx)
323        })
324    }
325
326    /// Run a future to completion on the pool, draining detached work as it
327    /// is spawned. Panics if the future stays blocked once nothing is runnable:
328    /// it would be waiting on a gate only the test can release.
329    pub fn block_on<R: 'static>(&mut self, future: impl Future<Output = R> + 'static) -> R {
330        let slot: std::rc::Rc<std::cell::RefCell<Option<R>>> =
331            std::rc::Rc::new(std::cell::RefCell::new(None));
332        let sink = slot.clone();
333        self.local
334            .spawn_local(self.tracked(async move {
335                let result = future.await;
336                *sink.borrow_mut() = Some(result);
337            }))
338            .expect("pool accepts tasks");
339        self.drain();
340        let result = slot.borrow_mut().take();
341        result.expect("block_on future stayed blocked on a test-owned gate")
342    }
343
344    /// Spawn driver-owned work onto the pool.
345    pub fn spawn(&self, task: impl Future<Output = ()> + 'static) {
346        self.local
347            .spawn_local(self.tracked(task))
348            .expect("pool accepts tasks");
349    }
350
351    fn move_immediate(&mut self) -> bool {
352        let tasks = std::mem::take(&mut self.runtime.queues.lock().immediate);
353        let moved = !tasks.is_empty();
354        for task in tasks {
355            self.local
356                .spawn_local(self.tracked(task))
357                .expect("pool accepts tasks");
358        }
359        moved
360    }
361
362    fn move_deferred(&mut self) -> bool {
363        let tasks = std::mem::take(&mut self.runtime.queues.lock().deferred);
364        let moved = !tasks.is_empty();
365        for task in tasks {
366            self.local
367                .spawn_local(self.tracked(task))
368                .expect("pool accepts tasks");
369        }
370        moved
371    }
372
373    /// Run until every task is blocked on a gate, an undelivered timer or a
374    /// scope gate and nothing is runnable, including deferred work that
375    /// becomes runnable once everything else has stalled.
376    pub fn drain(&mut self) {
377        loop {
378            loop {
379                self.pool.run_until_stalled();
380                if !self.move_immediate() {
381                    break;
382                }
383            }
384            if !self.move_deferred() {
385                return;
386            }
387        }
388    }
389
390    /// Advance elapsed and wall time by `ms`. With `deliver`, scheduler time
391    /// moves too and every timer due on the way fires in order, draining after
392    /// each; without it the clocks jump silently.
393    pub fn advance(&mut self, ms: i64, deliver: bool) {
394        if !deliver {
395            self.clock.shift(ms, false);
396            self.drain();
397            return;
398        }
399        let target = self.clock.scheduler_ns() + (ms.max(0) as u128) * 1_000_000;
400        loop {
401            self.drain();
402            match self.clock.pop_due(target) {
403                Some(timer) => {
404                    self.clock.set_scheduler(timer.at_ns);
405                    timer.fired.store(true, Ordering::SeqCst);
406                    if let Some(waker) = timer.waker.lock().take() {
407                        waker.wake();
408                    }
409                }
410                None => {
411                    self.clock.set_scheduler(target);
412                    break;
413                }
414            }
415        }
416        self.drain();
417    }
418
419    /// Advance by fractional microseconds without delivering timers (the
420    /// local-clock profile's environment ticks).
421    pub fn advance_micros(&mut self, micros: i64) {
422        self.clock.shift_ns(micros as i128 * 1_000, false);
423        self.drain();
424    }
425}
426
427/// The production [`SystemClock`] built over a [`VirtualClock`].
428///
429/// Its origin aligns to the shared millisecond grid of the virtual elapsed
430/// reading exactly as default instances align to the process grid, so
431/// instances constructed at different fractional virtual times share one
432/// expiry grid and the default alignment itself is what a controlled history
433/// exercises.
434pub fn grid_clock(base: &Arc<VirtualClock>) -> SystemClock {
435    let monotonic = base.clone();
436    let wall = base.clone();
437    SystemClock::with_sources(
438        move || {
439            let ns = monotonic.state.lock().elapsed_ns;
440            Duration::from_nanos(ns.min(u64::MAX as u128) as u64)
441        },
442        move || wall.wall_ms(),
443    )
444}