Skip to main content

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