Skip to main content

test_better_property/
check.rs

1//! The property runner: generate cases, run the predicate, shrink on failure.
2//!
3//! [`check`] 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/// Checks `property` against values from `strategy`, using [`Config::default`]
48/// and a reproducible [`Runner`].
49///
50/// Returns `Ok(())` if every generated case satisfies `property`, or a
51/// [`PropertyFailure`] carrying the shrunk counterexample otherwise. The run is
52/// deterministic: the same strategy and property pass or fail the same way
53/// every time (see [`Runner::deterministic`]). For an explicit case count or a
54/// randomized runner, use [`check_with`].
55///
56/// ```
57/// use test_better_core::TestResult;
58/// use test_better_matchers::{expect, lt};
59/// use test_better_property::check;
60///
61/// # fn main() -> TestResult {
62/// // Holds for every `u8`: doubling in `u16` never overflows.
63/// check(0u8..=255, |n| {
64///     let doubled = u16::from(n) * 2;
65///     expect!(doubled).to(lt(512u16))
66/// })
67/// .map_err(|f| f.failure)?;
68/// # Ok(())
69/// # }
70/// ```
71pub fn check<T, S, F>(strategy: S, property: F) -> Result<(), PropertyFailure<T>>
72where
73    S: Strategy<T>,
74    T: Clone,
75    F: FnMut(T) -> TestResult,
76{
77    check_with(
78        Config::default(),
79        &mut Runner::deterministic(),
80        strategy,
81        property,
82    )
83}
84
85/// Checks `property` against values from `strategy` with an explicit [`Config`]
86/// and [`Runner`].
87///
88/// This is [`check`] with its two defaults exposed: pass a [`Config`] to change
89/// the case count, and a [`Runner`] (for example [`Runner::randomized`]) to
90/// change the seeding.
91pub fn check_with<T, S, F>(
92    config: Config,
93    runner: &mut Runner,
94    strategy: S,
95    mut property: F,
96) -> Result<(), PropertyFailure<T>>
97where
98    S: Strategy<T>,
99    T: Clone,
100    F: FnMut(T) -> TestResult,
101{
102    for case in 0..config.cases {
103        // A strategy that cannot produce a value (an over-filtered strategy)
104        // is not a property failure; skip the case and try another draw.
105        let Ok(mut tree) = strategy.new_tree(runner) else {
106            continue;
107        };
108        let value = tree.current();
109        let Err(failure) = property(value.clone()) else {
110            continue;
111        };
112        // `value` failed: shrink toward a minimal counterexample.
113        let (shrunk, failure) = shrink(&mut tree, value.clone(), failure, &mut property);
114        return Err(PropertyFailure {
115            original: value,
116            shrunk,
117            failure,
118            cases: case + 1,
119        });
120    }
121    Ok(())
122}
123
124/// Drives the [`ValueTree`] shrink protocol from a known-failing value.
125///
126/// The protocol: `simplify` to a smaller candidate and test it. If it still
127/// fails, adopt it and `simplify` again. If it stopped failing, `complicate`
128/// back toward the last failure and test *that* candidate, repeating until
129/// `complicate` can move no further. The inner loop is what makes the search
130/// converge: every value `complicate` produces is re-tested, not skipped over
131/// by a premature `simplify`. `minimal` always holds the simplest value seen
132/// to still fail, so it is correct to return even though the tree's own
133/// `current()` may sit on a passing value when the search ends.
134fn shrink<T, VT, F>(
135    tree: &mut VT,
136    mut minimal: T,
137    mut minimal_failure: TestError,
138    property: &mut F,
139) -> (T, TestError)
140where
141    VT: ValueTree<T>,
142    T: Clone,
143    F: FnMut(T) -> TestResult,
144{
145    while tree.simplify() {
146        loop {
147            let candidate = tree.current();
148            match property(candidate.clone()) {
149                // Simpler and still failing: adopt it, then `simplify` again.
150                Err(failure) => {
151                    minimal = candidate;
152                    minimal_failure = failure;
153                    break;
154                }
155                // Simplified past the failure: walk back. If `complicate` can
156                // still move, test the value it lands on; if it cannot, the
157                // search is exhausted.
158                Ok(()) => {
159                    if !tree.complicate() {
160                        return (minimal, minimal_failure);
161                    }
162                }
163            }
164        }
165    }
166    (minimal, minimal_failure)
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    use test_better_core::{OrFail, TestResult};
174    use test_better_matchers::{eq, expect, ge, is_true, lt};
175
176    #[test]
177    fn a_property_that_always_holds_passes() -> TestResult {
178        let outcome = check(0u32..1_000, |n| expect!(n).to(lt(1_000u32)));
179        expect!(outcome.is_ok()).to(is_true())
180    }
181
182    #[test]
183    fn a_failing_property_shrinks_to_the_minimal_counterexample() -> TestResult {
184        // "every u32 is below 100" is false; the smallest counterexample is
185        // exactly 100, and `proptest` shrinks integers toward zero, so the
186        // shrink search must land on it.
187        let failure = check(proptest::num::u32::ANY, |n| expect!(n).to(lt(100u32)))
188            .err()
189            .or_fail_with("a property that is false for most u32 must fail")?;
190        expect!(failure.shrunk).to(eq(100u32))?;
191        // The original counterexample was some value at or above the bound...
192        expect!(failure.original).to(ge(100u32))?;
193        // ...and at least one case ran to find it.
194        expect!(failure.cases).to(ge(1u32))
195    }
196
197    #[test]
198    fn the_shrunk_failure_is_the_one_the_minimal_input_produces() -> TestResult {
199        let failure = check(proptest::num::i64::ANY, |n| expect!(n).to(lt(0i64)))
200            .err()
201            .or_fail_with("non-negative i64 values exist")?;
202        // The minimal non-negative i64 is 0.
203        expect!(failure.shrunk).to(eq(0i64))?;
204        // The carried `TestError` is the failure 0 itself produces.
205        let rendered = failure.failure.to_string();
206        expect!(rendered.contains("less than 0")).to(is_true())
207    }
208
209    #[test]
210    fn check_with_honors_a_smaller_case_count() -> TestResult {
211        // With a single case and an always-true property, exactly one draw is
212        // taken and the run still passes.
213        let mut runner = Runner::deterministic();
214        let outcome = check_with(Config { cases: 1 }, &mut runner, 0u32..10, |_| {
215            TestResult::Ok(())
216        });
217        expect!(outcome.is_ok()).to(is_true())
218    }
219}