1use crate::{
16 executors::{
17 Executor,
18 corpus::{
19 DynamicTargetCtx, StatelessReplayTarget, WorkerCorpus, register_replay_created,
20 rollback_replay_created,
21 },
22 corpus_io::read_corpus_tree,
23 invariant::{
24 call_after_invariant_function, call_invariant_function, did_fail_on_assert, execute_tx,
25 snapshot_edge_fingerprint,
26 },
27 },
28 inspectors::EdgeIndexMap,
29};
30use alloy_dyn_abi::JsonAbiExt;
31use alloy_json_abi::Function;
32use alloy_primitives::{Address, B256, Selector, hex, keccak256};
33use eyre::Result;
34use foundry_config::FuzzCorpusConfig;
35use foundry_evm_core::{
36 constants::{CHEATCODE_ADDRESS, MAGIC_ASSUME},
37 decode::SkipReason,
38 evm::FoundryEvmNetwork,
39};
40use foundry_evm_coverage::HitMaps;
41use foundry_evm_fuzz::{BasicTxDetails, invariant::FuzzRunIdentifiedContracts};
42use std::{
43 collections::HashMap,
44 fmt,
45 fs::File,
46 io::{BufWriter, Write},
47 path::{Path, PathBuf},
48};
49
50type EvmShowmap = HashMap<(B256, u32), u64>;
51const MAX_REPORTED_REPLAY_FAILURES: usize = 20;
52
53#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
55pub enum ShowmapDomain {
56 #[default]
57 Evm,
58 Sancov,
59 Both,
60}
61
62impl ShowmapDomain {
63 pub const fn includes_evm(self) -> bool {
64 matches!(self, Self::Evm | Self::Both)
65 }
66 pub const fn includes_sancov(self) -> bool {
67 matches!(self, Self::Sancov | Self::Both)
68 }
69}
70
71impl fmt::Display for ShowmapDomain {
72 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73 match self {
74 Self::Evm => f.write_str("evm"),
75 Self::Sancov => f.write_str("sancov"),
76 Self::Both => f.write_str("both"),
77 }
78 }
79}
80
81#[derive(Clone, Debug)]
83pub struct ShowmapOpts {
84 pub out_dir: PathBuf,
86 pub approach: String,
89 pub trial: String,
91 pub per_input: bool,
93 pub domain: ShowmapDomain,
95 pub emit_files: bool,
97}
98
99#[derive(Clone, Debug, Default)]
101pub struct ShowmapStats {
102 pub corpus_entries: usize,
104 pub showmap_files: usize,
106 pub skipped_entries: usize,
109 pub unreadable_entries: usize,
111 pub sancov_requested: bool,
114 pub sancov_observed: bool,
116}
117
118pub struct ShowmapReplayTarget<'a> {
120 pub stateless: Option<StatelessReplayTarget<'a>>,
121 pub fuzz_fail_on_revert: bool,
122 pub fuzzed_contracts: Option<&'a FuzzRunIdentifiedContracts>,
123 pub invariant_address: Option<Address>,
124 pub invariant_fns: &'a [(&'a Function, bool)],
125 pub invariant_replay: InvariantReplayOptions,
126 pub dynamic: Option<&'a DynamicTargetCtx<'a>>,
127}
128
129#[derive(Clone, Copy, Debug, Default)]
131pub struct InvariantReplayOptions {
132 pub check_interval: u32,
133 pub call_after_invariant: bool,
134 pub is_optimization: bool,
135}
136
137#[derive(Clone, Debug, PartialEq, Eq)]
139pub enum ReplayFailure {
140 Fuzz { selector: Selector, fingerprint: Option<B256>, output: B256 },
142 Handler {
146 target: Address,
147 selector: Selector,
148 fingerprint: Option<B256>,
149 terminal: bool,
150 invariant: Option<String>,
151 },
152 Invariant { name: String },
154 AfterInvariant,
156}
157
158impl fmt::Display for ReplayFailure {
159 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160 match self {
161 Self::Fuzz { selector, .. } => write!(f, "fuzz call {selector:?} failed"),
162 Self::Handler { target, selector, invariant, .. } => {
163 if let Some(invariant) = invariant {
164 write!(
165 f,
166 "invariant `{invariant}` failed on handler {selector:?} on {target:?}"
167 )
168 } else {
169 write!(f, "handler {selector:?} on {target:?} failed")
170 }
171 }
172 Self::Invariant { name } => write!(f, "invariant `{name}` broken"),
173 Self::AfterInvariant => f.write_str("afterInvariant broken"),
174 }
175 }
176}
177
178impl ReplayFailure {
179 const fn is_terminal(&self) -> bool {
181 !matches!(self, Self::Handler { terminal: false, .. })
182 }
183
184 const fn is_predicate(&self) -> bool {
186 matches!(self, Self::Invariant { .. } | Self::AfterInvariant)
187 }
188}
189
190fn record_replay_failure(slot: &mut Option<ReplayFailure>, failure: ReplayFailure) {
193 match slot {
194 None => *slot = Some(failure),
195 Some(existing) if !existing.is_terminal() && failure.is_terminal() => *slot = Some(failure),
196 Some(_) => {}
197 }
198}
199
200#[derive(Clone, Debug, Default, PartialEq, Eq)]
202pub struct ReplayObservation {
203 pub evm_edges: Vec<u8>,
205 pub sancov_edges: Vec<u8>,
207 pub failure: Option<ReplayFailure>,
209 pub replayed: usize,
211 pub unmatched: usize,
213 pub skipped: usize,
215}
216
217pub fn replay_corpus_to_showmap<FEN: FoundryEvmNetwork>(
224 executor: &Executor<FEN>,
225 corpus_dir: &Path,
226 target: ShowmapReplayTarget<'_>,
227 opts: &ShowmapOpts,
228) -> Result<ShowmapStats> {
229 let entries = read_corpus_tree(corpus_dir)?;
230 if opts.emit_files && entries.is_empty() {
231 return Err(eyre::eyre!("corpus directory not found: {}", corpus_dir.display()));
232 }
233
234 let approach_dir = opts.out_dir.join(&opts.approach);
235 if opts.emit_files {
236 foundry_common::fs::create_dir_all(&approach_dir)?;
237 }
238
239 let mut stats =
240 ShowmapStats { sancov_requested: opts.domain.includes_sancov(), ..Default::default() };
241 let mut replay_failures = Vec::new();
242 let mut failed_entries = 0usize;
243 let mut evm_buf = EvmShowmap::new();
246 let mut san_buf: Vec<u64> = Vec::new();
247
248 for entry in entries {
249 let tx_seq = match entry.read_tx_seq() {
250 Ok(seq) if !seq.is_empty() => seq,
251 Ok(_) => continue,
252 Err(err) => {
253 debug!(target: "showmap", %err, ?entry.path, "failed to read corpus entry");
254 stats.unreadable_entries += 1;
255 stats.skipped_entries += 1;
256 continue;
257 }
258 };
259
260 let mut had_accepted = false;
261 let mut executor = executor.clone();
262 let mut created: Vec<Address> = Vec::new();
264 let mut accepted = 0usize;
266 let mut last_accepted_checked_invariant = false;
267 let mut entry_failure: Option<ReplayFailure> = None;
268 for tx in &tx_seq {
269 if !WorkerCorpus::can_replay_tx(tx, target.stateless, target.fuzzed_contracts) {
270 continue;
271 }
272
273 let mut call_result = execute_tx(&mut executor, tx)?;
274 let fingerprint = snapshot_edge_fingerprint(&call_result);
276 if call_result.result.as_ref() == MAGIC_ASSUME
279 || (call_result.reverter == Some(CHEATCODE_ADDRESS)
280 && SkipReason::decode(&call_result.result).is_some())
281 {
282 continue;
283 }
284 if opts.domain.includes_evm() {
291 accumulate_evm(&mut evm_buf, call_result.line_coverage.as_ref());
292 }
293 if opts.domain.includes_sancov() {
294 accumulate_sancov(&mut san_buf, call_result.sancov_coverage.as_deref());
295 if let Some(buf) = call_result.sancov_coverage.as_mut() {
296 buf.fill(0);
297 }
298 }
299
300 had_accepted = true;
301
302 register_replay_created(
303 &call_result.state_changeset,
304 target.dynamic,
305 target.fuzzed_contracts,
306 &mut created,
307 );
308
309 let target_addr = tx.call_details.target;
310 let selector =
311 tx.call_details.calldata.get(..4).map(Selector::from_slice).unwrap_or_default();
312
313 if target.fuzzed_contracts.is_some() {
315 accepted += 1;
316 last_accepted_checked_invariant = false;
317 if !opts.emit_files
318 && let Some(failure) = invariant_handler_failure(
319 target_addr,
320 selector,
321 did_fail_on_assert(&call_result, &call_result.state_changeset),
322 target.invariant_fns,
323 &call_result,
324 fingerprint,
325 )
326 {
327 entry_failure = Some(failure);
328 break;
329 }
330 executor.commit(&mut call_result);
331 if !opts.emit_files
332 && should_check_invariant(
333 accepted,
334 target.invariant_replay.check_interval,
335 target.invariant_replay.is_optimization,
336 )
337 {
338 last_accepted_checked_invariant = true;
339 if !target.invariant_replay.is_optimization
340 && let Some(address) = target.invariant_address
341 && let Some(failure) =
342 broken_invariant(&executor, address, target.invariant_fns)?
343 {
344 entry_failure = Some(failure);
345 break;
346 }
347 }
348 } else if !opts.emit_files
349 && !fuzz_replay_call_succeeded(
350 &executor,
351 target_addr,
352 &mut call_result,
353 target.fuzz_fail_on_revert,
354 )
355 {
356 entry_failure = Some(ReplayFailure::Fuzz {
357 selector,
358 fingerprint,
359 output: keccak256(call_result.result.as_ref()),
360 });
361 break;
362 }
363 }
364 if !opts.emit_files
368 && entry_failure.is_none()
369 && accepted > 0
370 && target.fuzzed_contracts.is_some()
371 && let Some(address) = target.invariant_address
372 {
373 if !target.invariant_replay.is_optimization
374 && !last_accepted_checked_invariant
375 && let Some(failure) = broken_invariant(&executor, address, target.invariant_fns)?
376 {
377 entry_failure = Some(failure);
378 } else if target.invariant_replay.call_after_invariant
379 && let Some(failure) = broken_after_invariant(&executor, address)?
380 {
381 entry_failure = Some(failure);
382 }
383 }
384 if let Some(failure) = entry_failure {
385 rollback_replay_created(target.fuzzed_contracts, created);
386 failed_entries += 1;
387 if replay_failures.len() < MAX_REPORTED_REPLAY_FAILURES {
388 replay_failures.push(format!(
389 "corpus entry {} failed during replay: {failure}",
390 entry.path.display()
391 ));
392 }
393 continue;
394 }
395 rollback_replay_created(target.fuzzed_contracts, created);
396
397 if !had_accepted {
398 stats.skipped_entries += 1;
399 continue;
400 }
401 stats.corpus_entries += 1;
402 if !stats.sancov_observed && san_buf.iter().any(|&x| x != 0) {
403 stats.sancov_observed = true;
404 }
405
406 if opts.emit_files && opts.per_input {
407 let stem = format!("{}__{}-{}", opts.trial, entry.uuid, entry.timestamp);
409 stats.showmap_files +=
410 write_showmap_file(&approach_dir.join(format!("{stem}.txt")), &evm_buf, &san_buf)?;
411 evm_buf.clear();
413 san_buf.fill(0);
414 }
415 }
416
417 if opts.emit_files && !opts.per_input {
418 stats.showmap_files += write_showmap_file(
420 &approach_dir.join(format!("{}.txt", opts.trial)),
421 &evm_buf,
422 &san_buf,
423 )?;
424 }
425
426 if failed_entries > 0 {
427 return Err(eyre::eyre!(
428 "corpus replay failed:\n{}",
429 replay_failure_report(&replay_failures, failed_entries)
430 ));
431 }
432
433 Ok(stats)
434}
435
436pub struct MinimizationReplayInput<'a> {
437 pub sequence: &'a [BasicTxDetails],
438 pub evm_edge_indices: &'a mut EdgeIndexMap,
439 pub corpus: &'a FuzzCorpusConfig,
440}
441
442pub fn replay_sequence_for_minimization<FEN: FoundryEvmNetwork>(
444 executor: &Executor<FEN>,
445 input: MinimizationReplayInput<'_>,
446 target: ShowmapReplayTarget<'_>,
447) -> Result<ReplayObservation> {
448 let mut observation = ReplayObservation::default();
449 let mut executor = executor.clone();
450 executor.inspector_mut().collect_edge_coverage_with_config(input.corpus);
451 executor.inspector_mut().collect_sancov_edges(input.corpus.collect_sancov_edges());
452 executor.inspector_mut().collect_sancov_trace_cmp(input.corpus.collect_sancov_trace_cmp());
453
454 let mut created = Vec::new();
455 let mut accepted = 0usize;
456 let mut last_accepted_checked_invariant = false;
457 for tx in input.sequence {
458 if !WorkerCorpus::can_replay_tx(tx, target.stateless, target.fuzzed_contracts) {
459 observation.unmatched += 1;
460 continue;
461 }
462
463 let mut call_result = execute_tx(&mut executor, tx)?;
464 let target_addr = tx.call_details.target;
465 let selector =
466 tx.call_details.calldata.get(..4).map(Selector::from_slice).unwrap_or_default();
467 let fingerprint = snapshot_edge_fingerprint(&call_result);
468
469 if call_result.result.as_ref() == MAGIC_ASSUME
470 || (call_result.reverter == Some(CHEATCODE_ADDRESS)
471 && SkipReason::decode(&call_result.result).is_some())
472 {
473 observation.skipped += 1;
474 continue;
475 }
476
477 call_result.merge_all_coverage(
478 &mut observation.evm_edges,
479 input.evm_edge_indices,
480 &mut observation.sancov_edges,
481 );
482 observation.replayed += 1;
483
484 register_replay_created(
485 &call_result.state_changeset,
486 target.dynamic,
487 target.fuzzed_contracts,
488 &mut created,
489 );
490
491 if target.fuzzed_contracts.is_some() {
492 accepted += 1;
493 last_accepted_checked_invariant = false;
494 if let Some(failure) = invariant_handler_failure(
495 target_addr,
496 selector,
497 did_fail_on_assert(&call_result, &call_result.state_changeset),
498 target.invariant_fns,
499 &call_result,
500 fingerprint,
501 ) {
502 let terminal = failure.is_terminal();
503 record_replay_failure(&mut observation.failure, failure);
504 if terminal {
505 break;
506 }
507 }
508 executor.commit(&mut call_result);
509 if should_check_invariant(
510 accepted,
511 target.invariant_replay.check_interval,
512 target.invariant_replay.is_optimization,
513 ) {
514 last_accepted_checked_invariant = true;
515 if !target.invariant_replay.is_optimization
516 && !observation.failure.as_ref().is_some_and(ReplayFailure::is_predicate)
517 && let Some(address) = target.invariant_address
518 && let Some(failure) =
519 broken_invariant(&executor, address, target.invariant_fns)?
520 {
521 record_replay_failure(&mut observation.failure, failure);
522 }
523 }
524 } else if !fuzz_replay_call_succeeded(
525 &executor,
526 target_addr,
527 &mut call_result,
528 target.fuzz_fail_on_revert,
529 ) {
530 record_replay_failure(
531 &mut observation.failure,
532 ReplayFailure::Fuzz {
533 selector,
534 fingerprint,
535 output: keccak256(call_result.result.as_ref()),
536 },
537 );
538 break;
539 }
540
541 if observation.failure.as_ref().is_some_and(ReplayFailure::is_terminal) {
542 break;
543 }
544 }
545
546 if !observation.failure.as_ref().is_some_and(ReplayFailure::is_terminal)
547 && accepted > 0
548 && target.fuzzed_contracts.is_some()
549 && let Some(address) = target.invariant_address
550 {
551 if !target.invariant_replay.is_optimization
552 && !last_accepted_checked_invariant
553 && let Some(failure) = broken_invariant(&executor, address, target.invariant_fns)?
554 {
555 record_replay_failure(&mut observation.failure, failure);
556 } else if target.invariant_replay.call_after_invariant
557 && let Some(failure) = broken_after_invariant(&executor, address)?
558 {
559 record_replay_failure(&mut observation.failure, failure);
560 }
561 }
562
563 rollback_replay_created(target.fuzzed_contracts, created);
564 Ok(observation)
565}
566
567fn replay_failure_report(replay_failures: &[String], failed_entries: usize) -> String {
568 let mut report = replay_failures.join("\n");
569 let unreported = failed_entries.saturating_sub(replay_failures.len());
570 if unreported > 0 {
571 if !report.is_empty() {
572 report.push('\n');
573 }
574 report.push_str(&format!("... and {unreported} more failed corpus entries"));
575 }
576 report
577}
578
579fn invariant_handler_failure<FEN: FoundryEvmNetwork>(
583 target: Address,
584 selector: Selector,
585 assertion_failure: bool,
586 invariant_fns: &[(&Function, bool)],
587 call_result: &crate::executors::RawCallResult<FEN>,
588 fingerprint: Option<B256>,
589) -> Option<ReplayFailure> {
590 let fail_on_revert_invariant = invariant_fns
591 .iter()
592 .find_map(|(invariant, fail_on_revert)| (*fail_on_revert).then_some(invariant));
593 let fail_on_revert_failure = fail_on_revert_invariant.is_some()
594 && call_result.reverted
595 && call_result.result.as_ref() != MAGIC_ASSUME;
596 let failed = assertion_failure || fail_on_revert_failure;
597 failed.then_some(ReplayFailure::Handler {
598 target,
599 selector,
600 fingerprint,
601 terminal: fail_on_revert_failure && !assertion_failure,
602 invariant: fail_on_revert_failure.then(|| fail_on_revert_invariant.unwrap().name.clone()),
603 })
604}
605
606fn fuzz_replay_call_succeeded<FEN: FoundryEvmNetwork>(
607 executor: &Executor<FEN>,
608 target_addr: Address,
609 call_result: &mut crate::executors::RawCallResult<FEN>,
610 fail_on_revert: bool,
611) -> bool {
612 if !fail_on_revert
613 && call_result
614 .reverter
615 .is_some_and(|reverter| reverter != target_addr && reverter != CHEATCODE_ADDRESS)
616 {
617 true
618 } else {
619 executor.is_raw_call_mut_success(target_addr, call_result, false)
620 }
621}
622
623fn broken_invariant<FEN: FoundryEvmNetwork>(
624 executor: &Executor<FEN>,
625 invariant_address: Address,
626 invariant_fns: &[(&Function, bool)],
627) -> Result<Option<ReplayFailure>> {
628 for (invariant, _) in invariant_fns {
629 let (_, success) = call_invariant_function(
630 executor,
631 invariant_address,
632 invariant.abi_encode_input(&[])?.into(),
633 )?;
634 if !success {
635 return Ok(Some(ReplayFailure::Invariant { name: invariant.name.clone() }));
636 }
637 }
638 Ok(None)
639}
640
641fn broken_after_invariant<FEN: FoundryEvmNetwork>(
642 executor: &Executor<FEN>,
643 invariant_address: Address,
644) -> Result<Option<ReplayFailure>> {
645 let (_, success) = call_after_invariant_function(executor, invariant_address)?;
646 Ok((!success).then_some(ReplayFailure::AfterInvariant))
647}
648
649fn should_check_invariant(accepted: usize, check_interval: u32, is_optimization: bool) -> bool {
656 debug_assert!(accepted > 0);
657 is_optimization
658 || check_interval == 1
659 || (check_interval > 1 && accepted.is_multiple_of(check_interval as usize))
660}
661
662fn accumulate_evm(dst: &mut EvmShowmap, src: Option<&HitMaps>) {
664 let Some(maps) = src else { return };
665 for (hash, hitmap) in maps.iter() {
666 for (pc, hits) in hitmap.iter() {
667 let slot = dst.entry((*hash, pc)).or_default();
668 *slot = slot.saturating_add(hits as u64);
669 }
670 }
671}
672
673fn accumulate_sancov(dst: &mut Vec<u64>, src: Option<&[u8]>) {
675 let Some(src) = src else { return };
676 if dst.len() < src.len() {
677 dst.resize(src.len(), 0);
678 }
679 for (d, &s) in dst.iter_mut().zip(src) {
680 if s != 0 {
681 *d = d.saturating_add(s as u64);
682 }
683 }
684}
685
686fn write_showmap_file(path: &Path, evm: &EvmShowmap, san: &[u64]) -> Result<usize> {
689 let has_evm = evm.values().any(|&c| c != 0);
691 let has_san = san.iter().any(|&c| c != 0);
692 if !has_evm && !has_san {
693 return Ok(0);
694 }
695 let mut w = BufWriter::new(File::create_new(path).map_err(|err| {
696 eyre::eyre!(
697 "failed to create showmap file {}: {err}; pick a different --showmap-trial or remove \
698 the existing file",
699 path.display()
700 )
701 })?);
702 write_evm(&mut w, evm)?;
703 write_sancov(&mut w, san)?;
704 w.flush()?;
705 Ok(1)
706}
707
708fn write_evm<W: Write>(out: &mut W, evm: &EvmShowmap) -> std::io::Result<()> {
712 let mut entries = evm.iter().filter(|(_, count)| **count != 0).collect::<Vec<_>>();
713 entries.sort_unstable_by_key(|((hash, pc), _)| (*hash, *pc));
714
715 for ((hash, pc), count) in entries {
716 let h = hex::encode(&hash.as_slice()[..8]);
717 writeln!(out, "evm_{h}_{pc:04x}:{count}")?;
718 }
719 Ok(())
720}
721
722fn write_sancov<W: Write>(out: &mut W, bitmap: &[u64]) -> std::io::Result<()> {
723 for (idx, &count) in bitmap.iter().enumerate() {
724 if count != 0 {
725 writeln!(out, "sancov_0x{idx:04x}:{count}")?;
728 }
729 }
730 Ok(())
731}
732
733#[cfg(test)]
734mod tests {
735 use super::*;
736 use crate::executors::{RawCallResult, corpus_io::canonical_replay_dirs};
737 use foundry_evm_core::evm::EthEvmNetwork;
738 use revm::interpreter::InstructionResult;
739 use uuid::Uuid;
740
741 fn temp_dir() -> PathBuf {
742 let dir = std::env::temp_dir().join(format!("foundry-showmap-{}", Uuid::new_v4()));
743 std::fs::create_dir_all(&dir).unwrap();
744 dir
745 }
746
747 #[test]
748 fn accumulate_sancov_resizes_and_saturating_adds() {
749 let mut dst: Vec<u64> = vec![10];
750 accumulate_sancov(&mut dst, Some(&[1u8, 2, 3]));
751 assert_eq!(dst, vec![11, 2, 3]);
752 }
753
754 #[test]
755 fn write_evm_emits_only_nonzero_deterministic_ids() {
756 let mut buf: Vec<u8> = Vec::new();
757 let h = B256::with_last_byte(0xab);
758 let mut evm = EvmShowmap::new();
759 evm.insert((h, 1u32), 0u64); evm.insert((h, 0x2au32), 3u64);
761 write_evm(&mut buf, &evm).unwrap();
762 let h_hex = hex::encode(&h.as_slice()[..8]);
763 assert_eq!(String::from_utf8(buf).unwrap(), format!("evm_{h_hex}_002a:3\n"));
764 }
765
766 #[test]
767 fn write_sancov_emits_only_nonzero_hex_ids() {
768 let mut buf: Vec<u8> = Vec::new();
769 write_sancov(&mut buf, &[0, 3, 0, 1]).unwrap();
770 assert_eq!(String::from_utf8(buf).unwrap(), "sancov_0x0001:3\nsancov_0x0003:1\n");
771 }
772
773 #[test]
774 fn write_showmap_file_skips_when_empty() {
775 let dir = temp_dir();
776 let path = dir.join("trial.txt");
777 let written = write_showmap_file(&path, &EvmShowmap::new(), &[]).unwrap();
778 assert_eq!(written, 0);
779 assert!(!path.exists());
780 }
781
782 #[test]
783 fn write_showmap_file_writes_combined_domains() {
784 let dir = temp_dir();
785 let path = dir.join("trial.txt");
786 let h = B256::with_last_byte(0xff);
787 let mut evm = EvmShowmap::new();
788 evm.insert((h, 7u32), 5u64);
789 let written = write_showmap_file(&path, &evm, &[2]).unwrap();
790 assert_eq!(written, 1);
791 let body = std::fs::read_to_string(&path).unwrap();
792 let h_hex = hex::encode(&h.as_slice()[..8]);
793 assert_eq!(body, format!("evm_{h_hex}_0007:5\nsancov_0x0000:2\n"));
794 }
795
796 #[test]
797 fn write_showmap_file_does_not_overwrite_existing_file() {
798 let dir = temp_dir();
799 let path = dir.join("trial.txt");
800 std::fs::write(&path, "keep me").unwrap();
801 let h = B256::with_last_byte(0xff);
802 let mut evm = EvmShowmap::new();
803 evm.insert((h, 7u32), 5u64);
804 let err = write_showmap_file(&path, &evm, &[]).unwrap_err();
805 assert!(err.to_string().contains("pick a different --showmap-trial"), "{err:?}");
806 assert_eq!(std::fs::read_to_string(&path).unwrap(), "keep me");
807 }
808
809 #[test]
810 fn replay_failure_report_caps_details() {
811 let failures = (0..MAX_REPORTED_REPLAY_FAILURES)
812 .map(|idx| format!("corpus entry {idx} failed during replay: fuzz call failed"))
813 .collect::<Vec<_>>();
814 let report = replay_failure_report(&failures, MAX_REPORTED_REPLAY_FAILURES + 3);
815
816 assert!(report.contains("corpus entry 0 failed during replay"), "{report}");
817 assert!(report.contains("corpus entry 19 failed during replay"), "{report}");
818 assert!(report.contains("... and 3 more failed corpus entries"), "{report}");
819 assert!(!report.contains("corpus entry 20 failed during replay"), "{report}");
820 }
821
822 #[test]
823 fn should_check_invariant_matches_campaign_intervals() {
824 assert!(should_check_invariant(1, 1, false));
826 assert!(should_check_invariant(2, 1, false));
827 assert!(!should_check_invariant(1, 0, false));
829 assert!(!should_check_invariant(5, 0, false));
830 assert!(!should_check_invariant(1, 3, false));
832 assert!(!should_check_invariant(2, 3, false));
833 assert!(should_check_invariant(3, 3, false));
834 assert!(should_check_invariant(6, 3, false));
835 assert!(should_check_invariant(1, 0, true));
837 }
838
839 #[test]
840 fn invariant_handler_failure_ignores_plain_revert() {
841 let call_result = RawCallResult::<EthEvmNetwork> {
842 reverted: true,
843 exit_reason: Some(InstructionResult::Revert),
844 ..Default::default()
845 };
846 let failure = invariant_handler_failure(
847 Address::with_last_byte(1),
848 Selector::from([0xaa, 0xbb, 0xcc, 0xdd]),
849 did_fail_on_assert(&call_result, &call_result.state_changeset),
850 &[],
851 &call_result,
852 None,
853 );
854 assert_eq!(failure, None);
855 }
856
857 #[test]
858 fn invariant_handler_failure_reports_fail_on_revert() {
859 let call_result = RawCallResult::<EthEvmNetwork> {
860 reverted: true,
861 exit_reason: Some(InstructionResult::Revert),
862 ..Default::default()
863 };
864 let invariant = serde_json::from_value::<Function>(serde_json::json!({
865 "type": "function",
866 "name": "invariant_ok",
867 "inputs": [],
868 "outputs": [],
869 "stateMutability": "view"
870 }))
871 .unwrap();
872 let failure = invariant_handler_failure(
873 Address::with_last_byte(1),
874 Selector::from([0xaa, 0xbb, 0xcc, 0xdd]),
875 did_fail_on_assert(&call_result, &call_result.state_changeset),
876 &[(&invariant, true)],
877 &call_result,
878 None,
879 );
880 assert!(matches!(
881 failure,
882 Some(ReplayFailure::Handler {
883 terminal: true,
884 invariant: Some(name),
885 ..
886 }) if name == "invariant_ok"
887 ));
888 }
889
890 #[test]
891 fn canonical_replay_dirs_collects_all_workers() {
892 let dir = temp_dir();
893 let w0 = dir.join("worker0").join("corpus");
894 let w1 = dir.join("worker1").join("corpus");
895 std::fs::create_dir_all(&w0).unwrap();
896 std::fs::create_dir_all(&w1).unwrap();
897 assert_eq!(canonical_replay_dirs(&dir), vec![w0, w1]);
898 }
899
900 #[test]
901 fn canonical_replay_dirs_falls_back_when_no_workers() {
902 let dir = temp_dir();
903 assert_eq!(canonical_replay_dirs(&dir), vec![dir]);
904 }
905}