fresh/services/
time_source.rs1use chrono::{NaiveDate, Utc};
10use std::sync::atomic::{AtomicU64, Ordering};
11use std::sync::Arc;
12use std::time::{Duration, Instant};
13
14pub trait TimeSource: Send + Sync + std::fmt::Debug {
19 fn now(&self) -> Instant;
21
22 fn sleep(&self, duration: Duration);
26
27 fn today_date_string(&self) -> String;
31
32 fn elapsed_since(&self, earlier: Instant) -> Duration {
34 self.now().saturating_duration_since(earlier)
35 }
36}
37
38pub type SharedTimeSource = Arc<dyn TimeSource>;
40
41#[derive(Debug, Clone, Copy, Default)]
43pub struct RealTimeSource;
44
45impl RealTimeSource {
46 pub fn new() -> Self {
48 Self
49 }
50
51 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#[derive(Debug)]
93pub struct TestTimeSource {
94 logical_nanos: AtomicU64,
96 base_instant: Instant,
98 base_date: NaiveDate,
100}
101
102impl Default for TestTimeSource {
103 fn default() -> Self {
104 Self::new()
105 }
106}
107
108impl TestTimeSource {
109 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 pub fn shared() -> Arc<Self> {
120 Arc::new(Self::new())
121 }
122
123 pub fn advance(&self, duration: Duration) {
127 self.logical_nanos
128 .fetch_add(duration.as_nanos() as u64, Ordering::SeqCst);
129 }
130
131 pub fn elapsed(&self) -> Duration {
133 Duration::from_nanos(self.logical_nanos.load(Ordering::SeqCst))
134 }
135
136 pub fn reset(&self) {
138 self.logical_nanos.store(0, Ordering::SeqCst);
139 }
140
141 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 self.base_instant + self.elapsed()
152 }
153
154 fn sleep(&self, duration: Duration) {
155 self.advance(duration);
158 }
159
160 fn today_date_string(&self) -> String {
161 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 let _ = real.now();
252 let _ = test.now();
253 }
254}