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