forge/
result.rs

1//! Test outcomes.
2
3use crate::{
4    MultiContractRunner,
5    fuzz::{BaseCounterExample, FuzzedCases},
6    gas_report::GasReport,
7};
8use alloy_primitives::{
9    Address, Log,
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/// The aggregated result of a test run.
31#[derive(Clone, Debug)]
32pub struct TestOutcome {
33    /// The results of all test suites by their identifier (`path:contract_name`).
34    ///
35    /// Essentially `identifier => signature => result`.
36    pub results: BTreeMap<String, SuiteResult>,
37    /// Whether to allow test failures without failing the entire test run.
38    pub allow_failure: bool,
39    /// The decoder used to decode traces and logs.
40    ///
41    /// This is `None` if traces and logs were not decoded.
42    ///
43    /// Note that `Address` fields only contain the last executed test case's data.
44    pub last_run_decoder: Option<CallTraceDecoder>,
45    /// The gas report, if requested.
46    pub gas_report: Option<GasReport>,
47    /// The runner used to execute the tests.
48    pub runner: Option<MultiContractRunner>,
49}
50
51impl TestOutcome {
52    /// Creates a new test outcome with the given results.
53    pub fn new(
54        runner: Option<MultiContractRunner>,
55        results: BTreeMap<String, SuiteResult>,
56        allow_failure: bool,
57    ) -> Self {
58        Self { results, allow_failure, last_run_decoder: None, gas_report: None, runner }
59    }
60
61    /// Creates a new empty test outcome.
62    pub fn empty(runner: Option<MultiContractRunner>, allow_failure: bool) -> Self {
63        Self::new(runner, BTreeMap::new(), allow_failure)
64    }
65
66    /// Returns an iterator over all individual succeeding tests and their names.
67    pub fn successes(&self) -> impl Iterator<Item = (&String, &TestResult)> {
68        self.tests().filter(|(_, t)| t.status.is_success())
69    }
70
71    /// Returns an iterator over all individual skipped tests and their names.
72    pub fn skips(&self) -> impl Iterator<Item = (&String, &TestResult)> {
73        self.tests().filter(|(_, t)| t.status.is_skipped())
74    }
75
76    /// Returns an iterator over all individual failing tests and their names.
77    pub fn failures(&self) -> impl Iterator<Item = (&String, &TestResult)> {
78        self.tests().filter(|(_, t)| t.status.is_failure())
79    }
80
81    /// Returns an iterator over all individual tests and their names.
82    pub fn tests(&self) -> impl Iterator<Item = (&String, &TestResult)> {
83        self.results.values().flat_map(|suite| suite.tests())
84    }
85
86    /// Flattens the test outcome into a list of individual tests.
87    // TODO: Replace this with `tests` and make it return `TestRef<'_>`
88    pub fn into_tests_cloned(&self) -> impl Iterator<Item = SuiteTestResult> + '_ {
89        self.results
90            .iter()
91            .flat_map(|(file, suite)| {
92                suite
93                    .test_results
94                    .iter()
95                    .map(move |(sig, result)| (file.clone(), sig.clone(), result.clone()))
96            })
97            .map(|(artifact_id, signature, result)| SuiteTestResult {
98                artifact_id,
99                signature,
100                result,
101            })
102    }
103
104    /// Flattens the test outcome into a list of individual tests.
105    pub fn into_tests(self) -> impl Iterator<Item = SuiteTestResult> {
106        self.results
107            .into_iter()
108            .flat_map(|(file, suite)| {
109                suite.test_results.into_iter().map(move |t| (file.clone(), t))
110            })
111            .map(|(artifact_id, (signature, result))| SuiteTestResult {
112                artifact_id,
113                signature,
114                result,
115            })
116    }
117
118    /// Returns the number of tests that passed.
119    pub fn passed(&self) -> usize {
120        self.successes().count()
121    }
122
123    /// Returns the number of tests that were skipped.
124    pub fn skipped(&self) -> usize {
125        self.skips().count()
126    }
127
128    /// Returns the number of tests that failed.
129    pub fn failed(&self) -> usize {
130        self.failures().count()
131    }
132
133    /// Sums up all the durations of all individual test suites.
134    ///
135    /// Note that this is not necessarily the wall clock time of the entire test run.
136    pub fn total_time(&self) -> Duration {
137        self.results.values().map(|suite| suite.duration).sum()
138    }
139
140    /// Formats the aggregated summary of all test suites into a string (for printing).
141    pub fn summary(&self, wall_clock_time: Duration) -> String {
142        let num_test_suites = self.results.len();
143        let suites = if num_test_suites == 1 { "suite" } else { "suites" };
144        let total_passed = self.passed();
145        let total_failed = self.failed();
146        let total_skipped = self.skipped();
147        let total_tests = total_passed + total_failed + total_skipped;
148        format!(
149            "\nRan {} test {} in {:.2?} ({:.2?} CPU time): {} tests passed, {} failed, {} skipped ({} total tests)",
150            num_test_suites,
151            suites,
152            wall_clock_time,
153            self.total_time(),
154            total_passed.green(),
155            total_failed.red(),
156            total_skipped.yellow(),
157            total_tests
158        )
159    }
160
161    /// Checks if there are any failures and failures are disallowed.
162    pub fn ensure_ok(&self, silent: bool) -> eyre::Result<()> {
163        let outcome = self;
164        let failures = outcome.failures().count();
165        if outcome.allow_failure || failures == 0 {
166            return Ok(());
167        }
168
169        if shell::is_quiet() || silent {
170            std::process::exit(1);
171        }
172
173        sh_println!("\nFailing tests:")?;
174        for (suite_name, suite) in &outcome.results {
175            let failed = suite.failed();
176            if failed == 0 {
177                continue;
178            }
179
180            let term = if failed > 1 { "tests" } else { "test" };
181            sh_println!("Encountered {failed} failing {term} in {suite_name}")?;
182            for (name, result) in suite.failures() {
183                sh_println!("{}", result.short_result(name))?;
184            }
185            sh_println!()?;
186        }
187        let successes = outcome.passed();
188        sh_println!(
189            "Encountered a total of {} failing tests, {} tests succeeded",
190            failures.to_string().red(),
191            successes.to_string().green()
192        )?;
193
194        // Show helpful hint for rerunning failed tests
195        let test_word = if failures == 1 { "test" } else { "tests" };
196        sh_println!(
197            "\nTip: Run {} to retry only the {} failed {}",
198            "`forge test --rerun`".cyan(),
199            failures,
200            test_word
201        )?;
202
203        std::process::exit(1);
204    }
205
206    /// Removes first test result, if any.
207    pub fn remove_first(&mut self) -> Option<(String, String, TestResult)> {
208        self.results.iter_mut().find_map(|(suite_name, suite)| {
209            if let Some(test_name) = suite.test_results.keys().next().cloned() {
210                let result = suite.test_results.remove(&test_name).unwrap();
211                Some((suite_name.clone(), test_name, result))
212            } else {
213                None
214            }
215        })
216    }
217}
218
219/// A set of test results for a single test suite, which is all the tests in a single contract.
220#[derive(Clone, Debug, Serialize)]
221pub struct SuiteResult {
222    /// Wall clock time it took to execute all tests in this suite.
223    #[serde(with = "foundry_common::serde_helpers::duration")]
224    pub duration: Duration,
225    /// Individual test results: `test fn signature -> TestResult`.
226    pub test_results: BTreeMap<String, TestResult>,
227    /// Generated warnings.
228    pub warnings: Vec<String>,
229}
230
231impl SuiteResult {
232    pub fn new(
233        duration: Duration,
234        test_results: BTreeMap<String, TestResult>,
235        mut warnings: Vec<String>,
236    ) -> Self {
237        // Add deprecated cheatcodes warning, if any of them used in current test suite.
238        let mut deprecated_cheatcodes = HashMap::new();
239        for test_result in test_results.values() {
240            deprecated_cheatcodes.extend(test_result.deprecated_cheatcodes.clone());
241        }
242        if !deprecated_cheatcodes.is_empty() {
243            let mut warning =
244                "the following cheatcode(s) are deprecated and will be removed in future versions:"
245                    .to_string();
246            for (cheatcode, reason) in deprecated_cheatcodes {
247                write!(warning, "\n  {cheatcode}").unwrap();
248                if let Some(reason) = reason {
249                    write!(warning, ": {reason}").unwrap();
250                }
251            }
252            warnings.push(warning);
253        }
254
255        Self { duration, test_results, warnings }
256    }
257
258    /// Returns an iterator over all individual succeeding tests and their names.
259    pub fn successes(&self) -> impl Iterator<Item = (&String, &TestResult)> {
260        self.tests().filter(|(_, t)| t.status.is_success())
261    }
262
263    /// Returns an iterator over all individual skipped tests and their names.
264    pub fn skips(&self) -> impl Iterator<Item = (&String, &TestResult)> {
265        self.tests().filter(|(_, t)| t.status.is_skipped())
266    }
267
268    /// Returns an iterator over all individual failing tests and their names.
269    pub fn failures(&self) -> impl Iterator<Item = (&String, &TestResult)> {
270        self.tests().filter(|(_, t)| t.status.is_failure())
271    }
272
273    /// Returns the number of tests that passed.
274    pub fn passed(&self) -> usize {
275        self.successes().count()
276    }
277
278    /// Returns the number of tests that were skipped.
279    pub fn skipped(&self) -> usize {
280        self.skips().count()
281    }
282
283    /// Returns the number of tests that failed.
284    pub fn failed(&self) -> usize {
285        self.failures().count()
286    }
287
288    /// Iterator over all tests and their names
289    pub fn tests(&self) -> impl Iterator<Item = (&String, &TestResult)> {
290        self.test_results.iter()
291    }
292
293    /// Whether this test suite is empty.
294    pub fn is_empty(&self) -> bool {
295        self.test_results.is_empty()
296    }
297
298    /// The number of tests in this test suite.
299    pub fn len(&self) -> usize {
300        self.test_results.len()
301    }
302
303    /// Sums up all the durations of all individual tests in this suite.
304    ///
305    /// Note that this is not necessarily the wall clock time of the entire test suite.
306    pub fn total_time(&self) -> Duration {
307        self.test_results.values().map(|result| result.duration).sum()
308    }
309
310    /// Returns the summary of a single test suite.
311    pub fn summary(&self) -> String {
312        let failed = self.failed();
313        let result = if failed == 0 { "ok".green() } else { "FAILED".red() };
314        format!(
315            "Suite result: {}. {} passed; {} failed; {} skipped; finished in {:.2?} ({:.2?} CPU time)",
316            result,
317            self.passed().green(),
318            failed.red(),
319            self.skipped().yellow(),
320            self.duration,
321            self.total_time(),
322        )
323    }
324}
325
326/// The result of a single test in a test suite.
327///
328/// This is flattened from a [`TestOutcome`].
329#[derive(Clone, Debug)]
330pub struct SuiteTestResult {
331    /// The identifier of the artifact/contract in the form:
332    /// `<artifact file name>:<contract name>`.
333    pub artifact_id: String,
334    /// The function signature of the Solidity test.
335    pub signature: String,
336    /// The result of the executed test.
337    pub result: TestResult,
338}
339
340impl SuiteTestResult {
341    /// Returns the gas used by the test.
342    pub fn gas_used(&self) -> u64 {
343        self.result.kind.report().gas()
344    }
345
346    /// Returns the contract name of the artifact ID.
347    pub fn contract_name(&self) -> &str {
348        get_contract_name(&self.artifact_id)
349    }
350
351    /// Returns the file name of the artifact ID.
352    pub fn file_name(&self) -> &str {
353        get_file_name(&self.artifact_id)
354    }
355}
356
357/// The status of a test.
358#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
359pub enum TestStatus {
360    Success,
361    #[default]
362    Failure,
363    Skipped,
364}
365
366impl TestStatus {
367    /// Returns `true` if the test was successful.
368    #[inline]
369    pub fn is_success(self) -> bool {
370        matches!(self, Self::Success)
371    }
372
373    /// Returns `true` if the test failed.
374    #[inline]
375    pub fn is_failure(self) -> bool {
376        matches!(self, Self::Failure)
377    }
378
379    /// Returns `true` if the test was skipped.
380    #[inline]
381    pub fn is_skipped(self) -> bool {
382        matches!(self, Self::Skipped)
383    }
384}
385
386/// The result of an executed test.
387#[derive(Clone, Debug, Default, Serialize, Deserialize)]
388pub struct TestResult {
389    /// The test status, indicating whether the test case succeeded, failed, or was marked as
390    /// skipped. This means that the transaction executed properly, the test was marked as
391    /// skipped with vm.skip(), or that there was a revert and that the test was expected to
392    /// fail (prefixed with `testFail`)
393    pub status: TestStatus,
394
395    /// If there was a revert, this field will be populated. Note that the test can
396    /// still be successful (i.e self.success == true) when it's expected to fail.
397    pub reason: Option<String>,
398
399    /// Minimal reproduction test case for failing test
400    pub counterexample: Option<CounterExample>,
401
402    /// Any captured & parsed as strings logs along the test's execution which should
403    /// be printed to the user.
404    pub logs: Vec<Log>,
405
406    /// The decoded DSTest logging events and Hardhat's `console.log` from [logs](Self::logs).
407    /// Used for json output.
408    pub decoded_logs: Vec<String>,
409
410    /// What kind of test this was
411    pub kind: TestKind,
412
413    /// Traces
414    pub traces: Traces,
415
416    /// Additional traces to use for gas report.
417    ///
418    /// These are cleared after the gas report is analyzed.
419    #[serde(skip)]
420    pub gas_report_traces: Vec<Vec<CallTraceArena>>,
421
422    /// Raw line coverage info
423    #[serde(skip)]
424    pub line_coverage: Option<HitMaps>,
425
426    /// Labeled addresses
427    #[serde(rename = "labeled_addresses")] // Backwards compatibility.
428    pub labels: AddressHashMap<String>,
429
430    #[serde(with = "foundry_common::serde_helpers::duration")]
431    pub duration: Duration,
432
433    /// pc breakpoint char map
434    pub breakpoints: Breakpoints,
435
436    /// Any captured gas snapshots along the test's execution which should be accumulated.
437    pub gas_snapshots: BTreeMap<String, BTreeMap<String, String>>,
438
439    /// Deprecated cheatcodes (mapped to their replacements, if any) used in current test.
440    #[serde(skip)]
441    pub deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>,
442}
443
444impl fmt::Display for TestResult {
445    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
446        match self.status {
447            TestStatus::Success => "[PASS]".green().fmt(f),
448            TestStatus::Skipped => {
449                let mut s = String::from("[SKIP");
450                if let Some(reason) = &self.reason {
451                    write!(s, ": {reason}").unwrap();
452                }
453                s.push(']');
454                s.yellow().fmt(f)
455            }
456            TestStatus::Failure => {
457                let mut s = String::from("[FAIL");
458                if self.reason.is_some() || self.counterexample.is_some() {
459                    if let Some(reason) = &self.reason {
460                        write!(s, ": {reason}").unwrap();
461                    }
462
463                    if let Some(counterexample) = &self.counterexample {
464                        match counterexample {
465                            CounterExample::Single(ex) => {
466                                write!(s, "; counterexample: {ex}]").unwrap();
467                            }
468                            CounterExample::Sequence(original, sequence) => {
469                                s.push_str(
470                                    format!(
471                                        "]\n\t[Sequence] (original: {original}, shrunk: {})\n",
472                                        sequence.len()
473                                    )
474                                    .as_str(),
475                                );
476                                for ex in sequence {
477                                    writeln!(s, "{ex}").unwrap();
478                                }
479                            }
480                        }
481                    } else {
482                        s.push(']');
483                    }
484                } else {
485                    s.push(']');
486                }
487                s.red().wrap().fmt(f)
488            }
489        }
490    }
491}
492
493macro_rules! extend {
494    ($a:expr, $b:expr, $trace_kind:expr) => {
495        $a.logs.extend($b.logs);
496        $a.labels.extend($b.labels);
497        $a.traces.extend($b.traces.map(|traces| ($trace_kind, traces)));
498        $a.merge_coverages($b.line_coverage);
499    };
500}
501
502impl TestResult {
503    /// Creates a new test result starting from test setup results.
504    pub fn new(setup: &TestSetup) -> Self {
505        Self {
506            labels: setup.labels.clone(),
507            logs: setup.logs.clone(),
508            traces: setup.traces.clone(),
509            line_coverage: setup.coverage.clone(),
510            ..Default::default()
511        }
512    }
513
514    /// Creates a failed test result with given reason.
515    pub fn fail(reason: String) -> Self {
516        Self { status: TestStatus::Failure, reason: Some(reason), ..Default::default() }
517    }
518
519    /// Creates a test setup result.
520    pub fn setup_result(setup: TestSetup) -> Self {
521        let TestSetup {
522            address: _,
523            fuzz_fixtures: _,
524            logs,
525            labels,
526            traces,
527            coverage,
528            deployed_libs: _,
529            reason,
530            skipped,
531            deployment_failure: _,
532        } = setup;
533        Self {
534            status: if skipped { TestStatus::Skipped } else { TestStatus::Failure },
535            reason,
536            logs,
537            traces,
538            line_coverage: coverage,
539            labels,
540            ..Default::default()
541        }
542    }
543
544    /// Returns the skipped result for single test (used in skipped fuzz test too).
545    pub fn single_skip(&mut self, reason: SkipReason) {
546        self.status = TestStatus::Skipped;
547        self.reason = reason.0;
548    }
549
550    /// Returns the failed result with reason for single test.
551    pub fn single_fail(&mut self, reason: Option<String>) {
552        self.status = TestStatus::Failure;
553        self.reason = reason;
554    }
555
556    /// Returns the result for single test. Merges execution results (logs, labeled addresses,
557    /// traces and coverages) in initial setup results.
558    pub fn single_result(
559        &mut self,
560        success: bool,
561        reason: Option<String>,
562        raw_call_result: RawCallResult,
563    ) {
564        self.kind =
565            TestKind::Unit { gas: raw_call_result.gas_used.wrapping_sub(raw_call_result.stipend) };
566
567        extend!(self, raw_call_result, TraceKind::Execution);
568
569        self.status = match success {
570            true => TestStatus::Success,
571            false => TestStatus::Failure,
572        };
573        self.reason = reason;
574        self.duration = Duration::default();
575        self.gas_report_traces = Vec::new();
576
577        if let Some(cheatcodes) = raw_call_result.cheatcodes {
578            self.breakpoints = cheatcodes.breakpoints;
579            self.gas_snapshots = cheatcodes.gas_snapshots;
580            self.deprecated_cheatcodes = cheatcodes.deprecated;
581        }
582    }
583
584    /// Returns the result for a fuzzed test. Merges fuzz execution results (logs, labeled
585    /// addresses, traces and coverages) in initial setup results.
586    pub fn fuzz_result(&mut self, result: FuzzTestResult) {
587        self.kind = TestKind::Fuzz {
588            median_gas: result.median_gas(false),
589            mean_gas: result.mean_gas(false),
590            first_case: result.first_case,
591            runs: result.gas_by_case.len(),
592            failed_corpus_replays: result.failed_corpus_replays,
593        };
594
595        // Record logs, labels, traces and merge coverages.
596        extend!(self, result, TraceKind::Execution);
597
598        self.status = if result.skipped {
599            TestStatus::Skipped
600        } else if result.success {
601            TestStatus::Success
602        } else {
603            TestStatus::Failure
604        };
605        self.reason = result.reason;
606        self.counterexample = result.counterexample;
607        self.duration = Duration::default();
608        self.gas_report_traces = result.gas_report_traces.into_iter().map(|t| vec![t]).collect();
609        self.breakpoints = result.breakpoints.unwrap_or_default();
610        self.deprecated_cheatcodes = result.deprecated_cheatcodes;
611    }
612
613    /// Returns the fail result for fuzz test setup.
614    pub fn fuzz_setup_fail(&mut self, e: Report) {
615        self.kind = TestKind::Fuzz {
616            first_case: Default::default(),
617            runs: 0,
618            mean_gas: 0,
619            median_gas: 0,
620            failed_corpus_replays: 0,
621        };
622        self.status = TestStatus::Failure;
623        self.reason = Some(format!("failed to set up fuzz testing environment: {e}"));
624    }
625
626    /// Returns the skipped result for invariant test.
627    pub fn invariant_skip(&mut self, reason: SkipReason) {
628        self.kind = TestKind::Invariant {
629            runs: 1,
630            calls: 1,
631            reverts: 1,
632            metrics: HashMap::default(),
633            failed_corpus_replays: 0,
634        };
635        self.status = TestStatus::Skipped;
636        self.reason = reason.0;
637    }
638
639    /// Returns the fail result for replayed invariant test.
640    pub fn invariant_replay_fail(
641        &mut self,
642        replayed_entirely: bool,
643        invariant_name: &String,
644        call_sequence: Vec<BaseCounterExample>,
645    ) {
646        self.kind = TestKind::Invariant {
647            runs: 1,
648            calls: 1,
649            reverts: 1,
650            metrics: HashMap::default(),
651            failed_corpus_replays: 0,
652        };
653        self.status = TestStatus::Failure;
654        self.reason = if replayed_entirely {
655            Some(format!("{invariant_name} replay failure"))
656        } else {
657            Some(format!("{invariant_name} persisted failure revert"))
658        };
659        self.counterexample = Some(CounterExample::Sequence(call_sequence.len(), call_sequence));
660    }
661
662    /// Returns the fail result for invariant test setup.
663    pub fn invariant_setup_fail(&mut self, e: Report) {
664        self.kind = TestKind::Invariant {
665            runs: 0,
666            calls: 0,
667            reverts: 0,
668            metrics: HashMap::default(),
669            failed_corpus_replays: 0,
670        };
671        self.status = TestStatus::Failure;
672        self.reason = Some(format!("failed to set up invariant testing environment: {e}"));
673    }
674
675    /// Returns the invariant test result.
676    #[expect(clippy::too_many_arguments)]
677    pub fn invariant_result(
678        &mut self,
679        gas_report_traces: Vec<Vec<CallTraceArena>>,
680        success: bool,
681        reason: Option<String>,
682        counterexample: Option<CounterExample>,
683        cases: Vec<FuzzedCases>,
684        reverts: usize,
685        metrics: Map<String, InvariantMetrics>,
686        failed_corpus_replays: usize,
687    ) {
688        self.kind = TestKind::Invariant {
689            runs: cases.len(),
690            calls: cases.iter().map(|sequence| sequence.cases().len()).sum(),
691            reverts,
692            metrics,
693            failed_corpus_replays,
694        };
695        self.status = match success {
696            true => TestStatus::Success,
697            false => TestStatus::Failure,
698        };
699        self.reason = reason;
700        self.counterexample = counterexample;
701        self.gas_report_traces = gas_report_traces;
702    }
703
704    /// Returns the result for a table test. Merges table test execution results (logs, labeled
705    /// addresses, traces and coverages) in initial setup results.
706    pub fn table_result(&mut self, result: FuzzTestResult) {
707        self.kind = TestKind::Table {
708            median_gas: result.median_gas(false),
709            mean_gas: result.mean_gas(false),
710            runs: result.gas_by_case.len(),
711        };
712
713        // Record logs, labels, traces and merge coverages.
714        extend!(self, result, TraceKind::Execution);
715
716        self.status = if result.skipped {
717            TestStatus::Skipped
718        } else if result.success {
719            TestStatus::Success
720        } else {
721            TestStatus::Failure
722        };
723        self.reason = result.reason;
724        self.counterexample = result.counterexample;
725        self.duration = Duration::default();
726        self.gas_report_traces = result.gas_report_traces.into_iter().map(|t| vec![t]).collect();
727        self.breakpoints = result.breakpoints.unwrap_or_default();
728        self.deprecated_cheatcodes = result.deprecated_cheatcodes;
729    }
730
731    /// Returns `true` if this is the result of a fuzz test
732    pub fn is_fuzz(&self) -> bool {
733        matches!(self.kind, TestKind::Fuzz { .. })
734    }
735
736    /// Formats the test result into a string (for printing).
737    pub fn short_result(&self, name: &str) -> String {
738        format!("{self} {name} {}", self.kind.report())
739    }
740
741    /// Merges the given raw call result into `self`.
742    pub fn extend(&mut self, call_result: RawCallResult) {
743        extend!(self, call_result, TraceKind::Execution);
744    }
745
746    /// Merges the given coverage result into `self`.
747    pub fn merge_coverages(&mut self, other_coverage: Option<HitMaps>) {
748        HitMaps::merge_opt(&mut self.line_coverage, other_coverage);
749    }
750}
751
752/// Data report by a test.
753#[derive(Clone, Debug, PartialEq, Eq)]
754pub enum TestKindReport {
755    Unit {
756        gas: u64,
757    },
758    Fuzz {
759        runs: usize,
760        mean_gas: u64,
761        median_gas: u64,
762        failed_corpus_replays: usize,
763    },
764    Invariant {
765        runs: usize,
766        calls: usize,
767        reverts: usize,
768        metrics: Map<String, InvariantMetrics>,
769        failed_corpus_replays: usize,
770    },
771    Table {
772        runs: usize,
773        mean_gas: u64,
774        median_gas: u64,
775    },
776}
777
778impl fmt::Display for TestKindReport {
779    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
780        match self {
781            Self::Unit { gas } => {
782                write!(f, "(gas: {gas})")
783            }
784            Self::Fuzz { runs, mean_gas, median_gas, failed_corpus_replays } => {
785                if *failed_corpus_replays != 0 {
786                    write!(
787                        f,
788                        "(runs: {runs}, μ: {mean_gas}, ~: {median_gas}, failed corpus replays: {failed_corpus_replays})"
789                    )
790                } else {
791                    write!(f, "(runs: {runs}, μ: {mean_gas}, ~: {median_gas})")
792                }
793            }
794            Self::Invariant { runs, calls, reverts, metrics: _, failed_corpus_replays } => {
795                if *failed_corpus_replays != 0 {
796                    write!(
797                        f,
798                        "(runs: {runs}, calls: {calls}, reverts: {reverts}, failed corpus replays: {failed_corpus_replays})"
799                    )
800                } else {
801                    write!(f, "(runs: {runs}, calls: {calls}, reverts: {reverts})")
802                }
803            }
804            Self::Table { runs, mean_gas, median_gas } => {
805                write!(f, "(runs: {runs}, μ: {mean_gas}, ~: {median_gas})")
806            }
807        }
808    }
809}
810
811impl TestKindReport {
812    /// Returns the main gas value to compare against
813    pub fn gas(&self) -> u64 {
814        match *self {
815            Self::Unit { gas } => gas,
816            // We use the median for comparisons
817            Self::Fuzz { median_gas, .. } | Self::Table { median_gas, .. } => median_gas,
818            // We return 0 since it's not applicable
819            Self::Invariant { .. } => 0,
820        }
821    }
822}
823
824/// Various types of tests
825#[derive(Clone, Debug, Serialize, Deserialize)]
826pub enum TestKind {
827    /// A unit test.
828    Unit { gas: u64 },
829    /// A fuzz test.
830    Fuzz {
831        /// we keep this for the debugger
832        first_case: FuzzCase,
833        runs: usize,
834        mean_gas: u64,
835        median_gas: u64,
836        failed_corpus_replays: usize,
837    },
838    /// An invariant test.
839    Invariant {
840        runs: usize,
841        calls: usize,
842        reverts: usize,
843        metrics: Map<String, InvariantMetrics>,
844        failed_corpus_replays: usize,
845    },
846    /// A table test.
847    Table { runs: usize, mean_gas: u64, median_gas: u64 },
848}
849
850impl Default for TestKind {
851    fn default() -> Self {
852        Self::Unit { gas: 0 }
853    }
854}
855
856impl TestKind {
857    /// The gas consumed by this test
858    pub fn report(&self) -> TestKindReport {
859        match self {
860            Self::Unit { gas } => TestKindReport::Unit { gas: *gas },
861            Self::Fuzz { first_case: _, runs, mean_gas, median_gas, failed_corpus_replays } => {
862                TestKindReport::Fuzz {
863                    runs: *runs,
864                    mean_gas: *mean_gas,
865                    median_gas: *median_gas,
866                    failed_corpus_replays: *failed_corpus_replays,
867                }
868            }
869            Self::Invariant { runs, calls, reverts, metrics: _, failed_corpus_replays } => {
870                TestKindReport::Invariant {
871                    runs: *runs,
872                    calls: *calls,
873                    reverts: *reverts,
874                    metrics: HashMap::default(),
875                    failed_corpus_replays: *failed_corpus_replays,
876                }
877            }
878            Self::Table { runs, mean_gas, median_gas } => {
879                TestKindReport::Table { runs: *runs, mean_gas: *mean_gas, median_gas: *median_gas }
880            }
881        }
882    }
883}
884
885/// The result of a test setup.
886///
887/// Includes the deployment of the required libraries and the test contract itself, and the call to
888/// the `setUp()` function.
889#[derive(Clone, Debug, Default)]
890pub struct TestSetup {
891    /// The address at which the test contract was deployed.
892    pub address: Address,
893    /// Defined fuzz test fixtures.
894    pub fuzz_fixtures: FuzzFixtures,
895
896    /// The logs emitted during setup.
897    pub logs: Vec<Log>,
898    /// Addresses labeled during setup.
899    pub labels: AddressHashMap<String>,
900    /// Call traces of the setup.
901    pub traces: Traces,
902    /// Coverage info during setup.
903    pub coverage: Option<HitMaps>,
904    /// Addresses of external libraries deployed during setup.
905    pub deployed_libs: Vec<Address>,
906
907    /// The reason the setup failed, if it did.
908    pub reason: Option<String>,
909    /// Whether setup and entire test suite is skipped.
910    pub skipped: bool,
911    /// Whether the test failed to deploy.
912    pub deployment_failure: bool,
913}
914
915impl TestSetup {
916    pub fn failed(reason: String) -> Self {
917        Self { reason: Some(reason), ..Default::default() }
918    }
919
920    pub fn skipped(reason: String) -> Self {
921        Self { reason: Some(reason), skipped: true, ..Default::default() }
922    }
923
924    pub fn extend(&mut self, raw: RawCallResult, trace_kind: TraceKind) {
925        extend!(self, raw, trace_kind);
926    }
927
928    pub fn merge_coverages(&mut self, other_coverage: Option<HitMaps>) {
929        HitMaps::merge_opt(&mut self.coverage, other_coverage);
930    }
931}