forge/
result.rs

1//! Test outcomes.
2
3use crate::{
4    fuzz::{BaseCounterExample, FuzzedCases},
5    gas_report::GasReport,
6};
7use alloy_primitives::{
8    map::{AddressHashMap, HashMap},
9    Address, Log,
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::{invariant::InvariantMetrics, RawCallResult},
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 = "humantime_serde")]
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 coverage info
406    #[serde(skip)]
407    pub coverage: Option<HitMaps>,
408
409    /// Labeled addresses
410    pub labeled_addresses: AddressHashMap<String>,
411
412    pub duration: Duration,
413
414    /// pc breakpoint char map
415    pub breakpoints: Breakpoints,
416
417    /// Any captured gas snapshots along the test's execution which should be accumulated.
418    pub gas_snapshots: BTreeMap<String, BTreeMap<String, String>>,
419
420    /// Deprecated cheatcodes (mapped to their replacements, if any) used in current test.
421    #[serde(skip)]
422    pub deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>,
423}
424
425impl fmt::Display for TestResult {
426    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
427        match self.status {
428            TestStatus::Success => "[PASS]".green().fmt(f),
429            TestStatus::Skipped => {
430                let mut s = String::from("[SKIP");
431                if let Some(reason) = &self.reason {
432                    write!(s, ": {reason}").unwrap();
433                }
434                s.push(']');
435                s.yellow().fmt(f)
436            }
437            TestStatus::Failure => {
438                let mut s = String::from("[FAIL");
439                if self.reason.is_some() || self.counterexample.is_some() {
440                    if let Some(reason) = &self.reason {
441                        write!(s, ": {reason}").unwrap();
442                    }
443
444                    if let Some(counterexample) = &self.counterexample {
445                        match counterexample {
446                            CounterExample::Single(ex) => {
447                                write!(s, "; counterexample: {ex}]").unwrap();
448                            }
449                            CounterExample::Sequence(original, sequence) => {
450                                s.push_str(
451                                    format!(
452                                        "]\n\t[Sequence] (original: {original}, shrunk: {})\n",
453                                        sequence.len()
454                                    )
455                                    .as_str(),
456                                );
457                                for ex in sequence {
458                                    writeln!(s, "{ex}").unwrap();
459                                }
460                            }
461                        }
462                    } else {
463                        s.push(']');
464                    }
465                } else {
466                    s.push(']');
467                }
468                s.red().fmt(f)
469            }
470        }
471    }
472}
473
474impl TestResult {
475    /// Creates a new test result starting from test setup results.
476    pub fn new(setup: &TestSetup) -> Self {
477        Self {
478            labeled_addresses: setup.labels.clone(),
479            logs: setup.logs.clone(),
480            traces: setup.traces.clone(),
481            coverage: setup.coverage.clone(),
482            ..Default::default()
483        }
484    }
485
486    /// Creates a failed test result with given reason.
487    pub fn fail(reason: String) -> Self {
488        Self { status: TestStatus::Failure, reason: Some(reason), ..Default::default() }
489    }
490
491    /// Creates a test setup result.
492    pub fn setup_result(setup: TestSetup) -> Self {
493        Self {
494            status: if setup.skipped { TestStatus::Skipped } else { TestStatus::Failure },
495            reason: setup.reason,
496            logs: setup.logs,
497            traces: setup.traces,
498            coverage: setup.coverage,
499            labeled_addresses: setup.labels,
500            ..Default::default()
501        }
502    }
503
504    /// Returns the skipped result for single test (used in skipped fuzz test too).
505    pub fn single_skip(&mut self, reason: SkipReason) {
506        self.status = TestStatus::Skipped;
507        self.reason = reason.0;
508    }
509
510    /// Returns the failed result with reason for single test.
511    pub fn single_fail(&mut self, reason: Option<String>) {
512        self.status = TestStatus::Failure;
513        self.reason = reason;
514    }
515
516    /// Returns the result for single test. Merges execution results (logs, labeled addresses,
517    /// traces and coverages) in initial setup results.
518    pub fn single_result(
519        &mut self,
520        success: bool,
521        reason: Option<String>,
522        raw_call_result: RawCallResult,
523    ) {
524        self.kind =
525            TestKind::Unit { gas: raw_call_result.gas_used.wrapping_sub(raw_call_result.stipend) };
526
527        // Record logs, labels, traces and merge coverages.
528        self.logs.extend(raw_call_result.logs);
529        self.labeled_addresses.extend(raw_call_result.labels);
530        self.traces.extend(raw_call_result.traces.map(|traces| (TraceKind::Execution, traces)));
531        self.merge_coverages(raw_call_result.coverage);
532
533        self.status = match success {
534            true => TestStatus::Success,
535            false => TestStatus::Failure,
536        };
537        self.reason = reason;
538        self.duration = Duration::default();
539        self.gas_report_traces = Vec::new();
540
541        if let Some(cheatcodes) = raw_call_result.cheatcodes {
542            self.breakpoints = cheatcodes.breakpoints;
543            self.gas_snapshots = cheatcodes.gas_snapshots;
544            self.deprecated_cheatcodes = cheatcodes.deprecated;
545        }
546    }
547
548    /// Returns the result for a fuzzed test. Merges fuzz execution results (logs, labeled
549    /// addresses, traces and coverages) in initial setup results.
550    pub fn fuzz_result(&mut self, result: FuzzTestResult) {
551        self.kind = TestKind::Fuzz {
552            median_gas: result.median_gas(false),
553            mean_gas: result.mean_gas(false),
554            first_case: result.first_case,
555            runs: result.gas_by_case.len(),
556        };
557
558        // Record logs, labels, traces and merge coverages.
559        self.logs.extend(result.logs);
560        self.labeled_addresses.extend(result.labeled_addresses);
561        self.traces.extend(result.traces.map(|traces| (TraceKind::Execution, traces)));
562        self.merge_coverages(result.coverage);
563
564        self.status = if result.skipped {
565            TestStatus::Skipped
566        } else if result.success {
567            TestStatus::Success
568        } else {
569            TestStatus::Failure
570        };
571        self.reason = result.reason;
572        self.counterexample = result.counterexample;
573        self.duration = Duration::default();
574        self.gas_report_traces = result.gas_report_traces.into_iter().map(|t| vec![t]).collect();
575        self.breakpoints = result.breakpoints.unwrap_or_default();
576        self.deprecated_cheatcodes = result.deprecated_cheatcodes;
577    }
578
579    /// Returns the skipped result for invariant test.
580    pub fn invariant_skip(&mut self, reason: SkipReason) {
581        self.kind =
582            TestKind::Invariant { runs: 1, calls: 1, reverts: 1, metrics: HashMap::default() };
583        self.status = TestStatus::Skipped;
584        self.reason = reason.0;
585    }
586
587    /// Returns the fail result for replayed invariant test.
588    pub fn invariant_replay_fail(
589        &mut self,
590        replayed_entirely: bool,
591        invariant_name: &String,
592        call_sequence: Vec<BaseCounterExample>,
593    ) {
594        self.kind =
595            TestKind::Invariant { runs: 1, calls: 1, reverts: 1, metrics: HashMap::default() };
596        self.status = TestStatus::Failure;
597        self.reason = if replayed_entirely {
598            Some(format!("{invariant_name} replay failure"))
599        } else {
600            Some(format!("{invariant_name} persisted failure revert"))
601        };
602        self.counterexample = Some(CounterExample::Sequence(call_sequence.len(), call_sequence));
603    }
604
605    /// Returns the fail result for invariant test setup.
606    pub fn invariant_setup_fail(&mut self, e: Report) {
607        self.kind =
608            TestKind::Invariant { runs: 0, calls: 0, reverts: 0, metrics: HashMap::default() };
609        self.status = TestStatus::Failure;
610        self.reason = Some(format!("failed to set up invariant testing environment: {e}"));
611    }
612
613    /// Returns the invariant test result.
614    #[expect(clippy::too_many_arguments)]
615    pub fn invariant_result(
616        &mut self,
617        gas_report_traces: Vec<Vec<CallTraceArena>>,
618        success: bool,
619        reason: Option<String>,
620        counterexample: Option<CounterExample>,
621        cases: Vec<FuzzedCases>,
622        reverts: usize,
623        metrics: Map<String, InvariantMetrics>,
624    ) {
625        self.kind = TestKind::Invariant {
626            runs: cases.len(),
627            calls: cases.iter().map(|sequence| sequence.cases().len()).sum(),
628            reverts,
629            metrics,
630        };
631        self.status = match success {
632            true => TestStatus::Success,
633            false => TestStatus::Failure,
634        };
635        self.reason = reason;
636        self.counterexample = counterexample;
637        self.gas_report_traces = gas_report_traces;
638    }
639
640    /// Returns `true` if this is the result of a fuzz test
641    pub fn is_fuzz(&self) -> bool {
642        matches!(self.kind, TestKind::Fuzz { .. })
643    }
644
645    /// Formats the test result into a string (for printing).
646    pub fn short_result(&self, name: &str) -> String {
647        format!("{self} {name} {}", self.kind.report())
648    }
649
650    /// Merges the given raw call result into `self`.
651    pub fn extend(&mut self, call_result: RawCallResult) {
652        self.logs.extend(call_result.logs);
653        self.labeled_addresses.extend(call_result.labels);
654        self.traces.extend(call_result.traces.map(|traces| (TraceKind::Execution, traces)));
655        self.merge_coverages(call_result.coverage);
656    }
657
658    /// Merges the given coverage result into `self`.
659    pub fn merge_coverages(&mut self, other_coverage: Option<HitMaps>) {
660        HitMaps::merge_opt(&mut self.coverage, other_coverage);
661    }
662}
663
664/// Data report by a test.
665#[derive(Clone, Debug, PartialEq, Eq)]
666pub enum TestKindReport {
667    Unit { gas: u64 },
668    Fuzz { runs: usize, mean_gas: u64, median_gas: u64 },
669    Invariant { runs: usize, calls: usize, reverts: usize, metrics: Map<String, InvariantMetrics> },
670}
671
672impl fmt::Display for TestKindReport {
673    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
674        match self {
675            Self::Unit { gas } => {
676                write!(f, "(gas: {gas})")
677            }
678            Self::Fuzz { runs, mean_gas, median_gas } => {
679                write!(f, "(runs: {runs}, μ: {mean_gas}, ~: {median_gas})")
680            }
681            Self::Invariant { runs, calls, reverts, metrics: _ } => {
682                write!(f, "(runs: {runs}, calls: {calls}, reverts: {reverts})")
683            }
684        }
685    }
686}
687
688impl TestKindReport {
689    /// Returns the main gas value to compare against
690    pub fn gas(&self) -> u64 {
691        match *self {
692            Self::Unit { gas } => gas,
693            // We use the median for comparisons
694            Self::Fuzz { median_gas, .. } => median_gas,
695            // We return 0 since it's not applicable
696            Self::Invariant { .. } => 0,
697        }
698    }
699}
700
701/// Various types of tests
702#[derive(Clone, Debug, Serialize, Deserialize)]
703pub enum TestKind {
704    /// A unit test.
705    Unit { gas: u64 },
706    /// A fuzz test.
707    Fuzz {
708        /// we keep this for the debugger
709        first_case: FuzzCase,
710        runs: usize,
711        mean_gas: u64,
712        median_gas: u64,
713    },
714    /// An invariant test.
715    Invariant { runs: usize, calls: usize, reverts: usize, metrics: Map<String, InvariantMetrics> },
716}
717
718impl Default for TestKind {
719    fn default() -> Self {
720        Self::Unit { gas: 0 }
721    }
722}
723
724impl TestKind {
725    /// The gas consumed by this test
726    pub fn report(&self) -> TestKindReport {
727        match self {
728            Self::Unit { gas } => TestKindReport::Unit { gas: *gas },
729            Self::Fuzz { first_case: _, runs, mean_gas, median_gas } => {
730                TestKindReport::Fuzz { runs: *runs, mean_gas: *mean_gas, median_gas: *median_gas }
731            }
732            Self::Invariant { runs, calls, reverts, metrics: _ } => TestKindReport::Invariant {
733                runs: *runs,
734                calls: *calls,
735                reverts: *reverts,
736                metrics: HashMap::default(),
737            },
738        }
739    }
740}
741
742/// The result of a test setup.
743///
744/// Includes the deployment of the required libraries and the test contract itself, and the call to
745/// the `setUp()` function.
746#[derive(Clone, Debug, Default)]
747pub struct TestSetup {
748    /// The address at which the test contract was deployed.
749    pub address: Address,
750    /// Defined fuzz test fixtures.
751    pub fuzz_fixtures: FuzzFixtures,
752
753    /// The logs emitted during setup.
754    pub logs: Vec<Log>,
755    /// Addresses labeled during setup.
756    pub labels: AddressHashMap<String>,
757    /// Call traces of the setup.
758    pub traces: Traces,
759    /// Coverage info during setup.
760    pub coverage: Option<HitMaps>,
761    /// Addresses of external libraries deployed during setup.
762    pub deployed_libs: Vec<Address>,
763
764    /// The reason the setup failed, if it did.
765    pub reason: Option<String>,
766    /// Whether setup and entire test suite is skipped.
767    pub skipped: bool,
768    /// Whether the test failed to deploy.
769    pub deployment_failure: bool,
770}
771
772impl TestSetup {
773    pub fn failed(reason: String) -> Self {
774        Self { reason: Some(reason), ..Default::default() }
775    }
776
777    pub fn skipped(reason: String) -> Self {
778        Self { reason: Some(reason), skipped: true, ..Default::default() }
779    }
780
781    pub fn extend(&mut self, raw: RawCallResult, trace_kind: TraceKind) {
782        self.logs.extend(raw.logs);
783        self.labels.extend(raw.labels);
784        self.traces.extend(raw.traces.map(|traces| (trace_kind, traces)));
785        HitMaps::merge_opt(&mut self.coverage, raw.coverage);
786    }
787}