1use crate::{
4 MultiContractRunner,
5 fuzz::{BaseCounterExample, FuzzedCases},
6 gas_report::GasReport,
7};
8use alloy_primitives::{
9 Address, I256, Log, U256,
10 map::{AddressHashMap, HashMap},
11};
12use eyre::Report;
13use foundry_common::{get_contract_name, get_file_name, shell};
14use foundry_evm::{
15 core::Breakpoints,
16 coverage::HitMaps,
17 decode::SkipReason,
18 executors::{RawCallResult, invariant::InvariantMetrics},
19 fuzz::{CounterExample, FuzzCase, FuzzFixtures, FuzzTestResult},
20 traces::{CallTraceArena, CallTraceDecoder, TraceKind, Traces},
21};
22use serde::{Deserialize, Serialize};
23use std::{
24 collections::{BTreeMap, HashMap as Map},
25 fmt::{self, Write},
26 time::Duration,
27};
28use yansi::Paint;
29
30#[derive(Clone, Debug)]
32pub struct TestOutcome {
33 pub results: BTreeMap<String, SuiteResult>,
37 pub allow_failure: bool,
39 pub last_run_decoder: Option<CallTraceDecoder>,
45 pub gas_report: Option<GasReport>,
47 pub runner: Option<MultiContractRunner>,
49 pub fuzz_seed: Option<U256>,
51}
52
53impl TestOutcome {
54 pub fn new(
56 runner: Option<MultiContractRunner>,
57 results: BTreeMap<String, SuiteResult>,
58 allow_failure: bool,
59 fuzz_seed: Option<U256>,
60 ) -> Self {
61 Self { results, allow_failure, last_run_decoder: None, gas_report: None, runner, fuzz_seed }
62 }
63
64 pub fn empty(runner: Option<MultiContractRunner>, allow_failure: bool) -> Self {
66 Self::new(runner, BTreeMap::new(), allow_failure, None)
67 }
68
69 pub fn successes(&self) -> impl Iterator<Item = (&String, &TestResult)> {
71 self.tests().filter(|(_, t)| t.status.is_success())
72 }
73
74 pub fn skips(&self) -> impl Iterator<Item = (&String, &TestResult)> {
76 self.tests().filter(|(_, t)| t.status.is_skipped())
77 }
78
79 pub fn failures(&self) -> impl Iterator<Item = (&String, &TestResult)> {
81 self.tests().filter(|(_, t)| t.status.is_failure())
82 }
83
84 pub fn tests(&self) -> impl Iterator<Item = (&String, &TestResult)> {
86 self.results.values().flat_map(|suite| suite.tests())
87 }
88
89 pub fn into_tests_cloned(&self) -> impl Iterator<Item = SuiteTestResult> + '_ {
92 self.results
93 .iter()
94 .flat_map(|(file, suite)| {
95 suite
96 .test_results
97 .iter()
98 .map(move |(sig, result)| (file.clone(), sig.clone(), result.clone()))
99 })
100 .map(|(artifact_id, signature, result)| SuiteTestResult {
101 artifact_id,
102 signature,
103 result,
104 })
105 }
106
107 pub fn into_tests(self) -> impl Iterator<Item = SuiteTestResult> {
109 self.results
110 .into_iter()
111 .flat_map(|(file, suite)| {
112 suite.test_results.into_iter().map(move |t| (file.clone(), t))
113 })
114 .map(|(artifact_id, (signature, result))| SuiteTestResult {
115 artifact_id,
116 signature,
117 result,
118 })
119 }
120
121 pub fn passed(&self) -> usize {
123 self.successes().count()
124 }
125
126 pub fn skipped(&self) -> usize {
128 self.skips().count()
129 }
130
131 pub fn failed(&self) -> usize {
133 self.failures().count()
134 }
135
136 pub fn has_fuzz_failures(&self) -> bool {
138 self.failures().any(|(_, t)| t.kind.is_fuzz() || t.kind.is_invariant())
139 }
140
141 pub fn total_time(&self) -> Duration {
145 self.results.values().map(|suite| suite.duration).sum()
146 }
147
148 pub fn summary(&self, wall_clock_time: Duration) -> String {
150 let num_test_suites = self.results.len();
151 let suites = if num_test_suites == 1 { "suite" } else { "suites" };
152 let total_passed = self.passed();
153 let total_failed = self.failed();
154 let total_skipped = self.skipped();
155 let total_tests = total_passed + total_failed + total_skipped;
156 format!(
157 "\nRan {} test {} in {:.2?} ({:.2?} CPU time): {} tests passed, {} failed, {} skipped ({} total tests)",
158 num_test_suites,
159 suites,
160 wall_clock_time,
161 self.total_time(),
162 total_passed.green(),
163 total_failed.red(),
164 total_skipped.yellow(),
165 total_tests
166 )
167 }
168
169 pub fn ensure_ok(&self, silent: bool) -> eyre::Result<()> {
171 let outcome = self;
172 let failures = outcome.failures().count();
173 if outcome.allow_failure || failures == 0 {
174 return Ok(());
175 }
176
177 if shell::is_quiet() || silent {
178 std::process::exit(1);
179 }
180
181 sh_println!("\nFailing tests:")?;
182 for (suite_name, suite) in &outcome.results {
183 let failed = suite.failed();
184 if failed == 0 {
185 continue;
186 }
187
188 let term = if failed > 1 { "tests" } else { "test" };
189 sh_println!("Encountered {failed} failing {term} in {suite_name}")?;
190 for (name, result) in suite.failures() {
191 sh_println!("{}", result.short_result(name))?;
192 }
193 sh_println!()?;
194 }
195 let successes = outcome.passed();
196 sh_println!(
197 "Encountered a total of {} failing tests, {} tests succeeded",
198 failures.to_string().red(),
199 successes.to_string().green()
200 )?;
201
202 let test_word = if failures == 1 { "test" } else { "tests" };
204 sh_println!(
205 "\nTip: Run {} to retry only the {} failed {}",
206 "`forge test --rerun`".cyan(),
207 failures,
208 test_word
209 )?;
210
211 if let Some(seed) = self.fuzz_seed
213 && outcome.has_fuzz_failures()
214 {
215 sh_println!(
216 "\nFuzz seed: {} (use {} to reproduce)",
217 format!("{seed:#x}").cyan(),
218 "`--fuzz-seed`".cyan()
219 )?;
220 }
221
222 std::process::exit(1);
223 }
224
225 pub fn remove_first(&mut self) -> Option<(String, String, TestResult)> {
227 self.results.iter_mut().find_map(|(suite_name, suite)| {
228 if let Some(test_name) = suite.test_results.keys().next().cloned() {
229 let result = suite.test_results.remove(&test_name).unwrap();
230 Some((suite_name.clone(), test_name, result))
231 } else {
232 None
233 }
234 })
235 }
236}
237
238#[derive(Clone, Debug, Serialize)]
240pub struct SuiteResult {
241 #[serde(with = "foundry_common::serde_helpers::duration")]
243 pub duration: Duration,
244 pub test_results: BTreeMap<String, TestResult>,
246 pub warnings: Vec<String>,
248}
249
250impl SuiteResult {
251 pub fn new(
252 duration: Duration,
253 test_results: BTreeMap<String, TestResult>,
254 mut warnings: Vec<String>,
255 ) -> Self {
256 let mut deprecated_cheatcodes = HashMap::new();
258 for test_result in test_results.values() {
259 deprecated_cheatcodes.extend(test_result.deprecated_cheatcodes.clone());
260 }
261 if !deprecated_cheatcodes.is_empty() {
262 let mut warning =
263 "the following cheatcode(s) are deprecated and will be removed in future versions:"
264 .to_string();
265 for (cheatcode, reason) in deprecated_cheatcodes {
266 write!(warning, "\n {cheatcode}").unwrap();
267 if let Some(reason) = reason {
268 write!(warning, ": {reason}").unwrap();
269 }
270 }
271 warnings.push(warning);
272 }
273
274 Self { duration, test_results, warnings }
275 }
276
277 pub fn successes(&self) -> impl Iterator<Item = (&String, &TestResult)> {
279 self.tests().filter(|(_, t)| t.status.is_success())
280 }
281
282 pub fn skips(&self) -> impl Iterator<Item = (&String, &TestResult)> {
284 self.tests().filter(|(_, t)| t.status.is_skipped())
285 }
286
287 pub fn failures(&self) -> impl Iterator<Item = (&String, &TestResult)> {
289 self.tests().filter(|(_, t)| t.status.is_failure())
290 }
291
292 pub fn passed(&self) -> usize {
294 self.successes().count()
295 }
296
297 pub fn skipped(&self) -> usize {
299 self.skips().count()
300 }
301
302 pub fn failed(&self) -> usize {
304 self.failures().count()
305 }
306
307 pub fn tests(&self) -> impl Iterator<Item = (&String, &TestResult)> {
309 self.test_results.iter()
310 }
311
312 pub fn is_empty(&self) -> bool {
314 self.test_results.is_empty()
315 }
316
317 pub fn len(&self) -> usize {
319 self.test_results.len()
320 }
321
322 pub fn total_time(&self) -> Duration {
326 self.test_results.values().map(|result| result.duration).sum()
327 }
328
329 pub fn summary(&self) -> String {
331 let failed = self.failed();
332 let result = if failed == 0 { "ok".green() } else { "FAILED".red() };
333 format!(
334 "Suite result: {}. {} passed; {} failed; {} skipped; finished in {:.2?} ({:.2?} CPU time)",
335 result,
336 self.passed().green(),
337 failed.red(),
338 self.skipped().yellow(),
339 self.duration,
340 self.total_time(),
341 )
342 }
343}
344
345#[derive(Clone, Debug)]
349pub struct SuiteTestResult {
350 pub artifact_id: String,
353 pub signature: String,
355 pub result: TestResult,
357}
358
359impl SuiteTestResult {
360 pub fn gas_used(&self) -> u64 {
362 self.result.kind.report().gas()
363 }
364
365 pub fn contract_name(&self) -> &str {
367 get_contract_name(&self.artifact_id)
368 }
369
370 pub fn file_name(&self) -> &str {
372 get_file_name(&self.artifact_id)
373 }
374}
375
376#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
378pub enum TestStatus {
379 Success,
380 #[default]
381 Failure,
382 Skipped,
383}
384
385impl TestStatus {
386 #[inline]
388 pub fn is_success(self) -> bool {
389 matches!(self, Self::Success)
390 }
391
392 #[inline]
394 pub fn is_failure(self) -> bool {
395 matches!(self, Self::Failure)
396 }
397
398 #[inline]
400 pub fn is_skipped(self) -> bool {
401 matches!(self, Self::Skipped)
402 }
403}
404
405#[derive(Clone, Debug, Default, Serialize, Deserialize)]
407pub struct TestResult {
408 pub status: TestStatus,
413
414 pub reason: Option<String>,
417
418 pub counterexample: Option<CounterExample>,
420
421 pub logs: Vec<Log>,
424
425 pub decoded_logs: Vec<String>,
428
429 pub kind: TestKind,
431
432 pub traces: Traces,
434
435 #[serde(skip)]
439 pub gas_report_traces: Vec<Vec<CallTraceArena>>,
440
441 #[serde(skip)]
443 pub line_coverage: Option<HitMaps>,
444
445 #[serde(rename = "labeled_addresses")] pub labels: AddressHashMap<String>,
448
449 #[serde(with = "foundry_common::serde_helpers::duration")]
450 pub duration: Duration,
451
452 pub breakpoints: Breakpoints,
454
455 pub gas_snapshots: BTreeMap<String, BTreeMap<String, String>>,
457
458 #[serde(skip)]
460 pub deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>,
461}
462
463impl fmt::Display for TestResult {
464 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
465 match self.status {
466 TestStatus::Success => {
467 if let Some(CounterExample::Sequence(original, sequence)) = &self.counterexample {
469 let mut s = String::from("[PASS]");
470 s.push_str(
471 format!(
472 "\n\t[Best sequence] (original: {original}, shrunk: {})\n",
473 sequence.len()
474 )
475 .as_str(),
476 );
477 for ex in sequence {
478 writeln!(s, "{ex}").unwrap();
479 }
480 s.green().wrap().fmt(f)
481 } else {
482 "[PASS]".green().fmt(f)
483 }
484 }
485 TestStatus::Skipped => {
486 let mut s = String::from("[SKIP");
487 if let Some(reason) = &self.reason {
488 write!(s, ": {reason}").unwrap();
489 }
490 s.push(']');
491 s.yellow().fmt(f)
492 }
493 TestStatus::Failure => {
494 let mut s = String::from("[FAIL");
495 if self.reason.is_some() || self.counterexample.is_some() {
496 if let Some(reason) = &self.reason {
497 write!(s, ": {reason}").unwrap();
498 }
499
500 if let Some(counterexample) = &self.counterexample {
501 match counterexample {
502 CounterExample::Single(ex) => {
503 write!(s, "; counterexample: {ex}]").unwrap();
504 }
505 CounterExample::Sequence(original, sequence) => {
506 s.push_str(
507 format!(
508 "]\n\t[Sequence] (original: {original}, shrunk: {})\n",
509 sequence.len()
510 )
511 .as_str(),
512 );
513 for ex in sequence {
514 writeln!(s, "{ex}").unwrap();
515 }
516 }
517 }
518 } else {
519 s.push(']');
520 }
521 } else {
522 s.push(']');
523 }
524 s.red().wrap().fmt(f)
525 }
526 }
527 }
528}
529
530macro_rules! extend {
531 ($a:expr, $b:expr, $trace_kind:expr) => {
532 $a.logs.extend($b.logs);
533 $a.labels.extend($b.labels);
534 $a.traces.extend($b.traces.map(|traces| ($trace_kind, traces)));
535 $a.merge_coverages($b.line_coverage);
536 };
537}
538
539impl TestResult {
540 pub fn new(setup: &TestSetup) -> Self {
542 Self {
543 labels: setup.labels.clone(),
544 logs: setup.logs.clone(),
545 traces: setup.traces.clone(),
546 line_coverage: setup.coverage.clone(),
547 ..Default::default()
548 }
549 }
550
551 pub fn fail(reason: String) -> Self {
553 Self { status: TestStatus::Failure, reason: Some(reason), ..Default::default() }
554 }
555
556 pub fn setup_result(setup: TestSetup) -> Self {
558 let TestSetup {
559 address: _,
560 fuzz_fixtures: _,
561 logs,
562 labels,
563 traces,
564 coverage,
565 deployed_libs: _,
566 reason,
567 skipped,
568 deployment_failure: _,
569 } = setup;
570 Self {
571 status: if skipped { TestStatus::Skipped } else { TestStatus::Failure },
572 reason,
573 logs,
574 traces,
575 line_coverage: coverage,
576 labels,
577 ..Default::default()
578 }
579 }
580
581 pub fn single_skip(&mut self, reason: SkipReason) {
583 self.status = TestStatus::Skipped;
584 self.reason = reason.0;
585 }
586
587 pub fn single_fail(&mut self, reason: Option<String>) {
589 self.status = TestStatus::Failure;
590 self.reason = reason;
591 }
592
593 pub fn single_result(
596 &mut self,
597 success: bool,
598 reason: Option<String>,
599 raw_call_result: RawCallResult,
600 ) {
601 self.kind = TestKind::Unit {
602 gas: raw_call_result.gas_used.saturating_sub(raw_call_result.stipend),
603 };
604
605 extend!(self, raw_call_result, TraceKind::Execution);
606
607 self.status = match success {
608 true => TestStatus::Success,
609 false => TestStatus::Failure,
610 };
611 self.reason = reason;
612 self.duration = Duration::default();
613 self.gas_report_traces = Vec::new();
614
615 if let Some(cheatcodes) = raw_call_result.cheatcodes {
616 self.breakpoints = cheatcodes.breakpoints;
617 self.gas_snapshots = cheatcodes.gas_snapshots;
618 self.deprecated_cheatcodes = cheatcodes.deprecated;
619 }
620 }
621
622 pub fn fuzz_result(&mut self, result: FuzzTestResult) {
625 self.kind = TestKind::Fuzz {
626 median_gas: result.median_gas(false),
627 mean_gas: result.mean_gas(false),
628 first_case: result.first_case,
629 runs: result.gas_by_case.len(),
630 failed_corpus_replays: result.failed_corpus_replays,
631 };
632
633 extend!(self, result, TraceKind::Execution);
635
636 self.status = if result.skipped {
637 TestStatus::Skipped
638 } else if result.success {
639 TestStatus::Success
640 } else {
641 TestStatus::Failure
642 };
643 self.reason = result.reason;
644 self.counterexample = result.counterexample;
645 self.duration = Duration::default();
646 self.gas_report_traces = result.gas_report_traces.into_iter().map(|t| vec![t]).collect();
647 self.breakpoints = result.breakpoints.unwrap_or_default();
648 self.deprecated_cheatcodes = result.deprecated_cheatcodes;
649 }
650
651 pub fn fuzz_setup_fail(&mut self, e: Report) {
653 self.kind = TestKind::Fuzz {
654 first_case: Default::default(),
655 runs: 0,
656 mean_gas: 0,
657 median_gas: 0,
658 failed_corpus_replays: 0,
659 };
660 self.status = TestStatus::Failure;
661 debug!(?e, "failed to set up fuzz testing environment");
662 self.reason = Some(format!("failed to set up fuzz testing environment: {e}"));
663 }
664
665 pub fn invariant_skip(&mut self, reason: SkipReason) {
667 self.kind = TestKind::Invariant {
668 runs: 1,
669 calls: 1,
670 reverts: 1,
671 metrics: HashMap::default(),
672 failed_corpus_replays: 0,
673 optimization_best_value: None,
674 };
675 self.status = TestStatus::Skipped;
676 self.reason = reason.0;
677 }
678
679 pub fn invariant_replay_fail(
681 &mut self,
682 replayed_entirely: bool,
683 invariant_name: &String,
684 call_sequence: Vec<BaseCounterExample>,
685 ) {
686 self.kind = TestKind::Invariant {
687 runs: 1,
688 calls: 1,
689 reverts: 1,
690 metrics: HashMap::default(),
691 failed_corpus_replays: 0,
692 optimization_best_value: None,
693 };
694 self.status = TestStatus::Failure;
695 self.reason = if replayed_entirely {
696 Some(format!("{invariant_name} replay failure"))
697 } else {
698 Some(format!("{invariant_name} persisted failure revert"))
699 };
700 self.counterexample = Some(CounterExample::Sequence(call_sequence.len(), call_sequence));
701 }
702
703 pub fn invariant_setup_fail(&mut self, e: Report) {
705 self.kind = TestKind::Invariant {
706 runs: 0,
707 calls: 0,
708 reverts: 0,
709 metrics: HashMap::default(),
710 failed_corpus_replays: 0,
711 optimization_best_value: None,
712 };
713 self.status = TestStatus::Failure;
714 self.reason = Some(format!("failed to set up invariant testing environment: {e}"));
715 }
716
717 #[expect(clippy::too_many_arguments)]
719 pub fn invariant_result(
720 &mut self,
721 gas_report_traces: Vec<Vec<CallTraceArena>>,
722 success: bool,
723 reason: Option<String>,
724 counterexample: Option<CounterExample>,
725 cases: Vec<FuzzedCases>,
726 reverts: usize,
727 metrics: Map<String, InvariantMetrics>,
728 failed_corpus_replays: usize,
729 optimization_best_value: Option<I256>,
730 ) {
731 self.kind = TestKind::Invariant {
732 runs: cases.len(),
733 calls: cases.iter().map(|sequence| sequence.cases().len()).sum(),
734 reverts,
735 metrics,
736 failed_corpus_replays,
737 optimization_best_value,
738 };
739 self.status = if optimization_best_value.is_some() || success {
741 TestStatus::Success
742 } else {
743 TestStatus::Failure
744 };
745 self.reason = reason;
746 self.counterexample = counterexample;
747 self.gas_report_traces = gas_report_traces;
748 }
749
750 pub fn table_result(&mut self, result: FuzzTestResult) {
753 self.kind = TestKind::Table {
754 median_gas: result.median_gas(false),
755 mean_gas: result.mean_gas(false),
756 runs: result.gas_by_case.len(),
757 };
758
759 extend!(self, result, TraceKind::Execution);
761
762 self.status = if result.skipped {
763 TestStatus::Skipped
764 } else if result.success {
765 TestStatus::Success
766 } else {
767 TestStatus::Failure
768 };
769 self.reason = result.reason;
770 self.counterexample = result.counterexample;
771 self.duration = Duration::default();
772 self.gas_report_traces = result.gas_report_traces.into_iter().map(|t| vec![t]).collect();
773 self.breakpoints = result.breakpoints.unwrap_or_default();
774 self.deprecated_cheatcodes = result.deprecated_cheatcodes;
775 }
776
777 pub fn is_fuzz(&self) -> bool {
779 matches!(self.kind, TestKind::Fuzz { .. })
780 }
781
782 pub fn short_result(&self, name: &str) -> String {
784 format!("{self} {name} {}", self.kind.report())
785 }
786
787 pub fn extend(&mut self, call_result: RawCallResult) {
789 extend!(self, call_result, TraceKind::Execution);
790 }
791
792 pub fn merge_coverages(&mut self, other_coverage: Option<HitMaps>) {
794 HitMaps::merge_opt(&mut self.line_coverage, other_coverage);
795 }
796}
797
798#[derive(Clone, Debug, PartialEq, Eq)]
800pub enum TestKindReport {
801 Unit {
802 gas: u64,
803 },
804 Fuzz {
805 runs: usize,
806 mean_gas: u64,
807 median_gas: u64,
808 failed_corpus_replays: usize,
809 },
810 Invariant {
811 runs: usize,
812 calls: usize,
813 reverts: usize,
814 metrics: Map<String, InvariantMetrics>,
815 failed_corpus_replays: usize,
816 optimization_best_value: Option<I256>,
818 },
819 Table {
820 runs: usize,
821 mean_gas: u64,
822 median_gas: u64,
823 },
824}
825
826impl fmt::Display for TestKindReport {
827 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
828 match self {
829 Self::Unit { gas } => {
830 write!(f, "(gas: {gas})")
831 }
832 Self::Fuzz { runs, mean_gas, median_gas, failed_corpus_replays } => {
833 if *failed_corpus_replays != 0 {
834 write!(
835 f,
836 "(runs: {runs}, μ: {mean_gas}, ~: {median_gas}, failed corpus replays: {failed_corpus_replays})"
837 )
838 } else {
839 write!(f, "(runs: {runs}, μ: {mean_gas}, ~: {median_gas})")
840 }
841 }
842 Self::Invariant {
843 runs,
844 calls,
845 reverts,
846 metrics: _,
847 failed_corpus_replays,
848 optimization_best_value,
849 } => {
850 if let Some(best_value) = optimization_best_value {
852 write!(f, "(best: {best_value}, runs: {runs}, calls: {calls})")
853 } else if *failed_corpus_replays != 0 {
854 write!(
855 f,
856 "(runs: {runs}, calls: {calls}, reverts: {reverts}, failed corpus replays: {failed_corpus_replays})"
857 )
858 } else {
859 write!(f, "(runs: {runs}, calls: {calls}, reverts: {reverts})")
860 }
861 }
862 Self::Table { runs, mean_gas, median_gas } => {
863 write!(f, "(runs: {runs}, μ: {mean_gas}, ~: {median_gas})")
864 }
865 }
866 }
867}
868
869impl TestKindReport {
870 pub fn gas(&self) -> u64 {
872 match *self {
873 Self::Unit { gas } => gas,
874 Self::Fuzz { median_gas, .. } | Self::Table { median_gas, .. } => median_gas,
876 Self::Invariant { .. } => 0,
878 }
879 }
880}
881
882#[derive(Clone, Debug, Serialize, Deserialize)]
884pub enum TestKind {
885 Unit { gas: u64 },
887 Fuzz {
889 first_case: FuzzCase,
891 runs: usize,
892 mean_gas: u64,
893 median_gas: u64,
894 failed_corpus_replays: usize,
895 },
896 Invariant {
898 runs: usize,
899 calls: usize,
900 reverts: usize,
901 metrics: Map<String, InvariantMetrics>,
902 failed_corpus_replays: usize,
903 optimization_best_value: Option<I256>,
905 },
906 Table { runs: usize, mean_gas: u64, median_gas: u64 },
908}
909
910impl Default for TestKind {
911 fn default() -> Self {
912 Self::Unit { gas: 0 }
913 }
914}
915
916impl TestKind {
917 pub fn is_fuzz(&self) -> bool {
919 matches!(self, Self::Fuzz { .. })
920 }
921
922 pub fn is_invariant(&self) -> bool {
924 matches!(self, Self::Invariant { .. })
925 }
926
927 pub fn report(&self) -> TestKindReport {
929 match self {
930 Self::Unit { gas } => TestKindReport::Unit { gas: *gas },
931 Self::Fuzz { first_case: _, runs, mean_gas, median_gas, failed_corpus_replays } => {
932 TestKindReport::Fuzz {
933 runs: *runs,
934 mean_gas: *mean_gas,
935 median_gas: *median_gas,
936 failed_corpus_replays: *failed_corpus_replays,
937 }
938 }
939 Self::Invariant {
940 runs,
941 calls,
942 reverts,
943 metrics: _,
944 failed_corpus_replays,
945 optimization_best_value,
946 } => TestKindReport::Invariant {
947 runs: *runs,
948 calls: *calls,
949 reverts: *reverts,
950 metrics: HashMap::default(),
951 failed_corpus_replays: *failed_corpus_replays,
952 optimization_best_value: *optimization_best_value,
953 },
954 Self::Table { runs, mean_gas, median_gas } => {
955 TestKindReport::Table { runs: *runs, mean_gas: *mean_gas, median_gas: *median_gas }
956 }
957 }
958 }
959}
960
961#[derive(Clone, Debug, Default)]
966pub struct TestSetup {
967 pub address: Address,
969 pub fuzz_fixtures: FuzzFixtures,
971
972 pub logs: Vec<Log>,
974 pub labels: AddressHashMap<String>,
976 pub traces: Traces,
978 pub coverage: Option<HitMaps>,
980 pub deployed_libs: Vec<Address>,
982
983 pub reason: Option<String>,
985 pub skipped: bool,
987 pub deployment_failure: bool,
989}
990
991impl TestSetup {
992 pub fn failed(reason: String) -> Self {
993 Self { reason: Some(reason), ..Default::default() }
994 }
995
996 pub fn skipped(reason: String) -> Self {
997 Self { reason: Some(reason), skipped: true, ..Default::default() }
998 }
999
1000 pub fn extend(&mut self, raw: RawCallResult, trace_kind: TraceKind) {
1001 extend!(self, raw, trace_kind);
1002 }
1003
1004 pub fn merge_coverages(&mut self, other_coverage: Option<HitMaps>) {
1005 HitMaps::merge_opt(&mut self.coverage, other_coverage);
1006 }
1007}