arbtest/
lib.rs

1//! A powerful property-based testing library with a tiny API and a small implementation.
2//!
3//! ```rust
4//! use arbtest::arbtest;
5//!
6//! #[test]
7//! fn all_numbers_are_even() {
8//!     arbtest(|u| {
9//!         let number: u32 = u.arbitrary()?;
10//!         assert!(number % 2 == 0);
11//!         Ok(())
12//!     });
13//! }
14//! ```
15//!
16//! Features:
17//!
18//! - single-function public API,
19//! - no macros,
20//! - automatic minimization,
21//! - time budgeting,
22//! - fuzzer-compatible tests.
23//!
24//! The entry point is the [`arbtest`] function. It accepts a single argument --- a property to
25//! test. A property is a function with the following signature:
26//!
27//! ```
28//! /// Panics if the property does not hold.
29//! fn property(u: &mut arbitrary::Unstructured) -> arbitrary::Result<()>
30//! # { Ok(drop(u)) }
31//! ```
32//!
33//! The `u` argument is a finite random number generator from the [`arbitrary`] crate. You can use
34//! `u` to generate pseudo-random structured data:
35//!
36//! ```
37//! # fn property(u: &mut arbitrary::Unstructured) -> arbitrary::Result<()> {
38//! let ints: Vec<u32> = u.arbitrary()?;
39//! let fruit: &str = u.choose(&["apple", "banana", "cherimoya"])?;
40//! # Ok(()) }
41//! ```
42//!
43//! Or use the derive feature of the arbitrary crate to automatically generate arbitrary types:
44//!
45//! ```
46//! # fn property(u: &mut arbitrary::Unstructured) -> arbitrary::Result<()> {
47//! #[derive(arbitrary::Arbitrary)]
48//! struct Color { r: u8, g: u8, b: u8 }
49//!
50//! let random_color = u.arbitrary::<Color>()?;
51//! # Ok(()) }
52//! ```
53//! Property function should use randomly generated data to assert some interesting behavior of the
54//! implementation, which should hold for _any_ values. For example, converting a color to string
55//! and then parsing it back should result in the same color:
56//!
57//! ```
58//! # type Color = u8; // lol
59//! #[test]
60//! fn parse_is_display_inverted() {
61//!     arbtest(|u| {
62//!         let c1: Color = u.arbitrary();
63//!         let c2: Color = c1.to_string().parse().unwrap();
64//!         assert_eq!(c1, c2);
65//!         Ok(())
66//!     })
67//! }
68//! ```
69//!
70//! After you have supplied the property function, arbtest repeatedly runs it in a loop, passing
71//! more and more [`arbitrary::Unstructured`] bytes until the property panics. Upon a failure, a
72//! seed is printed. The seed can be used to deterministically replay the failure.
73//!
74//! ```text
75//! thread 'all_numbers_are_even' panicked at src/lib.rs:116:9:
76//! assertion failed: number % 2 == 0
77//! note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
78//!
79//! arbtest failed!
80//!     Seed: 0xa88e234400000020
81//! ```
82//!
83//! More features are available with builder-style API on the returned [`ArbTest`] object.
84//!
85//! ## Time Budgeting
86//!
87//! ```
88//! # use arbtest::arbtest; use std::time::Duration;
89//! # fn property(_: &mut arbitrary::Unstructured) -> arbitrary::Result<()> { Ok(()) }
90//! arbtest(property).budget_ms(1_000);
91//! arbtest(property).budget(Duration::from_secs(1));
92//! ```
93//!
94//! The [`budget`](ArbTest::budget) function controls how long the search loop runs, default is one
95//! hundred milliseconds. This default can be overridden with `ARBTEST_BUDGET_MS` environmental
96//! variable.
97//!
98//! ## Size Constraint
99//!
100//! ```
101//! # use arbtest::arbtest;
102//! # fn property(_: &mut arbitrary::Unstructured) -> arbitrary::Result<()> { Ok(()) }
103//! arbtest(property)
104//!     .size_min(1 << 4)
105//!     .size_max(1 << 16);
106//! ```
107//!
108//! Internally, [`arbitrary::Unstructured`] is just an `&[u8]` --- a slice of random bytes. The
109//! length of this slice determines how much randomness your tests gets to use. A shorter slice
110//! contains less entropy and leads to a simpler test case.
111//!
112//! The [`size_min`](ArbTest::size_min) and [`size_max`](ArbTest::size_max) parameters control the
113//! length of this slice: when looking for a failure, `arbtest` progressively increases the size
114//! from `size_min` to `size_max`.
115//!
116//! Note when trying to minimize a known failure, `arbtest` will try to go even smaller than
117//! `size_min`.
118//!
119//! ## Replay and Minimization
120//!
121//! ```
122//! # use arbtest::arbtest;
123//! # let property = |_: &mut arbitrary::Unstructured| -> arbitrary::Result<()> { Ok(()) };
124//! arbtest(property).seed(0x92);
125//! # let property = |_: &mut arbitrary::Unstructured| -> arbitrary::Result<()> { panic!() };
126//! arbtest(property).seed(0x92).minimize();
127//! ```
128//!
129//! When a [`seed`](ArbTest::seed) is specified, `arbtest` uses the seed to generate a fixed
130//! `Unstructured` and runs the property function once. This is useful to debug a test failure after
131//! a failing seed is found through search.
132//!
133//! If in addition to `seed` [`minimize`](ArbTest::minimize) is set, then `arbtest` will try to find
134//! a smaller seed which still triggers a failure. You could use [`budget`](ArbTest::budget) to
135//! control how long the minimization runs.
136//!
137//! ## When the Code Gets Run
138//!
139//! The [`arbtest`] function doesn't immediately run the code. Instead, it returns an [`ArbTest`]
140//! builder object that can be used to further tweak the behavior. The actual execution is triggered
141//! from the [`ArbTest::drop`]. If panicking in `drop` is not your thing, you can trigger
142//! the execution explicitly using [`ArbTest::run`] method:
143//!
144//! ```
145//! # use arbtest::arbtest;
146//! # fn property(_: &mut arbitrary::Unstructured) -> arbitrary::Result<()> { Ok(()) }
147//! let builder = arbtest(property);
148//! drop(builder); // This line actually runs the tests.
149//!
150//! arbtest(property).run(); // Request the run explicitly.
151//! ```
152//!
153//! ## Errors
154//!
155//! Property failures should be reported via a panic, for example, using `assert_eq!` macros.
156//! Returning an `Err(arbitrary::Error)` doesn't signal a test failure, it just means that there
157//! isn't enough entropy left to complete the test. Instead of returning an [`arbitrary::Error`], a
158//! test might choose to continue in a non-random way. For example, when testing a distributed
159//! system you might use the following template:
160//!
161//! ```text
162//! while !u.is_empty() && network.has_messages_in_flight() {
163//!     network.drop_and_permute_messages(u);
164//!     network.deliver_next_message();
165//! }
166//! while network.has_messages_in_flight() {
167//!     network.deliver_next_message();
168//! }
169//! ```
170//!
171//! ## Imports
172//!
173//! Recommended way to import:
174//!
175//! ```toml
176//! [dev-dependencies]
177//! arbtest = "0.3"
178//! ```
179//!
180//! ```
181//! #[cfg(test)]
182//! mod tests {
183//!     use arbtest::{arbtest, arbitrary};
184//!
185//!     fn my_property(u: &mut arbitrary::Unstructured) -> arbitrary::Result<()> { Ok(()) }
186//! }
187//! ```
188//!
189//! If you want to `#[derive(Arbitrary)]`, you need to explicitly add Cargo.toml dependency for the
190//! [`arbitrary`] crate:
191//!
192//! ```toml
193//! [dependencies]
194//! arbitrary = { version = "1", features = ["derive"] }
195//!
196//! [dev-dependencies]
197//! arbtest = "0.3"
198//! ```
199//!
200//! ```
201//! #[derive(arbitrary::Arbitrary)]
202//! struct Color { r: u8, g: u8, b: u8 }
203//!
204//! #[cfg(test)]
205//! mod tests {
206//!     use arbtest::arbtest;
207//!
208//!     #[test]
209//!     fn display_parse_identity() {
210//!         arbtest(|u| {
211//!             let c1: Color = u.arbitrary()?;
212//!             let c2: Color = c1.to_string().parse();
213//!             assert_eq!(c1, c2);
214//!             Ok(())
215//!         });
216//!     }
217//! }
218//! ```
219//!
220//! Note that `arbitrary` is a non-dev dependency. This is not strictly required, but is helpful to
221//! allow downstream crates to run their tests with arbitrary values of `Color`.
222//!
223//! ## Design
224//!
225//! Most of the heavy lifting is done by the [`arbitrary`] crate. Its [`arbitrary::Unstructured`] is
226//! a brilliant abstraction which works both for coverage-guided fuzzing as well as for automated
227//! minimization. That is, you can plug `arbtest` properties directly into `cargo fuzz`, API is
228//! fully compatible.
229//!
230//! Property function uses `&mut Unstructured` as an argument instead of `T: Arbitrary`, allowing
231//! the user to generate any `T` they want imperatively. The smaller benefit here is implementation
232//! simplicity --- the property type is not generic. The bigger benefit is that this API is more
233//! expressive, as it allows for _interactive_ properties. For example, a network simulation for a
234//! distributed system doesn't have to generate "failure plan" upfront, it can use `u` during the
235//! test run to make _dynamic_ decisions about which existing network packets to drop!
236//!
237//! A "seed" is an `u64`, by convention specified in hexadecimal. The low 32 bits of the seed
238//! specify the length of the underlying `Unstructured`. The high 32 bits are the random seed
239//! proper, which is feed into a simple xor-shift to generate `Unstructured` of the specified
240//! length.
241//!
242//! If you like this crate, you might enjoy <https://github.com/graydon/exhaustigen-rs> as well.
243#![deny(missing_docs)]
244
245use std::{
246    collections::hash_map::RandomState,
247    fmt,
248    hash::{BuildHasher, Hasher},
249    panic::AssertUnwindSafe,
250    time::{Duration, Instant},
251};
252
253#[doc(no_inline)]
254pub use arbitrary;
255
256/// Repeatedly test `property` with different random seeds.
257///
258/// Return value is an [`ArbTest`] builder object which can be used to tweak behavior.
259pub fn arbtest<P>(property: P) -> ArbTest<P>
260where
261    P: FnMut(&mut arbitrary::Unstructured<'_>) -> arbitrary::Result<()>,
262{
263    let options =
264        Options { size_min: 32, size_max: 65_536, budget: None, seed: None, minimize: false };
265    ArbTest { property, options, done: false }
266}
267
268/// A builder for a property-based test.
269///
270/// This builder allows customizing various aspects of the test, such as the
271/// initial random seed, the amount of iterations to try, or the amount of
272/// random numbers (entropy) each test run gets.
273///
274/// For convenience, `ArbTest` automatically runs the test on drop. You can use [`ArbTest::run`]
275/// to run the test explicitly.
276pub struct ArbTest<P>
277where
278    P: FnMut(&mut arbitrary::Unstructured<'_>) -> arbitrary::Result<()>,
279{
280    property: P,
281    options: Options,
282    done: bool,
283}
284
285struct Options {
286    size_min: u32,
287    size_max: u32,
288    budget: Option<Duration>,
289    seed: Option<Seed>,
290    minimize: bool,
291}
292
293impl<P> ArbTest<P>
294where
295    P: FnMut(&mut arbitrary::Unstructured<'_>) -> arbitrary::Result<()>,
296{
297    /// Sets the lower bound on the amount of random bytes each test run gets.
298    ///
299    /// Defaults to 32.
300    ///
301    /// Each randomized test gets an [arbitrary::Unstructured] as a source of
302    /// randomness. `Unstructured` can be thought of as a *finite* pseudo random
303    /// number generator, or, alternatively, as a finite sequence of random
304    /// numbers. The intuition here is that _shorter_ sequences lead to simpler
305    /// test cases.
306    ///
307    /// The `size` parameter controls the length of the initial random sequence.
308    /// More specifically, `arbtest` will run the test function multiple times,
309    /// increasing the amount of entropy from `size_min` to `size_max`.
310    pub fn size_min(mut self, size: u32) -> Self {
311        self.options.size_min = size;
312        self
313    }
314
315    /// Sets the upper bound on the amount of random bytes each test run gets.
316    ///
317    /// Defaults to 65 536.
318    ///
319    /// See [`ArbTest::size_min`].
320    pub fn size_max(mut self, size: u32) -> Self {
321        self.options.size_max = size;
322        self
323    }
324
325    /// Sets the approximate duration for the tests.
326    ///
327    /// Defaults to 100ms, can be overridden via `ARBTEST_BUDGET_MS` environmental variable.
328    ///
329    /// `arbtest` will re-run the test function until the time runs out or until it panics.
330    pub fn budget(mut self, value: Duration) -> Self {
331        self.options.budget = Some(value);
332        self
333    }
334
335    /// Sets the approximate duration for the tests, in milliseconds.
336    pub fn budget_ms(self, value: u64) -> Self {
337        self.budget(Duration::from_millis(value))
338    }
339
340    /// Fixes the random seed.
341    ///
342    /// Normally, `arbtest` runs the test function multiple times, picking a
343    /// fresh random seed of an increased complexity every time.
344    ///
345    /// If the `seed` is set explicitly, the `test` function is run only once.
346    pub fn seed(mut self, seed: u64) -> Self {
347        self.options.seed = Some(Seed::new(seed));
348        self
349    }
350
351    /// Whether to try to minimize the seed after failure.
352    pub fn minimize(mut self) -> Self {
353        self.options.minimize = true;
354        self
355    }
356
357    /// Runs the test.
358    ///
359    /// This is equivalent to just dropping `ArbTest`.
360    pub fn run(mut self) {
361        self.context().run();
362    }
363
364    fn context(&mut self) -> Context<'_, '_> {
365        assert!(!self.done);
366        self.done = true;
367        Context { property: &mut self.property, options: &self.options, buffer: Vec::new() }
368    }
369}
370
371impl<P> Drop for ArbTest<P>
372where
373    P: FnMut(&mut arbitrary::Unstructured<'_>) -> arbitrary::Result<()>,
374{
375    /// Runs property test.
376    ///
377    /// See [`ArbTest::run`].
378    fn drop(&mut self) {
379        if !self.done {
380            self.context().run();
381        }
382    }
383}
384
385type DynProperty<'a> = &'a mut dyn FnMut(&mut arbitrary::Unstructured<'_>) -> arbitrary::Result<()>;
386
387struct Context<'a, 'b> {
388    property: DynProperty<'a>,
389    options: &'b Options,
390    buffer: Vec<u8>,
391}
392
393impl<'a, 'b> Context<'a, 'b> {
394    fn run(&mut self) {
395        let budget = {
396            let default = Duration::from_millis(100);
397            self.options.budget.or_else(env_budget).unwrap_or(default)
398        };
399
400        match (self.options.seed.or_else(env_seed), self.options.minimize) {
401            (None, false) => self.run_search(budget),
402            (None, true) => panic!("can't minimize without a seed"),
403            (Some(seed), false) => self.run_reproduce(seed),
404            (Some(seed), true) => self.run_minimize(seed, budget),
405        }
406    }
407
408    fn run_search(&mut self, budget: Duration) {
409        let t = Instant::now();
410
411        let mut last_result = Ok(());
412        let mut seen_success = false;
413
414        let mut size = self.options.size_min;
415        'search: loop {
416            for _ in 0..3 {
417                if t.elapsed() > budget {
418                    break 'search;
419                }
420
421                let seed = Seed::gen(size);
422                {
423                    let guard = PrintSeedOnPanic::new(seed);
424                    last_result = self.try_seed(seed);
425                    seen_success = seen_success || last_result.is_ok();
426                    guard.defuse()
427                }
428            }
429
430            let bigger = (size as u64).saturating_mul(5) / 4;
431            size = bigger.clamp(0, self.options.size_max as u64) as u32;
432        }
433
434        if !seen_success {
435            let error = last_result.unwrap_err();
436            panic!("no fitting seeds, last error: {error}");
437        }
438    }
439
440    fn run_reproduce(&mut self, seed: Seed) {
441        let guard = PrintSeedOnPanic::new(seed);
442        self.try_seed(seed).unwrap_or_else(|error| panic!("{error}"));
443        guard.defuse()
444    }
445
446    fn run_minimize(&mut self, seed: Seed, budget: Duration) {
447        let old_hook = std::panic::take_hook();
448        std::panic::set_hook(Box::new(|_| ()));
449
450        if !self.try_seed_panics(seed) {
451            std::panic::set_hook(old_hook);
452            panic!("seed {seed} did not panic")
453        }
454
455        let mut seed = seed;
456        let t = std::time::Instant::now();
457
458        let minimizers = [|s| s / 2, |s| s * 9 / 10, |s| s - 1];
459        let mut minimizer = 0;
460
461        let mut last_minimization = Instant::now();
462        'search: loop {
463            let size = seed.size();
464            eprintln!("seed {seed}, seed size {size}, search time {:0.2?}", t.elapsed());
465            if size == 0 {
466                break;
467            }
468            loop {
469                if t.elapsed() > budget {
470                    break 'search;
471                }
472                if last_minimization.elapsed() > budget / 5 && minimizer < minimizers.len() - 1 {
473                    minimizer += 1;
474                }
475                let size = minimizers[minimizer](size);
476                let candidate_seed = Seed::gen(size);
477                if self.try_seed_panics(candidate_seed) {
478                    seed = candidate_seed;
479                    last_minimization = Instant::now();
480                    continue 'search;
481                }
482            }
483        }
484        std::panic::set_hook(old_hook);
485        let size = seed.size();
486        eprintln!("minimized");
487        eprintln!("seed {seed}, seed size {size}, search time {:0.2?}", t.elapsed());
488    }
489
490    fn try_seed(&mut self, seed: Seed) -> arbitrary::Result<()> {
491        seed.fill(&mut self.buffer);
492        let mut u = arbitrary::Unstructured::new(&self.buffer);
493        (self.property)(&mut u)
494    }
495
496    fn try_seed_panics(&mut self, seed: Seed) -> bool {
497        let mut me = AssertUnwindSafe(self);
498        std::panic::catch_unwind(move || {
499            let _ = me.try_seed(seed);
500        })
501        .is_err()
502    }
503}
504
505fn env_budget() -> Option<Duration> {
506    let var = std::env::var("ARBTEST_BUDGET_MS").ok()?;
507    let ms = var.parse::<u64>().ok()?;
508    Some(Duration::from_millis(ms))
509}
510
511fn env_seed() -> Option<Seed> {
512    let var = std::env::var("ARBTEST_SEED").ok()?;
513    // Check if it starts with "0x" and stip it if necessary before parsing as hex.
514    let repr = u64::from_str_radix(
515        if let Some(stripped_var) = var.strip_prefix("0x") { stripped_var } else { &var },
516        16,
517    )
518    .ok()?;
519    Some(Seed { repr })
520}
521
522/// Random seed used to generated an `[u8]` underpinning the `Unstructured`
523/// instance we pass to user's code.
524///
525/// The seed is two `u32` mashed together. Low half defines the *length* of the
526/// sequence, while the high bits are the random seed proper.
527///
528/// The reason for this encoding is to be able to print a seed as a single
529/// copy-pastable number.
530#[derive(Clone, Copy)]
531struct Seed {
532    repr: u64,
533}
534
535impl fmt::Display for Seed {
536    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
537        write!(f, "\x1b[1m0x{:016x}\x1b[0m", self.repr)
538    }
539}
540
541impl Seed {
542    fn new(repr: u64) -> Seed {
543        Seed { repr }
544    }
545    fn gen(size: u32) -> Seed {
546        let raw = RandomState::new().build_hasher().finish();
547        let repr = size as u64 | (raw << u32::BITS);
548        Seed { repr }
549    }
550    fn size(self) -> u32 {
551        self.repr as u32
552    }
553    fn rand(self) -> u32 {
554        (self.repr >> u32::BITS) as u32
555    }
556    fn fill(self, buf: &mut Vec<u8>) {
557        buf.clear();
558        buf.reserve(self.size() as usize);
559        let mut random = self.rand();
560        let mut rng = std::iter::repeat_with(move || {
561            random ^= random << 13;
562            random ^= random >> 17;
563            random ^= random << 5;
564            random
565        });
566        while buf.len() < self.size() as usize {
567            buf.extend(rng.next().unwrap().to_le_bytes());
568        }
569    }
570}
571
572struct PrintSeedOnPanic {
573    seed: Seed,
574    active: bool,
575}
576
577impl PrintSeedOnPanic {
578    fn new(seed: Seed) -> PrintSeedOnPanic {
579        PrintSeedOnPanic { seed, active: true }
580    }
581    fn defuse(mut self) {
582        self.active = false
583    }
584}
585
586impl Drop for PrintSeedOnPanic {
587    fn drop(&mut self) {
588        if self.active {
589            eprintln!("\narbtest failed!\n    Seed: {}\n\n", self.seed)
590        }
591    }
592}