Skip to main content

coven_foundation/
clock.rs

1//! Wall-clock source, injected so consumers read "now" deterministically in
2//! tests.
3//!
4//! Production wires [`SystemClock`] (real `Utc::now()`); tests construct a
5//! deterministic fake ([`FixedClock`] / [`ClosureClock`]) and pass it to the
6//! unit under test.
7//!
8//! The hybrid logical clock retains this same clock and derives epoch
9//! milliseconds from it. Other consumers use the full
10//! `DateTime<Utc>` for `created_at`, `updated_at`, and expiry comparisons.
11
12use chrono::{DateTime, Utc};
13use std::sync::Arc;
14use std::time::{Duration, Instant};
15
16/// Wall-clock source. Returns a full `DateTime<Utc>`; callers derive
17/// `.timestamp()` / `.to_rfc3339()` as they need.
18pub trait Clock: Send + Sync {
19    fn now(&self) -> DateTime<Utc>;
20}
21
22/// Shared handle to a clock. Held by `Clone` types (`CovenHandle`,
23/// `CovenReadHandle`) so they clone the handle, not the implementation.
24pub type ClockRef = Arc<dyn Clock>;
25
26/// A [`ClockRef`] reading epoch milliseconds from a test-supplied source, for
27/// the hybrid-logical-clock tests that think in milliseconds rather than
28/// `DateTime`s.
29#[cfg(any(test, feature = "test-utils"))]
30pub fn clock_from_millis(source: impl Fn() -> u64 + Send + Sync + 'static) -> ClockRef {
31    Arc::new(ClosureClock(move || {
32        let millis: i64 = source().try_into().expect("test clock millis fit in i64");
33        DateTime::from_timestamp_millis(millis).expect("valid test clock instant")
34    }))
35}
36
37/// Elapsed real time, for measuring how long a piece of work took.
38///
39/// Deliberately not a [`Clock`]. A clock answers "what instant is this" for
40/// values the store keeps — commit stamps, `created_at`, expiry comparisons —
41/// and tests pin it so those come out deterministic. Measuring a duration needs
42/// the opposite guarantees: a reading that only moves forward, never jumps when
43/// the system clock is corrected, and reports the real elapsed time even while a
44/// pinned clock says no time has passed. Both live here so ambient time has one
45/// owner.
46///
47/// Nothing durable derives from a stopwatch — only diagnostics read one — so how
48/// long a run took never changes what it produces.
49pub struct Stopwatch(Instant);
50
51impl Stopwatch {
52    /// Starts measuring from now.
53    pub fn start() -> Self {
54        Self(Instant::now())
55    }
56
57    /// Real time since [`start`](Self::start).
58    pub fn elapsed(&self) -> Duration {
59        self.0.elapsed()
60    }
61}
62
63/// Production clock: real wall time.
64pub struct SystemClock;
65
66impl Clock for SystemClock {
67    fn now(&self) -> DateTime<Utc> {
68        Utc::now()
69    }
70}
71
72// Test clock fakes are exposed to downstream crates' tests via the `test-utils`
73// feature, so any crate that consumes `Clock` tests against the same fakes
74// instead of mirroring them.
75#[cfg(any(test, feature = "test-utils"))]
76pub use fakes::{ClosureClock, FixedClock};
77
78#[cfg(any(test, feature = "test-utils"))]
79mod fakes {
80    use super::*;
81
82    /// Delegates each read to a supplied test function.
83    pub struct ClosureClock<F>(pub F);
84
85    impl<F> Clock for ClosureClock<F>
86    where
87        F: Fn() -> DateTime<Utc> + Send + Sync,
88    {
89        fn now(&self) -> DateTime<Utc> {
90            (self.0)()
91        }
92    }
93
94    /// Every `now()` returns the same instant.
95    pub struct FixedClock(pub DateTime<Utc>);
96
97    impl Clock for FixedClock {
98        fn now(&self) -> DateTime<Utc> {
99            self.0
100        }
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use std::sync::atomic::{AtomicU64, Ordering};
108
109    #[test]
110    fn a_stopwatch_advances_with_real_time_under_a_pinned_clock() {
111        let instant = DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
112            .unwrap()
113            .with_timezone(&Utc);
114        let clock = FixedClock(instant);
115        let stopwatch = Stopwatch::start();
116
117        std::thread::sleep(Duration::from_millis(5));
118
119        assert_eq!(clock.now(), instant);
120        assert!(stopwatch.elapsed() >= Duration::from_millis(5));
121    }
122
123    #[test]
124    fn fixed_clock_returns_same_instant() {
125        let instant = DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
126            .unwrap()
127            .with_timezone(&Utc);
128        let clock = FixedClock(instant);
129        assert_eq!(clock.now(), instant);
130        assert_eq!(clock.now(), instant);
131    }
132
133    #[test]
134    fn clock_is_usable_behind_the_shared_handle() {
135        let instant = DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
136            .unwrap()
137            .with_timezone(&Utc);
138        let clock: ClockRef = Arc::new(FixedClock(instant));
139        assert_eq!(clock.now(), instant);
140    }
141
142    #[test]
143    fn closure_clock_reads_the_supplied_source_each_time() {
144        let start = DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
145            .unwrap()
146            .with_timezone(&Utc);
147        let calls = AtomicU64::new(0);
148        let clock = ClosureClock(|| {
149            let seconds = calls.fetch_add(1, Ordering::SeqCst) as i64;
150            start + chrono::Duration::seconds(seconds)
151        });
152
153        assert_eq!(clock.now(), start);
154        assert_eq!(clock.now(), start + chrono::Duration::seconds(1));
155    }
156}