use super::*;
use crate::log;
use crate::mutators as m;
use std::fmt::{self, Debug};
use std::panic;
use std::prelude::v1::*;
use std::time::{Duration, Instant};
const DEFAULT_MIN_ITERS: usize = 1000;
const DEFAULT_MAX_DURATION: Duration = Duration::from_secs(1);
const DEFAULT_MAX_FACTOR: u32 = 10;
const DEFAULT_SHRINK_FACTOR: u32 = 4;
const SEED_ENV_VAR: &str = "MUTATIS_CHECK_SEED";
fn seed_from_env() -> Option<u64> {
match std::env::var(SEED_ENV_VAR) {
Ok(s) if s.trim().is_empty() => None,
Ok(s) => Some(parse_seed(&s)),
Err(std::env::VarError::NotPresent) => None,
Err(e) => panic!("{SEED_ENV_VAR}: {e}"),
}
}
fn parse_seed(s: &str) -> u64 {
s.trim()
.parse()
.unwrap_or_else(|e| panic!("{SEED_ENV_VAR}={s:?} is not a valid u64: {e}"))
}
pub type CheckResult<T> = std::result::Result<(), CheckError<T>>;
pub enum CheckError<T> {
Failed(CheckFailure<T>),
EmptyCorpus,
MutatorError(Error),
}
impl<T> From<Error> for CheckError<T> {
fn from(v: Error) -> Self {
Self::MutatorError(v)
}
}
impl<T> From<CheckFailure<T>> for CheckError<T> {
fn from(v: CheckFailure<T>) -> Self {
Self::Failed(v)
}
}
impl<T> std::error::Error for CheckError<T>
where
T: 'static + Debug,
{
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
CheckError::MutatorError(e) => Some(e),
CheckError::Failed(f) => Some(f),
CheckError::EmptyCorpus => None,
}
}
}
impl<T> fmt::Display for CheckError<T>
where
T: Debug,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CheckError::Failed(e) => write!(f, "check failure: {e}"),
CheckError::EmptyCorpus => write!(f, "cannot check an empty corpus"),
CheckError::MutatorError(e) => write!(f, "mutator error: {e}"),
}
}
}
impl<T> Debug for CheckError<T>
where
T: Debug,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
impl<T> CheckError<T> {
#[track_caller]
pub fn unwrap_failed(self) -> CheckFailure<T> {
match self {
CheckError::Failed(f) => f,
_ => panic!("CheckError::unwrap_failed called on non-failed CheckError"),
}
}
#[track_caller]
pub fn unwrap_mutator_error(self) -> Error {
match self {
CheckError::MutatorError(e) => e,
_ => panic!("CheckError::unwrap_error called on non-error CheckError"),
}
}
}
#[non_exhaustive]
pub struct CheckFailure<T> {
pub value: T,
pub message: String,
}
impl<T> fmt::Display for CheckFailure<T>
where
T: Debug,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let CheckFailure { value, message } = self;
write!(f, "failed on input {value:?}: {message}")
}
}
impl<T> Debug for CheckFailure<T>
where
T: Debug,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
impl<T> std::error::Error for CheckFailure<T> where T: Debug {}
#[derive(Debug)]
pub struct Check {
min_iters: Option<usize>,
max_iters: Option<usize>,
min_duration: Option<Duration>,
max_duration: Option<Duration>,
max_shrink_iters: Option<usize>,
max_shrink_duration: Option<Duration>,
seed: Option<u64>,
}
impl Default for Check {
fn default() -> Check {
Check::new()
}
}
impl Check {
pub fn new() -> Check {
Check {
min_iters: None,
max_iters: None,
min_duration: None,
max_duration: None,
max_shrink_iters: None,
max_shrink_duration: None,
seed: None,
}
}
pub fn min_iters(&mut self, min_iters: usize) -> &mut Check {
self.min_iters = Some(min_iters);
self
}
pub fn max_iters(&mut self, max_iters: usize) -> &mut Check {
self.max_iters = Some(max_iters);
self
}
pub fn iters(&mut self, iters: usize) -> &mut Check {
self.min_iters(iters).max_iters(iters)
}
pub fn min_duration(&mut self, min_duration: Duration) -> &mut Check {
self.min_duration = Some(min_duration);
self
}
pub fn max_duration(&mut self, max_duration: Duration) -> &mut Check {
self.max_duration = Some(max_duration);
self
}
pub fn duration(&mut self, duration: Duration) -> &mut Check {
self.min_duration(duration).max_duration(duration)
}
pub fn max_shrink_iters(&mut self, max_shrink_iters: usize) -> &mut Check {
self.max_shrink_iters = Some(max_shrink_iters);
self
}
pub fn max_shrink_duration(&mut self, max_shrink_duration: Duration) -> &mut Check {
self.max_shrink_duration = Some(max_shrink_duration);
self
}
#[deprecated(since = "0.5.4", note = "renamed to `Check::max_shrink_iters`")]
#[doc(hidden)]
pub fn shrink_iters(&mut self, shrink_iters: usize) -> &mut Check {
self.max_shrink_iters(shrink_iters)
}
fn min_iters_or_default(&self) -> usize {
self.min_iters.unwrap_or(DEFAULT_MIN_ITERS)
}
fn max_iters_or_default(&self) -> usize {
self.max_iters.unwrap_or_else(|| {
self.min_iters_or_default()
.saturating_mul(DEFAULT_MAX_FACTOR as usize)
})
}
fn min_duration_or_default(&self) -> Duration {
self.min_duration.unwrap_or(Duration::ZERO)
}
fn max_duration_or_default(&self) -> Duration {
self.max_duration.unwrap_or_else(|| {
self.min_duration.map_or(DEFAULT_MAX_DURATION, |min| {
min.saturating_mul(DEFAULT_MAX_FACTOR)
})
})
}
fn max_shrink_iters_or_default(&self) -> usize {
self.max_shrink_iters.unwrap_or_else(|| {
self.min_iters_or_default()
.saturating_mul(DEFAULT_SHRINK_FACTOR as usize)
})
}
fn max_shrink_duration_or_default(&self) -> Duration {
self.max_shrink_duration.unwrap_or_else(|| {
self.max_duration_or_default()
.saturating_mul(DEFAULT_SHRINK_FACTOR)
})
}
fn seed_or_default(&self) -> u64 {
self.seed
.or_else(seed_from_env)
.unwrap_or_else(crate::rng::next_default_seed)
}
pub fn seed(&mut self, seed: u64) -> &mut Check {
self.seed = Some(seed);
self
}
pub fn run<T, S>(
&self,
property: impl FnMut(&T) -> std::result::Result<(), S>,
) -> CheckResult<T>
where
T: Clone + Debug + Default + DefaultMutate,
S: ToString,
{
self.run_with(m::default::<T>(), [T::default()], property)
}
pub fn run_with<M, T, S>(
&self,
mut mutator: M,
initial_corpus: impl IntoIterator<Item = T>,
mut property: impl FnMut(&T) -> std::result::Result<(), S>,
) -> CheckResult<T>
where
M: Mutate<T>,
T: Clone + Debug,
S: ToString,
{
let start = Instant::now();
let mut corpus = initial_corpus.into_iter().collect::<Vec<_>>();
if corpus.is_empty() {
return Err(CheckError::EmptyCorpus);
}
for value in &corpus {
if let Err(msg) = Self::check_one(value, &mut property) {
return self.shrink(mutator, value.clone(), property, msg);
}
}
let seed = self.seed_or_default();
log::debug!("using RNG seed {seed}");
let mut session = Session::new().seed(seed);
let min_iters = self.min_iters_or_default();
let max_iters = self.max_iters_or_default();
let min_duration = self.min_duration_or_default();
let max_duration = self.max_duration_or_default();
let mut iters = 0_usize;
loop {
if iters >= min_iters {
let elapsed = start.elapsed();
let mins_met = elapsed >= min_duration;
let max_reached = iters >= max_iters || elapsed >= max_duration;
if mins_met && max_reached {
break;
}
}
let index = session.context.rng().gen_index(corpus.len()).unwrap();
match session.mutate_with(&mut mutator, &mut corpus[index]) {
Ok(()) => {}
Err(e) if e.is_exhausted() => {
corpus.swap_remove(index);
if corpus.is_empty() {
return Ok(());
}
continue;
}
Err(e) => return Err(e.into()),
}
if let Err(msg) = Self::check_one(&corpus[index], &mut property) {
return self.shrink(mutator, corpus[index].clone(), property, msg);
}
iters += 1;
}
Ok(())
}
fn check_one<T, S>(
value: &T,
mut property: impl FnMut(&T) -> std::result::Result<(), S>,
) -> std::result::Result<(), String>
where
T: Debug,
S: ToString,
{
match panic::catch_unwind(panic::AssertUnwindSafe(|| property(value))) {
Ok(Ok(())) => Ok(()),
Ok(Err(msg)) => Err(msg.to_string()),
Err(_) => Err("<panicked>".into()),
}
}
fn shrink<M, T, S>(
&self,
mut mutator: M,
mut value: T,
mut property: impl FnMut(&T) -> std::result::Result<(), S>,
mut message: String,
) -> CheckResult<T>
where
M: Mutate<T>,
T: Clone + Debug,
S: ToString,
{
let start = Instant::now();
log::warn!("failed on input {value:?}: {message}");
let max_shrink_iters = self.max_shrink_iters_or_default();
let max_shrink_duration = self.max_shrink_duration_or_default();
if max_shrink_iters == 0 || max_shrink_duration.is_zero() {
return Err(CheckFailure { value, message }.into());
}
log::debug!("shrinking for up to {max_shrink_iters} iters or {max_shrink_duration:?}...");
let seed = self.seed_or_default();
log::debug!("shrinking with RNG seed {seed}");
let mut session = Session::new().seed(seed).shrink(true);
for _ in 0..max_shrink_iters {
if start.elapsed() >= max_shrink_duration {
log::debug!("reached maximum shrink duration; stopping shrinking");
break;
}
let mut candidate = value.clone();
match session.mutate_with(&mut mutator, &mut candidate) {
Err(e) if e.is_exhausted() => break,
Err(e) => {
log::info!("got mutator error during shrinking, ignoring: {e}");
continue;
}
Ok(()) => {}
}
match Self::check_one(&candidate, &mut property) {
Ok(()) => {
}
Err(msg) => {
message = msg;
log::debug!("got failure for shrunken input {value:?}: {message}");
value = candidate;
}
}
}
log::info!("shrunk failing input down to {value:?}");
Err(CheckFailure { value, message }.into())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::result::Result;
fn check() -> Check {
let _ = env_logger::builder().is_test(true).try_init();
Check::new()
}
fn count_iters(check: &mut Check, delay: Duration) -> usize {
let mut calls = 0_usize;
check
.run_with(m::u32(), [0u32], |_: &u32| -> Result<(), String> {
calls += 1;
if !delay.is_zero() {
std::thread::sleep(delay);
}
Ok(())
})
.unwrap();
calls - 1
}
fn less_than_ten(x: &u8) -> Result<(), &'static str> {
if *x < 10 {
Ok(())
} else {
Err("expected < 10")
}
}
#[test]
fn check_shrinking_negative_ints() {
let failure = check()
.max_shrink_iters(1000)
.run_with(m::i32(), [-1i32, i32::MIN], |x: &i32| {
if *x >= 0 {
Ok(())
} else {
Err("negative")
}
})
.unwrap_err()
.unwrap_failed();
assert!(
failure.value < 0,
"{} is not a counterexample",
failure.value
);
}
#[test]
fn check_shrinking_out_of_range() {
let failure = check()
.max_shrink_iters(1000)
.run_with(m::mrange(10i32..=20i32), [5i32], |_: &i32| {
Err::<(), &str>("always fails")
})
.unwrap_err()
.unwrap_failed();
assert!(
(10..=20).contains(&failure.value),
"{} escaped the mutator's range",
failure.value
);
}
#[test]
fn parse_seed_accepts_decimal_and_trims() {
assert_eq!(parse_seed("42"), 42);
assert_eq!(parse_seed(" 42 "), 42);
assert_eq!(parse_seed("42\n"), 42);
assert_eq!(parse_seed("0"), 0);
assert_eq!(parse_seed(&u64::MAX.to_string()), u64::MAX);
}
#[test]
#[should_panic(expected = "MUTATIS_CHECK_SEED")]
fn parse_seed_rejects_non_numeric() {
parse_seed("abc");
}
#[test]
#[should_panic(expected = "MUTATIS_CHECK_SEED")]
fn parse_seed_rejects_overflow() {
parse_seed("18446744073709551616");
}
#[test]
fn seed_env_var_is_consulted() {
assert_eq!(std::env::var_os(SEED_ENV_VAR), None);
assert_ne!(
Check::new().seed_or_default(),
Check::new().seed_or_default()
);
std::env::set_var(SEED_ENV_VAR, "1234");
assert_eq!(Check::new().seed_or_default(), 1234);
assert_eq!(Check::new().seed(7).seed_or_default(), 7);
for empty in ["", " ", "\n"] {
std::env::set_var(SEED_ENV_VAR, empty);
assert_ne!(
Check::new().seed_or_default(),
Check::new().seed_or_default()
);
}
std::env::remove_var(SEED_ENV_VAR);
assert_ne!(Check::new().seed_or_default(), 1234);
}
struct ExhaustedForZero;
impl Mutate<u32> for ExhaustedForZero {
fn mutate(&mut self, c: &mut Candidates<'_>, value: &mut u32) -> crate::Result<()> {
if *value == 0 {
return Ok(());
}
c.mutation(|_| Ok(*value += 1))
}
}
#[test]
fn check_iters_is_exact() {
assert_eq!(count_iters(check().iters(37), Duration::ZERO), 37);
}
#[test]
fn check_min_iters_beats_max_iters() {
let iters = count_iters(check().min_iters(25).max_iters(5), Duration::ZERO);
assert_eq!(iters, 25);
}
#[test]
fn check_min_iters_beats_max_duration() {
let iters = count_iters(
check().min_iters(10).max_duration(Duration::from_millis(1)),
Duration::from_millis(1),
);
assert_eq!(iters, 10);
}
#[test]
fn check_min_duration_runs_past_min_iters() {
let min_duration = Duration::from_millis(20);
let start = Instant::now();
let iters = count_iters(
check().min_iters(1).min_duration(min_duration),
Duration::from_millis(1),
);
let elapsed = start.elapsed();
assert!(elapsed >= min_duration, "only ran for {elapsed:?}");
assert!(
iters > 1,
"ran {iters} iters, expected more than min_iters=1"
);
}
#[test]
fn check_duration_runs_for_at_least_that_long() {
let duration = Duration::from_millis(20);
let start = Instant::now();
count_iters(check().min_iters(0).duration(duration), Duration::ZERO);
let elapsed = start.elapsed();
assert!(elapsed >= duration, "only ran for {elapsed:?}");
}
#[test]
fn check_max_shrink_duration_bounds_shrinking() {
const MAX_SHRINK_DURATION_LIMIT: Duration = Duration::from_secs(4);
let mut max_shrink_duration = Duration::from_millis(1);
loop {
let start = Instant::now();
let failure = check()
.max_shrink_iters(usize::MAX)
.max_shrink_duration(max_shrink_duration)
.run_with(m::u8(), [u8::MAX], less_than_ten)
.unwrap_err()
.unwrap_failed();
let elapsed = start.elapsed();
assert!(
elapsed >= max_shrink_duration,
"only shrank for {elapsed:?}"
);
if failure.value != u8::MAX {
assert!(less_than_ten(&failure.value).is_err());
break;
}
assert!(
max_shrink_duration < MAX_SHRINK_DURATION_LIMIT,
"shrinking never made progress, even given {max_shrink_duration:?}; \
last shrank to {}",
failure.value
);
max_shrink_duration *= 2;
}
}
#[test]
fn check_zero_max_shrink_iters_disables_shrinking() {
let failure = check()
.max_shrink_iters(0)
.run_with(m::u8(), [u8::MAX], less_than_ten)
.unwrap_err()
.unwrap_failed();
assert_eq!(failure.value, u8::MAX);
}
#[test]
fn check_zero_max_shrink_duration_disables_shrinking() {
let failure = check()
.max_shrink_duration(Duration::ZERO)
.run_with(m::u8(), [u8::MAX], less_than_ten)
.unwrap_err()
.unwrap_failed();
assert_eq!(failure.value, u8::MAX);
}
#[test]
#[allow(deprecated)]
fn deprecated_shrink_iters_sets_max_shrink_iters() {
let failure = check()
.shrink_iters(0)
.run_with(m::u8(), [u8::MAX], less_than_ten)
.unwrap_err()
.unwrap_failed();
assert_eq!(failure.value, u8::MAX);
}
#[test]
fn check_run_with_okay() {
check()
.run_with(m::just(true), [true], |b: &bool| {
if *b {
Ok(())
} else {
Err("expected true!")
}
})
.unwrap();
}
#[test]
fn check_run_with_fail() {
let failure = check()
.run_with(m::bool(), [true], |b: &bool| {
if *b {
Ok(())
} else {
Err("expected true!")
}
})
.unwrap_err()
.unwrap_failed();
assert_eq!(failure.value, false);
assert_eq!(failure.message, "expected true!");
}
#[test]
fn check_run_with_empty_corpus() {
let result = check().run_with(m::bool(), [], |b: &bool| {
if *b {
Ok(())
} else {
Err("expected true!")
}
});
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), CheckError::EmptyCorpus));
}
#[test]
fn check_run_with_exhausted_value_is_dropped_and_not_rechecked() {
const ITERS: usize = 100;
let mut seen = Vec::new();
check()
.iters(ITERS)
.run_with(ExhaustedForZero, [1, 0], |x: &u32| -> Result<(), String> {
seen.push(*x);
Ok(())
})
.unwrap();
assert_eq!(seen.len(), 2 + ITERS);
let mut sorted = seen.clone();
sorted.sort_unstable();
sorted.dedup();
assert_eq!(sorted.len(), seen.len(), "checked a value twice: {seen:?}");
}
#[test]
fn check_run_with_exhausted_corpus_succeeds() {
let mut calls = 0_usize;
check()
.run_with(m::unit(), [(), ()], |_: &()| -> Result<(), String> {
calls += 1;
Ok(())
})
.unwrap();
assert_eq!(calls, 2);
}
#[test]
fn check_run_with_fail_and_shrink() {
let failure = check()
.max_shrink_iters(1000)
.run_with(m::u8(), [u8::MAX], |x: &u8| {
if *x < 10 {
Ok(())
} else {
Err("expected < 10")
}
})
.unwrap_err()
.unwrap_failed();
assert_eq!(failure.value, 10);
assert_eq!(failure.message, "expected < 10");
}
#[test]
fn check_run_with_fail_on_panic() {
let result = check().run_with(m::bool(), [true], |_: &bool| -> Result<(), String> {
std::panic!("oh no!")
});
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), CheckError::Failed(_)));
}
#[test]
fn check_run_with_fail_on_panic_and_shrink() {
let failure = check()
.max_shrink_iters(1000)
.run_with(m::u8(), [u8::MAX], |x: &u8| -> Result<(), String> {
assert!(*x < 10);
Ok(())
})
.unwrap_err()
.unwrap_failed();
assert_eq!(failure.value, 10);
assert_eq!(failure.message, "<panicked>");
}
}