Skip to main content

forge/
result.rs

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