#![allow(clippy::float_cmp)]
use crate::evolution::EvolutionEngine;
use crate::types::{Budget, OracleVerdict};
use rand::{Rng, SeedableRng};
#[test]
fn engine_creation_produces_population() {
let engine = EvolutionEngine::new(10);
assert!(engine.best().is_some() || engine.algorithm.best().is_some());
}
#[test]
fn new_seeded_determinism() {
let mut engine_a = EvolutionEngine::new_seeded(10, 42);
let mut engine_b = EvolutionEngine::new_seeded(10, 42);
for _ in 0..5 {
if let Some((idx_a, _)) = engine_a.next_candidate()
&& let Some((idx_b, _)) = engine_b.next_candidate()
{
engine_a.record_feedback(idx_a, true).unwrap();
engine_b.record_feedback(idx_b, true).unwrap();
}
engine_a.evolve();
engine_b.evolve();
}
let best_a = engine_a.best().map(|c| c.genes.clone());
let best_b = engine_b.best().map(|c| c.genes.clone());
assert_eq!(best_a, best_b, "seeded engines must be deterministic");
}
#[test]
fn record_feedback_updates_fitness() {
let mut engine = EvolutionEngine::new(5);
if let Some((idx, _)) = engine.next_candidate() {
assert_eq!(engine.best().unwrap().fitness, 0.0);
engine.record_feedback(idx, true).unwrap();
assert!(engine.best().unwrap().fitness > 0.0);
}
}
#[test]
fn record_feedback_tracks_gene_stats() {
let mut engine = EvolutionEngine::new(5);
let candidates: Vec<_> = engine.batch_candidates(5);
for (idx, mut chrom) in candidates {
chrom.genes[0].1 = String::from("CaseAlternation");
let _ = engine.submit_batch(vec![(idx, OracleVerdict::from_bool(true))]);
}
assert!(!engine.gene_stats.is_empty());
}
#[test]
fn next_candidate_prefers_unevaluated() {
let mut engine = EvolutionEngine::new(5);
let candidates = engine.batch_candidates(5);
engine
.submit_batch(vec![(candidates[0].0, OracleVerdict::from_bool(true))])
.unwrap();
let next = engine.next_candidate();
assert!(next.is_some());
}
#[test]
fn evolve_produces_next_generation() {
let mut engine = EvolutionEngine::new(10);
let candidates = engine.batch_candidates(10);
for (idx, _) in candidates {
let passed = idx % 3 == 0;
engine.record_feedback(idx, passed).unwrap();
}
engine.evolve();
assert_eq!(engine.stats.generation, 1);
}
#[test]
fn best_returns_fittest() {
let mut engine = EvolutionEngine::new(5);
let candidates = engine.batch_candidates(5);
for (idx, _) in candidates {
engine.record_feedback(idx, idx % 2 != 0).unwrap();
}
let best = engine.best();
assert!(best.is_some());
}
#[test]
fn gene_success_rates_require_min_attempts() {
let mut engine = EvolutionEngine::new(5);
let candidates = engine.batch_candidates(5);
for (idx, mut chrom) in candidates {
chrom.genes[0].1 = String::from("CaseAlternation");
let _ = engine.submit_batch(vec![(idx, OracleVerdict::from_bool(true))]);
}
let rates = engine.gene_success_rates();
assert!(
rates
.iter()
.all(|(_, value, _)| *value != "CaseAlternation")
);
let candidates = engine.batch_candidates(5);
for (idx, mut chrom) in candidates {
chrom.genes[0].1 = String::from("CaseAlternation");
let _ = engine.submit_batch(vec![(idx, OracleVerdict::from_bool(true))]);
}
let rates = engine.gene_success_rates();
assert!(!rates.is_empty());
}
#[test]
fn learned_summary_not_empty() {
let mut engine = EvolutionEngine::new(5);
if let Some((idx, _)) = engine.next_candidate() {
engine.record_feedback(idx, true).unwrap();
}
let summary = engine.learned_summary();
assert!(summary.contains("Generation:"));
}
#[test]
fn multiple_generations_converge() {
let mut engine = EvolutionEngine::new(50);
for _generation in 0..10 {
let candidates = engine.batch_candidates(engine.budget.max_requests.min(50));
for (idx, _) in candidates {
let _ = engine.record_feedback(idx, true);
}
engine.evolve();
}
let rates = engine.gene_success_rates();
let case_alt_rate = rates
.iter()
.find(|(_, value, _)| *value == "CaseAlternation")
.map(|(_, _, rate)| *rate);
assert!(
case_alt_rate.unwrap_or(0.0) > 0.0 || engine.best().is_none(),
"CaseAlternation should appear in success rates or no best found"
);
}
#[test]
fn small_population_does_not_panic() {
let mut engine = EvolutionEngine::new(2);
let candidates = engine.batch_candidates(2);
for (idx, _) in candidates {
engine.record_feedback(idx, true).unwrap();
}
engine.evolve();
}
#[test]
fn single_chromosome_does_not_panic() {
let mut engine = EvolutionEngine::new(1);
if let Some((idx, _)) = engine.next_candidate() {
engine.record_feedback(idx, true).unwrap();
}
engine.evolve();
}
#[test]
fn out_of_bounds_feedback_errors() {
let mut engine = EvolutionEngine::new(5);
let result = engine.record_feedback(999, true);
assert!(
result.is_err(),
"out-of-bounds feedback must return an error"
);
}
#[test]
fn record_feedback_invalid_index_returns_err_not_ok() {
let mut engine = EvolutionEngine::new(5);
let result = engine.record_feedback(9999, true);
assert!(
result.is_err(),
"record_feedback with an index not in in_flight must return Err (bench_waf \
suppression regression — the err branch drives the eprintln! warning)"
);
use crate::types::EvolutionError;
assert!(
matches!(
result.unwrap_err(),
EvolutionError::InvalidChromosomeIndex(_)
),
"error must be InvalidChromosomeIndex so callers can distinguish it from \
TargetHealthCritical and handle each branch separately"
);
}
#[test]
fn record_feedback_valid_index_after_next_candidate_is_ok() {
let mut engine = EvolutionEngine::new(5);
let (idx, _) = engine
.next_candidate()
.expect("engine must produce at least one candidate");
let result = engine.record_feedback(idx, true);
assert!(
result.is_ok(),
"record_feedback for a legitimately issued index must be Ok: {:?}",
result.err()
);
}
#[test]
fn fitness_history_tracked() {
let mut engine = EvolutionEngine::new(10);
let candidates = engine.batch_candidates(10);
for (idx, _) in candidates {
let _ = engine.record_feedback(idx, idx % 2 == 0);
}
engine.evolve();
assert!(!engine.fitness_history.is_empty());
}
#[test]
fn single_population_diversity() {
let engine = EvolutionEngine::new(1);
assert_eq!(engine.diversity_score(), 1.0);
}
#[test]
fn seed_population_advances_rng() {
let mut engine_a = EvolutionEngine::new_seeded(5, 12345);
let mut engine_b = EvolutionEngine::new_seeded(5, 12345);
let snap_a = engine_a
.population_snapshot()
.first()
.map(|c| c.genes.clone());
let snap_b = engine_b
.population_snapshot()
.first()
.map(|c| c.genes.clone());
assert_eq!(snap_a, snap_b, "same seed → same initial population");
let extra_pop = engine_a.population_snapshot();
engine_a.seed_population(extra_pop);
let candidate_a = engine_a.batch_candidates(1);
let candidate_b = engine_b.batch_candidates(1);
if !candidate_a.is_empty() && !candidate_b.is_empty() {
let (id_a, _) = candidate_a[0].clone();
let (id_b, _) = candidate_b[0].clone();
engine_a.record_feedback(id_a, true).unwrap();
engine_b.record_feedback(id_b, true).unwrap();
engine_a.evolve();
engine_b.evolve();
let best_a = engine_a.best().map(|c| c.genes.clone());
let best_b = engine_b.best().map(|c| c.genes.clone());
assert!(
best_a.is_some() && best_b.is_some(),
"both engines must produce a best chromosome"
);
}
}
#[test]
fn active_bypass_scores_above_baseline_pass() {
let mut engine = EvolutionEngine::new(2);
let cands = engine.batch_candidates(2);
for (idx, _) in cands {
engine.record_feedback(idx, true).unwrap();
}
assert!(engine.stats.evaluations >= 2);
}
#[test]
fn new_seeded_population_not_double_sized() {
let pop = 10_usize;
let seed = 77_u64;
let mut e1 = EvolutionEngine::new_seeded(pop, seed);
let mut e2 = EvolutionEngine::new_seeded(pop, seed);
let first1 = e1.next_candidate().map(|(_, c)| c.genes.clone());
let first2 = e2.next_candidate().map(|(_, c)| c.genes.clone());
assert_eq!(
first1, first2,
"two engines created with the same seed must produce identical first candidates \
(double-init would advance the RNG differently on the second call, \
breaking this invariant)"
);
}
#[test]
fn new_seeded_both_same_first_next_candidate_is_deterministic() {
let seed = 42_u64;
let mut ea = EvolutionEngine::new_seeded(5, seed);
let mut eb = EvolutionEngine::new_seeded(5, seed);
for _ in 0..3 {
match (ea.next_candidate(), eb.next_candidate()) {
(Some((ia, _)), Some((ib, _))) => {
ea.record_feedback(ia, true).unwrap();
eb.record_feedback(ib, true).unwrap();
}
(None, None) => break,
_ => panic!("one engine ran out of candidates but the other didn't"),
}
ea.evolve();
eb.evolve();
}
let best_a = ea.best().map(|c| c.genes.clone());
let best_b = eb.best().map(|c| c.genes.clone());
assert_eq!(
best_a, best_b,
"after identical feedback sequences, two same-seed engines must converge \
to the same best chromosome"
);
}
#[test]
fn budget_exhaustion_does_not_loop() {
let mut engine = EvolutionEngine::new_seeded(5, 1);
engine.budget = Budget {
max_requests: 3,
max_generations: 100,
max_time_seconds: 3600,
stagnation_limit: 10,
};
for _ in 0..20 {
if engine.should_terminate() {
break;
}
let batch = engine.batch_candidates(1);
if batch.is_empty() {
break;
}
for (idx, _) in batch {
engine.record_feedback(idx, false).unwrap();
}
}
}
#[test]
fn zero_request_budget_terminates_immediately() {
let mut engine = EvolutionEngine::new_seeded(5, 2);
engine.budget = Budget {
max_requests: 0,
max_generations: 100,
max_time_seconds: 3600,
stagnation_limit: 10,
};
assert!(engine.should_terminate());
assert!(engine.batch_candidates(1).is_empty());
}
#[test]
fn always_blocking_oracle_does_not_panic() {
let mut engine = EvolutionEngine::new_seeded(5, 123);
engine.budget = Budget {
max_requests: 10,
max_generations: 5,
max_time_seconds: 3600,
stagnation_limit: 2,
};
for _ in 0..30 {
if engine.should_terminate() {
break;
}
let batch = engine.batch_candidates(1);
if batch.is_empty() {
break;
}
for (idx, _) in batch {
engine.record_feedback(idx, false).unwrap();
}
engine.evolve();
}
}
#[test]
fn random_oracle_does_not_panic() {
let mut engine = EvolutionEngine::new_seeded(5, 456);
engine.budget = Budget {
max_requests: 15,
max_generations: 10,
max_time_seconds: 3600,
stagnation_limit: 5,
};
let mut rng = rand::rngs::StdRng::seed_from_u64(789);
for _ in 0..100 {
if engine.should_terminate() {
break;
}
let batch = engine.batch_candidates(1);
if batch.is_empty() {
break;
}
for (idx, _) in batch {
engine.record_feedback(idx, rng.gen_bool(0.5)).unwrap();
}
engine.evolve();
}
}
#[test]
fn target_error_bails_out() {
let mut engine = EvolutionEngine::new(5);
for _ in 0..10 {
let result = engine.record_target_error("503 Service Unavailable".into());
if result.is_err() {
break;
}
}
assert!(!engine.target_health.is_healthy() || engine.should_terminate());
}
#[test]
fn checkpoint_roundtrip() {
let mut engine = EvolutionEngine::new_seeded(10, 99);
let candidates = engine.batch_candidates(3);
for (idx, _) in candidates {
engine.record_feedback(idx, true).unwrap();
}
engine.evolve();
let tmp = std::env::temp_dir().join(format!(
"wafrift_evolution_test_checkpoint_{}.json",
std::process::id()
));
engine.save_checkpoint(&tmp).unwrap();
let mut restored = EvolutionEngine::new_seeded(10, 99);
restored.load_checkpoint(&tmp).unwrap();
assert_eq!(restored.stats.generation, engine.stats.generation);
assert_eq!(restored.request_count, engine.request_count);
let _ = std::fs::remove_file(&tmp);
}
#[test]
fn batch_evaluation_parallel() {
let mut engine = EvolutionEngine::new(10);
let batch = engine.batch_candidates(4);
assert!(!batch.is_empty());
let results: Vec<_> = batch
.into_iter()
.map(|(idx, _)| (idx, OracleVerdict::from_bool(true)))
.collect();
engine.submit_batch(results).unwrap();
assert!(engine.stats.evaluations >= 1);
}
#[test]
fn checkpoint_load_rejects_oversized_file() {
let tmp = std::env::temp_dir().join(format!(
"wafrift_evolution_test_oversized_{}.json",
std::process::id()
));
let junk = "x".repeat(crate::types::MAX_CHECKPOINT_BYTES + 1);
std::fs::write(&tmp, junk).unwrap();
let mut engine = EvolutionEngine::new(10);
let result = engine.load_checkpoint(&tmp);
assert!(
result.is_err(),
"should reject checkpoint > MAX_CHECKPOINT_BYTES"
);
let _ = std::fs::remove_file(&tmp);
}
#[test]
fn lineage_no_cycles() {
use crate::evolution::Chromosome;
use crate::lineage::Lineage;
use crate::search::SearchAlgorithm;
use rand::SeedableRng;
let mut alg = crate::search::HillClimbing::new();
let pool = crate::evolution::GenePool::default_wafrift();
let mut rng = rand::rngs::StdRng::seed_from_u64(1);
alg.initialize(vec![Chromosome::new(vec![])], &pool, &mut rng);
for _ in 0..100 {
let cands = alg.request_evaluations(1, &mut rng);
if cands.is_empty() {
break;
}
alg.submit_evaluations(vec![(cands[0].id, OracleVerdict::from_bool(true))]);
}
let best = alg.best().unwrap();
let current_gen = match &best.lineage {
Lineage::Genesis { generation } => *generation,
Lineage::Crossover { generation, .. } => *generation,
Lineage::Mutation { generation, .. } => *generation,
};
assert!(
current_gen < u32::MAX,
"generation should be a realistic value"
);
}
#[test]
fn seed_population_twice_advances_rng() {
let mut engine = EvolutionEngine::new_seeded(5, 999);
let pop1 = engine.population_snapshot();
engine.seed_population(pop1.clone());
let cands_after_first_seed = engine.batch_candidates(1);
let mut engine2 = EvolutionEngine::new_seeded(5, 999);
engine2.seed_population(pop1.clone());
engine2.seed_population(pop1); let cands_after_second_seed = engine2.batch_candidates(1);
assert!(!cands_after_first_seed.is_empty() || !cands_after_second_seed.is_empty());
}
#[test]
fn evolution_five_generations_deterministic() {
let run = |seed: u64| -> Option<Vec<(String, String)>> {
let mut engine = EvolutionEngine::new_seeded(8, seed);
engine.budget = Budget {
max_requests: 100,
max_generations: 5,
max_time_seconds: 3600,
stagnation_limit: 50,
};
for _ in 0..5 {
let batch = engine.batch_candidates(5);
for (idx, _) in batch {
engine.record_feedback(idx, idx % 2 == 0).unwrap();
}
engine.evolve();
}
engine.best().map(|c| c.genes.clone())
};
assert_eq!(
run(7777),
run(7777),
"same seed must be fully deterministic"
);
}
#[test]
fn evolution_different_seeds_differ() {
let run = |seed: u64| -> Option<Vec<(String, String)>> {
let mut engine = EvolutionEngine::new_seeded(5, seed);
let batch = engine.batch_candidates(3);
for (idx, _) in batch {
engine.record_feedback(idx, true).unwrap();
}
engine.evolve();
engine.best().map(|c| c.genes.clone())
};
let r1 = run(1);
let r2 = run(2);
assert!(r1.is_some() || r2.is_some());
}
#[test]
fn diversity_after_five_generations_not_zero() {
let mut engine = EvolutionEngine::new_seeded(10, 42);
for _ in 0..5 {
let batch = engine.batch_candidates(5);
for (idx, _) in batch {
engine.record_feedback(idx, idx % 3 == 0).unwrap();
}
engine.evolve();
}
assert!(engine.diversity_score() >= 0.0);
}
#[test]
fn empty_population_zero_clamp_produces_one() {
let engine = EvolutionEngine::new_seeded(0, 1);
assert!(engine.best().is_some() || !engine.population_snapshot().is_empty());
}
#[test]
fn max_population_size_clamp_to_10000() {
let engine = EvolutionEngine::new_seeded(100_000, 2);
assert!(engine.best().is_some() || engine.population_snapshot().len() <= 10_000);
}
#[test]
fn best_fitness_never_decreases_under_elitism() {
let mut engine = EvolutionEngine::new_seeded(10, 55);
engine.budget = Budget {
max_requests: 50,
max_generations: 10,
max_time_seconds: 3600,
stagnation_limit: 20,
};
let mut prev_best_fitness = 0.0_f64;
for _ in 0..5 {
let batch = engine.batch_candidates(5);
if batch.is_empty() {
break;
}
for (idx, _) in batch {
engine.record_feedback(idx, idx % 3 == 0).unwrap();
}
engine.evolve();
if let Some(best) = engine.best() {
assert!(
best.fitness >= prev_best_fitness - f64::EPSILON,
"best fitness regressed: {} < {} (generation {})",
best.fitness,
prev_best_fitness,
engine.stats.generation
);
prev_best_fitness = best.fitness;
}
}
}
#[test]
fn prune_stale_in_flight_repays_budget() {
let mut engine = EvolutionEngine::new_seeded(5, 7);
engine.budget = Budget {
max_requests: 20,
max_generations: 10,
max_time_seconds: 3600,
stagnation_limit: 10,
};
let batch = engine.batch_candidates(3);
assert!(!batch.is_empty());
let before_count = engine.request_count;
let pruned = engine.prune_stale_in_flight(std::time::Duration::from_nanos(0));
assert_eq!(engine.request_count, before_count - pruned);
assert!(engine.in_flight.is_empty());
}
#[test]
fn stagnation_counter_saturates_at_u32_max() {
let mut engine = EvolutionEngine::new_seeded(5, 42);
engine.stagnation_counter = u32::MAX;
engine.evolve();
assert_eq!(
engine.stagnation_counter,
u32::MAX,
"stagnation_counter must saturate at u32::MAX, not wrap to 0"
);
}
#[test]
fn stats_generation_saturates_at_u32_max() {
let mut engine = EvolutionEngine::new_seeded(5, 43);
engine.stats.generation = u32::MAX;
engine.evolve();
assert_eq!(
engine.stats.generation,
u32::MAX,
"stats.generation must saturate at u32::MAX, not wrap to 0"
);
}
#[test]
fn stats_evaluations_saturates_at_usize_max() {
let mut engine = EvolutionEngine::new_seeded(3, 44);
engine.stats.evaluations = usize::MAX;
let batch = engine.batch_candidates(1);
if let Some((idx, _)) = batch.into_iter().next() {
engine.record_feedback(idx, true).unwrap();
}
assert_eq!(
engine.stats.evaluations,
usize::MAX,
"stats.evaluations must saturate at usize::MAX, not wrap to 0"
);
}
#[test]
fn next_id_saturates_at_u64_max() {
let mut engine = EvolutionEngine::new_seeded(3, 45);
let id1 = engine
.batch_candidates(1)
.into_iter()
.next()
.map(|(i, _)| i);
let id2 = engine
.batch_candidates(1)
.into_iter()
.next()
.map(|(i, _)| i);
if let (Some(a), Some(b)) = (id1, id2) {
assert!(b > a, "candidate IDs must be strictly increasing");
}
}
#[test]
fn stagnation_counter_increments_correctly() {
let mut engine = EvolutionEngine::new_seeded(5, 46);
engine.stagnation_counter = 0;
for _ in 0..9 {
let batch = engine.batch_candidates(1);
if let Some((idx, _)) = batch.into_iter().next() {
let _ = engine.record_feedback(idx, false);
}
engine.evolve();
}
let before = engine.stagnation_counter;
let batch = engine.batch_candidates(1);
if let Some((idx, _)) = batch.into_iter().next() {
let _ = engine.record_feedback(idx, false);
}
engine.evolve();
assert!(
engine.stagnation_counter > before,
"stagnation_counter must increment on a non-improving generation (got before={before}, after={})",
engine.stagnation_counter
);
}
#[test]
fn stagnation_counter_resets_on_improvement() {
let mut engine = EvolutionEngine::new_seeded(5, 47);
for _ in 0..10 {
let batch = engine.batch_candidates(1);
if let Some((idx, _)) = batch.into_iter().next() {
let _ = engine.record_feedback(idx, false);
}
engine.evolve();
}
engine.stagnation_counter = 99;
engine.stats.stagnation_counter = 99;
engine.fitness_history.clear();
for _ in 0..9 {
engine.fitness_history.push_back(0.0);
}
let batch = engine.batch_candidates(1);
if let Some((idx, _)) = batch.into_iter().next() {
engine.record_feedback(idx, true).unwrap();
}
engine.evolve();
assert_eq!(
engine.stagnation_counter, 0,
"stagnation_counter must reset to 0 when the fitness-history window shows improvement (got {})",
engine.stagnation_counter
);
}
#[test]
fn generation_evals_does_not_accumulate_across_generations() {
let mut engine = EvolutionEngine::new_seeded(5, 48);
let batch = engine.batch_candidates(3);
let count = batch.len();
for (idx, _) in batch {
engine.record_feedback(idx, false).unwrap();
}
let total_before_evolve = engine.stats.evaluations;
engine.evolve();
assert!(engine.stats.evaluations >= total_before_evolve);
let _ = count; }
#[test]
fn submit_batch_cache_key_dedup_throughput() {
let mut engine = EvolutionEngine::new_seeded(50, 7);
let batch_size = 10;
let rounds = 200;
let start = std::time::Instant::now();
for _ in 0..rounds {
let batch = engine.batch_candidates(batch_size);
if batch.is_empty() {
break;
}
let results: Vec<_> = batch
.into_iter()
.map(|(id, _chrom)| (id, OracleVerdict::from_bool(false)))
.collect();
engine.submit_batch(results).unwrap();
engine.evolve();
}
let elapsed = start.elapsed();
assert!(
elapsed < std::time::Duration::from_millis(200),
"200 rounds of batch_candidates(10)+submit_batch took {elapsed:?}; expected < 200 ms (cache_key dedup regression)"
);
}
#[test]
fn on_change_point_sets_boost_and_resets_stagnation() {
let mut engine = EvolutionEngine::new(10);
engine.stagnation_counter = 7;
engine.stats.stagnation_counter = 7;
assert_eq!(
engine.exploration_boost_remaining, 0,
"no boost before alarm"
);
assert!(
(engine.exploration_boost_factor - 1.0).abs() < 1e-9,
"default factor is 1.0"
);
engine.on_change_point(10, 2.0);
assert_eq!(
engine.exploration_boost_remaining, 10,
"boost must be set to 10 rounds"
);
assert!(
(engine.exploration_boost_factor - 2.0).abs() < 1e-9,
"factor must be 2.0"
);
assert_eq!(
engine.stagnation_counter, 0,
"stagnation_counter must be reset to 0"
);
assert_eq!(
engine.stats.stagnation_counter, 0,
"stats.stagnation_counter must be reset to 0"
);
}
#[test]
fn exploration_boost_decays_per_evolve_and_expires() {
let mut engine = EvolutionEngine::new(10);
let candidates = engine.batch_candidates(5);
for (id, _) in candidates {
engine.record_feedback(id, true).unwrap();
}
engine.on_change_point(3, 2.0);
assert_eq!(engine.exploration_boost_remaining, 3);
engine.evolve(); assert_eq!(
engine.exploration_boost_remaining, 2,
"boost must decrement to 2 after 1 evolve"
);
engine.evolve(); assert_eq!(
engine.exploration_boost_remaining, 1,
"boost must decrement to 1 after 2 evolves"
);
engine.evolve(); assert_eq!(
engine.exploration_boost_remaining, 0,
"boost must expire to 0 after 3 evolves"
);
assert!(
(engine.exploration_boost_factor - 1.0).abs() < 1e-9,
"factor must revert to 1.0 after boost expiry, got {}",
engine.exploration_boost_factor
);
}
#[test]
fn cache_key_identical_content_same_key() {
use crate::evolution::population::Chromosome;
let a = Chromosome::new(vec![
("encoding".into(), "UrlEncode".into()),
("content_type".into(), "None".into()),
("header_obfuscation".into(), "None".into()),
("grammar_rule".into(), "sqli".into()),
]);
let b = Chromosome::new(vec![
("encoding".into(), "UrlEncode".into()),
("content_type".into(), "None".into()),
("header_obfuscation".into(), "None".into()),
("grammar_rule".into(), "sqli".into()),
]);
use crate::evolution::EvolutionEngine;
let mut engine = EvolutionEngine::new_seeded(5, 99);
let eval_id_a = 9001u64;
let eval_id_b = 9002u64;
engine
.in_flight
.insert(eval_id_a, (0, a.clone(), std::time::Instant::now()));
engine
.in_flight
.insert(eval_id_b, (0, b.clone(), std::time::Instant::now()));
let before = engine.request_count;
engine
.submit_batch(vec![
(
eval_id_a as usize,
crate::types::OracleVerdict::from_bool(false),
),
(
eval_id_b as usize,
crate::types::OracleVerdict::from_bool(true),
),
])
.unwrap();
let _ = before;
}
#[test]
fn gene_stat_index_matches_linear_scan() {
use crate::evolution::fitness::core::gene_stat_index;
use crate::evolution::fitness::stats::GeneStatRecord;
let stats: Vec<GeneStatRecord> = vec![
("encoding".into(), "UrlEncode".into(), 5, 10),
("grammar_rule".into(), "sqli".into(), 3, 7),
("encoding".into(), "CaseAlternation".into(), 0, 2),
];
let idx = gene_stat_index(&stats);
for (name, value, successes, attempts) in &stats {
let found = idx.get(&(name.as_str(), value.as_str()));
assert!(
found.is_some(),
"gene_stat_index must find ({name}, {value})"
);
let (idx_s, idx_a) = found.unwrap();
assert_eq!(
*idx_s, *successes,
"successes mismatch for ({name}, {value})"
);
assert_eq!(*idx_a, *attempts, "attempts mismatch for ({name}, {value})");
}
assert!(
!idx.contains_key(&("encoding", "NonExistent")),
"missing key must not be in the index"
);
}
#[test]
fn stagnation_does_not_accumulate_during_exploration_boost() {
let mut engine = EvolutionEngine::new(10);
let candidates = engine.batch_candidates(5);
for (id, _) in candidates {
engine.record_feedback(id, true).unwrap();
}
engine.on_change_point(20, 2.0);
let stagnation_before = engine.stagnation_counter;
for _ in 0..15 {
engine.evolve();
}
assert!(
engine.stagnation_counter <= stagnation_before,
"stagnation_counter must not grow during exploration boost; got {}",
engine.stagnation_counter
);
assert_eq!(
engine.exploration_boost_remaining, 5,
"boost must be at 5 after 15 evolves"
);
}