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