Skip to main content

hojicha_core/testing/
time_control.rs

1//! Time control utilities for deterministic testing
2//!
3//! This module provides utilities for controlling time in tests, similar to
4//! Tokio's `time::pause()` functionality.
5
6use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
7use std::sync::Arc;
8use std::time::Duration;
9
10/// A controller for virtual time in tests
11#[derive(Clone)]
12pub struct TimeController {
13    /// Whether time is paused
14    paused: Arc<AtomicBool>,
15    /// Current virtual time in milliseconds
16    current_ms: Arc<AtomicU64>,
17    /// Time scale factor (1.0 = real time, 0.0 = paused, >1.0 = fast forward)
18    time_scale: Arc<AtomicU64>, // Stored as fixed point (1000 = 1.0)
19}
20
21impl TimeController {
22    /// Create a new time controller with paused time
23    pub fn new_paused() -> Self {
24        Self {
25            paused: Arc::new(AtomicBool::new(true)),
26            current_ms: Arc::new(AtomicU64::new(0)),
27            time_scale: Arc::new(AtomicU64::new(0)), // 0.0 = paused
28        }
29    }
30
31    /// Create a new time controller with real time
32    pub fn new_real() -> Self {
33        Self {
34            paused: Arc::new(AtomicBool::new(false)),
35            current_ms: Arc::new(AtomicU64::new(0)),
36            time_scale: Arc::new(AtomicU64::new(1000)), // 1.0 = real time
37        }
38    }
39
40    /// Pause time progression
41    pub fn pause(&self) {
42        self.paused.store(true, Ordering::SeqCst);
43        self.time_scale.store(0, Ordering::SeqCst);
44    }
45
46    /// Resume time at normal speed
47    pub fn resume(&self) {
48        self.paused.store(false, Ordering::SeqCst);
49        self.time_scale.store(1000, Ordering::SeqCst);
50    }
51
52    /// Set time scale (0.0 = paused, 1.0 = normal, 2.0 = double speed)
53    pub fn set_scale(&self, scale: f64) {
54        let scale_fixed = (scale * 1000.0) as u64;
55        self.time_scale.store(scale_fixed, Ordering::SeqCst);
56        self.paused.store(scale == 0.0, Ordering::SeqCst);
57    }
58
59    /// Advance time by the given duration (only works when paused)
60    pub fn advance(&self, duration: Duration) -> Result<(), &'static str> {
61        if !self.paused.load(Ordering::SeqCst) {
62            return Err("Cannot advance time when not paused");
63        }
64
65        let ms = duration.as_millis() as u64;
66        self.current_ms.fetch_add(ms, Ordering::SeqCst);
67        Ok(())
68    }
69
70    /// Get the current virtual time
71    pub fn now(&self) -> Duration {
72        let ms = self.current_ms.load(Ordering::SeqCst);
73        Duration::from_millis(ms)
74    }
75
76    /// Sleep for the given duration
77    /// In paused mode: advances virtual time instantly
78    /// In real mode: actually sleeps
79    pub async fn sleep(&self, duration: Duration) {
80        if self.paused.load(Ordering::SeqCst) {
81            // Just advance virtual time
82            let _ = self.advance(duration);
83        } else {
84            // Apply time scale
85            let scale = self.time_scale.load(Ordering::SeqCst) as f64 / 1000.0;
86            if scale > 0.0 {
87                let scaled_duration = Duration::from_secs_f64(duration.as_secs_f64() / scale);
88                tokio::time::sleep(scaled_duration).await;
89            }
90        }
91    }
92
93    /// Block the current thread for the given duration
94    /// In paused mode: advances virtual time instantly
95    /// In real mode: actually blocks
96    pub fn sleep_blocking(&self, duration: Duration) {
97        if self.paused.load(Ordering::SeqCst) {
98            // Just advance virtual time
99            let _ = self.advance(duration);
100        } else {
101            // Apply time scale
102            let scale = self.time_scale.load(Ordering::SeqCst) as f64 / 1000.0;
103            if scale > 0.0 {
104                let scaled_duration = Duration::from_secs_f64(duration.as_secs_f64() / scale);
105                std::thread::sleep(scaled_duration);
106            }
107        }
108    }
109}
110
111/// Global time controller for tests
112use std::sync::LazyLock;
113
114static GLOBAL_TIME: LazyLock<TimeController> = LazyLock::new(TimeController::new_real);
115
116/// Pause global time (for use in tests)
117pub fn pause() {
118    GLOBAL_TIME.pause();
119}
120
121/// Resume global time
122pub fn resume() {
123    GLOBAL_TIME.resume();
124}
125
126/// Advance global time by the given duration
127pub fn advance(duration: Duration) -> Result<(), &'static str> {
128    GLOBAL_TIME.advance(duration)
129}
130
131/// Get a handle to the global time controller
132pub fn controller() -> TimeController {
133    GLOBAL_TIME.clone()
134}
135
136/// Macro for tests with paused time
137#[macro_export]
138macro_rules! test_with_paused_time {
139    ($name:ident, $body:block) => {
140        #[test]
141        fn $name() {
142            let _guard = $crate::testing::time_control::PausedTimeGuard::new();
143            $body
144        }
145    };
146}
147
148/// RAII guard that pauses time and restores it when dropped
149pub struct PausedTimeGuard {
150    was_paused: bool,
151}
152
153impl PausedTimeGuard {
154    /// Create a new paused time guard, saving the current time state
155    pub fn new() -> Self {
156        let was_paused = GLOBAL_TIME.paused.load(Ordering::SeqCst);
157        pause();
158        Self { was_paused }
159    }
160}
161
162impl Default for PausedTimeGuard {
163    fn default() -> Self {
164        Self::new()
165    }
166}
167
168impl Drop for PausedTimeGuard {
169    fn drop(&mut self) {
170        if !self.was_paused {
171            resume();
172        }
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn test_time_controller_pause_advance() {
182        let controller = TimeController::new_paused();
183
184        assert_eq!(controller.now(), Duration::ZERO);
185
186        controller.advance(Duration::from_secs(1)).unwrap();
187        assert_eq!(controller.now(), Duration::from_secs(1));
188
189        controller.advance(Duration::from_millis(500)).unwrap();
190        assert_eq!(controller.now(), Duration::from_millis(1500));
191    }
192
193    #[test]
194    fn test_time_controller_cannot_advance_when_running() {
195        let controller = TimeController::new_real();
196
197        let result = controller.advance(Duration::from_secs(1));
198        assert!(result.is_err());
199    }
200
201    #[test]
202    fn test_time_scale() {
203        let controller = TimeController::new_paused();
204
205        controller.set_scale(2.0); // Double speed
206        assert!(!controller.paused.load(Ordering::SeqCst));
207
208        controller.set_scale(0.0); // Paused
209        assert!(controller.paused.load(Ordering::SeqCst));
210    }
211
212    #[tokio::test]
213    async fn test_virtual_sleep() {
214        let controller = TimeController::new_paused();
215
216        let start = controller.now();
217        controller.sleep(Duration::from_secs(10)).await;
218        let end = controller.now();
219
220        // Should have advanced by 10 seconds instantly
221        assert_eq!(end - start, Duration::from_secs(10));
222    }
223
224    test_with_paused_time!(test_macro_paused_time, {
225        advance(Duration::from_secs(1)).unwrap();
226        assert_eq!(controller().now(), Duration::from_secs(1));
227    });
228}