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, Selector, 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/// A failure surfaced by an invariant test campaign — either a broken `invariant_*`
412/// predicate ([`Self::Predicate`]) or a handler-side assertion bug ([`Self::Handler`]).
413#[derive(Clone, Debug, Serialize, Deserialize)]
414#[serde(tag = "kind", rename_all = "snake_case")]
415pub enum InvariantFailure {
416    /// A broken `invariant_*` predicate.
417    Predicate {
418        /// Invariant function name (e.g. `invariant_cond3`).
419        name: String,
420        /// Revert reason or assertion failure message.
421        reason: String,
422        /// Counterexample sequence, when one is available.
423        #[serde(default, skip_serializing_if = "Option::is_none")]
424        counterexample: Option<CounterExample>,
425        /// Path where the counterexample was persisted for re-running and shrinking.
426        persisted_path: std::path::PathBuf,
427        /// Whether this failure is the campaign anchor (the `--mt`-selected invariant).
428        /// When `true` and this is the only failure, the function name is omitted on the
429        /// `[FAIL: ...]` line (the trailing summary already identifies it).
430        #[serde(default)]
431        is_anchor: bool,
432    },
433    /// A handler-side assertion bug discovered during the campaign.
434    Handler {
435        /// Best-effort human-readable name of the failing call, e.g. `Counter::increment` or
436        /// `0xabc...::0x12345678` when the contract/function cannot be resolved.
437        name: String,
438        /// Address of the handler whose call asserted/reverted with an assertion.
439        reverter: Address,
440        /// 4-byte selector of the failing handler function.
441        selector: Selector,
442        /// Decoded revert/assert reason.
443        reason: String,
444        /// Counterexample sequence leading up to (and including) the failing call.
445        #[serde(default, skip_serializing_if = "Option::is_none")]
446        counterexample: Option<CounterExample>,
447    },
448}
449
450impl InvariantFailure {
451    /// Reason rendered on the `[FAIL: ...]` line.
452    pub fn reason(&self) -> &str {
453        match self {
454            Self::Predicate { reason, .. } | Self::Handler { reason, .. } => reason,
455        }
456    }
457
458    /// Human-readable name (invariant fn name, or `Contract::function` for handler bugs).
459    pub fn name(&self) -> &str {
460        match self {
461            Self::Predicate { name, .. } | Self::Handler { name, .. } => name,
462        }
463    }
464
465    /// Counterexample sequence, when one is available.
466    pub const fn counterexample(&self) -> Option<&CounterExample> {
467        match self {
468            Self::Predicate { counterexample, .. } | Self::Handler { counterexample, .. } => {
469                counterexample.as_ref()
470            }
471        }
472    }
473}
474
475/// The result of an executed test.
476#[derive(Clone, Debug, Default, Serialize, Deserialize)]
477pub struct TestResult {
478    /// The test status, indicating whether the test case succeeded, failed, or was marked as
479    /// skipped. This means that the transaction executed properly, the test was marked as
480    /// skipped with vm.skip(), or that there was a revert and that the test was expected to
481    /// fail (prefixed with `testFail`)
482    pub status: TestStatus,
483
484    /// If there was a revert, this field will be populated. Note that the test can
485    /// still be successful (i.e self.success == true) when it's expected to fail.
486    pub reason: Option<String>,
487
488    /// All broken invariants in this campaign — anchor (`--mt` target) and any `assert_all`
489    /// secondaries — in source declaration order.
490    ///
491    /// For invariant tests, this is the single source of truth used by the renderer.
492    /// `reason` and `counterexample` are not populated for invariant tests.
493    #[serde(default, skip_serializing_if = "Vec::is_empty")]
494    pub invariant_failures: Vec<InvariantFailure>,
495
496    /// Directory where invariant failure counterexamples have been persisted (set when one or more
497    /// secondary invariant failures were written, so users can locate persisted counterexamples).
498    #[serde(default, skip_serializing_if = "Option::is_none")]
499    pub invariant_failure_dir: Option<std::path::PathBuf>,
500
501    /// Total number of invariants exercised in this `assert_all` run (primary + secondaries that
502    /// were not skipped by persisted-failure filtering). When `Some(n)` the test report renders
503    /// a `Suite assert_all: <broken>/<n> invariants broken` summary so users get an at-a-glance
504    /// health line without counting `[FAIL]` blocks. `None` for non-`assert_all` campaigns.
505    #[serde(default, skip_serializing_if = "Option::is_none")]
506    pub assert_all_invariant_count: Option<usize>,
507
508    /// Handler-side assertion bugs found during the campaign, deduped by
509    /// `(reverter, selector)` site (Medusa/Echidna semantics). Rendered in a dedicated
510    /// `Suite handlers:` section.
511    #[serde(default, skip_serializing_if = "Vec::is_empty")]
512    pub invariant_handler_failures: Vec<InvariantFailure>,
513
514    /// Minimal reproduction test case for failing test
515    pub counterexample: Option<CounterExample>,
516
517    /// Any captured & parsed as strings logs along the test's execution which should
518    /// be printed to the user.
519    pub logs: Vec<Log>,
520
521    /// The decoded DSTest logging events and Hardhat's `console.log` from [logs](Self::logs).
522    /// Used for json output.
523    pub decoded_logs: Vec<String>,
524
525    /// What kind of test this was
526    pub kind: TestKind,
527
528    /// Traces
529    pub traces: Traces,
530
531    /// Additional traces to use for gas report.
532    ///
533    /// These are cleared after the gas report is analyzed.
534    #[serde(skip)]
535    pub gas_report_traces: Vec<Vec<CallTraceArena>>,
536
537    /// Raw line coverage info
538    #[serde(skip)]
539    pub line_coverage: Option<HitMaps>,
540
541    /// Labeled addresses
542    #[serde(rename = "labeled_addresses")] // Backwards compatibility.
543    pub labels: AddressHashMap<String>,
544
545    #[serde(with = "foundry_common::serde_helpers::duration")]
546    pub duration: Duration,
547
548    /// pc breakpoint char map
549    pub breakpoints: Breakpoints,
550
551    /// Any captured gas snapshots along the test's execution which should be accumulated.
552    pub gas_snapshots: BTreeMap<String, BTreeMap<String, String>>,
553
554    /// Deprecated cheatcodes (mapped to their replacements, if any) used in current test.
555    #[serde(skip)]
556    pub deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>,
557}
558
559impl fmt::Display for TestResult {
560    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
561        match self.status {
562            TestStatus::Success => {
563                // For optimization mode, show the best example sequence in green.
564                if let Some(CounterExample::Sequence(original, sequence)) = &self.counterexample {
565                    let mut s = String::from("[PASS]");
566                    s.push_str(
567                        format!(
568                            "\n\t[Best sequence] (original: {original}, shrunk: {})\n",
569                            sequence.len()
570                        )
571                        .as_str(),
572                    );
573                    for ex in sequence {
574                        writeln!(s, "{ex}").unwrap();
575                    }
576                    s.green().wrap().fmt(f)
577                } else {
578                    "[PASS]".green().fmt(f)
579                }
580            }
581            TestStatus::Skipped => {
582                let mut s = String::from("[SKIP");
583                if let Some(reason) = &self.reason {
584                    write!(s, ": {reason}").unwrap();
585                }
586                s.push(']');
587                s.yellow().fmt(f)
588            }
589            TestStatus::Failure => {
590                let mut s = String::new();
591                let has_handler_failures = !self.invariant_handler_failures.is_empty();
592                let is_invariant_failure =
593                    !self.invariant_failures.is_empty() || has_handler_failures;
594                if !is_invariant_failure {
595                    // Non-invariant failure (unit / fuzz / DS-style): render from the legacy
596                    // `reason` / `counterexample` fields.
597                    s.push_str("[FAIL");
598                    if let Some(reason) = &self.reason {
599                        write!(s, ": {reason}").unwrap();
600                    }
601                    if let Some(counterexample) = &self.counterexample {
602                        match counterexample {
603                            CounterExample::Single(ex) => {
604                                write!(s, "; counterexample: {ex}]").unwrap();
605                            }
606                            CounterExample::Sequence(original, sequence) => {
607                                writeln!(
608                                    s,
609                                    "]\n\t[Sequence] (original: {original}, shrunk: {})",
610                                    sequence.len()
611                                )
612                                .unwrap();
613                                for ex in sequence {
614                                    writeln!(s, "{ex}").unwrap();
615                                }
616                            }
617                        }
618                    } else {
619                        s.push(']');
620                    }
621                } else if !self.invariant_failures.is_empty() {
622                    // Render every broken invariant uniformly. Show the function name on the
623                    // `[FAIL: ...]` line when there is >1 failure or the failure isn't the
624                    // anchor (the anchor's name is already on the trailing summary).
625                    let multi = self.invariant_failures.len() > 1;
626                    for (i, failure) in self.invariant_failures.iter().enumerate() {
627                        if i > 0 {
628                            s.push('\n');
629                        }
630                        // `is_anchor` only applies to predicate failures.
631                        let is_anchor =
632                            matches!(failure, InvariantFailure::Predicate { is_anchor: true, .. });
633                        let name_suffix = if multi || !is_anchor {
634                            format!(" {}", failure.name())
635                        } else {
636                            String::new()
637                        };
638                        // With a counterexample: full `[FAIL: reason]<suffix>\n\t[Sequence] ...`
639                        // block; otherwise just `[FAIL: reason]<suffix>`.
640                        if let Some(CounterExample::Sequence(original, sequence)) =
641                            failure.counterexample()
642                        {
643                            writeln!(
644                                s,
645                                "[FAIL: {}]{name_suffix}\n\t[Sequence] (original: {original}, shrunk: {})",
646                                failure.reason(),
647                                sequence.len()
648                            )
649                            .unwrap();
650                            for ex in sequence {
651                                writeln!(s, "{ex}").unwrap();
652                            }
653                        } else {
654                            write!(s, "[FAIL: {}]{name_suffix}", failure.reason()).unwrap();
655                        }
656                    }
657                }
658                // Suite roll-up: `Suite assert_all: <broken>/<total>` for multi-invariant
659                // `assert_all` campaigns.
660                if let Some(total) = self.assert_all_invariant_count
661                    && total > 1
662                    && is_invariant_failure
663                {
664                    writeln!(
665                        s,
666                        "\nSuite assert_all: {}/{total} invariants broken",
667                        self.invariant_failures.len()
668                    )
669                    .unwrap();
670                }
671                // Persistence note only for multi-invariant campaigns; rendered after the
672                // `Suite assert_all:` roll-up.
673                if self.invariant_failures.len() > 1
674                    && let Some(dir) = &self.invariant_failure_dir
675                {
676                    writeln!(
677                        s,
678                        "{} invariant failure(s) persisted to {} — rerun to shrink",
679                        self.invariant_failures.len(),
680                        dir.display()
681                    )
682                    .unwrap();
683                }
684                // Handler-side assertion bug section: bugs *inside* a fuzzed handler function,
685                // distinct from the invariant predicate violations rendered above. Surfaced as
686                // `Suite handlers: N assertion bug(s) found` + one `[FAIL: ...]` per site.
687                if has_handler_failures {
688                    // Leading blank line if a preceding section was rendered.
689                    let preceded = !self.invariant_failures.is_empty()
690                        || matches!(self.assert_all_invariant_count, Some(t) if t > 1);
691                    let prefix = if preceded { "\n" } else { "" };
692                    writeln!(
693                        s,
694                        "{prefix}Suite handlers: {} assertion bug(s) found",
695                        self.invariant_handler_failures.len()
696                    )
697                    .unwrap();
698                    for failure in &self.invariant_handler_failures {
699                        if let Some(CounterExample::Sequence(original, sequence)) =
700                            failure.counterexample()
701                        {
702                            writeln!(
703                                s,
704                                "[FAIL: {}] {}\n\t[Sequence] (original: {original}, shrunk: {})",
705                                failure.reason(),
706                                failure.name(),
707                                sequence.len()
708                            )
709                            .unwrap();
710                            for ex in sequence {
711                                writeln!(s, "{ex}").unwrap();
712                            }
713                        } else {
714                            writeln!(s, "[FAIL: {}] {}", failure.reason(), failure.name()).unwrap();
715                        }
716                    }
717                }
718                s.red().wrap().fmt(f)
719            }
720        }
721    }
722}
723
724macro_rules! extend {
725    ($a:expr, $b:expr, $trace_kind:expr) => {
726        $a.logs.extend($b.logs);
727        $a.labels.extend($b.labels);
728        $a.traces.extend($b.traces.map(|traces| ($trace_kind, traces)));
729        $a.merge_coverages($b.line_coverage);
730    };
731}
732
733impl TestResult {
734    /// Creates a new test result starting from test setup results.
735    pub fn new(setup: &TestSetup) -> Self {
736        Self {
737            labels: setup.labels.clone(),
738            logs: setup.logs.clone(),
739            traces: setup.traces.clone(),
740            line_coverage: setup.coverage.clone(),
741            ..Default::default()
742        }
743    }
744
745    /// Creates a failed test result with given reason.
746    pub fn fail(reason: String) -> Self {
747        Self { status: TestStatus::Failure, reason: Some(reason), ..Default::default() }
748    }
749
750    /// Creates a test setup result.
751    pub fn setup_result(setup: TestSetup) -> Self {
752        let TestSetup {
753            address: _,
754            fuzz_fixtures: _,
755            logs,
756            labels,
757            traces,
758            coverage,
759            deployed_libs: _,
760            reason,
761            skipped,
762            deployment_failure: _,
763        } = setup;
764        Self {
765            status: if skipped { TestStatus::Skipped } else { TestStatus::Failure },
766            reason,
767            logs,
768            traces,
769            line_coverage: coverage,
770            labels,
771            ..Default::default()
772        }
773    }
774
775    /// Returns the skipped result for single test (used in skipped fuzz test too).
776    pub fn single_skip(&mut self, reason: SkipReason) {
777        self.status = TestStatus::Skipped;
778        self.reason = reason.0;
779    }
780
781    /// Returns the failed result with reason for single test.
782    pub fn single_fail(&mut self, reason: Option<String>) {
783        self.status = TestStatus::Failure;
784        self.reason = reason;
785    }
786
787    /// Returns the result for single test. Merges execution results (logs, labeled addresses,
788    /// traces and coverages) in initial setup results.
789    pub fn single_result<FEN: FoundryEvmNetwork>(
790        &mut self,
791        success: bool,
792        reason: Option<String>,
793        raw_call_result: RawCallResult<FEN>,
794    ) {
795        self.kind = TestKind::Unit {
796            gas: raw_call_result.gas_used.saturating_sub(raw_call_result.stipend),
797        };
798
799        extend!(self, raw_call_result, TraceKind::Execution);
800
801        self.status = match success {
802            true => TestStatus::Success,
803            false => TestStatus::Failure,
804        };
805        self.reason = reason;
806        self.duration = Duration::default();
807        self.gas_report_traces = Vec::new();
808
809        if let Some(cheatcodes) = raw_call_result.cheatcodes {
810            self.breakpoints = cheatcodes.breakpoints;
811            self.gas_snapshots = cheatcodes.gas_snapshots;
812            self.deprecated_cheatcodes = cheatcodes.deprecated;
813        }
814    }
815
816    /// Returns the result for a fuzzed test. Merges fuzz execution results (logs, labeled
817    /// addresses, traces and coverages) in initial setup results.
818    pub fn fuzz_result(&mut self, result: FuzzTestResult) {
819        self.kind = TestKind::Fuzz {
820            median_gas: result.median_gas(false),
821            mean_gas: result.mean_gas(false),
822            first_case: result.first_case,
823            runs: result.gas_by_case.len(),
824            failed_corpus_replays: result.failed_corpus_replays,
825        };
826
827        // Record logs, labels, traces and merge coverages.
828        extend!(self, result, TraceKind::Execution);
829
830        self.status = if result.skipped {
831            TestStatus::Skipped
832        } else if result.success {
833            TestStatus::Success
834        } else {
835            TestStatus::Failure
836        };
837        self.reason = result.reason;
838        self.counterexample = result.counterexample;
839        self.duration = Duration::default();
840        self.gas_report_traces = result.gas_report_traces.into_iter().map(|t| vec![t]).collect();
841        self.breakpoints = result.breakpoints.unwrap_or_default();
842        self.deprecated_cheatcodes = result.deprecated_cheatcodes;
843    }
844
845    /// Returns the fail result for fuzz test setup.
846    pub fn fuzz_setup_fail(&mut self, e: Report) {
847        self.kind = TestKind::Fuzz {
848            first_case: Default::default(),
849            runs: 0,
850            mean_gas: 0,
851            median_gas: 0,
852            failed_corpus_replays: 0,
853        };
854        self.status = TestStatus::Failure;
855        debug!(?e, "failed to set up fuzz testing environment");
856        self.reason = Some(format!("failed to set up fuzz testing environment: {e}"));
857    }
858
859    /// Returns the skipped result for invariant test.
860    pub fn invariant_skip(&mut self, reason: SkipReason) {
861        self.kind = TestKind::Invariant {
862            runs: 1,
863            calls: 1,
864            reverts: 1,
865            metrics: HashMap::default(),
866            failed_corpus_replays: 0,
867            optimization_best_value: None,
868        };
869        self.status = TestStatus::Skipped;
870        self.reason = reason.0;
871    }
872
873    /// Returns the fail result for replayed invariant test.
874    pub fn invariant_replay_fail(
875        &mut self,
876        replayed_entirely: bool,
877        invariant_name: &String,
878        replay_reason: Option<String>,
879        call_sequence: Vec<BaseCounterExample>,
880    ) {
881        self.kind = TestKind::Invariant {
882            runs: 1,
883            calls: 1,
884            reverts: 1,
885            metrics: HashMap::default(),
886            failed_corpus_replays: 0,
887            optimization_best_value: None,
888        };
889        self.status = TestStatus::Failure;
890        self.reason = replay_reason.or_else(|| {
891            if replayed_entirely {
892                Some(format!("{invariant_name} replay failure"))
893            } else {
894                Some(format!("{invariant_name} persisted failure revert"))
895            }
896        });
897        self.counterexample = Some(CounterExample::Sequence(call_sequence.len(), call_sequence));
898    }
899
900    /// Returns the fail result for invariant test setup.
901    pub fn invariant_setup_fail(&mut self, e: Report) {
902        self.kind = TestKind::Invariant {
903            runs: 0,
904            calls: 0,
905            reverts: 0,
906            metrics: HashMap::default(),
907            failed_corpus_replays: 0,
908            optimization_best_value: None,
909        };
910        self.status = TestStatus::Failure;
911        self.reason = Some(format!("failed to set up invariant testing environment: {e}"));
912    }
913
914    /// Returns the invariant test result.
915    #[expect(clippy::too_many_arguments)]
916    pub fn invariant_result(
917        &mut self,
918        gas_report_traces: Vec<Vec<CallTraceArena>>,
919        success: bool,
920        invariant_failures: Vec<InvariantFailure>,
921        invariant_failure_dir: Option<std::path::PathBuf>,
922        assert_all_invariant_count: Option<usize>,
923        invariant_handler_failures: Vec<InvariantFailure>,
924        counterexample: Option<CounterExample>,
925        cases: Vec<FuzzedCases>,
926        reverts: usize,
927        metrics: Map<String, InvariantMetrics>,
928        failed_corpus_replays: usize,
929        optimization_best_value: Option<I256>,
930    ) {
931        self.kind = TestKind::Invariant {
932            runs: cases.len(),
933            calls: cases.iter().map(|sequence| sequence.cases().len()).sum(),
934            reverts,
935            metrics,
936            failed_corpus_replays,
937            optimization_best_value,
938        };
939        // For optimization mode (Some value), always succeed. For check mode (None), use success.
940        self.status = if optimization_best_value.is_some() || success {
941            TestStatus::Success
942        } else {
943            TestStatus::Failure
944        };
945        self.invariant_failures = invariant_failures;
946        self.invariant_failure_dir = invariant_failure_dir;
947        self.assert_all_invariant_count = assert_all_invariant_count;
948        self.invariant_handler_failures = invariant_handler_failures;
949        // `counterexample` is only used by the renderer for optimization mode (the "best
950        // sequence" rendered on success). Invariant check-mode failures live entirely in
951        // `invariant_failures`; `reason`/`counterexample` stay `None` for invariant tests.
952        self.counterexample = counterexample;
953        self.gas_report_traces = gas_report_traces;
954    }
955
956    /// Returns the result for a table test. Merges table test execution results (logs, labeled
957    /// addresses, traces and coverages) in initial setup results.
958    pub fn table_result(&mut self, result: FuzzTestResult) {
959        self.kind = TestKind::Table {
960            median_gas: result.median_gas(false),
961            mean_gas: result.mean_gas(false),
962            runs: result.gas_by_case.len(),
963        };
964
965        // Record logs, labels, traces and merge coverages.
966        extend!(self, result, TraceKind::Execution);
967
968        self.status = if result.skipped {
969            TestStatus::Skipped
970        } else if result.success {
971            TestStatus::Success
972        } else {
973            TestStatus::Failure
974        };
975        self.reason = result.reason;
976        self.counterexample = result.counterexample;
977        self.duration = Duration::default();
978        self.gas_report_traces = result.gas_report_traces.into_iter().map(|t| vec![t]).collect();
979        self.breakpoints = result.breakpoints.unwrap_or_default();
980        self.deprecated_cheatcodes = result.deprecated_cheatcodes;
981    }
982
983    /// Returns `true` if this is the result of a fuzz test
984    pub const fn is_fuzz(&self) -> bool {
985        matches!(self.kind, TestKind::Fuzz { .. })
986    }
987
988    /// Formats the test result into a string (for printing).
989    pub fn short_result(&self, name: &str) -> String {
990        format!("{self} {name} {}", self.kind.report())
991    }
992
993    /// Merges the given raw call result into `self`.
994    pub fn extend<FEN: FoundryEvmNetwork>(&mut self, call_result: RawCallResult<FEN>) {
995        extend!(self, call_result, TraceKind::Execution);
996    }
997
998    /// Merges the given coverage result into `self`.
999    pub fn merge_coverages(&mut self, other_coverage: Option<HitMaps>) {
1000        HitMaps::merge_opt(&mut self.line_coverage, other_coverage);
1001    }
1002}
1003
1004/// Data report by a test.
1005#[derive(Clone, Debug, PartialEq, Eq)]
1006pub enum TestKindReport {
1007    Unit {
1008        gas: u64,
1009    },
1010    Fuzz {
1011        runs: usize,
1012        mean_gas: u64,
1013        median_gas: u64,
1014        failed_corpus_replays: usize,
1015    },
1016    Invariant {
1017        runs: usize,
1018        calls: usize,
1019        reverts: usize,
1020        metrics: Map<String, InvariantMetrics>,
1021        failed_corpus_replays: usize,
1022        /// For optimization mode (int256 return): the best value achieved. None = check mode.
1023        optimization_best_value: Option<I256>,
1024    },
1025    Table {
1026        runs: usize,
1027        mean_gas: u64,
1028        median_gas: u64,
1029    },
1030}
1031
1032impl fmt::Display for TestKindReport {
1033    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1034        match self {
1035            Self::Unit { gas } => {
1036                write!(f, "(gas: {gas})")
1037            }
1038            Self::Fuzz { runs, mean_gas, median_gas, failed_corpus_replays } => {
1039                if *failed_corpus_replays != 0 {
1040                    write!(
1041                        f,
1042                        "(runs: {runs}, μ: {mean_gas}, ~: {median_gas}, failed corpus replays: {failed_corpus_replays})"
1043                    )
1044                } else {
1045                    write!(f, "(runs: {runs}, μ: {mean_gas}, ~: {median_gas})")
1046                }
1047            }
1048            Self::Invariant {
1049                runs,
1050                calls,
1051                reverts,
1052                metrics: _,
1053                failed_corpus_replays,
1054                optimization_best_value,
1055            } => {
1056                // If optimization_best_value is Some, this is optimization mode.
1057                if let Some(best_value) = optimization_best_value {
1058                    write!(f, "(best: {best_value}, runs: {runs}, calls: {calls})")
1059                } else if *failed_corpus_replays != 0 {
1060                    write!(
1061                        f,
1062                        "(runs: {runs}, calls: {calls}, reverts: {reverts}, failed corpus replays: {failed_corpus_replays})"
1063                    )
1064                } else {
1065                    write!(f, "(runs: {runs}, calls: {calls}, reverts: {reverts})")
1066                }
1067            }
1068            Self::Table { runs, mean_gas, median_gas } => {
1069                write!(f, "(runs: {runs}, μ: {mean_gas}, ~: {median_gas})")
1070            }
1071        }
1072    }
1073}
1074
1075impl TestKindReport {
1076    /// Returns the main gas value to compare against
1077    pub const fn gas(&self) -> u64 {
1078        match *self {
1079            Self::Unit { gas } => gas,
1080            // We use the median for comparisons
1081            Self::Fuzz { median_gas, .. } | Self::Table { median_gas, .. } => median_gas,
1082            // We return 0 since it's not applicable
1083            Self::Invariant { .. } => 0,
1084        }
1085    }
1086}
1087
1088/// Various types of tests
1089#[derive(Clone, Debug, Serialize, Deserialize)]
1090pub enum TestKind {
1091    /// A unit test.
1092    Unit { gas: u64 },
1093    /// A fuzz test.
1094    Fuzz {
1095        /// we keep this for the debugger
1096        first_case: FuzzCase,
1097        runs: usize,
1098        mean_gas: u64,
1099        median_gas: u64,
1100        failed_corpus_replays: usize,
1101    },
1102    /// An invariant test.
1103    Invariant {
1104        runs: usize,
1105        calls: usize,
1106        reverts: usize,
1107        metrics: Map<String, InvariantMetrics>,
1108        failed_corpus_replays: usize,
1109        /// For optimization mode (int256 return): the best value achieved. None = check mode.
1110        optimization_best_value: Option<I256>,
1111    },
1112    /// A table test.
1113    Table { runs: usize, mean_gas: u64, median_gas: u64 },
1114}
1115
1116impl Default for TestKind {
1117    fn default() -> Self {
1118        Self::Unit { gas: 0 }
1119    }
1120}
1121
1122impl TestKind {
1123    /// Returns `true` if this is a fuzz test.
1124    pub const fn is_fuzz(&self) -> bool {
1125        matches!(self, Self::Fuzz { .. })
1126    }
1127
1128    /// Returns `true` if this is an invariant test.
1129    pub const fn is_invariant(&self) -> bool {
1130        matches!(self, Self::Invariant { .. })
1131    }
1132
1133    /// The gas consumed by this test
1134    pub fn report(&self) -> TestKindReport {
1135        match self {
1136            Self::Unit { gas } => TestKindReport::Unit { gas: *gas },
1137            Self::Fuzz { first_case: _, runs, mean_gas, median_gas, failed_corpus_replays } => {
1138                TestKindReport::Fuzz {
1139                    runs: *runs,
1140                    mean_gas: *mean_gas,
1141                    median_gas: *median_gas,
1142                    failed_corpus_replays: *failed_corpus_replays,
1143                }
1144            }
1145            Self::Invariant {
1146                runs,
1147                calls,
1148                reverts,
1149                metrics: _,
1150                failed_corpus_replays,
1151                optimization_best_value,
1152            } => TestKindReport::Invariant {
1153                runs: *runs,
1154                calls: *calls,
1155                reverts: *reverts,
1156                metrics: HashMap::default(),
1157                failed_corpus_replays: *failed_corpus_replays,
1158                optimization_best_value: *optimization_best_value,
1159            },
1160            Self::Table { runs, mean_gas, median_gas } => {
1161                TestKindReport::Table { runs: *runs, mean_gas: *mean_gas, median_gas: *median_gas }
1162            }
1163        }
1164    }
1165}
1166
1167/// The result of a test setup.
1168///
1169/// Includes the deployment of the required libraries and the test contract itself, and the call to
1170/// the `setUp()` function.
1171#[derive(Clone, Debug, Default)]
1172pub struct TestSetup {
1173    /// The address at which the test contract was deployed.
1174    pub address: Address,
1175    /// Defined fuzz test fixtures.
1176    pub fuzz_fixtures: FuzzFixtures,
1177
1178    /// The logs emitted during setup.
1179    pub logs: Vec<Log>,
1180    /// Addresses labeled during setup.
1181    pub labels: AddressHashMap<String>,
1182    /// Call traces of the setup.
1183    pub traces: Traces,
1184    /// Coverage info during setup.
1185    pub coverage: Option<HitMaps>,
1186    /// Addresses of external libraries deployed during setup.
1187    pub deployed_libs: Vec<Address>,
1188
1189    /// The reason the setup failed, if it did.
1190    pub reason: Option<String>,
1191    /// Whether setup and entire test suite is skipped.
1192    pub skipped: bool,
1193    /// Whether the test failed to deploy.
1194    pub deployment_failure: bool,
1195}
1196
1197impl TestSetup {
1198    pub fn failed(reason: String) -> Self {
1199        Self { reason: Some(reason), ..Default::default() }
1200    }
1201
1202    pub fn skipped(reason: String) -> Self {
1203        Self { reason: Some(reason), skipped: true, ..Default::default() }
1204    }
1205
1206    pub fn extend<FEN: FoundryEvmNetwork>(
1207        &mut self,
1208        raw: RawCallResult<FEN>,
1209        trace_kind: TraceKind,
1210    ) {
1211        extend!(self, raw, trace_kind);
1212    }
1213
1214    pub fn merge_coverages(&mut self, other_coverage: Option<HitMaps>) {
1215        HitMaps::merge_opt(&mut self.coverage, other_coverage);
1216    }
1217}