qubit_clock/sleep/mock_sleeper.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 ******************************************************************************/
10use std::sync::{
11 Arc,
12 Condvar,
13 Mutex,
14 MutexGuard,
15};
16use std::time::Duration;
17
18#[cfg(feature = "tokio")]
19use tokio::sync::watch;
20
21use crate::sleep::Sleeper;
22#[cfg(feature = "tokio")]
23use crate::sleep::{
24 AsyncSleepFuture,
25 AsyncSleeper,
26};
27
28/// A manually controlled elapsed-time sleeper for deterministic tests.
29///
30/// This type implements [`Sleeper`] by waiting until manually controlled mock
31/// elapsed time reaches the requested target. When the `tokio` feature is
32/// enabled, it also implements `AsyncSleeper` with the same mock elapsed-time
33/// semantics.
34///
35/// # Testing guidance
36///
37/// Use this type when the code under test sleeps for retry, backoff, polling,
38/// or timeout intervals. `MockSleeper` controls elapsed sleep time only; it
39/// does not change the current time returned by [`crate::MockClock`]. If a
40/// component depends on both current-time reads and sleep completion, inject a
41/// `MockClock` and a `MockSleeper` separately and advance each one explicitly.
42#[derive(Clone, Debug)]
43pub struct MockSleeper {
44 shared: Arc<MockSleeperShared>,
45 #[cfg(feature = "tokio")]
46 async_time_epoch_sender: watch::Sender<u64>,
47}
48
49/// Shared state and condition variable for cloned mock sleepers.
50#[derive(Debug)]
51struct MockSleeperShared {
52 state: Mutex<MockSleeperState>,
53 time_changed: Condvar,
54}
55
56/// Mutable mock sleeper state guarded by [`MockSleeperShared::state`].
57#[derive(Debug)]
58struct MockSleeperState {
59 elapsed: Duration,
60 time_epoch: u64,
61}
62
63/// Snapshot of mock sleeper state read under the internal lock.
64#[derive(Clone, Copy, Debug)]
65struct MockSleeperSnapshot {
66 elapsed: Duration,
67}
68
69impl MockSleeper {
70 /// Creates a mock sleeper whose elapsed time starts at zero.
71 ///
72 /// # Returns
73 ///
74 /// A new mock sleeper with zero elapsed time.
75 #[must_use]
76 pub fn new() -> Self {
77 #[cfg(feature = "tokio")]
78 let (async_time_epoch_sender, _) = watch::channel(0);
79 Self {
80 shared: Arc::new(MockSleeperShared {
81 state: Mutex::new(MockSleeperState {
82 elapsed: Duration::ZERO,
83 time_epoch: 0,
84 }),
85 time_changed: Condvar::new(),
86 }),
87 #[cfg(feature = "tokio")]
88 async_time_epoch_sender,
89 }
90 }
91
92 /// Returns the current mock elapsed time.
93 ///
94 /// # Returns
95 ///
96 /// The elapsed time observed by this mock sleeper.
97 pub fn elapsed(&self) -> Duration {
98 self.current_state().elapsed
99 }
100
101 /// Sets the current mock elapsed time.
102 ///
103 /// This wakes all blocking and asynchronous sleepers so they can recheck
104 /// their target elapsed time.
105 ///
106 /// # Arguments
107 ///
108 /// * `elapsed` - The new elapsed time.
109 pub fn set_elapsed(&self, elapsed: Duration) {
110 let time_epoch = {
111 let mut state = self.lock_state();
112 state.elapsed = elapsed;
113 state.time_epoch = state.time_epoch.wrapping_add(1);
114 state.time_epoch
115 };
116 self.shared.time_changed.notify_all();
117 self.notify_async_time_changed(time_epoch);
118 }
119
120 /// Advances the mock elapsed time by a duration.
121 ///
122 /// # Arguments
123 ///
124 /// * `duration` - The amount to add to the current elapsed time.
125 pub fn advance(&self, duration: Duration) {
126 let time_epoch = {
127 let mut state = self.lock_state();
128 state.elapsed = state.elapsed.saturating_add(duration);
129 state.time_epoch = state.time_epoch.wrapping_add(1);
130 state.time_epoch
131 };
132 self.shared.time_changed.notify_all();
133 self.notify_async_time_changed(time_epoch);
134 }
135
136 /// Resets the mock elapsed time to zero.
137 pub fn reset(&self) {
138 self.set_elapsed(Duration::ZERO);
139 }
140
141 /// Reads a state snapshot under the sleeper lock.
142 ///
143 /// # Returns
144 ///
145 /// A snapshot of current elapsed time.
146 fn current_state(&self) -> MockSleeperSnapshot {
147 let state = self.lock_state();
148 Self::snapshot(&state)
149 }
150
151 /// Creates an immutable snapshot from locked state.
152 ///
153 /// # Arguments
154 ///
155 /// * `state` - The locked state to snapshot.
156 ///
157 /// # Returns
158 ///
159 /// The corresponding immutable snapshot.
160 fn snapshot(state: &MockSleeperState) -> MockSleeperSnapshot {
161 MockSleeperSnapshot {
162 elapsed: state.elapsed,
163 }
164 }
165
166 /// Locks the mock sleeper state.
167 ///
168 /// # Returns
169 ///
170 /// A guard for the sleeper state.
171 fn lock_state(&self) -> MutexGuard<'_, MockSleeperState> {
172 self.shared
173 .state
174 .lock()
175 .expect("mock sleeper state should not be poisoned")
176 }
177
178 /// Notifies asynchronous sleepers about a time change.
179 ///
180 /// # Arguments
181 ///
182 /// * `time_epoch` - The new time epoch value.
183 #[cfg(feature = "tokio")]
184 fn notify_async_time_changed(&self, time_epoch: u64) {
185 let _ = self.async_time_epoch_sender.send(time_epoch);
186 }
187
188 /// No-op when asynchronous sleeper support is disabled.
189 #[cfg(not(feature = "tokio"))]
190 fn notify_async_time_changed(&self, _time_epoch: u64) {}
191}
192
193impl Default for MockSleeper {
194 /// Creates a new mock sleeper.
195 fn default() -> Self {
196 Self::new()
197 }
198}
199
200impl Sleeper for MockSleeper {
201 /// Blocks until mock elapsed time has advanced by `duration`.
202 fn sleep_for(&self, duration: Duration) {
203 let target_elapsed = self.current_state().elapsed.saturating_add(duration);
204 let mut state = self.lock_state();
205 while state.elapsed < target_elapsed {
206 state = self
207 .shared
208 .time_changed
209 .wait(state)
210 .expect("mock sleeper state should not be poisoned");
211 }
212 }
213}
214
215#[cfg(feature = "tokio")]
216impl AsyncSleeper for MockSleeper {
217 /// Returns a future that completes when mock elapsed time reaches the target.
218 fn async_sleep_for<'a>(&'a self, duration: Duration) -> AsyncSleepFuture<'a> {
219 let snapshot = self.current_state();
220 let target_elapsed = snapshot.elapsed.saturating_add(duration);
221 let mut time_receiver = self.async_time_epoch_sender.subscribe();
222 Box::pin(async move {
223 if self.current_state().elapsed >= target_elapsed {
224 return;
225 }
226
227 loop {
228 if self.current_state().elapsed >= target_elapsed {
229 return;
230 }
231 time_receiver
232 .changed()
233 .await
234 .expect("mock sleeper sender should live while the sleeper is borrowed");
235 }
236 })
237 }
238}