Skip to main content

qubit_clock/mock/
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//! Clock view backed by a shared mock timeline.
11
12use std::time::Duration as StdDuration;
13
14use chrono::{
15    DateTime,
16    Duration,
17    Utc,
18};
19use parking_lot::{
20    Mutex,
21    MutexGuard,
22};
23use std::sync::Arc;
24
25use crate::{
26    Clock,
27    ControllableClock,
28    MockTimeError,
29    MockTimeline,
30    NanoClock,
31};
32
33/// Number of nanoseconds in one millisecond.
34const NANOS_PER_MILLISECOND: i128 = 1_000_000;
35/// Number of nanoseconds in one second.
36const NANOS_PER_SECOND: i128 = 1_000_000_000;
37
38/// Clock implementation whose readings are derived from a shared mock timeline.
39///
40/// `MockClock` is a controllable UTC clock for tests. It combines a monotonic
41/// [`MockTimeline`] with a wall-clock anchor. The current UTC reading is:
42///
43/// `wall_origin + timeline.elapsed()`
44///
45/// Creating a clock with [`at`](Self::at) or
46/// [`with_timeline`](Self::with_timeline) establishes the wall-clock value for
47/// the timeline's current elapsed instant. Later calls to
48/// [`set_time`](ControllableClock::set_time) move only that wall-clock anchor;
49/// they do not change elapsed mock time. Calls to [`advance`](Self::advance)
50/// or [`add_duration`](ControllableClock::add_duration) advance the underlying
51/// monotonic timeline and therefore wake timeline waiters such as mock sleeps.
52///
53/// This clock implements [`Clock`], [`NanoClock`], and [`ControllableClock`].
54/// It is frozen until its timeline advances, which makes elapsed-time tests
55/// deterministic and independent of wall-clock scheduling noise.
56///
57/// A cloned `MockClock` shares both the timeline and the wall-clock anchor with
58/// the original clock. A clock created with `MockClock::with_timeline` shares
59/// only the provided timeline; it owns its own wall-clock anchor.
60#[derive(Debug, Clone)]
61pub struct MockClock {
62    timeline: MockTimeline,
63    anchor: Arc<Mutex<MockClockAnchor>>,
64}
65
66/// Wall-clock mapping state for a mock clock.
67#[derive(Debug)]
68struct MockClockAnchor {
69    initial_wall_origin_nanos: i128,
70    wall_origin_nanos: i128,
71}
72
73impl MockClock {
74    /// Creates a mock clock anchored at the current system time.
75    ///
76    /// # Returns
77    /// A mock clock with a fresh zero-elapsed timeline.
78    #[must_use]
79    pub fn new() -> Self {
80        Self::at(Utc::now())
81    }
82
83    /// Creates a mock clock anchored at a specific UTC time.
84    ///
85    /// # Parameters
86    /// - `start`: UTC time returned while the associated timeline is at its
87    ///   current elapsed value.
88    ///
89    /// # Returns
90    /// A mock clock with a fresh timeline.
91    #[must_use]
92    pub fn at(start: DateTime<Utc>) -> Self {
93        Self::with_timeline(start, MockTimeline::new())
94    }
95
96    /// Creates a mock clock backed by an existing timeline.
97    ///
98    /// # Parameters
99    /// - `start`: UTC time returned for the timeline's current elapsed value.
100    /// - `timeline`: Shared mock timeline used by this clock.
101    ///
102    /// # Returns
103    /// A mock clock view over the provided timeline.
104    #[must_use]
105    pub fn with_timeline(start: DateTime<Utc>, timeline: MockTimeline) -> Self {
106        let wall_origin_nanos = datetime_to_nanos(start).saturating_sub(timeline_elapsed_i128(&timeline));
107        Self {
108            timeline,
109            anchor: Arc::new(Mutex::new(MockClockAnchor {
110                initial_wall_origin_nanos: wall_origin_nanos,
111                wall_origin_nanos,
112            })),
113        }
114    }
115
116    /// Returns the timeline backing this clock.
117    ///
118    /// # Returns
119    /// The shared mock timeline.
120    #[inline]
121    pub fn timeline(&self) -> MockTimeline {
122        self.timeline.clone()
123    }
124
125    /// Advances the shared mock timeline.
126    ///
127    /// # Parameters
128    /// - `duration`: Duration to add to the shared timeline.
129    #[inline]
130    pub fn advance(&self, duration: StdDuration) {
131        self.timeline.advance(duration);
132    }
133
134    /// Resets the shared timeline and this clock's wall-time anchor.
135    ///
136    /// # Returns
137    /// `Ok(())` when reset succeeds.
138    ///
139    /// # Errors
140    /// Returns [`MockTimeError::ActiveWaiters`] when timeline waiters are active.
141    pub fn try_reset(&self) -> Result<(), MockTimeError> {
142        self.timeline.reset()?;
143        self.reset_wall_anchor();
144        Ok(())
145    }
146
147    /// Resets only the wall-time anchor.
148    ///
149    /// This is used by [`crate::MockTime`] after it has reset the shared
150    /// timeline.
151    pub(crate) fn reset_wall_anchor(&self) {
152        let mut anchor = self.lock_anchor();
153        anchor.wall_origin_nanos = anchor.initial_wall_origin_nanos;
154    }
155
156    /// Calculates the current UTC timestamp in nanoseconds.
157    ///
158    /// # Returns
159    /// Current timestamp as Unix nanoseconds.
160    fn current_nanos(&self) -> i128 {
161        let anchor = self.lock_anchor();
162        anchor
163            .wall_origin_nanos
164            .saturating_add(timeline_elapsed_i128(&self.timeline))
165    }
166
167    /// Locks the wall-clock anchor.
168    ///
169    /// # Returns
170    /// A guard for anchor state.
171    fn lock_anchor(&self) -> MutexGuard<'_, MockClockAnchor> {
172        self.anchor.lock()
173    }
174}
175
176impl Default for MockClock {
177    /// Creates a mock clock anchored at the current system time.
178    fn default() -> Self {
179        Self::new()
180    }
181}
182
183impl Clock for MockClock {
184    /// Returns the current mock UTC timestamp in milliseconds.
185    fn millis(&self) -> i64 {
186        millis_from_nanos(self.current_nanos())
187    }
188
189    /// Returns the current mock UTC time with nanosecond precision.
190    fn time(&self) -> DateTime<Utc> {
191        datetime_from_nanos(self.current_nanos())
192    }
193}
194
195impl NanoClock for MockClock {
196    /// Returns the current mock UTC timestamp in nanoseconds.
197    fn nanos(&self) -> i128 {
198        self.current_nanos()
199    }
200}
201
202impl ControllableClock for MockClock {
203    /// Reanchors this clock so the current timeline instant reads as `instant`.
204    fn set_time(&self, instant: DateTime<Utc>) {
205        let mut anchor = self.lock_anchor();
206        anchor.wall_origin_nanos = datetime_to_nanos(instant).saturating_sub(timeline_elapsed_i128(&self.timeline));
207    }
208
209    /// Advances the shared mock timeline by a non-negative duration.
210    ///
211    /// # Panics
212    /// Panics if `duration` is negative or cannot be represented as
213    /// [`std::time::Duration`].
214    fn add_duration(&self, duration: Duration) {
215        let duration = duration
216            .to_std()
217            .expect("mock time can only be advanced by a non-negative duration");
218        self.timeline.advance(duration);
219    }
220
221    /// Resets the shared timeline and this clock's wall-time anchor.
222    ///
223    /// # Panics
224    /// Panics when active timeline waiters prevent reset.
225    fn reset(&self) {
226        self.try_reset()
227            .expect("mock clock reset should not run with active waiters");
228    }
229}
230
231/// Converts a UTC date-time to Unix nanoseconds.
232///
233/// # Parameters
234/// - `instant`: UTC timestamp to convert.
235///
236/// # Returns
237/// Unix timestamp in nanoseconds.
238fn datetime_to_nanos(instant: DateTime<Utc>) -> i128 {
239    (instant.timestamp() as i128)
240        .saturating_mul(NANOS_PER_SECOND)
241        .saturating_add(instant.timestamp_subsec_nanos() as i128)
242}
243
244/// Converts Unix nanoseconds to a UTC date-time, clamping out-of-range values.
245///
246/// # Parameters
247/// - `nanos`: Unix timestamp in nanoseconds.
248///
249/// # Returns
250/// UTC date-time represented by the timestamp, clamped to chrono bounds.
251fn datetime_from_nanos(nanos: i128) -> DateTime<Utc> {
252    let seconds = nanos.div_euclid(NANOS_PER_SECOND);
253    let sub_nanos = nanos.rem_euclid(NANOS_PER_SECOND) as u32;
254    let seconds = match i64::try_from(seconds) {
255        Ok(seconds) => seconds,
256        Err(_) if nanos < 0 => return DateTime::<Utc>::MIN_UTC,
257        Err(_) => return DateTime::<Utc>::MAX_UTC,
258    };
259    DateTime::from_timestamp(seconds, sub_nanos).unwrap_or({
260        if nanos < 0 {
261            DateTime::<Utc>::MIN_UTC
262        } else {
263            DateTime::<Utc>::MAX_UTC
264        }
265    })
266}
267
268/// Converts Unix nanoseconds to timestamp milliseconds using Euclidean division.
269///
270/// # Parameters
271/// - `nanos`: Unix timestamp in nanoseconds.
272///
273/// # Returns
274/// Timestamp milliseconds, saturated to `i64` bounds.
275fn millis_from_nanos(nanos: i128) -> i64 {
276    let millis = nanos.div_euclid(NANOS_PER_MILLISECOND);
277    match i64::try_from(millis) {
278        Ok(value) => value,
279        Err(_) if millis < 0 => i64::MIN,
280        Err(_) => i64::MAX,
281    }
282}
283
284/// Reads timeline elapsed nanoseconds as an `i128`, saturating on overflow.
285///
286/// # Parameters
287/// - `timeline`: Timeline to inspect.
288///
289/// # Returns
290/// Elapsed nanoseconds as `i128`.
291fn timeline_elapsed_i128(timeline: &MockTimeline) -> i128 {
292    i128::try_from(timeline.elapsed_nanos()).unwrap_or(i128::MAX)
293}