use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, SystemTime};
#[derive(Clone)]
pub struct Clock(Inner);
#[derive(Clone)]
enum Inner {
System,
Test {
base: SystemTime,
offset_ms: Arc<AtomicU64>,
},
}
impl Clock {
pub fn system() -> Self {
Clock(Inner::System)
}
pub fn test() -> Self {
Clock(Inner::Test {
base: SystemTime::now(),
offset_ms: Arc::new(AtomicU64::new(0)),
})
}
pub fn now(&self) -> SystemTime {
match &self.0 {
Inner::System => SystemTime::now(),
Inner::Test { base, offset_ms } => {
*base + Duration::from_millis(offset_ms.load(Ordering::SeqCst))
}
}
}
pub fn advance(&self, d: Duration) {
match &self.0 {
Inner::Test { offset_ms, .. } => {
offset_ms.fetch_add(d.as_millis().min(u64::MAX as u128) as u64, Ordering::SeqCst);
}
Inner::System => {
panic!("Clock::advance() is test-only — build the app with into_test()")
}
}
}
pub fn set(&self, when: SystemTime) {
match &self.0 {
Inner::Test { base, offset_ms } => {
let delta = when.duration_since(*base).unwrap_or_default();
offset_ms.store(
delta.as_millis().min(u64::MAX as u128) as u64,
Ordering::SeqCst,
);
}
Inner::System => {
panic!("Clock::set() is test-only — build the app with into_test()")
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::prelude::*;
#[tokio::test]
async fn clock_is_injectable_and_test_controllable() {
async fn now_ms(clock: Dep<Clock>) -> Result<Json<u128>> {
Ok(Json(
clock
.now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis(),
))
}
let t = App::new().route("/now", get(now_ms)).into_test();
let t0: u128 = t.get("/now").await.json();
t.clock().advance(std::time::Duration::from_secs(3600));
let t1: u128 = t.get("/now").await.json();
assert!(
t1 >= t0 + 3_600_000,
"advance moved the injected clock: {t0} -> {t1}"
);
}
#[test]
fn real_clock_tracks_system_time() {
let c = Clock::system();
let a = c.now();
let b = std::time::SystemTime::now();
assert!(b.duration_since(a).unwrap() < std::time::Duration::from_secs(1));
}
#[tokio::test]
async fn clock_resolves_in_task_contexts_too() {
let built = crate::App::new().build().unwrap();
let mut ctx = built.task_context();
let clock = ctx.resolve::<Clock>().await.unwrap();
let _ = clock.now(); }
#[test]
fn test_clock_clones_share_one_offset() {
let c = Clock::test();
let clone = c.clone();
let before = clone.now();
c.advance(Duration::from_secs(10));
let after = clone.now();
assert_eq!(
after.duration_since(before).unwrap(),
Duration::from_secs(10)
);
}
#[test]
fn set_pins_test_clock_to_an_absolute_instant() {
let c = Clock::test();
let target = SystemTime::now() + Duration::from_secs(86_400);
c.set(target);
let drift = c
.now()
.duration_since(target)
.unwrap_or_else(|e| e.duration());
assert!(
drift < Duration::from_millis(2),
"set pinned now() to target"
);
}
#[test]
#[should_panic(expected = "test-only")]
fn advancing_a_system_clock_panics_loudly() {
Clock::system().advance(Duration::from_secs(1));
}
}