Structured Testing
In addition to generating random byte slices, bolero supports generating well-formed types, with the bolero-generator crate.
Operation Example
Let's supposes we've implemented a MySet data structure. It has 3 operations:
insert(value)- inserts an value into the setremove(value)- removes an value from the setclear()- removes all values from the set
The operations can easily be modeled as an enum:
#![allow(unused_variables)] fn main() { use bolero::generator::TypeGenerator; #[derive(Debug, TypeGenerator)] enum Operation { Insert(u64), Remove(u64), Clear, } }
Note that we've added TypeGenerator to the list of derives. This enables bolero to generate random values for Operation. We can combine that with a Vec<Operation> and get a list of operations to perform on our MySet data structure.
use bolero::{check, generator::*}; use my_set::MySet; #[derive(Debug, TypeGenerator)] enum Operation { Insert(u64), Remove(u64), Clear, } fn main() { check!() .with_type::<Vec<Operation>>() .for_each(|operations| { let mut set = MySet::new(); for operation in operations.iter() { match operation { Operation::Insert(value) => { set.insert(value); } Operation::Remove(value) => { set.remove(value); } Operation::Clear => { set.clear(); } } } }) }
Controlling The Number Of Operations Generated
Using check!().with_type::<Vec<T>>() will generate vectors with lengths in the range 0..=64. If you need more control over the number of elements being generated you can instead use .with_generator and provide a customized generator:
use bolero::{check, generator::*}; use bolero::gen; use my_set::MySet; #[derive(Debug, TypeGenerator)] enum Operation { Insert(u64), Remove(u64), Clear, } fn main() { check!() // Generate 0 to 200 operations .with_generator(gen::<Vec<Operation>>().with().len(0..=200)) .for_each(|operations| { let mut set = MySet::new(); for operation in operations.iter() { match operation { Operation::Insert(value) => { set.insert(value); } Operation::Remove(value) => { set.remove(value); } Operation::Clear => { set.clear(); } } } // assertions go here }) }
Using Test Oracles
The basic test we constructed above will make sure we don't panic on any of the list of operations. We can take it to the next step by using a test oracle to make sure the behavior of MySet is actually correct. Here we'll use HashSet from the std library:
use bolero::{check, generator::*}; use my_set::MySet; use std::collections::HashSet; #[derive(Debug, TypeGenerator)] enum Operation { Insert(u64), Remove(u64), Clear, } fn main() { check!() .with_type::<Vec<Operation>>() .for_each(|operations| { let mut set = MySet::new(); let mut oracle = HashSet::new(); for operation in operations.iter() { match operation { Operation::Insert(value) => { set.insert(value); oracle.insert(value); } Operation::Remove(value) => { set.remove(value); oracle.remove(value); } Operation::Clear => { set.clear(); oracle.clear(); } } } assert!(set.iter().eq(oracle.iter())); }) }