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