forge/
result.rs

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