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 allow_failure: bool,
60 pub last_run_decoder: Option<CallTraceDecoder>,
66 pub gas_report: Option<GasReport>,
68 pub known_contracts: Option<ContractsByArtifact>,
70 pub fuzz_seed: Option<U256>,
72}
73
74impl TestOutcome {
75 pub const fn new(
77 known_contracts: Option<ContractsByArtifact>,
78 results: BTreeMap<String, SuiteResult>,
79 allow_failure: bool,
80 fuzz_seed: Option<U256>,
81 ) -> Self {
82 Self {
83 results,
84 allow_failure,
85 last_run_decoder: None,
86 gas_report: None,
87 known_contracts,
88 fuzz_seed,
89 }
90 }
91
92 pub const fn empty(known_contracts: Option<ContractsByArtifact>, allow_failure: bool) -> Self {
94 Self::new(known_contracts, BTreeMap::new(), allow_failure, None)
95 }
96
97 pub fn successes(&self) -> impl Iterator<Item = (&String, &TestResult)> {
99 self.tests().filter(|(_, t)| t.status.is_success())
100 }
101
102 pub fn skips(&self) -> impl Iterator<Item = (&String, &TestResult)> {
104 self.tests().filter(|(_, t)| t.status.is_skipped())
105 }
106
107 pub fn failures(&self) -> impl Iterator<Item = (&String, &TestResult)> {
109 self.tests().filter(|(_, t)| t.status.is_failure())
110 }
111
112 pub fn tests(&self) -> impl Iterator<Item = (&String, &TestResult)> {
114 self.results.values().flat_map(|suite| suite.tests())
115 }
116
117 pub fn symbolic_portfolio_diagnostics(&self) -> Option<PortfolioDiagnostics> {
119 let mut diagnostics = PortfolioDiagnostics::default();
120 for (_, result) in self.tests() {
121 if let Some(result_diagnostics) = &result.symbolic_portfolio_diagnostics {
122 diagnostics.merge(result_diagnostics);
123 }
124 }
125 (!diagnostics.is_empty()).then_some(diagnostics)
126 }
127
128 pub fn into_tests_cloned(&self) -> impl Iterator<Item = SuiteTestResult> + '_ {
131 self.results
132 .iter()
133 .flat_map(|(file, suite)| {
134 suite
135 .test_results
136 .iter()
137 .map(move |(sig, result)| (file.clone(), sig.clone(), result.clone()))
138 })
139 .map(|(artifact_id, signature, result)| SuiteTestResult {
140 artifact_id,
141 signature,
142 result,
143 })
144 }
145
146 pub fn into_tests(self) -> impl Iterator<Item = SuiteTestResult> {
148 self.results
149 .into_iter()
150 .flat_map(|(file, suite)| {
151 suite.test_results.into_iter().map(move |t| (file.clone(), t))
152 })
153 .map(|(artifact_id, (signature, result))| SuiteTestResult {
154 artifact_id,
155 signature,
156 result,
157 })
158 }
159
160 pub fn passed(&self) -> usize {
162 self.results.values().map(SuiteResult::passed).sum()
163 }
164
165 pub fn skipped(&self) -> usize {
167 self.results.values().map(SuiteResult::skipped).sum()
168 }
169
170 pub fn failed(&self) -> usize {
172 self.results.values().map(SuiteResult::failed).sum()
173 }
174
175 pub fn has_fuzz_failures(&self) -> bool {
177 self.failures().any(|(_, t)| t.kind.is_fuzz() || t.kind.is_invariant())
178 }
179
180 pub fn has_invariant_failures(&self) -> bool {
182 self.failures().any(|(_, t)| t.kind.is_invariant())
183 }
184
185 pub fn failed_tests_are_debuggable(&self) -> bool {
187 self.failures().all(|(_, result)| result.is_debuggable_failure())
188 }
189
190 fn invariant_workers_hint(&self) -> Option<usize> {
191 let mut workers = self.failures().filter_map(|(_, result)| result.kind.invariant_workers());
192 let first = workers.next()?;
193 (first > 1 && workers.all(|workers| workers == first)).then_some(first)
194 }
195
196 pub fn total_time(&self) -> Duration {
200 self.results.values().map(|suite| suite.duration).sum()
201 }
202
203 pub fn summary(&self, wall_clock_time: Duration) -> String {
205 let num_test_suites = self.results.len();
206 let suites = if num_test_suites == 1 { "suite" } else { "suites" };
207 let total_passed = self.passed();
208 let total_failed = self.failed();
209 let total_skipped = self.skipped();
210 let total_tests = total_passed + total_failed + total_skipped;
211 format!(
212 "\nRan {} test {} in {:.2?} ({:.2?} CPU time): {} tests passed, {} failed, {} skipped ({} total tests)",
213 num_test_suites,
214 suites,
215 wall_clock_time,
216 self.total_time(),
217 total_passed.green(),
218 total_failed.red(),
219 total_skipped.yellow(),
220 total_tests
221 )
222 }
223
224 pub fn ensure_ok(&self, silent: bool) -> eyre::Result<()> {
226 let outcome = self;
227 let failures = outcome.failures().count();
228 if outcome.allow_failure || failures == 0 {
229 return Ok(());
230 }
231
232 if shell::is_quiet() || silent {
233 std::process::exit(1);
234 }
235
236 sh_println!("\nFailing tests:")?;
237 for (suite_name, suite) in &outcome.results {
238 let failed = suite.failed();
239 if failed == 0 {
240 continue;
241 }
242
243 let term = if failed > 1 { "tests" } else { "test" };
244 sh_println!("Encountered {failed} failing {term} in {suite_name}")?;
245 for (name, result) in suite.failures() {
246 sh_println!("{}", result.short_result_with_suite(name, suite_name))?;
247 }
248 sh_println!()?;
249 }
250 let successes = outcome.passed();
251 sh_println!(
252 "Encountered a total of {} failing tests, {} tests succeeded",
253 failures.to_string().red(),
254 successes.to_string().green()
255 )?;
256
257 let test_word = if failures == 1 { "test" } else { "tests" };
259 sh_println!(
260 "\nTip: Run {} to retry only the {} failed {}",
261 "`forge test --rerun`".cyan(),
262 failures,
263 test_word
264 )?;
265 if outcome.failed_tests_are_debuggable() {
266 sh_println!(
267 "Tip: Run {} to inspect one failing test in the debugger",
268 "`forge test --debug --match-test <TEST_NAME>`".cyan()
269 )?;
270 }
271
272 if let Some(seed) = self.fuzz_seed
274 && outcome.has_fuzz_failures()
275 {
276 sh_println!(
277 "\nFuzz seed: {} (use {} to reproduce)",
278 format!("{seed:#x}").cyan(),
279 "`--fuzz-seed`".cyan()
280 )?;
281 if let Some(invariant_workers) = outcome.invariant_workers_hint() {
282 sh_println!(
283 "Invariant workers: {} (use {} to reproduce)",
284 invariant_workers,
285 format!("`--invariant-workers {invariant_workers}`").cyan()
286 )?;
287 }
288 }
289
290 std::process::exit(1);
291 }
292
293 pub fn remove_first(&mut self) -> Option<(String, String, TestResult)> {
295 self.results.iter_mut().find_map(|(suite_name, suite)| {
296 if let Some(test_name) = suite.test_results.keys().next().cloned() {
297 let result = suite.test_results.remove(&test_name).unwrap();
298 Some((suite_name.clone(), test_name, result))
299 } else {
300 None
301 }
302 })
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use super::*;
309
310 const SYMBOLIC_RESULT_SCHEMA_JSON: &str =
311 include_str!("../../evm/symbolic/assets/symbolic-result.schema.json");
312 const SYMBOLIC_COUNTEREXAMPLE_SCHEMA_JSON: &str =
313 include_str!("../../evm/symbolic/assets/symbolic-counterexample.schema.json");
314
315 fn schema_defs(schema: &serde_json::Value) -> &serde_json::Map<String, serde_json::Value> {
316 schema["$defs"].as_object().expect("schema $defs object")
317 }
318
319 fn assert_counterexample_schema_refs_resolve_offline() {
320 let counterexample_schema: serde_json::Value =
321 serde_json::from_str(SYMBOLIC_COUNTEREXAMPLE_SCHEMA_JSON).unwrap();
322 let result_schema: serde_json::Value =
323 serde_json::from_str(SYMBOLIC_RESULT_SCHEMA_JSON).unwrap();
324 let result_defs = schema_defs(&result_schema);
325 let counterexample_defs = schema_defs(&counterexample_schema);
326
327 fn visit_refs(
328 value: &serde_json::Value,
329 result_defs: &serde_json::Map<String, serde_json::Value>,
330 counterexample_defs: &serde_json::Map<String, serde_json::Value>,
331 ) {
332 match value {
333 serde_json::Value::Object(map) => {
334 if let Some(reference) = map.get("$ref").and_then(serde_json::Value::as_str) {
335 if let Some(name) = reference.strip_prefix(
336 "https://foundry-rs.github.io/schemas/symbolic-result.v1.schema.json#/$defs/",
337 ) {
338 assert!(result_defs.contains_key(name), "unresolved ref {reference}");
339 } else if let Some(name) = reference.strip_prefix("#/$defs/") {
340 assert!(
341 counterexample_defs.contains_key(name),
342 "unresolved ref {reference}"
343 );
344 } else {
345 panic!("unexpected schema ref {reference}");
346 }
347 }
348 for child in map.values() {
349 visit_refs(child, result_defs, counterexample_defs);
350 }
351 }
352 serde_json::Value::Array(values) => {
353 for child in values {
354 visit_refs(child, result_defs, counterexample_defs);
355 }
356 }
357 _ => {}
358 }
359 }
360
361 visit_refs(&counterexample_schema, result_defs, counterexample_defs);
362 }
363
364 #[test]
365 fn symbolic_result_schema_includes_solver_stats() {
366 let schema: serde_json::Value = serde_json::from_str(SYMBOLIC_RESULT_SCHEMA_JSON).unwrap();
367 let stats = schema["$defs"]["solver_stats"]["properties"]
368 .as_object()
369 .expect("solver stats properties");
370
371 for key in [
372 "paths",
373 "solver_queries",
374 "smt_queries",
375 "sat_queries",
376 "model_queries",
377 "sat_cache_hits",
378 "model_cache_hits",
379 "heuristic_witnesses",
380 "solver_time_ms",
381 "smt_input_bytes",
382 "smt_max_query_bytes",
383 "smt_build_time_ms",
384 "smt_max_query_time_ms",
385 ] {
386 assert!(stats.contains_key(key), "missing solver stats schema key {key}");
387 }
388 }
389
390 fn assert_counterexample_artifact_shape(value: &serde_json::Value) {
391 assert_counterexample_schema_refs_resolve_offline();
392 let object = value.as_object().expect("artifact object");
393 for key in [
394 "schema_version",
395 "schema",
396 "kind",
397 "test",
398 "replay",
399 "replay_semantics",
400 "bounds",
401 "solver",
402 "assumptions",
403 "call_trace",
404 "calls",
405 ] {
406 assert!(object.contains_key(key), "missing required artifact key {key}");
407 }
408 assert_eq!(value["schema_version"], 1);
409 assert_eq!(value["schema"], SYMBOLIC_COUNTEREXAMPLE_ARTIFACT_SCHEMA);
410 assert!(matches!(value["kind"].as_str(), Some("single_call" | "sequence")));
411 assert!(value["replay_semantics"].is_object());
412 assert!(!value["calls"].as_array().expect("calls array").is_empty());
413 for call in value["calls"].as_array().unwrap() {
414 let call = call.as_object().expect("call object");
415 for key in [
416 "warp",
417 "roll",
418 "sender",
419 "target",
420 "calldata",
421 "value",
422 "contract_name",
423 "function_name",
424 "signature",
425 "args",
426 "raw_args",
427 ] {
428 assert!(call.contains_key(key), "missing required call key {key}");
429 }
430 for key in ["warp", "roll", "value"] {
431 let Some(encoded) = call[key].as_str() else { continue };
432 let Some(hex) = encoded.strip_prefix("0x") else {
433 panic!("{key} must be 0x-prefixed hex quantity: {encoded}");
434 };
435 assert!(
436 hex == "0" || !hex.starts_with('0'),
437 "{key} must be compact hex quantity without leading zeros: {encoded}"
438 );
439 assert!(
440 hex.bytes().all(|byte| byte.is_ascii_hexdigit()),
441 "{key} must be hex quantity: {encoded}"
442 );
443 }
444 }
445 }
446
447 fn outcome_with_failed_invariant_workers(workers: &[usize]) -> TestOutcome {
448 let test_results = workers
449 .iter()
450 .enumerate()
451 .map(|(idx, workers)| {
452 (
453 format!("invariant{idx}()"),
454 TestResult {
455 status: TestStatus::Failure,
456 kind: TestKind::Invariant {
457 runs: 0,
458 calls: 0,
459 reverts: 0,
460 workers: *workers,
461 metrics: Map::new(),
462 failed_corpus_replays: 0,
463 optimization_best_value: None,
464 },
465 ..Default::default()
466 },
467 )
468 })
469 .collect();
470 TestOutcome::new(
471 None,
472 BTreeMap::from([(
473 "suite".to_string(),
474 SuiteResult::new(Duration::ZERO, test_results, Vec::new()),
475 )]),
476 false,
477 None,
478 )
479 }
480
481 fn outcome_with_results(test_results: Vec<TestResult>) -> TestOutcome {
482 TestOutcome::new(
483 None,
484 BTreeMap::from([(
485 "suite".to_string(),
486 SuiteResult::new(
487 Duration::ZERO,
488 test_results
489 .into_iter()
490 .enumerate()
491 .map(|(idx, result)| (format!("test{idx}()"), result))
492 .collect(),
493 Vec::new(),
494 ),
495 )]),
496 false,
497 None,
498 )
499 }
500
501 fn failed_result(kind: TestKind) -> TestResult {
502 TestResult { status: TestStatus::Failure, kind, ..Default::default() }
503 }
504
505 #[test]
506 fn failed_tests_are_debuggable_for_unit_failures() {
507 let outcome = outcome_with_results(vec![failed_result(TestKind::Unit { gas: 0 })]);
508
509 assert!(outcome.failed_tests_are_debuggable());
510 }
511
512 #[test]
513 fn failed_tests_are_not_debuggable_for_invariant_failures() {
514 let outcome = outcome_with_results(vec![failed_result(TestKind::Invariant {
515 runs: 0,
516 calls: 0,
517 reverts: 0,
518 workers: 1,
519 metrics: Map::new(),
520 failed_corpus_replays: 0,
521 optimization_best_value: None,
522 })]);
523
524 assert!(!outcome.failed_tests_are_debuggable());
525 }
526
527 #[test]
528 fn failed_tests_are_not_debuggable_for_symbolic_failures() {
529 let outcome = outcome_with_results(vec![failed_result(TestKind::Symbolic {
530 paths: 0,
531 solver_queries: 0,
532 smt_queries: 0,
533 sat_queries: 0,
534 model_queries: 0,
535 sat_cache_hits: 0,
536 model_cache_hits: 0,
537 heuristic_witnesses: 0,
538 solver_time_ms: 0,
539 smt_input_bytes: 0,
540 smt_max_query_bytes: 0,
541 smt_build_time_ms: 0,
542 smt_max_query_time_ms: 0,
543 })]);
544
545 assert!(!outcome.failed_tests_are_debuggable());
546 }
547
548 #[test]
549 fn failed_tests_are_not_debuggable_for_symbolic_backed_failures() {
550 let mut result = failed_result(TestKind::Unit { gas: 0 });
551 result.symbolic =
552 Some(SymbolicResult::pass(&SymbolicConfig::default(), SymbolicStats::default()));
553 let outcome = outcome_with_results(vec![result]);
554
555 assert!(!outcome.failed_tests_are_debuggable());
556 }
557
558 #[test]
559 fn invariant_workers_hint_requires_matching_parallel_worker_counts() {
560 assert_eq!(
561 outcome_with_failed_invariant_workers(&[3, 3]).invariant_workers_hint(),
562 Some(3)
563 );
564 assert_eq!(outcome_with_failed_invariant_workers(&[2, 3]).invariant_workers_hint(), None);
565 assert_eq!(outcome_with_failed_invariant_workers(&[1]).invariant_workers_hint(), None);
566 }
567
568 #[test]
569 fn invariant_kind_deserializes_legacy_payload_without_workers() {
570 let kind = serde_json::from_value::<TestKind>(serde_json::json!({
571 "Invariant": {
572 "runs": 4,
573 "calls": 10,
574 "reverts": 0,
575 "metrics": {},
576 "failed_corpus_replays": 0,
577 "optimization_best_value": null
578 }
579 }))
580 .unwrap();
581
582 assert_eq!(kind.invariant_workers(), Some(1));
583 }
584
585 #[test]
586 fn symbolic_counterexample_artifact_serializes_sequence_calls() {
587 let symbolic = SymbolicResult::pass(&SymbolicConfig::default(), SymbolicStats::default());
588 let call = SymbolicCounterexampleCall {
589 warp: Some(U256::from(12)),
590 roll: Some(U256::from(3)),
591 sender: Address::ZERO,
592 target: Address::ZERO,
593 calldata: Bytes::from_static(&[0x12, 0x34, 0x56, 0x78]),
594 value: Some(U256::from(9)),
595 contract_name: Some("Target".to_string()),
596 function_name: Some("step".to_string()),
597 signature: Some("step()".to_string()),
598 args: Some(String::new()),
599 raw_args: Some(String::new()),
600 };
601 let artifact = SymbolicCounterexampleArtifact::new(
602 SymbolicCounterexampleArtifactKind::Sequence,
603 SymbolicCounterexampleTestIdentity {
604 contract: "InvariantTest".to_string(),
605 test: "invariant_counter()".to_string(),
606 },
607 &symbolic,
608 SymbolicCounterexampleReplaySemantics { fail_on_revert: false },
609 vec![call.clone(), call],
610 );
611
612 let value = serde_json::to_value(artifact).unwrap();
613 assert_eq!(value["schema_version"], 1);
614 assert_eq!(value["schema"], SYMBOLIC_COUNTEREXAMPLE_ARTIFACT_SCHEMA);
615 assert_eq!(value["kind"], "sequence");
616 assert_eq!(value["replay_semantics"]["fail_on_revert"], false);
617 assert!(value.get("storage").is_none());
618 assert!(value.get("invariant_failure").is_none());
619 assert_eq!(value["calls"].as_array().unwrap().len(), 2);
620 assert_eq!(value["calls"][0]["calldata"], "0x12345678");
621 assert_eq!(value["calls"][0]["warp"], "0xc");
622 assert_eq!(value["calls"][0]["roll"], "0x3");
623 assert_eq!(value["calls"][0]["value"], "0x9");
624 assert_counterexample_artifact_shape(&value);
625
626 let decoded = serde_json::from_value::<SymbolicCounterexampleArtifact>(value).unwrap();
627 assert!(decoded.storage.is_empty());
628 assert!(decoded.invariant_failure.is_none());
629 }
630
631 #[test]
632 fn symbolic_counterexample_artifact_serializes_invariant_replay_metadata() {
633 let symbolic = SymbolicResult::pass(&SymbolicConfig::default(), SymbolicStats::default());
634 let call = SymbolicCounterexampleCall {
635 warp: None,
636 roll: None,
637 sender: Address::ZERO,
638 target: Address::repeat_byte(0x22),
639 calldata: Bytes::from_static(&[0x12, 0x34, 0x56, 0x78]),
640 value: None,
641 contract_name: Some("Target".to_string()),
642 function_name: Some("step".to_string()),
643 signature: Some("step()".to_string()),
644 args: Some(String::new()),
645 raw_args: Some(String::new()),
646 };
647 let artifact = SymbolicCounterexampleArtifact::new(
648 SymbolicCounterexampleArtifactKind::Sequence,
649 SymbolicCounterexampleTestIdentity {
650 contract: "InvariantTest".to_string(),
651 test: "invariant_counter()".to_string(),
652 },
653 &symbolic,
654 SymbolicCounterexampleReplaySemantics { fail_on_revert: true },
655 vec![call],
656 )
657 .with_storage(vec![SymbolicStorageAssignment {
658 address: Address::repeat_byte(0x11),
659 slot: U256::from(7),
660 value: U256::from(42),
661 }])
662 .with_invariant_failure(SymbolicInvariantArtifactFailure::Handler {
663 name: Some("Target::step".to_string()),
664 reverter: Address::repeat_byte(0x22),
665 selector: Selector::from([0x12, 0x34, 0x56, 0x78]),
666 fingerprint: B256::repeat_byte(0x33),
667 });
668
669 let value = serde_json::to_value(artifact.clone()).unwrap();
670 assert_eq!(value["storage"][0]["address"], format!("{:?}", Address::repeat_byte(0x11)));
671 assert_eq!(value["storage"][0]["slot"], "0x7");
672 assert_eq!(value["storage"][0]["value"], "0x2a");
673 assert_eq!(value["invariant_failure"]["kind"], "handler");
674 assert_eq!(value["invariant_failure"]["name"], "Target::step");
675 assert_eq!(
676 value["invariant_failure"]["reverter"],
677 format!("{:?}", Address::repeat_byte(0x22))
678 );
679 assert_eq!(value["invariant_failure"]["selector"], "0x12345678");
680 assert_eq!(
681 value["invariant_failure"]["fingerprint"],
682 format!("{:?}", B256::repeat_byte(0x33))
683 );
684 assert_counterexample_artifact_shape(&value);
685
686 let decoded = serde_json::from_value::<SymbolicCounterexampleArtifact>(value).unwrap();
687 assert_eq!(decoded.storage, artifact.storage);
688 assert_eq!(decoded.invariant_failure, artifact.invariant_failure);
689 }
690
691 #[test]
692 fn symbolic_counterexample_schema_includes_predicate_failure_sites() {
693 let schema: serde_json::Value =
694 serde_json::from_str(SYMBOLIC_COUNTEREXAMPLE_SCHEMA_JSON).unwrap();
695 let predicate = schema["$defs"]["invariant_failure"]["oneOf"]
696 .as_array()
697 .unwrap()
698 .iter()
699 .find(|variant| variant["properties"]["kind"]["const"] == "predicate")
700 .unwrap();
701 assert_eq!(predicate["properties"]["site"]["$ref"], "#/$defs/invariant_failure_site");
702
703 let site_schema = &schema["$defs"]["invariant_failure_site"];
704 let site_properties = site_schema["properties"].as_object().unwrap();
705 let required_site_properties = site_schema["required"].as_array().unwrap();
706 let site_kinds = site_properties["kind"]["enum"].as_array().unwrap();
707 for (site, expected_kind) in [
708 (
709 SymbolicInvariantFailureSite::SequenceCall {
710 target: Address::ZERO,
711 selector: Selector::ZERO,
712 fingerprint: B256::ZERO,
713 },
714 "sequence_call",
715 ),
716 (
717 SymbolicInvariantFailureSite::Invariant {
718 target: Address::ZERO,
719 selector: Selector::ZERO,
720 fingerprint: B256::ZERO,
721 },
722 "invariant",
723 ),
724 (
725 SymbolicInvariantFailureSite::AfterInvariant {
726 target: Address::ZERO,
727 selector: Selector::ZERO,
728 fingerprint: B256::ZERO,
729 },
730 "after_invariant",
731 ),
732 ] {
733 let failure = SymbolicInvariantArtifactFailure::Predicate {
734 name: "invariant_counter".to_string(),
735 site: Some(site),
736 };
737 let value = serde_json::to_value(failure).unwrap();
738 assert_eq!(value["site"]["kind"], expected_kind);
739 assert!(site_kinds.contains(&value["site"]["kind"]));
740 let site = value["site"].as_object().unwrap();
741 assert!(site.keys().all(|key| site_properties.contains_key(key)));
742 assert!(
743 required_site_properties.iter().all(|key| site.contains_key(key.as_str().unwrap()))
744 );
745 }
746 }
747
748 #[test]
749 fn symbolic_counterexample_artifact_serializes_zero_quantities_compactly() {
750 let symbolic = SymbolicResult::pass(&SymbolicConfig::default(), SymbolicStats::default());
751 let call = SymbolicCounterexampleCall {
752 warp: Some(U256::ZERO),
753 roll: Some(U256::ZERO),
754 sender: Address::ZERO,
755 target: Address::ZERO,
756 calldata: Bytes::from_static(&[0x12, 0x34, 0x56, 0x78]),
757 value: Some(U256::ZERO),
758 contract_name: Some("Target".to_string()),
759 function_name: Some("step".to_string()),
760 signature: Some("step()".to_string()),
761 args: Some(String::new()),
762 raw_args: Some(String::new()),
763 };
764 let artifact = SymbolicCounterexampleArtifact::new(
765 SymbolicCounterexampleArtifactKind::Sequence,
766 SymbolicCounterexampleTestIdentity {
767 contract: "InvariantTest".to_string(),
768 test: "invariant_counter()".to_string(),
769 },
770 &symbolic,
771 SymbolicCounterexampleReplaySemantics { fail_on_revert: false },
772 vec![call],
773 );
774
775 let value = serde_json::to_value(artifact).unwrap();
776 assert_eq!(value["calls"][0]["warp"], "0x0");
777 assert_eq!(value["calls"][0]["roll"], "0x0");
778 assert_eq!(value["calls"][0]["value"], "0x0");
779 assert_counterexample_artifact_shape(&value);
780 }
781}
782
783#[derive(Clone, Debug, Serialize)]
785pub struct SuiteResult {
786 #[serde(with = "foundry_common::serde_helpers::duration")]
788 pub duration: Duration,
789 pub test_results: BTreeMap<String, TestResult>,
791 pub warnings: Vec<String>,
793}
794
795impl SuiteResult {
796 pub fn new(
797 duration: Duration,
798 test_results: BTreeMap<String, TestResult>,
799 mut warnings: Vec<String>,
800 ) -> Self {
801 let mut deprecated_cheatcodes = HashMap::new();
803 for test_result in test_results.values() {
804 deprecated_cheatcodes.extend(test_result.deprecated_cheatcodes.clone());
805 }
806 if !deprecated_cheatcodes.is_empty() {
807 let mut warning =
808 "the following cheatcode(s) are deprecated and will be removed in future versions:"
809 .to_string();
810 for (cheatcode, reason) in deprecated_cheatcodes {
811 write!(warning, "\n {cheatcode}").unwrap();
812 if let Some(reason) = reason {
813 write!(warning, ": {reason}").unwrap();
814 }
815 }
816 warnings.push(warning);
817 }
818
819 Self { duration, test_results, warnings }
820 }
821
822 pub fn successes(&self) -> impl Iterator<Item = (&String, &TestResult)> {
824 self.tests().filter(|(_, t)| t.status.is_success())
825 }
826
827 pub fn skips(&self) -> impl Iterator<Item = (&String, &TestResult)> {
829 self.tests().filter(|(_, t)| t.status.is_skipped())
830 }
831
832 pub fn failures(&self) -> impl Iterator<Item = (&String, &TestResult)> {
834 self.tests().filter(|(_, t)| t.status.is_failure())
835 }
836
837 pub fn passed(&self) -> usize {
839 self.test_results.values().map(TestResult::passed_count).sum()
840 }
841
842 pub fn skipped(&self) -> usize {
844 self.test_results.values().map(TestResult::skipped_count).sum()
845 }
846
847 pub fn failed(&self) -> usize {
849 self.test_results.values().map(TestResult::failed_count).sum()
850 }
851
852 pub fn tests(&self) -> impl Iterator<Item = (&String, &TestResult)> {
854 self.test_results.iter()
855 }
856
857 pub fn is_empty(&self) -> bool {
859 self.test_results.is_empty()
860 }
861
862 pub fn len(&self) -> usize {
864 self.test_results.values().map(TestResult::logical_count).sum()
865 }
866
867 pub fn total_time(&self) -> Duration {
871 self.test_results.values().map(|result| result.duration).sum()
872 }
873
874 pub fn summary(&self) -> String {
876 let failed = self.failed();
877 let result = if failed == 0 { "ok".green() } else { "FAILED".red() };
878 format!(
879 "Suite result: {}. {} passed; {} failed; {} skipped; finished in {:.2?} ({:.2?} CPU time)",
880 result,
881 self.passed().green(),
882 failed.red(),
883 self.skipped().yellow(),
884 self.duration,
885 self.total_time(),
886 )
887 }
888}
889
890#[derive(Clone, Debug)]
894pub struct SuiteTestResult {
895 pub artifact_id: String,
898 pub signature: String,
900 pub result: TestResult,
902}
903
904impl SuiteTestResult {
905 pub fn gas_used(&self) -> u64 {
907 self.result.kind.report().gas()
908 }
909
910 pub fn contract_name(&self) -> &str {
912 get_contract_name(&self.artifact_id)
913 }
914
915 pub fn file_name(&self) -> &str {
917 get_file_name(&self.artifact_id)
918 }
919}
920
921#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
923pub enum TestStatus {
924 Success,
925 #[default]
926 Failure,
927 Skipped,
928}
929
930impl TestStatus {
931 #[inline]
933 pub const fn is_success(self) -> bool {
934 matches!(self, Self::Success)
935 }
936
937 #[inline]
939 pub const fn is_failure(self) -> bool {
940 matches!(self, Self::Failure)
941 }
942
943 #[inline]
945 pub const fn is_skipped(self) -> bool {
946 matches!(self, Self::Skipped)
947 }
948}
949
950#[derive(Clone, Debug, Serialize, Deserialize)]
953#[serde(tag = "kind", rename_all = "snake_case")]
954pub enum InvariantFailure {
955 Predicate {
957 name: String,
959 reason: String,
961 #[serde(default, skip_serializing_if = "Option::is_none")]
963 counterexample: Option<CounterExample>,
964 #[serde(default, skip_serializing_if = "Option::is_none")]
966 artifact: Option<SymbolicArtifactRef>,
967 #[serde(default, skip_serializing_if = "Option::is_none")]
969 minimization: Option<SymbolicCounterexampleMinimization>,
970 persisted_path: std::path::PathBuf,
972 #[serde(default)]
976 is_anchor: bool,
977 },
978 Handler {
980 name: String,
983 reverter: Address,
985 selector: Selector,
987 reason: String,
989 #[serde(default, skip_serializing_if = "Option::is_none")]
991 counterexample: Option<CounterExample>,
992 #[serde(default, skip_serializing_if = "Option::is_none")]
994 artifact: Option<SymbolicArtifactRef>,
995 },
996}
997
998impl InvariantFailure {
999 pub fn reason(&self) -> &str {
1001 match self {
1002 Self::Predicate { reason, .. } | Self::Handler { reason, .. } => reason,
1003 }
1004 }
1005
1006 pub fn name(&self) -> &str {
1008 match self {
1009 Self::Predicate { name, .. } | Self::Handler { name, .. } => name,
1010 }
1011 }
1012
1013 pub fn predicate_name(&self) -> Option<&str> {
1015 match self {
1016 Self::Predicate { name, .. } => Some(name),
1017 Self::Handler { .. } => None,
1018 }
1019 }
1020
1021 pub const fn counterexample(&self) -> Option<&CounterExample> {
1023 match self {
1024 Self::Predicate { counterexample, .. } | Self::Handler { counterexample, .. } => {
1025 counterexample.as_ref()
1026 }
1027 }
1028 }
1029
1030 pub const fn artifact(&self) -> Option<&SymbolicArtifactRef> {
1032 match self {
1033 Self::Predicate { artifact, .. } | Self::Handler { artifact, .. } => artifact.as_ref(),
1034 }
1035 }
1036
1037 pub const fn minimization(&self) -> Option<&SymbolicCounterexampleMinimization> {
1039 match self {
1040 Self::Predicate { minimization, .. } => minimization.as_ref(),
1041 Self::Handler { .. } => None,
1042 }
1043 }
1044}
1045
1046#[derive(Clone, Debug, Serialize, Deserialize)]
1048pub struct InvariantPredicateResult {
1049 pub name: String,
1051 pub status: TestStatus,
1053 #[serde(default, skip_serializing_if = "Option::is_none")]
1055 pub reason: Option<String>,
1056}
1057
1058#[derive(Clone, Debug, Serialize, Deserialize)]
1060pub struct SymbolicResult {
1061 #[serde(default = "symbolic_result_schema_version")]
1063 pub schema_version: u32,
1064 pub status: SymbolicResultStatus,
1066 pub incomplete: Option<SymbolicIncomplete>,
1068 pub bounds: SymbolicBounds,
1070 pub solver: SymbolicSolverMetadata,
1072 pub assumptions: Vec<SymbolicAssumption>,
1074 pub call_trace: SymbolicCallTrace,
1076 pub replay: SymbolicReplayMetadata,
1078 pub counterexample: Option<SymbolicCounterexample>,
1080 #[serde(default, skip_serializing_if = "Option::is_none")]
1082 pub corpus_seeds: Option<SymbolicCorpusSeedMetadata>,
1083 #[serde(default, skip_serializing_if = "Option::is_none")]
1085 pub artifact: Option<SymbolicArtifactRef>,
1086 #[serde(default, skip_serializing_if = "Option::is_none")]
1088 pub minimization: Option<SymbolicCounterexampleMinimization>,
1089}
1090
1091impl SymbolicResult {
1092 pub fn pass(config: &SymbolicConfig, stats: SymbolicStats) -> Self {
1094 Self::new(
1095 SymbolicResultStatus::Pass,
1096 config,
1097 stats,
1098 None,
1099 SymbolicReplayMetadata::not_required(),
1100 SymbolicCallTrace::none(),
1101 None,
1102 )
1103 }
1104
1105 pub fn fail_counterexample(
1107 config: &SymbolicConfig,
1108 stats: SymbolicStats,
1109 call_trace: SymbolicCallTrace,
1110 counterexample: SymbolicCounterexample,
1111 ) -> Self {
1112 Self::new(
1113 SymbolicResultStatus::FailCounterexample,
1114 config,
1115 stats,
1116 None,
1117 SymbolicReplayMetadata::confirmed(),
1118 call_trace,
1119 Some(counterexample),
1120 )
1121 }
1122
1123 pub fn fail_counterexample_sequence(
1125 config: &SymbolicConfig,
1126 stats: SymbolicStats,
1127 call_trace: SymbolicCallTrace,
1128 ) -> Self {
1129 Self::new(
1130 SymbolicResultStatus::FailCounterexample,
1131 config,
1132 stats,
1133 None,
1134 SymbolicReplayMetadata::confirmed(),
1135 call_trace,
1136 None,
1137 )
1138 }
1139
1140 pub fn incomplete(
1142 config: &SymbolicConfig,
1143 kind: SymbolicStopReason,
1144 reason: impl Into<String>,
1145 stats: SymbolicStats,
1146 replay: SymbolicReplayMetadata,
1147 call_trace: SymbolicCallTrace,
1148 counterexample: Option<SymbolicCounterexample>,
1149 ) -> Self {
1150 Self::new(
1151 SymbolicResultStatus::Incomplete,
1152 config,
1153 stats,
1154 Some(SymbolicIncomplete::new(kind, reason)),
1155 replay,
1156 call_trace,
1157 counterexample,
1158 )
1159 }
1160
1161 fn new(
1162 status: SymbolicResultStatus,
1163 config: &SymbolicConfig,
1164 stats: SymbolicStats,
1165 incomplete: Option<SymbolicIncomplete>,
1166 replay: SymbolicReplayMetadata,
1167 call_trace: SymbolicCallTrace,
1168 counterexample: Option<SymbolicCounterexample>,
1169 ) -> Self {
1170 Self {
1171 schema_version: SYMBOLIC_RESULT_SCHEMA_VERSION,
1172 status,
1173 incomplete,
1174 bounds: SymbolicBounds::from_config(config),
1175 solver: SymbolicSolverMetadata::from_config_and_stats(config, stats),
1176 assumptions: SymbolicAssumption::default_assumptions(),
1177 call_trace,
1178 replay,
1179 counterexample,
1180 corpus_seeds: None,
1181 artifact: None,
1182 minimization: None,
1183 }
1184 }
1185
1186 pub fn with_corpus_seeds(mut self, corpus_seeds: SymbolicCorpusSeedMetadata) -> Self {
1188 self.corpus_seeds = Some(corpus_seeds);
1189 self
1190 }
1191
1192 pub fn with_artifact(mut self, artifact: SymbolicArtifactRef) -> Self {
1194 self.artifact = Some(artifact);
1195 self
1196 }
1197
1198 pub fn with_minimization(mut self, minimization: SymbolicCounterexampleMinimization) -> Self {
1200 self.minimization = Some(minimization);
1201 self
1202 }
1203}
1204
1205#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1207pub struct SymbolicCorpusSeedMetadata {
1208 pub corpus_dir: Option<std::path::PathBuf>,
1210 pub limit: usize,
1212 pub loaded: usize,
1214 pub skipped: usize,
1216 pub used: Vec<SymbolicCorpusSeedRef>,
1218}
1219
1220#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1222pub struct SymbolicCorpusSeedRef {
1223 pub path: std::path::PathBuf,
1225 pub calldata: Bytes,
1227}
1228
1229#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1231pub struct SymbolicArtifactRef {
1232 pub schema: String,
1234 pub path: std::path::PathBuf,
1236}
1237
1238impl SymbolicArtifactRef {
1239 pub fn new(path: impl Into<std::path::PathBuf>) -> Self {
1241 Self { schema: SYMBOLIC_COUNTEREXAMPLE_ARTIFACT_SCHEMA.to_string(), path: path.into() }
1242 }
1243}
1244
1245#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1247pub struct SymbolicRegressionRef {
1248 pub artifact: std::path::PathBuf,
1250 pub path: std::path::PathBuf,
1252}
1253
1254#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1256#[serde(deny_unknown_fields)]
1257pub struct SymbolicCounterexampleMinimization {
1258 pub original: SymbolicArtifactRef,
1260 pub minimized: SymbolicArtifactRef,
1262 pub attempts: usize,
1264 pub accepted: usize,
1266 pub original_calldata_bytes: usize,
1268 pub minimized_calldata_bytes: usize,
1270 #[serde(default, skip_serializing_if = "Option::is_none")]
1272 pub original_sequence_len: Option<usize>,
1273 #[serde(default, skip_serializing_if = "Option::is_none")]
1275 pub minimized_sequence_len: Option<usize>,
1276}
1277
1278impl SymbolicCounterexampleMinimization {
1279 pub const fn new(
1281 original: SymbolicArtifactRef,
1282 minimized: SymbolicArtifactRef,
1283 attempts: usize,
1284 accepted: usize,
1285 original_calldata_bytes: usize,
1286 minimized_calldata_bytes: usize,
1287 ) -> Self {
1288 Self {
1289 original,
1290 minimized,
1291 attempts,
1292 accepted,
1293 original_calldata_bytes,
1294 minimized_calldata_bytes,
1295 original_sequence_len: None,
1296 minimized_sequence_len: None,
1297 }
1298 }
1299
1300 pub const fn with_sequence_lengths(
1302 mut self,
1303 original_sequence_len: usize,
1304 minimized_sequence_len: usize,
1305 ) -> Self {
1306 self.original_sequence_len = Some(original_sequence_len);
1307 self.minimized_sequence_len = Some(minimized_sequence_len);
1308 self
1309 }
1310}
1311
1312#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1314#[serde(rename_all = "snake_case")]
1315pub enum SymbolicResultStatus {
1316 Pass,
1318 FailCounterexample,
1320 Incomplete,
1322}
1323
1324#[derive(Clone, Debug, Serialize, Deserialize)]
1326pub struct SymbolicIncomplete {
1327 pub kind: String,
1329 pub reason: String,
1331}
1332
1333impl SymbolicIncomplete {
1334 fn new(kind: SymbolicStopReason, reason: impl Into<String>) -> Self {
1335 Self { kind: symbolic_stop_reason_kind(kind).to_string(), reason: reason.into() }
1336 }
1337}
1338
1339const fn symbolic_stop_reason_kind(kind: SymbolicStopReason) -> &'static str {
1340 match kind {
1341 SymbolicStopReason::Stuck => "stuck",
1342 SymbolicStopReason::RevertAll => "revert_all",
1343 SymbolicStopReason::Timeout => "timeout",
1344 SymbolicStopReason::Error => "error",
1345 }
1346}
1347
1348#[derive(Clone, Debug, Serialize, Deserialize)]
1350pub struct SymbolicBounds {
1351 pub timeout_seconds: Option<u32>,
1353 pub loop_bound: Option<u32>,
1355 pub max_depth: u32,
1357 pub max_paths: u32,
1359 pub invariant_depth: u32,
1361 pub exploration_order: SymbolicExplorationOrder,
1363 pub max_solver_queries: u32,
1365 pub default_dynamic_length: u32,
1367 pub max_dynamic_length: u32,
1369 pub array_lengths: Vec<u32>,
1371 pub dynamic_lengths: BTreeMap<String, Vec<u32>>,
1373 pub default_array_lengths: Vec<u32>,
1375 pub default_bytes_lengths: Vec<u32>,
1377 pub max_calldata_bytes: u32,
1379 pub symbolic_call_targets: bool,
1381 pub storage_layout: SymbolicStorageLayout,
1383}
1384
1385impl SymbolicBounds {
1386 fn from_config(config: &SymbolicConfig) -> Self {
1387 Self {
1388 timeout_seconds: config.timeout,
1389 loop_bound: config.loop_bound,
1390 max_depth: config.execution_depth(),
1391 max_paths: config.path_width(),
1392 invariant_depth: config.invariant_depth,
1393 exploration_order: config.exploration_order,
1394 max_solver_queries: config.max_solver_queries,
1395 default_dynamic_length: config.default_dynamic_length,
1396 max_dynamic_length: config.max_dynamic_length,
1397 array_lengths: config.array_lengths.clone(),
1398 dynamic_lengths: config.dynamic_lengths.clone(),
1399 default_array_lengths: config.default_array_lengths.clone(),
1400 default_bytes_lengths: config.default_bytes_lengths.clone(),
1401 max_calldata_bytes: config.max_calldata_bytes,
1402 symbolic_call_targets: config.symbolic_call_targets,
1403 storage_layout: config.storage_layout,
1404 }
1405 }
1406}
1407
1408#[derive(Clone, Debug, Serialize, Deserialize)]
1410pub struct SymbolicSolverMetadata {
1411 pub name: String,
1413 pub command: Option<String>,
1415 pub portfolio: Vec<String>,
1417 pub stats: SymbolicSolverStats,
1419}
1420
1421impl SymbolicSolverMetadata {
1422 fn from_config_and_stats(config: &SymbolicConfig, stats: SymbolicStats) -> Self {
1423 Self {
1424 name: config.solver.clone(),
1425 command: config.solver_command.clone(),
1426 portfolio: config.solver_portfolio.clone(),
1427 stats: SymbolicSolverStats::from(stats),
1428 }
1429 }
1430}
1431
1432#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1434pub struct SymbolicSolverStats {
1435 pub paths: usize,
1437 pub solver_queries: usize,
1439 pub smt_queries: usize,
1441 pub sat_queries: usize,
1443 pub model_queries: usize,
1445 pub sat_cache_hits: usize,
1447 pub model_cache_hits: usize,
1449 pub heuristic_witnesses: usize,
1451 pub solver_time_ms: u64,
1453 #[serde(default)]
1455 pub smt_input_bytes: u64,
1456 #[serde(default)]
1458 pub smt_max_query_bytes: u64,
1459 #[serde(default)]
1461 pub smt_build_time_ms: u64,
1462 #[serde(default)]
1464 pub smt_max_query_time_ms: u64,
1465}
1466
1467impl From<SymbolicStats> for SymbolicSolverStats {
1468 fn from(stats: SymbolicStats) -> Self {
1469 Self {
1470 paths: stats.paths,
1471 solver_queries: stats.solver_queries,
1472 smt_queries: stats.smt_queries,
1473 sat_queries: stats.sat_queries,
1474 model_queries: stats.model_queries,
1475 sat_cache_hits: stats.sat_cache_hits,
1476 model_cache_hits: stats.model_cache_hits,
1477 heuristic_witnesses: stats.heuristic_witnesses,
1478 solver_time_ms: stats.solver_time_ms,
1479 smt_input_bytes: stats.smt_input_bytes,
1480 smt_max_query_bytes: stats.smt_max_query_bytes,
1481 smt_build_time_ms: stats.smt_build_time_ms,
1482 smt_max_query_time_ms: stats.smt_max_query_time_ms,
1483 }
1484 }
1485}
1486
1487#[derive(Clone, Debug, Serialize, Deserialize)]
1489pub struct SymbolicAssumption {
1490 pub kind: String,
1492 pub description: String,
1494}
1495
1496impl SymbolicAssumption {
1497 fn default_assumptions() -> Vec<Self> {
1498 vec![
1499 Self {
1500 kind: "bounded_exploration".to_string(),
1501 description: "Result is scoped to the configured path, depth, solver-query, loop, calldata, and dynamic-length bounds.".to_string(),
1502 },
1503 Self {
1504 kind: "hash_model".to_string(),
1505 description: "Symbolic Keccak and hash-like precompile reasoning assumes collision and preimage resistance for modeled cases.".to_string(),
1506 },
1507 ]
1508 }
1509}
1510
1511#[derive(Clone, Debug, Serialize, Deserialize)]
1513pub struct SymbolicCallTrace {
1514 pub available: bool,
1516 pub source: Option<String>,
1518 pub format: Option<String>,
1520}
1521
1522impl SymbolicCallTrace {
1523 pub const fn none() -> Self {
1525 Self { available: false, source: None, format: None }
1526 }
1527
1528 pub fn test_result_traces(available: bool) -> Self {
1530 if !available {
1531 return Self::none();
1532 }
1533
1534 Self {
1535 available: true,
1536 source: Some("test_result.traces".to_string()),
1537 format: Some("foundry_call_trace_arena".to_string()),
1538 }
1539 }
1540}
1541
1542#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1544#[serde(rename_all = "snake_case")]
1545pub enum SymbolicReplayStatus {
1546 NotRequired,
1548 Confirmed,
1550 Mismatch,
1552 Error,
1554 Skipped,
1556}
1557
1558#[derive(Clone, Debug, Serialize, Deserialize)]
1560#[serde(deny_unknown_fields)]
1561pub struct SymbolicReplayMetadata {
1562 pub required: bool,
1564 pub status: SymbolicReplayStatus,
1566 pub reason: Option<String>,
1568}
1569
1570impl SymbolicReplayMetadata {
1571 pub const fn not_required() -> Self {
1573 Self { required: false, status: SymbolicReplayStatus::NotRequired, reason: None }
1574 }
1575
1576 pub const fn confirmed() -> Self {
1578 Self { required: true, status: SymbolicReplayStatus::Confirmed, reason: None }
1579 }
1580
1581 pub fn mismatch(reason: impl Into<String>) -> Self {
1583 Self { required: true, status: SymbolicReplayStatus::Mismatch, reason: Some(reason.into()) }
1584 }
1585
1586 pub fn error(reason: impl Into<String>) -> Self {
1588 Self { required: true, status: SymbolicReplayStatus::Error, reason: Some(reason.into()) }
1589 }
1590
1591 pub fn skipped(reason: impl Into<String>) -> Self {
1593 Self { required: true, status: SymbolicReplayStatus::Skipped, reason: Some(reason.into()) }
1594 }
1595}
1596
1597#[derive(Clone, Debug, Serialize, Deserialize)]
1599pub struct SymbolicCounterexample {
1600 pub calldata: Bytes,
1602 pub args: Option<String>,
1604 pub raw_args: Option<String>,
1606 pub value: Option<U256>,
1608}
1609
1610impl From<&BaseCounterExample> for SymbolicCounterexample {
1611 fn from(counterexample: &BaseCounterExample) -> Self {
1612 Self {
1613 calldata: counterexample.calldata.clone(),
1614 args: counterexample.args.clone(),
1615 raw_args: counterexample.raw_args.clone(),
1616 value: counterexample.value,
1617 }
1618 }
1619}
1620
1621#[derive(Clone, Debug, Serialize, Deserialize)]
1623#[serde(deny_unknown_fields)]
1624pub struct SymbolicCounterexampleArtifact {
1625 pub schema_version: u32,
1627 pub schema: String,
1629 pub kind: SymbolicCounterexampleArtifactKind,
1631 pub test: SymbolicCounterexampleTestIdentity,
1633 pub replay: SymbolicReplayMetadata,
1635 pub replay_semantics: SymbolicCounterexampleReplaySemantics,
1637 pub bounds: SymbolicBounds,
1639 pub solver: SymbolicSolverMetadata,
1641 pub assumptions: Vec<SymbolicAssumption>,
1643 pub call_trace: SymbolicCallTrace,
1645 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1647 pub storage: Vec<SymbolicStorageAssignment>,
1648 #[serde(default, skip_serializing_if = "Option::is_none")]
1650 pub invariant_failure: Option<SymbolicInvariantArtifactFailure>,
1651 pub calls: Vec<SymbolicCounterexampleCall>,
1653}
1654
1655impl SymbolicCounterexampleArtifact {
1656 pub fn new(
1658 kind: SymbolicCounterexampleArtifactKind,
1659 test: SymbolicCounterexampleTestIdentity,
1660 symbolic: &SymbolicResult,
1661 replay_semantics: SymbolicCounterexampleReplaySemantics,
1662 calls: Vec<SymbolicCounterexampleCall>,
1663 ) -> Self {
1664 Self {
1665 schema_version: SYMBOLIC_COUNTEREXAMPLE_ARTIFACT_SCHEMA_VERSION,
1666 schema: SYMBOLIC_COUNTEREXAMPLE_ARTIFACT_SCHEMA.to_string(),
1667 kind,
1668 test,
1669 replay: symbolic.replay.clone(),
1670 replay_semantics,
1671 bounds: symbolic.bounds.clone(),
1672 solver: symbolic.solver.clone(),
1673 assumptions: symbolic.assumptions.clone(),
1674 call_trace: symbolic.call_trace.clone(),
1675 storage: Vec::new(),
1676 invariant_failure: None,
1677 calls,
1678 }
1679 }
1680
1681 pub fn with_storage(mut self, storage: Vec<SymbolicStorageAssignment>) -> Self {
1683 self.storage = storage;
1684 self
1685 }
1686
1687 pub fn with_invariant_failure(
1689 mut self,
1690 invariant_failure: SymbolicInvariantArtifactFailure,
1691 ) -> Self {
1692 self.invariant_failure = Some(invariant_failure);
1693 self
1694 }
1695}
1696
1697#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
1699#[serde(deny_unknown_fields)]
1700pub struct SymbolicCounterexampleReplaySemantics {
1701 pub fail_on_revert: bool,
1703}
1704
1705#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1707#[serde(rename_all = "snake_case")]
1708pub enum SymbolicCounterexampleArtifactKind {
1709 SingleCall,
1711 Sequence,
1713}
1714
1715#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1717#[serde(tag = "kind", rename_all = "snake_case")]
1718pub enum SymbolicInvariantArtifactFailure {
1719 Predicate {
1721 name: String,
1723 #[serde(default, skip_serializing_if = "Option::is_none")]
1725 site: Option<SymbolicInvariantFailureSite>,
1726 },
1727 Handler {
1729 #[serde(default, skip_serializing_if = "Option::is_none")]
1731 name: Option<String>,
1732 reverter: Address,
1734 selector: Selector,
1736 fingerprint: B256,
1738 },
1739}
1740
1741#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1743#[serde(tag = "kind", rename_all = "snake_case")]
1744pub enum SymbolicInvariantFailureSite {
1745 SequenceCall { target: Address, selector: Selector, fingerprint: B256 },
1747 Invariant { target: Address, selector: Selector, fingerprint: B256 },
1749 AfterInvariant { target: Address, selector: Selector, fingerprint: B256 },
1751}
1752
1753#[derive(Clone, Debug, Serialize, Deserialize)]
1755#[serde(deny_unknown_fields)]
1756pub struct SymbolicCounterexampleTestIdentity {
1757 pub contract: String,
1759 pub test: String,
1761}
1762
1763#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1765#[serde(deny_unknown_fields)]
1766pub struct SymbolicCounterexampleCall {
1767 pub warp: Option<U256>,
1769 pub roll: Option<U256>,
1771 pub sender: Address,
1773 pub target: Address,
1775 pub calldata: Bytes,
1777 pub value: Option<U256>,
1779 pub contract_name: Option<String>,
1781 pub function_name: Option<String>,
1783 pub signature: Option<String>,
1785 pub args: Option<String>,
1787 pub raw_args: Option<String>,
1789}
1790
1791impl SymbolicCounterexampleCall {
1792 pub fn from_base_counterexample(
1794 counterexample: &BaseCounterExample,
1795 default_sender: Address,
1796 default_target: Address,
1797 ) -> Self {
1798 Self {
1799 warp: counterexample.warp,
1800 roll: counterexample.roll,
1801 sender: counterexample.sender.unwrap_or(default_sender),
1802 target: counterexample.addr.unwrap_or(default_target),
1803 calldata: counterexample.calldata.clone(),
1804 value: counterexample.value,
1805 contract_name: counterexample.contract_name.clone(),
1806 function_name: counterexample.func_name.clone(),
1807 signature: counterexample.signature.clone(),
1808 args: counterexample.args.clone(),
1809 raw_args: counterexample.raw_args.clone(),
1810 }
1811 }
1812
1813 pub fn to_base_counterexample(&self) -> BaseCounterExample {
1815 BaseCounterExample {
1816 warp: self.warp,
1817 roll: self.roll,
1818 sender: Some(self.sender),
1819 addr: Some(self.target),
1820 calldata: self.calldata.clone(),
1821 value: self.value,
1822 contract_name: self.contract_name.clone(),
1823 func_name: self.function_name.clone(),
1824 signature: self.signature.clone(),
1825 args: self.args.clone(),
1826 raw_args: self.raw_args.clone(),
1827 traces: None,
1828 show_solidity: false,
1829 fuzz: Default::default(),
1830 }
1831 }
1832
1833 pub fn to_basic_tx_details(&self) -> BasicTxDetails {
1835 BasicTxDetails {
1836 warp: self.warp,
1837 roll: self.roll,
1838 sender: self.sender,
1839 call_details: CallDetails {
1840 target: self.target,
1841 calldata: self.calldata.clone(),
1842 value: self.value,
1843 },
1844 }
1845 }
1846}
1847
1848#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1850pub struct TestResult {
1851 pub status: TestStatus,
1856
1857 pub reason: Option<String>,
1860
1861 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1866 pub invariant_failures: Vec<InvariantFailure>,
1867
1868 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1872 pub invariant_predicate_results: Vec<InvariantPredicateResult>,
1873
1874 #[serde(default, skip_serializing_if = "Option::is_none")]
1877 pub invariant_failure_dir: Option<std::path::PathBuf>,
1878
1879 #[serde(default, skip_serializing_if = "Option::is_none")]
1884 pub invariant_count: Option<usize>,
1885
1886 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1890 pub invariant_handler_failures: Vec<InvariantFailure>,
1891
1892 pub counterexample: Option<CounterExample>,
1894
1895 #[serde(default, skip_serializing_if = "Option::is_none")]
1900 pub counterexample_artifact: Option<SymbolicArtifactRef>,
1901
1902 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1904 pub counterexample_artifacts: Vec<SymbolicArtifactRef>,
1905
1906 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1908 pub symbolic_regressions: Vec<SymbolicRegressionRef>,
1909
1910 pub logs: Vec<Log>,
1913
1914 pub decoded_logs: Vec<String>,
1917
1918 pub kind: TestKind,
1920
1921 #[serde(default, skip_serializing_if = "Option::is_none")]
1923 pub symbolic: Option<SymbolicResult>,
1924
1925 pub traces: Traces,
1927
1928 #[serde(skip)]
1930 pub debug_bytecodes: AddressHashMap<Bytes>,
1931
1932 #[serde(skip)]
1936 pub gas_report_traces: Vec<Vec<CallTraceArena>>,
1937
1938 #[serde(skip)]
1940 pub line_coverage: Option<HitMaps>,
1941
1942 #[serde(rename = "labeled_addresses")] pub labels: AddressHashMap<String>,
1945
1946 #[serde(with = "foundry_common::serde_helpers::duration")]
1947 pub duration: Duration,
1948
1949 pub breakpoints: Breakpoints,
1951
1952 pub gas_snapshots: BTreeMap<String, BTreeMap<String, String>>,
1954
1955 #[serde(skip)]
1957 pub deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>,
1958
1959 #[serde(skip)]
1961 pub symbolic_portfolio_diagnostics: Option<PortfolioDiagnostics>,
1962
1963 #[serde(skip)]
1965 pub symbolic_diagnostics: Option<String>,
1966}
1967
1968impl fmt::Display for TestResult {
1969 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1970 f.write_str(&self.render_status_block(false, None))
1971 }
1972}
1973
1974impl TestResult {
1975 const fn is_debuggable_failure(&self) -> bool {
1978 self.status.is_failure()
1979 && !self.kind.is_invariant()
1980 && !self.kind.is_symbolic()
1981 && self.symbolic.is_none()
1982 }
1983
1984 pub fn add_counterexample_artifact(&mut self, artifact: SymbolicArtifactRef) {
1986 if !self.counterexample_artifacts.contains(&artifact) {
1987 self.counterexample_artifacts.push(artifact.clone());
1988 }
1989 if self.counterexample_artifact.is_none() {
1990 self.counterexample_artifact = Some(artifact);
1991 }
1992 }
1993
1994 fn render_status_block(
1995 &self,
1996 user_facing: bool,
1997 invariant_campaign_name: Option<&str>,
1998 ) -> String {
1999 match self.status {
2000 TestStatus::Success => {
2001 let mut s = String::from("[PASS]");
2003 if let Some(CounterExample::Sequence(original, sequence)) = &self.counterexample {
2004 s.push_str(
2005 format!(
2006 "\n\t[Best sequence] (original: {original}, shrunk: {})\n",
2007 sequence.len()
2008 )
2009 .as_str(),
2010 );
2011 for ex in sequence {
2012 writeln!(s, "{ex}").unwrap();
2013 }
2014 }
2015 self.write_invariant_predicate_results(
2016 &mut s,
2017 user_facing,
2018 true,
2019 invariant_campaign_name,
2020 );
2021 format!("{}", s.green().wrap())
2022 }
2023 TestStatus::Skipped => {
2024 let mut s = String::from("[SKIP");
2025 if let Some(reason) = &self.reason {
2026 write!(s, ": {reason}").unwrap();
2027 }
2028 s.push(']');
2029 self.write_invariant_predicate_results(
2030 &mut s,
2031 user_facing,
2032 true,
2033 invariant_campaign_name,
2034 );
2035 format!("{}", s.yellow())
2036 }
2037 TestStatus::Failure => {
2038 let mut s = String::new();
2039 let has_handler_failures = !self.invariant_handler_failures.is_empty();
2040 let is_invariant_failure =
2041 !self.invariant_failures.is_empty() || has_handler_failures;
2042 if !is_invariant_failure {
2043 s.push_str("[FAIL");
2046 if let Some(reason) = &self.reason {
2047 write!(s, ": {reason}").unwrap();
2048 }
2049 if let Some(counterexample) = &self.counterexample {
2050 match counterexample {
2051 CounterExample::Single(ex) => {
2052 write!(s, "; counterexample: {ex}]").unwrap();
2053 }
2054 CounterExample::Sequence(original, sequence) => {
2055 writeln!(
2056 s,
2057 "]\n\t[Sequence] (original: {original}, shrunk: {})",
2058 sequence.len()
2059 )
2060 .unwrap();
2061 for ex in sequence {
2062 writeln!(s, "{ex}").unwrap();
2063 }
2064 }
2065 }
2066 } else {
2067 s.push(']');
2068 }
2069 } else if !self.invariant_failures.is_empty() {
2070 let multi = self.invariant_failures.len() > 1;
2074 let is_campaign = self.invariant_count.is_some();
2075 for (i, failure) in self.invariant_failures.iter().enumerate() {
2076 if i > 0 {
2077 s.push('\n');
2078 }
2079 let is_anchor =
2080 matches!(failure, InvariantFailure::Predicate { is_anchor: true, .. });
2081 let name_suffix = if is_campaign || multi || !is_anchor {
2082 format!(" {}", failure.name())
2083 } else {
2084 String::new()
2085 };
2086 if let Some(CounterExample::Sequence(original, sequence)) =
2087 failure.counterexample()
2088 {
2089 writeln!(
2090 s,
2091 "[FAIL: {}]{name_suffix}\n\t[Sequence] (original: {original}, shrunk: {})",
2092 failure.reason(),
2093 sequence.len()
2094 )
2095 .unwrap();
2096 for ex in sequence {
2097 writeln!(s, "{ex}").unwrap();
2098 }
2099 } else {
2100 write!(s, "[FAIL: {}]{name_suffix}", failure.reason()).unwrap();
2101 }
2102 }
2103 }
2104
2105 let rollup_rendered = self.write_invariant_rollup(
2106 &mut s,
2107 user_facing,
2108 is_invariant_failure,
2109 invariant_campaign_name,
2110 );
2111 let show_predicate_header = if user_facing { !rollup_rendered } else { true };
2112 self.write_invariant_predicate_results(
2113 &mut s,
2114 user_facing,
2115 show_predicate_header,
2116 invariant_campaign_name,
2117 );
2118 self.write_invariant_persistence_note(&mut s);
2119 let handler_preceded = if user_facing {
2120 rollup_rendered
2121 || self.invariant_predicate_results.len() > 1
2122 || !self.invariant_failures.is_empty()
2123 } else {
2124 !self.invariant_failures.is_empty()
2125 || matches!(self.invariant_count, Some(t) if t > 1)
2126 };
2127 self.write_handler_failures(&mut s, user_facing, handler_preceded);
2128
2129 format!("{}", s.red().wrap())
2130 }
2131 }
2132 }
2133
2134 fn write_invariant_rollup(
2135 &self,
2136 s: &mut String,
2137 user_facing: bool,
2138 is_invariant_failure: bool,
2139 invariant_campaign_name: Option<&str>,
2140 ) -> bool {
2141 let Some(total) = self.invariant_count else {
2142 return false;
2143 };
2144 if total <= 1 || !is_invariant_failure {
2145 return false;
2146 }
2147
2148 writeln!(
2149 s,
2150 "\n{}: {}/{total} invariants broken",
2151 if user_facing {
2152 invariant_campaign_name.unwrap_or(INVARIANT_CAMPAIGN_FALLBACK_NAME)
2153 } else {
2154 "Predicates"
2155 },
2156 self.invariant_failures.len()
2157 )
2158 .unwrap();
2159 true
2160 }
2161
2162 fn write_invariant_persistence_note(&self, s: &mut String) {
2163 if self.invariant_failures.len() > 1
2164 && let Some(dir) = &self.invariant_failure_dir
2165 {
2166 writeln!(
2167 s,
2168 "{} invariant failure(s) persisted to {} — rerun to shrink",
2169 self.invariant_failures.len(),
2170 dir.display()
2171 )
2172 .unwrap();
2173 }
2174 }
2175
2176 fn write_handler_failures(&self, s: &mut String, user_facing: bool, preceded: bool) {
2177 if self.invariant_handler_failures.is_empty() {
2178 return;
2179 }
2180
2181 let prefix = if preceded { "\n" } else { "" };
2182 writeln!(
2183 s,
2184 "{prefix}{}: {} assertion bug(s) found",
2185 if user_facing { "Assertion Tests" } else { "Handler assertions" },
2186 self.invariant_handler_failures.len()
2187 )
2188 .unwrap();
2189 for failure in &self.invariant_handler_failures {
2190 if let Some(CounterExample::Sequence(original, sequence)) = failure.counterexample() {
2191 writeln!(
2192 s,
2193 "[FAIL: {}] {}\n\t[Sequence] (original: {original}, shrunk: {})",
2194 failure.reason(),
2195 failure.name(),
2196 sequence.len()
2197 )
2198 .unwrap();
2199 for ex in sequence {
2200 writeln!(s, "{ex}").unwrap();
2201 }
2202 } else {
2203 writeln!(s, "[FAIL: {}] {}", failure.reason(), failure.name()).unwrap();
2204 }
2205 }
2206 }
2207
2208 fn write_invariant_predicate_results(
2210 &self,
2211 s: &mut String,
2212 user_facing: bool,
2213 show_header: bool,
2214 invariant_campaign_name: Option<&str>,
2215 ) {
2216 if self.invariant_predicate_results.len() <= 1 {
2217 return;
2218 }
2219
2220 if show_header {
2221 s.push('\n');
2222 s.push_str(if user_facing {
2223 invariant_campaign_name.unwrap_or(INVARIANT_CAMPAIGN_FALLBACK_NAME)
2224 } else {
2225 "Predicates"
2226 });
2227 s.push_str(":\n");
2228 }
2229
2230 for predicate in &self.invariant_predicate_results {
2231 match predicate.status {
2232 TestStatus::Success => {
2233 writeln!(s, "[PASS] {}", predicate.name).unwrap();
2234 }
2235 TestStatus::Failure => {
2236 let reason = predicate.reason.as_deref().unwrap_or_default();
2237 writeln!(s, "[FAIL: {reason}] {}", predicate.name).unwrap();
2238 }
2239 TestStatus::Skipped => {
2240 if let Some(reason) = &predicate.reason {
2241 writeln!(s, "[SKIP: {reason}] {}", predicate.name).unwrap();
2242 } else {
2243 writeln!(s, "[SKIP] {}", predicate.name).unwrap();
2244 }
2245 }
2246 }
2247 }
2248 }
2249}
2250
2251macro_rules! extend {
2252 ($a:expr, $b:expr, $trace_kind:expr) => {
2253 $a.logs.extend($b.logs);
2254 $a.labels.extend($b.labels);
2255 $a.traces.extend($b.traces.map(|traces| ($trace_kind, traces)));
2256 $a.debug_bytecodes.extend($b.debug_bytecodes);
2257 $a.merge_coverages($b.line_coverage);
2258 };
2259}
2260
2261impl TestResult {
2262 pub fn new(setup: &TestSetup) -> Self {
2264 Self {
2265 labels: setup.labels.clone(),
2266 logs: setup.logs.clone(),
2267 traces: setup.traces.clone(),
2268 debug_bytecodes: setup.debug_bytecodes.clone(),
2269 line_coverage: setup.coverage.clone(),
2270 ..Default::default()
2271 }
2272 }
2273
2274 pub fn fail(reason: String) -> Self {
2276 Self { status: TestStatus::Failure, reason: Some(reason), ..Default::default() }
2277 }
2278
2279 pub fn setup_result(setup: TestSetup) -> Self {
2281 let TestSetup {
2282 address: _,
2283 fuzz_fixtures: _,
2284 logs,
2285 labels,
2286 traces,
2287 debug_bytecodes,
2288 coverage,
2289 deployed_libs: _,
2290 reason,
2291 skipped,
2292 ..
2293 } = setup;
2294 Self {
2295 status: if skipped { TestStatus::Skipped } else { TestStatus::Failure },
2296 reason,
2297 logs,
2298 traces,
2299 debug_bytecodes,
2300 line_coverage: coverage,
2301 labels,
2302 ..Default::default()
2303 }
2304 }
2305
2306 pub fn single_skip(&mut self, reason: SkipReason) {
2308 self.status = TestStatus::Skipped;
2309 self.reason = reason.0;
2310 }
2311
2312 pub fn single_fail(&mut self, reason: Option<String>) {
2314 self.status = TestStatus::Failure;
2315 self.reason = reason;
2316 }
2317
2318 pub fn single_result<FEN: FoundryEvmNetwork>(
2321 &mut self,
2322 success: bool,
2323 reason: Option<String>,
2324 raw_call_result: RawCallResult<FEN>,
2325 ) {
2326 self.kind = TestKind::Unit {
2327 gas: raw_call_result.gas_used.saturating_sub(raw_call_result.stipend),
2328 };
2329
2330 extend!(self, raw_call_result, TraceKind::Execution);
2331
2332 self.status = match success {
2333 true => TestStatus::Success,
2334 false => TestStatus::Failure,
2335 };
2336 self.reason = reason;
2337 self.duration = Duration::default();
2338 self.gas_report_traces = Vec::new();
2339
2340 if let Some(cheatcodes) = raw_call_result.cheatcodes {
2341 self.breakpoints = cheatcodes.breakpoints;
2342 self.gas_snapshots = cheatcodes.gas_snapshots;
2343 self.deprecated_cheatcodes = cheatcodes.deprecated;
2344 }
2345 }
2346
2347 pub fn fuzz_result(&mut self, result: FuzzTestResult) {
2350 self.kind = TestKind::Fuzz {
2351 median_gas: result.median_gas(false),
2352 mean_gas: result.mean_gas(false),
2353 first_case: result.first_case,
2354 runs: result.gas_by_case.len(),
2355 failed_corpus_replays: result.failed_corpus_replays,
2356 };
2357
2358 extend!(self, result, TraceKind::Execution);
2360
2361 self.status = if result.skipped {
2362 TestStatus::Skipped
2363 } else if result.success {
2364 TestStatus::Success
2365 } else {
2366 TestStatus::Failure
2367 };
2368 self.reason = result.reason;
2369 self.counterexample = result.counterexample;
2370 self.duration = Duration::default();
2371 self.gas_report_traces = result.gas_report_traces.into_iter().map(|t| vec![t]).collect();
2372 self.breakpoints = result.breakpoints.unwrap_or_default();
2373 self.deprecated_cheatcodes = result.deprecated_cheatcodes;
2374 }
2375
2376 pub fn fuzz_setup_fail(&mut self, e: Report) {
2378 self.kind = TestKind::Fuzz {
2379 first_case: Default::default(),
2380 runs: 0,
2381 mean_gas: 0,
2382 median_gas: 0,
2383 failed_corpus_replays: 0,
2384 };
2385 self.status = TestStatus::Failure;
2386 debug!(?e, "failed to set up fuzz testing environment");
2387 self.reason = Some(format!("failed to set up fuzz testing environment: {e}"));
2388 }
2389
2390 pub fn invariant_skip(&mut self, reason: SkipReason) {
2392 self.invariant_skip_with_predicates(reason, Vec::new());
2393 }
2394
2395 pub fn invariant_skip_with_predicates(
2397 &mut self,
2398 reason: SkipReason,
2399 invariant_predicate_results: Vec<InvariantPredicateResult>,
2400 ) {
2401 self.kind = TestKind::Invariant {
2402 runs: 1,
2403 calls: 1,
2404 reverts: 1,
2405 workers: default_invariant_workers(),
2406 metrics: HashMap::default(),
2407 failed_corpus_replays: 0,
2408 optimization_best_value: None,
2409 };
2410 self.status = TestStatus::Skipped;
2411 let predicate_count = invariant_predicate_results.len();
2412 let is_campaign = predicate_count > 1;
2413 self.reason = if is_campaign { None } else { reason.0 };
2414 self.invariant_count = is_campaign.then_some(predicate_count);
2415 self.invariant_predicate_results = invariant_predicate_results;
2416 }
2417
2418 pub fn invariant_replay_fail(
2420 &mut self,
2421 replayed_entirely: bool,
2422 invariant_name: &str,
2423 replay_reason: Option<String>,
2424 calls: usize,
2425 reverts: usize,
2426 call_sequence: Vec<BaseCounterExample>,
2427 ) {
2428 self.kind = TestKind::Invariant {
2429 runs: 1,
2430 calls,
2431 reverts,
2432 workers: default_invariant_workers(),
2433 metrics: HashMap::default(),
2434 failed_corpus_replays: 0,
2435 optimization_best_value: None,
2436 };
2437 self.status = TestStatus::Failure;
2438 self.reason = replay_reason.or_else(|| {
2439 if replayed_entirely {
2440 Some(format!("{invariant_name} replay failure"))
2441 } else {
2442 Some(format!("{invariant_name} persisted failure revert"))
2443 }
2444 });
2445 self.counterexample = Some(CounterExample::Sequence(call_sequence.len(), call_sequence));
2446 }
2447
2448 pub fn invariant_replay_success(&mut self, call_count: usize, reverts: usize) {
2450 self.kind = TestKind::Invariant {
2451 runs: 1,
2452 calls: call_count,
2453 reverts,
2454 workers: default_invariant_workers(),
2455 metrics: HashMap::default(),
2456 failed_corpus_replays: 0,
2457 optimization_best_value: None,
2458 };
2459 self.status = TestStatus::Success;
2460 self.reason = None;
2461 }
2462
2463 pub fn invariant_setup_fail(&mut self, e: Report) {
2465 self.kind = TestKind::Invariant {
2466 runs: 0,
2467 calls: 0,
2468 reverts: 0,
2469 workers: default_invariant_workers(),
2470 metrics: HashMap::default(),
2471 failed_corpus_replays: 0,
2472 optimization_best_value: None,
2473 };
2474 self.status = TestStatus::Failure;
2475 self.reason = Some(format!("failed to set up invariant testing environment: {e}"));
2476 }
2477
2478 #[expect(clippy::too_many_arguments)]
2480 pub fn invariant_result(
2481 &mut self,
2482 gas_report_traces: Vec<Vec<CallTraceArena>>,
2483 success: bool,
2484 invariant_failures: Vec<InvariantFailure>,
2485 invariant_predicate_results: Vec<InvariantPredicateResult>,
2486 invariant_failure_dir: Option<std::path::PathBuf>,
2487 invariant_count: Option<usize>,
2488 invariant_handler_failures: Vec<InvariantFailure>,
2489 counterexample: Option<CounterExample>,
2490 runs: usize,
2491 calls: usize,
2492 reverts: usize,
2493 metrics: Map<String, InvariantMetrics>,
2494 failed_corpus_replays: usize,
2495 workers: usize,
2496 optimization_best_value: Option<I256>,
2497 ) {
2498 self.kind = TestKind::Invariant {
2499 runs,
2500 calls,
2501 reverts,
2502 workers: workers.max(1),
2503 metrics,
2504 failed_corpus_replays,
2505 optimization_best_value,
2506 };
2507 self.status = if optimization_best_value.is_some() || success {
2509 TestStatus::Success
2510 } else {
2511 TestStatus::Failure
2512 };
2513 self.invariant_failures = invariant_failures;
2514 self.invariant_predicate_results = invariant_predicate_results;
2515 self.invariant_failure_dir = invariant_failure_dir;
2516 self.invariant_count = invariant_count;
2517 self.invariant_handler_failures = invariant_handler_failures;
2518 self.counterexample = counterexample;
2522 let artifacts = self
2523 .invariant_failures
2524 .iter()
2525 .chain(&self.invariant_handler_failures)
2526 .flat_map(|failure| {
2527 let mut artifacts = Vec::new();
2528 if let Some(artifact) = failure.artifact().cloned() {
2529 artifacts.push(artifact);
2530 }
2531 if let Some(minimization) = failure.minimization().cloned() {
2532 artifacts.push(minimization.original);
2533 artifacts.push(minimization.minimized);
2534 }
2535 artifacts
2536 })
2537 .collect::<Vec<_>>();
2538 for artifact in artifacts {
2539 self.add_counterexample_artifact(artifact);
2540 }
2541 self.gas_report_traces = gas_report_traces;
2542 }
2543
2544 pub fn table_result(&mut self, result: FuzzTestResult) {
2547 self.kind = TestKind::Table {
2548 median_gas: result.median_gas(false),
2549 mean_gas: result.mean_gas(false),
2550 runs: result.gas_by_case.len(),
2551 };
2552
2553 extend!(self, result, TraceKind::Execution);
2555
2556 self.status = if result.skipped {
2557 TestStatus::Skipped
2558 } else if result.success {
2559 TestStatus::Success
2560 } else {
2561 TestStatus::Failure
2562 };
2563 self.reason = result.reason;
2564 self.counterexample = result.counterexample;
2565 self.duration = Duration::default();
2566 self.gas_report_traces = result.gas_report_traces.into_iter().map(|t| vec![t]).collect();
2567 self.breakpoints = result.breakpoints.unwrap_or_default();
2568 self.deprecated_cheatcodes = result.deprecated_cheatcodes;
2569 }
2570
2571 pub fn symbolic_result(
2573 &mut self,
2574 status: TestStatus,
2575 reason: Option<String>,
2576 counterexample: Option<CounterExample>,
2577 symbolic: SymbolicResult,
2578 ) {
2579 let stats = symbolic.solver.stats;
2580 self.kind = TestKind::Symbolic {
2581 paths: stats.paths,
2582 solver_queries: stats.solver_queries,
2583 smt_queries: stats.smt_queries,
2584 sat_queries: stats.sat_queries,
2585 model_queries: stats.model_queries,
2586 sat_cache_hits: stats.sat_cache_hits,
2587 model_cache_hits: stats.model_cache_hits,
2588 heuristic_witnesses: stats.heuristic_witnesses,
2589 solver_time_ms: stats.solver_time_ms,
2590 smt_input_bytes: stats.smt_input_bytes,
2591 smt_max_query_bytes: stats.smt_max_query_bytes,
2592 smt_build_time_ms: stats.smt_build_time_ms,
2593 smt_max_query_time_ms: stats.smt_max_query_time_ms,
2594 };
2595 self.status = status;
2596 self.reason = reason;
2597 self.counterexample = counterexample;
2598 self.record_symbolic(symbolic);
2599 self.duration = Duration::default();
2600 }
2601
2602 pub(crate) fn record_symbolic(&mut self, symbolic: SymbolicResult) {
2604 if let Some(artifact) = symbolic.artifact.clone() {
2605 self.add_counterexample_artifact(artifact);
2606 }
2607 if let Some(minimization) = symbolic.minimization.clone() {
2608 self.add_counterexample_artifact(minimization.original);
2609 self.add_counterexample_artifact(minimization.minimized);
2610 }
2611 self.symbolic = Some(symbolic);
2612 }
2613
2614 pub fn replay_result(
2616 &mut self,
2617 corpus_entries: usize,
2618 showmap_files: usize,
2619 skipped_entries: usize,
2620 duration: Duration,
2621 ) {
2622 self.kind = TestKind::Replay { corpus_entries, showmap_files, skipped_entries };
2623 self.status = TestStatus::Success;
2624 self.duration = duration;
2625 }
2626
2627 pub fn replay_skip(&mut self, reason: impl Into<String>) {
2629 self.kind = TestKind::Replay { corpus_entries: 0, showmap_files: 0, skipped_entries: 0 };
2630 self.status = TestStatus::Skipped;
2631 self.reason = Some(reason.into());
2632 self.duration = Duration::default();
2633 }
2634
2635 pub const fn is_fuzz(&self) -> bool {
2637 matches!(self.kind, TestKind::Fuzz { .. })
2638 }
2639
2640 pub fn short_result(&self, name: &str) -> String {
2642 self.short_result_with_campaign_name(name, None)
2643 }
2644
2645 pub(crate) fn short_result_with_suite(&self, name: &str, suite_name: &str) -> String {
2646 self.short_result_with_campaign_name(name, Some(get_contract_name(suite_name)))
2647 }
2648
2649 fn short_result_with_campaign_name(&self, name: &str, contract_name: Option<&str>) -> String {
2650 let is_invariant_campaign = self.is_invariant_campaign();
2651 let name = if is_invariant_campaign {
2652 contract_name
2653 .map(invariant_campaign_display_name)
2654 .map(Cow::Owned)
2655 .unwrap_or(Cow::Borrowed(INVARIANT_CAMPAIGN_FALLBACK_NAME))
2656 } else {
2657 Cow::Borrowed(name)
2658 };
2659 let status = self.render_status_block(true, is_invariant_campaign.then_some(name.as_ref()));
2660 format!("{status} {name} {}", self.kind.report())
2661 }
2662
2663 const fn is_invariant_campaign(&self) -> bool {
2664 self.kind.is_invariant() && self.invariant_count.is_some()
2665 }
2666
2667 fn logical_count(&self) -> usize {
2668 let skipped = self.skipped_predicate_count();
2669 if skipped == 0 {
2670 1
2671 } else if self.status.is_skipped() && skipped == self.invariant_predicate_results.len() {
2672 skipped
2673 } else {
2674 1 + skipped
2675 }
2676 }
2677
2678 fn passed_count(&self) -> usize {
2679 usize::from(self.status.is_success())
2680 }
2681
2682 fn skipped_count(&self) -> usize {
2683 let skipped = self.skipped_predicate_count();
2684 if skipped == 0 && self.status.is_skipped() { 1 } else { skipped }
2685 }
2686
2687 fn failed_count(&self) -> usize {
2688 usize::from(self.status.is_failure())
2689 }
2690
2691 fn skipped_predicate_count(&self) -> usize {
2692 self.invariant_predicate_results
2693 .iter()
2694 .filter(|predicate| predicate.status.is_skipped())
2695 .count()
2696 }
2697
2698 pub fn extend<FEN: FoundryEvmNetwork>(&mut self, call_result: RawCallResult<FEN>) {
2700 extend!(self, call_result, TraceKind::Execution);
2701 }
2702
2703 pub(crate) fn extend_setup<FEN: FoundryEvmNetwork>(&mut self, call_result: RawCallResult<FEN>) {
2705 extend!(self, call_result, TraceKind::Setup);
2706 }
2707
2708 pub fn merge_coverages(&mut self, other_coverage: Option<HitMaps>) {
2710 HitMaps::merge_opt(&mut self.line_coverage, other_coverage);
2711 }
2712}
2713
2714#[derive(Clone, Debug, PartialEq, Eq)]
2716pub enum TestKindReport {
2717 Unit {
2718 gas: u64,
2719 },
2720 Fuzz {
2721 runs: usize,
2722 mean_gas: u64,
2723 median_gas: u64,
2724 failed_corpus_replays: usize,
2725 },
2726 Invariant {
2727 runs: usize,
2728 calls: usize,
2729 reverts: usize,
2730 metrics: Map<String, InvariantMetrics>,
2731 failed_corpus_replays: usize,
2732 optimization_best_value: Option<I256>,
2734 },
2735 Table {
2736 runs: usize,
2737 mean_gas: u64,
2738 median_gas: u64,
2739 },
2740 Symbolic {
2741 paths: usize,
2742 solver_queries: usize,
2743 smt_queries: usize,
2744 sat_queries: usize,
2745 model_queries: usize,
2746 sat_cache_hits: usize,
2747 model_cache_hits: usize,
2748 heuristic_witnesses: usize,
2749 solver_time_ms: u64,
2750 smt_input_bytes: u64,
2751 smt_max_query_bytes: u64,
2752 smt_build_time_ms: u64,
2753 smt_max_query_time_ms: u64,
2754 },
2755 Replay {
2757 corpus_entries: usize,
2758 showmap_files: usize,
2759 skipped_entries: usize,
2760 },
2761}
2762
2763impl fmt::Display for TestKindReport {
2764 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2765 match self {
2766 Self::Unit { gas } => {
2767 write!(f, "(gas: {gas})")
2768 }
2769 Self::Fuzz { runs, mean_gas, median_gas, failed_corpus_replays } => {
2770 if *failed_corpus_replays != 0 {
2771 write!(
2772 f,
2773 "(runs: {runs}, μ: {mean_gas}, ~: {median_gas}, failed corpus replays: {failed_corpus_replays})"
2774 )
2775 } else {
2776 write!(f, "(runs: {runs}, μ: {mean_gas}, ~: {median_gas})")
2777 }
2778 }
2779 Self::Invariant {
2780 runs,
2781 calls,
2782 reverts,
2783 metrics: _,
2784 failed_corpus_replays,
2785 optimization_best_value,
2786 } => {
2787 if let Some(best_value) = optimization_best_value {
2789 write!(f, "(best: {best_value}, runs: {runs}, calls: {calls})")
2790 } else if *failed_corpus_replays != 0 {
2791 write!(
2792 f,
2793 "(runs: {runs}, calls: {calls}, reverts: {reverts}, failed corpus replays: {failed_corpus_replays})"
2794 )
2795 } else {
2796 write!(f, "(runs: {runs}, calls: {calls}, reverts: {reverts})")
2797 }
2798 }
2799 Self::Table { runs, mean_gas, median_gas } => {
2800 write!(f, "(runs: {runs}, μ: {mean_gas}, ~: {median_gas})")
2801 }
2802 Self::Symbolic {
2803 paths,
2804 solver_queries,
2805 smt_queries,
2806 sat_queries,
2807 model_queries,
2808 sat_cache_hits,
2809 model_cache_hits,
2810 heuristic_witnesses,
2811 solver_time_ms,
2812 smt_input_bytes: _,
2813 smt_max_query_bytes: _,
2814 smt_build_time_ms: _,
2815 smt_max_query_time_ms: _,
2816 } => {
2817 write!(
2818 f,
2819 "(paths: {paths}, queries: {solver_queries}, smt: {smt_queries}, sat: {sat_queries} ({sat_cache_hits} cached), models: {model_queries} ({model_cache_hits} cached), hard-arith: {heuristic_witnesses}, solver: {solver_time_ms}ms)"
2820 )
2821 }
2822 Self::Replay { corpus_entries, showmap_files, skipped_entries } => {
2823 if *skipped_entries != 0 {
2824 write!(
2825 f,
2826 "(replay: {corpus_entries} entries, {showmap_files} files, {skipped_entries} skipped)"
2827 )
2828 } else {
2829 write!(f, "(replay: {corpus_entries} entries, {showmap_files} files)")
2830 }
2831 }
2832 }
2833 }
2834}
2835
2836impl TestKindReport {
2837 pub const fn gas(&self) -> u64 {
2839 match *self {
2840 Self::Unit { gas } => gas,
2841 Self::Fuzz { median_gas, .. } | Self::Table { median_gas, .. } => median_gas,
2843 Self::Invariant { .. } | Self::Symbolic { .. } | Self::Replay { .. } => 0,
2845 }
2846 }
2847}
2848
2849#[derive(Clone, Debug, Serialize, Deserialize)]
2851pub enum TestKind {
2852 Unit { gas: u64 },
2854 Fuzz {
2856 first_case: FuzzCase,
2858 runs: usize,
2859 mean_gas: u64,
2860 median_gas: u64,
2861 failed_corpus_replays: usize,
2862 },
2863 Invariant {
2865 runs: usize,
2866 calls: usize,
2867 reverts: usize,
2868 #[serde(default = "default_invariant_workers")]
2870 workers: usize,
2871 metrics: Map<String, InvariantMetrics>,
2872 failed_corpus_replays: usize,
2873 optimization_best_value: Option<I256>,
2875 },
2876 Table { runs: usize, mean_gas: u64, median_gas: u64 },
2878 Symbolic {
2880 paths: usize,
2881 solver_queries: usize,
2882 #[serde(default)]
2883 smt_queries: usize,
2884 #[serde(default)]
2885 sat_queries: usize,
2886 #[serde(default)]
2887 model_queries: usize,
2888 #[serde(default)]
2889 sat_cache_hits: usize,
2890 #[serde(default)]
2891 model_cache_hits: usize,
2892 #[serde(default)]
2893 heuristic_witnesses: usize,
2894 #[serde(default)]
2895 solver_time_ms: u64,
2896 #[serde(default)]
2897 smt_input_bytes: u64,
2898 #[serde(default)]
2899 smt_max_query_bytes: u64,
2900 #[serde(default)]
2901 smt_build_time_ms: u64,
2902 #[serde(default)]
2903 smt_max_query_time_ms: u64,
2904 },
2905 Replay { corpus_entries: usize, showmap_files: usize, skipped_entries: usize },
2907}
2908
2909impl Default for TestKind {
2910 fn default() -> Self {
2911 Self::Unit { gas: 0 }
2912 }
2913}
2914
2915impl TestKind {
2916 pub const fn is_fuzz(&self) -> bool {
2918 matches!(self, Self::Fuzz { .. })
2919 }
2920
2921 pub const fn is_invariant(&self) -> bool {
2923 matches!(self, Self::Invariant { .. })
2924 }
2925
2926 pub const fn is_symbolic(&self) -> bool {
2928 matches!(self, Self::Symbolic { .. })
2929 }
2930
2931 pub const fn invariant_workers(&self) -> Option<usize> {
2933 match self {
2934 Self::Invariant { workers, .. } => Some(*workers),
2935 _ => None,
2936 }
2937 }
2938
2939 pub fn report(&self) -> TestKindReport {
2941 match self {
2942 Self::Unit { gas } => TestKindReport::Unit { gas: *gas },
2943 Self::Fuzz { first_case: _, runs, mean_gas, median_gas, failed_corpus_replays } => {
2944 TestKindReport::Fuzz {
2945 runs: *runs,
2946 mean_gas: *mean_gas,
2947 median_gas: *median_gas,
2948 failed_corpus_replays: *failed_corpus_replays,
2949 }
2950 }
2951 Self::Invariant {
2952 runs,
2953 calls,
2954 reverts,
2955 workers: _,
2956 metrics: _,
2957 failed_corpus_replays,
2958 optimization_best_value,
2959 } => TestKindReport::Invariant {
2960 runs: *runs,
2961 calls: *calls,
2962 reverts: *reverts,
2963 metrics: HashMap::default(),
2964 failed_corpus_replays: *failed_corpus_replays,
2965 optimization_best_value: *optimization_best_value,
2966 },
2967 Self::Table { runs, mean_gas, median_gas } => {
2968 TestKindReport::Table { runs: *runs, mean_gas: *mean_gas, median_gas: *median_gas }
2969 }
2970 Self::Symbolic {
2971 paths,
2972 solver_queries,
2973 smt_queries,
2974 sat_queries,
2975 model_queries,
2976 sat_cache_hits,
2977 model_cache_hits,
2978 heuristic_witnesses,
2979 solver_time_ms,
2980 smt_input_bytes,
2981 smt_max_query_bytes,
2982 smt_build_time_ms,
2983 smt_max_query_time_ms,
2984 } => TestKindReport::Symbolic {
2985 paths: *paths,
2986 solver_queries: *solver_queries,
2987 smt_queries: *smt_queries,
2988 sat_queries: *sat_queries,
2989 model_queries: *model_queries,
2990 sat_cache_hits: *sat_cache_hits,
2991 model_cache_hits: *model_cache_hits,
2992 heuristic_witnesses: *heuristic_witnesses,
2993 solver_time_ms: *solver_time_ms,
2994 smt_input_bytes: *smt_input_bytes,
2995 smt_max_query_bytes: *smt_max_query_bytes,
2996 smt_build_time_ms: *smt_build_time_ms,
2997 smt_max_query_time_ms: *smt_max_query_time_ms,
2998 },
2999 Self::Replay { corpus_entries, showmap_files, skipped_entries } => {
3000 TestKindReport::Replay {
3001 corpus_entries: *corpus_entries,
3002 showmap_files: *showmap_files,
3003 skipped_entries: *skipped_entries,
3004 }
3005 }
3006 }
3007 }
3008}
3009
3010const fn default_invariant_workers() -> usize {
3011 1
3012}
3013
3014#[derive(Clone, Debug, Default)]
3019pub struct TestSetup {
3020 pub address: Address,
3022 pub fuzz_fixtures: FuzzFixtures,
3024
3025 pub logs: Vec<Log>,
3027 pub labels: AddressHashMap<String>,
3029 pub traces: Traces,
3031 pub debug_bytecodes: AddressHashMap<Bytes>,
3033 pub coverage: Option<HitMaps>,
3035 pub deployed_libs: Vec<Address>,
3037 pub(crate) fuzz_state: OnceLock<EvmFuzzState>,
3039
3040 pub reason: Option<String>,
3042 pub skipped: bool,
3044 pub deployment_failure: bool,
3046}
3047
3048impl TestSetup {
3049 pub fn failed(reason: String) -> Self {
3050 Self { reason: Some(reason), ..Default::default() }
3051 }
3052
3053 pub fn skipped(reason: String) -> Self {
3054 Self { reason: Some(reason), skipped: true, ..Default::default() }
3055 }
3056
3057 pub fn extend<FEN: FoundryEvmNetwork>(
3058 &mut self,
3059 raw: RawCallResult<FEN>,
3060 trace_kind: TraceKind,
3061 ) {
3062 extend!(self, raw, trace_kind);
3063 }
3064
3065 pub fn merge_coverages(&mut self, other_coverage: Option<HitMaps>) {
3066 HitMaps::merge_opt(&mut self.coverage, other_coverage);
3067 }
3068}