coven/
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`] / [`SteppingClock`]) and pass it to the
6//! unit under test.
7//!
8//! Sibling to [`crate::sync::hlc::Hlc`]'s `wall_clock`: that one returns `u64`
9//! millis for hybrid-logical-clock math, this one returns `DateTime<Utc>` for
10//! `created_at` / `updated_at` / expiry comparisons. Both are injected wall
11//! sources rooted at the same composition point; neither subsumes the other.
12
13use chrono::{DateTime, Utc};
14use std::sync::Arc;
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 (`Database`,
23/// `LibraryManager`) so they clone the handle, not the implementation.
24pub type ClockRef = Arc<dyn Clock>;
25
26/// Production clock: real wall time.
27pub struct SystemClock;
28
29impl Clock for SystemClock {
30    fn now(&self) -> DateTime<Utc> {
31        Utc::now()
32    }
33}
34
35// Test clock fakes are exposed to downstream crates' tests via the `test-utils`
36// feature, so any crate that consumes `Clock` tests against the same fakes
37// instead of mirroring them.
38#[cfg(any(test, feature = "test-utils"))]
39pub use fakes::{FixedClock, SteppingClock};
40
41#[cfg(any(test, feature = "test-utils"))]
42mod fakes {
43    use super::*;
44    use chrono::Duration;
45    use std::sync::atomic::{AtomicU64, Ordering};
46
47    /// Every `now()` returns the same instant.
48    pub struct FixedClock(pub DateTime<Utc>);
49
50    impl Clock for FixedClock {
51        fn now(&self) -> DateTime<Utc> {
52            self.0
53        }
54    }
55
56    /// Advances a fixed delta per `now()` call: the first call returns `start`,
57    /// the next `start + step`, and so on. For tests that assert ordering.
58    pub struct SteppingClock {
59        start: DateTime<Utc>,
60        step: Duration,
61        calls: AtomicU64,
62    }
63
64    impl SteppingClock {
65        pub fn new(start: DateTime<Utc>, step: Duration) -> Self {
66            Self {
67                start,
68                step,
69                calls: AtomicU64::new(0),
70            }
71        }
72    }
73
74    impl Clock for SteppingClock {
75        fn now(&self) -> DateTime<Utc> {
76            let n = self.calls.fetch_add(1, Ordering::SeqCst);
77            self.start + self.step * (n as i32)
78        }
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use chrono::Duration;
86
87    #[test]
88    fn fixed_clock_returns_same_instant() {
89        let instant = DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
90            .unwrap()
91            .with_timezone(&Utc);
92        let clock = FixedClock(instant);
93        assert_eq!(clock.now(), instant);
94        assert_eq!(clock.now(), instant);
95    }
96
97    #[test]
98    fn stepping_clock_advances_one_step_per_call() {
99        let start = DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
100            .unwrap()
101            .with_timezone(&Utc);
102        let clock = SteppingClock::new(start, Duration::seconds(10));
103        assert_eq!(clock.now(), start);
104        assert_eq!(clock.now(), start + Duration::seconds(10));
105        assert_eq!(clock.now(), start + Duration::seconds(20));
106    }
107
108    #[test]
109    fn clock_is_usable_behind_the_shared_handle() {
110        let instant = DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
111            .unwrap()
112            .with_timezone(&Utc);
113        let clock: ClockRef = Arc::new(FixedClock(instant));
114        assert_eq!(clock.now(), instant);
115    }
116}