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