use std::collections::BTreeSet;
use std::fmt;
use proptest::strategy::Strategy;
use proptest::test_runner::TestError;
use super::Sim;
use super::assert::{reset_sometimes_registry, sometimes_snapshot};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SweepFailure<T> {
pub seed: u64,
pub shrunk_ops: Vec<T>,
pub reason: String,
}
impl<T: fmt::Debug> fmt::Display for SweepFailure<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"AUTUMN_SIM_SEED=0x{:x} — shrunk to {} op(s): {:?} ({})",
self.seed,
self.shrunk_ops.len(),
self.shrunk_ops,
self.reason,
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SweepOutcome<T> {
Passed {
seeds_run: u64,
},
Failed {
seeds_run: u64,
failure: SweepFailure<T>,
},
Vacuous {
seeds_run: u64,
unsatisfied: BTreeSet<String>,
},
Empty,
}
pub fn sweep_proptest<T, S, F>(
seeds: impl IntoIterator<Item = u64>,
strategy: &S,
body: F,
) -> SweepOutcome<T>
where
T: fmt::Debug,
S: Strategy<Value = Vec<T>>,
F: Fn(&mut Sim, &[T]),
{
let mut seeds_run = 0u64;
let mut all_observed = BTreeSet::new();
let mut all_satisfied = BTreeSet::new();
for seed in seeds {
seeds_run += 1;
let mut seed_observed = BTreeSet::new();
let mut seed_satisfied = BTreeSet::new();
let result = Sim::run_proptest_with_case_hook(seed, strategy, &body, || {
let (observed, satisfied) = sometimes_snapshot();
seed_observed.extend(observed);
seed_satisfied.extend(satisfied);
reset_sometimes_registry();
});
all_observed.extend(seed_observed);
all_satisfied.extend(seed_satisfied);
if let Err(err) = result {
let (reason, shrunk_ops) = match err {
TestError::Fail(reason, shrunk_ops) => (reason.to_string(), shrunk_ops),
TestError::Abort(reason) => (format!("aborted: {reason}"), Vec::new()),
};
return SweepOutcome::Failed {
seeds_run,
failure: SweepFailure {
seed,
shrunk_ops,
reason,
},
};
}
}
if seeds_run == 0 {
return SweepOutcome::Empty;
}
let unsatisfied: BTreeSet<String> = all_observed.difference(&all_satisfied).cloned().collect();
if unsatisfied.is_empty() {
SweepOutcome::Passed { seeds_run }
} else {
SweepOutcome::Vacuous {
seeds_run,
unsatisfied,
}
}
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use super::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TinyOp {
Inc,
Dec,
}
fn tiny_op_strategy() -> impl Strategy<Value = Vec<TinyOp>> {
proptest::collection::vec(prop_oneof![Just(TinyOp::Inc), Just(TinyOp::Dec)], 0..16)
}
#[test]
fn failure_display_is_caller_agnostic_and_does_not_prescribe_a_replay_command() {
let failure = SweepFailure {
seed: 300u64,
shrunk_ops: vec![TinyOp::Inc],
reason: "example".to_owned(),
};
let rendered = failure.to_string();
assert!(
!rendered.contains("cargo run") && !rendered.contains("sim-sweep"),
"Display must not prescribe a specific replay command, got: {rendered:?}"
);
assert!(
rendered.contains("0x12c"),
"Display must still report the failing seed, got: {rendered:?}"
);
}
#[test]
fn sweep_passes_when_every_seed_passes_and_is_non_vacuous() {
let strategy = tiny_op_strategy();
let outcome = sweep_proptest(0..8, &strategy, |_sim, ops| {
crate::sometimes!(!ops.is_empty(), "generated-at-least-one-op");
for op in ops {
let _ = op;
}
});
assert_eq!(outcome, SweepOutcome::Passed { seeds_run: 8 });
}
#[test]
fn sweep_reports_empty_rather_than_a_silent_pass_for_a_zero_length_range() {
let strategy = tiny_op_strategy();
let outcome = sweep_proptest(0..0, &strategy, |_sim, _ops| {
panic!("body must never run for an empty seed range");
});
assert_eq!(outcome, SweepOutcome::Empty);
}
#[test]
fn sweep_reports_a_vacuous_sometimes_label_across_the_whole_range() {
let strategy = tiny_op_strategy();
let outcome = sweep_proptest(0..4, &strategy, |_sim, _ops| {
crate::sometimes!(false, "never-satisfied-label");
});
match outcome {
SweepOutcome::Vacuous {
seeds_run,
unsatisfied,
} => {
assert_eq!(seeds_run, 4);
assert_eq!(
unsatisfied.into_iter().collect::<Vec<_>>(),
vec!["never-satisfied-label".to_owned()]
);
}
other => panic!("expected a vacuous outcome, got {other:?}"),
}
}
#[test]
fn sweep_aggregates_sometimes_observations_across_every_case_in_a_seed() {
let strategy = tiny_op_strategy();
let outcome = sweep_proptest(0..1, &strategy, |_sim, ops| {
let has_inc = ops.iter().any(|op| *op == TinyOp::Inc);
crate::sometimes!(has_inc, "case-contained-an-inc");
});
match outcome {
SweepOutcome::Passed { seeds_run } => assert_eq!(seeds_run, 1),
other => panic!(
"expected a non-vacuous pass — some of the 256 cases in seed 0 must contain an Inc, got {other:?}"
),
}
}
#[test]
fn sweep_stops_at_the_first_failing_seed_and_shrinks_it() {
let strategy = tiny_op_strategy();
let outcome = sweep_proptest(0..16, &strategy, |_sim, ops| {
let mut run = 0;
for op in ops {
run = if *op == TinyOp::Inc { run + 1 } else { 0 };
assert!(run < 3, "three Incs in a row");
}
});
match outcome {
SweepOutcome::Failed { seeds_run, failure } => {
assert!(seeds_run >= 1);
assert_eq!(
failure.shrunk_ops,
vec![TinyOp::Inc, TinyOp::Inc, TinyOp::Inc]
);
}
other => panic!("expected a failing sweep, got {other:?}"),
}
}
#[test]
fn sweep_failure_is_deterministic_across_runs() {
let strategy = tiny_op_strategy();
let run = || {
sweep_proptest(0..16, &strategy, |_sim, ops| {
let mut run = 0;
for op in ops {
run = if *op == TinyOp::Inc { run + 1 } else { 0 };
assert!(run < 3, "three Incs in a row");
}
})
};
assert_eq!(run(), run());
}
}