use test_better_core::{TestError, TestResult};
use crate::strategy::{Runner, Strategy, ValueTree};
#[derive(Debug, Clone, Copy)]
pub struct Config {
pub cases: u32,
}
impl Default for Config {
fn default() -> Self {
Self { cases: 256 }
}
}
#[derive(Debug)]
pub struct PropertyFailure<T> {
pub original: T,
pub shrunk: T,
pub failure: TestError,
pub cases: u32,
}
pub fn for_all<T, S, F>(strategy: S, property: F) -> Result<(), PropertyFailure<T>>
where
S: Strategy<T>,
T: Clone,
F: FnMut(T) -> TestResult,
{
for_all_with(
Config::default(),
&mut Runner::deterministic(),
strategy,
property,
)
}
pub fn for_all_with<T, S, F>(
config: Config,
runner: &mut Runner,
strategy: S,
mut property: F,
) -> Result<(), PropertyFailure<T>>
where
S: Strategy<T>,
T: Clone,
F: FnMut(T) -> TestResult,
{
for case in 0..config.cases {
let Ok(mut tree) = strategy.new_tree(runner) else {
continue;
};
let value = tree.current();
let Err(failure) = property(value.clone()) else {
continue;
};
let (shrunk, failure) = shrink(&mut tree, value.clone(), failure, &mut property);
return Err(PropertyFailure {
original: value,
shrunk,
failure,
cases: case + 1,
});
}
Ok(())
}
fn shrink<T, VT, F>(
tree: &mut VT,
mut minimal: T,
mut minimal_failure: TestError,
property: &mut F,
) -> (T, TestError)
where
VT: ValueTree<T>,
T: Clone,
F: FnMut(T) -> TestResult,
{
while tree.simplify() {
loop {
let candidate = tree.current();
match property(candidate.clone()) {
Err(failure) => {
minimal = candidate;
minimal_failure = failure;
break;
}
Ok(()) => {
if !tree.complicate() {
return (minimal, minimal_failure);
}
}
}
}
}
(minimal, minimal_failure)
}
#[cfg(test)]
mod tests {
use super::*;
use test_better_core::{OrFail, TestResult};
use test_better_matchers::{check, eq, ge, is_true, lt};
#[test]
fn a_property_that_always_holds_passes() -> TestResult {
let outcome = for_all(0u32..1_000, |n| check!(n).satisfies(lt(1_000u32)));
check!(outcome.is_ok()).satisfies(is_true())
}
#[test]
fn a_failing_property_shrinks_to_the_minimal_counterexample() -> TestResult {
let failure = for_all(proptest::num::u32::ANY, |n| check!(n).satisfies(lt(100u32)))
.err()
.or_fail_with("a property that is false for most u32 must fail")?;
check!(failure.shrunk).satisfies(eq(100u32))?;
check!(failure.original).satisfies(ge(100u32))?;
check!(failure.cases).satisfies(ge(1u32))
}
#[test]
fn the_shrunk_failure_is_the_one_the_minimal_input_produces() -> TestResult {
let failure = for_all(proptest::num::i64::ANY, |n| check!(n).satisfies(lt(0i64)))
.err()
.or_fail_with("non-negative i64 values exist")?;
check!(failure.shrunk).satisfies(eq(0i64))?;
let rendered = failure.failure.to_string();
check!(rendered.contains("less than 0")).satisfies(is_true())
}
#[test]
fn for_all_with_honors_a_smaller_case_count() -> TestResult {
let mut runner = Runner::deterministic();
let outcome = for_all_with(Config { cases: 1 }, &mut runner, 0u32..10, |_| {
TestResult::Ok(())
});
check!(outcome.is_ok()).satisfies(is_true())
}
}