Skip to main content

forge/
result.rs

1//! Test outcomes.
2
3use crate::{fuzz::BaseCounterExample, gas_report::GasReport};
4use alloy_primitives::{
5    Address, Bytes, I256, Log, Selector, U256,
6    map::{AddressHashMap, HashMap},
7};
8use eyre::Report;
9use foundry_common::{ContractsByArtifact, get_contract_name, get_file_name, shell};
10use foundry_evm::{
11    core::{Breakpoints, evm::FoundryEvmNetwork},
12    coverage::HitMaps,
13    decode::SkipReason,
14    executors::{RawCallResult, invariant::InvariantMetrics},
15    fuzz::{CounterExample, FuzzCase, FuzzFixtures, FuzzTestResult},
16    traces::{CallTraceArena, CallTraceDecoder, TraceKind, Traces},
17};
18use foundry_evm_symbolic::{PortfolioDiagnostics, SymbolicStats};
19use serde::{Deserialize, Serialize};
20use std::{
21    borrow::Cow,
22    collections::{BTreeMap, HashMap as Map},
23    fmt::{self, Write},
24    time::Duration,
25};
26use yansi::Paint;
27
28pub(crate) fn invariant_campaign_display_name(contract_name: &str) -> String {
29    format!("{contract_name} invariants")
30}
31
32const INVARIANT_CAMPAIGN_FALLBACK_NAME: &str = "Invariant campaign";
33
34/// The aggregated result of a test run.
35#[derive(Clone, Debug)]
36pub struct TestOutcome {
37    /// The results of all test suites by their identifier (`path:contract_name`).
38    ///
39    /// Essentially `identifier => signature => result`.
40    pub results: BTreeMap<String, SuiteResult>,
41    /// Whether to allow test failures without failing the entire test run.
42    pub allow_failure: bool,
43    /// The decoder used to decode traces and logs.
44    ///
45    /// This is `None` if traces and logs were not decoded.
46    ///
47    /// Note that `Address` fields only contain the last executed test case's data.
48    pub last_run_decoder: Option<CallTraceDecoder>,
49    /// The gas report, if requested.
50    pub gas_report: Option<GasReport>,
51    /// Known contracts from the test run (used for coverage).
52    pub known_contracts: Option<ContractsByArtifact>,
53    /// The fuzz seed used for the test run.
54    pub fuzz_seed: Option<U256>,
55}
56
57impl TestOutcome {
58    /// Creates a new test outcome with the given results.
59    pub const fn new(
60        known_contracts: Option<ContractsByArtifact>,
61        results: BTreeMap<String, SuiteResult>,
62        allow_failure: bool,
63        fuzz_seed: Option<U256>,
64    ) -> Self {
65        Self {
66            results,
67            allow_failure,
68            last_run_decoder: None,
69            gas_report: None,
70            known_contracts,
71            fuzz_seed,
72        }
73    }
74
75    /// Creates a new empty test outcome.
76    pub const fn empty(known_contracts: Option<ContractsByArtifact>, allow_failure: bool) -> Self {
77        Self::new(known_contracts, BTreeMap::new(), allow_failure, None)
78    }
79
80    /// Returns an iterator over all individual succeeding tests and their names.
81    pub fn successes(&self) -> impl Iterator<Item = (&String, &TestResult)> {
82        self.tests().filter(|(_, t)| t.status.is_success())
83    }
84
85    /// Returns an iterator over all individual skipped tests and their names.
86    pub fn skips(&self) -> impl Iterator<Item = (&String, &TestResult)> {
87        self.tests().filter(|(_, t)| t.status.is_skipped())
88    }
89
90    /// Returns an iterator over all individual failing tests and their names.
91    pub fn failures(&self) -> impl Iterator<Item = (&String, &TestResult)> {
92        self.tests().filter(|(_, t)| t.status.is_failure())
93    }
94
95    /// Returns an iterator over all individual tests and their names.
96    pub fn tests(&self) -> impl Iterator<Item = (&String, &TestResult)> {
97        self.results.values().flat_map(|suite| suite.tests())
98    }
99
100    /// Returns merged symbolic solver portfolio diagnostics across all tests in this outcome.
101    pub fn symbolic_portfolio_diagnostics(&self) -> Option<PortfolioDiagnostics> {
102        let mut diagnostics = PortfolioDiagnostics::default();
103        for (_, result) in self.tests() {
104            if let Some(result_diagnostics) = &result.symbolic_portfolio_diagnostics {
105                diagnostics.merge(result_diagnostics);
106            }
107        }
108        (!diagnostics.is_empty()).then_some(diagnostics)
109    }
110
111    /// Flattens the test outcome into a list of individual tests.
112    // TODO: Replace this with `tests` and make it return `TestRef<'_>`
113    pub fn into_tests_cloned(&self) -> impl Iterator<Item = SuiteTestResult> + '_ {
114        self.results
115            .iter()
116            .flat_map(|(file, suite)| {
117                suite
118                    .test_results
119                    .iter()
120                    .map(move |(sig, result)| (file.clone(), sig.clone(), result.clone()))
121            })
122            .map(|(artifact_id, signature, result)| SuiteTestResult {
123                artifact_id,
124                signature,
125                result,
126            })
127    }
128
129    /// Flattens the test outcome into a list of individual tests.
130    pub fn into_tests(self) -> impl Iterator<Item = SuiteTestResult> {
131        self.results
132            .into_iter()
133            .flat_map(|(file, suite)| {
134                suite.test_results.into_iter().map(move |t| (file.clone(), t))
135            })
136            .map(|(artifact_id, (signature, result))| SuiteTestResult {
137                artifact_id,
138                signature,
139                result,
140            })
141    }
142
143    /// Returns the number of tests that passed.
144    pub fn passed(&self) -> usize {
145        self.results.values().map(SuiteResult::passed).sum()
146    }
147
148    /// Returns the number of tests that were skipped.
149    pub fn skipped(&self) -> usize {
150        self.results.values().map(SuiteResult::skipped).sum()
151    }
152
153    /// Returns the number of tests that failed.
154    pub fn failed(&self) -> usize {
155        self.results.values().map(SuiteResult::failed).sum()
156    }
157
158    /// Returns `true` if any fuzz or invariant test failed.
159    pub fn has_fuzz_failures(&self) -> bool {
160        self.failures().any(|(_, t)| t.kind.is_fuzz() || t.kind.is_invariant())
161    }
162
163    /// Returns `true` if any invariant test failed.
164    pub fn has_invariant_failures(&self) -> bool {
165        self.failures().any(|(_, t)| t.kind.is_invariant())
166    }
167
168    fn invariant_workers_hint(&self) -> Option<usize> {
169        let mut workers = self.failures().filter_map(|(_, result)| result.kind.invariant_workers());
170        let first = workers.next()?;
171        (first > 1 && workers.all(|workers| workers == first)).then_some(first)
172    }
173
174    /// Sums up all the durations of all individual test suites.
175    ///
176    /// Note that this is not necessarily the wall clock time of the entire test run.
177    pub fn total_time(&self) -> Duration {
178        self.results.values().map(|suite| suite.duration).sum()
179    }
180
181    /// Formats the aggregated summary of all test suites into a string (for printing).
182    pub fn summary(&self, wall_clock_time: Duration) -> String {
183        let num_test_suites = self.results.len();
184        let suites = if num_test_suites == 1 { "suite" } else { "suites" };
185        let total_passed = self.passed();
186        let total_failed = self.failed();
187        let total_skipped = self.skipped();
188        let total_tests = total_passed + total_failed + total_skipped;
189        format!(
190            "\nRan {} test {} in {:.2?} ({:.2?} CPU time): {} tests passed, {} failed, {} skipped ({} total tests)",
191            num_test_suites,
192            suites,
193            wall_clock_time,
194            self.total_time(),
195            total_passed.green(),
196            total_failed.red(),
197            total_skipped.yellow(),
198            total_tests
199        )
200    }
201
202    /// Checks if there are any failures and failures are disallowed.
203    //
204    // Exit-code policy: under `--machine` we honor the agent contract
205    // ([`ExitCode::TestFailure`]); legacy invocations preserve the
206    // historical exit-1 contract that scripts and CIs already depend on.
207    pub fn ensure_ok(&self, silent: bool) -> eyre::Result<()> {
208        let outcome = self;
209        let failures = outcome.failures().count();
210        if outcome.allow_failure || failures == 0 {
211            return Ok(());
212        }
213
214        if shell::is_quiet() || silent {
215            std::process::exit(test_failure_exit_code());
216        }
217
218        sh_println!("\nFailing tests:")?;
219        for (suite_name, suite) in &outcome.results {
220            let failed = suite.failed();
221            if failed == 0 {
222                continue;
223            }
224
225            let term = if failed > 1 { "tests" } else { "test" };
226            sh_println!("Encountered {failed} failing {term} in {suite_name}")?;
227            for (name, result) in suite.failures() {
228                sh_println!("{}", result.short_result_with_suite(name, suite_name))?;
229            }
230            sh_println!()?;
231        }
232        let successes = outcome.passed();
233        sh_println!(
234            "Encountered a total of {} failing tests, {} tests succeeded",
235            failures.to_string().red(),
236            successes.to_string().green()
237        )?;
238
239        // Show helpful hint for rerunning failed tests
240        let test_word = if failures == 1 { "test" } else { "tests" };
241        sh_println!(
242            "\nTip: Run {} to retry only the {} failed {}",
243            "`forge test --rerun`".cyan(),
244            failures,
245            test_word
246        )?;
247
248        // Print seed for fuzz/invariant test failures to enable reproduction.
249        if let Some(seed) = self.fuzz_seed
250            && outcome.has_fuzz_failures()
251        {
252            sh_println!(
253                "\nFuzz seed: {} (use {} to reproduce)",
254                format!("{seed:#x}").cyan(),
255                "`--fuzz-seed`".cyan()
256            )?;
257            if let Some(invariant_workers) = outcome.invariant_workers_hint() {
258                sh_println!(
259                    "Invariant workers: {} (use {} to reproduce)",
260                    invariant_workers,
261                    format!("`--invariant-workers {invariant_workers}`").cyan()
262                )?;
263            }
264        }
265
266        std::process::exit(test_failure_exit_code());
267    }
268
269    /// Removes first test result, if any.
270    pub fn remove_first(&mut self) -> Option<(String, String, TestResult)> {
271        self.results.iter_mut().find_map(|(suite_name, suite)| {
272            if let Some(test_name) = suite.test_results.keys().next().cloned() {
273                let result = suite.test_results.remove(&test_name).unwrap();
274                Some((suite_name.clone(), test_name, result))
275            } else {
276                None
277            }
278        })
279    }
280}
281
282/// Process exit code emitted when at least one test failed.
283fn test_failure_exit_code() -> i32 {
284    if foundry_cli::is_machine() { foundry_cli::ExitCode::TestFailure.to_i32() } else { 1 }
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290
291    fn outcome_with_failed_invariant_workers(workers: &[usize]) -> TestOutcome {
292        let test_results = workers
293            .iter()
294            .enumerate()
295            .map(|(idx, workers)| {
296                (
297                    format!("invariant{idx}()"),
298                    TestResult {
299                        status: TestStatus::Failure,
300                        kind: TestKind::Invariant {
301                            runs: 0,
302                            calls: 0,
303                            reverts: 0,
304                            workers: *workers,
305                            metrics: Map::new(),
306                            failed_corpus_replays: 0,
307                            optimization_best_value: None,
308                        },
309                        ..Default::default()
310                    },
311                )
312            })
313            .collect();
314        TestOutcome::new(
315            None,
316            BTreeMap::from([(
317                "suite".to_string(),
318                SuiteResult::new(Duration::ZERO, test_results, Vec::new()),
319            )]),
320            false,
321            None,
322        )
323    }
324
325    #[test]
326    fn invariant_workers_hint_requires_matching_parallel_worker_counts() {
327        assert_eq!(
328            outcome_with_failed_invariant_workers(&[3, 3]).invariant_workers_hint(),
329            Some(3)
330        );
331        assert_eq!(outcome_with_failed_invariant_workers(&[2, 3]).invariant_workers_hint(), None);
332        assert_eq!(outcome_with_failed_invariant_workers(&[1]).invariant_workers_hint(), None);
333    }
334
335    #[test]
336    fn invariant_kind_deserializes_legacy_payload_without_workers() {
337        let kind = serde_json::from_value::<TestKind>(serde_json::json!({
338            "Invariant": {
339                "runs": 4,
340                "calls": 10,
341                "reverts": 0,
342                "metrics": {},
343                "failed_corpus_replays": 0,
344                "optimization_best_value": null
345            }
346        }))
347        .unwrap();
348
349        assert_eq!(kind.invariant_workers(), Some(1));
350    }
351}
352
353/// A set of test results for a single test suite, which is all the tests in a single contract.
354#[derive(Clone, Debug, Serialize)]
355pub struct SuiteResult {
356    /// Wall clock time it took to execute all tests in this suite.
357    #[serde(with = "foundry_common::serde_helpers::duration")]
358    pub duration: Duration,
359    /// Individual test results: `test fn signature -> TestResult`.
360    pub test_results: BTreeMap<String, TestResult>,
361    /// Generated warnings.
362    pub warnings: Vec<String>,
363}
364
365impl SuiteResult {
366    pub fn new(
367        duration: Duration,
368        test_results: BTreeMap<String, TestResult>,
369        mut warnings: Vec<String>,
370    ) -> Self {
371        // Add deprecated cheatcodes warning, if any of them used in current test suite.
372        let mut deprecated_cheatcodes = HashMap::new();
373        for test_result in test_results.values() {
374            deprecated_cheatcodes.extend(test_result.deprecated_cheatcodes.clone());
375        }
376        if !deprecated_cheatcodes.is_empty() {
377            let mut warning =
378                "the following cheatcode(s) are deprecated and will be removed in future versions:"
379                    .to_string();
380            for (cheatcode, reason) in deprecated_cheatcodes {
381                write!(warning, "\n  {cheatcode}").unwrap();
382                if let Some(reason) = reason {
383                    write!(warning, ": {reason}").unwrap();
384                }
385            }
386            warnings.push(warning);
387        }
388
389        Self { duration, test_results, warnings }
390    }
391
392    /// Returns an iterator over all individual succeeding tests and their names.
393    pub fn successes(&self) -> impl Iterator<Item = (&String, &TestResult)> {
394        self.tests().filter(|(_, t)| t.status.is_success())
395    }
396
397    /// Returns an iterator over all individual skipped tests and their names.
398    pub fn skips(&self) -> impl Iterator<Item = (&String, &TestResult)> {
399        self.tests().filter(|(_, t)| t.status.is_skipped())
400    }
401
402    /// Returns an iterator over all individual failing tests and their names.
403    pub fn failures(&self) -> impl Iterator<Item = (&String, &TestResult)> {
404        self.tests().filter(|(_, t)| t.status.is_failure())
405    }
406
407    /// Returns the number of tests that passed.
408    pub fn passed(&self) -> usize {
409        self.test_results.values().map(TestResult::passed_count).sum()
410    }
411
412    /// Returns the number of tests that were skipped.
413    pub fn skipped(&self) -> usize {
414        self.test_results.values().map(TestResult::skipped_count).sum()
415    }
416
417    /// Returns the number of tests that failed.
418    pub fn failed(&self) -> usize {
419        self.test_results.values().map(TestResult::failed_count).sum()
420    }
421
422    /// Iterator over all tests and their names
423    pub fn tests(&self) -> impl Iterator<Item = (&String, &TestResult)> {
424        self.test_results.iter()
425    }
426
427    /// Whether this test suite is empty.
428    pub fn is_empty(&self) -> bool {
429        self.test_results.is_empty()
430    }
431
432    /// The number of tests in this test suite.
433    pub fn len(&self) -> usize {
434        self.test_results.values().map(TestResult::logical_count).sum()
435    }
436
437    /// Sums up all the durations of all individual tests in this suite.
438    ///
439    /// Note that this is not necessarily the wall clock time of the entire test suite.
440    pub fn total_time(&self) -> Duration {
441        self.test_results.values().map(|result| result.duration).sum()
442    }
443
444    /// Returns the summary of a single test suite.
445    pub fn summary(&self) -> String {
446        let failed = self.failed();
447        let result = if failed == 0 { "ok".green() } else { "FAILED".red() };
448        format!(
449            "Suite result: {}. {} passed; {} failed; {} skipped; finished in {:.2?} ({:.2?} CPU time)",
450            result,
451            self.passed().green(),
452            failed.red(),
453            self.skipped().yellow(),
454            self.duration,
455            self.total_time(),
456        )
457    }
458}
459
460/// The result of a single test in a test suite.
461///
462/// This is flattened from a [`TestOutcome`].
463#[derive(Clone, Debug)]
464pub struct SuiteTestResult {
465    /// The identifier of the artifact/contract in the form:
466    /// `<artifact file name>:<contract name>`.
467    pub artifact_id: String,
468    /// The function signature of the Solidity test.
469    pub signature: String,
470    /// The result of the executed test.
471    pub result: TestResult,
472}
473
474impl SuiteTestResult {
475    /// Returns the gas used by the test.
476    pub fn gas_used(&self) -> u64 {
477        self.result.kind.report().gas()
478    }
479
480    /// Returns the contract name of the artifact ID.
481    pub fn contract_name(&self) -> &str {
482        get_contract_name(&self.artifact_id)
483    }
484
485    /// Returns the file name of the artifact ID.
486    pub fn file_name(&self) -> &str {
487        get_file_name(&self.artifact_id)
488    }
489}
490
491/// The status of a test.
492#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
493pub enum TestStatus {
494    Success,
495    #[default]
496    Failure,
497    Skipped,
498}
499
500impl TestStatus {
501    /// Returns `true` if the test was successful.
502    #[inline]
503    pub const fn is_success(self) -> bool {
504        matches!(self, Self::Success)
505    }
506
507    /// Returns `true` if the test failed.
508    #[inline]
509    pub const fn is_failure(self) -> bool {
510        matches!(self, Self::Failure)
511    }
512
513    /// Returns `true` if the test was skipped.
514    #[inline]
515    pub const fn is_skipped(self) -> bool {
516        matches!(self, Self::Skipped)
517    }
518}
519
520/// A failure surfaced by an invariant test campaign — either a broken `invariant_*`
521/// predicate ([`Self::Predicate`]) or a handler-side assertion bug ([`Self::Handler`]).
522#[derive(Clone, Debug, Serialize, Deserialize)]
523#[serde(tag = "kind", rename_all = "snake_case")]
524pub enum InvariantFailure {
525    /// A broken `invariant_*` predicate.
526    Predicate {
527        /// Invariant function name (e.g. `invariant_cond3`).
528        name: String,
529        /// Revert reason or assertion failure message.
530        reason: String,
531        /// Counterexample sequence, when one is available.
532        #[serde(default, skip_serializing_if = "Option::is_none")]
533        counterexample: Option<CounterExample>,
534        /// Path where the counterexample was persisted for re-running and shrinking.
535        persisted_path: std::path::PathBuf,
536        /// Whether this failure is the stable campaign anchor.
537        /// When `true` and this is the only single-predicate failure, the function name is
538        /// omitted on the `[FAIL: ...]` line (the trailing summary already identifies it).
539        #[serde(default)]
540        is_anchor: bool,
541    },
542    /// A handler-side assertion bug discovered during the campaign.
543    Handler {
544        /// Best-effort human-readable name of the failing call, e.g. `Counter::increment` or
545        /// `0xabc...::0x12345678` when the contract/function cannot be resolved.
546        name: String,
547        /// Address of the handler whose call asserted/reverted with an assertion.
548        reverter: Address,
549        /// 4-byte selector of the failing handler function.
550        selector: Selector,
551        /// Decoded revert/assert reason.
552        reason: String,
553        /// Counterexample sequence leading up to (and including) the failing call.
554        #[serde(default, skip_serializing_if = "Option::is_none")]
555        counterexample: Option<CounterExample>,
556    },
557}
558
559impl InvariantFailure {
560    /// Reason rendered on the `[FAIL: ...]` line.
561    pub fn reason(&self) -> &str {
562        match self {
563            Self::Predicate { reason, .. } | Self::Handler { reason, .. } => reason,
564        }
565    }
566
567    /// Human-readable name (invariant fn name, or `Contract::function` for handler bugs).
568    pub fn name(&self) -> &str {
569        match self {
570            Self::Predicate { name, .. } | Self::Handler { name, .. } => name,
571        }
572    }
573
574    /// Invariant predicate name, if this is a predicate failure.
575    pub fn predicate_name(&self) -> Option<&str> {
576        match self {
577            Self::Predicate { name, .. } => Some(name),
578            Self::Handler { .. } => None,
579        }
580    }
581
582    /// Counterexample sequence, when one is available.
583    pub const fn counterexample(&self) -> Option<&CounterExample> {
584        match self {
585            Self::Predicate { counterexample, .. } | Self::Handler { counterexample, .. } => {
586                counterexample.as_ref()
587            }
588        }
589    }
590}
591
592/// Pass/fail status for an invariant predicate evaluated inside a contract-level campaign.
593#[derive(Clone, Debug, Serialize, Deserialize)]
594pub struct InvariantPredicateResult {
595    /// Invariant function name (e.g. `invariant_balance`).
596    pub name: String,
597    /// Predicate status within the logical campaign.
598    pub status: TestStatus,
599    /// Revert reason or assertion message when the predicate failed.
600    #[serde(default, skip_serializing_if = "Option::is_none")]
601    pub reason: Option<String>,
602}
603
604/// The result of an executed test.
605#[derive(Clone, Debug, Default, Serialize, Deserialize)]
606pub struct TestResult {
607    /// The test status, indicating whether the test case succeeded, failed, or was marked as
608    /// skipped. This means that the transaction executed properly, the test was marked as
609    /// skipped with vm.skip(), or that there was a revert and that the test was expected to
610    /// fail (prefixed with `testFail`)
611    pub status: TestStatus,
612
613    /// If there was a revert, this field will be populated. Note that the test can
614    /// still be successful (i.e self.success == true) when it's expected to fail.
615    pub reason: Option<String>,
616
617    /// All broken invariant predicates in this campaign in source declaration order.
618    ///
619    /// For invariant tests, this is the single source of truth used by the renderer.
620    /// `reason` and `counterexample` are not populated for invariant tests.
621    #[serde(default, skip_serializing_if = "Vec::is_empty")]
622    pub invariant_failures: Vec<InvariantFailure>,
623
624    /// Per-predicate outcomes for invariant campaigns. This preserves individual
625    /// `invariant_*` / `statefulFuzz*` pass/fail reporting when multiple predicates are checked
626    /// by one contract-level campaign.
627    #[serde(default, skip_serializing_if = "Vec::is_empty")]
628    pub invariant_predicate_results: Vec<InvariantPredicateResult>,
629
630    /// Directory where invariant failure counterexamples have been persisted (set when one or more
631    /// secondary invariant failures were written, so users can locate persisted counterexamples).
632    #[serde(default, skip_serializing_if = "Option::is_none")]
633    pub invariant_failure_dir: Option<std::path::PathBuf>,
634
635    /// Total number of invariant predicates exercised in this campaign. When `Some(n)` the
636    /// user-facing report renders a contract-level `<broken>/<n> invariants broken` summary so
637    /// users get an at-a-glance health line without counting `[FAIL]` blocks. `None` for
638    /// single-predicate campaigns.
639    #[serde(default, skip_serializing_if = "Option::is_none")]
640    pub invariant_count: Option<usize>,
641
642    /// Handler-side assertion bugs found during the campaign, deduped by
643    /// `(reverter, selector)` site (Medusa/Echidna semantics). Rendered in a dedicated
644    /// `Assertion Tests` section.
645    #[serde(default, skip_serializing_if = "Vec::is_empty")]
646    pub invariant_handler_failures: Vec<InvariantFailure>,
647
648    /// Minimal reproduction test case for failing test
649    pub counterexample: Option<CounterExample>,
650
651    /// Any captured & parsed as strings logs along the test's execution which should
652    /// be printed to the user.
653    pub logs: Vec<Log>,
654
655    /// The decoded DSTest logging events and Hardhat's `console.log` from [logs](Self::logs).
656    /// Used for json output.
657    pub decoded_logs: Vec<String>,
658
659    /// What kind of test this was
660    pub kind: TestKind,
661
662    /// Traces
663    pub traces: Traces,
664
665    /// Runtime bytecodes for contracts seen in debug traces.
666    #[serde(skip)]
667    pub debug_bytecodes: AddressHashMap<Bytes>,
668
669    /// Additional traces to use for gas report.
670    ///
671    /// These are cleared after the gas report is analyzed.
672    #[serde(skip)]
673    pub gas_report_traces: Vec<Vec<CallTraceArena>>,
674
675    /// Raw line coverage info
676    #[serde(skip)]
677    pub line_coverage: Option<HitMaps>,
678
679    /// Labeled addresses
680    #[serde(rename = "labeled_addresses")] // Backwards compatibility.
681    pub labels: AddressHashMap<String>,
682
683    #[serde(with = "foundry_common::serde_helpers::duration")]
684    pub duration: Duration,
685
686    /// pc breakpoint char map
687    pub breakpoints: Breakpoints,
688
689    /// Any captured gas snapshots along the test's execution which should be accumulated.
690    pub gas_snapshots: BTreeMap<String, BTreeMap<String, String>>,
691
692    /// Deprecated cheatcodes (mapped to their replacements, if any) used in current test.
693    #[serde(skip)]
694    pub deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>,
695
696    /// Staged solver portfolio diagnostics collected during symbolic execution.
697    #[serde(skip)]
698    pub symbolic_portfolio_diagnostics: Option<PortfolioDiagnostics>,
699
700    /// Verbose symbolic solver diagnostics deferred until test output rendering.
701    #[serde(skip)]
702    pub symbolic_diagnostics: Option<String>,
703}
704
705impl fmt::Display for TestResult {
706    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
707        f.write_str(&self.render_status_block(false, None))
708    }
709}
710
711impl TestResult {
712    fn render_status_block(
713        &self,
714        user_facing: bool,
715        invariant_campaign_name: Option<&str>,
716    ) -> String {
717        match self.status {
718            TestStatus::Success => {
719                // For optimization mode, show the best example sequence in green.
720                let mut s = String::from("[PASS]");
721                if let Some(CounterExample::Sequence(original, sequence)) = &self.counterexample {
722                    s.push_str(
723                        format!(
724                            "\n\t[Best sequence] (original: {original}, shrunk: {})\n",
725                            sequence.len()
726                        )
727                        .as_str(),
728                    );
729                    for ex in sequence {
730                        writeln!(s, "{ex}").unwrap();
731                    }
732                }
733                self.write_invariant_predicate_results(
734                    &mut s,
735                    user_facing,
736                    true,
737                    invariant_campaign_name,
738                );
739                format!("{}", s.green().wrap())
740            }
741            TestStatus::Skipped => {
742                let mut s = String::from("[SKIP");
743                if let Some(reason) = &self.reason {
744                    write!(s, ": {reason}").unwrap();
745                }
746                s.push(']');
747                self.write_invariant_predicate_results(
748                    &mut s,
749                    user_facing,
750                    true,
751                    invariant_campaign_name,
752                );
753                format!("{}", s.yellow())
754            }
755            TestStatus::Failure => {
756                let mut s = String::new();
757                let has_handler_failures = !self.invariant_handler_failures.is_empty();
758                let is_invariant_failure =
759                    !self.invariant_failures.is_empty() || has_handler_failures;
760                if !is_invariant_failure {
761                    // Non-invariant failure (unit / fuzz / DS-style): render from the legacy
762                    // `reason` / `counterexample` fields.
763                    s.push_str("[FAIL");
764                    if let Some(reason) = &self.reason {
765                        write!(s, ": {reason}").unwrap();
766                    }
767                    if let Some(counterexample) = &self.counterexample {
768                        match counterexample {
769                            CounterExample::Single(ex) => {
770                                write!(s, "; counterexample: {ex}]").unwrap();
771                            }
772                            CounterExample::Sequence(original, sequence) => {
773                                writeln!(
774                                    s,
775                                    "]\n\t[Sequence] (original: {original}, shrunk: {})",
776                                    sequence.len()
777                                )
778                                .unwrap();
779                                for ex in sequence {
780                                    writeln!(s, "{ex}").unwrap();
781                                }
782                            }
783                        }
784                    } else {
785                        s.push(']');
786                    }
787                } else if !self.invariant_failures.is_empty() {
788                    // Contract-level campaigns identify the broken predicate even when only one
789                    // predicate failed. Preserve the compact legacy shape only for the anchor of a
790                    // single-predicate run.
791                    let multi = self.invariant_failures.len() > 1;
792                    let is_campaign = self.invariant_count.is_some();
793                    for (i, failure) in self.invariant_failures.iter().enumerate() {
794                        if i > 0 {
795                            s.push('\n');
796                        }
797                        let is_anchor =
798                            matches!(failure, InvariantFailure::Predicate { is_anchor: true, .. });
799                        let name_suffix = if is_campaign || multi || !is_anchor {
800                            format!(" {}", failure.name())
801                        } else {
802                            String::new()
803                        };
804                        if let Some(CounterExample::Sequence(original, sequence)) =
805                            failure.counterexample()
806                        {
807                            writeln!(
808                                s,
809                                "[FAIL: {}]{name_suffix}\n\t[Sequence] (original: {original}, shrunk: {})",
810                                failure.reason(),
811                                sequence.len()
812                            )
813                            .unwrap();
814                            for ex in sequence {
815                                writeln!(s, "{ex}").unwrap();
816                            }
817                        } else {
818                            write!(s, "[FAIL: {}]{name_suffix}", failure.reason()).unwrap();
819                        }
820                    }
821                }
822
823                let rollup_rendered = self.write_invariant_rollup(
824                    &mut s,
825                    user_facing,
826                    is_invariant_failure,
827                    invariant_campaign_name,
828                );
829                let show_predicate_header = if user_facing { !rollup_rendered } else { true };
830                self.write_invariant_predicate_results(
831                    &mut s,
832                    user_facing,
833                    show_predicate_header,
834                    invariant_campaign_name,
835                );
836                self.write_invariant_persistence_note(&mut s);
837                let handler_preceded = if user_facing {
838                    rollup_rendered
839                        || self.invariant_predicate_results.len() > 1
840                        || !self.invariant_failures.is_empty()
841                } else {
842                    !self.invariant_failures.is_empty()
843                        || matches!(self.invariant_count, Some(t) if t > 1)
844                };
845                self.write_handler_failures(&mut s, user_facing, handler_preceded);
846
847                format!("{}", s.red().wrap())
848            }
849        }
850    }
851
852    fn write_invariant_rollup(
853        &self,
854        s: &mut String,
855        user_facing: bool,
856        is_invariant_failure: bool,
857        invariant_campaign_name: Option<&str>,
858    ) -> bool {
859        let Some(total) = self.invariant_count else {
860            return false;
861        };
862        if total <= 1 || !is_invariant_failure {
863            return false;
864        }
865
866        writeln!(
867            s,
868            "\n{}: {}/{total} invariants broken",
869            if user_facing {
870                invariant_campaign_name.unwrap_or(INVARIANT_CAMPAIGN_FALLBACK_NAME)
871            } else {
872                "Predicates"
873            },
874            self.invariant_failures.len()
875        )
876        .unwrap();
877        true
878    }
879
880    fn write_invariant_persistence_note(&self, s: &mut String) {
881        if self.invariant_failures.len() > 1
882            && let Some(dir) = &self.invariant_failure_dir
883        {
884            writeln!(
885                s,
886                "{} invariant failure(s) persisted to {} — rerun to shrink",
887                self.invariant_failures.len(),
888                dir.display()
889            )
890            .unwrap();
891        }
892    }
893
894    fn write_handler_failures(&self, s: &mut String, user_facing: bool, preceded: bool) {
895        if self.invariant_handler_failures.is_empty() {
896            return;
897        }
898
899        let prefix = if preceded { "\n" } else { "" };
900        writeln!(
901            s,
902            "{prefix}{}: {} assertion bug(s) found",
903            if user_facing { "Assertion Tests" } else { "Handler assertions" },
904            self.invariant_handler_failures.len()
905        )
906        .unwrap();
907        for failure in &self.invariant_handler_failures {
908            if let Some(CounterExample::Sequence(original, sequence)) = failure.counterexample() {
909                writeln!(
910                    s,
911                    "[FAIL: {}] {}\n\t[Sequence] (original: {original}, shrunk: {})",
912                    failure.reason(),
913                    failure.name(),
914                    sequence.len()
915                )
916                .unwrap();
917                for ex in sequence {
918                    writeln!(s, "{ex}").unwrap();
919                }
920            } else {
921                writeln!(s, "[FAIL: {}] {}", failure.reason(), failure.name()).unwrap();
922            }
923        }
924    }
925
926    /// Appends the invariant/property summary for multi-predicate campaigns.
927    fn write_invariant_predicate_results(
928        &self,
929        s: &mut String,
930        user_facing: bool,
931        show_header: bool,
932        invariant_campaign_name: Option<&str>,
933    ) {
934        if self.invariant_predicate_results.len() <= 1 {
935            return;
936        }
937
938        if show_header {
939            s.push('\n');
940            s.push_str(if user_facing {
941                invariant_campaign_name.unwrap_or(INVARIANT_CAMPAIGN_FALLBACK_NAME)
942            } else {
943                "Predicates"
944            });
945            s.push_str(":\n");
946        }
947
948        for predicate in &self.invariant_predicate_results {
949            match predicate.status {
950                TestStatus::Success => {
951                    writeln!(s, "[PASS] {}", predicate.name).unwrap();
952                }
953                TestStatus::Failure => {
954                    let reason = predicate.reason.as_deref().unwrap_or_default();
955                    writeln!(s, "[FAIL: {reason}] {}", predicate.name).unwrap();
956                }
957                TestStatus::Skipped => {
958                    if let Some(reason) = &predicate.reason {
959                        writeln!(s, "[SKIP: {reason}] {}", predicate.name).unwrap();
960                    } else {
961                        writeln!(s, "[SKIP] {}", predicate.name).unwrap();
962                    }
963                }
964            }
965        }
966    }
967}
968
969macro_rules! extend {
970    ($a:expr, $b:expr, $trace_kind:expr) => {
971        $a.logs.extend($b.logs);
972        $a.labels.extend($b.labels);
973        $a.traces.extend($b.traces.map(|traces| ($trace_kind, traces)));
974        $a.debug_bytecodes.extend($b.debug_bytecodes);
975        $a.merge_coverages($b.line_coverage);
976    };
977}
978
979impl TestResult {
980    /// Creates a new test result starting from test setup results.
981    pub fn new(setup: &TestSetup) -> Self {
982        Self {
983            labels: setup.labels.clone(),
984            logs: setup.logs.clone(),
985            traces: setup.traces.clone(),
986            debug_bytecodes: setup.debug_bytecodes.clone(),
987            line_coverage: setup.coverage.clone(),
988            ..Default::default()
989        }
990    }
991
992    /// Creates a failed test result with given reason.
993    pub fn fail(reason: String) -> Self {
994        Self { status: TestStatus::Failure, reason: Some(reason), ..Default::default() }
995    }
996
997    /// Creates a test setup result.
998    pub fn setup_result(setup: TestSetup) -> Self {
999        let TestSetup {
1000            address: _,
1001            fuzz_fixtures: _,
1002            logs,
1003            labels,
1004            traces,
1005            debug_bytecodes,
1006            coverage,
1007            deployed_libs: _,
1008            reason,
1009            skipped,
1010            deployment_failure: _,
1011        } = setup;
1012        Self {
1013            status: if skipped { TestStatus::Skipped } else { TestStatus::Failure },
1014            reason,
1015            logs,
1016            traces,
1017            debug_bytecodes,
1018            line_coverage: coverage,
1019            labels,
1020            ..Default::default()
1021        }
1022    }
1023
1024    /// Returns the skipped result for single test (used in skipped fuzz test too).
1025    pub fn single_skip(&mut self, reason: SkipReason) {
1026        self.status = TestStatus::Skipped;
1027        self.reason = reason.0;
1028    }
1029
1030    /// Returns the failed result with reason for single test.
1031    pub fn single_fail(&mut self, reason: Option<String>) {
1032        self.status = TestStatus::Failure;
1033        self.reason = reason;
1034    }
1035
1036    /// Returns the result for single test. Merges execution results (logs, labeled addresses,
1037    /// traces and coverages) in initial setup results.
1038    pub fn single_result<FEN: FoundryEvmNetwork>(
1039        &mut self,
1040        success: bool,
1041        reason: Option<String>,
1042        raw_call_result: RawCallResult<FEN>,
1043    ) {
1044        self.kind = TestKind::Unit {
1045            gas: raw_call_result.gas_used.saturating_sub(raw_call_result.stipend),
1046        };
1047
1048        extend!(self, raw_call_result, TraceKind::Execution);
1049
1050        self.status = match success {
1051            true => TestStatus::Success,
1052            false => TestStatus::Failure,
1053        };
1054        self.reason = reason;
1055        self.duration = Duration::default();
1056        self.gas_report_traces = Vec::new();
1057
1058        if let Some(cheatcodes) = raw_call_result.cheatcodes {
1059            self.breakpoints = cheatcodes.breakpoints;
1060            self.gas_snapshots = cheatcodes.gas_snapshots;
1061            self.deprecated_cheatcodes = cheatcodes.deprecated;
1062        }
1063    }
1064
1065    /// Returns the result for a fuzzed test. Merges fuzz execution results (logs, labeled
1066    /// addresses, traces and coverages) in initial setup results.
1067    pub fn fuzz_result(&mut self, result: FuzzTestResult) {
1068        self.kind = TestKind::Fuzz {
1069            median_gas: result.median_gas(false),
1070            mean_gas: result.mean_gas(false),
1071            first_case: result.first_case,
1072            runs: result.gas_by_case.len(),
1073            failed_corpus_replays: result.failed_corpus_replays,
1074        };
1075
1076        // Record logs, labels, traces and merge coverages.
1077        extend!(self, result, TraceKind::Execution);
1078
1079        self.status = if result.skipped {
1080            TestStatus::Skipped
1081        } else if result.success {
1082            TestStatus::Success
1083        } else {
1084            TestStatus::Failure
1085        };
1086        self.reason = result.reason;
1087        self.counterexample = result.counterexample;
1088        self.duration = Duration::default();
1089        self.gas_report_traces = result.gas_report_traces.into_iter().map(|t| vec![t]).collect();
1090        self.breakpoints = result.breakpoints.unwrap_or_default();
1091        self.deprecated_cheatcodes = result.deprecated_cheatcodes;
1092    }
1093
1094    /// Returns the fail result for fuzz test setup.
1095    pub fn fuzz_setup_fail(&mut self, e: Report) {
1096        self.kind = TestKind::Fuzz {
1097            first_case: Default::default(),
1098            runs: 0,
1099            mean_gas: 0,
1100            median_gas: 0,
1101            failed_corpus_replays: 0,
1102        };
1103        self.status = TestStatus::Failure;
1104        debug!(?e, "failed to set up fuzz testing environment");
1105        self.reason = Some(format!("failed to set up fuzz testing environment: {e}"));
1106    }
1107
1108    /// Returns the skipped result for invariant test.
1109    pub fn invariant_skip(&mut self, reason: SkipReason) {
1110        self.invariant_skip_with_predicates(reason, Vec::new());
1111    }
1112
1113    /// Returns the skipped result for invariant campaign with per-predicate outcomes.
1114    pub fn invariant_skip_with_predicates(
1115        &mut self,
1116        reason: SkipReason,
1117        invariant_predicate_results: Vec<InvariantPredicateResult>,
1118    ) {
1119        self.kind = TestKind::Invariant {
1120            runs: 1,
1121            calls: 1,
1122            reverts: 1,
1123            workers: default_invariant_workers(),
1124            metrics: HashMap::default(),
1125            failed_corpus_replays: 0,
1126            optimization_best_value: None,
1127        };
1128        self.status = TestStatus::Skipped;
1129        let predicate_count = invariant_predicate_results.len();
1130        let is_campaign = predicate_count > 1;
1131        self.reason = if is_campaign { None } else { reason.0 };
1132        self.invariant_count = is_campaign.then_some(predicate_count);
1133        self.invariant_predicate_results = invariant_predicate_results;
1134    }
1135
1136    /// Returns the fail result for replayed invariant test.
1137    pub fn invariant_replay_fail(
1138        &mut self,
1139        replayed_entirely: bool,
1140        invariant_name: &String,
1141        replay_reason: Option<String>,
1142        call_sequence: Vec<BaseCounterExample>,
1143    ) {
1144        self.kind = TestKind::Invariant {
1145            runs: 1,
1146            calls: 1,
1147            reverts: 1,
1148            workers: default_invariant_workers(),
1149            metrics: HashMap::default(),
1150            failed_corpus_replays: 0,
1151            optimization_best_value: None,
1152        };
1153        self.status = TestStatus::Failure;
1154        self.reason = replay_reason.or_else(|| {
1155            if replayed_entirely {
1156                Some(format!("{invariant_name} replay failure"))
1157            } else {
1158                Some(format!("{invariant_name} persisted failure revert"))
1159            }
1160        });
1161        self.counterexample = Some(CounterExample::Sequence(call_sequence.len(), call_sequence));
1162    }
1163
1164    /// Returns the fail result for invariant test setup.
1165    pub fn invariant_setup_fail(&mut self, e: Report) {
1166        self.kind = TestKind::Invariant {
1167            runs: 0,
1168            calls: 0,
1169            reverts: 0,
1170            workers: default_invariant_workers(),
1171            metrics: HashMap::default(),
1172            failed_corpus_replays: 0,
1173            optimization_best_value: None,
1174        };
1175        self.status = TestStatus::Failure;
1176        self.reason = Some(format!("failed to set up invariant testing environment: {e}"));
1177    }
1178
1179    /// Returns the invariant test result.
1180    #[expect(clippy::too_many_arguments)]
1181    pub fn invariant_result(
1182        &mut self,
1183        gas_report_traces: Vec<Vec<CallTraceArena>>,
1184        success: bool,
1185        invariant_failures: Vec<InvariantFailure>,
1186        invariant_predicate_results: Vec<InvariantPredicateResult>,
1187        invariant_failure_dir: Option<std::path::PathBuf>,
1188        invariant_count: Option<usize>,
1189        invariant_handler_failures: Vec<InvariantFailure>,
1190        counterexample: Option<CounterExample>,
1191        runs: usize,
1192        calls: usize,
1193        reverts: usize,
1194        metrics: Map<String, InvariantMetrics>,
1195        failed_corpus_replays: usize,
1196        workers: usize,
1197        optimization_best_value: Option<I256>,
1198    ) {
1199        self.kind = TestKind::Invariant {
1200            runs,
1201            calls,
1202            reverts,
1203            workers: workers.max(1),
1204            metrics,
1205            failed_corpus_replays,
1206            optimization_best_value,
1207        };
1208        // For optimization mode (Some value), always succeed. For check mode (None), use success.
1209        self.status = if optimization_best_value.is_some() || success {
1210            TestStatus::Success
1211        } else {
1212            TestStatus::Failure
1213        };
1214        self.invariant_failures = invariant_failures;
1215        self.invariant_predicate_results = invariant_predicate_results;
1216        self.invariant_failure_dir = invariant_failure_dir;
1217        self.invariant_count = invariant_count;
1218        self.invariant_handler_failures = invariant_handler_failures;
1219        // `counterexample` is only used by the renderer for optimization mode (the "best
1220        // sequence" rendered on success). Invariant check-mode failures live entirely in
1221        // `invariant_failures`; `reason`/`counterexample` stay `None` for invariant tests.
1222        self.counterexample = counterexample;
1223        self.gas_report_traces = gas_report_traces;
1224    }
1225
1226    /// Returns the result for a table test. Merges table test execution results (logs, labeled
1227    /// addresses, traces and coverages) in initial setup results.
1228    pub fn table_result(&mut self, result: FuzzTestResult) {
1229        self.kind = TestKind::Table {
1230            median_gas: result.median_gas(false),
1231            mean_gas: result.mean_gas(false),
1232            runs: result.gas_by_case.len(),
1233        };
1234
1235        // Record logs, labels, traces and merge coverages.
1236        extend!(self, result, TraceKind::Execution);
1237
1238        self.status = if result.skipped {
1239            TestStatus::Skipped
1240        } else if result.success {
1241            TestStatus::Success
1242        } else {
1243            TestStatus::Failure
1244        };
1245        self.reason = result.reason;
1246        self.counterexample = result.counterexample;
1247        self.duration = Duration::default();
1248        self.gas_report_traces = result.gas_report_traces.into_iter().map(|t| vec![t]).collect();
1249        self.breakpoints = result.breakpoints.unwrap_or_default();
1250        self.deprecated_cheatcodes = result.deprecated_cheatcodes;
1251    }
1252
1253    /// Returns the result for a symbolic test.
1254    pub fn symbolic_result(
1255        &mut self,
1256        success: bool,
1257        reason: Option<String>,
1258        counterexample: Option<CounterExample>,
1259        stats: SymbolicStats,
1260    ) {
1261        self.kind = TestKind::Symbolic {
1262            paths: stats.paths,
1263            solver_queries: stats.solver_queries,
1264            smt_queries: stats.smt_queries,
1265            sat_queries: stats.sat_queries,
1266            model_queries: stats.model_queries,
1267            sat_cache_hits: stats.sat_cache_hits,
1268            model_cache_hits: stats.model_cache_hits,
1269            heuristic_witnesses: stats.heuristic_witnesses,
1270            solver_time_ms: stats.solver_time_ms,
1271        };
1272        self.status = if success { TestStatus::Success } else { TestStatus::Failure };
1273        self.reason = reason;
1274        self.counterexample = counterexample;
1275        self.duration = Duration::default();
1276    }
1277
1278    /// Records a successful showmap replay result.
1279    pub fn replay_result(
1280        &mut self,
1281        corpus_entries: usize,
1282        showmap_files: usize,
1283        skipped_entries: usize,
1284        duration: Duration,
1285    ) {
1286        self.kind = TestKind::Replay { corpus_entries, showmap_files, skipped_entries };
1287        self.status = TestStatus::Success;
1288        self.duration = duration;
1289    }
1290
1291    /// Records a skipped showmap replay (e.g. unit test or no corpus available).
1292    pub fn replay_skip(&mut self, reason: impl Into<String>) {
1293        self.kind = TestKind::Replay { corpus_entries: 0, showmap_files: 0, skipped_entries: 0 };
1294        self.status = TestStatus::Skipped;
1295        self.reason = Some(reason.into());
1296        self.duration = Duration::default();
1297    }
1298
1299    /// Returns `true` if this is the result of a fuzz test
1300    pub const fn is_fuzz(&self) -> bool {
1301        matches!(self.kind, TestKind::Fuzz { .. })
1302    }
1303
1304    /// Formats the test result into a string (for printing).
1305    pub fn short_result(&self, name: &str) -> String {
1306        self.short_result_with_campaign_name(name, None)
1307    }
1308
1309    pub(crate) fn short_result_with_suite(&self, name: &str, suite_name: &str) -> String {
1310        self.short_result_with_campaign_name(name, Some(get_contract_name(suite_name)))
1311    }
1312
1313    fn short_result_with_campaign_name(&self, name: &str, contract_name: Option<&str>) -> String {
1314        let is_invariant_campaign = self.is_invariant_campaign();
1315        let name = if is_invariant_campaign {
1316            contract_name
1317                .map(invariant_campaign_display_name)
1318                .map(Cow::Owned)
1319                .unwrap_or(Cow::Borrowed(INVARIANT_CAMPAIGN_FALLBACK_NAME))
1320        } else {
1321            Cow::Borrowed(name)
1322        };
1323        let status = self.render_status_block(true, is_invariant_campaign.then_some(name.as_ref()));
1324        format!("{status} {name} {}", self.kind.report())
1325    }
1326
1327    const fn is_invariant_campaign(&self) -> bool {
1328        self.kind.is_invariant() && self.invariant_count.is_some()
1329    }
1330
1331    fn logical_count(&self) -> usize {
1332        let skipped = self.skipped_predicate_count();
1333        if skipped == 0 {
1334            1
1335        } else if self.status.is_skipped() && skipped == self.invariant_predicate_results.len() {
1336            skipped
1337        } else {
1338            1 + skipped
1339        }
1340    }
1341
1342    fn passed_count(&self) -> usize {
1343        usize::from(self.status.is_success())
1344    }
1345
1346    fn skipped_count(&self) -> usize {
1347        let skipped = self.skipped_predicate_count();
1348        if skipped == 0 && self.status.is_skipped() { 1 } else { skipped }
1349    }
1350
1351    fn failed_count(&self) -> usize {
1352        usize::from(self.status.is_failure())
1353    }
1354
1355    fn skipped_predicate_count(&self) -> usize {
1356        self.invariant_predicate_results
1357            .iter()
1358            .filter(|predicate| predicate.status.is_skipped())
1359            .count()
1360    }
1361
1362    /// Merges the given raw call result into `self`.
1363    pub fn extend<FEN: FoundryEvmNetwork>(&mut self, call_result: RawCallResult<FEN>) {
1364        extend!(self, call_result, TraceKind::Execution);
1365    }
1366
1367    /// Merges the given coverage result into `self`.
1368    pub fn merge_coverages(&mut self, other_coverage: Option<HitMaps>) {
1369        HitMaps::merge_opt(&mut self.line_coverage, other_coverage);
1370    }
1371}
1372
1373/// Data report by a test.
1374#[derive(Clone, Debug, PartialEq, Eq)]
1375pub enum TestKindReport {
1376    Unit {
1377        gas: u64,
1378    },
1379    Fuzz {
1380        runs: usize,
1381        mean_gas: u64,
1382        median_gas: u64,
1383        failed_corpus_replays: usize,
1384    },
1385    Invariant {
1386        runs: usize,
1387        calls: usize,
1388        reverts: usize,
1389        metrics: Map<String, InvariantMetrics>,
1390        failed_corpus_replays: usize,
1391        /// For optimization mode (int256 return): the best value achieved. None = check mode.
1392        optimization_best_value: Option<I256>,
1393    },
1394    Table {
1395        runs: usize,
1396        mean_gas: u64,
1397        median_gas: u64,
1398    },
1399    Symbolic {
1400        paths: usize,
1401        solver_queries: usize,
1402        smt_queries: usize,
1403        sat_queries: usize,
1404        model_queries: usize,
1405        sat_cache_hits: usize,
1406        model_cache_hits: usize,
1407        heuristic_witnesses: usize,
1408        solver_time_ms: u64,
1409    },
1410    /// Showmap corpus replay (no campaign performed).
1411    Replay {
1412        corpus_entries: usize,
1413        showmap_files: usize,
1414        skipped_entries: usize,
1415    },
1416}
1417
1418impl fmt::Display for TestKindReport {
1419    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1420        match self {
1421            Self::Unit { gas } => {
1422                write!(f, "(gas: {gas})")
1423            }
1424            Self::Fuzz { runs, mean_gas, median_gas, failed_corpus_replays } => {
1425                if *failed_corpus_replays != 0 {
1426                    write!(
1427                        f,
1428                        "(runs: {runs}, μ: {mean_gas}, ~: {median_gas}, failed corpus replays: {failed_corpus_replays})"
1429                    )
1430                } else {
1431                    write!(f, "(runs: {runs}, μ: {mean_gas}, ~: {median_gas})")
1432                }
1433            }
1434            Self::Invariant {
1435                runs,
1436                calls,
1437                reverts,
1438                metrics: _,
1439                failed_corpus_replays,
1440                optimization_best_value,
1441            } => {
1442                // If optimization_best_value is Some, this is optimization mode.
1443                if let Some(best_value) = optimization_best_value {
1444                    write!(f, "(best: {best_value}, runs: {runs}, calls: {calls})")
1445                } else if *failed_corpus_replays != 0 {
1446                    write!(
1447                        f,
1448                        "(runs: {runs}, calls: {calls}, reverts: {reverts}, failed corpus replays: {failed_corpus_replays})"
1449                    )
1450                } else {
1451                    write!(f, "(runs: {runs}, calls: {calls}, reverts: {reverts})")
1452                }
1453            }
1454            Self::Table { runs, mean_gas, median_gas } => {
1455                write!(f, "(runs: {runs}, μ: {mean_gas}, ~: {median_gas})")
1456            }
1457            Self::Symbolic {
1458                paths,
1459                solver_queries,
1460                smt_queries,
1461                sat_queries,
1462                model_queries,
1463                sat_cache_hits,
1464                model_cache_hits,
1465                heuristic_witnesses,
1466                solver_time_ms,
1467            } => {
1468                write!(
1469                    f,
1470                    "(paths: {paths}, queries: {solver_queries}, smt: {smt_queries}, sat: {sat_queries} ({sat_cache_hits} cached), models: {model_queries} ({model_cache_hits} cached), hard-arith: {heuristic_witnesses}, solver: {solver_time_ms}ms)"
1471                )
1472            }
1473            Self::Replay { corpus_entries, showmap_files, skipped_entries } => {
1474                if *skipped_entries != 0 {
1475                    write!(
1476                        f,
1477                        "(replay: {corpus_entries} entries, {showmap_files} files, {skipped_entries} skipped)"
1478                    )
1479                } else {
1480                    write!(f, "(replay: {corpus_entries} entries, {showmap_files} files)")
1481                }
1482            }
1483        }
1484    }
1485}
1486
1487impl TestKindReport {
1488    /// Returns the main gas value to compare against
1489    pub const fn gas(&self) -> u64 {
1490        match *self {
1491            Self::Unit { gas } => gas,
1492            // We use the median for comparisons
1493            Self::Fuzz { median_gas, .. } | Self::Table { median_gas, .. } => median_gas,
1494            // We return 0 since it's not applicable
1495            Self::Invariant { .. } | Self::Symbolic { .. } | Self::Replay { .. } => 0,
1496        }
1497    }
1498}
1499
1500/// Various types of tests
1501#[derive(Clone, Debug, Serialize, Deserialize)]
1502pub enum TestKind {
1503    /// A unit test.
1504    Unit { gas: u64 },
1505    /// A fuzz test.
1506    Fuzz {
1507        /// we keep this for the debugger
1508        first_case: FuzzCase,
1509        runs: usize,
1510        mean_gas: u64,
1511        median_gas: u64,
1512        failed_corpus_replays: usize,
1513    },
1514    /// An invariant test.
1515    Invariant {
1516        runs: usize,
1517        calls: usize,
1518        reverts: usize,
1519        /// Actual worker count used by this invariant campaign.
1520        #[serde(default = "default_invariant_workers")]
1521        workers: usize,
1522        metrics: Map<String, InvariantMetrics>,
1523        failed_corpus_replays: usize,
1524        /// For optimization mode (int256 return): the best value achieved. None = check mode.
1525        optimization_best_value: Option<I256>,
1526    },
1527    /// A table test.
1528    Table { runs: usize, mean_gas: u64, median_gas: u64 },
1529    /// A symbolic test.
1530    Symbolic {
1531        paths: usize,
1532        solver_queries: usize,
1533        #[serde(default)]
1534        smt_queries: usize,
1535        #[serde(default)]
1536        sat_queries: usize,
1537        #[serde(default)]
1538        model_queries: usize,
1539        #[serde(default)]
1540        sat_cache_hits: usize,
1541        #[serde(default)]
1542        model_cache_hits: usize,
1543        #[serde(default)]
1544        heuristic_witnesses: usize,
1545        #[serde(default)]
1546        solver_time_ms: u64,
1547    },
1548    /// Showmap corpus replay (no campaign performed).
1549    Replay { corpus_entries: usize, showmap_files: usize, skipped_entries: usize },
1550}
1551
1552impl Default for TestKind {
1553    fn default() -> Self {
1554        Self::Unit { gas: 0 }
1555    }
1556}
1557
1558impl TestKind {
1559    /// Returns `true` if this is a fuzz test.
1560    pub const fn is_fuzz(&self) -> bool {
1561        matches!(self, Self::Fuzz { .. })
1562    }
1563
1564    /// Returns `true` if this is an invariant test.
1565    pub const fn is_invariant(&self) -> bool {
1566        matches!(self, Self::Invariant { .. })
1567    }
1568
1569    /// Actual invariant campaign worker count, if this is an invariant test.
1570    pub const fn invariant_workers(&self) -> Option<usize> {
1571        match self {
1572            Self::Invariant { workers, .. } => Some(*workers),
1573            _ => None,
1574        }
1575    }
1576
1577    /// The gas consumed by this test
1578    pub fn report(&self) -> TestKindReport {
1579        match self {
1580            Self::Unit { gas } => TestKindReport::Unit { gas: *gas },
1581            Self::Fuzz { first_case: _, runs, mean_gas, median_gas, failed_corpus_replays } => {
1582                TestKindReport::Fuzz {
1583                    runs: *runs,
1584                    mean_gas: *mean_gas,
1585                    median_gas: *median_gas,
1586                    failed_corpus_replays: *failed_corpus_replays,
1587                }
1588            }
1589            Self::Invariant {
1590                runs,
1591                calls,
1592                reverts,
1593                workers: _,
1594                metrics: _,
1595                failed_corpus_replays,
1596                optimization_best_value,
1597            } => TestKindReport::Invariant {
1598                runs: *runs,
1599                calls: *calls,
1600                reverts: *reverts,
1601                metrics: HashMap::default(),
1602                failed_corpus_replays: *failed_corpus_replays,
1603                optimization_best_value: *optimization_best_value,
1604            },
1605            Self::Table { runs, mean_gas, median_gas } => {
1606                TestKindReport::Table { runs: *runs, mean_gas: *mean_gas, median_gas: *median_gas }
1607            }
1608            Self::Symbolic {
1609                paths,
1610                solver_queries,
1611                smt_queries,
1612                sat_queries,
1613                model_queries,
1614                sat_cache_hits,
1615                model_cache_hits,
1616                heuristic_witnesses,
1617                solver_time_ms,
1618            } => TestKindReport::Symbolic {
1619                paths: *paths,
1620                solver_queries: *solver_queries,
1621                smt_queries: *smt_queries,
1622                sat_queries: *sat_queries,
1623                model_queries: *model_queries,
1624                sat_cache_hits: *sat_cache_hits,
1625                model_cache_hits: *model_cache_hits,
1626                heuristic_witnesses: *heuristic_witnesses,
1627                solver_time_ms: *solver_time_ms,
1628            },
1629            Self::Replay { corpus_entries, showmap_files, skipped_entries } => {
1630                TestKindReport::Replay {
1631                    corpus_entries: *corpus_entries,
1632                    showmap_files: *showmap_files,
1633                    skipped_entries: *skipped_entries,
1634                }
1635            }
1636        }
1637    }
1638}
1639
1640const fn default_invariant_workers() -> usize {
1641    1
1642}
1643
1644/// The result of a test setup.
1645///
1646/// Includes the deployment of the required libraries and the test contract itself, and the call to
1647/// the `setUp()` function.
1648#[derive(Clone, Debug, Default)]
1649pub struct TestSetup {
1650    /// The address at which the test contract was deployed.
1651    pub address: Address,
1652    /// Defined fuzz test fixtures.
1653    pub fuzz_fixtures: FuzzFixtures,
1654
1655    /// The logs emitted during setup.
1656    pub logs: Vec<Log>,
1657    /// Addresses labeled during setup.
1658    pub labels: AddressHashMap<String>,
1659    /// Call traces of the setup.
1660    pub traces: Traces,
1661    /// Runtime bytecodes for contracts seen in setup traces.
1662    pub debug_bytecodes: AddressHashMap<Bytes>,
1663    /// Coverage info during setup.
1664    pub coverage: Option<HitMaps>,
1665    /// Addresses of external libraries deployed during setup.
1666    pub deployed_libs: Vec<Address>,
1667
1668    /// The reason the setup failed, if it did.
1669    pub reason: Option<String>,
1670    /// Whether setup and entire test suite is skipped.
1671    pub skipped: bool,
1672    /// Whether the test failed to deploy.
1673    pub deployment_failure: bool,
1674}
1675
1676impl TestSetup {
1677    pub fn failed(reason: String) -> Self {
1678        Self { reason: Some(reason), ..Default::default() }
1679    }
1680
1681    pub fn skipped(reason: String) -> Self {
1682        Self { reason: Some(reason), skipped: true, ..Default::default() }
1683    }
1684
1685    pub fn extend<FEN: FoundryEvmNetwork>(
1686        &mut self,
1687        raw: RawCallResult<FEN>,
1688        trace_kind: TraceKind,
1689    ) {
1690        extend!(self, raw, trace_kind);
1691    }
1692
1693    pub fn merge_coverages(&mut self, other_coverage: Option<HitMaps>) {
1694        HitMaps::merge_opt(&mut self.coverage, other_coverage);
1695    }
1696}