This repository has no description
1use std::future::Future;
2use std::pin::Pin;
3use std::time::{Duration, SystemTime, UNIX_EPOCH};
4
5use tokio::time::Instant;
6
7#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
8pub struct UnixMicros(u64);
9
10impl UnixMicros {
11 pub const fn new(value: u64) -> Self {
12 Self(value)
13 }
14
15 pub const fn raw(self) -> u64 {
16 self.0
17 }
18}
19
20pub type SleepFuture = Pin<Box<dyn Future<Output = ()> + Send + 'static>>;
21
22pub trait Clock: Send + Sync + 'static {
23 fn now_unix_micros(&self) -> UnixMicros;
24 fn now_instant(&self) -> Instant;
25 fn sleep(&self, duration: Duration) -> SleepFuture;
26 fn sleep_until(&self, deadline: Instant) -> SleepFuture;
27}
28
29#[derive(Clone, Copy, Debug)]
30pub struct SystemClock {
31 base_unix: UnixMicros,
32 base_instant: Instant,
33}
34
35impl SystemClock {
36 pub fn new() -> Self {
37 let raw = SystemTime::now()
38 .duration_since(UNIX_EPOCH)
39 .expect("system clock before unix epoch")
40 .as_micros();
41 Self {
42 base_unix: UnixMicros::new(u64::try_from(raw).unwrap_or(u64::MAX)),
43 base_instant: Instant::now(),
44 }
45 }
46}
47
48impl Default for SystemClock {
49 fn default() -> Self {
50 Self::new()
51 }
52}
53
54impl Clock for SystemClock {
55 fn now_unix_micros(&self) -> UnixMicros {
56 let elapsed = self
57 .now_instant()
58 .saturating_duration_since(self.base_instant);
59 let micros = u64::try_from(elapsed.as_micros()).unwrap_or(u64::MAX);
60 UnixMicros::new(self.base_unix.raw().saturating_add(micros))
61 }
62
63 fn now_instant(&self) -> Instant {
64 Instant::now()
65 }
66
67 fn sleep(&self, duration: Duration) -> SleepFuture {
68 Box::pin(tokio::time::sleep(duration))
69 }
70
71 fn sleep_until(&self, deadline: Instant) -> SleepFuture {
72 Box::pin(tokio::time::sleep_until(deadline))
73 }
74}
75
76#[derive(Clone, Debug)]
77pub struct SimClock {
78 base_unix: UnixMicros,
79 base_instant: Instant,
80}
81
82impl SimClock {
83 pub fn at(base_unix: UnixMicros) -> Self {
84 Self {
85 base_unix,
86 base_instant: Instant::now(),
87 }
88 }
89}
90
91impl Clock for SimClock {
92 fn now_unix_micros(&self) -> UnixMicros {
93 let elapsed = self
94 .now_instant()
95 .saturating_duration_since(self.base_instant);
96 let micros = u64::try_from(elapsed.as_micros()).unwrap_or(u64::MAX);
97 UnixMicros::new(self.base_unix.raw().saturating_add(micros))
98 }
99
100 fn now_instant(&self) -> Instant {
101 Instant::now()
102 }
103
104 fn sleep(&self, duration: Duration) -> SleepFuture {
105 Box::pin(tokio::time::sleep(duration))
106 }
107
108 fn sleep_until(&self, deadline: Instant) -> SleepFuture {
109 Box::pin(tokio::time::sleep_until(deadline))
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use std::sync::Arc;
117 use std::sync::atomic::{AtomicU64, Ordering};
118
119 struct ManualClock {
120 base: Instant,
121 unix_micros: AtomicU64,
122 }
123
124 impl ManualClock {
125 fn new(unix_micros: u64) -> Self {
126 Self {
127 base: Instant::now(),
128 unix_micros: AtomicU64::new(unix_micros),
129 }
130 }
131
132 fn advance(&self, micros: u64) {
133 self.unix_micros.fetch_add(micros, Ordering::SeqCst);
134 }
135 }
136
137 impl Clock for ManualClock {
138 fn now_unix_micros(&self) -> UnixMicros {
139 UnixMicros::new(self.unix_micros.load(Ordering::SeqCst))
140 }
141
142 fn now_instant(&self) -> Instant {
143 self.base + Duration::from_micros(self.unix_micros.load(Ordering::SeqCst))
144 }
145
146 fn sleep(&self, _: Duration) -> SleepFuture {
147 Box::pin(std::future::ready(()))
148 }
149
150 fn sleep_until(&self, _: Instant) -> SleepFuture {
151 Box::pin(std::future::ready(()))
152 }
153 }
154
155 #[tokio::test]
156 async fn external_clock_works_behind_dyn_arc() {
157 let clock: Arc<dyn Clock> = Arc::new(ManualClock::new(42));
158 assert_eq!(clock.now_unix_micros().raw(), 42);
159 let before = clock.now_instant();
160 clock.sleep(Duration::from_secs(60)).await;
161 assert_eq!(
162 clock.now_instant(),
163 before,
164 "manual clock's sleep must not advance virtual time on its own",
165 );
166 }
167
168 #[tokio::test]
169 async fn manual_clock_advance_moves_both_unix_and_instant() {
170 let clock = Arc::new(ManualClock::new(1_000));
171 let dyn_clock: Arc<dyn Clock> = clock.clone();
172 let unix_before = dyn_clock.now_unix_micros().raw();
173 let instant_before = dyn_clock.now_instant();
174 clock.advance(500);
175 assert_eq!(dyn_clock.now_unix_micros().raw(), unix_before + 500);
176 assert_eq!(
177 dyn_clock.now_instant(),
178 instant_before + Duration::from_micros(500),
179 );
180 }
181
182 #[tokio::test(start_paused = true)]
183 async fn sim_clock_anchors_unix_at_explicit_base_under_paused_time() {
184 let base = UnixMicros::new(1_700_000_000_000_000);
185 let clock = SimClock::at(base);
186 assert_eq!(
187 clock.now_unix_micros(),
188 base,
189 "sim clock unix axis must reflect the explicit base, not wall clock",
190 );
191 let bump = Duration::from_secs(10);
192 tokio::time::advance(bump).await;
193 assert_eq!(
194 clock.now_unix_micros().raw(),
195 base.raw() + u64::try_from(bump.as_micros()).unwrap(),
196 "sim clock unix axis must follow tokio virtual advance",
197 );
198 }
199
200 #[tokio::test(start_paused = true)]
201 async fn sim_clock_two_constructions_with_same_base_align() {
202 let base = UnixMicros::new(2_000_000_000_000_000);
203 let a = SimClock::at(base);
204 let b = SimClock::at(base);
205 assert_eq!(a.now_unix_micros(), b.now_unix_micros());
206 tokio::time::advance(Duration::from_secs(5)).await;
207 assert_eq!(
208 a.now_unix_micros(),
209 b.now_unix_micros(),
210 "two sim clocks anchored at the same base must agree on virtual time",
211 );
212 }
213
214 #[tokio::test(start_paused = true)]
215 async fn system_clock_axes_advance_together_under_paused_time() {
216 let clock = SystemClock::new();
217 let unix_before = clock.now_unix_micros().raw();
218 let instant_before = clock.now_instant();
219 let bump = Duration::from_secs(60);
220 tokio::time::advance(bump).await;
221 let unix_after = clock.now_unix_micros().raw();
222 let instant_after = clock.now_instant();
223 assert_eq!(
224 instant_after.saturating_duration_since(instant_before),
225 bump,
226 "instant axis must reflect virtual advance",
227 );
228 assert_eq!(
229 unix_after - unix_before,
230 u64::try_from(bump.as_micros()).unwrap(),
231 "unix axis must follow the same virtual advance, not wall clock",
232 );
233 }
234}