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