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 gen;
15mod runner;
16pub mod shrink;
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(gen::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 [shrink::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 [gen::other_shrinker].
117    fn with_shrinker(&self, other_shrink: BoxShrink<E>) -> BoxGen<E> {
118        gen::other_shrinker(self.clone_box(), other_shrink)
119    }
120
121    /// Concatenate together two generators. See [gen::chain].
122    fn chain(&self, other_gen: BoxGen<E>) -> BoxGen<E> {
123        gen::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 [gen::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        gen::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        gen::zip(gen::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        gen::zip(gen::zip(self.clone_box(), gen1), gen::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        gen::zip(gen::zip(self.clone_box(), gen1), gen2.zip_3(gen3, gen4)).map(
246            |((e0, e1), (e2, e3, e4))| (e0, e1, e2, e3, e4),
247            |(e0, e1, e2, e3, e4)| ((e0, e1), (e2, e3, e4)),
248        )
249    }
250
251    fn zip_6<E1, E2, E3, E4, E5>(
252        &self,
253        gen1: BoxGen<E1>,
254        gen2: BoxGen<E2>,
255        gen3: BoxGen<E3>,
256        gen4: BoxGen<E4>,
257        gen5: BoxGen<E5>,
258    ) -> BoxGen<(E0, E1, E2, E3, E4, E5)>
259    where
260        E1: Clone + 'static,
261        E2: Clone + 'static,
262        E3: Clone + 'static,
263        E4: Clone + 'static,
264        E5: Clone + 'static,
265    {
266        gen::zip(self.zip_3(gen1, gen2), gen3.zip_3(gen4, gen5)).map(
267            |((e0, e1, e2), (e3, e4, e5))| (e0, e1, e2, e3, e4, e5),
268            |(e0, e1, e2, e3, e4, e5)| ((e0, e1, e2), (e3, e4, e5)),
269        )
270    }
271}
272
273/// Non-object-safe trait for providing generator mapping.
274pub trait MapWithGen<E0>
275where
276    E0: Clone + 'static,
277{
278    /// See [gen::map].
279    fn map<E1>(
280        &self,
281        map_fn: fn(E0) -> E1,
282        unmap_fn: fn(E1) -> E0,
283    ) -> BoxGen<E1>
284    where
285        E1: Clone + 'static;
286}
287
288impl<E0: Clone + 'static> MapWithGen<E0> for dyn Gen<E0> {
289    fn map<E1>(
290        &self,
291        map_fn: fn(E0) -> E1,
292        unmap_fn: fn(E1) -> E0,
293    ) -> BoxGen<E1>
294    where
295        E1: Clone + 'static,
296    {
297        gen::map(self.clone_box(), map_fn, unmap_fn)
298    }
299}
300
301/// Non-object-safe trait for providing example filtering in generator.
302pub trait FilterWithGen<E>
303where
304    E: Clone + 'static,
305{
306    /// See [gen::filter].
307    fn filter<P>(&self, predicate: P) -> BoxGen<E>
308    where
309        P: Fn(&E) -> bool + Clone + 'static;
310}
311
312impl<E: Clone + 'static> FilterWithGen<E> for dyn Gen<E> {
313    fn filter<P>(&self, predicate: P) -> BoxGen<E>
314    where
315        P: Fn(&E) -> bool + Clone + 'static,
316    {
317        gen::filter(self.clone_box(), predicate)
318    }
319}
320
321/// The shrinker trait, for shrinking a failed example values into smaller ones.
322/// What is determined as a smaller value can be subjective and is up to author
323/// or tester to determine, but as a rule of thumb a smaller value should be
324/// easier to interpret, when a property is proven wrong.
325pub trait Shrink<E>: CloneShrink<E>
326where
327    E: Clone,
328{
329    /// Returns a series of smaller examples, given an original example.
330    fn candidates(&self, original: E) -> BoxIter<E>;
331}
332
333/// Non-object-safe trait for providing shrinker zipping.
334pub trait ZipWithShrink<E0>
335where
336    E0: Clone + 'static,
337{
338    /// See [shrink::zip].
339    fn zip<E1>(&self, other_shrink: BoxShrink<E1>) -> BoxShrink<(E0, E1)>
340    where
341        E1: Clone + 'static;
342}
343
344impl<E0: Clone + 'static> ZipWithShrink<E0> for dyn Shrink<E0> {
345    fn zip<E1>(&self, other_gen: BoxShrink<E1>) -> BoxShrink<(E0, E1)>
346    where
347        E1: Clone + 'static,
348    {
349        shrink::zip(self.clone_box(), other_gen)
350    }
351}
352
353/// Non-object-safe trait for providing shrinker mapping.
354pub trait MapWithShrink<E0>
355where
356    E0: Clone + 'static,
357{
358    /// See [shrink::map].
359    fn map<E1>(
360        &self,
361        map_fn: fn(E0) -> E1,
362        unmap_fn: fn(E1) -> E0,
363    ) -> BoxShrink<E1>
364    where
365        E1: Clone + 'static;
366}
367
368impl<E0: Clone + 'static> MapWithShrink<E0> for dyn Shrink<E0> {
369    fn map<E1>(
370        &self,
371        map_fn: fn(E0) -> E1,
372        unmap_fn: fn(E1) -> E0,
373    ) -> BoxShrink<E1>
374    where
375        E1: Clone + 'static,
376    {
377        shrink::map(self.clone_box(), map_fn, unmap_fn)
378    }
379}
380
381/// Non-object-safe trait for providing shrinker filtering.
382pub trait FilterWithShrink<E>
383where
384    E: Clone + 'static,
385{
386    /// See [shrink::filter].
387    fn filter<P>(&self, predicate: P) -> BoxShrink<E>
388    where
389        P: Fn(&E) -> bool + Clone + 'static;
390}
391
392impl<E: Clone + 'static> FilterWithShrink<E> for dyn Shrink<E> {
393    fn filter<P>(&self, predicate: P) -> BoxShrink<E>
394    where
395        P: Fn(&E) -> bool + Clone + 'static,
396    {
397        shrink::filter(self.clone_box(), predicate)
398    }
399}
400
401// Doctest the readme file
402#[doc = include_str!("../README.md")]
403#[cfg(doctest)]
404pub struct ReadmeDoctests;