Skip to main content

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}