use chrono::{DateTime, Utc};
use once_cell::sync::Lazy;
use parking_lot::Mutex;
static FROZEN_TIME: Lazy<Mutex<Option<DateTime<Utc>>>> = Lazy::new(|| Mutex::new(None));
#[cfg(test)]
pub(crate) static TESTING_TIME_LOCK: std::sync::LazyLock<std::sync::Mutex<()>> =
std::sync::LazyLock::new(|| std::sync::Mutex::new(()));
pub fn assert_changes<T, F>(before: T, f: F, expected_after: T)
where
T: PartialEq + std::fmt::Debug,
F: FnOnce(),
{
f();
assert_eq!(
before, expected_after,
"expected value to change to the requested final state"
);
}
pub fn assert_no_changes<T, F>(get_value: impl Fn() -> T, f: F)
where
T: PartialEq + std::fmt::Debug,
F: FnOnce(),
{
let before = get_value();
f();
let after = get_value();
assert_eq!(before, after, "expected value to remain unchanged");
}
pub fn assert_difference<F>(get_value: impl Fn() -> i64, expected_diff: i64, f: F)
where
F: FnOnce(),
{
let before = get_value();
f();
let after = get_value();
assert_eq!(
after - before,
expected_diff,
"expected numeric value to change by {expected_diff}, but changed by {}",
after - before
);
}
pub fn assert_no_difference<F>(get_value: impl Fn() -> i64, f: F)
where
F: FnOnce(),
{
assert_difference(get_value, 0, f);
}
#[derive(Debug)]
pub struct TimeFreezeGuard {
previous: Option<DateTime<Utc>>,
}
impl Drop for TimeFreezeGuard {
fn drop(&mut self) {
*FROZEN_TIME.lock() = self.previous;
}
}
pub fn freeze_time(at: DateTime<Utc>) -> TimeFreezeGuard {
let mut slot = FROZEN_TIME.lock();
let previous = slot.replace(at);
TimeFreezeGuard { previous }
}
pub(crate) fn frozen_now() -> Option<DateTime<Utc>> {
*FROZEN_TIME.lock()
}
#[cfg(test)]
mod tests {
use super::{
TESTING_TIME_LOCK, assert_changes, assert_difference, assert_no_changes,
assert_no_difference, freeze_time, frozen_now,
};
use chrono::{TimeZone as _, Utc};
use std::cell::Cell;
use std::rc::Rc;
#[test]
fn testing_assert_changes_accepts_shared_mutable_values() {
let counter = Rc::new(Cell::new(1));
let observed = Rc::clone(&counter);
assert_changes(observed, || counter.set(2), Rc::new(Cell::new(2)));
}
#[test]
#[should_panic(expected = "expected value to change")]
fn testing_assert_changes_panics_when_final_state_is_unexpected() {
let counter = Rc::new(Cell::new(1));
let observed = Rc::clone(&counter);
assert_changes(observed, || counter.set(2), Rc::new(Cell::new(3)));
}
#[test]
fn testing_assert_no_changes_passes_for_stable_values() {
let value = Cell::new(10);
assert_no_changes(
|| value.get(),
|| {
let _ = value.get();
},
);
}
#[test]
#[should_panic(expected = "expected value to remain unchanged")]
fn testing_assert_no_changes_panics_for_changed_values() {
let value = Cell::new(10);
assert_no_changes(|| value.get(), || value.set(20));
}
#[test]
fn testing_assert_difference_tracks_numeric_change() {
let value = Cell::new(5);
assert_difference(|| i64::from(value.get()), 3, || value.set(8));
}
#[test]
#[should_panic(expected = "expected numeric value to change by 2")]
fn testing_assert_difference_panics_for_wrong_delta() {
let value = Cell::new(5);
assert_difference(|| i64::from(value.get()), 2, || value.set(8));
}
#[test]
fn testing_assert_no_difference_accepts_no_change() {
let value = Cell::new(5);
assert_no_difference(
|| i64::from(value.get()),
|| {
let _ = value.get();
},
);
}
#[test]
#[should_panic(expected = "expected numeric value to change by 0")]
fn testing_assert_no_difference_panics_when_value_changes() {
let value = Cell::new(5);
assert_no_difference(|| i64::from(value.get()), || value.set(6));
}
#[test]
fn testing_freeze_time_sets_and_restores_time() {
let _lock = TESTING_TIME_LOCK.lock().unwrap();
let initial = frozen_now();
let frozen = Utc.with_ymd_and_hms(2024, 1, 1, 12, 0, 0).unwrap();
{
let _guard = freeze_time(frozen);
assert_eq!(frozen_now(), Some(frozen));
}
assert_eq!(frozen_now(), initial);
}
#[test]
fn testing_freeze_time_restores_previous_value_when_nested() {
let _lock = TESTING_TIME_LOCK.lock().unwrap();
let baseline = frozen_now();
let first = Utc.with_ymd_and_hms(2024, 1, 1, 12, 0, 0).unwrap();
let second = Utc.with_ymd_and_hms(2024, 1, 1, 13, 0, 0).unwrap();
let outer = freeze_time(first);
assert_eq!(frozen_now(), Some(first));
{
let _inner = freeze_time(second);
assert_eq!(frozen_now(), Some(second));
}
assert_eq!(frozen_now(), Some(first));
drop(outer);
assert_eq!(frozen_now(), baseline);
}
#[test]
fn testing_freeze_time_restores_baseline_after_panic() {
let _lock = TESTING_TIME_LOCK.lock().unwrap();
let baseline = frozen_now();
let frozen = Utc.with_ymd_and_hms(2024, 2, 1, 9, 30, 0).unwrap();
let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _guard = freeze_time(frozen);
assert_eq!(frozen_now(), Some(frozen));
panic!("boom");
}));
assert!(panic.is_err());
assert_eq!(frozen_now(), baseline);
}
#[test]
fn testing_nested_freeze_time_restores_outer_value_after_inner_panic() {
let _lock = TESTING_TIME_LOCK.lock().unwrap();
let baseline = frozen_now();
let first = Utc.with_ymd_and_hms(2024, 2, 1, 9, 30, 0).unwrap();
let second = Utc.with_ymd_and_hms(2024, 2, 1, 10, 30, 0).unwrap();
let outer = freeze_time(first);
let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _inner = freeze_time(second);
assert_eq!(frozen_now(), Some(second));
panic!("boom");
}));
assert!(panic.is_err());
assert_eq!(frozen_now(), Some(first));
drop(outer);
assert_eq!(frozen_now(), baseline);
}
#[test]
fn testing_assert_changes_accepts_multiple_mutations_before_final_state() {
let value = Rc::new(Cell::new(1));
let observed = Rc::clone(&value);
assert_changes(
observed,
|| {
value.set(2);
value.set(3);
},
Rc::new(Cell::new(3)),
);
}
#[test]
fn testing_assert_changes_panic_message_is_stable() {
use std::panic::{AssertUnwindSafe, catch_unwind};
let value = Rc::new(Cell::new(1));
let observed = Rc::clone(&value);
let panic = catch_unwind(AssertUnwindSafe(|| {
assert_changes(observed, || value.set(2), Rc::new(Cell::new(3)));
}))
.unwrap_err();
let message = panic
.downcast_ref::<String>()
.cloned()
.or_else(|| {
panic
.downcast_ref::<&str>()
.map(|message| (*message).to_owned())
})
.unwrap();
assert!(message.contains("expected value to change to the requested final state"));
}
#[test]
fn testing_assert_no_changes_panic_message_is_stable() {
use std::panic::{AssertUnwindSafe, catch_unwind};
let value = Cell::new(10);
let panic = catch_unwind(AssertUnwindSafe(|| {
assert_no_changes(|| value.get(), || value.set(20));
}))
.unwrap_err();
let message = panic
.downcast_ref::<String>()
.cloned()
.or_else(|| {
panic
.downcast_ref::<&str>()
.map(|message| (*message).to_owned())
})
.unwrap();
assert!(message.contains("expected value to remain unchanged"));
}
#[test]
fn testing_assert_difference_supports_negative_deltas() {
let value = Cell::new(5);
assert_difference(|| i64::from(value.get()), -2, || value.set(3));
}
#[test]
fn testing_assert_difference_supports_explicit_zero_delta() {
let value = Cell::new(5);
assert_difference(
|| i64::from(value.get()),
0,
|| {
let _ = value.get();
},
);
}
#[test]
fn testing_assert_difference_panic_reports_actual_delta() {
use std::panic::{AssertUnwindSafe, catch_unwind};
let value = Cell::new(5);
let panic = catch_unwind(AssertUnwindSafe(|| {
assert_difference(|| i64::from(value.get()), 2, || value.set(8));
}))
.unwrap_err();
let message = panic
.downcast_ref::<String>()
.cloned()
.or_else(|| {
panic
.downcast_ref::<&str>()
.map(|message| (*message).to_owned())
})
.unwrap();
assert!(message.contains("expected numeric value to change by 2"));
assert!(message.contains("changed by 3"));
}
#[test]
fn testing_assert_no_difference_panic_mentions_zero_delta() {
use std::panic::{AssertUnwindSafe, catch_unwind};
let value = Cell::new(5);
let panic = catch_unwind(AssertUnwindSafe(|| {
assert_no_difference(|| i64::from(value.get()), || value.set(6));
}))
.unwrap_err();
let message = panic
.downcast_ref::<String>()
.cloned()
.or_else(|| {
panic
.downcast_ref::<&str>()
.map(|message| (*message).to_owned())
})
.unwrap();
assert!(message.contains("expected numeric value to change by 0"));
}
#[test]
fn testing_nested_assert_difference_tracks_both_scopes() {
let outer = Cell::new(1);
let inner = Cell::new(10);
assert_difference(
|| i64::from(outer.get()),
2,
|| {
assert_difference(|| i64::from(inner.get()), -3, || inner.set(7));
outer.set(3);
},
);
}
#[test]
fn testing_assert_difference_can_wrap_assert_no_difference() {
let changed = Cell::new(1);
let stable = Cell::new(10);
assert_difference(
|| i64::from(changed.get()),
4,
|| {
assert_no_changes(
|| stable.get(),
|| {
let _ = stable.get();
},
);
changed.set(5);
},
);
}
#[test]
fn testing_assert_no_difference_can_wrap_assert_difference_on_other_value() {
let stable = Cell::new(10);
let changed = Cell::new(1);
assert_no_difference(
|| i64::from(stable.get()),
|| {
assert_difference(|| i64::from(changed.get()), 2, || changed.set(3));
},
);
}
#[test]
fn testing_assert_changes_supports_refcell_backed_values() {
use std::cell::RefCell;
let value = Rc::new(RefCell::new(String::from("draft")));
let observed = Rc::clone(&value);
assert_changes(
observed,
|| *value.borrow_mut() = String::from("published"),
Rc::new(RefCell::new(String::from("published"))),
);
}
#[test]
fn testing_assert_no_changes_allows_nested_reads() {
let value = Cell::new(10);
assert_no_changes(
|| value.get(),
|| {
let before = value.get();
let after = value.get();
assert_eq!(before, after);
},
);
}
#[test]
fn testing_assert_difference_observes_multiple_updates() {
let value = Cell::new(1);
assert_difference(
|| i64::from(value.get()),
4,
|| {
value.set(3);
value.set(5);
},
);
}
#[test]
fn testing_assert_difference_handles_negative_start_values() {
let value = Cell::new(-3);
assert_difference(|| i64::from(value.get()), 5, || value.set(2));
}
#[test]
fn testing_frozen_now_matches_baseline_without_guard() {
let _lock = TESTING_TIME_LOCK.lock().unwrap();
let baseline = frozen_now();
assert_eq!(frozen_now(), baseline);
}
}