test_better_property/strategy.rs
1//! The `Strategy<T>` seam: the trait the property runner is written against,
2//! and the `proptest` backend that satisfies it.
3//!
4//! The seam is deliberately small. A [`Strategy`] knows how to draw one
5//! [`ValueTree`] from a [`Runner`]'s randomness; a `ValueTree` holds a current
6//! value and can `simplify`/`complicate` it. That is exactly enough to drive
7//! `proptest`'s integrated shrinking, and it leaves room for a second backend
8//! later without promising one today.
9//!
10//! `proptest` plugs in through a blanket impl: every
11//! `proptest::strategy::Strategy` *is* a [`Strategy`] here, so a property test
12//! names ordinary `proptest` strategies and the runner never mentions
13//! `proptest` in its own signatures.
14
15use std::fmt;
16
17/// An opaque error from a strategy that could not produce a value.
18///
19/// It wraps the backend's own generation error so callers do not depend on the
20/// backend's type. In practice the simple strategies a property test uses
21/// (`any::<T>()`, numeric ranges) never fail to generate; this surfaces only
22/// for heavily filtered strategies that exhaust their rejection budget.
23#[derive(Debug, Clone)]
24pub struct GenError(proptest::test_runner::Reason);
25
26impl fmt::Display for GenError {
27 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28 write!(f, "strategy could not generate a value: {}", self.0)
29 }
30}
31
32impl std::error::Error for GenError {}
33
34/// The per-run state a [`Strategy`] draws from: the random number generator
35/// and the backend's bookkeeping.
36///
37/// It wraps `proptest`'s `TestRunner`. The seam owns this type so the backend
38/// does not appear in the property runner's public signatures.
39pub struct Runner {
40 inner: proptest::test_runner::TestRunner,
41}
42
43impl Runner {
44 /// A runner with a fixed, reproducible seed: the same sequence of generated
45 /// values on every run.
46 ///
47 /// This is the default behind [`for_all`](crate::for_all). A property test that
48 /// is reproducible cannot flake from the RNG: it passes or fails the same
49 /// way every time, in CI and on a laptop. A caller who wants a fresh seed
50 /// each run constructs one with [`Runner::randomized`] and passes it to
51 /// [`for_all_with`](crate::for_all_with).
52 #[must_use]
53 pub fn deterministic() -> Self {
54 Self {
55 inner: proptest::test_runner::TestRunner::deterministic(),
56 }
57 }
58
59 /// A runner seeded from the environment, or randomly when the environment
60 /// says nothing, like a stock `proptest` run.
61 #[must_use]
62 pub fn randomized() -> Self {
63 Self {
64 inner: proptest::test_runner::TestRunner::default(),
65 }
66 }
67
68 /// The wrapped backend runner. Private: only the `proptest` adapter impl in
69 /// this module reaches through the seam.
70 fn backend(&mut self) -> &mut proptest::test_runner::TestRunner {
71 &mut self.inner
72 }
73}
74
75impl Default for Runner {
76 /// The reproducible runner, the same as [`Runner::deterministic`].
77 fn default() -> Self {
78 Self::deterministic()
79 }
80}
81
82/// A source of values of type `T`, with shrinking, for property testing.
83///
84/// This is the seam between `test-better`'s property runner
85/// ([`for_all`](crate::for_all)) and a concrete generation/shrinking backend. The
86/// crate ships exactly one backend, `proptest`: every `proptest::strategy::Strategy`
87/// is a `Strategy` here through a blanket impl.
88///
89/// # The blanket impl and its one limitation
90///
91/// Because the blanket `impl<S: proptest::strategy::Strategy> Strategy for S`
92/// covers every `proptest` strategy, a user type that *also* happens to be a
93/// `proptest::strategy::Strategy` cannot carry a hand-written `Strategy` impl
94/// (coherence cannot prove the two do not overlap). This is acceptable today:
95/// `proptest` is the one backend and every strategy is a `proptest` strategy
96/// already. The trait is a seam for a future backend, not a finished
97/// portability layer.
98pub trait Strategy<T> {
99 /// The shrinkable, in-progress value this strategy produces.
100 type Tree: ValueTree<T>;
101
102 /// Draws one fresh value tree from `runner`'s randomness.
103 ///
104 /// # Errors
105 ///
106 /// Returns [`GenError`] if the strategy could not produce a value (a
107 /// filtered strategy that exhausted its rejection budget).
108 fn new_tree(&self, runner: &mut Runner) -> Result<Self::Tree, GenError>;
109}
110
111/// A single generated value that can be shrunk toward a simpler one.
112///
113/// After a failing case the runner calls [`simplify`](Self::simplify) to get a
114/// smaller candidate; if a candidate shrank so far it stopped failing, the
115/// runner calls [`complicate`](Self::complicate) to walk back. Together they
116/// binary-search toward a minimal counterexample.
117pub trait ValueTree<T> {
118 /// The current value.
119 fn current(&self) -> T;
120
121 /// Replaces the current value with a simpler one. Returns `true` if it
122 /// moved, `false` if the value is already as simple as the tree can make
123 /// it.
124 fn simplify(&mut self) -> bool;
125
126 /// Walks back toward the last value that still failed, undoing a
127 /// [`simplify`](Self::simplify) that shrank past the failure. Returns
128 /// `true` if it moved.
129 fn complicate(&mut self) -> bool;
130}
131
132/// Adapts a `proptest` value tree to the seam's [`ValueTree`].
133///
134/// Produced by the blanket [`Strategy`] impl; rarely named directly.
135pub struct ProptestTree<VT>(VT);
136
137impl<VT, T> ValueTree<T> for ProptestTree<VT>
138where
139 VT: proptest::strategy::ValueTree<Value = T>,
140{
141 fn current(&self) -> T {
142 self.0.current()
143 }
144
145 fn simplify(&mut self) -> bool {
146 self.0.simplify()
147 }
148
149 fn complicate(&mut self) -> bool {
150 self.0.complicate()
151 }
152}
153
154impl<S, T> Strategy<T> for S
155where
156 S: proptest::strategy::Strategy<Value = T>,
157{
158 type Tree = ProptestTree<S::Tree>;
159
160 fn new_tree(&self, runner: &mut Runner) -> Result<Self::Tree, GenError> {
161 proptest::strategy::Strategy::new_tree(self, runner.backend())
162 .map(ProptestTree)
163 .map_err(GenError)
164 }
165}
166
167/// The default [`Strategy`] for a type: `proptest`'s `any::<T>()`, surfaced
168/// through the seam.
169///
170/// This is what [`property!`](crate::property) uses when the closure binding
171/// has a type annotation and no `using` clause. It is available for direct use
172/// too: `for_all(any::<u32>(), |n| ...)` is the same as naming the `u32`
173/// strategy inline. The `quickcheck` counterpart is
174/// [`arbitrary`](crate::quickcheck_bridge::arbitrary).
175///
176/// ```
177/// use test_better_core::TestResult;
178/// use test_better_matchers::{check, ge};
179/// use test_better_property::{any, for_all};
180///
181/// # fn main() -> TestResult {
182/// for_all(any::<u8>(), |n: u8| check!(u16::from(n)).satisfies(ge(0u16)))
183/// .map_err(|f| f.failure)?;
184/// # Ok(())
185/// # }
186/// ```
187#[must_use]
188pub fn any<T>() -> impl Strategy<T>
189where
190 T: proptest::arbitrary::Arbitrary,
191{
192 proptest::arbitrary::any::<T>()
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 use test_better_core::{OrFail, TestResult};
200 use test_better_matchers::{check, ge, is_true};
201
202 #[test]
203 fn a_proptest_strategy_is_a_seam_strategy() -> TestResult {
204 // The blanket impl makes a numeric range usable through the seam with
205 // no wrapping at the call site.
206 let mut runner = Runner::deterministic();
207 let tree = (0u32..10).new_tree(&mut runner).or_fail()?;
208 check!(tree.current() < 10).satisfies(is_true())
209 }
210
211 #[test]
212 fn simplify_shrinks_the_current_value_toward_its_origin() -> TestResult {
213 // `proptest` shrinks integers toward zero, so simplifying repeatedly
214 // never *grows* the value.
215 let mut runner = Runner::deterministic();
216 let mut tree = (5u32..1_000).new_tree(&mut runner).or_fail()?;
217 let start = tree.current();
218 while tree.simplify() {}
219 check!(start).satisfies(ge(tree.current()))
220 }
221}