Skip to main content

forge/cmd/test/
mod.rs

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
126// Loads project's figment and merges the build cli arguments into it
127foundry_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/// Output format for EVM execution profiles.
289#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, ValueEnum)]
290pub enum EvmProfileFormat {
291    /// Speedscope format, opens in speedscope.app.
292    #[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/// CLI mirror of `foundry_evm::executors::ShowmapDomain`.
314#[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    // `MultiContractRunner::build` strips the root prefix from artifact source paths so the
394    // identifiers it constructs are project-relative. Match that here for the filter check
395    // (notably for the `--rerun` failure list, which is persisted relative) but return the
396    // original absolute source paths so downstream compilation can locate them.
397    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            // ABI-only compilation can omit test functions with invalid bodies, so preserve the
409            // existing filter behavior for conventional test files instead of treating them as
410            // fixtures.
411            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/// Shared campaign arguments for `forge fuzz run`.
426#[derive(Clone, Debug, Parser)]
427#[command(next_help_heading = "Campaign options")]
428pub struct CampaignArgs {
429    /// Number of runs to execute for each fuzz or invariant campaign.
430    #[arg(long, value_name = "RUNS")]
431    pub runs: Option<u64>,
432
433    /// Campaign-global timeout in seconds.
434    #[arg(long, value_name = "TIMEOUT")]
435    pub timeout: Option<u32>,
436
437    /// Set seed used to generate randomness during fuzz runs.
438    #[arg(long)]
439    pub seed: Option<U256>,
440
441    /// Number of calls executed to try to break invariants in one run.
442    #[arg(long, value_name = "DEPTH")]
443    pub depth: Option<u32>,
444
445    /// Minimum sampled invariant depth when `--depth-mode random` is active.
446    #[arg(long, value_name = "DEPTH")]
447    pub min_depth: Option<u32>,
448
449    /// How invariant run depth is selected.
450    #[arg(long, value_name = "fixed|random")]
451    pub depth_mode: Option<InvariantDepthMode>,
452
453    /// Number of workers to use for invariant test campaigns, or `auto` to derive from `--jobs`.
454    #[arg(long, value_name = "WORKERS")]
455    pub workers: Option<InvariantWorkers>,
456
457    /// Directory for fuzz and invariant corpus persistence.
458    #[arg(long, value_name = "PATH", value_hint = ValueHint::DirPath)]
459    pub corpus_dir: Option<PathBuf>,
460
461    /// Percent of calldata generated from the dictionary.
462    #[arg(long, value_name = "PERCENT")]
463    pub dictionary_weight: Option<u32>,
464
465    /// Maximum dictionary addresses, or `max`.
466    #[arg(long, value_name = "N|max")]
467    pub dictionary_addresses: Option<String>,
468
469    /// Maximum dictionary values, or `max`.
470    #[arg(long, value_name = "N|max")]
471    pub dictionary_values: Option<String>,
472
473    /// Maximum dictionary literals, or `max`.
474    #[arg(long, value_name = "N|max")]
475    pub dictionary_literals: Option<String>,
476
477    /// Percent chance that coverage-guided fuzzing generates fresh input instead of mutating
478    /// corpus input.
479    #[arg(long, value_name = "PERCENT")]
480    pub corpus_random_sequence_weight: Option<u32>,
481
482    /// Percent chance that fuzzed payable calls carry non-zero msg.value.
483    #[arg(long, value_name = "PERCENT")]
484    pub payable_value_weight: Option<u32>,
485
486    /// Corpus mutation weight for splice.
487    #[arg(long, value_name = "WEIGHT")]
488    pub mutation_weight_splice: Option<u32>,
489
490    /// Corpus mutation weight for repeat.
491    #[arg(long, value_name = "WEIGHT")]
492    pub mutation_weight_repeat: Option<u32>,
493
494    /// Corpus mutation weight for interleave.
495    #[arg(long, value_name = "WEIGHT")]
496    pub mutation_weight_interleave: Option<u32>,
497
498    /// Corpus mutation weight for prefix replacement.
499    #[arg(long, value_name = "WEIGHT")]
500    pub mutation_weight_prefix: Option<u32>,
501
502    /// Corpus mutation weight for suffix replacement.
503    #[arg(long, value_name = "WEIGHT")]
504    pub mutation_weight_suffix: Option<u32>,
505
506    /// Corpus mutation weight for ABI argument mutation.
507    #[arg(long, value_name = "WEIGHT")]
508    pub mutation_weight_abi: Option<u32>,
509
510    /// Corpus mutation weight for comparison-operand mutation.
511    #[arg(long, value_name = "WEIGHT")]
512    pub mutation_weight_cmp: Option<u32>,
513
514    /// Directory for fuzz branch frontier artifacts.
515    #[arg(long, value_name = "PATH", value_hint = ValueHint::DirPath)]
516    pub frontier_dir: Option<PathBuf>,
517
518    /// Maximum number of fuzz branch frontier records to write per test.
519    #[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/// CLI arguments for `forge test`.
530#[derive(Clone, Debug, Default, Parser)]
531#[command(next_help_heading = "Test options")]
532pub struct TestArgs {
533    /// Internal mode used by `forge fuzz`.
534    #[arg(skip)]
535    fuzz_only: FuzzOnlyMode,
536
537    /// Internal showmap/replay override used by `forge fuzz replay`.
538    #[arg(skip)]
539    pub(crate) showmap_override: Option<ShowmapConfig>,
540
541    /// Internal mode used by `forge fuzz replay` to replay persisted fuzz failures.
542    #[arg(skip)]
543    pub(crate) fuzz_failure_replay: bool,
544
545    /// Internal override used by `forge fuzz run --runs` for invariant campaigns.
546    #[arg(skip)]
547    pub(crate) invariant_runs_override: Option<u64>,
548
549    /// Internal override used by `forge fuzz run --timeout` for invariant campaigns.
550    #[arg(skip)]
551    pub(crate) invariant_timeout_override: Option<u32>,
552
553    // Include global options for users of this struct.
554    #[command(flatten)]
555    pub global: GlobalArgs,
556
557    /// The contract file you want to test, it's a shortcut for --match-path.
558    #[arg(value_hint = ValueHint::FilePath)]
559    pub path: Option<GlobMatcher>,
560
561    /// Run a single test in the debugger.
562    ///
563    /// The matching test will be opened in the debugger regardless of the outcome of the test.
564    ///
565    /// If the matching test is a fuzz test, then it will open the debugger on the first failure
566    /// case. If the fuzz test does not fail, it will open the debugger on the last fuzz case.
567    #[arg(long, conflicts_with_all = ["flamegraph", "flamechart", "evm_profile", "decode_internal", "rerun"])]
568    debug: bool,
569
570    /// Debugger layout to use.
571    #[arg(long = "debug-layout", requires = "debug", value_enum)]
572    debug_layout: Option<DebuggerLayout>,
573
574    /// Generate a flamegraph for a single test. Implies `--decode-internal`.
575    ///
576    /// A flame graph is used to visualize which functions or operations within the smart contract
577    /// are consuming the most gas overall in a sorted manner.
578    #[arg(long, conflicts_with_all = ["flamechart", "evm_profile", "json", "junit", "list"])]
579    flamegraph: bool,
580
581    /// Generate a flamechart for a single test. Implies `--decode-internal`.
582    ///
583    /// A flame chart shows the gas usage over time, illustrating when each function is
584    /// called (execution order) and how much gas it consumes at each point in the timeline.
585    #[arg(long, conflicts_with_all = ["flamegraph", "evm_profile", "json", "junit", "list"])]
586    flamechart: bool,
587
588    /// Generate an execution profile for a single test.
589    ///
590    /// Creates a profile where each EVM call is recorded with gas consumption.
591    /// Opens the profile in speedscope.app unless `--no-open` is passed.
592    /// Implies `--decode-internal`.
593    #[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    /// Don't open the profile in the browser (for `--evm-profile`).
604    ///
605    /// The profile is saved to disk without starting the local viewer server.
606    #[arg(long, requires = "evm_profile")]
607    no_open: bool,
608
609    #[command(flatten)]
610    tracing: TracingArgs,
611
612    /// Dumps all debugger steps to file.
613    #[arg(
614        long,
615        requires = "debug",
616        value_hint = ValueHint::FilePath,
617        value_name = "PATH"
618    )]
619    dump: Option<PathBuf>,
620
621    /// Print a gas report.
622    #[arg(long, env = "FORGE_GAS_REPORT")]
623    gas_report: bool,
624
625    /// Check gas snapshots against previous runs.
626    #[arg(long, env = "FORGE_SNAPSHOT_CHECK")]
627    gas_snapshot_check: Option<bool>,
628
629    /// Enable/disable recording of gas snapshot results.
630    #[arg(long, env = "FORGE_SNAPSHOT_EMIT")]
631    gas_snapshot_emit: Option<bool>,
632
633    /// Exit with code 0 even if a test fails.
634    #[arg(long, env = "FORGE_ALLOW_FAILURE")]
635    allow_failure: bool,
636
637    /// Suppress successful test traces and show only traces for failures.
638    #[arg(long, short, env = "FORGE_SUPPRESS_SUCCESSFUL_TRACES", help_heading = "Trace options")]
639    suppress_successful_traces: bool,
640
641    /// Output test results as JUnit XML report.
642    #[arg(long, conflicts_with_all = ["quiet", "json", "gas_report", "summary", "list", "show_progress"], help_heading = "Display options")]
643    pub junit: bool,
644
645    /// Stop running tests after the first failure.
646    #[arg(long)]
647    pub fail_fast: bool,
648
649    /// The Etherscan (or equivalent) API key.
650    #[arg(long, env = "ETHERSCAN_API_KEY", value_name = "KEY")]
651    etherscan_api_key: Option<String>,
652
653    /// List tests instead of running them.
654    #[arg(long, short, conflicts_with_all = ["show_progress", "decode_internal", "summary"], help_heading = "Display options")]
655    list: bool,
656
657    /// Set seed used to generate randomness during your fuzz runs.
658    #[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    /// Number of workers to use for invariant test campaigns, or `auto` to derive from `--jobs`.
665    #[arg(long, env = "FOUNDRY_INVARIANT_WORKERS", value_name = "WORKERS")]
666    pub invariant_workers: Option<InvariantWorkers>,
667
668    /// Run only the fuzz case at the given 1-based run index.
669    #[arg(long, env = "FOUNDRY_FUZZ_RUN", value_name = "RUN")]
670    pub fuzz_run: Option<u32>,
671
672    /// Run the fuzz case from the given worker. Requires `--fuzz-run`.
673    #[arg(long, env = "FOUNDRY_FUZZ_WORKER", value_name = "WORKER", requires = "fuzz_run")]
674    pub fuzz_worker: Option<u32>,
675
676    /// Timeout for each fuzz run in seconds.
677    #[arg(long, env = "FOUNDRY_FUZZ_TIMEOUT", value_name = "TIMEOUT")]
678    pub fuzz_timeout: Option<u64>,
679
680    /// Percent of fuzz calldata generated from the dictionary.
681    #[arg(long, env = "FOUNDRY_FUZZ_DICTIONARY_WEIGHT", value_name = "PERCENT")]
682    pub fuzz_dictionary_weight: Option<u32>,
683
684    /// Maximum fuzz dictionary addresses, or `max`.
685    #[arg(long, env = "FOUNDRY_FUZZ_MAX_FUZZ_DICTIONARY_ADDRESSES", value_name = "N|max")]
686    pub fuzz_dictionary_addresses: Option<String>,
687
688    /// Maximum fuzz dictionary values, or `max`.
689    #[arg(long, env = "FOUNDRY_FUZZ_MAX_FUZZ_DICTIONARY_VALUES", value_name = "N|max")]
690    pub fuzz_dictionary_values: Option<String>,
691
692    /// Maximum fuzz dictionary literals, or `max`.
693    #[arg(long, env = "FOUNDRY_FUZZ_MAX_FUZZ_DICTIONARY_LITERALS", value_name = "N|max")]
694    pub fuzz_dictionary_literals: Option<String>,
695
696    /// Percent chance that coverage-guided fuzzing generates fresh input instead of mutating
697    /// corpus input.
698    #[arg(long, env = "FOUNDRY_FUZZ_CORPUS_RANDOM_SEQUENCE_WEIGHT", value_name = "PERCENT")]
699    pub fuzz_corpus_random_sequence_weight: Option<u32>,
700
701    /// Directory for fuzz corpus persistence.
702    #[arg(long, env = "FOUNDRY_FUZZ_CORPUS_DIR", value_name = "PATH", value_hint = ValueHint::DirPath)]
703    pub fuzz_corpus_dir: Option<PathBuf>,
704
705    /// Directory for fuzz branch frontier artifacts.
706    #[arg(long, env = "FOUNDRY_FUZZ_FRONTIER_DIR", value_name = "PATH", value_hint = ValueHint::DirPath)]
707    pub fuzz_frontier_dir: Option<PathBuf>,
708
709    /// Maximum number of fuzz branch frontier records to write per test.
710    #[arg(long, env = "FOUNDRY_FUZZ_FRONTIER_LIMIT", value_name = "COUNT")]
711    pub fuzz_frontier_limit: Option<usize>,
712
713    /// Percent chance that fuzzed payable calls carry non-zero msg.value.
714    #[arg(long, env = "FOUNDRY_FUZZ_PAYABLE_VALUE_WEIGHT", value_name = "PERCENT")]
715    pub fuzz_payable_value_weight: Option<u32>,
716
717    /// Corpus mutation weight for splice.
718    #[arg(long, env = "FOUNDRY_FUZZ_MUTATION_WEIGHT_SPLICE", value_name = "WEIGHT")]
719    pub fuzz_mutation_weight_splice: Option<u32>,
720
721    /// Corpus mutation weight for repeat.
722    #[arg(long, env = "FOUNDRY_FUZZ_MUTATION_WEIGHT_REPEAT", value_name = "WEIGHT")]
723    pub fuzz_mutation_weight_repeat: Option<u32>,
724
725    /// Corpus mutation weight for interleave.
726    #[arg(long, env = "FOUNDRY_FUZZ_MUTATION_WEIGHT_INTERLEAVE", value_name = "WEIGHT")]
727    pub fuzz_mutation_weight_interleave: Option<u32>,
728
729    /// Corpus mutation weight for prefix replacement.
730    #[arg(long, env = "FOUNDRY_FUZZ_MUTATION_WEIGHT_PREFIX", value_name = "WEIGHT")]
731    pub fuzz_mutation_weight_prefix: Option<u32>,
732
733    /// Corpus mutation weight for suffix replacement.
734    #[arg(long, env = "FOUNDRY_FUZZ_MUTATION_WEIGHT_SUFFIX", value_name = "WEIGHT")]
735    pub fuzz_mutation_weight_suffix: Option<u32>,
736
737    /// Corpus mutation weight for ABI argument mutation.
738    #[arg(long, env = "FOUNDRY_FUZZ_MUTATION_WEIGHT_ABI", value_name = "WEIGHT")]
739    pub fuzz_mutation_weight_abi: Option<u32>,
740
741    /// Corpus mutation weight for comparison-operand mutation.
742    #[arg(long, env = "FOUNDRY_FUZZ_MUTATION_WEIGHT_CMP", value_name = "WEIGHT")]
743    pub fuzz_mutation_weight_cmp: Option<u32>,
744
745    /// File to rerun fuzz failures from.
746    #[arg(long)]
747    pub fuzz_input_file: Option<String>,
748
749    /// Number of calls executed to try to break invariants in one run.
750    #[arg(long, env = "FOUNDRY_INVARIANT_DEPTH", value_name = "DEPTH")]
751    pub invariant_depth: Option<u32>,
752
753    /// Minimum sampled invariant depth when `--invariant-depth-mode random` is active.
754    #[arg(long, env = "FOUNDRY_INVARIANT_MIN_DEPTH", value_name = "DEPTH")]
755    pub invariant_min_depth: Option<u32>,
756
757    /// How invariant run depth is selected.
758    #[arg(long, env = "FOUNDRY_INVARIANT_DEPTH_MODE", value_name = "fixed|random")]
759    pub invariant_depth_mode: Option<InvariantDepthMode>,
760
761    /// Percent of invariant calldata/senders generated from the dictionary.
762    #[arg(long, env = "FOUNDRY_INVARIANT_DICTIONARY_WEIGHT", value_name = "PERCENT")]
763    pub invariant_dictionary_weight: Option<u32>,
764
765    /// Maximum invariant dictionary addresses, or `max`.
766    #[arg(long, env = "FOUNDRY_INVARIANT_MAX_FUZZ_DICTIONARY_ADDRESSES", value_name = "N|max")]
767    pub invariant_dictionary_addresses: Option<String>,
768
769    /// Maximum invariant dictionary values, or `max`.
770    #[arg(long, env = "FOUNDRY_INVARIANT_MAX_FUZZ_DICTIONARY_VALUES", value_name = "N|max")]
771    pub invariant_dictionary_values: Option<String>,
772
773    /// Maximum invariant dictionary literals, or `max`.
774    #[arg(long, env = "FOUNDRY_INVARIANT_MAX_FUZZ_DICTIONARY_LITERALS", value_name = "N|max")]
775    pub invariant_dictionary_literals: Option<String>,
776
777    /// Percent chance that coverage-guided invariant fuzzing injects fresh calls while extending
778    /// corpus sequences.
779    #[arg(long, env = "FOUNDRY_INVARIANT_CORPUS_RANDOM_SEQUENCE_WEIGHT", value_name = "PERCENT")]
780    pub invariant_corpus_random_sequence_weight: Option<u32>,
781
782    /// Directory for invariant corpus persistence.
783    #[arg(long, env = "FOUNDRY_INVARIANT_CORPUS_DIR", value_name = "PATH", value_hint = ValueHint::DirPath)]
784    pub invariant_corpus_dir: Option<PathBuf>,
785
786    /// Percent chance that fuzzed payable invariant calls carry non-zero msg.value.
787    #[arg(long, env = "FOUNDRY_INVARIANT_PAYABLE_VALUE_WEIGHT", value_name = "PERCENT")]
788    pub invariant_payable_value_weight: Option<u32>,
789
790    /// Corpus mutation weight for splice.
791    #[arg(long, env = "FOUNDRY_INVARIANT_MUTATION_WEIGHT_SPLICE", value_name = "WEIGHT")]
792    pub invariant_mutation_weight_splice: Option<u32>,
793
794    /// Corpus mutation weight for repeat.
795    #[arg(long, env = "FOUNDRY_INVARIANT_MUTATION_WEIGHT_REPEAT", value_name = "WEIGHT")]
796    pub invariant_mutation_weight_repeat: Option<u32>,
797
798    /// Corpus mutation weight for interleave.
799    #[arg(long, env = "FOUNDRY_INVARIANT_MUTATION_WEIGHT_INTERLEAVE", value_name = "WEIGHT")]
800    pub invariant_mutation_weight_interleave: Option<u32>,
801
802    /// Corpus mutation weight for prefix replacement.
803    #[arg(long, env = "FOUNDRY_INVARIANT_MUTATION_WEIGHT_PREFIX", value_name = "WEIGHT")]
804    pub invariant_mutation_weight_prefix: Option<u32>,
805
806    /// Corpus mutation weight for suffix replacement.
807    #[arg(long, env = "FOUNDRY_INVARIANT_MUTATION_WEIGHT_SUFFIX", value_name = "WEIGHT")]
808    pub invariant_mutation_weight_suffix: Option<u32>,
809
810    /// Corpus mutation weight for ABI argument mutation.
811    #[arg(long, env = "FOUNDRY_INVARIANT_MUTATION_WEIGHT_ABI", value_name = "WEIGHT")]
812    pub invariant_mutation_weight_abi: Option<u32>,
813
814    /// Corpus mutation weight for comparison-operand mutation.
815    #[arg(long, env = "FOUNDRY_INVARIANT_MUTATION_WEIGHT_CMP", value_name = "WEIGHT")]
816    pub invariant_mutation_weight_cmp: Option<u32>,
817
818    /// Run symbolic check*/prove*/invariant*/statefulFuzz* tests.
819    #[arg(long, env = "FOUNDRY_SYMBOLIC")]
820    pub symbolic: bool,
821
822    /// Replay a durable symbolic counterexample artifact emitted by `forge test --symbolic`.
823    #[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    /// Emit Solidity regression tests for confirmed symbolic counterexamples.
846    #[arg(long, env = "FOUNDRY_SYMBOLIC_EMIT_REGRESSION")]
847    pub emit_regression: bool,
848
849    /// File or directory for generated symbolic regression tests.
850    #[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    /// Overwrite existing generated symbolic regression tests.
860    #[arg(long, env = "FOUNDRY_SYMBOLIC_REGRESSION_OVERWRITE", requires = "emit_regression")]
861    pub regression_overwrite: bool,
862
863    /// Run fuzz tests symbolically and persist non-failing concrete inputs to the fuzz corpus.
864    #[arg(long, env = "FOUNDRY_SYMBOLIC_SEED_CORPUS")]
865    pub symbolic_seed_corpus: bool,
866
867    /// Run fuzz tests symbolically using existing fuzz corpus entries as path-priority hints.
868    #[arg(long, env = "FOUNDRY_SYMBOLIC_USE_FUZZ_CORPUS")]
869    pub symbolic_use_fuzz_corpus: bool,
870
871    /// Maximum number of fuzz corpus entries to import for one symbolic test.
872    #[arg(long, env = "FOUNDRY_SYMBOLIC_CORPUS_SEED_LIMIT", value_name = "COUNT")]
873    pub symbolic_corpus_seed_limit: Option<usize>,
874
875    /// Run targeted symbolic solving from existing fuzz branch frontier artifacts.
876    #[arg(long, env = "FOUNDRY_SYMBOLIC_USE_FUZZ_FRONTIERS")]
877    pub symbolic_use_fuzz_frontiers: bool,
878
879    /// Maximum number of fuzz branch frontiers to try for one symbolic test.
880    #[arg(long, env = "FOUNDRY_SYMBOLIC_FRONTIER_LIMIT", value_name = "COUNT")]
881    pub symbolic_frontier_limit: Option<usize>,
882
883    /// Comma-separated fuzz branch frontier artifact IDs to try.
884    #[arg(long, env = "FOUNDRY_SYMBOLIC_FRONTIER_IDS", value_name = "IDS", value_delimiter = ',')]
885    pub symbolic_frontier_ids: Option<Vec<u64>>,
886
887    /// Comma-separated fuzz branch frontier comparison PCs to try.
888    #[arg(long, env = "FOUNDRY_SYMBOLIC_FRONTIER_PCS", value_name = "PCS", value_delimiter = ',')]
889    pub symbolic_frontier_pcs: Option<Vec<usize>>,
890
891    /// Comma-separated fuzz branch frontier calldata selectors to try.
892    #[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    /// Solver executable used for symbolic tests.
901    #[arg(long, env = "FOUNDRY_SYMBOLIC_SOLVER", value_name = "PATH_OR_NAME")]
902    pub symbolic_solver: Option<String>,
903
904    /// Exact solver command used for symbolic tests.
905    #[arg(long, env = "FOUNDRY_SYMBOLIC_SOLVER_COMMAND", value_name = "COMMAND")]
906    pub symbolic_solver_command: Option<String>,
907
908    /// Comma-separated SMT solver names or commands to race in parallel for symbolic tests.
909    #[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    /// SMT solver timeout in seconds; also bounds symbolic invariant exploration.
918    #[arg(long, env = "FOUNDRY_SYMBOLIC_TIMEOUT", value_name = "SECONDS")]
919    pub symbolic_timeout: Option<u32>,
920
921    /// Halmos-compatible symbolic loop bound.
922    #[arg(long, env = "FOUNDRY_SYMBOLIC_LOOP", value_name = "N")]
923    pub symbolic_loop: Option<u32>,
924
925    /// Halmos-compatible symbolic execution depth alias.
926    #[arg(long, env = "FOUNDRY_SYMBOLIC_DEPTH", value_name = "N")]
927    pub symbolic_depth: Option<u32>,
928
929    /// Halmos-compatible symbolic path width alias.
930    #[arg(long, env = "FOUNDRY_SYMBOLIC_WIDTH", value_name = "N")]
931    pub symbolic_width: Option<u32>,
932
933    /// Maximum number of opcodes executed along a symbolic path.
934    #[arg(long, env = "FOUNDRY_SYMBOLIC_MAX_DEPTH", value_name = "N")]
935    pub symbolic_max_depth: Option<u32>,
936
937    /// Maximum number of symbolic paths to explore per test.
938    #[arg(long, env = "FOUNDRY_SYMBOLIC_MAX_PATHS", value_name = "N")]
939    pub symbolic_max_paths: Option<u32>,
940
941    /// Maximum number of calls in a bounded symbolic invariant sequence.
942    #[arg(long, env = "FOUNDRY_SYMBOLIC_INVARIANT_DEPTH", value_name = "N")]
943    pub symbolic_invariant_depth: Option<u32>,
944
945    /// Maximum number of solver queries per symbolic test.
946    #[arg(long, env = "FOUNDRY_SYMBOLIC_MAX_SOLVER_QUERIES", value_name = "N")]
947    pub symbolic_max_solver_queries: Option<u32>,
948
949    /// Default bounded length for symbolic dynamic ABI inputs.
950    #[arg(long, env = "FOUNDRY_SYMBOLIC_DEFAULT_DYNAMIC_LENGTH", value_name = "N")]
951    pub symbolic_default_dynamic_length: Option<u32>,
952
953    /// Maximum permitted bounded length for symbolic dynamic ABI inputs.
954    #[arg(long, env = "FOUNDRY_SYMBOLIC_MAX_DYNAMIC_LENGTH", value_name = "N")]
955    pub symbolic_max_dynamic_length: Option<u32>,
956
957    /// Per-dynamic-input symbolic lengths, applied in ABI traversal order.
958    #[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    /// Maximum symbolic calldata size in bytes.
967    #[arg(long, env = "FOUNDRY_SYMBOLIC_MAX_CALLDATA_BYTES", value_name = "N")]
968    pub symbolic_max_calldata_bytes: Option<u32>,
969
970    /// Expand symbolic external call targets over known deployed contracts.
971    #[arg(long, env = "FOUNDRY_SYMBOLIC_CALL_TARGETS")]
972    pub symbolic_call_targets: bool,
973
974    /// Dump SMT-LIB queries issued by symbolic tests.
975    #[arg(long, env = "FOUNDRY_SYMBOLIC_DUMP_SMT")]
976    pub symbolic_dump_smt: bool,
977
978    /// Symbolic storage modelling mode.
979    #[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    /// Show test execution progress.
988    #[arg(long, conflicts_with_all = ["quiet", "json"], help_heading = "Display options")]
989    pub show_progress: bool,
990
991    /// Re-run recorded test failures from last run.
992    /// If no failure recorded then regular test run is performed.
993    #[arg(long)]
994    pub rerun: bool,
995
996    /// Print the given opcodes in trace output, with their gas
997    /// cost and the storage slot and value, if available.
998    ///
999    /// Accepts a comma-separated list of opcode names, e.g.
1000    /// `--opcodes SLOAD,MLOAD,SSTORE`. Names are in uppercase.
1001    /// Requires `-vvvvv` to render.
1002    #[arg(long, value_parser = parse_opcode, value_delimiter(','), conflicts_with_all = ["json", "junit", "list", "debug"])]
1003    pub opcodes: Vec<OpCode>,
1004
1005    /// Print test summary table.
1006    #[arg(long, help_heading = "Display options")]
1007    pub summary: bool,
1008
1009    /// Print detailed test summary table.
1010    #[arg(long, help_heading = "Display options", requires = "summary")]
1011    pub detailed: bool,
1012
1013    /// Replay the persisted corpus and emit AFL-`afl-showmap`-style coverage
1014    /// files at the given output directory. Disables the regular fuzz/invariant
1015    /// campaign and skips unit tests.
1016    #[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    /// Emit one showmap file per corpus entry (default: one aggregated file per test).
1026    #[arg(long, help_heading = "Showmap replay", requires = "showmap_out")]
1027    pub showmap_per_input: bool,
1028
1029    /// Coverage domain(s) to dump.
1030    #[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    /// Approach name (used as a subdirectory of `--showmap-out`).
1040    #[arg(
1041        long,
1042        default_value = "replay",
1043        help_heading = "Showmap replay",
1044        requires = "showmap_out"
1045    )]
1046    pub showmap_approach: String,
1047
1048    /// Trial identifier embedded in each showmap filename. Defaults to a unique
1049    /// `trial-<unix_nanos>` so reruns don't overwrite previous trials.
1050    #[arg(long, help_heading = "Showmap replay", requires = "showmap_out")]
1051    pub showmap_trial: Option<String>,
1052
1053    /// Override the corpus directory to replay (defaults to the per-test
1054    /// `corpus_dir` resolved from config).
1055    #[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    /// Enable mutation testing.
1077    /// If passed with file paths, only those files will be tested.
1078    #[arg(long, num_args(0..), value_name = "PATH")]
1079    pub mutate: Option<Vec<PathBuf>>,
1080
1081    /// Specify which files to mutate with glob pattern matching.
1082    ///
1083    /// Mutually exclusive with passing explicit paths to `--mutate`; either
1084    /// supply paths to `--mutate` or use this glob filter, not both.
1085    #[arg(long, value_name = "PATTERN", requires = "mutate", conflicts_with = "mutate_contract")]
1086    pub mutate_path: Option<GlobMatcher>,
1087
1088    /// Only mutate contracts whose name matches the specified regex pattern.
1089    ///
1090    /// Mutually exclusive with `--mutate-path`.
1091    #[arg(long, value_name = "REGEX", requires = "mutate")]
1092    pub mutate_contract: Option<regex::Regex>,
1093
1094    /// Number of parallel workers for mutation testing.
1095    /// Defaults to the number of CPU cores.
1096    #[arg(long, value_name = "JOBS", requires = "mutate")]
1097    pub mutation_jobs: Option<usize>,
1098
1099    /// Best-effort per-mutant wall-clock timeout in seconds. Mutants that
1100    /// exceed it are recorded as "timed out" and cleanup continues in the
1101    /// background with bounded pending workers.
1102    ///
1103    /// Analogous to `--invariant-timeout` for invariant campaigns.
1104    #[arg(long, value_name = "TIMEOUT", requires = "mutate")]
1105    pub mutation_timeout: Option<u32>,
1106
1107    /// Override optimizer runs for mutation testing compile-and-test runs.
1108    #[arg(long, value_name = "RUNS", requires = "mutate")]
1109    pub mutation_optimizer_runs: Option<u32>,
1110
1111    /// Override via-ir for mutation testing compile-and-test runs.
1112    #[arg(long, default_missing_value = "true", num_args = 0..=1, requires = "mutate")]
1113    pub mutation_via_ir: Option<bool>,
1114
1115    /// Enable brutalization mode.
1116    ///
1117    /// Catches latent bugs that normal tests miss because the EVM initializes
1118    /// memory to zero and registers to clean values. Applies source-level
1119    /// sanitizers before compiling:
1120    ///
1121    /// - Dirties unused bits in sub-256-bit type casts (address, uint8, bytes4, etc.) to catch
1122    ///   assembly code that assumes clean upper bits when using legacy codegen. Via-IR may clean
1123    ///   these bits before inline assembly observes them.
1124    /// - Fills scratch space (0x00-0x3f) and memory beyond the free memory pointer with junk to
1125    ///   catch uninitialized memory reads
1126    /// - Misaligns the free memory pointer to catch word-alignment assumptions
1127    ///
1128    /// If `forge test` passes but `forge test --brutalize` fails, the code has
1129    /// a robustness issue that could manifest when called in a different context.
1130    // TODO: evaluate if we can relax the conflict with replay_symbolic_artifact
1131    #[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        // Mutation testing has bespoke orchestration that is not compatible with these modes.
1147        // Run this before compiling when the caller owns the build step so project errors do not
1148        // mask CLI conflicts.
1149        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    /// Builds a `ShowmapConfig` from the showmap CLI flags, if `--showmap-out` is set.
1235    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        // Default trial id uses nanosecond precision so back-to-back invocations
1242        // don't collide and overwrite each other's output files.
1243        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    /// Restricts this test invocation to fuzz and invariant tests.
1265    pub(crate) const fn enable_fuzz_only(&mut self) {
1266        self.fuzz_only = FuzzOnlyMode::Enabled;
1267    }
1268
1269    /// Restricts this test invocation to fuzz and invariant tests and enables a default fuzz corpus
1270    /// dir after user config is loaded.
1271    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    /// Overrides showmap config for callers that reuse replay mode without the
1289    /// `forge test --showmap-*` CLI flags.
1290    pub(crate) fn set_showmap_override(&mut self, showmap: ShowmapConfig) {
1291        self.showmap_override = Some(showmap);
1292    }
1293
1294    /// Sets replay-critical options for internal fuzz minimizer callers.
1295    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    /// Replays persisted fuzz failures without running a new fuzz campaign.
1309    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    /// Builds the delegated `forge test` invocation for `forge fuzz run`.
1379    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    /// Returns a list of files that need to be compiled in order to run all the tests that match
1529    /// the given filter.
1530    ///
1531    /// For filtered runs, this includes all configured source files, non-test artifacts outside
1532    /// script-only paths, and runnable tests that match the filter. Non-test artifacts remain
1533    /// available because tests may resolve them dynamically through cheatcodes.
1534    #[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        // An empty filter doesn't filter out anything.
1543        // We can still optimize slightly by excluding scripts.
1544        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                    // Preserve path-filter behavior for conventional test files while still
1558                    // scanning non-test fixtures under the test root.
1559                    .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    /// Executes all the tests in the project.
1586    ///
1587    /// This will trigger the build process first. On success all test contracts that match the
1588    /// configured filter will be executed
1589    ///
1590    /// Returns the test results for all matching tests.
1591    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    /// Compile and run tests with brutalization applied to source files.
1615    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        // Merge all configs.
1683        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            // need to re-configure here to also catch additional remappings
1690            config = self.load_config()?;
1691        }
1692        if should_mutate {
1693            // Force dyn test linking and cache usage for mutation testing after any config reload.
1694            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        // Set up the project.
1702        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    /// Executes all the tests in the project.
1884    ///
1885    /// See [`Self::compile_and_run`] for more details.
1886    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        // Explicitly enable isolation for gas reports for more correct gas accounting.
1923        if self.gas_report {
1924            evm_opts.isolate = true;
1925        } else {
1926            // Do not collect gas report traces if gas report is not enabled.
1927            config.fuzz.gas_report_samples = 0;
1928            config.invariant.gas_report_samples = 0;
1929        }
1930
1931        // Generate a random fuzz seed if none provided, for reproducibility.
1932        config.fuzz.seed = config
1933            .fuzz
1934            .seed
1935            .or_else(|| Some(U256::from_be_bytes(rand::rng().random::<[u8; 32]>())));
1936
1937        // Create test options from general project settings and compiler output.
1938        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        // Determine executor verbosity.
1948        if evm_opts.verbosity < 3 && (self.gas_report || trace_output.is_some()) {
1949            evm_opts.verbosity = 3;
1950        }
1951
1952        // Enable internal tracing for more informative flamegraph/profile.
1953        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        // Choose the internal function tracing mode, if --decode-internal is provided.
1957        let decode_internal = if decode_internal_enabled {
1958            // If more than one function matched, we enable simple tracing.
1959            // If only one function matched, we enable full tracing. This is done in `run_tests`.
1960            InternalTraceMode::Simple
1961        } else {
1962            InternalTraceMode::None
1963        };
1964
1965        // Auto-detect network from fork chain ID when not explicitly configured.
1966        evm_opts.infer_network_from_fork().await;
1967
1968        // Clone config and evm_opts before dispatch (needed for mutation testing).
1969        let config_for_mutation = config.clone();
1970        let evm_opts_for_mutation = evm_opts.clone();
1971
1972        // Detect per-test network annotations.
1973        let override_networks =
1974            execution.inline_config.referenced_override_networks(&config.profile);
1975
1976        let (libraries, mut outcome) = if override_networks.is_empty() {
1977            // Single-pass: no per-test network overrides, use global network setting.
1978            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            // Multi-pass: run each distinct network separately and merge results.
1991            let all_override_networks = override_networks.clone();
1992            let multi_pass_timer = Instant::now();
1993
1994            // Default pass: global network, runs tests without an explicit network annotation.
1995            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            // Override passes: one per annotated network.
2014            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            // Print the merged summary (per-pass summaries are suppressed in `run_tests_inner`).
2038            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 traces.
2124                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            // Get first non-empty suite result. We will have only one such entry.
2203            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            // Prefer execution traces for normal debug runs, but when execution never starts
2210            // (for example if `setUp()` reverts), fall back to available setup/deployment traces.
2211            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            // Run the debugger.
2227            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        // All tests have been run once before reaching this point
2246        if let Some(mutate) = &self.mutate {
2247            // Check outcome here, stop if any test failed
2248            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            // A green baseline that ran zero non-skipped tests is not useful:
2257            // every compileable mutant would be reported as `Alive` (no test
2258            // failed, so nothing killed it), which produces a wildly
2259            // misleading mutation report. Hard-error so users get an actual
2260            // signal that their filter / path / setup matched nothing.
2261            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            // Explicit paths on --mutate cannot be combined with the --mutate-path
2270            // glob filter: clap can't express this directly because --mutate takes
2271            // an optional list of paths.
2272            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            // The mutation runner builds a single-pass `MultiContractRunner`
2279            // (`runner.rs::compile_and_test_inner`) and does not honor inline
2280            // per-test network annotations. If the project declares network
2281            // overrides, running mutation testing would silently execute those
2282            // tests on the wrong network and produce false survivors / kills.
2283            // Bail with a clear error rather than do the wrong thing silently.
2284            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            // The mutation runner symlinks dependency directories (`lib`,
2294            // `node_modules`, `dependencies`) into each per-mutant TempDir for
2295            // performance — see `workspace::copy_project`. That isolation
2296            // breaks down if tests can write to those shared trees, either via
2297            // `vm.writeFile` (broad `fs_permissions`) or arbitrary `ffi` calls.
2298            // Detect both up front so users aren't surprised by races or
2299            // corruption of their real dependency tree.
2300            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            // Only refuse write-capable `fs_permissions` whose path can actually
2310            // reach one of the symlinked dependency trees. Scoped writes (e.g.
2311            // `./out`, `./snapshots`) are safe because they target paths that
2312            // never resolve into the shared `lib`/`node_modules`/`dependencies`
2313            // trees.
2314            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                // Carry the same filter args (--match-test, --match-contract,
2441                // --match-path, positional path shorthand, --rerun, ...) and
2442                // isolation flag the baseline actually used, so every mutant
2443                // exercises the exact same test set under the same execution
2444                // model. We pull from the materialized `filter`, not the raw
2445                // CLI flags on `self`, because the baseline applies extras:
2446                // the positional `forge test <path>` shorthand is folded into
2447                // `path_pattern`, and `--rerun` injects last-run failures
2448                // into `test_pattern`. Using `self.filter.clone()` would lose
2449                // those and let mutant runs silently diverge from baseline.
2450                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            // Output JSON if requested
2469            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    /// Build the test runner and execute tests for a specific network type.
2481    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    /// Dispatches `build_and_run_tests` to the correct network type based on `evm_opts.networks`.
2541    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    /// Run all tests that matches the filter predicate from a test runner
2610    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        // If we need to render to a serialized format, we should not print anything else to stdout.
2627        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                // Try to suggest a test when there's no match.
2653                if let Some(test_pattern) = &filter.args().test_pattern {
2654                    let test_name = test_pattern.as_str();
2655                    // Filter contracts but not test functions.
2656                    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 exactly one test matched, we enable full tracing.
2735        if num_filtered == 1 && runner.decode_internal != InternalTraceMode::None {
2736            runner.decode_internal = InternalTraceMode::Full;
2737        }
2738
2739        // Run tests in a non-streaming fashion and collect results for serialization.
2740        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                        // Decode logs at level 2 and above.
2746                        test_result.decoded_logs = decode_console_logs(&test_result.logs);
2747                    } else {
2748                        // Empty logs for non verbose runs.
2749                        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(), &regressions);
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(), &regressions);
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        // Capture multi-pass state before moving `runner` into the spawn task.
2797        // In multi-pass mode the per-pass summary is suppressed; the merged summary is
2798        // printed once by the caller after all passes complete.
2799        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        // Run tests in a streaming fashion.
2804        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        // Set up trace identifiers.
2813        let mut identifier = TraceIdentifiers::new().with_local(&known_contracts);
2814
2815        // Avoid using external identifiers for gas report as we decode more traces and this will be
2816        // expensive. Also skip external identifiers for local tests (no remote chain) to avoid
2817        // unnecessary Etherscan API calls that significantly slow down test execution.
2818        if !self.gas_report && remote_chain.is_some() {
2819            identifier = identifier.with_external(&config, remote_chain)?;
2820        }
2821
2822        // Build the trace decoder.
2823        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        // Signatures are of no value for gas reports.
2832        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            // In multi-pass (per-test network override) mode, skip suites that contributed no
2865            // tests to this pass so we don't emit a stray blank line in the suite header or
2866            // pollute the outcome with empty entries.
2867            if is_multi_pass && !has_tests && suite_result.warnings.is_empty() {
2868                continue;
2869            }
2870
2871            // Clear the addresses and labels from previous test.
2872            decoder.clear_addresses();
2873
2874            // Some outputs need trace identities even if the textual trace is not rendered.
2875            let always_identify_traces = self.gas_report
2876                || self.debug
2877                || self.flamegraph
2878                || self.flamechart
2879                || self.evm_profile.is_some();
2880
2881            // Print suite header.
2882            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            // Process individual test results, printing logs and traces when necessary.
2894            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                    // Display invariant metrics if invariant kind.
2918                    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                    // We only display logs at level 2 and above
2925                    if verbosity >= 2 && show_traces {
2926                        // We only decode logs from Hardhat and DS-style console events
2927                        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                // We shouldn't break out of the outer loop directly here so that we finish
2939                // processing the remaining tests and print the suite summary.
2940                any_test_failed |= result.status == TestStatus::Failure;
2941
2942                // Clear the addresses and labels from previous runs.
2943                decoder.clear_addresses();
2944                if identify_addresses {
2945                    decoder.labels.extend(result.labels.iter().map(|(k, v)| (*k, v.clone())));
2946                }
2947
2948                // Identify addresses and decode traces.
2949                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                        // Trace verbosity.
2969                        // - 0..3: nothing.
2970                        // - 3: only display traces for failed tests.
2971                        // - 4: also display the setup trace for failed tests.
2972                        // - 5..: display all traces for all tests, including storage changes.
2973                        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                // Extract and display backtrace for failed tests when trace verbosity >= 3.
3006                // At trace verbosity 3-4 backtraces show contract/function names only.
3007                // At trace verbosity 5 backtraces include source file locations.
3008                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                    // Lazily initialize the backtrace builder on first failure
3016                    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                        // Re-execute setup and deployment traces to collect identities created in
3039                        // setUp and constructor.
3040                        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                // Clear memory.
3061                result.gas_report_traces = Default::default();
3062
3063                // Collect and merge gas snapshots.
3064                for (group, new_snapshots) in &result.gas_snapshots {
3065                    gas_snapshots.entry(group.clone()).or_default().extend(new_snapshots.clone());
3066                }
3067            }
3068
3069            // Write gas snapshots to disk if any were collected.
3070            if !gas_snapshots.is_empty() {
3071                // By default `gas_snapshot_check` is set to `false` in the config.
3072                //
3073                // The user can either:
3074                // - Set `FORGE_SNAPSHOT_CHECK=true` in the environment.
3075                // - Pass `--gas-snapshot-check=true` as a CLI argument.
3076                // - Set `gas_snapshot_check = true` in the config.
3077                //
3078                // If the user passes `--gas-snapshot-check=<bool>` then it will override the config
3079                // and the environment variable, disabling the check if `false` is passed.
3080                //
3081                // Exiting early with code 1 if differences are found.
3082                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 the snapshot file doesn't exist, we can't compare so we skip.
3086                            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                // By default `gas_snapshot_emit` is set to `true` in the config.
3131                //
3132                // The user can either:
3133                // - Set `FORGE_SNAPSHOT_EMIT=false` in the environment.
3134                // - Pass `--gas-snapshot-emit=false` as a CLI argument.
3135                // - Set `gas_snapshot_emit = false` in the config.
3136                //
3137                // If the user passes `--gas-snapshot-emit=<bool>` then it will override the config
3138                // and the environment variable, enabling the check if `true` is passed.
3139                if self.gas_snapshot_emit.unwrap_or(config.gas_snapshot_emit) {
3140                    // Create `snapshots` directory if it doesn't exist.
3141                    fs::create_dir_all(&config.snapshots)?;
3142
3143                    // Write gas snapshots to disk per group.
3144                    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            // Print suite summary.
3155            if !silent && has_tests {
3156                sh_println!("{}", suite_result.summary())?;
3157            }
3158
3159            // Add the suite result to the outcome.
3160            outcome.results.insert(contract_name, suite_result);
3161
3162            // Stop processing the remaining suites if any test failed and `fail_fast` is set.
3163            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(), &regressions);
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        // Reattach the task.
3203        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 test run failures to enable replaying.
3215        persist_run_failures(&config, &outcome);
3216
3217        Ok(outcome)
3218    }
3219
3220    /// Returns the flattened [`FilterArgs`] arguments merged with [`Config`].
3221    /// Loads and applies filter from file if only last test run failures performed.
3222    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    /// Returns whether `BuildArgs` was configured with `--watch`
3254    pub const fn is_watch(&self) -> bool {
3255        self.watch.watch.is_some()
3256    }
3257
3258    /// Returns the [`watchexec::Config`] necessary to bootstrap a new watch loop.
3259    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        // Mutation-testing CLI overrides
3547        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
3587/// Lists all matching tests
3588fn 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
3734/// Merges `other` into `base` by extending suite results.
3735///
3736/// For suites that appear in both, test results are combined (function-level pass routing ensures
3737/// each function appears in exactly one pass, so there are no key conflicts in practice).
3738fn 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
3802/// Load persisted filter (with last test run failures) from file.
3803fn 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
3830/// Persist filter with last test run failures (only if there's any failure).
3831fn 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
3869/// Generate test report in JUnit XML report format.
3870fn 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
3888/// Adds JUnit test cases for a test result.
3889///
3890/// Invariant campaigns are expanded into per-predicate and per-handler cases so CI can report
3891/// contract-level execution without losing failure attribution.
3892fn 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
3966/// Adds a single JUnit test case to the suite.
3967fn 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
3990/// Helper for assembling JUnit output strings.
3991struct JunitOutput {
3992    result_report: TestKindReport,
3993    logs: Option<Vec<String>>,
3994}
3995
3996impl JunitOutput {
3997    /// Creates a JUnit output helper for a test result.
3998    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    /// Renders the suite-level `system-out` payload.
4007    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    /// Renders the case-level `system-out` payload.
4015    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    /// Appends captured console logs to the output payload.
4050    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    // <https://github.com/foundry-rs/foundry/issues/5913>
4135    #[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}