use std::panic::AssertUnwindSafe;
use crate::error::SearchPolicy;
use crate::rng::{
ChoiceMeta, ChoiceSequence, Feature, FeedbackState, ScalarFeedback, XoshiroState,
is_iteration_rejected,
};
use crate::{Error, Result, TestCaseContext};
const CORPUS_SIZE: usize = 64;
const MUTATION_DENOM: u64 = 4;
const RANDOM_RESTART_DENOM: u64 = 8;
const LOW_SCORE_DENOM: u64 = 4;
const REJECTED_PICK_DENOM: u64 = 8;
const MAX_GLOBAL_FEATURES: usize = 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Stats {
pub accepted_iterations: usize,
pub rejected_iterations: usize,
pub total_samples: usize,
pub discovered_features: usize,
pub max_corpus_size: usize,
}
pub struct Runner {
seed: u64,
iterations: usize,
stats: Stats,
}
fn rejection_limit(iterations: usize) -> usize {
const FLOOR: usize = 1024;
FLOOR.max(iterations.saturating_mul(10))
}
impl Runner {
pub fn new(seed: u64, iterations: usize) -> Self {
Self {
seed,
iterations,
stats: Stats::default(),
}
}
pub fn stats(&self) -> Stats {
self.stats
}
pub fn run_targeted<F>(&mut self, f: F) -> Result<()>
where
F: Fn(&mut TestCaseContext) -> std::result::Result<(), Box<dyn std::error::Error>>,
{
self.stats = Stats::default();
let mut search = TargetedSearch::new(self.seed);
let rejection_cap = rejection_limit(self.iterations);
let mut accepted: usize = 0;
let mut rejected: usize = 0;
let mut total_samples: usize = 0;
while accepted < self.iterations {
let mut ctx = search.next_context();
ctx.set_inside_runner();
ctx.enable_targeted();
let outcome = std::panic::catch_unwind(AssertUnwindSafe(|| f(&mut ctx)));
let rejection = ctx.take_rejection();
total_samples = total_samples.saturating_add(ctx.total_samples());
if let Some(state) = rejection {
rejected += 1;
if rejected > rejection_cap {
record_stats(self, accepted, rejected, total_samples, 0, 0);
return Err(too_many_rejections(
self,
&mut ctx,
accepted,
rejected,
state.location,
SearchPolicy::Targeted,
));
}
continue;
}
let message = match outcome {
Ok(Ok(())) => {
let feedback = ctx.take_feedback();
let score = match feedback {
FeedbackState::Targeted { max_score } => match max_score {
ScalarFeedback::Valid(score) => score,
ScalarFeedback::Missing => {
record_stats(self, accepted, rejected, total_samples, 0, 0);
return Err(Error::from_missing_feedback(
self.seed,
accepted,
self.iterations,
ctx.take_generated(),
self.stats,
));
}
ScalarFeedback::Invalid => {
record_stats(self, accepted, rejected, total_samples, 0, 0);
return Err(Error::from_invalid_feedback(
self.seed,
accepted,
self.iterations,
ctx.take_generated(),
self.stats,
));
}
},
FeedbackState::Disabled => {
unreachable!("run_targeted enables targeted mode before each case")
}
FeedbackState::SemanticCoverage(_) => {
unreachable!(
"run_corpus_guided_with_policy enables corpus-guided mode before each case"
)
}
};
let sequence = ctx
.take_sequence()
.expect("run_targeted contexts are always recording or exploring");
search.corpus.admit(sequence, score);
accepted += 1;
continue;
}
Ok(Err(err)) => format!("{err}"),
Err(panic) => {
if is_iteration_rejected(&*panic) {
rejected += 1;
if rejected > rejection_cap {
record_stats(self, accepted, rejected, total_samples, 0, 0);
return Err(too_many_rejections(
self,
&mut ctx,
accepted,
rejected,
std::panic::Location::caller(),
SearchPolicy::Targeted,
));
}
continue;
}
panic_message(panic)
}
};
record_stats(self, accepted, rejected, total_samples, 0, 0);
let generated = ctx.take_generated();
return Err(Error::from_panic(
self.seed,
accepted,
self.iterations,
message,
generated,
self.stats,
SearchPolicy::Targeted,
));
}
record_stats(self, accepted, rejected, total_samples, 0, 0);
Ok(())
}
pub fn run_corpus_guided<F>(&mut self, f: F) -> Result<()>
where
F: Fn(&mut TestCaseContext) -> std::result::Result<(), Box<dyn std::error::Error>>,
{
self.run_corpus_guided_with_policy(CorpusPolicy::SemanticWithPriority, f)
}
pub fn run_corpus_guided_with_policy<F>(&mut self, policy: CorpusPolicy, f: F) -> Result<()>
where
F: Fn(&mut TestCaseContext) -> std::result::Result<(), Box<dyn std::error::Error>>,
{
self.stats = Stats::default();
let mut search = CorpusGuidedSearch::new(self.seed, policy);
let rejection_cap = rejection_limit(self.iterations);
let mut accepted: usize = 0;
let mut rejected: usize = 0;
let mut total_samples: usize = 0;
while accepted < self.iterations {
let mut ctx = search.next_context();
ctx.set_inside_runner();
ctx.enable_corpus_guided();
let outcome = std::panic::catch_unwind(AssertUnwindSafe(|| f(&mut ctx)));
let rejection = ctx.take_rejection();
total_samples = total_samples.saturating_add(ctx.total_samples());
if let Some(state) = rejection {
rejected += 1;
if rejected > rejection_cap {
record_corpus_stats(self, &search, accepted, rejected, total_samples);
return Err(too_many_rejections(
self,
&mut ctx,
accepted,
rejected,
state.location,
SearchPolicy::CorpusGuided(policy),
));
}
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;
}
let message = match outcome {
Ok(Ok(())) => {
let feedback = ctx.take_feedback();
let mut cov = match feedback {
FeedbackState::SemanticCoverage(cov) => cov,
_ => unreachable!(
"run_corpus_guided_with_policy enables corpus-guided mode before each case"
),
};
let features = cov.take_features();
let priority = cov.priority();
if let Some(sequence) = ctx.take_sequence() {
search.corpus.admit_accepted(sequence, features, priority);
}
accepted += 1;
continue;
}
Ok(Err(err)) => format!("{err}"),
Err(panic) => {
if is_iteration_rejected(&*panic) {
rejected += 1;
if rejected > rejection_cap {
record_corpus_stats(self, &search, accepted, rejected, total_samples);
return Err(too_many_rejections(
self,
&mut ctx,
accepted,
rejected,
std::panic::Location::caller(),
SearchPolicy::CorpusGuided(policy),
));
}
continue;
}
panic_message(panic)
}
};
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_corpus_guided_with_policy enables corpus-guided mode before each case"
),
};
return Err(Error::from_panic(
self.seed,
accepted,
self.iterations,
message,
generated,
self.stats,
SearchPolicy::CorpusGuided(policy),
)
.with_semantic(
semantic_features,
accepted + rejected + 1,
));
}
record_corpus_stats(self, &search, accepted, rejected, total_samples);
Ok(())
}
pub fn run<F>(&mut self, f: F) -> Result<()>
where
F: Fn(&mut TestCaseContext) -> std::result::Result<(), Box<dyn std::error::Error>>,
{
self.stats = Stats::default();
let mut ctx = TestCaseContext::new(self.seed);
ctx.set_inside_runner();
let rejection_cap = rejection_limit(self.iterations);
let mut accepted: usize = 0;
let mut rejected: usize = 0;
while accepted < self.iterations {
ctx.clear_generated();
let outcome = std::panic::catch_unwind(AssertUnwindSafe(|| f(&mut ctx)));
let rejection = ctx.take_rejection();
if let Some(state) = rejection {
let _ = outcome;
rejected += 1;
if rejected > rejection_cap {
record_stats(self, accepted, rejected, ctx.total_samples(), 0, 0);
let generated = ctx.take_generated();
return Err(Error::from_too_many_rejections(
self.seed,
accepted,
self.iterations,
rejected,
state.location,
generated,
self.stats,
SearchPolicy::Uniform,
));
}
continue;
}
let message = match outcome {
Ok(Ok(())) => {
accepted += 1;
continue;
}
Ok(Err(err)) => format!("{err}"),
Err(panic) => {
if is_iteration_rejected(&*panic) {
rejected += 1;
if rejected > rejection_cap {
record_stats(self, accepted, rejected, ctx.total_samples(), 0, 0);
let generated = ctx.take_generated();
let unknown_location = std::panic::Location::caller();
return Err(Error::from_too_many_rejections(
self.seed,
accepted,
self.iterations,
rejected,
unknown_location,
generated,
self.stats,
SearchPolicy::Uniform,
));
}
continue;
}
panic_message(panic)
}
};
record_stats(self, accepted, rejected, ctx.total_samples(), 0, 0);
let generated = ctx.take_generated();
return Err(Error::from_panic(
self.seed,
accepted,
self.iterations,
message,
generated,
self.stats,
SearchPolicy::Uniform,
));
}
record_stats(self, accepted, rejected, ctx.total_samples(), 0, 0);
Ok(())
}
}
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_iterations: accepted,
rejected_iterations: 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,
accepted: usize,
rejected: usize,
location: &'static std::panic::Location<'static>,
policy: SearchPolicy,
) -> Error {
let generated = ctx.take_generated();
let err = Error::from_too_many_rejections(
runner.seed,
accepted,
runner.iterations,
rejected,
location,
generated,
runner.stats,
policy,
);
if matches!(policy, SearchPolicy::CorpusGuided(_)) {
let features = match ctx.take_feedback() {
FeedbackState::SemanticCoverage(mut cov) => cov.take_features(),
_ => Vec::new(),
};
return err.with_semantic(features, accepted + rejected);
}
err
}
struct CorpusEntry {
sequence: ChoiceSequence,
score: f64,
}
struct Corpus {
entries: Vec<CorpusEntry>,
}
impl Corpus {
fn new() -> Self {
Self {
entries: Vec::with_capacity(CORPUS_SIZE),
}
}
fn is_empty(&self) -> bool {
self.entries.is_empty()
}
fn min_index(&self) -> usize {
let mut min_idx = 0;
for (i, entry) in self.entries.iter().enumerate().skip(1) {
if entry.score < self.entries[min_idx].score {
min_idx = i;
}
}
min_idx
}
fn admit(&mut self, sequence: ChoiceSequence, score: f64) -> bool {
if self.entries.len() < CORPUS_SIZE {
self.entries.push(CorpusEntry { sequence, score });
return true;
}
let min_idx = self.min_index();
if score > self.entries[min_idx].score {
self.entries[min_idx] = CorpusEntry { sequence, score };
return true;
}
false
}
fn lowest(&self) -> &CorpusEntry {
&self.entries[self.min_index()]
}
}
struct TargetedSearch {
corpus: Corpus,
prng: XoshiroState,
}
impl TargetedSearch {
fn new(seed: u64) -> Self {
Self {
corpus: Corpus::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 mut sequence = self.pick().sequence.clone();
mutate_sequence(&mut sequence, &mut self.prng);
TestCaseContext::exploring(sequence, explore_seed)
}
}
fn pick(&mut self) -> &CorpusEntry {
if self.prng.sample_below(LOW_SCORE_DENOM) == 0 {
self.corpus.lowest()
} else {
let idx = self.prng.sample_below(self.corpus.entries.len() as u64) as usize;
&self.corpus.entries[idx]
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CorpusPolicy {
SemanticOnly,
SemanticWithPriority,
}
struct SemanticEntry {
sequence: ChoiceSequence,
novel: Vec<Feature>,
group: Vec<Feature>,
score: Option<f64>,
}
struct SemanticCorpus {
policy: CorpusPolicy,
accepted: Vec<SemanticEntry>,
rejected: Vec<SemanticEntry>,
observed: Vec<Feature>,
}
impl SemanticCorpus {
fn new(policy: CorpusPolicy) -> Self {
Self {
policy,
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>,
score: Option<f64>,
) -> bool {
let novel = self.novel_features(&case_features);
let registered = self.register(&novel);
if !registered.is_empty() {
self.accepted.push(SemanticEntry {
sequence,
novel: registered,
group: case_features,
score,
});
self.evict_if_over_capacity();
return true;
}
if self.policy == CorpusPolicy::SemanticWithPriority
&& let (Some(score), Some(victim)) = (score, self.group_lowest(&case_features))
&& Self::score_beats(Some(score), self.accepted[victim].score)
{
self.accepted[victim] = SemanticEntry {
sequence,
novel: Vec::new(),
group: case_features,
score: Some(score),
};
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,
group: case_features,
score: None,
});
self.evict_if_over_capacity();
true
}
fn group_lowest(&self, case_features: &[Feature]) -> Option<usize> {
let mut best: Option<usize> = None;
for (i, entry) in self.accepted.iter().enumerate() {
if !entry.group.iter().any(|f| case_features.contains(f)) {
continue;
}
match best {
None => best = Some(i),
Some(b) => {
if Self::score_beats(self.accepted[b].score, entry.score) {
best = Some(i);
}
}
}
}
best
}
fn score_beats(a: Option<f64>, b: Option<f64>) -> bool {
match (a, b) {
(Some(a), Some(b)) => a > b,
(Some(_), None) => true,
(None, _) => false,
}
}
fn weakest_accepted(&self) -> usize {
let mut min = 0;
for (i, entry) in self.accepted.iter().enumerate().skip(1) {
let incumbent = self.accepted[min].score;
if Self::score_beats(incumbent, entry.score) {
min = i;
}
}
min
}
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, _)) => {
let weaker = if entry.novel.len() != incumbent.novel.len() {
entry.novel.len() < incumbent.novel.len()
} else if self.policy == CorpusPolicy::SemanticWithPriority {
Self::score_beats(incumbent.score, entry.score)
} else {
false
};
if weaker {
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, policy: CorpusPolicy) -> Self {
Self {
corpus: SemanticCorpus::new(policy),
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 low = self.prng.sample_below(LOW_SCORE_DENOM) == 0;
let idx = if low {
self.corpus.weakest_accepted()
} else {
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::*;
#[test]
fn corpus_admits_everything_until_full() {
let mut corpus = Corpus::new();
for i in 0..CORPUS_SIZE {
assert!(
corpus.admit(ChoiceSequence::default(), i as f64),
"entry {i} must be admitted while below capacity"
);
}
assert_eq!(corpus.entries.len(), CORPUS_SIZE);
}
#[test]
fn corpus_replaces_lowest_score_when_full() {
let mut corpus = Corpus::new();
for i in 0..CORPUS_SIZE {
corpus.admit(ChoiceSequence::default(), i as f64);
}
assert!(
corpus.admit(ChoiceSequence::default(), CORPUS_SIZE as f64),
"a new best score must be admitted"
);
let scores: Vec<f64> = corpus.entries.iter().map(|e| e.score).collect();
assert!(scores.contains(&(CORPUS_SIZE as f64)));
assert!(
!scores.contains(&0.0),
"the lowest score must have been evicted: {scores:?}"
);
}
#[test]
fn corpus_keeps_incumbent_on_tie_when_full() {
let mut corpus = Corpus::new();
for i in 0..CORPUS_SIZE {
corpus.admit(ChoiceSequence::default(), i as f64);
}
assert!(!corpus.admit(ChoiceSequence::default(), 0.0));
let scores: Vec<f64> = corpus.entries.iter().map(|e| e.score).collect();
assert!(scores.contains(&0.0), "incumbent must survive: {scores:?}");
}
#[test]
fn corpus_rejects_score_not_beating_lowest() {
let mut corpus = Corpus::new();
for i in 0..CORPUS_SIZE {
corpus.admit(ChoiceSequence::default(), i as f64);
}
assert!(
!corpus.admit(ChoiceSequence::default(), -1.0),
"a score below the lowest must be rejected"
);
assert_eq!(corpus.entries.len(), CORPUS_SIZE);
}
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(CorpusPolicy::SemanticWithPriority);
assert!(
corpus.admit_accepted(ChoiceSequence::default(), vec![event_feature("a")], None),
"a case with a novel feature must be admitted"
);
assert!(
!corpus.admit_accepted(ChoiceSequence::default(), vec![event_feature("a")], None),
"a case with only known features must not be admitted without priority"
);
assert_eq!(corpus.accepted.len(), 1);
}
#[test]
fn semantic_only_policy_ignores_priority() {
let mut corpus = SemanticCorpus::new(CorpusPolicy::SemanticOnly);
assert!(
corpus.admit_accepted(
ChoiceSequence::default(),
vec![event_feature("a")],
Some(100.0)
),
"a novel feature must still be admitted"
);
assert!(
!corpus.admit_accepted(
ChoiceSequence::default(),
vec![event_feature("a")],
Some(200.0)
),
"SemanticOnly must not admit by priority"
);
assert_eq!(corpus.accepted.len(), 1);
assert_eq!(corpus.accepted[0].score, Some(100.0));
}
#[test]
fn semantic_corpus_admits_by_priority_within_group() {
let mut corpus = SemanticCorpus::new(CorpusPolicy::SemanticWithPriority);
corpus.admit_accepted(
ChoiceSequence::default(),
vec![event_feature("a")],
Some(1.0),
);
assert!(
corpus.admit_accepted(
ChoiceSequence::default(),
vec![event_feature("a")],
Some(2.0)
),
"a higher score in the same group must replace the group's lowest"
);
assert_eq!(corpus.accepted.len(), 1);
assert_eq!(corpus.accepted[0].score, Some(2.0));
assert!(
!corpus.admit_accepted(
ChoiceSequence::default(),
vec![event_feature("a")],
Some(1.5)
),
"a score below the group's current entry must not be admitted"
);
assert_eq!(corpus.accepted.len(), 1);
}
#[test]
fn semantic_corpus_group_lowest_replaces_lowest_not_highest() {
let mut corpus = SemanticCorpus::new(CorpusPolicy::SemanticWithPriority);
let a = event_feature("a");
let b = event_feature("b");
assert!(corpus.admit_accepted(ChoiceSequence::default(), vec![a.clone()], Some(1.0)));
assert!(corpus.admit_accepted(ChoiceSequence::default(), vec![a.clone(), b], Some(3.0)));
assert_eq!(corpus.accepted.len(), 2);
assert!(
corpus.admit_accepted(ChoiceSequence::default(), vec![a.clone()], Some(2.0)),
"a mid-score case must replace the group's lowest-scored member"
);
assert_eq!(corpus.accepted.len(), 2);
assert_eq!(
corpus.accepted[0].score,
Some(2.0),
"the lowest-scored member (1.0) must have been replaced: {:?}",
corpus.accepted.iter().map(|e| e.score).collect::<Vec<_>>()
);
assert_eq!(corpus.accepted[1].score, Some(3.0));
assert_eq!(corpus.accepted[0].group, vec![a]);
}
#[test]
fn semantic_corpus_missing_score_is_group_lowest() {
let mut corpus = SemanticCorpus::new(CorpusPolicy::SemanticWithPriority);
let a = event_feature("a");
let b = event_feature("b");
assert!(corpus.admit_accepted(ChoiceSequence::default(), vec![a.clone()], None));
assert!(corpus.admit_accepted(ChoiceSequence::default(), vec![a.clone(), b], Some(3.0)));
assert!(
corpus.admit_accepted(ChoiceSequence::default(), vec![a.clone()], Some(1.0)),
"a finite score must replace the missing-scored group member"
);
assert_eq!(corpus.accepted.len(), 2);
assert_eq!(corpus.accepted[0].score, Some(1.0));
assert_eq!(corpus.accepted[1].score, Some(3.0));
assert!(corpus.accepted.iter().all(|e| e.score.is_some()));
}
#[test]
fn score_beats_missing_counts_as_lowest() {
assert!(SemanticCorpus::score_beats(Some(1.0), None));
assert!(!SemanticCorpus::score_beats(None, Some(1.0)));
assert!(!SemanticCorpus::score_beats(None, None));
assert!(SemanticCorpus::score_beats(Some(2.0), Some(1.0)));
assert!(!SemanticCorpus::score_beats(Some(1.0), Some(2.0)));
assert!(
!SemanticCorpus::score_beats(Some(1.0), Some(1.0)),
"a tie must not beat the incumbent"
);
}
#[test]
fn semantic_corpus_register_stops_at_global_cap() {
let mut corpus = SemanticCorpus::new(CorpusPolicy::SemanticWithPriority);
for i in 0..MAX_GLOBAL_FEATURES {
assert!(corpus.admit_accepted(
ChoiceSequence::default(),
vec![bucket_feature("f", i as u64)],
Some(1.0),
));
}
assert_eq!(corpus.observed.len(), MAX_GLOBAL_FEATURES);
assert!(
!corpus.admit_accepted(
ChoiceSequence::default(),
vec![bucket_feature("f", MAX_GLOBAL_FEATURES as u64)],
Some(1.0),
),
"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(CorpusPolicy::SemanticWithPriority);
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],
None,
));
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(CorpusPolicy::SemanticWithPriority);
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(CorpusPolicy::SemanticWithPriority);
for i in 0..CORPUS_SIZE {
corpus.admit_accepted(
ChoiceSequence::default(),
vec![bucket_feature("single", i as u64)],
Some(i as f64),
);
}
assert_eq!(corpus.accepted.len(), CORPUS_SIZE);
corpus.admit_accepted(
ChoiceSequence::default(),
vec![event_feature("two-a"), event_feature("two-b")],
Some(1000.0),
);
assert_eq!(corpus.accepted.len(), CORPUS_SIZE);
assert!(
corpus.accepted.iter().any(|e| e.score == Some(1000.0)),
"the feature-richest entry must survive eviction"
);
assert!(
!corpus.accepted.iter().any(|e| e.score == Some(0.0)),
"the weakest single-feature entry must be evicted"
);
}
#[test]
fn semantic_corpus_evicts_across_both_queues() {
let mut corpus = SemanticCorpus::new(CorpusPolicy::SemanticWithPriority);
for i in 0..CORPUS_SIZE {
corpus.admit_accepted(
ChoiceSequence::default(),
vec![bucket_feature("accepted", i as u64)],
Some(i as f64),
);
}
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!(
corpus.rejected.is_empty(),
"the rejected entry must be the weakest across both queues"
);
}
#[test]
fn semantic_corpus_unscored_novel_case_evicts_itself_when_full_of_scores() {
let mut corpus = SemanticCorpus::new(CorpusPolicy::SemanticWithPriority);
for i in 0..CORPUS_SIZE {
corpus.admit_accepted(
ChoiceSequence::default(),
vec![bucket_feature("scored", i as u64)],
Some(i as f64),
);
}
assert_eq!(corpus.accepted.len(), CORPUS_SIZE);
let novel = event_feature("unscored-novel");
assert!(
corpus.admit_accepted(ChoiceSequence::default(), vec![novel.clone()], None),
"the novel feature must still be registered"
);
assert_eq!(corpus.accepted.len(), CORPUS_SIZE);
assert!(
!corpus.accepted.iter().any(|e| e.novel.contains(&novel)),
"the unscored novel entry must evict itself: {:?}",
corpus
.accepted
.iter()
.map(|e| e.novel.clone())
.collect::<Vec<_>>()
);
assert!(
corpus.observed.contains(&novel),
"the feature must remain registered globally"
);
}
#[test]
fn semantic_corpus_unscored_novel_case_survives_when_corpus_unscored() {
let mut corpus = SemanticCorpus::new(CorpusPolicy::SemanticWithPriority);
for i in 0..CORPUS_SIZE {
corpus.admit_accepted(
ChoiceSequence::default(),
vec![bucket_feature("unscored", i as u64)],
None,
);
}
assert_eq!(corpus.accepted.len(), CORPUS_SIZE);
let novel = event_feature("survivor");
assert!(corpus.admit_accepted(ChoiceSequence::default(), vec![novel.clone()], None));
assert_eq!(corpus.accepted.len(), CORPUS_SIZE);
assert!(
corpus.accepted.iter().any(|e| e.novel.contains(&novel)),
"the new unscored entry must survive FIFO eviction"
);
assert!(
!corpus
.accepted
.iter()
.any(|e| e.novel.contains(&bucket_feature("unscored", 0))),
"the earliest arrival must be evicted"
);
}
#[test]
fn corpus_guided_search_replays_accepted_entries() {
let mut search = CorpusGuidedSearch::new(42, CorpusPolicy::SemanticWithPriority);
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")], Some(1.0));
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 semantic_corpus_weakest_accepted_returns_lowest_score() {
let mut corpus = SemanticCorpus::new(CorpusPolicy::SemanticWithPriority);
corpus.admit_accepted(
ChoiceSequence::default(),
vec![event_feature("a")],
Some(3.0),
);
corpus.admit_accepted(
ChoiceSequence::default(),
vec![event_feature("b")],
Some(1.0),
);
assert_eq!(
corpus.weakest_accepted(),
1,
"the lowest score must be picked"
);
corpus.admit_accepted(ChoiceSequence::default(), vec![event_feature("c")], None);
assert_eq!(
corpus.weakest_accepted(),
2,
"missing must count as the lowest"
);
corpus.admit_accepted(
ChoiceSequence::default(),
vec![event_feature("d")],
Some(1.0),
);
assert_eq!(
corpus.weakest_accepted(),
2,
"a tie must keep the earlier arrival"
);
}
#[test]
fn corpus_guided_search_picks_from_rejected_queue_when_accepted_empty() {
let mut search = CorpusGuidedSearch::new(7, CorpusPolicy::SemanticWithPriority);
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 targeted_search_evolves_corpus_entries() {
let mut search = TargetedSearch::new(42);
let mut seq = ChoiceSequence::default();
seq.push_draw(
7u64.to_le_bytes().to_vec(),
ChoiceMeta::Bounded { bound: 10 },
);
search.corpus.admit(seq, 1.0);
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 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_and_targeted() {
let mut runner = Runner::new(1, 4);
runner
.run(|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 mut runner = Runner::new(1, 4);
runner
.run_targeted(|ctx| {
crate::sample_u32(ctx);
ctx.maximize(0.5);
Ok(())
})
.unwrap();
let stats = runner.stats();
assert_eq!(stats.discovered_features, 0);
assert_eq!(stats.max_corpus_size, 0);
let err = Runner::new(1, 4)
.run(|_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, 4)
.run_targeted(|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, 4);
runner
.run_corpus_guided_with_policy(CorpusPolicy::SemanticOnly, |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, 100);
runner
.run_corpus_guided_with_policy(CorpusPolicy::SemanticOnly, |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, 103);
let err = runner
.run_corpus_guided_with_policy(CorpusPolicy::SemanticOnly, |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_iterations, 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, CorpusPolicy::SemanticOnly);
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], None)
);
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, 8);
runner
.run_corpus_guided_with_policy(CorpusPolicy::SemanticOnly, |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_iterations, 1);
assert_eq!(stats.discovered_features, 1);
assert_eq!(stats.max_corpus_size, 1);
}
#[test]
fn stats_corpus_fields_zero_iterations() {
let mut runner = Runner::new(1, 0);
runner
.run_corpus_guided_with_policy(CorpusPolicy::SemanticOnly, |_ctx| {
panic!("closure must not be invoked with zero iterations");
})
.expect("zero iterations must succeed");
assert_eq!(runner.stats(), Stats::default());
}