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, get_file_name, shell};
13use foundry_config::{SymbolicConfig, SymbolicExplorationOrder, SymbolicStorageLayout};
14use foundry_evm::{
15    core::{Breakpoints, evm::FoundryEvmNetwork},
16    coverage::HitMaps,
17    decode::SkipReason,
18    executors::{RawCallResult, invariant::InvariantMetrics},
19    fuzz::{
20        CallDetails, CounterExample, FuzzCase, FuzzFixtures, FuzzTestResult,
21        strategies::EvmFuzzState,
22    },
23    traces::{CallTraceArena, CallTraceDecoder, TraceKind, Traces},
24};
25use foundry_evm_symbolic::{
26    PortfolioDiagnostics, SymbolicStats, SymbolicStopReason, SymbolicStorageAssignment,
27};
28use serde::{Deserialize, Serialize};
29use std::{
30    borrow::Cow,
31    collections::{BTreeMap, HashMap as Map},
32    fmt::{self, Write},
33    sync::OnceLock,
34    time::Duration,
35};
36use yansi::Paint;
37
38pub(crate) fn invariant_campaign_display_name(contract_name: &str) -> String {
39    format!("{contract_name} invariants")
40}
41
42const INVARIANT_CAMPAIGN_FALLBACK_NAME: &str = "Invariant campaign";
43const SYMBOLIC_RESULT_SCHEMA_VERSION: u32 = 1;
44pub const SYMBOLIC_COUNTEREXAMPLE_ARTIFACT_SCHEMA: &str = "foundry:symbolic.counterexample@v1";
45pub const SYMBOLIC_COUNTEREXAMPLE_ARTIFACT_SCHEMA_VERSION: u32 = 1;
46
47const fn symbolic_result_schema_version() -> u32 {
48    SYMBOLIC_RESULT_SCHEMA_VERSION
49}
50
51/// The aggregated result of a test run.
52#[derive(Clone, Debug)]
53pub struct TestOutcome {
54    /// The results of all test suites by their identifier (`path:contract_name`).
55    ///
56    /// Essentially `identifier => signature => result`.
57    pub results: BTreeMap<String, SuiteResult>,
58    /// Whether to allow test failures without failing the entire test run.
59    pub allow_failure: bool,
60    /// The decoder used to decode traces and logs.
61    ///
62    /// This is `None` if traces and logs were not decoded.
63    ///
64    /// Note that `Address` fields only contain the last executed test case's data.
65    pub last_run_decoder: Option<CallTraceDecoder>,
66    /// The gas report, if requested.
67    pub gas_report: Option<GasReport>,
68    /// Known contracts from the test run (used for coverage).
69    pub known_contracts: Option<ContractsByArtifact>,
70    /// The fuzz seed used for the test run.
71    pub fuzz_seed: Option<U256>,
72}
73
74impl TestOutcome {
75    /// Creates a new test outcome with the given results.
76    pub const fn new(
77        known_contracts: Option<ContractsByArtifact>,
78        results: BTreeMap<String, SuiteResult>,
79        allow_failure: bool,
80        fuzz_seed: Option<U256>,
81    ) -> Self {
82        Self {
83            results,
84            allow_failure,
85            last_run_decoder: None,
86            gas_report: None,
87            known_contracts,
88            fuzz_seed,
89        }
90    }
91
92    /// Creates a new empty test outcome.
93    pub const fn empty(known_contracts: Option<ContractsByArtifact>, allow_failure: bool) -> Self {
94        Self::new(known_contracts, BTreeMap::new(), allow_failure, None)
95    }
96
97    /// Returns an iterator over all individual succeeding tests and their names.
98    pub fn successes(&self) -> impl Iterator<Item = (&String, &TestResult)> {
99        self.tests().filter(|(_, t)| t.status.is_success())
100    }
101
102    /// Returns an iterator over all individual skipped tests and their names.
103    pub fn skips(&self) -> impl Iterator<Item = (&String, &TestResult)> {
104        self.tests().filter(|(_, t)| t.status.is_skipped())
105    }
106
107    /// Returns an iterator over all individual failing tests and their names.
108    pub fn failures(&self) -> impl Iterator<Item = (&String, &TestResult)> {
109        self.tests().filter(|(_, t)| t.status.is_failure())
110    }
111
112    /// Returns an iterator over all individual tests and their names.
113    pub fn tests(&self) -> impl Iterator<Item = (&String, &TestResult)> {
114        self.results.values().flat_map(|suite| suite.tests())
115    }
116
117    /// Returns merged symbolic solver portfolio diagnostics across all tests in this outcome.
118    pub fn symbolic_portfolio_diagnostics(&self) -> Option<PortfolioDiagnostics> {
119        let mut diagnostics = PortfolioDiagnostics::default();
120        for (_, result) in self.tests() {
121            if let Some(result_diagnostics) = &result.symbolic_portfolio_diagnostics {
122                diagnostics.merge(result_diagnostics);
123            }
124        }
125        (!diagnostics.is_empty()).then_some(diagnostics)
126    }
127
128    /// Flattens the test outcome into a list of individual tests.
129    // TODO: Replace this with `tests` and make it return `TestRef<'_>`
130    pub fn into_tests_cloned(&self) -> impl Iterator<Item = SuiteTestResult> + '_ {
131        self.results
132            .iter()
133            .flat_map(|(file, suite)| {
134                suite
135                    .test_results
136                    .iter()
137                    .map(move |(sig, result)| (file.clone(), sig.clone(), result.clone()))
138            })
139            .map(|(artifact_id, signature, result)| SuiteTestResult {
140                artifact_id,
141                signature,
142                result,
143            })
144    }
145
146    /// Flattens the test outcome into a list of individual tests.
147    pub fn into_tests(self) -> impl Iterator<Item = SuiteTestResult> {
148        self.results
149            .into_iter()
150            .flat_map(|(file, suite)| {
151                suite.test_results.into_iter().map(move |t| (file.clone(), t))
152            })
153            .map(|(artifact_id, (signature, result))| SuiteTestResult {
154                artifact_id,
155                signature,
156                result,
157            })
158    }
159
160    /// Returns the number of tests that passed.
161    pub fn passed(&self) -> usize {
162        self.results.values().map(SuiteResult::passed).sum()
163    }
164
165    /// Returns the number of tests that were skipped.
166    pub fn skipped(&self) -> usize {
167        self.results.values().map(SuiteResult::skipped).sum()
168    }
169
170    /// Returns the number of tests that failed.
171    pub fn failed(&self) -> usize {
172        self.results.values().map(SuiteResult::failed).sum()
173    }
174
175    /// Returns `true` if any fuzz or invariant test failed.
176    pub fn has_fuzz_failures(&self) -> bool {
177        self.failures().any(|(_, t)| t.kind.is_fuzz() || t.kind.is_invariant())
178    }
179
180    /// Returns `true` if any invariant test failed.
181    pub fn has_invariant_failures(&self) -> bool {
182        self.failures().any(|(_, t)| t.kind.is_invariant())
183    }
184
185    /// Returns `true` if all failing tests can be meaningfully inspected with `forge test --debug`.
186    pub fn failed_tests_are_debuggable(&self) -> bool {
187        self.failures().all(|(_, result)| result.is_debuggable_failure())
188    }
189
190    fn invariant_workers_hint(&self) -> Option<usize> {
191        let mut workers = self.failures().filter_map(|(_, result)| result.kind.invariant_workers());
192        let first = workers.next()?;
193        (first > 1 && workers.all(|workers| workers == first)).then_some(first)
194    }
195
196    /// Sums up all the durations of all individual test suites.
197    ///
198    /// Note that this is not necessarily the wall clock time of the entire test run.
199    pub fn total_time(&self) -> Duration {
200        self.results.values().map(|suite| suite.duration).sum()
201    }
202
203    /// Formats the aggregated summary of all test suites into a string (for printing).
204    pub fn summary(&self, wall_clock_time: Duration) -> String {
205        let num_test_suites = self.results.len();
206        let suites = if num_test_suites == 1 { "suite" } else { "suites" };
207        let total_passed = self.passed();
208        let total_failed = self.failed();
209        let total_skipped = self.skipped();
210        let total_tests = total_passed + total_failed + total_skipped;
211        format!(
212            "\nRan {} test {} in {:.2?} ({:.2?} CPU time): {} tests passed, {} failed, {} skipped ({} total tests)",
213            num_test_suites,
214            suites,
215            wall_clock_time,
216            self.total_time(),
217            total_passed.green(),
218            total_failed.red(),
219            total_skipped.yellow(),
220            total_tests
221        )
222    }
223
224    /// Checks if there are any failures and failures are disallowed.
225    pub fn ensure_ok(&self, silent: bool) -> eyre::Result<()> {
226        let outcome = self;
227        let failures = outcome.failures().count();
228        if outcome.allow_failure || failures == 0 {
229            return Ok(());
230        }
231
232        if shell::is_quiet() || silent {
233            std::process::exit(1);
234        }
235
236        sh_println!("\nFailing tests:")?;
237        for (suite_name, suite) in &outcome.results {
238            let failed = suite.failed();
239            if failed == 0 {
240                continue;
241            }
242
243            let term = if failed > 1 { "tests" } else { "test" };
244            sh_println!("Encountered {failed} failing {term} in {suite_name}")?;
245            for (name, result) in suite.failures() {
246                sh_println!("{}", result.short_result_with_suite(name, suite_name))?;
247            }
248            sh_println!()?;
249        }
250        let successes = outcome.passed();
251        sh_println!(
252            "Encountered a total of {} failing tests, {} tests succeeded",
253            failures.to_string().red(),
254            successes.to_string().green()
255        )?;
256
257        // Show helpful hint for rerunning failed tests
258        let test_word = if failures == 1 { "test" } else { "tests" };
259        sh_println!(
260            "\nTip: Run {} to retry only the {} failed {}",
261            "`forge test --rerun`".cyan(),
262            failures,
263            test_word
264        )?;
265        if outcome.failed_tests_are_debuggable() {
266            sh_println!(
267                "Tip: Run {} to inspect one failing test in the debugger",
268                "`forge test --debug --match-test <TEST_NAME>`".cyan()
269            )?;
270        }
271
272        // Print seed for fuzz/invariant test failures to enable reproduction.
273        if let Some(seed) = self.fuzz_seed
274            && outcome.has_fuzz_failures()
275        {
276            sh_println!(
277                "\nFuzz seed: {} (use {} to reproduce)",
278                format!("{seed:#x}").cyan(),
279                "`--fuzz-seed`".cyan()
280            )?;
281            if let Some(invariant_workers) = outcome.invariant_workers_hint() {
282                sh_println!(
283                    "Invariant workers: {} (use {} to reproduce)",
284                    invariant_workers,
285                    format!("`--invariant-workers {invariant_workers}`").cyan()
286                )?;
287            }
288        }
289
290        std::process::exit(1);
291    }
292
293    /// Removes first test result, if any.
294    pub fn remove_first(&mut self) -> Option<(String, String, TestResult)> {
295        self.results.iter_mut().find_map(|(suite_name, suite)| {
296            if let Some(test_name) = suite.test_results.keys().next().cloned() {
297                let result = suite.test_results.remove(&test_name).unwrap();
298                Some((suite_name.clone(), test_name, result))
299            } else {
300                None
301            }
302        })
303    }
304}
305
306#[cfg(test)]
307mod tests {
308    use super::*;
309
310    const SYMBOLIC_RESULT_SCHEMA_JSON: &str =
311        include_str!("../../evm/symbolic/assets/symbolic-result.schema.json");
312    const SYMBOLIC_COUNTEREXAMPLE_SCHEMA_JSON: &str =
313        include_str!("../../evm/symbolic/assets/symbolic-counterexample.schema.json");
314
315    fn schema_defs(schema: &serde_json::Value) -> &serde_json::Map<String, serde_json::Value> {
316        schema["$defs"].as_object().expect("schema $defs object")
317    }
318
319    fn assert_counterexample_schema_refs_resolve_offline() {
320        let counterexample_schema: serde_json::Value =
321            serde_json::from_str(SYMBOLIC_COUNTEREXAMPLE_SCHEMA_JSON).unwrap();
322        let result_schema: serde_json::Value =
323            serde_json::from_str(SYMBOLIC_RESULT_SCHEMA_JSON).unwrap();
324        let result_defs = schema_defs(&result_schema);
325        let counterexample_defs = schema_defs(&counterexample_schema);
326
327        fn visit_refs(
328            value: &serde_json::Value,
329            result_defs: &serde_json::Map<String, serde_json::Value>,
330            counterexample_defs: &serde_json::Map<String, serde_json::Value>,
331        ) {
332            match value {
333                serde_json::Value::Object(map) => {
334                    if let Some(reference) = map.get("$ref").and_then(serde_json::Value::as_str) {
335                        if let Some(name) = reference.strip_prefix(
336                            "https://foundry-rs.github.io/schemas/symbolic-result.v1.schema.json#/$defs/",
337                        ) {
338                            assert!(result_defs.contains_key(name), "unresolved ref {reference}");
339                        } else if let Some(name) = reference.strip_prefix("#/$defs/") {
340                            assert!(
341                                counterexample_defs.contains_key(name),
342                                "unresolved ref {reference}"
343                            );
344                        } else {
345                            panic!("unexpected schema ref {reference}");
346                        }
347                    }
348                    for child in map.values() {
349                        visit_refs(child, result_defs, counterexample_defs);
350                    }
351                }
352                serde_json::Value::Array(values) => {
353                    for child in values {
354                        visit_refs(child, result_defs, counterexample_defs);
355                    }
356                }
357                _ => {}
358            }
359        }
360
361        visit_refs(&counterexample_schema, result_defs, counterexample_defs);
362    }
363
364    #[test]
365    fn symbolic_result_schema_includes_solver_stats() {
366        let schema: serde_json::Value = serde_json::from_str(SYMBOLIC_RESULT_SCHEMA_JSON).unwrap();
367        let stats = schema["$defs"]["solver_stats"]["properties"]
368            .as_object()
369            .expect("solver stats properties");
370
371        for key in [
372            "paths",
373            "solver_queries",
374            "smt_queries",
375            "sat_queries",
376            "model_queries",
377            "sat_cache_hits",
378            "model_cache_hits",
379            "heuristic_witnesses",
380            "solver_time_ms",
381            "smt_input_bytes",
382            "smt_max_query_bytes",
383            "smt_build_time_ms",
384            "smt_max_query_time_ms",
385        ] {
386            assert!(stats.contains_key(key), "missing solver stats schema key {key}");
387        }
388    }
389
390    fn assert_counterexample_artifact_shape(value: &serde_json::Value) {
391        assert_counterexample_schema_refs_resolve_offline();
392        let object = value.as_object().expect("artifact object");
393        for key in [
394            "schema_version",
395            "schema",
396            "kind",
397            "test",
398            "replay",
399            "replay_semantics",
400            "bounds",
401            "solver",
402            "assumptions",
403            "call_trace",
404            "calls",
405        ] {
406            assert!(object.contains_key(key), "missing required artifact key {key}");
407        }
408        assert_eq!(value["schema_version"], 1);
409        assert_eq!(value["schema"], SYMBOLIC_COUNTEREXAMPLE_ARTIFACT_SCHEMA);
410        assert!(matches!(value["kind"].as_str(), Some("single_call" | "sequence")));
411        assert!(value["replay_semantics"].is_object());
412        assert!(!value["calls"].as_array().expect("calls array").is_empty());
413        for call in value["calls"].as_array().unwrap() {
414            let call = call.as_object().expect("call object");
415            for key in [
416                "warp",
417                "roll",
418                "sender",
419                "target",
420                "calldata",
421                "value",
422                "contract_name",
423                "function_name",
424                "signature",
425                "args",
426                "raw_args",
427            ] {
428                assert!(call.contains_key(key), "missing required call key {key}");
429            }
430            for key in ["warp", "roll", "value"] {
431                let Some(encoded) = call[key].as_str() else { continue };
432                let Some(hex) = encoded.strip_prefix("0x") else {
433                    panic!("{key} must be 0x-prefixed hex quantity: {encoded}");
434                };
435                assert!(
436                    hex == "0" || !hex.starts_with('0'),
437                    "{key} must be compact hex quantity without leading zeros: {encoded}"
438                );
439                assert!(
440                    hex.bytes().all(|byte| byte.is_ascii_hexdigit()),
441                    "{key} must be hex quantity: {encoded}"
442                );
443            }
444        }
445    }
446
447    fn outcome_with_failed_invariant_workers(workers: &[usize]) -> TestOutcome {
448        let test_results = workers
449            .iter()
450            .enumerate()
451            .map(|(idx, workers)| {
452                (
453                    format!("invariant{idx}()"),
454                    TestResult {
455                        status: TestStatus::Failure,
456                        kind: TestKind::Invariant {
457                            runs: 0,
458                            calls: 0,
459                            reverts: 0,
460                            workers: *workers,
461                            metrics: Map::new(),
462                            failed_corpus_replays: 0,
463                            optimization_best_value: None,
464                        },
465                        ..Default::default()
466                    },
467                )
468            })
469            .collect();
470        TestOutcome::new(
471            None,
472            BTreeMap::from([(
473                "suite".to_string(),
474                SuiteResult::new(Duration::ZERO, test_results, Vec::new()),
475            )]),
476            false,
477            None,
478        )
479    }
480
481    fn outcome_with_results(test_results: Vec<TestResult>) -> TestOutcome {
482        TestOutcome::new(
483            None,
484            BTreeMap::from([(
485                "suite".to_string(),
486                SuiteResult::new(
487                    Duration::ZERO,
488                    test_results
489                        .into_iter()
490                        .enumerate()
491                        .map(|(idx, result)| (format!("test{idx}()"), result))
492                        .collect(),
493                    Vec::new(),
494                ),
495            )]),
496            false,
497            None,
498        )
499    }
500
501    fn failed_result(kind: TestKind) -> TestResult {
502        TestResult { status: TestStatus::Failure, kind, ..Default::default() }
503    }
504
505    #[test]
506    fn failed_tests_are_debuggable_for_unit_failures() {
507        let outcome = outcome_with_results(vec![failed_result(TestKind::Unit { gas: 0 })]);
508
509        assert!(outcome.failed_tests_are_debuggable());
510    }
511
512    #[test]
513    fn failed_tests_are_not_debuggable_for_invariant_failures() {
514        let outcome = outcome_with_results(vec![failed_result(TestKind::Invariant {
515            runs: 0,
516            calls: 0,
517            reverts: 0,
518            workers: 1,
519            metrics: Map::new(),
520            failed_corpus_replays: 0,
521            optimization_best_value: None,
522        })]);
523
524        assert!(!outcome.failed_tests_are_debuggable());
525    }
526
527    #[test]
528    fn failed_tests_are_not_debuggable_for_symbolic_failures() {
529        let outcome = outcome_with_results(vec![failed_result(TestKind::Symbolic {
530            paths: 0,
531            solver_queries: 0,
532            smt_queries: 0,
533            sat_queries: 0,
534            model_queries: 0,
535            sat_cache_hits: 0,
536            model_cache_hits: 0,
537            heuristic_witnesses: 0,
538            solver_time_ms: 0,
539            smt_input_bytes: 0,
540            smt_max_query_bytes: 0,
541            smt_build_time_ms: 0,
542            smt_max_query_time_ms: 0,
543        })]);
544
545        assert!(!outcome.failed_tests_are_debuggable());
546    }
547
548    #[test]
549    fn failed_tests_are_not_debuggable_for_symbolic_backed_failures() {
550        let mut result = failed_result(TestKind::Unit { gas: 0 });
551        result.symbolic =
552            Some(SymbolicResult::pass(&SymbolicConfig::default(), SymbolicStats::default()));
553        let outcome = outcome_with_results(vec![result]);
554
555        assert!(!outcome.failed_tests_are_debuggable());
556    }
557
558    #[test]
559    fn invariant_workers_hint_requires_matching_parallel_worker_counts() {
560        assert_eq!(
561            outcome_with_failed_invariant_workers(&[3, 3]).invariant_workers_hint(),
562            Some(3)
563        );
564        assert_eq!(outcome_with_failed_invariant_workers(&[2, 3]).invariant_workers_hint(), None);
565        assert_eq!(outcome_with_failed_invariant_workers(&[1]).invariant_workers_hint(), None);
566    }
567
568    #[test]
569    fn invariant_kind_deserializes_legacy_payload_without_workers() {
570        let kind = serde_json::from_value::<TestKind>(serde_json::json!({
571            "Invariant": {
572                "runs": 4,
573                "calls": 10,
574                "reverts": 0,
575                "metrics": {},
576                "failed_corpus_replays": 0,
577                "optimization_best_value": null
578            }
579        }))
580        .unwrap();
581
582        assert_eq!(kind.invariant_workers(), Some(1));
583    }
584
585    #[test]
586    fn symbolic_counterexample_artifact_serializes_sequence_calls() {
587        let symbolic = SymbolicResult::pass(&SymbolicConfig::default(), SymbolicStats::default());
588        let call = SymbolicCounterexampleCall {
589            warp: Some(U256::from(12)),
590            roll: Some(U256::from(3)),
591            sender: Address::ZERO,
592            target: Address::ZERO,
593            calldata: Bytes::from_static(&[0x12, 0x34, 0x56, 0x78]),
594            value: Some(U256::from(9)),
595            contract_name: Some("Target".to_string()),
596            function_name: Some("step".to_string()),
597            signature: Some("step()".to_string()),
598            args: Some(String::new()),
599            raw_args: Some(String::new()),
600        };
601        let artifact = SymbolicCounterexampleArtifact::new(
602            SymbolicCounterexampleArtifactKind::Sequence,
603            SymbolicCounterexampleTestIdentity {
604                contract: "InvariantTest".to_string(),
605                test: "invariant_counter()".to_string(),
606            },
607            &symbolic,
608            SymbolicCounterexampleReplaySemantics { fail_on_revert: false },
609            vec![call.clone(), call],
610        );
611
612        let value = serde_json::to_value(artifact).unwrap();
613        assert_eq!(value["schema_version"], 1);
614        assert_eq!(value["schema"], SYMBOLIC_COUNTEREXAMPLE_ARTIFACT_SCHEMA);
615        assert_eq!(value["kind"], "sequence");
616        assert_eq!(value["replay_semantics"]["fail_on_revert"], false);
617        assert!(value.get("storage").is_none());
618        assert!(value.get("invariant_failure").is_none());
619        assert_eq!(value["calls"].as_array().unwrap().len(), 2);
620        assert_eq!(value["calls"][0]["calldata"], "0x12345678");
621        assert_eq!(value["calls"][0]["warp"], "0xc");
622        assert_eq!(value["calls"][0]["roll"], "0x3");
623        assert_eq!(value["calls"][0]["value"], "0x9");
624        assert_counterexample_artifact_shape(&value);
625
626        let decoded = serde_json::from_value::<SymbolicCounterexampleArtifact>(value).unwrap();
627        assert!(decoded.storage.is_empty());
628        assert!(decoded.invariant_failure.is_none());
629    }
630
631    #[test]
632    fn symbolic_counterexample_artifact_serializes_invariant_replay_metadata() {
633        let symbolic = SymbolicResult::pass(&SymbolicConfig::default(), SymbolicStats::default());
634        let call = SymbolicCounterexampleCall {
635            warp: None,
636            roll: None,
637            sender: Address::ZERO,
638            target: Address::repeat_byte(0x22),
639            calldata: Bytes::from_static(&[0x12, 0x34, 0x56, 0x78]),
640            value: None,
641            contract_name: Some("Target".to_string()),
642            function_name: Some("step".to_string()),
643            signature: Some("step()".to_string()),
644            args: Some(String::new()),
645            raw_args: Some(String::new()),
646        };
647        let artifact = SymbolicCounterexampleArtifact::new(
648            SymbolicCounterexampleArtifactKind::Sequence,
649            SymbolicCounterexampleTestIdentity {
650                contract: "InvariantTest".to_string(),
651                test: "invariant_counter()".to_string(),
652            },
653            &symbolic,
654            SymbolicCounterexampleReplaySemantics { fail_on_revert: true },
655            vec![call],
656        )
657        .with_storage(vec![SymbolicStorageAssignment {
658            address: Address::repeat_byte(0x11),
659            slot: U256::from(7),
660            value: U256::from(42),
661        }])
662        .with_invariant_failure(SymbolicInvariantArtifactFailure::Handler {
663            name: Some("Target::step".to_string()),
664            reverter: Address::repeat_byte(0x22),
665            selector: Selector::from([0x12, 0x34, 0x56, 0x78]),
666            fingerprint: B256::repeat_byte(0x33),
667        });
668
669        let value = serde_json::to_value(artifact.clone()).unwrap();
670        assert_eq!(value["storage"][0]["address"], format!("{:?}", Address::repeat_byte(0x11)));
671        assert_eq!(value["storage"][0]["slot"], "0x7");
672        assert_eq!(value["storage"][0]["value"], "0x2a");
673        assert_eq!(value["invariant_failure"]["kind"], "handler");
674        assert_eq!(value["invariant_failure"]["name"], "Target::step");
675        assert_eq!(
676            value["invariant_failure"]["reverter"],
677            format!("{:?}", Address::repeat_byte(0x22))
678        );
679        assert_eq!(value["invariant_failure"]["selector"], "0x12345678");
680        assert_eq!(
681            value["invariant_failure"]["fingerprint"],
682            format!("{:?}", B256::repeat_byte(0x33))
683        );
684        assert_counterexample_artifact_shape(&value);
685
686        let decoded = serde_json::from_value::<SymbolicCounterexampleArtifact>(value).unwrap();
687        assert_eq!(decoded.storage, artifact.storage);
688        assert_eq!(decoded.invariant_failure, artifact.invariant_failure);
689    }
690
691    #[test]
692    fn symbolic_counterexample_schema_includes_predicate_failure_sites() {
693        let schema: serde_json::Value =
694            serde_json::from_str(SYMBOLIC_COUNTEREXAMPLE_SCHEMA_JSON).unwrap();
695        let predicate = schema["$defs"]["invariant_failure"]["oneOf"]
696            .as_array()
697            .unwrap()
698            .iter()
699            .find(|variant| variant["properties"]["kind"]["const"] == "predicate")
700            .unwrap();
701        assert_eq!(predicate["properties"]["site"]["$ref"], "#/$defs/invariant_failure_site");
702
703        let site_schema = &schema["$defs"]["invariant_failure_site"];
704        let site_properties = site_schema["properties"].as_object().unwrap();
705        let required_site_properties = site_schema["required"].as_array().unwrap();
706        let site_kinds = site_properties["kind"]["enum"].as_array().unwrap();
707        for (site, expected_kind) in [
708            (
709                SymbolicInvariantFailureSite::SequenceCall {
710                    target: Address::ZERO,
711                    selector: Selector::ZERO,
712                    fingerprint: B256::ZERO,
713                },
714                "sequence_call",
715            ),
716            (
717                SymbolicInvariantFailureSite::Invariant {
718                    target: Address::ZERO,
719                    selector: Selector::ZERO,
720                    fingerprint: B256::ZERO,
721                },
722                "invariant",
723            ),
724            (
725                SymbolicInvariantFailureSite::AfterInvariant {
726                    target: Address::ZERO,
727                    selector: Selector::ZERO,
728                    fingerprint: B256::ZERO,
729                },
730                "after_invariant",
731            ),
732        ] {
733            let failure = SymbolicInvariantArtifactFailure::Predicate {
734                name: "invariant_counter".to_string(),
735                site: Some(site),
736            };
737            let value = serde_json::to_value(failure).unwrap();
738            assert_eq!(value["site"]["kind"], expected_kind);
739            assert!(site_kinds.contains(&value["site"]["kind"]));
740            let site = value["site"].as_object().unwrap();
741            assert!(site.keys().all(|key| site_properties.contains_key(key)));
742            assert!(
743                required_site_properties.iter().all(|key| site.contains_key(key.as_str().unwrap()))
744            );
745        }
746    }
747
748    #[test]
749    fn symbolic_counterexample_artifact_serializes_zero_quantities_compactly() {
750        let symbolic = SymbolicResult::pass(&SymbolicConfig::default(), SymbolicStats::default());
751        let call = SymbolicCounterexampleCall {
752            warp: Some(U256::ZERO),
753            roll: Some(U256::ZERO),
754            sender: Address::ZERO,
755            target: Address::ZERO,
756            calldata: Bytes::from_static(&[0x12, 0x34, 0x56, 0x78]),
757            value: Some(U256::ZERO),
758            contract_name: Some("Target".to_string()),
759            function_name: Some("step".to_string()),
760            signature: Some("step()".to_string()),
761            args: Some(String::new()),
762            raw_args: Some(String::new()),
763        };
764        let artifact = SymbolicCounterexampleArtifact::new(
765            SymbolicCounterexampleArtifactKind::Sequence,
766            SymbolicCounterexampleTestIdentity {
767                contract: "InvariantTest".to_string(),
768                test: "invariant_counter()".to_string(),
769            },
770            &symbolic,
771            SymbolicCounterexampleReplaySemantics { fail_on_revert: false },
772            vec![call],
773        );
774
775        let value = serde_json::to_value(artifact).unwrap();
776        assert_eq!(value["calls"][0]["warp"], "0x0");
777        assert_eq!(value["calls"][0]["roll"], "0x0");
778        assert_eq!(value["calls"][0]["value"], "0x0");
779        assert_counterexample_artifact_shape(&value);
780    }
781}
782
783/// A set of test results for a single test suite, which is all the tests in a single contract.
784#[derive(Clone, Debug, Serialize)]
785pub struct SuiteResult {
786    /// Wall clock time it took to execute all tests in this suite.
787    #[serde(with = "foundry_common::serde_helpers::duration")]
788    pub duration: Duration,
789    /// Individual test results: `test fn signature -> TestResult`.
790    pub test_results: BTreeMap<String, TestResult>,
791    /// Generated warnings.
792    pub warnings: Vec<String>,
793}
794
795impl SuiteResult {
796    pub fn new(
797        duration: Duration,
798        test_results: BTreeMap<String, TestResult>,
799        mut warnings: Vec<String>,
800    ) -> Self {
801        // Add deprecated cheatcodes warning, if any of them used in current test suite.
802        let mut deprecated_cheatcodes = HashMap::new();
803        for test_result in test_results.values() {
804            deprecated_cheatcodes.extend(test_result.deprecated_cheatcodes.clone());
805        }
806        if !deprecated_cheatcodes.is_empty() {
807            let mut warning =
808                "the following cheatcode(s) are deprecated and will be removed in future versions:"
809                    .to_string();
810            for (cheatcode, reason) in deprecated_cheatcodes {
811                write!(warning, "\n  {cheatcode}").unwrap();
812                if let Some(reason) = reason {
813                    write!(warning, ": {reason}").unwrap();
814                }
815            }
816            warnings.push(warning);
817        }
818
819        Self { duration, test_results, warnings }
820    }
821
822    /// Returns an iterator over all individual succeeding tests and their names.
823    pub fn successes(&self) -> impl Iterator<Item = (&String, &TestResult)> {
824        self.tests().filter(|(_, t)| t.status.is_success())
825    }
826
827    /// Returns an iterator over all individual skipped tests and their names.
828    pub fn skips(&self) -> impl Iterator<Item = (&String, &TestResult)> {
829        self.tests().filter(|(_, t)| t.status.is_skipped())
830    }
831
832    /// Returns an iterator over all individual failing tests and their names.
833    pub fn failures(&self) -> impl Iterator<Item = (&String, &TestResult)> {
834        self.tests().filter(|(_, t)| t.status.is_failure())
835    }
836
837    /// Returns the number of tests that passed.
838    pub fn passed(&self) -> usize {
839        self.test_results.values().map(TestResult::passed_count).sum()
840    }
841
842    /// Returns the number of tests that were skipped.
843    pub fn skipped(&self) -> usize {
844        self.test_results.values().map(TestResult::skipped_count).sum()
845    }
846
847    /// Returns the number of tests that failed.
848    pub fn failed(&self) -> usize {
849        self.test_results.values().map(TestResult::failed_count).sum()
850    }
851
852    /// Iterator over all tests and their names
853    pub fn tests(&self) -> impl Iterator<Item = (&String, &TestResult)> {
854        self.test_results.iter()
855    }
856
857    /// Whether this test suite is empty.
858    pub fn is_empty(&self) -> bool {
859        self.test_results.is_empty()
860    }
861
862    /// The number of tests in this test suite.
863    pub fn len(&self) -> usize {
864        self.test_results.values().map(TestResult::logical_count).sum()
865    }
866
867    /// Sums up all the durations of all individual tests in this suite.
868    ///
869    /// Note that this is not necessarily the wall clock time of the entire test suite.
870    pub fn total_time(&self) -> Duration {
871        self.test_results.values().map(|result| result.duration).sum()
872    }
873
874    /// Returns the summary of a single test suite.
875    pub fn summary(&self) -> String {
876        let failed = self.failed();
877        let result = if failed == 0 { "ok".green() } else { "FAILED".red() };
878        format!(
879            "Suite result: {}. {} passed; {} failed; {} skipped; finished in {:.2?} ({:.2?} CPU time)",
880            result,
881            self.passed().green(),
882            failed.red(),
883            self.skipped().yellow(),
884            self.duration,
885            self.total_time(),
886        )
887    }
888}
889
890/// The result of a single test in a test suite.
891///
892/// This is flattened from a [`TestOutcome`].
893#[derive(Clone, Debug)]
894pub struct SuiteTestResult {
895    /// The identifier of the artifact/contract in the form:
896    /// `<artifact file name>:<contract name>`.
897    pub artifact_id: String,
898    /// The function signature of the Solidity test.
899    pub signature: String,
900    /// The result of the executed test.
901    pub result: TestResult,
902}
903
904impl SuiteTestResult {
905    /// Returns the gas used by the test.
906    pub fn gas_used(&self) -> u64 {
907        self.result.kind.report().gas()
908    }
909
910    /// Returns the contract name of the artifact ID.
911    pub fn contract_name(&self) -> &str {
912        get_contract_name(&self.artifact_id)
913    }
914
915    /// Returns the file name of the artifact ID.
916    pub fn file_name(&self) -> &str {
917        get_file_name(&self.artifact_id)
918    }
919}
920
921/// The status of a test.
922#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
923pub enum TestStatus {
924    Success,
925    #[default]
926    Failure,
927    Skipped,
928}
929
930impl TestStatus {
931    /// Returns `true` if the test was successful.
932    #[inline]
933    pub const fn is_success(self) -> bool {
934        matches!(self, Self::Success)
935    }
936
937    /// Returns `true` if the test failed.
938    #[inline]
939    pub const fn is_failure(self) -> bool {
940        matches!(self, Self::Failure)
941    }
942
943    /// Returns `true` if the test was skipped.
944    #[inline]
945    pub const fn is_skipped(self) -> bool {
946        matches!(self, Self::Skipped)
947    }
948}
949
950/// A failure surfaced by an invariant test campaign — either a broken `invariant_*`
951/// predicate ([`Self::Predicate`]) or a handler-side assertion bug ([`Self::Handler`]).
952#[derive(Clone, Debug, Serialize, Deserialize)]
953#[serde(tag = "kind", rename_all = "snake_case")]
954pub enum InvariantFailure {
955    /// A broken `invariant_*` predicate.
956    Predicate {
957        /// Invariant function name (e.g. `invariant_cond3`).
958        name: String,
959        /// Revert reason or assertion failure message.
960        reason: String,
961        /// Counterexample sequence, when one is available.
962        #[serde(default, skip_serializing_if = "Option::is_none")]
963        counterexample: Option<CounterExample>,
964        /// Durable replay artifact for this counterexample, when one was written.
965        #[serde(default, skip_serializing_if = "Option::is_none")]
966        artifact: Option<SymbolicArtifactRef>,
967        /// Deterministic concrete minimization details for this sequence, when minimized.
968        #[serde(default, skip_serializing_if = "Option::is_none")]
969        minimization: Option<SymbolicCounterexampleMinimization>,
970        /// Path where the counterexample was persisted for re-running and shrinking.
971        persisted_path: std::path::PathBuf,
972        /// Whether this failure is the stable campaign anchor.
973        /// When `true` and this is the only single-predicate failure, the function name is
974        /// omitted on the `[FAIL: ...]` line (the trailing summary already identifies it).
975        #[serde(default)]
976        is_anchor: bool,
977    },
978    /// A handler-side assertion bug discovered during the campaign.
979    Handler {
980        /// Best-effort human-readable name of the failing call, e.g. `Counter::increment` or
981        /// `0xabc...::0x12345678` when the contract/function cannot be resolved.
982        name: String,
983        /// Address of the handler whose call asserted/reverted with an assertion.
984        reverter: Address,
985        /// 4-byte selector of the failing handler function.
986        selector: Selector,
987        /// Decoded revert/assert reason.
988        reason: String,
989        /// Counterexample sequence leading up to (and including) the failing call.
990        #[serde(default, skip_serializing_if = "Option::is_none")]
991        counterexample: Option<CounterExample>,
992        /// Durable replay artifact for this counterexample, when one was written.
993        #[serde(default, skip_serializing_if = "Option::is_none")]
994        artifact: Option<SymbolicArtifactRef>,
995    },
996}
997
998impl InvariantFailure {
999    /// Reason rendered on the `[FAIL: ...]` line.
1000    pub fn reason(&self) -> &str {
1001        match self {
1002            Self::Predicate { reason, .. } | Self::Handler { reason, .. } => reason,
1003        }
1004    }
1005
1006    /// Human-readable name (invariant fn name, or `Contract::function` for handler bugs).
1007    pub fn name(&self) -> &str {
1008        match self {
1009            Self::Predicate { name, .. } | Self::Handler { name, .. } => name,
1010        }
1011    }
1012
1013    /// Invariant predicate name, if this is a predicate failure.
1014    pub fn predicate_name(&self) -> Option<&str> {
1015        match self {
1016            Self::Predicate { name, .. } => Some(name),
1017            Self::Handler { .. } => None,
1018        }
1019    }
1020
1021    /// Counterexample sequence, when one is available.
1022    pub const fn counterexample(&self) -> Option<&CounterExample> {
1023        match self {
1024            Self::Predicate { counterexample, .. } | Self::Handler { counterexample, .. } => {
1025                counterexample.as_ref()
1026            }
1027        }
1028    }
1029
1030    /// Durable replay artifact for this failure, when one was written.
1031    pub const fn artifact(&self) -> Option<&SymbolicArtifactRef> {
1032        match self {
1033            Self::Predicate { artifact, .. } | Self::Handler { artifact, .. } => artifact.as_ref(),
1034        }
1035    }
1036
1037    /// Deterministic concrete minimization details for predicate failures.
1038    pub const fn minimization(&self) -> Option<&SymbolicCounterexampleMinimization> {
1039        match self {
1040            Self::Predicate { minimization, .. } => minimization.as_ref(),
1041            Self::Handler { .. } => None,
1042        }
1043    }
1044}
1045
1046/// Pass/fail status for an invariant predicate evaluated inside a contract-level campaign.
1047#[derive(Clone, Debug, Serialize, Deserialize)]
1048pub struct InvariantPredicateResult {
1049    /// Invariant function name (e.g. `invariant_balance`).
1050    pub name: String,
1051    /// Predicate status within the logical campaign.
1052    pub status: TestStatus,
1053    /// Revert reason or assertion message when the predicate failed.
1054    #[serde(default, skip_serializing_if = "Option::is_none")]
1055    pub reason: Option<String>,
1056}
1057
1058/// Stable machine-readable outcome for `forge test --symbolic` JSON output.
1059#[derive(Clone, Debug, Serialize, Deserialize)]
1060pub struct SymbolicResult {
1061    /// Schema version for the symbolic result object.
1062    #[serde(default = "symbolic_result_schema_version")]
1063    pub schema_version: u32,
1064    /// Normalized symbolic outcome.
1065    pub status: SymbolicResultStatus,
1066    /// Incomplete reason when [`Self::status`] is [`SymbolicResultStatus::Incomplete`].
1067    pub incomplete: Option<SymbolicIncomplete>,
1068    /// Effective bounds used by this symbolic run.
1069    pub bounds: SymbolicBounds,
1070    /// Solver identity and counters collected during this run.
1071    pub solver: SymbolicSolverMetadata,
1072    /// Soundness assumptions that bound what a `pass` proves.
1073    pub assumptions: Vec<SymbolicAssumption>,
1074    /// Where an agent can find the concrete replay trace, when one was produced.
1075    pub call_trace: SymbolicCallTrace,
1076    /// Concrete replay metadata for counterexample candidates.
1077    pub replay: SymbolicReplayMetadata,
1078    /// Concrete counterexample data, when the solver produced a candidate.
1079    pub counterexample: Option<SymbolicCounterexample>,
1080    /// Fuzz corpus seeds imported into symbolic execution, when enabled.
1081    #[serde(default, skip_serializing_if = "Option::is_none")]
1082    pub corpus_seeds: Option<SymbolicCorpusSeedMetadata>,
1083    /// Durable counterexample artifact, when one was written.
1084    #[serde(default, skip_serializing_if = "Option::is_none")]
1085    pub artifact: Option<SymbolicArtifactRef>,
1086    /// Deterministic concrete minimization details, when a replayed counterexample was minimized.
1087    #[serde(default, skip_serializing_if = "Option::is_none")]
1088    pub minimization: Option<SymbolicCounterexampleMinimization>,
1089}
1090
1091impl SymbolicResult {
1092    /// Creates a symbolic pass result.
1093    pub fn pass(config: &SymbolicConfig, stats: SymbolicStats) -> Self {
1094        Self::new(
1095            SymbolicResultStatus::Pass,
1096            config,
1097            stats,
1098            None,
1099            SymbolicReplayMetadata::not_required(),
1100            SymbolicCallTrace::none(),
1101            None,
1102        )
1103    }
1104
1105    /// Creates a symbolic counterexample result that concrete replay confirmed.
1106    pub fn fail_counterexample(
1107        config: &SymbolicConfig,
1108        stats: SymbolicStats,
1109        call_trace: SymbolicCallTrace,
1110        counterexample: SymbolicCounterexample,
1111    ) -> Self {
1112        Self::new(
1113            SymbolicResultStatus::FailCounterexample,
1114            config,
1115            stats,
1116            None,
1117            SymbolicReplayMetadata::confirmed(),
1118            call_trace,
1119            Some(counterexample),
1120        )
1121    }
1122
1123    /// Creates a symbolic sequence counterexample result that concrete replay confirmed.
1124    pub fn fail_counterexample_sequence(
1125        config: &SymbolicConfig,
1126        stats: SymbolicStats,
1127        call_trace: SymbolicCallTrace,
1128    ) -> Self {
1129        Self::new(
1130            SymbolicResultStatus::FailCounterexample,
1131            config,
1132            stats,
1133            None,
1134            SymbolicReplayMetadata::confirmed(),
1135            call_trace,
1136            None,
1137        )
1138    }
1139
1140    /// Creates an incomplete symbolic result.
1141    pub fn incomplete(
1142        config: &SymbolicConfig,
1143        kind: SymbolicStopReason,
1144        reason: impl Into<String>,
1145        stats: SymbolicStats,
1146        replay: SymbolicReplayMetadata,
1147        call_trace: SymbolicCallTrace,
1148        counterexample: Option<SymbolicCounterexample>,
1149    ) -> Self {
1150        Self::new(
1151            SymbolicResultStatus::Incomplete,
1152            config,
1153            stats,
1154            Some(SymbolicIncomplete::new(kind, reason)),
1155            replay,
1156            call_trace,
1157            counterexample,
1158        )
1159    }
1160
1161    fn new(
1162        status: SymbolicResultStatus,
1163        config: &SymbolicConfig,
1164        stats: SymbolicStats,
1165        incomplete: Option<SymbolicIncomplete>,
1166        replay: SymbolicReplayMetadata,
1167        call_trace: SymbolicCallTrace,
1168        counterexample: Option<SymbolicCounterexample>,
1169    ) -> Self {
1170        Self {
1171            schema_version: SYMBOLIC_RESULT_SCHEMA_VERSION,
1172            status,
1173            incomplete,
1174            bounds: SymbolicBounds::from_config(config),
1175            solver: SymbolicSolverMetadata::from_config_and_stats(config, stats),
1176            assumptions: SymbolicAssumption::default_assumptions(),
1177            call_trace,
1178            replay,
1179            counterexample,
1180            corpus_seeds: None,
1181            artifact: None,
1182            minimization: None,
1183        }
1184    }
1185
1186    /// Attaches fuzz corpus import metadata to this symbolic result.
1187    pub fn with_corpus_seeds(mut self, corpus_seeds: SymbolicCorpusSeedMetadata) -> Self {
1188        self.corpus_seeds = Some(corpus_seeds);
1189        self
1190    }
1191
1192    /// Attaches a durable replay artifact reference to this symbolic result.
1193    pub fn with_artifact(mut self, artifact: SymbolicArtifactRef) -> Self {
1194        self.artifact = Some(artifact);
1195        self
1196    }
1197
1198    /// Attaches deterministic minimization metadata to this symbolic result.
1199    pub fn with_minimization(mut self, minimization: SymbolicCounterexampleMinimization) -> Self {
1200        self.minimization = Some(minimization);
1201        self
1202    }
1203}
1204
1205/// Fuzz corpus import metadata for a symbolic run.
1206#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1207pub struct SymbolicCorpusSeedMetadata {
1208    /// Corpus root used for the current test, after contract/test path expansion.
1209    pub corpus_dir: Option<std::path::PathBuf>,
1210    /// Maximum imported seeds allowed by configuration.
1211    pub limit: usize,
1212    /// Number of corpus files considered.
1213    pub loaded: usize,
1214    /// Number of corpus files skipped because they were unreadable or not a matching single call.
1215    pub skipped: usize,
1216    /// Seeds modeled by symbolic execution as path-priority hints.
1217    pub used: Vec<SymbolicCorpusSeedRef>,
1218}
1219
1220/// One fuzz corpus seed modeled by symbolic execution.
1221#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1222pub struct SymbolicCorpusSeedRef {
1223    /// Corpus file path.
1224    pub path: std::path::PathBuf,
1225    /// ABI-encoded calldata imported from the corpus file.
1226    pub calldata: Bytes,
1227}
1228
1229/// Reference to a durable symbolic counterexample artifact.
1230#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1231pub struct SymbolicArtifactRef {
1232    /// Artifact schema id.
1233    pub schema: String,
1234    /// Path to the artifact file.
1235    pub path: std::path::PathBuf,
1236}
1237
1238impl SymbolicArtifactRef {
1239    /// Creates a reference to a symbolic counterexample artifact.
1240    pub fn new(path: impl Into<std::path::PathBuf>) -> Self {
1241        Self { schema: SYMBOLIC_COUNTEREXAMPLE_ARTIFACT_SCHEMA.to_string(), path: path.into() }
1242    }
1243}
1244
1245/// Reference to a generated Solidity regression test for a symbolic counterexample.
1246#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1247pub struct SymbolicRegressionRef {
1248    /// Source counterexample artifact path.
1249    pub artifact: std::path::PathBuf,
1250    /// Generated Solidity regression test path.
1251    pub path: std::path::PathBuf,
1252}
1253
1254/// Before/after artifact references and counters for concrete symbolic counterexample minimization.
1255#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1256#[serde(deny_unknown_fields)]
1257pub struct SymbolicCounterexampleMinimization {
1258    /// Original confirmed replay artifact before minimization.
1259    pub original: SymbolicArtifactRef,
1260    /// Minimized confirmed replay artifact after minimization.
1261    pub minimized: SymbolicArtifactRef,
1262    /// Number of concrete replay candidates tried.
1263    pub attempts: usize,
1264    /// Number of replay candidates accepted.
1265    pub accepted: usize,
1266    /// ABI calldata byte length before minimization.
1267    pub original_calldata_bytes: usize,
1268    /// ABI calldata byte length after minimization.
1269    pub minimized_calldata_bytes: usize,
1270    /// Stateful sequence length before minimization, when this minimized a sequence.
1271    #[serde(default, skip_serializing_if = "Option::is_none")]
1272    pub original_sequence_len: Option<usize>,
1273    /// Stateful sequence length after minimization, when this minimized a sequence.
1274    #[serde(default, skip_serializing_if = "Option::is_none")]
1275    pub minimized_sequence_len: Option<usize>,
1276}
1277
1278impl SymbolicCounterexampleMinimization {
1279    /// Creates concrete minimization metadata.
1280    pub const fn new(
1281        original: SymbolicArtifactRef,
1282        minimized: SymbolicArtifactRef,
1283        attempts: usize,
1284        accepted: usize,
1285        original_calldata_bytes: usize,
1286        minimized_calldata_bytes: usize,
1287    ) -> Self {
1288        Self {
1289            original,
1290            minimized,
1291            attempts,
1292            accepted,
1293            original_calldata_bytes,
1294            minimized_calldata_bytes,
1295            original_sequence_len: None,
1296            minimized_sequence_len: None,
1297        }
1298    }
1299
1300    /// Adds stateful sequence lengths to minimization metadata.
1301    pub const fn with_sequence_lengths(
1302        mut self,
1303        original_sequence_len: usize,
1304        minimized_sequence_len: usize,
1305    ) -> Self {
1306        self.original_sequence_len = Some(original_sequence_len);
1307        self.minimized_sequence_len = Some(minimized_sequence_len);
1308        self
1309    }
1310}
1311
1312/// Normalized symbolic outcome names for agents and other JSON consumers.
1313#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1314#[serde(rename_all = "snake_case")]
1315pub enum SymbolicResultStatus {
1316    /// All explored paths completed without a feasible failure.
1317    Pass,
1318    /// A solver counterexample was replayed concretely and still failed.
1319    FailCounterexample,
1320    /// The engine stopped before a proof or replayed counterexample.
1321    Incomplete,
1322}
1323
1324/// Incomplete symbolic run reason.
1325#[derive(Clone, Debug, Serialize, Deserialize)]
1326pub struct SymbolicIncomplete {
1327    /// Stable reason kind.
1328    pub kind: String,
1329    /// Human-readable detail.
1330    pub reason: String,
1331}
1332
1333impl SymbolicIncomplete {
1334    fn new(kind: SymbolicStopReason, reason: impl Into<String>) -> Self {
1335        Self { kind: symbolic_stop_reason_kind(kind).to_string(), reason: reason.into() }
1336    }
1337}
1338
1339const fn symbolic_stop_reason_kind(kind: SymbolicStopReason) -> &'static str {
1340    match kind {
1341        SymbolicStopReason::Stuck => "stuck",
1342        SymbolicStopReason::RevertAll => "revert_all",
1343        SymbolicStopReason::Timeout => "timeout",
1344        SymbolicStopReason::Error => "error",
1345    }
1346}
1347
1348/// Effective symbolic exploration bounds used by the run.
1349#[derive(Clone, Debug, Serialize, Deserialize)]
1350pub struct SymbolicBounds {
1351    /// Optional solver timeout in seconds.
1352    pub timeout_seconds: Option<u32>,
1353    /// Optional loop-unrolling bound.
1354    pub loop_bound: Option<u32>,
1355    /// Effective per-path opcode depth limit.
1356    pub max_depth: u32,
1357    /// Effective symbolic path width limit.
1358    pub max_paths: u32,
1359    /// Maximum calls in a bounded symbolic invariant sequence.
1360    pub invariant_depth: u32,
1361    /// Pending path exploration order.
1362    pub exploration_order: SymbolicExplorationOrder,
1363    /// Maximum normalized solver queries.
1364    pub max_solver_queries: u32,
1365    /// Default bounded length for dynamic ABI inputs.
1366    pub default_dynamic_length: u32,
1367    /// Maximum permitted bounded dynamic ABI input length.
1368    pub max_dynamic_length: u32,
1369    /// Positional dynamic-leaf bounded lengths.
1370    pub array_lengths: Vec<u32>,
1371    /// Named dynamic-leaf bounded lengths.
1372    pub dynamic_lengths: BTreeMap<String, Vec<u32>>,
1373    /// Default array lengths when no explicit dynamic length exists.
1374    pub default_array_lengths: Vec<u32>,
1375    /// Default bytes/string lengths when no explicit dynamic length exists.
1376    pub default_bytes_lengths: Vec<u32>,
1377    /// Maximum generated symbolic calldata size in bytes.
1378    pub max_calldata_bytes: u32,
1379    /// Whether symbolic call targets can range over known deployed contracts.
1380    pub symbolic_call_targets: bool,
1381    /// Storage modelling mode.
1382    pub storage_layout: SymbolicStorageLayout,
1383}
1384
1385impl SymbolicBounds {
1386    fn from_config(config: &SymbolicConfig) -> Self {
1387        Self {
1388            timeout_seconds: config.timeout,
1389            loop_bound: config.loop_bound,
1390            max_depth: config.execution_depth(),
1391            max_paths: config.path_width(),
1392            invariant_depth: config.invariant_depth,
1393            exploration_order: config.exploration_order,
1394            max_solver_queries: config.max_solver_queries,
1395            default_dynamic_length: config.default_dynamic_length,
1396            max_dynamic_length: config.max_dynamic_length,
1397            array_lengths: config.array_lengths.clone(),
1398            dynamic_lengths: config.dynamic_lengths.clone(),
1399            default_array_lengths: config.default_array_lengths.clone(),
1400            default_bytes_lengths: config.default_bytes_lengths.clone(),
1401            max_calldata_bytes: config.max_calldata_bytes,
1402            symbolic_call_targets: config.symbolic_call_targets,
1403            storage_layout: config.storage_layout,
1404        }
1405    }
1406}
1407
1408/// Solver identity and counters.
1409#[derive(Clone, Debug, Serialize, Deserialize)]
1410pub struct SymbolicSolverMetadata {
1411    /// Configured solver name.
1412    pub name: String,
1413    /// Exact configured solver command, when set.
1414    pub command: Option<String>,
1415    /// Configured solver portfolio entries, when any.
1416    pub portfolio: Vec<String>,
1417    /// Run counters.
1418    pub stats: SymbolicSolverStats,
1419}
1420
1421impl SymbolicSolverMetadata {
1422    fn from_config_and_stats(config: &SymbolicConfig, stats: SymbolicStats) -> Self {
1423        Self {
1424            name: config.solver.clone(),
1425            command: config.solver_command.clone(),
1426            portfolio: config.solver_portfolio.clone(),
1427            stats: SymbolicSolverStats::from(stats),
1428        }
1429    }
1430}
1431
1432/// Symbolic engine and solver counters.
1433#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1434pub struct SymbolicSolverStats {
1435    /// Number of explored symbolic paths.
1436    pub paths: usize,
1437    /// Number of normalized solver queries issued during the run.
1438    pub solver_queries: usize,
1439    /// Number of queries sent to the SMT backend after local fast paths.
1440    pub smt_queries: usize,
1441    /// Number of satisfiability checks requested by the executor.
1442    pub sat_queries: usize,
1443    /// Number of concrete model requests requested by the executor.
1444    pub model_queries: usize,
1445    /// Number of satisfiability checks served from the normalized cache.
1446    pub sat_cache_hits: usize,
1447    /// Number of model requests served from the normalized model cache.
1448    pub model_cache_hits: usize,
1449    /// Number of satisfiable witnesses produced by local hard-arithmetic search.
1450    pub heuristic_witnesses: usize,
1451    /// Wall-clock time spent waiting on backend solver subprocesses, in milliseconds.
1452    pub solver_time_ms: u64,
1453    /// Total SMT-LIB input bytes sent to backend solver subprocesses.
1454    #[serde(default)]
1455    pub smt_input_bytes: u64,
1456    /// Largest single SMT-LIB query input sent to a backend solver subprocess, in bytes.
1457    #[serde(default)]
1458    pub smt_max_query_bytes: u64,
1459    /// Wall-clock time spent building SMT-LIB query strings, in milliseconds.
1460    #[serde(default)]
1461    pub smt_build_time_ms: u64,
1462    /// Longest single backend solver subprocess query, in milliseconds.
1463    #[serde(default)]
1464    pub smt_max_query_time_ms: u64,
1465}
1466
1467impl From<SymbolicStats> for SymbolicSolverStats {
1468    fn from(stats: SymbolicStats) -> Self {
1469        Self {
1470            paths: stats.paths,
1471            solver_queries: stats.solver_queries,
1472            smt_queries: stats.smt_queries,
1473            sat_queries: stats.sat_queries,
1474            model_queries: stats.model_queries,
1475            sat_cache_hits: stats.sat_cache_hits,
1476            model_cache_hits: stats.model_cache_hits,
1477            heuristic_witnesses: stats.heuristic_witnesses,
1478            solver_time_ms: stats.solver_time_ms,
1479            smt_input_bytes: stats.smt_input_bytes,
1480            smt_max_query_bytes: stats.smt_max_query_bytes,
1481            smt_build_time_ms: stats.smt_build_time_ms,
1482            smt_max_query_time_ms: stats.smt_max_query_time_ms,
1483        }
1484    }
1485}
1486
1487/// Explicit symbolic assumption attached to a result.
1488#[derive(Clone, Debug, Serialize, Deserialize)]
1489pub struct SymbolicAssumption {
1490    /// Stable assumption kind.
1491    pub kind: String,
1492    /// Human-readable detail.
1493    pub description: String,
1494}
1495
1496impl SymbolicAssumption {
1497    fn default_assumptions() -> Vec<Self> {
1498        vec![
1499            Self {
1500                kind: "bounded_exploration".to_string(),
1501                description: "Result is scoped to the configured path, depth, solver-query, loop, calldata, and dynamic-length bounds.".to_string(),
1502            },
1503            Self {
1504                kind: "hash_model".to_string(),
1505                description: "Symbolic Keccak and hash-like precompile reasoning assumes collision and preimage resistance for modeled cases.".to_string(),
1506            },
1507        ]
1508    }
1509}
1510
1511/// Concrete replay trace locator.
1512#[derive(Clone, Debug, Serialize, Deserialize)]
1513pub struct SymbolicCallTrace {
1514    /// Whether replay produced a trace that may be present in this test result.
1515    pub available: bool,
1516    /// JSON location for the trace when available.
1517    pub source: Option<String>,
1518    /// Trace format at the source location.
1519    pub format: Option<String>,
1520}
1521
1522impl SymbolicCallTrace {
1523    /// No concrete trace was produced.
1524    pub const fn none() -> Self {
1525        Self { available: false, source: None, format: None }
1526    }
1527
1528    /// A concrete replay trace may be available in the normal test result traces field.
1529    pub fn test_result_traces(available: bool) -> Self {
1530        if !available {
1531            return Self::none();
1532        }
1533
1534        Self {
1535            available: true,
1536            source: Some("test_result.traces".to_string()),
1537            format: Some("foundry_call_trace_arena".to_string()),
1538        }
1539    }
1540}
1541
1542/// Counterexample replay status.
1543#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1544#[serde(rename_all = "snake_case")]
1545pub enum SymbolicReplayStatus {
1546    /// No replay was required for this result.
1547    NotRequired,
1548    /// Concrete replay confirmed the symbolic counterexample.
1549    Confirmed,
1550    /// Concrete replay did not reproduce the symbolic counterexample.
1551    Mismatch,
1552    /// Concrete replay could not execute because of an error.
1553    Error,
1554    /// Concrete replay was skipped by `vm.skip`.
1555    Skipped,
1556}
1557
1558/// Replay metadata for symbolic counterexample candidates.
1559#[derive(Clone, Debug, Serialize, Deserialize)]
1560#[serde(deny_unknown_fields)]
1561pub struct SymbolicReplayMetadata {
1562    /// Whether the symbolic outcome required concrete replay.
1563    pub required: bool,
1564    /// Stable replay status.
1565    pub status: SymbolicReplayStatus,
1566    /// Optional replay detail or mismatch reason.
1567    pub reason: Option<String>,
1568}
1569
1570impl SymbolicReplayMetadata {
1571    /// No replay was required.
1572    pub const fn not_required() -> Self {
1573        Self { required: false, status: SymbolicReplayStatus::NotRequired, reason: None }
1574    }
1575
1576    /// Concrete replay confirmed the counterexample.
1577    pub const fn confirmed() -> Self {
1578        Self { required: true, status: SymbolicReplayStatus::Confirmed, reason: None }
1579    }
1580
1581    /// Concrete replay did not reproduce the symbolic counterexample.
1582    pub fn mismatch(reason: impl Into<String>) -> Self {
1583        Self { required: true, status: SymbolicReplayStatus::Mismatch, reason: Some(reason.into()) }
1584    }
1585
1586    /// Concrete replay errored before the candidate could be confirmed.
1587    pub fn error(reason: impl Into<String>) -> Self {
1588        Self { required: true, status: SymbolicReplayStatus::Error, reason: Some(reason.into()) }
1589    }
1590
1591    /// Concrete replay was skipped by the test.
1592    pub fn skipped(reason: impl Into<String>) -> Self {
1593        Self { required: true, status: SymbolicReplayStatus::Skipped, reason: Some(reason.into()) }
1594    }
1595}
1596
1597/// Stable symbolic counterexample payload.
1598#[derive(Clone, Debug, Serialize, Deserialize)]
1599pub struct SymbolicCounterexample {
1600    /// ABI-encoded calldata for replay.
1601    pub calldata: Bytes,
1602    /// Pretty-formatted ABI arguments, when decoded.
1603    pub args: Option<String>,
1604    /// Raw ABI arguments, when decoded.
1605    pub raw_args: Option<String>,
1606    /// Ether value sent with the call, when any.
1607    pub value: Option<U256>,
1608}
1609
1610impl From<&BaseCounterExample> for SymbolicCounterexample {
1611    fn from(counterexample: &BaseCounterExample) -> Self {
1612        Self {
1613            calldata: counterexample.calldata.clone(),
1614            args: counterexample.args.clone(),
1615            raw_args: counterexample.raw_args.clone(),
1616            value: counterexample.value,
1617        }
1618    }
1619}
1620
1621/// Durable symbolic counterexample artifact.
1622#[derive(Clone, Debug, Serialize, Deserialize)]
1623#[serde(deny_unknown_fields)]
1624pub struct SymbolicCounterexampleArtifact {
1625    /// Artifact schema version.
1626    pub schema_version: u32,
1627    /// Artifact schema id.
1628    pub schema: String,
1629    /// Whether this counterexample is a single test call or a stateful sequence.
1630    pub kind: SymbolicCounterexampleArtifactKind,
1631    /// Test identity that produced this counterexample.
1632    pub test: SymbolicCounterexampleTestIdentity,
1633    /// Concrete replay metadata for the counterexample candidate.
1634    pub replay: SymbolicReplayMetadata,
1635    /// Replay semantics that must remain stable when this artifact is replayed.
1636    pub replay_semantics: SymbolicCounterexampleReplaySemantics,
1637    /// Effective bounds used by this symbolic run.
1638    pub bounds: SymbolicBounds,
1639    /// Solver identity and counters collected during this run.
1640    pub solver: SymbolicSolverMetadata,
1641    /// Soundness assumptions that bound what a `pass` proves.
1642    pub assumptions: Vec<SymbolicAssumption>,
1643    /// Where an agent can find the concrete replay trace, when one was produced.
1644    pub call_trace: SymbolicCallTrace,
1645    /// Concrete setup-storage assignments required before replaying this artifact.
1646    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1647    pub storage: Vec<SymbolicStorageAssignment>,
1648    /// Stateful invariant failure origin, when this sequence came from symbolic invariants.
1649    #[serde(default, skip_serializing_if = "Option::is_none")]
1650    pub invariant_failure: Option<SymbolicInvariantArtifactFailure>,
1651    /// Concrete replay calls.
1652    pub calls: Vec<SymbolicCounterexampleCall>,
1653}
1654
1655impl SymbolicCounterexampleArtifact {
1656    /// Creates a durable symbolic counterexample artifact from a symbolic result and call list.
1657    pub fn new(
1658        kind: SymbolicCounterexampleArtifactKind,
1659        test: SymbolicCounterexampleTestIdentity,
1660        symbolic: &SymbolicResult,
1661        replay_semantics: SymbolicCounterexampleReplaySemantics,
1662        calls: Vec<SymbolicCounterexampleCall>,
1663    ) -> Self {
1664        Self {
1665            schema_version: SYMBOLIC_COUNTEREXAMPLE_ARTIFACT_SCHEMA_VERSION,
1666            schema: SYMBOLIC_COUNTEREXAMPLE_ARTIFACT_SCHEMA.to_string(),
1667            kind,
1668            test,
1669            replay: symbolic.replay.clone(),
1670            replay_semantics,
1671            bounds: symbolic.bounds.clone(),
1672            solver: symbolic.solver.clone(),
1673            assumptions: symbolic.assumptions.clone(),
1674            call_trace: symbolic.call_trace.clone(),
1675            storage: Vec::new(),
1676            invariant_failure: None,
1677            calls,
1678        }
1679    }
1680
1681    /// Attaches setup-storage assignments required for concrete replay.
1682    pub fn with_storage(mut self, storage: Vec<SymbolicStorageAssignment>) -> Self {
1683        self.storage = storage;
1684        self
1685    }
1686
1687    /// Attaches stateful invariant failure origin metadata.
1688    pub fn with_invariant_failure(
1689        mut self,
1690        invariant_failure: SymbolicInvariantArtifactFailure,
1691    ) -> Self {
1692        self.invariant_failure = Some(invariant_failure);
1693        self
1694    }
1695}
1696
1697/// Concrete replay semantics captured when a symbolic artifact is confirmed.
1698#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
1699#[serde(deny_unknown_fields)]
1700pub struct SymbolicCounterexampleReplaySemantics {
1701    /// Whether an invariant sequence replay treats any target-call revert as a failure.
1702    pub fail_on_revert: bool,
1703}
1704
1705/// Symbolic counterexample artifact shape.
1706#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1707#[serde(rename_all = "snake_case")]
1708pub enum SymbolicCounterexampleArtifactKind {
1709    /// A single stateless symbolic test call.
1710    SingleCall,
1711    /// A stateful sequence of calls.
1712    Sequence,
1713}
1714
1715/// Stateful invariant failure origin for a persisted symbolic sequence artifact.
1716#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1717#[serde(tag = "kind", rename_all = "snake_case")]
1718pub enum SymbolicInvariantArtifactFailure {
1719    /// An invariant predicate failed.
1720    Predicate {
1721        /// Invariant function name.
1722        name: String,
1723        /// Exact concrete failure site confirmed during replay.
1724        #[serde(default, skip_serializing_if = "Option::is_none")]
1725        site: Option<SymbolicInvariantFailureSite>,
1726    },
1727    /// A target/handler call asserted before an invariant predicate failed.
1728    Handler {
1729        /// Best-effort human-readable handler function name.
1730        #[serde(default, skip_serializing_if = "Option::is_none")]
1731        name: Option<String>,
1732        /// Address of the handler whose call asserted.
1733        reverter: Address,
1734        /// 4-byte selector of the failing handler call.
1735        selector: Selector,
1736        /// Stable edge fingerprint for the failing handler site.
1737        fingerprint: B256,
1738    },
1739}
1740
1741/// Concrete invariant failure site stored in symbolic replay artifacts.
1742#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1743#[serde(tag = "kind", rename_all = "snake_case")]
1744pub enum SymbolicInvariantFailureSite {
1745    /// Target/handler call failed before the invariant predicate.
1746    SequenceCall { target: Address, selector: Selector, fingerprint: B256 },
1747    /// Invariant predicate failed.
1748    Invariant { target: Address, selector: Selector, fingerprint: B256 },
1749    /// `afterInvariant` hook failed.
1750    AfterInvariant { target: Address, selector: Selector, fingerprint: B256 },
1751}
1752
1753/// Test identity for a symbolic counterexample artifact.
1754#[derive(Clone, Debug, Serialize, Deserialize)]
1755#[serde(deny_unknown_fields)]
1756pub struct SymbolicCounterexampleTestIdentity {
1757    /// Contract identifier as reported by Forge.
1758    pub contract: String,
1759    /// Test function signature.
1760    pub test: String,
1761}
1762
1763/// One concrete call in a symbolic counterexample artifact.
1764#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1765#[serde(deny_unknown_fields)]
1766pub struct SymbolicCounterexampleCall {
1767    /// Amount to increase block timestamp before executing the call.
1768    pub warp: Option<U256>,
1769    /// Amount to increase block number before executing the call.
1770    pub roll: Option<U256>,
1771    /// Sender used for the call.
1772    pub sender: Address,
1773    /// Target address called.
1774    pub target: Address,
1775    /// ABI-encoded calldata for replay.
1776    pub calldata: Bytes,
1777    /// Ether value sent with the call, when any.
1778    pub value: Option<U256>,
1779    /// Human-readable contract identifier, when known.
1780    pub contract_name: Option<String>,
1781    /// ABI function name, when known.
1782    pub function_name: Option<String>,
1783    /// ABI function signature, when known.
1784    pub signature: Option<String>,
1785    /// Pretty-formatted ABI arguments, when decoded.
1786    pub args: Option<String>,
1787    /// Raw ABI arguments, when decoded.
1788    pub raw_args: Option<String>,
1789}
1790
1791impl SymbolicCounterexampleCall {
1792    /// Creates an artifact call from Foundry's base counterexample shape.
1793    pub fn from_base_counterexample(
1794        counterexample: &BaseCounterExample,
1795        default_sender: Address,
1796        default_target: Address,
1797    ) -> Self {
1798        Self {
1799            warp: counterexample.warp,
1800            roll: counterexample.roll,
1801            sender: counterexample.sender.unwrap_or(default_sender),
1802            target: counterexample.addr.unwrap_or(default_target),
1803            calldata: counterexample.calldata.clone(),
1804            value: counterexample.value,
1805            contract_name: counterexample.contract_name.clone(),
1806            function_name: counterexample.func_name.clone(),
1807            signature: counterexample.signature.clone(),
1808            args: counterexample.args.clone(),
1809            raw_args: counterexample.raw_args.clone(),
1810        }
1811    }
1812
1813    /// Creates Foundry's display counterexample shape from an artifact call.
1814    pub fn to_base_counterexample(&self) -> BaseCounterExample {
1815        BaseCounterExample {
1816            warp: self.warp,
1817            roll: self.roll,
1818            sender: Some(self.sender),
1819            addr: Some(self.target),
1820            calldata: self.calldata.clone(),
1821            value: self.value,
1822            contract_name: self.contract_name.clone(),
1823            func_name: self.function_name.clone(),
1824            signature: self.signature.clone(),
1825            args: self.args.clone(),
1826            raw_args: self.raw_args.clone(),
1827            traces: None,
1828            show_solidity: false,
1829            fuzz: Default::default(),
1830        }
1831    }
1832
1833    /// Converts an artifact call into Foundry's invariant replay transaction shape.
1834    pub fn to_basic_tx_details(&self) -> BasicTxDetails {
1835        BasicTxDetails {
1836            warp: self.warp,
1837            roll: self.roll,
1838            sender: self.sender,
1839            call_details: CallDetails {
1840                target: self.target,
1841                calldata: self.calldata.clone(),
1842                value: self.value,
1843            },
1844        }
1845    }
1846}
1847
1848/// The result of an executed test.
1849#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1850pub struct TestResult {
1851    /// The test status, indicating whether the test case succeeded, failed, or was marked as
1852    /// skipped. This means that the transaction executed properly, the test was marked as
1853    /// skipped with vm.skip(), or that there was a revert and that the test was expected to
1854    /// fail (prefixed with `testFail`)
1855    pub status: TestStatus,
1856
1857    /// If there was a revert, this field will be populated. Note that the test can
1858    /// still be successful (i.e self.success == true) when it's expected to fail.
1859    pub reason: Option<String>,
1860
1861    /// All broken invariant predicates in this campaign in source declaration order.
1862    ///
1863    /// For invariant tests, this is the single source of truth used by the renderer.
1864    /// `reason` and `counterexample` are not populated for invariant tests.
1865    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1866    pub invariant_failures: Vec<InvariantFailure>,
1867
1868    /// Per-predicate outcomes for invariant campaigns. This preserves individual
1869    /// `invariant_*` / `statefulFuzz*` pass/fail reporting when multiple predicates are checked
1870    /// by one contract-level campaign.
1871    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1872    pub invariant_predicate_results: Vec<InvariantPredicateResult>,
1873
1874    /// Directory where invariant failure counterexamples have been persisted (set when one or more
1875    /// secondary invariant failures were written, so users can locate persisted counterexamples).
1876    #[serde(default, skip_serializing_if = "Option::is_none")]
1877    pub invariant_failure_dir: Option<std::path::PathBuf>,
1878
1879    /// Total number of invariant predicates exercised in this campaign. When `Some(n)` the
1880    /// user-facing report renders a contract-level `<broken>/<n> invariants broken` summary so
1881    /// users get an at-a-glance health line without counting `[FAIL]` blocks. `None` for
1882    /// single-predicate campaigns.
1883    #[serde(default, skip_serializing_if = "Option::is_none")]
1884    pub invariant_count: Option<usize>,
1885
1886    /// Handler-side assertion bugs found during the campaign, deduped by
1887    /// `(reverter, selector)` site (Medusa/Echidna semantics). Rendered in a dedicated
1888    /// `Assertion Tests` section.
1889    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1890    pub invariant_handler_failures: Vec<InvariantFailure>,
1891
1892    /// Minimal reproduction test case for failing test
1893    pub counterexample: Option<CounterExample>,
1894
1895    /// Legacy durable replay artifact for the top-level counterexample, when one was written.
1896    ///
1897    /// Prefer [`Self::counterexample_artifacts`] for new consumers; this compatibility field is
1898    /// maintained by [`Self::add_counterexample_artifact`] for older JSON readers.
1899    #[serde(default, skip_serializing_if = "Option::is_none")]
1900    pub counterexample_artifact: Option<SymbolicArtifactRef>,
1901
1902    /// All durable replay artifacts produced for this test result, normalized for JSON consumers.
1903    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1904    pub counterexample_artifacts: Vec<SymbolicArtifactRef>,
1905
1906    /// Generated Solidity regression tests for this symbolic counterexample.
1907    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1908    pub symbolic_regressions: Vec<SymbolicRegressionRef>,
1909
1910    /// Any captured & parsed as strings logs along the test's execution which should
1911    /// be printed to the user.
1912    pub logs: Vec<Log>,
1913
1914    /// The decoded DSTest logging events and Hardhat's `console.log` from [logs](Self::logs).
1915    /// Used for json output.
1916    pub decoded_logs: Vec<String>,
1917
1918    /// What kind of test this was
1919    pub kind: TestKind,
1920
1921    /// Stable symbolic result object for `forge test --symbolic --json`.
1922    #[serde(default, skip_serializing_if = "Option::is_none")]
1923    pub symbolic: Option<SymbolicResult>,
1924
1925    /// Traces
1926    pub traces: Traces,
1927
1928    /// Runtime bytecodes for contracts seen in debug traces.
1929    #[serde(skip)]
1930    pub debug_bytecodes: AddressHashMap<Bytes>,
1931
1932    /// Additional traces to use for gas report.
1933    ///
1934    /// These are cleared after the gas report is analyzed.
1935    #[serde(skip)]
1936    pub gas_report_traces: Vec<Vec<CallTraceArena>>,
1937
1938    /// Raw line coverage info
1939    #[serde(skip)]
1940    pub line_coverage: Option<HitMaps>,
1941
1942    /// Labeled addresses
1943    #[serde(rename = "labeled_addresses")] // Backwards compatibility.
1944    pub labels: AddressHashMap<String>,
1945
1946    #[serde(with = "foundry_common::serde_helpers::duration")]
1947    pub duration: Duration,
1948
1949    /// pc breakpoint char map
1950    pub breakpoints: Breakpoints,
1951
1952    /// Any captured gas snapshots along the test's execution which should be accumulated.
1953    pub gas_snapshots: BTreeMap<String, BTreeMap<String, String>>,
1954
1955    /// Deprecated cheatcodes (mapped to their replacements, if any) used in current test.
1956    #[serde(skip)]
1957    pub deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>,
1958
1959    /// Staged solver portfolio diagnostics collected during symbolic execution.
1960    #[serde(skip)]
1961    pub symbolic_portfolio_diagnostics: Option<PortfolioDiagnostics>,
1962
1963    /// Verbose symbolic solver diagnostics deferred until test output rendering.
1964    #[serde(skip)]
1965    pub symbolic_diagnostics: Option<String>,
1966}
1967
1968impl fmt::Display for TestResult {
1969    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1970        f.write_str(&self.render_status_block(false, None))
1971    }
1972}
1973
1974impl TestResult {
1975    /// Returns `true` if this failed result can be meaningfully inspected with
1976    /// `forge test --debug --match-test`.
1977    const fn is_debuggable_failure(&self) -> bool {
1978        self.status.is_failure()
1979            && !self.kind.is_invariant()
1980            && !self.kind.is_symbolic()
1981            && self.symbolic.is_none()
1982    }
1983
1984    /// Adds a durable replay artifact to the normalized list and legacy top-level field.
1985    pub fn add_counterexample_artifact(&mut self, artifact: SymbolicArtifactRef) {
1986        if !self.counterexample_artifacts.contains(&artifact) {
1987            self.counterexample_artifacts.push(artifact.clone());
1988        }
1989        if self.counterexample_artifact.is_none() {
1990            self.counterexample_artifact = Some(artifact);
1991        }
1992    }
1993
1994    fn render_status_block(
1995        &self,
1996        user_facing: bool,
1997        invariant_campaign_name: Option<&str>,
1998    ) -> String {
1999        match self.status {
2000            TestStatus::Success => {
2001                // For optimization mode, show the best example sequence in green.
2002                let mut s = String::from("[PASS]");
2003                if let Some(CounterExample::Sequence(original, sequence)) = &self.counterexample {
2004                    s.push_str(
2005                        format!(
2006                            "\n\t[Best sequence] (original: {original}, shrunk: {})\n",
2007                            sequence.len()
2008                        )
2009                        .as_str(),
2010                    );
2011                    for ex in sequence {
2012                        writeln!(s, "{ex}").unwrap();
2013                    }
2014                }
2015                self.write_invariant_predicate_results(
2016                    &mut s,
2017                    user_facing,
2018                    true,
2019                    invariant_campaign_name,
2020                );
2021                format!("{}", s.green().wrap())
2022            }
2023            TestStatus::Skipped => {
2024                let mut s = String::from("[SKIP");
2025                if let Some(reason) = &self.reason {
2026                    write!(s, ": {reason}").unwrap();
2027                }
2028                s.push(']');
2029                self.write_invariant_predicate_results(
2030                    &mut s,
2031                    user_facing,
2032                    true,
2033                    invariant_campaign_name,
2034                );
2035                format!("{}", s.yellow())
2036            }
2037            TestStatus::Failure => {
2038                let mut s = String::new();
2039                let has_handler_failures = !self.invariant_handler_failures.is_empty();
2040                let is_invariant_failure =
2041                    !self.invariant_failures.is_empty() || has_handler_failures;
2042                if !is_invariant_failure {
2043                    // Non-invariant failure (unit / fuzz / DS-style): render from the legacy
2044                    // `reason` / `counterexample` fields.
2045                    s.push_str("[FAIL");
2046                    if let Some(reason) = &self.reason {
2047                        write!(s, ": {reason}").unwrap();
2048                    }
2049                    if let Some(counterexample) = &self.counterexample {
2050                        match counterexample {
2051                            CounterExample::Single(ex) => {
2052                                write!(s, "; counterexample: {ex}]").unwrap();
2053                            }
2054                            CounterExample::Sequence(original, sequence) => {
2055                                writeln!(
2056                                    s,
2057                                    "]\n\t[Sequence] (original: {original}, shrunk: {})",
2058                                    sequence.len()
2059                                )
2060                                .unwrap();
2061                                for ex in sequence {
2062                                    writeln!(s, "{ex}").unwrap();
2063                                }
2064                            }
2065                        }
2066                    } else {
2067                        s.push(']');
2068                    }
2069                } else if !self.invariant_failures.is_empty() {
2070                    // Contract-level campaigns identify the broken predicate even when only one
2071                    // predicate failed. Preserve the compact legacy shape only for the anchor of a
2072                    // single-predicate run.
2073                    let multi = self.invariant_failures.len() > 1;
2074                    let is_campaign = self.invariant_count.is_some();
2075                    for (i, failure) in self.invariant_failures.iter().enumerate() {
2076                        if i > 0 {
2077                            s.push('\n');
2078                        }
2079                        let is_anchor =
2080                            matches!(failure, InvariantFailure::Predicate { is_anchor: true, .. });
2081                        let name_suffix = if is_campaign || multi || !is_anchor {
2082                            format!(" {}", failure.name())
2083                        } else {
2084                            String::new()
2085                        };
2086                        if let Some(CounterExample::Sequence(original, sequence)) =
2087                            failure.counterexample()
2088                        {
2089                            writeln!(
2090                                s,
2091                                "[FAIL: {}]{name_suffix}\n\t[Sequence] (original: {original}, shrunk: {})",
2092                                failure.reason(),
2093                                sequence.len()
2094                            )
2095                            .unwrap();
2096                            for ex in sequence {
2097                                writeln!(s, "{ex}").unwrap();
2098                            }
2099                        } else {
2100                            write!(s, "[FAIL: {}]{name_suffix}", failure.reason()).unwrap();
2101                        }
2102                    }
2103                }
2104
2105                let rollup_rendered = self.write_invariant_rollup(
2106                    &mut s,
2107                    user_facing,
2108                    is_invariant_failure,
2109                    invariant_campaign_name,
2110                );
2111                let show_predicate_header = if user_facing { !rollup_rendered } else { true };
2112                self.write_invariant_predicate_results(
2113                    &mut s,
2114                    user_facing,
2115                    show_predicate_header,
2116                    invariant_campaign_name,
2117                );
2118                self.write_invariant_persistence_note(&mut s);
2119                let handler_preceded = if user_facing {
2120                    rollup_rendered
2121                        || self.invariant_predicate_results.len() > 1
2122                        || !self.invariant_failures.is_empty()
2123                } else {
2124                    !self.invariant_failures.is_empty()
2125                        || matches!(self.invariant_count, Some(t) if t > 1)
2126                };
2127                self.write_handler_failures(&mut s, user_facing, handler_preceded);
2128
2129                format!("{}", s.red().wrap())
2130            }
2131        }
2132    }
2133
2134    fn write_invariant_rollup(
2135        &self,
2136        s: &mut String,
2137        user_facing: bool,
2138        is_invariant_failure: bool,
2139        invariant_campaign_name: Option<&str>,
2140    ) -> bool {
2141        let Some(total) = self.invariant_count else {
2142            return false;
2143        };
2144        if total <= 1 || !is_invariant_failure {
2145            return false;
2146        }
2147
2148        writeln!(
2149            s,
2150            "\n{}: {}/{total} invariants broken",
2151            if user_facing {
2152                invariant_campaign_name.unwrap_or(INVARIANT_CAMPAIGN_FALLBACK_NAME)
2153            } else {
2154                "Predicates"
2155            },
2156            self.invariant_failures.len()
2157        )
2158        .unwrap();
2159        true
2160    }
2161
2162    fn write_invariant_persistence_note(&self, s: &mut String) {
2163        if self.invariant_failures.len() > 1
2164            && let Some(dir) = &self.invariant_failure_dir
2165        {
2166            writeln!(
2167                s,
2168                "{} invariant failure(s) persisted to {} — rerun to shrink",
2169                self.invariant_failures.len(),
2170                dir.display()
2171            )
2172            .unwrap();
2173        }
2174    }
2175
2176    fn write_handler_failures(&self, s: &mut String, user_facing: bool, preceded: bool) {
2177        if self.invariant_handler_failures.is_empty() {
2178            return;
2179        }
2180
2181        let prefix = if preceded { "\n" } else { "" };
2182        writeln!(
2183            s,
2184            "{prefix}{}: {} assertion bug(s) found",
2185            if user_facing { "Assertion Tests" } else { "Handler assertions" },
2186            self.invariant_handler_failures.len()
2187        )
2188        .unwrap();
2189        for failure in &self.invariant_handler_failures {
2190            if let Some(CounterExample::Sequence(original, sequence)) = failure.counterexample() {
2191                writeln!(
2192                    s,
2193                    "[FAIL: {}] {}\n\t[Sequence] (original: {original}, shrunk: {})",
2194                    failure.reason(),
2195                    failure.name(),
2196                    sequence.len()
2197                )
2198                .unwrap();
2199                for ex in sequence {
2200                    writeln!(s, "{ex}").unwrap();
2201                }
2202            } else {
2203                writeln!(s, "[FAIL: {}] {}", failure.reason(), failure.name()).unwrap();
2204            }
2205        }
2206    }
2207
2208    /// Appends the invariant/property summary for multi-predicate campaigns.
2209    fn write_invariant_predicate_results(
2210        &self,
2211        s: &mut String,
2212        user_facing: bool,
2213        show_header: bool,
2214        invariant_campaign_name: Option<&str>,
2215    ) {
2216        if self.invariant_predicate_results.len() <= 1 {
2217            return;
2218        }
2219
2220        if show_header {
2221            s.push('\n');
2222            s.push_str(if user_facing {
2223                invariant_campaign_name.unwrap_or(INVARIANT_CAMPAIGN_FALLBACK_NAME)
2224            } else {
2225                "Predicates"
2226            });
2227            s.push_str(":\n");
2228        }
2229
2230        for predicate in &self.invariant_predicate_results {
2231            match predicate.status {
2232                TestStatus::Success => {
2233                    writeln!(s, "[PASS] {}", predicate.name).unwrap();
2234                }
2235                TestStatus::Failure => {
2236                    let reason = predicate.reason.as_deref().unwrap_or_default();
2237                    writeln!(s, "[FAIL: {reason}] {}", predicate.name).unwrap();
2238                }
2239                TestStatus::Skipped => {
2240                    if let Some(reason) = &predicate.reason {
2241                        writeln!(s, "[SKIP: {reason}] {}", predicate.name).unwrap();
2242                    } else {
2243                        writeln!(s, "[SKIP] {}", predicate.name).unwrap();
2244                    }
2245                }
2246            }
2247        }
2248    }
2249}
2250
2251macro_rules! extend {
2252    ($a:expr, $b:expr, $trace_kind:expr) => {
2253        $a.logs.extend($b.logs);
2254        $a.labels.extend($b.labels);
2255        $a.traces.extend($b.traces.map(|traces| ($trace_kind, traces)));
2256        $a.debug_bytecodes.extend($b.debug_bytecodes);
2257        $a.merge_coverages($b.line_coverage);
2258    };
2259}
2260
2261impl TestResult {
2262    /// Creates a new test result starting from test setup results.
2263    pub fn new(setup: &TestSetup) -> Self {
2264        Self {
2265            labels: setup.labels.clone(),
2266            logs: setup.logs.clone(),
2267            traces: setup.traces.clone(),
2268            debug_bytecodes: setup.debug_bytecodes.clone(),
2269            line_coverage: setup.coverage.clone(),
2270            ..Default::default()
2271        }
2272    }
2273
2274    /// Creates a failed test result with given reason.
2275    pub fn fail(reason: String) -> Self {
2276        Self { status: TestStatus::Failure, reason: Some(reason), ..Default::default() }
2277    }
2278
2279    /// Creates a test setup result.
2280    pub fn setup_result(setup: TestSetup) -> Self {
2281        let TestSetup {
2282            address: _,
2283            fuzz_fixtures: _,
2284            logs,
2285            labels,
2286            traces,
2287            debug_bytecodes,
2288            coverage,
2289            deployed_libs: _,
2290            reason,
2291            skipped,
2292            ..
2293        } = setup;
2294        Self {
2295            status: if skipped { TestStatus::Skipped } else { TestStatus::Failure },
2296            reason,
2297            logs,
2298            traces,
2299            debug_bytecodes,
2300            line_coverage: coverage,
2301            labels,
2302            ..Default::default()
2303        }
2304    }
2305
2306    /// Returns the skipped result for single test (used in skipped fuzz test too).
2307    pub fn single_skip(&mut self, reason: SkipReason) {
2308        self.status = TestStatus::Skipped;
2309        self.reason = reason.0;
2310    }
2311
2312    /// Returns the failed result with reason for single test.
2313    pub fn single_fail(&mut self, reason: Option<String>) {
2314        self.status = TestStatus::Failure;
2315        self.reason = reason;
2316    }
2317
2318    /// Returns the result for single test. Merges execution results (logs, labeled addresses,
2319    /// traces and coverages) in initial setup results.
2320    pub fn single_result<FEN: FoundryEvmNetwork>(
2321        &mut self,
2322        success: bool,
2323        reason: Option<String>,
2324        raw_call_result: RawCallResult<FEN>,
2325    ) {
2326        self.kind = TestKind::Unit {
2327            gas: raw_call_result.gas_used.saturating_sub(raw_call_result.stipend),
2328        };
2329
2330        extend!(self, raw_call_result, TraceKind::Execution);
2331
2332        self.status = match success {
2333            true => TestStatus::Success,
2334            false => TestStatus::Failure,
2335        };
2336        self.reason = reason;
2337        self.duration = Duration::default();
2338        self.gas_report_traces = Vec::new();
2339
2340        if let Some(cheatcodes) = raw_call_result.cheatcodes {
2341            self.breakpoints = cheatcodes.breakpoints;
2342            self.gas_snapshots = cheatcodes.gas_snapshots;
2343            self.deprecated_cheatcodes = cheatcodes.deprecated;
2344        }
2345    }
2346
2347    /// Returns the result for a fuzzed test. Merges fuzz execution results (logs, labeled
2348    /// addresses, traces and coverages) in initial setup results.
2349    pub fn fuzz_result(&mut self, result: FuzzTestResult) {
2350        self.kind = TestKind::Fuzz {
2351            median_gas: result.median_gas(false),
2352            mean_gas: result.mean_gas(false),
2353            first_case: result.first_case,
2354            runs: result.gas_by_case.len(),
2355            failed_corpus_replays: result.failed_corpus_replays,
2356        };
2357
2358        // Record logs, labels, traces and merge coverages.
2359        extend!(self, result, TraceKind::Execution);
2360
2361        self.status = if result.skipped {
2362            TestStatus::Skipped
2363        } else if result.success {
2364            TestStatus::Success
2365        } else {
2366            TestStatus::Failure
2367        };
2368        self.reason = result.reason;
2369        self.counterexample = result.counterexample;
2370        self.duration = Duration::default();
2371        self.gas_report_traces = result.gas_report_traces.into_iter().map(|t| vec![t]).collect();
2372        self.breakpoints = result.breakpoints.unwrap_or_default();
2373        self.deprecated_cheatcodes = result.deprecated_cheatcodes;
2374    }
2375
2376    /// Returns the fail result for fuzz test setup.
2377    pub fn fuzz_setup_fail(&mut self, e: Report) {
2378        self.kind = TestKind::Fuzz {
2379            first_case: Default::default(),
2380            runs: 0,
2381            mean_gas: 0,
2382            median_gas: 0,
2383            failed_corpus_replays: 0,
2384        };
2385        self.status = TestStatus::Failure;
2386        debug!(?e, "failed to set up fuzz testing environment");
2387        self.reason = Some(format!("failed to set up fuzz testing environment: {e}"));
2388    }
2389
2390    /// Returns the skipped result for invariant test.
2391    pub fn invariant_skip(&mut self, reason: SkipReason) {
2392        self.invariant_skip_with_predicates(reason, Vec::new());
2393    }
2394
2395    /// Returns the skipped result for invariant campaign with per-predicate outcomes.
2396    pub fn invariant_skip_with_predicates(
2397        &mut self,
2398        reason: SkipReason,
2399        invariant_predicate_results: Vec<InvariantPredicateResult>,
2400    ) {
2401        self.kind = TestKind::Invariant {
2402            runs: 1,
2403            calls: 1,
2404            reverts: 1,
2405            workers: default_invariant_workers(),
2406            metrics: HashMap::default(),
2407            failed_corpus_replays: 0,
2408            optimization_best_value: None,
2409        };
2410        self.status = TestStatus::Skipped;
2411        let predicate_count = invariant_predicate_results.len();
2412        let is_campaign = predicate_count > 1;
2413        self.reason = if is_campaign { None } else { reason.0 };
2414        self.invariant_count = is_campaign.then_some(predicate_count);
2415        self.invariant_predicate_results = invariant_predicate_results;
2416    }
2417
2418    /// Returns the fail result for replayed invariant test.
2419    pub fn invariant_replay_fail(
2420        &mut self,
2421        replayed_entirely: bool,
2422        invariant_name: &str,
2423        replay_reason: Option<String>,
2424        calls: usize,
2425        reverts: usize,
2426        call_sequence: Vec<BaseCounterExample>,
2427    ) {
2428        self.kind = TestKind::Invariant {
2429            runs: 1,
2430            calls,
2431            reverts,
2432            workers: default_invariant_workers(),
2433            metrics: HashMap::default(),
2434            failed_corpus_replays: 0,
2435            optimization_best_value: None,
2436        };
2437        self.status = TestStatus::Failure;
2438        self.reason = replay_reason.or_else(|| {
2439            if replayed_entirely {
2440                Some(format!("{invariant_name} replay failure"))
2441            } else {
2442                Some(format!("{invariant_name} persisted failure revert"))
2443            }
2444        });
2445        self.counterexample = Some(CounterExample::Sequence(call_sequence.len(), call_sequence));
2446    }
2447
2448    /// Returns the success result for a replayed invariant test.
2449    pub fn invariant_replay_success(&mut self, call_count: usize, reverts: usize) {
2450        self.kind = TestKind::Invariant {
2451            runs: 1,
2452            calls: call_count,
2453            reverts,
2454            workers: default_invariant_workers(),
2455            metrics: HashMap::default(),
2456            failed_corpus_replays: 0,
2457            optimization_best_value: None,
2458        };
2459        self.status = TestStatus::Success;
2460        self.reason = None;
2461    }
2462
2463    /// Returns the fail result for invariant test setup.
2464    pub fn invariant_setup_fail(&mut self, e: Report) {
2465        self.kind = TestKind::Invariant {
2466            runs: 0,
2467            calls: 0,
2468            reverts: 0,
2469            workers: default_invariant_workers(),
2470            metrics: HashMap::default(),
2471            failed_corpus_replays: 0,
2472            optimization_best_value: None,
2473        };
2474        self.status = TestStatus::Failure;
2475        self.reason = Some(format!("failed to set up invariant testing environment: {e}"));
2476    }
2477
2478    /// Returns the invariant test result.
2479    #[expect(clippy::too_many_arguments)]
2480    pub fn invariant_result(
2481        &mut self,
2482        gas_report_traces: Vec<Vec<CallTraceArena>>,
2483        success: bool,
2484        invariant_failures: Vec<InvariantFailure>,
2485        invariant_predicate_results: Vec<InvariantPredicateResult>,
2486        invariant_failure_dir: Option<std::path::PathBuf>,
2487        invariant_count: Option<usize>,
2488        invariant_handler_failures: Vec<InvariantFailure>,
2489        counterexample: Option<CounterExample>,
2490        runs: usize,
2491        calls: usize,
2492        reverts: usize,
2493        metrics: Map<String, InvariantMetrics>,
2494        failed_corpus_replays: usize,
2495        workers: usize,
2496        optimization_best_value: Option<I256>,
2497    ) {
2498        self.kind = TestKind::Invariant {
2499            runs,
2500            calls,
2501            reverts,
2502            workers: workers.max(1),
2503            metrics,
2504            failed_corpus_replays,
2505            optimization_best_value,
2506        };
2507        // For optimization mode (Some value), always succeed. For check mode (None), use success.
2508        self.status = if optimization_best_value.is_some() || success {
2509            TestStatus::Success
2510        } else {
2511            TestStatus::Failure
2512        };
2513        self.invariant_failures = invariant_failures;
2514        self.invariant_predicate_results = invariant_predicate_results;
2515        self.invariant_failure_dir = invariant_failure_dir;
2516        self.invariant_count = invariant_count;
2517        self.invariant_handler_failures = invariant_handler_failures;
2518        // `counterexample` is only used by the renderer for optimization mode (the "best
2519        // sequence" rendered on success). Invariant check-mode failures live entirely in
2520        // `invariant_failures`; `reason`/`counterexample` stay `None` for invariant tests.
2521        self.counterexample = counterexample;
2522        let artifacts = self
2523            .invariant_failures
2524            .iter()
2525            .chain(&self.invariant_handler_failures)
2526            .flat_map(|failure| {
2527                let mut artifacts = Vec::new();
2528                if let Some(artifact) = failure.artifact().cloned() {
2529                    artifacts.push(artifact);
2530                }
2531                if let Some(minimization) = failure.minimization().cloned() {
2532                    artifacts.push(minimization.original);
2533                    artifacts.push(minimization.minimized);
2534                }
2535                artifacts
2536            })
2537            .collect::<Vec<_>>();
2538        for artifact in artifacts {
2539            self.add_counterexample_artifact(artifact);
2540        }
2541        self.gas_report_traces = gas_report_traces;
2542    }
2543
2544    /// Returns the result for a table test. Merges table test execution results (logs, labeled
2545    /// addresses, traces and coverages) in initial setup results.
2546    pub fn table_result(&mut self, result: FuzzTestResult) {
2547        self.kind = TestKind::Table {
2548            median_gas: result.median_gas(false),
2549            mean_gas: result.mean_gas(false),
2550            runs: result.gas_by_case.len(),
2551        };
2552
2553        // Record logs, labels, traces and merge coverages.
2554        extend!(self, result, TraceKind::Execution);
2555
2556        self.status = if result.skipped {
2557            TestStatus::Skipped
2558        } else if result.success {
2559            TestStatus::Success
2560        } else {
2561            TestStatus::Failure
2562        };
2563        self.reason = result.reason;
2564        self.counterexample = result.counterexample;
2565        self.duration = Duration::default();
2566        self.gas_report_traces = result.gas_report_traces.into_iter().map(|t| vec![t]).collect();
2567        self.breakpoints = result.breakpoints.unwrap_or_default();
2568        self.deprecated_cheatcodes = result.deprecated_cheatcodes;
2569    }
2570
2571    /// Returns the result for a symbolic test.
2572    pub fn symbolic_result(
2573        &mut self,
2574        status: TestStatus,
2575        reason: Option<String>,
2576        counterexample: Option<CounterExample>,
2577        symbolic: SymbolicResult,
2578    ) {
2579        let stats = symbolic.solver.stats;
2580        self.kind = TestKind::Symbolic {
2581            paths: stats.paths,
2582            solver_queries: stats.solver_queries,
2583            smt_queries: stats.smt_queries,
2584            sat_queries: stats.sat_queries,
2585            model_queries: stats.model_queries,
2586            sat_cache_hits: stats.sat_cache_hits,
2587            model_cache_hits: stats.model_cache_hits,
2588            heuristic_witnesses: stats.heuristic_witnesses,
2589            solver_time_ms: stats.solver_time_ms,
2590            smt_input_bytes: stats.smt_input_bytes,
2591            smt_max_query_bytes: stats.smt_max_query_bytes,
2592            smt_build_time_ms: stats.smt_build_time_ms,
2593            smt_max_query_time_ms: stats.smt_max_query_time_ms,
2594        };
2595        self.status = status;
2596        self.reason = reason;
2597        self.counterexample = counterexample;
2598        self.record_symbolic(symbolic);
2599        self.duration = Duration::default();
2600    }
2601
2602    /// Records symbolic execution metadata without changing the test status/kind.
2603    pub(crate) fn record_symbolic(&mut self, symbolic: SymbolicResult) {
2604        if let Some(artifact) = symbolic.artifact.clone() {
2605            self.add_counterexample_artifact(artifact);
2606        }
2607        if let Some(minimization) = symbolic.minimization.clone() {
2608            self.add_counterexample_artifact(minimization.original);
2609            self.add_counterexample_artifact(minimization.minimized);
2610        }
2611        self.symbolic = Some(symbolic);
2612    }
2613
2614    /// Records a successful showmap replay result.
2615    pub fn replay_result(
2616        &mut self,
2617        corpus_entries: usize,
2618        showmap_files: usize,
2619        skipped_entries: usize,
2620        duration: Duration,
2621    ) {
2622        self.kind = TestKind::Replay { corpus_entries, showmap_files, skipped_entries };
2623        self.status = TestStatus::Success;
2624        self.duration = duration;
2625    }
2626
2627    /// Records a skipped showmap replay (e.g. unit test or no corpus available).
2628    pub fn replay_skip(&mut self, reason: impl Into<String>) {
2629        self.kind = TestKind::Replay { corpus_entries: 0, showmap_files: 0, skipped_entries: 0 };
2630        self.status = TestStatus::Skipped;
2631        self.reason = Some(reason.into());
2632        self.duration = Duration::default();
2633    }
2634
2635    /// Returns `true` if this is the result of a fuzz test
2636    pub const fn is_fuzz(&self) -> bool {
2637        matches!(self.kind, TestKind::Fuzz { .. })
2638    }
2639
2640    /// Formats the test result into a string (for printing).
2641    pub fn short_result(&self, name: &str) -> String {
2642        self.short_result_with_campaign_name(name, None)
2643    }
2644
2645    pub(crate) fn short_result_with_suite(&self, name: &str, suite_name: &str) -> String {
2646        self.short_result_with_campaign_name(name, Some(get_contract_name(suite_name)))
2647    }
2648
2649    fn short_result_with_campaign_name(&self, name: &str, contract_name: Option<&str>) -> String {
2650        let is_invariant_campaign = self.is_invariant_campaign();
2651        let name = if is_invariant_campaign {
2652            contract_name
2653                .map(invariant_campaign_display_name)
2654                .map(Cow::Owned)
2655                .unwrap_or(Cow::Borrowed(INVARIANT_CAMPAIGN_FALLBACK_NAME))
2656        } else {
2657            Cow::Borrowed(name)
2658        };
2659        let status = self.render_status_block(true, is_invariant_campaign.then_some(name.as_ref()));
2660        format!("{status} {name} {}", self.kind.report())
2661    }
2662
2663    const fn is_invariant_campaign(&self) -> bool {
2664        self.kind.is_invariant() && self.invariant_count.is_some()
2665    }
2666
2667    fn logical_count(&self) -> usize {
2668        let skipped = self.skipped_predicate_count();
2669        if skipped == 0 {
2670            1
2671        } else if self.status.is_skipped() && skipped == self.invariant_predicate_results.len() {
2672            skipped
2673        } else {
2674            1 + skipped
2675        }
2676    }
2677
2678    fn passed_count(&self) -> usize {
2679        usize::from(self.status.is_success())
2680    }
2681
2682    fn skipped_count(&self) -> usize {
2683        let skipped = self.skipped_predicate_count();
2684        if skipped == 0 && self.status.is_skipped() { 1 } else { skipped }
2685    }
2686
2687    fn failed_count(&self) -> usize {
2688        usize::from(self.status.is_failure())
2689    }
2690
2691    fn skipped_predicate_count(&self) -> usize {
2692        self.invariant_predicate_results
2693            .iter()
2694            .filter(|predicate| predicate.status.is_skipped())
2695            .count()
2696    }
2697
2698    /// Merges the given raw call result into `self`.
2699    pub fn extend<FEN: FoundryEvmNetwork>(&mut self, call_result: RawCallResult<FEN>) {
2700        extend!(self, call_result, TraceKind::Execution);
2701    }
2702
2703    /// Merges the given pre-test setup result into `self`.
2704    pub(crate) fn extend_setup<FEN: FoundryEvmNetwork>(&mut self, call_result: RawCallResult<FEN>) {
2705        extend!(self, call_result, TraceKind::Setup);
2706    }
2707
2708    /// Merges the given coverage result into `self`.
2709    pub fn merge_coverages(&mut self, other_coverage: Option<HitMaps>) {
2710        HitMaps::merge_opt(&mut self.line_coverage, other_coverage);
2711    }
2712}
2713
2714/// Data report by a test.
2715#[derive(Clone, Debug, PartialEq, Eq)]
2716pub enum TestKindReport {
2717    Unit {
2718        gas: u64,
2719    },
2720    Fuzz {
2721        runs: usize,
2722        mean_gas: u64,
2723        median_gas: u64,
2724        failed_corpus_replays: usize,
2725    },
2726    Invariant {
2727        runs: usize,
2728        calls: usize,
2729        reverts: usize,
2730        metrics: Map<String, InvariantMetrics>,
2731        failed_corpus_replays: usize,
2732        /// For optimization mode (int256 return): the best value achieved. None = check mode.
2733        optimization_best_value: Option<I256>,
2734    },
2735    Table {
2736        runs: usize,
2737        mean_gas: u64,
2738        median_gas: u64,
2739    },
2740    Symbolic {
2741        paths: usize,
2742        solver_queries: usize,
2743        smt_queries: usize,
2744        sat_queries: usize,
2745        model_queries: usize,
2746        sat_cache_hits: usize,
2747        model_cache_hits: usize,
2748        heuristic_witnesses: usize,
2749        solver_time_ms: u64,
2750        smt_input_bytes: u64,
2751        smt_max_query_bytes: u64,
2752        smt_build_time_ms: u64,
2753        smt_max_query_time_ms: u64,
2754    },
2755    /// Showmap corpus replay (no campaign performed).
2756    Replay {
2757        corpus_entries: usize,
2758        showmap_files: usize,
2759        skipped_entries: usize,
2760    },
2761}
2762
2763impl fmt::Display for TestKindReport {
2764    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2765        match self {
2766            Self::Unit { gas } => {
2767                write!(f, "(gas: {gas})")
2768            }
2769            Self::Fuzz { runs, mean_gas, median_gas, failed_corpus_replays } => {
2770                if *failed_corpus_replays != 0 {
2771                    write!(
2772                        f,
2773                        "(runs: {runs}, μ: {mean_gas}, ~: {median_gas}, failed corpus replays: {failed_corpus_replays})"
2774                    )
2775                } else {
2776                    write!(f, "(runs: {runs}, μ: {mean_gas}, ~: {median_gas})")
2777                }
2778            }
2779            Self::Invariant {
2780                runs,
2781                calls,
2782                reverts,
2783                metrics: _,
2784                failed_corpus_replays,
2785                optimization_best_value,
2786            } => {
2787                // If optimization_best_value is Some, this is optimization mode.
2788                if let Some(best_value) = optimization_best_value {
2789                    write!(f, "(best: {best_value}, runs: {runs}, calls: {calls})")
2790                } else if *failed_corpus_replays != 0 {
2791                    write!(
2792                        f,
2793                        "(runs: {runs}, calls: {calls}, reverts: {reverts}, failed corpus replays: {failed_corpus_replays})"
2794                    )
2795                } else {
2796                    write!(f, "(runs: {runs}, calls: {calls}, reverts: {reverts})")
2797                }
2798            }
2799            Self::Table { runs, mean_gas, median_gas } => {
2800                write!(f, "(runs: {runs}, μ: {mean_gas}, ~: {median_gas})")
2801            }
2802            Self::Symbolic {
2803                paths,
2804                solver_queries,
2805                smt_queries,
2806                sat_queries,
2807                model_queries,
2808                sat_cache_hits,
2809                model_cache_hits,
2810                heuristic_witnesses,
2811                solver_time_ms,
2812                smt_input_bytes: _,
2813                smt_max_query_bytes: _,
2814                smt_build_time_ms: _,
2815                smt_max_query_time_ms: _,
2816            } => {
2817                write!(
2818                    f,
2819                    "(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)"
2820                )
2821            }
2822            Self::Replay { corpus_entries, showmap_files, skipped_entries } => {
2823                if *skipped_entries != 0 {
2824                    write!(
2825                        f,
2826                        "(replay: {corpus_entries} entries, {showmap_files} files, {skipped_entries} skipped)"
2827                    )
2828                } else {
2829                    write!(f, "(replay: {corpus_entries} entries, {showmap_files} files)")
2830                }
2831            }
2832        }
2833    }
2834}
2835
2836impl TestKindReport {
2837    /// Returns the main gas value to compare against
2838    pub const fn gas(&self) -> u64 {
2839        match *self {
2840            Self::Unit { gas } => gas,
2841            // We use the median for comparisons
2842            Self::Fuzz { median_gas, .. } | Self::Table { median_gas, .. } => median_gas,
2843            // We return 0 since it's not applicable
2844            Self::Invariant { .. } | Self::Symbolic { .. } | Self::Replay { .. } => 0,
2845        }
2846    }
2847}
2848
2849/// Various types of tests
2850#[derive(Clone, Debug, Serialize, Deserialize)]
2851pub enum TestKind {
2852    /// A unit test.
2853    Unit { gas: u64 },
2854    /// A fuzz test.
2855    Fuzz {
2856        /// we keep this for the debugger
2857        first_case: FuzzCase,
2858        runs: usize,
2859        mean_gas: u64,
2860        median_gas: u64,
2861        failed_corpus_replays: usize,
2862    },
2863    /// An invariant test.
2864    Invariant {
2865        runs: usize,
2866        calls: usize,
2867        reverts: usize,
2868        /// Actual worker count used by this invariant campaign.
2869        #[serde(default = "default_invariant_workers")]
2870        workers: usize,
2871        metrics: Map<String, InvariantMetrics>,
2872        failed_corpus_replays: usize,
2873        /// For optimization mode (int256 return): the best value achieved. None = check mode.
2874        optimization_best_value: Option<I256>,
2875    },
2876    /// A table test.
2877    Table { runs: usize, mean_gas: u64, median_gas: u64 },
2878    /// A symbolic test.
2879    Symbolic {
2880        paths: usize,
2881        solver_queries: usize,
2882        #[serde(default)]
2883        smt_queries: usize,
2884        #[serde(default)]
2885        sat_queries: usize,
2886        #[serde(default)]
2887        model_queries: usize,
2888        #[serde(default)]
2889        sat_cache_hits: usize,
2890        #[serde(default)]
2891        model_cache_hits: usize,
2892        #[serde(default)]
2893        heuristic_witnesses: usize,
2894        #[serde(default)]
2895        solver_time_ms: u64,
2896        #[serde(default)]
2897        smt_input_bytes: u64,
2898        #[serde(default)]
2899        smt_max_query_bytes: u64,
2900        #[serde(default)]
2901        smt_build_time_ms: u64,
2902        #[serde(default)]
2903        smt_max_query_time_ms: u64,
2904    },
2905    /// Showmap corpus replay (no campaign performed).
2906    Replay { corpus_entries: usize, showmap_files: usize, skipped_entries: usize },
2907}
2908
2909impl Default for TestKind {
2910    fn default() -> Self {
2911        Self::Unit { gas: 0 }
2912    }
2913}
2914
2915impl TestKind {
2916    /// Returns `true` if this is a fuzz test.
2917    pub const fn is_fuzz(&self) -> bool {
2918        matches!(self, Self::Fuzz { .. })
2919    }
2920
2921    /// Returns `true` if this is an invariant test.
2922    pub const fn is_invariant(&self) -> bool {
2923        matches!(self, Self::Invariant { .. })
2924    }
2925
2926    /// Returns `true` if this is a symbolic test.
2927    pub const fn is_symbolic(&self) -> bool {
2928        matches!(self, Self::Symbolic { .. })
2929    }
2930
2931    /// Actual invariant campaign worker count, if this is an invariant test.
2932    pub const fn invariant_workers(&self) -> Option<usize> {
2933        match self {
2934            Self::Invariant { workers, .. } => Some(*workers),
2935            _ => None,
2936        }
2937    }
2938
2939    /// The gas consumed by this test
2940    pub fn report(&self) -> TestKindReport {
2941        match self {
2942            Self::Unit { gas } => TestKindReport::Unit { gas: *gas },
2943            Self::Fuzz { first_case: _, runs, mean_gas, median_gas, failed_corpus_replays } => {
2944                TestKindReport::Fuzz {
2945                    runs: *runs,
2946                    mean_gas: *mean_gas,
2947                    median_gas: *median_gas,
2948                    failed_corpus_replays: *failed_corpus_replays,
2949                }
2950            }
2951            Self::Invariant {
2952                runs,
2953                calls,
2954                reverts,
2955                workers: _,
2956                metrics: _,
2957                failed_corpus_replays,
2958                optimization_best_value,
2959            } => TestKindReport::Invariant {
2960                runs: *runs,
2961                calls: *calls,
2962                reverts: *reverts,
2963                metrics: HashMap::default(),
2964                failed_corpus_replays: *failed_corpus_replays,
2965                optimization_best_value: *optimization_best_value,
2966            },
2967            Self::Table { runs, mean_gas, median_gas } => {
2968                TestKindReport::Table { runs: *runs, mean_gas: *mean_gas, median_gas: *median_gas }
2969            }
2970            Self::Symbolic {
2971                paths,
2972                solver_queries,
2973                smt_queries,
2974                sat_queries,
2975                model_queries,
2976                sat_cache_hits,
2977                model_cache_hits,
2978                heuristic_witnesses,
2979                solver_time_ms,
2980                smt_input_bytes,
2981                smt_max_query_bytes,
2982                smt_build_time_ms,
2983                smt_max_query_time_ms,
2984            } => TestKindReport::Symbolic {
2985                paths: *paths,
2986                solver_queries: *solver_queries,
2987                smt_queries: *smt_queries,
2988                sat_queries: *sat_queries,
2989                model_queries: *model_queries,
2990                sat_cache_hits: *sat_cache_hits,
2991                model_cache_hits: *model_cache_hits,
2992                heuristic_witnesses: *heuristic_witnesses,
2993                solver_time_ms: *solver_time_ms,
2994                smt_input_bytes: *smt_input_bytes,
2995                smt_max_query_bytes: *smt_max_query_bytes,
2996                smt_build_time_ms: *smt_build_time_ms,
2997                smt_max_query_time_ms: *smt_max_query_time_ms,
2998            },
2999            Self::Replay { corpus_entries, showmap_files, skipped_entries } => {
3000                TestKindReport::Replay {
3001                    corpus_entries: *corpus_entries,
3002                    showmap_files: *showmap_files,
3003                    skipped_entries: *skipped_entries,
3004                }
3005            }
3006        }
3007    }
3008}
3009
3010const fn default_invariant_workers() -> usize {
3011    1
3012}
3013
3014/// The result of a test setup.
3015///
3016/// Includes the deployment of the required libraries and the test contract itself, and the call to
3017/// the `setUp()` function.
3018#[derive(Clone, Debug, Default)]
3019pub struct TestSetup {
3020    /// The address at which the test contract was deployed.
3021    pub address: Address,
3022    /// Defined fuzz test fixtures.
3023    pub fuzz_fixtures: FuzzFixtures,
3024
3025    /// The logs emitted during setup.
3026    pub logs: Vec<Log>,
3027    /// Addresses labeled during setup.
3028    pub labels: AddressHashMap<String>,
3029    /// Call traces of the setup.
3030    pub traces: Traces,
3031    /// Runtime bytecodes for contracts seen in setup traces.
3032    pub debug_bytecodes: AddressHashMap<Bytes>,
3033    /// Coverage info during setup.
3034    pub coverage: Option<HitMaps>,
3035    /// Addresses of external libraries deployed during setup.
3036    pub deployed_libs: Vec<Address>,
3037    /// Cached setup-derived fuzz dictionary for stateless fuzz tests.
3038    pub(crate) fuzz_state: OnceLock<EvmFuzzState>,
3039
3040    /// The reason the setup failed, if it did.
3041    pub reason: Option<String>,
3042    /// Whether setup and entire test suite is skipped.
3043    pub skipped: bool,
3044    /// Whether the test failed to deploy.
3045    pub deployment_failure: bool,
3046}
3047
3048impl TestSetup {
3049    pub fn failed(reason: String) -> Self {
3050        Self { reason: Some(reason), ..Default::default() }
3051    }
3052
3053    pub fn skipped(reason: String) -> Self {
3054        Self { reason: Some(reason), skipped: true, ..Default::default() }
3055    }
3056
3057    pub fn extend<FEN: FoundryEvmNetwork>(
3058        &mut self,
3059        raw: RawCallResult<FEN>,
3060        trace_kind: TraceKind,
3061    ) {
3062        extend!(self, raw, trace_kind);
3063    }
3064
3065    pub fn merge_coverages(&mut self, other_coverage: Option<HitMaps>) {
3066        HitMaps::merge_opt(&mut self.coverage, other_coverage);
3067    }
3068}