rusty_ts/time/clock.rs
1//! Clock sources for absolute and elapsed-mode timestamping.
2//!
3//! Per `plan.md` AD-008 and HINT-003: the `Clock` trait is injectable so
4//! snapshot tests can pin time to a deterministic fixed instant. The three
5//! impls cover:
6//!
7//! - `Wall` — system local wall clock (default for absolute timestamps).
8//! - `Monotonic` — std::time::Instant-backed monotonic source for the
9//! `-m` flag; unaffected by NTP / manual clock adjustments.
10//! - `Fixed` — test-only, returns a constant `DateTime<Utc>` so byte-level
11//! snapshot tests are deterministic regardless of when they run.
12
13use chrono::{DateTime, Utc};
14use std::time::Instant;
15
16/// Abstract clock source. Implementors must produce a `DateTime<Utc>` on
17/// every call to `now()`. Implementors that track elapsed time (Monotonic,
18/// Fixed) maintain their own internal anchor and must not require external
19/// synchronisation.
20pub trait Clock {
21 /// Current instant as a UTC datetime. The timezone-resolution layer
22 /// (`crate::time::tz`) is responsible for converting to the rendered
23 /// zone.
24 fn now(&self) -> DateTime<Utc>;
25}
26
27/// System wall clock via `chrono::Utc::now()`. The default for absolute
28/// timestamps in Default mode. Reflects any NTP / manual adjustments.
29#[derive(Debug, Default, Clone, Copy)]
30pub struct Wall;
31
32impl Clock for Wall {
33 fn now(&self) -> DateTime<Utc> {
34 Utc::now()
35 }
36}
37
38/// Monotonic clock anchored at construction time. Reports a `DateTime<Utc>`
39/// derived as `program_start_wall + elapsed_since_start`. The wall component
40/// is fixed at construction; only the elapsed component advances. Used by
41/// `-m` to make `-i` and `-s` elapsed measurements robust against clock
42/// adjustments.
43#[derive(Debug)]
44pub struct Monotonic {
45 /// Wall-clock anchor captured at construction.
46 anchor_wall: DateTime<Utc>,
47 /// Monotonic anchor captured at construction.
48 anchor_mono: Instant,
49}
50
51impl Monotonic {
52 /// Capture both wall and monotonic anchors at the moment of construction.
53 pub fn new() -> Self {
54 Self {
55 anchor_wall: Utc::now(),
56 anchor_mono: Instant::now(),
57 }
58 }
59}
60
61impl Default for Monotonic {
62 fn default() -> Self {
63 Self::new()
64 }
65}
66
67impl Clock for Monotonic {
68 fn now(&self) -> DateTime<Utc> {
69 let elapsed = self.anchor_mono.elapsed();
70 self.anchor_wall + chrono::Duration::from_std(elapsed).unwrap_or(chrono::Duration::zero())
71 }
72}
73
74/// Fixed clock for snapshot determinism. Always returns the same instant.
75/// Test-only — gated to `cfg(test)` callers and the dev-only test harness.
76#[derive(Debug, Clone, Copy)]
77pub struct Fixed {
78 instant: DateTime<Utc>,
79}
80
81impl Fixed {
82 /// Pin the clock at a specific UTC datetime.
83 pub fn new(instant: DateTime<Utc>) -> Self {
84 Self { instant }
85 }
86}
87
88impl Clock for Fixed {
89 fn now(&self) -> DateTime<Utc> {
90 self.instant
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use chrono::TimeZone;
98
99 #[test]
100 fn fixed_clock_is_deterministic() {
101 let target = Utc.with_ymd_and_hms(2026, 5, 22, 14, 30, 45).unwrap();
102 let clock = Fixed::new(target);
103 assert_eq!(clock.now(), target);
104 assert_eq!(clock.now(), target);
105 }
106
107 #[test]
108 fn wall_clock_is_monotonic_ish() {
109 let clock = Wall;
110 let a = clock.now();
111 let b = clock.now();
112 assert!(b >= a, "wall clock went backwards: {a} -> {b}");
113 }
114
115 #[test]
116 fn monotonic_clock_advances() {
117 let clock = Monotonic::new();
118 let a = clock.now();
119 std::thread::sleep(std::time::Duration::from_millis(2));
120 let b = clock.now();
121 assert!(b > a, "monotonic clock did not advance: {a} -> {b}");
122 }
123}