use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Duration;
#[derive(Clone)]
pub struct TimeController {
paused: Arc<AtomicBool>,
current_ms: Arc<AtomicU64>,
time_scale: Arc<AtomicU64>, }
impl TimeController {
pub fn new_paused() -> Self {
Self {
paused: Arc::new(AtomicBool::new(true)),
current_ms: Arc::new(AtomicU64::new(0)),
time_scale: Arc::new(AtomicU64::new(0)), }
}
pub fn new_real() -> Self {
Self {
paused: Arc::new(AtomicBool::new(false)),
current_ms: Arc::new(AtomicU64::new(0)),
time_scale: Arc::new(AtomicU64::new(1000)), }
}
pub fn pause(&self) {
self.paused.store(true, Ordering::SeqCst);
self.time_scale.store(0, Ordering::SeqCst);
}
pub fn resume(&self) {
self.paused.store(false, Ordering::SeqCst);
self.time_scale.store(1000, Ordering::SeqCst);
}
pub fn set_scale(&self, scale: f64) {
let scale_fixed = (scale * 1000.0) as u64;
self.time_scale.store(scale_fixed, Ordering::SeqCst);
self.paused.store(scale == 0.0, Ordering::SeqCst);
}
pub fn advance(&self, duration: Duration) -> Result<(), &'static str> {
if !self.paused.load(Ordering::SeqCst) {
return Err("Cannot advance time when not paused");
}
let ms = duration.as_millis() as u64;
self.current_ms.fetch_add(ms, Ordering::SeqCst);
Ok(())
}
pub fn now(&self) -> Duration {
let ms = self.current_ms.load(Ordering::SeqCst);
Duration::from_millis(ms)
}
pub async fn sleep(&self, duration: Duration) {
if self.paused.load(Ordering::SeqCst) {
let _ = self.advance(duration);
} else {
let scale = self.time_scale.load(Ordering::SeqCst) as f64 / 1000.0;
if scale > 0.0 {
let scaled_duration = Duration::from_secs_f64(duration.as_secs_f64() / scale);
tokio::time::sleep(scaled_duration).await;
}
}
}
pub fn sleep_blocking(&self, duration: Duration) {
if self.paused.load(Ordering::SeqCst) {
let _ = self.advance(duration);
} else {
let scale = self.time_scale.load(Ordering::SeqCst) as f64 / 1000.0;
if scale > 0.0 {
let scaled_duration = Duration::from_secs_f64(duration.as_secs_f64() / scale);
std::thread::sleep(scaled_duration);
}
}
}
}
use std::sync::LazyLock;
static GLOBAL_TIME: LazyLock<TimeController> = LazyLock::new(TimeController::new_real);
pub fn pause() {
GLOBAL_TIME.pause();
}
pub fn resume() {
GLOBAL_TIME.resume();
}
pub fn advance(duration: Duration) -> Result<(), &'static str> {
GLOBAL_TIME.advance(duration)
}
pub fn controller() -> TimeController {
GLOBAL_TIME.clone()
}
#[macro_export]
macro_rules! test_with_paused_time {
($name:ident, $body:block) => {
#[test]
fn $name() {
let _guard = $crate::testing::time_control::PausedTimeGuard::new();
$body
}
};
}
pub struct PausedTimeGuard {
was_paused: bool,
}
impl PausedTimeGuard {
pub fn new() -> Self {
let was_paused = GLOBAL_TIME.paused.load(Ordering::SeqCst);
pause();
Self { was_paused }
}
}
impl Default for PausedTimeGuard {
fn default() -> Self {
Self::new()
}
}
impl Drop for PausedTimeGuard {
fn drop(&mut self) {
if !self.was_paused {
resume();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_time_controller_pause_advance() {
let controller = TimeController::new_paused();
assert_eq!(controller.now(), Duration::ZERO);
controller.advance(Duration::from_secs(1)).unwrap();
assert_eq!(controller.now(), Duration::from_secs(1));
controller.advance(Duration::from_millis(500)).unwrap();
assert_eq!(controller.now(), Duration::from_millis(1500));
}
#[test]
fn test_time_controller_cannot_advance_when_running() {
let controller = TimeController::new_real();
let result = controller.advance(Duration::from_secs(1));
assert!(result.is_err());
}
#[test]
fn test_time_scale() {
let controller = TimeController::new_paused();
controller.set_scale(2.0); assert!(!controller.paused.load(Ordering::SeqCst));
controller.set_scale(0.0); assert!(controller.paused.load(Ordering::SeqCst));
}
#[tokio::test]
async fn test_virtual_sleep() {
let controller = TimeController::new_paused();
let start = controller.now();
controller.sleep(Duration::from_secs(10)).await;
let end = controller.now();
assert_eq!(end - start, Duration::from_secs(10));
}
test_with_paused_time!(test_macro_paused_time, {
advance(Duration::from_secs(1)).unwrap();
assert_eq!(controller().now(), Duration::from_secs(1));
});
}