use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
pub fn now_rfc3339() -> String {
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let (year, month, day, hour, min, sec) = civil_from_unix_secs(secs);
format!("{year:04}-{month:02}-{day:02}T{hour:02}:{min:02}:{sec:02}Z")
}
fn civil_from_unix_secs(secs: u64) -> (i64, u32, u32, u32, u32, u32) {
let days = (secs / 86_400) as i64;
let rem = secs % 86_400;
let (hour, min, sec) = (
(rem / 3600) as u32,
((rem % 3600) / 60) as u32,
(rem % 60) as u32,
);
let z = days + 719_468;
let era = if z >= 0 { z } else { z - 146_096 } / 146_097;
let doe = z - era * 146_097; let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365; let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let day = (doy - (153 * mp + 2) / 5 + 1) as u32; let month = (if mp < 10 { mp + 3 } else { mp - 9 }) as u32; let year = if month <= 2 { y + 1 } else { y };
(year, month, day, hour, min, sec)
}
#[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));
}
#[test]
fn civil_from_unix_secs_matches_known_anchors() {
assert_eq!(super::civil_from_unix_secs(0), (1970, 1, 1, 0, 0, 0));
assert_eq!(
super::civil_from_unix_secs(946_684_800),
(2000, 1, 1, 0, 0, 0)
);
assert_eq!(
super::civil_from_unix_secs(1_583_020_799),
(2020, 2, 29, 23, 59, 59)
);
assert_eq!(
super::civil_from_unix_secs(1_785_069_296),
(2026, 7, 26, 12, 34, 56)
);
}
#[test]
fn now_rfc3339_is_seconds_precision_utc_with_z() {
let s = now_rfc3339();
assert_eq!(s.len(), 20, "YYYY-MM-DDTHH:MM:SSZ is 20 chars: {s}");
assert!(s.ends_with('Z'), "UTC `Z` suffix, not an offset: {s}");
assert_eq!(&s[4..5], "-");
assert_eq!(&s[10..11], "T");
assert!(!s.contains('.'), "no fractional seconds: {s}");
let year: i64 = s[..4].parse().expect("leading 4-digit year");
assert!(year >= 2024, "reads the real clock: {s}");
}
#[test]
fn format_zero_pads_every_field() {
let (y, mo, d, h, mi, s) = super::civil_from_unix_secs(946_684_800);
assert_eq!(
format!("{y:04}-{mo:02}-{d:02}T{h:02}:{mi:02}:{s:02}Z"),
"2000-01-01T00:00:00Z"
);
}
}