1use chrono::{DateTime, Utc};
2use once_cell::sync::Lazy;
3use parking_lot::Mutex;
4
5static FROZEN_TIME: Lazy<Mutex<Option<DateTime<Utc>>>> = Lazy::new(|| Mutex::new(None));
6#[cfg(test)]
7pub(crate) static TESTING_TIME_LOCK: std::sync::LazyLock<std::sync::Mutex<()>> =
8 std::sync::LazyLock::new(|| std::sync::Mutex::new(()));
9
10pub fn assert_changes<T, F>(before: T, f: F, expected_after: T)
15where
16 T: PartialEq + std::fmt::Debug,
17 F: FnOnce(),
18{
19 f();
20 assert_eq!(
21 before, expected_after,
22 "expected value to change to the requested final state"
23 );
24}
25
26pub fn assert_no_changes<T, F>(get_value: impl Fn() -> T, f: F)
28where
29 T: PartialEq + std::fmt::Debug,
30 F: FnOnce(),
31{
32 let before = get_value();
33 f();
34 let after = get_value();
35 assert_eq!(before, after, "expected value to remain unchanged");
36}
37
38pub fn assert_difference<F>(get_value: impl Fn() -> i64, expected_diff: i64, f: F)
40where
41 F: FnOnce(),
42{
43 let before = get_value();
44 f();
45 let after = get_value();
46 assert_eq!(
47 after - before,
48 expected_diff,
49 "expected numeric value to change by {expected_diff}, but changed by {}",
50 after - before
51 );
52}
53
54pub fn assert_no_difference<F>(get_value: impl Fn() -> i64, f: F)
56where
57 F: FnOnce(),
58{
59 assert_difference(get_value, 0, f);
60}
61
62#[derive(Debug)]
64pub struct TimeFreezeGuard {
65 previous: Option<DateTime<Utc>>,
66}
67
68impl Drop for TimeFreezeGuard {
69 fn drop(&mut self) {
70 *FROZEN_TIME.lock() = self.previous;
71 }
72}
73
74pub fn freeze_time(at: DateTime<Utc>) -> TimeFreezeGuard {
76 let mut slot = FROZEN_TIME.lock();
77 let previous = slot.replace(at);
78 TimeFreezeGuard { previous }
79}
80
81pub(crate) fn frozen_now() -> Option<DateTime<Utc>> {
82 *FROZEN_TIME.lock()
83}
84
85#[cfg(test)]
86mod tests {
87 use super::{
88 TESTING_TIME_LOCK, assert_changes, assert_difference, assert_no_changes,
89 assert_no_difference, freeze_time, frozen_now,
90 };
91 use chrono::{TimeZone as _, Utc};
92 use std::cell::Cell;
93 use std::rc::Rc;
94
95 #[test]
96 fn testing_assert_changes_accepts_shared_mutable_values() {
97 let counter = Rc::new(Cell::new(1));
98 let observed = Rc::clone(&counter);
99
100 assert_changes(observed, || counter.set(2), Rc::new(Cell::new(2)));
101 }
102
103 #[test]
104 #[should_panic(expected = "expected value to change")]
105 fn testing_assert_changes_panics_when_final_state_is_unexpected() {
106 let counter = Rc::new(Cell::new(1));
107 let observed = Rc::clone(&counter);
108
109 assert_changes(observed, || counter.set(2), Rc::new(Cell::new(3)));
110 }
111
112 #[test]
113 fn testing_assert_no_changes_passes_for_stable_values() {
114 let value = Cell::new(10);
115
116 assert_no_changes(
117 || value.get(),
118 || {
119 let _ = value.get();
120 },
121 );
122 }
123
124 #[test]
125 #[should_panic(expected = "expected value to remain unchanged")]
126 fn testing_assert_no_changes_panics_for_changed_values() {
127 let value = Cell::new(10);
128
129 assert_no_changes(|| value.get(), || value.set(20));
130 }
131
132 #[test]
133 fn testing_assert_difference_tracks_numeric_change() {
134 let value = Cell::new(5);
135
136 assert_difference(|| i64::from(value.get()), 3, || value.set(8));
137 }
138
139 #[test]
140 #[should_panic(expected = "expected numeric value to change by 2")]
141 fn testing_assert_difference_panics_for_wrong_delta() {
142 let value = Cell::new(5);
143
144 assert_difference(|| i64::from(value.get()), 2, || value.set(8));
145 }
146
147 #[test]
148 fn testing_assert_no_difference_accepts_no_change() {
149 let value = Cell::new(5);
150
151 assert_no_difference(
152 || i64::from(value.get()),
153 || {
154 let _ = value.get();
155 },
156 );
157 }
158
159 #[test]
160 #[should_panic(expected = "expected numeric value to change by 0")]
161 fn testing_assert_no_difference_panics_when_value_changes() {
162 let value = Cell::new(5);
163
164 assert_no_difference(|| i64::from(value.get()), || value.set(6));
165 }
166
167 #[test]
168 fn testing_freeze_time_sets_and_restores_time() {
169 let _lock = TESTING_TIME_LOCK.lock().unwrap();
170 let initial = frozen_now();
171 let frozen = Utc.with_ymd_and_hms(2024, 1, 1, 12, 0, 0).unwrap();
172
173 {
174 let _guard = freeze_time(frozen);
175 assert_eq!(frozen_now(), Some(frozen));
176 }
177
178 assert_eq!(frozen_now(), initial);
179 }
180
181 #[test]
182 fn testing_freeze_time_restores_previous_value_when_nested() {
183 let _lock = TESTING_TIME_LOCK.lock().unwrap();
184 let baseline = frozen_now();
185 let first = Utc.with_ymd_and_hms(2024, 1, 1, 12, 0, 0).unwrap();
186 let second = Utc.with_ymd_and_hms(2024, 1, 1, 13, 0, 0).unwrap();
187
188 let outer = freeze_time(first);
189 assert_eq!(frozen_now(), Some(first));
190 {
191 let _inner = freeze_time(second);
192 assert_eq!(frozen_now(), Some(second));
193 }
194 assert_eq!(frozen_now(), Some(first));
195 drop(outer);
196 assert_eq!(frozen_now(), baseline);
197 }
198
199 #[test]
200 fn testing_freeze_time_restores_baseline_after_panic() {
201 let _lock = TESTING_TIME_LOCK.lock().unwrap();
202 let baseline = frozen_now();
203 let frozen = Utc.with_ymd_and_hms(2024, 2, 1, 9, 30, 0).unwrap();
204
205 let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
206 let _guard = freeze_time(frozen);
207 assert_eq!(frozen_now(), Some(frozen));
208 panic!("boom");
209 }));
210
211 assert!(panic.is_err());
212 assert_eq!(frozen_now(), baseline);
213 }
214
215 #[test]
216 fn testing_nested_freeze_time_restores_outer_value_after_inner_panic() {
217 let _lock = TESTING_TIME_LOCK.lock().unwrap();
218 let baseline = frozen_now();
219 let first = Utc.with_ymd_and_hms(2024, 2, 1, 9, 30, 0).unwrap();
220 let second = Utc.with_ymd_and_hms(2024, 2, 1, 10, 30, 0).unwrap();
221
222 let outer = freeze_time(first);
223 let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
224 let _inner = freeze_time(second);
225 assert_eq!(frozen_now(), Some(second));
226 panic!("boom");
227 }));
228
229 assert!(panic.is_err());
230 assert_eq!(frozen_now(), Some(first));
231 drop(outer);
232 assert_eq!(frozen_now(), baseline);
233 }
234
235 #[test]
236 fn testing_assert_changes_accepts_multiple_mutations_before_final_state() {
237 let value = Rc::new(Cell::new(1));
238 let observed = Rc::clone(&value);
239
240 assert_changes(
241 observed,
242 || {
243 value.set(2);
244 value.set(3);
245 },
246 Rc::new(Cell::new(3)),
247 );
248 }
249
250 #[test]
251 fn testing_assert_changes_panic_message_is_stable() {
252 use std::panic::{AssertUnwindSafe, catch_unwind};
253
254 let value = Rc::new(Cell::new(1));
255 let observed = Rc::clone(&value);
256
257 let panic = catch_unwind(AssertUnwindSafe(|| {
258 assert_changes(observed, || value.set(2), Rc::new(Cell::new(3)));
259 }))
260 .unwrap_err();
261
262 let message = panic
263 .downcast_ref::<String>()
264 .cloned()
265 .or_else(|| {
266 panic
267 .downcast_ref::<&str>()
268 .map(|message| (*message).to_owned())
269 })
270 .unwrap();
271
272 assert!(message.contains("expected value to change to the requested final state"));
273 }
274
275 #[test]
276 fn testing_assert_no_changes_panic_message_is_stable() {
277 use std::panic::{AssertUnwindSafe, catch_unwind};
278
279 let value = Cell::new(10);
280
281 let panic = catch_unwind(AssertUnwindSafe(|| {
282 assert_no_changes(|| value.get(), || value.set(20));
283 }))
284 .unwrap_err();
285
286 let message = panic
287 .downcast_ref::<String>()
288 .cloned()
289 .or_else(|| {
290 panic
291 .downcast_ref::<&str>()
292 .map(|message| (*message).to_owned())
293 })
294 .unwrap();
295
296 assert!(message.contains("expected value to remain unchanged"));
297 }
298
299 #[test]
300 fn testing_assert_difference_supports_negative_deltas() {
301 let value = Cell::new(5);
302
303 assert_difference(|| i64::from(value.get()), -2, || value.set(3));
304 }
305
306 #[test]
307 fn testing_assert_difference_supports_explicit_zero_delta() {
308 let value = Cell::new(5);
309
310 assert_difference(
311 || i64::from(value.get()),
312 0,
313 || {
314 let _ = value.get();
315 },
316 );
317 }
318
319 #[test]
320 fn testing_assert_difference_panic_reports_actual_delta() {
321 use std::panic::{AssertUnwindSafe, catch_unwind};
322
323 let value = Cell::new(5);
324
325 let panic = catch_unwind(AssertUnwindSafe(|| {
326 assert_difference(|| i64::from(value.get()), 2, || value.set(8));
327 }))
328 .unwrap_err();
329
330 let message = panic
331 .downcast_ref::<String>()
332 .cloned()
333 .or_else(|| {
334 panic
335 .downcast_ref::<&str>()
336 .map(|message| (*message).to_owned())
337 })
338 .unwrap();
339
340 assert!(message.contains("expected numeric value to change by 2"));
341 assert!(message.contains("changed by 3"));
342 }
343
344 #[test]
345 fn testing_assert_no_difference_panic_mentions_zero_delta() {
346 use std::panic::{AssertUnwindSafe, catch_unwind};
347
348 let value = Cell::new(5);
349
350 let panic = catch_unwind(AssertUnwindSafe(|| {
351 assert_no_difference(|| i64::from(value.get()), || value.set(6));
352 }))
353 .unwrap_err();
354
355 let message = panic
356 .downcast_ref::<String>()
357 .cloned()
358 .or_else(|| {
359 panic
360 .downcast_ref::<&str>()
361 .map(|message| (*message).to_owned())
362 })
363 .unwrap();
364
365 assert!(message.contains("expected numeric value to change by 0"));
366 }
367
368 #[test]
369 fn testing_nested_assert_difference_tracks_both_scopes() {
370 let outer = Cell::new(1);
371 let inner = Cell::new(10);
372
373 assert_difference(
374 || i64::from(outer.get()),
375 2,
376 || {
377 assert_difference(|| i64::from(inner.get()), -3, || inner.set(7));
378 outer.set(3);
379 },
380 );
381 }
382
383 #[test]
384 fn testing_assert_difference_can_wrap_assert_no_difference() {
385 let changed = Cell::new(1);
386 let stable = Cell::new(10);
387
388 assert_difference(
389 || i64::from(changed.get()),
390 4,
391 || {
392 assert_no_changes(
393 || stable.get(),
394 || {
395 let _ = stable.get();
396 },
397 );
398 changed.set(5);
399 },
400 );
401 }
402
403 #[test]
404 fn testing_assert_no_difference_can_wrap_assert_difference_on_other_value() {
405 let stable = Cell::new(10);
406 let changed = Cell::new(1);
407
408 assert_no_difference(
409 || i64::from(stable.get()),
410 || {
411 assert_difference(|| i64::from(changed.get()), 2, || changed.set(3));
412 },
413 );
414 }
415
416 #[test]
417 fn testing_assert_changes_supports_refcell_backed_values() {
418 use std::cell::RefCell;
419
420 let value = Rc::new(RefCell::new(String::from("draft")));
421 let observed = Rc::clone(&value);
422
423 assert_changes(
424 observed,
425 || *value.borrow_mut() = String::from("published"),
426 Rc::new(RefCell::new(String::from("published"))),
427 );
428 }
429
430 #[test]
431 fn testing_assert_no_changes_allows_nested_reads() {
432 let value = Cell::new(10);
433
434 assert_no_changes(
435 || value.get(),
436 || {
437 let before = value.get();
438 let after = value.get();
439 assert_eq!(before, after);
440 },
441 );
442 }
443
444 #[test]
445 fn testing_assert_difference_observes_multiple_updates() {
446 let value = Cell::new(1);
447
448 assert_difference(
449 || i64::from(value.get()),
450 4,
451 || {
452 value.set(3);
453 value.set(5);
454 },
455 );
456 }
457
458 #[test]
459 fn testing_assert_difference_handles_negative_start_values() {
460 let value = Cell::new(-3);
461
462 assert_difference(|| i64::from(value.get()), 5, || value.set(2));
463 }
464
465 #[test]
466 fn testing_frozen_now_matches_baseline_without_guard() {
467 let _lock = TESTING_TIME_LOCK.lock().unwrap();
468 let baseline = frozen_now();
469
470 assert_eq!(frozen_now(), baseline);
471 }
472}