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            // TODO: Avoid process::exit
171            std::process::exit(1);
172        }
173
174        sh_println!("\nFailing tests:")?;
175        for (suite_name, suite) in &outcome.results {
176            let failed = suite.failed();
177            if failed == 0 {
178                continue;
179            }
180
181            let term = if failed > 1 { "tests" } else { "test" };
182            sh_println!("Encountered {failed} failing {term} in {suite_name}")?;
183            for (name, result) in suite.failures() {
184                sh_println!("{}", result.short_result(name))?;
185            }
186            sh_println!()?;
187        }
188        let successes = outcome.passed();
189        sh_println!(
190            "Encountered a total of {} failing tests, {} tests succeeded",
191            failures.to_string().red(),
192            successes.to_string().green()
193        )?;
194
195        // Show helpful hint for rerunning failed tests
196        let test_word = if failures == 1 { "test" } else { "tests" };
197        sh_println!(
198            "\nTip: Run {} to retry only the {} failed {}",
199            "`forge test --rerun`".cyan(),
200            failures,
201            test_word
202        )?;
203
204        // TODO: Avoid process::exit
205        std::process::exit(1);
206    }
207
208    /// Removes first test result, if any.
209    pub fn remove_first(&mut self) -> Option<(String, String, TestResult)> {
210        self.results.iter_mut().find_map(|(suite_name, suite)| {
211            if let Some(test_name) = suite.test_results.keys().next().cloned() {
212                let result = suite.test_results.remove(&test_name).unwrap();
213                Some((suite_name.clone(), test_name, result))
214            } else {
215                None
216            }
217        })
218    }
219}
220
221/// A set of test results for a single test suite, which is all the tests in a single contract.
222#[derive(Clone, Debug, Serialize)]
223pub struct SuiteResult {
224    /// Wall clock time it took to execute all tests in this suite.
225    #[serde(with = "foundry_common::serde_helpers::duration")]
226    pub duration: Duration,
227    /// Individual test results: `test fn signature -> TestResult`.
228    pub test_results: BTreeMap<String, TestResult>,
229    /// Generated warnings.
230    pub warnings: Vec<String>,
231}
232
233impl SuiteResult {
234    pub fn new(
235        duration: Duration,
236        test_results: BTreeMap<String, TestResult>,
237        mut warnings: Vec<String>,
238    ) -> Self {
239        // Add deprecated cheatcodes warning, if any of them used in current test suite.
240        let mut deprecated_cheatcodes = HashMap::new();
241        for test_result in test_results.values() {
242            deprecated_cheatcodes.extend(test_result.deprecated_cheatcodes.clone());
243        }
244        if !deprecated_cheatcodes.is_empty() {
245            let mut warning =
246                "the following cheatcode(s) are deprecated and will be removed in future versions:"
247                    .to_string();
248            for (cheatcode, reason) in deprecated_cheatcodes {
249                write!(warning, "\n  {cheatcode}").unwrap();
250                if let Some(reason) = reason {
251                    write!(warning, ": {reason}").unwrap();
252                }
253            }
254            warnings.push(warning);
255        }
256
257        Self { duration, test_results, warnings }
258    }
259
260    /// Returns an iterator over all individual succeeding tests and their names.
261    pub fn successes(&self) -> impl Iterator<Item = (&String, &TestResult)> {
262        self.tests().filter(|(_, t)| t.status.is_success())
263    }
264
265    /// Returns an iterator over all individual skipped tests and their names.
266    pub fn skips(&self) -> impl Iterator<Item = (&String, &TestResult)> {
267        self.tests().filter(|(_, t)| t.status.is_skipped())
268    }
269
270    /// Returns an iterator over all individual failing tests and their names.
271    pub fn failures(&self) -> impl Iterator<Item = (&String, &TestResult)> {
272        self.tests().filter(|(_, t)| t.status.is_failure())
273    }
274
275    /// Returns the number of tests that passed.
276    pub fn passed(&self) -> usize {
277        self.successes().count()
278    }
279
280    /// Returns the number of tests that were skipped.
281    pub fn skipped(&self) -> usize {
282        self.skips().count()
283    }
284
285    /// Returns the number of tests that failed.
286    pub fn failed(&self) -> usize {
287        self.failures().count()
288    }
289
290    /// Iterator over all tests and their names
291    pub fn tests(&self) -> impl Iterator<Item = (&String, &TestResult)> {
292        self.test_results.iter()
293    }
294
295    /// Whether this test suite is empty.
296    pub fn is_empty(&self) -> bool {
297        self.test_results.is_empty()
298    }
299
300    /// The number of tests in this test suite.
301    pub fn len(&self) -> usize {
302        self.test_results.len()
303    }
304
305    /// Sums up all the durations of all individual tests in this suite.
306    ///
307    /// Note that this is not necessarily the wall clock time of the entire test suite.
308    pub fn total_time(&self) -> Duration {
309        self.test_results.values().map(|result| result.duration).sum()
310    }
311
312    /// Returns the summary of a single test suite.
313    pub fn summary(&self) -> String {
314        let failed = self.failed();
315        let result = if failed == 0 { "ok".green() } else { "FAILED".red() };
316        format!(
317            "Suite result: {}. {} passed; {} failed; {} skipped; finished in {:.2?} ({:.2?} CPU time)",
318            result,
319            self.passed().green(),
320            failed.red(),
321            self.skipped().yellow(),
322            self.duration,
323            self.total_time(),
324        )
325    }
326}
327
328/// The result of a single test in a test suite.
329///
330/// This is flattened from a [`TestOutcome`].
331#[derive(Clone, Debug)]
332pub struct SuiteTestResult {
333    /// The identifier of the artifact/contract in the form:
334    /// `<artifact file name>:<contract name>`.
335    pub artifact_id: String,
336    /// The function signature of the Solidity test.
337    pub signature: String,
338    /// The result of the executed test.
339    pub result: TestResult,
340}
341
342impl SuiteTestResult {
343    /// Returns the gas used by the test.
344    pub fn gas_used(&self) -> u64 {
345        self.result.kind.report().gas()
346    }
347
348    /// Returns the contract name of the artifact ID.
349    pub fn contract_name(&self) -> &str {
350        get_contract_name(&self.artifact_id)
351    }
352
353    /// Returns the file name of the artifact ID.
354    pub fn file_name(&self) -> &str {
355        get_file_name(&self.artifact_id)
356    }
357}
358
359/// The status of a test.
360#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
361pub enum TestStatus {
362    Success,
363    #[default]
364    Failure,
365    Skipped,
366}
367
368impl TestStatus {
369    /// Returns `true` if the test was successful.
370    #[inline]
371    pub fn is_success(self) -> bool {
372        matches!(self, Self::Success)
373    }
374
375    /// Returns `true` if the test failed.
376    #[inline]
377    pub fn is_failure(self) -> bool {
378        matches!(self, Self::Failure)
379    }
380
381    /// Returns `true` if the test was skipped.
382    #[inline]
383    pub fn is_skipped(self) -> bool {
384        matches!(self, Self::Skipped)
385    }
386}
387
388/// The result of an executed test.
389#[derive(Clone, Debug, Default, Serialize, Deserialize)]
390pub struct TestResult {
391    /// The test status, indicating whether the test case succeeded, failed, or was marked as
392    /// skipped. This means that the transaction executed properly, the test was marked as
393    /// skipped with vm.skip(), or that there was a revert and that the test was expected to
394    /// fail (prefixed with `testFail`)
395    pub status: TestStatus,
396
397    /// If there was a revert, this field will be populated. Note that the test can
398    /// still be successful (i.e self.success == true) when it's expected to fail.
399    pub reason: Option<String>,
400
401    /// Minimal reproduction test case for failing test
402    pub counterexample: Option<CounterExample>,
403
404    /// Any captured & parsed as strings logs along the test's execution which should
405    /// be printed to the user.
406    pub logs: Vec<Log>,
407
408    /// The decoded DSTest logging events and Hardhat's `console.log` from [logs](Self::logs).
409    /// Used for json output.
410    pub decoded_logs: Vec<String>,
411
412    /// What kind of test this was
413    pub kind: TestKind,
414
415    /// Traces
416    pub traces: Traces,
417
418    /// Additional traces to use for gas report.
419    ///
420    /// These are cleared after the gas report is analyzed.
421    #[serde(skip)]
422    pub gas_report_traces: Vec<Vec<CallTraceArena>>,
423
424    /// Raw line coverage info
425    #[serde(skip)]
426    pub line_coverage: Option<HitMaps>,
427
428    /// Labeled addresses
429    #[serde(rename = "labeled_addresses")] // Backwards compatibility.
430    pub labels: AddressHashMap<String>,
431
432    #[serde(with = "foundry_common::serde_helpers::duration")]
433    pub duration: Duration,
434
435    /// pc breakpoint char map
436    pub breakpoints: Breakpoints,
437
438    /// Any captured gas snapshots along the test's execution which should be accumulated.
439    pub gas_snapshots: BTreeMap<String, BTreeMap<String, String>>,
440
441    /// Deprecated cheatcodes (mapped to their replacements, if any) used in current test.
442    #[serde(skip)]
443    pub deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>,
444}
445
446impl fmt::Display for TestResult {
447    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
448        match self.status {
449            TestStatus::Success => "[PASS]".green().fmt(f),
450            TestStatus::Skipped => {
451                let mut s = String::from("[SKIP");
452                if let Some(reason) = &self.reason {
453                    write!(s, ": {reason}").unwrap();
454                }
455                s.push(']');
456                s.yellow().fmt(f)
457            }
458            TestStatus::Failure => {
459                let mut s = String::from("[FAIL");
460                if self.reason.is_some() || self.counterexample.is_some() {
461                    if let Some(reason) = &self.reason {
462                        write!(s, ": {reason}").unwrap();
463                    }
464
465                    if let Some(counterexample) = &self.counterexample {
466                        match counterexample {
467                            CounterExample::Single(ex) => {
468                                write!(s, "; counterexample: {ex}]").unwrap();
469                            }
470                            CounterExample::Sequence(original, sequence) => {
471                                s.push_str(
472                                    format!(
473                                        "]\n\t[Sequence] (original: {original}, shrunk: {})\n",
474                                        sequence.len()
475                                    )
476                                    .as_str(),
477                                );
478                                for ex in sequence {
479                                    writeln!(s, "{ex}").unwrap();
480                                }
481                            }
482                        }
483                    } else {
484                        s.push(']');
485                    }
486                } else {
487                    s.push(']');
488                }
489                s.red().wrap().fmt(f)
490            }
491        }
492    }
493}
494
495macro_rules! extend {
496    ($a:expr, $b:expr, $trace_kind:expr) => {
497        $a.logs.extend($b.logs);
498        $a.labels.extend($b.labels);
499        $a.traces.extend($b.traces.map(|traces| ($trace_kind, traces)));
500        $a.merge_coverages($b.line_coverage);
501    };
502}
503
504impl TestResult {
505    /// Creates a new test result starting from test setup results.
506    pub fn new(setup: &TestSetup) -> Self {
507        Self {
508            labels: setup.labels.clone(),
509            logs: setup.logs.clone(),
510            traces: setup.traces.clone(),
511            line_coverage: setup.coverage.clone(),
512            ..Default::default()
513        }
514    }
515
516    /// Creates a failed test result with given reason.
517    pub fn fail(reason: String) -> Self {
518        Self { status: TestStatus::Failure, reason: Some(reason), ..Default::default() }
519    }
520
521    /// Creates a test setup result.
522    pub fn setup_result(setup: TestSetup) -> Self {
523        let TestSetup {
524            address: _,
525            fuzz_fixtures: _,
526            logs,
527            labels,
528            traces,
529            coverage,
530            deployed_libs: _,
531            reason,
532            skipped,
533            deployment_failure: _,
534        } = setup;
535        Self {
536            status: if skipped { TestStatus::Skipped } else { TestStatus::Failure },
537            reason,
538            logs,
539            traces,
540            line_coverage: coverage,
541            labels,
542            ..Default::default()
543        }
544    }
545
546    /// Returns the skipped result for single test (used in skipped fuzz test too).
547    pub fn single_skip(&mut self, reason: SkipReason) {
548        self.status = TestStatus::Skipped;
549        self.reason = reason.0;
550    }
551
552    /// Returns the failed result with reason for single test.
553    pub fn single_fail(&mut self, reason: Option<String>) {
554        self.status = TestStatus::Failure;
555        self.reason = reason;
556    }
557
558    /// Returns the result for single test. Merges execution results (logs, labeled addresses,
559    /// traces and coverages) in initial setup results.
560    pub fn single_result(
561        &mut self,
562        success: bool,
563        reason: Option<String>,
564        raw_call_result: RawCallResult,
565    ) {
566        self.kind =
567            TestKind::Unit { gas: raw_call_result.gas_used.wrapping_sub(raw_call_result.stipend) };
568
569        extend!(self, raw_call_result, TraceKind::Execution);
570
571        self.status = match success {
572            true => TestStatus::Success,
573            false => TestStatus::Failure,
574        };
575        self.reason = reason;
576        self.duration = Duration::default();
577        self.gas_report_traces = Vec::new();
578
579        if let Some(cheatcodes) = raw_call_result.cheatcodes {
580            self.breakpoints = cheatcodes.breakpoints;
581            self.gas_snapshots = cheatcodes.gas_snapshots;
582            self.deprecated_cheatcodes = cheatcodes.deprecated;
583        }
584    }
585
586    /// Returns the result for a fuzzed test. Merges fuzz execution results (logs, labeled
587    /// addresses, traces and coverages) in initial setup results.
588    pub fn fuzz_result(&mut self, result: FuzzTestResult) {
589        self.kind = TestKind::Fuzz {
590            median_gas: result.median_gas(false),
591            mean_gas: result.mean_gas(false),
592            first_case: result.first_case,
593            runs: result.gas_by_case.len(),
594            failed_corpus_replays: result.failed_corpus_replays,
595        };
596
597        // Record logs, labels, traces and merge coverages.
598        extend!(self, result, TraceKind::Execution);
599
600        self.status = if result.skipped {
601            TestStatus::Skipped
602        } else if result.success {
603            TestStatus::Success
604        } else {
605            TestStatus::Failure
606        };
607        self.reason = result.reason;
608        self.counterexample = result.counterexample;
609        self.duration = Duration::default();
610        self.gas_report_traces = result.gas_report_traces.into_iter().map(|t| vec![t]).collect();
611        self.breakpoints = result.breakpoints.unwrap_or_default();
612        self.deprecated_cheatcodes = result.deprecated_cheatcodes;
613    }
614
615    /// Returns the fail result for fuzz test setup.
616    pub fn fuzz_setup_fail(&mut self, e: Report) {
617        self.kind = TestKind::Fuzz {
618            first_case: Default::default(),
619            runs: 0,
620            mean_gas: 0,
621            median_gas: 0,
622            failed_corpus_replays: 0,
623        };
624        self.status = TestStatus::Failure;
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}