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}