monkey_test/
lib.rs

1#![doc(
2    issue_tracker_base_url = "https://github.com/jockbert/monkey_test/issues/"
3)]
4#![doc(
5    html_logo_url = "https://raw.githubusercontent.com/jockbert/monkey_test/main/assets/doc/logo-256.png"
6)]
7#![doc(
8    html_favicon_url = "https://raw.githubusercontent.com/jockbert/monkey_test/main/assets/doc/logo.ico"
9)]
10#![warn(missing_docs)]
11#![doc = include_str!("../DOCUMENTATION.md")]
12
13mod config;
14pub mod gens;
15mod runner;
16pub mod shrinks;
17
18#[cfg(test)]
19mod testing;
20
21// Re-export details from config-module
22pub use config::*;
23
24/// Main entry point for writing property based tests using the monkey-test
25/// tool.
26///
27/// # Example
28/// ```should_panic
29/// use monkey_test::*;
30///
31/// monkey_test()
32///   .with_generator(gens::u8::any())
33///   .assert_true(|x| x < 15);
34/// ```
35pub fn monkey_test() -> Conf {
36    Conf::default()
37}
38
39/// A boxed iterator of example type `E`
40pub type BoxIter<E> = Box<dyn Iterator<Item = E>>;
41
42/// A boxed shrinker of example type `E`
43pub type BoxShrink<E> = Box<dyn Shrink<E>>;
44
45/// A boxed generator of example type `E`
46pub type BoxGen<E> = Box<dyn Gen<E>>;
47
48/// A property is something that should hold, for all given examples.
49pub type Property<E> = fn(E) -> bool;
50
51/// Trait that enables cloning a boxed generator.
52#[doc(hidden)]
53pub trait CloneGen<E> {
54    fn clone_box(&self) -> BoxGen<E>;
55}
56
57impl<E: Clone + 'static, T> CloneGen<E> for T
58where
59    T: Gen<E> + Clone + 'static,
60{
61    fn clone_box(&self) -> BoxGen<E> {
62        Box::new(self.clone())
63    }
64}
65
66impl<E: Clone> Clone for BoxGen<E> {
67    fn clone(&self) -> Self {
68        self.clone_box()
69    }
70}
71
72/// Trait that enables cloning a boxed shrinker.
73#[doc(hidden)]
74pub trait CloneShrink<E> {
75    fn clone_box(&self) -> BoxShrink<E>;
76}
77
78impl<E: Clone + 'static, T> CloneShrink<E> for T
79where
80    T: Shrink<E> + Clone + 'static,
81{
82    fn clone_box(&self) -> BoxShrink<E> {
83        Box::new(self.clone())
84    }
85}
86
87impl<E: Clone> Clone for BoxShrink<E> {
88    fn clone(&self) -> Self {
89        self.clone_box()
90    }
91}
92
93impl<E> core::fmt::Debug for dyn Gen<E> {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        f.write_str(std::any::type_name::<Self>())
96    }
97}
98
99/// The generator trait, for producing example values to test in a property.
100pub trait Gen<E: Clone + 'static>: CloneGen<E> {
101    /// Produce a example iterator from the generator, given a randomization
102    /// seed.
103    fn examples(&self, seed: u64) -> BoxIter<E>;
104
105    /// Returns a predefined shrinker, or a empty shrinker if no suitable
106    /// exists.
107    ///
108    /// This enables distributing a default shrinker with given generator,
109    /// reducing the need to explicitly configure a shrinker at place of use.
110    ///
111    /// When implementing a [Gen], you can return a empty [shrinks::none]
112    /// shrinker, if that makes the implementation easier, but when you will not
113    /// get any shrinking functionality applied to failing example.
114    fn shrinker(&self) -> BoxShrink<E>;
115
116    /// Bind another shrinker to generator. See [gens::other_shrinker].
117    fn with_shrinker(&self, other_shrink: BoxShrink<E>) -> BoxGen<E> {
118        gens::other_shrinker(self.clone_box(), other_shrink)
119    }
120
121    /// Concatenate together two generators. See [gens::chain].
122    fn chain(&self, other_gen: BoxGen<E>) -> BoxGen<E> {
123        gens::chain(self.clone_box(), other_gen)
124    }
125}
126
127/// Non-object-safe trait for providing generator zipping.
128///
129/// The [ZipWithGen::zip] extension method cannot be implemented diectly on
130/// [Gen] object trait, since generic method in respect to other type `E1`, does
131/// not seem to be allowed on trait objects.
132pub trait ZipWithGen<E0>
133where
134    E0: Clone + 'static,
135{
136    /// See [gens::zip].
137    fn zip<E1>(&self, other_gen: BoxGen<E1>) -> BoxGen<(E0, E1)>
138    where
139        E1: Clone + 'static;
140
141    /// Zip together 3 generators.
142    fn zip_3<E1, E2>(
143        &self,
144        gen1: BoxGen<E1>,
145        gen2: BoxGen<E2>,
146    ) -> BoxGen<(E0, E1, E2)>
147    where
148        E1: Clone + 'static,
149        E2: Clone + 'static;
150
151    /// Zip together 4 generators.
152    fn zip_4<E1, E2, E3>(
153        &self,
154        gen1: BoxGen<E1>,
155        gen2: BoxGen<E2>,
156        gen3: BoxGen<E3>,
157    ) -> BoxGen<(E0, E1, E2, E3)>
158    where
159        E1: Clone + 'static,
160        E2: Clone + 'static,
161        E3: Clone + 'static;
162
163    /// Zip together 5 generators.
164    fn zip_5<E1, E2, E3, E4>(
165        &self,
166        gen1: BoxGen<E1>,
167        gen2: BoxGen<E2>,
168        gen3: BoxGen<E3>,
169        gen4: BoxGen<E4>,
170    ) -> BoxGen<(E0, E1, E2, E3, E4)>
171    where
172        E1: Clone + 'static,
173        E2: Clone + 'static,
174        E3: Clone + 'static,
175        E4: Clone + 'static;
176
177    /// Zip together 6 generators.
178    fn zip_6<E1, E2, E3, E4, E5>(
179        &self,
180        gen1: BoxGen<E1>,
181        gen2: BoxGen<E2>,
182        gen3: BoxGen<E3>,
183        gen4: BoxGen<E4>,
184        gen5: BoxGen<E5>,
185    ) -> BoxGen<(E0, E1, E2, E3, E4, E5)>
186    where
187        E1: Clone + 'static,
188        E2: Clone + 'static,
189        E3: Clone + 'static,
190        E4: Clone + 'static,
191        E5: Clone + 'static;
192}
193
194impl<E0: Clone + 'static> ZipWithGen<E0> for dyn Gen<E0> {
195    fn zip<E1>(&self, other_gen: BoxGen<E1>) -> BoxGen<(E0, E1)>
196    where
197        E1: Clone + 'static,
198    {
199        gens::zip(self.clone_box(), other_gen)
200    }
201
202    fn zip_3<E1, E2>(
203        &self,
204        gen1: BoxGen<E1>,
205        gen2: BoxGen<E2>,
206    ) -> BoxGen<(E0, E1, E2)>
207    where
208        E1: Clone + 'static,
209        E2: Clone + 'static,
210    {
211        gens::zip(gens::zip(self.clone_box(), gen1), gen2)
212            .map(|((e0, e1), e2)| (e0, e1, e2), |(e0, e1, e2)| ((e0, e1), e2))
213    }
214
215    fn zip_4<E1, E2, E3>(
216        &self,
217        gen1: BoxGen<E1>,
218        gen2: BoxGen<E2>,
219        gen3: BoxGen<E3>,
220    ) -> BoxGen<(E0, E1, E2, E3)>
221    where
222        E1: Clone + 'static,
223        E2: Clone + 'static,
224        E3: Clone + 'static,
225    {
226        gens::zip(gens::zip(self.clone_box(), gen1), gens::zip(gen2, gen3)).map(
227            |((e0, e1), (e2, e3))| (e0, e1, e2, e3),
228            |(e0, e1, e2, e3)| ((e0, e1), (e2, e3)),
229        )
230    }
231
232    fn zip_5<E1, E2, E3, E4>(
233        &self,
234        gen1: BoxGen<E1>,
235        gen2: BoxGen<E2>,
236        gen3: BoxGen<E3>,
237        gen4: BoxGen<E4>,
238    ) -> BoxGen<(E0, E1, E2, E3, E4)>
239    where
240        E1: Clone + 'static,
241        E2: Clone + 'static,
242        E3: Clone + 'static,
243        E4: Clone + 'static,
244    {
245        gens::zip(gens::zip(self.clone_box(), gen1), gen2.zip_3(gen3, gen4))
246            .map(
247                |((e0, e1), (e2, e3, e4))| (e0, e1, e2, e3, e4),
248                |(e0, e1, e2, e3, e4)| ((e0, e1), (e2, e3, e4)),
249            )
250    }
251
252    fn zip_6<E1, E2, E3, E4, E5>(
253        &self,
254        gen1: BoxGen<E1>,
255        gen2: BoxGen<E2>,
256        gen3: BoxGen<E3>,
257        gen4: BoxGen<E4>,
258        gen5: BoxGen<E5>,
259    ) -> BoxGen<(E0, E1, E2, E3, E4, E5)>
260    where
261        E1: Clone + 'static,
262        E2: Clone + 'static,
263        E3: Clone + 'static,
264        E4: Clone + 'static,
265        E5: Clone + 'static,
266    {
267        gens::zip(self.zip_3(gen1, gen2), gen3.zip_3(gen4, gen5)).map(
268            |((e0, e1, e2), (e3, e4, e5))| (e0, e1, e2, e3, e4, e5),
269            |(e0, e1, e2, e3, e4, e5)| ((e0, e1, e2), (e3, e4, e5)),
270        )
271    }
272}
273
274/// Non-object-safe trait for providing generator mapping.
275pub trait MapWithGen<E0>
276where
277    E0: Clone + 'static,
278{
279    /// See [gens::map].
280    fn map<E1>(
281        &self,
282        map_fn: fn(E0) -> E1,
283        unmap_fn: fn(E1) -> E0,
284    ) -> BoxGen<E1>
285    where
286        E1: Clone + 'static;
287}
288
289impl<E0: Clone + 'static> MapWithGen<E0> for dyn Gen<E0> {
290    fn map<E1>(
291        &self,
292        map_fn: fn(E0) -> E1,
293        unmap_fn: fn(E1) -> E0,
294    ) -> BoxGen<E1>
295    where
296        E1: Clone + 'static,
297    {
298        gens::map(self.clone_box(), map_fn, unmap_fn)
299    }
300}
301
302/// Non-object-safe trait for providing example filtering in generator.
303pub trait FilterWithGen<E>
304where
305    E: Clone + 'static,
306{
307    /// See [gens::filter].
308    fn filter<P>(&self, predicate: P) -> BoxGen<E>
309    where
310        P: Fn(&E) -> bool + Clone + 'static;
311}
312
313impl<E: Clone + 'static> FilterWithGen<E> for dyn Gen<E> {
314    fn filter<P>(&self, predicate: P) -> BoxGen<E>
315    where
316        P: Fn(&E) -> bool + Clone + 'static,
317    {
318        gens::filter(self.clone_box(), predicate)
319    }
320}
321
322/// The shrinker trait, for shrinking a failed example values into smaller ones.
323/// What is determined as a smaller value can be subjective and is up to author
324/// or tester to determine, but as a rule of thumb a smaller value should be
325/// easier to interpret, when a property is proven wrong.
326pub trait Shrink<E>: CloneShrink<E>
327where
328    E: Clone,
329{
330    /// Returns a series of smaller examples, given an original example.
331    fn candidates(&self, original: E) -> BoxIter<E>;
332}
333
334/// Non-object-safe trait for providing shrinker zipping.
335pub trait ZipWithShrink<E0>
336where
337    E0: Clone + 'static,
338{
339    /// See [shrinks::zip].
340    fn zip<E1>(&self, other_shrink: BoxShrink<E1>) -> BoxShrink<(E0, E1)>
341    where
342        E1: Clone + 'static;
343}
344
345impl<E0: Clone + 'static> ZipWithShrink<E0> for dyn Shrink<E0> {
346    fn zip<E1>(&self, other_gen: BoxShrink<E1>) -> BoxShrink<(E0, E1)>
347    where
348        E1: Clone + 'static,
349    {
350        shrinks::zip(self.clone_box(), other_gen)
351    }
352}
353
354/// Non-object-safe trait for providing shrinker mapping.
355pub trait MapWithShrink<E0>
356where
357    E0: Clone + 'static,
358{
359    /// See [shrinks::map].
360    fn map<E1>(
361        &self,
362        map_fn: fn(E0) -> E1,
363        unmap_fn: fn(E1) -> E0,
364    ) -> BoxShrink<E1>
365    where
366        E1: Clone + 'static;
367}
368
369impl<E0: Clone + 'static> MapWithShrink<E0> for dyn Shrink<E0> {
370    fn map<E1>(
371        &self,
372        map_fn: fn(E0) -> E1,
373        unmap_fn: fn(E1) -> E0,
374    ) -> BoxShrink<E1>
375    where
376        E1: Clone + 'static,
377    {
378        shrinks::map(self.clone_box(), map_fn, unmap_fn)
379    }
380}
381
382/// Non-object-safe trait for providing shrinker filtering.
383pub trait FilterWithShrink<E>
384where
385    E: Clone + 'static,
386{
387    /// See [shrinks::filter].
388    fn filter<P>(&self, predicate: P) -> BoxShrink<E>
389    where
390        P: Fn(&E) -> bool + Clone + 'static;
391}
392
393impl<E: Clone + 'static> FilterWithShrink<E> for dyn Shrink<E> {
394    fn filter<P>(&self, predicate: P) -> BoxShrink<E>
395    where
396        P: Fn(&E) -> bool + Clone + 'static,
397    {
398        shrinks::filter(self.clone_box(), predicate)
399    }
400}
401
402// Doctest the readme file
403#[doc = include_str!("../README.md")]
404#[cfg(doctest)]
405pub struct ReadmeDoctests;