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