use std::panic::{catch_unwind, RefUnwindSafe, UnwindSafe};
use crate::hints::Hints;
use crate::runner::Error;
use crate::runner::{self, LimitSeries};
use crate::stats::Stats;
use crate::{hints, Fate, Limit, Prng};
#[derive(Debug, Clone)]
pub struct Config {
pub start_limit: Limit,
pub end_limit: Limit,
pub passes: u64,
pub hints_enabled: bool,
pub stats_enabled: bool,
}
#[derive(Debug)]
pub struct Counterexample {
pub prng: Prng,
pub limit: Limit,
pub hints: Option<Hints>,
pub error: Error,
}
#[derive(Debug)]
pub struct Report {
pub passes: u64,
pub stats: Option<Stats>,
pub counterexample: Option<Counterexample>,
}
pub fn run<T>(prng: Prng, config: &Config, test: T) -> Report
where
T: Fn(Fate) + UnwindSafe + RefUnwindSafe,
{
let limit_series = LimitSeries::new(config.start_limit, config.end_limit, config.passes);
let test_runs = || search_counterexample(prng, limit_series, &test);
let ((passes, counterexample_without_hints), stats) =
runner::util::collect_stats(config.stats_enabled, test_runs);
let counterexample = if config.hints_enabled {
counterexample_without_hints
.map(|counterexample| rerun_counterexample(counterexample, &test))
} else {
counterexample_without_hints
};
Report {
passes,
stats,
counterexample,
}
}
fn search_counterexample<T>(
mut prng: Prng,
limit_series: LimitSeries,
test: &T,
) -> (u64, Option<Counterexample>)
where
T: Fn(Fate) + UnwindSafe + RefUnwindSafe,
{
let mut passes = 0;
let mut limits = limit_series.into_iter();
let counterexample = loop {
let limit = match limits.next() {
None => break None,
Some(limit) => limit,
};
let prng_before_run = prng.clone();
let test_result = catch_unwind(|| {
let fate = Fate::new(&mut prng, limit);
test(fate);
prng
});
prng = match test_result {
Err(err) => {
let counterexample = Counterexample {
prng: prng_before_run,
limit,
hints: None,
error: Error(err),
};
break Some(counterexample);
}
Ok(prng_after_run) => prng_after_run,
};
passes += 1;
};
(passes, counterexample)
}
fn rerun_counterexample<T>(counterexample: Counterexample, test: &T) -> Counterexample
where
T: Fn(Fate) + UnwindSafe + RefUnwindSafe,
{
let (test_result, hints) = {
let mut prng = counterexample.prng.clone();
let limit = counterexample.limit;
hints::collect(|| {
catch_unwind(move || {
let fate = Fate::new(&mut prng, limit);
test(fate)
})
})
};
match test_result {
Ok(()) => counterexample,
Err(err) => Counterexample {
hints: Some(hints),
error: Error(err),
..counterexample
},
}
}
#[cfg(test)]
mod tests {
use crate::runner::repeatedly::{run, Config};
use crate::{hints, Prng, Seed};
fn default_prng() -> Prng {
Prng::from_seed(Seed::from(42))
}
fn default_config() -> Config {
Config {
start_limit: 0.into(),
end_limit: 100.into(),
passes: 100,
hints_enabled: true,
stats_enabled: false,
}
}
#[test]
fn zero_passes_if_test_fails() {
let config = default_config();
let report = run(default_prng(), &config, |_| panic!());
assert_eq!(report.passes, 0);
}
#[test]
fn full_passes_if_test_succeeds() {
let config = default_config();
let report = run(default_prng(), &config, |_| ());
assert_eq!(report.passes, config.passes);
}
#[test]
fn has_counterproof_if_test_fails() {
let config = default_config();
let report = run(default_prng(), &config, |_| panic!());
assert!(report.counterexample.is_some());
}
#[test]
fn no_counterproof_if_test_succeeds() {
let config = default_config();
let report = run(default_prng(), &config, |_| ());
assert!(report.counterexample.is_none());
}
#[test]
fn no_hints_if_disabled() {
let config = Config {
hints_enabled: false,
..default_config()
};
let report = run(default_prng(), &config, |_| panic!());
let counterexample = report.counterexample.unwrap();
assert!(counterexample.hints.is_none());
}
#[test]
fn no_hints_if_enabled_but_failure_not_reproducible() {
if cfg!(feature = "hints") {
let config = Config {
hints_enabled: true,
passes: 1,
..default_config()
};
for _ in 0..10 {
let (report, has_failed) = hints::collect(|| {
run(default_prng(), &config, |_| {
let should_fail = Seed::random().0 % 2 == 0;
hints::add(|| format!("{}", should_fail));
if should_fail {
panic!();
}
})
});
let failure_was_not_reproducible =
&has_failed.0[0].text == "true" && &has_failed.0[1].text == "false";
if failure_was_not_reproducible {
let counterexample = report.counterexample.unwrap();
assert!(counterexample.hints.is_none());
}
}
}
}
#[test]
fn has_hints_if_enabled_and_test_deterministic() {
let config = Config {
hints_enabled: true,
..default_config()
};
let report = run(default_prng(), &config, |_| panic!());
let counterexample = report.counterexample.unwrap();
assert!(counterexample.hints.is_some());
}
#[test]
fn no_stats_if_disabled_and_test_succeeds() {
let config = Config {
stats_enabled: false,
..default_config()
};
let report = run(default_prng(), &config, |_| ());
let stats = report.stats;
assert!(stats.is_none());
}
#[test]
fn no_stats_if_disabled_and_test_fails() {
let config = Config {
stats_enabled: false,
..default_config()
};
let report = run(default_prng(), &config, |_| panic!());
let stats = report.stats;
assert!(stats.is_none());
}
#[test]
fn has_stats_if_enabled_test_succeeds() {
let config = Config {
stats_enabled: true,
..default_config()
};
let report = run(default_prng(), &config, |_| ());
let stats = report.stats;
assert!(stats.is_some());
}
#[test]
fn has_stats_if_enabled_and_test_fails() {
let config = Config {
stats_enabled: true,
..default_config()
};
let report = run(default_prng(), &config, |_| panic!());
let stats = report.stats;
assert!(stats.is_some());
}
}