Skip to main content

steam_client/utils/
clock.rs

1//! Clock abstraction for testable time operations.
2//!
3//! This module provides a `Clock` trait that abstracts time operations,
4//! enabling deterministic testing of time-dependent logic.
5//!
6//! # Example
7//!
8//! ```rust,ignore
9//! use steam_client::clock::{Clock, SystemClock, MockClock};
10//! use std::time::Duration;
11//!
12//! // Production: use SystemClock
13//! let clock = SystemClock;
14//! let now = clock.now();
15//!
16//! // Testing: use MockClock for deterministic tests
17//! let mock = MockClock::new();
18//! let t1 = mock.now();
19//! mock.advance(Duration::from_secs(10));
20//! let t2 = mock.now();
21//! assert_eq!(t2 - t1, Duration::from_secs(10));
22//!
23//! // Async sleep - MockClock advances time instantly
24//! mock.sleep(Duration::from_secs(5)).await;
25//! assert_eq!(mock.current_offset(), Duration::from_secs(15));
26//! ```
27
28use std::{
29    future::Future,
30    pin::Pin,
31    sync::{Arc, Mutex},
32    time::{Duration, Instant, SystemTime, UNIX_EPOCH},
33};
34
35/// Trait for time operations including getting current time and async sleeping.
36///
37/// This trait abstracts time operations, allowing mock implementations
38/// for deterministic testing of time-dependent logic.
39pub trait Clock: Send + Sync + std::fmt::Debug {
40    /// Get the current instant in time.
41    fn now(&self) -> Instant;
42
43    /// Get the current wall clock time in seconds since the Unix epoch.
44    fn timestamp(&self) -> u64;
45
46    /// Async sleep for the given duration.
47    ///
48    /// For production clocks, this actually waits for the specified duration.
49    /// For mock clocks, this advances the simulated time and returns
50    /// immediately.
51    fn sleep(&self, duration: Duration) -> Pin<Box<dyn Future<Output = ()> + Send + '_>>;
52}
53
54/// System clock implementation using the real system time.
55///
56/// This is the default clock used in production code.
57#[derive(Debug, Clone, Copy, Default)]
58pub struct SystemClock;
59
60impl Clock for SystemClock {
61    fn now(&self) -> Instant {
62        Instant::now()
63    }
64
65    fn timestamp(&self) -> u64 {
66        SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or(Duration::ZERO).as_secs()
67    }
68
69    fn sleep(&self, duration: Duration) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
70        Box::pin(tokio::time::sleep(duration))
71    }
72}
73
74/// Mock clock for testing with controllable time.
75///
76/// Allows tests to advance time deterministically without actual delays.
77///
78/// # Example
79///
80/// ```rust
81/// use std::time::Duration;
82///
83/// use steam_client::utils::clock::{Clock, MockClock};
84///
85/// let clock = MockClock::new();
86///
87/// let t1 = clock.now();
88/// clock.advance(Duration::from_secs(5));
89/// let t2 = clock.now();
90///
91/// assert_eq!(t2 - t1, Duration::from_secs(5));
92/// ```
93#[derive(Debug, Clone)]
94pub struct MockClock {
95    /// The current simulated time, stored as offset from base instant.
96    offset: Arc<Mutex<Duration>>,
97    /// Base instant (captured at creation time).
98    base: Instant,
99    /// Base system time (captured at creation time).
100    base_system: SystemTime,
101}
102
103impl MockClock {
104    /// Create a new mock clock starting at the current time.
105    pub fn new() -> Self {
106        Self { offset: Arc::new(Mutex::new(Duration::ZERO)), base: Instant::now(), base_system: SystemTime::now() }
107    }
108
109    /// Advance the clock by the given duration.
110    ///
111    /// This moves the simulated time forward without blocking.
112    pub fn advance(&self, duration: Duration) {
113        let mut offset = self.offset.lock().expect("mutex poisoned");
114        *offset += duration;
115    }
116
117    /// Set the clock to a specific offset from the base time.
118    pub fn set_offset(&self, offset: Duration) {
119        let mut current = self.offset.lock().expect("mutex poisoned");
120        *current = offset;
121    }
122
123    /// Get the current offset from the base time.
124    pub fn current_offset(&self) -> Duration {
125        *self.offset.lock().expect("mutex poisoned")
126    }
127
128    /// Reset the clock to the base time (zero offset).
129    pub fn reset(&self) {
130        let mut offset = self.offset.lock().expect("mutex poisoned");
131        *offset = Duration::ZERO;
132    }
133}
134
135impl Default for MockClock {
136    fn default() -> Self {
137        Self::new()
138    }
139}
140
141impl Clock for MockClock {
142    fn now(&self) -> Instant {
143        let offset = self.offset.lock().expect("mutex poisoned");
144        self.base + *offset
145    }
146
147    fn timestamp(&self) -> u64 {
148        let offset = self.offset.lock().expect("mutex poisoned");
149        self.base_system.checked_add(*offset).unwrap_or(self.base_system).duration_since(UNIX_EPOCH).unwrap_or(Duration::ZERO).as_secs()
150    }
151
152    fn sleep(&self, duration: Duration) -> Pin<Box<dyn Future<Output = ()> + Send + '_>> {
153        // Advance the mock time immediately and return a completed future
154        self.advance(duration);
155        Box::pin(std::future::ready(()))
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn test_system_clock_advances() {
165        let clock = SystemClock;
166        let t1 = clock.now();
167        std::thread::sleep(Duration::from_millis(10));
168        let t2 = clock.now();
169
170        assert!(t2 > t1);
171    }
172
173    #[test]
174    fn test_system_clock_timestamp() {
175        let clock = SystemClock;
176        let t1 = clock.timestamp();
177        std::thread::sleep(Duration::from_millis(1100));
178        let t2 = clock.timestamp();
179
180        assert!(t2 > t1);
181    }
182
183    #[test]
184    fn test_mock_clock_initial_time() {
185        let clock = MockClock::new();
186        let t1 = clock.now();
187        let t2 = clock.now();
188
189        // Without advancing, time should be the same
190        assert_eq!(t1, t2);
191    }
192
193    #[test]
194    fn test_mock_clock_advance() {
195        let clock = MockClock::new();
196
197        let t1 = clock.now();
198        clock.advance(Duration::from_secs(10));
199        let t2 = clock.now();
200
201        assert_eq!(t2 - t1, Duration::from_secs(10));
202    }
203
204    #[test]
205    fn test_mock_clock_timestamp_advances() {
206        let clock = MockClock::new();
207        let t1 = clock.timestamp();
208
209        clock.advance(Duration::from_secs(10));
210        let t2 = clock.timestamp();
211
212        assert_eq!(t2, t1 + 10);
213    }
214
215    #[test]
216    fn test_mock_clock_multiple_advances() {
217        let clock = MockClock::new();
218
219        let t1 = clock.now();
220        clock.advance(Duration::from_secs(5));
221        clock.advance(Duration::from_secs(3));
222        let t2 = clock.now();
223
224        assert_eq!(t2 - t1, Duration::from_secs(8));
225    }
226
227    #[test]
228    fn test_mock_clock_set_offset() {
229        let clock = MockClock::new();
230
231        clock.set_offset(Duration::from_secs(100));
232        assert_eq!(clock.current_offset(), Duration::from_secs(100));
233    }
234
235    #[test]
236    fn test_mock_clock_reset() {
237        let clock = MockClock::new();
238
239        clock.advance(Duration::from_secs(50));
240        clock.reset();
241
242        assert_eq!(clock.current_offset(), Duration::ZERO);
243    }
244
245    #[test]
246    fn test_mock_clock_clone_shares_state() {
247        let clock = MockClock::new();
248        let clone = clock.clone();
249
250        let t1 = clock.now();
251        clone.advance(Duration::from_secs(20));
252        let t2 = clock.now();
253
254        // Clone should share state with original
255        assert_eq!(t2 - t1, Duration::from_secs(20));
256    }
257
258    #[tokio::test]
259    async fn test_system_clock_sleep() {
260        let clock = SystemClock;
261        let start = clock.now();
262
263        // Sleep for a short duration
264        clock.sleep(Duration::from_millis(10)).await;
265
266        let elapsed = clock.now() - start;
267        // Should have waited at least 10ms
268        assert!(elapsed >= Duration::from_millis(10));
269    }
270
271    #[tokio::test]
272    async fn test_mock_clock_sleep_advances_time() {
273        let clock = MockClock::new();
274
275        assert_eq!(clock.current_offset(), Duration::ZERO);
276
277        // Sleep should advance mock time instantly
278        clock.sleep(Duration::from_secs(10)).await;
279        assert_eq!(clock.current_offset(), Duration::from_secs(10));
280
281        // Multiple sleeps should accumulate
282        clock.sleep(Duration::from_secs(5)).await;
283        assert_eq!(clock.current_offset(), Duration::from_secs(15));
284    }
285
286    #[tokio::test]
287    async fn test_mock_clock_sleep_is_instant() {
288        let clock = MockClock::new();
289
290        // Even a long mock sleep should return instantly
291        let real_start = std::time::Instant::now();
292        clock.sleep(Duration::from_secs(3600)).await; // 1 hour
293        let real_elapsed = real_start.elapsed();
294
295        // Should complete in well under 1 second of real time
296        assert!(real_elapsed < Duration::from_secs(1));
297
298        // But mock time should have advanced
299        assert_eq!(clock.current_offset(), Duration::from_secs(3600));
300    }
301}