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| check!(n).satisfies(lt(100u32)))
191 .err()
192 .or_fail_with("a property that is false for most u32 must fail")?;
193 check!(failure.shrunk).satisfies(eq(100u32))?;
194 // The original counterexample was some value at or above the bound...
195 check!(failure.original).satisfies(ge(100u32))?;
196 // ...and at least one case ran to find it.
197 check!(failure.cases).satisfies(ge(1u32))
198 }
199
200 #[test]
201 fn the_shrunk_failure_is_the_one_the_minimal_input_produces() -> TestResult {
202 let failure = for_all(proptest::num::i64::ANY, |n| check!(n).satisfies(lt(0i64)))
203 .err()
204 .or_fail_with("non-negative i64 values exist")?;
205 // The minimal non-negative i64 is 0.
206 check!(failure.shrunk).satisfies(eq(0i64))?;
207 // The carried `TestError` is the failure 0 itself produces.
208 let rendered = failure.failure.to_string();
209 check!(rendered.contains("less than 0")).satisfies(is_true())
210 }
211
212 #[test]
213 fn for_all_with_honors_a_smaller_case_count() -> TestResult {
214 // With a single case and an always-true property, exactly one draw is
215 // taken and the run still passes.
216 let mut runner = Runner::deterministic();
217 let outcome = for_all_with(Config { cases: 1 }, &mut runner, 0u32..10, |_| {
218 TestResult::Ok(())
219 });
220 check!(outcome.is_ok()).satisfies(is_true())
221 }
222}