//! Injected clock for deterministic time in tests and consistent frame timestamps. //! //! All relative-time rendering (e.g., "3h ago") uses [`Clock::now()`] rather //! than wall-clock time directly. This enables: //! - Deterministic snapshot tests via [`FakeClock`] //! - Consistent timestamps within a single frame render pass use std::sync::{Arc, Mutex}; use chrono::{DateTime, TimeDelta, Utc}; /// Trait for obtaining the current time. /// /// Inject via `Arc` to allow swapping between real and fake clocks. pub trait Clock: Send + Sync { /// Returns the current time. fn now(&self) -> DateTime; } // --------------------------------------------------------------------------- // SystemClock // --------------------------------------------------------------------------- /// Real wall-clock time via `chrono::Utc::now()`. #[derive(Debug, Clone, Copy)] pub struct SystemClock; impl Clock for SystemClock { fn now(&self) -> DateTime { Utc::now() } } // --------------------------------------------------------------------------- // FakeClock // --------------------------------------------------------------------------- /// A controllable clock for tests. Returns a frozen time that can be /// advanced or set explicitly. /// /// `FakeClock` is `Clone` (shares the inner `Arc`) and `Send + Sync` /// for use across `Cmd::task` threads. #[derive(Debug, Clone)] pub struct FakeClock { inner: Arc>>, } impl FakeClock { /// Create a fake clock frozen at the given time. #[must_use] pub fn new(time: DateTime) -> Self { Self { inner: Arc::new(Mutex::new(time)), } } /// Advance the clock by `duration`. Uses `checked_add` to handle overflow /// gracefully — if the addition would overflow, the time is not changed. pub fn advance(&self, duration: TimeDelta) { let mut guard = self.inner.lock().expect("FakeClock mutex poisoned"); if let Some(advanced) = guard.checked_add_signed(duration) { *guard = advanced; } } /// Set the clock to an exact time. pub fn set(&self, time: DateTime) { let mut guard = self.inner.lock().expect("FakeClock mutex poisoned"); *guard = time; } } impl Clock for FakeClock { fn now(&self) -> DateTime { *self.inner.lock().expect("FakeClock mutex poisoned") } } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- #[cfg(test)] mod tests { use super::*; use chrono::TimeZone; fn fixed_time() -> DateTime { Utc.with_ymd_and_hms(2026, 2, 12, 12, 0, 0).unwrap() } #[test] fn test_fake_clock_frozen() { let clock = FakeClock::new(fixed_time()); let t1 = clock.now(); let t2 = clock.now(); assert_eq!(t1, t2); assert_eq!(t1, fixed_time()); } #[test] fn test_fake_clock_advance() { let clock = FakeClock::new(fixed_time()); clock.advance(TimeDelta::hours(3)); let expected = Utc.with_ymd_and_hms(2026, 2, 12, 15, 0, 0).unwrap(); assert_eq!(clock.now(), expected); } #[test] fn test_fake_clock_set() { let clock = FakeClock::new(fixed_time()); let new_time = Utc.with_ymd_and_hms(2030, 1, 1, 0, 0, 0).unwrap(); clock.set(new_time); assert_eq!(clock.now(), new_time); } #[test] fn test_fake_clock_clone_shares_state() { let clock1 = FakeClock::new(fixed_time()); let clock2 = clock1.clone(); clock1.advance(TimeDelta::minutes(30)); // Both clones see the advanced time. assert_eq!(clock1.now(), clock2.now()); } #[test] fn test_system_clock_returns_reasonable_time() { let clock = SystemClock; let now = clock.now(); // Sanity: time should be after 2025. assert!(now.year() >= 2025); } #[test] fn test_fake_clock_is_send_sync() { fn assert_send_sync() {} assert_send_sync::(); assert_send_sync::(); } #[test] fn test_clock_trait_object_works() { let fake: Arc = Arc::new(FakeClock::new(fixed_time())); assert_eq!(fake.now(), fixed_time()); let real: Arc = Arc::new(SystemClock); let _ = real.now(); // Just verify it doesn't panic. } use chrono::Datelike; }