use std::panic::AssertUnwindSafe;
use crate::error::SearchPolicy;
use crate::rng::{
ChoiceMeta, ChoiceSequence, Feature, FeedbackState, RejectionState, XoshiroState,
is_iteration_rejected,
};
use crate::{RunError, RunResult, TestCaseContext, TestResult};
const CORPUS_SIZE: usize = 64;
const MUTATION_DENOM: u64 = 4;
const RANDOM_RESTART_DENOM: u64 = 8;
const REJECTED_PICK_DENOM: u64 = 8;
const MAX_GLOBAL_FEATURES: usize = 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Stats {
pub accepted_cases: usize,
pub rejected_cases: usize,
pub total_samples: usize,
pub discovered_features: usize,
pub max_corpus_size: 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_feedback_guided<F>(&mut self, cases: usize, f: F) -> RunResult
where
F: Fn(&mut TestCaseContext) -> TestResult,
{
self.stats = Stats::default();
let mut search = CorpusGuidedSearch::new(self.seed);
let rejection_cap = rejection_limit(cases);
let mut accepted: usize = 0;
let mut rejected: usize = 0;
let mut total_samples: usize = 0;
while accepted < cases {
let mut ctx = search.next_context();
ctx.set_inside_runner();
ctx.enable_corpus_guided();
let verdict = run_case(&f, &mut ctx);
total_samples = total_samples.saturating_add(ctx.total_samples());
match verdict {
CaseVerdict::Rejected(state) => {
rejected += 1;
if rejected > rejection_cap {
record_corpus_stats(self, &search, accepted, rejected, total_samples);
return Err(too_many_rejections(
self,
&mut ctx,
state.location,
SearchPolicy::FeedbackGuided,
cases,
));
}
let feedback = ctx.take_feedback();
if let FeedbackState::SemanticCoverage(mut cov) = feedback {
let features = cov.take_features();
if let Some(sequence) = ctx.take_sequence() {
search.corpus.admit_rejected(sequence, features);
}
}
continue;
}
CaseVerdict::Completed(CaseOutcome::Passed) => {
let feedback = ctx.take_feedback();
let mut cov = match feedback {
FeedbackState::SemanticCoverage(cov) => cov,
_ => unreachable!(
"run_feedback_guided enables feedback-guided mode before each case"
),
};
let features = cov.take_features();
if let Some(sequence) = ctx.take_sequence() {
search.corpus.admit_accepted(sequence, features);
}
accepted += 1;
continue;
}
CaseVerdict::Completed(CaseOutcome::Failed(message)) => {
record_corpus_stats(self, &search, accepted, rejected, total_samples);
let generated = ctx.take_generated();
let feedback = ctx.take_feedback();
let semantic_features = match feedback {
FeedbackState::SemanticCoverage(mut cov) => cov.take_features(),
_ => unreachable!(
"run_feedback_guided enables feedback-guided mode before each case"
),
};
return Err(RunError::from_panic(
self.seed,
cases,
message,
generated,
self.stats,
SearchPolicy::FeedbackGuided,
)
.with_semantic(
semantic_features,
accepted + rejected + 1,
));
}
}
}
record_corpus_stats(self, &search, accepted, rejected, total_samples);
Ok(())
}
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(), 0, 0);
let generated = ctx.take_generated();
return Err(RunError::from_too_many_rejections(
self.seed,
cases,
state.location,
generated,
self.stats,
SearchPolicy::Uniform,
));
}
continue;
}
CaseVerdict::Completed(CaseOutcome::Passed) => {
accepted += 1;
continue;
}
CaseVerdict::Completed(CaseOutcome::Failed(message)) => {
record_stats(self, accepted, rejected, ctx.total_samples(), 0, 0);
let generated = ctx.take_generated();
return Err(RunError::from_panic(
self.seed,
cases,
message,
generated,
self.stats,
SearchPolicy::Uniform,
));
}
}
}
record_stats(self, accepted, rejected, ctx.total_samples(), 0, 0);
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: {}, discovered_features: {}, max_corpus_size: {} }} }}",
self.seed,
self.stats.accepted_cases,
self.stats.rejected_cases,
self.stats.total_samples,
self.stats.discovered_features,
self.stats.max_corpus_size,
)
}
}
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,
discovered_features: usize,
max_corpus_size: usize,
) {
runner.stats = Stats {
accepted_cases: accepted,
rejected_cases: rejected,
total_samples,
discovered_features,
max_corpus_size,
};
}
fn corpus_stats(search: &CorpusGuidedSearch) -> (usize, usize) {
(
search.corpus.observed.len(),
search.corpus.accepted.len() + search.corpus.rejected.len(),
)
}
fn record_corpus_stats(
runner: &mut Runner,
search: &CorpusGuidedSearch,
accepted: usize,
rejected: usize,
total_samples: usize,
) {
let (discovered_features, max_corpus_size) = corpus_stats(search);
record_stats(
runner,
accepted,
rejected,
total_samples,
discovered_features,
max_corpus_size,
);
}
fn too_many_rejections(
runner: &mut Runner,
ctx: &mut TestCaseContext,
location: &'static std::panic::Location<'static>,
policy: SearchPolicy,
cases: usize,
) -> RunError {
let generated = ctx.take_generated();
let err = RunError::from_too_many_rejections(
runner.seed,
cases,
location,
generated,
runner.stats,
policy,
);
if matches!(policy, SearchPolicy::FeedbackGuided) {
let features = match ctx.take_feedback() {
FeedbackState::SemanticCoverage(mut cov) => cov.take_features(),
_ => Vec::new(),
};
let last_candidate = runner.stats.accepted_cases + runner.stats.rejected_cases;
return err.with_semantic(features, last_candidate);
}
err
}
struct SemanticEntry {
sequence: ChoiceSequence,
novel: Vec<Feature>,
}
struct SemanticCorpus {
accepted: Vec<SemanticEntry>,
rejected: Vec<SemanticEntry>,
observed: Vec<Feature>,
}
impl SemanticCorpus {
fn new() -> Self {
Self {
accepted: Vec::with_capacity(CORPUS_SIZE),
rejected: Vec::with_capacity(CORPUS_SIZE),
observed: Vec::new(),
}
}
fn is_empty(&self) -> bool {
self.accepted.is_empty() && self.rejected.is_empty()
}
fn novel_features(&self, case_features: &[Feature]) -> Vec<Feature> {
case_features
.iter()
.filter(|f| !self.observed.contains(f))
.cloned()
.collect()
}
fn register(&mut self, novel: &[Feature]) -> Vec<Feature> {
let mut registered = Vec::new();
for feature in novel {
if self.observed.len() >= MAX_GLOBAL_FEATURES {
break;
}
self.observed.push(feature.clone());
registered.push(feature.clone());
}
registered
}
fn admit_accepted(&mut self, sequence: ChoiceSequence, case_features: Vec<Feature>) -> bool {
let novel = self.novel_features(&case_features);
let registered = self.register(&novel);
if !registered.is_empty() {
self.accepted.push(SemanticEntry {
sequence,
novel: registered,
});
self.evict_if_over_capacity();
return true;
}
false
}
fn admit_rejected(&mut self, sequence: ChoiceSequence, case_features: Vec<Feature>) -> bool {
let novel = self.novel_features(&case_features);
let registered = self.register(&novel);
if registered.is_empty() {
return false;
}
self.rejected.push(SemanticEntry {
sequence,
novel: registered,
});
self.evict_if_over_capacity();
true
}
fn weakest_overall(&self) -> usize {
let mut weakest: Option<(&SemanticEntry, usize)> = None;
for (i, entry) in self.accepted.iter().chain(self.rejected.iter()).enumerate() {
match weakest {
None => weakest = Some((entry, i)),
Some((incumbent, _)) => {
if entry.novel.len() < incumbent.novel.len() {
weakest = Some((entry, i));
}
}
}
}
weakest.expect("corpus is non-empty while evicting").1
}
fn evict_if_over_capacity(&mut self) {
while self.accepted.len() + self.rejected.len() > CORPUS_SIZE {
let idx = self.weakest_overall();
if idx < self.accepted.len() {
self.accepted.remove(idx);
} else {
self.rejected.remove(idx - self.accepted.len());
}
}
}
}
struct CorpusGuidedSearch {
corpus: SemanticCorpus,
prng: XoshiroState,
}
impl CorpusGuidedSearch {
fn new(seed: u64) -> Self {
Self {
corpus: SemanticCorpus::new(),
prng: XoshiroState::from_seed(seed),
}
}
fn next_context(&mut self) -> TestCaseContext {
let restart = self.corpus.is_empty() || self.prng.sample_below(RANDOM_RESTART_DENOM) == 0;
if restart {
let case_seed = self.prng.next_u64();
TestCaseContext::recording(case_seed)
} else {
let explore_seed = self.prng.next_u64();
let use_rejected = !self.corpus.rejected.is_empty()
&& (self.corpus.accepted.is_empty()
|| self.prng.sample_below(REJECTED_PICK_DENOM) == 0);
let mut sequence = if use_rejected {
let idx = self.prng.sample_below(self.corpus.rejected.len() as u64) as usize;
self.corpus.rejected[idx].sequence.clone()
} else {
let idx = self.prng.sample_below(self.corpus.accepted.len() as u64) as usize;
self.corpus.accepted[idx].sequence.clone()
};
mutate_sequence(&mut sequence, &mut self.prng);
TestCaseContext::exploring(sequence, explore_seed)
}
}
}
fn mutate_sequence(sequence: &mut ChoiceSequence, prng: &mut XoshiroState) {
let (draws, metas) = sequence.draws_and_metas();
for (draw, meta) in draws.iter_mut().zip(metas.iter()) {
if prng.sample_below(MUTATION_DENOM) != 0 {
continue;
}
match meta {
ChoiceMeta::Bounded { bound } => {
if draw.len() != 8 {
continue;
}
if *bound <= 1 {
continue;
}
let new_value = prng.sample_below(*bound);
draw[..8].copy_from_slice(&new_value.to_le_bytes());
}
ChoiceMeta::Choice { len } => {
if draw.len() != 8 {
continue;
}
if *len <= 1 {
continue;
}
let new_value = prng.sample_below(*len as u64);
draw[..8].copy_from_slice(&new_value.to_le_bytes());
}
ChoiceMeta::Integer => {
match draw.len() {
1 => draw.copy_from_slice(&[prng.next_u64() as u8]),
2 => draw.copy_from_slice(&(prng.next_u64() as u16).to_le_bytes()),
4 => draw.copy_from_slice(&(prng.next_u64() as u32).to_le_bytes()),
8 => draw.copy_from_slice(&prng.next_u64().to_le_bytes()),
16 => {
let lo = prng.next_u64();
let hi = prng.next_u64();
draw[..8].copy_from_slice(&lo.to_le_bytes());
draw[8..].copy_from_slice(&hi.to_le_bytes());
}
width => {
debug_assert!(false, "unexpected integer draw width: {width}");
prng.fill(draw);
}
}
}
ChoiceMeta::Raw => {
prng.fill(draw);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn event_feature(label: &'static str) -> Feature {
Feature {
label,
kind: crate::rng::FeatureKind::Event(crate::rng::EventBucket::One),
}
}
fn bucket_feature(label: &'static str, value: u64) -> Feature {
Feature {
label,
kind: crate::rng::FeatureKind::Bucket { value },
}
}
#[test]
fn semantic_corpus_admits_novel_features() {
let mut corpus = SemanticCorpus::new();
assert!(
corpus.admit_accepted(ChoiceSequence::default(), vec![event_feature("a")]),
"a case with a novel feature must be admitted"
);
assert!(
!corpus.admit_accepted(ChoiceSequence::default(), vec![event_feature("a")]),
"a case with only known features must not be admitted"
);
assert_eq!(corpus.accepted.len(), 1);
}
#[test]
fn semantic_corpus_register_stops_at_global_cap() {
let mut corpus = SemanticCorpus::new();
for i in 0..MAX_GLOBAL_FEATURES {
assert!(corpus.admit_accepted(
ChoiceSequence::default(),
vec![bucket_feature("f", i as u64)],
));
}
assert_eq!(corpus.observed.len(), MAX_GLOBAL_FEATURES);
assert!(
!corpus.admit_accepted(
ChoiceSequence::default(),
vec![bucket_feature("f", MAX_GLOBAL_FEATURES as u64)],
),
"a novel feature after the cap must not be admitted"
);
assert_eq!(corpus.observed.len(), MAX_GLOBAL_FEATURES);
assert_eq!(
corpus.accepted.len(),
CORPUS_SIZE,
"the corpus itself stays bounded by eviction"
);
}
#[test]
fn semantic_corpus_register_partially_registers_at_cap_boundary() {
let mut corpus = SemanticCorpus::new();
let prefix: Vec<Feature> = (0..MAX_GLOBAL_FEATURES - 1)
.map(|i| bucket_feature("g", i as u64))
.collect();
assert_eq!(corpus.register(&prefix).len(), MAX_GLOBAL_FEATURES - 1);
let fits = bucket_feature("g", (MAX_GLOBAL_FEATURES - 1) as u64);
let overflows = bucket_feature("g", MAX_GLOBAL_FEATURES as u64);
assert!(corpus.admit_accepted(ChoiceSequence::default(), vec![fits.clone(), overflows],));
assert_eq!(corpus.accepted[0].novel, vec![fits]);
assert_eq!(corpus.observed.len(), MAX_GLOBAL_FEATURES);
}
#[test]
fn semantic_corpus_rejected_queue_holds_novel_cases() {
let mut corpus = SemanticCorpus::new();
assert!(
corpus.admit_rejected(ChoiceSequence::default(), vec![event_feature("r")]),
"a rejected case with a novel feature must enter the rejected queue"
);
assert!(
!corpus.admit_rejected(ChoiceSequence::default(), vec![event_feature("r")]),
"a rejected case with only known features must not be kept"
);
assert_eq!(corpus.rejected.len(), 1);
assert_eq!(corpus.observed.len(), 1);
}
#[test]
fn semantic_corpus_evicts_fewest_features_first() {
let mut corpus = SemanticCorpus::new();
for i in 0..CORPUS_SIZE {
corpus.admit_accepted(
ChoiceSequence::default(),
vec![bucket_feature("single", i as u64)],
);
}
assert_eq!(corpus.accepted.len(), CORPUS_SIZE);
corpus.admit_accepted(
ChoiceSequence::default(),
vec![event_feature("two-a"), event_feature("two-b")],
);
assert_eq!(corpus.accepted.len(), CORPUS_SIZE);
assert!(
corpus
.accepted
.iter()
.any(|e| e.novel.contains(&event_feature("two-a"))),
"the feature-richest entry must survive eviction"
);
assert!(
!corpus
.accepted
.iter()
.any(|e| e.novel.contains(&bucket_feature("single", 0))),
"the earliest single-feature entry must be evicted"
);
}
#[test]
fn semantic_corpus_evicts_across_both_queues() {
let mut corpus = SemanticCorpus::new();
for i in 0..CORPUS_SIZE {
corpus.admit_accepted(
ChoiceSequence::default(),
vec![bucket_feature("accepted", i as u64)],
);
}
assert_eq!(corpus.accepted.len(), CORPUS_SIZE);
corpus.admit_rejected(ChoiceSequence::default(), vec![event_feature("rejected")]);
assert_eq!(corpus.accepted.len() + corpus.rejected.len(), CORPUS_SIZE);
assert_eq!(
corpus.rejected.len(),
1,
"the rejected entry must survive the earliest-arrival tie-break"
);
assert!(
!corpus
.accepted
.iter()
.any(|e| e.novel.contains(&bucket_feature("accepted", 0))),
"the earliest accepted entry must be evicted"
);
}
#[test]
fn corpus_guided_search_replays_accepted_entries() {
let mut search = CorpusGuidedSearch::new(42);
let mut seq = ChoiceSequence::default();
seq.push_draw(
7u64.to_le_bytes().to_vec(),
ChoiceMeta::Bounded { bound: 10 },
);
search.corpus.admit_accepted(seq, vec![event_feature("a")]);
let mut in_domain = 0;
for _ in 0..16 {
let mut ctx = search.next_context();
ctx.set_next_choice_meta(ChoiceMeta::Bounded { bound: 10 });
let mut buf = [0u8; 8];
ctx.fill(&mut buf);
let x = u64::from_le_bytes(buf);
let _ = ctx.take_sequence();
if x < 10 {
in_domain += 1;
}
}
assert!(
in_domain >= 8,
"most candidates must replay an in-domain draw: {in_domain}"
);
}
#[test]
fn corpus_guided_search_picks_from_rejected_queue_when_accepted_empty() {
let mut search = CorpusGuidedSearch::new(7);
let mut seq = ChoiceSequence::default();
seq.push_draw(
3u64.to_le_bytes().to_vec(),
ChoiceMeta::Bounded { bound: 10 },
);
search.corpus.admit_rejected(seq, vec![event_feature("r")]);
let mut seen_replay = false;
for _ in 0..16 {
let mut ctx = search.next_context();
ctx.set_next_choice_meta(ChoiceMeta::Bounded { bound: 10 });
let mut buf = [0u8; 8];
ctx.fill(&mut buf);
let x = u64::from_le_bytes(buf);
let _ = ctx.take_sequence();
if x == 3 {
seen_replay = true;
}
}
assert!(
seen_replay,
"the rejected entry must be replayed while the accepted queue is empty"
);
}
#[test]
fn mutation_stays_within_bounded_domain() {
let mut prng = XoshiroState::from_seed(1234);
let mut mutated = false;
let mut raw_regenerated = false;
let original_raw = vec![0xAB; 8];
for _ in 0..100 {
let mut seq = ChoiceSequence::default();
seq.push_draw(
7u64.to_le_bytes().to_vec(),
ChoiceMeta::Bounded { bound: 10 },
);
seq.push_draw(original_raw.clone(), ChoiceMeta::Raw);
mutate_sequence(&mut seq, &mut prng);
let x = u64::from_le_bytes(seq.draws()[0][..8].try_into().unwrap());
assert!(x < 10, "mutated bounded draw {x} escaped its domain");
if x != 7 {
mutated = true;
}
if seq.draws()[1] != original_raw {
raw_regenerated = true;
}
}
assert!(mutated, "mutation must have occurred at least once");
assert!(
raw_regenerated,
"raw draw regeneration must have occurred at least once"
);
}
#[test]
fn mutation_preserves_choice_index_domain() {
let mut prng = XoshiroState::from_seed(99);
for _ in 0..100 {
let mut seq = ChoiceSequence::default();
seq.push_draw(3u64.to_le_bytes().to_vec(), ChoiceMeta::Choice { len: 4 });
mutate_sequence(&mut seq, &mut prng);
let idx = u64::from_le_bytes(seq.draws()[0][..8].try_into().unwrap());
assert!(idx < 4, "mutated choice index {idx} escaped its domain");
}
}
}
#[test]
fn mutation_rewrites_integer_draws() {
let mut prng = XoshiroState::from_seed(77);
let mut changed = false;
for _ in 0..100 {
let mut seq = ChoiceSequence::default();
seq.push_draw(5u64.to_le_bytes().to_vec(), ChoiceMeta::Integer);
mutate_sequence(&mut seq, &mut prng);
let x = u64::from_le_bytes(seq.draws()[0][..8].try_into().unwrap());
if x != 5 {
changed = true;
}
}
assert!(changed, "integer draws must be rewritten to another value");
}
#[test]
fn mutation_rewrites_integer_draws_across_widths() {
let mut prng = XoshiroState::from_seed(31337);
let mut changed = 0u32;
for width in [1usize, 2, 4, 8, 16] {
for _ in 0..64 {
let mut seq = ChoiceSequence::default();
seq.push_draw(vec![0xAB; width], ChoiceMeta::Integer);
let before = seq.draws()[0].clone();
mutate_sequence(&mut seq, &mut prng);
if seq.draws()[0] != before {
changed += 1;
break;
}
}
}
assert_eq!(changed, 5, "every integer width must be rewritable");
}
#[test]
fn mutation_rewrites_upper_bytes_of_16_byte_integer_draw() {
let mut prng = XoshiroState::from_seed(1337);
let mut low_changed = false;
let mut high_changed = false;
for _ in 0..256 {
let mut seq = ChoiceSequence::default();
seq.push_draw(vec![0xAB; 16], ChoiceMeta::Integer);
let before = seq.draws()[0].clone();
mutate_sequence(&mut seq, &mut prng);
let after = &seq.draws()[0];
low_changed |= after[..8] != before[..8];
high_changed |= after[8..] != before[8..];
if low_changed && high_changed {
break;
}
}
assert!(low_changed, "low 8 bytes must be rewritable");
assert!(high_changed, "high 8 bytes must be rewritable");
}
#[test]
fn stats_corpus_fields_are_zero_for_uniform() {
let mut runner = Runner::new(1);
runner
.run(4, |ctx| {
crate::sample_u32(ctx);
Ok(())
})
.unwrap();
let stats = runner.stats();
assert_eq!(stats.discovered_features, 0);
assert_eq!(stats.max_corpus_size, 0);
let err = Runner::new(1)
.run(4, |_ctx| {
Err::<(), Box<dyn std::error::Error>>("boom".into())
})
.expect_err("returned Err must fail the run");
assert_eq!(err.stats().discovered_features, 0);
assert_eq!(err.stats().max_corpus_size, 0);
let err = Runner::new(1)
.run(4, |ctx| {
crate::sample_u32(ctx);
panic!("boom");
})
.expect_err("panicking closure must fail the run");
assert_eq!(err.stats().discovered_features, 0);
assert_eq!(err.stats().max_corpus_size, 0);
}
#[test]
fn stats_corpus_fields_reflect_observed_features() {
let mut runner = Runner::new(1);
runner
.run_feedback_guided(4, |ctx| {
ctx.bucket("b", 1);
Ok(())
})
.unwrap();
let stats = runner.stats();
assert_eq!(stats.discovered_features, 1);
assert_eq!(stats.max_corpus_size, 1);
}
#[test]
fn stats_corpus_fields_respect_corpus_cap() {
let case = std::cell::Cell::new(0u64);
let mut runner = Runner::new(1);
runner
.run_feedback_guided(100, |ctx| {
let i = case.get();
case.set(i + 1);
ctx.bucket("b", i);
Ok(())
})
.unwrap();
let stats = runner.stats();
assert_eq!(stats.max_corpus_size, CORPUS_SIZE);
assert_eq!(
stats.discovered_features, 100,
"the observation set must keep growing past the corpus cap"
);
}
#[test]
fn stats_corpus_fields_on_too_many_rejections() {
let case = std::cell::Cell::new(0u64);
let mut runner = Runner::new(1);
let err = runner
.run_feedback_guided(103, |ctx| {
let i = case.get();
case.set(i + 1);
ctx.bucket("b", i);
ctx.reject_case();
})
.expect_err("rejecting every case must hit the rejection cap");
let stats = err.stats();
assert_eq!(stats.rejected_cases, rejection_limit(103) + 1);
assert_eq!(stats.discovered_features, MAX_GLOBAL_FEATURES);
assert_eq!(stats.max_corpus_size, CORPUS_SIZE);
}
#[test]
fn corpus_stats_matches_corpus_state() {
use crate::rng::{EventBucket, FeatureKind};
let mut search = CorpusGuidedSearch::new(1);
assert_eq!(corpus_stats(&search), (0, 0));
let feature = Feature {
label: "a",
kind: FeatureKind::Event(EventBucket::One),
};
assert!(
search
.corpus
.admit_accepted(ChoiceSequence::default(), vec![feature])
);
assert_eq!(corpus_stats(&search), (1, 1));
let feature = Feature {
label: "b",
kind: FeatureKind::Event(EventBucket::One),
};
assert!(
search
.corpus
.admit_rejected(ChoiceSequence::default(), vec![feature])
);
assert_eq!(corpus_stats(&search), (2, 2));
}
#[test]
fn stats_corpus_fields_include_rejected_case_features() {
let case = std::cell::Cell::new(0u64);
let mut runner = Runner::new(1);
runner
.run_feedback_guided(8, |ctx| {
let i = case.get();
case.set(i + 1);
ctx.bucket("b", 1);
if i == 0 {
ctx.reject_case();
}
Ok(())
})
.expect("run must succeed");
let stats = runner.stats();
assert_eq!(stats.rejected_cases, 1);
assert_eq!(stats.discovered_features, 1);
assert_eq!(stats.max_corpus_size, 1);
}
#[test]
fn stats_corpus_fields_zero_cases() {
let mut runner = Runner::new(1);
runner
.run_feedback_guided(0, |_ctx| {
panic!("closure must not be invoked with zero cases");
})
.expect("zero cases must succeed");
assert_eq!(runner.stats(), Stats::default());
}