1use crate::{
4 fuzz::{BaseCounterExample, FuzzedCases},
5 gas_report::GasReport,
6};
7use alloy_primitives::{
8 Address, I256, Log, Selector, 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, Serialize, Deserialize)]
414#[serde(tag = "kind", rename_all = "snake_case")]
415pub enum InvariantFailure {
416 Predicate {
418 name: String,
420 reason: String,
422 #[serde(default, skip_serializing_if = "Option::is_none")]
424 counterexample: Option<CounterExample>,
425 persisted_path: std::path::PathBuf,
427 #[serde(default)]
431 is_anchor: bool,
432 },
433 Handler {
435 name: String,
438 reverter: Address,
440 selector: Selector,
442 reason: String,
444 #[serde(default, skip_serializing_if = "Option::is_none")]
446 counterexample: Option<CounterExample>,
447 },
448}
449
450impl InvariantFailure {
451 pub fn reason(&self) -> &str {
453 match self {
454 Self::Predicate { reason, .. } | Self::Handler { reason, .. } => reason,
455 }
456 }
457
458 pub fn name(&self) -> &str {
460 match self {
461 Self::Predicate { name, .. } | Self::Handler { name, .. } => name,
462 }
463 }
464
465 pub const fn counterexample(&self) -> Option<&CounterExample> {
467 match self {
468 Self::Predicate { counterexample, .. } | Self::Handler { counterexample, .. } => {
469 counterexample.as_ref()
470 }
471 }
472 }
473}
474
475#[derive(Clone, Debug, Default, Serialize, Deserialize)]
477pub struct TestResult {
478 pub status: TestStatus,
483
484 pub reason: Option<String>,
487
488 #[serde(default, skip_serializing_if = "Vec::is_empty")]
494 pub invariant_failures: Vec<InvariantFailure>,
495
496 #[serde(default, skip_serializing_if = "Option::is_none")]
499 pub invariant_failure_dir: Option<std::path::PathBuf>,
500
501 #[serde(default, skip_serializing_if = "Option::is_none")]
506 pub assert_all_invariant_count: Option<usize>,
507
508 #[serde(default, skip_serializing_if = "Vec::is_empty")]
512 pub invariant_handler_failures: Vec<InvariantFailure>,
513
514 pub counterexample: Option<CounterExample>,
516
517 pub logs: Vec<Log>,
520
521 pub decoded_logs: Vec<String>,
524
525 pub kind: TestKind,
527
528 pub traces: Traces,
530
531 #[serde(skip)]
535 pub gas_report_traces: Vec<Vec<CallTraceArena>>,
536
537 #[serde(skip)]
539 pub line_coverage: Option<HitMaps>,
540
541 #[serde(rename = "labeled_addresses")] pub labels: AddressHashMap<String>,
544
545 #[serde(with = "foundry_common::serde_helpers::duration")]
546 pub duration: Duration,
547
548 pub breakpoints: Breakpoints,
550
551 pub gas_snapshots: BTreeMap<String, BTreeMap<String, String>>,
553
554 #[serde(skip)]
556 pub deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>,
557}
558
559impl fmt::Display for TestResult {
560 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
561 match self.status {
562 TestStatus::Success => {
563 if let Some(CounterExample::Sequence(original, sequence)) = &self.counterexample {
565 let mut s = String::from("[PASS]");
566 s.push_str(
567 format!(
568 "\n\t[Best sequence] (original: {original}, shrunk: {})\n",
569 sequence.len()
570 )
571 .as_str(),
572 );
573 for ex in sequence {
574 writeln!(s, "{ex}").unwrap();
575 }
576 s.green().wrap().fmt(f)
577 } else {
578 "[PASS]".green().fmt(f)
579 }
580 }
581 TestStatus::Skipped => {
582 let mut s = String::from("[SKIP");
583 if let Some(reason) = &self.reason {
584 write!(s, ": {reason}").unwrap();
585 }
586 s.push(']');
587 s.yellow().fmt(f)
588 }
589 TestStatus::Failure => {
590 let mut s = String::new();
591 let has_handler_failures = !self.invariant_handler_failures.is_empty();
592 let is_invariant_failure =
593 !self.invariant_failures.is_empty() || has_handler_failures;
594 if !is_invariant_failure {
595 s.push_str("[FAIL");
598 if let Some(reason) = &self.reason {
599 write!(s, ": {reason}").unwrap();
600 }
601 if let Some(counterexample) = &self.counterexample {
602 match counterexample {
603 CounterExample::Single(ex) => {
604 write!(s, "; counterexample: {ex}]").unwrap();
605 }
606 CounterExample::Sequence(original, sequence) => {
607 writeln!(
608 s,
609 "]\n\t[Sequence] (original: {original}, shrunk: {})",
610 sequence.len()
611 )
612 .unwrap();
613 for ex in sequence {
614 writeln!(s, "{ex}").unwrap();
615 }
616 }
617 }
618 } else {
619 s.push(']');
620 }
621 } else if !self.invariant_failures.is_empty() {
622 let multi = self.invariant_failures.len() > 1;
626 for (i, failure) in self.invariant_failures.iter().enumerate() {
627 if i > 0 {
628 s.push('\n');
629 }
630 let is_anchor =
632 matches!(failure, InvariantFailure::Predicate { is_anchor: true, .. });
633 let name_suffix = if multi || !is_anchor {
634 format!(" {}", failure.name())
635 } else {
636 String::new()
637 };
638 if let Some(CounterExample::Sequence(original, sequence)) =
641 failure.counterexample()
642 {
643 writeln!(
644 s,
645 "[FAIL: {}]{name_suffix}\n\t[Sequence] (original: {original}, shrunk: {})",
646 failure.reason(),
647 sequence.len()
648 )
649 .unwrap();
650 for ex in sequence {
651 writeln!(s, "{ex}").unwrap();
652 }
653 } else {
654 write!(s, "[FAIL: {}]{name_suffix}", failure.reason()).unwrap();
655 }
656 }
657 }
658 if let Some(total) = self.assert_all_invariant_count
661 && total > 1
662 && is_invariant_failure
663 {
664 writeln!(
665 s,
666 "\nSuite assert_all: {}/{total} invariants broken",
667 self.invariant_failures.len()
668 )
669 .unwrap();
670 }
671 if self.invariant_failures.len() > 1
674 && let Some(dir) = &self.invariant_failure_dir
675 {
676 writeln!(
677 s,
678 "{} invariant failure(s) persisted to {} — rerun to shrink",
679 self.invariant_failures.len(),
680 dir.display()
681 )
682 .unwrap();
683 }
684 if has_handler_failures {
688 let preceded = !self.invariant_failures.is_empty()
690 || matches!(self.assert_all_invariant_count, Some(t) if t > 1);
691 let prefix = if preceded { "\n" } else { "" };
692 writeln!(
693 s,
694 "{prefix}Suite handlers: {} assertion bug(s) found",
695 self.invariant_handler_failures.len()
696 )
697 .unwrap();
698 for failure in &self.invariant_handler_failures {
699 if let Some(CounterExample::Sequence(original, sequence)) =
700 failure.counterexample()
701 {
702 writeln!(
703 s,
704 "[FAIL: {}] {}\n\t[Sequence] (original: {original}, shrunk: {})",
705 failure.reason(),
706 failure.name(),
707 sequence.len()
708 )
709 .unwrap();
710 for ex in sequence {
711 writeln!(s, "{ex}").unwrap();
712 }
713 } else {
714 writeln!(s, "[FAIL: {}] {}", failure.reason(), failure.name()).unwrap();
715 }
716 }
717 }
718 s.red().wrap().fmt(f)
719 }
720 }
721 }
722}
723
724macro_rules! extend {
725 ($a:expr, $b:expr, $trace_kind:expr) => {
726 $a.logs.extend($b.logs);
727 $a.labels.extend($b.labels);
728 $a.traces.extend($b.traces.map(|traces| ($trace_kind, traces)));
729 $a.merge_coverages($b.line_coverage);
730 };
731}
732
733impl TestResult {
734 pub fn new(setup: &TestSetup) -> Self {
736 Self {
737 labels: setup.labels.clone(),
738 logs: setup.logs.clone(),
739 traces: setup.traces.clone(),
740 line_coverage: setup.coverage.clone(),
741 ..Default::default()
742 }
743 }
744
745 pub fn fail(reason: String) -> Self {
747 Self { status: TestStatus::Failure, reason: Some(reason), ..Default::default() }
748 }
749
750 pub fn setup_result(setup: TestSetup) -> Self {
752 let TestSetup {
753 address: _,
754 fuzz_fixtures: _,
755 logs,
756 labels,
757 traces,
758 coverage,
759 deployed_libs: _,
760 reason,
761 skipped,
762 deployment_failure: _,
763 } = setup;
764 Self {
765 status: if skipped { TestStatus::Skipped } else { TestStatus::Failure },
766 reason,
767 logs,
768 traces,
769 line_coverage: coverage,
770 labels,
771 ..Default::default()
772 }
773 }
774
775 pub fn single_skip(&mut self, reason: SkipReason) {
777 self.status = TestStatus::Skipped;
778 self.reason = reason.0;
779 }
780
781 pub fn single_fail(&mut self, reason: Option<String>) {
783 self.status = TestStatus::Failure;
784 self.reason = reason;
785 }
786
787 pub fn single_result<FEN: FoundryEvmNetwork>(
790 &mut self,
791 success: bool,
792 reason: Option<String>,
793 raw_call_result: RawCallResult<FEN>,
794 ) {
795 self.kind = TestKind::Unit {
796 gas: raw_call_result.gas_used.saturating_sub(raw_call_result.stipend),
797 };
798
799 extend!(self, raw_call_result, TraceKind::Execution);
800
801 self.status = match success {
802 true => TestStatus::Success,
803 false => TestStatus::Failure,
804 };
805 self.reason = reason;
806 self.duration = Duration::default();
807 self.gas_report_traces = Vec::new();
808
809 if let Some(cheatcodes) = raw_call_result.cheatcodes {
810 self.breakpoints = cheatcodes.breakpoints;
811 self.gas_snapshots = cheatcodes.gas_snapshots;
812 self.deprecated_cheatcodes = cheatcodes.deprecated;
813 }
814 }
815
816 pub fn fuzz_result(&mut self, result: FuzzTestResult) {
819 self.kind = TestKind::Fuzz {
820 median_gas: result.median_gas(false),
821 mean_gas: result.mean_gas(false),
822 first_case: result.first_case,
823 runs: result.gas_by_case.len(),
824 failed_corpus_replays: result.failed_corpus_replays,
825 };
826
827 extend!(self, result, TraceKind::Execution);
829
830 self.status = if result.skipped {
831 TestStatus::Skipped
832 } else if result.success {
833 TestStatus::Success
834 } else {
835 TestStatus::Failure
836 };
837 self.reason = result.reason;
838 self.counterexample = result.counterexample;
839 self.duration = Duration::default();
840 self.gas_report_traces = result.gas_report_traces.into_iter().map(|t| vec![t]).collect();
841 self.breakpoints = result.breakpoints.unwrap_or_default();
842 self.deprecated_cheatcodes = result.deprecated_cheatcodes;
843 }
844
845 pub fn fuzz_setup_fail(&mut self, e: Report) {
847 self.kind = TestKind::Fuzz {
848 first_case: Default::default(),
849 runs: 0,
850 mean_gas: 0,
851 median_gas: 0,
852 failed_corpus_replays: 0,
853 };
854 self.status = TestStatus::Failure;
855 debug!(?e, "failed to set up fuzz testing environment");
856 self.reason = Some(format!("failed to set up fuzz testing environment: {e}"));
857 }
858
859 pub fn invariant_skip(&mut self, reason: SkipReason) {
861 self.kind = TestKind::Invariant {
862 runs: 1,
863 calls: 1,
864 reverts: 1,
865 metrics: HashMap::default(),
866 failed_corpus_replays: 0,
867 optimization_best_value: None,
868 };
869 self.status = TestStatus::Skipped;
870 self.reason = reason.0;
871 }
872
873 pub fn invariant_replay_fail(
875 &mut self,
876 replayed_entirely: bool,
877 invariant_name: &String,
878 replay_reason: Option<String>,
879 call_sequence: Vec<BaseCounterExample>,
880 ) {
881 self.kind = TestKind::Invariant {
882 runs: 1,
883 calls: 1,
884 reverts: 1,
885 metrics: HashMap::default(),
886 failed_corpus_replays: 0,
887 optimization_best_value: None,
888 };
889 self.status = TestStatus::Failure;
890 self.reason = replay_reason.or_else(|| {
891 if replayed_entirely {
892 Some(format!("{invariant_name} replay failure"))
893 } else {
894 Some(format!("{invariant_name} persisted failure revert"))
895 }
896 });
897 self.counterexample = Some(CounterExample::Sequence(call_sequence.len(), call_sequence));
898 }
899
900 pub fn invariant_setup_fail(&mut self, e: Report) {
902 self.kind = TestKind::Invariant {
903 runs: 0,
904 calls: 0,
905 reverts: 0,
906 metrics: HashMap::default(),
907 failed_corpus_replays: 0,
908 optimization_best_value: None,
909 };
910 self.status = TestStatus::Failure;
911 self.reason = Some(format!("failed to set up invariant testing environment: {e}"));
912 }
913
914 #[expect(clippy::too_many_arguments)]
916 pub fn invariant_result(
917 &mut self,
918 gas_report_traces: Vec<Vec<CallTraceArena>>,
919 success: bool,
920 invariant_failures: Vec<InvariantFailure>,
921 invariant_failure_dir: Option<std::path::PathBuf>,
922 assert_all_invariant_count: Option<usize>,
923 invariant_handler_failures: Vec<InvariantFailure>,
924 counterexample: Option<CounterExample>,
925 cases: Vec<FuzzedCases>,
926 reverts: usize,
927 metrics: Map<String, InvariantMetrics>,
928 failed_corpus_replays: usize,
929 optimization_best_value: Option<I256>,
930 ) {
931 self.kind = TestKind::Invariant {
932 runs: cases.len(),
933 calls: cases.iter().map(|sequence| sequence.cases().len()).sum(),
934 reverts,
935 metrics,
936 failed_corpus_replays,
937 optimization_best_value,
938 };
939 self.status = if optimization_best_value.is_some() || success {
941 TestStatus::Success
942 } else {
943 TestStatus::Failure
944 };
945 self.invariant_failures = invariant_failures;
946 self.invariant_failure_dir = invariant_failure_dir;
947 self.assert_all_invariant_count = assert_all_invariant_count;
948 self.invariant_handler_failures = invariant_handler_failures;
949 self.counterexample = counterexample;
953 self.gas_report_traces = gas_report_traces;
954 }
955
956 pub fn table_result(&mut self, result: FuzzTestResult) {
959 self.kind = TestKind::Table {
960 median_gas: result.median_gas(false),
961 mean_gas: result.mean_gas(false),
962 runs: result.gas_by_case.len(),
963 };
964
965 extend!(self, result, TraceKind::Execution);
967
968 self.status = if result.skipped {
969 TestStatus::Skipped
970 } else if result.success {
971 TestStatus::Success
972 } else {
973 TestStatus::Failure
974 };
975 self.reason = result.reason;
976 self.counterexample = result.counterexample;
977 self.duration = Duration::default();
978 self.gas_report_traces = result.gas_report_traces.into_iter().map(|t| vec![t]).collect();
979 self.breakpoints = result.breakpoints.unwrap_or_default();
980 self.deprecated_cheatcodes = result.deprecated_cheatcodes;
981 }
982
983 pub const fn is_fuzz(&self) -> bool {
985 matches!(self.kind, TestKind::Fuzz { .. })
986 }
987
988 pub fn short_result(&self, name: &str) -> String {
990 format!("{self} {name} {}", self.kind.report())
991 }
992
993 pub fn extend<FEN: FoundryEvmNetwork>(&mut self, call_result: RawCallResult<FEN>) {
995 extend!(self, call_result, TraceKind::Execution);
996 }
997
998 pub fn merge_coverages(&mut self, other_coverage: Option<HitMaps>) {
1000 HitMaps::merge_opt(&mut self.line_coverage, other_coverage);
1001 }
1002}
1003
1004#[derive(Clone, Debug, PartialEq, Eq)]
1006pub enum TestKindReport {
1007 Unit {
1008 gas: u64,
1009 },
1010 Fuzz {
1011 runs: usize,
1012 mean_gas: u64,
1013 median_gas: u64,
1014 failed_corpus_replays: usize,
1015 },
1016 Invariant {
1017 runs: usize,
1018 calls: usize,
1019 reverts: usize,
1020 metrics: Map<String, InvariantMetrics>,
1021 failed_corpus_replays: usize,
1022 optimization_best_value: Option<I256>,
1024 },
1025 Table {
1026 runs: usize,
1027 mean_gas: u64,
1028 median_gas: u64,
1029 },
1030}
1031
1032impl fmt::Display for TestKindReport {
1033 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1034 match self {
1035 Self::Unit { gas } => {
1036 write!(f, "(gas: {gas})")
1037 }
1038 Self::Fuzz { runs, mean_gas, median_gas, failed_corpus_replays } => {
1039 if *failed_corpus_replays != 0 {
1040 write!(
1041 f,
1042 "(runs: {runs}, μ: {mean_gas}, ~: {median_gas}, failed corpus replays: {failed_corpus_replays})"
1043 )
1044 } else {
1045 write!(f, "(runs: {runs}, μ: {mean_gas}, ~: {median_gas})")
1046 }
1047 }
1048 Self::Invariant {
1049 runs,
1050 calls,
1051 reverts,
1052 metrics: _,
1053 failed_corpus_replays,
1054 optimization_best_value,
1055 } => {
1056 if let Some(best_value) = optimization_best_value {
1058 write!(f, "(best: {best_value}, runs: {runs}, calls: {calls})")
1059 } else if *failed_corpus_replays != 0 {
1060 write!(
1061 f,
1062 "(runs: {runs}, calls: {calls}, reverts: {reverts}, failed corpus replays: {failed_corpus_replays})"
1063 )
1064 } else {
1065 write!(f, "(runs: {runs}, calls: {calls}, reverts: {reverts})")
1066 }
1067 }
1068 Self::Table { runs, mean_gas, median_gas } => {
1069 write!(f, "(runs: {runs}, μ: {mean_gas}, ~: {median_gas})")
1070 }
1071 }
1072 }
1073}
1074
1075impl TestKindReport {
1076 pub const fn gas(&self) -> u64 {
1078 match *self {
1079 Self::Unit { gas } => gas,
1080 Self::Fuzz { median_gas, .. } | Self::Table { median_gas, .. } => median_gas,
1082 Self::Invariant { .. } => 0,
1084 }
1085 }
1086}
1087
1088#[derive(Clone, Debug, Serialize, Deserialize)]
1090pub enum TestKind {
1091 Unit { gas: u64 },
1093 Fuzz {
1095 first_case: FuzzCase,
1097 runs: usize,
1098 mean_gas: u64,
1099 median_gas: u64,
1100 failed_corpus_replays: usize,
1101 },
1102 Invariant {
1104 runs: usize,
1105 calls: usize,
1106 reverts: usize,
1107 metrics: Map<String, InvariantMetrics>,
1108 failed_corpus_replays: usize,
1109 optimization_best_value: Option<I256>,
1111 },
1112 Table { runs: usize, mean_gas: u64, median_gas: u64 },
1114}
1115
1116impl Default for TestKind {
1117 fn default() -> Self {
1118 Self::Unit { gas: 0 }
1119 }
1120}
1121
1122impl TestKind {
1123 pub const fn is_fuzz(&self) -> bool {
1125 matches!(self, Self::Fuzz { .. })
1126 }
1127
1128 pub const fn is_invariant(&self) -> bool {
1130 matches!(self, Self::Invariant { .. })
1131 }
1132
1133 pub fn report(&self) -> TestKindReport {
1135 match self {
1136 Self::Unit { gas } => TestKindReport::Unit { gas: *gas },
1137 Self::Fuzz { first_case: _, runs, mean_gas, median_gas, failed_corpus_replays } => {
1138 TestKindReport::Fuzz {
1139 runs: *runs,
1140 mean_gas: *mean_gas,
1141 median_gas: *median_gas,
1142 failed_corpus_replays: *failed_corpus_replays,
1143 }
1144 }
1145 Self::Invariant {
1146 runs,
1147 calls,
1148 reverts,
1149 metrics: _,
1150 failed_corpus_replays,
1151 optimization_best_value,
1152 } => TestKindReport::Invariant {
1153 runs: *runs,
1154 calls: *calls,
1155 reverts: *reverts,
1156 metrics: HashMap::default(),
1157 failed_corpus_replays: *failed_corpus_replays,
1158 optimization_best_value: *optimization_best_value,
1159 },
1160 Self::Table { runs, mean_gas, median_gas } => {
1161 TestKindReport::Table { runs: *runs, mean_gas: *mean_gas, median_gas: *median_gas }
1162 }
1163 }
1164 }
1165}
1166
1167#[derive(Clone, Debug, Default)]
1172pub struct TestSetup {
1173 pub address: Address,
1175 pub fuzz_fixtures: FuzzFixtures,
1177
1178 pub logs: Vec<Log>,
1180 pub labels: AddressHashMap<String>,
1182 pub traces: Traces,
1184 pub coverage: Option<HitMaps>,
1186 pub deployed_libs: Vec<Address>,
1188
1189 pub reason: Option<String>,
1191 pub skipped: bool,
1193 pub deployment_failure: bool,
1195}
1196
1197impl TestSetup {
1198 pub fn failed(reason: String) -> Self {
1199 Self { reason: Some(reason), ..Default::default() }
1200 }
1201
1202 pub fn skipped(reason: String) -> Self {
1203 Self { reason: Some(reason), skipped: true, ..Default::default() }
1204 }
1205
1206 pub fn extend<FEN: FoundryEvmNetwork>(
1207 &mut self,
1208 raw: RawCallResult<FEN>,
1209 trace_kind: TraceKind,
1210 ) {
1211 extend!(self, raw, trace_kind);
1212 }
1213
1214 pub fn merge_coverages(&mut self, other_coverage: Option<HitMaps>) {
1215 HitMaps::merge_opt(&mut self.coverage, other_coverage);
1216 }
1217}