Development Documentation (main branch) - For stable release docs, see docs.rs/eidetica

eidetica/
clock.rs

1//! Time provider abstraction
2//!
3//! This module provides a [`Clock`] trait that abstracts over time sources,
4//! allowing production code to use real system time while tests can use
5//! controllable mock time.
6//!
7//! # Example
8//!
9//! ```
10//! use eidetica::{Clock, SystemClock};
11//!
12//! let clock = SystemClock;
13//! let millis = clock.now_millis();
14//! let rfc3339 = clock.now_rfc3339();
15//! ```
16
17use std::fmt::Debug;
18use std::time::{SystemTime, UNIX_EPOCH};
19
20#[cfg(any(test, feature = "testing"))]
21use std::sync::Mutex;
22
23/// A time provider for getting current timestamps.
24///
25/// This trait abstracts over time sources to enable:
26/// - Controllable time in tests (fixed starting point, manual advance)
27/// - Monotonic timestamps within a single clock instance
28pub trait Clock: Send + Sync + Debug {
29    /// Returns the current time as milliseconds since Unix epoch.
30    fn now_millis(&self) -> u64;
31
32    /// Returns the current time as an RFC3339-formatted string.
33    fn now_rfc3339(&self) -> String;
34
35    /// Get current time as seconds since Unix epoch.
36    ///
37    /// Convenience method that converts milliseconds to seconds.
38    fn now_secs(&self) -> i64 {
39        (self.now_millis() / 1000) as i64
40    }
41}
42
43/// Production clock using real system time.
44///
45/// This is the default clock implementation used in production code.
46/// It calls through to [`std::time::SystemTime`] and [`chrono::Utc`].
47#[derive(Debug, Clone, Copy, Default)]
48pub struct SystemClock;
49
50impl Clock for SystemClock {
51    fn now_millis(&self) -> u64 {
52        SystemTime::now()
53            .duration_since(UNIX_EPOCH)
54            .map(|d| d.as_millis() as u64)
55            .unwrap_or(0)
56    }
57
58    fn now_rfc3339(&self) -> String {
59        chrono::Utc::now().to_rfc3339()
60    }
61}
62
63/// Test clock with auto-advancing time.
64///
65/// This clock auto-advances on each `now_millis()` call, providing monotonically
66/// increasing timestamps. Use `hold()` to temporarily freeze the clock for tests
67/// needing stable timestamps.
68///
69/// Note: While timestamps are monotonic, concurrent threads may receive values
70/// in non-deterministic order depending on scheduling.
71///
72/// # Example
73///
74/// ```
75/// use eidetica::{Clock, FixedClock};
76///
77/// let clock = FixedClock::new(1000);
78/// let t1 = clock.now_millis();  // Returns 1000, then advances
79/// let t2 = clock.now_millis();  // Returns next value
80/// assert!(t2 > t1);
81///
82/// // Use hold() for stable timestamps
83/// {
84///     let _hold = clock.hold();
85///     let a = clock.now_millis();
86///     let b = clock.now_millis();
87///     assert_eq!(a, b);  // Frozen
88/// }
89/// ```
90#[cfg(any(test, feature = "testing"))]
91pub struct FixedClock {
92    state: Mutex<FixedClockState>,
93}
94
95#[cfg(any(test, feature = "testing"))]
96struct FixedClockState {
97    millis: u64,
98    held: bool,
99}
100
101/// RAII guard that freezes a [`FixedClock`] while held.
102///
103/// The clock resumes auto-advancing when this guard is dropped.
104#[cfg(any(test, feature = "testing"))]
105pub struct ClockHold<'a>(&'a FixedClock);
106
107#[cfg(any(test, feature = "testing"))]
108impl Drop for ClockHold<'_> {
109    fn drop(&mut self) {
110        self.0.state.lock().unwrap().held = false;
111    }
112}
113
114#[cfg(any(test, feature = "testing"))]
115impl FixedClock {
116    /// Create a new fixed clock with the given initial time in milliseconds.
117    pub fn new(millis: u64) -> Self {
118        Self {
119            state: Mutex::new(FixedClockState {
120                millis,
121                held: false,
122            }),
123        }
124    }
125
126    /// Hold the clock, preventing auto-advance until the guard is dropped.
127    ///
128    /// Returns an RAII guard that releases the hold when dropped.
129    /// Use a scoped block for clean auto-release:
130    ///
131    /// ```
132    /// # use eidetica::{Clock, FixedClock};
133    /// let clock = FixedClock::new(1000);
134    /// let expected = {
135    ///     let _hold = clock.hold();
136    ///     clock.now_millis()  // Frozen value
137    /// };  // hold released
138    /// ```
139    pub fn hold(&self) -> ClockHold<'_> {
140        self.state.lock().unwrap().held = true;
141        ClockHold(self)
142    }
143
144    /// Advance the clock by the given number of milliseconds.
145    pub fn advance(&self, ms: u64) {
146        self.state.lock().unwrap().millis += ms;
147    }
148
149    /// Set the clock to a specific time in milliseconds.
150    pub fn set(&self, ms: u64) {
151        self.state.lock().unwrap().millis = ms;
152    }
153
154    /// Get the current time without advancing (even if not held).
155    pub fn get(&self) -> u64 {
156        self.state.lock().unwrap().millis
157    }
158}
159
160#[cfg(any(test, feature = "testing"))]
161impl Clock for FixedClock {
162    fn now_millis(&self) -> u64 {
163        let mut state = self.state.lock().unwrap();
164        if state.held {
165            state.millis
166        } else {
167            let t = state.millis;
168            state.millis += 1;
169            t
170        }
171    }
172
173    fn now_rfc3339(&self) -> String {
174        use chrono::{TimeZone, Utc};
175        // Use now_millis() to get consistent auto-advance behavior
176        let millis = self.now_millis();
177        let secs = (millis / 1000) as i64;
178        let nanos = ((millis % 1000) * 1_000_000) as u32;
179        Utc.timestamp_opt(secs, nanos)
180            .single()
181            .map(|dt| dt.to_rfc3339())
182            .unwrap_or_else(|| "1970-01-01T00:00:00+00:00".to_string())
183    }
184}
185
186#[cfg(any(test, feature = "testing"))]
187impl Default for FixedClock {
188    fn default() -> Self {
189        // Default to a reasonable timestamp (2024-01-01 00:00:00 UTC)
190        Self::new(1704067200000)
191    }
192}
193
194#[cfg(any(test, feature = "testing"))]
195impl Clone for FixedClock {
196    fn clone(&self) -> Self {
197        // Clone creates independent clock at current value, not held
198        Self::new(self.get())
199    }
200}
201
202#[cfg(any(test, feature = "testing"))]
203impl Debug for FixedClock {
204    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
205        let state = self.state.lock().unwrap();
206        f.debug_struct("FixedClock")
207            .field("millis", &state.millis)
208            .field("held", &state.held)
209            .finish()
210    }
211}
212
213#[cfg(test)]
214mod fixed_clock_tests {
215    use super::*;
216
217    #[test]
218    fn fixed_clock_auto_advances() {
219        let clock = FixedClock::new(1000);
220        let t1 = clock.now_millis();
221        assert_eq!(t1, 1000); // Initial value correct
222        let t2 = clock.now_millis();
223        let t3 = clock.now_millis();
224        assert!(t2 > t1); // Advances after each call
225        assert!(t3 > t2);
226    }
227
228    #[test]
229    fn fixed_clock_get_does_not_advance() {
230        let clock = FixedClock::new(1000);
231        let initial = clock.get();
232        assert_eq!(clock.get(), initial); // get() doesn't change value
233        assert_eq!(clock.get(), initial);
234        let after_now = clock.now_millis(); // now_millis() advances
235        assert!(clock.get() > initial); // Value changed after now_millis()
236        assert_eq!(after_now, initial); // now_millis() returned the pre-advance value
237    }
238
239    #[test]
240    fn fixed_clock_hold_freezes() {
241        let clock = FixedClock::new(1000);
242        let frozen_value = {
243            let _hold = clock.hold();
244            let v1 = clock.now_millis();
245            let v2 = clock.now_millis();
246            let v3 = clock.now_millis();
247            assert_eq!(v1, v2); // Frozen - no advance
248            assert_eq!(v2, v3);
249            v1
250        };
251        // After hold drops, auto-advance resumes
252        let t1 = clock.now_millis();
253        let t2 = clock.now_millis();
254        assert_eq!(t1, frozen_value); // First call returns frozen value
255        assert!(t2 > t1); // Then advances again
256    }
257
258    #[test]
259    fn fixed_clock_manual_advance() {
260        let clock = FixedClock::new(1000);
261        clock.advance(500);
262        assert_eq!(clock.get(), 1500);
263    }
264
265    #[test]
266    fn fixed_clock_set() {
267        let clock = FixedClock::new(1000);
268        clock.set(5000);
269        assert_eq!(clock.get(), 5000);
270    }
271
272    #[test]
273    fn fixed_clock_rfc3339() {
274        // 2024-01-01 00:00:00 UTC = 1704067200000 ms
275        let clock = FixedClock::new(1704067200000);
276        let _hold = clock.hold();
277        let rfc3339 = clock.now_rfc3339();
278        assert!(rfc3339.starts_with("2024-01-01T00:00:00"));
279    }
280}