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        }
1217    }
1218
1219    /// Converts an artifact call into Foundry's invariant replay transaction shape.
1220    pub fn to_basic_tx_details(&self) -> BasicTxDetails {
1221        BasicTxDetails {
1222            warp: self.warp,
1223            roll: self.roll,
1224            sender: self.sender,
1225            call_details: CallDetails {
1226                target: self.target,
1227                calldata: self.calldata.clone(),
1228                value: self.value,
1229            },
1230        }
1231    }
1232}
1233
1234/// The result of an executed test.
1235#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1236pub struct TestResult {
1237    /// The test status, indicating whether the test case succeeded, failed, or was marked as
1238    /// skipped. This means that the transaction executed properly, or the test was marked as
1239    /// skipped with vm.skip().
1240    pub status: TestStatus,
1241
1242    /// If there was a revert, this field will be populated.
1243    pub reason: Option<String>,
1244
1245    /// The active fork's block number after execution, if any.
1246    #[serde(default, skip_serializing_if = "Option::is_none")]
1247    pub fork_block_number: Option<u64>,
1248
1249    /// All broken invariant predicates in this campaign in source declaration order.
1250    ///
1251    /// For invariant tests, this is the single source of truth used by the renderer.
1252    /// `reason` and `counterexample` are not populated for invariant tests.
1253    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1254    pub invariant_failures: Vec<InvariantFailure>,
1255
1256    /// Per-predicate outcomes for invariant campaigns. This preserves individual
1257    /// `invariant_*` / `statefulFuzz*` pass/fail reporting when multiple predicates are checked
1258    /// by one contract-level campaign.
1259    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1260    pub invariant_predicate_results: Vec<InvariantPredicateResult>,
1261
1262    /// Directory where invariant failure counterexamples have been persisted (set when one or more
1263    /// secondary invariant failures were written, so users can locate persisted counterexamples).
1264    #[serde(default, skip_serializing_if = "Option::is_none")]
1265    pub invariant_failure_dir: Option<PathBuf>,
1266
1267    /// Total number of invariant predicates exercised in this campaign. When `Some(n)` the
1268    /// user-facing report renders a contract-level `<broken>/<n> invariants broken` summary so
1269    /// users get an at-a-glance health line without counting `[FAIL]` blocks. `None` for
1270    /// single-predicate campaigns.
1271    #[serde(default, skip_serializing_if = "Option::is_none")]
1272    pub invariant_count: Option<usize>,
1273
1274    /// Handler-side assertion bugs found during the campaign, deduped by
1275    /// `(reverter, selector)` site (Medusa/Echidna semantics). Rendered in a dedicated
1276    /// `Assertion Tests` section.
1277    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1278    pub invariant_handler_failures: Vec<InvariantFailure>,
1279
1280    /// Minimal reproduction test case for failing test
1281    pub counterexample: Option<CounterExample>,
1282
1283    /// Legacy durable replay artifact for the top-level counterexample, when one was written.
1284    ///
1285    /// Prefer [`Self::counterexample_artifacts`] for new consumers; this compatibility field is
1286    /// maintained by [`Self::add_counterexample_artifact`] for older JSON readers.
1287    #[serde(default, skip_serializing_if = "Option::is_none")]
1288    pub counterexample_artifact: Option<SymbolicArtifactRef>,
1289
1290    /// All durable replay artifacts produced for this test result, normalized for JSON consumers.
1291    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1292    pub counterexample_artifacts: Vec<SymbolicArtifactRef>,
1293
1294    /// Generated Solidity regression tests for this symbolic counterexample.
1295    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1296    pub symbolic_regressions: Vec<SymbolicRegressionRef>,
1297
1298    /// Any captured & parsed as strings logs along the test's execution which should
1299    /// be printed to the user.
1300    pub logs: Vec<Log>,
1301
1302    /// The decoded DSTest logging events and Hardhat's `console.log` from [logs](Self::logs).
1303    /// Used for json output.
1304    pub decoded_logs: Vec<String>,
1305
1306    /// What kind of test this was
1307    pub kind: TestKind,
1308
1309    /// Stable symbolic result object for `forge test --symbolic --json`.
1310    #[serde(default, skip_serializing_if = "Option::is_none")]
1311    pub symbolic: Option<SymbolicResult>,
1312
1313    /// Traces
1314    pub traces: Traces,
1315
1316    /// Runtime bytecodes for contracts seen in debug traces.
1317    #[serde(skip)]
1318    pub debug_bytecodes: AddressHashMap<Bytes>,
1319
1320    /// Additional traces to use for gas report.
1321    ///
1322    /// These are cleared after the gas report is analyzed.
1323    #[serde(skip)]
1324    pub gas_report_traces: Vec<Vec<CallTraceArena>>,
1325
1326    /// Raw line coverage info
1327    #[serde(skip)]
1328    pub line_coverage: Option<HitMaps>,
1329
1330    /// Labeled addresses
1331    #[serde(rename = "labeled_addresses")] // Backwards compatibility.
1332    pub labels: AddressHashMap<String>,
1333
1334    #[serde(with = "foundry_common::serde_helpers::duration")]
1335    pub duration: Duration,
1336
1337    /// pc breakpoint char map
1338    pub breakpoints: Breakpoints,
1339
1340    /// Any captured gas snapshots along the test's execution which should be accumulated.
1341    pub gas_snapshots: BTreeMap<String, BTreeMap<String, String>>,
1342
1343    /// Deprecated cheatcodes (mapped to their replacements, if any) used in current test.
1344    #[serde(skip)]
1345    pub deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>,
1346}
1347
1348impl fmt::Display for TestResult {
1349    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1350        f.write_str(&self.render(false, None))
1351    }
1352}
1353
1354/// Appends a `[label] (original: N, shrunk: M)` header followed by one line per call.
1355fn write_sequence(s: &mut String, label: &str, original: usize, sequence: &[BaseCounterExample]) {
1356    writeln!(s, "\n\t[{label}] (original: {original}, shrunk: {})", sequence.len()).unwrap();
1357    for ex in sequence {
1358        writeln!(s, "{ex}").unwrap();
1359    }
1360}
1361
1362/// Appends `[FAIL: reason]{name_suffix}` plus the counterexample sequence, if any.
1363///
1364/// Returns `true` if a sequence (ending in a newline) was written.
1365fn write_failure(s: &mut String, failure: &InvariantFailure, name_suffix: &str) -> bool {
1366    write!(s, "[FAIL: {}]{name_suffix}", failure.reason()).unwrap();
1367    if let Some(CounterExample::Sequence(original, sequence)) = failure.counterexample() {
1368        write_sequence(s, "Sequence", *original, sequence);
1369        return true;
1370    }
1371    false
1372}
1373
1374/// All durable replay artifacts referenced by a counterexample: its own artifact plus the
1375/// before/after artifacts of its minimization, if any.
1376fn replay_artifacts<'a>(
1377    artifact: Option<&'a SymbolicArtifactRef>,
1378    minimization: Option<&'a SymbolicCounterexampleMinimization>,
1379) -> impl Iterator<Item = &'a SymbolicArtifactRef> {
1380    artifact.into_iter().chain(minimization.into_iter().flat_map(|m| [&m.original, &m.minimized]))
1381}
1382
1383impl TestResult {
1384    /// Returns `true` if this failed result can be meaningfully inspected with
1385    /// `forge test --debug --match-test`.
1386    const fn is_debuggable_failure(&self) -> bool {
1387        self.status.is_failure()
1388            && !self.kind.is_invariant()
1389            && !self.kind.is_symbolic()
1390            && self.symbolic.is_none()
1391    }
1392
1393    /// Adds a durable replay artifact to the normalized list and legacy top-level field.
1394    pub fn add_counterexample_artifact(&mut self, artifact: SymbolicArtifactRef) {
1395        if !self.counterexample_artifacts.contains(&artifact) {
1396            self.counterexample_artifacts.push(artifact.clone());
1397        }
1398        if self.counterexample_artifact.is_none() {
1399            self.counterexample_artifact = Some(artifact);
1400        }
1401    }
1402
1403    /// Renders the status block, either for the console (`user_facing`) or for JUnit output.
1404    fn render(&self, user_facing: bool, campaign_name: Option<&str>) -> String {
1405        let header = if user_facing {
1406            campaign_name.unwrap_or(INVARIANT_CAMPAIGN_FALLBACK_NAME)
1407        } else {
1408            "Predicates"
1409        };
1410        let mut s = String::new();
1411        match self.status {
1412            TestStatus::Success => {
1413                s.push_str("[PASS]");
1414                // For optimization mode, show the best example sequence in green.
1415                if let Some(CounterExample::Sequence(original, sequence)) = &self.counterexample {
1416                    write_sequence(&mut s, "Best sequence", *original, sequence);
1417                }
1418                self.write_predicates(&mut s, header, true);
1419                s.green().wrap().to_string()
1420            }
1421            TestStatus::Skipped => {
1422                s.push_str("[SKIP");
1423                if let Some(reason) = &self.reason {
1424                    write!(s, ": {reason}").unwrap();
1425                }
1426                s.push(']');
1427                self.write_predicates(&mut s, header, true);
1428                s.yellow().to_string()
1429            }
1430            TestStatus::Failure => {
1431                let is_invariant_failure = !self.invariant_failures.is_empty()
1432                    || !self.invariant_handler_failures.is_empty();
1433                if is_invariant_failure {
1434                    // Contract-level campaigns identify the broken predicate even when only one
1435                    // predicate failed. Preserve the compact legacy shape only for the anchor of a
1436                    // single-predicate run.
1437                    let named = self.invariant_count.is_some() || self.invariant_failures.len() > 1;
1438                    for (i, failure) in self.invariant_failures.iter().enumerate() {
1439                        if i > 0 {
1440                            s.push('\n');
1441                        }
1442                        let is_anchor =
1443                            matches!(failure, InvariantFailure::Predicate { is_anchor: true, .. });
1444                        let suffix = if named || !is_anchor {
1445                            format!(" {}", failure.name())
1446                        } else {
1447                            String::new()
1448                        };
1449                        write_failure(&mut s, failure, &suffix);
1450                    }
1451                } else {
1452                    // Non-invariant failure (unit / fuzz / DS-style): render from the legacy
1453                    // `reason` / `counterexample` fields.
1454                    s.push_str("[FAIL");
1455                    if let Some(reason) = &self.reason {
1456                        write!(s, ": {reason}").unwrap();
1457                    }
1458                    match &self.counterexample {
1459                        Some(CounterExample::Single(ex)) => {
1460                            write!(s, "; counterexample: {ex}]").unwrap();
1461                        }
1462                        Some(CounterExample::Sequence(original, sequence)) => {
1463                            s.push(']');
1464                            write_sequence(&mut s, "Sequence", *original, sequence);
1465                        }
1466                        None => s.push(']'),
1467                    }
1468                }
1469
1470                let broken = self.invariant_failures.len();
1471                let rollup = match self.invariant_count {
1472                    Some(total) if total > 1 && is_invariant_failure => {
1473                        writeln!(s, "\n{header}: {broken}/{total} invariants broken").unwrap();
1474                        true
1475                    }
1476                    _ => false,
1477                };
1478                self.write_predicates(&mut s, header, !user_facing || !rollup);
1479                if broken > 1
1480                    && let Some(dir) = &self.invariant_failure_dir
1481                {
1482                    writeln!(
1483                        s,
1484                        "{broken} invariant failure(s) persisted to {} — rerun to shrink",
1485                        dir.display()
1486                    )
1487                    .unwrap();
1488                }
1489
1490                if !self.invariant_handler_failures.is_empty() {
1491                    // Separate the section from anything rendered above it.
1492                    let preceded = rollup
1493                        || broken > 0
1494                        || (user_facing && self.invariant_predicate_results.len() > 1);
1495                    writeln!(
1496                        s,
1497                        "{}{}: {} assertion bug(s) found",
1498                        if preceded { "\n" } else { "" },
1499                        if user_facing { "Assertion Tests" } else { "Handler assertions" },
1500                        self.invariant_handler_failures.len()
1501                    )
1502                    .unwrap();
1503                    for failure in &self.invariant_handler_failures {
1504                        if !write_failure(&mut s, failure, &format!(" {}", failure.name())) {
1505                            s.push('\n');
1506                        }
1507                    }
1508                }
1509
1510                s.red().wrap().to_string()
1511            }
1512        }
1513    }
1514
1515    /// Appends the per-predicate summary for multi-predicate campaigns.
1516    fn write_predicates(&self, s: &mut String, header: &str, show_header: bool) {
1517        if self.invariant_predicate_results.len() <= 1 {
1518            return;
1519        }
1520        if show_header {
1521            write!(s, "\n{header}:\n").unwrap();
1522        }
1523        for predicate in &self.invariant_predicate_results {
1524            let name = &predicate.name;
1525            match (predicate.status, &predicate.reason) {
1526                (TestStatus::Success, _) => writeln!(s, "[PASS] {name}"),
1527                (TestStatus::Failure, reason) => {
1528                    writeln!(s, "[FAIL: {}] {name}", reason.as_deref().unwrap_or_default())
1529                }
1530                (TestStatus::Skipped, Some(reason)) => writeln!(s, "[SKIP: {reason}] {name}"),
1531                (TestStatus::Skipped, None) => writeln!(s, "[SKIP] {name}"),
1532            }
1533            .unwrap();
1534        }
1535    }
1536}
1537
1538macro_rules! extend {
1539    ($a:expr, $b:expr, $trace_kind:expr) => {
1540        if $b.fork_block_number.is_some() {
1541            $a.fork_block_number = $b.fork_block_number;
1542        }
1543        $a.logs.extend($b.logs);
1544        $a.labels.extend($b.labels);
1545        $a.traces.extend($b.traces.map(|traces| ($trace_kind, traces)));
1546        $a.debug_bytecodes.extend($b.debug_bytecodes);
1547        $a.merge_coverages($b.line_coverage);
1548    };
1549}
1550
1551/// Forge-side outcome of an invariant campaign, recorded into a [`TestResult`].
1552#[derive(Default)]
1553pub struct InvariantOutcome {
1554    /// Whether every checked invariant held.
1555    pub success: bool,
1556    /// Fork block number the campaign ran against, if any.
1557    pub fork_block_number: Option<u64>,
1558    /// Broken invariants, each with its shrunk call sequence.
1559    pub failures: Vec<InvariantFailure>,
1560    /// Handler assertion failures found while running the campaign.
1561    pub handler_failures: Vec<InvariantFailure>,
1562    /// Per-invariant pass/fail/skip rows.
1563    pub predicate_results: Vec<InvariantPredicateResult>,
1564    /// Directory the failing sequences were persisted to.
1565    pub failure_dir: Option<PathBuf>,
1566    /// Number of invariants checked, when the campaign ran more than one.
1567    pub invariant_count: Option<usize>,
1568    /// Best sequence found in optimization mode.
1569    pub counterexample: Option<CounterExample>,
1570    /// Traces collected for the gas report.
1571    pub gas_report_traces: Vec<Vec<CallTraceArena>>,
1572}
1573
1574/// Invariant kind for results that did not run a real campaign (setup failures, replays, skips).
1575pub(crate) fn invariant_kind(runs: usize, calls: usize, reverts: usize) -> TestKind {
1576    TestKind::Invariant {
1577        runs,
1578        calls,
1579        reverts,
1580        workers: 1,
1581        metrics: Default::default(),
1582        failed_corpus_replays: 0,
1583        optimization_best_value: None,
1584    }
1585}
1586
1587impl TestResult {
1588    /// Creates a new test result starting from test setup results.
1589    pub fn new(setup: &TestSetup) -> Self {
1590        Self {
1591            labels: setup.labels.clone(),
1592            logs: setup.logs.clone(),
1593            traces: setup.traces.clone(),
1594            debug_bytecodes: setup.debug_bytecodes.clone(),
1595            line_coverage: setup.coverage.clone(),
1596            fork_block_number: setup.fork_block_number,
1597            ..Default::default()
1598        }
1599    }
1600
1601    /// Creates a failed test result with given reason.
1602    pub fn fail(reason: String) -> Self {
1603        Self { status: TestStatus::Failure, reason: Some(reason), ..Default::default() }
1604    }
1605
1606    /// Creates a test setup result.
1607    pub fn setup_result(setup: TestSetup) -> Self {
1608        Self {
1609            status: if setup.skipped { TestStatus::Skipped } else { TestStatus::Failure },
1610            reason: setup.reason,
1611            logs: setup.logs,
1612            traces: setup.traces,
1613            debug_bytecodes: setup.debug_bytecodes,
1614            line_coverage: setup.coverage,
1615            labels: setup.labels,
1616            fork_block_number: setup.fork_block_number,
1617            ..Default::default()
1618        }
1619    }
1620
1621    /// Returns the skipped result for single test (used in skipped fuzz test too).
1622    pub fn single_skip(&mut self, reason: SkipReason) {
1623        self.status = TestStatus::Skipped;
1624        self.reason = reason.0;
1625    }
1626
1627    /// Returns the failed result with reason for single test.
1628    pub fn single_fail(&mut self, reason: Option<String>) {
1629        self.status = TestStatus::Failure;
1630        self.reason = reason;
1631    }
1632
1633    /// Returns the result for single test. Merges execution results (logs, labeled addresses,
1634    /// traces and coverages) in initial setup results.
1635    pub fn single_result<FEN: FoundryEvmNetwork>(
1636        &mut self,
1637        success: bool,
1638        reason: Option<String>,
1639        raw_call_result: RawCallResult<FEN>,
1640    ) {
1641        self.kind = TestKind::Unit {
1642            gas: raw_call_result.gas_used.saturating_sub(raw_call_result.stipend),
1643        };
1644
1645        extend!(self, raw_call_result, TraceKind::Execution);
1646
1647        self.status = if success { TestStatus::Success } else { TestStatus::Failure };
1648        self.reason = reason;
1649        self.duration = Duration::default();
1650        self.gas_report_traces = Vec::new();
1651
1652        if let Some(cheatcodes) = raw_call_result.cheatcodes {
1653            self.breakpoints = cheatcodes.breakpoints;
1654            self.gas_snapshots = cheatcodes.gas_snapshots;
1655            self.deprecated_cheatcodes = cheatcodes.deprecated;
1656        }
1657    }
1658
1659    /// Returns the result for a fuzzed test. Merges fuzz execution results (logs, labeled
1660    /// addresses, traces and coverages) in initial setup results.
1661    pub fn fuzz_result(&mut self, mut result: FuzzTestResult) {
1662        let kind = TestKind::Fuzz {
1663            median_gas: result.median_gas(false),
1664            mean_gas: result.mean_gas(false),
1665            first_case: std::mem::take(&mut result.first_case),
1666            runs: result.gas_by_case.len(),
1667            failed_corpus_replays: result.failed_corpus_replays,
1668        };
1669        self.campaign_result(kind, result);
1670    }
1671
1672    /// Returns the result for a table test. Merges table test execution results (logs, labeled
1673    /// addresses, traces and coverages) in initial setup results.
1674    pub fn table_result(&mut self, result: FuzzTestResult) {
1675        let kind = TestKind::Table {
1676            median_gas: result.median_gas(false),
1677            mean_gas: result.mean_gas(false),
1678            runs: result.gas_by_case.len(),
1679        };
1680        self.campaign_result(kind, result);
1681    }
1682
1683    fn campaign_result(&mut self, kind: TestKind, result: FuzzTestResult) {
1684        self.kind = kind;
1685
1686        extend!(self, result, TraceKind::Execution);
1687
1688        self.status = if result.skipped {
1689            TestStatus::Skipped
1690        } else if result.success {
1691            TestStatus::Success
1692        } else {
1693            TestStatus::Failure
1694        };
1695        self.reason = result.reason;
1696        self.counterexample = result.counterexample;
1697        self.duration = Duration::default();
1698        self.gas_report_traces = result.gas_report_traces.into_iter().map(|t| vec![t]).collect();
1699        self.breakpoints = result.breakpoints.unwrap_or_default();
1700        self.deprecated_cheatcodes = result.deprecated_cheatcodes;
1701    }
1702
1703    /// Returns the fail result for fuzz test setup.
1704    pub fn fuzz_setup_fail(&mut self, e: Report) {
1705        self.kind = TestKind::Fuzz {
1706            first_case: Default::default(),
1707            runs: 0,
1708            mean_gas: 0,
1709            median_gas: 0,
1710            failed_corpus_replays: 0,
1711        };
1712        self.status = TestStatus::Failure;
1713        debug!(?e, "failed to set up fuzz testing environment");
1714        self.reason = Some(format!("failed to set up fuzz testing environment: {e}"));
1715    }
1716
1717    /// Returns the skipped result for invariant campaign with per-predicate outcomes.
1718    pub fn invariant_skip_with_predicates(
1719        &mut self,
1720        reason: SkipReason,
1721        invariant_predicate_results: Vec<InvariantPredicateResult>,
1722    ) {
1723        self.kind = invariant_kind(1, 1, 1);
1724        self.status = TestStatus::Skipped;
1725        let predicate_count = invariant_predicate_results.len();
1726        let is_campaign = predicate_count > 1;
1727        self.reason = if is_campaign { None } else { reason.0 };
1728        self.invariant_count = is_campaign.then_some(predicate_count);
1729        self.invariant_predicate_results = invariant_predicate_results;
1730    }
1731
1732    /// Returns the fail result for replayed invariant test.
1733    pub fn invariant_replay_fail(
1734        &mut self,
1735        outcome: CheckSequenceOutcome,
1736        invariant_name: &str,
1737        fallback_reason: Option<String>,
1738        call_sequence: Vec<BaseCounterExample>,
1739    ) {
1740        self.kind = invariant_kind(1, outcome.calls_count, outcome.reverts);
1741        self.status = TestStatus::Failure;
1742        self.reason = Some(outcome.reason.or(fallback_reason).unwrap_or_else(|| {
1743            let what = if outcome.replayed_entirely {
1744                "replay failure"
1745            } else {
1746                "persisted failure revert"
1747            };
1748            format!("{invariant_name} {what}")
1749        }));
1750        self.counterexample = Some(CounterExample::Sequence(call_sequence.len(), call_sequence));
1751    }
1752
1753    /// Returns the success result for a replayed invariant test.
1754    pub fn invariant_replay_success(&mut self, call_count: usize, reverts: usize) {
1755        self.kind = invariant_kind(1, call_count, reverts);
1756        self.status = TestStatus::Success;
1757        self.reason = None;
1758    }
1759
1760    /// Returns the fail result for invariant test setup.
1761    pub fn invariant_setup_fail(&mut self, e: Report) {
1762        self.kind = invariant_kind(0, 0, 0);
1763        self.status = TestStatus::Failure;
1764        self.reason = Some(format!("failed to set up invariant testing environment: {e}"));
1765    }
1766
1767    /// Returns the invariant test result.
1768    pub fn invariant_result(&mut self, kind: TestKind, outcome: InvariantOutcome) {
1769        // For optimization mode (Some value), always succeed. For check mode (None), use success.
1770        let optimizing =
1771            matches!(kind, TestKind::Invariant { optimization_best_value: Some(_), .. });
1772        self.kind = kind;
1773        self.status =
1774            if optimizing || outcome.success { TestStatus::Success } else { TestStatus::Failure };
1775        self.fork_block_number = outcome.fork_block_number;
1776        self.invariant_predicate_results = outcome.predicate_results;
1777        self.invariant_failure_dir = outcome.failure_dir;
1778        self.invariant_count = outcome.invariant_count;
1779        // `counterexample` is only used by the renderer for optimization mode (the "best
1780        // sequence" rendered on success). Invariant check-mode failures live entirely in
1781        // `invariant_failures`; `reason`/`counterexample` stay `None` for invariant tests.
1782        self.counterexample = outcome.counterexample;
1783        for artifact in outcome
1784            .failures
1785            .iter()
1786            .chain(&outcome.handler_failures)
1787            .flat_map(|failure| replay_artifacts(failure.artifact(), failure.minimization()))
1788        {
1789            self.add_counterexample_artifact(artifact.clone());
1790        }
1791        self.invariant_failures = outcome.failures;
1792        self.invariant_handler_failures = outcome.handler_failures;
1793        self.gas_report_traces = outcome.gas_report_traces;
1794    }
1795
1796    /// Returns the result for a symbolic test.
1797    pub fn symbolic_result(
1798        &mut self,
1799        status: TestStatus,
1800        reason: Option<String>,
1801        counterexample: Option<CounterExample>,
1802        symbolic: SymbolicResult,
1803    ) {
1804        self.kind = TestKind::Symbolic(symbolic.solver.stats);
1805        self.status = status;
1806        self.reason = reason;
1807        self.counterexample = counterexample;
1808        self.record_symbolic(symbolic);
1809        self.duration = Duration::default();
1810    }
1811
1812    /// Records symbolic execution metadata without changing the test status/kind.
1813    pub(crate) fn record_symbolic(&mut self, symbolic: SymbolicResult) {
1814        for artifact in replay_artifacts(symbolic.artifact.as_ref(), symbolic.minimization.as_ref())
1815        {
1816            self.add_counterexample_artifact(artifact.clone());
1817        }
1818        self.symbolic = Some(symbolic);
1819    }
1820
1821    /// Records a successful showmap replay result.
1822    pub fn replay_result(
1823        &mut self,
1824        corpus_entries: usize,
1825        showmap_files: usize,
1826        skipped_entries: usize,
1827        duration: Duration,
1828    ) {
1829        self.kind = TestKind::Replay { corpus_entries, showmap_files, skipped_entries };
1830        self.status = TestStatus::Success;
1831        self.duration = duration;
1832    }
1833
1834    /// Records a skipped showmap replay (e.g. unit test or no corpus available).
1835    pub fn replay_skip(&mut self, reason: impl Into<String>) {
1836        self.kind = TestKind::Replay { corpus_entries: 0, showmap_files: 0, skipped_entries: 0 };
1837        self.status = TestStatus::Skipped;
1838        self.reason = Some(reason.into());
1839        self.duration = Duration::default();
1840    }
1841
1842    /// Marks a campaign stopped early by fail-fast or Ctrl-C as skipped, so a partial run is not
1843    /// reported as passing.
1844    pub fn interrupt(&mut self) {
1845        let reason = Some("interrupted".to_string());
1846        self.status = TestStatus::Skipped;
1847        for predicate in &mut self.invariant_predicate_results {
1848            if predicate.status.is_success() {
1849                predicate.status = TestStatus::Skipped;
1850                predicate.reason.clone_from(&reason);
1851            }
1852        }
1853        // Multi-predicate campaigns carry reasons per predicate.
1854        if self.invariant_count.is_none() {
1855            self.reason = reason;
1856        }
1857    }
1858
1859    /// Formats the test result into a string (for printing), naming invariant campaigns after
1860    /// the suite's contract.
1861    pub(crate) fn short_result_with_suite(&self, name: &str, suite_name: &str) -> String {
1862        let campaign = (self.kind.is_invariant() && self.invariant_count.is_some())
1863            .then(|| invariant_campaign_display_name(get_contract_name(suite_name)));
1864        let name = campaign.as_deref().unwrap_or(name);
1865        let status = self.render(true, campaign.as_deref());
1866        let block = match self.fork_block_number {
1867            Some(block) if self.status.is_failure() => format!(" (block: {block})"),
1868            _ => String::new(),
1869        };
1870        format!("{status} {name}{block} {}", self.kind.report())
1871    }
1872
1873    /// The number of logical tests this result stands for: skipped predicates of a campaign are
1874    /// counted individually.
1875    fn logical_count(&self) -> usize {
1876        let skipped = self.skipped_predicate_count();
1877        if skipped == 0 {
1878            1
1879        } else if self.status.is_skipped() && skipped == self.invariant_predicate_results.len() {
1880            skipped
1881        } else {
1882            1 + skipped
1883        }
1884    }
1885
1886    fn skipped_count(&self) -> usize {
1887        let skipped = self.skipped_predicate_count();
1888        if skipped == 0 && self.status.is_skipped() { 1 } else { skipped }
1889    }
1890
1891    fn skipped_predicate_count(&self) -> usize {
1892        self.invariant_predicate_results.iter().filter(|p| p.status.is_skipped()).count()
1893    }
1894
1895    /// Merges the given raw call result into `self`.
1896    pub fn extend<FEN: FoundryEvmNetwork>(&mut self, call_result: RawCallResult<FEN>) {
1897        extend!(self, call_result, TraceKind::Execution);
1898    }
1899
1900    /// Merges the given pre-test setup result into `self`.
1901    pub(crate) fn extend_setup<FEN: FoundryEvmNetwork>(&mut self, call_result: RawCallResult<FEN>) {
1902        extend!(self, call_result, TraceKind::Setup);
1903    }
1904
1905    /// Merges the given coverage result into `self`.
1906    pub fn merge_coverages(&mut self, other_coverage: Option<HitMaps>) {
1907        HitMaps::merge_opt(&mut self.line_coverage, other_coverage);
1908    }
1909}
1910
1911/// Data report by a test.
1912#[derive(Clone, Debug, PartialEq, Eq)]
1913pub enum TestKindReport {
1914    Unit {
1915        gas: u64,
1916    },
1917    Fuzz {
1918        runs: usize,
1919        mean_gas: u64,
1920        median_gas: u64,
1921        failed_corpus_replays: usize,
1922    },
1923    Invariant {
1924        runs: usize,
1925        calls: usize,
1926        reverts: usize,
1927        failed_corpus_replays: usize,
1928        /// For optimization mode (int256 return): the best value achieved. None = check mode.
1929        optimization_best_value: Option<I256>,
1930    },
1931    Table {
1932        runs: usize,
1933        mean_gas: u64,
1934        median_gas: u64,
1935    },
1936    Symbolic(SymbolicStats),
1937    /// Showmap corpus replay (no campaign performed).
1938    Replay {
1939        corpus_entries: usize,
1940        showmap_files: usize,
1941        skipped_entries: usize,
1942    },
1943}
1944
1945impl fmt::Display for TestKindReport {
1946    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1947        match self {
1948            Self::Unit { gas } => write!(f, "(gas: {gas})"),
1949            Self::Fuzz { runs, mean_gas, median_gas, failed_corpus_replays } => {
1950                write!(f, "(runs: {runs}, μ: {mean_gas}, ~: {median_gas}")?;
1951                if *failed_corpus_replays != 0 {
1952                    write!(f, ", failed corpus replays: {failed_corpus_replays}")?;
1953                }
1954                f.write_str(")")
1955            }
1956            Self::Invariant {
1957                runs,
1958                calls,
1959                reverts,
1960                failed_corpus_replays,
1961                optimization_best_value,
1962            } => {
1963                if let Some(best_value) = optimization_best_value {
1964                    return write!(f, "(best: {best_value}, runs: {runs}, calls: {calls})");
1965                }
1966                write!(f, "(runs: {runs}, calls: {calls}, reverts: {reverts}")?;
1967                if *failed_corpus_replays != 0 {
1968                    write!(f, ", failed corpus replays: {failed_corpus_replays}")?;
1969                }
1970                f.write_str(")")
1971            }
1972            Self::Table { runs, mean_gas, median_gas } => {
1973                write!(f, "(runs: {runs}, μ: {mean_gas}, ~: {median_gas})")
1974            }
1975            Self::Symbolic(SymbolicStats {
1976                paths,
1977                solver_queries,
1978                smt_queries,
1979                sat_queries,
1980                model_queries,
1981                sat_cache_hits,
1982                model_cache_hits,
1983                heuristic_witnesses,
1984                solver_time_ms,
1985                ..
1986            }) => {
1987                write!(
1988                    f,
1989                    "(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)"
1990                )
1991            }
1992            Self::Replay { corpus_entries, showmap_files, skipped_entries } => {
1993                write!(f, "(replay: {corpus_entries} entries, {showmap_files} files")?;
1994                if *skipped_entries != 0 {
1995                    write!(f, ", {skipped_entries} skipped")?;
1996                }
1997                f.write_str(")")
1998            }
1999        }
2000    }
2001}
2002
2003impl TestKindReport {
2004    /// Returns the main gas value to compare against
2005    pub const fn gas(&self) -> u64 {
2006        match *self {
2007            Self::Unit { gas } => gas,
2008            // We use the median for comparisons
2009            Self::Fuzz { median_gas, .. } | Self::Table { median_gas, .. } => median_gas,
2010            // We return 0 since it's not applicable
2011            Self::Invariant { .. } | Self::Symbolic { .. } | Self::Replay { .. } => 0,
2012        }
2013    }
2014}
2015
2016/// Various types of tests
2017#[derive(Clone, Debug, Serialize, Deserialize)]
2018pub enum TestKind {
2019    /// A unit test.
2020    Unit { gas: u64 },
2021    /// A fuzz test.
2022    Fuzz {
2023        /// we keep this for the debugger
2024        first_case: FuzzCase,
2025        runs: usize,
2026        mean_gas: u64,
2027        median_gas: u64,
2028        failed_corpus_replays: usize,
2029    },
2030    /// An invariant test.
2031    Invariant {
2032        runs: usize,
2033        calls: usize,
2034        reverts: usize,
2035        /// Actual worker count used by this invariant campaign.
2036        #[serde(default = "default_invariant_workers")]
2037        workers: usize,
2038        metrics: Map<String, InvariantMetrics>,
2039        failed_corpus_replays: usize,
2040        /// For optimization mode (int256 return): the best value achieved. None = check mode.
2041        optimization_best_value: Option<I256>,
2042    },
2043    /// A table test.
2044    Table { runs: usize, mean_gas: u64, median_gas: u64 },
2045    /// A symbolic test.
2046    Symbolic(SymbolicStats),
2047    /// Showmap corpus replay (no campaign performed).
2048    Replay { corpus_entries: usize, showmap_files: usize, skipped_entries: usize },
2049}
2050
2051impl Default for TestKind {
2052    fn default() -> Self {
2053        Self::Unit { gas: 0 }
2054    }
2055}
2056
2057impl TestKind {
2058    /// Returns `true` if this is a fuzz test.
2059    pub const fn is_fuzz(&self) -> bool {
2060        matches!(self, Self::Fuzz { .. })
2061    }
2062
2063    /// Returns `true` if this is an invariant test.
2064    pub const fn is_invariant(&self) -> bool {
2065        matches!(self, Self::Invariant { .. })
2066    }
2067
2068    /// Returns `true` if this is a symbolic test.
2069    pub const fn is_symbolic(&self) -> bool {
2070        matches!(self, Self::Symbolic { .. })
2071    }
2072
2073    /// Actual invariant campaign worker count, if this is an invariant test.
2074    pub const fn invariant_workers(&self) -> Option<usize> {
2075        match self {
2076            Self::Invariant { workers, .. } => Some(*workers),
2077            _ => None,
2078        }
2079    }
2080
2081    /// The gas consumed by this test
2082    pub const fn report(&self) -> TestKindReport {
2083        match *self {
2084            Self::Unit { gas } => TestKindReport::Unit { gas },
2085            Self::Fuzz { runs, mean_gas, median_gas, failed_corpus_replays, .. } => {
2086                TestKindReport::Fuzz { runs, mean_gas, median_gas, failed_corpus_replays }
2087            }
2088            Self::Invariant {
2089                runs,
2090                calls,
2091                reverts,
2092                failed_corpus_replays,
2093                optimization_best_value,
2094                ..
2095            } => TestKindReport::Invariant {
2096                runs,
2097                calls,
2098                reverts,
2099                failed_corpus_replays,
2100                optimization_best_value,
2101            },
2102            Self::Table { runs, mean_gas, median_gas } => {
2103                TestKindReport::Table { runs, mean_gas, median_gas }
2104            }
2105            Self::Symbolic(stats) => TestKindReport::Symbolic(stats),
2106            Self::Replay { corpus_entries, showmap_files, skipped_entries } => {
2107                TestKindReport::Replay { corpus_entries, showmap_files, skipped_entries }
2108            }
2109        }
2110    }
2111}
2112
2113const fn default_invariant_workers() -> usize {
2114    1
2115}
2116
2117/// The result of a test setup.
2118///
2119/// Includes the deployment of the required libraries and the test contract itself, and the call to
2120/// the `setUp()` function.
2121#[derive(Clone, Debug, Default)]
2122pub struct TestSetup {
2123    /// The address at which the test contract was deployed.
2124    pub address: Address,
2125    /// Defined fuzz test fixtures.
2126    pub fuzz_fixtures: FuzzFixtures,
2127
2128    /// The logs emitted during setup.
2129    pub logs: Vec<Log>,
2130    /// Addresses labeled during setup.
2131    pub labels: AddressHashMap<String>,
2132    /// Call traces of the setup.
2133    pub traces: Traces,
2134    /// Runtime bytecodes for contracts seen in setup traces.
2135    pub debug_bytecodes: AddressHashMap<Bytes>,
2136    /// Coverage info during setup.
2137    pub coverage: Option<HitMaps>,
2138    /// Addresses of external libraries deployed during setup.
2139    pub deployed_libs: Vec<Address>,
2140    /// The active fork's block number after setup, if any.
2141    pub fork_block_number: Option<u64>,
2142    /// Cached setup-derived fuzz dictionary for stateless fuzz tests.
2143    pub(crate) fuzz_state: OnceLock<EvmFuzzState>,
2144
2145    /// The reason the setup failed, if it did.
2146    pub reason: Option<String>,
2147    /// Whether setup and entire test suite is skipped.
2148    pub skipped: bool,
2149    /// Whether the test failed to deploy.
2150    pub deployment_failure: bool,
2151}
2152
2153impl TestSetup {
2154    pub fn failed(reason: String) -> Self {
2155        Self { reason: Some(reason), ..Default::default() }
2156    }
2157
2158    pub fn skipped(reason: String) -> Self {
2159        Self { reason: Some(reason), skipped: true, ..Default::default() }
2160    }
2161
2162    pub fn extend<FEN: FoundryEvmNetwork>(
2163        &mut self,
2164        raw: RawCallResult<FEN>,
2165        trace_kind: TraceKind,
2166    ) {
2167        extend!(self, raw, trace_kind);
2168    }
2169
2170    pub fn merge_coverages(&mut self, other_coverage: Option<HitMaps>) {
2171        HitMaps::merge_opt(&mut self.coverage, other_coverage);
2172    }
2173}
2174
2175pub(crate) fn invariant_campaign_display_name(contract_name: &str) -> String {
2176    format!("{contract_name} invariants")
2177}
2178
2179const fn symbolic_result_schema_version() -> u32 {
2180    SYMBOLIC_RESULT_SCHEMA_VERSION
2181}
2182
2183#[cfg(test)]
2184mod tests {
2185    use super::*;
2186    use foundry_cli::utils::parse_json;
2187
2188    const SYMBOLIC_RESULT_SCHEMA: &str =
2189        include_str!("../../evm/symbolic/assets/symbolic-result.schema.json");
2190    const SYMBOLIC_COUNTEREXAMPLE_SCHEMA: &str =
2191        include_str!("../../evm/symbolic/assets/symbolic-counterexample.schema.json");
2192
2193    fn schema_defs(schema: &serde_json::Value) -> &serde_json::Map<String, serde_json::Value> {
2194        schema["$defs"].as_object().expect("schema $defs object")
2195    }
2196
2197    /// Collects every `$ref` target in `value`.
2198    fn collect_refs<'a>(value: &'a serde_json::Value, refs: &mut Vec<&'a str>) {
2199        match value {
2200            serde_json::Value::Object(map) => {
2201                refs.extend(map.get("$ref").and_then(serde_json::Value::as_str));
2202                for child in map.values() {
2203                    collect_refs(child, refs);
2204                }
2205            }
2206            serde_json::Value::Array(values) => {
2207                for child in values {
2208                    collect_refs(child, refs);
2209                }
2210            }
2211            _ => {}
2212        }
2213    }
2214
2215    #[test]
2216    fn symbolic_schemas_match_result_types() {
2217        let result_schema: serde_json::Value = parse_json(SYMBOLIC_RESULT_SCHEMA).unwrap();
2218        let counterexample_schema: serde_json::Value =
2219            parse_json(SYMBOLIC_COUNTEREXAMPLE_SCHEMA).unwrap();
2220        let result_defs = schema_defs(&result_schema);
2221        let counterexample_defs = schema_defs(&counterexample_schema);
2222
2223        // Every counterexample `$ref` must resolve offline, either locally or into the result
2224        // schema.
2225        let mut refs = Vec::new();
2226        collect_refs(&counterexample_schema, &mut refs);
2227        for reference in refs {
2228            let resolved = if let Some(name) = reference.strip_prefix(
2229                "https://foundry-rs.github.io/schemas/symbolic-result.v1.schema.json#/$defs/",
2230            ) {
2231                result_defs.contains_key(name)
2232            } else if let Some(name) = reference.strip_prefix("#/$defs/") {
2233                counterexample_defs.contains_key(name)
2234            } else {
2235                false
2236            };
2237            assert!(resolved, "unresolved schema ref {reference}");
2238        }
2239
2240        // The solver stats schema must list exactly the serialized `SymbolicStats` fields.
2241        let stats = serde_json::to_value(SymbolicStats::default()).unwrap();
2242        let mut expected = stats.as_object().unwrap().keys().collect::<Vec<_>>();
2243        let mut actual = result_defs["solver_stats"]["properties"]
2244            .as_object()
2245            .unwrap()
2246            .keys()
2247            .collect::<Vec<_>>();
2248        expected.sort();
2249        actual.sort();
2250        assert_eq!(actual, expected);
2251    }
2252
2253    fn outcome_with_results(test_results: Vec<TestResult>) -> TestOutcome {
2254        let test_results = test_results
2255            .into_iter()
2256            .enumerate()
2257            .map(|(idx, result)| (format!("test{idx}()"), result))
2258            .collect();
2259        let suite = SuiteResult::new(Duration::ZERO, test_results, Vec::new());
2260        TestOutcome::new(None, BTreeMap::from([("suite".to_string(), suite)]), false, None)
2261    }
2262
2263    fn failed_result(kind: TestKind) -> TestResult {
2264        TestResult { status: TestStatus::Failure, kind, ..Default::default() }
2265    }
2266
2267    fn failed_invariant(workers: usize) -> TestResult {
2268        let mut kind = invariant_kind(0, 0, 0);
2269        if let TestKind::Invariant { workers: w, .. } = &mut kind {
2270            *w = workers;
2271        }
2272        failed_result(kind)
2273    }
2274
2275    #[test]
2276    fn failed_tests_are_debuggable_only_for_concrete_failures() {
2277        let unit = failed_result(TestKind::Unit { gas: 0 });
2278        assert!(outcome_with_results(vec![unit.clone()]).failed_tests_are_debuggable());
2279        assert!(!outcome_with_results(vec![failed_invariant(1)]).failed_tests_are_debuggable());
2280        assert!(
2281            !outcome_with_results(vec![failed_result(
2282                TestKind::Symbolic(SymbolicStats::default())
2283            )])
2284            .failed_tests_are_debuggable()
2285        );
2286
2287        let mut symbolic_backed = unit;
2288        symbolic_backed.symbolic =
2289            Some(SymbolicResult::pass(&SymbolicConfig::default(), SymbolicStats::default()));
2290        assert!(!outcome_with_results(vec![symbolic_backed]).failed_tests_are_debuggable());
2291    }
2292
2293    #[test]
2294    fn invariant_workers_hint_requires_matching_parallel_worker_counts() {
2295        let hint = |workers: &[usize]| {
2296            outcome_with_results(workers.iter().map(|&w| failed_invariant(w)).collect())
2297                .invariant_workers_hint()
2298        };
2299        assert_eq!(hint(&[3, 3]), Some(3));
2300        assert_eq!(hint(&[2, 3]), None);
2301        assert_eq!(hint(&[1]), None);
2302    }
2303
2304    #[test]
2305    fn invariant_kind_deserializes_legacy_payload_without_workers() {
2306        let kind = serde_json::from_value::<TestKind>(serde_json::json!({
2307            "Invariant": {
2308                "runs": 4,
2309                "calls": 10,
2310                "reverts": 0,
2311                "metrics": {},
2312                "failed_corpus_replays": 0,
2313                "optimization_best_value": null
2314            }
2315        }))
2316        .unwrap();
2317
2318        assert_eq!(kind.invariant_workers(), Some(1));
2319    }
2320}