Skip to main content

forge/
result.rs

1//! Test outcomes.
2
3use crate::{
4    fuzz::{BaseCounterExample, BasicTxDetails},
5    gas_report::GasReport,
6};
7use alloy_primitives::{
8    Address, B256, Bytes, I256, Log, Selector, U256,
9    map::{AddressHashMap, HashMap},
10};
11use eyre::Report;
12use foundry_common::{ContractsByArtifact, get_contract_name, shell};
13use foundry_config::{SymbolicConfig, SymbolicExplorationOrder, SymbolicStorageLayout};
14use foundry_evm::{
15    core::{Breakpoints, evm::FoundryEvmNetwork},
16    coverage::HitMaps,
17    decode::SkipReason,
18    executors::{
19        RawCallResult,
20        invariant::{CheckSequenceFailureSite, CheckSequenceOutcome, InvariantMetrics},
21    },
22    fuzz::{
23        CallDetails, CounterExample, FuzzCase, FuzzFixtures, FuzzTestResult,
24        strategies::EvmFuzzState,
25    },
26    traces::{CallTraceArena, CallTraceDecoder, TraceKind, Traces},
27};
28use foundry_evm_symbolic::{
29    PortfolioDiagnostics, SymbolicStats, SymbolicStopReason, SymbolicStorageAssignment,
30};
31use serde::{Deserialize, Serialize};
32use std::{
33    collections::{BTreeMap, HashMap as Map},
34    fmt::{self, Write},
35    path::PathBuf,
36    sync::OnceLock,
37    time::Duration,
38};
39use yansi::Paint;
40
41const INVARIANT_CAMPAIGN_FALLBACK_NAME: &str = "Invariant campaign";
42const SYMBOLIC_RESULT_SCHEMA_VERSION: u32 = 1;
43pub const SYMBOLIC_COUNTEREXAMPLE_ARTIFACT_SCHEMA: &str = "foundry:symbolic.counterexample@v1";
44pub const SYMBOLIC_COUNTEREXAMPLE_ARTIFACT_SCHEMA_VERSION: u32 = 1;
45
46/// The aggregated result of a test run.
47#[derive(Clone, Debug)]
48pub struct TestOutcome {
49    /// The results of all test suites by their identifier (`path:contract_name`).
50    ///
51    /// Essentially `identifier => signature => result`.
52    pub results: BTreeMap<String, SuiteResult>,
53    /// Complete results for JSON file output, including suites hidden from fail-fast console
54    /// output.
55    pub(crate) json_file_results: Option<BTreeMap<String, SuiteResult>>,
56    /// Whether to allow test failures without failing the entire test run.
57    pub allow_failure: bool,
58    /// The decoder used to decode traces and logs.
59    ///
60    /// This is `None` if traces and logs were not decoded.
61    ///
62    /// Note that `Address` fields only contain the last executed test case's data.
63    pub last_run_decoder: Option<CallTraceDecoder>,
64    /// The gas report, if requested.
65    pub gas_report: Option<GasReport>,
66    /// Known contracts from the test run (used for coverage).
67    pub known_contracts: Option<ContractsByArtifact>,
68    /// The fuzz seed used for the test run.
69    pub fuzz_seed: Option<U256>,
70}
71
72impl TestOutcome {
73    /// Creates a new test outcome with the given results.
74    pub const fn new(
75        known_contracts: Option<ContractsByArtifact>,
76        results: BTreeMap<String, SuiteResult>,
77        allow_failure: bool,
78        fuzz_seed: Option<U256>,
79    ) -> Self {
80        Self {
81            results,
82            json_file_results: None,
83            allow_failure,
84            last_run_decoder: None,
85            gas_report: None,
86            known_contracts,
87            fuzz_seed,
88        }
89    }
90
91    /// Creates a new empty test outcome.
92    pub const fn empty(known_contracts: Option<ContractsByArtifact>, allow_failure: bool) -> Self {
93        Self::new(known_contracts, BTreeMap::new(), allow_failure, None)
94    }
95
96    /// Returns an iterator over all individual succeeding tests and their names.
97    pub fn successes(&self) -> impl Iterator<Item = (&String, &TestResult)> {
98        self.tests().filter(|(_, t)| t.status.is_success())
99    }
100
101    /// Returns an iterator over all individual skipped tests and their names.
102    pub fn skips(&self) -> impl Iterator<Item = (&String, &TestResult)> {
103        self.tests().filter(|(_, t)| t.status.is_skipped())
104    }
105
106    /// Returns an iterator over all individual failing tests and their names.
107    pub fn failures(&self) -> impl Iterator<Item = (&String, &TestResult)> {
108        self.tests().filter(|(_, t)| t.status.is_failure())
109    }
110
111    /// Returns an iterator over all individual tests and their names.
112    pub fn tests(&self) -> impl Iterator<Item = (&String, &TestResult)> {
113        self.results.values().flat_map(|suite| suite.tests())
114    }
115
116    /// Flattens the test outcome into a list of individual tests.
117    pub fn into_tests(self) -> impl Iterator<Item = SuiteTestResult> {
118        self.results.into_iter().flat_map(|(artifact_id, suite)| {
119            suite.test_results.into_iter().map(move |(signature, result)| SuiteTestResult {
120                artifact_id: artifact_id.clone(),
121                signature,
122                result,
123            })
124        })
125    }
126
127    /// Returns the number of tests that passed.
128    pub fn passed(&self) -> usize {
129        self.results.values().map(SuiteResult::passed).sum()
130    }
131
132    /// Returns the number of tests that were skipped.
133    pub fn skipped(&self) -> usize {
134        self.results.values().map(SuiteResult::skipped).sum()
135    }
136
137    /// Returns the number of tests that failed.
138    pub fn failed(&self) -> usize {
139        self.results.values().map(SuiteResult::failed).sum()
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    /// Returns `true` if all failing tests can be meaningfully inspected with `forge test --debug`.
148    fn failed_tests_are_debuggable(&self) -> bool {
149        self.failures().all(|(_, result)| result.is_debuggable_failure())
150    }
151
152    /// Returns the shared parallel worker count of all failing invariant tests, if they agree.
153    fn invariant_workers_hint(&self) -> Option<usize> {
154        let mut workers = self.failures().filter_map(|(_, result)| result.kind.invariant_workers());
155        let first = workers.next()?;
156        (first > 1 && workers.all(|workers| workers == first)).then_some(first)
157    }
158
159    /// Sums up all the durations of all individual test suites.
160    ///
161    /// Note that this is not necessarily the wall clock time of the entire test run.
162    pub fn total_time(&self) -> Duration {
163        self.results.values().map(|suite| suite.duration).sum()
164    }
165
166    /// Formats the aggregated summary of all test suites into a string (for printing).
167    pub fn summary(&self, wall_clock_time: Duration) -> String {
168        let num_test_suites = self.results.len();
169        let suites = if num_test_suites == 1 { "suite" } else { "suites" };
170        let (passed, failed, skipped) = (self.passed(), self.failed(), self.skipped());
171        format!(
172            "\nRan {num_test_suites} test {suites} in {wall_clock_time:.2?} ({:.2?} CPU time): {} tests passed, {} failed, {} skipped ({} total tests)",
173            self.total_time(),
174            passed.green(),
175            failed.red(),
176            skipped.yellow(),
177            passed + failed + skipped
178        )
179    }
180
181    /// Checks if there are any failures and failures are disallowed.
182    pub fn ensure_ok(&self, silent: bool) -> eyre::Result<()> {
183        let failures = self.failures().count();
184        if self.allow_failure || failures == 0 {
185            return Ok(());
186        }
187
188        if shell::is_quiet() || silent {
189            std::process::exit(1);
190        }
191
192        sh_println!("\nFailing tests:")?;
193        for (suite_name, suite) in &self.results {
194            let failed = suite.failed();
195            if failed == 0 {
196                continue;
197            }
198
199            let term = if failed > 1 { "tests" } else { "test" };
200            sh_println!("Encountered {failed} failing {term} in {suite_name}")?;
201            for (name, result) in suite.failures() {
202                sh_println!("{}", result.short_result_with_suite(name, suite_name))?;
203            }
204            sh_println!()?;
205        }
206        sh_println!(
207            "Encountered a total of {} failing tests, {} tests succeeded",
208            failures.to_string().red(),
209            self.passed().to_string().green()
210        )?;
211
212        let test_word = if failures == 1 { "test" } else { "tests" };
213        sh_println!(
214            "\nTip: Run {} to retry only the {failures} failed {test_word}",
215            "`forge test --rerun`".cyan()
216        )?;
217        if self.failed_tests_are_debuggable() {
218            sh_println!(
219                "Tip: Run {} to inspect one failing test in the debugger",
220                "`forge test --debug --match-test <TEST_NAME>`".cyan()
221            )?;
222        }
223
224        // Print seed for fuzz/invariant test failures to enable reproduction.
225        if let Some(seed) = self.fuzz_seed
226            && self.has_fuzz_failures()
227        {
228            sh_println!(
229                "\nFuzz seed: {} (use {} to reproduce)",
230                format!("{seed:#x}").cyan(),
231                "`--fuzz-seed`".cyan()
232            )?;
233            if let Some(invariant_workers) = self.invariant_workers_hint() {
234                sh_println!(
235                    "Invariant workers: {invariant_workers} (use {} to reproduce)",
236                    format!("`--invariant-workers {invariant_workers}`").cyan()
237                )?;
238            }
239        }
240
241        std::process::exit(1);
242    }
243
244    /// Removes first test result, if any.
245    pub fn remove_first(&mut self) -> Option<(String, String, TestResult)> {
246        self.results.iter_mut().find_map(|(suite_name, suite)| {
247            let (test_name, result) = suite.test_results.pop_first()?;
248            Some((suite_name.clone(), test_name, result))
249        })
250    }
251}
252
253/// A set of test results for a single test suite, which is all the tests in a single contract.
254#[derive(Clone, Debug, Serialize)]
255pub struct SuiteResult {
256    /// Wall clock time it took to execute all tests in this suite.
257    #[serde(with = "foundry_common::serde_helpers::duration")]
258    pub duration: Duration,
259    /// Individual test results: `test fn signature -> TestResult`.
260    pub test_results: BTreeMap<String, TestResult>,
261    /// Generated warnings.
262    pub warnings: Vec<String>,
263}
264
265impl SuiteResult {
266    pub fn new(
267        duration: Duration,
268        test_results: BTreeMap<String, TestResult>,
269        mut warnings: Vec<String>,
270    ) -> Self {
271        // Add deprecated cheatcodes warning, if any of them used in current test suite.
272        let deprecated_cheatcodes = test_results
273            .values()
274            .flat_map(|result| result.deprecated_cheatcodes.iter().map(|(k, v)| (*k, *v)))
275            .collect::<HashMap<_, _>>();
276        if !deprecated_cheatcodes.is_empty() {
277            let mut warning =
278                "the following cheatcode(s) are deprecated and will be removed in future versions:"
279                    .to_string();
280            for (cheatcode, reason) in deprecated_cheatcodes {
281                write!(warning, "\n  {cheatcode}").unwrap();
282                if let Some(reason) = reason {
283                    write!(warning, ": {reason}").unwrap();
284                }
285            }
286            warnings.push(warning);
287        }
288
289        Self { duration, test_results, warnings }
290    }
291
292    /// Returns an iterator over all individual succeeding tests and their names.
293    pub fn successes(&self) -> impl Iterator<Item = (&String, &TestResult)> {
294        self.tests().filter(|(_, t)| t.status.is_success())
295    }
296
297    /// Returns an iterator over all individual skipped tests and their names.
298    pub fn skips(&self) -> impl Iterator<Item = (&String, &TestResult)> {
299        self.tests().filter(|(_, t)| t.status.is_skipped())
300    }
301
302    /// Returns an iterator over all individual failing tests and their names.
303    pub fn failures(&self) -> impl Iterator<Item = (&String, &TestResult)> {
304        self.tests().filter(|(_, t)| t.status.is_failure())
305    }
306
307    /// Returns the number of tests that passed.
308    pub fn passed(&self) -> usize {
309        self.test_results.values().filter(|t| t.status.is_success()).count()
310    }
311
312    /// Returns the number of tests that were skipped.
313    pub fn skipped(&self) -> usize {
314        self.test_results.values().map(TestResult::skipped_count).sum()
315    }
316
317    /// Returns the number of tests that failed.
318    pub fn failed(&self) -> usize {
319        self.test_results.values().filter(|t| t.status.is_failure()).count()
320    }
321
322    /// Iterator over all tests and their names
323    pub fn tests(&self) -> impl Iterator<Item = (&String, &TestResult)> {
324        self.test_results.iter()
325    }
326
327    /// Whether this test suite is empty.
328    pub fn is_empty(&self) -> bool {
329        self.test_results.is_empty()
330    }
331
332    /// The number of tests in this test suite.
333    pub fn len(&self) -> usize {
334        self.test_results.values().map(TestResult::logical_count).sum()
335    }
336
337    /// Sums up all the durations of all individual tests in this suite.
338    ///
339    /// Note that this is not necessarily the wall clock time of the entire test suite.
340    pub fn total_time(&self) -> Duration {
341        self.test_results.values().map(|result| result.duration).sum()
342    }
343
344    /// Returns the summary of a single test suite.
345    pub fn summary(&self) -> String {
346        let failed = self.failed();
347        let result = if failed == 0 { "ok".green() } else { "FAILED".red() };
348        format!(
349            "Suite result: {result}. {} passed; {} failed; {} skipped; finished in {:.2?} ({:.2?} CPU time)",
350            self.passed().green(),
351            failed.red(),
352            self.skipped().yellow(),
353            self.duration,
354            self.total_time(),
355        )
356    }
357}
358
359/// The result of a single test in a test suite.
360///
361/// This is flattened from a [`TestOutcome`].
362#[derive(Clone, Debug)]
363pub struct SuiteTestResult {
364    /// The identifier of the artifact/contract in the form:
365    /// `<artifact file name>:<contract name>`.
366    pub artifact_id: String,
367    /// The function signature of the Solidity test.
368    pub signature: String,
369    /// The result of the executed test.
370    pub result: TestResult,
371}
372
373impl SuiteTestResult {
374    /// Returns the gas used by the test.
375    pub const fn gas_used(&self) -> u64 {
376        self.result.kind.report().gas()
377    }
378
379    /// Returns the contract name of the artifact ID.
380    pub fn contract_name(&self) -> &str {
381        get_contract_name(&self.artifact_id)
382    }
383}
384
385/// The status of a test.
386#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
387pub enum TestStatus {
388    Success,
389    #[default]
390    Failure,
391    Skipped,
392}
393
394impl TestStatus {
395    /// Returns `true` if the test was successful.
396    #[inline]
397    pub const fn is_success(self) -> bool {
398        matches!(self, Self::Success)
399    }
400
401    /// Returns `true` if the test failed.
402    #[inline]
403    pub const fn is_failure(self) -> bool {
404        matches!(self, Self::Failure)
405    }
406
407    /// Returns `true` if the test was skipped.
408    #[inline]
409    pub const fn is_skipped(self) -> bool {
410        matches!(self, Self::Skipped)
411    }
412}
413
414/// A failure surfaced by an invariant test campaign — either a broken `invariant_*`
415/// predicate ([`Self::Predicate`]) or a handler-side assertion bug ([`Self::Handler`]).
416#[derive(Clone, Debug, Serialize, Deserialize)]
417#[serde(tag = "kind", rename_all = "snake_case")]
418pub enum InvariantFailure {
419    /// A broken `invariant_*` predicate.
420    Predicate {
421        /// Invariant function name (e.g. `invariant_cond3`).
422        name: String,
423        /// Revert reason or assertion failure message.
424        reason: String,
425        /// Counterexample sequence, when one is available.
426        #[serde(default, skip_serializing_if = "Option::is_none")]
427        counterexample: Option<CounterExample>,
428        /// Durable replay artifact for this counterexample, when one was written.
429        #[serde(default, skip_serializing_if = "Option::is_none")]
430        artifact: Option<SymbolicArtifactRef>,
431        /// Deterministic concrete minimization details for this sequence, when minimized.
432        #[serde(default, skip_serializing_if = "Option::is_none")]
433        minimization: Option<SymbolicCounterexampleMinimization>,
434        /// Path where the counterexample was persisted for re-running and shrinking.
435        persisted_path: PathBuf,
436        /// Whether this failure is the stable campaign anchor.
437        /// When `true` and this is the only single-predicate failure, the function name is
438        /// omitted on the `[FAIL: ...]` line (the trailing summary already identifies it).
439        #[serde(default)]
440        is_anchor: bool,
441    },
442    /// A handler-side assertion bug discovered during the campaign.
443    Handler {
444        /// Best-effort human-readable name of the failing call, e.g. `Counter::increment` or
445        /// `0xabc...::0x12345678` when the contract/function cannot be resolved.
446        name: String,
447        /// Address of the handler whose call asserted/reverted with an assertion.
448        reverter: Address,
449        /// 4-byte selector of the failing handler function.
450        selector: Selector,
451        /// Decoded revert/assert reason.
452        reason: String,
453        /// Counterexample sequence leading up to (and including) the failing call.
454        #[serde(default, skip_serializing_if = "Option::is_none")]
455        counterexample: Option<CounterExample>,
456        /// Durable replay artifact for this counterexample, when one was written.
457        #[serde(default, skip_serializing_if = "Option::is_none")]
458        artifact: Option<SymbolicArtifactRef>,
459    },
460}
461
462impl InvariantFailure {
463    /// Reason rendered on the `[FAIL: ...]` line.
464    pub fn reason(&self) -> &str {
465        match self {
466            Self::Predicate { reason, .. } | Self::Handler { reason, .. } => reason,
467        }
468    }
469
470    /// Human-readable name (invariant fn name, or `Contract::function` for handler bugs).
471    pub fn name(&self) -> &str {
472        match self {
473            Self::Predicate { name, .. } | Self::Handler { name, .. } => name,
474        }
475    }
476
477    /// Invariant predicate name, if this is a predicate failure.
478    pub fn predicate_name(&self) -> Option<&str> {
479        match self {
480            Self::Predicate { name, .. } => Some(name),
481            Self::Handler { .. } => None,
482        }
483    }
484
485    /// Counterexample sequence, when one is available.
486    pub const fn counterexample(&self) -> Option<&CounterExample> {
487        match self {
488            Self::Predicate { counterexample, .. } | Self::Handler { counterexample, .. } => {
489                counterexample.as_ref()
490            }
491        }
492    }
493
494    /// Durable replay artifact for this failure, when one was written.
495    pub const fn artifact(&self) -> Option<&SymbolicArtifactRef> {
496        match self {
497            Self::Predicate { artifact, .. } | Self::Handler { artifact, .. } => artifact.as_ref(),
498        }
499    }
500
501    /// Deterministic concrete minimization details for predicate failures.
502    pub const fn minimization(&self) -> Option<&SymbolicCounterexampleMinimization> {
503        match self {
504            Self::Predicate { minimization, .. } => minimization.as_ref(),
505            Self::Handler { .. } => None,
506        }
507    }
508}
509
510/// Pass/fail status for an invariant predicate evaluated inside a contract-level campaign.
511#[derive(Clone, Debug, Serialize, Deserialize)]
512pub struct InvariantPredicateResult {
513    /// Invariant function name (e.g. `invariant_balance`).
514    pub name: String,
515    /// Predicate status within the logical campaign.
516    pub status: TestStatus,
517    /// Revert reason or assertion message when the predicate failed.
518    #[serde(default, skip_serializing_if = "Option::is_none")]
519    pub reason: Option<String>,
520}
521
522/// Stable machine-readable outcome for `forge test --symbolic` JSON output.
523#[derive(Clone, Debug, Serialize, Deserialize)]
524pub struct SymbolicResult {
525    /// Schema version for the symbolic result object.
526    #[serde(default = "symbolic_result_schema_version")]
527    pub schema_version: u32,
528    /// Normalized symbolic outcome.
529    pub status: SymbolicResultStatus,
530    /// Incomplete reason when [`Self::status`] is [`SymbolicResultStatus::Incomplete`].
531    pub incomplete: Option<SymbolicIncomplete>,
532    /// Effective bounds used by this symbolic run.
533    pub bounds: SymbolicBounds,
534    /// Solver identity and counters collected during this run.
535    pub solver: SymbolicSolverMetadata,
536    /// Soundness assumptions that bound what a `pass` proves.
537    pub assumptions: Vec<SymbolicAssumption>,
538    /// Where an agent can find the concrete replay trace, when one was produced.
539    pub call_trace: SymbolicCallTrace,
540    /// Concrete replay metadata for counterexample candidates.
541    pub replay: SymbolicReplayMetadata,
542    /// Concrete counterexample data, when the solver produced a candidate.
543    pub counterexample: Option<SymbolicCounterexample>,
544    /// Fuzz corpus seeds imported into symbolic execution, when enabled.
545    #[serde(default, skip_serializing_if = "Option::is_none")]
546    pub corpus_seeds: Option<SymbolicCorpusSeedMetadata>,
547    /// Durable counterexample artifact, when one was written.
548    #[serde(default, skip_serializing_if = "Option::is_none")]
549    pub artifact: Option<SymbolicArtifactRef>,
550    /// Deterministic concrete minimization details, when a replayed counterexample was minimized.
551    #[serde(default, skip_serializing_if = "Option::is_none")]
552    pub minimization: Option<SymbolicCounterexampleMinimization>,
553}
554
555impl SymbolicResult {
556    /// Creates a symbolic pass result.
557    pub fn pass(config: &SymbolicConfig, stats: SymbolicStats) -> Self {
558        Self::base(config, stats)
559    }
560
561    /// Creates a symbolic counterexample result that concrete replay confirmed.
562    pub fn fail_counterexample(
563        config: &SymbolicConfig,
564        stats: SymbolicStats,
565        call_trace: SymbolicCallTrace,
566        counterexample: SymbolicCounterexample,
567    ) -> Self {
568        Self {
569            counterexample: Some(counterexample),
570            ..Self::fail_counterexample_sequence(config, stats, call_trace)
571        }
572    }
573
574    /// Creates a symbolic sequence counterexample result that concrete replay confirmed.
575    pub fn fail_counterexample_sequence(
576        config: &SymbolicConfig,
577        stats: SymbolicStats,
578        call_trace: SymbolicCallTrace,
579    ) -> Self {
580        Self {
581            status: SymbolicResultStatus::FailCounterexample,
582            replay: SymbolicReplayMetadata::confirmed(),
583            call_trace,
584            ..Self::base(config, stats)
585        }
586    }
587
588    /// Creates an incomplete symbolic result.
589    pub fn incomplete(
590        config: &SymbolicConfig,
591        kind: SymbolicStopReason,
592        reason: impl Into<String>,
593        stats: SymbolicStats,
594        replay: SymbolicReplayMetadata,
595        call_trace: SymbolicCallTrace,
596        counterexample: Option<SymbolicCounterexample>,
597    ) -> Self {
598        Self {
599            status: SymbolicResultStatus::Incomplete,
600            incomplete: Some(SymbolicIncomplete::new(kind, reason)),
601            replay,
602            call_trace,
603            counterexample,
604            ..Self::base(config, stats)
605        }
606    }
607
608    /// A passing result carrying the run's bounds, solver metadata and assumptions.
609    fn base(config: &SymbolicConfig, stats: SymbolicStats) -> Self {
610        Self {
611            schema_version: SYMBOLIC_RESULT_SCHEMA_VERSION,
612            status: SymbolicResultStatus::Pass,
613            incomplete: None,
614            bounds: SymbolicBounds::from_config(config),
615            solver: SymbolicSolverMetadata {
616                name: config.solver.clone(),
617                command: config.solver_command.clone(),
618                portfolio: config.solver_portfolio.clone(),
619                stats,
620            },
621            assumptions: SymbolicAssumption::default_assumptions(),
622            call_trace: SymbolicCallTrace::none(),
623            replay: SymbolicReplayMetadata::not_required(),
624            counterexample: None,
625            corpus_seeds: None,
626            artifact: None,
627            minimization: None,
628        }
629    }
630
631    /// Attaches fuzz corpus import metadata to this symbolic result.
632    pub fn with_corpus_seeds(mut self, corpus_seeds: SymbolicCorpusSeedMetadata) -> Self {
633        self.corpus_seeds = Some(corpus_seeds);
634        self
635    }
636
637    /// Attaches a durable replay artifact reference to this symbolic result.
638    pub fn with_artifact(mut self, artifact: SymbolicArtifactRef) -> Self {
639        self.artifact = Some(artifact);
640        self
641    }
642
643    /// Attaches deterministic minimization metadata to this symbolic result.
644    pub fn with_minimization(mut self, minimization: SymbolicCounterexampleMinimization) -> Self {
645        self.minimization = Some(minimization);
646        self
647    }
648}
649
650/// Fuzz corpus import metadata for a symbolic run.
651#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
652pub struct SymbolicCorpusSeedMetadata {
653    /// Corpus root used for the current test, after contract/test path expansion.
654    pub corpus_dir: Option<PathBuf>,
655    /// Maximum imported seeds allowed by configuration.
656    pub limit: usize,
657    /// Number of corpus files considered.
658    pub loaded: usize,
659    /// Number of corpus files skipped because they were unreadable or not a matching single call.
660    pub skipped: usize,
661    /// Seeds modeled by symbolic execution as path-priority hints.
662    pub used: Vec<SymbolicCorpusSeedRef>,
663}
664
665/// One fuzz corpus seed modeled by symbolic execution.
666#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
667pub struct SymbolicCorpusSeedRef {
668    /// Corpus file path.
669    pub path: PathBuf,
670    /// ABI-encoded calldata imported from the corpus file.
671    pub calldata: Bytes,
672}
673
674/// Reference to a durable symbolic counterexample artifact.
675#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
676pub struct SymbolicArtifactRef {
677    /// Artifact schema id.
678    pub schema: String,
679    /// Path to the artifact file.
680    pub path: PathBuf,
681}
682
683impl SymbolicArtifactRef {
684    /// Creates a reference to a symbolic counterexample artifact.
685    pub fn new(path: impl Into<PathBuf>) -> Self {
686        Self { schema: SYMBOLIC_COUNTEREXAMPLE_ARTIFACT_SCHEMA.to_string(), path: path.into() }
687    }
688}
689
690/// Reference to a generated Solidity regression test for a symbolic counterexample.
691#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
692pub struct SymbolicRegressionRef {
693    /// Source counterexample artifact path.
694    pub artifact: PathBuf,
695    /// Generated Solidity regression test path.
696    pub path: PathBuf,
697}
698
699/// Before/after artifact references and counters for concrete symbolic counterexample minimization.
700#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
701#[serde(deny_unknown_fields)]
702pub struct SymbolicCounterexampleMinimization {
703    /// Original confirmed replay artifact before minimization.
704    pub original: SymbolicArtifactRef,
705    /// Minimized confirmed replay artifact after minimization.
706    pub minimized: SymbolicArtifactRef,
707    /// Number of concrete replay candidates tried.
708    pub attempts: usize,
709    /// Number of replay candidates accepted.
710    pub accepted: usize,
711    /// ABI calldata byte length before minimization.
712    pub original_calldata_bytes: usize,
713    /// ABI calldata byte length after minimization.
714    pub minimized_calldata_bytes: usize,
715    /// Stateful sequence length before minimization, when this minimized a sequence.
716    #[serde(default, skip_serializing_if = "Option::is_none")]
717    pub original_sequence_len: Option<usize>,
718    /// Stateful sequence length after minimization, when this minimized a sequence.
719    #[serde(default, skip_serializing_if = "Option::is_none")]
720    pub minimized_sequence_len: Option<usize>,
721}
722
723impl SymbolicCounterexampleMinimization {
724    /// Creates concrete minimization metadata.
725    pub const fn new(
726        original: SymbolicArtifactRef,
727        minimized: SymbolicArtifactRef,
728        attempts: usize,
729        accepted: usize,
730        original_calldata_bytes: usize,
731        minimized_calldata_bytes: usize,
732    ) -> Self {
733        Self {
734            original,
735            minimized,
736            attempts,
737            accepted,
738            original_calldata_bytes,
739            minimized_calldata_bytes,
740            original_sequence_len: None,
741            minimized_sequence_len: None,
742        }
743    }
744
745    /// Adds stateful sequence lengths to minimization metadata.
746    pub const fn with_sequence_lengths(
747        mut self,
748        original_sequence_len: usize,
749        minimized_sequence_len: usize,
750    ) -> Self {
751        self.original_sequence_len = Some(original_sequence_len);
752        self.minimized_sequence_len = Some(minimized_sequence_len);
753        self
754    }
755}
756
757/// Normalized symbolic outcome names for agents and other JSON consumers.
758#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
759#[serde(rename_all = "snake_case")]
760pub enum SymbolicResultStatus {
761    /// All explored paths completed without a feasible failure.
762    Pass,
763    /// A solver counterexample was replayed concretely and still failed.
764    FailCounterexample,
765    /// The engine stopped before a proof or replayed counterexample.
766    Incomplete,
767}
768
769/// Incomplete symbolic run reason.
770#[derive(Clone, Debug, Serialize, Deserialize)]
771pub struct SymbolicIncomplete {
772    /// Stable reason kind.
773    pub kind: String,
774    /// Human-readable detail.
775    pub reason: String,
776}
777
778impl SymbolicIncomplete {
779    fn new(kind: SymbolicStopReason, reason: impl Into<String>) -> Self {
780        let kind = match kind {
781            SymbolicStopReason::Stuck => "stuck",
782            SymbolicStopReason::RevertAll => "revert_all",
783            SymbolicStopReason::Timeout => "timeout",
784            SymbolicStopReason::Error => "error",
785        };
786        Self { kind: kind.to_string(), reason: reason.into() }
787    }
788}
789
790/// Effective symbolic exploration bounds used by the run.
791#[derive(Clone, Debug, Serialize, Deserialize)]
792pub struct SymbolicBounds {
793    /// Optional solver timeout in seconds.
794    pub timeout_seconds: Option<u32>,
795    /// Optional loop-unrolling bound.
796    pub loop_bound: Option<u32>,
797    /// Effective per-path opcode depth limit.
798    pub max_depth: u32,
799    /// Effective symbolic path width limit.
800    pub max_paths: u32,
801    /// Maximum calls in a bounded symbolic invariant sequence.
802    pub invariant_depth: u32,
803    /// Pending path exploration order.
804    pub exploration_order: SymbolicExplorationOrder,
805    /// Maximum normalized solver queries.
806    pub max_solver_queries: u32,
807    /// Default bounded length for dynamic ABI inputs.
808    pub default_dynamic_length: u32,
809    /// Maximum permitted bounded dynamic ABI input length.
810    pub max_dynamic_length: u32,
811    /// Positional dynamic-leaf bounded lengths.
812    pub array_lengths: Vec<u32>,
813    /// Named dynamic-leaf bounded lengths.
814    pub dynamic_lengths: BTreeMap<String, Vec<u32>>,
815    /// Default array lengths when no explicit dynamic length exists.
816    pub default_array_lengths: Vec<u32>,
817    /// Default bytes/string lengths when no explicit dynamic length exists.
818    pub default_bytes_lengths: Vec<u32>,
819    /// Maximum generated symbolic calldata size in bytes.
820    pub max_calldata_bytes: u32,
821    /// Whether symbolic call targets can range over known deployed contracts.
822    pub symbolic_call_targets: bool,
823    /// Storage modelling mode.
824    pub storage_layout: SymbolicStorageLayout,
825}
826
827impl SymbolicBounds {
828    fn from_config(config: &SymbolicConfig) -> Self {
829        Self {
830            timeout_seconds: config.timeout,
831            loop_bound: config.loop_bound,
832            max_depth: config.execution_depth(),
833            max_paths: config.path_width(),
834            invariant_depth: config.invariant_depth,
835            exploration_order: config.exploration_order,
836            max_solver_queries: config.max_solver_queries,
837            default_dynamic_length: config.default_dynamic_length,
838            max_dynamic_length: config.max_dynamic_length,
839            array_lengths: config.array_lengths.clone(),
840            dynamic_lengths: config.dynamic_lengths.clone(),
841            default_array_lengths: config.default_array_lengths.clone(),
842            default_bytes_lengths: config.default_bytes_lengths.clone(),
843            max_calldata_bytes: config.max_calldata_bytes,
844            symbolic_call_targets: config.symbolic_call_targets,
845            storage_layout: config.storage_layout,
846        }
847    }
848}
849
850/// Solver identity and counters.
851#[derive(Clone, Debug, Serialize, Deserialize)]
852pub struct SymbolicSolverMetadata {
853    /// Configured solver name.
854    pub name: String,
855    /// Exact configured solver command, when set.
856    pub command: Option<String>,
857    /// Configured solver portfolio entries, when any.
858    pub portfolio: Vec<String>,
859    /// Run counters.
860    pub stats: SymbolicStats,
861}
862
863/// Explicit symbolic assumption attached to a result.
864#[derive(Clone, Debug, Serialize, Deserialize)]
865pub struct SymbolicAssumption {
866    /// Stable assumption kind.
867    pub kind: String,
868    /// Human-readable detail.
869    pub description: String,
870}
871
872impl SymbolicAssumption {
873    fn default_assumptions() -> Vec<Self> {
874        vec![
875            Self {
876                kind: "bounded_exploration".to_string(),
877                description: "Result is scoped to the configured path, depth, solver-query, loop, calldata, and dynamic-length bounds.".to_string(),
878            },
879            Self {
880                kind: "hash_model".to_string(),
881                description: "Symbolic Keccak and hash-like precompile reasoning assumes collision and preimage resistance for modeled cases.".to_string(),
882            },
883        ]
884    }
885}
886
887/// Concrete replay trace locator.
888#[derive(Clone, Debug, Serialize, Deserialize)]
889pub struct SymbolicCallTrace {
890    /// Whether replay produced a trace that may be present in this test result.
891    pub available: bool,
892    /// JSON location for the trace when available.
893    pub source: Option<String>,
894    /// Trace format at the source location.
895    pub format: Option<String>,
896}
897
898impl SymbolicCallTrace {
899    /// No concrete trace was produced.
900    pub const fn none() -> Self {
901        Self { available: false, source: None, format: None }
902    }
903
904    /// A concrete replay trace may be available in the normal test result traces field.
905    pub fn test_result_traces(available: bool) -> Self {
906        Self {
907            available,
908            source: available.then(|| "test_result.traces".to_string()),
909            format: available.then(|| "foundry_call_trace_arena".to_string()),
910        }
911    }
912}
913
914/// Counterexample replay status.
915#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
916#[serde(rename_all = "snake_case")]
917pub enum SymbolicReplayStatus {
918    /// No replay was required for this result.
919    NotRequired,
920    /// Concrete replay confirmed the symbolic counterexample.
921    Confirmed,
922    /// Concrete replay did not reproduce the symbolic counterexample.
923    Mismatch,
924    /// Concrete replay could not execute because of an error.
925    Error,
926    /// Concrete replay was skipped by `vm.skip`.
927    Skipped,
928}
929
930/// Replay metadata for symbolic counterexample candidates.
931#[derive(Clone, Debug, Serialize, Deserialize)]
932#[serde(deny_unknown_fields)]
933pub struct SymbolicReplayMetadata {
934    /// Whether the symbolic outcome required concrete replay.
935    pub required: bool,
936    /// Stable replay status.
937    pub status: SymbolicReplayStatus,
938    /// Optional replay detail or mismatch reason.
939    pub reason: Option<String>,
940}
941
942impl SymbolicReplayMetadata {
943    /// No replay was required.
944    pub const fn not_required() -> Self {
945        Self { required: false, status: SymbolicReplayStatus::NotRequired, reason: None }
946    }
947
948    /// Concrete replay confirmed the counterexample.
949    pub const fn confirmed() -> Self {
950        Self { required: true, status: SymbolicReplayStatus::Confirmed, reason: None }
951    }
952
953    /// Concrete replay did not reproduce the symbolic counterexample.
954    pub fn mismatch(reason: impl Into<String>) -> Self {
955        Self { required: true, status: SymbolicReplayStatus::Mismatch, reason: Some(reason.into()) }
956    }
957
958    /// Concrete replay errored before the candidate could be confirmed.
959    pub fn error(reason: impl Into<String>) -> Self {
960        Self { required: true, status: SymbolicReplayStatus::Error, reason: Some(reason.into()) }
961    }
962
963    /// Concrete replay was skipped by the test.
964    pub fn skipped(reason: impl Into<String>) -> Self {
965        Self { required: true, status: SymbolicReplayStatus::Skipped, reason: Some(reason.into()) }
966    }
967}
968
969/// Stable symbolic counterexample payload.
970#[derive(Clone, Debug, Serialize, Deserialize)]
971pub struct SymbolicCounterexample {
972    /// ABI-encoded calldata for replay.
973    pub calldata: Bytes,
974    /// Pretty-formatted ABI arguments, when decoded.
975    pub args: Option<String>,
976    /// Raw ABI arguments, when decoded.
977    pub raw_args: Option<String>,
978    /// Ether value sent with the call, when any.
979    pub value: Option<U256>,
980}
981
982impl From<&BaseCounterExample> for SymbolicCounterexample {
983    fn from(counterexample: &BaseCounterExample) -> Self {
984        Self {
985            calldata: counterexample.calldata.clone(),
986            args: counterexample.args.clone(),
987            raw_args: counterexample.raw_args.clone(),
988            value: counterexample.value,
989        }
990    }
991}
992
993/// Durable symbolic counterexample artifact.
994#[derive(Clone, Debug, Serialize, Deserialize)]
995#[serde(deny_unknown_fields)]
996pub struct SymbolicCounterexampleArtifact {
997    /// Artifact schema version.
998    pub schema_version: u32,
999    /// Artifact schema id.
1000    pub schema: String,
1001    /// Whether this counterexample is a single test call or a stateful sequence.
1002    pub kind: SymbolicCounterexampleArtifactKind,
1003    /// Test identity that produced this counterexample.
1004    pub test: SymbolicCounterexampleTestIdentity,
1005    /// Concrete replay metadata for the counterexample candidate.
1006    pub replay: SymbolicReplayMetadata,
1007    /// Replay semantics that must remain stable when this artifact is replayed.
1008    pub replay_semantics: SymbolicCounterexampleReplaySemantics,
1009    /// Effective bounds used by this symbolic run.
1010    pub bounds: SymbolicBounds,
1011    /// Solver identity and counters collected during this run.
1012    pub solver: SymbolicSolverMetadata,
1013    /// Soundness assumptions that bound what a `pass` proves.
1014    pub assumptions: Vec<SymbolicAssumption>,
1015    /// Where an agent can find the concrete replay trace, when one was produced.
1016    pub call_trace: SymbolicCallTrace,
1017    /// Concrete setup-storage assignments required before replaying this artifact.
1018    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1019    pub storage: Vec<SymbolicStorageAssignment>,
1020    /// Stateful invariant failure origin, when this sequence came from symbolic invariants.
1021    #[serde(default, skip_serializing_if = "Option::is_none")]
1022    pub invariant_failure: Option<SymbolicInvariantArtifactFailure>,
1023    /// Concrete replay calls.
1024    pub calls: Vec<SymbolicCounterexampleCall>,
1025}
1026
1027impl SymbolicCounterexampleArtifact {
1028    /// Creates a durable symbolic counterexample artifact from a symbolic result and call list.
1029    pub fn new(
1030        kind: SymbolicCounterexampleArtifactKind,
1031        test: SymbolicCounterexampleTestIdentity,
1032        symbolic: &SymbolicResult,
1033        replay_semantics: SymbolicCounterexampleReplaySemantics,
1034        calls: Vec<SymbolicCounterexampleCall>,
1035    ) -> Self {
1036        Self {
1037            schema_version: SYMBOLIC_COUNTEREXAMPLE_ARTIFACT_SCHEMA_VERSION,
1038            schema: SYMBOLIC_COUNTEREXAMPLE_ARTIFACT_SCHEMA.to_string(),
1039            kind,
1040            test,
1041            replay: symbolic.replay.clone(),
1042            replay_semantics,
1043            bounds: symbolic.bounds.clone(),
1044            solver: symbolic.solver.clone(),
1045            assumptions: symbolic.assumptions.clone(),
1046            call_trace: symbolic.call_trace.clone(),
1047            storage: Vec::new(),
1048            invariant_failure: None,
1049            calls,
1050        }
1051    }
1052
1053    /// Attaches setup-storage assignments required for concrete replay.
1054    pub fn with_storage(mut self, storage: Vec<SymbolicStorageAssignment>) -> Self {
1055        self.storage = storage;
1056        self
1057    }
1058
1059    /// Attaches stateful invariant failure origin metadata.
1060    pub fn with_invariant_failure(
1061        mut self,
1062        invariant_failure: SymbolicInvariantArtifactFailure,
1063    ) -> Self {
1064        self.invariant_failure = Some(invariant_failure);
1065        self
1066    }
1067}
1068
1069/// Concrete replay semantics captured when a symbolic artifact is confirmed.
1070#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
1071#[serde(deny_unknown_fields)]
1072pub struct SymbolicCounterexampleReplaySemantics {
1073    /// Whether an invariant sequence replay treats any target-call revert as a failure.
1074    pub fail_on_revert: bool,
1075}
1076
1077/// Symbolic counterexample artifact shape.
1078#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1079#[serde(rename_all = "snake_case")]
1080pub enum SymbolicCounterexampleArtifactKind {
1081    /// A single stateless symbolic test call.
1082    SingleCall,
1083    /// A stateful sequence of calls.
1084    Sequence,
1085}
1086
1087/// Stateful invariant failure origin for a persisted symbolic sequence artifact.
1088#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1089#[serde(tag = "kind", rename_all = "snake_case")]
1090pub enum SymbolicInvariantArtifactFailure {
1091    /// An invariant predicate failed.
1092    Predicate {
1093        /// Invariant function name.
1094        name: String,
1095        /// Exact concrete failure site confirmed during replay.
1096        #[serde(default, skip_serializing_if = "Option::is_none")]
1097        site: Option<SymbolicInvariantFailureSite>,
1098    },
1099    /// A target/handler call asserted before an invariant predicate failed.
1100    Handler {
1101        /// Best-effort human-readable handler function name.
1102        #[serde(default, skip_serializing_if = "Option::is_none")]
1103        name: Option<String>,
1104        /// Address of the handler whose call asserted.
1105        reverter: Address,
1106        /// 4-byte selector of the failing handler call.
1107        selector: Selector,
1108        /// Stable edge fingerprint for the failing handler site.
1109        fingerprint: B256,
1110    },
1111}
1112
1113/// Concrete invariant failure site stored in symbolic replay artifacts.
1114#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1115#[serde(tag = "kind", rename_all = "snake_case")]
1116pub enum SymbolicInvariantFailureSite {
1117    /// Target/handler call failed before the invariant predicate.
1118    SequenceCall { target: Address, selector: Selector, fingerprint: B256 },
1119    /// Invariant predicate failed.
1120    Invariant { target: Address, selector: Selector, fingerprint: B256 },
1121    /// `afterInvariant` hook failed.
1122    AfterInvariant { target: Address, selector: Selector, fingerprint: B256 },
1123}
1124
1125impl From<CheckSequenceFailureSite> for SymbolicInvariantFailureSite {
1126    fn from(site: CheckSequenceFailureSite) -> Self {
1127        match site {
1128            CheckSequenceFailureSite::SequenceCall { target, selector, fingerprint } => {
1129                Self::SequenceCall { target, selector, fingerprint }
1130            }
1131            CheckSequenceFailureSite::Invariant { target, selector, fingerprint } => {
1132                Self::Invariant { target, selector, fingerprint }
1133            }
1134            CheckSequenceFailureSite::AfterInvariant { target, selector, fingerprint } => {
1135                Self::AfterInvariant { target, selector, fingerprint }
1136            }
1137        }
1138    }
1139}
1140
1141/// Test identity for a symbolic counterexample artifact.
1142#[derive(Clone, Debug, Serialize, Deserialize)]
1143#[serde(deny_unknown_fields)]
1144pub struct SymbolicCounterexampleTestIdentity {
1145    /// Contract identifier as reported by Forge.
1146    pub contract: String,
1147    /// Test function signature.
1148    pub test: String,
1149}
1150
1151/// One concrete call in a symbolic counterexample artifact.
1152#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1153#[serde(deny_unknown_fields)]
1154pub struct SymbolicCounterexampleCall {
1155    /// Amount to increase block timestamp before executing the call.
1156    pub warp: Option<U256>,
1157    /// Amount to increase block number before executing the call.
1158    pub roll: Option<U256>,
1159    /// Sender used for the call.
1160    pub sender: Address,
1161    /// Target address called.
1162    pub target: Address,
1163    /// ABI-encoded calldata for replay.
1164    pub calldata: Bytes,
1165    /// Ether value sent with the call, when any.
1166    pub value: Option<U256>,
1167    /// Human-readable contract identifier, when known.
1168    pub contract_name: Option<String>,
1169    /// ABI function name, when known.
1170    pub function_name: Option<String>,
1171    /// ABI function signature, when known.
1172    pub signature: Option<String>,
1173    /// Pretty-formatted ABI arguments, when decoded.
1174    pub args: Option<String>,
1175    /// Raw ABI arguments, when decoded.
1176    pub raw_args: Option<String>,
1177}
1178
1179impl SymbolicCounterexampleCall {
1180    /// Creates an artifact call from Foundry's base counterexample shape.
1181    pub fn from_base_counterexample(
1182        counterexample: &BaseCounterExample,
1183        default_sender: Address,
1184        default_target: Address,
1185    ) -> Self {
1186        Self {
1187            warp: counterexample.warp,
1188            roll: counterexample.roll,
1189            sender: counterexample.sender.unwrap_or(default_sender),
1190            target: counterexample.addr.unwrap_or(default_target),
1191            calldata: counterexample.calldata.clone(),
1192            value: counterexample.value,
1193            contract_name: counterexample.contract_name.clone(),
1194            function_name: counterexample.func_name.clone(),
1195            signature: counterexample.signature.clone(),
1196            args: counterexample.args.clone(),
1197            raw_args: counterexample.raw_args.clone(),
1198        }
1199    }
1200
1201    /// Creates Foundry's display counterexample shape from an artifact call.
1202    pub fn to_base_counterexample(&self) -> BaseCounterExample {
1203        BaseCounterExample {
1204            warp: self.warp,
1205            roll: self.roll,
1206            sender: Some(self.sender),
1207            addr: Some(self.target),
1208            calldata: self.calldata.clone(),
1209            value: self.value,
1210            contract_name: self.contract_name.clone(),
1211            func_name: self.function_name.clone(),
1212            signature: self.signature.clone(),
1213            args: self.args.clone(),
1214            raw_args: self.raw_args.clone(),
1215            traces: None,
1216            show_solidity: false,
1217            fuzz: Default::default(),
1218        }
1219    }
1220
1221    /// Converts an artifact call into Foundry's invariant replay transaction shape.
1222    pub fn to_basic_tx_details(&self) -> BasicTxDetails {
1223        BasicTxDetails {
1224            warp: self.warp,
1225            roll: self.roll,
1226            sender: self.sender,
1227            call_details: CallDetails {
1228                target: self.target,
1229                calldata: self.calldata.clone(),
1230                value: self.value,
1231            },
1232        }
1233    }
1234}
1235
1236/// The result of an executed test.
1237#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1238pub struct TestResult {
1239    /// The test status, indicating whether the test case succeeded, failed, or was marked as
1240    /// skipped. This means that the transaction executed properly, the test was marked as
1241    /// skipped with vm.skip(), or that there was a revert and that the test was expected to
1242    /// fail (prefixed with `testFail`)
1243    pub status: TestStatus,
1244
1245    /// If there was a revert, this field will be populated. Note that the test can
1246    /// still be successful (i.e self.success == true) when it's expected to fail.
1247    pub reason: Option<String>,
1248
1249    /// The active fork's block number after execution, if any.
1250    #[serde(default, skip_serializing_if = "Option::is_none")]
1251    pub fork_block_number: Option<u64>,
1252
1253    /// All broken invariant predicates in this campaign in source declaration order.
1254    ///
1255    /// For invariant tests, this is the single source of truth used by the renderer.
1256    /// `reason` and `counterexample` are not populated for invariant tests.
1257    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1258    pub invariant_failures: Vec<InvariantFailure>,
1259
1260    /// Per-predicate outcomes for invariant campaigns. This preserves individual
1261    /// `invariant_*` / `statefulFuzz*` pass/fail reporting when multiple predicates are checked
1262    /// by one contract-level campaign.
1263    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1264    pub invariant_predicate_results: Vec<InvariantPredicateResult>,
1265
1266    /// Directory where invariant failure counterexamples have been persisted (set when one or more
1267    /// secondary invariant failures were written, so users can locate persisted counterexamples).
1268    #[serde(default, skip_serializing_if = "Option::is_none")]
1269    pub invariant_failure_dir: Option<PathBuf>,
1270
1271    /// Total number of invariant predicates exercised in this campaign. When `Some(n)` the
1272    /// user-facing report renders a contract-level `<broken>/<n> invariants broken` summary so
1273    /// users get an at-a-glance health line without counting `[FAIL]` blocks. `None` for
1274    /// single-predicate campaigns.
1275    #[serde(default, skip_serializing_if = "Option::is_none")]
1276    pub invariant_count: Option<usize>,
1277
1278    /// Handler-side assertion bugs found during the campaign, deduped by
1279    /// `(reverter, selector)` site (Medusa/Echidna semantics). Rendered in a dedicated
1280    /// `Assertion Tests` section.
1281    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1282    pub invariant_handler_failures: Vec<InvariantFailure>,
1283
1284    /// Minimal reproduction test case for failing test
1285    pub counterexample: Option<CounterExample>,
1286
1287    /// Legacy durable replay artifact for the top-level counterexample, when one was written.
1288    ///
1289    /// Prefer [`Self::counterexample_artifacts`] for new consumers; this compatibility field is
1290    /// maintained by [`Self::add_counterexample_artifact`] for older JSON readers.
1291    #[serde(default, skip_serializing_if = "Option::is_none")]
1292    pub counterexample_artifact: Option<SymbolicArtifactRef>,
1293
1294    /// All durable replay artifacts produced for this test result, normalized for JSON consumers.
1295    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1296    pub counterexample_artifacts: Vec<SymbolicArtifactRef>,
1297
1298    /// Generated Solidity regression tests for this symbolic counterexample.
1299    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1300    pub symbolic_regressions: Vec<SymbolicRegressionRef>,
1301
1302    /// Any captured & parsed as strings logs along the test's execution which should
1303    /// be printed to the user.
1304    pub logs: Vec<Log>,
1305
1306    /// The decoded DSTest logging events and Hardhat's `console.log` from [logs](Self::logs).
1307    /// Used for json output.
1308    pub decoded_logs: Vec<String>,
1309
1310    /// What kind of test this was
1311    pub kind: TestKind,
1312
1313    /// Stable symbolic result object for `forge test --symbolic --json`.
1314    #[serde(default, skip_serializing_if = "Option::is_none")]
1315    pub symbolic: Option<SymbolicResult>,
1316
1317    /// Traces
1318    pub traces: Traces,
1319
1320    /// Runtime bytecodes for contracts seen in debug traces.
1321    #[serde(skip)]
1322    pub debug_bytecodes: AddressHashMap<Bytes>,
1323
1324    /// Additional traces to use for gas report.
1325    ///
1326    /// These are cleared after the gas report is analyzed.
1327    #[serde(skip)]
1328    pub gas_report_traces: Vec<Vec<CallTraceArena>>,
1329
1330    /// Raw line coverage info
1331    #[serde(skip)]
1332    pub line_coverage: Option<HitMaps>,
1333
1334    /// Labeled addresses
1335    #[serde(rename = "labeled_addresses")] // Backwards compatibility.
1336    pub labels: AddressHashMap<String>,
1337
1338    #[serde(with = "foundry_common::serde_helpers::duration")]
1339    pub duration: Duration,
1340
1341    /// pc breakpoint char map
1342    pub breakpoints: Breakpoints,
1343
1344    /// Any captured gas snapshots along the test's execution which should be accumulated.
1345    pub gas_snapshots: BTreeMap<String, BTreeMap<String, String>>,
1346
1347    /// Deprecated cheatcodes (mapped to their replacements, if any) used in current test.
1348    #[serde(skip)]
1349    pub deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>,
1350
1351    /// Staged solver portfolio diagnostics collected during symbolic execution.
1352    #[serde(skip)]
1353    pub symbolic_portfolio_diagnostics: Option<PortfolioDiagnostics>,
1354
1355    /// Verbose symbolic solver diagnostics deferred until test output rendering.
1356    #[serde(skip)]
1357    pub symbolic_diagnostics: Option<String>,
1358}
1359
1360impl fmt::Display for TestResult {
1361    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1362        f.write_str(&self.render(false, None))
1363    }
1364}
1365
1366/// Appends a `[label] (original: N, shrunk: M)` header followed by one line per call.
1367fn write_sequence(s: &mut String, label: &str, original: usize, sequence: &[BaseCounterExample]) {
1368    writeln!(s, "\n\t[{label}] (original: {original}, shrunk: {})", sequence.len()).unwrap();
1369    for ex in sequence {
1370        writeln!(s, "{ex}").unwrap();
1371    }
1372}
1373
1374/// Appends `[FAIL: reason]{name_suffix}` plus the counterexample sequence, if any.
1375///
1376/// Returns `true` if a sequence (ending in a newline) was written.
1377fn write_failure(s: &mut String, failure: &InvariantFailure, name_suffix: &str) -> bool {
1378    write!(s, "[FAIL: {}]{name_suffix}", failure.reason()).unwrap();
1379    if let Some(CounterExample::Sequence(original, sequence)) = failure.counterexample() {
1380        write_sequence(s, "Sequence", *original, sequence);
1381        return true;
1382    }
1383    false
1384}
1385
1386/// All durable replay artifacts referenced by a counterexample: its own artifact plus the
1387/// before/after artifacts of its minimization, if any.
1388fn replay_artifacts<'a>(
1389    artifact: Option<&'a SymbolicArtifactRef>,
1390    minimization: Option<&'a SymbolicCounterexampleMinimization>,
1391) -> impl Iterator<Item = &'a SymbolicArtifactRef> {
1392    artifact.into_iter().chain(minimization.into_iter().flat_map(|m| [&m.original, &m.minimized]))
1393}
1394
1395impl TestResult {
1396    /// Returns `true` if this failed result can be meaningfully inspected with
1397    /// `forge test --debug --match-test`.
1398    const fn is_debuggable_failure(&self) -> bool {
1399        self.status.is_failure()
1400            && !self.kind.is_invariant()
1401            && !self.kind.is_symbolic()
1402            && self.symbolic.is_none()
1403    }
1404
1405    /// Adds a durable replay artifact to the normalized list and legacy top-level field.
1406    pub fn add_counterexample_artifact(&mut self, artifact: SymbolicArtifactRef) {
1407        if !self.counterexample_artifacts.contains(&artifact) {
1408            self.counterexample_artifacts.push(artifact.clone());
1409        }
1410        if self.counterexample_artifact.is_none() {
1411            self.counterexample_artifact = Some(artifact);
1412        }
1413    }
1414
1415    /// Renders the status block, either for the console (`user_facing`) or for JUnit output.
1416    fn render(&self, user_facing: bool, campaign_name: Option<&str>) -> String {
1417        let header = if user_facing {
1418            campaign_name.unwrap_or(INVARIANT_CAMPAIGN_FALLBACK_NAME)
1419        } else {
1420            "Predicates"
1421        };
1422        let mut s = String::new();
1423        match self.status {
1424            TestStatus::Success => {
1425                s.push_str("[PASS]");
1426                // For optimization mode, show the best example sequence in green.
1427                if let Some(CounterExample::Sequence(original, sequence)) = &self.counterexample {
1428                    write_sequence(&mut s, "Best sequence", *original, sequence);
1429                }
1430                self.write_predicates(&mut s, header, true);
1431                s.green().wrap().to_string()
1432            }
1433            TestStatus::Skipped => {
1434                s.push_str("[SKIP");
1435                if let Some(reason) = &self.reason {
1436                    write!(s, ": {reason}").unwrap();
1437                }
1438                s.push(']');
1439                self.write_predicates(&mut s, header, true);
1440                s.yellow().to_string()
1441            }
1442            TestStatus::Failure => {
1443                let is_invariant_failure = !self.invariant_failures.is_empty()
1444                    || !self.invariant_handler_failures.is_empty();
1445                if is_invariant_failure {
1446                    // Contract-level campaigns identify the broken predicate even when only one
1447                    // predicate failed. Preserve the compact legacy shape only for the anchor of a
1448                    // single-predicate run.
1449                    let named = self.invariant_count.is_some() || self.invariant_failures.len() > 1;
1450                    for (i, failure) in self.invariant_failures.iter().enumerate() {
1451                        if i > 0 {
1452                            s.push('\n');
1453                        }
1454                        let is_anchor =
1455                            matches!(failure, InvariantFailure::Predicate { is_anchor: true, .. });
1456                        let suffix = if named || !is_anchor {
1457                            format!(" {}", failure.name())
1458                        } else {
1459                            String::new()
1460                        };
1461                        write_failure(&mut s, failure, &suffix);
1462                    }
1463                } else {
1464                    // Non-invariant failure (unit / fuzz / DS-style): render from the legacy
1465                    // `reason` / `counterexample` fields.
1466                    s.push_str("[FAIL");
1467                    if let Some(reason) = &self.reason {
1468                        write!(s, ": {reason}").unwrap();
1469                    }
1470                    match &self.counterexample {
1471                        Some(CounterExample::Single(ex)) => {
1472                            write!(s, "; counterexample: {ex}]").unwrap();
1473                        }
1474                        Some(CounterExample::Sequence(original, sequence)) => {
1475                            s.push(']');
1476                            write_sequence(&mut s, "Sequence", *original, sequence);
1477                        }
1478                        None => s.push(']'),
1479                    }
1480                }
1481
1482                let broken = self.invariant_failures.len();
1483                let rollup = match self.invariant_count {
1484                    Some(total) if total > 1 && is_invariant_failure => {
1485                        writeln!(s, "\n{header}: {broken}/{total} invariants broken").unwrap();
1486                        true
1487                    }
1488                    _ => false,
1489                };
1490                self.write_predicates(&mut s, header, !user_facing || !rollup);
1491                if broken > 1
1492                    && let Some(dir) = &self.invariant_failure_dir
1493                {
1494                    writeln!(
1495                        s,
1496                        "{broken} invariant failure(s) persisted to {} — rerun to shrink",
1497                        dir.display()
1498                    )
1499                    .unwrap();
1500                }
1501
1502                if !self.invariant_handler_failures.is_empty() {
1503                    // Separate the section from anything rendered above it.
1504                    let preceded = rollup
1505                        || broken > 0
1506                        || (user_facing && self.invariant_predicate_results.len() > 1);
1507                    writeln!(
1508                        s,
1509                        "{}{}: {} assertion bug(s) found",
1510                        if preceded { "\n" } else { "" },
1511                        if user_facing { "Assertion Tests" } else { "Handler assertions" },
1512                        self.invariant_handler_failures.len()
1513                    )
1514                    .unwrap();
1515                    for failure in &self.invariant_handler_failures {
1516                        if !write_failure(&mut s, failure, &format!(" {}", failure.name())) {
1517                            s.push('\n');
1518                        }
1519                    }
1520                }
1521
1522                s.red().wrap().to_string()
1523            }
1524        }
1525    }
1526
1527    /// Appends the per-predicate summary for multi-predicate campaigns.
1528    fn write_predicates(&self, s: &mut String, header: &str, show_header: bool) {
1529        if self.invariant_predicate_results.len() <= 1 {
1530            return;
1531        }
1532        if show_header {
1533            write!(s, "\n{header}:\n").unwrap();
1534        }
1535        for predicate in &self.invariant_predicate_results {
1536            let name = &predicate.name;
1537            match (predicate.status, &predicate.reason) {
1538                (TestStatus::Success, _) => writeln!(s, "[PASS] {name}"),
1539                (TestStatus::Failure, reason) => {
1540                    writeln!(s, "[FAIL: {}] {name}", reason.as_deref().unwrap_or_default())
1541                }
1542                (TestStatus::Skipped, Some(reason)) => writeln!(s, "[SKIP: {reason}] {name}"),
1543                (TestStatus::Skipped, None) => writeln!(s, "[SKIP] {name}"),
1544            }
1545            .unwrap();
1546        }
1547    }
1548}
1549
1550macro_rules! extend {
1551    ($a:expr, $b:expr, $trace_kind:expr) => {
1552        if $b.fork_block_number.is_some() {
1553            $a.fork_block_number = $b.fork_block_number;
1554        }
1555        $a.logs.extend($b.logs);
1556        $a.labels.extend($b.labels);
1557        $a.traces.extend($b.traces.map(|traces| ($trace_kind, traces)));
1558        $a.debug_bytecodes.extend($b.debug_bytecodes);
1559        $a.merge_coverages($b.line_coverage);
1560    };
1561}
1562
1563/// Forge-side outcome of an invariant campaign, recorded into a [`TestResult`].
1564#[derive(Default)]
1565pub struct InvariantOutcome {
1566    /// Whether every checked invariant held.
1567    pub success: bool,
1568    /// Fork block number the campaign ran against, if any.
1569    pub fork_block_number: Option<u64>,
1570    /// Broken invariants, each with its shrunk call sequence.
1571    pub failures: Vec<InvariantFailure>,
1572    /// Handler assertion failures found while running the campaign.
1573    pub handler_failures: Vec<InvariantFailure>,
1574    /// Per-invariant pass/fail/skip rows.
1575    pub predicate_results: Vec<InvariantPredicateResult>,
1576    /// Directory the failing sequences were persisted to.
1577    pub failure_dir: Option<PathBuf>,
1578    /// Number of invariants checked, when the campaign ran more than one.
1579    pub invariant_count: Option<usize>,
1580    /// Best sequence found in optimization mode.
1581    pub counterexample: Option<CounterExample>,
1582    /// Traces collected for the gas report.
1583    pub gas_report_traces: Vec<Vec<CallTraceArena>>,
1584}
1585
1586/// Invariant kind for results that did not run a real campaign (setup failures, replays, skips).
1587pub(crate) fn invariant_kind(runs: usize, calls: usize, reverts: usize) -> TestKind {
1588    TestKind::Invariant {
1589        runs,
1590        calls,
1591        reverts,
1592        workers: 1,
1593        metrics: Default::default(),
1594        failed_corpus_replays: 0,
1595        optimization_best_value: None,
1596    }
1597}
1598
1599impl TestResult {
1600    /// Creates a new test result starting from test setup results.
1601    pub fn new(setup: &TestSetup) -> Self {
1602        Self {
1603            labels: setup.labels.clone(),
1604            logs: setup.logs.clone(),
1605            traces: setup.traces.clone(),
1606            debug_bytecodes: setup.debug_bytecodes.clone(),
1607            line_coverage: setup.coverage.clone(),
1608            fork_block_number: setup.fork_block_number,
1609            ..Default::default()
1610        }
1611    }
1612
1613    /// Creates a failed test result with given reason.
1614    pub fn fail(reason: String) -> Self {
1615        Self { status: TestStatus::Failure, reason: Some(reason), ..Default::default() }
1616    }
1617
1618    /// Creates a test setup result.
1619    pub fn setup_result(setup: TestSetup) -> Self {
1620        Self {
1621            status: if setup.skipped { TestStatus::Skipped } else { TestStatus::Failure },
1622            reason: setup.reason,
1623            logs: setup.logs,
1624            traces: setup.traces,
1625            debug_bytecodes: setup.debug_bytecodes,
1626            line_coverage: setup.coverage,
1627            labels: setup.labels,
1628            fork_block_number: setup.fork_block_number,
1629            ..Default::default()
1630        }
1631    }
1632
1633    /// Returns the skipped result for single test (used in skipped fuzz test too).
1634    pub fn single_skip(&mut self, reason: SkipReason) {
1635        self.status = TestStatus::Skipped;
1636        self.reason = reason.0;
1637    }
1638
1639    /// Returns the failed result with reason for single test.
1640    pub fn single_fail(&mut self, reason: Option<String>) {
1641        self.status = TestStatus::Failure;
1642        self.reason = reason;
1643    }
1644
1645    /// Returns the result for single test. Merges execution results (logs, labeled addresses,
1646    /// traces and coverages) in initial setup results.
1647    pub fn single_result<FEN: FoundryEvmNetwork>(
1648        &mut self,
1649        success: bool,
1650        reason: Option<String>,
1651        raw_call_result: RawCallResult<FEN>,
1652    ) {
1653        self.kind = TestKind::Unit {
1654            gas: raw_call_result.gas_used.saturating_sub(raw_call_result.stipend),
1655        };
1656
1657        extend!(self, raw_call_result, TraceKind::Execution);
1658
1659        self.status = if success { TestStatus::Success } else { TestStatus::Failure };
1660        self.reason = reason;
1661        self.duration = Duration::default();
1662        self.gas_report_traces = Vec::new();
1663
1664        if let Some(cheatcodes) = raw_call_result.cheatcodes {
1665            self.breakpoints = cheatcodes.breakpoints;
1666            self.gas_snapshots = cheatcodes.gas_snapshots;
1667            self.deprecated_cheatcodes = cheatcodes.deprecated;
1668        }
1669    }
1670
1671    /// Returns the result for a fuzzed test. Merges fuzz execution results (logs, labeled
1672    /// addresses, traces and coverages) in initial setup results.
1673    pub fn fuzz_result(&mut self, mut result: FuzzTestResult) {
1674        let kind = TestKind::Fuzz {
1675            median_gas: result.median_gas(false),
1676            mean_gas: result.mean_gas(false),
1677            first_case: std::mem::take(&mut result.first_case),
1678            runs: result.gas_by_case.len(),
1679            failed_corpus_replays: result.failed_corpus_replays,
1680        };
1681        self.campaign_result(kind, result);
1682    }
1683
1684    /// Returns the result for a table test. Merges table test execution results (logs, labeled
1685    /// addresses, traces and coverages) in initial setup results.
1686    pub fn table_result(&mut self, result: FuzzTestResult) {
1687        let kind = TestKind::Table {
1688            median_gas: result.median_gas(false),
1689            mean_gas: result.mean_gas(false),
1690            runs: result.gas_by_case.len(),
1691        };
1692        self.campaign_result(kind, result);
1693    }
1694
1695    fn campaign_result(&mut self, kind: TestKind, result: FuzzTestResult) {
1696        self.kind = kind;
1697
1698        extend!(self, result, TraceKind::Execution);
1699
1700        self.status = if result.skipped {
1701            TestStatus::Skipped
1702        } else if result.success {
1703            TestStatus::Success
1704        } else {
1705            TestStatus::Failure
1706        };
1707        self.reason = result.reason;
1708        self.counterexample = result.counterexample;
1709        self.duration = Duration::default();
1710        self.gas_report_traces = result.gas_report_traces.into_iter().map(|t| vec![t]).collect();
1711        self.breakpoints = result.breakpoints.unwrap_or_default();
1712        self.deprecated_cheatcodes = result.deprecated_cheatcodes;
1713    }
1714
1715    /// Returns the fail result for fuzz test setup.
1716    pub fn fuzz_setup_fail(&mut self, e: Report) {
1717        self.kind = TestKind::Fuzz {
1718            first_case: Default::default(),
1719            runs: 0,
1720            mean_gas: 0,
1721            median_gas: 0,
1722            failed_corpus_replays: 0,
1723        };
1724        self.status = TestStatus::Failure;
1725        debug!(?e, "failed to set up fuzz testing environment");
1726        self.reason = Some(format!("failed to set up fuzz testing environment: {e}"));
1727    }
1728
1729    /// Returns the skipped result for invariant campaign with per-predicate outcomes.
1730    pub fn invariant_skip_with_predicates(
1731        &mut self,
1732        reason: SkipReason,
1733        invariant_predicate_results: Vec<InvariantPredicateResult>,
1734    ) {
1735        self.kind = invariant_kind(1, 1, 1);
1736        self.status = TestStatus::Skipped;
1737        let predicate_count = invariant_predicate_results.len();
1738        let is_campaign = predicate_count > 1;
1739        self.reason = if is_campaign { None } else { reason.0 };
1740        self.invariant_count = is_campaign.then_some(predicate_count);
1741        self.invariant_predicate_results = invariant_predicate_results;
1742    }
1743
1744    /// Returns the fail result for replayed invariant test.
1745    pub fn invariant_replay_fail(
1746        &mut self,
1747        outcome: CheckSequenceOutcome,
1748        invariant_name: &str,
1749        fallback_reason: Option<String>,
1750        call_sequence: Vec<BaseCounterExample>,
1751    ) {
1752        self.kind = invariant_kind(1, outcome.calls_count, outcome.reverts);
1753        self.status = TestStatus::Failure;
1754        self.reason = Some(outcome.reason.or(fallback_reason).unwrap_or_else(|| {
1755            let what = if outcome.replayed_entirely {
1756                "replay failure"
1757            } else {
1758                "persisted failure revert"
1759            };
1760            format!("{invariant_name} {what}")
1761        }));
1762        self.counterexample = Some(CounterExample::Sequence(call_sequence.len(), call_sequence));
1763    }
1764
1765    /// Returns the success result for a replayed invariant test.
1766    pub fn invariant_replay_success(&mut self, call_count: usize, reverts: usize) {
1767        self.kind = invariant_kind(1, call_count, reverts);
1768        self.status = TestStatus::Success;
1769        self.reason = None;
1770    }
1771
1772    /// Returns the fail result for invariant test setup.
1773    pub fn invariant_setup_fail(&mut self, e: Report) {
1774        self.kind = invariant_kind(0, 0, 0);
1775        self.status = TestStatus::Failure;
1776        self.reason = Some(format!("failed to set up invariant testing environment: {e}"));
1777    }
1778
1779    /// Returns the invariant test result.
1780    pub fn invariant_result(&mut self, kind: TestKind, outcome: InvariantOutcome) {
1781        // For optimization mode (Some value), always succeed. For check mode (None), use success.
1782        let optimizing =
1783            matches!(kind, TestKind::Invariant { optimization_best_value: Some(_), .. });
1784        self.kind = kind;
1785        self.status =
1786            if optimizing || outcome.success { TestStatus::Success } else { TestStatus::Failure };
1787        self.fork_block_number = outcome.fork_block_number;
1788        self.invariant_predicate_results = outcome.predicate_results;
1789        self.invariant_failure_dir = outcome.failure_dir;
1790        self.invariant_count = outcome.invariant_count;
1791        // `counterexample` is only used by the renderer for optimization mode (the "best
1792        // sequence" rendered on success). Invariant check-mode failures live entirely in
1793        // `invariant_failures`; `reason`/`counterexample` stay `None` for invariant tests.
1794        self.counterexample = outcome.counterexample;
1795        for artifact in outcome
1796            .failures
1797            .iter()
1798            .chain(&outcome.handler_failures)
1799            .flat_map(|failure| replay_artifacts(failure.artifact(), failure.minimization()))
1800        {
1801            self.add_counterexample_artifact(artifact.clone());
1802        }
1803        self.invariant_failures = outcome.failures;
1804        self.invariant_handler_failures = outcome.handler_failures;
1805        self.gas_report_traces = outcome.gas_report_traces;
1806    }
1807
1808    /// Returns the result for a symbolic test.
1809    pub fn symbolic_result(
1810        &mut self,
1811        status: TestStatus,
1812        reason: Option<String>,
1813        counterexample: Option<CounterExample>,
1814        symbolic: SymbolicResult,
1815    ) {
1816        self.kind = TestKind::Symbolic(symbolic.solver.stats);
1817        self.status = status;
1818        self.reason = reason;
1819        self.counterexample = counterexample;
1820        self.record_symbolic(symbolic);
1821        self.duration = Duration::default();
1822    }
1823
1824    /// Records symbolic execution metadata without changing the test status/kind.
1825    pub(crate) fn record_symbolic(&mut self, symbolic: SymbolicResult) {
1826        for artifact in replay_artifacts(symbolic.artifact.as_ref(), symbolic.minimization.as_ref())
1827        {
1828            self.add_counterexample_artifact(artifact.clone());
1829        }
1830        self.symbolic = Some(symbolic);
1831    }
1832
1833    /// Records a successful showmap replay result.
1834    pub fn replay_result(
1835        &mut self,
1836        corpus_entries: usize,
1837        showmap_files: usize,
1838        skipped_entries: usize,
1839        duration: Duration,
1840    ) {
1841        self.kind = TestKind::Replay { corpus_entries, showmap_files, skipped_entries };
1842        self.status = TestStatus::Success;
1843        self.duration = duration;
1844    }
1845
1846    /// Records a skipped showmap replay (e.g. unit test or no corpus available).
1847    pub fn replay_skip(&mut self, reason: impl Into<String>) {
1848        self.kind = TestKind::Replay { corpus_entries: 0, showmap_files: 0, skipped_entries: 0 };
1849        self.status = TestStatus::Skipped;
1850        self.reason = Some(reason.into());
1851        self.duration = Duration::default();
1852    }
1853
1854    /// Formats the test result into a string (for printing), naming invariant campaigns after
1855    /// the suite's contract.
1856    pub(crate) fn short_result_with_suite(&self, name: &str, suite_name: &str) -> String {
1857        let campaign = (self.kind.is_invariant() && self.invariant_count.is_some())
1858            .then(|| invariant_campaign_display_name(get_contract_name(suite_name)));
1859        let name = campaign.as_deref().unwrap_or(name);
1860        let status = self.render(true, campaign.as_deref());
1861        let block = match self.fork_block_number {
1862            Some(block) if self.status.is_failure() => format!(" (block: {block})"),
1863            _ => String::new(),
1864        };
1865        format!("{status} {name}{block} {}", self.kind.report())
1866    }
1867
1868    /// The number of logical tests this result stands for: skipped predicates of a campaign are
1869    /// counted individually.
1870    fn logical_count(&self) -> usize {
1871        let skipped = self.skipped_predicate_count();
1872        if skipped == 0 {
1873            1
1874        } else if self.status.is_skipped() && skipped == self.invariant_predicate_results.len() {
1875            skipped
1876        } else {
1877            1 + skipped
1878        }
1879    }
1880
1881    fn skipped_count(&self) -> usize {
1882        let skipped = self.skipped_predicate_count();
1883        if skipped == 0 && self.status.is_skipped() { 1 } else { skipped }
1884    }
1885
1886    fn skipped_predicate_count(&self) -> usize {
1887        self.invariant_predicate_results.iter().filter(|p| p.status.is_skipped()).count()
1888    }
1889
1890    /// Merges the given raw call result into `self`.
1891    pub fn extend<FEN: FoundryEvmNetwork>(&mut self, call_result: RawCallResult<FEN>) {
1892        extend!(self, call_result, TraceKind::Execution);
1893    }
1894
1895    /// Merges the given pre-test setup result into `self`.
1896    pub(crate) fn extend_setup<FEN: FoundryEvmNetwork>(&mut self, call_result: RawCallResult<FEN>) {
1897        extend!(self, call_result, TraceKind::Setup);
1898    }
1899
1900    /// Merges the given coverage result into `self`.
1901    pub fn merge_coverages(&mut self, other_coverage: Option<HitMaps>) {
1902        HitMaps::merge_opt(&mut self.line_coverage, other_coverage);
1903    }
1904}
1905
1906/// Data report by a test.
1907#[derive(Clone, Debug, PartialEq, Eq)]
1908pub enum TestKindReport {
1909    Unit {
1910        gas: u64,
1911    },
1912    Fuzz {
1913        runs: usize,
1914        mean_gas: u64,
1915        median_gas: u64,
1916        failed_corpus_replays: usize,
1917    },
1918    Invariant {
1919        runs: usize,
1920        calls: usize,
1921        reverts: usize,
1922        failed_corpus_replays: usize,
1923        /// For optimization mode (int256 return): the best value achieved. None = check mode.
1924        optimization_best_value: Option<I256>,
1925    },
1926    Table {
1927        runs: usize,
1928        mean_gas: u64,
1929        median_gas: u64,
1930    },
1931    Symbolic(SymbolicStats),
1932    /// Showmap corpus replay (no campaign performed).
1933    Replay {
1934        corpus_entries: usize,
1935        showmap_files: usize,
1936        skipped_entries: usize,
1937    },
1938}
1939
1940impl fmt::Display for TestKindReport {
1941    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1942        match self {
1943            Self::Unit { gas } => write!(f, "(gas: {gas})"),
1944            Self::Fuzz { runs, mean_gas, median_gas, failed_corpus_replays } => {
1945                write!(f, "(runs: {runs}, μ: {mean_gas}, ~: {median_gas}")?;
1946                if *failed_corpus_replays != 0 {
1947                    write!(f, ", failed corpus replays: {failed_corpus_replays}")?;
1948                }
1949                f.write_str(")")
1950            }
1951            Self::Invariant {
1952                runs,
1953                calls,
1954                reverts,
1955                failed_corpus_replays,
1956                optimization_best_value,
1957            } => {
1958                if let Some(best_value) = optimization_best_value {
1959                    return write!(f, "(best: {best_value}, runs: {runs}, calls: {calls})");
1960                }
1961                write!(f, "(runs: {runs}, calls: {calls}, reverts: {reverts}")?;
1962                if *failed_corpus_replays != 0 {
1963                    write!(f, ", failed corpus replays: {failed_corpus_replays}")?;
1964                }
1965                f.write_str(")")
1966            }
1967            Self::Table { runs, mean_gas, median_gas } => {
1968                write!(f, "(runs: {runs}, μ: {mean_gas}, ~: {median_gas})")
1969            }
1970            Self::Symbolic(SymbolicStats {
1971                paths,
1972                solver_queries,
1973                smt_queries,
1974                sat_queries,
1975                model_queries,
1976                sat_cache_hits,
1977                model_cache_hits,
1978                heuristic_witnesses,
1979                solver_time_ms,
1980                ..
1981            }) => {
1982                write!(
1983                    f,
1984                    "(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)"
1985                )
1986            }
1987            Self::Replay { corpus_entries, showmap_files, skipped_entries } => {
1988                write!(f, "(replay: {corpus_entries} entries, {showmap_files} files")?;
1989                if *skipped_entries != 0 {
1990                    write!(f, ", {skipped_entries} skipped")?;
1991                }
1992                f.write_str(")")
1993            }
1994        }
1995    }
1996}
1997
1998impl TestKindReport {
1999    /// Returns the main gas value to compare against
2000    pub const fn gas(&self) -> u64 {
2001        match *self {
2002            Self::Unit { gas } => gas,
2003            // We use the median for comparisons
2004            Self::Fuzz { median_gas, .. } | Self::Table { median_gas, .. } => median_gas,
2005            // We return 0 since it's not applicable
2006            Self::Invariant { .. } | Self::Symbolic { .. } | Self::Replay { .. } => 0,
2007        }
2008    }
2009}
2010
2011/// Various types of tests
2012#[derive(Clone, Debug, Serialize, Deserialize)]
2013pub enum TestKind {
2014    /// A unit test.
2015    Unit { gas: u64 },
2016    /// A fuzz test.
2017    Fuzz {
2018        /// we keep this for the debugger
2019        first_case: FuzzCase,
2020        runs: usize,
2021        mean_gas: u64,
2022        median_gas: u64,
2023        failed_corpus_replays: usize,
2024    },
2025    /// An invariant test.
2026    Invariant {
2027        runs: usize,
2028        calls: usize,
2029        reverts: usize,
2030        /// Actual worker count used by this invariant campaign.
2031        #[serde(default = "default_invariant_workers")]
2032        workers: usize,
2033        metrics: Map<String, InvariantMetrics>,
2034        failed_corpus_replays: usize,
2035        /// For optimization mode (int256 return): the best value achieved. None = check mode.
2036        optimization_best_value: Option<I256>,
2037    },
2038    /// A table test.
2039    Table { runs: usize, mean_gas: u64, median_gas: u64 },
2040    /// A symbolic test.
2041    Symbolic(SymbolicStats),
2042    /// Showmap corpus replay (no campaign performed).
2043    Replay { corpus_entries: usize, showmap_files: usize, skipped_entries: usize },
2044}
2045
2046impl Default for TestKind {
2047    fn default() -> Self {
2048        Self::Unit { gas: 0 }
2049    }
2050}
2051
2052impl TestKind {
2053    /// Returns `true` if this is a fuzz test.
2054    pub const fn is_fuzz(&self) -> bool {
2055        matches!(self, Self::Fuzz { .. })
2056    }
2057
2058    /// Returns `true` if this is an invariant test.
2059    pub const fn is_invariant(&self) -> bool {
2060        matches!(self, Self::Invariant { .. })
2061    }
2062
2063    /// Returns `true` if this is a symbolic test.
2064    pub const fn is_symbolic(&self) -> bool {
2065        matches!(self, Self::Symbolic { .. })
2066    }
2067
2068    /// Actual invariant campaign worker count, if this is an invariant test.
2069    pub const fn invariant_workers(&self) -> Option<usize> {
2070        match self {
2071            Self::Invariant { workers, .. } => Some(*workers),
2072            _ => None,
2073        }
2074    }
2075
2076    /// The gas consumed by this test
2077    pub const fn report(&self) -> TestKindReport {
2078        match *self {
2079            Self::Unit { gas } => TestKindReport::Unit { gas },
2080            Self::Fuzz { runs, mean_gas, median_gas, failed_corpus_replays, .. } => {
2081                TestKindReport::Fuzz { runs, mean_gas, median_gas, failed_corpus_replays }
2082            }
2083            Self::Invariant {
2084                runs,
2085                calls,
2086                reverts,
2087                failed_corpus_replays,
2088                optimization_best_value,
2089                ..
2090            } => TestKindReport::Invariant {
2091                runs,
2092                calls,
2093                reverts,
2094                failed_corpus_replays,
2095                optimization_best_value,
2096            },
2097            Self::Table { runs, mean_gas, median_gas } => {
2098                TestKindReport::Table { runs, mean_gas, median_gas }
2099            }
2100            Self::Symbolic(stats) => TestKindReport::Symbolic(stats),
2101            Self::Replay { corpus_entries, showmap_files, skipped_entries } => {
2102                TestKindReport::Replay { corpus_entries, showmap_files, skipped_entries }
2103            }
2104        }
2105    }
2106}
2107
2108const fn default_invariant_workers() -> usize {
2109    1
2110}
2111
2112/// The result of a test setup.
2113///
2114/// Includes the deployment of the required libraries and the test contract itself, and the call to
2115/// the `setUp()` function.
2116#[derive(Clone, Debug, Default)]
2117pub struct TestSetup {
2118    /// The address at which the test contract was deployed.
2119    pub address: Address,
2120    /// Defined fuzz test fixtures.
2121    pub fuzz_fixtures: FuzzFixtures,
2122
2123    /// The logs emitted during setup.
2124    pub logs: Vec<Log>,
2125    /// Addresses labeled during setup.
2126    pub labels: AddressHashMap<String>,
2127    /// Call traces of the setup.
2128    pub traces: Traces,
2129    /// Runtime bytecodes for contracts seen in setup traces.
2130    pub debug_bytecodes: AddressHashMap<Bytes>,
2131    /// Coverage info during setup.
2132    pub coverage: Option<HitMaps>,
2133    /// Addresses of external libraries deployed during setup.
2134    pub deployed_libs: Vec<Address>,
2135    /// The active fork's block number after setup, if any.
2136    pub fork_block_number: Option<u64>,
2137    /// Cached setup-derived fuzz dictionary for stateless fuzz tests.
2138    pub(crate) fuzz_state: OnceLock<EvmFuzzState>,
2139
2140    /// The reason the setup failed, if it did.
2141    pub reason: Option<String>,
2142    /// Whether setup and entire test suite is skipped.
2143    pub skipped: bool,
2144    /// Whether the test failed to deploy.
2145    pub deployment_failure: bool,
2146}
2147
2148impl TestSetup {
2149    pub fn failed(reason: String) -> Self {
2150        Self { reason: Some(reason), ..Default::default() }
2151    }
2152
2153    pub fn skipped(reason: String) -> Self {
2154        Self { reason: Some(reason), skipped: true, ..Default::default() }
2155    }
2156
2157    pub fn extend<FEN: FoundryEvmNetwork>(
2158        &mut self,
2159        raw: RawCallResult<FEN>,
2160        trace_kind: TraceKind,
2161    ) {
2162        extend!(self, raw, trace_kind);
2163    }
2164
2165    pub fn merge_coverages(&mut self, other_coverage: Option<HitMaps>) {
2166        HitMaps::merge_opt(&mut self.coverage, other_coverage);
2167    }
2168}
2169
2170pub(crate) fn invariant_campaign_display_name(contract_name: &str) -> String {
2171    format!("{contract_name} invariants")
2172}
2173
2174const fn symbolic_result_schema_version() -> u32 {
2175    SYMBOLIC_RESULT_SCHEMA_VERSION
2176}
2177
2178#[cfg(test)]
2179mod tests {
2180    use super::*;
2181
2182    const SYMBOLIC_RESULT_SCHEMA: &str =
2183        include_str!("../../evm/symbolic/assets/symbolic-result.schema.json");
2184    const SYMBOLIC_COUNTEREXAMPLE_SCHEMA: &str =
2185        include_str!("../../evm/symbolic/assets/symbolic-counterexample.schema.json");
2186
2187    fn schema_defs(schema: &serde_json::Value) -> &serde_json::Map<String, serde_json::Value> {
2188        schema["$defs"].as_object().expect("schema $defs object")
2189    }
2190
2191    /// Collects every `$ref` target in `value`.
2192    fn collect_refs<'a>(value: &'a serde_json::Value, refs: &mut Vec<&'a str>) {
2193        match value {
2194            serde_json::Value::Object(map) => {
2195                refs.extend(map.get("$ref").and_then(serde_json::Value::as_str));
2196                for child in map.values() {
2197                    collect_refs(child, refs);
2198                }
2199            }
2200            serde_json::Value::Array(values) => {
2201                for child in values {
2202                    collect_refs(child, refs);
2203                }
2204            }
2205            _ => {}
2206        }
2207    }
2208
2209    #[test]
2210    fn symbolic_schemas_match_result_types() {
2211        let result_schema: serde_json::Value =
2212            serde_json::from_str(SYMBOLIC_RESULT_SCHEMA).unwrap();
2213        let counterexample_schema: serde_json::Value =
2214            serde_json::from_str(SYMBOLIC_COUNTEREXAMPLE_SCHEMA).unwrap();
2215        let result_defs = schema_defs(&result_schema);
2216        let counterexample_defs = schema_defs(&counterexample_schema);
2217
2218        // Every counterexample `$ref` must resolve offline, either locally or into the result
2219        // schema.
2220        let mut refs = Vec::new();
2221        collect_refs(&counterexample_schema, &mut refs);
2222        for reference in refs {
2223            let resolved = if let Some(name) = reference.strip_prefix(
2224                "https://foundry-rs.github.io/schemas/symbolic-result.v1.schema.json#/$defs/",
2225            ) {
2226                result_defs.contains_key(name)
2227            } else if let Some(name) = reference.strip_prefix("#/$defs/") {
2228                counterexample_defs.contains_key(name)
2229            } else {
2230                false
2231            };
2232            assert!(resolved, "unresolved schema ref {reference}");
2233        }
2234
2235        // The solver stats schema must list exactly the serialized `SymbolicStats` fields.
2236        let stats = serde_json::to_value(SymbolicStats::default()).unwrap();
2237        let mut expected = stats.as_object().unwrap().keys().collect::<Vec<_>>();
2238        let mut actual = result_defs["solver_stats"]["properties"]
2239            .as_object()
2240            .unwrap()
2241            .keys()
2242            .collect::<Vec<_>>();
2243        expected.sort();
2244        actual.sort();
2245        assert_eq!(actual, expected);
2246    }
2247
2248    fn outcome_with_results(test_results: Vec<TestResult>) -> TestOutcome {
2249        let test_results = test_results
2250            .into_iter()
2251            .enumerate()
2252            .map(|(idx, result)| (format!("test{idx}()"), result))
2253            .collect();
2254        let suite = SuiteResult::new(Duration::ZERO, test_results, Vec::new());
2255        TestOutcome::new(None, BTreeMap::from([("suite".to_string(), suite)]), false, None)
2256    }
2257
2258    fn failed_result(kind: TestKind) -> TestResult {
2259        TestResult { status: TestStatus::Failure, kind, ..Default::default() }
2260    }
2261
2262    fn failed_invariant(workers: usize) -> TestResult {
2263        let mut kind = invariant_kind(0, 0, 0);
2264        if let TestKind::Invariant { workers: w, .. } = &mut kind {
2265            *w = workers;
2266        }
2267        failed_result(kind)
2268    }
2269
2270    #[test]
2271    fn failed_tests_are_debuggable_only_for_concrete_failures() {
2272        let unit = failed_result(TestKind::Unit { gas: 0 });
2273        assert!(outcome_with_results(vec![unit.clone()]).failed_tests_are_debuggable());
2274        assert!(!outcome_with_results(vec![failed_invariant(1)]).failed_tests_are_debuggable());
2275        assert!(
2276            !outcome_with_results(vec![failed_result(
2277                TestKind::Symbolic(SymbolicStats::default())
2278            )])
2279            .failed_tests_are_debuggable()
2280        );
2281
2282        let mut symbolic_backed = unit;
2283        symbolic_backed.symbolic =
2284            Some(SymbolicResult::pass(&SymbolicConfig::default(), SymbolicStats::default()));
2285        assert!(!outcome_with_results(vec![symbolic_backed]).failed_tests_are_debuggable());
2286    }
2287
2288    #[test]
2289    fn invariant_workers_hint_requires_matching_parallel_worker_counts() {
2290        let hint = |workers: &[usize]| {
2291            outcome_with_results(workers.iter().map(|&w| failed_invariant(w)).collect())
2292                .invariant_workers_hint()
2293        };
2294        assert_eq!(hint(&[3, 3]), Some(3));
2295        assert_eq!(hint(&[2, 3]), None);
2296        assert_eq!(hint(&[1]), None);
2297    }
2298
2299    #[test]
2300    fn invariant_kind_deserializes_legacy_payload_without_workers() {
2301        let kind = serde_json::from_value::<TestKind>(serde_json::json!({
2302            "Invariant": {
2303                "runs": 4,
2304                "calls": 10,
2305                "reverts": 0,
2306                "metrics": {},
2307                "failed_corpus_replays": 0,
2308                "optimization_best_value": null
2309            }
2310        }))
2311        .unwrap();
2312
2313        assert_eq!(kind.invariant_workers(), Some(1));
2314    }
2315}