1use crate::executors::{Executor, RawCallResult, invariant::execute_tx};
2use alloy_dyn_abi::JsonAbiExt;
3use alloy_json_abi::Function;
4use alloy_primitives::Bytes;
5use eyre::eyre;
6use foundry_config::FuzzCorpusConfig;
7use foundry_evm_fuzz::{
8 BasicTxDetails,
9 invariant::FuzzRunIdentifiedContracts,
10 strategies::{EvmFuzzState, mutate_param_value},
11};
12use proptest::{
13 prelude::{Just, Rng, Strategy},
14 prop_oneof,
15 strategy::{BoxedStrategy, ValueTree},
16 test_runner::TestRunner,
17};
18use serde::Serialize;
19use std::{
20 fmt,
21 path::PathBuf,
22 time::{SystemTime, UNIX_EPOCH},
23};
24use uuid::Uuid;
25
26const METADATA_SUFFIX: &str = "metadata.json";
27const JSON_EXTENSION: &str = ".json";
28const FAVORABILITY_THRESHOLD: f64 = 0.3;
29const COVERAGE_MAP_SIZE: usize = 65536;
30
31#[derive(Debug, Clone)]
33enum MutationType {
34 Splice,
36 Repeat,
38 Interleave,
40 Prefix,
42 Suffix,
44 Abi,
46}
47
48#[derive(Serialize)]
50struct CorpusEntry {
51 uuid: Uuid,
53 total_mutations: usize,
55 new_finds_produced: usize,
57 #[serde(skip_serializing)]
59 tx_seq: Vec<BasicTxDetails>,
60 is_favored: bool,
63}
64
65impl CorpusEntry {
66 pub fn new(tx_seq: Vec<BasicTxDetails>, path: PathBuf) -> eyre::Result<Self> {
68 let uuid = if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
69 Uuid::try_from(stem.strip_suffix(JSON_EXTENSION).unwrap_or(stem).to_string())?
70 } else {
71 Uuid::new_v4()
72 };
73 Ok(Self { uuid, total_mutations: 0, new_finds_produced: 0, tx_seq, is_favored: false })
74 }
75
76 pub fn from_tx_seq(tx_seq: &[BasicTxDetails]) -> Self {
78 Self {
79 uuid: Uuid::new_v4(),
80 total_mutations: 0,
81 new_finds_produced: 0,
82 tx_seq: tx_seq.into(),
83 is_favored: false,
84 }
85 }
86}
87
88#[derive(Serialize, Default)]
89pub(crate) struct CorpusMetrics {
90 cumulative_edges_seen: usize,
92 cumulative_features_seen: usize,
94 corpus_count: usize,
96 favored_items: usize,
98}
99
100impl fmt::Display for CorpusMetrics {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 writeln!(f)?;
103 writeln!(f, " - cumulative edges seen: {}", self.cumulative_edges_seen)?;
104 writeln!(f, " - cumulative features seen: {}", self.cumulative_features_seen)?;
105 writeln!(f, " - corpus count: {}", self.corpus_count)?;
106 write!(f, " - favored items: {}", self.favored_items)?;
107 Ok(())
108 }
109}
110
111impl CorpusMetrics {
112 pub fn update_seen(&mut self, is_edge: bool) {
114 if is_edge {
115 self.cumulative_edges_seen += 1;
116 } else {
117 self.cumulative_features_seen += 1;
118 }
119 }
120
121 pub fn update_favored(&mut self, is_favored: bool, corpus_favored: bool) {
123 if is_favored && !corpus_favored {
124 self.favored_items += 1;
125 } else if !is_favored && corpus_favored {
126 self.favored_items -= 1;
127 }
128 }
129}
130
131pub(crate) struct CorpusManager {
133 tx_generator: BoxedStrategy<BasicTxDetails>,
135 mutation_generator: BoxedStrategy<MutationType>,
137 config: FuzzCorpusConfig,
139 in_memory_corpus: Vec<CorpusEntry>,
142 current_mutated: Option<Uuid>,
144 failed_replays: usize,
146 history_map: Vec<u8>,
148 pub(crate) metrics: CorpusMetrics,
150}
151
152impl CorpusManager {
153 pub fn new(
154 config: FuzzCorpusConfig,
155 tx_generator: BoxedStrategy<BasicTxDetails>,
156 executor: &Executor,
157 fuzzed_function: Option<&Function>,
158 fuzzed_contracts: Option<&FuzzRunIdentifiedContracts>,
159 ) -> eyre::Result<Self> {
160 let mutation_generator = prop_oneof![
161 Just(MutationType::Splice),
162 Just(MutationType::Repeat),
163 Just(MutationType::Interleave),
164 Just(MutationType::Prefix),
165 Just(MutationType::Suffix),
166 Just(MutationType::Abi),
167 ]
168 .boxed();
169 let mut history_map = vec![0u8; COVERAGE_MAP_SIZE];
170 let mut metrics = CorpusMetrics::default();
171 let mut in_memory_corpus = vec![];
172 let mut failed_replays = 0;
173
174 let Some(corpus_dir) = &config.corpus_dir else {
176 return Ok(Self {
177 tx_generator,
178 mutation_generator,
179 config,
180 in_memory_corpus,
181 current_mutated: None,
182 failed_replays,
183 history_map,
184 metrics,
185 });
186 };
187
188 if !corpus_dir.is_dir() {
190 foundry_common::fs::create_dir_all(corpus_dir)?;
191 }
192
193 let can_replay_tx = |tx: &BasicTxDetails| -> bool {
194 fuzzed_contracts.is_some_and(|contracts| contracts.targets.lock().can_replay(tx))
195 || fuzzed_function.is_some_and(|function| {
196 tx.call_details
197 .calldata
198 .get(..4)
199 .is_some_and(|selector| function.selector() == selector)
200 })
201 };
202
203 'corpus_replay: for entry in std::fs::read_dir(corpus_dir)? {
204 let path = entry?.path();
205 if path.is_file()
206 && let Some(name) = path.file_name().and_then(|s| s.to_str())
207 && name.contains(METADATA_SUFFIX)
208 {
209 continue;
211 }
212
213 let read_corpus_result = match path.extension().and_then(|ext| ext.to_str()) {
214 Some("gz") => foundry_common::fs::read_json_gzip_file::<Vec<BasicTxDetails>>(&path),
215 _ => foundry_common::fs::read_json_file::<Vec<BasicTxDetails>>(&path),
216 };
217
218 let Ok(tx_seq) = read_corpus_result else {
219 trace!(target: "corpus", "failed to load corpus from {}", path.display());
220 continue;
221 };
222
223 if !tx_seq.is_empty() {
224 let mut executor = executor.clone();
226 for tx in &tx_seq {
227 if can_replay_tx(tx) {
228 let mut call_result = execute_tx(&mut executor, tx)?;
229 let (new_coverage, is_edge) =
230 call_result.merge_edge_coverage(&mut history_map);
231 if new_coverage {
232 metrics.update_seen(is_edge);
233 }
234
235 if fuzzed_contracts.is_some() {
237 executor.commit(&mut call_result);
238 }
239 } else {
240 failed_replays += 1;
241
242 if fuzzed_function.is_some() {
245 continue 'corpus_replay;
246 }
247 }
248 }
249
250 metrics.corpus_count += 1;
251
252 trace!(
253 target: "corpus",
254 "load sequence with len {} from corpus file {}",
255 tx_seq.len(),
256 path.display()
257 );
258
259 in_memory_corpus.push(CorpusEntry::new(tx_seq, path)?);
261 }
262 }
263
264 Ok(Self {
265 tx_generator,
266 mutation_generator,
267 config,
268 in_memory_corpus,
269 current_mutated: None,
270 failed_replays,
271 history_map,
272 metrics,
273 })
274 }
275
276 pub fn process_inputs(&mut self, inputs: &[BasicTxDetails], new_coverage: bool) {
280 let Some(corpus_dir) = &self.config.corpus_dir else {
282 return;
283 };
284
285 if let Some(uuid) = &self.current_mutated {
287 if let Some(corpus) =
288 self.in_memory_corpus.iter_mut().find(|corpus| corpus.uuid.eq(uuid))
289 {
290 corpus.total_mutations += 1;
291 if new_coverage {
292 corpus.new_finds_produced += 1
293 }
294 let is_favored = (corpus.new_finds_produced as f64 / corpus.total_mutations as f64)
295 > FAVORABILITY_THRESHOLD;
296 self.metrics.update_favored(is_favored, corpus.is_favored);
297 corpus.is_favored = is_favored;
298
299 trace!(
300 target: "corpus",
301 "updated corpus {}, total mutations: {}, new finds: {}",
302 corpus.uuid, corpus.total_mutations, corpus.new_finds_produced
303 );
304 }
305
306 self.current_mutated = None;
307 }
308
309 if !new_coverage {
311 return;
312 }
313
314 let corpus = CorpusEntry::from_tx_seq(inputs);
315 let corpus_uuid = corpus.uuid;
316
317 let write_result = if self.config.corpus_gzip {
319 foundry_common::fs::write_json_gzip_file(
320 corpus_dir.join(format!("{corpus_uuid}{JSON_EXTENSION}.gz")).as_path(),
321 &corpus.tx_seq,
322 )
323 } else {
324 foundry_common::fs::write_json_file(
325 corpus_dir.join(format!("{corpus_uuid}{JSON_EXTENSION}")).as_path(),
326 &corpus.tx_seq,
327 )
328 };
329
330 if let Err(err) = write_result {
331 debug!(target: "corpus", %err, "Failed to record call sequence {:?}", &corpus.tx_seq);
332 } else {
333 trace!(
334 target: "corpus",
335 "persisted {} inputs for new coverage in {corpus_uuid} corpus",
336 &corpus.tx_seq.len()
337 );
338 }
339
340 self.metrics.corpus_count += 1;
343 self.in_memory_corpus.push(corpus);
344 }
345
346 pub fn new_inputs(
349 &mut self,
350 test_runner: &mut TestRunner,
351 fuzz_state: &EvmFuzzState,
352 targeted_contracts: &FuzzRunIdentifiedContracts,
353 ) -> eyre::Result<Vec<BasicTxDetails>> {
354 let mut new_seq = vec![];
355
356 if !self.config.is_coverage_guided() {
359 new_seq.push(self.new_tx(test_runner)?);
360 return Ok(new_seq);
361 };
362
363 if !self.in_memory_corpus.is_empty() {
364 self.evict_oldest_corpus()?;
365
366 let mutation_type = self
367 .mutation_generator
368 .new_tree(test_runner)
369 .map_err(|err| eyre!("Could not generate mutation type {err}"))?
370 .current();
371 let rng = test_runner.rng();
372 let corpus_len = self.in_memory_corpus.len();
373 let primary = &self.in_memory_corpus[rng.random_range(0..corpus_len)];
374 let secondary = &self.in_memory_corpus[rng.random_range(0..corpus_len)];
375
376 match mutation_type {
377 MutationType::Splice => {
378 trace!(target: "corpus", "splice {} and {}", primary.uuid, secondary.uuid);
379
380 self.current_mutated = Some(primary.uuid);
381
382 let start1 = rng.random_range(0..primary.tx_seq.len());
383 let end1 = rng.random_range(start1..primary.tx_seq.len());
384
385 let start2 = rng.random_range(0..secondary.tx_seq.len());
386 let end2 = rng.random_range(start2..secondary.tx_seq.len());
387
388 for tx in primary.tx_seq.iter().take(end1).skip(start1) {
389 new_seq.push(tx.clone());
390 }
391 for tx in secondary.tx_seq.iter().take(end2).skip(start2) {
392 new_seq.push(tx.clone());
393 }
394 }
395 MutationType::Repeat => {
396 let corpus = if rng.random::<bool>() { primary } else { secondary };
397 trace!(target: "corpus", "repeat {}", corpus.uuid);
398
399 self.current_mutated = Some(corpus.uuid);
400
401 new_seq = corpus.tx_seq.clone();
402 let start = rng.random_range(0..corpus.tx_seq.len());
403 let end = rng.random_range(start..corpus.tx_seq.len());
404 let item_idx = rng.random_range(0..corpus.tx_seq.len());
405 let repeated = vec![new_seq[item_idx].clone(); end - start];
406 new_seq.splice(start..end, repeated);
407 }
408 MutationType::Interleave => {
409 trace!(target: "corpus", "interleave {} with {}", primary.uuid, secondary.uuid);
410
411 self.current_mutated = Some(primary.uuid);
412
413 for (tx1, tx2) in primary.tx_seq.iter().zip(secondary.tx_seq.iter()) {
414 let tx = if rng.random::<bool>() { tx1.clone() } else { tx2.clone() };
416 new_seq.push(tx);
417 }
418 }
419 MutationType::Prefix => {
420 let corpus = if rng.random::<bool>() { primary } else { secondary };
421 trace!(target: "corpus", "overwrite prefix of {}", corpus.uuid);
422
423 self.current_mutated = Some(corpus.uuid);
424
425 new_seq = corpus.tx_seq.clone();
426 for i in 0..rng.random_range(0..=new_seq.len()) {
427 new_seq[i] = self.new_tx(test_runner)?;
428 }
429 }
430 MutationType::Suffix => {
431 let corpus = if rng.random::<bool>() { primary } else { secondary };
432 trace!(target: "corpus", "overwrite suffix of {}", corpus.uuid);
433
434 self.current_mutated = Some(corpus.uuid);
435
436 new_seq = corpus.tx_seq.clone();
437 for i in new_seq.len() - rng.random_range(0..new_seq.len())..corpus.tx_seq.len()
438 {
439 new_seq[i] = self.new_tx(test_runner)?;
440 }
441 }
442 MutationType::Abi => {
443 let targets = targeted_contracts.targets.lock();
444 let corpus = if rng.random::<bool>() { primary } else { secondary };
445 trace!(target: "corpus", "ABI mutate args of {}", corpus.uuid);
446
447 self.current_mutated = Some(corpus.uuid);
448
449 new_seq = corpus.tx_seq.clone();
450
451 let idx = rng.random_range(0..new_seq.len());
452 let tx = new_seq.get_mut(idx).unwrap();
453 if let (_, Some(function)) = targets.fuzzed_artifacts(tx) {
454 if !function.inputs.is_empty() {
457 self.abi_mutate(tx, function, test_runner, fuzz_state)?;
458 }
459 }
460 }
461 }
462 }
463
464 if new_seq.is_empty() {
466 new_seq.push(self.new_tx(test_runner)?);
467 }
468 trace!(target: "corpus", "new sequence of {} calls generated", new_seq.len());
469
470 Ok(new_seq)
471 }
472
473 pub fn new_input(
476 &mut self,
477 test_runner: &mut TestRunner,
478 fuzz_state: &EvmFuzzState,
479 function: &Function,
480 ) -> eyre::Result<Bytes> {
481 if !self.config.is_coverage_guided() {
483 return Ok(self.new_tx(test_runner)?.call_details.calldata);
484 }
485
486 let tx = if !self.in_memory_corpus.is_empty() {
487 self.evict_oldest_corpus()?;
488
489 let corpus = &self.in_memory_corpus
490 [test_runner.rng().random_range(0..self.in_memory_corpus.len())];
491 self.current_mutated = Some(corpus.uuid);
492 let new_seq = corpus.tx_seq.clone();
493 let mut tx = new_seq.first().unwrap().clone();
494 self.abi_mutate(&mut tx, function, test_runner, fuzz_state)?;
495 tx
496 } else {
497 self.new_tx(test_runner)?
498 };
499
500 Ok(tx.call_details.calldata)
501 }
502
503 pub fn generate_next_input(
510 &mut self,
511 test_runner: &mut TestRunner,
512 sequence: &[BasicTxDetails],
513 discarded: bool,
514 depth: usize,
515 ) -> eyre::Result<BasicTxDetails> {
516 if self.config.corpus_dir.is_none() || discarded {
519 return self.new_tx(test_runner);
520 }
521
522 if depth > sequence.len().saturating_sub(1) || test_runner.rng().random_ratio(1, 10) {
525 return self.new_tx(test_runner);
526 }
527
528 Ok(sequence[depth].clone())
530 }
531
532 pub fn new_tx(&mut self, test_runner: &mut TestRunner) -> eyre::Result<BasicTxDetails> {
534 Ok(self
535 .tx_generator
536 .new_tree(test_runner)
537 .map_err(|_| eyre!("Could not generate case"))?
538 .current())
539 }
540
541 pub fn failed_replays(self) -> usize {
543 self.failed_replays
544 }
545
546 pub fn merge_edge_coverage(&mut self, call_result: &mut RawCallResult) -> bool {
548 if !self.config.collect_edge_coverage() {
549 return false;
550 }
551
552 let (new_coverage, is_edge) = call_result.merge_edge_coverage(&mut self.history_map);
553 if new_coverage {
554 self.metrics.update_seen(is_edge);
555 }
556 new_coverage
557 }
558
559 fn evict_oldest_corpus(&mut self) -> eyre::Result<()> {
562 if self.in_memory_corpus.len() > self.config.corpus_min_size.max(1)
563 && let Some(index) = self.in_memory_corpus.iter().position(|corpus| {
564 corpus.total_mutations > self.config.corpus_min_mutations && !corpus.is_favored
565 })
566 {
567 let corpus = self.in_memory_corpus.get(index).unwrap();
568
569 let uuid = corpus.uuid;
570 debug!(target: "corpus", "evict corpus {uuid}");
571
572 let eviction_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
574 foundry_common::fs::write_json_file(
575 self.config
576 .corpus_dir
577 .clone()
578 .unwrap()
579 .join(format!("{uuid}-{eviction_time}-{METADATA_SUFFIX}"))
580 .as_path(),
581 &corpus,
582 )?;
583
584 self.in_memory_corpus.remove(index);
586 }
587 Ok(())
588 }
589
590 fn abi_mutate(
593 &self,
594 tx: &mut BasicTxDetails,
595 function: &Function,
596 test_runner: &mut TestRunner,
597 fuzz_state: &EvmFuzzState,
598 ) -> eyre::Result<()> {
599 let mut arg_mutation_rounds =
601 test_runner.rng().random_range(0..=function.inputs.len()).max(1);
602 let round_arg_idx: Vec<usize> = if function.inputs.len() <= 1 {
603 vec![0]
604 } else {
605 (0..arg_mutation_rounds)
606 .map(|_| test_runner.rng().random_range(0..function.inputs.len()))
607 .collect()
608 };
609 let mut prev_inputs = function
610 .abi_decode_input(&tx.call_details.calldata[4..])
611 .map_err(|err| eyre!("failed to load previous inputs: {err}"))?;
612
613 while arg_mutation_rounds > 0 {
614 let idx = round_arg_idx[arg_mutation_rounds - 1];
615 prev_inputs[idx] = mutate_param_value(
616 &function
617 .inputs
618 .get(idx)
619 .expect("Could not get input to mutate")
620 .selector_type()
621 .parse()?,
622 prev_inputs[idx].clone(),
623 test_runner,
624 fuzz_state,
625 );
626 arg_mutation_rounds -= 1;
627 }
628
629 tx.call_details.calldata =
630 function.abi_encode_input(&prev_inputs).map_err(|e| eyre!(e.to_string()))?.into();
631 Ok(())
632 }
633}
634
635#[cfg(test)]
636mod tests {
637 use super::*;
638 use alloy_primitives::Address;
639 use std::fs;
640
641 fn basic_tx() -> BasicTxDetails {
642 BasicTxDetails {
643 warp: None,
644 roll: None,
645 sender: Address::ZERO,
646 call_details: foundry_evm_fuzz::CallDetails {
647 target: Address::ZERO,
648 calldata: Bytes::new(),
649 },
650 }
651 }
652
653 fn temp_corpus_dir() -> PathBuf {
654 let dir = std::env::temp_dir().join(format!("foundry-corpus-tests-{}", Uuid::new_v4()));
655 let _ = fs::create_dir_all(&dir);
656 dir
657 }
658
659 fn new_manager_with_single_corpus() -> (CorpusManager, Uuid) {
660 let tx_gen = Just(basic_tx()).boxed();
661 let config = FuzzCorpusConfig {
662 corpus_dir: Some(temp_corpus_dir()),
663 corpus_gzip: false,
664 corpus_min_mutations: 0,
665 corpus_min_size: 0,
666 ..Default::default()
667 };
668
669 let tx_seq = vec![basic_tx()];
670 let corpus = CorpusEntry::from_tx_seq(&tx_seq);
671 let seed_uuid = corpus.uuid;
672
673 let manager = CorpusManager {
674 tx_generator: tx_gen,
675 mutation_generator: Just(MutationType::Repeat).boxed(),
676 config,
677 in_memory_corpus: vec![corpus],
678 current_mutated: Some(seed_uuid),
679 failed_replays: 0,
680 history_map: vec![0u8; COVERAGE_MAP_SIZE],
681 metrics: CorpusMetrics::default(),
682 };
683
684 (manager, seed_uuid)
685 }
686
687 #[test]
688 fn favored_sets_true_and_metrics_increment_when_ratio_gt_threshold() {
689 let (mut manager, uuid) = new_manager_with_single_corpus();
690 let corpus = manager.in_memory_corpus.iter_mut().find(|c| c.uuid == uuid).unwrap();
691 corpus.total_mutations = 4;
692 corpus.new_finds_produced = 2; corpus.is_favored = false;
694
695 assert_eq!(manager.metrics.favored_items, 0);
697
698 manager.current_mutated = Some(uuid);
700 manager.process_inputs(&[basic_tx()], true);
701
702 let corpus = manager.in_memory_corpus.iter().find(|c| c.uuid == uuid).unwrap();
703 assert!(corpus.is_favored, "expected favored to be true when ratio > threshold");
704 assert_eq!(
705 manager.metrics.favored_items, 1,
706 "favored_items should increment on false→true"
707 );
708 }
709
710 #[test]
711 fn favored_sets_false_and_metrics_decrement_when_ratio_lt_threshold() {
712 let (mut manager, uuid) = new_manager_with_single_corpus();
713 let corpus = manager.in_memory_corpus.iter_mut().find(|c| c.uuid == uuid).unwrap();
714 corpus.total_mutations = 9;
715 corpus.new_finds_produced = 3; corpus.is_favored = true; manager.metrics.favored_items = 1;
719
720 manager.current_mutated = Some(uuid);
722 manager.process_inputs(&[basic_tx()], false);
723
724 let corpus = manager.in_memory_corpus.iter().find(|c| c.uuid == uuid).unwrap();
725 assert!(!corpus.is_favored, "expected favored to be false when ratio < threshold");
726 assert_eq!(
727 manager.metrics.favored_items, 0,
728 "favored_items should decrement on true→false"
729 );
730 }
731
732 #[test]
733 fn favored_is_false_on_ratio_equal_threshold() {
734 let (mut manager, uuid) = new_manager_with_single_corpus();
735 let corpus = manager.in_memory_corpus.iter_mut().find(|c| c.uuid == uuid).unwrap();
736 corpus.total_mutations = 9;
738 corpus.new_finds_produced = 2;
739 corpus.is_favored = false;
740
741 manager.current_mutated = Some(uuid);
742 manager.process_inputs(&[basic_tx()], true);
743
744 let corpus = manager.in_memory_corpus.iter().find(|c| c.uuid == uuid).unwrap();
745 assert!(
746 !(corpus.is_favored),
747 "with strict '>' comparison, favored must be false when ratio == threshold"
748 );
749 }
750
751 #[test]
752 fn eviction_skips_favored_and_evicts_non_favored() {
753 let tx_gen = Just(basic_tx()).boxed();
755 let config = FuzzCorpusConfig {
756 corpus_dir: Some(temp_corpus_dir()),
757 corpus_min_mutations: 0,
758 corpus_min_size: 0,
759 ..Default::default()
760 };
761
762 let mut favored = CorpusEntry::from_tx_seq(&[basic_tx()]);
763 favored.total_mutations = 2;
764 favored.is_favored = true;
765
766 let mut non_favored = CorpusEntry::from_tx_seq(&[basic_tx()]);
767 non_favored.total_mutations = 2;
768 non_favored.is_favored = false;
769 let non_favored_uuid = non_favored.uuid;
770
771 let mut manager = CorpusManager {
772 tx_generator: tx_gen,
773 mutation_generator: Just(MutationType::Repeat).boxed(),
774 config,
775 in_memory_corpus: vec![favored, non_favored],
776 current_mutated: None,
777 failed_replays: 0,
778 history_map: vec![0u8; COVERAGE_MAP_SIZE],
779 metrics: CorpusMetrics::default(),
780 };
781
782 manager.evict_oldest_corpus().unwrap();
784 assert_eq!(manager.in_memory_corpus.len(), 1);
785 assert!(manager.in_memory_corpus.iter().all(|c| c.is_favored));
786
787 manager.evict_oldest_corpus().unwrap();
789 assert_eq!(manager.in_memory_corpus.len(), 1, "favored corpus must not be evicted");
790
791 assert!(manager.in_memory_corpus.iter().all(|c| c.uuid != non_favored_uuid));
793 }
794}