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