Skip to main content

codlet_core/
clock.rs

1//! Time abstraction (RFC-020 clock contract).
2//!
3//! All expiry checks go through [`Clock`] so production code is testable with
4//! a fixed time without system-clock dependencies. The clock is always
5//! wall-time monotonic in production; only `FixedClock` (under `test-utils`) is non-monotonic.
6
7use std::time::{Duration, SystemTime, UNIX_EPOCH};
8
9/// A source of the current wall-clock time, expressed as seconds since the
10/// Unix epoch (UTC). Implementations must be infallible and must return a
11/// non-decreasing value in production.
12pub trait Clock {
13    /// Current time as seconds since the Unix epoch (UTC).
14    fn unix_now(&self) -> u64;
15
16    /// Convenience: current time plus `offset`.
17    fn unix_now_plus(&self, offset: Duration) -> u64 {
18        self.unix_now().saturating_add(offset.as_secs())
19    }
20}
21
22/// Production clock backed by [`SystemTime`].
23#[derive(Debug, Default, Clone, Copy)]
24pub struct SystemClock;
25
26impl SystemClock {
27    /// Construct the system clock.
28    #[must_use]
29    pub fn new() -> Self {
30        Self
31    }
32}
33
34impl Clock for SystemClock {
35    fn unix_now(&self) -> u64 {
36        SystemTime::now()
37            .duration_since(UNIX_EPOCH)
38            .map(|d| d.as_secs())
39            .unwrap_or(0)
40    }
41}
42
43/// Deterministic clock that always returns the same instant. Available under
44/// `test-utils` and in this crate's own tests. Useful for expiry boundary tests.
45#[cfg(any(test, feature = "test-utils"))]
46#[derive(Debug, Clone, Copy)]
47pub struct FixedClock(pub u64);
48
49#[cfg(any(test, feature = "test-utils"))]
50impl FixedClock {
51    /// A clock pinned to `unix_secs`.
52    #[must_use]
53    pub fn at(unix_secs: u64) -> Self {
54        Self(unix_secs)
55    }
56
57    /// Advance the fixed clock by `secs`, returning a new `FixedClock`.
58    #[must_use]
59    pub fn advance(self, secs: u64) -> Self {
60        Self(self.0.saturating_add(secs))
61    }
62}
63
64#[cfg(any(test, feature = "test-utils"))]
65impl Clock for FixedClock {
66    fn unix_now(&self) -> u64 {
67        self.0
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn system_clock_is_positive() {
77        let t = SystemClock::new().unix_now();
78        // 2024-01-01T00:00:00Z = 1_704_067_200
79        assert!(t > 1_704_067_200, "clock looks wrong: {t}");
80    }
81
82    #[test]
83    fn fixed_clock_is_deterministic() {
84        let c = FixedClock::at(1_000_000);
85        assert_eq!(c.unix_now(), 1_000_000);
86        assert_eq!(c.unix_now(), 1_000_000);
87    }
88
89    #[test]
90    fn unix_now_plus_offsets_correctly() {
91        let c = FixedClock::at(1_000);
92        assert_eq!(c.unix_now_plus(Duration::from_secs(100)), 1_100);
93    }
94
95    #[test]
96    fn advance_produces_later_clock() {
97        let c = FixedClock::at(1_000).advance(60);
98        assert_eq!(c.unix_now(), 1_060);
99    }
100}