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}