Skip to main content

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}