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}