coven_foundation/
clock.rs1use chrono::{DateTime, Utc};
13use std::sync::Arc;
14use std::time::{Duration, Instant};
15
16pub trait Clock: Send + Sync {
19 fn now(&self) -> DateTime<Utc>;
20}
21
22pub type ClockRef = Arc<dyn Clock>;
25
26#[cfg(any(test, feature = "test-utils"))]
30pub fn clock_from_millis(source: impl Fn() -> u64 + Send + Sync + 'static) -> ClockRef {
31 Arc::new(ClosureClock(move || {
32 let millis: i64 = source().try_into().expect("test clock millis fit in i64");
33 DateTime::from_timestamp_millis(millis).expect("valid test clock instant")
34 }))
35}
36
37pub struct Stopwatch(Instant);
50
51impl Stopwatch {
52 pub fn start() -> Self {
54 Self(Instant::now())
55 }
56
57 pub fn elapsed(&self) -> Duration {
59 self.0.elapsed()
60 }
61}
62
63pub struct SystemClock;
65
66impl Clock for SystemClock {
67 fn now(&self) -> DateTime<Utc> {
68 Utc::now()
69 }
70}
71
72#[cfg(any(test, feature = "test-utils"))]
76pub use fakes::{ClosureClock, FixedClock};
77
78#[cfg(any(test, feature = "test-utils"))]
79mod fakes {
80 use super::*;
81
82 pub struct ClosureClock<F>(pub F);
84
85 impl<F> Clock for ClosureClock<F>
86 where
87 F: Fn() -> DateTime<Utc> + Send + Sync,
88 {
89 fn now(&self) -> DateTime<Utc> {
90 (self.0)()
91 }
92 }
93
94 pub struct FixedClock(pub DateTime<Utc>);
96
97 impl Clock for FixedClock {
98 fn now(&self) -> DateTime<Utc> {
99 self.0
100 }
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use std::sync::atomic::{AtomicU64, Ordering};
108
109 #[test]
110 fn a_stopwatch_advances_with_real_time_under_a_pinned_clock() {
111 let instant = DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
112 .unwrap()
113 .with_timezone(&Utc);
114 let clock = FixedClock(instant);
115 let stopwatch = Stopwatch::start();
116
117 std::thread::sleep(Duration::from_millis(5));
118
119 assert_eq!(clock.now(), instant);
120 assert!(stopwatch.elapsed() >= Duration::from_millis(5));
121 }
122
123 #[test]
124 fn fixed_clock_returns_same_instant() {
125 let instant = DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
126 .unwrap()
127 .with_timezone(&Utc);
128 let clock = FixedClock(instant);
129 assert_eq!(clock.now(), instant);
130 assert_eq!(clock.now(), instant);
131 }
132
133 #[test]
134 fn clock_is_usable_behind_the_shared_handle() {
135 let instant = DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
136 .unwrap()
137 .with_timezone(&Utc);
138 let clock: ClockRef = Arc::new(FixedClock(instant));
139 assert_eq!(clock.now(), instant);
140 }
141
142 #[test]
143 fn closure_clock_reads_the_supplied_source_each_time() {
144 let start = DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
145 .unwrap()
146 .with_timezone(&Utc);
147 let calls = AtomicU64::new(0);
148 let clock = ClosureClock(|| {
149 let seconds = calls.fetch_add(1, Ordering::SeqCst) as i64;
150 start + chrono::Duration::seconds(seconds)
151 });
152
153 assert_eq!(clock.now(), start);
154 assert_eq!(clock.now(), start + chrono::Duration::seconds(1));
155 }
156}