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, shell};
13use foundry_config::{SymbolicConfig, SymbolicExplorationOrder, SymbolicStorageLayout};
14use foundry_evm::{
15 core::{Breakpoints, evm::FoundryEvmNetwork},
16 coverage::HitMaps,
17 decode::SkipReason,
18 executors::{
19 RawCallResult,
20 invariant::{CheckSequenceFailureSite, CheckSequenceOutcome, InvariantMetrics},
21 },
22 fuzz::{
23 CallDetails, CounterExample, FuzzCase, FuzzFixtures, FuzzTestResult,
24 strategies::EvmFuzzState,
25 },
26 traces::{CallTraceArena, CallTraceDecoder, TraceKind, Traces},
27};
28use foundry_evm_symbolic::{SymbolicStats, SymbolicStopReason, SymbolicStorageAssignment};
29use serde::{Deserialize, Serialize};
30use std::{
31 collections::{BTreeMap, HashMap as Map},
32 fmt::{self, Write},
33 path::PathBuf,
34 sync::OnceLock,
35 time::Duration,
36};
37use yansi::Paint;
38
39const INVARIANT_CAMPAIGN_FALLBACK_NAME: &str = "Invariant campaign";
40const SYMBOLIC_RESULT_SCHEMA_VERSION: u32 = 1;
41pub const SYMBOLIC_COUNTEREXAMPLE_ARTIFACT_SCHEMA: &str = "foundry:symbolic.counterexample@v1";
42pub const SYMBOLIC_COUNTEREXAMPLE_ARTIFACT_SCHEMA_VERSION: u32 = 1;
43
44#[derive(Clone, Debug)]
46pub struct TestOutcome {
47 pub results: BTreeMap<String, SuiteResult>,
51 pub(crate) json_file_results: Option<BTreeMap<String, SuiteResult>>,
54 pub allow_failure: bool,
56 pub last_run_decoder: Option<CallTraceDecoder>,
62 pub gas_report: Option<GasReport>,
64 pub known_contracts: Option<ContractsByArtifact>,
66 pub fuzz_seed: Option<U256>,
68}
69
70impl TestOutcome {
71 pub const fn new(
73 known_contracts: Option<ContractsByArtifact>,
74 results: BTreeMap<String, SuiteResult>,
75 allow_failure: bool,
76 fuzz_seed: Option<U256>,
77 ) -> Self {
78 Self {
79 results,
80 json_file_results: None,
81 allow_failure,
82 last_run_decoder: None,
83 gas_report: None,
84 known_contracts,
85 fuzz_seed,
86 }
87 }
88
89 pub const fn empty(known_contracts: Option<ContractsByArtifact>, allow_failure: bool) -> Self {
91 Self::new(known_contracts, BTreeMap::new(), allow_failure, None)
92 }
93
94 pub fn successes(&self) -> impl Iterator<Item = (&String, &TestResult)> {
96 self.tests().filter(|(_, t)| t.status.is_success())
97 }
98
99 pub fn skips(&self) -> impl Iterator<Item = (&String, &TestResult)> {
101 self.tests().filter(|(_, t)| t.status.is_skipped())
102 }
103
104 pub fn failures(&self) -> impl Iterator<Item = (&String, &TestResult)> {
106 self.tests().filter(|(_, t)| t.status.is_failure())
107 }
108
109 pub fn tests(&self) -> impl Iterator<Item = (&String, &TestResult)> {
111 self.results.values().flat_map(|suite| suite.tests())
112 }
113
114 pub fn into_tests(self) -> impl Iterator<Item = SuiteTestResult> {
116 self.results.into_iter().flat_map(|(artifact_id, suite)| {
117 suite.test_results.into_iter().map(move |(signature, result)| SuiteTestResult {
118 artifact_id: artifact_id.clone(),
119 signature,
120 result,
121 })
122 })
123 }
124
125 pub fn passed(&self) -> usize {
127 self.results.values().map(SuiteResult::passed).sum()
128 }
129
130 pub fn skipped(&self) -> usize {
132 self.results.values().map(SuiteResult::skipped).sum()
133 }
134
135 pub fn failed(&self) -> usize {
137 self.results.values().map(SuiteResult::failed).sum()
138 }
139
140 pub fn has_fuzz_failures(&self) -> bool {
142 self.failures().any(|(_, t)| t.kind.is_fuzz() || t.kind.is_invariant())
143 }
144
145 fn failed_tests_are_debuggable(&self) -> bool {
147 self.failures().all(|(_, result)| result.is_debuggable_failure())
148 }
149
150 fn invariant_workers_hint(&self) -> Option<usize> {
152 let mut workers = self.failures().filter_map(|(_, result)| result.kind.invariant_workers());
153 let first = workers.next()?;
154 (first > 1 && workers.all(|workers| workers == first)).then_some(first)
155 }
156
157 pub fn total_time(&self) -> Duration {
161 self.results.values().map(|suite| suite.duration).sum()
162 }
163
164 pub fn summary(&self, wall_clock_time: Duration) -> String {
166 let num_test_suites = self.results.len();
167 let suites = if num_test_suites == 1 { "suite" } else { "suites" };
168 let (passed, failed, skipped) = (self.passed(), self.failed(), self.skipped());
169 format!(
170 "\nRan {num_test_suites} test {suites} in {wall_clock_time:.2?} ({:.2?} CPU time): {} tests passed, {} failed, {} skipped ({} total tests)",
171 self.total_time(),
172 passed.green(),
173 failed.red(),
174 skipped.yellow(),
175 passed + failed + skipped
176 )
177 }
178
179 pub fn ensure_ok(&self, silent: bool) -> eyre::Result<()> {
181 let failures = self.failures().count();
182 if self.allow_failure || failures == 0 {
183 return Ok(());
184 }
185
186 if shell::is_quiet() || silent {
187 std::process::exit(1);
188 }
189
190 sh_println!("\nFailing tests:")?;
191 for (suite_name, suite) in &self.results {
192 let failed = suite.failed();
193 if failed == 0 {
194 continue;
195 }
196
197 let term = if failed > 1 { "tests" } else { "test" };
198 sh_println!("Encountered {failed} failing {term} in {suite_name}")?;
199 for (name, result) in suite.failures() {
200 sh_println!("{}", result.short_result_with_suite(name, suite_name))?;
201 }
202 sh_println!()?;
203 }
204 sh_println!(
205 "Encountered a total of {} failing tests, {} tests succeeded",
206 failures.to_string().red(),
207 self.passed().to_string().green()
208 )?;
209
210 let test_word = if failures == 1 { "test" } else { "tests" };
211 sh_println!(
212 "\nTip: Run {} to retry only the {failures} failed {test_word}",
213 "`forge test --rerun`".cyan()
214 )?;
215 if self.failed_tests_are_debuggable() {
216 sh_println!(
217 "Tip: Run {} to inspect one failing test in the debugger",
218 "`forge test --debug --match-test <TEST_NAME>`".cyan()
219 )?;
220 }
221
222 if let Some(seed) = self.fuzz_seed
224 && self.has_fuzz_failures()
225 {
226 sh_println!(
227 "\nFuzz seed: {} (use {} to reproduce)",
228 format!("{seed:#x}").cyan(),
229 "`--fuzz-seed`".cyan()
230 )?;
231 if let Some(invariant_workers) = self.invariant_workers_hint() {
232 sh_println!(
233 "Invariant workers: {invariant_workers} (use {} to reproduce)",
234 format!("`--invariant-workers {invariant_workers}`").cyan()
235 )?;
236 }
237 }
238
239 std::process::exit(1);
240 }
241
242 pub fn remove_first(&mut self) -> Option<(String, String, TestResult)> {
244 self.results.iter_mut().find_map(|(suite_name, suite)| {
245 let (test_name, result) = suite.test_results.pop_first()?;
246 Some((suite_name.clone(), test_name, result))
247 })
248 }
249}
250
251#[derive(Clone, Debug, Serialize)]
253pub struct SuiteResult {
254 #[serde(with = "foundry_common::serde_helpers::duration")]
256 pub duration: Duration,
257 pub test_results: BTreeMap<String, TestResult>,
259 pub warnings: Vec<String>,
261}
262
263impl SuiteResult {
264 pub fn new(
265 duration: Duration,
266 test_results: BTreeMap<String, TestResult>,
267 mut warnings: Vec<String>,
268 ) -> Self {
269 let deprecated_cheatcodes = test_results
271 .values()
272 .flat_map(|result| result.deprecated_cheatcodes.iter().map(|(k, v)| (*k, *v)))
273 .collect::<HashMap<_, _>>();
274 if !deprecated_cheatcodes.is_empty() {
275 let mut warning =
276 "the following cheatcode(s) are deprecated and will be removed in future versions:"
277 .to_string();
278 for (cheatcode, reason) in deprecated_cheatcodes {
279 write!(warning, "\n {cheatcode}").unwrap();
280 if let Some(reason) = reason {
281 write!(warning, ": {reason}").unwrap();
282 }
283 }
284 warnings.push(warning);
285 }
286
287 Self { duration, test_results, warnings }
288 }
289
290 pub fn successes(&self) -> impl Iterator<Item = (&String, &TestResult)> {
292 self.tests().filter(|(_, t)| t.status.is_success())
293 }
294
295 pub fn skips(&self) -> impl Iterator<Item = (&String, &TestResult)> {
297 self.tests().filter(|(_, t)| t.status.is_skipped())
298 }
299
300 pub fn failures(&self) -> impl Iterator<Item = (&String, &TestResult)> {
302 self.tests().filter(|(_, t)| t.status.is_failure())
303 }
304
305 pub fn passed(&self) -> usize {
307 self.test_results.values().filter(|t| t.status.is_success()).count()
308 }
309
310 pub fn skipped(&self) -> usize {
312 self.test_results.values().map(TestResult::skipped_count).sum()
313 }
314
315 pub fn failed(&self) -> usize {
317 self.test_results.values().filter(|t| t.status.is_failure()).count()
318 }
319
320 pub fn tests(&self) -> impl Iterator<Item = (&String, &TestResult)> {
322 self.test_results.iter()
323 }
324
325 pub fn is_empty(&self) -> bool {
327 self.test_results.is_empty()
328 }
329
330 pub fn len(&self) -> usize {
332 self.test_results.values().map(TestResult::logical_count).sum()
333 }
334
335 pub fn total_time(&self) -> Duration {
339 self.test_results.values().map(|result| result.duration).sum()
340 }
341
342 pub fn summary(&self) -> String {
344 let failed = self.failed();
345 let result = if failed == 0 { "ok".green() } else { "FAILED".red() };
346 format!(
347 "Suite result: {result}. {} passed; {} failed; {} skipped; finished in {:.2?} ({:.2?} CPU time)",
348 self.passed().green(),
349 failed.red(),
350 self.skipped().yellow(),
351 self.duration,
352 self.total_time(),
353 )
354 }
355}
356
357#[derive(Clone, Debug)]
361pub struct SuiteTestResult {
362 pub artifact_id: String,
365 pub signature: String,
367 pub result: TestResult,
369}
370
371impl SuiteTestResult {
372 pub const fn gas_used(&self) -> u64 {
374 self.result.kind.report().gas()
375 }
376
377 pub fn contract_name(&self) -> &str {
379 get_contract_name(&self.artifact_id)
380 }
381}
382
383#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
385pub enum TestStatus {
386 Success,
387 #[default]
388 Failure,
389 Skipped,
390}
391
392impl TestStatus {
393 #[inline]
395 pub const fn is_success(self) -> bool {
396 matches!(self, Self::Success)
397 }
398
399 #[inline]
401 pub const fn is_failure(self) -> bool {
402 matches!(self, Self::Failure)
403 }
404
405 #[inline]
407 pub const fn is_skipped(self) -> bool {
408 matches!(self, Self::Skipped)
409 }
410}
411
412#[derive(Clone, Debug, Serialize, Deserialize)]
415#[serde(tag = "kind", rename_all = "snake_case")]
416pub enum InvariantFailure {
417 Predicate {
419 name: String,
421 reason: String,
423 #[serde(default, skip_serializing_if = "Option::is_none")]
425 counterexample: Option<CounterExample>,
426 #[serde(default, skip_serializing_if = "Option::is_none")]
428 artifact: Option<SymbolicArtifactRef>,
429 #[serde(default, skip_serializing_if = "Option::is_none")]
431 minimization: Option<SymbolicCounterexampleMinimization>,
432 persisted_path: PathBuf,
434 #[serde(default)]
438 is_anchor: bool,
439 },
440 Handler {
442 name: String,
445 reverter: Address,
447 selector: Selector,
449 reason: String,
451 #[serde(default, skip_serializing_if = "Option::is_none")]
453 counterexample: Option<CounterExample>,
454 #[serde(default, skip_serializing_if = "Option::is_none")]
456 artifact: Option<SymbolicArtifactRef>,
457 },
458}
459
460impl InvariantFailure {
461 pub fn reason(&self) -> &str {
463 match self {
464 Self::Predicate { reason, .. } | Self::Handler { reason, .. } => reason,
465 }
466 }
467
468 pub fn name(&self) -> &str {
470 match self {
471 Self::Predicate { name, .. } | Self::Handler { name, .. } => name,
472 }
473 }
474
475 pub fn predicate_name(&self) -> Option<&str> {
477 match self {
478 Self::Predicate { name, .. } => Some(name),
479 Self::Handler { .. } => None,
480 }
481 }
482
483 pub const fn counterexample(&self) -> Option<&CounterExample> {
485 match self {
486 Self::Predicate { counterexample, .. } | Self::Handler { counterexample, .. } => {
487 counterexample.as_ref()
488 }
489 }
490 }
491
492 pub const fn artifact(&self) -> Option<&SymbolicArtifactRef> {
494 match self {
495 Self::Predicate { artifact, .. } | Self::Handler { artifact, .. } => artifact.as_ref(),
496 }
497 }
498
499 pub const fn minimization(&self) -> Option<&SymbolicCounterexampleMinimization> {
501 match self {
502 Self::Predicate { minimization, .. } => minimization.as_ref(),
503 Self::Handler { .. } => None,
504 }
505 }
506}
507
508#[derive(Clone, Debug, Serialize, Deserialize)]
510pub struct InvariantPredicateResult {
511 pub name: String,
513 pub status: TestStatus,
515 #[serde(default, skip_serializing_if = "Option::is_none")]
517 pub reason: Option<String>,
518}
519
520#[derive(Clone, Debug, Serialize, Deserialize)]
522pub struct SymbolicResult {
523 #[serde(default = "symbolic_result_schema_version")]
525 pub schema_version: u32,
526 pub status: SymbolicResultStatus,
528 pub incomplete: Option<SymbolicIncomplete>,
530 pub bounds: SymbolicBounds,
532 pub solver: SymbolicSolverMetadata,
534 pub assumptions: Vec<SymbolicAssumption>,
536 pub call_trace: SymbolicCallTrace,
538 pub replay: SymbolicReplayMetadata,
540 pub counterexample: Option<SymbolicCounterexample>,
542 #[serde(default, skip_serializing_if = "Option::is_none")]
544 pub corpus_seeds: Option<SymbolicCorpusSeedMetadata>,
545 #[serde(default, skip_serializing_if = "Option::is_none")]
547 pub artifact: Option<SymbolicArtifactRef>,
548 #[serde(default, skip_serializing_if = "Option::is_none")]
550 pub minimization: Option<SymbolicCounterexampleMinimization>,
551}
552
553impl SymbolicResult {
554 pub fn pass(config: &SymbolicConfig, stats: SymbolicStats) -> Self {
556 Self::base(config, stats)
557 }
558
559 pub fn fail_counterexample(
561 config: &SymbolicConfig,
562 stats: SymbolicStats,
563 call_trace: SymbolicCallTrace,
564 counterexample: SymbolicCounterexample,
565 ) -> Self {
566 Self {
567 counterexample: Some(counterexample),
568 ..Self::fail_counterexample_sequence(config, stats, call_trace)
569 }
570 }
571
572 pub fn fail_counterexample_sequence(
574 config: &SymbolicConfig,
575 stats: SymbolicStats,
576 call_trace: SymbolicCallTrace,
577 ) -> Self {
578 Self {
579 status: SymbolicResultStatus::FailCounterexample,
580 replay: SymbolicReplayMetadata::confirmed(),
581 call_trace,
582 ..Self::base(config, stats)
583 }
584 }
585
586 pub fn incomplete(
588 config: &SymbolicConfig,
589 kind: SymbolicStopReason,
590 reason: impl Into<String>,
591 stats: SymbolicStats,
592 replay: SymbolicReplayMetadata,
593 call_trace: SymbolicCallTrace,
594 counterexample: Option<SymbolicCounterexample>,
595 ) -> Self {
596 Self {
597 status: SymbolicResultStatus::Incomplete,
598 incomplete: Some(SymbolicIncomplete::new(kind, reason)),
599 replay,
600 call_trace,
601 counterexample,
602 ..Self::base(config, stats)
603 }
604 }
605
606 fn base(config: &SymbolicConfig, stats: SymbolicStats) -> Self {
608 Self {
609 schema_version: SYMBOLIC_RESULT_SCHEMA_VERSION,
610 status: SymbolicResultStatus::Pass,
611 incomplete: None,
612 bounds: SymbolicBounds::from_config(config),
613 solver: SymbolicSolverMetadata {
614 name: config.solver.clone(),
615 command: config.solver_command.clone(),
616 portfolio: config.solver_portfolio.clone(),
617 stats,
618 },
619 assumptions: SymbolicAssumption::default_assumptions(),
620 call_trace: SymbolicCallTrace::none(),
621 replay: SymbolicReplayMetadata::not_required(),
622 counterexample: None,
623 corpus_seeds: None,
624 artifact: None,
625 minimization: None,
626 }
627 }
628
629 pub fn with_corpus_seeds(mut self, corpus_seeds: SymbolicCorpusSeedMetadata) -> Self {
631 self.corpus_seeds = Some(corpus_seeds);
632 self
633 }
634
635 pub fn with_artifact(mut self, artifact: SymbolicArtifactRef) -> Self {
637 self.artifact = Some(artifact);
638 self
639 }
640
641 pub fn with_minimization(mut self, minimization: SymbolicCounterexampleMinimization) -> Self {
643 self.minimization = Some(minimization);
644 self
645 }
646}
647
648#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
650pub struct SymbolicCorpusSeedMetadata {
651 pub corpus_dir: Option<PathBuf>,
653 pub limit: usize,
655 pub loaded: usize,
657 pub skipped: usize,
659 pub used: Vec<SymbolicCorpusSeedRef>,
661}
662
663#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
665pub struct SymbolicCorpusSeedRef {
666 pub path: PathBuf,
668 pub calldata: Bytes,
670}
671
672#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
674pub struct SymbolicArtifactRef {
675 pub schema: String,
677 pub path: PathBuf,
679}
680
681impl SymbolicArtifactRef {
682 pub fn new(path: impl Into<PathBuf>) -> Self {
684 Self { schema: SYMBOLIC_COUNTEREXAMPLE_ARTIFACT_SCHEMA.to_string(), path: path.into() }
685 }
686}
687
688#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
690pub struct SymbolicRegressionRef {
691 pub artifact: PathBuf,
693 pub path: PathBuf,
695}
696
697#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
699#[serde(deny_unknown_fields)]
700pub struct SymbolicCounterexampleMinimization {
701 pub original: SymbolicArtifactRef,
703 pub minimized: SymbolicArtifactRef,
705 pub attempts: usize,
707 pub accepted: usize,
709 pub original_calldata_bytes: usize,
711 pub minimized_calldata_bytes: usize,
713 #[serde(default, skip_serializing_if = "Option::is_none")]
715 pub original_sequence_len: Option<usize>,
716 #[serde(default, skip_serializing_if = "Option::is_none")]
718 pub minimized_sequence_len: Option<usize>,
719}
720
721impl SymbolicCounterexampleMinimization {
722 pub const fn new(
724 original: SymbolicArtifactRef,
725 minimized: SymbolicArtifactRef,
726 attempts: usize,
727 accepted: usize,
728 original_calldata_bytes: usize,
729 minimized_calldata_bytes: usize,
730 ) -> Self {
731 Self {
732 original,
733 minimized,
734 attempts,
735 accepted,
736 original_calldata_bytes,
737 minimized_calldata_bytes,
738 original_sequence_len: None,
739 minimized_sequence_len: None,
740 }
741 }
742
743 pub const fn with_sequence_lengths(
745 mut self,
746 original_sequence_len: usize,
747 minimized_sequence_len: usize,
748 ) -> Self {
749 self.original_sequence_len = Some(original_sequence_len);
750 self.minimized_sequence_len = Some(minimized_sequence_len);
751 self
752 }
753}
754
755#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
757#[serde(rename_all = "snake_case")]
758pub enum SymbolicResultStatus {
759 Pass,
761 FailCounterexample,
763 Incomplete,
765}
766
767#[derive(Clone, Debug, Serialize, Deserialize)]
769pub struct SymbolicIncomplete {
770 pub kind: String,
772 pub reason: String,
774}
775
776impl SymbolicIncomplete {
777 fn new(kind: SymbolicStopReason, reason: impl Into<String>) -> Self {
778 let kind = match kind {
779 SymbolicStopReason::Stuck => "stuck",
780 SymbolicStopReason::RevertAll => "revert_all",
781 SymbolicStopReason::Timeout => "timeout",
782 SymbolicStopReason::Error => "error",
783 };
784 Self { kind: kind.to_string(), reason: reason.into() }
785 }
786}
787
788#[derive(Clone, Debug, Serialize, Deserialize)]
790pub struct SymbolicBounds {
791 pub timeout_seconds: Option<u32>,
793 pub loop_bound: Option<u32>,
795 pub max_depth: u32,
797 pub max_paths: u32,
799 pub invariant_depth: u32,
801 pub exploration_order: SymbolicExplorationOrder,
803 pub max_solver_queries: u32,
805 pub default_dynamic_length: u32,
807 pub max_dynamic_length: u32,
809 pub array_lengths: Vec<u32>,
811 pub dynamic_lengths: BTreeMap<String, Vec<u32>>,
813 pub default_array_lengths: Vec<u32>,
815 pub default_bytes_lengths: Vec<u32>,
817 pub max_calldata_bytes: u32,
819 pub symbolic_call_targets: bool,
821 pub storage_layout: SymbolicStorageLayout,
823}
824
825impl SymbolicBounds {
826 fn from_config(config: &SymbolicConfig) -> Self {
827 Self {
828 timeout_seconds: config.timeout,
829 loop_bound: config.loop_bound,
830 max_depth: config.execution_depth(),
831 max_paths: config.path_width(),
832 invariant_depth: config.invariant_depth,
833 exploration_order: config.exploration_order,
834 max_solver_queries: config.max_solver_queries,
835 default_dynamic_length: config.default_dynamic_length,
836 max_dynamic_length: config.max_dynamic_length,
837 array_lengths: config.array_lengths.clone(),
838 dynamic_lengths: config.dynamic_lengths.clone(),
839 default_array_lengths: config.default_array_lengths.clone(),
840 default_bytes_lengths: config.default_bytes_lengths.clone(),
841 max_calldata_bytes: config.max_calldata_bytes,
842 symbolic_call_targets: config.symbolic_call_targets,
843 storage_layout: config.storage_layout,
844 }
845 }
846}
847
848#[derive(Clone, Debug, Serialize, Deserialize)]
850pub struct SymbolicSolverMetadata {
851 pub name: String,
853 pub command: Option<String>,
855 pub portfolio: Vec<String>,
857 pub stats: SymbolicStats,
859}
860
861#[derive(Clone, Debug, Serialize, Deserialize)]
863pub struct SymbolicAssumption {
864 pub kind: String,
866 pub description: String,
868}
869
870impl SymbolicAssumption {
871 fn default_assumptions() -> Vec<Self> {
872 vec![
873 Self {
874 kind: "bounded_exploration".to_string(),
875 description: "Result is scoped to the configured path, depth, solver-query, loop, calldata, and dynamic-length bounds.".to_string(),
876 },
877 Self {
878 kind: "hash_model".to_string(),
879 description: "Symbolic Keccak and hash-like precompile reasoning assumes collision and preimage resistance for modeled cases.".to_string(),
880 },
881 ]
882 }
883}
884
885#[derive(Clone, Debug, Serialize, Deserialize)]
887pub struct SymbolicCallTrace {
888 pub available: bool,
890 pub source: Option<String>,
892 pub format: Option<String>,
894}
895
896impl SymbolicCallTrace {
897 pub const fn none() -> Self {
899 Self { available: false, source: None, format: None }
900 }
901
902 pub fn test_result_traces(available: bool) -> Self {
904 Self {
905 available,
906 source: available.then(|| "test_result.traces".to_string()),
907 format: available.then(|| "foundry_call_trace_arena".to_string()),
908 }
909 }
910}
911
912#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
914#[serde(rename_all = "snake_case")]
915pub enum SymbolicReplayStatus {
916 NotRequired,
918 Confirmed,
920 Mismatch,
922 Error,
924 Skipped,
926}
927
928#[derive(Clone, Debug, Serialize, Deserialize)]
930#[serde(deny_unknown_fields)]
931pub struct SymbolicReplayMetadata {
932 pub required: bool,
934 pub status: SymbolicReplayStatus,
936 pub reason: Option<String>,
938}
939
940impl SymbolicReplayMetadata {
941 pub const fn not_required() -> Self {
943 Self { required: false, status: SymbolicReplayStatus::NotRequired, reason: None }
944 }
945
946 pub const fn confirmed() -> Self {
948 Self { required: true, status: SymbolicReplayStatus::Confirmed, reason: None }
949 }
950
951 pub fn mismatch(reason: impl Into<String>) -> Self {
953 Self { required: true, status: SymbolicReplayStatus::Mismatch, reason: Some(reason.into()) }
954 }
955
956 pub fn error(reason: impl Into<String>) -> Self {
958 Self { required: true, status: SymbolicReplayStatus::Error, reason: Some(reason.into()) }
959 }
960
961 pub fn skipped(reason: impl Into<String>) -> Self {
963 Self { required: true, status: SymbolicReplayStatus::Skipped, reason: Some(reason.into()) }
964 }
965}
966
967#[derive(Clone, Debug, Serialize, Deserialize)]
969pub struct SymbolicCounterexample {
970 pub calldata: Bytes,
972 pub args: Option<String>,
974 pub raw_args: Option<String>,
976 pub value: Option<U256>,
978}
979
980impl From<&BaseCounterExample> for SymbolicCounterexample {
981 fn from(counterexample: &BaseCounterExample) -> Self {
982 Self {
983 calldata: counterexample.calldata.clone(),
984 args: counterexample.args.clone(),
985 raw_args: counterexample.raw_args.clone(),
986 value: counterexample.value,
987 }
988 }
989}
990
991#[derive(Clone, Debug, Serialize, Deserialize)]
993#[serde(deny_unknown_fields)]
994pub struct SymbolicCounterexampleArtifact {
995 pub schema_version: u32,
997 pub schema: String,
999 pub kind: SymbolicCounterexampleArtifactKind,
1001 pub test: SymbolicCounterexampleTestIdentity,
1003 pub replay: SymbolicReplayMetadata,
1005 pub replay_semantics: SymbolicCounterexampleReplaySemantics,
1007 pub bounds: SymbolicBounds,
1009 pub solver: SymbolicSolverMetadata,
1011 pub assumptions: Vec<SymbolicAssumption>,
1013 pub call_trace: SymbolicCallTrace,
1015 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1017 pub storage: Vec<SymbolicStorageAssignment>,
1018 #[serde(default, skip_serializing_if = "Option::is_none")]
1020 pub invariant_failure: Option<SymbolicInvariantArtifactFailure>,
1021 pub calls: Vec<SymbolicCounterexampleCall>,
1023}
1024
1025impl SymbolicCounterexampleArtifact {
1026 pub fn new(
1028 kind: SymbolicCounterexampleArtifactKind,
1029 test: SymbolicCounterexampleTestIdentity,
1030 symbolic: &SymbolicResult,
1031 replay_semantics: SymbolicCounterexampleReplaySemantics,
1032 calls: Vec<SymbolicCounterexampleCall>,
1033 ) -> Self {
1034 Self {
1035 schema_version: SYMBOLIC_COUNTEREXAMPLE_ARTIFACT_SCHEMA_VERSION,
1036 schema: SYMBOLIC_COUNTEREXAMPLE_ARTIFACT_SCHEMA.to_string(),
1037 kind,
1038 test,
1039 replay: symbolic.replay.clone(),
1040 replay_semantics,
1041 bounds: symbolic.bounds.clone(),
1042 solver: symbolic.solver.clone(),
1043 assumptions: symbolic.assumptions.clone(),
1044 call_trace: symbolic.call_trace.clone(),
1045 storage: Vec::new(),
1046 invariant_failure: None,
1047 calls,
1048 }
1049 }
1050
1051 pub fn with_storage(mut self, storage: Vec<SymbolicStorageAssignment>) -> Self {
1053 self.storage = storage;
1054 self
1055 }
1056
1057 pub fn with_invariant_failure(
1059 mut self,
1060 invariant_failure: SymbolicInvariantArtifactFailure,
1061 ) -> Self {
1062 self.invariant_failure = Some(invariant_failure);
1063 self
1064 }
1065}
1066
1067#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
1069#[serde(deny_unknown_fields)]
1070pub struct SymbolicCounterexampleReplaySemantics {
1071 pub fail_on_revert: bool,
1073}
1074
1075#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1077#[serde(rename_all = "snake_case")]
1078pub enum SymbolicCounterexampleArtifactKind {
1079 SingleCall,
1081 Sequence,
1083}
1084
1085#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1087#[serde(tag = "kind", rename_all = "snake_case")]
1088pub enum SymbolicInvariantArtifactFailure {
1089 Predicate {
1091 name: String,
1093 #[serde(default, skip_serializing_if = "Option::is_none")]
1095 site: Option<SymbolicInvariantFailureSite>,
1096 },
1097 Handler {
1099 #[serde(default, skip_serializing_if = "Option::is_none")]
1101 name: Option<String>,
1102 reverter: Address,
1104 selector: Selector,
1106 fingerprint: B256,
1108 },
1109}
1110
1111#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
1113#[serde(tag = "kind", rename_all = "snake_case")]
1114pub enum SymbolicInvariantFailureSite {
1115 SequenceCall { target: Address, selector: Selector, fingerprint: B256 },
1117 Invariant { target: Address, selector: Selector, fingerprint: B256 },
1119 AfterInvariant { target: Address, selector: Selector, fingerprint: B256 },
1121}
1122
1123impl From<CheckSequenceFailureSite> for SymbolicInvariantFailureSite {
1124 fn from(site: CheckSequenceFailureSite) -> Self {
1125 match site {
1126 CheckSequenceFailureSite::SequenceCall { target, selector, fingerprint } => {
1127 Self::SequenceCall { target, selector, fingerprint }
1128 }
1129 CheckSequenceFailureSite::Invariant { target, selector, fingerprint } => {
1130 Self::Invariant { target, selector, fingerprint }
1131 }
1132 CheckSequenceFailureSite::AfterInvariant { target, selector, fingerprint } => {
1133 Self::AfterInvariant { target, selector, fingerprint }
1134 }
1135 }
1136 }
1137}
1138
1139#[derive(Clone, Debug, Serialize, Deserialize)]
1141#[serde(deny_unknown_fields)]
1142pub struct SymbolicCounterexampleTestIdentity {
1143 pub contract: String,
1145 pub test: String,
1147}
1148
1149#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1151#[serde(deny_unknown_fields)]
1152pub struct SymbolicCounterexampleCall {
1153 pub warp: Option<U256>,
1155 pub roll: Option<U256>,
1157 pub sender: Address,
1159 pub target: Address,
1161 pub calldata: Bytes,
1163 pub value: Option<U256>,
1165 pub contract_name: Option<String>,
1167 pub function_name: Option<String>,
1169 pub signature: Option<String>,
1171 pub args: Option<String>,
1173 pub raw_args: Option<String>,
1175}
1176
1177impl SymbolicCounterexampleCall {
1178 pub fn from_base_counterexample(
1180 counterexample: &BaseCounterExample,
1181 default_sender: Address,
1182 default_target: Address,
1183 ) -> Self {
1184 Self {
1185 warp: counterexample.warp,
1186 roll: counterexample.roll,
1187 sender: counterexample.sender.unwrap_or(default_sender),
1188 target: counterexample.addr.unwrap_or(default_target),
1189 calldata: counterexample.calldata.clone(),
1190 value: counterexample.value,
1191 contract_name: counterexample.contract_name.clone(),
1192 function_name: counterexample.func_name.clone(),
1193 signature: counterexample.signature.clone(),
1194 args: counterexample.args.clone(),
1195 raw_args: counterexample.raw_args.clone(),
1196 }
1197 }
1198
1199 pub fn to_base_counterexample(&self) -> BaseCounterExample {
1201 BaseCounterExample {
1202 warp: self.warp,
1203 roll: self.roll,
1204 sender: Some(self.sender),
1205 addr: Some(self.target),
1206 calldata: self.calldata.clone(),
1207 value: self.value,
1208 contract_name: self.contract_name.clone(),
1209 func_name: self.function_name.clone(),
1210 signature: self.signature.clone(),
1211 args: self.args.clone(),
1212 raw_args: self.raw_args.clone(),
1213 traces: None,
1214 show_solidity: false,
1215 fuzz: Default::default(),
1216 }
1217 }
1218
1219 pub fn to_basic_tx_details(&self) -> BasicTxDetails {
1221 BasicTxDetails {
1222 warp: self.warp,
1223 roll: self.roll,
1224 sender: self.sender,
1225 call_details: CallDetails {
1226 target: self.target,
1227 calldata: self.calldata.clone(),
1228 value: self.value,
1229 },
1230 }
1231 }
1232}
1233
1234#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1236pub struct TestResult {
1237 pub status: TestStatus,
1241
1242 pub reason: Option<String>,
1244
1245 #[serde(default, skip_serializing_if = "Option::is_none")]
1247 pub fork_block_number: Option<u64>,
1248
1249 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1254 pub invariant_failures: Vec<InvariantFailure>,
1255
1256 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1260 pub invariant_predicate_results: Vec<InvariantPredicateResult>,
1261
1262 #[serde(default, skip_serializing_if = "Option::is_none")]
1265 pub invariant_failure_dir: Option<PathBuf>,
1266
1267 #[serde(default, skip_serializing_if = "Option::is_none")]
1272 pub invariant_count: Option<usize>,
1273
1274 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1278 pub invariant_handler_failures: Vec<InvariantFailure>,
1279
1280 pub counterexample: Option<CounterExample>,
1282
1283 #[serde(default, skip_serializing_if = "Option::is_none")]
1288 pub counterexample_artifact: Option<SymbolicArtifactRef>,
1289
1290 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1292 pub counterexample_artifacts: Vec<SymbolicArtifactRef>,
1293
1294 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1296 pub symbolic_regressions: Vec<SymbolicRegressionRef>,
1297
1298 pub logs: Vec<Log>,
1301
1302 pub decoded_logs: Vec<String>,
1305
1306 pub kind: TestKind,
1308
1309 #[serde(default, skip_serializing_if = "Option::is_none")]
1311 pub symbolic: Option<SymbolicResult>,
1312
1313 pub traces: Traces,
1315
1316 #[serde(skip)]
1318 pub debug_bytecodes: AddressHashMap<Bytes>,
1319
1320 #[serde(skip)]
1324 pub gas_report_traces: Vec<Vec<CallTraceArena>>,
1325
1326 #[serde(skip)]
1328 pub line_coverage: Option<HitMaps>,
1329
1330 #[serde(rename = "labeled_addresses")] pub labels: AddressHashMap<String>,
1333
1334 #[serde(with = "foundry_common::serde_helpers::duration")]
1335 pub duration: Duration,
1336
1337 pub breakpoints: Breakpoints,
1339
1340 pub gas_snapshots: BTreeMap<String, BTreeMap<String, String>>,
1342
1343 #[serde(skip)]
1345 pub deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>,
1346}
1347
1348impl fmt::Display for TestResult {
1349 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1350 f.write_str(&self.render(false, None))
1351 }
1352}
1353
1354fn write_sequence(s: &mut String, label: &str, original: usize, sequence: &[BaseCounterExample]) {
1356 writeln!(s, "\n\t[{label}] (original: {original}, shrunk: {})", sequence.len()).unwrap();
1357 for ex in sequence {
1358 writeln!(s, "{ex}").unwrap();
1359 }
1360}
1361
1362fn write_failure(s: &mut String, failure: &InvariantFailure, name_suffix: &str) -> bool {
1366 write!(s, "[FAIL: {}]{name_suffix}", failure.reason()).unwrap();
1367 if let Some(CounterExample::Sequence(original, sequence)) = failure.counterexample() {
1368 write_sequence(s, "Sequence", *original, sequence);
1369 return true;
1370 }
1371 false
1372}
1373
1374fn replay_artifacts<'a>(
1377 artifact: Option<&'a SymbolicArtifactRef>,
1378 minimization: Option<&'a SymbolicCounterexampleMinimization>,
1379) -> impl Iterator<Item = &'a SymbolicArtifactRef> {
1380 artifact.into_iter().chain(minimization.into_iter().flat_map(|m| [&m.original, &m.minimized]))
1381}
1382
1383impl TestResult {
1384 const fn is_debuggable_failure(&self) -> bool {
1387 self.status.is_failure()
1388 && !self.kind.is_invariant()
1389 && !self.kind.is_symbolic()
1390 && self.symbolic.is_none()
1391 }
1392
1393 pub fn add_counterexample_artifact(&mut self, artifact: SymbolicArtifactRef) {
1395 if !self.counterexample_artifacts.contains(&artifact) {
1396 self.counterexample_artifacts.push(artifact.clone());
1397 }
1398 if self.counterexample_artifact.is_none() {
1399 self.counterexample_artifact = Some(artifact);
1400 }
1401 }
1402
1403 fn render(&self, user_facing: bool, campaign_name: Option<&str>) -> String {
1405 let header = if user_facing {
1406 campaign_name.unwrap_or(INVARIANT_CAMPAIGN_FALLBACK_NAME)
1407 } else {
1408 "Predicates"
1409 };
1410 let mut s = String::new();
1411 match self.status {
1412 TestStatus::Success => {
1413 s.push_str("[PASS]");
1414 if let Some(CounterExample::Sequence(original, sequence)) = &self.counterexample {
1416 write_sequence(&mut s, "Best sequence", *original, sequence);
1417 }
1418 self.write_predicates(&mut s, header, true);
1419 s.green().wrap().to_string()
1420 }
1421 TestStatus::Skipped => {
1422 s.push_str("[SKIP");
1423 if let Some(reason) = &self.reason {
1424 write!(s, ": {reason}").unwrap();
1425 }
1426 s.push(']');
1427 self.write_predicates(&mut s, header, true);
1428 s.yellow().to_string()
1429 }
1430 TestStatus::Failure => {
1431 let is_invariant_failure = !self.invariant_failures.is_empty()
1432 || !self.invariant_handler_failures.is_empty();
1433 if is_invariant_failure {
1434 let named = self.invariant_count.is_some() || self.invariant_failures.len() > 1;
1438 for (i, failure) in self.invariant_failures.iter().enumerate() {
1439 if i > 0 {
1440 s.push('\n');
1441 }
1442 let is_anchor =
1443 matches!(failure, InvariantFailure::Predicate { is_anchor: true, .. });
1444 let suffix = if named || !is_anchor {
1445 format!(" {}", failure.name())
1446 } else {
1447 String::new()
1448 };
1449 write_failure(&mut s, failure, &suffix);
1450 }
1451 } else {
1452 s.push_str("[FAIL");
1455 if let Some(reason) = &self.reason {
1456 write!(s, ": {reason}").unwrap();
1457 }
1458 match &self.counterexample {
1459 Some(CounterExample::Single(ex)) => {
1460 write!(s, "; counterexample: {ex}]").unwrap();
1461 }
1462 Some(CounterExample::Sequence(original, sequence)) => {
1463 s.push(']');
1464 write_sequence(&mut s, "Sequence", *original, sequence);
1465 }
1466 None => s.push(']'),
1467 }
1468 }
1469
1470 let broken = self.invariant_failures.len();
1471 let rollup = match self.invariant_count {
1472 Some(total) if total > 1 && is_invariant_failure => {
1473 writeln!(s, "\n{header}: {broken}/{total} invariants broken").unwrap();
1474 true
1475 }
1476 _ => false,
1477 };
1478 self.write_predicates(&mut s, header, !user_facing || !rollup);
1479 if broken > 1
1480 && let Some(dir) = &self.invariant_failure_dir
1481 {
1482 writeln!(
1483 s,
1484 "{broken} invariant failure(s) persisted to {} — rerun to shrink",
1485 dir.display()
1486 )
1487 .unwrap();
1488 }
1489
1490 if !self.invariant_handler_failures.is_empty() {
1491 let preceded = rollup
1493 || broken > 0
1494 || (user_facing && self.invariant_predicate_results.len() > 1);
1495 writeln!(
1496 s,
1497 "{}{}: {} assertion bug(s) found",
1498 if preceded { "\n" } else { "" },
1499 if user_facing { "Assertion Tests" } else { "Handler assertions" },
1500 self.invariant_handler_failures.len()
1501 )
1502 .unwrap();
1503 for failure in &self.invariant_handler_failures {
1504 if !write_failure(&mut s, failure, &format!(" {}", failure.name())) {
1505 s.push('\n');
1506 }
1507 }
1508 }
1509
1510 s.red().wrap().to_string()
1511 }
1512 }
1513 }
1514
1515 fn write_predicates(&self, s: &mut String, header: &str, show_header: bool) {
1517 if self.invariant_predicate_results.len() <= 1 {
1518 return;
1519 }
1520 if show_header {
1521 write!(s, "\n{header}:\n").unwrap();
1522 }
1523 for predicate in &self.invariant_predicate_results {
1524 let name = &predicate.name;
1525 match (predicate.status, &predicate.reason) {
1526 (TestStatus::Success, _) => writeln!(s, "[PASS] {name}"),
1527 (TestStatus::Failure, reason) => {
1528 writeln!(s, "[FAIL: {}] {name}", reason.as_deref().unwrap_or_default())
1529 }
1530 (TestStatus::Skipped, Some(reason)) => writeln!(s, "[SKIP: {reason}] {name}"),
1531 (TestStatus::Skipped, None) => writeln!(s, "[SKIP] {name}"),
1532 }
1533 .unwrap();
1534 }
1535 }
1536}
1537
1538macro_rules! extend {
1539 ($a:expr, $b:expr, $trace_kind:expr) => {
1540 if $b.fork_block_number.is_some() {
1541 $a.fork_block_number = $b.fork_block_number;
1542 }
1543 $a.logs.extend($b.logs);
1544 $a.labels.extend($b.labels);
1545 $a.traces.extend($b.traces.map(|traces| ($trace_kind, traces)));
1546 $a.debug_bytecodes.extend($b.debug_bytecodes);
1547 $a.merge_coverages($b.line_coverage);
1548 };
1549}
1550
1551#[derive(Default)]
1553pub struct InvariantOutcome {
1554 pub success: bool,
1556 pub fork_block_number: Option<u64>,
1558 pub failures: Vec<InvariantFailure>,
1560 pub handler_failures: Vec<InvariantFailure>,
1562 pub predicate_results: Vec<InvariantPredicateResult>,
1564 pub failure_dir: Option<PathBuf>,
1566 pub invariant_count: Option<usize>,
1568 pub counterexample: Option<CounterExample>,
1570 pub gas_report_traces: Vec<Vec<CallTraceArena>>,
1572}
1573
1574pub(crate) fn invariant_kind(runs: usize, calls: usize, reverts: usize) -> TestKind {
1576 TestKind::Invariant {
1577 runs,
1578 calls,
1579 reverts,
1580 workers: 1,
1581 metrics: Default::default(),
1582 failed_corpus_replays: 0,
1583 optimization_best_value: None,
1584 }
1585}
1586
1587impl TestResult {
1588 pub fn new(setup: &TestSetup) -> Self {
1590 Self {
1591 labels: setup.labels.clone(),
1592 logs: setup.logs.clone(),
1593 traces: setup.traces.clone(),
1594 debug_bytecodes: setup.debug_bytecodes.clone(),
1595 line_coverage: setup.coverage.clone(),
1596 fork_block_number: setup.fork_block_number,
1597 ..Default::default()
1598 }
1599 }
1600
1601 pub fn fail(reason: String) -> Self {
1603 Self { status: TestStatus::Failure, reason: Some(reason), ..Default::default() }
1604 }
1605
1606 pub fn setup_result(setup: TestSetup) -> Self {
1608 Self {
1609 status: if setup.skipped { TestStatus::Skipped } else { TestStatus::Failure },
1610 reason: setup.reason,
1611 logs: setup.logs,
1612 traces: setup.traces,
1613 debug_bytecodes: setup.debug_bytecodes,
1614 line_coverage: setup.coverage,
1615 labels: setup.labels,
1616 fork_block_number: setup.fork_block_number,
1617 ..Default::default()
1618 }
1619 }
1620
1621 pub fn single_skip(&mut self, reason: SkipReason) {
1623 self.status = TestStatus::Skipped;
1624 self.reason = reason.0;
1625 }
1626
1627 pub fn single_fail(&mut self, reason: Option<String>) {
1629 self.status = TestStatus::Failure;
1630 self.reason = reason;
1631 }
1632
1633 pub fn single_result<FEN: FoundryEvmNetwork>(
1636 &mut self,
1637 success: bool,
1638 reason: Option<String>,
1639 raw_call_result: RawCallResult<FEN>,
1640 ) {
1641 self.kind = TestKind::Unit {
1642 gas: raw_call_result.gas_used.saturating_sub(raw_call_result.stipend),
1643 };
1644
1645 extend!(self, raw_call_result, TraceKind::Execution);
1646
1647 self.status = if success { TestStatus::Success } else { TestStatus::Failure };
1648 self.reason = reason;
1649 self.duration = Duration::default();
1650 self.gas_report_traces = Vec::new();
1651
1652 if let Some(cheatcodes) = raw_call_result.cheatcodes {
1653 self.breakpoints = cheatcodes.breakpoints;
1654 self.gas_snapshots = cheatcodes.gas_snapshots;
1655 self.deprecated_cheatcodes = cheatcodes.deprecated;
1656 }
1657 }
1658
1659 pub fn fuzz_result(&mut self, mut result: FuzzTestResult) {
1662 let kind = TestKind::Fuzz {
1663 median_gas: result.median_gas(false),
1664 mean_gas: result.mean_gas(false),
1665 first_case: std::mem::take(&mut result.first_case),
1666 runs: result.gas_by_case.len(),
1667 failed_corpus_replays: result.failed_corpus_replays,
1668 };
1669 self.campaign_result(kind, result);
1670 }
1671
1672 pub fn table_result(&mut self, result: FuzzTestResult) {
1675 let kind = TestKind::Table {
1676 median_gas: result.median_gas(false),
1677 mean_gas: result.mean_gas(false),
1678 runs: result.gas_by_case.len(),
1679 };
1680 self.campaign_result(kind, result);
1681 }
1682
1683 fn campaign_result(&mut self, kind: TestKind, result: FuzzTestResult) {
1684 self.kind = kind;
1685
1686 extend!(self, result, TraceKind::Execution);
1687
1688 self.status = if result.skipped {
1689 TestStatus::Skipped
1690 } else if result.success {
1691 TestStatus::Success
1692 } else {
1693 TestStatus::Failure
1694 };
1695 self.reason = result.reason;
1696 self.counterexample = result.counterexample;
1697 self.duration = Duration::default();
1698 self.gas_report_traces = result.gas_report_traces.into_iter().map(|t| vec![t]).collect();
1699 self.breakpoints = result.breakpoints.unwrap_or_default();
1700 self.deprecated_cheatcodes = result.deprecated_cheatcodes;
1701 }
1702
1703 pub fn fuzz_setup_fail(&mut self, e: Report) {
1705 self.kind = TestKind::Fuzz {
1706 first_case: Default::default(),
1707 runs: 0,
1708 mean_gas: 0,
1709 median_gas: 0,
1710 failed_corpus_replays: 0,
1711 };
1712 self.status = TestStatus::Failure;
1713 debug!(?e, "failed to set up fuzz testing environment");
1714 self.reason = Some(format!("failed to set up fuzz testing environment: {e}"));
1715 }
1716
1717 pub fn invariant_skip_with_predicates(
1719 &mut self,
1720 reason: SkipReason,
1721 invariant_predicate_results: Vec<InvariantPredicateResult>,
1722 ) {
1723 self.kind = invariant_kind(1, 1, 1);
1724 self.status = TestStatus::Skipped;
1725 let predicate_count = invariant_predicate_results.len();
1726 let is_campaign = predicate_count > 1;
1727 self.reason = if is_campaign { None } else { reason.0 };
1728 self.invariant_count = is_campaign.then_some(predicate_count);
1729 self.invariant_predicate_results = invariant_predicate_results;
1730 }
1731
1732 pub fn invariant_replay_fail(
1734 &mut self,
1735 outcome: CheckSequenceOutcome,
1736 invariant_name: &str,
1737 fallback_reason: Option<String>,
1738 call_sequence: Vec<BaseCounterExample>,
1739 ) {
1740 self.kind = invariant_kind(1, outcome.calls_count, outcome.reverts);
1741 self.status = TestStatus::Failure;
1742 self.reason = Some(outcome.reason.or(fallback_reason).unwrap_or_else(|| {
1743 let what = if outcome.replayed_entirely {
1744 "replay failure"
1745 } else {
1746 "persisted failure revert"
1747 };
1748 format!("{invariant_name} {what}")
1749 }));
1750 self.counterexample = Some(CounterExample::Sequence(call_sequence.len(), call_sequence));
1751 }
1752
1753 pub fn invariant_replay_success(&mut self, call_count: usize, reverts: usize) {
1755 self.kind = invariant_kind(1, call_count, reverts);
1756 self.status = TestStatus::Success;
1757 self.reason = None;
1758 }
1759
1760 pub fn invariant_setup_fail(&mut self, e: Report) {
1762 self.kind = invariant_kind(0, 0, 0);
1763 self.status = TestStatus::Failure;
1764 self.reason = Some(format!("failed to set up invariant testing environment: {e}"));
1765 }
1766
1767 pub fn invariant_result(&mut self, kind: TestKind, outcome: InvariantOutcome) {
1769 let optimizing =
1771 matches!(kind, TestKind::Invariant { optimization_best_value: Some(_), .. });
1772 self.kind = kind;
1773 self.status =
1774 if optimizing || outcome.success { TestStatus::Success } else { TestStatus::Failure };
1775 self.fork_block_number = outcome.fork_block_number;
1776 self.invariant_predicate_results = outcome.predicate_results;
1777 self.invariant_failure_dir = outcome.failure_dir;
1778 self.invariant_count = outcome.invariant_count;
1779 self.counterexample = outcome.counterexample;
1783 for artifact in outcome
1784 .failures
1785 .iter()
1786 .chain(&outcome.handler_failures)
1787 .flat_map(|failure| replay_artifacts(failure.artifact(), failure.minimization()))
1788 {
1789 self.add_counterexample_artifact(artifact.clone());
1790 }
1791 self.invariant_failures = outcome.failures;
1792 self.invariant_handler_failures = outcome.handler_failures;
1793 self.gas_report_traces = outcome.gas_report_traces;
1794 }
1795
1796 pub fn symbolic_result(
1798 &mut self,
1799 status: TestStatus,
1800 reason: Option<String>,
1801 counterexample: Option<CounterExample>,
1802 symbolic: SymbolicResult,
1803 ) {
1804 self.kind = TestKind::Symbolic(symbolic.solver.stats);
1805 self.status = status;
1806 self.reason = reason;
1807 self.counterexample = counterexample;
1808 self.record_symbolic(symbolic);
1809 self.duration = Duration::default();
1810 }
1811
1812 pub(crate) fn record_symbolic(&mut self, symbolic: SymbolicResult) {
1814 for artifact in replay_artifacts(symbolic.artifact.as_ref(), symbolic.minimization.as_ref())
1815 {
1816 self.add_counterexample_artifact(artifact.clone());
1817 }
1818 self.symbolic = Some(symbolic);
1819 }
1820
1821 pub fn replay_result(
1823 &mut self,
1824 corpus_entries: usize,
1825 showmap_files: usize,
1826 skipped_entries: usize,
1827 duration: Duration,
1828 ) {
1829 self.kind = TestKind::Replay { corpus_entries, showmap_files, skipped_entries };
1830 self.status = TestStatus::Success;
1831 self.duration = duration;
1832 }
1833
1834 pub fn replay_skip(&mut self, reason: impl Into<String>) {
1836 self.kind = TestKind::Replay { corpus_entries: 0, showmap_files: 0, skipped_entries: 0 };
1837 self.status = TestStatus::Skipped;
1838 self.reason = Some(reason.into());
1839 self.duration = Duration::default();
1840 }
1841
1842 pub fn interrupt(&mut self) {
1845 let reason = Some("interrupted".to_string());
1846 self.status = TestStatus::Skipped;
1847 for predicate in &mut self.invariant_predicate_results {
1848 if predicate.status.is_success() {
1849 predicate.status = TestStatus::Skipped;
1850 predicate.reason.clone_from(&reason);
1851 }
1852 }
1853 if self.invariant_count.is_none() {
1855 self.reason = reason;
1856 }
1857 }
1858
1859 pub(crate) fn short_result_with_suite(&self, name: &str, suite_name: &str) -> String {
1862 let campaign = (self.kind.is_invariant() && self.invariant_count.is_some())
1863 .then(|| invariant_campaign_display_name(get_contract_name(suite_name)));
1864 let name = campaign.as_deref().unwrap_or(name);
1865 let status = self.render(true, campaign.as_deref());
1866 let block = match self.fork_block_number {
1867 Some(block) if self.status.is_failure() => format!(" (block: {block})"),
1868 _ => String::new(),
1869 };
1870 format!("{status} {name}{block} {}", self.kind.report())
1871 }
1872
1873 fn logical_count(&self) -> usize {
1876 let skipped = self.skipped_predicate_count();
1877 if skipped == 0 {
1878 1
1879 } else if self.status.is_skipped() && skipped == self.invariant_predicate_results.len() {
1880 skipped
1881 } else {
1882 1 + skipped
1883 }
1884 }
1885
1886 fn skipped_count(&self) -> usize {
1887 let skipped = self.skipped_predicate_count();
1888 if skipped == 0 && self.status.is_skipped() { 1 } else { skipped }
1889 }
1890
1891 fn skipped_predicate_count(&self) -> usize {
1892 self.invariant_predicate_results.iter().filter(|p| p.status.is_skipped()).count()
1893 }
1894
1895 pub fn extend<FEN: FoundryEvmNetwork>(&mut self, call_result: RawCallResult<FEN>) {
1897 extend!(self, call_result, TraceKind::Execution);
1898 }
1899
1900 pub(crate) fn extend_setup<FEN: FoundryEvmNetwork>(&mut self, call_result: RawCallResult<FEN>) {
1902 extend!(self, call_result, TraceKind::Setup);
1903 }
1904
1905 pub fn merge_coverages(&mut self, other_coverage: Option<HitMaps>) {
1907 HitMaps::merge_opt(&mut self.line_coverage, other_coverage);
1908 }
1909}
1910
1911#[derive(Clone, Debug, PartialEq, Eq)]
1913pub enum TestKindReport {
1914 Unit {
1915 gas: u64,
1916 },
1917 Fuzz {
1918 runs: usize,
1919 mean_gas: u64,
1920 median_gas: u64,
1921 failed_corpus_replays: usize,
1922 },
1923 Invariant {
1924 runs: usize,
1925 calls: usize,
1926 reverts: usize,
1927 failed_corpus_replays: usize,
1928 optimization_best_value: Option<I256>,
1930 },
1931 Table {
1932 runs: usize,
1933 mean_gas: u64,
1934 median_gas: u64,
1935 },
1936 Symbolic(SymbolicStats),
1937 Replay {
1939 corpus_entries: usize,
1940 showmap_files: usize,
1941 skipped_entries: usize,
1942 },
1943}
1944
1945impl fmt::Display for TestKindReport {
1946 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1947 match self {
1948 Self::Unit { gas } => write!(f, "(gas: {gas})"),
1949 Self::Fuzz { runs, mean_gas, median_gas, failed_corpus_replays } => {
1950 write!(f, "(runs: {runs}, μ: {mean_gas}, ~: {median_gas}")?;
1951 if *failed_corpus_replays != 0 {
1952 write!(f, ", failed corpus replays: {failed_corpus_replays}")?;
1953 }
1954 f.write_str(")")
1955 }
1956 Self::Invariant {
1957 runs,
1958 calls,
1959 reverts,
1960 failed_corpus_replays,
1961 optimization_best_value,
1962 } => {
1963 if let Some(best_value) = optimization_best_value {
1964 return write!(f, "(best: {best_value}, runs: {runs}, calls: {calls})");
1965 }
1966 write!(f, "(runs: {runs}, calls: {calls}, reverts: {reverts}")?;
1967 if *failed_corpus_replays != 0 {
1968 write!(f, ", failed corpus replays: {failed_corpus_replays}")?;
1969 }
1970 f.write_str(")")
1971 }
1972 Self::Table { runs, mean_gas, median_gas } => {
1973 write!(f, "(runs: {runs}, μ: {mean_gas}, ~: {median_gas})")
1974 }
1975 Self::Symbolic(SymbolicStats {
1976 paths,
1977 solver_queries,
1978 smt_queries,
1979 sat_queries,
1980 model_queries,
1981 sat_cache_hits,
1982 model_cache_hits,
1983 heuristic_witnesses,
1984 solver_time_ms,
1985 ..
1986 }) => {
1987 write!(
1988 f,
1989 "(paths: {paths}, queries: {solver_queries}, smt: {smt_queries}, sat: {sat_queries} ({sat_cache_hits} cached), models: {model_queries} ({model_cache_hits} cached), hard-arith: {heuristic_witnesses}, solver: {solver_time_ms}ms)"
1990 )
1991 }
1992 Self::Replay { corpus_entries, showmap_files, skipped_entries } => {
1993 write!(f, "(replay: {corpus_entries} entries, {showmap_files} files")?;
1994 if *skipped_entries != 0 {
1995 write!(f, ", {skipped_entries} skipped")?;
1996 }
1997 f.write_str(")")
1998 }
1999 }
2000 }
2001}
2002
2003impl TestKindReport {
2004 pub const fn gas(&self) -> u64 {
2006 match *self {
2007 Self::Unit { gas } => gas,
2008 Self::Fuzz { median_gas, .. } | Self::Table { median_gas, .. } => median_gas,
2010 Self::Invariant { .. } | Self::Symbolic { .. } | Self::Replay { .. } => 0,
2012 }
2013 }
2014}
2015
2016#[derive(Clone, Debug, Serialize, Deserialize)]
2018pub enum TestKind {
2019 Unit { gas: u64 },
2021 Fuzz {
2023 first_case: FuzzCase,
2025 runs: usize,
2026 mean_gas: u64,
2027 median_gas: u64,
2028 failed_corpus_replays: usize,
2029 },
2030 Invariant {
2032 runs: usize,
2033 calls: usize,
2034 reverts: usize,
2035 #[serde(default = "default_invariant_workers")]
2037 workers: usize,
2038 metrics: Map<String, InvariantMetrics>,
2039 failed_corpus_replays: usize,
2040 optimization_best_value: Option<I256>,
2042 },
2043 Table { runs: usize, mean_gas: u64, median_gas: u64 },
2045 Symbolic(SymbolicStats),
2047 Replay { corpus_entries: usize, showmap_files: usize, skipped_entries: usize },
2049}
2050
2051impl Default for TestKind {
2052 fn default() -> Self {
2053 Self::Unit { gas: 0 }
2054 }
2055}
2056
2057impl TestKind {
2058 pub const fn is_fuzz(&self) -> bool {
2060 matches!(self, Self::Fuzz { .. })
2061 }
2062
2063 pub const fn is_invariant(&self) -> bool {
2065 matches!(self, Self::Invariant { .. })
2066 }
2067
2068 pub const fn is_symbolic(&self) -> bool {
2070 matches!(self, Self::Symbolic { .. })
2071 }
2072
2073 pub const fn invariant_workers(&self) -> Option<usize> {
2075 match self {
2076 Self::Invariant { workers, .. } => Some(*workers),
2077 _ => None,
2078 }
2079 }
2080
2081 pub const fn report(&self) -> TestKindReport {
2083 match *self {
2084 Self::Unit { gas } => TestKindReport::Unit { gas },
2085 Self::Fuzz { runs, mean_gas, median_gas, failed_corpus_replays, .. } => {
2086 TestKindReport::Fuzz { runs, mean_gas, median_gas, failed_corpus_replays }
2087 }
2088 Self::Invariant {
2089 runs,
2090 calls,
2091 reverts,
2092 failed_corpus_replays,
2093 optimization_best_value,
2094 ..
2095 } => TestKindReport::Invariant {
2096 runs,
2097 calls,
2098 reverts,
2099 failed_corpus_replays,
2100 optimization_best_value,
2101 },
2102 Self::Table { runs, mean_gas, median_gas } => {
2103 TestKindReport::Table { runs, mean_gas, median_gas }
2104 }
2105 Self::Symbolic(stats) => TestKindReport::Symbolic(stats),
2106 Self::Replay { corpus_entries, showmap_files, skipped_entries } => {
2107 TestKindReport::Replay { corpus_entries, showmap_files, skipped_entries }
2108 }
2109 }
2110 }
2111}
2112
2113const fn default_invariant_workers() -> usize {
2114 1
2115}
2116
2117#[derive(Clone, Debug, Default)]
2122pub struct TestSetup {
2123 pub address: Address,
2125 pub fuzz_fixtures: FuzzFixtures,
2127
2128 pub logs: Vec<Log>,
2130 pub labels: AddressHashMap<String>,
2132 pub traces: Traces,
2134 pub debug_bytecodes: AddressHashMap<Bytes>,
2136 pub coverage: Option<HitMaps>,
2138 pub deployed_libs: Vec<Address>,
2140 pub fork_block_number: Option<u64>,
2142 pub(crate) fuzz_state: OnceLock<EvmFuzzState>,
2144
2145 pub reason: Option<String>,
2147 pub skipped: bool,
2149 pub deployment_failure: bool,
2151}
2152
2153impl TestSetup {
2154 pub fn failed(reason: String) -> Self {
2155 Self { reason: Some(reason), ..Default::default() }
2156 }
2157
2158 pub fn skipped(reason: String) -> Self {
2159 Self { reason: Some(reason), skipped: true, ..Default::default() }
2160 }
2161
2162 pub fn extend<FEN: FoundryEvmNetwork>(
2163 &mut self,
2164 raw: RawCallResult<FEN>,
2165 trace_kind: TraceKind,
2166 ) {
2167 extend!(self, raw, trace_kind);
2168 }
2169
2170 pub fn merge_coverages(&mut self, other_coverage: Option<HitMaps>) {
2171 HitMaps::merge_opt(&mut self.coverage, other_coverage);
2172 }
2173}
2174
2175pub(crate) fn invariant_campaign_display_name(contract_name: &str) -> String {
2176 format!("{contract_name} invariants")
2177}
2178
2179const fn symbolic_result_schema_version() -> u32 {
2180 SYMBOLIC_RESULT_SCHEMA_VERSION
2181}
2182
2183#[cfg(test)]
2184mod tests {
2185 use super::*;
2186 use foundry_cli::utils::parse_json;
2187
2188 const SYMBOLIC_RESULT_SCHEMA: &str =
2189 include_str!("../../evm/symbolic/assets/symbolic-result.schema.json");
2190 const SYMBOLIC_COUNTEREXAMPLE_SCHEMA: &str =
2191 include_str!("../../evm/symbolic/assets/symbolic-counterexample.schema.json");
2192
2193 fn schema_defs(schema: &serde_json::Value) -> &serde_json::Map<String, serde_json::Value> {
2194 schema["$defs"].as_object().expect("schema $defs object")
2195 }
2196
2197 fn collect_refs<'a>(value: &'a serde_json::Value, refs: &mut Vec<&'a str>) {
2199 match value {
2200 serde_json::Value::Object(map) => {
2201 refs.extend(map.get("$ref").and_then(serde_json::Value::as_str));
2202 for child in map.values() {
2203 collect_refs(child, refs);
2204 }
2205 }
2206 serde_json::Value::Array(values) => {
2207 for child in values {
2208 collect_refs(child, refs);
2209 }
2210 }
2211 _ => {}
2212 }
2213 }
2214
2215 #[test]
2216 fn symbolic_schemas_match_result_types() {
2217 let result_schema: serde_json::Value = parse_json(SYMBOLIC_RESULT_SCHEMA).unwrap();
2218 let counterexample_schema: serde_json::Value =
2219 parse_json(SYMBOLIC_COUNTEREXAMPLE_SCHEMA).unwrap();
2220 let result_defs = schema_defs(&result_schema);
2221 let counterexample_defs = schema_defs(&counterexample_schema);
2222
2223 let mut refs = Vec::new();
2226 collect_refs(&counterexample_schema, &mut refs);
2227 for reference in refs {
2228 let resolved = if let Some(name) = reference.strip_prefix(
2229 "https://foundry-rs.github.io/schemas/symbolic-result.v1.schema.json#/$defs/",
2230 ) {
2231 result_defs.contains_key(name)
2232 } else if let Some(name) = reference.strip_prefix("#/$defs/") {
2233 counterexample_defs.contains_key(name)
2234 } else {
2235 false
2236 };
2237 assert!(resolved, "unresolved schema ref {reference}");
2238 }
2239
2240 let stats = serde_json::to_value(SymbolicStats::default()).unwrap();
2242 let mut expected = stats.as_object().unwrap().keys().collect::<Vec<_>>();
2243 let mut actual = result_defs["solver_stats"]["properties"]
2244 .as_object()
2245 .unwrap()
2246 .keys()
2247 .collect::<Vec<_>>();
2248 expected.sort();
2249 actual.sort();
2250 assert_eq!(actual, expected);
2251 }
2252
2253 fn outcome_with_results(test_results: Vec<TestResult>) -> TestOutcome {
2254 let test_results = test_results
2255 .into_iter()
2256 .enumerate()
2257 .map(|(idx, result)| (format!("test{idx}()"), result))
2258 .collect();
2259 let suite = SuiteResult::new(Duration::ZERO, test_results, Vec::new());
2260 TestOutcome::new(None, BTreeMap::from([("suite".to_string(), suite)]), false, None)
2261 }
2262
2263 fn failed_result(kind: TestKind) -> TestResult {
2264 TestResult { status: TestStatus::Failure, kind, ..Default::default() }
2265 }
2266
2267 fn failed_invariant(workers: usize) -> TestResult {
2268 let mut kind = invariant_kind(0, 0, 0);
2269 if let TestKind::Invariant { workers: w, .. } = &mut kind {
2270 *w = workers;
2271 }
2272 failed_result(kind)
2273 }
2274
2275 #[test]
2276 fn failed_tests_are_debuggable_only_for_concrete_failures() {
2277 let unit = failed_result(TestKind::Unit { gas: 0 });
2278 assert!(outcome_with_results(vec![unit.clone()]).failed_tests_are_debuggable());
2279 assert!(!outcome_with_results(vec![failed_invariant(1)]).failed_tests_are_debuggable());
2280 assert!(
2281 !outcome_with_results(vec![failed_result(
2282 TestKind::Symbolic(SymbolicStats::default())
2283 )])
2284 .failed_tests_are_debuggable()
2285 );
2286
2287 let mut symbolic_backed = unit;
2288 symbolic_backed.symbolic =
2289 Some(SymbolicResult::pass(&SymbolicConfig::default(), SymbolicStats::default()));
2290 assert!(!outcome_with_results(vec![symbolic_backed]).failed_tests_are_debuggable());
2291 }
2292
2293 #[test]
2294 fn invariant_workers_hint_requires_matching_parallel_worker_counts() {
2295 let hint = |workers: &[usize]| {
2296 outcome_with_results(workers.iter().map(|&w| failed_invariant(w)).collect())
2297 .invariant_workers_hint()
2298 };
2299 assert_eq!(hint(&[3, 3]), Some(3));
2300 assert_eq!(hint(&[2, 3]), None);
2301 assert_eq!(hint(&[1]), None);
2302 }
2303
2304 #[test]
2305 fn invariant_kind_deserializes_legacy_payload_without_workers() {
2306 let kind = serde_json::from_value::<TestKind>(serde_json::json!({
2307 "Invariant": {
2308 "runs": 4,
2309 "calls": 10,
2310 "reverts": 0,
2311 "metrics": {},
2312 "failed_corpus_replays": 0,
2313 "optimization_best_value": null
2314 }
2315 }))
2316 .unwrap();
2317
2318 assert_eq!(kind.invariant_workers(), Some(1));
2319 }
2320}