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}