use std::panic::AssertUnwindSafe;
use crate::rng::{RejectionState, is_iteration_rejected};
use crate::{RunError, RunResult, TestCaseContext, TestResult};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Stats {
pub accepted_cases: usize,
pub rejected_cases: usize,
pub total_samples: usize,
}
pub struct Runner {
seed: u64,
stats: Stats,
}
fn rejection_limit(cases: usize) -> usize {
const FLOOR: usize = 1024;
FLOOR.max(cases.saturating_mul(10))
}
impl Runner {
pub fn new(seed: u64) -> Self {
Self {
seed,
stats: Stats::default(),
}
}
pub fn stats(&self) -> Stats {
self.stats
}
pub fn run<F>(&mut self, cases: usize, f: F) -> RunResult
where
F: Fn(&mut TestCaseContext) -> TestResult,
{
self.stats = Stats::default();
let mut ctx = TestCaseContext::new(self.seed);
ctx.set_inside_runner();
let rejection_cap = rejection_limit(cases);
let mut accepted: usize = 0;
let mut rejected: usize = 0;
while accepted < cases {
ctx.clear_generated();
match run_case(&f, &mut ctx) {
CaseVerdict::Rejected(state) => {
rejected += 1;
if rejected > rejection_cap {
record_stats(self, accepted, rejected, ctx.total_samples());
let generated = ctx.take_generated();
return Err(RunError::from_too_many_rejections(
self.seed,
cases,
state.location,
generated,
self.stats,
));
}
continue;
}
CaseVerdict::Completed(CaseOutcome::Passed) => {
accepted += 1;
continue;
}
CaseVerdict::Completed(CaseOutcome::Failed(message)) => {
record_stats(self, accepted, rejected, ctx.total_samples());
let generated = ctx.take_generated();
return Err(RunError::from_panic(
self.seed, cases, message, generated, self.stats,
));
}
}
}
record_stats(self, accepted, rejected, ctx.total_samples());
Ok(())
}
}
impl std::fmt::Display for Runner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"noprop::Runner {{ seed: {:#018x}, stats: {{ accepted: {}, rejected: {}, total_samples: {} }} }}",
self.seed,
self.stats.accepted_cases,
self.stats.rejected_cases,
self.stats.total_samples,
)
}
}
enum CaseOutcome {
Passed,
Failed(String),
}
enum CaseVerdict {
Rejected(RejectionState),
Completed(CaseOutcome),
}
fn run_case<F>(f: &F, ctx: &mut TestCaseContext) -> CaseVerdict
where
F: Fn(&mut TestCaseContext) -> TestResult,
{
let outcome = std::panic::catch_unwind(AssertUnwindSafe(|| f(ctx)));
if let Some(state) = ctx.take_rejection() {
return CaseVerdict::Rejected(state);
}
let message = match outcome {
Ok(Ok(())) => return CaseVerdict::Completed(CaseOutcome::Passed),
Ok(Err(err)) => format!("{err}"),
Err(panic) => {
if is_iteration_rejected(&*panic) {
let location = std::panic::Location::caller();
return CaseVerdict::Rejected(RejectionState { location });
}
panic_message(panic)
}
};
CaseVerdict::Completed(CaseOutcome::Failed(message))
}
fn panic_message(payload: Box<dyn std::any::Any + Send>) -> String {
if let Some(s) = payload.downcast_ref::<&'static str>() {
(*s).to_string()
} else if let Some(s) = payload.downcast_ref::<String>() {
s.clone()
} else {
"unknown panic payload".to_string()
}
}
fn record_stats(runner: &mut Runner, accepted: usize, rejected: usize, total_samples: usize) {
runner.stats = Stats {
accepted_cases: accepted,
rejected_cases: rejected,
total_samples,
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejection_cap_accepts_exactly_at_the_boundary() {
let cases = 1;
let cap = rejection_limit(cases);
let count = std::cell::Cell::new(0usize);
let mut runner = Runner::new(1);
runner
.run(cases, |ctx| {
let n = count.get();
count.set(n + 1);
if n < cap {
ctx.reject_case();
}
Ok(())
})
.expect("rejecting exactly rejection_limit(cases) times must not exceed the cap");
let stats = runner.stats();
assert_eq!(stats.accepted_cases, cases);
assert_eq!(stats.rejected_cases, cap);
}
}