1use 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#[derive(Clone, Debug)]
53pub struct TestOutcome {
54 pub results: BTreeMap<String, SuiteResult>,
58 pub(crate) json_file_results: Option<BTreeMap<String, SuiteResult>>,
61 pub allow_failure: bool,
63 pub last_run_decoder: Option<CallTraceDecoder>,
69 pub gas_report: Option<GasReport>,
71 pub known_contracts: Option<ContractsByArtifact>,
73 pub fuzz_seed: Option<U256>,
75}
76
77impl TestOutcome {
78 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 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 pub fn successes(&self) -> impl Iterator<Item = (&String, &TestResult)> {
103 self.tests().filter(|(_, t)| t.status.is_success())
104 }
105
106 pub fn skips(&self) -> impl Iterator<Item = (&String, &TestResult)> {
108 self.tests().filter(|(_, t)| t.status.is_skipped())
109 }
110
111 pub fn failures(&self) -> impl Iterator<Item = (&String, &TestResult)> {
113 self.tests().filter(|(_, t)| t.status.is_failure())
114 }
115
116 pub fn tests(&self) -> impl Iterator<Item = (&String, &TestResult)> {
118 self.results.values().flat_map(|suite| suite.tests())
119 }
120
121 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 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 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 pub fn passed(&self) -> usize {
166 self.results.values().map(SuiteResult::passed).sum()
167 }
168
169 pub fn skipped(&self) -> usize {
171 self.results.values().map(SuiteResult::skipped).sum()
172 }
173
174 pub fn failed(&self) -> usize {
176 self.results.values().map(SuiteResult::failed).sum()
177 }
178
179 pub fn has_fuzz_failures(&self) -> bool {
181 self.failures().any(|(_, t)| t.kind.is_fuzz() || t.kind.is_invariant())
182 }
183
184 pub fn has_invariant_failures(&self) -> bool {
186 self.failures().any(|(_, t)| t.kind.is_invariant())
187 }
188
189 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 pub fn total_time(&self) -> Duration {
204 self.results.values().map(|suite| suite.duration).sum()
205 }
206
207 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 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 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 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 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#[derive(Clone, Debug, Serialize)]
789pub struct SuiteResult {
790 #[serde(with = "foundry_common::serde_helpers::duration")]
792 pub duration: Duration,
793 pub test_results: BTreeMap<String, TestResult>,
795 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 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 pub fn successes(&self) -> impl Iterator<Item = (&String, &TestResult)> {
828 self.tests().filter(|(_, t)| t.status.is_success())
829 }
830
831 pub fn skips(&self) -> impl Iterator<Item = (&String, &TestResult)> {
833 self.tests().filter(|(_, t)| t.status.is_skipped())
834 }
835
836 pub fn failures(&self) -> impl Iterator<Item = (&String, &TestResult)> {
838 self.tests().filter(|(_, t)| t.status.is_failure())
839 }
840
841 pub fn passed(&self) -> usize {
843 self.test_results.values().map(TestResult::passed_count).sum()
844 }
845
846 pub fn skipped(&self) -> usize {
848 self.test_results.values().map(TestResult::skipped_count).sum()
849 }
850
851 pub fn failed(&self) -> usize {
853 self.test_results.values().map(TestResult::failed_count).sum()
854 }
855
856 pub fn tests(&self) -> impl Iterator<Item = (&String, &TestResult)> {
858 self.test_results.iter()
859 }
860
861 pub fn is_empty(&self) -> bool {
863 self.test_results.is_empty()
864 }
865
866 pub fn len(&self) -> usize {
868 self.test_results.values().map(TestResult::logical_count).sum()
869 }
870
871 pub fn total_time(&self) -> Duration {
875 self.test_results.values().map(|result| result.duration).sum()
876 }
877
878 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#[derive(Clone, Debug)]
898pub struct SuiteTestResult {
899 pub artifact_id: String,
902 pub signature: String,
904 pub result: TestResult,
906}
907
908impl SuiteTestResult {
909 pub fn gas_used(&self) -> u64 {
911 self.result.kind.report().gas()
912 }
913
914 pub fn contract_name(&self) -> &str {
916 get_contract_name(&self.artifact_id)
917 }
918
919 pub fn file_name(&self) -> &str {
921 get_file_name(&self.artifact_id)
922 }
923}
924
925#[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 #[inline]
937 pub const fn is_success(self) -> bool {
938 matches!(self, Self::Success)
939 }
940
941 #[inline]
943 pub const fn is_failure(self) -> bool {
944 matches!(self, Self::Failure)
945 }
946
947 #[inline]
949 pub const fn is_skipped(self) -> bool {
950 matches!(self, Self::Skipped)
951 }
952}
953
954#[derive(Clone, Debug, Serialize, Deserialize)]
957#[serde(tag = "kind", rename_all = "snake_case")]
958pub enum InvariantFailure {
959 Predicate {
961 name: String,
963 reason: String,
965 #[serde(default, skip_serializing_if = "Option::is_none")]
967 counterexample: Option<CounterExample>,
968 #[serde(default, skip_serializing_if = "Option::is_none")]
970 artifact: Option<SymbolicArtifactRef>,
971 #[serde(default, skip_serializing_if = "Option::is_none")]
973 minimization: Option<SymbolicCounterexampleMinimization>,
974 persisted_path: std::path::PathBuf,
976 #[serde(default)]
980 is_anchor: bool,
981 },
982 Handler {
984 name: String,
987 reverter: Address,
989 selector: Selector,
991 reason: String,
993 #[serde(default, skip_serializing_if = "Option::is_none")]
995 counterexample: Option<CounterExample>,
996 #[serde(default, skip_serializing_if = "Option::is_none")]
998 artifact: Option<SymbolicArtifactRef>,
999 },
1000}
1001
1002impl InvariantFailure {
1003 pub fn reason(&self) -> &str {
1005 match self {
1006 Self::Predicate { reason, .. } | Self::Handler { reason, .. } => reason,
1007 }
1008 }
1009
1010 pub fn name(&self) -> &str {
1012 match self {
1013 Self::Predicate { name, .. } | Self::Handler { name, .. } => name,
1014 }
1015 }
1016
1017 pub fn predicate_name(&self) -> Option<&str> {
1019 match self {
1020 Self::Predicate { name, .. } => Some(name),
1021 Self::Handler { .. } => None,
1022 }
1023 }
1024
1025 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 pub const fn artifact(&self) -> Option<&SymbolicArtifactRef> {
1036 match self {
1037 Self::Predicate { artifact, .. } | Self::Handler { artifact, .. } => artifact.as_ref(),
1038 }
1039 }
1040
1041 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#[derive(Clone, Debug, Serialize, Deserialize)]
1052pub struct InvariantPredicateResult {
1053 pub name: String,
1055 pub status: TestStatus,
1057 #[serde(default, skip_serializing_if = "Option::is_none")]
1059 pub reason: Option<String>,
1060}
1061
1062#[derive(Clone, Debug, Serialize, Deserialize)]
1064pub struct SymbolicResult {
1065 #[serde(default = "symbolic_result_schema_version")]
1067 pub schema_version: u32,
1068 pub status: SymbolicResultStatus,
1070 pub incomplete: Option<SymbolicIncomplete>,
1072 pub bounds: SymbolicBounds,
1074 pub solver: SymbolicSolverMetadata,
1076 pub assumptions: Vec<SymbolicAssumption>,
1078 pub call_trace: SymbolicCallTrace,
1080 pub replay: SymbolicReplayMetadata,
1082 pub counterexample: Option<SymbolicCounterexample>,
1084 #[serde(default, skip_serializing_if = "Option::is_none")]
1086 pub corpus_seeds: Option<SymbolicCorpusSeedMetadata>,
1087 #[serde(default, skip_serializing_if = "Option::is_none")]
1089 pub artifact: Option<SymbolicArtifactRef>,
1090 #[serde(default, skip_serializing_if = "Option::is_none")]
1092 pub minimization: Option<SymbolicCounterexampleMinimization>,
1093}
1094
1095impl SymbolicResult {
1096 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 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 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 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 pub fn with_corpus_seeds(mut self, corpus_seeds: SymbolicCorpusSeedMetadata) -> Self {
1192 self.corpus_seeds = Some(corpus_seeds);
1193 self
1194 }
1195
1196 pub fn with_artifact(mut self, artifact: SymbolicArtifactRef) -> Self {
1198 self.artifact = Some(artifact);
1199 self
1200 }
1201
1202 pub fn with_minimization(mut self, minimization: SymbolicCounterexampleMinimization) -> Self {
1204 self.minimization = Some(minimization);
1205 self
1206 }
1207}
1208
1209#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1211pub struct SymbolicCorpusSeedMetadata {
1212 pub corpus_dir: Option<std::path::PathBuf>,
1214 pub limit: usize,
1216 pub loaded: usize,
1218 pub skipped: usize,
1220 pub used: Vec<SymbolicCorpusSeedRef>,
1222}
1223
1224#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1226pub struct SymbolicCorpusSeedRef {
1227 pub path: std::path::PathBuf,
1229 pub calldata: Bytes,
1231}
1232
1233#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1235pub struct SymbolicArtifactRef {
1236 pub schema: String,
1238 pub path: std::path::PathBuf,
1240}
1241
1242impl SymbolicArtifactRef {
1243 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#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1251pub struct SymbolicRegressionRef {
1252 pub artifact: std::path::PathBuf,
1254 pub path: std::path::PathBuf,
1256}
1257
1258#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1260#[serde(deny_unknown_fields)]
1261pub struct SymbolicCounterexampleMinimization {
1262 pub original: SymbolicArtifactRef,
1264 pub minimized: SymbolicArtifactRef,
1266 pub attempts: usize,
1268 pub accepted: usize,
1270 pub original_calldata_bytes: usize,
1272 pub minimized_calldata_bytes: usize,
1274 #[serde(default, skip_serializing_if = "Option::is_none")]
1276 pub original_sequence_len: Option<usize>,
1277 #[serde(default, skip_serializing_if = "Option::is_none")]
1279 pub minimized_sequence_len: Option<usize>,
1280}
1281
1282impl SymbolicCounterexampleMinimization {
1283 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 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1318#[serde(rename_all = "snake_case")]
1319pub enum SymbolicResultStatus {
1320 Pass,
1322 FailCounterexample,
1324 Incomplete,
1326}
1327
1328#[derive(Clone, Debug, Serialize, Deserialize)]
1330pub struct SymbolicIncomplete {
1331 pub kind: String,
1333 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#[derive(Clone, Debug, Serialize, Deserialize)]
1354pub struct SymbolicBounds {
1355 pub timeout_seconds: Option<u32>,
1357 pub loop_bound: Option<u32>,
1359 pub max_depth: u32,
1361 pub max_paths: u32,
1363 pub invariant_depth: u32,
1365 pub exploration_order: SymbolicExplorationOrder,
1367 pub max_solver_queries: u32,
1369 pub default_dynamic_length: u32,
1371 pub max_dynamic_length: u32,
1373 pub array_lengths: Vec<u32>,
1375 pub dynamic_lengths: BTreeMap<String, Vec<u32>>,
1377 pub default_array_lengths: Vec<u32>,
1379 pub default_bytes_lengths: Vec<u32>,
1381 pub max_calldata_bytes: u32,
1383 pub symbolic_call_targets: bool,
1385 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#[derive(Clone, Debug, Serialize, Deserialize)]
1414pub struct SymbolicSolverMetadata {
1415 pub name: String,
1417 pub command: Option<String>,
1419 pub portfolio: Vec<String>,
1421 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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1438pub struct SymbolicSolverStats {
1439 pub paths: usize,
1441 pub solver_queries: usize,
1443 pub smt_queries: usize,
1445 pub sat_queries: usize,
1447 pub model_queries: usize,
1449 pub sat_cache_hits: usize,
1451 pub model_cache_hits: usize,
1453 pub heuristic_witnesses: usize,
1455 pub solver_time_ms: u64,
1457 #[serde(default)]
1459 pub smt_input_bytes: u64,
1460 #[serde(default)]
1462 pub smt_max_query_bytes: u64,
1463 #[serde(default)]
1465 pub smt_build_time_ms: u64,
1466 #[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#[derive(Clone, Debug, Serialize, Deserialize)]
1493pub struct SymbolicAssumption {
1494 pub kind: String,
1496 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#[derive(Clone, Debug, Serialize, Deserialize)]
1517pub struct SymbolicCallTrace {
1518 pub available: bool,
1520 pub source: Option<String>,
1522 pub format: Option<String>,
1524}
1525
1526impl SymbolicCallTrace {
1527 pub const fn none() -> Self {
1529 Self { available: false, source: None, format: None }
1530 }
1531
1532 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1548#[serde(rename_all = "snake_case")]
1549pub enum SymbolicReplayStatus {
1550 NotRequired,
1552 Confirmed,
1554 Mismatch,
1556 Error,
1558 Skipped,
1560}
1561
1562#[derive(Clone, Debug, Serialize, Deserialize)]
1564#[serde(deny_unknown_fields)]
1565pub struct SymbolicReplayMetadata {
1566 pub required: bool,
1568 pub status: SymbolicReplayStatus,
1570 pub reason: Option<String>,
1572}
1573
1574impl SymbolicReplayMetadata {
1575 pub const fn not_required() -> Self {
1577 Self { required: false, status: SymbolicReplayStatus::NotRequired, reason: None }
1578 }
1579
1580 pub const fn confirmed() -> Self {
1582 Self { required: true, status: SymbolicReplayStatus::Confirmed, reason: None }
1583 }
1584
1585 pub fn mismatch(reason: impl Into<String>) -> Self {
1587 Self { required: true, status: SymbolicReplayStatus::Mismatch, reason: Some(reason.into()) }
1588 }
1589
1590 pub fn error(reason: impl Into<String>) -> Self {
1592 Self { required: true, status: SymbolicReplayStatus::Error, reason: Some(reason.into()) }
1593 }
1594
1595 pub fn skipped(reason: impl Into<String>) -> Self {
1597 Self { required: true, status: SymbolicReplayStatus::Skipped, reason: Some(reason.into()) }
1598 }
1599}
1600
1601#[derive(Clone, Debug, Serialize, Deserialize)]
1603pub struct SymbolicCounterexample {
1604 pub calldata: Bytes,
1606 pub args: Option<String>,
1608 pub raw_args: Option<String>,
1610 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#[derive(Clone, Debug, Serialize, Deserialize)]
1627#[serde(deny_unknown_fields)]
1628pub struct SymbolicCounterexampleArtifact {
1629 pub schema_version: u32,
1631 pub schema: String,
1633 pub kind: SymbolicCounterexampleArtifactKind,
1635 pub test: SymbolicCounterexampleTestIdentity,
1637 pub replay: SymbolicReplayMetadata,
1639 pub replay_semantics: SymbolicCounterexampleReplaySemantics,
1641 pub bounds: SymbolicBounds,
1643 pub solver: SymbolicSolverMetadata,
1645 pub assumptions: Vec<SymbolicAssumption>,
1647 pub call_trace: SymbolicCallTrace,
1649 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1651 pub storage: Vec<SymbolicStorageAssignment>,
1652 #[serde(default, skip_serializing_if = "Option::is_none")]
1654 pub invariant_failure: Option<SymbolicInvariantArtifactFailure>,
1655 pub calls: Vec<SymbolicCounterexampleCall>,
1657}
1658
1659impl SymbolicCounterexampleArtifact {
1660 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 pub fn with_storage(mut self, storage: Vec<SymbolicStorageAssignment>) -> Self {
1687 self.storage = storage;
1688 self
1689 }
1690
1691 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#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
1703#[serde(deny_unknown_fields)]
1704pub struct SymbolicCounterexampleReplaySemantics {
1705 pub fail_on_revert: bool,
1707}
1708
1709#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1711#[serde(rename_all = "snake_case")]
1712pub enum SymbolicCounterexampleArtifactKind {
1713 SingleCall,
1715 Sequence,
1717}
1718
1719#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1721#[serde(tag = "kind", rename_all = "snake_case")]
1722pub enum SymbolicInvariantArtifactFailure {
1723 Predicate {
1725 name: String,
1727 #[serde(default, skip_serializing_if = "Option::is_none")]
1729 site: Option<SymbolicInvariantFailureSite>,
1730 },
1731 Handler {
1733 #[serde(default, skip_serializing_if = "Option::is_none")]
1735 name: Option<String>,
1736 reverter: Address,
1738 selector: Selector,
1740 fingerprint: B256,
1742 },
1743}
1744
1745#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1747#[serde(tag = "kind", rename_all = "snake_case")]
1748pub enum SymbolicInvariantFailureSite {
1749 SequenceCall { target: Address, selector: Selector, fingerprint: B256 },
1751 Invariant { target: Address, selector: Selector, fingerprint: B256 },
1753 AfterInvariant { target: Address, selector: Selector, fingerprint: B256 },
1755}
1756
1757#[derive(Clone, Debug, Serialize, Deserialize)]
1759#[serde(deny_unknown_fields)]
1760pub struct SymbolicCounterexampleTestIdentity {
1761 pub contract: String,
1763 pub test: String,
1765}
1766
1767#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1769#[serde(deny_unknown_fields)]
1770pub struct SymbolicCounterexampleCall {
1771 pub warp: Option<U256>,
1773 pub roll: Option<U256>,
1775 pub sender: Address,
1777 pub target: Address,
1779 pub calldata: Bytes,
1781 pub value: Option<U256>,
1783 pub contract_name: Option<String>,
1785 pub function_name: Option<String>,
1787 pub signature: Option<String>,
1789 pub args: Option<String>,
1791 pub raw_args: Option<String>,
1793}
1794
1795impl SymbolicCounterexampleCall {
1796 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 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 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#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1854pub struct TestResult {
1855 pub status: TestStatus,
1860
1861 pub reason: Option<String>,
1864
1865 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1870 pub invariant_failures: Vec<InvariantFailure>,
1871
1872 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1876 pub invariant_predicate_results: Vec<InvariantPredicateResult>,
1877
1878 #[serde(default, skip_serializing_if = "Option::is_none")]
1881 pub invariant_failure_dir: Option<std::path::PathBuf>,
1882
1883 #[serde(default, skip_serializing_if = "Option::is_none")]
1888 pub invariant_count: Option<usize>,
1889
1890 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1894 pub invariant_handler_failures: Vec<InvariantFailure>,
1895
1896 pub counterexample: Option<CounterExample>,
1898
1899 #[serde(default, skip_serializing_if = "Option::is_none")]
1904 pub counterexample_artifact: Option<SymbolicArtifactRef>,
1905
1906 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1908 pub counterexample_artifacts: Vec<SymbolicArtifactRef>,
1909
1910 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1912 pub symbolic_regressions: Vec<SymbolicRegressionRef>,
1913
1914 pub logs: Vec<Log>,
1917
1918 pub decoded_logs: Vec<String>,
1921
1922 pub kind: TestKind,
1924
1925 #[serde(default, skip_serializing_if = "Option::is_none")]
1927 pub symbolic: Option<SymbolicResult>,
1928
1929 pub traces: Traces,
1931
1932 #[serde(skip)]
1934 pub debug_bytecodes: AddressHashMap<Bytes>,
1935
1936 #[serde(skip)]
1940 pub gas_report_traces: Vec<Vec<CallTraceArena>>,
1941
1942 #[serde(skip)]
1944 pub line_coverage: Option<HitMaps>,
1945
1946 #[serde(rename = "labeled_addresses")] pub labels: AddressHashMap<String>,
1949
1950 #[serde(with = "foundry_common::serde_helpers::duration")]
1951 pub duration: Duration,
1952
1953 pub breakpoints: Breakpoints,
1955
1956 pub gas_snapshots: BTreeMap<String, BTreeMap<String, String>>,
1958
1959 #[serde(skip)]
1961 pub deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>,
1962
1963 #[serde(skip)]
1965 pub symbolic_portfolio_diagnostics: Option<PortfolioDiagnostics>,
1966
1967 #[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 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 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 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 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 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 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 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 pub fn fail(reason: String) -> Self {
2280 Self { status: TestStatus::Failure, reason: Some(reason), ..Default::default() }
2281 }
2282
2283 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 pub fn single_skip(&mut self, reason: SkipReason) {
2312 self.status = TestStatus::Skipped;
2313 self.reason = reason.0;
2314 }
2315
2316 pub fn single_fail(&mut self, reason: Option<String>) {
2318 self.status = TestStatus::Failure;
2319 self.reason = reason;
2320 }
2321
2322 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 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 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 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 pub fn invariant_skip(&mut self, reason: SkipReason) {
2396 self.invariant_skip_with_predicates(reason, Vec::new());
2397 }
2398
2399 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 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 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 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 #[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 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 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 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 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 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 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 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 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 pub const fn is_fuzz(&self) -> bool {
2641 matches!(self.kind, TestKind::Fuzz { .. })
2642 }
2643
2644 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 pub fn extend<FEN: FoundryEvmNetwork>(&mut self, call_result: RawCallResult<FEN>) {
2704 extend!(self, call_result, TraceKind::Execution);
2705 }
2706
2707 pub(crate) fn extend_setup<FEN: FoundryEvmNetwork>(&mut self, call_result: RawCallResult<FEN>) {
2709 extend!(self, call_result, TraceKind::Setup);
2710 }
2711
2712 pub fn merge_coverages(&mut self, other_coverage: Option<HitMaps>) {
2714 HitMaps::merge_opt(&mut self.line_coverage, other_coverage);
2715 }
2716}
2717
2718#[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 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 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 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 pub const fn gas(&self) -> u64 {
2843 match *self {
2844 Self::Unit { gas } => gas,
2845 Self::Fuzz { median_gas, .. } | Self::Table { median_gas, .. } => median_gas,
2847 Self::Invariant { .. } | Self::Symbolic { .. } | Self::Replay { .. } => 0,
2849 }
2850 }
2851}
2852
2853#[derive(Clone, Debug, Serialize, Deserialize)]
2855pub enum TestKind {
2856 Unit { gas: u64 },
2858 Fuzz {
2860 first_case: FuzzCase,
2862 runs: usize,
2863 mean_gas: u64,
2864 median_gas: u64,
2865 failed_corpus_replays: usize,
2866 },
2867 Invariant {
2869 runs: usize,
2870 calls: usize,
2871 reverts: usize,
2872 #[serde(default = "default_invariant_workers")]
2874 workers: usize,
2875 metrics: Map<String, InvariantMetrics>,
2876 failed_corpus_replays: usize,
2877 optimization_best_value: Option<I256>,
2879 },
2880 Table { runs: usize, mean_gas: u64, median_gas: u64 },
2882 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 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 pub const fn is_fuzz(&self) -> bool {
2922 matches!(self, Self::Fuzz { .. })
2923 }
2924
2925 pub const fn is_invariant(&self) -> bool {
2927 matches!(self, Self::Invariant { .. })
2928 }
2929
2930 pub const fn is_symbolic(&self) -> bool {
2932 matches!(self, Self::Symbolic { .. })
2933 }
2934
2935 pub const fn invariant_workers(&self) -> Option<usize> {
2937 match self {
2938 Self::Invariant { workers, .. } => Some(*workers),
2939 _ => None,
2940 }
2941 }
2942
2943 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#[derive(Clone, Debug, Default)]
3023pub struct TestSetup {
3024 pub address: Address,
3026 pub fuzz_fixtures: FuzzFixtures,
3028
3029 pub logs: Vec<Log>,
3031 pub labels: AddressHashMap<String>,
3033 pub traces: Traces,
3035 pub debug_bytecodes: AddressHashMap<Bytes>,
3037 pub coverage: Option<HitMaps>,
3039 pub deployed_libs: Vec<Address>,
3041 pub(crate) fuzz_state: OnceLock<EvmFuzzState>,
3043
3044 pub reason: Option<String>,
3046 pub skipped: bool,
3048 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}