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