Skip to main content

term_test_support/
clock.rs

1//! A thread-safe virtual clock for deterministic timer tests.
2
3use std::fmt;
4use std::sync::Arc;
5use std::sync::atomic::{AtomicU64, Ordering};
6use std::time::{Duration, Instant};
7
8/// A manually advanced clock, `Send + Sync` and cheaply cloneable.
9///
10/// All clones share one offset. `now()` returns `base + elapsed`, where
11/// `base` is fixed at construction and `elapsed` only changes through
12/// [`advance`](ManualClock::advance). Tests move virtual time forward; no
13/// wall-clock waiting is ever involved.
14///
15/// Note: advancing the clock does NOT run any scheduler work by itself.
16/// Virtual time only moves the timestamp provider; tests must still drain
17/// their scheduler explicitly after each advance before asserting.
18#[derive(Clone)]
19pub struct ManualClock {
20    base: Instant,
21    elapsed_nanos: Arc<AtomicU64>,
22}
23
24impl ManualClock {
25    /// Create a clock whose `now()` starts at the current instant.
26    pub fn new() -> Self {
27        Self {
28            base: Instant::now(),
29            elapsed_nanos: Arc::new(AtomicU64::new(0)),
30        }
31    }
32
33    /// Move virtual time forward by `by`.
34    ///
35    /// The offset is stored as u64 nanoseconds (about 584 years of headroom);
36    /// larger advances saturate rather than wrap.
37    pub fn advance(&self, by: Duration) {
38        let add = u64::try_from(by.as_nanos()).unwrap_or(u64::MAX);
39        self.elapsed_nanos.fetch_add(add, Ordering::Release);
40    }
41
42    /// The current virtual instant.
43    pub fn now(&self) -> Instant {
44        let offset = Duration::from_nanos(self.elapsed_nanos.load(Ordering::Acquire));
45        self.base
46            .checked_add(offset)
47            .expect("virtual clock overflowed Instant range")
48    }
49
50    /// A shared closure form suitable for injection points that take
51    /// `Arc<dyn Fn() -> Instant + Send + Sync>`.
52    pub fn as_closure(self: &Arc<Self>) -> Arc<dyn Fn() -> Instant + Send + Sync> {
53        let clone = Arc::clone(self);
54        Arc::new(move || clone.now())
55    }
56}
57
58impl Default for ManualClock {
59    fn default() -> Self {
60        Self::new()
61    }
62}
63
64impl fmt::Debug for ManualClock {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        f.debug_struct("ManualClock")
67            .field(
68                "elapsed",
69                &Duration::from_nanos(self.elapsed_nanos.load(Ordering::Acquire)),
70            )
71            .finish()
72    }
73}
74
75#[allow(clippy::unwrap_used)]
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn now_starts_at_construction_time() {
82        let clock = ManualClock::new();
83        let before = Instant::now();
84        let now = clock.now();
85        let after = Instant::now();
86        assert!(now <= after && now >= before - Duration::from_millis(10));
87    }
88
89    #[test]
90    fn advance_moves_now_forward_monotonically() {
91        let clock = ManualClock::new();
92        let t0 = clock.now();
93        clock.advance(Duration::from_millis(50));
94        let t1 = clock.now();
95        assert_eq!(t1.duration_since(t0), Duration::from_millis(50));
96        clock.advance(Duration::from_millis(25));
97        assert_eq!(clock.now().duration_since(t0), Duration::from_millis(75));
98    }
99
100    #[test]
101    fn clones_share_the_offset() {
102        let clock = Arc::new(ManualClock::new());
103        let other = Arc::clone(&clock);
104        std::thread::spawn(move || {
105            other.advance(Duration::from_millis(10));
106        })
107        .join()
108        .unwrap();
109        assert_eq!(
110            clock.now().duration_since(clock.base),
111            Duration::from_millis(10)
112        );
113    }
114}