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