use std::fmt;
use proptest::arbitrary::{Arbitrary, any};
use proptest::strategy::{Strategy, ValueTree};
use proptest::test_runner::{Config, RngAlgorithm, TestError, TestRng, TestRunner};
use rand::{RngCore, SeedableRng};
use rand_chacha::ChaCha8Rng;
use super::Sim;
const OP_GEN_STREAM_SALT: u64 = 0x0B_D1_2E_4A_0B_D1_2E_4A;
const DEFAULT_OP_COUNT: std::ops::Range<usize> = 1..64;
fn stream_rng(seed: u64, salt: u64) -> TestRng {
let mut seeder = ChaCha8Rng::seed_from_u64(seed ^ salt);
let mut bytes = [0u8; 32];
seeder.fill_bytes(&mut bytes);
TestRng::from_seed(RngAlgorithm::ChaCha, &bytes)
}
const EMBEDDED_CASES: u32 = 256;
fn embedded_config() -> Config {
Config {
failure_persistence: None,
fork: false,
timeout: 0,
cases: EMBEDDED_CASES,
..Config::default()
}
}
impl Sim {
pub fn gen_ops<T>(&mut self) -> Vec<T>
where
T: fmt::Debug + Arbitrary,
{
self.gen_ops_with(proptest::collection::vec(any::<T>(), DEFAULT_OP_COUNT))
}
pub fn gen_ops_with<T>(&mut self, strategy: impl Strategy<Value = Vec<T>>) -> Vec<T>
where
T: fmt::Debug,
{
let mut runner =
TestRunner::new_with_rng(embedded_config(), stream_rng(self.seed, OP_GEN_STREAM_SALT));
strategy
.new_tree(&mut runner)
.expect("op strategy generation should not fail")
.current()
}
pub fn run_proptest<T, S, F>(seed: u64, strategy: S, body: F) -> Result<(), TestError<Vec<T>>>
where
T: fmt::Debug,
S: Strategy<Value = Vec<T>>,
F: Fn(&mut Self, &[T]),
{
let result = Self::run_proptest_with_ref(seed, &strategy, body);
if let Err(ref err) = result
&& let TestError::Fail(ref reason, ref shrunk_ops) = *err
{
eprintln!(
"AUTUMN_SIM_SEED=0x{seed:x} — shrunk to {} op(s): {shrunk_ops:?} ({reason})",
shrunk_ops.len()
);
}
result
}
pub(crate) fn run_proptest_with_ref<T, S, F>(
seed: u64,
strategy: &S,
body: F,
) -> Result<(), TestError<Vec<T>>>
where
T: fmt::Debug,
S: Strategy<Value = Vec<T>>,
F: Fn(&mut Self, &[T]),
{
Self::run_proptest_with_case_hook(seed, strategy, body, || {})
}
pub(crate) fn run_proptest_with_case_hook<T, S, F, H>(
seed: u64,
strategy: &S,
body: F,
on_case: H,
) -> Result<(), TestError<Vec<T>>>
where
T: fmt::Debug,
S: Strategy<Value = Vec<T>>,
F: Fn(&mut Self, &[T]),
H: FnMut(),
{
let on_case = std::cell::RefCell::new(on_case);
let mut runner =
TestRunner::new_with_rng(embedded_config(), stream_rng(seed, OP_GEN_STREAM_SALT));
runner.run(strategy, |ops| {
let mut sim = Self::from_seed(seed);
body(&mut sim, &ops);
(on_case.borrow_mut())();
Ok(())
})
}
}
#[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 gen_ops_with_is_deterministic_for_the_same_seed() {
let mut a = Sim::from_seed(42);
let mut b = Sim::from_seed(42);
let ops_a = a.gen_ops_with(tiny_op_strategy());
let ops_b = b.gen_ops_with(tiny_op_strategy());
assert_eq!(ops_a, ops_b);
}
#[test]
fn gen_ops_with_diverges_across_seeds() {
let mut a = Sim::from_seed(1);
let mut b = Sim::from_seed(2);
let ops_a = a.gen_ops_with(tiny_op_strategy());
let ops_b = b.gen_ops_with(tiny_op_strategy());
assert_ne!(
ops_a, ops_b,
"different seeds should overwhelmingly diverge"
);
}
#[test]
fn gen_ops_with_is_independent_of_seeded_entropy_stream() {
let mut sim = Sim::from_seed(7);
let baseline = sim.gen_ops_with(tiny_op_strategy());
let mut sim2 = Sim::from_seed(7);
let _ = sim2.rng().next_u64();
let after_rng_use = sim2.gen_ops_with(tiny_op_strategy());
assert_eq!(baseline, after_rng_use);
}
#[test]
fn embedded_config_forces_fork_and_timeout_off() {
let config = embedded_config();
assert!(
!config.fork,
"the embedded op-driver runner must never fork — it has no test_name"
);
assert_eq!(
config.timeout, 0,
"the embedded op-driver runner has no fork timeout to honor"
);
}
#[test]
fn embedded_config_pins_the_case_count_ignoring_proptest_cases() {
assert_eq!(
embedded_config().cases,
EMBEDDED_CASES,
"the embedded op-driver runner must always run exactly EMBEDDED_CASES cases"
);
}
#[test]
fn run_proptest_rebuilds_a_fresh_sim_per_case() {
let seeds_seen = std::sync::Mutex::new(Vec::new());
let result = Sim::run_proptest(0, tiny_op_strategy(), |sim, _ops| {
seeds_seen.lock().unwrap().push(sim.seed);
});
assert!(result.is_ok());
let seen = seeds_seen.into_inner().unwrap();
assert!(!seen.is_empty());
assert!(
seen.iter().all(|&s| s == 0),
"every case must see the same base seed"
);
}
#[test]
fn run_proptest_shrinks_a_failing_sequence_to_a_minimal_reproduction() {
let result = Sim::run_proptest(123, tiny_op_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");
}
});
let err = result.expect_err("the seeded sweep must find a failing sequence");
match err {
TestError::Fail(_, shrunk_ops) => {
assert_eq!(
shrunk_ops,
vec![TinyOp::Inc, TinyOp::Inc, TinyOp::Inc],
"shrinking should reduce to the minimal 3-Inc counterexample, got {shrunk_ops:?}"
);
}
TestError::Abort(reason) => panic!("expected a shrunk failure, got an abort: {reason}"),
}
}
}