darkbio_clock/paused.rs
1// clock-rs: virtual clock for testing blocking code
2// Copyright 2026 Dark Bio AG. All rights reserved.
3//
4// Use of this source code is governed by a BSD-style
5// license that can be found in the LICENSE file.
6
7//! Paused clocks, which move only when a test advances them.
8
9use crate::{Clock, Signal};
10use std::sync::{Arc, Mutex, MutexGuard, PoisonError, Weak};
11use std::time::{Duration, Instant};
12
13#[cfg_attr(docsrs, doc(cfg(feature = "test-clock")))]
14impl Clock {
15 /// Creates a paused clock, set to the current real time. It moves only
16 /// when [`Clock::advance`] or [`Clock::advance_to`] moves it.
17 pub fn paused() -> Self {
18 let now = Instant::now();
19 Self {
20 paused: Some(Arc::new(Paused {
21 start: now,
22 state: Mutex::new(PausedState {
23 now,
24 signals: Vec::new(),
25 }),
26 })),
27 }
28 }
29
30 /// Moves a paused clock forward by `by` and wakes all its waiters. It
31 /// returns once they are notified, without waiting for them to act. A zero
32 /// step does nothing.
33 ///
34 /// # Panics
35 ///
36 /// Panics on a real clock, or if the new time overflows [`Instant`]. The time
37 /// is unchanged after a panic.
38 pub fn advance(&self, by: Duration) {
39 self.advance_with(|now| {
40 now.checked_add(by)
41 .expect("clock advance overflows Instant")
42 });
43 }
44
45 /// Moves a paused clock forward to `target` and wakes all its waiters. It
46 /// returns once they are notified, without waiting for them to act.
47 /// Advancing to the current time does nothing.
48 ///
49 /// # Panics
50 ///
51 /// Panics on a real clock, or if `target` is earlier than the current time.
52 /// The time is unchanged after a panic.
53 pub fn advance_to(&self, target: Instant) {
54 self.advance_with(|now| {
55 assert!(target >= now, "clock cannot go backwards");
56 target
57 });
58 }
59
60 /// Moves a paused clock to the time `next` picks from the current one, then
61 /// wakes its waiters. `next` panics on a bad step before anything changes.
62 fn advance_with(&self, next: impl FnOnce(Instant) -> Instant) {
63 let Some(paused) = &self.paused else {
64 panic!("real clock cannot be advanced");
65 };
66 let mut state = paused.lock();
67 let now = next(state.now);
68 if now == state.now {
69 return;
70 }
71 state.now = now;
72 let mut signals = Vec::with_capacity(state.signals.len());
73 state.signals.retain(|signal| match signal.upgrade() {
74 Some(signal) => {
75 signals.push(signal);
76 true
77 }
78 None => false,
79 });
80 drop(state);
81
82 // Wake outside the clock lock, so woken threads can read the time at once
83 for signal in signals {
84 signal.notify_all();
85 }
86 }
87}
88
89impl PartialEq for Clock {
90 /// Compares identity. Real clocks are all equal; a paused clock equals only
91 /// its own clones.
92 fn eq(&self, other: &Self) -> bool {
93 match (&self.paused, &other.paused) {
94 (None, None) => true,
95 (Some(ours), Some(theirs)) => Arc::ptr_eq(ours, theirs),
96 _ => false,
97 }
98 }
99}
100
101impl Eq for Clock {}
102
103/// A paused clock's state, shared by its clones and its waiters.
104pub(crate) struct Paused {
105 /// Time the clock started at, to show how far it has advanced.
106 start: Instant,
107 /// Current time and the waiters to wake when it moves.
108 state: Mutex<PausedState>,
109}
110
111/// Mutable part of a paused clock.
112struct PausedState {
113 /// Current time of the clock.
114 now: Instant,
115 /// Signals of the clock's waiters. Entries of dropped waiters are pruned
116 /// when advancing and before the list grows.
117 signals: Vec<Weak<Signal>>,
118}
119
120impl Paused {
121 /// Returns the clock's current time.
122 pub(crate) fn now(&self) -> Instant {
123 self.lock().now
124 }
125
126 /// Returns how far the clock has advanced since its creation.
127 pub(crate) fn advanced(&self) -> Duration {
128 self.lock().now - self.start
129 }
130
131 /// Registers a waiter's signal to wake on every advance.
132 pub(crate) fn register(&self, signal: &Arc<Signal>) {
133 let mut state = self.lock();
134 // Prune dropped waiters before the list grows, so it tracks the live ones
135 if state.signals.len() == state.signals.capacity() {
136 state.signals.retain(|signal| signal.strong_count() > 0);
137 }
138 state.signals.push(Arc::downgrade(signal));
139 }
140
141 /// Locks the state. An advance panics before it writes anything, so a lock
142 /// poisoned by that panic still guards a consistent state.
143 fn lock(&self) -> MutexGuard<'_, PausedState> {
144 self.state.lock().unwrap_or_else(PoisonError::into_inner)
145 }
146}