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}