Skip to main content

torrust_tracker_deployer_lib/testing/
mock_clock.rs

1//! Mock clock for testing
2//!
3//! This module provides a mock clock implementation that allows
4//! controlling time in tests for deterministic behavior.
5
6use chrono::{DateTime, Duration, Utc};
7use std::sync::Arc;
8
9use parking_lot::Mutex;
10
11use crate::shared::Clock;
12
13/// Mock clock for testing that allows controlling time
14///
15/// This clock implementation allows tests to:
16/// - Set a fixed time point
17/// - Advance time manually without actual delays
18/// - Make time-dependent tests deterministic
19///
20/// The clock uses interior mutability to allow advancing time
21/// while implementing the `Clock` trait which takes `&self`.
22///
23/// # Example
24///
25/// ```rust
26/// use torrust_tracker_deployer_lib::testing::MockClock;
27/// use torrust_tracker_deployer_lib::shared::Clock;
28/// use chrono::{TimeZone, Utc};
29///
30/// let fixed_time = Utc.with_ymd_and_hms(2025, 10, 7, 12, 0, 0).unwrap();
31/// let clock = MockClock::new(fixed_time);
32///
33/// // Time is fixed
34/// assert_eq!(clock.now(), fixed_time);
35/// assert_eq!(clock.now(), fixed_time); // Still the same
36///
37/// // Advance time
38/// clock.advance_secs(60);
39/// let expected = Utc.with_ymd_and_hms(2025, 10, 7, 12, 1, 0).unwrap();
40/// assert_eq!(clock.now(), expected);
41/// ```
42#[derive(Debug, Clone)]
43pub struct MockClock {
44    /// Current time maintained by the mock clock
45    current_time: Arc<Mutex<DateTime<Utc>>>,
46}
47
48impl MockClock {
49    /// Create a new mock clock with a fixed starting time
50    ///
51    /// # Example
52    ///
53    /// ```rust
54    /// use torrust_tracker_deployer_lib::testing::MockClock;
55    /// use chrono::{TimeZone, Utc};
56    ///
57    /// let fixed_time = Utc.with_ymd_and_hms(2025, 10, 7, 12, 0, 0).unwrap();
58    /// let clock = MockClock::new(fixed_time);
59    /// ```
60    #[must_use]
61    pub fn new(initial_time: DateTime<Utc>) -> Self {
62        Self {
63            current_time: Arc::new(Mutex::new(initial_time)),
64        }
65    }
66
67    /// Advance the clock by the specified duration
68    ///
69    /// # Panics
70    ///
71    /// Panics if the internal mutex is poisoned (which would indicate a panic occurred
72    /// while holding the lock in another thread).
73    ///
74    /// # Example
75    ///
76    /// ```rust
77    /// use torrust_tracker_deployer_lib::testing::MockClock;
78    /// use torrust_tracker_deployer_lib::shared::Clock;
79    /// use chrono::{Duration, TimeZone, Utc};
80    ///
81    /// let initial = Utc.with_ymd_and_hms(2025, 10, 7, 12, 0, 0).unwrap();
82    /// let clock = MockClock::new(initial);
83    ///
84    /// clock.advance(Duration::hours(2));
85    /// let expected = Utc.with_ymd_and_hms(2025, 10, 7, 14, 0, 0).unwrap();
86    /// assert_eq!(clock.now(), expected);
87    /// ```
88    pub fn advance(&self, duration: Duration) {
89        let mut time = self.current_time.lock();
90        *time += duration;
91    }
92
93    /// Advance the clock by the specified number of seconds
94    ///
95    /// Convenience method for advancing time without creating a `Duration`.
96    ///
97    /// # Example
98    ///
99    /// ```rust
100    /// use torrust_tracker_deployer_lib::testing::MockClock;
101    /// use torrust_tracker_deployer_lib::shared::Clock;
102    /// use chrono::{TimeZone, Utc};
103    ///
104    /// let initial = Utc.with_ymd_and_hms(2025, 10, 7, 12, 0, 0).unwrap();
105    /// let clock = MockClock::new(initial);
106    ///
107    /// clock.advance_secs(30);
108    /// let expected = Utc.with_ymd_and_hms(2025, 10, 7, 12, 0, 30).unwrap();
109    /// assert_eq!(clock.now(), expected);
110    /// ```
111    pub fn advance_secs(&self, secs: i64) {
112        self.advance(Duration::seconds(secs));
113    }
114
115    /// Set the clock to a specific time
116    ///
117    /// # Panics
118    ///
119    /// Panics if the internal mutex is poisoned (which would indicate a panic occurred
120    /// while holding the lock in another thread).
121    ///
122    /// # Example
123    ///
124    /// ```rust
125    /// use torrust_tracker_deployer_lib::testing::MockClock;
126    /// use torrust_tracker_deployer_lib::shared::Clock;
127    /// use chrono::{TimeZone, Utc};
128    ///
129    /// let initial = Utc.with_ymd_and_hms(2025, 10, 7, 12, 0, 0).unwrap();
130    /// let clock = MockClock::new(initial);
131    ///
132    /// let new_time = Utc.with_ymd_and_hms(2025, 12, 25, 18, 30, 0).unwrap();
133    /// clock.set_time(new_time);
134    /// assert_eq!(clock.now(), new_time);
135    /// ```
136    pub fn set_time(&self, time: DateTime<Utc>) {
137        let mut current = self.current_time.lock();
138        *current = time;
139    }
140}
141
142impl Clock for MockClock {
143    fn now(&self) -> DateTime<Utc> {
144        *self.current_time.lock()
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use chrono::TimeZone;
152
153    #[test]
154    fn it_should_return_fixed_time_when_not_advanced() {
155        let fixed_time = Utc.with_ymd_and_hms(2025, 10, 7, 12, 0, 0).unwrap();
156        let clock = MockClock::new(fixed_time);
157
158        assert_eq!(clock.now(), fixed_time);
159        assert_eq!(clock.now(), fixed_time);
160        assert_eq!(clock.now(), fixed_time);
161    }
162
163    #[test]
164    fn it_should_advance_time_by_duration() {
165        let initial = Utc.with_ymd_and_hms(2025, 10, 7, 12, 0, 0).unwrap();
166        let clock = MockClock::new(initial);
167
168        clock.advance(Duration::hours(2) + Duration::minutes(30));
169
170        let expected = Utc.with_ymd_and_hms(2025, 10, 7, 14, 30, 0).unwrap();
171        assert_eq!(clock.now(), expected);
172    }
173
174    #[test]
175    fn it_should_advance_time_by_seconds() {
176        let initial = Utc.with_ymd_and_hms(2025, 10, 7, 12, 0, 0).unwrap();
177        let clock = MockClock::new(initial);
178
179        clock.advance_secs(90);
180
181        let expected = Utc.with_ymd_and_hms(2025, 10, 7, 12, 1, 30).unwrap();
182        assert_eq!(clock.now(), expected);
183    }
184
185    #[test]
186    fn it_should_set_time_to_specific_point() {
187        let initial = Utc.with_ymd_and_hms(2025, 10, 7, 12, 0, 0).unwrap();
188        let clock = MockClock::new(initial);
189
190        let new_time = Utc.with_ymd_and_hms(2025, 12, 25, 18, 30, 0).unwrap();
191        clock.set_time(new_time);
192
193        assert_eq!(clock.now(), new_time);
194    }
195
196    #[test]
197    fn it_should_support_multiple_advances() {
198        let initial = Utc.with_ymd_and_hms(2025, 10, 7, 12, 0, 0).unwrap();
199        let clock = MockClock::new(initial);
200
201        clock.advance_secs(30);
202        clock.advance_secs(30);
203        clock.advance_secs(30);
204
205        let expected = Utc.with_ymd_and_hms(2025, 10, 7, 12, 1, 30).unwrap();
206        assert_eq!(clock.now(), expected);
207    }
208
209    #[test]
210    fn it_should_be_clonable() {
211        let initial = Utc.with_ymd_and_hms(2025, 10, 7, 12, 0, 0).unwrap();
212        let clock1 = MockClock::new(initial);
213        let clock2 = clock1.clone();
214
215        // Both clones share the same time
216        clock1.advance_secs(60);
217        assert_eq!(clock1.now(), clock2.now());
218    }
219}