Skip to main content

test_better_property/
quickcheck_bridge.rs

1//! The `quickcheck` bridge: a best-effort second backend behind the
2//! `quickcheck` feature.
3//!
4//! [`arbitrary`] turns any `quickcheck::Arbitrary` type into a seam
5//! [`Strategy<T>`], so a property test can name `arbitrary::<MyType>()` wherever
6//! it would name a `proptest` strategy. This exists to prove the [`Strategy`]
7//! trait is a real seam and not a `proptest`-shaped hole.
8//!
9//! # Reduced fidelity
10//!
11//! The bridge is honest about two limitations, both inherent to `quickcheck`'s
12//! model rather than bugs:
13//!
14//! - **Generation is not seeded by the [`Runner`].** `quickcheck::Gen` owns its
15//!   own RNG and cannot be constructed from an external seed, so an
16//!   `arbitrary()` strategy draws fresh entropy every run. [`Runner::deterministic`]
17//!   does *not* make a `quickcheck`-backed property reproducible; only
18//!   `proptest`-backed strategies honor it.
19//! - **Shrinking is `quickcheck`'s linear `shrink`, not integrated shrinking.**
20//!   `quickcheck::Arbitrary::shrink` yields a flat iterator of smaller
21//!   candidates with no `complicate` step. The bridge maps that onto the seam's
22//!   `simplify`/`complicate` protocol faithfully (every sibling candidate is
23//!   tried, and an accepted candidate is recursively shrunk), but it is still
24//!   `quickcheck`'s search, not `proptest`'s.
25//!
26//! For a property that must be reproducible, prefer a `proptest` strategy.
27
28use std::marker::PhantomData;
29
30use quickcheck::Arbitrary;
31
32use crate::strategy::{GenError, Runner, Strategy, ValueTree};
33
34/// The default `quickcheck::Gen` size, the generator's notion of "how big".
35///
36/// `quickcheck`'s own test harness uses 100; the bridge matches it so a type's
37/// `Arbitrary` impl behaves the same through the seam as it does under
38/// `quickcheck` directly.
39const DEFAULT_GEN_SIZE: usize = 100;
40
41/// Bridges a `quickcheck::Arbitrary` type into the seam as a [`Strategy<T>`].
42///
43/// Use it anywhere a strategy is expected:
44///
45/// ```
46/// use test_better_core::TestResult;
47/// use test_better_matchers::{check, lt};
48/// use test_better_property::{arbitrary, for_all};
49///
50/// # fn main() -> TestResult {
51/// // A `u8` is `quickcheck::Arbitrary`; doubling one in `u16` never overflows.
52/// for_all(arbitrary::<u8>(), |n: u8| {
53///     check!(u16::from(n) * 2).satisfies(lt(512u16))
54/// })
55/// .map_err(|f| f.failure)?;
56/// # Ok(())
57/// # }
58/// ```
59#[must_use]
60pub fn arbitrary<T: Arbitrary>() -> ArbitraryStrategy<T> {
61    ArbitraryStrategy {
62        size: DEFAULT_GEN_SIZE,
63        _marker: PhantomData,
64    }
65}
66
67/// A [`Strategy`] that generates values of `T` through `quickcheck::Arbitrary`.
68///
69/// Created by [`arbitrary`]; rarely named directly.
70#[derive(Debug, Clone, Copy)]
71pub struct ArbitraryStrategy<T> {
72    size: usize,
73    // `fn() -> T` so the marker is `Send`/`Sync` regardless of `T` and does not
74    // claim to own a `T`.
75    _marker: PhantomData<fn() -> T>,
76}
77
78impl<T: Arbitrary> Strategy<T> for ArbitraryStrategy<T> {
79    type Tree = QuickcheckTree<T>;
80
81    fn new_tree(&self, _runner: &mut Runner) -> Result<Self::Tree, GenError> {
82        // `_runner` is deliberately unused: `quickcheck::Gen` owns its RNG and
83        // cannot be seeded from the seam's `Runner`. See the module docs.
84        let mut generator = quickcheck::Gen::new(self.size);
85        let value = T::arbitrary(&mut generator);
86        Ok(QuickcheckTree::new(value))
87    }
88}
89
90/// Adapts `quickcheck`'s linear `shrink` iterator to the seam's [`ValueTree`].
91///
92/// `quickcheck` has no `complicate` step: `Arbitrary::shrink` is a flat iterator
93/// of smaller candidates. This tree maps that onto the `simplify`/`complicate`
94/// protocol the runner drives. `accepted` is the simplest value seen to still
95/// fail; `siblings` is its remaining shrink candidates; `candidate` is the one
96/// handed to the runner but not yet judged.
97///
98/// - [`simplify`](ValueTree::simplify), called again after the runner adopted a
99///   `candidate`, promotes that candidate to `accepted` and shrinks *it*; then
100///   it hands out the next sibling. The runner calling `simplify` again is the
101///   signal that the last `candidate` reproduced the failure.
102/// - [`complicate`](ValueTree::complicate), called when a `candidate` did *not*
103///   reproduce the failure, discards it and tries the next sibling instead.
104///   This is how every candidate in `quickcheck`'s flat iterator gets a turn.
105pub struct QuickcheckTree<T> {
106    accepted: T,
107    siblings: Box<dyn Iterator<Item = T>>,
108    candidate: Option<T>,
109}
110
111impl<T: Arbitrary> QuickcheckTree<T> {
112    fn new(value: T) -> Self {
113        let siblings = value.shrink();
114        Self {
115            accepted: value,
116            siblings,
117            candidate: None,
118        }
119    }
120}
121
122impl<T: Arbitrary> ValueTree<T> for QuickcheckTree<T> {
123    fn current(&self) -> T {
124        self.candidate
125            .clone()
126            .unwrap_or_else(|| self.accepted.clone())
127    }
128
129    fn simplify(&mut self) -> bool {
130        // A pending `candidate` here means the runner is calling `simplify`
131        // again, which it only does after the candidate reproduced the failure:
132        // adopt it as the new baseline and shrink from there.
133        if let Some(accepted) = self.candidate.take() {
134            self.siblings = accepted.shrink();
135            self.accepted = accepted;
136        }
137        match self.siblings.next() {
138            Some(next) => {
139                self.candidate = Some(next);
140                true
141            }
142            None => false,
143        }
144    }
145
146    fn complicate(&mut self) -> bool {
147        // The current `candidate` did not reproduce the failure: drop it and
148        // try the next sibling shrink of the accepted baseline.
149        match self.siblings.next() {
150            Some(next) => {
151                self.candidate = Some(next);
152                true
153            }
154            None => {
155                self.candidate = None;
156                false
157            }
158        }
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    use test_better_core::{OrFail, TestResult};
167    use test_better_matchers::{check, eq, is_true, le};
168
169    #[test]
170    fn an_arbitrary_type_is_a_seam_strategy() -> TestResult {
171        // `u32` is `quickcheck::Arbitrary`; drawing through the seam yields a
172        // value with no wrapping at the call site.
173        let mut runner = Runner::deterministic();
174        let tree = arbitrary::<u32>().new_tree(&mut runner).or_fail()?;
175        // Every `u32` is `<= u32::MAX`; the point is only that a value came out.
176        check!(tree.current()).satisfies(le(u32::MAX))
177    }
178
179    #[test]
180    fn simplify_walks_quickcheck_shrink_toward_zero() -> TestResult {
181        // `quickcheck` shrinks integers toward zero. Starting from a known
182        // value and simplifying as far as the tree will go must not grow it.
183        let mut tree = QuickcheckTree::new(500u32);
184        let start = tree.current();
185        while tree.simplify() {}
186        check!(tree.current() <= start).satisfies(is_true())
187    }
188
189    #[test]
190    fn complicate_advances_to_the_next_sibling_candidate() -> TestResult {
191        // From a small value, `simplify` hands out the first shrink candidate;
192        // `complicate` (the candidate "passed") must move to a different one,
193        // not stall on the first.
194        let mut tree = QuickcheckTree::new(8u32);
195        check!(tree.simplify()).satisfies(is_true())?;
196        let first = tree.current();
197        // `complicate` either advances to another sibling or reports the
198        // iterator is exhausted; if it advanced, the value must have changed.
199        if tree.complicate() {
200            check!(tree.current() != first).satisfies(is_true())?;
201        }
202        // The accepted baseline is untouched by an unaccepted candidate.
203        check!(tree.accepted).satisfies(eq(8u32))
204    }
205}