jerrycan_core/clock.rs
1//! Injectable time. Handlers/extensions take `Dep<Clock>` and call `now()`;
2//! tests control it via `TestApp::clock().advance(..)`. The serve engine's
3//! own timeouts deliberately stay on real tokio time — Clock is for DOMAIN
4//! time (rate windows, schedules, expiry), not transport timeouts.
5//!
6//! `App::new()` provides a [`Clock::system`] singleton by default; `provide`
7//! a different one to override it. `into_test()` swaps in a [`Clock::test`]
8//! that [`TestApp::clock`](crate::TestApp::clock) hands back so a test can
9//! [`advance`](Clock::advance) or [`set`](Clock::set) the injected clock and
10//! observe the effect through real requests.
11
12use std::sync::Arc;
13use std::sync::atomic::{AtomicU64, Ordering};
14use std::time::{Duration, SystemTime};
15
16/// An injectable source of "now". Cloning is cheap and, for a test clock,
17/// shares the same controllable offset — so a handle handed to a test moves
18/// in lockstep with the clock the handler resolves.
19#[derive(Clone)]
20pub struct Clock(Inner);
21
22#[derive(Clone)]
23enum Inner {
24 /// Reads the real system clock on every `now()`.
25 System,
26 /// Test clock: a fixed base plus a controllable offset in milliseconds.
27 /// The offset lives behind an `Arc<AtomicU64>` so every clone — including
28 /// the one resolved inside a handler and the one held by `TestApp` —
29 /// observes the same advances.
30 Test {
31 base: SystemTime,
32 offset_ms: Arc<AtomicU64>,
33 },
34}
35
36impl Clock {
37 /// The real clock: `now()` reads `SystemTime::now()` each call. This is
38 /// what `App::new()` provides by default.
39 pub fn system() -> Self {
40 Clock(Inner::System)
41 }
42
43 /// A controllable test clock. The base is `SystemTime::now()` at creation
44 /// — tests assert on *movement* (`advance`/`set`), not on an absolute base,
45 /// so a realistic starting instant is fine and avoids surprising callers
46 /// that subtract `UNIX_EPOCH`.
47 pub fn test() -> Self {
48 Clock(Inner::Test {
49 base: SystemTime::now(),
50 offset_ms: Arc::new(AtomicU64::new(0)),
51 })
52 }
53
54 /// The current instant. For [`Clock::system`] this is the live system
55 /// clock; for [`Clock::test`] it is `base + accumulated offset`.
56 pub fn now(&self) -> SystemTime {
57 match &self.0 {
58 Inner::System => SystemTime::now(),
59 Inner::Test { base, offset_ms } => {
60 *base + Duration::from_millis(offset_ms.load(Ordering::SeqCst))
61 }
62 }
63 }
64
65 /// Move a test clock forward by `d`. Panics on a system clock — advancing
66 /// real time is meaningless, and silently ignoring it would hide a test bug.
67 pub fn advance(&self, d: Duration) {
68 match &self.0 {
69 Inner::Test { offset_ms, .. } => {
70 // Saturate rather than wrap: a test that advances past ~584M
71 // years is a bug, but wrapping would be a *silent* one.
72 offset_ms.fetch_add(d.as_millis().min(u64::MAX as u128) as u64, Ordering::SeqCst);
73 }
74 Inner::System => {
75 panic!("Clock::advance() is test-only — build the app with into_test()")
76 }
77 }
78 }
79
80 /// Pin a test clock to an absolute instant. Panics on a system clock for
81 /// the same reason as [`advance`](Clock::advance). Useful for cron/expiry
82 /// tests that need a specific wall-clock time rather than a delta.
83 pub fn set(&self, when: SystemTime) {
84 match &self.0 {
85 Inner::Test { base, offset_ms } => {
86 // Express `when` as an offset from the fixed base. Times at or
87 // before the base clamp to zero (the test clock never runs
88 // backwards before its own base).
89 let delta = when.duration_since(*base).unwrap_or_default();
90 offset_ms.store(
91 delta.as_millis().min(u64::MAX as u128) as u64,
92 Ordering::SeqCst,
93 );
94 }
95 Inner::System => {
96 panic!("Clock::set() is test-only — build the app with into_test()")
97 }
98 }
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105 use crate::prelude::*;
106
107 #[tokio::test]
108 async fn clock_is_injectable_and_test_controllable() {
109 async fn now_ms(clock: Dep<Clock>) -> Result<Json<u128>> {
110 Ok(Json(
111 clock
112 .now()
113 .duration_since(std::time::UNIX_EPOCH)
114 .unwrap()
115 .as_millis(),
116 ))
117 }
118 let t = App::new().route("/now", get(now_ms)).into_test();
119 let t0: u128 = t.get("/now").await.json();
120 t.clock().advance(std::time::Duration::from_secs(3600));
121 let t1: u128 = t.get("/now").await.json();
122 assert!(
123 t1 >= t0 + 3_600_000,
124 "advance moved the injected clock: {t0} -> {t1}"
125 );
126 }
127
128 #[test]
129 fn real_clock_tracks_system_time() {
130 let c = Clock::system();
131 let a = c.now();
132 let b = std::time::SystemTime::now();
133 assert!(b.duration_since(a).unwrap() < std::time::Duration::from_secs(1));
134 }
135
136 #[tokio::test]
137 async fn clock_resolves_in_task_contexts_too() {
138 let built = crate::App::new().build().unwrap();
139 let mut ctx = built.task_context();
140 let clock = ctx.resolve::<Clock>().await.unwrap();
141 let _ = clock.now(); // resolvable outside requests — jobs need this
142 }
143
144 #[test]
145 fn test_clock_clones_share_one_offset() {
146 // The handle TestApp keeps and the Arc a handler resolves must move
147 // together — that is the whole point of an injectable test clock.
148 let c = Clock::test();
149 let clone = c.clone();
150 let before = clone.now();
151 c.advance(Duration::from_secs(10));
152 let after = clone.now();
153 assert_eq!(
154 after.duration_since(before).unwrap(),
155 Duration::from_secs(10)
156 );
157 }
158
159 #[test]
160 fn set_pins_test_clock_to_an_absolute_instant() {
161 let c = Clock::test();
162 let target = SystemTime::now() + Duration::from_secs(86_400);
163 c.set(target);
164 // Within a millisecond of the requested instant (we store ms offsets).
165 let drift = c
166 .now()
167 .duration_since(target)
168 .unwrap_or_else(|e| e.duration());
169 assert!(
170 drift < Duration::from_millis(2),
171 "set pinned now() to target"
172 );
173 }
174
175 #[test]
176 #[should_panic(expected = "test-only")]
177 fn advancing_a_system_clock_panics_loudly() {
178 Clock::system().advance(Duration::from_secs(1));
179 }
180}