Skip to main content

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}