Skip to main content

secunit_capture/
time.rs

1//! Time source for capture envelopes. Real wall clock by default; can
2//! be pinned for deterministic test output via either:
3//! - the `SECUNIT_CAPTURE_FIXED_TIME` env var (for CLI integration tests
4//!   that span a child process), or
5//! - the in-process [`set_fixed_time_for_tests`] helper (for unit tests
6//!   that don't want to fight other parallel tests for the env var).
7
8use std::sync::atomic::{AtomicI64, Ordering};
9use std::sync::{Mutex, MutexGuard, OnceLock};
10
11use chrono::{DateTime, SecondsFormat, Utc};
12
13/// Returns the current UTC instant, formatted as `YYYY-MM-DDTHH:MM:SSZ`
14/// (no fractional seconds — capture envelopes round to whole seconds so
15/// hashes are stable across machines with different clock resolutions).
16pub fn now_iso8601() -> String {
17    fixed_now().to_rfc3339_opts(SecondsFormat::Secs, true)
18}
19
20fn fixed_now() -> DateTime<Utc> {
21    let pinned_ts = OVERRIDE_TS.load(Ordering::SeqCst);
22    if pinned_ts != i64::MIN {
23        if let Some(dt) = DateTime::from_timestamp(pinned_ts, 0) {
24            return dt;
25        }
26    }
27    if let Ok(s) = std::env::var("SECUNIT_CAPTURE_FIXED_TIME") {
28        if let Ok(dt) = DateTime::parse_from_rfc3339(&s) {
29            return dt.with_timezone(&Utc);
30        }
31    }
32    Utc::now()
33}
34
35/// Sentinel value meaning "no override". Tests pin specific instants;
36/// `i64::MIN` is far outside any plausible captured_at.
37static OVERRIDE_TS: AtomicI64 = AtomicI64::new(i64::MIN);
38
39fn fixed_time_lock() -> &'static Mutex<()> {
40    static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
41    LOCK.get_or_init(|| Mutex::new(()))
42}
43
44/// Pin the clock for the duration of the returned guard. The guard
45/// holds a process-wide mutex so concurrent time-pinning tests
46/// serialize cleanly — without this, rust's parallel test runner can
47/// have one test's "drop" race against another test's "read".
48pub fn set_fixed_time_for_tests(rfc3339: &str) -> FixedTimeGuard {
49    let dt = DateTime::parse_from_rfc3339(rfc3339)
50        .expect("set_fixed_time_for_tests: invalid RFC-3339")
51        .with_timezone(&Utc);
52    let guard = fixed_time_lock().lock().unwrap_or_else(|p| p.into_inner());
53    OVERRIDE_TS.store(dt.timestamp(), Ordering::SeqCst);
54    FixedTimeGuard { _g: guard }
55}
56
57#[must_use = "drop the guard to release the time pin"]
58pub struct FixedTimeGuard {
59    _g: MutexGuard<'static, ()>,
60}
61
62impl Drop for FixedTimeGuard {
63    fn drop(&mut self) {
64        OVERRIDE_TS.store(i64::MIN, Ordering::SeqCst);
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn fixed_time_is_honored_via_helper() {
74        let _g = set_fixed_time_for_tests("2026-05-01T12:00:00Z");
75        assert_eq!(now_iso8601(), "2026-05-01T12:00:00Z");
76    }
77}