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 fork_block_number: None,
1217 }
1218 }
1219
1220 pub fn to_basic_tx_details(&self) -> BasicTxDetails {
1222 BasicTxDetails {
1223 warp: self.warp,
1224 roll: self.roll,
1225 sender: self.sender,
1226 call_details: CallDetails {
1227 target: self.target,
1228 calldata: self.calldata.clone(),
1229 value: self.value,
1230 },
1231 }
1232 }
1233}
1234
1235#[derive(Clone, Debug, Default, Serialize, Deserialize)]
1237pub struct TestResult {
1238 pub status: TestStatus,
1242
1243 pub reason: Option<String>,
1245
1246 #[serde(default, skip_serializing_if = "Option::is_none")]
1248 pub fork_block_number: Option<u64>,
1249
1250 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1255 pub invariant_failures: Vec<InvariantFailure>,
1256
1257 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1261 pub invariant_predicate_results: Vec<InvariantPredicateResult>,
1262
1263 #[serde(default, skip_serializing_if = "Option::is_none")]
1266 pub invariant_failure_dir: Option<PathBuf>,
1267
1268 #[serde(default, skip_serializing_if = "Option::is_none")]
1273 pub invariant_count: Option<usize>,
1274
1275 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1279 pub invariant_handler_failures: Vec<InvariantFailure>,
1280
1281 pub counterexample: Option<CounterExample>,
1283
1284 #[serde(default, skip_serializing_if = "Option::is_none")]
1289 pub counterexample_artifact: Option<SymbolicArtifactRef>,
1290
1291 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1293 pub counterexample_artifacts: Vec<SymbolicArtifactRef>,
1294
1295 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1297 pub symbolic_regressions: Vec<SymbolicRegressionRef>,
1298
1299 pub logs: Vec<Log>,
1302
1303 pub decoded_logs: Vec<String>,
1306
1307 pub kind: TestKind,
1309
1310 #[serde(default, skip_serializing_if = "Option::is_none")]
1312 pub symbolic: Option<SymbolicResult>,
1313
1314 pub traces: Traces,
1316
1317 #[serde(skip)]
1319 pub debug_bytecodes: AddressHashMap<Bytes>,
1320
1321 #[serde(skip)]
1325 pub gas_report_traces: Vec<Vec<CallTraceArena>>,
1326
1327 #[serde(skip)]
1329 pub line_coverage: Option<HitMaps>,
1330
1331 #[serde(rename = "labeled_addresses")] pub labels: AddressHashMap<String>,
1334
1335 #[serde(with = "foundry_common::serde_helpers::duration")]
1336 pub duration: Duration,
1337
1338 pub breakpoints: Breakpoints,
1340
1341 pub gas_snapshots: BTreeMap<String, BTreeMap<String, String>>,
1343
1344 #[serde(skip)]
1346 pub deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>,
1347}
1348
1349impl fmt::Display for TestResult {
1350 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1351 f.write_str(&self.render(false, None))
1352 }
1353}
1354
1355fn write_sequence(s: &mut String, label: &str, original: usize, sequence: &[BaseCounterExample]) {
1357 writeln!(s, "\n\t[{label}] (original: {original}, shrunk: {})", sequence.len()).unwrap();
1358 for ex in sequence {
1359 writeln!(s, "{ex}").unwrap();
1360 }
1361}
1362
1363fn write_failure(s: &mut String, failure: &InvariantFailure, name_suffix: &str) -> bool {
1367 write!(s, "[FAIL: {}]{name_suffix}", failure.reason()).unwrap();
1368 if let Some(CounterExample::Sequence(original, sequence)) = failure.counterexample() {
1369 write_sequence(s, "Sequence", *original, sequence);
1370 return true;
1371 }
1372 false
1373}
1374
1375fn replay_artifacts<'a>(
1378 artifact: Option<&'a SymbolicArtifactRef>,
1379 minimization: Option<&'a SymbolicCounterexampleMinimization>,
1380) -> impl Iterator<Item = &'a SymbolicArtifactRef> {
1381 artifact.into_iter().chain(minimization.into_iter().flat_map(|m| [&m.original, &m.minimized]))
1382}
1383
1384impl TestResult {
1385 const fn is_debuggable_failure(&self) -> bool {
1388 self.status.is_failure()
1389 && !self.kind.is_invariant()
1390 && !self.kind.is_symbolic()
1391 && self.symbolic.is_none()
1392 }
1393
1394 pub fn add_counterexample_artifact(&mut self, artifact: SymbolicArtifactRef) {
1396 if !self.counterexample_artifacts.contains(&artifact) {
1397 self.counterexample_artifacts.push(artifact.clone());
1398 }
1399 if self.counterexample_artifact.is_none() {
1400 self.counterexample_artifact = Some(artifact);
1401 }
1402 }
1403
1404 fn render(&self, user_facing: bool, campaign_name: Option<&str>) -> String {
1406 let header = if user_facing {
1407 campaign_name.unwrap_or(INVARIANT_CAMPAIGN_FALLBACK_NAME)
1408 } else {
1409 "Predicates"
1410 };
1411 let mut s = String::new();
1412 match self.status {
1413 TestStatus::Success => {
1414 s.push_str("[PASS]");
1415 if let Some(CounterExample::Sequence(original, sequence)) = &self.counterexample {
1417 write_sequence(&mut s, "Best sequence", *original, sequence);
1418 }
1419 self.write_predicates(&mut s, header, true);
1420 s.green().wrap().to_string()
1421 }
1422 TestStatus::Skipped => {
1423 s.push_str("[SKIP");
1424 if let Some(reason) = &self.reason {
1425 write!(s, ": {reason}").unwrap();
1426 }
1427 s.push(']');
1428 self.write_predicates(&mut s, header, true);
1429 s.yellow().to_string()
1430 }
1431 TestStatus::Failure => {
1432 let is_invariant_failure = !self.invariant_failures.is_empty()
1433 || !self.invariant_handler_failures.is_empty();
1434 if is_invariant_failure {
1435 let named = self.invariant_count.is_some() || self.invariant_failures.len() > 1;
1439 for (i, failure) in self.invariant_failures.iter().enumerate() {
1440 if i > 0 {
1441 s.push('\n');
1442 }
1443 let is_anchor =
1444 matches!(failure, InvariantFailure::Predicate { is_anchor: true, .. });
1445 let suffix = if named || !is_anchor {
1446 format!(" {}", failure.name())
1447 } else {
1448 String::new()
1449 };
1450 write_failure(&mut s, failure, &suffix);
1451 }
1452 } else {
1453 s.push_str("[FAIL");
1456 if let Some(reason) = &self.reason {
1457 write!(s, ": {reason}").unwrap();
1458 }
1459 match &self.counterexample {
1460 Some(CounterExample::Single(ex)) => {
1461 write!(s, "; counterexample: {ex}]").unwrap();
1462 }
1463 Some(CounterExample::Sequence(original, sequence)) => {
1464 s.push(']');
1465 write_sequence(&mut s, "Sequence", *original, sequence);
1466 }
1467 None => s.push(']'),
1468 }
1469 }
1470
1471 let broken = self.invariant_failures.len();
1472 let rollup = match self.invariant_count {
1473 Some(total) if total > 1 && is_invariant_failure => {
1474 writeln!(s, "\n{header}: {broken}/{total} invariants broken").unwrap();
1475 true
1476 }
1477 _ => false,
1478 };
1479 self.write_predicates(&mut s, header, !user_facing || !rollup);
1480 if broken > 1
1481 && let Some(dir) = &self.invariant_failure_dir
1482 {
1483 writeln!(
1484 s,
1485 "{broken} invariant failure(s) persisted to {} — rerun to shrink",
1486 dir.display()
1487 )
1488 .unwrap();
1489 }
1490
1491 if !self.invariant_handler_failures.is_empty() {
1492 let preceded = rollup
1494 || broken > 0
1495 || (user_facing && self.invariant_predicate_results.len() > 1);
1496 writeln!(
1497 s,
1498 "{}{}: {} assertion bug(s) found",
1499 if preceded { "\n" } else { "" },
1500 if user_facing { "Assertion Tests" } else { "Handler assertions" },
1501 self.invariant_handler_failures.len()
1502 )
1503 .unwrap();
1504 for failure in &self.invariant_handler_failures {
1505 if !write_failure(&mut s, failure, &format!(" {}", failure.name())) {
1506 s.push('\n');
1507 }
1508 }
1509 }
1510
1511 s.red().wrap().to_string()
1512 }
1513 }
1514 }
1515
1516 fn write_predicates(&self, s: &mut String, header: &str, show_header: bool) {
1518 if self.invariant_predicate_results.len() <= 1 {
1519 return;
1520 }
1521 if show_header {
1522 write!(s, "\n{header}:\n").unwrap();
1523 }
1524 for predicate in &self.invariant_predicate_results {
1525 let name = &predicate.name;
1526 match (predicate.status, &predicate.reason) {
1527 (TestStatus::Success, _) => writeln!(s, "[PASS] {name}"),
1528 (TestStatus::Failure, reason) => {
1529 writeln!(s, "[FAIL: {}] {name}", reason.as_deref().unwrap_or_default())
1530 }
1531 (TestStatus::Skipped, Some(reason)) => writeln!(s, "[SKIP: {reason}] {name}"),
1532 (TestStatus::Skipped, None) => writeln!(s, "[SKIP] {name}"),
1533 }
1534 .unwrap();
1535 }
1536 }
1537}
1538
1539macro_rules! extend {
1540 ($a:expr, $b:expr, $trace_kind:expr) => {
1541 if $b.fork_block_number.is_some() {
1542 $a.fork_block_number = $b.fork_block_number;
1543 }
1544 $a.logs.extend($b.logs);
1545 $a.labels.extend($b.labels);
1546 $a.traces.extend($b.traces.map(|traces| ($trace_kind, traces)));
1547 $a.debug_bytecodes.extend($b.debug_bytecodes);
1548 $a.merge_coverages($b.line_coverage);
1549 };
1550}
1551
1552#[derive(Default)]
1554pub struct InvariantOutcome {
1555 pub success: bool,
1557 pub fork_block_number: Option<u64>,
1559 pub failures: Vec<InvariantFailure>,
1561 pub handler_failures: Vec<InvariantFailure>,
1563 pub predicate_results: Vec<InvariantPredicateResult>,
1565 pub failure_dir: Option<PathBuf>,
1567 pub invariant_count: Option<usize>,
1569 pub counterexample: Option<CounterExample>,
1571 pub gas_report_traces: Vec<Vec<CallTraceArena>>,
1573}
1574
1575pub(crate) fn invariant_kind(runs: usize, calls: usize, reverts: usize) -> TestKind {
1577 TestKind::Invariant {
1578 runs,
1579 calls,
1580 reverts,
1581 workers: 1,
1582 metrics: Default::default(),
1583 failed_corpus_replays: 0,
1584 optimization_best_value: None,
1585 }
1586}
1587
1588impl TestResult {
1589 pub fn new(setup: &TestSetup) -> Self {
1591 Self {
1592 labels: setup.labels.clone(),
1593 logs: setup.logs.clone(),
1594 traces: setup.traces.clone(),
1595 debug_bytecodes: setup.debug_bytecodes.clone(),
1596 line_coverage: setup.coverage.clone(),
1597 fork_block_number: setup.fork_block_number,
1598 ..Default::default()
1599 }
1600 }
1601
1602 pub fn fail(reason: String) -> Self {
1604 Self { status: TestStatus::Failure, reason: Some(reason), ..Default::default() }
1605 }
1606
1607 pub fn setup_result(setup: TestSetup) -> Self {
1609 Self {
1610 status: if setup.skipped { TestStatus::Skipped } else { TestStatus::Failure },
1611 reason: setup.reason,
1612 logs: setup.logs,
1613 traces: setup.traces,
1614 debug_bytecodes: setup.debug_bytecodes,
1615 line_coverage: setup.coverage,
1616 labels: setup.labels,
1617 fork_block_number: setup.fork_block_number,
1618 ..Default::default()
1619 }
1620 }
1621
1622 pub fn single_skip(&mut self, reason: SkipReason) {
1624 self.status = TestStatus::Skipped;
1625 self.reason = reason.0;
1626 }
1627
1628 pub fn single_fail(&mut self, reason: Option<String>) {
1630 self.status = TestStatus::Failure;
1631 self.reason = reason;
1632 }
1633
1634 pub fn single_result<FEN: FoundryEvmNetwork>(
1637 &mut self,
1638 success: bool,
1639 reason: Option<String>,
1640 raw_call_result: RawCallResult<FEN>,
1641 ) {
1642 self.kind = TestKind::Unit {
1643 gas: raw_call_result.gas_used.saturating_sub(raw_call_result.stipend),
1644 };
1645
1646 extend!(self, raw_call_result, TraceKind::Execution);
1647
1648 self.status = if success { TestStatus::Success } else { TestStatus::Failure };
1649 self.reason = reason;
1650 self.duration = Duration::default();
1651 self.gas_report_traces = Vec::new();
1652
1653 if let Some(cheatcodes) = raw_call_result.cheatcodes {
1654 self.breakpoints = cheatcodes.breakpoints;
1655 self.gas_snapshots = cheatcodes.gas_snapshots;
1656 self.deprecated_cheatcodes = cheatcodes.deprecated;
1657 }
1658 }
1659
1660 pub fn fuzz_result(&mut self, mut result: FuzzTestResult) {
1663 let kind = TestKind::Fuzz {
1664 median_gas: result.median_gas(false),
1665 mean_gas: result.mean_gas(false),
1666 first_case: std::mem::take(&mut result.first_case),
1667 runs: result.gas_by_case.len(),
1668 failed_corpus_replays: result.failed_corpus_replays,
1669 };
1670 self.campaign_result(kind, result);
1671 }
1672
1673 pub fn table_result(&mut self, result: FuzzTestResult) {
1676 let kind = TestKind::Table {
1677 median_gas: result.median_gas(false),
1678 mean_gas: result.mean_gas(false),
1679 runs: result.gas_by_case.len(),
1680 };
1681 self.campaign_result(kind, result);
1682 }
1683
1684 fn campaign_result(&mut self, kind: TestKind, result: FuzzTestResult) {
1685 self.kind = kind;
1686
1687 extend!(self, result, TraceKind::Execution);
1688
1689 self.status = if result.skipped {
1690 TestStatus::Skipped
1691 } else if result.success {
1692 TestStatus::Success
1693 } else {
1694 TestStatus::Failure
1695 };
1696 self.reason = result.reason;
1697 self.counterexample = result.counterexample;
1698 self.duration = Duration::default();
1699 self.gas_report_traces = result.gas_report_traces.into_iter().map(|t| vec![t]).collect();
1700 self.breakpoints = result.breakpoints.unwrap_or_default();
1701 self.deprecated_cheatcodes = result.deprecated_cheatcodes;
1702 }
1703
1704 pub fn fuzz_setup_fail(&mut self, e: Report) {
1706 self.kind = TestKind::Fuzz {
1707 first_case: Default::default(),
1708 runs: 0,
1709 mean_gas: 0,
1710 median_gas: 0,
1711 failed_corpus_replays: 0,
1712 };
1713 self.status = TestStatus::Failure;
1714 debug!(?e, "failed to set up fuzz testing environment");
1715 self.reason = Some(format!("failed to set up fuzz testing environment: {e}"));
1716 }
1717
1718 pub fn invariant_skip_with_predicates(
1720 &mut self,
1721 reason: SkipReason,
1722 invariant_predicate_results: Vec<InvariantPredicateResult>,
1723 ) {
1724 self.kind = invariant_kind(1, 1, 1);
1725 self.status = TestStatus::Skipped;
1726 let predicate_count = invariant_predicate_results.len();
1727 let is_campaign = predicate_count > 1;
1728 self.reason = if is_campaign { None } else { reason.0 };
1729 self.invariant_count = is_campaign.then_some(predicate_count);
1730 self.invariant_predicate_results = invariant_predicate_results;
1731 }
1732
1733 pub fn invariant_replay_fail(
1735 &mut self,
1736 outcome: CheckSequenceOutcome,
1737 invariant_name: &str,
1738 fallback_reason: Option<String>,
1739 call_sequence: Vec<BaseCounterExample>,
1740 ) {
1741 self.kind = invariant_kind(1, outcome.calls_count, outcome.reverts);
1742 self.status = TestStatus::Failure;
1743 self.reason = Some(outcome.reason.or(fallback_reason).unwrap_or_else(|| {
1744 let what = if outcome.replayed_entirely {
1745 "replay failure"
1746 } else {
1747 "persisted failure revert"
1748 };
1749 format!("{invariant_name} {what}")
1750 }));
1751 self.counterexample = Some(CounterExample::Sequence(call_sequence.len(), call_sequence));
1752 }
1753
1754 pub fn invariant_replay_success(&mut self, call_count: usize, reverts: usize) {
1756 self.kind = invariant_kind(1, call_count, reverts);
1757 self.status = TestStatus::Success;
1758 self.reason = None;
1759 }
1760
1761 pub fn invariant_setup_fail(&mut self, e: Report) {
1763 self.kind = invariant_kind(0, 0, 0);
1764 self.status = TestStatus::Failure;
1765 self.reason = Some(format!("failed to set up invariant testing environment: {e}"));
1766 }
1767
1768 pub fn invariant_result(&mut self, kind: TestKind, outcome: InvariantOutcome) {
1770 let optimizing =
1772 matches!(kind, TestKind::Invariant { optimization_best_value: Some(_), .. });
1773 self.kind = kind;
1774 self.status =
1775 if optimizing || outcome.success { TestStatus::Success } else { TestStatus::Failure };
1776 self.fork_block_number = outcome.fork_block_number;
1777 self.invariant_predicate_results = outcome.predicate_results;
1778 self.invariant_failure_dir = outcome.failure_dir;
1779 self.invariant_count = outcome.invariant_count;
1780 self.counterexample = outcome.counterexample;
1784 for artifact in outcome
1785 .failures
1786 .iter()
1787 .chain(&outcome.handler_failures)
1788 .flat_map(|failure| replay_artifacts(failure.artifact(), failure.minimization()))
1789 {
1790 self.add_counterexample_artifact(artifact.clone());
1791 }
1792 self.invariant_failures = outcome.failures;
1793 self.invariant_handler_failures = outcome.handler_failures;
1794 self.gas_report_traces = outcome.gas_report_traces;
1795 }
1796
1797 pub fn symbolic_result(
1799 &mut self,
1800 status: TestStatus,
1801 reason: Option<String>,
1802 counterexample: Option<CounterExample>,
1803 symbolic: SymbolicResult,
1804 ) {
1805 self.kind = TestKind::Symbolic(symbolic.solver.stats);
1806 self.status = status;
1807 self.reason = reason;
1808 self.counterexample = counterexample;
1809 self.record_symbolic(symbolic);
1810 self.duration = Duration::default();
1811 }
1812
1813 pub(crate) fn record_symbolic(&mut self, symbolic: SymbolicResult) {
1815 for artifact in replay_artifacts(symbolic.artifact.as_ref(), symbolic.minimization.as_ref())
1816 {
1817 self.add_counterexample_artifact(artifact.clone());
1818 }
1819 self.symbolic = Some(symbolic);
1820 }
1821
1822 pub fn replay_result(
1824 &mut self,
1825 corpus_entries: usize,
1826 showmap_files: usize,
1827 skipped_entries: usize,
1828 duration: Duration,
1829 ) {
1830 self.kind = TestKind::Replay { corpus_entries, showmap_files, skipped_entries };
1831 self.status = TestStatus::Success;
1832 self.duration = duration;
1833 }
1834
1835 pub fn replay_skip(&mut self, reason: impl Into<String>) {
1837 self.kind = TestKind::Replay { corpus_entries: 0, showmap_files: 0, skipped_entries: 0 };
1838 self.status = TestStatus::Skipped;
1839 self.reason = Some(reason.into());
1840 self.duration = Duration::default();
1841 }
1842
1843 pub fn interrupt(&mut self) {
1846 let reason = Some("interrupted".to_string());
1847 self.status = TestStatus::Skipped;
1848 for predicate in &mut self.invariant_predicate_results {
1849 if predicate.status.is_success() {
1850 predicate.status = TestStatus::Skipped;
1851 predicate.reason.clone_from(&reason);
1852 }
1853 }
1854 if self.invariant_count.is_none() {
1856 self.reason = reason;
1857 }
1858 }
1859
1860 pub(crate) fn short_result_with_suite(&self, name: &str, suite_name: &str) -> String {
1863 let campaign = (self.kind.is_invariant() && self.invariant_count.is_some())
1864 .then(|| invariant_campaign_display_name(get_contract_name(suite_name)));
1865 let name = campaign.as_deref().unwrap_or(name);
1866 let status = self.render(true, campaign.as_deref());
1867 let block = match self.fork_block_number {
1868 Some(block) if self.status.is_failure() => format!(" (block: {block})"),
1869 _ => String::new(),
1870 };
1871 format!("{status} {name}{block} {}", self.kind.report())
1872 }
1873
1874 fn logical_count(&self) -> usize {
1877 let skipped = self.skipped_predicate_count();
1878 if skipped == 0 {
1879 1
1880 } else if self.status.is_skipped() && skipped == self.invariant_predicate_results.len() {
1881 skipped
1882 } else {
1883 1 + skipped
1884 }
1885 }
1886
1887 fn skipped_count(&self) -> usize {
1888 let skipped = self.skipped_predicate_count();
1889 if skipped == 0 && self.status.is_skipped() { 1 } else { skipped }
1890 }
1891
1892 fn skipped_predicate_count(&self) -> usize {
1893 self.invariant_predicate_results.iter().filter(|p| p.status.is_skipped()).count()
1894 }
1895
1896 pub fn extend<FEN: FoundryEvmNetwork>(&mut self, call_result: RawCallResult<FEN>) {
1898 extend!(self, call_result, TraceKind::Execution);
1899 }
1900
1901 pub(crate) fn extend_setup<FEN: FoundryEvmNetwork>(&mut self, call_result: RawCallResult<FEN>) {
1903 extend!(self, call_result, TraceKind::Setup);
1904 }
1905
1906 pub fn merge_coverages(&mut self, other_coverage: Option<HitMaps>) {
1908 HitMaps::merge_opt(&mut self.line_coverage, other_coverage);
1909 }
1910}
1911
1912#[derive(Clone, Debug, PartialEq, Eq)]
1914pub enum TestKindReport {
1915 Unit {
1916 gas: u64,
1917 },
1918 Fuzz {
1919 runs: usize,
1920 mean_gas: u64,
1921 median_gas: u64,
1922 failed_corpus_replays: usize,
1923 },
1924 Invariant {
1925 runs: usize,
1926 calls: usize,
1927 reverts: usize,
1928 failed_corpus_replays: usize,
1929 optimization_best_value: Option<I256>,
1931 },
1932 Table {
1933 runs: usize,
1934 mean_gas: u64,
1935 median_gas: u64,
1936 },
1937 Symbolic(SymbolicStats),
1938 Replay {
1940 corpus_entries: usize,
1941 showmap_files: usize,
1942 skipped_entries: usize,
1943 },
1944}
1945
1946impl fmt::Display for TestKindReport {
1947 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1948 match self {
1949 Self::Unit { gas } => write!(f, "(gas: {gas})"),
1950 Self::Fuzz { runs, mean_gas, median_gas, failed_corpus_replays } => {
1951 write!(f, "(runs: {runs}, μ: {mean_gas}, ~: {median_gas}")?;
1952 if *failed_corpus_replays != 0 {
1953 write!(f, ", failed corpus replays: {failed_corpus_replays}")?;
1954 }
1955 f.write_str(")")
1956 }
1957 Self::Invariant {
1958 runs,
1959 calls,
1960 reverts,
1961 failed_corpus_replays,
1962 optimization_best_value,
1963 } => {
1964 if let Some(best_value) = optimization_best_value {
1965 return write!(f, "(best: {best_value}, runs: {runs}, calls: {calls})");
1966 }
1967 write!(f, "(runs: {runs}, calls: {calls}, reverts: {reverts}")?;
1968 if *failed_corpus_replays != 0 {
1969 write!(f, ", failed corpus replays: {failed_corpus_replays}")?;
1970 }
1971 f.write_str(")")
1972 }
1973 Self::Table { runs, mean_gas, median_gas } => {
1974 write!(f, "(runs: {runs}, μ: {mean_gas}, ~: {median_gas})")
1975 }
1976 Self::Symbolic(SymbolicStats {
1977 paths,
1978 solver_queries,
1979 smt_queries,
1980 sat_queries,
1981 model_queries,
1982 sat_cache_hits,
1983 model_cache_hits,
1984 heuristic_witnesses,
1985 solver_time_ms,
1986 ..
1987 }) => {
1988 write!(
1989 f,
1990 "(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)"
1991 )
1992 }
1993 Self::Replay { corpus_entries, showmap_files, skipped_entries } => {
1994 write!(f, "(replay: {corpus_entries} entries, {showmap_files} files")?;
1995 if *skipped_entries != 0 {
1996 write!(f, ", {skipped_entries} skipped")?;
1997 }
1998 f.write_str(")")
1999 }
2000 }
2001 }
2002}
2003
2004impl TestKindReport {
2005 pub const fn gas(&self) -> u64 {
2007 match *self {
2008 Self::Unit { gas } => gas,
2009 Self::Fuzz { median_gas, .. } | Self::Table { median_gas, .. } => median_gas,
2011 Self::Invariant { .. } | Self::Symbolic { .. } | Self::Replay { .. } => 0,
2013 }
2014 }
2015}
2016
2017#[derive(Clone, Debug, Serialize, Deserialize)]
2019pub enum TestKind {
2020 Unit { gas: u64 },
2022 Fuzz {
2024 first_case: FuzzCase,
2026 runs: usize,
2027 mean_gas: u64,
2028 median_gas: u64,
2029 failed_corpus_replays: usize,
2030 },
2031 Invariant {
2033 runs: usize,
2034 calls: usize,
2035 reverts: usize,
2036 #[serde(default = "default_invariant_workers")]
2038 workers: usize,
2039 metrics: Map<String, InvariantMetrics>,
2040 failed_corpus_replays: usize,
2041 optimization_best_value: Option<I256>,
2043 },
2044 Table { runs: usize, mean_gas: u64, median_gas: u64 },
2046 Symbolic(SymbolicStats),
2048 Replay { corpus_entries: usize, showmap_files: usize, skipped_entries: usize },
2050}
2051
2052impl Default for TestKind {
2053 fn default() -> Self {
2054 Self::Unit { gas: 0 }
2055 }
2056}
2057
2058impl TestKind {
2059 pub const fn is_fuzz(&self) -> bool {
2061 matches!(self, Self::Fuzz { .. })
2062 }
2063
2064 pub const fn is_invariant(&self) -> bool {
2066 matches!(self, Self::Invariant { .. })
2067 }
2068
2069 pub const fn is_symbolic(&self) -> bool {
2071 matches!(self, Self::Symbolic { .. })
2072 }
2073
2074 pub const fn invariant_workers(&self) -> Option<usize> {
2076 match self {
2077 Self::Invariant { workers, .. } => Some(*workers),
2078 _ => None,
2079 }
2080 }
2081
2082 pub const fn report(&self) -> TestKindReport {
2084 match *self {
2085 Self::Unit { gas } => TestKindReport::Unit { gas },
2086 Self::Fuzz { runs, mean_gas, median_gas, failed_corpus_replays, .. } => {
2087 TestKindReport::Fuzz { runs, mean_gas, median_gas, failed_corpus_replays }
2088 }
2089 Self::Invariant {
2090 runs,
2091 calls,
2092 reverts,
2093 failed_corpus_replays,
2094 optimization_best_value,
2095 ..
2096 } => TestKindReport::Invariant {
2097 runs,
2098 calls,
2099 reverts,
2100 failed_corpus_replays,
2101 optimization_best_value,
2102 },
2103 Self::Table { runs, mean_gas, median_gas } => {
2104 TestKindReport::Table { runs, mean_gas, median_gas }
2105 }
2106 Self::Symbolic(stats) => TestKindReport::Symbolic(stats),
2107 Self::Replay { corpus_entries, showmap_files, skipped_entries } => {
2108 TestKindReport::Replay { corpus_entries, showmap_files, skipped_entries }
2109 }
2110 }
2111 }
2112}
2113
2114const fn default_invariant_workers() -> usize {
2115 1
2116}
2117
2118#[derive(Clone, Debug, Default)]
2123pub struct TestSetup {
2124 pub address: Address,
2126 pub fuzz_fixtures: FuzzFixtures,
2128
2129 pub logs: Vec<Log>,
2131 pub labels: AddressHashMap<String>,
2133 pub traces: Traces,
2135 pub debug_bytecodes: AddressHashMap<Bytes>,
2137 pub coverage: Option<HitMaps>,
2139 pub deployed_libs: Vec<Address>,
2141 pub fork_block_number: Option<u64>,
2143 pub(crate) fuzz_state: OnceLock<EvmFuzzState>,
2145
2146 pub reason: Option<String>,
2148 pub skipped: bool,
2150 pub deployment_failure: bool,
2152}
2153
2154impl TestSetup {
2155 pub fn failed(reason: String) -> Self {
2156 Self { reason: Some(reason), ..Default::default() }
2157 }
2158
2159 pub fn skipped(reason: String) -> Self {
2160 Self { reason: Some(reason), skipped: true, ..Default::default() }
2161 }
2162
2163 pub fn extend<FEN: FoundryEvmNetwork>(
2164 &mut self,
2165 raw: RawCallResult<FEN>,
2166 trace_kind: TraceKind,
2167 ) {
2168 extend!(self, raw, trace_kind);
2169 }
2170
2171 pub fn merge_coverages(&mut self, other_coverage: Option<HitMaps>) {
2172 HitMaps::merge_opt(&mut self.coverage, other_coverage);
2173 }
2174}
2175
2176pub(crate) fn invariant_campaign_display_name(contract_name: &str) -> String {
2177 format!("{contract_name} invariants")
2178}
2179
2180const fn symbolic_result_schema_version() -> u32 {
2181 SYMBOLIC_RESULT_SCHEMA_VERSION
2182}
2183
2184#[cfg(test)]
2185mod tests {
2186 use super::*;
2187 use foundry_cli::utils::parse_json;
2188
2189 const SYMBOLIC_RESULT_SCHEMA: &str =
2190 include_str!("../../evm/symbolic/assets/symbolic-result.schema.json");
2191 const SYMBOLIC_COUNTEREXAMPLE_SCHEMA: &str =
2192 include_str!("../../evm/symbolic/assets/symbolic-counterexample.schema.json");
2193
2194 fn schema_defs(schema: &serde_json::Value) -> &serde_json::Map<String, serde_json::Value> {
2195 schema["$defs"].as_object().expect("schema $defs object")
2196 }
2197
2198 fn collect_refs<'a>(value: &'a serde_json::Value, refs: &mut Vec<&'a str>) {
2200 match value {
2201 serde_json::Value::Object(map) => {
2202 refs.extend(map.get("$ref").and_then(serde_json::Value::as_str));
2203 for child in map.values() {
2204 collect_refs(child, refs);
2205 }
2206 }
2207 serde_json::Value::Array(values) => {
2208 for child in values {
2209 collect_refs(child, refs);
2210 }
2211 }
2212 _ => {}
2213 }
2214 }
2215
2216 #[test]
2217 fn symbolic_schemas_match_result_types() {
2218 let result_schema: serde_json::Value = parse_json(SYMBOLIC_RESULT_SCHEMA).unwrap();
2219 let counterexample_schema: serde_json::Value =
2220 parse_json(SYMBOLIC_COUNTEREXAMPLE_SCHEMA).unwrap();
2221 let result_defs = schema_defs(&result_schema);
2222 let counterexample_defs = schema_defs(&counterexample_schema);
2223
2224 let mut refs = Vec::new();
2227 collect_refs(&counterexample_schema, &mut refs);
2228 for reference in refs {
2229 let resolved = if let Some(name) = reference.strip_prefix(
2230 "https://foundry-rs.github.io/schemas/symbolic-result.v1.schema.json#/$defs/",
2231 ) {
2232 result_defs.contains_key(name)
2233 } else if let Some(name) = reference.strip_prefix("#/$defs/") {
2234 counterexample_defs.contains_key(name)
2235 } else {
2236 false
2237 };
2238 assert!(resolved, "unresolved schema ref {reference}");
2239 }
2240
2241 let stats = serde_json::to_value(SymbolicStats::default()).unwrap();
2243 let mut expected = stats.as_object().unwrap().keys().collect::<Vec<_>>();
2244 let mut actual = result_defs["solver_stats"]["properties"]
2245 .as_object()
2246 .unwrap()
2247 .keys()
2248 .collect::<Vec<_>>();
2249 expected.sort();
2250 actual.sort();
2251 assert_eq!(actual, expected);
2252 }
2253
2254 fn outcome_with_results(test_results: Vec<TestResult>) -> TestOutcome {
2255 let test_results = test_results
2256 .into_iter()
2257 .enumerate()
2258 .map(|(idx, result)| (format!("test{idx}()"), result))
2259 .collect();
2260 let suite = SuiteResult::new(Duration::ZERO, test_results, Vec::new());
2261 TestOutcome::new(None, BTreeMap::from([("suite".to_string(), suite)]), false, None)
2262 }
2263
2264 fn failed_result(kind: TestKind) -> TestResult {
2265 TestResult { status: TestStatus::Failure, kind, ..Default::default() }
2266 }
2267
2268 fn failed_invariant(workers: usize) -> TestResult {
2269 let mut kind = invariant_kind(0, 0, 0);
2270 if let TestKind::Invariant { workers: w, .. } = &mut kind {
2271 *w = workers;
2272 }
2273 failed_result(kind)
2274 }
2275
2276 #[test]
2277 fn failed_tests_are_debuggable_only_for_concrete_failures() {
2278 let unit = failed_result(TestKind::Unit { gas: 0 });
2279 assert!(outcome_with_results(vec![unit.clone()]).failed_tests_are_debuggable());
2280 assert!(!outcome_with_results(vec![failed_invariant(1)]).failed_tests_are_debuggable());
2281 assert!(
2282 !outcome_with_results(vec![failed_result(
2283 TestKind::Symbolic(SymbolicStats::default())
2284 )])
2285 .failed_tests_are_debuggable()
2286 );
2287
2288 let mut symbolic_backed = unit;
2289 symbolic_backed.symbolic =
2290 Some(SymbolicResult::pass(&SymbolicConfig::default(), SymbolicStats::default()));
2291 assert!(!outcome_with_results(vec![symbolic_backed]).failed_tests_are_debuggable());
2292 }
2293
2294 #[test]
2295 fn invariant_workers_hint_requires_matching_parallel_worker_counts() {
2296 let hint = |workers: &[usize]| {
2297 outcome_with_results(workers.iter().map(|&w| failed_invariant(w)).collect())
2298 .invariant_workers_hint()
2299 };
2300 assert_eq!(hint(&[3, 3]), Some(3));
2301 assert_eq!(hint(&[2, 3]), None);
2302 assert_eq!(hint(&[1]), None);
2303 }
2304
2305 #[test]
2306 fn invariant_kind_deserializes_legacy_payload_without_workers() {
2307 let kind = serde_json::from_value::<TestKind>(serde_json::json!({
2308 "Invariant": {
2309 "runs": 4,
2310 "calls": 10,
2311 "reverts": 0,
2312 "metrics": {},
2313 "failed_corpus_replays": 0,
2314 "optimization_best_value": null
2315 }
2316 }))
2317 .unwrap();
2318
2319 assert_eq!(kind.invariant_workers(), Some(1));
2320 }
2321}