qubit_clock/mock_clock.rs
1/*******************************************************************************
2 *
3 * Copyright (c) 2025 - 2026 Haixing Hu.
4 *
5 * SPDX-License-Identifier: Apache-2.0
6 *
7 * Licensed under the Apache License, Version 2.0.
8 *
9 ******************************************************************************/
10//! Mock clock implementation for testing.
11//!
12//! This module provides [`MockClock`], a controllable clock implementation
13//! designed for testing scenarios where precise control over time is needed.
14//!
15
16use crate::{
17 Clock,
18 ControllableClock,
19 MockClockProgression,
20 MonotonicClock,
21 SystemClock,
22};
23use chrono::{
24 DateTime,
25 Duration,
26 Utc,
27};
28use std::sync::{
29 Arc,
30 Mutex,
31 MutexGuard,
32};
33
34/// A controllable clock implementation for testing.
35///
36/// `MockClock` allows you to adjust logical time, making it useful for testing
37/// time-dependent code. Readings are frozen after construction by default.
38/// [`set_time()`](ControllableClock::set_time) reanchors the logical time
39/// without changing the current progression mode or auto-advance settings.
40///
41/// # Features
42///
43/// - Align the logical current time to a specific time
44/// - Advance the clock by a duration
45/// - Automatically advance time on each call
46/// - Switch between frozen and monotonic progression
47/// - Reset to the initial creation state
48///
49/// # Thread Safety
50///
51/// This type is thread-safe, using `Arc<Mutex<>>` internally to protect its
52/// mutable state.
53///
54/// # Examples
55///
56/// ```
57/// use qubit_clock::{Clock, ControllableClock, MockClock};
58/// use chrono::{DateTime, Duration, Utc};
59///
60/// let clock = MockClock::new();
61///
62/// // Set to a specific time
63/// let fixed_time = DateTime::parse_from_rfc3339(
64/// "2024-01-01T00:00:00Z"
65/// ).unwrap().with_timezone(&Utc);
66/// clock.set_time(fixed_time);
67/// assert_eq!(clock.time(), fixed_time);
68///
69/// // Advance by 1 hour
70/// clock.add_duration(Duration::hours(1));
71/// assert_eq!(clock.time(), fixed_time + Duration::hours(1));
72///
73/// // Reset to initial state
74/// clock.reset();
75/// ```
76///
77#[derive(Debug, Clone)]
78pub struct MockClock {
79 inner: Arc<Mutex<MockClockInner>>,
80}
81
82#[derive(Debug)]
83struct MockClockInner {
84 /// The frozen time captured when this clock was created.
85 initial_time: i64,
86 /// The progression mode captured when this clock was created.
87 initial_progression: MockClockProgression,
88 /// The epoch time to use as the base (milliseconds since epoch).
89 epoch: i64,
90 /// The monotonic clock used when monotonic progression is enabled.
91 monotonic_clock: MonotonicClock,
92 /// The monotonic reading corresponding to `epoch`.
93 monotonic_base_millis: i64,
94 /// The current progression mode.
95 progression: MockClockProgression,
96 /// Additional milliseconds to add to the current time.
97 millis_to_add: i64,
98 /// Milliseconds to add on each call to `millis()`.
99 millis_to_add_each_time: i64,
100 /// Whether to automatically add `millis_to_add_each_time` on each call.
101 add_every_time: bool,
102}
103
104impl MockClock {
105 #[inline]
106 fn lock_inner(&self) -> MutexGuard<'_, MockClockInner> {
107 match self.inner.lock() {
108 Ok(guard) => guard,
109 Err(poisoned) => poisoned.into_inner(),
110 }
111 }
112
113 #[inline]
114 fn current_millis(inner: &MockClockInner) -> i64 {
115 let elapsed = if inner.progression.is_monotonic() {
116 inner
117 .monotonic_clock
118 .millis()
119 .saturating_sub(inner.monotonic_base_millis)
120 } else {
121 0
122 };
123 inner
124 .epoch
125 .saturating_add(elapsed)
126 .saturating_add(inner.millis_to_add)
127 }
128
129 #[inline]
130 fn rebase_at_current(inner: &mut MockClockInner) {
131 let current = Self::current_millis(inner);
132 inner.epoch = current;
133 inner.millis_to_add = 0;
134 inner.monotonic_base_millis = inner.monotonic_clock.millis();
135 }
136
137 /// Creates a new `MockClock`.
138 ///
139 /// The clock is initialized with the current system time and remains
140 /// frozen at that instant until adjusted by the control methods or switched
141 /// to monotonic progression.
142 ///
143 /// # Returns
144 ///
145 /// A new `MockClock` instance.
146 ///
147 /// # Examples
148 ///
149 /// ```
150 /// use qubit_clock::MockClock;
151 ///
152 /// let clock = MockClock::new();
153 /// ```
154 ///
155 pub fn new() -> Self {
156 Self::with_progression(MockClockProgression::Frozen)
157 }
158
159 /// Creates a new `MockClock` with the specified progression mode.
160 ///
161 /// The clock starts at the current system time. In
162 /// [`Frozen`](MockClockProgression::Frozen) mode, readings stay fixed until
163 /// explicitly advanced. In [`Monotonic`](MockClockProgression::Monotonic)
164 /// mode, readings progress naturally from the initial system time.
165 ///
166 /// # Arguments
167 ///
168 /// * `progression` - The initial progression mode.
169 ///
170 /// # Examples
171 ///
172 /// ```
173 /// use qubit_clock::{MockClock, MockClockProgression};
174 ///
175 /// let clock = MockClock::with_progression(MockClockProgression::Monotonic);
176 /// assert_eq!(clock.progression(), MockClockProgression::Monotonic);
177 /// ```
178 ///
179 pub fn with_progression(progression: MockClockProgression) -> Self {
180 let initial_time = SystemClock::new().millis();
181 let monotonic_clock = MonotonicClock::new();
182 let monotonic_base_millis = monotonic_clock.millis();
183 MockClock {
184 inner: Arc::new(Mutex::new(MockClockInner {
185 initial_time,
186 initial_progression: progression,
187 epoch: initial_time,
188 monotonic_clock,
189 monotonic_base_millis,
190 progression,
191 millis_to_add: 0,
192 millis_to_add_each_time: 0,
193 add_every_time: false,
194 })),
195 }
196 }
197
198 /// Returns the current progression mode.
199 ///
200 /// # Examples
201 ///
202 /// ```
203 /// use qubit_clock::{MockClock, MockClockProgression};
204 ///
205 /// let clock = MockClock::new();
206 /// assert_eq!(clock.progression(), MockClockProgression::Frozen);
207 /// ```
208 pub fn progression(&self) -> MockClockProgression {
209 self.lock_inner().progression
210 }
211
212 /// Switches the clock progression mode without changing the current reading.
213 ///
214 /// The current logical reading is first folded into the clock's base state,
215 /// so changing between frozen and monotonic modes does not cause an
216 /// immediate time jump.
217 ///
218 /// # Arguments
219 ///
220 /// * `progression` - The new progression mode.
221 ///
222 /// # Examples
223 ///
224 /// ```
225 /// use qubit_clock::{MockClock, MockClockProgression};
226 ///
227 /// let clock = MockClock::new();
228 /// clock.set_progression(MockClockProgression::Monotonic);
229 /// assert_eq!(clock.progression(), MockClockProgression::Monotonic);
230 /// ```
231 pub fn set_progression(&self, progression: MockClockProgression) {
232 let mut inner = self.lock_inner();
233 Self::rebase_at_current(&mut inner);
234 inner.progression = progression;
235 }
236
237 /// Returns whether monotonic progression is enabled.
238 ///
239 /// # Examples
240 ///
241 /// ```
242 /// use qubit_clock::MockClock;
243 ///
244 /// let clock = MockClock::new();
245 /// assert!(!clock.monotonic_progression_enabled());
246 /// ```
247 pub fn monotonic_progression_enabled(&self) -> bool {
248 self.progression().is_monotonic()
249 }
250
251 /// Enables or disables monotonic progression.
252 ///
253 /// This is a boolean convenience wrapper around
254 /// [`set_progression()`](MockClock::set_progression).
255 ///
256 /// # Arguments
257 ///
258 /// * `enabled` - `true` to use monotonic progression, `false` to freeze.
259 ///
260 /// # Examples
261 ///
262 /// ```
263 /// use qubit_clock::MockClock;
264 ///
265 /// let clock = MockClock::new();
266 /// clock.set_monotonic_progression_enabled(true);
267 /// assert!(clock.monotonic_progression_enabled());
268 /// ```
269 pub fn set_monotonic_progression_enabled(&self, enabled: bool) {
270 let progression = if enabled {
271 MockClockProgression::Monotonic
272 } else {
273 MockClockProgression::Frozen
274 };
275 self.set_progression(progression);
276 }
277
278 /// Adds a fixed amount of milliseconds to the clock.
279 ///
280 /// # Arguments
281 ///
282 /// * `millis` - The number of milliseconds to add.
283 /// * `add_every_time` - If `true`, the specified milliseconds will be
284 /// added on every call to [`millis()`](Clock::millis). If `false`, the
285 /// milliseconds are added only once.
286 ///
287 /// # Examples
288 ///
289 /// ```
290 /// use qubit_clock::{Clock, MockClock};
291 ///
292 /// let clock = MockClock::new();
293 /// let before = clock.millis();
294 ///
295 /// // Add 1000ms once
296 /// clock.add_millis(1000, false);
297 /// assert_eq!(clock.millis(), before + 1000);
298 ///
299 /// // Add 100ms on every call
300 /// clock.add_millis(100, true);
301 /// let t1 = clock.millis();
302 /// let t2 = clock.millis();
303 /// assert_eq!(t2 - t1, 100);
304 /// ```
305 ///
306 pub fn add_millis(&self, millis: i64, add_every_time: bool) {
307 if add_every_time {
308 self.set_auto_advance_millis(millis);
309 } else {
310 self.advance_millis(millis);
311 }
312 }
313
314 /// Advances the clock by a fixed amount once.
315 ///
316 /// This method updates the offset used by [`millis()`](Clock::millis) and
317 /// [`time()`](Clock::time) without enabling auto-advance. If the
318 /// accumulated offset exceeds the `i64` range, it saturates at the nearest
319 /// boundary.
320 ///
321 /// # Arguments
322 ///
323 /// * `millis` - The milliseconds to add once.
324 ///
325 /// # Examples
326 ///
327 /// ```
328 /// use qubit_clock::{Clock, MockClock};
329 ///
330 /// let clock = MockClock::new();
331 /// let before = clock.millis();
332 /// clock.advance_millis(1000);
333 /// assert_eq!(clock.millis(), before + 1000);
334 /// ```
335 pub fn advance_millis(&self, millis: i64) {
336 let mut inner = self.lock_inner();
337 inner.millis_to_add = inner.millis_to_add.saturating_add(millis);
338 }
339
340 /// Enables auto-advance on each read operation.
341 ///
342 /// After calling this method, each call to [`millis()`](Clock::millis) or
343 /// [`time()`](Clock::time) returns the current logical time and advances
344 /// the next read by `millis`.
345 ///
346 /// # Arguments
347 ///
348 /// * `millis` - The milliseconds to advance on each read.
349 ///
350 /// # Examples
351 ///
352 /// ```
353 /// use qubit_clock::{Clock, MockClock};
354 ///
355 /// let clock = MockClock::new();
356 /// clock.set_auto_advance_millis(100);
357 /// let t1 = clock.millis();
358 /// let t2 = clock.millis();
359 /// assert_eq!(t2 - t1, 100);
360 /// ```
361 pub fn set_auto_advance_millis(&self, millis: i64) {
362 let mut inner = self.lock_inner();
363 inner.millis_to_add_each_time = millis;
364 inner.add_every_time = true;
365 }
366
367 /// Disables auto-advance behavior.
368 ///
369 /// This method clears the per-read advance setting. Subsequent read
370 /// operations will no longer mutate the clock state.
371 ///
372 /// # Examples
373 ///
374 /// ```
375 /// use qubit_clock::{Clock, MockClock};
376 ///
377 /// let clock = MockClock::new();
378 /// clock.set_auto_advance_millis(100);
379 /// let _ = clock.millis();
380 /// clock.clear_auto_advance();
381 /// let t1 = clock.millis();
382 /// let t2 = clock.millis();
383 /// assert_eq!(t2, t1);
384 /// ```
385 pub fn clear_auto_advance(&self) {
386 let mut inner = self.lock_inner();
387 inner.millis_to_add_each_time = 0;
388 inner.add_every_time = false;
389 }
390}
391
392impl Default for MockClock {
393 #[inline]
394 fn default() -> Self {
395 Self::new()
396 }
397}
398
399impl Clock for MockClock {
400 fn millis(&self) -> i64 {
401 let mut inner = self.lock_inner();
402 let result = Self::current_millis(&inner);
403
404 if inner.add_every_time {
405 inner.millis_to_add = inner
406 .millis_to_add
407 .saturating_add(inner.millis_to_add_each_time);
408 }
409
410 result
411 }
412}
413
414impl ControllableClock for MockClock {
415 fn set_time(&self, instant: DateTime<Utc>) {
416 let mut inner = self.lock_inner();
417 inner.epoch = instant.timestamp_millis();
418 inner.monotonic_base_millis = inner.monotonic_clock.millis();
419 inner.millis_to_add = 0;
420 }
421
422 #[inline]
423 fn add_duration(&self, duration: Duration) {
424 let millis = duration.num_milliseconds();
425 self.advance_millis(millis);
426 }
427
428 fn reset(&self) {
429 let mut inner = self.lock_inner();
430 inner.epoch = inner.initial_time;
431 inner.progression = inner.initial_progression;
432 inner.monotonic_base_millis = inner.monotonic_clock.millis();
433 inner.millis_to_add = 0;
434 inner.millis_to_add_each_time = 0;
435 inner.add_every_time = false;
436 }
437}