Skip to main content

forge/cmd/test/
mod.rs

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