1use crate::clock::Clock;
10use crate::quality::{ClockSource, SyncQuality};
11use crate::timestamp::{Timestamp, WallTime};
12#[cfg(not(target_has_atomic = "64"))]
13use core::cell::Cell;
14#[cfg(target_has_atomic = "64")]
15use core::sync::atomic::{AtomicI64, AtomicU64, Ordering};
16use core::time::Duration;
17#[cfg(not(target_has_atomic = "64"))]
18use critical_section::Mutex;
19
20#[derive(Debug)]
24struct MonoCell {
25 #[cfg(target_has_atomic = "64")]
26 ns: AtomicU64,
27 #[cfg(not(target_has_atomic = "64"))]
28 ns: Mutex<Cell<u64>>,
29}
30
31impl MonoCell {
32 const fn new(start_ns: u64) -> Self {
33 Self {
34 #[cfg(target_has_atomic = "64")]
35 ns: AtomicU64::new(start_ns),
36 #[cfg(not(target_has_atomic = "64"))]
37 ns: Mutex::new(Cell::new(start_ns)),
38 }
39 }
40
41 fn saturating_add(&self, dt: u64) {
42 #[cfg(target_has_atomic = "64")]
43 {
44 let mut cur = self.ns.load(Ordering::Relaxed);
46 loop {
47 let next = cur.saturating_add(dt);
48 match self
49 .ns
50 .compare_exchange_weak(cur, next, Ordering::AcqRel, Ordering::Relaxed)
51 {
52 Ok(_) => return,
53 Err(seen) => cur = seen,
54 }
55 }
56 }
57 #[cfg(not(target_has_atomic = "64"))]
58 {
59 critical_section::with(|cs| {
60 let c = self.ns.borrow(cs);
61 c.set(c.get().saturating_add(dt));
62 });
63 }
64 }
65
66 fn load(&self) -> u64 {
67 #[cfg(target_has_atomic = "64")]
68 {
69 self.ns.load(Ordering::Acquire)
70 }
71 #[cfg(not(target_has_atomic = "64"))]
72 {
73 critical_section::with(|cs| self.ns.borrow(cs).get())
74 }
75 }
76}
77
78#[derive(Debug)]
81struct WallCell {
82 #[cfg(target_has_atomic = "64")]
83 ns: AtomicI64,
84 #[cfg(not(target_has_atomic = "64"))]
85 ns: Mutex<Cell<i64>>,
86}
87
88impl WallCell {
89 const fn new(start_ns: i64) -> Self {
90 Self {
91 #[cfg(target_has_atomic = "64")]
92 ns: AtomicI64::new(start_ns),
93 #[cfg(not(target_has_atomic = "64"))]
94 ns: Mutex::new(Cell::new(start_ns)),
95 }
96 }
97
98 fn saturating_add(&self, dt: i64) {
99 #[cfg(target_has_atomic = "64")]
100 {
101 let mut cur = self.ns.load(Ordering::Relaxed);
102 loop {
103 let next = cur.saturating_add(dt);
104 match self
105 .ns
106 .compare_exchange_weak(cur, next, Ordering::AcqRel, Ordering::Relaxed)
107 {
108 Ok(_) => return,
109 Err(seen) => cur = seen,
110 }
111 }
112 }
113 #[cfg(not(target_has_atomic = "64"))]
114 {
115 critical_section::with(|cs| {
116 let c = self.ns.borrow(cs);
117 c.set(c.get().saturating_add(dt));
118 });
119 }
120 }
121
122 fn load(&self) -> i64 {
123 #[cfg(target_has_atomic = "64")]
124 {
125 self.ns.load(Ordering::Acquire)
126 }
127 #[cfg(not(target_has_atomic = "64"))]
128 {
129 critical_section::with(|cs| self.ns.borrow(cs).get())
130 }
131 }
132}
133
134#[derive(Debug)]
163pub struct MockClock {
164 mono: MonoCell,
165 wall: WallCell,
166}
167
168impl MockClock {
169 #[must_use]
172 pub fn start_at(mono_ns: u64, wall_ns: u64) -> Self {
173 Self {
174 mono: MonoCell::new(mono_ns),
175 wall: WallCell::new(clamp_i64(wall_ns)),
176 }
177 }
178
179 #[must_use]
181 pub fn new() -> Self {
182 Self::start_at(0, 0)
183 }
184
185 pub fn advance(&self, dt: Duration) {
187 let ns = u64::try_from(dt.as_nanos()).unwrap_or(u64::MAX);
188 self.mono.saturating_add(ns);
189 }
190
191 pub fn advance_nanos(&self, ns: u64) {
193 self.mono.saturating_add(ns);
194 }
195
196 pub fn step_wall(&self, offset_ns: i64) {
199 self.wall.saturating_add(offset_ns);
200 }
201
202 #[must_use]
204 pub fn wall_nanos(&self) -> u64 {
205 let ns = self.wall.load();
206 u64::try_from(ns).unwrap_or(0)
207 }
208
209 #[must_use]
211 pub fn mono_nanos(&self) -> u64 {
212 self.mono.load()
213 }
214}
215
216impl Default for MockClock {
217 fn default() -> Self {
218 Self::new()
219 }
220}
221
222impl Clock for MockClock {
223 fn now(&self) -> Timestamp {
224 Timestamp {
225 wall: WallTime::from_unix_nanos(self.wall_nanos()),
226 mono: self.mono_nanos(),
227 }
228 }
229
230 fn quality(&self) -> SyncQuality {
231 SyncQuality::Fallback
233 }
234
235 fn source(&self) -> ClockSource {
236 ClockSource::Mock
237 }
238}
239
240#[allow(clippy::cast_possible_wrap)]
244const fn clamp_i64(wall_ns: u64) -> i64 {
245 if wall_ns > i64::MAX as u64 {
246 i64::MAX
247 } else {
248 wall_ns as i64
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::{MockClock, WallTime};
255 use crate::clock::Clock;
256 use crate::quality::{ClockSource, SyncQuality};
257 use core::time::Duration;
258
259 #[test]
262 fn mock_sequence_is_reproducible() {
263 let run = || {
264 let c = MockClock::start_at(1_000, 5_000);
265 c.advance(Duration::from_millis(2));
266 c.advance_nanos(500);
267 c.step_wall(-1_234);
268 c.now()
269 };
270 let a = run();
271 let b = run();
272 assert_eq!(a, b);
273 assert_eq!(a.mono, 2_000_500 + 1_000);
274 assert_eq!(a.wall, WallTime::from_unix_nanos(5_000 - 1_234));
275 }
276
277 #[test]
279 fn mock_step_wall_independent_of_mono() {
280 let c = MockClock::start_at(0, 1_000_000_000);
281 c.advance_nanos(10);
282 c.step_wall(-2_000_000_000); let t = c.now();
284 assert_eq!(t.mono, 10);
285 assert_eq!(t.wall.unix_nanos(), 0); assert_ne!(t.wall.unix_nanos(), t.mono);
287 }
288
289 #[test]
291 fn mock_saturates_at_bounds() {
292 let c = MockClock::start_at(u64::MAX - 5, 0);
293 c.advance_nanos(100);
294 assert_eq!(c.mono_nanos(), u64::MAX);
295 c.step_wall(i64::MIN);
296 c.step_wall(i64::MIN);
297 assert_eq!(c.wall_nanos(), 0);
298 }
299
300 #[test]
302 fn mock_reports_source_and_quality() {
303 let c = MockClock::new();
304 assert_eq!(c.source(), ClockSource::Mock);
305 assert_eq!(c.quality(), SyncQuality::Fallback);
306 assert_eq!(c.now().mono, 0);
307 }
308}