Skip to main content

ddd_toolkit_core/mock/
clock.rs

1use crate::port::clock::Clock;
2
3/// A [`Clock`] that always returns a configured time until [`FixedClock::set`]
4/// changes it.
5///
6/// # Examples
7///
8/// ```
9/// use ddd_toolkit_core::mock::clock::FixedClock;
10/// use ddd_toolkit_core::port::clock::Clock;
11///
12/// let noon = "2026-07-21T12:00:00Z".parse().unwrap();
13/// let clock = FixedClock::new(noon);
14///
15/// // now() returns the configured instant, regardless of wall-clock time...
16/// assert_eq!(clock.now(), noon);
17///
18/// // ...until the test explicitly advances it.
19/// let evening = "2026-07-21T18:00:00Z".parse().unwrap();
20/// clock.set(evening);
21/// assert_eq!(clock.now(), evening);
22/// ```
23#[derive(Debug)]
24pub struct FixedClock(std::sync::Mutex<chrono::DateTime<chrono::Utc>>);
25
26impl FixedClock {
27    /// Creates a `FixedClock` that returns `now` until [`FixedClock::set`]
28    /// is called.
29    pub fn new(now: chrono::DateTime<chrono::Utc>) -> Self {
30        Self(std::sync::Mutex::new(now))
31    }
32
33    /// Changes the time this clock returns.
34    pub fn set(&self, now: chrono::DateTime<chrono::Utc>) {
35        *self.0.lock().unwrap_or_else(|e| e.into_inner()) = now;
36    }
37}
38
39impl Clock for FixedClock {
40    fn now(&self) -> chrono::DateTime<chrono::Utc> {
41        *self.0.lock().unwrap_or_else(|e| e.into_inner())
42    }
43}
44
45#[cfg(test)]
46mod test {
47    use super::*;
48
49    fn utc(rfc3339: &str) -> chrono::DateTime<chrono::Utc> {
50        chrono::DateTime::parse_from_rfc3339(rfc3339)
51            .unwrap()
52            .with_timezone(&chrono::Utc)
53    }
54
55    #[test]
56    fn new_then_now_returns_configured_time() {
57        let time = utc("2026-07-19T00:00:00Z");
58        let clock = FixedClock::new(time);
59
60        assert_eq!(clock.now(), time);
61    }
62
63    #[test]
64    fn now_returns_equal_time_on_repeated_calls() {
65        let time = utc("2026-07-19T00:00:00Z");
66        let clock = FixedClock::new(time);
67
68        assert_eq!(clock.now(), time);
69        assert_eq!(clock.now(), time);
70        assert_eq!(clock.now(), time);
71    }
72
73    #[test]
74    fn set_changes_time_returned_by_now() {
75        let clock = FixedClock::new(utc("2026-07-19T00:00:00Z"));
76
77        let later = utc("2026-07-20T12:34:56Z");
78        clock.set(later);
79
80        assert_eq!(clock.now(), later);
81    }
82
83    #[test]
84    fn set_from_another_thread_is_visible_to_now() {
85        let clock = std::sync::Arc::new(FixedClock::new(utc("2026-07-19T00:00:00Z")));
86
87        let later = utc("2026-07-20T12:34:56Z");
88        let writer = std::sync::Arc::clone(&clock);
89        std::thread::spawn(move || writer.set(later))
90            .join()
91            .unwrap();
92
93        assert_eq!(clock.now(), later);
94    }
95
96    #[test]
97    fn box_dyn_clock_is_usable() {
98        let time = utc("2026-07-19T00:00:00Z");
99        let clock: Box<dyn Clock> = Box::new(FixedClock::new(time));
100
101        assert_eq!(clock.now(), time);
102    }
103
104    #[test]
105    fn debug_format_contains_type_name() {
106        let clock = FixedClock::new(utc("2026-07-19T00:00:00Z"));
107
108        assert!(format!("{clock:?}").contains("FixedClock"));
109    }
110
111    #[test]
112    fn now_returns_extreme_timestamps_as_is() {
113        let clock = FixedClock::new(chrono::DateTime::<chrono::Utc>::MIN_UTC);
114        assert_eq!(clock.now(), chrono::DateTime::<chrono::Utc>::MIN_UTC);
115
116        clock.set(chrono::DateTime::<chrono::Utc>::MAX_UTC);
117        assert_eq!(clock.now(), chrono::DateTime::<chrono::Utc>::MAX_UTC);
118    }
119
120    // A panic while the lock is held poisons the Mutex; now()/set() must
121    // recover the guard instead of panicking forever.
122    #[test]
123    fn clock_recovers_from_poisoned_mutex() {
124        let time = utc("2026-07-19T00:00:00Z");
125        let clock = FixedClock::new(time);
126
127        let poisoned = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
128            let _guard = clock.0.lock().unwrap();
129            panic!("simulated panic while holding the lock");
130        }));
131        assert!(poisoned.is_err());
132        assert!(clock.0.is_poisoned());
133
134        assert_eq!(clock.now(), time);
135    }
136}