Skip to main content

rustrails_support/
testing.rs

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
10/// Asserts that a shared value changed to the expected state during closure execution.
11///
12/// This helper is intended for values with shared mutability semantics, such as
13/// `Rc<Cell<T>>`, where the cloned `before` handle observes the final state.
14pub 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
26/// Asserts that a computed value did not change during closure execution.
27pub 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
38/// Asserts that a numeric value changed by the expected amount during closure execution.
39pub 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
54/// Asserts that a numeric value did not change during closure execution.
55pub 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/// A guard that restores the previously frozen time when dropped.
63#[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
74/// Freezes the current testing time until the returned guard is dropped.
75pub 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}