1use super::{fuzz::FuzzRunArgs, install, watch::WatchArgs};
2use crate::{
3 MultiContractRunner, MultiContractRunnerBuilder, brutalizer,
4 decode::decode_console_logs,
5 gas_report::GasReport,
6 multi_runner::{
7 FuzzMinimizeConfig, FuzzMinimizeEdgeIndices, FuzzMinimizeMode, FuzzMinimizeObservation,
8 MultiNetworkConfig, ShowmapConfig, SymbolicArtifactReplayConfig, TestFunctionMatcher,
9 is_generated_symbolic_regression_contract,
10 },
11 mutation::{MutationRunConfig, run_mutation_testing},
12 result::{
13 SYMBOLIC_COUNTEREXAMPLE_ARTIFACT_SCHEMA, SuiteResult, SymbolicCounterexampleArtifact,
14 SymbolicReplayStatus, TestKindReport, TestOutcome, TestResult, TestStatus,
15 },
16 symbolic_regression::{
17 SymbolicRegressionConfig, attach_symbolic_regressions_to_suites,
18 collect_symbolic_artifacts_from_suites, emit_symbolic_regressions,
19 },
20 traces::{
21 CallTraceDecoderBuilder, InternalTraceMode, TraceKind,
22 debug::{ContractSources, DebugTraceIdentifier},
23 decode_trace_arena, folded_stack_trace,
24 identifier::SignaturesIdentifier,
25 speedscope,
26 },
27 workspace,
28};
29use alloy_primitives::U256;
30use chrono::Utc;
31use clap::{Parser, ValueEnum, ValueHint};
32use dialoguer::{Select, console::Term};
33use eyre::{Context, OptionExt, Result, bail};
34use foundry_cli::{
35 opts::{BuildOpts, EvmArgs, GlobalArgs, TracingArgs},
36 utils::{self, FoundryPathExt, LoadConfig},
37};
38use foundry_common::{
39 EmptyTestFilter, TestFilter, TestFunctionExt, TestFunctionKind,
40 compile::{ProjectCompiler, compile_abi_project},
41 fs, sh_status, sh_warn, shell,
42};
43use foundry_compilers::{
44 ProjectCompileOutput,
45 artifacts::Libraries,
46 compilers::{
47 Language,
48 multi::{MultiCompiler, MultiCompilerLanguage},
49 },
50 utils::source_files_iter,
51};
52use foundry_config::{
53 Config, InlineConfig, InvariantDepthMode, InvariantWorkers, figment,
54 figment::{
55 Metadata, Profile, Provider,
56 value::{Dict, Map, Value},
57 },
58 filter::GlobMatcher,
59 fs_permissions::FsAccessPermission,
60};
61use foundry_debugger::{Debugger, DebuggerLayout};
62#[cfg(feature = "optimism")]
63use foundry_evm::core::evm::OpEvmNetwork;
64use foundry_evm::{
65 core::evm::{
66 BlockEnvFor, EthEvmNetwork, FoundryEvmNetwork, SpecFor, TempoEvmNetwork, TxEnvFor,
67 },
68 executors::ShowmapDomain,
69 fuzz::{BasicTxDetails, CounterExample},
70 hardforks::TempoHardfork,
71 opts::EvmOpts,
72 traces::{
73 backtrace::BacktraceBuilder, identifier::TraceIdentifiers, prune_trace_depth,
74 trace_arena_at_depth,
75 },
76};
77use foundry_tui::tui_mode;
78use rand::Rng;
79use regex::Regex;
80use revm::{bytecode::opcode::OpCode, context::Transaction};
81use std::{
82 collections::{BTreeMap, BTreeSet},
83 fmt::Write,
84 path::{Path, PathBuf},
85 sync::{Arc, Mutex, mpsc::channel},
86 time::{Duration, Instant},
87};
88use tempfile::TempDir;
89use yansi::Paint;
90
91mod evm_profile_server;
92mod filter;
93mod summary;
94use crate::{
95 result::TestKind,
96 runner::{count_runnable_invariant_campaign_anchors, function_matches_network_pass},
97 traces::render_trace_arena_inner,
98};
99use filter::RerunFailures;
100pub use filter::{FilterArgs, ProjectPathsAwareFilter, RerunFailure};
101use quick_junit::{NonSuccessKind, Report, TestCase, TestCaseStatus, TestSuite};
102use summary::{TestSummaryReport, format_invariant_metrics_table};
103
104const DEBUGGER_MATCHING_TESTS_DISPLAY_LIMIT: usize = 12;
105const AUTO_FUZZ_FAILURE_DIR: &str = "fuzz";
106const AUTO_CORPUS_DIR: &str = "corpus";
107
108#[derive(Clone, Copy, Debug, Default)]
109enum FuzzOnlyMode {
110 #[default]
111 Disabled,
112 Enabled,
113 WithAutoFuzzCorpus,
114}
115
116impl FuzzOnlyMode {
117 const fn is_enabled(self) -> bool {
118 !matches!(self, Self::Disabled)
119 }
120
121 const fn uses_auto_fuzz_corpus(self) -> bool {
122 matches!(self, Self::WithAutoFuzzCorpus)
123 }
124}
125
126foundry_config::merge_impl_figment_convert!(TestArgs, build, evm);
128
129fn validate_showmap_name(kind: &str, name: &str) -> Result<()> {
130 let path = Path::new(name);
131 if name.is_empty()
132 || path.is_absolute()
133 || path.components().count() != 1
134 || name.contains(['/', '\\'])
135 || matches!(name, "." | "..")
136 {
137 bail!(
138 "invalid {kind} `{name}`: expected a single file-name component without path separators"
139 );
140 }
141 Ok(())
142}
143
144fn validate_showmap_config(showmap: &ShowmapConfig) -> Result<()> {
145 validate_showmap_name("showmap approach", &showmap.approach)?;
146 validate_showmap_name("showmap trial", &showmap.trial)
147}
148
149pub(crate) struct FuzzMinimizeReplaySession {
150 filter: ProjectPathsAwareFilter,
151 passes: Vec<FuzzMinimizeReplayPass>,
152}
153
154type FuzzMinimizeReplay = Box<dyn Fn(&ProjectPathsAwareFilter, FuzzMinimizeConfig) -> Result<()>>;
155
156struct FuzzMinimizeReplayPass {
157 target_count: usize,
158 replay: FuzzMinimizeReplay,
159}
160
161impl FuzzMinimizeReplaySession {
162 pub(crate) fn replay(
163 &self,
164 sequence: Vec<BasicTxDetails>,
165 evm_edge_indices: FuzzMinimizeEdgeIndices,
166 mode: FuzzMinimizeMode,
167 ) -> Result<Vec<FuzzMinimizeObservation>> {
168 let observations = Arc::new(Mutex::new(Vec::new()));
169 let fuzz_minimize = FuzzMinimizeConfig {
170 input: sequence.into(),
171 mode,
172 evm_edge_indices,
173 observations: observations.clone(),
174 };
175
176 for pass in &self.passes {
177 if pass.target_count == 0 {
178 continue;
179 }
180 (pass.replay)(&self.filter, fuzz_minimize.clone())?;
181 }
182
183 let observations = observations
184 .lock()
185 .map_err(|_| eyre::eyre!("minimize observations lock poisoned"))?
186 .clone();
187 if observations.is_empty() {
188 bail!("fuzz minimization replay produced no observation for the matched test");
189 }
190 Ok(observations)
191 }
192}
193
194fn replay_with_runner<FEN: FoundryEvmNetwork>(
195 runner: &MultiContractRunner<FEN>,
196 filter: &ProjectPathsAwareFilter,
197 fuzz_minimize: FuzzMinimizeConfig,
198) -> Result<()> {
199 let mut runner = runner.clone();
200 runner.tcfg.fuzz_minimize = Some(fuzz_minimize);
201 let results = runner.test_collect(filter)?;
202 for (suite, suite_result) in results {
203 for (test, test_result) in suite_result.test_results {
204 if test_result.status == TestStatus::Failure {
205 bail!(
206 "fuzz minimization replay failed for {suite}::{test}: {}",
207 test_result.reason.as_deref().unwrap_or("unknown error")
208 );
209 }
210 }
211 }
212 Ok(())
213}
214
215fn fuzz_minimize_replay<FEN: FoundryEvmNetwork>(
216 runner: MultiContractRunner<FEN>,
217 filter: &ProjectPathsAwareFilter,
218) -> FuzzMinimizeReplayPass {
219 let target_count = count_fuzz_minimize_targets(&runner, filter);
220 FuzzMinimizeReplayPass {
221 target_count,
222 replay: Box::new(move |filter, fuzz_minimize| {
223 replay_with_runner(&runner, filter, fuzz_minimize)
224 }),
225 }
226}
227
228fn count_fuzz_minimize_targets<FEN: FoundryEvmNetwork>(
229 runner: &MultiContractRunner<FEN>,
230 filter: &dyn TestFilter,
231) -> usize {
232 runner
233 .matching_contracts(filter)
234 .map(|(id, contract)| {
235 let contract_name = id.identifier();
236 let fuzz_targets = contract
237 .abi
238 .functions()
239 .filter(|func| func.is_fuzz_test())
240 .filter(|func| filter.matches_test_function_in_contract(&contract_name, func))
241 .filter(|func| {
242 function_matches_network_pass(
243 &runner.tcfg.multi_network.all_override_networks,
244 runner.tcfg.multi_network.pass_network.as_ref(),
245 runner.tcfg.inline_config.network_for(
246 &runner.tcfg.config.profile,
247 &contract_name,
248 &func.name,
249 ),
250 )
251 })
252 .count();
253 let invariant_targets = count_runnable_invariant_campaign_anchors(
254 &contract.abi,
255 filter,
256 crate::runner::InvariantCampaignScope {
257 config: &runner.tcfg.config,
258 inline_config: &runner.tcfg.inline_config,
259 contract_name: &contract_name,
260 all_override_networks: &runner.tcfg.multi_network.all_override_networks,
261 pass_network: runner.tcfg.multi_network.pass_network.as_ref(),
262 },
263 );
264 fuzz_targets + invariant_targets
265 })
266 .sum()
267}
268
269#[derive(Clone, Copy)]
270enum NetworkDispatchKind {
271 Tempo,
272 #[cfg(feature = "optimism")]
273 Optimism,
274 Eth,
275}
276
277const fn network_dispatch_kind(evm_opts: &EvmOpts) -> NetworkDispatchKind {
278 if evm_opts.networks.is_tempo() {
279 return NetworkDispatchKind::Tempo;
280 }
281
282 #[cfg(feature = "optimism")]
283 if evm_opts.networks.is_optimism() {
284 return NetworkDispatchKind::Optimism;
285 }
286
287 NetworkDispatchKind::Eth
288}
289
290#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, ValueEnum)]
292pub enum EvmProfileFormat {
293 #[default]
295 Speedscope,
296}
297
298#[derive(Clone, Copy, Debug, PartialEq, Eq)]
299enum TraceOutputKind {
300 Flamegraph,
301 Flamechart,
302 EvmProfile(EvmProfileFormat),
303}
304
305impl TraceOutputKind {
306 const fn label(self) -> &'static str {
307 match self {
308 Self::Flamegraph => "flamegraph",
309 Self::Flamechart => "flamechart",
310 Self::EvmProfile(_) => "EVM profile",
311 }
312 }
313}
314
315#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, ValueEnum)]
317#[clap(rename_all = "lowercase")]
318pub enum ShowmapDomainArg {
319 #[default]
320 Evm,
321 Sancov,
322 Both,
323}
324
325impl From<ShowmapDomainArg> for ShowmapDomain {
326 fn from(d: ShowmapDomainArg) -> Self {
327 match d {
328 ShowmapDomainArg::Evm => Self::Evm,
329 ShowmapDomainArg::Sancov => Self::Sancov,
330 ShowmapDomainArg::Both => Self::Both,
331 }
332 }
333}
334
335#[derive(Clone, Debug)]
336pub(crate) struct TestExecutionOptions {
337 pub(crate) coverage: bool,
338 pub(crate) should_debug: bool,
339 pub(crate) decode_internal: InternalTraceMode,
340 pub(crate) multi_network: MultiNetworkConfig,
341 pub(crate) replay_symbolic_artifact: Option<SymbolicArtifactReplayConfig>,
342 pub(crate) inline_config: Arc<InlineConfig>,
343 pub(crate) selected_sources: BTreeSet<PathBuf>,
344}
345
346impl TestExecutionOptions {
347 pub(crate) fn default_run(inline_config: Arc<InlineConfig>) -> Self {
348 Self {
349 coverage: false,
350 should_debug: false,
351 decode_internal: InternalTraceMode::None,
352 multi_network: MultiNetworkConfig::default(),
353 replay_symbolic_artifact: None,
354 inline_config,
355 selected_sources: BTreeSet::new(),
356 }
357 }
358
359 pub(crate) fn coverage(inline_config: Arc<InlineConfig>) -> Self {
360 Self { coverage: true, ..Self::default_run(inline_config) }
361 }
362}
363
364#[derive(Clone)]
365struct FuzzMinimizeNetworkPassOptions {
366 inline_config: Arc<InlineConfig>,
367 multi_network: MultiNetworkConfig,
368}
369
370struct CompiledTestProject {
371 project_root: PathBuf,
372 config: Config,
373 evm_opts: EvmOpts,
374 output: ProjectCompileOutput,
375 filter: ProjectPathsAwareFilter,
376 inline_config: Arc<InlineConfig>,
377 replay_symbolic_artifact: Option<SymbolicArtifactReplayConfig>,
378 selected_sources: BTreeSet<PathBuf>,
379}
380
381fn sources_to_compile_from_artifacts(
382 config: &Config,
383 test_filter: &ProjectPathsAwareFilter,
384 artifacts: &ProjectCompileOutput,
385 test_matcher: &TestFunctionMatcher<'_>,
386) -> BTreeSet<PathBuf> {
387 let paths = config.project_paths::<MultiCompilerLanguage>();
388 let empty_filter = EmptyTestFilter::default();
389 let filter_args = test_filter.args();
390 let has_contract_or_test_filter = filter_args.test_pattern.is_some()
391 || filter_args.test_pattern_inverse.is_some()
392 || filter_args.contract_pattern.is_some()
393 || filter_args.contract_pattern_inverse.is_some();
394
395 artifacts
400 .artifact_ids()
401 .filter_map(|(id, artifact)| artifact.abi.as_ref().map(|abi| (id, abi)))
402 .filter(|(id, abi)| {
403 if id.source.starts_with(&paths.sources) {
404 return true;
405 }
406 if paths.is_script(&id.source) && !paths.is_test(&id.source) {
407 return false;
408 }
409 let stripped = id.clone().with_stripped_file_prefixes(&config.root);
410 if stripped.source.is_sol_test() {
414 return if has_contract_or_test_filter {
415 test_matcher.matches_contract(test_filter, &stripped, abi)
416 } else {
417 test_filter.matches_path(&stripped.source)
418 };
419 }
420 !test_matcher.matches_contract(&empty_filter, &stripped, abi)
421 || test_matcher.matches_contract(test_filter, &stripped, abi)
422 })
423 .map(|(id, _)| id.source)
424 .collect()
425}
426
427#[derive(Clone, Debug, Parser)]
429#[command(next_help_heading = "Campaign options")]
430pub struct CampaignArgs {
431 #[arg(long, value_name = "RUNS")]
433 pub runs: Option<u64>,
434
435 #[arg(long, value_name = "TIMEOUT")]
437 pub timeout: Option<u32>,
438
439 #[arg(long)]
441 pub seed: Option<U256>,
442
443 #[arg(long, value_name = "DEPTH")]
445 pub depth: Option<u32>,
446
447 #[arg(long, value_name = "DEPTH")]
449 pub min_depth: Option<u32>,
450
451 #[arg(long, value_name = "fixed|random")]
453 pub depth_mode: Option<InvariantDepthMode>,
454
455 #[arg(long, value_name = "WORKERS")]
457 pub workers: Option<InvariantWorkers>,
458
459 #[arg(long, value_name = "PATH", value_hint = ValueHint::DirPath)]
461 pub corpus_dir: Option<PathBuf>,
462
463 #[arg(long, value_name = "PERCENT")]
465 pub dictionary_weight: Option<u32>,
466
467 #[arg(long, value_name = "N|max")]
469 pub dictionary_addresses: Option<String>,
470
471 #[arg(long, value_name = "N|max")]
473 pub dictionary_values: Option<String>,
474
475 #[arg(long, value_name = "N|max")]
477 pub dictionary_literals: Option<String>,
478
479 #[arg(long, value_name = "PERCENT")]
482 pub corpus_random_sequence_weight: Option<u32>,
483
484 #[arg(long, value_name = "PERCENT")]
486 pub payable_value_weight: Option<u32>,
487
488 #[arg(long, value_name = "WEIGHT")]
490 pub mutation_weight_splice: Option<u32>,
491
492 #[arg(long, value_name = "WEIGHT")]
494 pub mutation_weight_repeat: Option<u32>,
495
496 #[arg(long, value_name = "WEIGHT")]
498 pub mutation_weight_interleave: Option<u32>,
499
500 #[arg(long, value_name = "WEIGHT")]
502 pub mutation_weight_prefix: Option<u32>,
503
504 #[arg(long, value_name = "WEIGHT")]
506 pub mutation_weight_suffix: Option<u32>,
507
508 #[arg(long, value_name = "WEIGHT")]
510 pub mutation_weight_abi: Option<u32>,
511
512 #[arg(long, value_name = "WEIGHT")]
514 pub mutation_weight_cmp: Option<u32>,
515
516 #[arg(long, value_name = "PATH", value_hint = ValueHint::DirPath)]
518 pub frontier_dir: Option<PathBuf>,
519
520 #[arg(long, value_name = "COUNT")]
522 pub frontier_limit: Option<usize>,
523}
524
525#[derive(Clone, Copy, Debug, Default)]
526struct MatchedEngineCounts {
527 fuzz: usize,
528 invariant: usize,
529}
530
531#[derive(Clone, Debug, Default, Parser)]
533#[command(next_help_heading = "Test options")]
534pub struct TestArgs {
535 #[arg(skip)]
537 fuzz_only: FuzzOnlyMode,
538
539 #[arg(skip)]
541 pub(crate) showmap_override: Option<ShowmapConfig>,
542
543 #[arg(skip)]
545 pub(crate) fuzz_failure_replay: bool,
546
547 #[arg(skip)]
549 pub(crate) invariant_runs_override: Option<u64>,
550
551 #[arg(skip)]
553 pub(crate) invariant_timeout_override: Option<u32>,
554
555 #[command(flatten)]
557 pub global: GlobalArgs,
558
559 #[arg(value_hint = ValueHint::FilePath)]
561 pub path: Option<GlobMatcher>,
562
563 #[arg(long, conflicts_with_all = ["flamegraph", "flamechart", "evm_profile", "decode_internal", "rerun"])]
570 debug: bool,
571
572 #[arg(long = "debug-layout", requires = "debug", value_enum)]
574 debug_layout: Option<DebuggerLayout>,
575
576 #[arg(
581 long,
582 group = "trace_output",
583 conflicts_with_all = ["flamechart", "evm_profile", "json", "junit", "list"]
584 )]
585 flamegraph: bool,
586
587 #[arg(
592 long,
593 group = "trace_output",
594 conflicts_with_all = ["flamegraph", "evm_profile", "json", "junit", "list"]
595 )]
596 flamechart: bool,
597
598 #[arg(
604 long,
605 value_name = "FORMAT",
606 num_args = 0..=1,
607 default_missing_value = "speedscope",
608 value_enum,
609 group = "trace_output",
610 conflicts_with_all = ["flamegraph", "flamechart", "json", "junit", "list"]
611 )]
612 evm_profile: Option<EvmProfileFormat>,
613
614 #[arg(long, requires = "trace_output")]
618 no_open: bool,
619
620 #[command(flatten)]
621 tracing: TracingArgs,
622
623 #[arg(
625 long,
626 requires = "debug",
627 value_hint = ValueHint::FilePath,
628 value_name = "PATH"
629 )]
630 dump: Option<PathBuf>,
631
632 #[arg(long, env = "FORGE_GAS_REPORT")]
634 gas_report: bool,
635
636 #[arg(long, env = "FORGE_SNAPSHOT_CHECK")]
638 gas_snapshot_check: Option<bool>,
639
640 #[arg(long, env = "FORGE_SNAPSHOT_EMIT")]
642 gas_snapshot_emit: Option<bool>,
643
644 #[arg(long, env = "FORGE_ALLOW_FAILURE")]
646 allow_failure: bool,
647
648 #[arg(long, short, env = "FORGE_SUPPRESS_SUCCESSFUL_TRACES", help_heading = "Trace options")]
650 suppress_successful_traces: bool,
651
652 #[arg(
654 long,
655 value_name = "PATH",
656 value_hint = ValueHint::FilePath,
657 conflicts_with = "list",
658 help_heading = "Display options"
659 )]
660 json_file: Option<PathBuf>,
661
662 #[arg(long, conflicts_with_all = ["quiet", "json", "gas_report", "summary", "list", "show_progress"], help_heading = "Display options")]
664 pub junit: bool,
665
666 #[arg(long)]
668 pub fail_fast: bool,
669
670 #[arg(long, env = "ETHERSCAN_API_KEY", value_name = "KEY")]
672 etherscan_api_key: Option<String>,
673
674 #[arg(long, short, conflicts_with_all = ["show_progress", "decode_internal", "summary"], help_heading = "Display options")]
676 list: bool,
677
678 #[arg(long)]
680 pub fuzz_seed: Option<U256>,
681
682 #[arg(long, env = "FOUNDRY_FUZZ_RUNS", value_name = "RUNS")]
683 pub fuzz_runs: Option<u64>,
684
685 #[arg(long, env = "FOUNDRY_INVARIANT_WORKERS", value_name = "WORKERS")]
687 pub invariant_workers: Option<InvariantWorkers>,
688
689 #[arg(long, env = "FOUNDRY_FUZZ_RUN", value_name = "RUN")]
691 pub fuzz_run: Option<u32>,
692
693 #[arg(long, env = "FOUNDRY_FUZZ_WORKER", value_name = "WORKER", requires = "fuzz_run")]
695 pub fuzz_worker: Option<u32>,
696
697 #[arg(long, env = "FOUNDRY_FUZZ_TIMEOUT", value_name = "TIMEOUT")]
699 pub fuzz_timeout: Option<u64>,
700
701 #[arg(long, env = "FOUNDRY_FUZZ_DICTIONARY_WEIGHT", value_name = "PERCENT")]
703 pub fuzz_dictionary_weight: Option<u32>,
704
705 #[arg(long, env = "FOUNDRY_FUZZ_MAX_FUZZ_DICTIONARY_ADDRESSES", value_name = "N|max")]
707 pub fuzz_dictionary_addresses: Option<String>,
708
709 #[arg(long, env = "FOUNDRY_FUZZ_MAX_FUZZ_DICTIONARY_VALUES", value_name = "N|max")]
711 pub fuzz_dictionary_values: Option<String>,
712
713 #[arg(long, env = "FOUNDRY_FUZZ_MAX_FUZZ_DICTIONARY_LITERALS", value_name = "N|max")]
715 pub fuzz_dictionary_literals: Option<String>,
716
717 #[arg(long, env = "FOUNDRY_FUZZ_CORPUS_RANDOM_SEQUENCE_WEIGHT", value_name = "PERCENT")]
720 pub fuzz_corpus_random_sequence_weight: Option<u32>,
721
722 #[arg(long, env = "FOUNDRY_FUZZ_CORPUS_DIR", value_name = "PATH", value_hint = ValueHint::DirPath)]
724 pub fuzz_corpus_dir: Option<PathBuf>,
725
726 #[arg(long, env = "FOUNDRY_FUZZ_FRONTIER_DIR", value_name = "PATH", value_hint = ValueHint::DirPath)]
728 pub fuzz_frontier_dir: Option<PathBuf>,
729
730 #[arg(long, env = "FOUNDRY_FUZZ_FRONTIER_LIMIT", value_name = "COUNT")]
732 pub fuzz_frontier_limit: Option<usize>,
733
734 #[arg(long, env = "FOUNDRY_FUZZ_PAYABLE_VALUE_WEIGHT", value_name = "PERCENT")]
736 pub fuzz_payable_value_weight: Option<u32>,
737
738 #[arg(long, env = "FOUNDRY_FUZZ_MUTATION_WEIGHT_SPLICE", value_name = "WEIGHT")]
740 pub fuzz_mutation_weight_splice: Option<u32>,
741
742 #[arg(long, env = "FOUNDRY_FUZZ_MUTATION_WEIGHT_REPEAT", value_name = "WEIGHT")]
744 pub fuzz_mutation_weight_repeat: Option<u32>,
745
746 #[arg(long, env = "FOUNDRY_FUZZ_MUTATION_WEIGHT_INTERLEAVE", value_name = "WEIGHT")]
748 pub fuzz_mutation_weight_interleave: Option<u32>,
749
750 #[arg(long, env = "FOUNDRY_FUZZ_MUTATION_WEIGHT_PREFIX", value_name = "WEIGHT")]
752 pub fuzz_mutation_weight_prefix: Option<u32>,
753
754 #[arg(long, env = "FOUNDRY_FUZZ_MUTATION_WEIGHT_SUFFIX", value_name = "WEIGHT")]
756 pub fuzz_mutation_weight_suffix: Option<u32>,
757
758 #[arg(long, env = "FOUNDRY_FUZZ_MUTATION_WEIGHT_ABI", value_name = "WEIGHT")]
760 pub fuzz_mutation_weight_abi: Option<u32>,
761
762 #[arg(long, env = "FOUNDRY_FUZZ_MUTATION_WEIGHT_CMP", value_name = "WEIGHT")]
764 pub fuzz_mutation_weight_cmp: Option<u32>,
765
766 #[arg(long)]
768 pub fuzz_input_file: Option<String>,
769
770 #[arg(long, env = "FOUNDRY_INVARIANT_DEPTH", value_name = "DEPTH")]
772 pub invariant_depth: Option<u32>,
773
774 #[arg(long, env = "FOUNDRY_INVARIANT_MIN_DEPTH", value_name = "DEPTH")]
776 pub invariant_min_depth: Option<u32>,
777
778 #[arg(long, env = "FOUNDRY_INVARIANT_DEPTH_MODE", value_name = "fixed|random")]
780 pub invariant_depth_mode: Option<InvariantDepthMode>,
781
782 #[arg(long, env = "FOUNDRY_INVARIANT_DICTIONARY_WEIGHT", value_name = "PERCENT")]
784 pub invariant_dictionary_weight: Option<u32>,
785
786 #[arg(long, env = "FOUNDRY_INVARIANT_MAX_FUZZ_DICTIONARY_ADDRESSES", value_name = "N|max")]
788 pub invariant_dictionary_addresses: Option<String>,
789
790 #[arg(long, env = "FOUNDRY_INVARIANT_MAX_FUZZ_DICTIONARY_VALUES", value_name = "N|max")]
792 pub invariant_dictionary_values: Option<String>,
793
794 #[arg(long, env = "FOUNDRY_INVARIANT_MAX_FUZZ_DICTIONARY_LITERALS", value_name = "N|max")]
796 pub invariant_dictionary_literals: Option<String>,
797
798 #[arg(long, env = "FOUNDRY_INVARIANT_CORPUS_RANDOM_SEQUENCE_WEIGHT", value_name = "PERCENT")]
801 pub invariant_corpus_random_sequence_weight: Option<u32>,
802
803 #[arg(long, env = "FOUNDRY_INVARIANT_CORPUS_DIR", value_name = "PATH", value_hint = ValueHint::DirPath)]
805 pub invariant_corpus_dir: Option<PathBuf>,
806
807 #[arg(long, env = "FOUNDRY_INVARIANT_PAYABLE_VALUE_WEIGHT", value_name = "PERCENT")]
809 pub invariant_payable_value_weight: Option<u32>,
810
811 #[arg(long, env = "FOUNDRY_INVARIANT_MUTATION_WEIGHT_SPLICE", value_name = "WEIGHT")]
813 pub invariant_mutation_weight_splice: Option<u32>,
814
815 #[arg(long, env = "FOUNDRY_INVARIANT_MUTATION_WEIGHT_REPEAT", value_name = "WEIGHT")]
817 pub invariant_mutation_weight_repeat: Option<u32>,
818
819 #[arg(long, env = "FOUNDRY_INVARIANT_MUTATION_WEIGHT_INTERLEAVE", value_name = "WEIGHT")]
821 pub invariant_mutation_weight_interleave: Option<u32>,
822
823 #[arg(long, env = "FOUNDRY_INVARIANT_MUTATION_WEIGHT_PREFIX", value_name = "WEIGHT")]
825 pub invariant_mutation_weight_prefix: Option<u32>,
826
827 #[arg(long, env = "FOUNDRY_INVARIANT_MUTATION_WEIGHT_SUFFIX", value_name = "WEIGHT")]
829 pub invariant_mutation_weight_suffix: Option<u32>,
830
831 #[arg(long, env = "FOUNDRY_INVARIANT_MUTATION_WEIGHT_ABI", value_name = "WEIGHT")]
833 pub invariant_mutation_weight_abi: Option<u32>,
834
835 #[arg(long, env = "FOUNDRY_INVARIANT_MUTATION_WEIGHT_CMP", value_name = "WEIGHT")]
837 pub invariant_mutation_weight_cmp: Option<u32>,
838
839 #[arg(long, env = "FOUNDRY_SYMBOLIC")]
841 pub symbolic: bool,
842
843 #[arg(
845 long,
846 value_name = "PATH",
847 value_hint = ValueHint::FilePath,
848 conflicts_with_all = [
849 "debug",
850 "flamegraph",
851 "flamechart",
852 "rerun",
853 "fuzz_input_file",
854 "showmap_out",
855 "path",
856 "test_pattern",
857 "test_pattern_inverse",
858 "contract_pattern",
859 "contract_pattern_inverse",
860 "path_pattern",
861 "no-match-path",
862 ],
863 )]
864 pub replay_symbolic_artifact: Option<PathBuf>,
865
866 #[arg(long, env = "FOUNDRY_SYMBOLIC_EMIT_REGRESSION")]
868 pub emit_regression: bool,
869
870 #[arg(
872 long,
873 env = "FOUNDRY_SYMBOLIC_REGRESSION_OUT",
874 value_name = "PATH",
875 value_hint = ValueHint::AnyPath,
876 requires = "emit_regression"
877 )]
878 pub regression_out: Option<PathBuf>,
879
880 #[arg(long, env = "FOUNDRY_SYMBOLIC_REGRESSION_OVERWRITE", requires = "emit_regression")]
882 pub regression_overwrite: bool,
883
884 #[arg(long, env = "FOUNDRY_SYMBOLIC_SEED_CORPUS")]
886 pub symbolic_seed_corpus: bool,
887
888 #[arg(long, env = "FOUNDRY_SYMBOLIC_USE_FUZZ_CORPUS")]
890 pub symbolic_use_fuzz_corpus: bool,
891
892 #[arg(long, env = "FOUNDRY_SYMBOLIC_CORPUS_SEED_LIMIT", value_name = "COUNT")]
894 pub symbolic_corpus_seed_limit: Option<usize>,
895
896 #[arg(long, env = "FOUNDRY_SYMBOLIC_USE_FUZZ_FRONTIERS")]
898 pub symbolic_use_fuzz_frontiers: bool,
899
900 #[arg(long, env = "FOUNDRY_SYMBOLIC_FRONTIER_LIMIT", value_name = "COUNT")]
902 pub symbolic_frontier_limit: Option<usize>,
903
904 #[arg(long, env = "FOUNDRY_SYMBOLIC_FRONTIER_IDS", value_name = "IDS", value_delimiter = ',')]
906 pub symbolic_frontier_ids: Option<Vec<u64>>,
907
908 #[arg(long, env = "FOUNDRY_SYMBOLIC_FRONTIER_PCS", value_name = "PCS", value_delimiter = ',')]
910 pub symbolic_frontier_pcs: Option<Vec<usize>>,
911
912 #[arg(
914 long,
915 env = "FOUNDRY_SYMBOLIC_FRONTIER_SELECTORS",
916 value_name = "SELECTORS",
917 value_delimiter = ','
918 )]
919 pub symbolic_frontier_selectors: Option<Vec<String>>,
920
921 #[arg(long, env = "FOUNDRY_SYMBOLIC_SOLVER", value_name = "PATH_OR_NAME")]
923 pub symbolic_solver: Option<String>,
924
925 #[arg(long, env = "FOUNDRY_SYMBOLIC_SOLVER_COMMAND", value_name = "COMMAND")]
927 pub symbolic_solver_command: Option<String>,
928
929 #[arg(
931 long,
932 env = "FOUNDRY_SYMBOLIC_SOLVER_PORTFOLIO",
933 value_delimiter = ',',
934 value_name = "SOLVER_OR_COMMAND,..."
935 )]
936 pub symbolic_solver_portfolio: Option<Vec<String>>,
937
938 #[arg(long, env = "FOUNDRY_SYMBOLIC_TIMEOUT", value_name = "SECONDS")]
940 pub symbolic_timeout: Option<u32>,
941
942 #[arg(long, env = "FOUNDRY_SYMBOLIC_LOOP", value_name = "N")]
944 pub symbolic_loop: Option<u32>,
945
946 #[arg(long, env = "FOUNDRY_SYMBOLIC_DEPTH", value_name = "N")]
948 pub symbolic_depth: Option<u32>,
949
950 #[arg(long, env = "FOUNDRY_SYMBOLIC_WIDTH", value_name = "N")]
952 pub symbolic_width: Option<u32>,
953
954 #[arg(long, env = "FOUNDRY_SYMBOLIC_MAX_DEPTH", value_name = "N")]
956 pub symbolic_max_depth: Option<u32>,
957
958 #[arg(long, env = "FOUNDRY_SYMBOLIC_MAX_PATHS", value_name = "N")]
960 pub symbolic_max_paths: Option<u32>,
961
962 #[arg(long, env = "FOUNDRY_SYMBOLIC_INVARIANT_DEPTH", value_name = "N")]
964 pub symbolic_invariant_depth: Option<u32>,
965
966 #[arg(long, env = "FOUNDRY_SYMBOLIC_MAX_SOLVER_QUERIES", value_name = "N")]
968 pub symbolic_max_solver_queries: Option<u32>,
969
970 #[arg(long, env = "FOUNDRY_SYMBOLIC_DEFAULT_DYNAMIC_LENGTH", value_name = "N")]
972 pub symbolic_default_dynamic_length: Option<u32>,
973
974 #[arg(long, env = "FOUNDRY_SYMBOLIC_MAX_DYNAMIC_LENGTH", value_name = "N")]
976 pub symbolic_max_dynamic_length: Option<u32>,
977
978 #[arg(
980 long,
981 env = "FOUNDRY_SYMBOLIC_ARRAY_LENGTHS",
982 value_delimiter = ',',
983 value_name = "N,..."
984 )]
985 pub symbolic_array_lengths: Option<Vec<u32>>,
986
987 #[arg(long, env = "FOUNDRY_SYMBOLIC_MAX_CALLDATA_BYTES", value_name = "N")]
989 pub symbolic_max_calldata_bytes: Option<u32>,
990
991 #[arg(long, env = "FOUNDRY_SYMBOLIC_CALL_TARGETS")]
993 pub symbolic_call_targets: bool,
994
995 #[arg(long, env = "FOUNDRY_SYMBOLIC_DUMP_SMT")]
997 pub symbolic_dump_smt: bool,
998
999 #[arg(
1001 long,
1002 env = "FOUNDRY_SYMBOLIC_STORAGE_LAYOUT",
1003 value_name = "solidity|generic",
1004 value_parser = ["solidity", "generic"]
1005 )]
1006 pub symbolic_storage_layout: Option<String>,
1007
1008 #[arg(long, conflicts_with_all = ["quiet", "json"], help_heading = "Display options")]
1010 pub show_progress: bool,
1011
1012 #[arg(long)]
1015 pub rerun: bool,
1016
1017 #[arg(long, value_parser = parse_opcode, value_delimiter(','), conflicts_with_all = ["json", "junit", "list", "debug"])]
1024 pub opcodes: Vec<OpCode>,
1025
1026 #[arg(long, help_heading = "Display options")]
1028 pub summary: bool,
1029
1030 #[arg(long, help_heading = "Display options", requires = "summary")]
1032 pub detailed: bool,
1033
1034 #[arg(
1038 long,
1039 value_name = "DIR",
1040 value_hint = ValueHint::DirPath,
1041 help_heading = "Showmap replay",
1042 conflicts_with_all = ["debug", "flamegraph", "flamechart", "evm_profile", "rerun", "fuzz_input_file", "gas_report"],
1043 )]
1044 pub showmap_out: Option<PathBuf>,
1045
1046 #[arg(long, help_heading = "Showmap replay", requires = "showmap_out")]
1048 pub showmap_per_input: bool,
1049
1050 #[arg(
1052 long,
1053 value_enum,
1054 default_value_t = ShowmapDomainArg::Evm,
1055 help_heading = "Showmap replay",
1056 requires = "showmap_out",
1057 )]
1058 pub showmap_domain: ShowmapDomainArg,
1059
1060 #[arg(
1062 long,
1063 default_value = "replay",
1064 help_heading = "Showmap replay",
1065 requires = "showmap_out"
1066 )]
1067 pub showmap_approach: String,
1068
1069 #[arg(long, help_heading = "Showmap replay", requires = "showmap_out")]
1072 pub showmap_trial: Option<String>,
1073
1074 #[arg(
1077 long,
1078 value_name = "PATH",
1079 value_hint = ValueHint::DirPath,
1080 help_heading = "Showmap replay",
1081 requires = "showmap_out",
1082 )]
1083 pub showmap_corpus_dir: Option<PathBuf>,
1084
1085 #[command(flatten)]
1086 filter: FilterArgs,
1087
1088 #[command(flatten)]
1089 evm: EvmArgs,
1090
1091 #[command(flatten)]
1092 pub build: BuildOpts,
1093
1094 #[command(flatten)]
1095 pub watch: WatchArgs,
1096
1097 #[arg(long, num_args(0..), value_name = "PATH")]
1100 pub mutate: Option<Vec<PathBuf>>,
1101
1102 #[arg(long, value_name = "PATTERN", requires = "mutate", conflicts_with = "mutate_contract")]
1107 pub mutate_path: Option<GlobMatcher>,
1108
1109 #[arg(long, value_name = "REGEX", requires = "mutate")]
1113 pub mutate_contract: Option<regex::Regex>,
1114
1115 #[arg(long, value_name = "JOBS", requires = "mutate")]
1118 pub mutation_jobs: Option<usize>,
1119
1120 #[arg(long, value_name = "TIMEOUT", requires = "mutate")]
1126 pub mutation_timeout: Option<u32>,
1127
1128 #[arg(long, value_name = "RUNS", requires = "mutate")]
1130 pub mutation_optimizer_runs: Option<u32>,
1131
1132 #[arg(long, default_missing_value = "true", num_args = 0..=1, requires = "mutate")]
1134 pub mutation_via_ir: Option<bool>,
1135
1136 #[arg(long, conflicts_with_all = ["mutate", "replay_symbolic_artifact"])]
1153 pub brutalize: bool,
1154}
1155
1156impl TestArgs {
1157 pub async fn run(mut self) -> Result<TestOutcome> {
1158 trace!(target: "forge::test", "executing test command");
1159 self.compile_and_run().await
1160 }
1161
1162 pub(crate) fn ensure_mutation_mode_compatible(&self, coverage: bool) -> Result<()> {
1163 if self.mutate.is_none() {
1164 return Ok(());
1165 }
1166
1167 let mut conflicts = Vec::new();
1171 if self.list {
1172 conflicts.push("--list");
1173 }
1174 if self.debug {
1175 conflicts.push("--debug");
1176 }
1177 if self.flamegraph {
1178 conflicts.push("--flamegraph");
1179 }
1180 if self.flamechart {
1181 conflicts.push("--flamechart");
1182 }
1183 if self.evm_profile.is_some() {
1184 conflicts.push("--evm-profile");
1185 }
1186 if self.junit {
1187 conflicts.push("--junit");
1188 }
1189 if self.json_file.is_some() {
1190 conflicts.push("--json-file");
1191 }
1192 if coverage {
1193 conflicts.push("coverage");
1194 }
1195 if self.showmap_out.is_some() {
1196 conflicts.push("--showmap-out");
1197 }
1198 if self.replay_symbolic_artifact.is_some() {
1199 conflicts.push("--replay-symbolic-artifact");
1200 }
1201 if !conflicts.is_empty() {
1202 bail!(
1203 "`--mutate` cannot be combined with: {}. Re-run without those flags to use \
1204 mutation testing.",
1205 conflicts.join(", ")
1206 );
1207 }
1208
1209 Ok(())
1210 }
1211
1212 pub(crate) fn ensure_coverage_mode_compatible(&self) -> Result<()> {
1213 self.ensure_mutation_mode_compatible(true)?;
1214
1215 let mut conflicts = Vec::new();
1216 if shell::is_json() {
1217 conflicts.push("--json");
1218 }
1219 if self.junit {
1220 conflicts.push("--junit");
1221 }
1222 if self.json_file.is_some() {
1223 conflicts.push("--json-file");
1224 }
1225 if self.list {
1226 conflicts.push("--list");
1227 }
1228 if self.debug {
1229 conflicts.push("--debug");
1230 }
1231 if self.flamegraph {
1232 conflicts.push("--flamegraph");
1233 }
1234 if self.flamechart {
1235 conflicts.push("--flamechart");
1236 }
1237 if self.evm_profile.is_some() {
1238 conflicts.push("--evm-profile");
1239 }
1240 if self.showmap_out.is_some() {
1241 conflicts.push("--showmap-out");
1242 }
1243 if self.brutalize {
1244 conflicts.push("--brutalize");
1245 }
1246 if self.replay_symbolic_artifact.is_some() {
1247 conflicts.push("--replay-symbolic-artifact");
1248 }
1249 if !conflicts.is_empty() {
1250 bail!(
1251 "`forge coverage` cannot be combined with: {}. Use `--report lcov` for an \
1252 interoperable coverage report or `--report attribution` for per-test JSON \
1253 attribution.",
1254 conflicts.join(", ")
1255 );
1256 }
1257
1258 Ok(())
1259 }
1260
1261 fn showmap_config(&self) -> Result<Option<ShowmapConfig>> {
1263 if let Some(showmap) = self.showmap_override.clone() {
1264 validate_showmap_config(&showmap)?;
1265 return Ok(Some(showmap));
1266 }
1267
1268 let trial = self.showmap_trial.clone().unwrap_or_else(|| {
1271 let ns = std::time::SystemTime::now()
1272 .duration_since(std::time::UNIX_EPOCH)
1273 .map(|d| d.as_nanos())
1274 .unwrap_or(0);
1275 format!("trial-{ns}")
1276 });
1277 let Some(out_dir) = self.showmap_out.clone() else { return Ok(None) };
1278 let showmap = ShowmapConfig {
1279 out_dir,
1280 approach: self.showmap_approach.clone(),
1281 trial,
1282 per_input: self.showmap_per_input,
1283 domain: self.showmap_domain.into(),
1284 corpus_dir: self.showmap_corpus_dir.clone(),
1285 emit_files: true,
1286 };
1287 validate_showmap_config(&showmap)?;
1288 Ok(Some(showmap))
1289 }
1290
1291 pub(crate) const fn enable_fuzz_only(&mut self) {
1293 self.fuzz_only = FuzzOnlyMode::Enabled;
1294 }
1295
1296 pub(crate) const fn enable_fuzz_only_with_auto_fuzz_corpus(&mut self) {
1299 self.fuzz_only = FuzzOnlyMode::WithAutoFuzzCorpus;
1300 }
1301
1302 fn apply_auto_fuzz_corpus_dir(&self, config: &mut Config) {
1303 if !self.fuzz_only.uses_auto_fuzz_corpus() {
1304 return;
1305 }
1306
1307 if config.fuzz.corpus.corpus_dir.is_none() {
1308 config.fuzz.corpus.corpus_dir = Some(match &config.fuzz.failure_persist_dir {
1309 Some(root) => root.join(AUTO_CORPUS_DIR),
1310 None => config.cache_path.join(AUTO_FUZZ_FAILURE_DIR).join(AUTO_CORPUS_DIR),
1311 });
1312 }
1313 }
1314
1315 pub(crate) fn set_showmap_override(&mut self, showmap: ShowmapConfig) {
1318 self.showmap_override = Some(showmap);
1319 }
1320
1321 pub(crate) fn set_fuzz_minimize_replay_options(
1323 &mut self,
1324 global: GlobalArgs,
1325 evm: EvmArgs,
1326 build: BuildOpts,
1327 filter: FilterArgs,
1328 ) {
1329 self.global = global;
1330 self.evm = evm;
1331 self.build = build;
1332 self.filter = filter;
1333 }
1334
1335 pub(crate) const fn enable_fuzz_failure_replay(&mut self) {
1337 self.fuzz_failure_replay = true;
1338 }
1339
1340 fn warn_unsupported_engine_flags(
1341 &self,
1342 output: &ProjectCompileOutput,
1343 config: &Config,
1344 inline_config: &InlineConfig,
1345 filter: &ProjectPathsAwareFilter,
1346 multi_network: &MultiNetworkConfig,
1347 ) -> Result<()> {
1348 if !self.fuzz_only.is_enabled() {
1349 return Ok(());
1350 }
1351 let counts = matched_engine_counts(output, config, inline_config, filter, multi_network);
1352 if counts.fuzz == 0 && counts.invariant == 0 {
1353 return Ok(());
1354 }
1355
1356 if counts.fuzz == 0 && counts.invariant > 0 {
1357 if self.fuzz_frontier_dir.is_some() {
1358 sh_warn!(
1359 "`--frontier-dir` only applies to fuzz tests; no matched fuzz tests were found."
1360 )?;
1361 }
1362 if self.fuzz_frontier_limit.is_some() {
1363 sh_warn!(
1364 "`--frontier-limit` only applies to fuzz tests; no matched fuzz tests were found."
1365 )?;
1366 }
1367 if self.fuzz_run.is_some() {
1368 sh_warn!(
1369 "`--fuzz-run` only applies to fuzz tests; no matched fuzz tests were found."
1370 )?;
1371 }
1372 if self.fuzz_input_file.is_some() {
1373 sh_warn!(
1374 "`--fuzz-input-file` only applies to fuzz tests; no matched fuzz tests were found."
1375 )?;
1376 }
1377 }
1378
1379 if counts.invariant == 0 && counts.fuzz > 0 {
1380 if self.invariant_depth.is_some() {
1381 sh_warn!(
1382 "`--depth` only applies to invariant tests; no matched invariant tests were found."
1383 )?;
1384 }
1385 if self.invariant_min_depth.is_some() {
1386 sh_warn!(
1387 "`--min-depth` only applies to invariant tests; no matched invariant tests were found."
1388 )?;
1389 }
1390 if self.invariant_depth_mode.is_some() {
1391 sh_warn!(
1392 "`--depth-mode` only applies to invariant tests; no matched invariant tests were found."
1393 )?;
1394 }
1395 if self.invariant_workers.is_some() {
1396 sh_warn!(
1397 "`--workers` only applies to invariant tests; no matched invariant tests were found."
1398 )?;
1399 }
1400 }
1401
1402 Ok(())
1403 }
1404
1405 pub(crate) fn from_fuzz_run(args: FuzzRunArgs) -> Self {
1407 let mut test = Self {
1408 fuzz_only: FuzzOnlyMode::Enabled,
1409 global: args.global,
1410 path: args.path,
1411 gas_report: args.gas_report,
1412 allow_failure: args.allow_failure,
1413 junit: args.junit,
1414 fail_fast: args.fail_fast,
1415 etherscan_api_key: args.etherscan_api_key,
1416 list: args.list,
1417 fuzz_input_file: args.fuzz_input_file,
1418 show_progress: args.show_progress,
1419 rerun: args.rerun,
1420 showmap_out: args.showmap_out,
1421 showmap_per_input: args.showmap_per_input,
1422 showmap_domain: args.showmap_domain,
1423 showmap_approach: args.showmap_approach,
1424 showmap_trial: args.showmap_trial,
1425 showmap_corpus_dir: args.showmap_corpus_dir,
1426 filter: args.filter,
1427 evm: args.evm,
1428 build: args.build,
1429 ..Self::default()
1430 };
1431 test.apply_fuzz_run_campaign(args.campaign);
1432 test
1433 }
1434
1435 fn apply_fuzz_run_campaign(&mut self, campaign: CampaignArgs) {
1436 self.fuzz_seed = campaign.seed;
1437 self.fuzz_runs = campaign.runs;
1438 self.invariant_runs_override = campaign.runs;
1439
1440 self.fuzz_timeout = campaign.timeout.map(u64::from);
1441 self.invariant_timeout_override = campaign.timeout;
1442
1443 self.fuzz_dictionary_weight = campaign.dictionary_weight;
1444 self.invariant_dictionary_weight = campaign.dictionary_weight;
1445 self.fuzz_dictionary_addresses = campaign.dictionary_addresses.clone();
1446 self.invariant_dictionary_addresses = campaign.dictionary_addresses;
1447 self.fuzz_dictionary_values = campaign.dictionary_values.clone();
1448 self.invariant_dictionary_values = campaign.dictionary_values;
1449 self.fuzz_dictionary_literals = campaign.dictionary_literals.clone();
1450 self.invariant_dictionary_literals = campaign.dictionary_literals;
1451
1452 self.fuzz_corpus_random_sequence_weight = campaign.corpus_random_sequence_weight;
1453 self.invariant_corpus_random_sequence_weight = campaign.corpus_random_sequence_weight;
1454 self.fuzz_corpus_dir = campaign.corpus_dir.clone();
1455 self.invariant_corpus_dir = campaign.corpus_dir;
1456
1457 self.fuzz_payable_value_weight = campaign.payable_value_weight;
1458 self.invariant_payable_value_weight = campaign.payable_value_weight;
1459 self.fuzz_mutation_weight_splice = campaign.mutation_weight_splice;
1460 self.invariant_mutation_weight_splice = campaign.mutation_weight_splice;
1461 self.fuzz_mutation_weight_repeat = campaign.mutation_weight_repeat;
1462 self.invariant_mutation_weight_repeat = campaign.mutation_weight_repeat;
1463 self.fuzz_mutation_weight_interleave = campaign.mutation_weight_interleave;
1464 self.invariant_mutation_weight_interleave = campaign.mutation_weight_interleave;
1465 self.fuzz_mutation_weight_prefix = campaign.mutation_weight_prefix;
1466 self.invariant_mutation_weight_prefix = campaign.mutation_weight_prefix;
1467 self.fuzz_mutation_weight_suffix = campaign.mutation_weight_suffix;
1468 self.invariant_mutation_weight_suffix = campaign.mutation_weight_suffix;
1469 self.fuzz_mutation_weight_abi = campaign.mutation_weight_abi;
1470 self.invariant_mutation_weight_abi = campaign.mutation_weight_abi;
1471 self.fuzz_mutation_weight_cmp = campaign.mutation_weight_cmp;
1472 self.invariant_mutation_weight_cmp = campaign.mutation_weight_cmp;
1473
1474 self.fuzz_frontier_dir = campaign.frontier_dir;
1475 self.fuzz_frontier_limit = campaign.frontier_limit;
1476 self.invariant_depth = campaign.depth;
1477 self.invariant_min_depth = campaign.min_depth;
1478 self.invariant_depth_mode = campaign.depth_mode;
1479 self.invariant_workers = campaign.workers;
1480 }
1481
1482 fn load_symbolic_artifact_replay(&self) -> Result<Option<SymbolicArtifactReplayConfig>> {
1483 let Some(path) = &self.replay_symbolic_artifact else {
1484 return Ok(None);
1485 };
1486
1487 if !self.filter.is_empty() || self.path.is_some() {
1488 bail!(
1489 "symbolic artifact mode cannot be combined with test selection filters; \
1490 the artifact selects its original target"
1491 );
1492 }
1493
1494 let value = foundry_common::fs::read_json_file::<serde_json::Value>(path).wrap_err(
1495 format!("failed to read symbolic counterexample artifact {}", path.display()),
1496 )?;
1497 let schema_version =
1498 value.get("schema_version").and_then(serde_json::Value::as_u64).ok_or_else(|| {
1499 eyre::eyre!(
1500 "symbolic counterexample artifact {} is missing numeric schema_version",
1501 path.display()
1502 )
1503 })?;
1504 if schema_version != 1 {
1505 bail!(
1506 "unsupported symbolic counterexample artifact schema version {} in {}",
1507 schema_version,
1508 path.display()
1509 );
1510 }
1511 let schema = value.get("schema").and_then(serde_json::Value::as_str).ok_or_else(|| {
1512 eyre::eyre!(
1513 "symbolic counterexample artifact {} is missing string schema",
1514 path.display()
1515 )
1516 })?;
1517 if schema != SYMBOLIC_COUNTEREXAMPLE_ARTIFACT_SCHEMA {
1518 bail!(
1519 "unsupported symbolic counterexample artifact schema `{}` in {}",
1520 schema,
1521 path.display()
1522 );
1523 }
1524 let artifact = serde_json::from_value::<SymbolicCounterexampleArtifact>(value).wrap_err(
1525 format!("failed to parse symbolic counterexample artifact {}", path.display()),
1526 )?;
1527 if artifact.calls.is_empty() {
1528 bail!("symbolic counterexample artifact {} has no calls", path.display());
1529 }
1530 if artifact.replay.status != SymbolicReplayStatus::Confirmed {
1531 bail!(
1532 "symbolic counterexample artifact {} replay status must be confirmed, got {:?}",
1533 path.display(),
1534 artifact.replay.status,
1535 );
1536 }
1537 let Some((artifact_path, contract_name)) = artifact.test.contract.rsplit_once(':') else {
1538 bail!(
1539 "symbolic counterexample artifact {} test.contract must be `path:Contract`, got `{}`",
1540 path.display(),
1541 artifact.test.contract,
1542 );
1543 };
1544 if artifact_path.is_empty() || contract_name.is_empty() {
1545 bail!(
1546 "symbolic counterexample artifact {} test.contract must be `path:Contract`, got `{}`",
1547 path.display(),
1548 artifact.test.contract,
1549 );
1550 }
1551
1552 Ok(Some(SymbolicArtifactReplayConfig { artifact, path: path.clone() }))
1553 }
1554
1555 #[instrument(target = "forge::test", skip_all)]
1562 fn get_sources_to_compile(
1563 &self,
1564 config: &Config,
1565 test_filter: &ProjectPathsAwareFilter,
1566 inline_config: Option<Arc<InlineConfig>>,
1567 symbolic_artifact_replay: Option<&SymbolicArtifactReplayConfig>,
1568 ) -> Result<(BTreeSet<PathBuf>, Option<Arc<InlineConfig>>)> {
1569 if test_filter.is_empty() {
1572 return Ok((
1573 source_files_iter(&config.src, MultiCompilerLanguage::FILE_EXTENSIONS)
1574 .chain(source_files_iter(&config.test, MultiCompilerLanguage::FILE_EXTENSIONS))
1575 .collect(),
1576 None,
1577 ));
1578 }
1579
1580 let mut project = config.create_project(true, true)?;
1581 let sources = source_files_iter(&config.src, MultiCompilerLanguage::FILE_EXTENSIONS)
1582 .chain(
1583 source_files_iter(&config.test, MultiCompilerLanguage::FILE_EXTENSIONS)
1584 .filter(|path| !path.is_sol_test() || test_filter.matches_path(path)),
1587 )
1588 .collect::<BTreeSet<_>>();
1589 let output = compile_abi_project(
1590 &mut project,
1591 ProjectCompiler::new()
1592 .files(sources)
1593 .dynamic_test_linking(config.dynamic_test_linking)
1594 .quiet(true),
1595 )?;
1596 if output.has_compiler_errors() {
1597 sh_println!("{output}")?;
1598 eyre::bail!("Compilation failed");
1599 }
1600
1601 let inline_config = match inline_config {
1602 Some(inline_config) => inline_config,
1603 None => Arc::new(InlineConfig::new_parsed(&output, config)?),
1604 };
1605 let test_matcher =
1606 TestFunctionMatcher::new(config, &inline_config, symbolic_artifact_replay);
1607 let files = sources_to_compile_from_artifacts(config, test_filter, &output, &test_matcher);
1608
1609 Ok((files, Some(inline_config)))
1610 }
1611
1612 pub async fn compile_and_run(&mut self) -> Result<TestOutcome> {
1619 if self.brutalize {
1620 return self.compile_and_run_brutalized().await;
1621 }
1622
1623 self.ensure_mutation_mode_compatible(false)?;
1624
1625 let compiled = self.compile_project().await?;
1626 self.run_tests(
1627 &compiled.project_root,
1628 compiled.config,
1629 compiled.evm_opts,
1630 &compiled.output,
1631 &compiled.filter,
1632 TestExecutionOptions {
1633 replay_symbolic_artifact: compiled.replay_symbolic_artifact,
1634 selected_sources: compiled.selected_sources,
1635 ..TestExecutionOptions::default_run(compiled.inline_config)
1636 },
1637 )
1638 .await
1639 }
1640
1641 async fn compile_and_run_brutalized(&mut self) -> Result<TestOutcome> {
1643 let (mut config, evm_opts) = self.load_config_and_evm_opts()?;
1644
1645 if install::install_missing_dependencies(&mut config).await && config.auto_detect_remappings
1646 {
1647 config = self.load_config()?;
1648 }
1649
1650 let rerun_failures = self.rerun.then(|| last_run_failures(&config));
1651 let silent = shell::is_json();
1652 let temp_dir = TempDir::with_prefix("forge_brutalize_")?;
1653 let temp_path = temp_dir.path();
1654
1655 if config.via_ir && !silent {
1656 sh_warn!(
1657 "--brutalize value cast dirty-bits checks are ineffective with via-IR; memory and free-memory-pointer checks still apply"
1658 )?;
1659 }
1660
1661 if !silent {
1662 sh_status!("Brutalizing source files...")?;
1663 }
1664
1665 workspace::copy_project(&config, temp_path)?;
1666 let count = brutalizer::brutalize_project(&config, temp_path)?;
1667
1668 if !silent {
1669 sh_status!("Brutalized {count} source files, compiling from temp workspace...")?;
1670 }
1671
1672 let test_failures_file = config.test_failures_file.clone();
1673 let mut config = workspace::rebase_config_paths(&config, temp_path).sanitized();
1674 config.test_failures_file = test_failures_file;
1675 self.apply_auto_fuzz_corpus_dir(&mut config);
1676 let project = config.project()?;
1677 let project_root = project.paths.root.clone();
1678 let replay_symbolic_artifact = self.load_symbolic_artifact_replay()?;
1679 let filter = self.filter_with_rerun_failures(&config, rerun_failures)?;
1680
1681 let (files, inline_config) =
1682 self.get_sources_to_compile(&config, &filter, None, replay_symbolic_artifact.as_ref())?;
1683 let output = ProjectCompiler::new()
1684 .dynamic_test_linking(config.dynamic_test_linking)
1685 .quiet(shell::is_json() || self.junit)
1686 .files(files.clone())
1687 .compile(&project)?;
1688 let inline_config = match inline_config {
1689 Some(inline_config) => inline_config,
1690 None => Arc::new(InlineConfig::new_parsed(&output, &config)?),
1691 };
1692
1693 self.run_tests(
1694 &project_root,
1695 config,
1696 evm_opts,
1697 &output,
1698 &filter,
1699 TestExecutionOptions {
1700 replay_symbolic_artifact,
1701 selected_sources: files,
1702 ..TestExecutionOptions::default_run(inline_config)
1703 },
1704 )
1705 .await
1706 }
1707
1708 async fn compile_project(&mut self) -> Result<CompiledTestProject> {
1709 let (mut config, evm_opts) = self.load_config_and_evm_opts()?;
1711
1712 let should_mutate = self.mutate.is_some();
1713
1714 if install::install_missing_dependencies(&mut config).await && config.auto_detect_remappings
1715 {
1716 config = self.load_config()?;
1718 }
1719 if should_mutate {
1720 config.dynamic_test_linking = true;
1722 config.cache = true;
1723 apply_mutation_compiler_overrides(&mut config);
1724 }
1725
1726 self.apply_auto_fuzz_corpus_dir(&mut config);
1727
1728 let mut project = config.project()?;
1730 let project_root = project.paths.root.clone();
1731
1732 let replay_symbolic_artifact = self.load_symbolic_artifact_replay()?;
1733
1734 let mut filter = self.filter(&config)?;
1735 if let Some(replay) = &replay_symbolic_artifact {
1736 let filter_args = filter.args_mut();
1737 filter_args.test_pattern_inverse = None;
1738 filter_args.contract_pattern_inverse = None;
1739 filter_args.path_pattern_inverse = None;
1740 let (path, contract) = replay
1741 .artifact
1742 .test
1743 .contract
1744 .rsplit_once(':')
1745 .map_or(("", replay.artifact.test.contract.as_str()), |(path, contract)| {
1746 (path, contract)
1747 });
1748 filter_args.test_pattern =
1749 Some(Regex::new(&format!("^{}$", regex::escape(&replay.artifact.test.test)))?);
1750 filter_args.contract_pattern =
1751 Some(Regex::new(&format!("^{}$", regex::escape(contract)))?);
1752 if !path.is_empty() {
1753 filter_args.path_pattern = Some(globset::escape(path).parse::<GlobMatcher>()?);
1754 }
1755 }
1756 trace!(target: "forge::test", ?filter, "using filter");
1757
1758 let dynamic_test_linking = config.dynamic_test_linking;
1759 let quiet = shell::is_json() || self.junit;
1760
1761 if self.list {
1762 let output = compile_abi_project(
1763 &mut project,
1764 ProjectCompiler::new().dynamic_test_linking(dynamic_test_linking).quiet(quiet),
1765 )?;
1766 let inline_config = Arc::new(InlineConfig::new_parsed(&output, &config)?);
1767 return Ok(CompiledTestProject {
1768 project_root,
1769 config,
1770 evm_opts,
1771 output,
1772 filter,
1773 inline_config,
1774 replay_symbolic_artifact,
1775 selected_sources: BTreeSet::new(),
1776 });
1777 }
1778
1779 let compile = |files| {
1780 ProjectCompiler::new()
1781 .dynamic_test_linking(dynamic_test_linking)
1782 .quiet(quiet)
1783 .files(files)
1784 .compile(&project)
1785 };
1786
1787 let (selected_sources, inline_config) =
1788 self.get_sources_to_compile(&config, &filter, None, replay_symbolic_artifact.as_ref())?;
1789 let mut output = compile(selected_sources.clone());
1790 if should_mutate {
1791 output = output.wrap_err(
1792 "Mutation testing compiler profile failed to compile before applying mutations",
1793 );
1794 }
1795 let output = output?;
1796 let inline_config = match inline_config {
1797 Some(inline_config) => inline_config,
1798 None => Arc::new(InlineConfig::new_parsed(&output, &config)?),
1799 };
1800
1801 Ok(CompiledTestProject {
1802 project_root,
1803 config,
1804 evm_opts,
1805 output,
1806 filter,
1807 inline_config,
1808 replay_symbolic_artifact,
1809 selected_sources,
1810 })
1811 }
1812
1813 pub(crate) async fn prepare_fuzz_minimize_replay(
1814 &mut self,
1815 corpus_dir: &Path,
1816 ) -> Result<FuzzMinimizeReplaySession> {
1817 let compiled = self.compile_project().await?;
1818 let CompiledTestProject { mut config, mut evm_opts, output, filter, inline_config, .. } =
1819 compiled;
1820
1821 if config.fuzz.run == Some(0) {
1822 bail!("`fuzz.run` must be greater than 0");
1823 }
1824
1825 if self.gas_report {
1826 evm_opts.isolate = true;
1827 } else {
1828 config.fuzz.gas_report_samples = 0;
1829 config.invariant.gas_report_samples = 0;
1830 }
1831 if config.fuzz.corpus.corpus_dir.is_none() {
1832 config.fuzz.corpus.corpus_dir = Some(corpus_dir.to_path_buf());
1833 }
1834 if config.invariant.corpus.corpus_dir.is_none() {
1835 config.invariant.corpus.corpus_dir = Some(corpus_dir.to_path_buf());
1836 }
1837
1838 config.fuzz.seed = config.fuzz.seed.or(Some(U256::ZERO));
1839
1840 evm_opts.infer_network_from_fork().await;
1841
1842 let override_networks = inline_config.referenced_override_networks(&config.profile);
1843 let mut passes = Vec::new();
1844
1845 if override_networks.is_empty() {
1846 passes.push(
1847 self.dispatch_fuzz_minimize_network(
1848 &evm_opts,
1849 config,
1850 evm_opts.clone(),
1851 &output,
1852 FuzzMinimizeNetworkPassOptions {
1853 inline_config: inline_config.clone(),
1854 multi_network: MultiNetworkConfig::default(),
1855 },
1856 &filter,
1857 )
1858 .await?,
1859 );
1860 } else {
1861 let all_override_networks = override_networks.clone();
1862 passes.push(
1863 self.dispatch_fuzz_minimize_network(
1864 &evm_opts,
1865 config.clone(),
1866 evm_opts.clone(),
1867 &output,
1868 FuzzMinimizeNetworkPassOptions {
1869 inline_config: inline_config.clone(),
1870 multi_network: MultiNetworkConfig {
1871 all_override_networks: all_override_networks.clone(),
1872 pass_network: None,
1873 },
1874 },
1875 &filter,
1876 )
1877 .await?,
1878 );
1879
1880 for &network in &override_networks {
1881 let mut pass_evm_opts = evm_opts.clone();
1882 pass_evm_opts.networks = network.into();
1883 passes.push(
1884 self.dispatch_fuzz_minimize_network(
1885 &pass_evm_opts,
1886 config.clone(),
1887 pass_evm_opts.clone(),
1888 &output,
1889 FuzzMinimizeNetworkPassOptions {
1890 inline_config: inline_config.clone(),
1891 multi_network: MultiNetworkConfig {
1892 all_override_networks: all_override_networks.clone(),
1893 pass_network: Some(network),
1894 },
1895 },
1896 &filter,
1897 )
1898 .await?,
1899 );
1900 }
1901 }
1902
1903 if passes.iter().all(|pass| pass.target_count == 0) {
1904 bail!("fuzz minimization requires at least one matched fuzz or invariant test");
1905 }
1906
1907 Ok(FuzzMinimizeReplaySession { filter, passes })
1908 }
1909
1910 pub(crate) async fn run_tests(
1914 &mut self,
1915 project_root: &Path,
1916 mut config: Config,
1917 mut evm_opts: EvmOpts,
1918 output: &ProjectCompileOutput,
1919 filter: &ProjectPathsAwareFilter,
1920 mut execution: TestExecutionOptions,
1921 ) -> Result<TestOutcome> {
1922 self.ensure_mutation_mode_compatible(execution.coverage)?;
1923
1924 if config.fuzz.run == Some(0) {
1925 bail!("`fuzz.run` must be greater than 0");
1926 }
1927
1928 self.warn_unsupported_engine_flags(
1929 output,
1930 &config,
1931 &execution.inline_config,
1932 filter,
1933 &execution.multi_network,
1934 )?;
1935
1936 if self.list {
1937 return list_from_output(
1938 output,
1939 &config,
1940 &execution.inline_config,
1941 filter,
1942 self.fuzz_only.is_enabled(),
1943 execution.replay_symbolic_artifact.as_ref(),
1944 );
1945 }
1946
1947 let mut filter = filter.clone();
1948
1949 if self.gas_report {
1951 evm_opts.isolate = true;
1952 } else {
1953 config.fuzz.gas_report_samples = 0;
1955 config.invariant.gas_report_samples = 0;
1956 }
1957
1958 config.fuzz.seed = config
1960 .fuzz
1961 .seed
1962 .or_else(|| Some(U256::from_be_bytes(rand::rng().random::<[u8; 32]>())));
1963
1964 execution.should_debug = self.debug;
1966 let trace_output = if self.flamegraph {
1967 Some(TraceOutputKind::Flamegraph)
1968 } else if self.flamechart {
1969 Some(TraceOutputKind::Flamechart)
1970 } else {
1971 self.evm_profile.map(TraceOutputKind::EvmProfile)
1972 };
1973
1974 if evm_opts.verbosity < 3 && (self.gas_report || trace_output.is_some()) {
1976 evm_opts.verbosity = 3;
1977 }
1978
1979 config.tracing = self.tracing.resolve(&config.tracing, evm_opts.verbosity);
1981 let json_trace_depth = config.tracing.trace_depth;
1982 let decode_internal_enabled = config.tracing.decode_internal || trace_output.is_some();
1983
1984 let decode_internal = if decode_internal_enabled {
1986 InternalTraceMode::Simple
1989 } else {
1990 InternalTraceMode::None
1991 };
1992
1993 evm_opts.infer_network_from_fork().await;
1995
1996 let config_for_mutation = config.clone();
1998 let evm_opts_for_mutation = evm_opts.clone();
1999
2000 let override_networks =
2002 execution.inline_config.referenced_override_networks(&config.profile);
2003
2004 let (libraries, mut outcome) = if override_networks.is_empty() {
2005 execution.decode_internal = decode_internal;
2007 execution.multi_network = MultiNetworkConfig::default();
2008 self.dispatch_network(
2009 &evm_opts,
2010 config,
2011 evm_opts.clone(),
2012 output,
2013 &mut filter,
2014 execution.clone(),
2015 )
2016 .await?
2017 } else {
2018 let all_override_networks = override_networks.clone();
2020 let multi_pass_timer = Instant::now();
2021
2022 let (libraries, mut outcome) = self
2024 .dispatch_network(
2025 &evm_opts,
2026 config.clone(),
2027 evm_opts.clone(),
2028 output,
2029 &mut filter,
2030 TestExecutionOptions {
2031 decode_internal,
2032 multi_network: MultiNetworkConfig {
2033 all_override_networks: all_override_networks.clone(),
2034 pass_network: None,
2035 },
2036 ..execution.clone()
2037 },
2038 )
2039 .await?;
2040
2041 for &network in &override_networks {
2043 let mut pass_evm_opts = evm_opts.clone();
2044 pass_evm_opts.networks = network.into();
2045 let (_, pass_outcome) = self
2046 .dispatch_network(
2047 &pass_evm_opts,
2048 config.clone(),
2049 pass_evm_opts.clone(),
2050 output,
2051 &mut filter,
2052 TestExecutionOptions {
2053 decode_internal,
2054 multi_network: MultiNetworkConfig {
2055 all_override_networks: all_override_networks.clone(),
2056 pass_network: Some(network),
2057 },
2058 ..execution.clone()
2059 },
2060 )
2061 .await?;
2062 merge_outcomes(&mut outcome, pass_outcome);
2063 }
2064
2065 if !self.summary && !shell::is_json() {
2067 sh_println!("{}", outcome.summary(multi_pass_timer.elapsed()))?;
2068 }
2069 if self.summary && !outcome.results.is_empty() {
2070 let summary_report = TestSummaryReport::new(self.detailed, outcome.clone());
2071 sh_println!("{}", &summary_report)?;
2072 }
2073
2074 (libraries, outcome)
2075 };
2076
2077 if let Some(replay) = &execution.replay_symbolic_artifact {
2078 let replayed = outcome.tests().count();
2079 if replayed == 0 {
2080 bail!(
2081 "symbolic artifact target `{}::{}` was not found",
2082 replay.artifact.test.contract,
2083 replay.artifact.test.test
2084 );
2085 }
2086 if replayed > 1 {
2087 bail!(
2088 "symbolic artifact target `{}::{}` matched {} tests; replay requires exactly one target",
2089 replay.artifact.test.contract,
2090 replay.artifact.test.test,
2091 replayed
2092 );
2093 }
2094 }
2095
2096 if let Some(path) = &self.json_file {
2097 let mut results =
2098 outcome.json_file_results.take().unwrap_or_else(|| outcome.results.clone());
2099 prepare_results_for_json(&mut results, evm_opts.verbosity, json_trace_depth);
2100 fs::write_json_file(path, &results)?;
2101 }
2102
2103 if let Some(trace_output) = trace_output {
2104 enum RenderedTraceOutput {
2105 Flame {
2106 file_name: String,
2107 title: String,
2108 flame_chart: bool,
2109 folded_stack_trace: Vec<String>,
2110 },
2111 EvmProfile {
2112 profile_json: Vec<u8>,
2113 test_name: String,
2114 contract: String,
2115 },
2116 }
2117
2118 let rendered = {
2119 let output_label = trace_output.label();
2120 let no_tests = match trace_output {
2121 TraceOutputKind::EvmProfile(_) => {
2122 "cannot generate EVM profile: no tests were executed"
2123 }
2124 TraceOutputKind::Flamegraph | TraceOutputKind::Flamechart => {
2125 "no tests were executed"
2126 }
2127 };
2128 if !outcome.results.values().any(|suite| !suite.test_results.is_empty()) {
2129 return Err(eyre::eyre!("{no_tests}"));
2130 }
2131 let decoder = outcome.last_run_decoder.clone().ok_or_else(|| {
2132 eyre::eyre!("cannot generate {output_label}: missing trace decoder")
2133 })?;
2134 let (suite_name, test_name, test_result) = outcome
2135 .results
2136 .iter_mut()
2137 .find_map(|(suite_name, suite)| {
2138 suite.test_results.iter_mut().next().map(|(test_name, result)| {
2139 (suite_name.as_str(), test_name.as_str(), result)
2140 })
2141 })
2142 .ok_or_else(|| eyre::eyre!("{no_tests}"))?;
2143 let contract = suite_name.split(':').next_back().unwrap();
2144 let test_name_trimmed = test_name.trim_end_matches("()");
2145
2146 let (_, arena) = test_result
2147 .traces
2148 .iter_mut()
2149 .find(|(kind, _)| *kind == TraceKind::Execution)
2150 .ok_or_else(|| {
2151 eyre::eyre!(
2152 "cannot generate {output_label} for {contract}::{test_name_trimmed}: \
2153 no execution trace (test may have failed in setUp/constructor or been \
2154 skipped)"
2155 )
2156 })?;
2157
2158 decode_trace_arena(arena, &decoder).await;
2160
2161 match trace_output {
2162 TraceOutputKind::Flamegraph | TraceOutputKind::Flamechart => {
2163 let mut folded_stack_trace =
2164 folded_stack_trace::build(arena, self.evm.isolate);
2165 let flame_chart = matches!(trace_output, TraceOutputKind::Flamechart);
2166 if flame_chart {
2167 folded_stack_trace.reverse();
2168 }
2169 let label = trace_output.label();
2170 RenderedTraceOutput::Flame {
2171 file_name: format!("cache/{label}_{contract}_{test_name_trimmed}.svg"),
2172 title: format!("{label} {contract}::{test_name_trimmed}"),
2173 flame_chart,
2174 folded_stack_trace,
2175 }
2176 }
2177 TraceOutputKind::EvmProfile(EvmProfileFormat::Speedscope) => {
2178 let profile = speedscope::builder::build(
2179 arena,
2180 test_name_trimmed,
2181 contract,
2182 self.evm.isolate,
2183 );
2184 RenderedTraceOutput::EvmProfile {
2185 profile_json: serde_json::to_vec(&profile)?,
2186 test_name: test_name_trimmed.to_string(),
2187 contract: contract.to_string(),
2188 }
2189 }
2190 }
2191 };
2192
2193 match rendered {
2194 RenderedTraceOutput::Flame {
2195 file_name,
2196 title,
2197 flame_chart,
2198 folded_stack_trace,
2199 } => {
2200 let file =
2201 std::fs::File::create(&file_name).wrap_err("failed to create file")?;
2202 let file = std::io::BufWriter::new(file);
2203
2204 let mut options = inferno::flamegraph::Options::default();
2205 options.title = title;
2206 options.count_name = "gas".to_string();
2207 options.flame_chart = flame_chart;
2208
2209 inferno::flamegraph::from_lines(
2210 &mut options,
2211 folded_stack_trace.iter().map(String::as_str),
2212 file,
2213 )
2214 .wrap_err("failed to write svg")?;
2215 sh_println!("Saved to {file_name}")?;
2216
2217 if !self.no_open
2218 && let Err(e) = opener::open(&file_name)
2219 {
2220 sh_err!("Failed to open {file_name}; please open it manually: {e}")?;
2221 }
2222 }
2223 RenderedTraceOutput::EvmProfile { profile_json, test_name, contract } => {
2224 let profile_path = format!("cache/evm_profile_{contract}_{test_name}.json");
2225 fs::write(&profile_path, &profile_json)?;
2226
2227 sh_println!("Profile saved to {profile_path}")?;
2228
2229 if self.no_open {
2230 return Ok(outcome);
2231 }
2232
2233 evm_profile_server::serve_and_open(profile_json, &test_name, &contract).await?;
2234 }
2235 }
2236 }
2237
2238 if execution.should_debug {
2239 let (_, _, test_result) =
2241 outcome.remove_first().ok_or_eyre("no tests were executed")?;
2242
2243 let sources =
2244 ContractSources::from_project_output(output, project_root, Some(&libraries))?;
2245
2246 let mut traces = {
2249 let execution = test_result
2250 .traces
2251 .iter()
2252 .filter(|(kind, _)| kind.is_execution())
2253 .cloned()
2254 .collect::<Vec<_>>();
2255 if execution.is_empty() { test_result.traces.clone() } else { execution }
2256 };
2257 if let Some(decoder) = &outcome.last_run_decoder {
2258 for (_, arena) in &mut traces {
2259 decode_trace_arena(arena, decoder).await;
2260 }
2261 }
2262
2263 let mut builder = Debugger::builder()
2265 .traces(traces)
2266 .sources(sources)
2267 .breakpoints(test_result.breakpoints)
2268 .layout(self.debug_layout.unwrap_or_default());
2269
2270 if let Some(decoder) = &outcome.last_run_decoder {
2271 builder = builder.decoder(decoder);
2272 }
2273
2274 let mut debugger = builder.build();
2275 if let Some(dump_path) = &self.dump {
2276 debugger.dump_to_file(dump_path)?;
2277 } else {
2278 debugger.try_run_tui()?;
2279 }
2280 }
2281
2282 if let Some(mutate) = &self.mutate {
2284 if outcome.failed() > 0 {
2286 eyre::bail!(
2287 "Mutation testing compiler profile failed its unmutated baseline run; \
2288 adjust `--mutation-via-ir` / `--mutation-optimizer-runs` or fix the tests \
2289 before running mutation testing"
2290 );
2291 }
2292
2293 if outcome.successes().next().is_none() {
2299 eyre::bail!(
2300 "Mutation testing requires at least one passing baseline test; the current \
2301 filter/path selection matched zero non-skipped tests. Loosen `--match-test` / \
2302 `--match-contract` / `--match-path` or check the project layout."
2303 );
2304 }
2305
2306 if !mutate.is_empty() && self.mutate_path.is_some() {
2310 eyre::bail!(
2311 "`--mutate-path <PATTERN>` cannot be combined with explicit paths passed to `--mutate`; pass either paths or a glob pattern, not both"
2312 );
2313 }
2314
2315 if !override_networks.is_empty() {
2322 eyre::bail!(
2323 "Mutation testing does not yet support inline per-test network overrides \
2324 (found {} annotated network(s)). Re-run without `--mutate` or remove the \
2325 per-test network annotations.",
2326 override_networks.len()
2327 );
2328 }
2329
2330 if config_for_mutation.ffi {
2338 eyre::bail!(
2339 "Mutation testing is unsafe with `ffi = true`: per-mutant workspaces share \
2340 symlinked dependency directories, and arbitrary FFI commands run by tests \
2341 can race or corrupt the real `lib`/`node_modules`/`dependencies` trees. \
2342 Disable ffi in your foundry.toml to run mutation tests."
2343 );
2344 }
2345
2346 let root = &config_for_mutation.root;
2352 let canonicalize_through_existing_ancestor = |path: &Path| -> PathBuf {
2353 let resolved =
2354 if path.is_absolute() { path.to_path_buf() } else { root.join(path) };
2355 if let Ok(canon) = dunce::canonicalize(&resolved) {
2356 return canon;
2357 }
2358
2359 let mut missing = Vec::new();
2360 let mut ancestor = resolved.as_path();
2361 while !ancestor.exists() {
2362 let Some(name) = ancestor.file_name() else { break };
2363 missing.push(name.to_owned());
2364 let Some(parent) = ancestor.parent() else { break };
2365 ancestor = parent;
2366 }
2367
2368 let mut canon = dunce::canonicalize(ancestor).unwrap_or_else(|_| ancestor.into());
2369 for component in missing.iter().rev() {
2370 canon.push(component);
2371 }
2372 canon
2373 };
2374
2375 let mut shared_dep_dirs: Vec<PathBuf> = config_for_mutation
2376 .libs
2377 .iter()
2378 .filter(|p| p.exists())
2379 .map(|p| canonicalize_through_existing_ancestor(p))
2380 .collect();
2381 for dep_dir in ["node_modules", "dependencies"] {
2382 let dep_path = root.join(dep_dir);
2383 if dep_path.exists() && dep_path.is_dir() {
2384 shared_dep_dirs.push(canonicalize_through_existing_ancestor(&dep_path));
2385 }
2386 }
2387
2388 let effective_permission = |path: &Path| -> Option<FsAccessPermission> {
2389 let mut max_path_len = 0;
2390 let mut highest_permission = FsAccessPermission::None;
2391
2392 for perm in &config_for_mutation.fs_permissions.permissions {
2393 let permission_path = canonicalize_through_existing_ancestor(&perm.path);
2394 if path.starts_with(&permission_path) {
2395 let path_len = permission_path.components().count();
2396 if path_len > max_path_len {
2397 max_path_len = path_len;
2398 highest_permission = perm.access;
2399 } else if path_len == max_path_len {
2400 highest_permission = match (highest_permission, perm.access) {
2401 (FsAccessPermission::ReadWrite, _)
2402 | (FsAccessPermission::Read, FsAccessPermission::Write)
2403 | (FsAccessPermission::Write, FsAccessPermission::Read) => {
2404 FsAccessPermission::ReadWrite
2405 }
2406 (FsAccessPermission::None, perm) => perm,
2407 (existing_perm, _) => existing_perm,
2408 };
2409 }
2410 }
2411 }
2412
2413 (max_path_len > 0).then_some(highest_permission)
2414 };
2415
2416 let grants_write = |path: &Path| {
2417 matches!(
2418 effective_permission(path),
2419 Some(FsAccessPermission::Write | FsAccessPermission::ReadWrite)
2420 )
2421 };
2422
2423 let unsafe_write_paths: Vec<&Path> = config_for_mutation
2424 .fs_permissions
2425 .permissions
2426 .iter()
2427 .filter(|perm| {
2428 matches!(perm.access, FsAccessPermission::Write | FsAccessPermission::ReadWrite)
2429 })
2430 .filter(|perm| {
2431 let perm_path = canonicalize_through_existing_ancestor(&perm.path);
2432 shared_dep_dirs.iter().any(|dep| {
2433 if perm_path.starts_with(dep) {
2434 grants_write(&perm_path)
2435 } else if dep.starts_with(&perm_path) {
2436 grants_write(dep)
2437 } else {
2438 false
2439 }
2440 })
2441 })
2442 .map(|p| p.path.as_path())
2443 .collect();
2444
2445 if !unsafe_write_paths.is_empty() {
2446 let paths = unsafe_write_paths
2447 .iter()
2448 .map(|p| format!(" - {}", p.display()))
2449 .collect::<Vec<_>>()
2450 .join("\n");
2451 eyre::bail!(
2452 "Mutation testing is unsafe with write-capable `fs_permissions` that can \
2453 reach the symlinked dependency trees (`lib`/`node_modules`/`dependencies`); \
2454 per-mutant workspaces share those trees, so `vm.writeFile` calls would race \
2455 against or corrupt your real dependencies. Restrict the following \
2456 `fs_permissions` entries to read-only or scope them away from dependency \
2457 paths:\n{paths}"
2458 );
2459 }
2460
2461 let json_output = shell::is_json();
2462 let selected_sources_relative = execution
2463 .selected_sources
2464 .iter()
2465 .filter_map(|path| {
2466 path.strip_prefix(&config_for_mutation.root).ok().map(PathBuf::from)
2467 })
2468 .collect::<Vec<_>>();
2469
2470 let mutation_config = MutationRunConfig {
2471 mutate_paths: mutate.clone(),
2472 mutate_path_pattern: self.mutate_path.clone(),
2473 mutate_contract_pattern: self.mutate_contract.clone(),
2474 num_workers: self.mutation_jobs.unwrap_or(0),
2475 show_progress: self.show_progress,
2476 json_output,
2477 filter_args: filter.args().clone(),
2488 rerun_failures: filter.rerun_failures().map(|failures| failures.to_vec()),
2489 selected_sources_relative,
2490 isolate: evm_opts_for_mutation.isolate,
2491 };
2492
2493 let result = run_mutation_testing(
2494 Arc::new(config_for_mutation.clone()),
2495 output,
2496 evm_opts_for_mutation.clone(),
2497 mutation_config,
2498 )
2499 .await?;
2500
2501 if result.cancelled {
2502 std::process::exit(130);
2503 }
2504
2505 if json_output {
2507 let json_output = result.summary.to_json_output(result.duration_secs);
2508 sh_println!("{}", serde_json::to_string(&json_output)?)?;
2509 }
2510
2511 outcome = TestOutcome::empty(None, true);
2512 }
2513
2514 Ok(outcome)
2515 }
2516
2517 async fn build_and_run_tests<FEN: FoundryEvmNetwork>(
2519 &self,
2520 config: Config,
2521 evm_opts: EvmOpts,
2522 output: &ProjectCompileOutput,
2523 filter: &mut ProjectPathsAwareFilter,
2524 execution: TestExecutionOptions,
2525 ) -> eyre::Result<(Libraries, TestOutcome)> {
2526 let verbosity = evm_opts.verbosity;
2527 let (evm_env, tx_env, fork) =
2528 evm_opts.env_resolved::<SpecFor<FEN>, BlockEnvFor<FEN>, TxEnvFor<FEN>>().await?;
2529 let create2_deployer_available =
2530 evm_opts.can_use_create2_deployer_resolved(fork.as_ref()).await?;
2531 let fork_block_number = fork.as_ref().map(|fork| fork.number());
2532
2533 let config = Arc::new(config);
2534 let showmap = self.showmap_config()?;
2535 let runner = MultiContractRunnerBuilder::new(config.clone(), execution.inline_config)
2536 .set_debug(execution.should_debug)
2537 .set_decode_internal(execution.decode_internal)
2538 .set_record_all_steps(self.evm_profile.is_some())
2539 .initial_balance(evm_opts.initial_balance)
2540 .sender(evm_opts.sender)
2541 .with_fork(evm_opts.get_fork(&config, evm_env.cfg_env.chain_id, fork_block_number))
2542 .enable_isolation(evm_opts.isolate)
2543 .fail_fast(self.fail_fast)
2544 .set_coverage(execution.coverage)
2545 .with_multi_network(execution.multi_network)
2546 .with_showmap(showmap)
2547 .with_fuzz_only(self.fuzz_only.is_enabled())
2548 .with_fuzz_failure_replay(self.fuzz_failure_replay)
2549 .with_symbolic_artifact_replay(execution.replay_symbolic_artifact)
2550 .with_create2_deployer_available(create2_deployer_available)
2551 .build::<FEN, MultiCompiler>(output, evm_env, tx_env, evm_opts)?;
2552
2553 let libraries = runner.libraries.clone();
2554 let outcome = self.run_tests_inner(runner, config, verbosity, filter, output).await?;
2555 Ok((libraries, outcome))
2556 }
2557
2558 async fn build_fuzz_minimize_runner<FEN: FoundryEvmNetwork>(
2559 &self,
2560 config: Config,
2561 evm_opts: EvmOpts,
2562 output: &ProjectCompileOutput,
2563 options: FuzzMinimizeNetworkPassOptions,
2564 ) -> eyre::Result<MultiContractRunner<FEN>> {
2565 let (evm_env, tx_env, fork) =
2566 evm_opts.env_resolved::<SpecFor<FEN>, BlockEnvFor<FEN>, TxEnvFor<FEN>>().await?;
2567 let create2_deployer_available =
2568 evm_opts.can_use_create2_deployer_resolved(fork.as_ref()).await?;
2569 let fork_block_number = fork.as_ref().map(|fork| fork.number());
2570
2571 let config = Arc::new(config);
2572 MultiContractRunnerBuilder::new(config.clone(), options.inline_config)
2573 .initial_balance(evm_opts.initial_balance)
2574 .sender(evm_opts.sender)
2575 .with_fork(evm_opts.get_fork(&config, evm_env.cfg_env.chain_id, fork_block_number))
2576 .enable_isolation(evm_opts.isolate)
2577 .fail_fast(self.fail_fast)
2578 .with_multi_network(options.multi_network)
2579 .with_fuzz_only(self.fuzz_only.is_enabled())
2580 .with_fuzz_failure_replay(self.fuzz_failure_replay)
2581 .with_create2_deployer_available(create2_deployer_available)
2582 .build::<FEN, MultiCompiler>(output, evm_env, tx_env, evm_opts)
2583 }
2584
2585 async fn dispatch_network(
2587 &self,
2588 dispatch_opts: &EvmOpts,
2589 config: Config,
2590 evm_opts: EvmOpts,
2591 output: &ProjectCompileOutput,
2592 filter: &mut ProjectPathsAwareFilter,
2593 execution: TestExecutionOptions,
2594 ) -> eyre::Result<(Libraries, TestOutcome)> {
2595 match network_dispatch_kind(dispatch_opts) {
2596 NetworkDispatchKind::Tempo => {
2597 self.build_and_run_tests::<TempoEvmNetwork>(
2598 config, evm_opts, output, filter, execution,
2599 )
2600 .await
2601 }
2602 #[cfg(feature = "optimism")]
2603 NetworkDispatchKind::Optimism => {
2604 self.build_and_run_tests::<OpEvmNetwork>(
2605 config, evm_opts, output, filter, execution,
2606 )
2607 .await
2608 }
2609 NetworkDispatchKind::Eth => {
2610 self.build_and_run_tests::<EthEvmNetwork>(
2611 config, evm_opts, output, filter, execution,
2612 )
2613 .await
2614 }
2615 }
2616 }
2617
2618 async fn dispatch_fuzz_minimize_network(
2619 &self,
2620 dispatch_opts: &EvmOpts,
2621 config: Config,
2622 evm_opts: EvmOpts,
2623 output: &ProjectCompileOutput,
2624 options: FuzzMinimizeNetworkPassOptions,
2625 filter: &ProjectPathsAwareFilter,
2626 ) -> eyre::Result<FuzzMinimizeReplayPass> {
2627 match network_dispatch_kind(dispatch_opts) {
2628 NetworkDispatchKind::Tempo => self
2629 .build_fuzz_minimize_runner::<TempoEvmNetwork>(config, evm_opts, output, options)
2630 .await
2631 .map(|runner| fuzz_minimize_replay(runner, filter)),
2632 #[cfg(feature = "optimism")]
2633 NetworkDispatchKind::Optimism => self
2634 .build_fuzz_minimize_runner::<OpEvmNetwork>(config, evm_opts, output, options)
2635 .await
2636 .map(|runner| fuzz_minimize_replay(runner, filter)),
2637 NetworkDispatchKind::Eth => self
2638 .build_fuzz_minimize_runner::<EthEvmNetwork>(config, evm_opts, output, options)
2639 .await
2640 .map(|runner| fuzz_minimize_replay(runner, filter)),
2641 }
2642 }
2643
2644 fn symbolic_regression_config(&self, config: &Config) -> Option<SymbolicRegressionConfig> {
2645 self.emit_regression.then(|| SymbolicRegressionConfig {
2646 out: self
2647 .regression_out
2648 .clone()
2649 .map(|path| if path.is_relative() { config.root.join(path) } else { path }),
2650 overwrite: self.regression_overwrite,
2651 })
2652 }
2653
2654 async fn run_tests_inner<FEN: FoundryEvmNetwork>(
2656 &self,
2657 mut runner: MultiContractRunner<FEN>,
2658 config: Arc<Config>,
2659 verbosity: u8,
2660 filter: &mut ProjectPathsAwareFilter,
2661 output: &ProjectCompileOutput,
2662 ) -> eyre::Result<TestOutcome> {
2663 let fuzz_seed = config.fuzz.seed;
2664 if self.list {
2665 return list(runner, filter);
2666 }
2667 let symbolic_regression = self.symbolic_regression_config(&config);
2668
2669 trace!(target: "forge::test", "running all tests");
2670
2671 let silent = self.gas_report && shell::is_json()
2673 || self.summary && shell::is_json()
2674 || self.mutate.is_some() && shell::is_json();
2675 let tracing = &config.tracing;
2676 let trace_verbosity = tracing.verbosity;
2677
2678 let mut num_filtered = runner.matching_test_functions(filter).count();
2679
2680 if !self.opcodes.is_empty() && trace_verbosity < 5 {
2681 sh_eprintln!()?;
2682 eyre::bail!("Not enough verbosity. Use -vvvvv to show opcodes.");
2683 }
2684
2685 if num_filtered == 0 {
2686 let total_tests = if filter.is_empty() {
2687 num_filtered
2688 } else {
2689 runner.matching_test_functions(&EmptyTestFilter::default()).count()
2690 };
2691 if total_tests == 0 {
2692 sh_warn!(
2693 "No tests found in project! Forge looks for functions that start with `test`"
2694 )?;
2695 } else {
2696 let mut msg = format!("no tests match the provided pattern:\n{filter}");
2697 if let Some(test_pattern) = &filter.args().test_pattern {
2699 let test_name = test_pattern.as_str();
2700 let candidates = runner.all_test_functions(filter).map(|f| &f.name);
2702 if let Some(suggestion) = utils::did_you_mean(test_name, candidates).pop() {
2703 write!(msg, "\nDid you mean `{suggestion}`?")?;
2704 }
2705 }
2706 sh_warn!("{msg}")?;
2707 }
2708 return Ok(TestOutcome::empty(Some(runner.known_contracts.clone()), false));
2709 }
2710
2711 let debug_selection_term = Term::stderr();
2712 let interactive_debug_selection = self.debug
2713 && num_filtered != 1
2714 && tui_mode().is_interactive()
2715 && debug_selection_term.is_term();
2716 let mut matching_debug_tests = if interactive_debug_selection {
2717 collect_matching_debug_tests(&runner.list_signatures(filter))
2718 } else if self.debug && num_filtered != 1 {
2719 collect_matching_debug_tests(&runner.list(filter))
2720 } else {
2721 Vec::new()
2722 };
2723 if interactive_debug_selection {
2724 ctrlc::set_handler(|| {
2725 let _ = Term::stderr().show_cursor();
2726 std::process::exit(130);
2727 })?;
2728
2729 let Some(selected) = Select::new()
2730 .with_prompt("Select a test to debug")
2731 .items(
2732 matching_debug_tests
2733 .iter()
2734 .map(|test| format!("{}.{}", test.contract, test.test)),
2735 )
2736 .max_length(DEBUGGER_MATCHING_TESTS_DISPLAY_LIMIT)
2737 .interact_on_opt(&debug_selection_term)?
2738 else {
2739 bail!("Debugger test selection cancelled");
2740 };
2741
2742 filter.set_rerun_failures(vec![matching_debug_tests.swap_remove(selected)]);
2743 num_filtered = 1;
2744 }
2745
2746 if num_filtered != 1
2747 && (self.debug || self.flamegraph || self.flamechart || self.evm_profile.is_some())
2748 {
2749 let action = if self.flamegraph {
2750 "generate a flamegraph"
2751 } else if self.flamechart {
2752 "generate a flamechart"
2753 } else if self.evm_profile.is_some() {
2754 "generate an EVM profile"
2755 } else {
2756 "run the debugger"
2757 };
2758 let filter_hint = if filter.is_empty() {
2759 String::new()
2760 } else {
2761 format!("\n\nFilter used:\n{filter}")
2762 };
2763 let matching_tests_hint = if self.debug {
2764 format_matching_debug_tests(&matching_debug_tests).unwrap_or_default()
2765 } else {
2766 String::new()
2767 };
2768 let narrowing_hint = if self.debug {
2769 "Use --match-test <TEST_NAME>, --match-contract, and --match-path to further limit the search."
2770 } else {
2771 "Use --match-contract and --match-path to further limit the search."
2772 };
2773 eyre::bail!(
2774 "{num_filtered} tests matched your criteria, but exactly 1 test must match in order to {action}.{matching_tests_hint}\n\n\
2775 {narrowing_hint}{filter_hint}",
2776 );
2777 }
2778
2779 if num_filtered == 1 && runner.decode_internal != InternalTraceMode::None {
2781 runner.decode_internal = InternalTraceMode::Full;
2782 }
2783
2784 if self.mutate.is_none() && !self.gas_report && !self.summary && shell::is_json() {
2786 let mut results = runner.test_collect(filter)?;
2787 prepare_results_for_json(&mut results, verbosity, tracing.trace_depth);
2788 if let Some(regression) = &symbolic_regression {
2789 let artifacts = collect_symbolic_artifacts_from_suites(results.values());
2790 let regressions = emit_symbolic_regressions(
2791 &config,
2792 regression,
2793 &runner.known_contracts,
2794 &artifacts,
2795 )?;
2796 attach_symbolic_regressions_to_suites(results.values_mut(), ®ressions);
2797 }
2798 sh_println!("{}", serde_json::to_string(&results)?)?;
2799 let kc = runner.known_contracts.clone();
2800 return Ok(TestOutcome::new(Some(kc), results, self.allow_failure, fuzz_seed));
2801 }
2802
2803 if self.junit {
2804 let mut results = runner.test_collect(filter)?;
2805 if let Some(regression) = &symbolic_regression {
2806 let artifacts = collect_symbolic_artifacts_from_suites(results.values());
2807 let regressions = emit_symbolic_regressions(
2808 &config,
2809 regression,
2810 &runner.known_contracts,
2811 &artifacts,
2812 )?;
2813 attach_symbolic_regressions_to_suites(results.values_mut(), ®ressions);
2814 }
2815 sh_println!("{}", junit_xml_report(&results, verbosity).to_string()?)?;
2816 let kc = runner.known_contracts.clone();
2817 return Ok(TestOutcome::new(Some(kc), results, self.allow_failure, fuzz_seed));
2818 }
2819
2820 let remote_chain =
2821 if runner.fork.is_some() { runner.tx_env.chain_id().map(Into::into) } else { None };
2822 let known_contracts = runner.known_contracts.clone();
2823
2824 let libraries = runner.libraries.clone();
2825
2826 let is_multi_pass = !runner.tcfg.multi_network.all_override_networks.is_empty();
2830 let is_tempo_network = runner.tcfg.evm_opts.networks.is_tempo();
2831 let decode_internal = runner.decode_internal != InternalTraceMode::None;
2832
2833 let (tx, rx) = channel::<(String, SuiteResult)>();
2835 let timer = Instant::now();
2836 let show_progress = config.show_progress;
2837 let handle = tokio::task::spawn_blocking({
2838 let filter = filter.clone();
2839 move || runner.test(&filter, tx, show_progress).map(|()| runner)
2840 });
2841
2842 let mut identifier = TraceIdentifiers::new().with_local(&known_contracts);
2844
2845 if !self.gas_report && remote_chain.is_some() {
2849 identifier = identifier.with_external(&config, remote_chain)?;
2850 }
2851
2852 let mut builder = CallTraceDecoderBuilder::new()
2854 .with_tracing_config(tracing)
2855 .with_known_contracts(&known_contracts)
2856 .with_chain_id(remote_chain.map(|c| c.id()))
2857 .with_tempo_hardfork(
2858 (is_tempo_network || remote_chain.is_some_and(|chain| chain.is_tempo()))
2859 .then(|| config.evm_spec_id::<TempoHardfork>()),
2860 );
2861 if !self.gas_report {
2863 builder =
2864 builder.with_signature_identifier(SignaturesIdentifier::from_config(&config)?);
2865 }
2866
2867 if decode_internal {
2868 let sources =
2869 ContractSources::from_project_output(output, &config.root, Some(&libraries))?;
2870 builder = builder.with_debug_identifier(DebugTraceIdentifier::new(sources));
2871 }
2872 let mut decoder = builder.build();
2873
2874 let mut gas_report = self.gas_report.then(|| {
2875 GasReport::new(
2876 config.gas_reports.clone(),
2877 config.gas_reports_ignore.clone(),
2878 config.gas_reports_include_tests,
2879 )
2880 });
2881
2882 let mut gas_snapshots = BTreeMap::<String, BTreeMap<String, String>>::new();
2883
2884 let mut outcome = TestOutcome::empty(None, self.allow_failure);
2885 outcome.fuzz_seed = fuzz_seed;
2886
2887 let mut any_test_failed = false;
2888 let mut backtrace_builder = None;
2889 while let Ok((contract_name, mut suite_result)) = rx.recv() {
2890 let len = suite_result.len();
2891 let tests = &mut suite_result.test_results;
2892 let has_tests = !tests.is_empty();
2893
2894 if is_multi_pass && !has_tests && suite_result.warnings.is_empty() {
2898 continue;
2899 }
2900
2901 decoder.clear_addresses();
2903
2904 let always_identify_traces = self.gas_report
2906 || self.debug
2907 || self.flamegraph
2908 || self.flamechart
2909 || self.evm_profile.is_some();
2910
2911 if !silent {
2913 sh_println!()?;
2914 for warning in &suite_result.warnings {
2915 sh_warn!("{warning}")?;
2916 }
2917 if has_tests {
2918 let tests = if len > 1 { "tests" } else { "test" };
2919 sh_println!("Ran {len} {tests} for {contract_name}")?;
2920 }
2921 }
2922
2923 for (name, result) in tests {
2925 let test_failed = result.status.is_failure();
2926 let show_traces = !self.suppress_successful_traces || test_failed;
2927 let render_trace_output = should_render_trace_output(silent, show_traces);
2928 let should_include_trace = |kind: &TraceKind| match kind {
2929 TraceKind::Execution => {
2930 (trace_verbosity == 3 && test_failed) || trace_verbosity >= 4
2931 }
2932 TraceKind::Setup => {
2933 (trace_verbosity == 4 && test_failed) || trace_verbosity >= 5
2934 }
2935 TraceKind::Deployment => false,
2936 };
2937 let renders_trace = render_trace_output
2938 && result.traces.iter().any(|(kind, _)| should_include_trace(kind));
2939 let identify_addresses = always_identify_traces || renders_trace;
2940
2941 if !silent {
2942 sh_println!("{}", result.short_result_with_suite(name, &contract_name))?;
2943 for artifact in &result.counterexample_artifacts {
2944 sh_warn!("Counterexample artifact: {}", artifact.path.display())?;
2945 }
2946
2947 if let TestKind::Invariant { metrics, .. } = &result.kind
2949 && !metrics.is_empty()
2950 {
2951 let _ = sh_println!("\n{}\n", format_invariant_metrics_table(metrics));
2952 }
2953
2954 if verbosity >= 2 && show_traces {
2956 let console_logs = decode_console_logs(&result.logs);
2958 if !console_logs.is_empty() {
2959 sh_println!("Logs:")?;
2960 for log in console_logs {
2961 sh_println!(" {log}")?;
2962 }
2963 sh_println!()?;
2964 }
2965 }
2966 }
2967
2968 any_test_failed |= result.status == TestStatus::Failure;
2971
2972 decoder.clear_addresses();
2974 if identify_addresses {
2975 decoder.labels.extend(result.labels.iter().map(|(k, v)| (*k, v.clone())));
2976 }
2977
2978 let mut decoded_traces = if renders_trace {
2980 Vec::with_capacity(result.traces.len())
2981 } else {
2982 Vec::new()
2983 };
2984 if identify_addresses || renders_trace {
2985 for (kind, arena) in &mut result.traces {
2986 if identify_addresses {
2987 if self.debug && !result.debug_bytecodes.is_empty() {
2988 let mut local_identifier = TraceIdentifiers::new()
2989 .with_local_and_bytecodes(
2990 &known_contracts,
2991 &result.debug_bytecodes,
2992 );
2993 decoder.identify(arena, &mut local_identifier);
2994 }
2995 decoder.identify(arena, &mut identifier);
2996 }
2997
2998 let should_include = should_include_trace(kind);
3004
3005 if renders_trace && should_include {
3006 decoder.opcodes = self.opcodes.clone();
3007 decode_trace_arena(arena, &decoder).await;
3008
3009 if let Some(trace_depth) = tracing.trace_depth {
3010 let mut arena = arena.clone();
3011 prune_trace_depth(&mut arena, trace_depth);
3012 decoded_traces.push(render_trace_arena_inner(
3013 &arena,
3014 false,
3015 trace_verbosity > 4,
3016 ));
3017 } else {
3018 decoded_traces.push(render_trace_arena_inner(
3019 arena,
3020 false,
3021 trace_verbosity > 4,
3022 ));
3023 }
3024 }
3025 }
3026 }
3027
3028 if !silent && show_traces && !decoded_traces.is_empty() {
3029 sh_println!("Traces:")?;
3030 for trace in &decoded_traces {
3031 sh_println!("{trace}")?;
3032 }
3033 }
3034
3035 if !silent
3039 && result.status.is_failure()
3040 && trace_verbosity >= 3
3041 && !result.traces.is_empty()
3042 && let Some((_, arena)) =
3043 result.traces.iter().find(|(kind, _)| matches!(kind, TraceKind::Execution))
3044 {
3045 let builder = backtrace_builder.get_or_insert_with(|| {
3047 BacktraceBuilder::new(
3048 output,
3049 config.root.clone(),
3050 config.parsed_libraries().ok(),
3051 config.via_ir,
3052 )
3053 });
3054
3055 let backtrace = builder.from_traces(arena);
3056
3057 if !backtrace.is_empty() {
3058 sh_println!("{}", backtrace)?;
3059 }
3060 }
3061
3062 if let Some(gas_report) = &mut gas_report {
3063 gas_report.analyze(result.traces.iter().map(|(_, a)| &a.arena), &decoder).await;
3064
3065 for trace in &result.gas_report_traces {
3066 decoder.clear_addresses();
3067
3068 for (kind, arena) in &result.traces {
3071 if !matches!(kind, TraceKind::Execution) {
3072 decoder.identify(arena, &mut identifier);
3073 }
3074 }
3075
3076 for arena in trace {
3077 decoder.identify(arena, &mut identifier);
3078 gas_report.analyze([arena], &decoder).await;
3079 }
3080 }
3081 }
3082
3083 if shell::is_json()
3084 && let Some(trace_depth) = tracing.trace_depth
3085 {
3086 for (_, arena) in &mut result.traces {
3087 *arena = trace_arena_at_depth(arena, trace_depth);
3088 }
3089 }
3090 result.gas_report_traces = Default::default();
3092
3093 for (group, new_snapshots) in &result.gas_snapshots {
3095 gas_snapshots.entry(group.clone()).or_default().extend(new_snapshots.clone());
3096 }
3097 }
3098
3099 if !gas_snapshots.is_empty() {
3101 if self.gas_snapshot_check.unwrap_or(config.gas_snapshot_check) {
3113 let differences_found =
3114 gas_snapshots.iter().fold(false, |mut found, (group, snapshots)| {
3115 if !&config.snapshots.join(format!("{group}.json")).exists() {
3117 return found;
3118 }
3119
3120 let previous_snapshots: BTreeMap<String, String> =
3121 fs::read_json_file(&config.snapshots.join(format!("{group}.json")))
3122 .expect("Failed to read snapshots from disk");
3123
3124 let diff: BTreeMap<_, _> = snapshots
3125 .iter()
3126 .filter_map(|(k, v)| {
3127 previous_snapshots.get(k).and_then(|previous_snapshot| {
3128 (previous_snapshot != v).then(|| {
3129 (k.clone(), (previous_snapshot.clone(), v.clone()))
3130 })
3131 })
3132 })
3133 .collect();
3134
3135 if !diff.is_empty() {
3136 let _ = sh_eprintln!(
3137 "{}",
3138 format!("\n[{group}] Failed to match snapshots:").red().bold()
3139 );
3140
3141 for (key, (previous_snapshot, snapshot)) in &diff {
3142 let _ = sh_eprintln!(
3143 "{}",
3144 format!("- [{key}] {previous_snapshot} → {snapshot}").red()
3145 );
3146 }
3147
3148 found = true;
3149 }
3150
3151 found
3152 });
3153
3154 if differences_found {
3155 sh_eprintln!()?;
3156 eyre::bail!("Snapshots differ from previous run");
3157 }
3158 }
3159
3160 if self.gas_snapshot_emit.unwrap_or(config.gas_snapshot_emit) {
3170 fs::create_dir_all(&config.snapshots)?;
3172
3173 for (group, snapshots) in &gas_snapshots {
3175 fs::write_pretty_json_file(
3176 &config.snapshots.join(format!("{group}.json")),
3177 &snapshots,
3178 )
3179 .expect("Failed to write gas snapshots to disk");
3180 }
3181 }
3182 }
3183
3184 if !silent && has_tests {
3186 sh_println!("{}", suite_result.summary())?;
3187 }
3188
3189 outcome.results.insert(contract_name, suite_result);
3191
3192 if self.fail_fast && any_test_failed {
3194 break;
3195 }
3196 }
3197 if let Some(regression) = &symbolic_regression {
3198 let artifacts = collect_symbolic_artifacts_from_suites(outcome.results.values());
3199 let regressions =
3200 emit_symbolic_regressions(&config, regression, &known_contracts, &artifacts)?;
3201 attach_symbolic_regressions_to_suites(outcome.results.values_mut(), ®ressions);
3202 if !silent {
3203 for regression in regressions {
3204 sh_warn!(
3205 "Regression test: {} (from {})",
3206 regression.path.display(),
3207 regression.artifact.display()
3208 )?;
3209 }
3210 }
3211 }
3212 outcome.last_run_decoder = Some(decoder);
3213 let duration = timer.elapsed();
3214
3215 trace!(target: "forge::test", len=outcome.results.len(), %any_test_failed, "done with results");
3216
3217 if let Some(gas_report) = gas_report {
3218 let finalized = gas_report.finalize();
3219 sh_println!("{finalized}")?;
3220 outcome.gas_report = Some(finalized);
3221 }
3222
3223 if !is_multi_pass && !self.summary && !shell::is_json() {
3224 sh_println!("{}", outcome.summary(duration))?;
3225 }
3226
3227 if !is_multi_pass && self.summary && !outcome.results.is_empty() {
3228 let summary_report = TestSummaryReport::new(self.detailed, outcome.clone());
3229 sh_println!("{summary_report}")?;
3230 }
3231
3232 let json_results_rx = self.json_file.is_some().then_some(rx);
3234
3235 match handle.await {
3237 Ok(result) => {
3238 let runner = result?;
3239 outcome.known_contracts = Some(runner.known_contracts);
3240 }
3241 Err(e) => match e.try_into_panic() {
3242 Ok(payload) => std::panic::resume_unwind(payload),
3243 Err(e) => return Err(e.into()),
3244 },
3245 }
3246
3247 if let Some(rx) = json_results_rx {
3249 let mut results = outcome.results.clone();
3250 for (contract_name, suite_result) in rx.try_iter() {
3251 if is_multi_pass
3252 && suite_result.test_results.is_empty()
3253 && suite_result.warnings.is_empty()
3254 {
3255 continue;
3256 }
3257 results.insert(contract_name, suite_result);
3258 }
3259 outcome.json_file_results = Some(results);
3260 }
3261
3262 persist_run_failures(&config, &outcome);
3264
3265 Ok(outcome)
3266 }
3267
3268 pub fn filter(&self, config: &Config) -> Result<ProjectPathsAwareFilter> {
3271 self.filter_with_rerun_failures(config, None)
3272 }
3273
3274 fn filter_with_rerun_failures(
3275 &self,
3276 config: &Config,
3277 loaded_rerun_failures: Option<LastRunFailures>,
3278 ) -> Result<ProjectPathsAwareFilter> {
3279 let mut filter = self.filter.clone();
3280 let rerun_failures = if self.rerun {
3281 let failures = loaded_rerun_failures.unwrap_or_else(|| last_run_failures(config));
3282 filter.test_pattern = failures.test_pattern;
3283 failures.failures
3284 } else {
3285 None
3286 };
3287 if filter.path_pattern.is_some() {
3288 if self.path.is_some() {
3289 bail!("Can not supply both --match-path and |path|");
3290 }
3291 } else {
3292 filter.path_pattern = self.path.clone();
3293 }
3294 let mut filter = filter.merge_with_config(config);
3295 if let Some(failures) = rerun_failures {
3296 filter.set_rerun_failures(failures);
3297 }
3298 Ok(filter)
3299 }
3300
3301 pub const fn is_watch(&self) -> bool {
3303 self.watch.watch.is_some()
3304 }
3305
3306 pub(crate) fn watchexec_config(&self) -> Result<watchexec::Config> {
3308 self.watch.watchexec_config(|| {
3309 let config = self.load_config()?;
3310 Ok([config.src, config.test])
3311 })
3312 }
3313}
3314
3315fn prepare_results_for_json(
3316 results: &mut BTreeMap<String, SuiteResult>,
3317 verbosity: u8,
3318 trace_depth: Option<usize>,
3319) {
3320 for suite_result in results.values_mut() {
3321 for test_result in suite_result.test_results.values_mut() {
3322 if verbosity >= 2 {
3323 test_result.decoded_logs = decode_console_logs(&test_result.logs);
3324 } else {
3325 test_result.logs = Vec::new();
3326 }
3327 for (_, arena) in &mut test_result.traces {
3328 for node in arena.nodes_mut() {
3330 node.trace.decoded = None;
3331 for log in &mut node.logs {
3332 log.decoded = None;
3333 }
3334 for step in &mut node.trace.steps {
3335 step.decoded = None;
3336 }
3337 }
3338 if let Some(trace_depth) = trace_depth {
3339 *arena = trace_arena_at_depth(arena, trace_depth);
3340 }
3341 }
3342 }
3343 }
3344}
3345
3346const fn should_render_trace_output(silent: bool, show_traces: bool) -> bool {
3347 !silent && show_traces
3348}
3349
3350impl Provider for TestArgs {
3351 fn metadata(&self) -> Metadata {
3352 Metadata::named("Core Build Args Provider")
3353 }
3354
3355 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
3356 let mut dict = Dict::default();
3357
3358 let mut fuzz_dict = Dict::default();
3359 if let Some(fuzz_seed) = self.fuzz_seed {
3360 fuzz_dict.insert("seed".to_string(), fuzz_seed.to_string().into());
3361 }
3362 if let Some(fuzz_runs) = self.fuzz_runs {
3363 fuzz_dict.insert("runs".to_string(), fuzz_runs.into());
3364 }
3365 if let Some(fuzz_run) = self.fuzz_run {
3366 fuzz_dict.insert("run".to_string(), fuzz_run.into());
3367 }
3368 if let Some(fuzz_worker) = self.fuzz_worker {
3369 fuzz_dict.insert("worker".to_string(), fuzz_worker.into());
3370 }
3371 if let Some(fuzz_timeout) = self.fuzz_timeout {
3372 fuzz_dict.insert("timeout".to_string(), fuzz_timeout.into());
3373 }
3374 if let Some(fuzz_dictionary_weight) = self.fuzz_dictionary_weight {
3375 fuzz_dict.insert("dictionary_weight".to_string(), fuzz_dictionary_weight.into());
3376 }
3377 if let Some(fuzz_dictionary_addresses) = self.fuzz_dictionary_addresses.clone() {
3378 fuzz_dict.insert(
3379 "max_fuzz_dictionary_addresses".to_string(),
3380 fuzz_dictionary_addresses.into(),
3381 );
3382 }
3383 if let Some(fuzz_dictionary_values) = self.fuzz_dictionary_values.clone() {
3384 fuzz_dict
3385 .insert("max_fuzz_dictionary_values".to_string(), fuzz_dictionary_values.into());
3386 }
3387 if let Some(fuzz_dictionary_literals) = self.fuzz_dictionary_literals.clone() {
3388 fuzz_dict.insert(
3389 "max_fuzz_dictionary_literals".to_string(),
3390 fuzz_dictionary_literals.into(),
3391 );
3392 }
3393 if let Some(fuzz_corpus_random_sequence_weight) = self.fuzz_corpus_random_sequence_weight {
3394 fuzz_dict.insert(
3395 "corpus_random_sequence_weight".to_string(),
3396 fuzz_corpus_random_sequence_weight.into(),
3397 );
3398 }
3399 if let Some(fuzz_corpus_dir) = self.fuzz_corpus_dir.clone() {
3400 fuzz_dict.insert(
3401 "corpus_dir".to_string(),
3402 fuzz_corpus_dir.to_string_lossy().to_string().into(),
3403 );
3404 }
3405 if let Some(fuzz_frontier_dir) = self.fuzz_frontier_dir.clone() {
3406 fuzz_dict.insert(
3407 "frontier_dir".to_string(),
3408 fuzz_frontier_dir.to_string_lossy().to_string().into(),
3409 );
3410 }
3411 if let Some(fuzz_frontier_limit) = self.fuzz_frontier_limit {
3412 fuzz_dict.insert("frontier_limit".to_string(), fuzz_frontier_limit.into());
3413 }
3414 if let Some(fuzz_payable_value_weight) = self.fuzz_payable_value_weight {
3415 fuzz_dict.insert("payable_value_weight".to_string(), fuzz_payable_value_weight.into());
3416 }
3417 if let Some(weight) = self.fuzz_mutation_weight_splice {
3418 fuzz_dict.insert("mutation_weight_splice".to_string(), weight.into());
3419 }
3420 if let Some(weight) = self.fuzz_mutation_weight_repeat {
3421 fuzz_dict.insert("mutation_weight_repeat".to_string(), weight.into());
3422 }
3423 if let Some(weight) = self.fuzz_mutation_weight_interleave {
3424 fuzz_dict.insert("mutation_weight_interleave".to_string(), weight.into());
3425 }
3426 if let Some(weight) = self.fuzz_mutation_weight_prefix {
3427 fuzz_dict.insert("mutation_weight_prefix".to_string(), weight.into());
3428 }
3429 if let Some(weight) = self.fuzz_mutation_weight_suffix {
3430 fuzz_dict.insert("mutation_weight_suffix".to_string(), weight.into());
3431 }
3432 if let Some(weight) = self.fuzz_mutation_weight_abi {
3433 fuzz_dict.insert("mutation_weight_abi".to_string(), weight.into());
3434 }
3435 if let Some(weight) = self.fuzz_mutation_weight_cmp {
3436 fuzz_dict.insert("mutation_weight_cmp".to_string(), weight.into());
3437 }
3438 if let Some(fuzz_input_file) = self.fuzz_input_file.clone() {
3439 fuzz_dict.insert("failure_persist_file".to_string(), fuzz_input_file.into());
3440 }
3441 dict.insert("fuzz".to_string(), fuzz_dict.into());
3442
3443 let mut invariant_dict = Dict::default();
3444 if let Some(invariant_runs) = self.invariant_runs_override {
3445 invariant_dict.insert("runs".to_string(), invariant_runs.into());
3446 }
3447 if let Some(invariant_depth) = self.invariant_depth {
3448 invariant_dict.insert("depth".to_string(), invariant_depth.into());
3449 }
3450 if let Some(invariant_min_depth) = self.invariant_min_depth {
3451 invariant_dict.insert("min_depth".to_string(), invariant_min_depth.into());
3452 }
3453 if let Some(invariant_depth_mode) = self.invariant_depth_mode {
3454 invariant_dict
3455 .insert("depth_mode".to_string(), Value::serialize(invariant_depth_mode)?);
3456 }
3457 if let Some(invariant_workers) = self.invariant_workers {
3458 invariant_dict.insert("workers".to_string(), Value::serialize(invariant_workers)?);
3459 }
3460 if let Some(invariant_dictionary_weight) = self.invariant_dictionary_weight {
3461 invariant_dict
3462 .insert("dictionary_weight".to_string(), invariant_dictionary_weight.into());
3463 }
3464 if let Some(invariant_dictionary_addresses) = self.invariant_dictionary_addresses.clone() {
3465 invariant_dict.insert(
3466 "max_fuzz_dictionary_addresses".to_string(),
3467 invariant_dictionary_addresses.into(),
3468 );
3469 }
3470 if let Some(invariant_dictionary_values) = self.invariant_dictionary_values.clone() {
3471 invariant_dict.insert(
3472 "max_fuzz_dictionary_values".to_string(),
3473 invariant_dictionary_values.into(),
3474 );
3475 }
3476 if let Some(invariant_dictionary_literals) = self.invariant_dictionary_literals.clone() {
3477 invariant_dict.insert(
3478 "max_fuzz_dictionary_literals".to_string(),
3479 invariant_dictionary_literals.into(),
3480 );
3481 }
3482 if let Some(invariant_corpus_random_sequence_weight) =
3483 self.invariant_corpus_random_sequence_weight
3484 {
3485 invariant_dict.insert(
3486 "corpus_random_sequence_weight".to_string(),
3487 invariant_corpus_random_sequence_weight.into(),
3488 );
3489 invariant_dict
3490 .insert("corpus_random_sequence_weight_configured".to_string(), true.into());
3491 }
3492 if let Some(invariant_corpus_dir) = self.invariant_corpus_dir.clone() {
3493 invariant_dict.insert(
3494 "corpus_dir".to_string(),
3495 invariant_corpus_dir.to_string_lossy().to_string().into(),
3496 );
3497 }
3498 if let Some(invariant_payable_value_weight) = self.invariant_payable_value_weight {
3499 invariant_dict
3500 .insert("payable_value_weight".to_string(), invariant_payable_value_weight.into());
3501 }
3502 if let Some(invariant_timeout) = self.invariant_timeout_override {
3503 invariant_dict.insert("timeout".to_string(), invariant_timeout.into());
3504 }
3505 if let Some(weight) = self.invariant_mutation_weight_splice {
3506 invariant_dict.insert("mutation_weight_splice".to_string(), weight.into());
3507 }
3508 if let Some(weight) = self.invariant_mutation_weight_repeat {
3509 invariant_dict.insert("mutation_weight_repeat".to_string(), weight.into());
3510 }
3511 if let Some(weight) = self.invariant_mutation_weight_interleave {
3512 invariant_dict.insert("mutation_weight_interleave".to_string(), weight.into());
3513 }
3514 if let Some(weight) = self.invariant_mutation_weight_prefix {
3515 invariant_dict.insert("mutation_weight_prefix".to_string(), weight.into());
3516 }
3517 if let Some(weight) = self.invariant_mutation_weight_suffix {
3518 invariant_dict.insert("mutation_weight_suffix".to_string(), weight.into());
3519 }
3520 if let Some(weight) = self.invariant_mutation_weight_abi {
3521 invariant_dict.insert("mutation_weight_abi".to_string(), weight.into());
3522 }
3523 if let Some(weight) = self.invariant_mutation_weight_cmp {
3524 invariant_dict.insert("mutation_weight_cmp".to_string(), weight.into());
3525 }
3526 if !invariant_dict.is_empty() {
3527 dict.insert("invariant".to_string(), invariant_dict.into());
3528 }
3529
3530 let mut symbolic_dict = Dict::default();
3531 if self.symbolic {
3532 symbolic_dict.insert("enabled".to_string(), true.into());
3533 }
3534 if self.symbolic_seed_corpus {
3535 symbolic_dict.insert("seed_corpus".to_string(), true.into());
3536 }
3537 if self.symbolic_use_fuzz_corpus {
3538 symbolic_dict.insert("use_fuzz_corpus".to_string(), true.into());
3539 }
3540 if let Some(corpus_seed_limit) = self.symbolic_corpus_seed_limit {
3541 symbolic_dict.insert("corpus_seed_limit".to_string(), corpus_seed_limit.into());
3542 }
3543 if self.symbolic_use_fuzz_frontiers {
3544 symbolic_dict.insert("use_fuzz_frontiers".to_string(), true.into());
3545 }
3546 if let Some(frontier_limit) = self.symbolic_frontier_limit {
3547 symbolic_dict.insert("frontier_limit".to_string(), frontier_limit.into());
3548 }
3549 if let Some(frontier_ids) = self.symbolic_frontier_ids.clone() {
3550 symbolic_dict.insert("frontier_ids".to_string(), frontier_ids.into());
3551 }
3552 if let Some(frontier_pcs) = self.symbolic_frontier_pcs.clone() {
3553 symbolic_dict.insert("frontier_pcs".to_string(), frontier_pcs.into());
3554 }
3555 if let Some(frontier_selectors) = self.symbolic_frontier_selectors.clone() {
3556 symbolic_dict.insert("frontier_selectors".to_string(), frontier_selectors.into());
3557 }
3558 if let Some(solver) = self.symbolic_solver.clone() {
3559 symbolic_dict.insert("solver".to_string(), solver.into());
3560 }
3561 if let Some(solver_command) = self.symbolic_solver_command.clone() {
3562 symbolic_dict.insert("solver_command".to_string(), solver_command.into());
3563 }
3564 if let Some(solver_portfolio) = self.symbolic_solver_portfolio.clone() {
3565 symbolic_dict.insert("solver_portfolio".to_string(), solver_portfolio.into());
3566 }
3567 if let Some(timeout) = self.symbolic_timeout {
3568 symbolic_dict.insert("timeout".to_string(), timeout.into());
3569 }
3570 if let Some(loop_bound) = self.symbolic_loop {
3571 symbolic_dict.insert("loop".to_string(), loop_bound.into());
3572 }
3573 if let Some(depth) = self.symbolic_depth {
3574 symbolic_dict.insert("depth".to_string(), depth.into());
3575 }
3576 if let Some(width) = self.symbolic_width {
3577 symbolic_dict.insert("width".to_string(), width.into());
3578 }
3579 if let Some(max_depth) = self.symbolic_max_depth {
3580 symbolic_dict.insert("max_depth".to_string(), max_depth.into());
3581 }
3582 if let Some(max_paths) = self.symbolic_max_paths {
3583 symbolic_dict.insert("max_paths".to_string(), max_paths.into());
3584 }
3585 if let Some(invariant_depth) = self.symbolic_invariant_depth {
3586 symbolic_dict.insert("invariant_depth".to_string(), invariant_depth.into());
3587 }
3588 if let Some(max_solver_queries) = self.symbolic_max_solver_queries {
3589 symbolic_dict.insert("max_solver_queries".to_string(), max_solver_queries.into());
3590 }
3591 if let Some(default_dynamic_length) = self.symbolic_default_dynamic_length {
3592 symbolic_dict
3593 .insert("default_dynamic_length".to_string(), default_dynamic_length.into());
3594 }
3595 if let Some(max_dynamic_length) = self.symbolic_max_dynamic_length {
3596 symbolic_dict.insert("max_dynamic_length".to_string(), max_dynamic_length.into());
3597 }
3598 if let Some(array_lengths) = self.symbolic_array_lengths.clone() {
3599 symbolic_dict.insert("array_lengths".to_string(), array_lengths.into());
3600 }
3601 if let Some(max_calldata_bytes) = self.symbolic_max_calldata_bytes {
3602 symbolic_dict.insert("max_calldata_bytes".to_string(), max_calldata_bytes.into());
3603 }
3604 if self.symbolic_call_targets {
3605 symbolic_dict.insert("symbolic_call_targets".to_string(), true.into());
3606 }
3607 if self.symbolic_dump_smt {
3608 symbolic_dict.insert("dump_smt".to_string(), true.into());
3609 }
3610 if let Some(storage_layout) = self.symbolic_storage_layout.clone() {
3611 symbolic_dict.insert("storage_layout".to_string(), storage_layout.into());
3612 }
3613 dict.insert("symbolic".to_string(), symbolic_dict.into());
3614
3615 if let Some(etherscan_api_key) =
3616 self.etherscan_api_key.as_ref().filter(|s| !s.trim().is_empty())
3617 {
3618 dict.insert("etherscan_api_key".to_string(), etherscan_api_key.clone().into());
3619 }
3620
3621 if self.show_progress {
3622 dict.insert("show_progress".to_string(), true.into());
3623 }
3624
3625 if self.mutation_timeout.is_some()
3627 || self.mutation_optimizer_runs.is_some()
3628 || self.mutation_via_ir.is_some()
3629 {
3630 let mut mutation_dict = Dict::default();
3631 if let Some(timeout) = self.mutation_timeout {
3632 mutation_dict.insert("timeout".to_string(), timeout.into());
3633 }
3634 if let Some(optimizer_runs) = self.mutation_optimizer_runs {
3635 mutation_dict.insert("optimizer_runs".to_string(), optimizer_runs.into());
3636 }
3637 if let Some(via_ir) = self.mutation_via_ir {
3638 mutation_dict.insert("via_ir".to_string(), via_ir.into());
3639 }
3640 dict.insert("mutation".to_string(), mutation_dict.into());
3641 }
3642
3643 Ok(Map::from([(Config::selected_profile(), dict)]))
3644 }
3645}
3646
3647fn parse_opcode(s: &str) -> Result<OpCode, String> {
3648 OpCode::parse(s).ok_or_else(|| format!("invalid opcode: {s}"))
3649}
3650
3651const fn apply_mutation_compiler_overrides(config: &mut Config) {
3652 if let Some(optimizer_runs) = config.mutation.optimizer_runs {
3653 let default_optimizer_settings =
3654 matches!(config.optimizer, Some(false)) && matches!(config.optimizer_runs, Some(200));
3655 config.optimizer_runs = Some(optimizer_runs as usize);
3656 if default_optimizer_settings {
3657 config.optimizer = None;
3658 }
3659 config.normalize_optimizer_settings();
3660 }
3661 if let Some(via_ir) = config.mutation.via_ir {
3662 config.via_ir = via_ir;
3663 }
3664}
3665
3666fn list<FEN: FoundryEvmNetwork>(
3668 runner: MultiContractRunner<FEN>,
3669 filter: &ProjectPathsAwareFilter,
3670) -> Result<TestOutcome> {
3671 let results = runner.list(filter);
3672 print_list_results(&results)?;
3673 Ok(TestOutcome::empty(Some(runner.known_contracts), false))
3674}
3675
3676fn list_from_output(
3677 output: &ProjectCompileOutput,
3678 config: &Config,
3679 inline_config: &InlineConfig,
3680 filter: &ProjectPathsAwareFilter,
3681 fuzz_only: bool,
3682 symbolic_artifact_replay: Option<&SymbolicArtifactReplayConfig>,
3683) -> Result<TestOutcome> {
3684 let matcher = TestFunctionMatcher::new(config, inline_config, symbolic_artifact_replay);
3685 let results = output
3686 .artifact_ids()
3687 .filter_map(|(id, artifact)| {
3688 let abi = artifact.abi.as_ref()?;
3689 let id = id.with_stripped_file_prefixes(&config.root);
3690 let deployable = abi
3691 .constructor
3692 .as_ref()
3693 .map(|constructor| constructor.inputs.is_empty())
3694 .unwrap_or(true);
3695 if !deployable || !matcher.matches_contract(filter, &id, abi) {
3696 return None;
3697 }
3698 let source = id.source.as_path().display().to_string();
3699 let identifier = id.identifier();
3700 let name = id.name;
3701 let generated_symbolic_regression = is_generated_symbolic_regression_contract(abi);
3702 let tests = abi
3703 .functions()
3704 .filter(|func| {
3705 let kind = matcher.test_function_kind(
3706 &identifier,
3707 func,
3708 generated_symbolic_regression,
3709 );
3710 (!fuzz_only
3711 || matches!(
3712 kind,
3713 TestFunctionKind::FuzzTest { .. } | TestFunctionKind::InvariantTest
3714 ))
3715 && filter.matches_test_function_kind_in_contract(&identifier, func, kind)
3716 })
3717 .map(|func| func.name.clone())
3718 .collect::<Vec<_>>();
3719 (!tests.is_empty()).then_some((source, name, tests))
3720 })
3721 .fold(
3722 BTreeMap::<String, BTreeMap<String, Vec<String>>>::new(),
3723 |mut acc, (source, name, tests)| {
3724 acc.entry(source).or_default().insert(name, tests);
3725 acc
3726 },
3727 );
3728
3729 print_list_results(&results)?;
3730 Ok(TestOutcome::empty(None, false))
3731}
3732
3733fn matched_engine_counts(
3734 output: &ProjectCompileOutput,
3735 config: &Config,
3736 inline_config: &InlineConfig,
3737 filter: &ProjectPathsAwareFilter,
3738 multi_network: &MultiNetworkConfig,
3739) -> MatchedEngineCounts {
3740 let matcher = TestFunctionMatcher::new(config, inline_config, None);
3741 output
3742 .artifact_ids()
3743 .filter_map(|(id, artifact)| artifact.abi.as_ref().map(|abi| (id, abi)))
3744 .filter_map(|(id, abi)| {
3745 let id = id.with_stripped_file_prefixes(&config.root);
3746 let deployable = abi
3747 .constructor
3748 .as_ref()
3749 .map(|constructor| constructor.inputs.is_empty())
3750 .unwrap_or(true);
3751 if !deployable || !matcher.matches_contract(filter, &id, abi) {
3752 return None;
3753 }
3754
3755 let contract_name = id.identifier();
3756 let generated_symbolic_regression = is_generated_symbolic_regression_contract(abi);
3757 let fuzz = abi
3758 .functions()
3759 .filter_map(|func| {
3760 let kind = matcher.test_function_kind(
3761 &contract_name,
3762 func,
3763 generated_symbolic_regression,
3764 );
3765 matches!(kind, TestFunctionKind::FuzzTest { .. }).then_some((func, kind))
3766 })
3767 .filter(|(func, kind)| {
3768 filter.matches_test_function_kind_in_contract(&contract_name, func, *kind)
3769 })
3770 .filter(|(func, _)| {
3771 function_matches_network_pass(
3772 &multi_network.all_override_networks,
3773 multi_network.pass_network.as_ref(),
3774 inline_config.network_for(&config.profile, &contract_name, &func.name),
3775 )
3776 })
3777 .count();
3778 let invariant = count_runnable_invariant_campaign_anchors(
3779 abi,
3780 filter,
3781 crate::runner::InvariantCampaignScope {
3782 config,
3783 inline_config,
3784 contract_name: &contract_name,
3785 all_override_networks: &multi_network.all_override_networks,
3786 pass_network: multi_network.pass_network.as_ref(),
3787 },
3788 );
3789 Some(MatchedEngineCounts { fuzz, invariant })
3790 })
3791 .fold(MatchedEngineCounts::default(), |mut acc, counts| {
3792 acc.fuzz += counts.fuzz;
3793 acc.invariant += counts.invariant;
3794 acc
3795 })
3796}
3797
3798fn print_list_results(results: &BTreeMap<String, BTreeMap<String, Vec<String>>>) -> Result<()> {
3799 if shell::is_json() {
3800 sh_println!("{}", serde_json::to_string(&results)?)?;
3801 } else {
3802 for (file, contracts) in results {
3803 sh_println!("{file}")?;
3804 for (contract, tests) in contracts {
3805 sh_println!(" {contract}")?;
3806 sh_println!(" {}\n", tests.join("\n "))?;
3807 }
3808 }
3809 }
3810 Ok(())
3811}
3812
3813fn merge_outcomes(base: &mut TestOutcome, mut other: TestOutcome) {
3818 if let Some(other_results) = other.json_file_results.take() {
3819 let base_results = base.json_file_results.get_or_insert_with(|| base.results.clone());
3820 merge_suite_results(base_results, other_results);
3821 }
3822 merge_suite_results(&mut base.results, other.results);
3823 if let Some(decoder) = other.last_run_decoder {
3824 base.last_run_decoder = Some(decoder);
3825 }
3826}
3827
3828fn merge_suite_results(
3829 base: &mut BTreeMap<String, SuiteResult>,
3830 other: BTreeMap<String, SuiteResult>,
3831) {
3832 for (suite_id, other_suite) in other {
3833 match base.entry(suite_id) {
3834 std::collections::btree_map::Entry::Vacant(e) => {
3835 e.insert(other_suite);
3836 }
3837 std::collections::btree_map::Entry::Occupied(mut e) => {
3838 let base_suite = e.get_mut();
3839 base_suite.test_results.extend(other_suite.test_results);
3840 base_suite.warnings.extend(other_suite.warnings);
3841 base_suite.duration = base_suite.duration.max(other_suite.duration);
3842 }
3843 }
3844 }
3845}
3846
3847fn collect_matching_debug_tests(
3848 matching_tests: &BTreeMap<String, BTreeMap<String, Vec<String>>>,
3849) -> Vec<RerunFailure> {
3850 let mut tests = Vec::new();
3851 for (source, contracts) in matching_tests {
3852 for (contract, contract_tests) in contracts {
3853 let contract = format!("{source}:{contract}");
3854 tests.extend(
3855 contract_tests
3856 .iter()
3857 .map(|test| RerunFailure { contract: contract.clone(), test: test.clone() }),
3858 );
3859 }
3860 }
3861 tests
3862}
3863
3864fn format_matching_debug_tests(matching_tests: &[RerunFailure]) -> Option<String> {
3865 if matching_tests.is_empty() {
3866 return None;
3867 }
3868
3869 let mut output = String::from("\n\nMatching tests:");
3870 for test in matching_tests.iter().take(DEBUGGER_MATCHING_TESTS_DISPLAY_LIMIT) {
3871 output.push_str("\n ");
3872 output.push_str(&test.contract);
3873 output.push('.');
3874 output.push_str(&test.test);
3875 }
3876
3877 if matching_tests.len() > DEBUGGER_MATCHING_TESTS_DISPLAY_LIMIT {
3878 output.push_str(&format!(
3879 "\n ... and {} more",
3880 matching_tests.len() - DEBUGGER_MATCHING_TESTS_DISPLAY_LIMIT
3881 ));
3882 }
3883
3884 Some(output)
3885}
3886
3887struct LastRunFailures {
3888 test_pattern: Option<regex::Regex>,
3889 failures: Option<Vec<RerunFailure>>,
3890}
3891
3892fn last_run_failures(config: &Config) -> LastRunFailures {
3894 let Ok(filter) = fs::read_to_string(&config.test_failures_file) else {
3895 return LastRunFailures { test_pattern: None, failures: None };
3896 };
3897
3898 if let Ok(failures) = serde_json::from_str::<RerunFailures>(&filter) {
3899 if failures.failures.is_empty() {
3900 return LastRunFailures { test_pattern: None, failures: None };
3901 }
3902 let test_pattern = failures
3903 .failures
3904 .iter()
3905 .map(|failure| regex::escape(&failure.test))
3906 .collect::<Vec<_>>()
3907 .join("|");
3908 let test_pattern = Regex::new(&test_pattern).ok();
3909 return LastRunFailures { test_pattern, failures: Some(failures.failures) };
3910 }
3911
3912 let test_pattern = Regex::new(&filter)
3913 .inspect_err(|e| {
3914 _ = sh_warn!("failed to parse test filter from {:?}: {e}", config.test_failures_file)
3915 })
3916 .ok();
3917 LastRunFailures { test_pattern, failures: None }
3918}
3919
3920fn persist_run_failures(config: &Config, outcome: &TestOutcome) {
3922 if outcome.failed() > 0 && fs::create_file(&config.test_failures_file).is_ok() {
3923 let failures = outcome
3924 .results
3925 .iter()
3926 .flat_map(|(contract, suite)| {
3927 suite.test_results.iter().filter(|(_, result)| result.status.is_failure()).flat_map(
3928 move |(test_name, test_result)| {
3929 rerun_filter_matches(test_name, test_result)
3930 .map(move |test| RerunFailure { contract: contract.clone(), test })
3931 },
3932 )
3933 })
3934 .collect::<Vec<_>>();
3935
3936 let output = serde_json::to_string(&RerunFailures { version: 1, failures });
3937 if let Ok(output) = output {
3938 let _ = fs::write(&config.test_failures_file, output);
3939 }
3940 }
3941}
3942
3943fn rerun_filter_matches<'a>(
3944 test_name: &'a str,
3945 test_result: &'a TestResult,
3946) -> impl Iterator<Item = String> + 'a {
3947 let has_predicate_failures =
3948 test_result.invariant_failures.iter().any(|failure| failure.predicate_name().is_some());
3949 let predicate_failures =
3950 test_result.invariant_failures.iter().filter_map(|failure| failure.predicate_name());
3951
3952 let fallback = test_name.is_any_test().then(|| test_name.split('(').next()).flatten();
3953
3954 predicate_failures
3955 .chain(fallback.into_iter().filter(move |_| !has_predicate_failures))
3956 .map(str::to_owned)
3957}
3958
3959fn junit_xml_report(results: &BTreeMap<String, SuiteResult>, verbosity: u8) -> Report {
3961 let mut total_duration = Duration::default();
3962 let mut junit_report = Report::new("Test run");
3963 junit_report.set_timestamp(Utc::now());
3964 for (suite_name, suite_result) in results {
3965 let mut test_suite = TestSuite::new(suite_name);
3966 total_duration += suite_result.duration;
3967 test_suite.set_time(suite_result.duration);
3968 test_suite.set_system_out(suite_result.summary());
3969 for (test_name, test_result) in &suite_result.test_results {
3970 add_junit_test_cases(&mut test_suite, test_name, test_result, verbosity);
3971 }
3972 junit_report.add_test_suite(test_suite);
3973 }
3974 junit_report.set_time(total_duration);
3975 junit_report
3976}
3977
3978fn add_junit_test_cases(
3983 test_suite: &mut TestSuite,
3984 test_name: &str,
3985 test_result: &TestResult,
3986 verbosity: u8,
3987) {
3988 let output = JunitOutput::new(test_result, verbosity);
3989 let expanded_invariant = test_result.kind.is_invariant()
3990 && (!test_result.invariant_predicate_results.is_empty()
3991 || !test_result.invariant_handler_failures.is_empty());
3992
3993 if !expanded_invariant {
3994 add_junit_test_case(
3995 test_suite,
3996 test_name,
3997 test_result.status,
3998 test_result.reason.as_deref(),
3999 test_result,
4000 output.system_out(test_result, test_name),
4001 );
4002 return;
4003 }
4004
4005 let mut add_expanded_case =
4006 |name: &str,
4007 status: TestStatus,
4008 reason: Option<&str>,
4009 counterexample: Option<&CounterExample>| {
4010 add_junit_test_case(
4011 test_suite,
4012 name,
4013 status,
4014 reason,
4015 test_result,
4016 output.case_system_out(status, reason, name, counterexample),
4017 );
4018 };
4019
4020 if test_result.invariant_predicate_results.is_empty() {
4021 let failure = test_result.invariant_failures.first();
4022 let status = if failure.is_some() { TestStatus::Failure } else { TestStatus::Success };
4023 add_expanded_case(
4024 test_name,
4025 status,
4026 failure.map(|failure| failure.reason()),
4027 failure.and_then(|failure| failure.counterexample()),
4028 );
4029 } else {
4030 for predicate in &test_result.invariant_predicate_results {
4031 let failure = test_result
4032 .invariant_failures
4033 .iter()
4034 .find(|failure| failure.name() == predicate.name.as_str());
4035 let name = format!("{}()", predicate.name);
4036 add_expanded_case(
4037 &name,
4038 predicate.status,
4039 predicate.reason.as_deref(),
4040 failure.and_then(|failure| failure.counterexample()),
4041 );
4042 }
4043 }
4044
4045 for failure in &test_result.invariant_handler_failures {
4046 let name = format!("handler {}", failure.name());
4047 add_expanded_case(
4048 &name,
4049 TestStatus::Failure,
4050 Some(failure.reason()),
4051 failure.counterexample(),
4052 );
4053 }
4054}
4055
4056fn add_junit_test_case(
4058 test_suite: &mut TestSuite,
4059 test_name: &str,
4060 status: TestStatus,
4061 message: Option<&str>,
4062 test_result: &TestResult,
4063 system_out: String,
4064) {
4065 let mut test_status = match status {
4066 TestStatus::Success => TestCaseStatus::success(),
4067 TestStatus::Failure => TestCaseStatus::non_success(NonSuccessKind::Failure),
4068 TestStatus::Skipped => TestCaseStatus::skipped(),
4069 };
4070 if let Some(message) = message {
4071 test_status.set_message(message);
4072 }
4073
4074 let mut test_case = TestCase::new(test_name, test_status);
4075 test_case.set_time(test_result.duration);
4076 test_case.set_system_out(system_out);
4077 test_suite.add_test_case(test_case);
4078}
4079
4080struct JunitOutput {
4082 result_report: TestKindReport,
4083 logs: Option<Vec<String>>,
4084}
4085
4086impl JunitOutput {
4087 fn new(test_result: &TestResult, verbosity: u8) -> Self {
4089 Self {
4090 result_report: test_result.kind.report(),
4091 logs: (verbosity >= 2 && !test_result.logs.is_empty())
4092 .then(|| decode_console_logs(&test_result.logs)),
4093 }
4094 }
4095
4096 fn system_out(&self, test_result: &TestResult, test_name: &str) -> String {
4098 let mut sys_out = String::new();
4099 write!(sys_out, "{test_result} {test_name} {}", self.result_report).unwrap();
4100 self.append_logs(&mut sys_out);
4101 sys_out
4102 }
4103
4104 fn case_system_out(
4106 &self,
4107 status: TestStatus,
4108 message: Option<&str>,
4109 test_name: &str,
4110 counterexample: Option<&CounterExample>,
4111 ) -> String {
4112 let mut sys_out = String::new();
4113 match status {
4114 TestStatus::Success => write!(sys_out, "[PASS]").unwrap(),
4115 TestStatus::Failure => {
4116 let message = message.unwrap_or_default();
4117 write!(sys_out, "[FAIL: {message}]").unwrap();
4118 }
4119 TestStatus::Skipped => {
4120 if let Some(message) = message {
4121 write!(sys_out, "[SKIP: {message}]").unwrap();
4122 } else {
4123 write!(sys_out, "[SKIP]").unwrap();
4124 }
4125 }
4126 }
4127 write!(sys_out, " {test_name} {}", self.result_report).unwrap();
4128 if let Some(CounterExample::Sequence(original, sequence)) = counterexample {
4129 writeln!(sys_out, "\n\t[Sequence] (original: {original}, shrunk: {})", sequence.len())
4130 .unwrap();
4131 for ex in sequence {
4132 writeln!(sys_out, "{ex}").unwrap();
4133 }
4134 }
4135 self.append_logs(&mut sys_out);
4136 sys_out
4137 }
4138
4139 fn append_logs(&self, sys_out: &mut String) {
4141 if let Some(logs) = &self.logs {
4142 write!(sys_out, "\\nLogs:\\n").unwrap();
4143 for log in logs {
4144 write!(sys_out, " {log}\\n").unwrap();
4145 }
4146 }
4147 }
4148}
4149
4150#[cfg(test)]
4151mod tests {
4152 use super::*;
4153 use foundry_config::Chain;
4154
4155 #[test]
4156 fn watch_parse() {
4157 let args: TestArgs = TestArgs::parse_from(["foundry-cli", "-vw"]);
4158 assert!(args.watch.watch.is_some());
4159 }
4160
4161 #[test]
4162 fn fuzz_seed() {
4163 let args: TestArgs = TestArgs::parse_from(["foundry-cli", "--fuzz-seed", "0x10"]);
4164 assert!(args.fuzz_seed.is_some());
4165 }
4166
4167 #[test]
4168 fn showmap_override_validates_path_component_names() {
4169 let mut args = TestArgs::parse_from(["foundry-cli"]);
4170 args.set_showmap_override(ShowmapConfig {
4171 out_dir: PathBuf::from("showmap"),
4172 approach: "../outside".to_string(),
4173 trial: "trial".to_string(),
4174 per_input: false,
4175 domain: ShowmapDomain::Evm,
4176 corpus_dir: None,
4177 emit_files: false,
4178 });
4179
4180 let err = args.showmap_config().unwrap_err().to_string();
4181 assert!(err.contains("expected a single file-name component"), "{err}");
4182 }
4183
4184 #[test]
4185 fn depth_trace() {
4186 let args: TestArgs = TestArgs::parse_from(["foundry-cli", "--trace-depth", "2"]);
4187 assert!(args.tracing.trace_depth.is_some());
4188 }
4189
4190 #[test]
4191 fn compact_labels_trace() {
4192 let args: TestArgs = TestArgs::parse_from(["foundry-cli", "--compact-labels"]);
4193 assert!(args.tracing.compact_labels);
4194 }
4195
4196 #[test]
4197 fn silent_output_disables_trace_rendering() {
4198 assert!(!should_render_trace_output(true, true));
4199 assert!(!should_render_trace_output(false, false));
4200 assert!(should_render_trace_output(false, true));
4201 }
4202
4203 #[test]
4204 fn debugger_test_candidates_preserve_exact_suite_ids() {
4205 let matching = BTreeMap::from([(
4206 "test/Counter.t.sol".to_string(),
4207 BTreeMap::from([(
4208 "CounterTest".to_string(),
4209 vec!["testFuzz_SetNumber(uint256)".to_string(), "test_Increment()".to_string()],
4210 )]),
4211 )]);
4212
4213 let candidates = collect_matching_debug_tests(&matching);
4214
4215 assert_eq!(candidates[0].contract, "test/Counter.t.sol:CounterTest");
4216 assert_eq!(candidates[0].test, "testFuzz_SetNumber(uint256)");
4217 assert_eq!(candidates[1].test, "test_Increment()");
4218 assert_eq!(
4219 format_matching_debug_tests(&candidates).unwrap(),
4220 "\n\nMatching tests:\n test/Counter.t.sol:CounterTest.testFuzz_SetNumber(uint256)\n test/Counter.t.sol:CounterTest.test_Increment()"
4221 );
4222 }
4223
4224 #[test]
4226 fn fuzz_seed_exists() {
4227 let args: TestArgs =
4228 TestArgs::parse_from(["foundry-cli", "-vvv", "--gas-report", "--fuzz-seed", "0x10"]);
4229 assert!(args.fuzz_seed.is_some());
4230 }
4231
4232 #[test]
4233 fn fuzz_run() {
4234 let args: TestArgs = TestArgs::parse_from(["foundry-cli", "--fuzz-run", "10"]);
4235 assert_eq!(args.fuzz_run, Some(10));
4236 assert_eq!(args.fuzz_worker, None);
4237 }
4238
4239 #[test]
4240 fn fuzz_run_adapter_writes_unified_campaign_dials_sparsely() {
4241 let args = FuzzRunArgs::parse_from([
4242 "foundry-cli",
4243 "--runs",
4244 "9",
4245 "--timeout",
4246 "3",
4247 "--seed",
4248 "0x10",
4249 "--depth",
4250 "7",
4251 "--workers",
4252 "2",
4253 ]);
4254 let args = TestArgs::from_fuzz_run(args);
4255 let figment = figment::Figment::from(&args);
4256
4257 assert_eq!(figment.extract_inner::<u64>("fuzz.runs").unwrap(), 9);
4258 assert_eq!(figment.extract_inner::<u64>("fuzz.timeout").unwrap(), 3);
4259 assert_eq!(figment.extract_inner::<String>("fuzz.seed").unwrap(), "16");
4260 assert_eq!(figment.extract_inner::<u64>("invariant.runs").unwrap(), 9);
4261 assert_eq!(figment.extract_inner::<u32>("invariant.timeout").unwrap(), 3);
4262 assert_eq!(figment.extract_inner::<u32>("invariant.depth").unwrap(), 7);
4263 assert_eq!(
4264 figment.extract_inner::<InvariantWorkers>("invariant.workers").unwrap(),
4265 InvariantWorkers::Fixed(std::num::NonZeroUsize::new(2).unwrap())
4266 );
4267 }
4268
4269 #[test]
4270 fn fuzz_run_adapter_writes_invariant_workers_sparsely() {
4271 let args = TestArgs::from_fuzz_run(FuzzRunArgs::parse_from(["foundry-cli"]));
4272 let figment = figment::Figment::from(&args);
4273
4274 assert_eq!(args.invariant_workers, None);
4275 assert!(figment.extract_inner::<InvariantWorkers>("invariant.workers").is_err());
4276 }
4277
4278 #[test]
4279 fn mutation_compiler_overrides_are_extracted() {
4280 let args = TestArgs::parse_from([
4281 "foundry-cli",
4282 "--mutate",
4283 "--mutation-optimizer-runs",
4284 "1",
4285 "--mutation-via-ir",
4286 "false",
4287 ]);
4288 assert_eq!(args.mutation_optimizer_runs, Some(1));
4289 assert_eq!(args.mutation_via_ir, Some(false));
4290
4291 let figment = figment::Figment::from(&args);
4292 assert_eq!(figment.extract_inner::<u32>("mutation.optimizer_runs").unwrap(), 1);
4293 assert!(!figment.extract_inner::<bool>("mutation.via_ir").unwrap());
4294 }
4295
4296 #[test]
4297 fn mutation_compiler_overrides_update_only_mutation_config_clone() {
4298 let mut config = Config {
4299 optimizer_runs: Some(999),
4300 via_ir: true,
4301 mutation: foundry_config::MutationConfig {
4302 optimizer_runs: Some(1),
4303 via_ir: Some(false),
4304 ..Default::default()
4305 },
4306 ..Default::default()
4307 };
4308
4309 apply_mutation_compiler_overrides(&mut config);
4310
4311 assert_eq!(config.optimizer_runs, Some(1));
4312 assert!(!config.via_ir);
4313 }
4314
4315 #[test]
4316 fn mutation_optimizer_runs_normalize_default_optimizer_settings() {
4317 let mut config = Config {
4318 optimizer: Some(false),
4319 optimizer_runs: Some(200),
4320 mutation: foundry_config::MutationConfig {
4321 optimizer_runs: Some(1),
4322 ..Default::default()
4323 },
4324 ..Default::default()
4325 };
4326
4327 apply_mutation_compiler_overrides(&mut config);
4328
4329 assert_eq!(config.optimizer, Some(true));
4330 assert_eq!(config.optimizer_runs, Some(1));
4331 }
4332
4333 #[test]
4334 fn invariant_workers() {
4335 let args = TestArgs::parse_from(["foundry-cli", "--invariant-workers", "4"]);
4336 assert_eq!(
4337 args.invariant_workers,
4338 Some(InvariantWorkers::Fixed(std::num::NonZeroUsize::new(4).unwrap()))
4339 );
4340
4341 let figment = figment::Figment::from(&args);
4342 assert_eq!(
4343 figment.extract_inner::<InvariantWorkers>("invariant.workers").unwrap(),
4344 InvariantWorkers::Fixed(std::num::NonZeroUsize::new(4).unwrap())
4345 );
4346 }
4347
4348 #[test]
4349 fn invariant_workers_accepts_auto() {
4350 let args = TestArgs::parse_from(["foundry-cli", "--invariant-workers", "auto"]);
4351 assert_eq!(args.invariant_workers, Some(InvariantWorkers::Auto));
4352
4353 let figment = figment::Figment::from(&args);
4354 assert_eq!(
4355 figment.extract_inner::<InvariantWorkers>("invariant.workers").unwrap(),
4356 InvariantWorkers::Auto
4357 );
4358 }
4359
4360 #[test]
4361 fn invariant_workers_env_accepts_auto() {
4362 static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
4363
4364 let _guard = ENV_LOCK.lock().unwrap();
4365 let previous = std::env::var_os("FOUNDRY_INVARIANT_WORKERS");
4366 unsafe {
4367 std::env::set_var("FOUNDRY_INVARIANT_WORKERS", "auto");
4368 }
4369
4370 let args = TestArgs::try_parse_from(["foundry-cli"]);
4371
4372 unsafe {
4373 if let Some(previous) = previous {
4374 std::env::set_var("FOUNDRY_INVARIANT_WORKERS", previous);
4375 } else {
4376 std::env::remove_var("FOUNDRY_INVARIANT_WORKERS");
4377 }
4378 }
4379
4380 assert_eq!(args.unwrap().invariant_workers, Some(InvariantWorkers::Auto));
4381 }
4382
4383 #[test]
4384 fn corpus_dir_env_vars_are_parsed() {
4385 static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
4386
4387 let _guard = ENV_LOCK.lock().unwrap();
4388 let previous_fuzz = std::env::var_os("FOUNDRY_FUZZ_CORPUS_DIR");
4389 let previous_invariant = std::env::var_os("FOUNDRY_INVARIANT_CORPUS_DIR");
4390 unsafe {
4391 std::env::set_var("FOUNDRY_FUZZ_CORPUS_DIR", "env_fuzz_corpus");
4392 std::env::set_var("FOUNDRY_INVARIANT_CORPUS_DIR", "env_invariant_corpus");
4393 }
4394
4395 let args = TestArgs::try_parse_from(["foundry-cli"]);
4396
4397 unsafe {
4398 if let Some(previous) = previous_fuzz {
4399 std::env::set_var("FOUNDRY_FUZZ_CORPUS_DIR", previous);
4400 } else {
4401 std::env::remove_var("FOUNDRY_FUZZ_CORPUS_DIR");
4402 }
4403 if let Some(previous) = previous_invariant {
4404 std::env::set_var("FOUNDRY_INVARIANT_CORPUS_DIR", previous);
4405 } else {
4406 std::env::remove_var("FOUNDRY_INVARIANT_CORPUS_DIR");
4407 }
4408 }
4409
4410 let args = args.unwrap();
4411 assert_eq!(args.fuzz_corpus_dir, Some(PathBuf::from("env_fuzz_corpus")));
4412 assert_eq!(args.invariant_corpus_dir, Some(PathBuf::from("env_invariant_corpus")));
4413 }
4414
4415 #[test]
4416 fn auto_fuzz_corpus_defaults_to_cache_failure_layout() {
4417 let mut args = TestArgs::parse_from(["foundry-cli"]);
4418 args.enable_fuzz_only_with_auto_fuzz_corpus();
4419 let mut config = Config::default();
4420
4421 args.apply_auto_fuzz_corpus_dir(&mut config);
4422
4423 assert_eq!(
4424 config.fuzz.corpus.corpus_dir,
4425 Some(config.cache_path.join(AUTO_FUZZ_FAILURE_DIR).join(AUTO_CORPUS_DIR))
4426 );
4427 assert_eq!(config.invariant.corpus.corpus_dir, None);
4428 }
4429
4430 #[test]
4431 fn auto_fuzz_corpus_uses_configured_failure_persist_dirs() {
4432 let mut args = TestArgs::parse_from(["foundry-cli"]);
4433 args.enable_fuzz_only_with_auto_fuzz_corpus();
4434 let mut config = Config::default();
4435 config.fuzz.failure_persist_dir = Some(PathBuf::from("custom_fuzz_failures"));
4436
4437 args.apply_auto_fuzz_corpus_dir(&mut config);
4438
4439 assert_eq!(
4440 config.fuzz.corpus.corpus_dir,
4441 Some(PathBuf::from("custom_fuzz_failures").join(AUTO_CORPUS_DIR))
4442 );
4443 assert_eq!(config.invariant.corpus.corpus_dir, None);
4444 }
4445
4446 #[test]
4447 fn auto_fuzz_corpus_preserves_configured_corpus_dirs() {
4448 let mut args = TestArgs::parse_from(["foundry-cli"]);
4449 args.enable_fuzz_only_with_auto_fuzz_corpus();
4450 let mut config = Config::default();
4451 config.fuzz.corpus.corpus_dir = Some(PathBuf::from("configured_fuzz_corpus"));
4452 config.invariant.corpus.corpus_dir = Some(PathBuf::from("configured_invariant_corpus"));
4453
4454 args.apply_auto_fuzz_corpus_dir(&mut config);
4455
4456 assert_eq!(config.fuzz.corpus.corpus_dir, Some(PathBuf::from("configured_fuzz_corpus")));
4457 assert_eq!(
4458 config.invariant.corpus.corpus_dir,
4459 Some(PathBuf::from("configured_invariant_corpus"))
4460 );
4461 }
4462
4463 #[test]
4464 fn fuzz_only_does_not_enable_auto_fuzz_corpus() {
4465 let mut args = TestArgs::parse_from(["foundry-cli"]);
4466 args.enable_fuzz_only();
4467 let mut config = Config::default();
4468
4469 args.apply_auto_fuzz_corpus_dir(&mut config);
4470
4471 assert_eq!(config.fuzz.corpus.corpus_dir, None);
4472 assert_eq!(config.invariant.corpus.corpus_dir, None);
4473 }
4474
4475 #[test]
4476 fn fuzz_and_invariant_config_flags() {
4477 let args = TestArgs::parse_from([
4478 "foundry-cli",
4479 "--fuzz-dictionary-weight",
4480 "35",
4481 "--fuzz-dictionary-addresses",
4482 "max",
4483 "--fuzz-dictionary-values",
4484 "1234",
4485 "--fuzz-dictionary-literals",
4486 "4321",
4487 "--fuzz-corpus-random-sequence-weight",
4488 "55",
4489 "--fuzz-corpus-dir",
4490 "fuzz_corpus",
4491 "--fuzz-frontier-dir",
4492 "fuzz_frontiers",
4493 "--fuzz-frontier-limit",
4494 "7",
4495 "--fuzz-payable-value-weight",
4496 "12",
4497 "--fuzz-mutation-weight-splice",
4498 "4",
4499 "--fuzz-mutation-weight-abi",
4500 "3",
4501 "--fuzz-mutation-weight-cmp",
4502 "5",
4503 "--symbolic-use-fuzz-frontiers",
4504 "--symbolic-frontier-limit",
4505 "3",
4506 "--symbolic-frontier-ids",
4507 "4,9",
4508 "--symbolic-frontier-pcs",
4509 "123,456",
4510 "--symbolic-frontier-selectors",
4511 "0x12345678,deadbeef",
4512 "--invariant-depth",
4513 "300",
4514 "--invariant-min-depth",
4515 "20",
4516 "--invariant-depth-mode",
4517 "random",
4518 "--invariant-workers",
4519 "4",
4520 "--invariant-dictionary-weight",
4521 "45",
4522 "--invariant-dictionary-addresses",
4523 "8765",
4524 "--invariant-dictionary-values",
4525 "max",
4526 "--invariant-dictionary-literals",
4527 "6789",
4528 "--invariant-corpus-random-sequence-weight",
4529 "25",
4530 "--invariant-corpus-dir",
4531 "invariant_corpus",
4532 "--invariant-payable-value-weight",
4533 "34",
4534 "--invariant-mutation-weight-splice",
4535 "2",
4536 "--invariant-mutation-weight-cmp",
4537 "7",
4538 ]);
4539
4540 let figment = figment::Figment::from(&args);
4541 assert_eq!(figment.extract_inner::<u32>("fuzz.dictionary_weight").unwrap(), 35);
4542 assert_eq!(
4543 figment.extract_inner::<String>("fuzz.max_fuzz_dictionary_addresses").unwrap(),
4544 "max"
4545 );
4546 assert_eq!(
4547 figment.extract_inner::<String>("fuzz.max_fuzz_dictionary_values").unwrap(),
4548 "1234"
4549 );
4550 assert_eq!(
4551 figment.extract_inner::<String>("fuzz.max_fuzz_dictionary_literals").unwrap(),
4552 "4321"
4553 );
4554 assert_eq!(figment.extract_inner::<u32>("fuzz.corpus_random_sequence_weight").unwrap(), 55);
4555 assert_eq!(
4556 figment.extract_inner::<PathBuf>("fuzz.corpus_dir").unwrap(),
4557 PathBuf::from("fuzz_corpus")
4558 );
4559 assert_eq!(
4560 figment.extract_inner::<PathBuf>("fuzz.frontier_dir").unwrap(),
4561 PathBuf::from("fuzz_frontiers")
4562 );
4563 assert_eq!(figment.extract_inner::<usize>("fuzz.frontier_limit").unwrap(), 7);
4564 assert_eq!(figment.extract_inner::<u32>("fuzz.payable_value_weight").unwrap(), 12);
4565 assert_eq!(figment.extract_inner::<u32>("fuzz.mutation_weight_splice").unwrap(), 4);
4566 assert_eq!(figment.extract_inner::<u32>("fuzz.mutation_weight_abi").unwrap(), 3);
4567 assert_eq!(figment.extract_inner::<u32>("fuzz.mutation_weight_cmp").unwrap(), 5);
4568 assert!(figment.extract_inner::<bool>("symbolic.use_fuzz_frontiers").unwrap());
4569 assert_eq!(figment.extract_inner::<usize>("symbolic.frontier_limit").unwrap(), 3);
4570 assert_eq!(figment.extract_inner::<Vec<u64>>("symbolic.frontier_ids").unwrap(), vec![4, 9]);
4571 assert_eq!(
4572 figment.extract_inner::<Vec<usize>>("symbolic.frontier_pcs").unwrap(),
4573 vec![123, 456]
4574 );
4575 assert_eq!(
4576 figment.extract_inner::<Vec<String>>("symbolic.frontier_selectors").unwrap(),
4577 vec!["0x12345678", "deadbeef"]
4578 );
4579 assert_eq!(figment.extract_inner::<u32>("invariant.depth").unwrap(), 300);
4580 assert_eq!(figment.extract_inner::<u32>("invariant.min_depth").unwrap(), 20);
4581 assert_eq!(
4582 figment.extract_inner::<InvariantDepthMode>("invariant.depth_mode").unwrap(),
4583 InvariantDepthMode::Random
4584 );
4585 assert_eq!(figment.extract_inner::<u32>("invariant.dictionary_weight").unwrap(), 45);
4586 assert_eq!(
4587 figment.extract_inner::<String>("invariant.max_fuzz_dictionary_addresses").unwrap(),
4588 "8765"
4589 );
4590 assert_eq!(
4591 figment.extract_inner::<String>("invariant.max_fuzz_dictionary_values").unwrap(),
4592 "max"
4593 );
4594 assert_eq!(
4595 figment.extract_inner::<String>("invariant.max_fuzz_dictionary_literals").unwrap(),
4596 "6789"
4597 );
4598 assert_eq!(
4599 figment.extract_inner::<u32>("invariant.corpus_random_sequence_weight").unwrap(),
4600 25
4601 );
4602 assert_eq!(
4603 figment.extract_inner::<PathBuf>("invariant.corpus_dir").unwrap(),
4604 PathBuf::from("invariant_corpus")
4605 );
4606 assert_eq!(figment.extract_inner::<u32>("invariant.payable_value_weight").unwrap(), 34);
4607 assert_eq!(figment.extract_inner::<u32>("invariant.mutation_weight_splice").unwrap(), 2);
4608 assert_eq!(figment.extract_inner::<u32>("invariant.mutation_weight_cmp").unwrap(), 7);
4609
4610 let config = Config::default().merge_inline_provider(&args).unwrap();
4611 assert_eq!(config.fuzz.dictionary.dictionary_weight, 35);
4612 assert_eq!(config.fuzz.dictionary.max_fuzz_dictionary_addresses, usize::MAX);
4613 assert_eq!(config.fuzz.dictionary.max_fuzz_dictionary_values, 1234);
4614 assert_eq!(config.fuzz.dictionary.max_fuzz_dictionary_literals, 4321);
4615 assert_eq!(config.fuzz.corpus.corpus_random_sequence_weight, 55);
4616 assert_eq!(config.fuzz.corpus.corpus_dir, Some(PathBuf::from("fuzz_corpus")));
4617 assert_eq!(config.fuzz.corpus.frontier_dir, Some(PathBuf::from("fuzz_frontiers")));
4618 assert_eq!(config.fuzz.corpus.frontier_limit, 7);
4619 assert_eq!(config.fuzz.corpus.payable_value_weight, 12);
4620 assert_eq!(config.fuzz.corpus.mutation_weights.mutation_weight_splice, 4);
4621 assert_eq!(config.fuzz.corpus.mutation_weights.mutation_weight_abi, 3);
4622 assert_eq!(config.fuzz.corpus.mutation_weights.mutation_weight_cmp, 5);
4623 assert!(config.symbolic.use_fuzz_frontiers);
4624 assert_eq!(config.symbolic.frontier_limit, 3);
4625 assert_eq!(config.symbolic.frontier_ids, vec![4, 9]);
4626 assert_eq!(config.symbolic.frontier_pcs, vec![123, 456]);
4627 assert_eq!(config.symbolic.frontier_selectors, vec!["0x12345678", "deadbeef"]);
4628 assert_eq!(config.invariant.depth, 300);
4629 assert_eq!(config.invariant.min_depth, 20);
4630 assert_eq!(config.invariant.depth_mode, InvariantDepthMode::Random);
4631 assert_eq!(config.invariant.dictionary.dictionary_weight, 45);
4632 assert_eq!(config.invariant.dictionary.max_fuzz_dictionary_addresses, 8765);
4633 assert_eq!(config.invariant.dictionary.max_fuzz_dictionary_values, usize::MAX);
4634 assert_eq!(config.invariant.dictionary.max_fuzz_dictionary_literals, 6789);
4635 assert_eq!(config.invariant.corpus.corpus_random_sequence_weight, 25);
4636 assert_eq!(config.invariant.corpus.corpus_dir, Some(PathBuf::from("invariant_corpus")));
4637 assert!(config.invariant.corpus_random_sequence_weight_configured);
4638 assert_eq!(
4639 config.invariant.workers,
4640 InvariantWorkers::Fixed(std::num::NonZeroUsize::new(4).unwrap())
4641 );
4642 assert!(config.invariant.workers_configured);
4643 assert_eq!(config.invariant.corpus.payable_value_weight, 34);
4644 assert_eq!(config.invariant.corpus.mutation_weights.mutation_weight_splice, 2);
4645 assert_eq!(config.invariant.corpus.mutation_weights.mutation_weight_cmp, 7);
4646 }
4647
4648 #[test]
4649 fn extract_chain() {
4650 let test = |arg: &str, expected: Chain| {
4651 let args = TestArgs::parse_from(["foundry-cli", arg]);
4652 assert_eq!(args.evm.env.chain, Some(expected));
4653 let (config, evm_opts) = args.load_config_and_evm_opts().unwrap();
4654 assert_eq!(config.chain, Some(expected));
4655 assert_eq!(evm_opts.env.chain_id, Some(expected.id()));
4656 };
4657 test("--chain-id=1", Chain::mainnet());
4658 test("--chain-id=42", Chain::from_id(42));
4659 }
4660}