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