Skip to main content

test_better_property/
check.rs

1//! The property runner: generate cases, run the predicate, shrink on failure.
2//!
3//! [`for_all`] is the user-facing surface. It draws values from a [`Strategy`],
4//! runs a `T -> TestResult` predicate against each, and, on the first failure,
5//! drives the [`ValueTree`] shrink protocol to a minimal counterexample. The
6//! `property!` macro is a thin syntactic wrapper over this; the shrunk-failure
7//! *rendering* is handled separately, so a [`PropertyFailure`] here is plain
8//! structured data.
9
10use test_better_core::{TestError, TestResult};
11
12use crate::strategy::{Runner, Strategy, ValueTree};
13
14/// How a property run is configured.
15#[derive(Debug, Clone, Copy)]
16pub struct Config {
17    /// How many generated cases to try before concluding the property holds.
18    pub cases: u32,
19}
20
21impl Default for Config {
22    /// 256 cases, matching `proptest`'s own default.
23    fn default() -> Self {
24        Self { cases: 256 }
25    }
26}
27
28/// A property that did not hold.
29///
30/// It carries the counterexample twice: `original` is the first generated
31/// input that failed, `shrunk` is the minimal failing input the shrink search
32/// reached. `failure` is the [`TestError`] the shrunk input produced, and
33/// `cases` is how many inputs ran (including the failing one) before shrinking
34/// began.
35#[derive(Debug)]
36pub struct PropertyFailure<T> {
37    /// The first generated input that failed the property.
38    pub original: T,
39    /// The minimal failing input the shrink search reached.
40    pub shrunk: T,
41    /// The failure produced by `shrunk`.
42    pub failure: TestError,
43    /// How many cases ran (including the failing one) before shrinking began.
44    pub cases: u32,
45}
46
47/// Asserts that `property` holds for every value drawn from `strategy`, using
48/// [`Config::default`] and a reproducible [`Runner`].
49///
50/// The name follows the `∀x. P(x)` reading from logic and standard
51/// property-testing vocabulary (Haskell's `forAll`, ScalaCheck's `forAll`).
52///
53/// Returns `Ok(())` if every generated case satisfies `property`, or a
54/// [`PropertyFailure`] carrying the shrunk counterexample otherwise. The run is
55/// deterministic: the same strategy and property pass or fail the same way
56/// every time (see [`Runner::deterministic`]). For an explicit case count or a
57/// randomized runner, use [`for_all_with`].
58///
59/// ```
60/// use test_better_core::TestResult;
61/// use test_better_matchers::{check, lt};
62/// use test_better_property::for_all;
63///
64/// # fn main() -> TestResult {
65/// // Holds for every `u8`: doubling in `u16` never overflows.
66/// for_all(0u8..=255, |n| {
67///     let doubled = u16::from(n) * 2;
68///     check!(doubled).satisfies(lt(512u16))
69/// })
70/// .map_err(|f| f.failure)?;
71/// # Ok(())
72/// # }
73/// ```
74pub fn for_all<T, S, F>(strategy: S, property: F) -> Result<(), PropertyFailure<T>>
75where
76    S: Strategy<T>,
77    T: Clone,
78    F: FnMut(T) -> TestResult,
79{
80    for_all_with(
81        Config::default(),
82        &mut Runner::deterministic(),
83        strategy,
84        property,
85    )
86}
87
88/// Asserts that `property` holds for every value drawn from `strategy`, with an
89/// explicit [`Config`] and [`Runner`].
90///
91/// This is [`for_all`] with its two defaults exposed: pass a [`Config`] to
92/// change the case count, and a [`Runner`] (for example [`Runner::randomized`])
93/// to change the seeding.
94pub fn for_all_with<T, S, F>(
95    config: Config,
96    runner: &mut Runner,
97    strategy: S,
98    mut property: F,
99) -> Result<(), PropertyFailure<T>>
100where
101    S: Strategy<T>,
102    T: Clone,
103    F: FnMut(T) -> TestResult,
104{
105    for case in 0..config.cases {
106        // A strategy that cannot produce a value (an over-filtered strategy)
107        // is not a property failure; skip the case and try another draw.
108        let Ok(mut tree) = strategy.new_tree(runner) else {
109            continue;
110        };
111        let value = tree.current();
112        let Err(failure) = property(value.clone()) else {
113            continue;
114        };
115        // `value` failed: shrink toward a minimal counterexample.
116        let (shrunk, failure) = shrink(&mut tree, value.clone(), failure, &mut property);
117        return Err(PropertyFailure {
118            original: value,
119            shrunk,
120            failure,
121            cases: case + 1,
122        });
123    }
124    Ok(())
125}
126
127/// Drives the [`ValueTree`] shrink protocol from a known-failing value.
128///
129/// The protocol: `simplify` to a smaller candidate and test it. If it still
130/// fails, adopt it and `simplify` again. If it stopped failing, `complicate`
131/// back toward the last failure and test *that* candidate, repeating until
132/// `complicate` can move no further. The inner loop is what makes the search
133/// converge: every value `complicate` produces is re-tested, not skipped over
134/// by a premature `simplify`. `minimal` always holds the simplest value seen
135/// to still fail, so it is correct to return even though the tree's own
136/// `current()` may sit on a passing value when the search ends.
137fn shrink<T, VT, F>(
138    tree: &mut VT,
139    mut minimal: T,
140    mut minimal_failure: TestError,
141    property: &mut F,
142) -> (T, TestError)
143where
144    VT: ValueTree<T>,
145    T: Clone,
146    F: FnMut(T) -> TestResult,
147{
148    while tree.simplify() {
149        loop {
150            let candidate = tree.current();
151            match property(candidate.clone()) {
152                // Simpler and still failing: adopt it, then `simplify` again.
153                Err(failure) => {
154                    minimal = candidate;
155                    minimal_failure = failure;
156                    break;
157                }
158                // Simplified past the failure: walk back. If `complicate` can
159                // still move, test the value it lands on; if it cannot, the
160                // search is exhausted.
161                Ok(()) => {
162                    if !tree.complicate() {
163                        return (minimal, minimal_failure);
164                    }
165                }
166            }
167        }
168    }
169    (minimal, minimal_failure)
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    use test_better_core::{OrFail, TestResult};
177    use test_better_matchers::{check, eq, ge, is_true, lt};
178
179    #[test]
180    fn a_property_that_always_holds_passes() -> TestResult {
181        let outcome = for_all(0u32..1_000, |n| check!(n).satisfies(lt(1_000u32)));
182        check!(outcome.is_ok()).satisfies(is_true())
183    }
184
185    #[test]
186    fn a_failing_property_shrinks_to_the_minimal_counterexample() -> TestResult {
187        // "every u32 is below 100" is false; the smallest counterexample is
188        // exactly 100, and `proptest` shrinks integers toward zero, so the
189        // shrink search must land on it.
190        let failure = for_all(proptest::num::u32::ANY, |n| {
191            check!(n).satisfies(lt(100u32))
192        })
193        .err()
194        .or_fail_with("a property that is false for most u32 must fail")?;
195        check!(failure.shrunk).satisfies(eq(100u32))?;
196        // The original counterexample was some value at or above the bound...
197        check!(failure.original).satisfies(ge(100u32))?;
198        // ...and at least one case ran to find it.
199        check!(failure.cases).satisfies(ge(1u32))
200    }
201
202    #[test]
203    fn the_shrunk_failure_is_the_one_the_minimal_input_produces() -> TestResult {
204        let failure = for_all(proptest::num::i64::ANY, |n| {
205            check!(n).satisfies(lt(0i64))
206        })
207        .err()
208        .or_fail_with("non-negative i64 values exist")?;
209        // The minimal non-negative i64 is 0.
210        check!(failure.shrunk).satisfies(eq(0i64))?;
211        // The carried `TestError` is the failure 0 itself produces.
212        let rendered = failure.failure.to_string();
213        check!(rendered.contains("less than 0")).satisfies(is_true())
214    }
215
216    #[test]
217    fn for_all_with_honors_a_smaller_case_count() -> TestResult {
218        // With a single case and an always-true property, exactly one draw is
219        // taken and the run still passes.
220        let mut runner = Runner::deterministic();
221        let outcome = for_all_with(Config { cases: 1 }, &mut runner, 0u32..10, |_| {
222            TestResult::Ok(())
223        });
224        check!(outcome.is_ok()).satisfies(is_true())
225    }
226}