Skip to main content

fresh/services/
time_source.rs

1//! Time source abstraction for testability.
2//!
3//! This module provides a `TimeSource` trait that abstracts time-related operations,
4//! allowing production code to use real system time while tests can use a controllable
5//! mock implementation for fast, deterministic testing.
6//!
7//! See `docs/internal/TIMESOURCE_DESIGN.md` for the full design document.
8
9use chrono::{NaiveDate, Utc};
10use std::sync::atomic::{AtomicU64, Ordering};
11use std::sync::Arc;
12use std::time::{Duration, Instant};
13
14/// Abstraction over time-related operations.
15///
16/// This trait allows production code to use real system time while tests
17/// can use a controllable mock implementation for fast, deterministic testing.
18pub trait TimeSource: Send + Sync + std::fmt::Debug {
19    /// Get the current instant for measuring elapsed time.
20    fn now(&self) -> Instant;
21
22    /// Sleep for the specified duration.
23    ///
24    /// In tests, this may be a no-op or advance logical time.
25    fn sleep(&self, duration: Duration);
26
27    /// Get today's date.
28    fn today_date(&self) -> NaiveDate;
29
30    /// Calculate elapsed time since an earlier instant.
31    fn elapsed_since(&self, earlier: Instant) -> Duration {
32        self.now().saturating_duration_since(earlier)
33    }
34}
35
36/// Type alias for shared time source.
37pub type SharedTimeSource = Arc<dyn TimeSource>;
38
39/// Production implementation using actual system time.
40#[derive(Debug, Clone, Copy, Default)]
41pub struct RealTimeSource;
42
43impl RealTimeSource {
44    /// Create a new RealTimeSource.
45    pub fn new() -> Self {
46        Self
47    }
48
49    /// Create a shared RealTimeSource.
50    pub fn shared() -> SharedTimeSource {
51        Arc::new(Self)
52    }
53}
54
55impl TimeSource for RealTimeSource {
56    fn now(&self) -> Instant {
57        Instant::now()
58    }
59
60    fn sleep(&self, duration: Duration) {
61        std::thread::sleep(duration);
62    }
63
64    fn today_date(&self) -> NaiveDate {
65        Utc::now().date_naive()
66    }
67}
68
69/// Test implementation with controllable time.
70///
71/// - `now()` returns a logical instant based on internal counter
72/// - `sleep()` advances logical time (no actual sleeping)
73/// - Time can be advanced manually via `advance()`
74/// - `today_date()` returns a date based on base_date + elapsed days
75///
76/// # Example
77///
78/// ```
79/// use fresh::services::time_source::{TimeSource, TestTimeSource};
80/// use std::time::Duration;
81///
82/// let time = TestTimeSource::new();
83/// let start = time.now();
84///
85/// // No actual sleeping - just advances logical time
86/// time.sleep(Duration::from_secs(5));
87///
88/// assert!(time.elapsed_since(start) >= Duration::from_secs(5));
89/// ```
90#[derive(Debug)]
91pub struct TestTimeSource {
92    /// Logical time in nanoseconds since creation.
93    logical_nanos: AtomicU64,
94    /// Base instant (real time at creation, used for Instant arithmetic).
95    base_instant: Instant,
96    /// Base date for calendar calculations.
97    base_date: NaiveDate,
98}
99
100impl Default for TestTimeSource {
101    fn default() -> Self {
102        Self::new()
103    }
104}
105
106impl TestTimeSource {
107    /// Create a new TestTimeSource with logical time starting at zero.
108    pub fn new() -> Self {
109        Self {
110            logical_nanos: AtomicU64::new(0),
111            base_instant: Instant::now(),
112            base_date: Utc::now().date_naive(),
113        }
114    }
115
116    /// Create a shared TestTimeSource.
117    pub fn shared() -> Arc<Self> {
118        Arc::new(Self::new())
119    }
120
121    /// Advance logical time by the given duration.
122    ///
123    /// This is the primary way to simulate time passage in tests.
124    pub fn advance(&self, duration: Duration) {
125        self.logical_nanos
126            .fetch_add(duration.as_nanos() as u64, Ordering::SeqCst);
127    }
128
129    /// Get the logical elapsed time since creation.
130    pub fn elapsed(&self) -> Duration {
131        Duration::from_nanos(self.logical_nanos.load(Ordering::SeqCst))
132    }
133
134    /// Reset logical time to zero.
135    pub fn reset(&self) {
136        self.logical_nanos.store(0, Ordering::SeqCst);
137    }
138
139    /// Get the current logical time in nanoseconds.
140    pub fn nanos(&self) -> u64 {
141        self.logical_nanos.load(Ordering::SeqCst)
142    }
143}
144
145impl TimeSource for TestTimeSource {
146    fn now(&self) -> Instant {
147        // Return base_instant + logical elapsed time.
148        // This ensures the returned Instant is valid for duration calculations.
149        self.base_instant + self.elapsed()
150    }
151
152    fn sleep(&self, duration: Duration) {
153        // No actual sleeping - just advance logical time.
154        // This makes tests run instantly while still simulating time passage.
155        self.advance(duration);
156    }
157
158    fn today_date(&self) -> NaiveDate {
159        // Calculate days elapsed from logical time
160        let elapsed_days = (self.elapsed().as_secs() / 86400) as i64;
161        self.base_date
162            .checked_add_signed(chrono::Duration::days(elapsed_days))
163            .unwrap_or(self.base_date)
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn real_time_source_now_advances() {
173        let ts = RealTimeSource::new();
174        let t1 = ts.now();
175        std::thread::sleep(Duration::from_millis(1));
176        let t2 = ts.now();
177        assert!(t2 > t1);
178    }
179
180    #[test]
181    fn test_time_source_starts_at_zero() {
182        let ts = TestTimeSource::new();
183        assert_eq!(ts.nanos(), 0);
184        assert_eq!(ts.elapsed(), Duration::ZERO);
185    }
186
187    #[test]
188    fn test_time_source_advance() {
189        let ts = TestTimeSource::new();
190        let start = ts.now();
191
192        ts.advance(Duration::from_secs(5));
193
194        assert_eq!(ts.elapsed(), Duration::from_secs(5));
195        assert!(ts.elapsed_since(start) >= Duration::from_secs(5));
196    }
197
198    #[test]
199    fn test_time_source_sleep_advances_time() {
200        let ts = TestTimeSource::new();
201        let start = ts.now();
202
203        ts.sleep(Duration::from_millis(100));
204
205        assert_eq!(ts.elapsed(), Duration::from_millis(100));
206        assert!(ts.elapsed_since(start) >= Duration::from_millis(100));
207    }
208
209    #[test]
210    fn test_time_source_reset() {
211        let ts = TestTimeSource::new();
212        ts.advance(Duration::from_secs(10));
213        assert_eq!(ts.elapsed(), Duration::from_secs(10));
214
215        ts.reset();
216        assert_eq!(ts.elapsed(), Duration::ZERO);
217    }
218
219    #[test]
220    fn test_time_source_thread_safe() {
221        use std::thread;
222
223        let ts = Arc::new(TestTimeSource::new());
224        let ts_clone = ts.clone();
225
226        let handle = thread::spawn(move || {
227            for _ in 0..100 {
228                ts_clone.advance(Duration::from_millis(1));
229            }
230        });
231
232        for _ in 0..100 {
233            ts.advance(Duration::from_millis(1));
234        }
235
236        handle.join().unwrap();
237
238        assert_eq!(ts.elapsed(), Duration::from_millis(200));
239    }
240
241    #[test]
242    fn shared_time_source_works() {
243        let real: SharedTimeSource = RealTimeSource::shared();
244        let test: SharedTimeSource = TestTimeSource::shared();
245
246        // Both should implement TimeSource
247        let _ = real.now();
248        let _ = test.now();
249    }
250}