use std::{
sync::{
Arc,
Mutex,
PoisonError,
atomic::{AtomicUsize, Ordering},
mpsc,
},
thread,
time::Duration,
};
use proptest::prelude::*;
use serial_test::serial;
use super::{
Metric,
MetricsRecorder,
MetricsRecorderGuard,
PasswordReuseOutcomeMetric,
install_metrics_recorder,
record,
};
const DEADLINE: Duration = Duration::from_secs(10);
const SAMPLE: Metric = Metric::PasswordReuse(PasswordReuseOutcomeMetric::Reused);
struct Reentrant<F: Fn() + Send + Sync> {
on_record: F,
seen: Mutex<Vec<Metric>>,
}
impl<F: Fn() + Send + Sync> MetricsRecorder for Reentrant<F> {
fn record(&self, metric: Metric) {
(self.on_record)();
self.seen
.lock()
.unwrap_or_else(PoisonError::into_inner)
.push(metric);
}
}
struct Counting(AtomicUsize);
impl MetricsRecorder for Counting {
fn record(&self, _metric: Metric) { self.0.fetch_add(1, Ordering::Relaxed); }
}
fn record_completes_within_deadline() -> bool {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
record(SAMPLE);
tx.send(()).unwrap_or_default();
});
rx.recv_timeout(DEADLINE).is_ok()
}
fn fail_wedged(guards: Vec<MetricsRecorderGuard>, message: &str) -> ! {
for guard in guards {
std::mem::forget(guard);
}
panic!("{message}");
}
#[test]
#[serial(metrics_recorder)]
fn a_recorder_may_install_another_recorder_from_its_callback() {
let inner = Arc::new(Counting(AtomicUsize::new(0)));
let installed = Arc::clone(&inner);
let recorder = Arc::new(Reentrant {
on_record: move || {
let guard =
install_metrics_recorder(Arc::clone(&installed) as Arc<dyn MetricsRecorder>);
drop(guard);
},
seen: Mutex::new(Vec::new()),
});
let outer = Arc::clone(&recorder);
let guard = install_metrics_recorder(recorder as Arc<dyn MetricsRecorder>);
if !record_completes_within_deadline() {
fail_wedged(
vec![guard],
"recording deadlocked: install_metrics_recorder was called while record held the lock",
);
}
assert_eq!(
outer
.seen
.lock()
.unwrap_or_else(PoisonError::into_inner)
.as_slice(),
&[SAMPLE],
"the outer recorder did not receive the count",
);
}
#[test]
#[serial(metrics_recorder)]
fn a_recorder_may_drop_its_own_guard_from_its_callback() {
let outer = Arc::new(Counting(AtomicUsize::new(0)));
let outer_guard = install_metrics_recorder(Arc::clone(&outer) as Arc<dyn MetricsRecorder>);
let stashed: Arc<Mutex<Option<MetricsRecorderGuard>>> = Arc::new(Mutex::new(None));
let dropper = Arc::clone(&stashed);
let recorder = Arc::new(Reentrant {
on_record: move || {
let taken = dropper
.lock()
.unwrap_or_else(PoisonError::into_inner)
.take();
drop(taken);
},
seen: Mutex::new(Vec::new()),
});
let inner = Arc::clone(&recorder);
*stashed.lock().unwrap_or_else(PoisonError::into_inner) = Some(install_metrics_recorder(
recorder as Arc<dyn MetricsRecorder>,
));
if !record_completes_within_deadline() {
let abandoned = stashed
.lock()
.unwrap_or_else(PoisonError::into_inner)
.take();
let mut guards = vec![outer_guard];
guards.extend(abandoned);
fail_wedged(
guards,
"recording deadlocked: a guard dropped while record held the lock",
);
}
assert_eq!(
inner
.seen
.lock()
.unwrap_or_else(PoisonError::into_inner)
.as_slice(),
&[SAMPLE],
"the recorder did not finish its callback after uninstalling itself",
);
assert_eq!(
outer.0.load(Ordering::Relaxed),
0,
"the restored recorder should not have seen this count",
);
}
#[derive(Debug, Clone, Copy)]
enum Step {
Install,
Uninstall,
}
#[derive(Default)]
struct Model {
recorders: Vec<(Arc<Counting>, usize)>,
held: Vec<(Arc<Counting>, MetricsRecorderGuard)>,
}
impl Drop for Model {
fn drop(&mut self) { while self.held.pop().is_some() {} }
}
impl Model {
fn apply(&mut self, step: Step) {
match step {
Step::Install => {
let recorder = Arc::new(Counting(AtomicUsize::new(0)));
let guard =
install_metrics_recorder(Arc::clone(&recorder) as Arc<dyn MetricsRecorder>);
self.recorders.push((Arc::clone(&recorder), 0));
self.held.push((recorder, guard));
}
Step::Uninstall => {
self.held.pop();
}
}
}
fn record_once(&mut self) {
record(SAMPLE);
let Some((top, _)) = self.held.last() else {
return;
};
let credited = self
.recorders
.iter_mut()
.find(|(recorder, _)| Arc::ptr_eq(recorder, top));
if let Some((_, owed)) = credited {
*owed += 1;
}
}
fn discrepancies(&self) -> Vec<(usize, usize, usize)> {
self.recorders
.iter()
.enumerate()
.filter_map(|(order, (recorder, owed))| {
let observed = recorder.0.load(Ordering::Relaxed);
(observed != *owed).then_some((order, observed, *owed))
})
.collect()
}
}
fn steps() -> impl Strategy<Value = Vec<Step>> {
prop::collection::vec(
prop_oneof![Just(Step::Install), Just(Step::Uninstall)],
0..12,
)
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(64))]
#[test]
#[serial(metrics_recorder)]
fn a_count_reaches_only_the_newest_held_recorder(steps in steps()) {
let mut model = Model::default();
for step in steps {
model.apply(step);
model.record_once();
prop_assert_eq!(model.discrepancies(), Vec::new());
}
}
}