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 = TestKind::Unit {
565            gas: raw_call_result.gas_used.saturating_sub(raw_call_result.stipend),
566        };
567
568        extend!(self, raw_call_result, TraceKind::Execution);
569
570        self.status = match success {
571            true => TestStatus::Success,
572            false => TestStatus::Failure,
573        };
574        self.reason = reason;
575        self.duration = Duration::default();
576        self.gas_report_traces = Vec::new();
577
578        if let Some(cheatcodes) = raw_call_result.cheatcodes {
579            self.breakpoints = cheatcodes.breakpoints;
580            self.gas_snapshots = cheatcodes.gas_snapshots;
581            self.deprecated_cheatcodes = cheatcodes.deprecated;
582        }
583    }
584
585    /// Returns the result for a fuzzed test. Merges fuzz execution results (logs, labeled
586    /// addresses, traces and coverages) in initial setup results.
587    pub fn fuzz_result(&mut self, result: FuzzTestResult) {
588        self.kind = TestKind::Fuzz {
589            median_gas: result.median_gas(false),
590            mean_gas: result.mean_gas(false),
591            first_case: result.first_case,
592            runs: result.gas_by_case.len(),
593            failed_corpus_replays: result.failed_corpus_replays,
594        };
595
596        // Record logs, labels, traces and merge coverages.
597        extend!(self, result, TraceKind::Execution);
598
599        self.status = if result.skipped {
600            TestStatus::Skipped
601        } else if result.success {
602            TestStatus::Success
603        } else {
604            TestStatus::Failure
605        };
606        self.reason = result.reason;
607        self.counterexample = result.counterexample;
608        self.duration = Duration::default();
609        self.gas_report_traces = result.gas_report_traces.into_iter().map(|t| vec![t]).collect();
610        self.breakpoints = result.breakpoints.unwrap_or_default();
611        self.deprecated_cheatcodes = result.deprecated_cheatcodes;
612    }
613
614    /// Returns the fail result for fuzz test setup.
615    pub fn fuzz_setup_fail(&mut self, e: Report) {
616        self.kind = TestKind::Fuzz {
617            first_case: Default::default(),
618            runs: 0,
619            mean_gas: 0,
620            median_gas: 0,
621            failed_corpus_replays: 0,
622        };
623        self.status = TestStatus::Failure;
624        debug!(?e, "failed to set up fuzz testing environment");
625        self.reason = Some(format!("failed to set up fuzz testing environment: {e}"));
626    }
627
628    /// Returns the skipped result for invariant test.
629    pub fn invariant_skip(&mut self, reason: SkipReason) {
630        self.kind = TestKind::Invariant {
631            runs: 1,
632            calls: 1,
633            reverts: 1,
634            metrics: HashMap::default(),
635            failed_corpus_replays: 0,
636        };
637        self.status = TestStatus::Skipped;
638        self.reason = reason.0;
639    }
640
641    /// Returns the fail result for replayed invariant test.
642    pub fn invariant_replay_fail(
643        &mut self,
644        replayed_entirely: bool,
645        invariant_name: &String,
646        call_sequence: Vec<BaseCounterExample>,
647    ) {
648        self.kind = TestKind::Invariant {
649            runs: 1,
650            calls: 1,
651            reverts: 1,
652            metrics: HashMap::default(),
653            failed_corpus_replays: 0,
654        };
655        self.status = TestStatus::Failure;
656        self.reason = if replayed_entirely {
657            Some(format!("{invariant_name} replay failure"))
658        } else {
659            Some(format!("{invariant_name} persisted failure revert"))
660        };
661        self.counterexample = Some(CounterExample::Sequence(call_sequence.len(), call_sequence));
662    }
663
664    /// Returns the fail result for invariant test setup.
665    pub fn invariant_setup_fail(&mut self, e: Report) {
666        self.kind = TestKind::Invariant {
667            runs: 0,
668            calls: 0,
669            reverts: 0,
670            metrics: HashMap::default(),
671            failed_corpus_replays: 0,
672        };
673        self.status = TestStatus::Failure;
674        self.reason = Some(format!("failed to set up invariant testing environment: {e}"));
675    }
676
677    /// Returns the invariant test result.
678    #[expect(clippy::too_many_arguments)]
679    pub fn invariant_result(
680        &mut self,
681        gas_report_traces: Vec<Vec<CallTraceArena>>,
682        success: bool,
683        reason: Option<String>,
684        counterexample: Option<CounterExample>,
685        cases: Vec<FuzzedCases>,
686        reverts: usize,
687        metrics: Map<String, InvariantMetrics>,
688        failed_corpus_replays: usize,
689    ) {
690        self.kind = TestKind::Invariant {
691            runs: cases.len(),
692            calls: cases.iter().map(|sequence| sequence.cases().len()).sum(),
693            reverts,
694            metrics,
695            failed_corpus_replays,
696        };
697        self.status = match success {
698            true => TestStatus::Success,
699            false => TestStatus::Failure,
700        };
701        self.reason = reason;
702        self.counterexample = counterexample;
703        self.gas_report_traces = gas_report_traces;
704    }
705
706    /// Returns the result for a table test. Merges table test execution results (logs, labeled
707    /// addresses, traces and coverages) in initial setup results.
708    pub fn table_result(&mut self, result: FuzzTestResult) {
709        self.kind = TestKind::Table {
710            median_gas: result.median_gas(false),
711            mean_gas: result.mean_gas(false),
712            runs: result.gas_by_case.len(),
713        };
714
715        // Record logs, labels, traces and merge coverages.
716        extend!(self, result, TraceKind::Execution);
717
718        self.status = if result.skipped {
719            TestStatus::Skipped
720        } else if result.success {
721            TestStatus::Success
722        } else {
723            TestStatus::Failure
724        };
725        self.reason = result.reason;
726        self.counterexample = result.counterexample;
727        self.duration = Duration::default();
728        self.gas_report_traces = result.gas_report_traces.into_iter().map(|t| vec![t]).collect();
729        self.breakpoints = result.breakpoints.unwrap_or_default();
730        self.deprecated_cheatcodes = result.deprecated_cheatcodes;
731    }
732
733    /// Returns `true` if this is the result of a fuzz test
734    pub fn is_fuzz(&self) -> bool {
735        matches!(self.kind, TestKind::Fuzz { .. })
736    }
737
738    /// Formats the test result into a string (for printing).
739    pub fn short_result(&self, name: &str) -> String {
740        format!("{self} {name} {}", self.kind.report())
741    }
742
743    /// Merges the given raw call result into `self`.
744    pub fn extend(&mut self, call_result: RawCallResult) {
745        extend!(self, call_result, TraceKind::Execution);
746    }
747
748    /// Merges the given coverage result into `self`.
749    pub fn merge_coverages(&mut self, other_coverage: Option<HitMaps>) {
750        HitMaps::merge_opt(&mut self.line_coverage, other_coverage);
751    }
752}
753
754/// Data report by a test.
755#[derive(Clone, Debug, PartialEq, Eq)]
756pub enum TestKindReport {
757    Unit {
758        gas: u64,
759    },
760    Fuzz {
761        runs: usize,
762        mean_gas: u64,
763        median_gas: u64,
764        failed_corpus_replays: usize,
765    },
766    Invariant {
767        runs: usize,
768        calls: usize,
769        reverts: usize,
770        metrics: Map<String, InvariantMetrics>,
771        failed_corpus_replays: usize,
772    },
773    Table {
774        runs: usize,
775        mean_gas: u64,
776        median_gas: u64,
777    },
778}
779
780impl fmt::Display for TestKindReport {
781    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
782        match self {
783            Self::Unit { gas } => {
784                write!(f, "(gas: {gas})")
785            }
786            Self::Fuzz { runs, mean_gas, median_gas, failed_corpus_replays } => {
787                if *failed_corpus_replays != 0 {
788                    write!(
789                        f,
790                        "(runs: {runs}, μ: {mean_gas}, ~: {median_gas}, failed corpus replays: {failed_corpus_replays})"
791                    )
792                } else {
793                    write!(f, "(runs: {runs}, μ: {mean_gas}, ~: {median_gas})")
794                }
795            }
796            Self::Invariant { runs, calls, reverts, metrics: _, failed_corpus_replays } => {
797                if *failed_corpus_replays != 0 {
798                    write!(
799                        f,
800                        "(runs: {runs}, calls: {calls}, reverts: {reverts}, failed corpus replays: {failed_corpus_replays})"
801                    )
802                } else {
803                    write!(f, "(runs: {runs}, calls: {calls}, reverts: {reverts})")
804                }
805            }
806            Self::Table { runs, mean_gas, median_gas } => {
807                write!(f, "(runs: {runs}, μ: {mean_gas}, ~: {median_gas})")
808            }
809        }
810    }
811}
812
813impl TestKindReport {
814    /// Returns the main gas value to compare against
815    pub fn gas(&self) -> u64 {
816        match *self {
817            Self::Unit { gas } => gas,
818            // We use the median for comparisons
819            Self::Fuzz { median_gas, .. } | Self::Table { median_gas, .. } => median_gas,
820            // We return 0 since it's not applicable
821            Self::Invariant { .. } => 0,
822        }
823    }
824}
825
826/// Various types of tests
827#[derive(Clone, Debug, Serialize, Deserialize)]
828pub enum TestKind {
829    /// A unit test.
830    Unit { gas: u64 },
831    /// A fuzz test.
832    Fuzz {
833        /// we keep this for the debugger
834        first_case: FuzzCase,
835        runs: usize,
836        mean_gas: u64,
837        median_gas: u64,
838        failed_corpus_replays: usize,
839    },
840    /// An invariant test.
841    Invariant {
842        runs: usize,
843        calls: usize,
844        reverts: usize,
845        metrics: Map<String, InvariantMetrics>,
846        failed_corpus_replays: usize,
847    },
848    /// A table test.
849    Table { runs: usize, mean_gas: u64, median_gas: u64 },
850}
851
852impl Default for TestKind {
853    fn default() -> Self {
854        Self::Unit { gas: 0 }
855    }
856}
857
858impl TestKind {
859    /// The gas consumed by this test
860    pub fn report(&self) -> TestKindReport {
861        match self {
862            Self::Unit { gas } => TestKindReport::Unit { gas: *gas },
863            Self::Fuzz { first_case: _, runs, mean_gas, median_gas, failed_corpus_replays } => {
864                TestKindReport::Fuzz {
865                    runs: *runs,
866                    mean_gas: *mean_gas,
867                    median_gas: *median_gas,
868                    failed_corpus_replays: *failed_corpus_replays,
869                }
870            }
871            Self::Invariant { runs, calls, reverts, metrics: _, failed_corpus_replays } => {
872                TestKindReport::Invariant {
873                    runs: *runs,
874                    calls: *calls,
875                    reverts: *reverts,
876                    metrics: HashMap::default(),
877                    failed_corpus_replays: *failed_corpus_replays,
878                }
879            }
880            Self::Table { runs, mean_gas, median_gas } => {
881                TestKindReport::Table { runs: *runs, mean_gas: *mean_gas, median_gas: *median_gas }
882            }
883        }
884    }
885}
886
887/// The result of a test setup.
888///
889/// Includes the deployment of the required libraries and the test contract itself, and the call to
890/// the `setUp()` function.
891#[derive(Clone, Debug, Default)]
892pub struct TestSetup {
893    /// The address at which the test contract was deployed.
894    pub address: Address,
895    /// Defined fuzz test fixtures.
896    pub fuzz_fixtures: FuzzFixtures,
897
898    /// The logs emitted during setup.
899    pub logs: Vec<Log>,
900    /// Addresses labeled during setup.
901    pub labels: AddressHashMap<String>,
902    /// Call traces of the setup.
903    pub traces: Traces,
904    /// Coverage info during setup.
905    pub coverage: Option<HitMaps>,
906    /// Addresses of external libraries deployed during setup.
907    pub deployed_libs: Vec<Address>,
908
909    /// The reason the setup failed, if it did.
910    pub reason: Option<String>,
911    /// Whether setup and entire test suite is skipped.
912    pub skipped: bool,
913    /// Whether the test failed to deploy.
914    pub deployment_failure: bool,
915}
916
917impl TestSetup {
918    pub fn failed(reason: String) -> Self {
919        Self { reason: Some(reason), ..Default::default() }
920    }
921
922    pub fn skipped(reason: String) -> Self {
923        Self { reason: Some(reason), skipped: true, ..Default::default() }
924    }
925
926    pub fn extend(&mut self, raw: RawCallResult, trace_kind: TraceKind) {
927        extend!(self, raw, trace_kind);
928    }
929
930    pub fn merge_coverages(&mut self, other_coverage: Option<HitMaps>) {
931        HitMaps::merge_opt(&mut self.coverage, other_coverage);
932    }
933}