xerv_core/testing/providers/
clock.rs

1//! Clock provider for time abstraction.
2//!
3//! Allows tests to use a mock clock with controllable time, while production
4//! code uses the real system clock.
5
6use parking_lot::Mutex;
7use std::sync::atomic::{AtomicU64, Ordering};
8use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
9
10/// Provider trait for time operations.
11///
12/// This trait abstracts all time-related operations, allowing tests to
13/// control time precisely for deterministic behavior.
14pub trait ClockProvider: Send + Sync {
15    /// Get a monotonic instant (for measuring durations).
16    fn now(&self) -> u64;
17
18    /// Get the current system time as milliseconds since UNIX epoch.
19    fn system_time_millis(&self) -> u64;
20
21    /// Sleep for the specified duration.
22    ///
23    /// In mock implementations, this may return immediately or track
24    /// pending sleeps for manual advancement.
25    fn sleep(
26        &self,
27        duration: Duration,
28    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send + '_>>;
29
30    /// Advance time by the specified duration (mock-only operation).
31    ///
32    /// Real implementations should do nothing.
33    fn advance(&self, duration: Duration);
34
35    /// Check if this is a mock clock.
36    fn is_mock(&self) -> bool;
37}
38
39/// Real clock that uses system time.
40#[derive(Debug, Clone)]
41pub struct RealClock {
42    start: Instant,
43}
44
45impl RealClock {
46    /// Create a new real clock.
47    pub fn new() -> Self {
48        Self {
49            start: Instant::now(),
50        }
51    }
52}
53
54impl Default for RealClock {
55    fn default() -> Self {
56        Self::new()
57    }
58}
59
60impl ClockProvider for RealClock {
61    fn now(&self) -> u64 {
62        self.start.elapsed().as_nanos() as u64
63    }
64
65    fn system_time_millis(&self) -> u64 {
66        SystemTime::now()
67            .duration_since(UNIX_EPOCH)
68            .expect("System time before UNIX epoch")
69            .as_millis() as u64
70    }
71
72    fn sleep(
73        &self,
74        duration: Duration,
75    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send + '_>> {
76        Box::pin(async move {
77            tokio::time::sleep(duration).await;
78        })
79    }
80
81    fn advance(&self, _duration: Duration) {
82        // Real clock cannot be manually advanced
83    }
84
85    fn is_mock(&self) -> bool {
86        false
87    }
88}
89
90/// Mock clock for testing with controllable time.
91///
92/// The mock clock starts at a fixed or specified time and only advances
93/// when explicitly told to via `advance()`.
94pub struct MockClock {
95    /// Current time in nanoseconds since start.
96    current_nanos: AtomicU64,
97    /// System time in milliseconds since UNIX epoch.
98    system_time_millis: AtomicU64,
99    /// Pending sleeps that should be woken when time is advanced.
100    pending_sleeps: Mutex<Vec<PendingSleep>>,
101}
102
103struct PendingSleep {
104    wake_at_nanos: u64,
105    waker: Option<std::task::Waker>,
106}
107
108impl MockClock {
109    /// Create a mock clock starting at time zero.
110    pub fn new() -> Self {
111        Self {
112            current_nanos: AtomicU64::new(0),
113            system_time_millis: AtomicU64::new(0),
114            pending_sleeps: Mutex::new(Vec::new()),
115        }
116    }
117
118    /// Create a mock clock fixed at the specified ISO 8601 time.
119    ///
120    /// # Example
121    ///
122    /// ```
123    /// use xerv_core::testing::{MockClock, ClockProvider};
124    ///
125    /// let clock = MockClock::fixed("2024-01-15T10:30:00Z");
126    /// assert!(clock.system_time_millis() > 0);
127    /// ```
128    pub fn fixed(iso_time: &str) -> Self {
129        let dt = chrono::DateTime::parse_from_rfc3339(iso_time)
130            .expect("Invalid ISO 8601 datetime format");
131        let millis = dt.timestamp_millis() as u64;
132
133        Self {
134            current_nanos: AtomicU64::new(0),
135            system_time_millis: AtomicU64::new(millis),
136            pending_sleeps: Mutex::new(Vec::new()),
137        }
138    }
139
140    /// Create a mock clock starting at the current system time.
141    pub fn at_now() -> Self {
142        let millis = SystemTime::now()
143            .duration_since(UNIX_EPOCH)
144            .expect("System time before UNIX epoch")
145            .as_millis() as u64;
146
147        Self {
148            current_nanos: AtomicU64::new(0),
149            system_time_millis: AtomicU64::new(millis),
150            pending_sleeps: Mutex::new(Vec::new()),
151        }
152    }
153
154    /// Get the current monotonic time in nanoseconds.
155    pub fn current_nanos(&self) -> u64 {
156        self.current_nanos.load(Ordering::SeqCst)
157    }
158
159    /// Wake any pending sleeps whose deadline has passed.
160    fn wake_expired_sleeps(&self, current: u64) {
161        let mut sleeps = self.pending_sleeps.lock();
162        sleeps.retain_mut(|sleep| {
163            if sleep.wake_at_nanos <= current {
164                if let Some(waker) = sleep.waker.take() {
165                    waker.wake();
166                }
167                false // Remove from list
168            } else {
169                true // Keep in list
170            }
171        });
172    }
173}
174
175impl Default for MockClock {
176    fn default() -> Self {
177        Self::new()
178    }
179}
180
181impl ClockProvider for MockClock {
182    fn now(&self) -> u64 {
183        self.current_nanos.load(Ordering::SeqCst)
184    }
185
186    fn system_time_millis(&self) -> u64 {
187        self.system_time_millis.load(Ordering::SeqCst)
188    }
189
190    fn sleep(
191        &self,
192        duration: Duration,
193    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send + '_>> {
194        let wake_at = self.current_nanos.load(Ordering::SeqCst) + duration.as_nanos() as u64;
195
196        // If already past the deadline, return immediately
197        if self.current_nanos.load(Ordering::SeqCst) >= wake_at {
198            return Box::pin(std::future::ready(()));
199        }
200
201        // Create a future that will complete when time is advanced past wake_at
202        let clock_ref = self as *const Self;
203
204        Box::pin(MockSleepFuture {
205            wake_at,
206            clock: clock_ref,
207            registered: false,
208        })
209    }
210
211    fn advance(&self, duration: Duration) {
212        let nanos = duration.as_nanos() as u64;
213        let new_time = self.current_nanos.fetch_add(nanos, Ordering::SeqCst) + nanos;
214
215        // Also advance system time
216        let millis = duration.as_millis() as u64;
217        self.system_time_millis.fetch_add(millis, Ordering::SeqCst);
218
219        // Wake any sleeps that should now complete
220        self.wake_expired_sleeps(new_time);
221    }
222
223    fn is_mock(&self) -> bool {
224        true
225    }
226}
227
228/// Future returned by MockClock::sleep().
229struct MockSleepFuture {
230    wake_at: u64,
231    clock: *const MockClock,
232    registered: bool,
233}
234
235// SAFETY: The clock pointer is valid for the lifetime of the test
236unsafe impl Send for MockSleepFuture {}
237
238impl std::future::Future for MockSleepFuture {
239    type Output = ();
240
241    fn poll(
242        mut self: std::pin::Pin<&mut Self>,
243        cx: &mut std::task::Context<'_>,
244    ) -> std::task::Poll<Self::Output> {
245        // SAFETY: clock pointer is valid during test
246        let clock = unsafe { &*self.clock };
247
248        let current = clock.current_nanos.load(Ordering::SeqCst);
249
250        if current >= self.wake_at {
251            std::task::Poll::Ready(())
252        } else {
253            // Register for wake-up if not already registered
254            if !self.registered {
255                let mut sleeps = clock.pending_sleeps.lock();
256                sleeps.push(PendingSleep {
257                    wake_at_nanos: self.wake_at,
258                    waker: Some(cx.waker().clone()),
259                });
260                self.registered = true;
261            } else {
262                // Update waker in case it changed
263                let mut sleeps = clock.pending_sleeps.lock();
264                for sleep in sleeps.iter_mut() {
265                    if sleep.wake_at_nanos == self.wake_at {
266                        sleep.waker = Some(cx.waker().clone());
267                        break;
268                    }
269                }
270            }
271            std::task::Poll::Pending
272        }
273    }
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279    use std::sync::Arc;
280
281    #[test]
282    fn real_clock_advances() {
283        let clock = RealClock::new();
284        let t1 = clock.now();
285        std::thread::sleep(Duration::from_millis(10));
286        let t2 = clock.now();
287        assert!(t2 > t1);
288    }
289
290    #[test]
291    fn mock_clock_fixed_time() {
292        let clock = MockClock::fixed("2024-01-15T10:30:00Z");
293        let millis = clock.system_time_millis();
294        // 2024-01-15T10:30:00Z = 1705314600000 ms since epoch
295        assert_eq!(millis, 1705314600000);
296    }
297
298    #[test]
299    fn mock_clock_does_not_advance_automatically() {
300        let clock = MockClock::new();
301        let t1 = clock.now();
302        std::thread::sleep(Duration::from_millis(10));
303        let t2 = clock.now();
304        assert_eq!(t1, t2);
305    }
306
307    #[test]
308    fn mock_clock_advance() {
309        let clock = MockClock::new();
310        assert_eq!(clock.now(), 0);
311
312        clock.advance(Duration::from_secs(1));
313        assert_eq!(clock.now(), 1_000_000_000);
314
315        clock.advance(Duration::from_millis(500));
316        assert_eq!(clock.now(), 1_500_000_000);
317    }
318
319    #[test]
320    fn mock_clock_system_time_advances() {
321        let clock = MockClock::fixed("2024-01-15T10:30:00Z");
322        let t1 = clock.system_time_millis();
323
324        clock.advance(Duration::from_secs(60));
325        let t2 = clock.system_time_millis();
326
327        assert_eq!(t2 - t1, 60_000);
328    }
329
330    #[tokio::test]
331    async fn mock_clock_sleep_completes_on_advance() {
332        let clock = Arc::new(MockClock::new());
333        let clock_ref = Arc::clone(&clock);
334
335        // Spawn a task that sleeps
336        let handle = tokio::spawn(async move {
337            clock_ref.sleep(Duration::from_secs(1)).await;
338            true
339        });
340
341        // Give the task a moment to start
342        tokio::task::yield_now().await;
343
344        // Advance time past the sleep duration
345        clock.advance(Duration::from_secs(2));
346
347        // The sleep should now complete
348        let result = tokio::time::timeout(Duration::from_millis(100), handle)
349            .await
350            .expect("Timed out waiting for sleep")
351            .expect("Task panicked");
352
353        assert!(result);
354    }
355}