Skip to main content

foundry_config/
lib.rs

1//! # foundry-config
2//!
3//! Foundry configuration.
4
5#![cfg_attr(not(test), warn(unused_crate_dependencies))]
6#![cfg_attr(docsrs, feature(doc_cfg))]
7
8#[macro_use]
9extern crate tracing;
10
11use crate::cache::StorageCachingConfig;
12use alloy_primitives::{Address, B256, FixedBytes, U256, address, map::AddressHashMap};
13use eyre::{ContextCompat, WrapErr};
14use figment::{
15    Error, Figment, Metadata, Profile, Provider,
16    providers::{Env, Format, Serialized, Toml},
17    value::{Dict, Map, Value},
18};
19use filter::GlobMatcher;
20use foundry_compilers::{
21    ArtifactOutput, ConfigurableArtifacts, Graph, Project, ProjectPathsConfig,
22    RestrictionsWithVersion, VyperLanguage,
23    artifacts::{
24        BytecodeHash, DebuggingSettings, EvmVersion, Libraries, ModelCheckerSettings,
25        ModelCheckerTarget, Optimizer, OptimizerDetails, RevertStrings, Settings, SettingsMetadata,
26        Severity,
27        output_selection::{ContractOutputSelection, OutputSelection},
28        remappings::{RelativeRemapping, Remapping},
29        serde_helpers,
30    },
31    cache::SOLIDITY_FILES_CACHE_FILENAME,
32    compilers::{
33        Compiler,
34        multi::{MultiCompiler, MultiCompilerSettings},
35        solc::{Solc, SolcCompiler},
36        vyper::{Vyper, VyperSettings},
37    },
38    error::SolcError,
39    multi::{MultiCompilerParser, MultiCompilerRestrictions},
40    solc::{CliSettings, SolcLanguage, SolcSettings},
41};
42#[cfg(windows)]
43use path_slash::PathBufExt as _;
44use regex::Regex;
45use semver::Version;
46use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
47use std::{
48    borrow::Cow,
49    collections::BTreeMap,
50    fs,
51    io::{self, Write as _},
52    path::{Path, PathBuf},
53    str::FromStr,
54    sync::Mutex,
55};
56
57mod macros;
58
59pub mod utils;
60pub use foundry_evm_hardforks::{
61    ExecutionSpec, FoundryHardfork, FromEvmVersion, evm_spec_id, evm_spec_id_from_str,
62};
63pub use utils::*;
64
65mod endpoints;
66pub use endpoints::{
67    ResolvedRpcEndpoint, ResolvedRpcEndpoints, RpcEndpoint, RpcEndpointUrl, RpcEndpoints,
68    builtin_rpc_url,
69};
70
71mod etherscan;
72pub use etherscan::EtherscanConfigError;
73use etherscan::{EtherscanConfigs, EtherscanEnvProvider, ResolvedEtherscanConfig};
74
75pub mod resolve;
76pub use resolve::UnresolvedEnvVarError;
77
78pub mod cache;
79use cache::{Cache, ChainCache};
80
81pub mod fmt;
82pub use fmt::FormatterConfig;
83
84pub mod lint;
85pub use lint::{LinterConfig, Severity as LintSeverity};
86
87pub mod fs_permissions;
88pub use fs_permissions::FsPermissions;
89use fs_permissions::PathPermission;
90
91pub mod error;
92use error::ExtractConfigError;
93pub use error::SolidityErrorCode;
94
95pub mod doc;
96pub use doc::DocConfig;
97
98pub mod filter;
99pub use filter::SkipBuildFilters;
100
101mod warning;
102pub use warning::*;
103
104pub mod fix;
105
106// reexport so cli types can implement `figment::Provider` to easily merge compiler arguments
107pub use alloy_chains::{Chain, NamedChain};
108pub use figment;
109
110pub mod providers;
111pub use providers::Remappings;
112use providers::*;
113
114mod fuzz;
115pub use fuzz::{FuzzConfig, FuzzCorpusConfig, FuzzCorpusMutationWeights, FuzzDictionaryConfig};
116
117mod invariant;
118pub use invariant::{InvariantConfig, InvariantDepthMode, InvariantWorkers};
119
120mod symbolic;
121pub use symbolic::{SymbolicConfig, SymbolicExplorationOrder, SymbolicStorageLayout};
122
123mod coverage;
124pub use coverage::{CoverageConfig, CoverageReportKind, parse_lcov_version};
125
126mod trace;
127pub use trace::TracingConfig;
128
129mod fee;
130pub use fee::Eip1559FeeEstimatePreset;
131
132pub mod mutation;
133pub use mutation::{MutationConfig, MutatorType};
134
135mod inline;
136pub use inline::{InlineConfig, InlineConfigError, NatSpec};
137
138pub mod soldeer;
139use soldeer::{SoldeerConfig, SoldeerDependencyConfig};
140
141mod vyper;
142pub use vyper::VyperConfig;
143
144mod bind_json;
145use bind_json::BindJsonConfig;
146
147mod compilation;
148pub use compilation::{CompilationRestrictions, SettingsOverrides};
149
150pub mod extend;
151use extend::Extends;
152
153use foundry_evm_networks::NetworkConfigs;
154pub use semver;
155
156#[cfg(not(test))]
157static SELECTED_PROFILE: std::sync::OnceLock<Profile> = std::sync::OnceLock::new();
158static WARNED_LOCAL_COMPILERS: Mutex<Vec<PathBuf>> = Mutex::new(Vec::new());
159
160fn warn_local_compiler(path: &Path) {
161    let mut warned = WARNED_LOCAL_COMPILERS.lock().unwrap_or_else(|err| err.into_inner());
162    if warned.iter().any(|warned_path| warned_path == path) {
163        return;
164    }
165    warned.push(path.to_path_buf());
166
167    let mut stderr = io::stderr().lock();
168    let _ = writeln!(
169        stderr,
170        "Warning: this project is configured to use a local compiler executable:\n  {path:?}\n\
171         Running this executable may execute arbitrary code."
172    );
173}
174
175/// Foundry configuration
176///
177/// # Defaults
178///
179/// All configuration values have a default, documented in the [fields](#fields)
180/// section below. [`Config::default()`] returns the default values for
181/// the default profile while [`Config::with_root()`] returns the values based on the given
182/// directory. [`Config::load()`] starts with the default profile and merges various providers into
183/// the config, same for [`Config::load_with_root()`], but there the default values are determined
184/// by [`Config::with_root()`]
185///
186/// # Provider Details
187///
188/// `Config` is a Figment [`Provider`] with the following characteristics:
189///
190///   * **Profile**
191///
192///     The profile is set to the value of the `profile` field.
193///
194///   * **Metadata**
195///
196///     This provider is named `Foundry Config`. It does not specify a
197///     [`Source`](figment::Source) and uses default interpolation.
198///
199///   * **Data**
200///
201///     The data emitted by this provider are the keys and values corresponding
202///     to the fields and values of the structure. The dictionary is emitted to
203///     the "default" meta-profile.
204///
205/// Note that these behaviors differ from those of [`Config::figment()`].
206#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
207pub struct Config {
208    /// The selected profile. **(default: _default_ `default`)**
209    ///
210    /// **Note:** This field is never serialized nor deserialized. When a
211    /// `Config` is merged into a `Figment` as a `Provider`, this profile is
212    /// selected on the `Figment`. When a `Config` is extracted, this field is
213    /// set to the extracting Figment's selected `Profile`.
214    #[serde(skip)]
215    pub profile: Profile,
216    /// The list of all profiles defined in the config.
217    ///
218    /// See `profile`.
219    #[serde(skip)]
220    pub profiles: Vec<Profile>,
221
222    /// The root path where the config detection started from, [`Config::with_root`].
223    // We're skipping serialization here, so it won't be included in the [`Config::to_string()`]
224    // representation, but will be deserialized from the `Figment` so that forge commands can
225    // override it.
226    #[serde(default = "root_default", skip_serializing)]
227    pub root: PathBuf,
228
229    /// Configuration for extending from another foundry.toml (base) file.
230    ///
231    /// Can be either a string path or an object with path and strategy.
232    /// Base files cannot extend (inherit) other files.
233    #[serde(default, skip_serializing)]
234    pub extends: Option<Extends>,
235
236    /// Path of the sources directory.
237    ///
238    /// Defaults to `src`.
239    pub src: PathBuf,
240    /// Path of the tests directory.
241    pub test: PathBuf,
242    /// Path of the scripts directory.
243    pub script: PathBuf,
244    /// Path to the artifacts directory.
245    pub out: PathBuf,
246    /// Paths to all library folders, such as `lib`, or `node_modules`.
247    pub libs: Vec<PathBuf>,
248    /// Remappings to use for this repo
249    #[serde(serialize_with = "remappings_serde::serialize")]
250    pub remappings: Vec<RelativeRemapping>,
251    /// Whether to autodetect remappings.
252    pub auto_detect_remappings: bool,
253    /// Library addresses to link.
254    pub libraries: Vec<String>,
255    /// Whether to enable the build cache.
256    pub cache: bool,
257    /// The path to the cache store.
258    pub cache_path: PathBuf,
259    /// Whether to dynamically link tests.
260    pub dynamic_test_linking: bool,
261    /// Where the gas snapshots are stored.
262    pub snapshots: PathBuf,
263    /// Whether to check for differences against previously stored gas snapshots.
264    pub gas_snapshot_check: bool,
265    /// Whether to emit gas snapshots to disk.
266    pub gas_snapshot_emit: bool,
267    /// The path to store broadcast logs at.
268    pub broadcast: PathBuf,
269    /// Additional paths passed to `solc --allow-paths`.
270    pub allow_paths: Vec<PathBuf>,
271    /// Additional paths passed to `solc --include-path`.
272    pub include_paths: Vec<PathBuf>,
273    /// Glob patterns for file paths to skip when building and executing contracts.
274    pub skip: Vec<GlobMatcher>,
275    /// Whether to forcefully clean all project artifacts before running commands.
276    pub force: bool,
277    /// The EVM version to use when building contracts.
278    #[serde(with = "from_str_lowercase")]
279    pub evm_version: EvmVersion,
280    /// The runtime hardfork to use when executing tests and scripts.
281    pub hardfork: Option<FoundryHardfork>,
282    /// List of contracts to generate gas reports for.
283    pub gas_reports: Vec<String>,
284    /// List of contracts to ignore for gas reports.
285    pub gas_reports_ignore: Vec<String>,
286    /// Whether to include gas reports for tests.
287    pub gas_reports_include_tests: bool,
288    /// The Solc instance to use if any.
289    ///
290    /// This takes precedence over `auto_detect_solc`, if a version is set then this overrides
291    /// auto-detection.
292    ///
293    /// **Note** for backwards compatibility reasons this also accepts solc_version from the toml
294    /// file, see `BackwardsCompatTomlProvider`.
295    ///
296    /// Avoid using this field directly; call the related `solc` methods instead.
297    #[doc(hidden)]
298    pub solc: Option<SolcReq>,
299    /// Whether to autodetect the solc compiler version to use.
300    pub auto_detect_solc: bool,
301    /// Offline mode, if set, network access (downloading solc) is disallowed.
302    ///
303    /// Relationship with `auto_detect_solc`:
304    ///    - if `auto_detect_solc = true` and `offline = true`, the required solc version(s) will
305    ///      be auto detected but if the solc version is not installed, it will _not_ try to
306    ///      install it
307    pub offline: bool,
308    /// Whether to activate optimizer
309    pub optimizer: Option<bool>,
310    /// The number of runs specifies roughly how often each opcode of the deployed code will be
311    /// executed across the life-time of the contract. This means it is a trade-off parameter
312    /// between code size (deploy cost) and code execution cost (cost after deployment).
313    /// An `optimizer_runs` parameter of `1` will produce short but expensive code. In contrast, a
314    /// larger `optimizer_runs` parameter will produce longer but more gas efficient code. The
315    /// maximum value of the parameter is `2**32-1`.
316    ///
317    /// A common misconception is that this parameter specifies the number of iterations of the
318    /// optimizer. This is not true: The optimizer will always run as many times as it can
319    /// still improve the code.
320    pub optimizer_runs: Option<usize>,
321    /// Switch optimizer components on or off in detail.
322    /// The "enabled" switch above provides two defaults which can be
323    /// tweaked here. If "details" is given, "enabled" can be omitted.
324    pub optimizer_details: Option<OptimizerDetails>,
325    /// Model checker settings.
326    pub model_checker: Option<ModelCheckerSettings>,
327    /// Verbosity to use for global output.
328    pub verbosity: u8,
329    /// url of the rpc server that should be used for any rpc calls
330    pub eth_rpc_url: Option<String>,
331    /// Whether to accept invalid certificates for the rpc server.
332    pub eth_rpc_accept_invalid_certs: bool,
333    /// Whether to disable automatic proxy detection for the rpc server.
334    ///
335    /// This can help in sandboxed environments (e.g., Cursor IDE sandbox, macOS App Sandbox)
336    /// where system proxy detection via SCDynamicStore causes crashes.
337    pub eth_rpc_no_proxy: bool,
338    /// JWT secret that should be used for any rpc calls
339    pub eth_rpc_jwt: Option<String>,
340    /// Timeout that should be used for any rpc calls
341    pub eth_rpc_timeout: Option<u64>,
342    /// Headers that should be used for any rpc calls
343    ///
344    /// # Example
345    ///
346    /// rpc_headers = ["x-custom-header:value", "x-another-header:another-value"]
347    ///
348    /// You can also the ETH_RPC_HEADERS env variable like so:
349    /// `ETH_RPC_HEADERS="x-custom-header:value x-another-header:another-value"`
350    pub eth_rpc_headers: Option<Vec<String>>,
351    /// Print the equivalent curl command instead of making the RPC request.
352    pub eth_rpc_curl: bool,
353    /// etherscan API key, or alias for an `EtherscanConfig` in `etherscan` table
354    pub etherscan_api_key: Option<String>,
355    /// Multiple etherscan api configs and their aliases
356    #[serde(default, skip_serializing_if = "EtherscanConfigs::is_empty")]
357    pub etherscan: EtherscanConfigs,
358    /// List of solidity error codes to always silence in the compiler output.
359    pub ignored_error_codes: Vec<SolidityErrorCode>,
360    /// List of (path prefix, solidity error codes) to silence in the compiler output.
361    pub ignored_error_codes_from: Vec<(PathBuf, Vec<SolidityErrorCode>)>,
362    /// List of file paths to ignore.
363    #[serde(rename = "ignored_warnings_from")]
364    pub ignored_file_paths: Vec<PathBuf>,
365    /// Diagnostic level (minimum) at which the process should finish with a non-zero exit.
366    pub deny: DenyLevel,
367    /// DEPRECATED: use `deny` instead.
368    #[serde(default, skip_serializing)]
369    pub deny_warnings: bool,
370    /// Only run test functions matching the specified regex pattern.
371    #[serde(rename = "match_test")]
372    pub test_pattern: Option<RegexWrapper>,
373    /// Only run test functions that do not match the specified regex pattern.
374    #[serde(rename = "no_match_test")]
375    pub test_pattern_inverse: Option<RegexWrapper>,
376    /// Only run tests in contracts matching the specified regex pattern.
377    #[serde(rename = "match_contract")]
378    pub contract_pattern: Option<RegexWrapper>,
379    /// Only run tests in contracts that do not match the specified regex pattern.
380    #[serde(rename = "no_match_contract")]
381    pub contract_pattern_inverse: Option<RegexWrapper>,
382    /// Only run tests in source files matching the specified glob pattern.
383    #[serde(rename = "match_path", with = "from_opt_glob")]
384    pub path_pattern: Option<globset::Glob>,
385    /// Only run tests in source files that do not match the specified glob pattern.
386    #[serde(rename = "no_match_path", with = "from_opt_glob")]
387    pub path_pattern_inverse: Option<globset::Glob>,
388    /// Only show coverage for files that do not match the specified regex pattern.
389    #[serde(rename = "no_match_coverage")]
390    pub coverage_pattern_inverse: Option<RegexWrapper>,
391    /// Path where last test run failures are recorded.
392    pub test_failures_file: PathBuf,
393    /// Path where mutation tests are cached, to resume running them
394    pub mutation_dir: PathBuf,
395    /// Max concurrent threads to use.
396    pub threads: Option<usize>,
397    /// Whether to show test execution progress.
398    pub show_progress: bool,
399    /// Configuration for fuzz testing
400    pub fuzz: FuzzConfig,
401    /// Configuration for invariant testing
402    pub invariant: InvariantConfig,
403    /// Configuration for symbolic testing
404    pub symbolic: SymbolicConfig,
405    /// Configuration for `forge coverage`
406    pub coverage: CoverageConfig,
407    /// Configuration for mutation testing
408    pub mutation: MutationConfig,
409    /// Configuration for trace rendering.
410    pub tracing: TracingConfig,
411    /// Whether to allow ffi cheatcodes in test
412    pub ffi: bool,
413    /// Whether to show `console.log` outputs in realtime during script/test execution
414    pub live_logs: bool,
415    /// Whether to allow `expectRevert` for internal functions.
416    pub allow_internal_expect_revert: bool,
417    /// Use the create 2 factory in all cases including tests and non-broadcasting scripts.
418    pub always_use_create_2_factory: bool,
419    /// Controls how EIP-1559 fees are estimated for `forge script` broadcasts
420    /// (`low` / `market` / `aggressive`). Defaults to `market`, which preserves
421    /// the historical behavior (`base_fee * 2 + 20th-percentile priority fee`).
422    #[serde(default)]
423    pub eip1559_fee_estimate: Eip1559FeeEstimatePreset,
424    /// Sets a timeout in seconds for vm.prompt cheatcodes
425    pub prompt_timeout: u64,
426    /// The address which will be executing all tests
427    pub sender: Address,
428    /// The tx.origin value during EVM execution
429    pub tx_origin: Address,
430    /// the initial balance of each deployed test contract
431    pub initial_balance: U256,
432    /// the block.number value during EVM execution
433    #[serde(
434        deserialize_with = "crate::deserialize_u64_to_u256",
435        serialize_with = "crate::serialize_u64_or_u256"
436    )]
437    pub block_number: U256,
438    /// pins the block number for the state fork
439    pub fork_block_number: Option<u64>,
440    /// The chain name or EIP-155 chain ID.
441    #[serde(rename = "chain_id", alias = "chain")]
442    pub chain: Option<Chain>,
443    /// Block gas limit.
444    pub gas_limit: GasLimit,
445    /// EIP-170: Contract code size limit in bytes. Useful to increase this because of tests.
446    pub code_size_limit: Option<usize>,
447    /// `tx.gasprice` value during EVM execution.
448    ///
449    /// This is an Option, so we can determine in fork mode whether to use the config's gas price
450    /// (if set by user) or the remote client's gas price.
451    pub gas_price: Option<u64>,
452    /// The base fee in a block.
453    pub block_base_fee_per_gas: u64,
454    /// The `block.coinbase` value during EVM execution.
455    pub block_coinbase: Address,
456    /// The `block.timestamp` value during EVM execution.
457    #[serde(
458        deserialize_with = "crate::deserialize_u64_to_u256",
459        serialize_with = "crate::serialize_u64_or_u256"
460    )]
461    pub block_timestamp: U256,
462    /// The `block.difficulty` value during EVM execution.
463    pub block_difficulty: u64,
464    /// Before merge the `block.max_hash`, after merge it is `block.prevrandao`.
465    pub block_prevrandao: B256,
466    /// The `block.gaslimit` value during EVM execution.
467    pub block_gas_limit: Option<GasLimit>,
468    /// The memory limit per EVM execution in bytes.
469    /// If this limit is exceeded, a `MemoryLimitOOG` result is thrown.
470    ///
471    /// The default is 128MiB.
472    pub memory_limit: u64,
473    /// Additional output selection for all contracts, such as "ir", "devdoc", "storageLayout",
474    /// etc.
475    ///
476    /// See the [Solc Compiler Api](https://docs.soliditylang.org/en/latest/using-the-compiler.html#compiler-api) for more information.
477    ///
478    /// The following values are always set because they're required by `forge`:
479    /// ```json
480    /// {
481    ///   "*": [
482    ///       "abi",
483    ///       "evm.bytecode",
484    ///       "evm.deployedBytecode",
485    ///       "evm.methodIdentifiers"
486    ///     ]
487    /// }
488    /// ```
489    #[serde(default)]
490    pub extra_output: Vec<ContractOutputSelection>,
491    /// If set, a separate JSON file will be emitted for every contract depending on the
492    /// selection, eg. `extra_output_files = ["metadata"]` will create a `metadata.json` for
493    /// each contract in the project.
494    ///
495    /// See [Contract Metadata](https://docs.soliditylang.org/en/latest/metadata.html) for more information.
496    ///
497    /// The difference between `extra_output = ["metadata"]` and
498    /// `extra_output_files = ["metadata"]` is that the former will include the
499    /// contract's metadata in the contract's json artifact, whereas the latter will emit the
500    /// output selection as separate files.
501    #[serde(default)]
502    pub extra_output_files: Vec<ContractOutputSelection>,
503    /// Whether to print the names of the compiled contracts.
504    pub names: bool,
505    /// Whether to print the sizes of the compiled contracts.
506    pub sizes: bool,
507    /// If set to true, changes compilation pipeline to go through the Yul intermediate
508    /// representation.
509    pub via_ir: bool,
510    /// Whether to turn on SSA CFG-based code generation via the IR.
511    /// This is an experimental feature (as of 0.8.35) that requires `experimental` to be enabled.
512    /// Enabling this option will also enable `via_ir` if it is not already enabled.
513    pub via_ssa_cfg: bool,
514    /// Whether to enable Solidity's experimental mode.
515    ///
516    /// This passes `--experimental` to solc, which is required by Solidity 0.8.35+ for
517    /// experimental features.
518    pub experimental: bool,
519    /// Whether to include the AST as JSON in the compiler output.
520    pub ast: bool,
521    /// RPC storage caching settings determines what chains and endpoints to cache
522    pub rpc_storage_caching: StorageCachingConfig,
523    /// Disables storage caching entirely. This overrides any settings made in
524    /// `rpc_storage_caching`
525    pub no_storage_caching: bool,
526    /// Disables rate limiting entirely. This overrides any settings made in
527    /// `compute_units_per_second`
528    pub no_rpc_rate_limit: bool,
529    /// Multiple rpc endpoints and their aliases
530    #[serde(default, skip_serializing_if = "RpcEndpoints::is_empty")]
531    pub rpc_endpoints: RpcEndpoints,
532    /// Whether to store the referenced sources in the metadata as literal data.
533    pub use_literal_content: bool,
534    /// Whether to include the metadata hash.
535    ///
536    /// The metadata hash is machine dependent. By default, this is set to [BytecodeHash::None] to allow for deterministic code, See: <https://docs.soliditylang.org/en/latest/metadata.html>
537    #[serde(with = "from_str_lowercase")]
538    pub bytecode_hash: BytecodeHash,
539    /// Whether to append the metadata hash to the bytecode.
540    ///
541    /// If this is `false` and the `bytecode_hash` option above is not `None` solc will issue a
542    /// warning.
543    pub cbor_metadata: bool,
544    /// How to treat revert (and require) reason strings.
545    #[serde(with = "serde_helpers::display_from_str_opt")]
546    pub revert_strings: Option<RevertStrings>,
547    /// Whether to compile in sparse mode
548    ///
549    /// If this option is enabled, only the required contracts/files will be selected to be
550    /// included in solc's output selection, see also [`OutputSelection`].
551    pub sparse_mode: bool,
552    /// Generates additional build info json files for every new build, containing the
553    /// `CompilerInput` and `CompilerOutput`.
554    pub build_info: bool,
555    /// The path to the `build-info` directory that contains the build info json files.
556    pub build_info_path: Option<PathBuf>,
557    /// Configuration for `forge fmt`
558    pub fmt: FormatterConfig,
559    /// Configuration for `forge lint`
560    pub lint: LinterConfig,
561    /// Configuration for `forge doc`
562    pub doc: DocConfig,
563    /// Configuration for `forge bind-json`
564    pub bind_json: BindJsonConfig,
565    /// Configures the permissions of cheat codes that touch the file system.
566    ///
567    /// This includes what operations can be executed (read, write)
568    pub fs_permissions: FsPermissions,
569
570    /// Whether to enable call isolation.
571    ///
572    /// Useful for more correct gas accounting and EVM behavior in general.
573    pub isolate: bool,
574
575    /// Whether to disable the block gas limit checks.
576    pub disable_block_gas_limit: bool,
577
578    /// Whether to enable the tx gas limit checks as imposed by Osaka (EIP-7825).
579    pub enable_tx_gas_limit: bool,
580
581    /// Deprecated address-label alias; use [`TracingConfig::labels`].
582    #[serde(default, skip_serializing_if = "AddressHashMap::is_empty")]
583    pub labels: AddressHashMap<String>,
584
585    /// Whether to enable safety checks for `vm.getCode` and `vm.getDeployedCode` invocations.
586    /// If disabled, it is possible to access artifacts which were not recompiled or cached.
587    pub unchecked_cheatcode_artifacts: bool,
588
589    /// CREATE2 salt to use for the library deployment in scripts.
590    pub create2_library_salt: B256,
591
592    /// The CREATE2 deployer address to use.
593    pub create2_deployer: Address,
594
595    /// Configuration for Vyper compiler
596    pub vyper: VyperConfig,
597
598    /// Soldeer dependencies
599    pub dependencies: Option<SoldeerDependencyConfig>,
600
601    /// Soldeer custom configs
602    pub soldeer: Option<SoldeerConfig>,
603
604    /// Whether failed assertions should revert.
605    ///
606    /// Note that this only applies to native (cheatcode) assertions, invoked on Vm contract.
607    pub assertions_revert: bool,
608
609    /// Whether `failed()` should be invoked to check if the test have failed.
610    pub legacy_assertions: bool,
611
612    /// Optional additional CLI arguments to pass to `solc` binary.
613    #[serde(default, skip_serializing_if = "Vec::is_empty")]
614    pub extra_args: Vec<String>,
615
616    /// Networks with enabled features.
617    #[serde(flatten)]
618    pub networks: NetworkConfigs,
619
620    /// Timeout for transactions in seconds.
621    pub transaction_timeout: u64,
622
623    /// Warnings gathered when loading the Config. See [`WarningsProvider`] for more information.
624    #[serde(rename = "__warnings", default, skip_serializing)]
625    pub warnings: Vec<Warning>,
626
627    /// Additional settings profiles to use when compiling.
628    #[serde(default)]
629    pub additional_compiler_profiles: Vec<SettingsOverrides>,
630
631    /// Restrictions on compilation of certain files.
632    #[serde(default)]
633    pub compilation_restrictions: Vec<CompilationRestrictions>,
634
635    /// Whether to enable script execution protection.
636    pub script_execution_protection: bool,
637
638    /// PRIVATE: This structure may grow, As such, constructing this structure should
639    /// _always_ be done using a public constructor or update syntax:
640    ///
641    /// ```ignore
642    /// use foundry_config::Config;
643    ///
644    /// let config = Config { src: "other".into(), ..Default::default() };
645    /// ```
646    #[doc(hidden)]
647    #[serde(skip)]
648    pub _non_exhaustive: (),
649}
650
651/// Diagnostic level (minimum) at which the process should finish with a non-zero exit.
652#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum, Default, Serialize)]
653#[serde(rename_all = "lowercase")]
654pub enum DenyLevel {
655    /// Always exit with zero code.
656    #[default]
657    Never,
658    /// Exit with a non-zero code if any warnings are found.
659    Warnings,
660    /// Exit with a non-zero code if any notes or warnings are found.
661    Notes,
662}
663
664// Custom deserialization to make `DenyLevel` parsing case-insensitive and backwards compatible with
665// booleans.
666impl<'de> Deserialize<'de> for DenyLevel {
667    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
668    where
669        D: Deserializer<'de>,
670    {
671        struct DenyLevelVisitor;
672
673        impl<'de> de::Visitor<'de> for DenyLevelVisitor {
674            type Value = DenyLevel;
675
676            fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
677                formatter.write_str("one of the following strings: `never`, `warnings`, `notes`")
678            }
679
680            fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
681            where
682                E: de::Error,
683            {
684                Ok(DenyLevel::from(value))
685            }
686
687            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
688            where
689                E: de::Error,
690            {
691                DenyLevel::from_str(value).map_err(de::Error::custom)
692            }
693        }
694
695        deserializer.deserialize_any(DenyLevelVisitor)
696    }
697}
698
699impl FromStr for DenyLevel {
700    type Err = String;
701
702    fn from_str(s: &str) -> Result<Self, Self::Err> {
703        match s.to_lowercase().as_str() {
704            "warnings" | "warning" | "w" => Ok(Self::Warnings),
705            "notes" | "note" | "n" => Ok(Self::Notes),
706            "never" | "false" | "f" => Ok(Self::Never),
707            _ => Err(format!(
708                "unknown variant: found `{s}`, expected one of `never`, `warnings`, `notes`"
709            )),
710        }
711    }
712}
713
714impl From<bool> for DenyLevel {
715    fn from(deny: bool) -> Self {
716        if deny { Self::Warnings } else { Self::Never }
717    }
718}
719
720impl DenyLevel {
721    /// Returns `true` if the deny level includes warnings.
722    pub const fn warnings(&self) -> bool {
723        match self {
724            Self::Never => false,
725            Self::Warnings | Self::Notes => true,
726        }
727    }
728
729    /// Returns `true` if the deny level includes notes.
730    pub const fn notes(&self) -> bool {
731        match self {
732            Self::Never | Self::Warnings => false,
733            Self::Notes => true,
734        }
735    }
736
737    /// Returns `true` if the deny level is set to never (only errors).
738    pub const fn never(&self) -> bool {
739        match self {
740            Self::Never => true,
741            Self::Warnings | Self::Notes => false,
742        }
743    }
744}
745
746/// Mapping of fallback standalone sections. See [`FallbackProfileProvider`].
747pub const STANDALONE_FALLBACK_SECTIONS: &[(&str, &str)] = &[("invariant", "fuzz")];
748
749/// Deprecated keys and their replacements.
750///
751/// See [Warning::DeprecatedKey]
752pub const DEPRECATIONS: &[(&str, &str)] =
753    &[("cancun", "evm_version = Cancun"), ("deny_warnings", "deny = warnings")];
754
755impl Config {
756    /// The default profile: "default"
757    pub const DEFAULT_PROFILE: Profile = Profile::Default;
758
759    /// The hardhat profile: "hardhat"
760    pub const HARDHAT_PROFILE: Profile = Profile::const_new("hardhat");
761
762    /// TOML section for profiles
763    pub const PROFILE_SECTION: &'static str = "profile";
764
765    /// External config sections, ignored from warnings.
766    pub const EXTERNAL_SECTION: &'static str = "external";
767
768    /// Standalone sections in the config which get integrated into the selected profile
769    pub const STANDALONE_SECTIONS: &'static [&'static str] = &[
770        "rpc_endpoints",
771        "etherscan",
772        "fmt",
773        "lint",
774        "doc",
775        "fuzz",
776        "invariant",
777        "symbolic",
778        "coverage",
779        "mutation",
780        "tracing",
781        "labels",
782        "dependencies",
783        "soldeer",
784        "vyper",
785        "bind_json",
786    ];
787
788    pub(crate) fn is_standalone_section<T: ?Sized + PartialEq<str>>(section: &T) -> bool {
789        section == Self::PROFILE_SECTION
790            || section == Self::EXTERNAL_SECTION
791            || Self::STANDALONE_SECTIONS.iter().any(|s| section == *s)
792    }
793
794    /// File name of config toml file
795    pub const FILE_NAME: &'static str = "foundry.toml";
796
797    const DEFAULT_SRC: &'static str = "src";
798
799    /// The name of the directory foundry reserves for itself under the user's home directory: `~`
800    pub const FOUNDRY_DIR_NAME: &'static str = ".foundry";
801
802    /// Default address for tx.origin
803    ///
804    /// `0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38`
805    pub const DEFAULT_SENDER: Address = address!("0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38");
806
807    /// Default salt for create2 library deployments
808    pub const DEFAULT_CREATE2_LIBRARY_SALT: FixedBytes<32> = FixedBytes::<32>::ZERO;
809
810    /// Default create2 deployer
811    pub const DEFAULT_CREATE2_DEPLOYER: Address =
812        address!("0x4e59b44847b379578588920ca78fbf26c0b4956c");
813
814    /// Loads the `Config` from the current directory.
815    ///
816    /// See [`figment`](Self::figment) for more details.
817    pub fn load() -> Result<Self, ExtractConfigError> {
818        Self::from_provider(Self::figment())
819    }
820
821    /// Loads the `Config` with the given `providers` preset.
822    ///
823    /// See [`figment`](Self::figment) for more details.
824    pub fn load_with_providers(providers: FigmentProviders) -> Result<Self, ExtractConfigError> {
825        Self::from_provider(Self::default().to_figment(providers))
826    }
827
828    /// Loads the `Config` from the given root directory.
829    ///
830    /// See [`figment_with_root`](Self::figment_with_root) for more details.
831    #[track_caller]
832    pub fn load_with_root(root: impl AsRef<Path>) -> Result<Self, ExtractConfigError> {
833        Self::from_provider(Self::figment_with_root(root.as_ref()))
834    }
835
836    /// Loads the `Config` from the given root directory, allowing profile fallback.
837    ///
838    /// Unlike [`load_with_root`](Self::load_with_root), if the selected profile (via
839    /// `FOUNDRY_PROFILE`) does not exist in the config, this falls back to the default profile
840    /// instead of returning an error. This is useful for loading nested lib/dependency configs
841    /// that may not define all profiles the main project uses.
842    #[track_caller]
843    pub fn load_with_root_and_fallback(root: impl AsRef<Path>) -> Result<Self, ExtractConfigError> {
844        let figment = Self::figment_with_root(root.as_ref());
845        Self::from_figment_fallback(Figment::from(figment))
846    }
847
848    /// Attempts to extract a `Config` from `provider`, returning the result.
849    ///
850    /// # Example
851    ///
852    /// ```rust
853    /// use figment::providers::{Env, Format, Toml};
854    /// use foundry_config::Config;
855    ///
856    /// // Use foundry's default `Figment`, but allow values from `other.toml`
857    /// // to supersede its values.
858    /// let figment = Config::figment().merge(Toml::file("other.toml").nested());
859    ///
860    /// let config = Config::from_provider(figment);
861    /// ```
862    #[doc(alias = "try_from")]
863    pub fn from_provider<T: Provider>(provider: T) -> Result<Self, ExtractConfigError> {
864        trace!("load config with provider: {:?}", provider.metadata());
865        Self::from_figment(Figment::from(provider.legacy_labels()))
866    }
867
868    /// Applies an inline provider on top of the current config without reloading external
869    /// providers such as `foundry.toml`, env vars, or remappings.
870    pub fn merge_inline_provider<T: Provider>(&self, provider: T) -> Result<Self, Error> {
871        let provider = Figment::from(provider.legacy_labels()).select(self.profile.clone());
872        let invariant_corpus_random_sequence_weight_configured =
873            self.invariant.corpus_random_sequence_weight_configured
874                || provider.contains("invariant.corpus_random_sequence_weight")
875                || provider
876                    .extract_inner::<bool>("invariant.corpus_random_sequence_weight_configured")
877                    .unwrap_or(false);
878        let invariant_workers_configured = self.invariant.workers_configured
879            || provider.contains("invariant.workers")
880            || provider.extract_inner::<bool>("invariant.workers_configured").unwrap_or(false)
881            || provider.extract_inner::<InvariantWorkers>("invariant.workers").is_ok();
882        let figment = self.to_figment(FigmentProviders::None).merge(provider);
883        let mut config = figment.extract::<Self>()?;
884        config.profile = self.profile.clone();
885        config.profiles = self.profiles.clone();
886        config.invariant.corpus_random_sequence_weight_configured =
887            invariant_corpus_random_sequence_weight_configured;
888        config.invariant.workers_configured = invariant_workers_configured;
889        config.normalize_hardfork_settings()?;
890
891        Ok(config)
892    }
893
894    #[doc(hidden)]
895    #[deprecated(note = "use `Config::from_provider` instead")]
896    pub fn try_from<T: Provider>(provider: T) -> Result<Self, ExtractConfigError> {
897        Self::from_provider(provider)
898    }
899
900    fn from_figment(figment: Figment) -> Result<Self, ExtractConfigError> {
901        Self::from_figment_inner(figment, true)
902    }
903
904    /// Same as `from_figment` but allows unknown profiles, falling back to default profile.
905    /// Used when loading nested lib configs that may not define all profiles.
906    fn from_figment_fallback(figment: Figment) -> Result<Self, ExtractConfigError> {
907        Self::from_figment_inner(figment, false)
908    }
909
910    fn from_figment_inner(
911        figment: Figment,
912        strict_profile: bool,
913    ) -> Result<Self, ExtractConfigError> {
914        let invariant_corpus_random_sequence_weight_configured = figment
915            .extract_inner::<bool>("invariant.corpus_random_sequence_weight_configured")
916            .unwrap_or_else(|_| {
917                figment_value_is_configured(&figment, "invariant.corpus_random_sequence_weight")
918            });
919        let invariant_workers_configured = figment
920            .extract_inner::<bool>("invariant.workers_configured")
921            .unwrap_or_else(|_| figment_value_is_configured(&figment, "invariant.workers"));
922        let mut config = figment.extract::<Self>().map_err(ExtractConfigError::new)?;
923        config.invariant.corpus_random_sequence_weight_configured =
924            invariant_corpus_random_sequence_weight_configured;
925        config.invariant.workers_configured = invariant_workers_configured;
926        let selected_profile = figment.profile().clone();
927
928        // The `"profile"` profile contains all the profiles as keys.
929        fn add_profile(profiles: &mut Vec<Profile>, profile: &Profile) {
930            if !profiles.contains(profile) {
931                profiles.push(profile.clone());
932            }
933        }
934        let figment = figment.select(Self::PROFILE_SECTION);
935        if let Ok(data) = figment.data()
936            && let Some(profiles) = data.get(&Profile::new(Self::PROFILE_SECTION))
937        {
938            for profile in profiles.keys() {
939                add_profile(&mut config.profiles, &Profile::new(profile));
940            }
941        }
942        add_profile(&mut config.profiles, &Self::DEFAULT_PROFILE);
943
944        // Check if the selected profile exists.
945        if config.profiles.contains(&selected_profile) {
946            config.profile = selected_profile;
947        } else {
948            // Fall back to the default profile. In strict mode (top-level loads), emit a warning
949            // so users are informed when an unknown profile (e.g. via `FOUNDRY_PROFILE`) is
950            // selected; the silent fallback is reserved for nested lib configs.
951            if strict_profile {
952                config
953                    .warnings
954                    .push(Warning::UnknownProfile { profile: selected_profile.to_string() });
955            }
956            config.profile = Self::DEFAULT_PROFILE;
957        }
958
959        config.normalize_optimizer_settings();
960        config.normalize_hardfork_settings().map_err(ExtractConfigError::new)?;
961
962        // Validate optimizer_runs does not exceed u32::MAX (Solidity compiler limit)
963        if let Some(runs) = config.optimizer_runs
964            && runs > u32::MAX as usize
965        {
966            return Err(ExtractConfigError::new(Error::from(format!(
967                "`optimizer_runs` value {} exceeds maximum allowed value of {}",
968                runs,
969                u32::MAX
970            ))));
971        }
972
973        Ok(config)
974    }
975
976    fn normalize_hardfork_settings(&mut self) -> Result<(), Error> {
977        self.networks.validate().map_err(Error::from)?;
978        let Some(hardfork) = self.hardfork else { return Ok(()) };
979        self.networks = self.networks.normalize_for_hardfork(hardfork).map_err(Error::from)?;
980        Ok(())
981    }
982
983    fn uses_default_src(&self) -> bool {
984        self.src == Path::new(Self::DEFAULT_SRC)
985    }
986
987    /// Returns the populated [Figment] using the requested [FigmentProviders] preset.
988    ///
989    /// This will merge various providers, such as env,toml,remappings into the figment if
990    /// requested.
991    pub fn to_figment(&self, providers: FigmentProviders) -> Figment {
992        // Note that `Figment::from` here is a method on `Figment` rather than the `From` impl below
993
994        if providers.is_none() {
995            return Figment::from(self);
996        }
997
998        let root = self.root.as_path();
999        let profile = Self::selected_profile();
1000        let mut figment = Figment::default()
1001            .merge(DappHardhatDirProvider { root, detect_src: self.uses_default_src() });
1002
1003        // merge global foundry.toml file
1004        if let Some(global_toml) = Self::foundry_dir_toml().filter(|p| p.exists()) {
1005            figment = Self::merge_toml_provider(
1006                figment,
1007                TomlFileProvider::new(None, global_toml),
1008                profile.clone(),
1009            );
1010        }
1011        // merge local foundry.toml file
1012        figment = Self::merge_toml_provider(
1013            figment,
1014            TomlFileProvider::new(Some("FOUNDRY_CONFIG"), root.join(Self::FILE_NAME)),
1015            profile.clone(),
1016        );
1017
1018        // merge environment variables
1019        figment = figment
1020            .merge(
1021                Env::prefixed("DAPP_")
1022                    .ignore(&["REMAPPINGS", "LIBRARIES", "FFI", "FS_PERMISSIONS"])
1023                    .global()
1024                    .legacy_labels(),
1025            )
1026            .merge(
1027                Env::prefixed("DAPP_TEST_")
1028                    .ignore(&["CACHE", "FUZZ_RUNS", "DEPTH", "FFI", "FS_PERMISSIONS"])
1029                    .global()
1030                    .legacy_labels(),
1031            )
1032            .merge(DappEnvCompatProvider)
1033            .merge(EtherscanEnvProvider::default())
1034            .merge(
1035                Env::prefixed("FOUNDRY_")
1036                    .ignore(&["PROFILE", "REMAPPINGS", "LIBRARIES", "FFI", "FS_PERMISSIONS"])
1037                    .map(|key| {
1038                        let key = key.as_str();
1039                        if Self::STANDALONE_SECTIONS.iter().any(|section| {
1040                            key.starts_with(&format!("{}_", section.to_ascii_uppercase()))
1041                        }) {
1042                            key.replacen('_', ".", 1).into()
1043                        } else {
1044                            key.into()
1045                        }
1046                    })
1047                    .global()
1048                    .legacy_labels(),
1049            )
1050            .select(profile.clone());
1051
1052        // only resolve remappings if all providers are requested
1053        if providers.is_all() {
1054            // we try to merge remappings after we've merged all other providers, this prevents
1055            // redundant fs lookups to determine the default remappings that are eventually updated
1056            // by other providers, like the toml file
1057            let remappings = RemappingsProvider {
1058                auto_detect_remappings: figment
1059                    .extract_inner::<bool>("auto_detect_remappings")
1060                    .unwrap_or(true),
1061                lib_paths: figment
1062                    .extract_inner::<Vec<PathBuf>>("libs")
1063                    .map(Cow::Owned)
1064                    .unwrap_or_else(|_| Cow::Borrowed(&self.libs)),
1065                root,
1066                remappings: figment.extract_inner::<Vec<Remapping>>("remappings"),
1067            };
1068            figment = figment.merge(remappings);
1069        }
1070
1071        let invariant_corpus_random_sequence_weight_configured = self
1072            .invariant
1073            .corpus_random_sequence_weight_configured
1074            || figment
1075                .extract_inner::<bool>("invariant.corpus_random_sequence_weight_configured")
1076                .unwrap_or_else(|_| {
1077                    figment_value_is_configured(&figment, "invariant.corpus_random_sequence_weight")
1078                });
1079        let invariant_workers_configured = self.invariant.workers_configured
1080            || figment.extract_inner::<bool>("invariant.workers_configured").unwrap_or_else(|_| {
1081                figment.extract_inner::<InvariantWorkers>("invariant.workers").is_ok()
1082            });
1083
1084        // normalize defaults
1085        figment = self.normalize_defaults(figment);
1086        if invariant_corpus_random_sequence_weight_configured {
1087            figment = figment.merge(("invariant.corpus_random_sequence_weight_configured", true));
1088        }
1089        if invariant_workers_configured {
1090            figment = figment.merge(("invariant.workers_configured", true));
1091        }
1092
1093        Figment::from(self).merge(figment).select(profile)
1094    }
1095
1096    /// The config supports relative paths and tracks the root path separately see
1097    /// `Config::with_root`
1098    ///
1099    /// This joins all relative paths with the current root and attempts to make them canonic
1100    #[must_use]
1101    pub fn canonic(self) -> Self {
1102        let root = self.root.clone();
1103        self.canonic_at(root)
1104    }
1105
1106    /// Joins all relative paths with the given root so that paths that are defined as:
1107    ///
1108    /// ```toml
1109    /// [profile.default]
1110    /// src = "src"
1111    /// out = "./out"
1112    /// libs = ["lib", "/var/lib"]
1113    /// ```
1114    ///
1115    /// Will be made canonic with the given root:
1116    ///
1117    /// ```toml
1118    /// [profile.default]
1119    /// src = "<root>/src"
1120    /// out = "<root>/out"
1121    /// libs = ["<root>/lib", "/var/lib"]
1122    /// ```
1123    #[must_use]
1124    pub fn canonic_at(mut self, root: impl Into<PathBuf>) -> Self {
1125        let root = canonic(root);
1126
1127        fn p(root: &Path, rem: &Path) -> PathBuf {
1128            canonic(root.join(rem))
1129        }
1130
1131        self.src = p(&root, &self.src);
1132        self.test = p(&root, &self.test);
1133        self.script = p(&root, &self.script);
1134        self.out = p(&root, &self.out);
1135        self.broadcast = p(&root, &self.broadcast);
1136        self.cache_path = p(&root, &self.cache_path);
1137        self.snapshots = p(&root, &self.snapshots);
1138        self.test_failures_file = p(&root, &self.test_failures_file);
1139
1140        if let Some(build_info_path) = self.build_info_path {
1141            self.build_info_path = Some(p(&root, &build_info_path));
1142        }
1143
1144        self.libs = self.libs.into_iter().map(|lib| p(&root, &lib)).collect();
1145
1146        self.remappings = self
1147            .remappings
1148            .into_iter()
1149            .map(|r| relative_remapping_preserving_context_boundary(r.into(), &root))
1150            .collect();
1151
1152        self.allow_paths = self.allow_paths.into_iter().map(|allow| p(&root, &allow)).collect();
1153
1154        self.include_paths = self.include_paths.into_iter().map(|allow| p(&root, &allow)).collect();
1155
1156        self.fs_permissions.join_all(&root);
1157
1158        if let Some(model_checker) = &mut self.model_checker {
1159            model_checker.contracts = std::mem::take(&mut model_checker.contracts)
1160                .into_iter()
1161                .map(|(path, contracts)| {
1162                    (format!("{}", p(&root, path.as_ref()).display()), contracts)
1163                })
1164                .collect();
1165        }
1166
1167        self
1168    }
1169
1170    /// Normalizes the evm version if a [SolcReq] is set
1171    pub fn normalized_evm_version(mut self) -> Self {
1172        self.normalize_evm_version();
1173        self
1174    }
1175
1176    /// Normalizes optimizer settings.
1177    /// See <https://github.com/foundry-rs/foundry/issues/9665>
1178    pub const fn normalized_optimizer_settings(mut self) -> Self {
1179        self.normalize_optimizer_settings();
1180        self
1181    }
1182
1183    /// Normalizes the evm version if a [SolcReq] is set to a valid version.
1184    pub fn normalize_evm_version(&mut self) {
1185        self.evm_version = self.get_normalized_evm_version();
1186    }
1187
1188    /// Normalizes optimizer settings:
1189    /// - with default settings, optimizer is set to false and optimizer runs to 200
1190    /// - if optimizer is set and optimizer runs not specified, then optimizer runs is set to 200
1191    /// - enable optimizer if not explicitly set and optimizer runs set to a value greater than 0
1192    pub const fn normalize_optimizer_settings(&mut self) {
1193        match (self.optimizer, self.optimizer_runs) {
1194            // Default: set the optimizer to false and optimizer runs to 200.
1195            (None, None) => {
1196                self.optimizer = Some(false);
1197                self.optimizer_runs = Some(200);
1198            }
1199            // Set the optimizer runs to 200 if the `optimizer` config set.
1200            (Some(_), None) => self.optimizer_runs = Some(200),
1201            // Enables optimizer if the `optimizer_runs` has been set with a value greater than 0.
1202            (None, Some(runs)) => self.optimizer = Some(runs > 0),
1203            _ => {}
1204        }
1205    }
1206
1207    /// Returns the normalized [EvmVersion] for the current solc version, or the configured one.
1208    pub fn get_normalized_evm_version(&self) -> EvmVersion {
1209        if let Some(version) = self.solc_version()
1210            && let Some(evm_version) = self.evm_version.normalize_version_solc(&version)
1211        {
1212            return evm_version;
1213        }
1214        self.evm_version
1215    }
1216
1217    /// Returns a sanitized version of the Config where are paths are set correctly and potential
1218    /// duplicates are resolved
1219    ///
1220    /// See [`Self::canonic`]
1221    #[must_use]
1222    pub fn sanitized(self) -> Self {
1223        let mut config = self.canonic();
1224
1225        config.sanitize_remappings();
1226
1227        config.libs.sort_unstable();
1228        config.libs.dedup();
1229
1230        config
1231    }
1232
1233    /// Cleans up any duplicate `Remapping` and sorts them
1234    ///
1235    /// On windows this will convert any `\` in the remapping path into a `/`
1236    #[allow(clippy::missing_const_for_fn)]
1237    pub fn sanitize_remappings(&mut self) {
1238        #[cfg(target_os = "windows")]
1239        {
1240            // force `/` in remappings on windows
1241            use path_slash::PathBufExt;
1242            self.remappings.iter_mut().for_each(|r| {
1243                r.path.path = r.path.path.to_slash_lossy().into_owned().into();
1244            });
1245        }
1246    }
1247
1248    /// Returns the directory in which dependencies should be installed
1249    ///
1250    /// Returns the first dir from `libs` that is not `node_modules` or `lib` if `libs` is empty
1251    pub fn install_lib_dir(&self) -> &Path {
1252        self.libs
1253            .iter()
1254            .find(|p| !p.ends_with("node_modules"))
1255            .map(|p| p.as_path())
1256            .unwrap_or_else(|| Path::new("lib"))
1257    }
1258
1259    /// Serves as the entrypoint for obtaining the project.
1260    ///
1261    /// Returns the `Project` configured with all `solc` and path related values.
1262    ///
1263    /// *Note*: this also _cleans_ [`Project::cleanup`] the workspace if `force` is set to true.
1264    ///
1265    /// # Example
1266    ///
1267    /// ```
1268    /// use foundry_config::Config;
1269    /// let config = Config::load_with_root(".")?.sanitized();
1270    /// let project = config.project()?;
1271    /// # Ok::<_, eyre::Error>(())
1272    /// ```
1273    pub fn project(&self) -> Result<Project<MultiCompiler>, SolcError> {
1274        self.create_project(self.cache, false)
1275    }
1276
1277    /// Same as [`Self::project()`] but sets configures the project to not emit artifacts and ignore
1278    /// cache.
1279    pub fn ephemeral_project(&self) -> Result<Project<MultiCompiler>, SolcError> {
1280        self.create_project(false, true)
1281    }
1282
1283    /// A cached, in-memory project that does not request any artifacts.
1284    ///
1285    /// Use this when you just want the source graph or the Solar compiler context.
1286    pub fn solar_project(&self) -> Result<Project<MultiCompiler>, SolcError> {
1287        let ui_testing = std::env::var_os("FOUNDRY_LINT_UI_TESTING").is_some();
1288        let mut project = self.create_project(self.cache && !ui_testing, false)?;
1289        project.update_output_selection(|selection| {
1290            // We have to request something to populate `contracts` in the output and thus
1291            // artifacts.
1292            *selection = OutputSelection::common_output_selection(["abi".into()]);
1293        });
1294        Ok(project)
1295    }
1296
1297    /// Builds mapping with additional settings profiles.
1298    fn additional_settings(
1299        &self,
1300        base: &MultiCompilerSettings,
1301    ) -> BTreeMap<String, MultiCompilerSettings> {
1302        let mut map = BTreeMap::new();
1303
1304        for profile in &self.additional_compiler_profiles {
1305            let mut settings = base.clone();
1306            profile.apply(&mut settings);
1307            map.insert(profile.name.clone(), settings);
1308        }
1309
1310        map
1311    }
1312
1313    /// Resolves globs and builds a mapping from individual source files to their restrictions
1314    #[expect(clippy::disallowed_macros)]
1315    fn restrictions(
1316        &self,
1317        paths: &ProjectPathsConfig,
1318    ) -> Result<BTreeMap<PathBuf, RestrictionsWithVersion<MultiCompilerRestrictions>>, SolcError>
1319    {
1320        let mut map: BTreeMap<PathBuf, RestrictionsWithVersion<MultiCompilerRestrictions>> =
1321            BTreeMap::new();
1322        if self.compilation_restrictions.is_empty() {
1323            return Ok(BTreeMap::new());
1324        }
1325
1326        let graph = Graph::<MultiCompilerParser>::resolve(paths)?;
1327        let (sources, _) = graph.into_sources();
1328
1329        for res in &self.compilation_restrictions {
1330            for source in sources.keys().filter(|path| {
1331                if res.paths.is_match(path) {
1332                    true
1333                } else if let Ok(path) = path.strip_prefix(&paths.root) {
1334                    res.paths.is_match(path)
1335                } else {
1336                    false
1337                }
1338            }) {
1339                let res: RestrictionsWithVersion<_> =
1340                    res.clone().try_into().map_err(SolcError::msg)?;
1341                if map.contains_key(source) {
1342                    let value = map.remove(source.as_path()).unwrap();
1343                    if let Some(merged) = value.clone().merge(res) {
1344                        map.insert(source.clone(), merged);
1345                    } else {
1346                        // `sh_warn!` is a circular dependency, preventing us from using it here.
1347                        eprintln!(
1348                            "{}",
1349                            yansi::Paint::yellow(&format!(
1350                                "Failed to merge compilation restrictions for {}",
1351                                source.display()
1352                            ))
1353                        );
1354                        map.insert(source.clone(), value);
1355                    }
1356                } else {
1357                    map.insert(source.clone(), res);
1358                }
1359            }
1360        }
1361
1362        Ok(map)
1363    }
1364
1365    /// Creates a [`Project`] with the given `cached` and `no_artifacts` flags.
1366    ///
1367    /// Prefer using [`Self::project`] or [`Self::ephemeral_project`] instead.
1368    pub fn create_project(&self, cached: bool, no_artifacts: bool) -> Result<Project, SolcError> {
1369        let settings = self.compiler_settings()?;
1370        let paths = self.project_paths();
1371
1372        // Strip "./" prefix for consistent path matching
1373        let parse_path = |path: &PathBuf| path.strip_prefix("./").unwrap_or(path).to_path_buf();
1374
1375        let mut builder = Project::builder()
1376            .artifacts(self.configured_artifacts_handler())
1377            .additional_settings(self.additional_settings(&settings))
1378            .restrictions(self.restrictions(&paths)?)
1379            .settings(settings)
1380            .paths(paths)
1381            .ignore_error_codes(self.ignored_error_codes.iter().copied().map(Into::into))
1382            .ignore_error_codes_from(self.ignored_error_codes_from.iter().map(|(path, codes)| {
1383                (parse_path(path), codes.iter().copied().map(Into::into).collect())
1384            }))
1385            .ignore_paths(self.ignored_file_paths.iter().map(parse_path).collect())
1386            .set_compiler_severity_filter(if self.deny.warnings() {
1387                Severity::Warning
1388            } else {
1389                Severity::Error
1390            })
1391            .set_offline(self.offline)
1392            .set_cached(cached)
1393            .set_build_info(!no_artifacts && self.build_info)
1394            .set_no_artifacts(no_artifacts);
1395
1396        if !self.skip.is_empty() {
1397            let filter = SkipBuildFilters::new(self.skip.clone(), self.root.clone());
1398            builder = builder.sparse_output(filter);
1399        }
1400
1401        let project = builder.build(self.compiler()?)?;
1402
1403        // `ProjectBuilder` slashes paths on Windows. Re-encode a contextual remapping's trailing
1404        // directory boundary with the native separator so a later `Remapping::to_string` does not
1405        // discard it while converting the context back to slash-separated solc syntax.
1406        #[cfg(windows)]
1407        let mut project = project;
1408        #[cfg(windows)]
1409        for remapping in &mut project.paths.remappings {
1410            if let Some(context) = &mut remapping.context
1411                && context.ends_with('/')
1412            {
1413                context.pop();
1414                context.push(std::path::MAIN_SEPARATOR);
1415            }
1416        }
1417
1418        if self.force {
1419            // Warnings are intentionally dropped here because `sh_warn!` is a circular
1420            // dependency. Callers that need warnings should call `cleanup()` directly.
1421            let _ = self.cleanup(&project);
1422        }
1423
1424        Ok(project)
1425    }
1426
1427    /// Disables optimizations and enables viaIR with minimum optimization if `ir_minimum` is true.
1428    pub fn disable_optimizations(&self, project: &mut Project, ir_minimum: bool) {
1429        if ir_minimum {
1430            // Enable viaIR with minimum optimization: https://github.com/ethereum/solidity/issues/12533#issuecomment-1013073350
1431            // And also in new releases of Solidity: https://github.com/ethereum/solidity/issues/13972#issuecomment-1628632202
1432            project.settings.solc.settings = std::mem::take(&mut project.settings.solc.settings)
1433                .with_via_ir_minimum_optimization();
1434
1435            // Sanitize settings for solc 0.8.4 if version cannot be detected: https://github.com/foundry-rs/foundry/issues/9322
1436            // But keep the EVM version: https://github.com/ethereum/solidity/issues/15775
1437            let evm_version = project.settings.solc.evm_version;
1438            let version = self.solc_version().unwrap_or_else(|| Version::new(0, 8, 4));
1439            project.settings.solc.settings.sanitize(&version, SolcLanguage::Solidity);
1440            project.settings.solc.evm_version = evm_version;
1441        } else {
1442            project.settings.solc.optimizer.disable();
1443            project.settings.solc.optimizer.runs = None;
1444            project.settings.solc.optimizer.details = None;
1445            project.settings.solc.via_ir = None;
1446        }
1447    }
1448
1449    /// Cleans the project.
1450    ///
1451    /// Returns a list of warning messages for any non-fatal cleanup failures. Cleanup is
1452    /// best-effort: all steps are attempted even if some fail.
1453    pub fn cleanup<C: Compiler, T: ArtifactOutput<CompilerContract = C::CompilerContract>>(
1454        &self,
1455        project: &Project<C, T>,
1456    ) -> Result<Vec<String>, SolcError> {
1457        let mut warnings = Vec::new();
1458
1459        if let Err(err) = project.cleanup() {
1460            warnings.push(format!("failed to clean project artifacts: {err}"));
1461        }
1462
1463        // Remove last test run failures file.
1464        if let Err(err) = fs::remove_file(&self.test_failures_file)
1465            && err.kind() != io::ErrorKind::NotFound
1466        {
1467            warnings.push(format!(
1468                "failed to remove test failures file {}: {err}",
1469                self.test_failures_file.display()
1470            ));
1471        }
1472
1473        // Remove mutation test cache directory
1474        let _ = fs::remove_dir_all(project.root().join(&self.mutation_dir));
1475
1476        // Remove fuzz and invariant cache directories.
1477        let mut remove_test_dir = |test_dir: &Option<PathBuf>| {
1478            if let Some(test_dir) = test_dir {
1479                let path = project.root().join(test_dir);
1480                if let Err(err) = fs::remove_dir_all(&path)
1481                    && err.kind() != io::ErrorKind::NotFound
1482                {
1483                    warnings.push(format!(
1484                        "failed to remove test cache directory {}: {err}",
1485                        path.display()
1486                    ));
1487                }
1488            }
1489        };
1490        remove_test_dir(&self.fuzz.failure_persist_dir);
1491        remove_test_dir(&self.fuzz.corpus.corpus_dir);
1492        remove_test_dir(&self.fuzz.corpus.frontier_dir);
1493        remove_test_dir(&self.invariant.corpus.corpus_dir);
1494        remove_test_dir(&self.invariant.failure_persist_dir);
1495
1496        Ok(warnings)
1497    }
1498
1499    /// Ensures that the configured version is installed if explicitly set
1500    ///
1501    /// If `solc` is [`SolcReq::Version`] then this will download and install the solc version if
1502    /// it's missing, unless the `offline` flag is enabled, in which case an error is thrown.
1503    ///
1504    /// If `solc` is [`SolcReq::Local`] then this will ensure that the path exists.
1505    fn ensure_solc(&self) -> Result<Option<Solc>, SolcError> {
1506        if let Some(solc) = &self.solc {
1507            let solc = match solc {
1508                SolcReq::Version(version) => {
1509                    if let Some(solc) = Solc::find_svm_installed_version(version)? {
1510                        solc
1511                    } else {
1512                        if self.offline {
1513                            return Err(SolcError::msg(format!(
1514                                "can't install missing solc {version} in offline mode"
1515                            )));
1516                        }
1517                        Solc::blocking_install(version)?
1518                    }
1519                }
1520                SolcReq::Local(solc) => {
1521                    if !solc.is_file() {
1522                        return Err(SolcError::msg(format!("`solc` {solc:?} does not exist")));
1523                    }
1524                    warn_local_compiler(solc);
1525                    Solc::new(solc)?
1526                }
1527            };
1528            return Ok(Some(solc));
1529        }
1530
1531        Ok(None)
1532    }
1533
1534    /// Returns the Spec derived from the configured [EvmVersion]
1535    pub fn evm_spec_id<SPEC: FromEvmVersion>(&self) -> SPEC {
1536        self.hardfork.map(Into::into).unwrap_or_else(|| evm_spec_id(self.evm_version))
1537    }
1538
1539    /// Returns whether the compiler version should be auto-detected
1540    ///
1541    /// Returns `false` if `solc_version` is explicitly set, otherwise returns the value of
1542    /// `auto_detect_solc`
1543    pub const fn is_auto_detect(&self) -> bool {
1544        if self.solc.is_some() {
1545            return false;
1546        }
1547        self.auto_detect_solc
1548    }
1549
1550    /// Whether caching should be enabled for the given chain id
1551    pub fn enable_caching(&self, endpoint: &str, chain_id: impl Into<u64>) -> bool {
1552        !self.no_storage_caching
1553            && self.rpc_storage_caching.enable_for_chain_id(chain_id.into())
1554            && self.rpc_storage_caching.enable_for_endpoint(endpoint)
1555    }
1556
1557    /// Returns the `ProjectPathsConfig` sub set of the config.
1558    ///
1559    /// **NOTE**: this uses the paths as they are and does __not__ modify them, see
1560    /// `[Self::sanitized]`
1561    ///
1562    /// # Example
1563    ///
1564    /// ```
1565    /// use foundry_compilers::solc::Solc;
1566    /// use foundry_config::Config;
1567    /// let config = Config::load_with_root(".")?.sanitized();
1568    /// let paths = config.project_paths::<Solc>();
1569    /// # Ok::<_, eyre::Error>(())
1570    /// ```
1571    pub fn project_paths<L>(&self) -> ProjectPathsConfig<L> {
1572        let mut builder = ProjectPathsConfig::builder()
1573            .cache(self.cache_path.join(SOLIDITY_FILES_CACHE_FILENAME))
1574            .sources(&self.src)
1575            .tests(&self.test)
1576            .scripts(&self.script)
1577            .artifacts(&self.out)
1578            .libs(self.libs.iter())
1579            .remappings(self.project_remappings())
1580            .allowed_path(&self.root)
1581            .allowed_paths(&self.libs)
1582            .allowed_paths(&self.allow_paths)
1583            .include_paths(&self.include_paths);
1584
1585        if let Some(build_info_path) = &self.build_info_path {
1586            builder = builder.build_infos(build_info_path);
1587        }
1588
1589        builder.build_with_root(&self.root)
1590    }
1591
1592    /// Returns configuration for a compiler to use when setting up a [Project].
1593    pub fn solc_compiler(&self) -> Result<SolcCompiler, SolcError> {
1594        if let Some(solc) = self.ensure_solc()? {
1595            Ok(SolcCompiler::Specific(solc))
1596        } else {
1597            Ok(SolcCompiler::AutoDetect)
1598        }
1599    }
1600
1601    /// Returns the solc version, if any.
1602    pub fn solc_version(&self) -> Option<Version> {
1603        self.solc.as_ref().and_then(|solc| solc.try_version().ok())
1604    }
1605
1606    /// Returns configured [Vyper] compiler.
1607    pub fn vyper_compiler(&self) -> Result<Option<Vyper>, SolcError> {
1608        // Only instantiate Vyper if there are any Vyper files in the project.
1609        if !self.project_paths::<VyperLanguage>().has_input_files() {
1610            return Ok(None);
1611        }
1612        let vyper = if let Some(path) = &self.vyper.path {
1613            warn_local_compiler(path);
1614            Some(Vyper::new(path)?)
1615        } else {
1616            Vyper::new("vyper").ok()
1617        };
1618        Ok(vyper)
1619    }
1620
1621    /// Returns configuration for a compiler to use when setting up a [Project].
1622    pub fn compiler(&self) -> Result<MultiCompiler, SolcError> {
1623        Ok(MultiCompiler { solc: Some(self.solc_compiler()?), vyper: self.vyper_compiler()? })
1624    }
1625
1626    /// Returns configured [MultiCompilerSettings].
1627    pub fn compiler_settings(&self) -> Result<MultiCompilerSettings, SolcError> {
1628        Ok(MultiCompilerSettings { solc: self.solc_settings()?, vyper: self.vyper_settings()? })
1629    }
1630
1631    /// Returns all configured remappings.
1632    pub fn get_all_remappings(&self) -> impl Iterator<Item = Remapping> + '_ {
1633        self.remappings.iter().map(|m| m.clone().into())
1634    }
1635
1636    /// Returns project remappings with absolute aliases for relative filesystem contexts.
1637    fn project_remappings(&self) -> Vec<Remapping> {
1638        let remappings = self.get_all_remappings().collect::<Vec<_>>();
1639        let mut adjusted = Vec::with_capacity(remappings.len());
1640
1641        // External source unit names remain absolute in the compiler input, while configured
1642        // contexts are root-relative. Preserve the configured order and add an equivalent absolute
1643        // context immediately after each relative form so both the project resolver and compiler
1644        // select the same mapping.
1645        for remapping in &remappings {
1646            adjusted.push(remapping.clone());
1647
1648            let Some(context) = remapping.context.as_deref() else { continue };
1649            if Path::new(context).is_absolute() {
1650                continue;
1651            }
1652            let Ok(context_path) = foundry_compilers::utils::normalize_solidity_import_path(
1653                &self.root,
1654                Path::new(context),
1655            ) else {
1656                continue;
1657            };
1658            // `normalize_solidity_import_path` returns a slash path on Windows. Convert it back to
1659            // a native path before it enters `ProjectBuilder`, which performs the one canonical
1660            // slash conversion for compiler source-unit names.
1661            #[cfg(windows)]
1662            let context_path = PathBuf::from_slash(context_path.to_string_lossy());
1663            let mut context_path = context_path.display().to_string();
1664            if context.ends_with(['/', '\\']) && !context_path.ends_with(['/', '\\']) {
1665                context_path.push(std::path::MAIN_SEPARATOR);
1666            }
1667
1668            let mut absolute = remapping.clone();
1669            absolute.context = Some(context_path);
1670            if !remappings.contains(&absolute) && !adjusted.contains(&absolute) {
1671                adjusted.push(absolute);
1672            }
1673        }
1674        adjusted
1675    }
1676
1677    /// Returns the configured rpc jwt secret
1678    ///
1679    /// Returns:
1680    ///    - The jwt secret, if configured
1681    ///
1682    /// # Example
1683    ///
1684    /// ```
1685    /// use foundry_config::Config;
1686    /// # fn t() {
1687    /// let config = Config::with_root("./");
1688    /// let rpc_jwt = config.get_rpc_jwt_secret().unwrap().unwrap();
1689    /// # }
1690    /// ```
1691    pub fn get_rpc_jwt_secret(&self) -> Result<Option<Cow<'_, str>>, UnresolvedEnvVarError> {
1692        Ok(self.eth_rpc_jwt.as_ref().map(|jwt| Cow::Borrowed(jwt.as_str())))
1693    }
1694
1695    /// Returns the configured rpc url
1696    ///
1697    /// Returns:
1698    ///    - the matching, resolved url of  `rpc_endpoints` if `eth_rpc_url` is an alias
1699    ///    - the `eth_rpc_url` as-is if it isn't an alias
1700    ///
1701    /// # Example
1702    ///
1703    /// ```
1704    /// use foundry_config::Config;
1705    /// # fn t() {
1706    /// let config = Config::with_root("./");
1707    /// let rpc_url = config.get_rpc_url().unwrap().unwrap();
1708    /// # }
1709    /// ```
1710    pub fn get_rpc_url(&self) -> Option<Result<Cow<'_, str>, UnresolvedEnvVarError>> {
1711        let maybe_alias = self.eth_rpc_url.as_deref()?;
1712        if let Some(alias) = self.get_rpc_url_with_alias(maybe_alias) {
1713            Some(alias)
1714        } else {
1715            Some(Ok(Cow::Borrowed(self.eth_rpc_url.as_deref()?)))
1716        }
1717    }
1718
1719    /// Resolves the given alias to a matching rpc url
1720    ///
1721    /// # Returns
1722    ///
1723    /// In order of resolution:
1724    ///
1725    /// - the matching, resolved url of `rpc_endpoints` if `maybe_alias` is an alias
1726    /// - a mesc resolved url if `maybe_alias` is a known alias in mesc
1727    /// - `None` otherwise
1728    ///
1729    /// # Note on mesc
1730    ///
1731    /// The endpoint is queried for in mesc under the `foundry` profile, allowing users to customize
1732    /// endpoints for Foundry specifically.
1733    ///
1734    /// # Example
1735    ///
1736    /// ```
1737    /// use foundry_config::Config;
1738    /// # fn t() {
1739    /// let config = Config::with_root("./");
1740    /// let rpc_url = config.get_rpc_url_with_alias("mainnet").unwrap().unwrap();
1741    /// # }
1742    /// ```
1743    pub fn get_rpc_url_with_alias(
1744        &self,
1745        maybe_alias: &str,
1746    ) -> Option<Result<Cow<'_, str>, UnresolvedEnvVarError>> {
1747        let mut endpoints = self.rpc_endpoints.clone().resolved();
1748        if let Some(endpoint) = endpoints.remove(maybe_alias) {
1749            return Some(endpoint.url().map(Cow::Owned));
1750        }
1751
1752        if let Some(mesc_url) = self.get_rpc_url_from_mesc(maybe_alias) {
1753            return Some(Ok(Cow::Owned(mesc_url)));
1754        }
1755
1756        if let Some(builtin) = crate::endpoints::builtin_rpc_url(maybe_alias) {
1757            return Some(Ok(Cow::Borrowed(builtin)));
1758        }
1759
1760        None
1761    }
1762
1763    /// Attempts to resolve the URL for the given alias from [`mesc`](https://github.com/paradigmxyz/mesc)
1764    pub fn get_rpc_url_from_mesc(&self, maybe_alias: &str) -> Option<String> {
1765        // Note: mesc requires a MESC_PATH in the env, which the user can configure and is expected
1766        // to be part of the shell profile, default is ~/mesc.json
1767        let mesc_config = mesc::load::load_config_data()
1768            .inspect_err(|err| debug!(%err, "failed to load mesc config"))
1769            .ok()?;
1770
1771        if let Ok(Some(endpoint)) =
1772            mesc::query::get_endpoint_by_query(&mesc_config, maybe_alias, Some("foundry"))
1773        {
1774            return Some(endpoint.url);
1775        }
1776
1777        if maybe_alias.chars().all(|c| c.is_numeric()) {
1778            // try to lookup the mesc network by chain id if alias is numeric
1779            // This only succeeds if the chain id has a default:
1780            // "network_defaults": {
1781            //    "50104": "sophon_50104"
1782            // }
1783            if let Ok(Some(endpoint)) =
1784                mesc::query::get_endpoint_by_network(&mesc_config, maybe_alias, Some("foundry"))
1785            {
1786                return Some(endpoint.url);
1787            }
1788        }
1789
1790        None
1791    }
1792
1793    /// Returns the configured rpc, or the fallback url
1794    ///
1795    /// # Example
1796    ///
1797    /// ```
1798    /// use foundry_config::Config;
1799    /// # fn t() {
1800    /// let config = Config::with_root("./");
1801    /// let rpc_url = config.get_rpc_url_or("http://localhost:8545").unwrap();
1802    /// # }
1803    /// ```
1804    pub fn get_rpc_url_or<'a>(
1805        &'a self,
1806        fallback: impl Into<Cow<'a, str>>,
1807    ) -> Result<Cow<'a, str>, UnresolvedEnvVarError> {
1808        if let Some(url) = self.get_rpc_url() { url } else { Ok(fallback.into()) }
1809    }
1810
1811    /// Returns the configured rpc or `"http://localhost:8545"` if no `eth_rpc_url` is set
1812    ///
1813    /// # Example
1814    ///
1815    /// ```
1816    /// use foundry_config::Config;
1817    /// # fn t() {
1818    /// let config = Config::with_root("./");
1819    /// let rpc_url = config.get_rpc_url_or_localhost_http().unwrap();
1820    /// # }
1821    /// ```
1822    pub fn get_rpc_url_or_localhost_http(&self) -> Result<Cow<'_, str>, UnresolvedEnvVarError> {
1823        self.get_rpc_url_or("http://localhost:8545")
1824    }
1825
1826    /// Returns the `EtherscanConfig` to use, if any
1827    ///
1828    /// Returns
1829    ///  - the matching `ResolvedEtherscanConfig` of the `etherscan` table if `etherscan_api_key` is
1830    ///    an alias
1831    ///  - the matching `ResolvedEtherscanConfig` of the `etherscan` table if a `chain` is
1832    ///    configured. an alias
1833    ///  - the Mainnet  `ResolvedEtherscanConfig` if `etherscan_api_key` is set, `None` otherwise
1834    ///
1835    /// # Example
1836    ///
1837    /// ```
1838    /// use foundry_config::Config;
1839    /// # fn t() {
1840    /// let config = Config::with_root("./");
1841    /// let etherscan_config = config.get_etherscan_config().unwrap().unwrap();
1842    /// let client = etherscan_config.into_client().unwrap();
1843    /// # }
1844    /// ```
1845    pub fn get_etherscan_config(
1846        &self,
1847    ) -> Option<Result<ResolvedEtherscanConfig, EtherscanConfigError>> {
1848        self.get_etherscan_config_with_chain(None).transpose()
1849    }
1850
1851    /// Same as [`Self::get_etherscan_config()`] but optionally updates the config with the given
1852    /// `chain`, and `etherscan_api_key`
1853    ///
1854    /// If not matching alias was found, then this will try to find the first entry in the table
1855    /// with a matching chain id. If an etherscan_api_key is already set it will take precedence
1856    /// over the chain's entry in the table.
1857    pub fn get_etherscan_config_with_chain(
1858        &self,
1859        chain: Option<Chain>,
1860    ) -> Result<Option<ResolvedEtherscanConfig>, EtherscanConfigError> {
1861        if let Some(maybe_alias) = self.etherscan_api_key.as_ref().or(self.eth_rpc_url.as_ref())
1862            && self.etherscan.contains_key(maybe_alias)
1863        {
1864            return self.etherscan.clone().resolved().remove(maybe_alias).transpose();
1865        }
1866
1867        // try to find by comparing chain IDs after resolving
1868        if let Some(res) = chain
1869            .or(self.chain)
1870            .and_then(|chain| self.etherscan.clone().resolved().find_chain(chain))
1871        {
1872            match (res, self.etherscan_api_key.as_ref()) {
1873                (Ok(mut config), Some(key)) => {
1874                    // we update the key, because if an etherscan_api_key is set, it should take
1875                    // precedence over the entry, since this is usually set via env var or CLI args.
1876                    config.key.clone_from(key);
1877                    return Ok(Some(config));
1878                }
1879                (Ok(config), None) => return Ok(Some(config)),
1880                (Err(err), None) => return Err(err),
1881                (Err(_), Some(_)) => {
1882                    // use the etherscan key as fallback
1883                }
1884            }
1885        }
1886
1887        // etherscan fallback via API key
1888        if let Some(key) = self.etherscan_api_key.as_ref() {
1889            return Ok(ResolvedEtherscanConfig::create(
1890                key,
1891                chain.or(self.chain).unwrap_or_default(),
1892            ));
1893        }
1894        Ok(None)
1895    }
1896
1897    /// Helper function to just get the API key
1898    ///
1899    /// Optionally updates the config with the given `chain`.
1900    ///
1901    /// See also [Self::get_etherscan_config_with_chain]
1902    #[expect(clippy::disallowed_macros)]
1903    pub fn get_etherscan_api_key(&self, chain: Option<Chain>) -> Option<String> {
1904        self.get_etherscan_config_with_chain(chain)
1905            .map_err(|e| {
1906                // `sh_warn!` is a circular dependency, preventing us from using it here.
1907                eprintln!(
1908                    "{}: failed getting etherscan config: {e}",
1909                    yansi::Paint::yellow("Warning"),
1910                );
1911            })
1912            .ok()
1913            .flatten()
1914            .map(|c| c.key)
1915    }
1916
1917    /// Returns the remapping for the project's _src_ directory
1918    ///
1919    /// **Note:** this will add an additional `<src>/=<src path>` remapping here so imports that
1920    /// look like `import {Foo} from "src/Foo.sol";` are properly resolved.
1921    ///
1922    /// This is due the fact that `solc`'s VFS resolves [direct imports](https://docs.soliditylang.org/en/develop/path-resolution.html#direct-imports) that start with the source directory's name.
1923    pub fn get_source_dir_remapping(&self) -> Option<Remapping> {
1924        get_dir_remapping(&self.src)
1925    }
1926
1927    /// Returns the remapping for the project's _test_ directory, but only if it exists
1928    pub fn get_test_dir_remapping(&self) -> Option<Remapping> {
1929        if self.root.join(&self.test).exists() { get_dir_remapping(&self.test) } else { None }
1930    }
1931
1932    /// Returns the remapping for the project's _script_ directory, but only if it exists
1933    pub fn get_script_dir_remapping(&self) -> Option<Remapping> {
1934        if self.root.join(&self.script).exists() { get_dir_remapping(&self.script) } else { None }
1935    }
1936
1937    /// Returns the `Optimizer` based on the configured settings
1938    ///
1939    /// Note: optimizer details can be set independently of `enabled`
1940    /// See also: <https://github.com/foundry-rs/foundry/issues/7689>
1941    /// and  <https://github.com/ethereum/solidity/blob/bbb7f58be026fdc51b0b4694a6f25c22a1425586/docs/using-the-compiler.rst?plain=1#L293-L294>
1942    pub fn optimizer(&self) -> Optimizer {
1943        Optimizer {
1944            enabled: self.optimizer,
1945            runs: self.optimizer_runs,
1946            // we always set the details because `enabled` is effectively a specific details profile
1947            // that can still be modified
1948            details: self.optimizer_details.clone(),
1949        }
1950    }
1951
1952    /// returns the [`foundry_compilers::ConfigurableArtifacts`] for this config, that includes the
1953    /// `extra_output` fields
1954    pub fn configured_artifacts_handler(&self) -> ConfigurableArtifacts {
1955        let mut extra_output = self.extra_output.clone();
1956
1957        // Sourcify verification requires solc metadata output. Since, it doesn't
1958        // affect the UX & performance of the compiler, output the metadata files
1959        // by default.
1960        // For more info see: <https://github.com/foundry-rs/foundry/issues/2795>
1961        // Metadata is not emitted as separate file because this breaks typechain support: <https://github.com/foundry-rs/foundry/issues/2969>
1962        if !extra_output.contains(&ContractOutputSelection::Metadata) {
1963            extra_output.push(ContractOutputSelection::Metadata);
1964        }
1965
1966        ConfigurableArtifacts::new(extra_output, self.extra_output_files.iter().copied())
1967    }
1968
1969    /// Parses all libraries in the form of
1970    /// `<file>:<lib>:<addr>`
1971    pub fn parsed_libraries(&self) -> Result<Libraries, SolcError> {
1972        Libraries::parse(&self.libraries)
1973    }
1974
1975    /// Returns all libraries with applied remappings. Same as `self.solc_settings()?.libraries`.
1976    pub fn libraries_with_remappings(&self) -> Result<Libraries, SolcError> {
1977        let paths: ProjectPathsConfig = self.project_paths();
1978        Ok(self.parsed_libraries()?.apply(|libs| paths.apply_lib_remappings(libs)))
1979    }
1980
1981    /// Returns the configured `solc` `Settings` that includes:
1982    /// - all libraries
1983    /// - the optimizer (including details, if configured)
1984    /// - evm version
1985    pub fn solc_settings(&self) -> Result<SolcSettings, SolcError> {
1986        // By default if no targets are specifically selected the model checker uses all targets.
1987        // This might be too much here, so only enable assertion checks.
1988        // If users wish to enable all options they need to do so explicitly.
1989        let mut model_checker = self.model_checker.clone();
1990        if let Some(model_checker_settings) = &mut model_checker
1991            && model_checker_settings.targets.is_none()
1992        {
1993            model_checker_settings.targets = Some(vec![ModelCheckerTarget::Assert]);
1994        }
1995
1996        let mut settings = Settings {
1997            libraries: self.libraries_with_remappings()?,
1998            optimizer: self.optimizer(),
1999            evm_version: Some(self.evm_version),
2000            metadata: Some(SettingsMetadata {
2001                use_literal_content: Some(self.use_literal_content),
2002                bytecode_hash: Some(self.bytecode_hash),
2003                cbor_metadata: Some(self.cbor_metadata),
2004            }),
2005            debug: self.revert_strings.map(|revert_strings| DebuggingSettings {
2006                revert_strings: Some(revert_strings),
2007                // Not used.
2008                debug_info: Vec::new(),
2009            }),
2010            model_checker,
2011            // via_ssa_cfg implies via_ir only when via_ir is omitted, so we need to set both flags
2012            // to true if via_ssa_cfg is enabled.
2013            via_ir: Some(self.via_ir || self.via_ssa_cfg),
2014            via_ssa_cfg: Some(self.via_ssa_cfg),
2015            experimental: Some(self.experimental),
2016            // Not used.
2017            stop_after: None,
2018            // Set in project paths.
2019            remappings: Vec::new(),
2020            // Set with `with_extra_output` below.
2021            output_selection: Default::default(),
2022        }
2023        .with_extra_output(self.configured_artifacts_handler().output_selection());
2024
2025        // We're keeping AST in `--build-info` for backwards compatibility with HardHat.
2026        if self.ast || self.build_info {
2027            settings = settings.with_ast();
2028        }
2029
2030        let cli_settings =
2031            CliSettings { extra_args: self.extra_args.clone(), ..Default::default() };
2032
2033        Ok(SolcSettings { settings, cli_settings })
2034    }
2035
2036    /// Returns the configured [VyperSettings] that includes:
2037    /// - evm version
2038    pub fn vyper_settings(&self) -> Result<VyperSettings, SolcError> {
2039        // Let `opt_level` override `optimize` so child profiles can switch away from an
2040        // inherited optimization mode without sending both mutually exclusive settings to Vyper.
2041        let optimize = if self.vyper.opt_level.is_some() { None } else { self.vyper.optimize };
2042
2043        Ok(VyperSettings {
2044            evm_version: Some(self.evm_version),
2045            optimize,
2046            opt_level: self.vyper.opt_level,
2047            bytecode_metadata: None,
2048            // TODO: We don't yet have a way to deserialize other outputs correctly, so request only
2049            // those for now. It should be enough to run tests and deploy contracts.
2050            output_selection: OutputSelection::common_output_selection([
2051                "abi".to_string(),
2052                "evm.bytecode".to_string(),
2053                "evm.deployedBytecode".to_string(),
2054            ]),
2055            search_paths: None,
2056            experimental_codegen: self.vyper.experimental_codegen,
2057            debug: self.vyper.debug,
2058            enable_decimals: self.vyper.enable_decimals,
2059            venom_experimental: self.vyper.venom_experimental,
2060            venom: self.vyper.venom.clone(),
2061        })
2062    }
2063
2064    /// Returns the default figment
2065    ///
2066    /// The default figment reads from the following sources, in ascending
2067    /// priority order:
2068    ///
2069    ///   1. [`Config::default()`] (see [defaults](#defaults))
2070    ///   2. `foundry.toml` _or_ filename in `FOUNDRY_CONFIG` environment variable
2071    ///   3. `FOUNDRY_` prefixed environment variables
2072    ///
2073    /// The profile selected is the value set in the `FOUNDRY_PROFILE`
2074    /// environment variable. If it is not set, it defaults to `default`.
2075    ///
2076    /// # Example
2077    ///
2078    /// ```rust
2079    /// use foundry_config::Config;
2080    /// use serde::Deserialize;
2081    ///
2082    /// let my_config = Config::figment().extract::<Config>();
2083    /// ```
2084    pub fn figment() -> Figment {
2085        Self::default().into()
2086    }
2087
2088    /// Returns the default figment enhanced with additional context extracted from the provided
2089    /// root, like remappings and directories.
2090    ///
2091    /// # Example
2092    ///
2093    /// ```rust
2094    /// use foundry_config::Config;
2095    /// use serde::Deserialize;
2096    ///
2097    /// let my_config = Config::figment_with_root(".").extract::<Config>();
2098    /// ```
2099    pub fn figment_with_root(root: impl AsRef<Path>) -> Figment {
2100        Self::with_root(root.as_ref()).into()
2101    }
2102
2103    #[doc(hidden)]
2104    #[track_caller]
2105    pub fn figment_with_root_opt(root: Option<&Path>) -> Figment {
2106        let root = match root {
2107            Some(root) => root,
2108            None => &find_project_root(None).expect("could not determine project root"),
2109        };
2110        Self::figment_with_root(root)
2111    }
2112
2113    /// Creates a new Config that adds additional context extracted from the provided root.
2114    ///
2115    /// # Example
2116    ///
2117    /// ```rust
2118    /// use foundry_config::Config;
2119    /// let my_config = Config::with_root(".");
2120    /// ```
2121    pub fn with_root(root: impl AsRef<Path>) -> Self {
2122        Self::_with_root(root.as_ref())
2123    }
2124
2125    fn _with_root(root: &Path) -> Self {
2126        // autodetect paths
2127        let paths = ProjectPathsConfig::builder().build_with_root::<()>(root);
2128        let artifacts: PathBuf = paths.artifacts.file_name().unwrap().into();
2129        let mut config = Self::default();
2130        if config.uses_default_src() {
2131            config.src = paths.sources.file_name().unwrap().into();
2132        }
2133        config.root = paths.root;
2134        config.out = artifacts.clone();
2135        config.libs =
2136            paths.libraries.into_iter().map(|lib| lib.file_name().unwrap().into()).collect();
2137        config.fs_permissions = FsPermissions::new([PathPermission::read(artifacts)]);
2138        config
2139    }
2140
2141    /// Returns the default config but with hardhat paths
2142    pub fn hardhat() -> Self {
2143        Self {
2144            src: "contracts".into(),
2145            out: "artifacts".into(),
2146            libs: vec!["node_modules".into()],
2147            ..Self::default()
2148        }
2149    }
2150
2151    /// Extracts a basic subset of the config, used for initialisations.
2152    ///
2153    /// # Example
2154    ///
2155    /// ```rust
2156    /// use foundry_config::Config;
2157    /// let my_config = Config::with_root(".").into_basic();
2158    /// ```
2159    pub fn into_basic(self) -> BasicConfig {
2160        BasicConfig {
2161            profile: self.profile,
2162            src: self.src,
2163            out: self.out,
2164            libs: self.libs,
2165            remappings: self.remappings,
2166            network: self.networks.resolved_network().map(|network| network.name().to_string()),
2167        }
2168    }
2169
2170    /// Updates the `foundry.toml` file for the given `root` based on the provided closure.
2171    ///
2172    /// **Note:** the closure will only be invoked if the `foundry.toml` file exists, See
2173    /// [Self::get_config_path()] and if the closure returns `true`.
2174    pub fn update_at<F>(root: &Path, f: F) -> eyre::Result<()>
2175    where
2176        F: FnOnce(&Self, &mut toml_edit::DocumentMut) -> bool,
2177    {
2178        let config = Self::load_with_root(root)?.sanitized();
2179        config.update(|doc| f(&config, doc))
2180    }
2181
2182    /// Updates the `foundry.toml` file this `Config` ias based on with the provided closure.
2183    ///
2184    /// **Note:** the closure will only be invoked if the `foundry.toml` file exists, See
2185    /// [Self::get_config_path()] and if the closure returns `true`
2186    pub fn update<F>(&self, f: F) -> eyre::Result<()>
2187    where
2188        F: FnOnce(&mut toml_edit::DocumentMut) -> bool,
2189    {
2190        let file_path = self.get_config_path();
2191        if !file_path.exists() {
2192            return Ok(());
2193        }
2194        let contents = fs::read_to_string(&file_path)?;
2195        let mut doc = contents.parse::<toml_edit::DocumentMut>()?;
2196        if f(&mut doc) {
2197            fs::write(file_path, doc.to_string())?;
2198        }
2199        Ok(())
2200    }
2201
2202    /// Sets the `libs` entry inside a `foundry.toml` file but only if it exists
2203    ///
2204    /// # Errors
2205    ///
2206    /// An error if the `foundry.toml` could not be parsed.
2207    pub fn update_libs(&self) -> eyre::Result<()> {
2208        self.update(|doc| {
2209            let profile = self.profile.as_str().as_str();
2210            let root = &self.root;
2211            let libs: toml_edit::Value = self
2212                .libs
2213                .iter()
2214                .map(|path| {
2215                    let path =
2216                        if let Ok(relative) = path.strip_prefix(root) { relative } else { path };
2217                    toml_edit::Value::from(&*path.to_string_lossy())
2218                })
2219                .collect();
2220            let libs = toml_edit::value(libs);
2221            doc[Self::PROFILE_SECTION][profile]["libs"] = libs;
2222            true
2223        })
2224    }
2225
2226    /// Serialize the config type as a String of TOML.
2227    ///
2228    /// This serializes to a table with the name of the profile
2229    ///
2230    /// ```toml
2231    /// [profile.default]
2232    /// src = "src"
2233    /// out = "out"
2234    /// libs = ["lib"]
2235    /// # ...
2236    /// ```
2237    pub fn to_string_pretty(&self) -> Result<String, toml::ser::Error> {
2238        // serializing to value first to prevent `ValueAfterTable` errors
2239        let mut value = toml::Value::try_from(self)?;
2240        // Config map always gets serialized as a table
2241        let value_table = value.as_table_mut().unwrap();
2242        // remove standalone sections from inner table
2243        let standalone_sections = Self::STANDALONE_SECTIONS
2244            .iter()
2245            .filter_map(|section| {
2246                let section = section.to_string();
2247                value_table.remove(&section).map(|value| (section, value))
2248            })
2249            .collect::<Vec<_>>();
2250        // wrap inner table in [profile.<profile>]
2251        let mut wrapping_table = [(
2252            Self::PROFILE_SECTION.into(),
2253            toml::Value::Table([(self.profile.to_string(), value)].into_iter().collect()),
2254        )]
2255        .into_iter()
2256        .collect::<toml::map::Map<_, _>>();
2257        // insert standalone sections
2258        for (section, value) in standalone_sections {
2259            wrapping_table.insert(section, value);
2260        }
2261        // stringify
2262        toml::to_string_pretty(&toml::Value::Table(wrapping_table))
2263    }
2264
2265    /// Returns the path to the `foundry.toml` of this `Config`.
2266    pub fn get_config_path(&self) -> PathBuf {
2267        self.root.join(Self::FILE_NAME)
2268    }
2269
2270    /// Returns the selected profile.
2271    ///
2272    /// If the `FOUNDRY_PROFILE` env variable is not set, this returns the `DEFAULT_PROFILE`.
2273    pub fn selected_profile() -> Profile {
2274        // Can't cache in tests because the env var can change.
2275        #[cfg(test)]
2276        {
2277            Self::force_selected_profile()
2278        }
2279        #[cfg(not(test))]
2280        {
2281            SELECTED_PROFILE.get_or_init(Self::force_selected_profile).clone()
2282        }
2283    }
2284
2285    /// Sets the selected profile before it is initialized.
2286    ///
2287    /// Returns the previously selected profile if it was already initialized to a different value.
2288    pub fn try_set_selected_profile(profile: Profile) -> Result<(), Profile> {
2289        #[cfg(test)]
2290        {
2291            let _ = profile;
2292            Ok(())
2293        }
2294        #[cfg(not(test))]
2295        {
2296            match SELECTED_PROFILE.set(profile) {
2297                Ok(()) => Ok(()),
2298                Err(profile) if SELECTED_PROFILE.get() == Some(&profile) => Ok(()),
2299                Err(_) => Err(SELECTED_PROFILE.get().expect("profile initialized").clone()),
2300            }
2301        }
2302    }
2303
2304    fn force_selected_profile() -> Profile {
2305        Profile::from_env_or("FOUNDRY_PROFILE", Self::DEFAULT_PROFILE)
2306    }
2307
2308    /// Returns the path to foundry's global TOML file: `~/.foundry/foundry.toml`.
2309    pub fn foundry_dir_toml() -> Option<PathBuf> {
2310        Self::foundry_dir().map(|p| p.join(Self::FILE_NAME))
2311    }
2312
2313    /// Returns the path to foundry's config dir: `~/.foundry/`.
2314    pub fn foundry_dir() -> Option<PathBuf> {
2315        dirs::home_dir().map(|p| p.join(Self::FOUNDRY_DIR_NAME))
2316    }
2317
2318    /// Returns the path to foundry's cache dir: `~/.foundry/cache`.
2319    pub fn foundry_cache_dir() -> Option<PathBuf> {
2320        Self::foundry_dir().map(|p| p.join("cache"))
2321    }
2322
2323    /// Returns the path to foundry rpc cache dir: `~/.foundry/cache/rpc`.
2324    pub fn foundry_rpc_cache_dir() -> Option<PathBuf> {
2325        Some(Self::foundry_cache_dir()?.join("rpc"))
2326    }
2327    /// Returns the path to foundry chain's cache dir: `~/.foundry/cache/rpc/<chain>`
2328    pub fn foundry_chain_cache_dir(chain_id: impl Into<Chain>) -> Option<PathBuf> {
2329        Some(Self::foundry_rpc_cache_dir()?.join(chain_id.into().to_string()))
2330    }
2331
2332    /// Returns the path to foundry's etherscan cache dir: `~/.foundry/cache/etherscan`.
2333    pub fn foundry_etherscan_cache_dir() -> Option<PathBuf> {
2334        Some(Self::foundry_cache_dir()?.join("etherscan"))
2335    }
2336
2337    /// Returns the path to foundry's keystores dir: `~/.foundry/keystores`.
2338    pub fn foundry_keystores_dir() -> Option<PathBuf> {
2339        Some(Self::foundry_dir()?.join("keystores"))
2340    }
2341
2342    /// Returns the path to foundry's etherscan cache dir for `chain_id`:
2343    /// `~/.foundry/cache/etherscan/<chain>`
2344    pub fn foundry_etherscan_chain_cache_dir(chain_id: impl Into<Chain>) -> Option<PathBuf> {
2345        Some(Self::foundry_etherscan_cache_dir()?.join(chain_id.into().to_string()))
2346    }
2347
2348    /// Returns the path to the cache dir of the `block` on the `chain`:
2349    /// `~/.foundry/cache/rpc/<chain>/<block>`
2350    pub fn foundry_block_cache_dir(chain_id: impl Into<Chain>, block: u64) -> Option<PathBuf> {
2351        Some(Self::foundry_chain_cache_dir(chain_id)?.join(format!("{block}")))
2352    }
2353
2354    /// Returns the path to the cache file of the `block` on the `chain`:
2355    /// `~/.foundry/cache/rpc/<chain>/<block>/storage.json`
2356    pub fn foundry_block_cache_file(chain_id: impl Into<Chain>, block: u64) -> Option<PathBuf> {
2357        Some(Self::foundry_block_cache_dir(chain_id, block)?.join("storage.json"))
2358    }
2359
2360    /// Returns the path to `foundry`'s data directory inside the user's data directory.
2361    ///
2362    /// | Platform | Value                                         | Example                                          |
2363    /// | -------  | --------------------------------------------- | ------------------------------------------------ |
2364    /// | Linux    | `$XDG_CONFIG_HOME` or `$HOME`/.config/foundry | /home/alice/.config/foundry                      |
2365    /// | macOS    | `$HOME`/Library/Application Support/foundry   | /Users/Alice/Library/Application Support/foundry |
2366    /// | Windows  | `{FOLDERID_RoamingAppData}/foundry`           | C:\Users\Alice\AppData\Roaming/foundry           |
2367    pub fn data_dir() -> eyre::Result<PathBuf> {
2368        let path = dirs::data_dir().wrap_err("Failed to find data directory")?.join("foundry");
2369        std::fs::create_dir_all(&path).wrap_err("Failed to create module directory")?;
2370        Ok(path)
2371    }
2372
2373    /// Returns the path to the `foundry.toml` file, the file is searched for in
2374    /// the current working directory and all parent directories until the root,
2375    /// and the first hit is used.
2376    ///
2377    /// If this search comes up empty, then it checks if a global `foundry.toml` exists at
2378    /// `~/.foundry/foundry.toml`, see [`Self::foundry_dir_toml`].
2379    pub fn find_config_file() -> Option<PathBuf> {
2380        fn find(path: &Path) -> Option<PathBuf> {
2381            if path.is_absolute() {
2382                return match path.is_file() {
2383                    true => Some(path.to_path_buf()),
2384                    false => None,
2385                };
2386            }
2387            let cwd = std::env::current_dir().ok()?;
2388            let mut cwd = cwd.as_path();
2389            loop {
2390                let file_path = cwd.join(path);
2391                if file_path.is_file() {
2392                    return Some(file_path);
2393                }
2394                cwd = cwd.parent()?;
2395            }
2396        }
2397        find(Env::var_or("FOUNDRY_CONFIG", Self::FILE_NAME).as_ref())
2398            .or_else(|| Self::foundry_dir_toml().filter(|p| p.exists()))
2399    }
2400
2401    /// Clears the foundry cache.
2402    ///
2403    /// Returns warnings for any non-fatal deletion failures.
2404    pub fn clean_foundry_cache() -> eyre::Result<Vec<String>> {
2405        if let Some(cache_dir) = Self::foundry_cache_dir() {
2406            let path = cache_dir.as_path();
2407            if let Err(err) = fs::remove_dir_all(path)
2408                && err.kind() != io::ErrorKind::NotFound
2409            {
2410                return Ok(vec![format!(
2411                    "failed to remove foundry cache at {}: {err}",
2412                    path.display()
2413                )]);
2414            }
2415        } else {
2416            eyre::bail!("failed to get foundry_cache_dir");
2417        }
2418
2419        Ok(vec![])
2420    }
2421
2422    /// Clears the foundry cache for `chain`.
2423    ///
2424    /// Returns warnings for any non-fatal deletion failures.
2425    pub fn clean_foundry_chain_cache(chain: Chain) -> eyre::Result<Vec<String>> {
2426        if let Some(cache_dir) = Self::foundry_chain_cache_dir(chain) {
2427            let path = cache_dir.as_path();
2428            if let Err(err) = fs::remove_dir_all(path)
2429                && err.kind() != io::ErrorKind::NotFound
2430            {
2431                return Ok(vec![format!(
2432                    "failed to remove foundry cache for chain {chain} at {}: {err}",
2433                    path.display()
2434                )]);
2435            }
2436        } else {
2437            eyre::bail!("failed to get foundry_chain_cache_dir");
2438        }
2439
2440        Ok(vec![])
2441    }
2442
2443    /// Clears the foundry cache for `chain` and `block`.
2444    ///
2445    /// Returns warnings for any non-fatal deletion failures.
2446    pub fn clean_foundry_block_cache(chain: Chain, block: u64) -> eyre::Result<Vec<String>> {
2447        if let Some(cache_dir) = Self::foundry_block_cache_dir(chain, block) {
2448            let path = cache_dir.as_path();
2449            if let Err(err) = fs::remove_dir_all(path)
2450                && err.kind() != io::ErrorKind::NotFound
2451            {
2452                return Ok(vec![format!(
2453                    "failed to remove foundry cache for chain {chain} block {block} at {}: {err}",
2454                    path.display()
2455                )]);
2456            }
2457        } else {
2458            eyre::bail!("failed to get foundry_block_cache_dir");
2459        }
2460
2461        Ok(vec![])
2462    }
2463
2464    /// Clears the foundry etherscan cache.
2465    ///
2466    /// Returns warnings for any non-fatal deletion failures.
2467    pub fn clean_foundry_etherscan_cache() -> eyre::Result<Vec<String>> {
2468        if let Some(cache_dir) = Self::foundry_etherscan_cache_dir() {
2469            let path = cache_dir.as_path();
2470            if let Err(err) = fs::remove_dir_all(path)
2471                && err.kind() != io::ErrorKind::NotFound
2472            {
2473                return Ok(vec![format!(
2474                    "failed to remove foundry etherscan cache at {}: {err}",
2475                    path.display()
2476                )]);
2477            }
2478        } else {
2479            eyre::bail!("failed to get foundry_etherscan_cache_dir");
2480        }
2481
2482        Ok(vec![])
2483    }
2484
2485    /// Clears the foundry etherscan cache for `chain`.
2486    ///
2487    /// Returns warnings for any non-fatal deletion failures.
2488    pub fn clean_foundry_etherscan_chain_cache(chain: Chain) -> eyre::Result<Vec<String>> {
2489        if let Some(cache_dir) = Self::foundry_etherscan_chain_cache_dir(chain) {
2490            let path = cache_dir.as_path();
2491            if let Err(err) = fs::remove_dir_all(path)
2492                && err.kind() != io::ErrorKind::NotFound
2493            {
2494                return Ok(vec![format!(
2495                    "failed to remove foundry etherscan cache for chain {chain} at {}: {err}",
2496                    path.display()
2497                )]);
2498            }
2499        } else {
2500            eyre::bail!("failed to get foundry_etherscan_cache_dir for chain: {}", chain);
2501        }
2502
2503        Ok(vec![])
2504    }
2505
2506    /// List the data in the foundry cache.
2507    pub fn list_foundry_cache() -> eyre::Result<Cache> {
2508        if let Some(cache_dir) = Self::foundry_rpc_cache_dir() {
2509            let mut cache = Cache { chains: vec![] };
2510            let Some(entries) = Self::ignore_not_found(cache_dir.read_dir())? else {
2511                return Ok(cache);
2512            };
2513            for entry in entries {
2514                let Some(entry) = Self::ignore_not_found(entry)? else {
2515                    continue;
2516                };
2517                let Some(metadata) = Self::ignore_not_found(fs::metadata(entry.path()))? else {
2518                    continue;
2519                };
2520                if !metadata.is_dir() {
2521                    continue;
2522                }
2523                if let Ok(chain) = Chain::from_str(&entry.file_name().to_string_lossy()) {
2524                    cache.chains.push(Self::list_foundry_chain_cache(chain)?);
2525                }
2526            }
2527            Ok(cache)
2528        } else {
2529            eyre::bail!("failed to get foundry_cache_dir");
2530        }
2531    }
2532
2533    /// List the cached data for `chain`.
2534    pub fn list_foundry_chain_cache(chain: Chain) -> eyre::Result<ChainCache> {
2535        let block_explorer_data_size = match Self::foundry_etherscan_chain_cache_dir(chain) {
2536            Some(cache_dir) => Self::get_cached_block_explorer_data(&cache_dir)?,
2537            None => {
2538                warn!("failed to access foundry_etherscan_chain_cache_dir");
2539                0
2540            }
2541        };
2542
2543        if let Some(cache_dir) = Self::foundry_chain_cache_dir(chain) {
2544            let blocks = Self::get_cached_blocks(&cache_dir)?;
2545            Ok(ChainCache {
2546                name: chain.to_string(),
2547                blocks,
2548                block_explorer: block_explorer_data_size,
2549            })
2550        } else {
2551            eyre::bail!("failed to get foundry_chain_cache_dir");
2552        }
2553    }
2554
2555    /// The path provided to this function should point to a cached chain folder.
2556    fn get_cached_blocks(chain_path: &Path) -> eyre::Result<Vec<(String, u64)>> {
2557        let mut blocks = vec![];
2558        let Some(entries) = Self::ignore_not_found(chain_path.read_dir())? else {
2559            return Ok(blocks);
2560        };
2561        for block in entries {
2562            let Some(block) = Self::ignore_not_found(block)? else {
2563                continue;
2564            };
2565            if let Some(block) = Self::get_cached_block(block)? {
2566                blocks.push(block);
2567            }
2568        }
2569        Ok(blocks)
2570    }
2571
2572    fn get_cached_block(block: fs::DirEntry) -> eyre::Result<Option<(String, u64)>> {
2573        let file_name = block.file_name();
2574        let Some(metadata) = Self::ignore_not_found(fs::symlink_metadata(block.path()))? else {
2575            return Ok(None);
2576        };
2577        let size = if metadata.is_dir() {
2578            let Some(cache_files) = Self::ignore_not_found(block.path().read_dir())? else {
2579                return Ok(None);
2580            };
2581            let mut size = 0;
2582            for cache_file in cache_files {
2583                let Some(cache_file) = Self::ignore_not_found(cache_file)? else {
2584                    continue;
2585                };
2586                size += Self::get_cache_file_size(cache_file)?.unwrap_or_default();
2587            }
2588            if size == 0 {
2589                return Ok(None);
2590            }
2591            size
2592        } else if metadata.is_file() && file_name.to_string_lossy().chars().all(char::is_numeric) {
2593            metadata.len()
2594        } else {
2595            return Ok(None);
2596        };
2597        Ok(Some((file_name.to_string_lossy().into_owned(), size)))
2598    }
2599
2600    fn get_cache_file_size(cache_file: fs::DirEntry) -> eyre::Result<Option<u64>> {
2601        let Some(metadata) = Self::ignore_not_found(fs::symlink_metadata(cache_file.path()))?
2602        else {
2603            return Ok(None);
2604        };
2605        let cache_file_name = cache_file.file_name();
2606        let cache_file_name = cache_file_name.to_string_lossy();
2607        if !metadata.is_file()
2608            || (cache_file_name != "storage.json"
2609                && !cache_file_name
2610                    .strip_prefix("storage-")
2611                    .and_then(|name| name.strip_suffix(".json"))
2612                    .is_some_and(|hash| {
2613                        hash.len() == 64 && hash.bytes().all(|b| b.is_ascii_hexdigit())
2614                    }))
2615        {
2616            return Ok(None);
2617        }
2618        Ok(Some(metadata.len()))
2619    }
2620
2621    /// The path provided to this function should point to the etherscan cache for a chain.
2622    fn get_cached_block_explorer_data(chain_path: &Path) -> eyre::Result<u64> {
2623        let Some(entries) = Self::ignore_not_found(fs::read_dir(chain_path))? else {
2624            return Ok(0);
2625        };
2626        Self::get_cached_dir_size(entries)
2627    }
2628
2629    fn get_cached_dir_size(entries: fs::ReadDir) -> eyre::Result<u64> {
2630        let mut size = 0;
2631        for entry in entries {
2632            let Some(entry) = Self::ignore_not_found(entry)? else {
2633                continue;
2634            };
2635            size += Self::get_cached_entry_size(entry)?.unwrap_or_default();
2636        }
2637        Ok(size)
2638    }
2639
2640    fn get_cached_entry_size(entry: fs::DirEntry) -> eyre::Result<Option<u64>> {
2641        let Some(metadata) = Self::ignore_not_found(fs::symlink_metadata(entry.path()))? else {
2642            return Ok(None);
2643        };
2644        if metadata.is_dir() {
2645            let Some(entries) = Self::ignore_not_found(fs::read_dir(entry.path()))? else {
2646                return Ok(None);
2647            };
2648            Ok(Some(Self::get_cached_dir_size(entries)?))
2649        } else {
2650            Ok(Some(metadata.len()))
2651        }
2652    }
2653
2654    /// Treats cache entries removed during enumeration as absent.
2655    fn ignore_not_found<T>(result: io::Result<T>) -> eyre::Result<Option<T>> {
2656        match result {
2657            Ok(value) => Ok(Some(value)),
2658            Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
2659            Err(err) => Err(err.into()),
2660        }
2661    }
2662
2663    fn merge_toml_provider(
2664        mut figment: Figment,
2665        toml_provider: impl Provider,
2666        profile: Profile,
2667    ) -> Figment {
2668        figment = figment.select(profile.clone());
2669
2670        // add warnings
2671        figment = {
2672            let warnings = WarningsProvider::for_figment(&toml_provider, &figment);
2673            figment.merge(warnings)
2674        };
2675
2676        // use [profile.<profile>] as [<profile>]
2677        let mut profiles = vec![Self::DEFAULT_PROFILE];
2678        if profile != Self::DEFAULT_PROFILE {
2679            profiles.push(profile.clone());
2680        }
2681        // Apply key fixes before selecting profiles, while standalone sections and profile names
2682        // are still distinguishable.
2683        let provider = ForcedSnakeCaseData(toml_provider).strict_select(profiles);
2684        let provider = &BackwardsCompatTomlProvider(provider);
2685
2686        // merge the default profile as a base
2687        if profile != Self::DEFAULT_PROFILE {
2688            figment = figment.merge(provider.rename(Self::DEFAULT_PROFILE, profile.clone()));
2689        }
2690        // merge special keys into config
2691        for standalone_key in Self::STANDALONE_SECTIONS {
2692            if let Some((_, fallback)) =
2693                STANDALONE_FALLBACK_SECTIONS.iter().find(|(key, _)| standalone_key == key)
2694            {
2695                figment = figment.merge(
2696                    provider
2697                        .fallback(standalone_key, fallback)
2698                        .wrap(profile.clone(), standalone_key),
2699                );
2700            } else {
2701                figment = figment.merge(provider.wrap(profile.clone(), standalone_key));
2702            }
2703        }
2704        // merge the profile
2705        figment = figment.merge(provider);
2706        figment
2707    }
2708
2709    /// Check if any defaults need to be normalized.
2710    ///
2711    /// This normalizes the default `evm_version` if a `solc` was provided in the config.
2712    ///
2713    /// See also <https://github.com/foundry-rs/foundry/issues/7014>
2714    fn normalize_defaults(&self, mut figment: Figment) -> Figment {
2715        if figment.contains("evm_version") {
2716            return figment;
2717        }
2718
2719        // Normalize `evm_version` based on the provided solc version.
2720        if let Ok(solc) = figment.extract_inner::<SolcReq>("solc")
2721            && let Some(version) = solc
2722                .try_version()
2723                .ok()
2724                .and_then(|version| self.evm_version.normalize_version_solc(&version))
2725        {
2726            let profile = figment.profile().clone();
2727            figment = figment.merge(Serialized::default("evm_version", version).profile(profile));
2728        }
2729
2730        // Normalize `deny` based on the provided `deny_warnings` value.
2731        if figment.extract_inner::<bool>("deny_warnings").unwrap_or(false)
2732            && figment.extract_inner("deny") == Ok(DenyLevel::Never)
2733        {
2734            figment = figment.merge(("deny", DenyLevel::Warnings));
2735        }
2736
2737        figment
2738    }
2739}
2740
2741impl From<Config> for Figment {
2742    fn from(c: Config) -> Self {
2743        (&c).into()
2744    }
2745}
2746impl From<&Config> for Figment {
2747    fn from(c: &Config) -> Self {
2748        c.to_figment(FigmentProviders::All)
2749    }
2750}
2751
2752/// Determines what providers should be used when loading the [`Figment`] for a [`Config`].
2753#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2754pub enum FigmentProviders {
2755    /// Include all providers.
2756    #[default]
2757    All,
2758    /// Only include necessary providers that are useful for cast commands.
2759    ///
2760    /// This will exclude more expensive providers such as remappings.
2761    Cast,
2762    /// Only include necessary providers that are useful for anvil.
2763    ///
2764    /// This will exclude more expensive providers such as remappings.
2765    Anvil,
2766    /// Don't include any providers.
2767    None,
2768}
2769
2770impl FigmentProviders {
2771    /// Returns true if all providers should be included.
2772    pub const fn is_all(&self) -> bool {
2773        matches!(self, Self::All)
2774    }
2775
2776    /// Returns true if this is the cast preset.
2777    pub const fn is_cast(&self) -> bool {
2778        matches!(self, Self::Cast)
2779    }
2780
2781    /// Returns true if this is the anvil preset.
2782    pub const fn is_anvil(&self) -> bool {
2783        matches!(self, Self::Anvil)
2784    }
2785
2786    /// Returns true if no providers should be included.
2787    pub const fn is_none(&self) -> bool {
2788        matches!(self, Self::None)
2789    }
2790}
2791
2792fn figment_value_is_configured(figment: &Figment, key: &str) -> bool {
2793    figment.find_metadata(key).is_some_and(|metadata| metadata.name.as_ref() != "Foundry Config")
2794}
2795
2796/// Wrapper type for [`regex::Regex`] that implements [`PartialEq`] and [`serde`] traits.
2797#[derive(Clone, Debug, Serialize, Deserialize)]
2798#[serde(transparent)]
2799pub struct RegexWrapper {
2800    #[serde(with = "serde_regex")]
2801    inner: regex::Regex,
2802}
2803
2804impl std::ops::Deref for RegexWrapper {
2805    type Target = regex::Regex;
2806
2807    fn deref(&self) -> &Self::Target {
2808        &self.inner
2809    }
2810}
2811
2812impl std::cmp::PartialEq for RegexWrapper {
2813    fn eq(&self, other: &Self) -> bool {
2814        self.as_str() == other.as_str()
2815    }
2816}
2817
2818impl Eq for RegexWrapper {}
2819
2820impl From<RegexWrapper> for regex::Regex {
2821    fn from(wrapper: RegexWrapper) -> Self {
2822        wrapper.inner
2823    }
2824}
2825
2826impl From<regex::Regex> for RegexWrapper {
2827    fn from(re: Regex) -> Self {
2828        Self { inner: re }
2829    }
2830}
2831
2832mod serde_regex {
2833    use regex::Regex;
2834    use serde::{Deserialize, Deserializer, Serializer};
2835
2836    pub(crate) fn serialize<S>(value: &Regex, serializer: S) -> Result<S::Ok, S::Error>
2837    where
2838        S: Serializer,
2839    {
2840        serializer.serialize_str(value.as_str())
2841    }
2842
2843    pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Regex, D::Error>
2844    where
2845        D: Deserializer<'de>,
2846    {
2847        let s = String::deserialize(deserializer)?;
2848        Regex::new(&s).map_err(serde::de::Error::custom)
2849    }
2850}
2851
2852/// Ser/de `globset::Glob` explicitly to handle `Option<Glob>` properly
2853pub(crate) mod from_opt_glob {
2854    use serde::{Deserialize, Deserializer, Serializer};
2855
2856    pub fn serialize<S>(value: &Option<globset::Glob>, serializer: S) -> Result<S::Ok, S::Error>
2857    where
2858        S: Serializer,
2859    {
2860        match value {
2861            Some(glob) => serializer.serialize_str(glob.glob()),
2862            None => serializer.serialize_none(),
2863        }
2864    }
2865
2866    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<globset::Glob>, D::Error>
2867    where
2868        D: Deserializer<'de>,
2869    {
2870        let s: Option<String> = Option::deserialize(deserializer)?;
2871        if let Some(s) = s {
2872            return Ok(Some(globset::Glob::new(&s).map_err(serde::de::Error::custom)?));
2873        }
2874        Ok(None)
2875    }
2876}
2877
2878/// Parses a config profile
2879///
2880/// All `Profile` date is ignored by serde, however the `Config::to_string_pretty` includes it and
2881/// returns a toml table like
2882///
2883/// ```toml
2884/// #[profile.default]
2885/// src = "..."
2886/// ```
2887/// This ignores the `#[profile.default]` part in the toml
2888pub fn parse_with_profile<T: serde::de::DeserializeOwned>(
2889    s: &str,
2890) -> Result<Option<(Profile, T)>, Error> {
2891    let figment = Config::merge_toml_provider(
2892        Figment::new(),
2893        Toml::string(s).nested(),
2894        Config::DEFAULT_PROFILE,
2895    );
2896    if figment.profiles().any(|p| p == Config::DEFAULT_PROFILE) {
2897        Ok(Some((Config::DEFAULT_PROFILE, figment.select(Config::DEFAULT_PROFILE).extract()?)))
2898    } else {
2899        Ok(None)
2900    }
2901}
2902
2903impl Provider for Config {
2904    fn metadata(&self) -> Metadata {
2905        Metadata::named("Foundry Config")
2906    }
2907
2908    #[track_caller]
2909    fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
2910        let mut data = Serialized::defaults(self).data()?;
2911        let root = Value::serialize(self.root.clone())?;
2912        let labels = Value::serialize(&self.labels)?;
2913        if let Some(entry) = data.get_mut(&Self::DEFAULT_PROFILE) {
2914            entry.insert("root".to_string(), root.clone());
2915            entry.insert("labels".to_string(), labels.clone());
2916            normalize_legacy_labels_in_profile(entry);
2917        }
2918        if let Some(entry) = data.get_mut(&self.profile) {
2919            entry.insert("root".to_string(), root);
2920            entry.insert("labels".to_string(), labels);
2921            normalize_legacy_labels_in_profile(entry);
2922        }
2923        Ok(data)
2924    }
2925
2926    fn profile(&self) -> Option<Profile> {
2927        Some(self.profile.clone())
2928    }
2929}
2930
2931impl Default for Config {
2932    fn default() -> Self {
2933        Self {
2934            profile: Self::DEFAULT_PROFILE,
2935            profiles: vec![Self::DEFAULT_PROFILE],
2936            fs_permissions: FsPermissions::new([PathPermission::read("out")]),
2937            isolate: true,
2938            root: root_default(),
2939            extends: None,
2940            src: Self::DEFAULT_SRC.into(),
2941            test: "test".into(),
2942            script: "script".into(),
2943            out: "out".into(),
2944            libs: vec!["lib".into()],
2945            cache: true,
2946            dynamic_test_linking: true,
2947            cache_path: "cache".into(),
2948            broadcast: "broadcast".into(),
2949            snapshots: "snapshots".into(),
2950            gas_snapshot_check: false,
2951            gas_snapshot_emit: true,
2952            allow_paths: vec![],
2953            include_paths: vec![],
2954            force: false,
2955            evm_version: EvmVersion::Osaka,
2956            hardfork: None,
2957            gas_reports: vec!["*".to_string()],
2958            gas_reports_ignore: vec![],
2959            gas_reports_include_tests: false,
2960            solc: None,
2961            vyper: Default::default(),
2962            auto_detect_solc: true,
2963            offline: false,
2964            optimizer: None,
2965            optimizer_runs: None,
2966            optimizer_details: None,
2967            model_checker: None,
2968            extra_output: Default::default(),
2969            extra_output_files: Default::default(),
2970            names: false,
2971            sizes: false,
2972            test_pattern: None,
2973            test_pattern_inverse: None,
2974            contract_pattern: None,
2975            contract_pattern_inverse: None,
2976            path_pattern: None,
2977            path_pattern_inverse: None,
2978            coverage_pattern_inverse: None,
2979            test_failures_file: "cache/test-failures".into(),
2980            mutation_dir: "cache/mutation".into(),
2981            threads: None,
2982            show_progress: false,
2983            fuzz: FuzzConfig::new("cache/fuzz".into()),
2984            invariant: InvariantConfig::new("cache/invariant".into()),
2985            symbolic: SymbolicConfig::default(),
2986            coverage: CoverageConfig::default(),
2987            mutation: MutationConfig::default(),
2988            tracing: TracingConfig::default(),
2989            always_use_create_2_factory: false,
2990            eip1559_fee_estimate: Eip1559FeeEstimatePreset::default(),
2991            ffi: false,
2992            live_logs: false,
2993            allow_internal_expect_revert: false,
2994            prompt_timeout: 120,
2995            sender: Self::DEFAULT_SENDER,
2996            tx_origin: Self::DEFAULT_SENDER,
2997            initial_balance: U256::from((1u128 << 96) - 1),
2998            block_number: U256::from(1),
2999            fork_block_number: None,
3000            chain: None,
3001            gas_limit: (1u64 << 30).into(), // ~1B
3002            code_size_limit: None,
3003            gas_price: None,
3004            block_base_fee_per_gas: 0,
3005            block_coinbase: Address::ZERO,
3006            block_timestamp: U256::from(1),
3007            block_difficulty: 0,
3008            block_prevrandao: Default::default(),
3009            block_gas_limit: None,
3010            disable_block_gas_limit: false,
3011            enable_tx_gas_limit: false,
3012            memory_limit: 1 << 27, // 2**27 = 128MiB = 134_217_728 bytes
3013            eth_rpc_url: None,
3014            eth_rpc_accept_invalid_certs: false,
3015            eth_rpc_no_proxy: false,
3016            eth_rpc_jwt: None,
3017            eth_rpc_timeout: None,
3018            eth_rpc_headers: None,
3019            eth_rpc_curl: false,
3020            etherscan_api_key: None,
3021            verbosity: 0,
3022            remappings: vec![],
3023            auto_detect_remappings: true,
3024            libraries: vec![],
3025            ignored_error_codes: vec![
3026                SolidityErrorCode::SpdxLicenseNotProvided,
3027                SolidityErrorCode::ContractExceeds24576Bytes,
3028                SolidityErrorCode::ContractInitCodeSizeExceeds49152Bytes,
3029                SolidityErrorCode::TransientStorageUsed,
3030                SolidityErrorCode::TransferDeprecated,
3031                SolidityErrorCode::NatspecMemorySafeAssemblyDeprecated,
3032            ],
3033            ignored_error_codes_from: vec![],
3034            ignored_file_paths: vec![],
3035            deny: DenyLevel::Never,
3036            deny_warnings: false,
3037            via_ir: false,
3038            via_ssa_cfg: false,
3039            experimental: false,
3040            ast: false,
3041            rpc_storage_caching: Default::default(),
3042            rpc_endpoints: Default::default(),
3043            etherscan: Default::default(),
3044            no_storage_caching: false,
3045            no_rpc_rate_limit: false,
3046            use_literal_content: false,
3047            bytecode_hash: BytecodeHash::Ipfs,
3048            cbor_metadata: true,
3049            revert_strings: None,
3050            sparse_mode: false,
3051            build_info: false,
3052            build_info_path: None,
3053            fmt: Default::default(),
3054            lint: Default::default(),
3055            doc: Default::default(),
3056            bind_json: Default::default(),
3057            labels: Default::default(),
3058            unchecked_cheatcode_artifacts: false,
3059            create2_library_salt: Self::DEFAULT_CREATE2_LIBRARY_SALT,
3060            create2_deployer: Self::DEFAULT_CREATE2_DEPLOYER,
3061            skip: vec![],
3062            dependencies: Default::default(),
3063            soldeer: Default::default(),
3064            assertions_revert: true,
3065            legacy_assertions: false,
3066            warnings: vec![],
3067            extra_args: vec![],
3068            networks: Default::default(),
3069            transaction_timeout: 120,
3070            additional_compiler_profiles: Default::default(),
3071            compilation_restrictions: Default::default(),
3072            script_execution_protection: true,
3073            _non_exhaustive: (),
3074        }
3075    }
3076}
3077
3078/// Wrapper for the config's `gas_limit` value necessary because toml-rs can't handle larger number
3079/// because integers are stored signed: <https://github.com/alexcrichton/toml-rs/issues/256>
3080///
3081/// Due to this limitation this type will be serialized/deserialized as String if it's larger than
3082/// `i64`
3083#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
3084pub struct GasLimit(#[serde(deserialize_with = "crate::deserialize_u64_or_max")] pub u64);
3085
3086impl From<u64> for GasLimit {
3087    fn from(gas: u64) -> Self {
3088        Self(gas)
3089    }
3090}
3091
3092impl From<GasLimit> for u64 {
3093    fn from(gas: GasLimit) -> Self {
3094        gas.0
3095    }
3096}
3097
3098impl Serialize for GasLimit {
3099    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
3100    where
3101        S: Serializer,
3102    {
3103        if self.0 == u64::MAX {
3104            serializer.serialize_str("max")
3105        } else if self.0 > i64::MAX as u64 {
3106            serializer.serialize_str(&self.0.to_string())
3107        } else {
3108            serializer.serialize_u64(self.0)
3109        }
3110    }
3111}
3112
3113/// Variants for selecting the [`Solc`] instance
3114#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
3115#[serde(untagged)]
3116pub enum SolcReq {
3117    /// Requires a specific solc version, that's either already installed (via `svm`) or will be
3118    /// auto installed (via `svm`)
3119    Version(Version),
3120    /// Path to an existing local solc installation
3121    Local(PathBuf),
3122}
3123
3124impl SolcReq {
3125    /// Tries to get the solc version from the `SolcReq`
3126    ///
3127    /// If the `SolcReq` is a `Version` it will return the version, if it's a path to a binary it
3128    /// will try to get the version from the binary.
3129    pub fn try_version(&self) -> Result<Version, SolcError> {
3130        match self {
3131            Self::Version(version) => Ok(version.clone()),
3132            Self::Local(path) => {
3133                warn_local_compiler(path);
3134                Solc::new(path).map(|solc| solc.version)
3135            }
3136        }
3137    }
3138}
3139
3140impl<T: AsRef<str>> From<T> for SolcReq {
3141    fn from(s: T) -> Self {
3142        let s = s.as_ref();
3143        if let Ok(v) = Version::from_str(s) { Self::Version(v) } else { Self::Local(s.into()) }
3144    }
3145}
3146
3147/// A subset of the foundry `Config`
3148/// used to initialize a `foundry.toml` file
3149///
3150/// # Example
3151///
3152/// ```rust
3153/// use foundry_config::{BasicConfig, Config};
3154/// use serde::Deserialize;
3155///
3156/// let my_config = Config::figment().extract::<BasicConfig>();
3157/// ```
3158#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
3159pub struct BasicConfig {
3160    /// the profile tag: `[profile.default]`
3161    #[serde(skip)]
3162    pub profile: Profile,
3163    /// path of the source contracts dir, like `src` or `contracts`
3164    pub src: PathBuf,
3165    /// path to where artifacts shut be written to
3166    pub out: PathBuf,
3167    /// all library folders to include, `lib`, `node_modules`
3168    pub libs: Vec<PathBuf>,
3169    /// `Remappings` to use for this repo
3170    #[serde(
3171        default,
3172        skip_serializing_if = "Vec::is_empty",
3173        serialize_with = "remappings_serde::serialize"
3174    )]
3175    pub remappings: Vec<RelativeRemapping>,
3176    /// The explicitly selected network (e.g. `"ethereum"`, `"monad"`, or `"tempo"`).
3177    #[serde(skip)]
3178    pub network: Option<String>,
3179}
3180
3181impl BasicConfig {
3182    /// Serialize the config as a String of TOML.
3183    ///
3184    /// This serializes to a table with the name of the profile
3185    pub fn to_string_pretty(&self) -> Result<String, toml::ser::Error> {
3186        let mut profile_body = toml::Value::try_from(self)?;
3187        if let Some(ref network) = self.network
3188            && let toml::Value::Table(ref mut table) = profile_body
3189        {
3190            table.insert("network".to_string(), toml::Value::String(network.clone()));
3191        }
3192
3193        let mut profile_section = toml::value::Table::new();
3194        profile_section.insert(self.profile.to_string(), profile_body);
3195
3196        let mut document = toml::value::Table::new();
3197        document.insert("profile".to_string(), toml::Value::Table(profile_section));
3198
3199        if self.network.as_deref() == Some("tempo") {
3200            let mut endpoints = toml::value::Table::new();
3201            endpoints.insert(
3202                "tempo".to_string(),
3203                toml::Value::String(crate::endpoints::TEMPO_RPC_URL.to_string()),
3204            );
3205            endpoints.insert(
3206                "moderato".to_string(),
3207                toml::Value::String(crate::endpoints::MODERATO_RPC_URL.to_string()),
3208            );
3209            document.insert("rpc_endpoints".to_string(), toml::Value::Table(endpoints));
3210        }
3211
3212        let body = toml::to_string_pretty(&toml::Value::Table(document))?;
3213        Ok(format!(
3214            "{body}\n# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options\n"
3215        ))
3216    }
3217}
3218
3219mod remappings_serde {
3220    use foundry_compilers::artifacts::remappings::RelativeRemapping;
3221    #[cfg(windows)]
3222    use path_slash::PathExt as _;
3223    use serde::{Serialize, Serializer};
3224    #[cfg(windows)]
3225    use std::path::Path;
3226
3227    #[cfg(windows)]
3228    fn slash_context(context: &str) -> String {
3229        let has_trailing_separator =
3230            context.as_bytes().last().is_some_and(|c| *c == b'/' || *c == b'\\');
3231        let mut context = Path::new(context).to_slash_lossy().into_owned();
3232        if has_trailing_separator && !context.ends_with('/') {
3233            context.push('/');
3234        }
3235        context
3236    }
3237
3238    pub fn serialize<S>(remappings: &[RelativeRemapping], serializer: S) -> Result<S::Ok, S::Error>
3239    where
3240        S: Serializer,
3241    {
3242        remappings
3243            .iter()
3244            .map(|remapping| {
3245                let Some(context) = &remapping.context else { return remapping.to_string() };
3246                let mut remapping = remapping.clone();
3247                remapping.context = None;
3248                #[cfg(windows)]
3249                let context = slash_context(context);
3250                #[cfg(not(windows))]
3251                let context = context.as_str();
3252                format!("{context}:{remapping}")
3253            })
3254            .collect::<Vec<_>>()
3255            .serialize(serializer)
3256    }
3257}
3258
3259pub(crate) mod from_str_lowercase {
3260    use serde::{Deserialize, Deserializer, Serializer};
3261    use std::str::FromStr;
3262
3263    pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
3264    where
3265        T: std::fmt::Display,
3266        S: Serializer,
3267    {
3268        serializer.collect_str(&value.to_string().to_lowercase())
3269    }
3270
3271    pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
3272    where
3273        D: Deserializer<'de>,
3274        T: FromStr,
3275        T::Err: std::fmt::Display,
3276    {
3277        String::deserialize(deserializer)?.to_lowercase().parse().map_err(serde::de::Error::custom)
3278    }
3279}
3280
3281fn canonic(path: impl Into<PathBuf>) -> PathBuf {
3282    let path = path.into();
3283    foundry_compilers::utils::canonicalize(&path).unwrap_or(path)
3284}
3285
3286fn root_default() -> PathBuf {
3287    ".".into()
3288}
3289
3290#[cfg(test)]
3291mod tests {
3292    use super::*;
3293    use crate::{
3294        cache::{CachedChains, CachedEndpoints},
3295        endpoints::RpcEndpointType,
3296        etherscan::ResolvedEtherscanConfigs,
3297        fmt::IndentStyle,
3298    };
3299    use NamedChain::Moonbeam;
3300    use endpoints::{RpcAuth, RpcEndpointConfig};
3301    use figment::error::Kind::InvalidType;
3302    use foundry_compilers::artifacts::{
3303        ModelCheckerEngine, YulDetails,
3304        vyper::{VyperOptimizationLevel, VyperOptimizationMode, VyperVenomSettings},
3305    };
3306    use foundry_evm_hardforks::{TempoHardfork, latest_active_tempo_hardfork};
3307    use similar_asserts::assert_eq;
3308    use soldeer_core::remappings::RemappingsLocation;
3309    use std::{
3310        fs::File,
3311        io::{self, Write},
3312        num::NonZeroUsize,
3313    };
3314    use tempfile::tempdir;
3315
3316    // Helper function to clear `__warnings` in config, since it will be populated during loading
3317    // from file, causing testing problem when comparing to those created from `default()`, etc.
3318    fn clear_warning(config: &mut Config) {
3319        config.warnings = vec![];
3320    }
3321
3322    fn mark_serialized_invariant_provenance(config: &mut Config) {
3323        config.invariant.corpus_random_sequence_weight_configured = true;
3324        config.invariant.workers_configured = true;
3325    }
3326
3327    #[test]
3328    fn project_remappings_alias_relative_filesystem_contexts_in_place() {
3329        let root = tempdir().unwrap();
3330        let dependency = root.path().join("dependency");
3331        let absolute_dependency = root.path().join("absolute-dependency");
3332        fs::create_dir(&dependency).unwrap();
3333        fs::create_dir(&absolute_dependency).unwrap();
3334
3335        let global_before = Remapping {
3336            context: None,
3337            name: "global-before/".into(),
3338            path: "lib/global-before/".into(),
3339        };
3340        let relative = Remapping {
3341            context: Some(format!("dependency{}", std::path::MAIN_SEPARATOR)),
3342            name: "relative/".into(),
3343            path: "lib/relative/".into(),
3344        };
3345        let absolute = Remapping {
3346            context: Some(format!(
3347                "{}{}",
3348                absolute_dependency.display(),
3349                std::path::MAIN_SEPARATOR
3350            )),
3351            name: "absolute/".into(),
3352            path: "lib/absolute/".into(),
3353        };
3354        let missing = Remapping {
3355            context: Some(format!("missing{}", std::path::MAIN_SEPARATOR)),
3356            name: "missing/".into(),
3357            path: "lib/missing/".into(),
3358        };
3359        let global_after = Remapping {
3360            context: None,
3361            name: "global-after/".into(),
3362            path: "lib/global-after/".into(),
3363        };
3364        let mut config = Config::with_root(root.path());
3365        config.remappings = [
3366            global_before.clone(),
3367            relative.clone(),
3368            absolute.clone(),
3369            missing.clone(),
3370            global_after.clone(),
3371        ]
3372        .map(Into::into)
3373        .into();
3374
3375        let mut absolute_alias = relative.clone();
3376        let absolute_context = config.root.join("dependency");
3377        #[cfg(windows)]
3378        let absolute_context = PathBuf::from_slash(absolute_context.to_string_lossy());
3379        let mut absolute_context = absolute_context.display().to_string();
3380        absolute_context.push(std::path::MAIN_SEPARATOR);
3381        absolute_alias.context = Some(absolute_context);
3382        assert_eq!(
3383            config.project_remappings(),
3384            vec![global_before, relative, absolute_alias, absolute, missing, global_after]
3385        );
3386    }
3387
3388    #[test]
3389    fn default_sender() {
3390        assert_eq!(Config::DEFAULT_SENDER, address!("0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38"));
3391    }
3392
3393    #[test]
3394    fn test_caching() {
3395        let mut config = Config::default();
3396        let chain_id = NamedChain::Mainnet;
3397        let url = "https://eth-mainnet.alchemyapi";
3398        assert!(config.enable_caching(url, chain_id));
3399
3400        config.no_storage_caching = true;
3401        assert!(!config.enable_caching(url, chain_id));
3402
3403        config.no_storage_caching = false;
3404        assert!(!config.enable_caching(url, NamedChain::Dev));
3405    }
3406
3407    #[test]
3408    fn test_install_dir() {
3409        figment::Jail::expect_with(|jail| {
3410            let config = Config::load().unwrap();
3411            assert_eq!(config.install_lib_dir(), PathBuf::from("lib"));
3412            jail.create_file(
3413                "foundry.toml",
3414                r"
3415                [profile.default]
3416                libs = ['node_modules', 'lib']
3417            ",
3418            )?;
3419            let config = Config::load().unwrap();
3420            assert_eq!(config.install_lib_dir(), PathBuf::from("lib"));
3421
3422            jail.create_file(
3423                "foundry.toml",
3424                r"
3425                [profile.default]
3426                libs = ['custom', 'node_modules', 'lib']
3427            ",
3428            )?;
3429            let config = Config::load().unwrap();
3430            assert_eq!(config.install_lib_dir(), PathBuf::from("custom"));
3431
3432            Ok(())
3433        });
3434    }
3435
3436    #[test]
3437    fn test_figment_is_default() {
3438        figment::Jail::expect_with(|_| {
3439            let mut default: Config = Config::figment().extract()?;
3440            let default2 = Config::default();
3441            default.profile = default2.profile.clone();
3442            default.profiles = default2.profiles.clone();
3443            assert_eq!(default, default2);
3444            Ok(())
3445        });
3446    }
3447
3448    #[test]
3449    fn figment_profiles() {
3450        figment::Jail::expect_with(|jail| {
3451            jail.create_file(
3452                "foundry.toml",
3453                r"
3454                [foo.baz]
3455                libs = ['node_modules', 'lib']
3456
3457                [profile.default]
3458                libs = ['node_modules', 'lib']
3459
3460                [profile.ci]
3461                libs = ['node_modules', 'lib']
3462
3463                [profile.local]
3464                libs = ['node_modules', 'lib']
3465            ",
3466            )?;
3467
3468            let config = crate::Config::load().unwrap();
3469            let expected: &[figment::Profile] = &["ci".into(), "default".into(), "local".into()];
3470            assert_eq!(config.profiles, expected);
3471
3472            Ok(())
3473        });
3474    }
3475
3476    #[test]
3477    fn test_default_round_trip() {
3478        figment::Jail::expect_with(|_| {
3479            let original = Config::figment();
3480            let roundtrip = Figment::from(Config::from_provider(&original).unwrap());
3481            for figment in &[original, roundtrip] {
3482                let config = Config::from_provider(figment).unwrap();
3483                assert_eq!(config, Config::default().normalized_optimizer_settings());
3484            }
3485            Ok(())
3486        });
3487    }
3488
3489    #[test]
3490    fn ffi_env_disallowed() {
3491        figment::Jail::expect_with(|jail| {
3492            jail.set_env("FOUNDRY_FFI", "true");
3493            jail.set_env("FFI", "true");
3494            jail.set_env("DAPP_FFI", "true");
3495            let config = Config::load().unwrap();
3496            assert!(!config.ffi);
3497
3498            Ok(())
3499        });
3500    }
3501
3502    #[test]
3503    fn test_profile_env() {
3504        figment::Jail::expect_with(|jail| {
3505            jail.set_env("FOUNDRY_PROFILE", "default");
3506            let figment = Config::figment();
3507            assert_eq!(figment.profile(), "default");
3508
3509            jail.set_env("FOUNDRY_PROFILE", "hardhat");
3510            let figment: Figment = Config::hardhat().into();
3511            assert_eq!(figment.profile(), "hardhat");
3512
3513            jail.create_file(
3514                "foundry.toml",
3515                r"
3516                [profile.default]
3517                libs = ['lib']
3518                [profile.local]
3519                libs = ['modules']
3520            ",
3521            )?;
3522            jail.set_env("FOUNDRY_PROFILE", "local");
3523            let config = Config::load().unwrap();
3524            assert_eq!(config.libs, vec![PathBuf::from("modules")]);
3525
3526            Ok(())
3527        });
3528    }
3529
3530    #[test]
3531    fn test_default_test_path() {
3532        figment::Jail::expect_with(|_| {
3533            let config = Config::default();
3534            let paths_config = config.project_paths::<Solc>();
3535            assert_eq!(paths_config.tests, PathBuf::from(r"test"));
3536            Ok(())
3537        });
3538    }
3539
3540    #[test]
3541    fn test_custom_src_skips_directory_auto_detection() {
3542        figment::Jail::expect_with(|jail| {
3543            fs::create_dir(jail.directory().join("contracts")).unwrap();
3544
3545            let config = Config { root: jail.directory().into(), ..Default::default() };
3546            let figment: Figment = config.into();
3547            let config = figment.extract::<Config>().unwrap();
3548            assert_eq!(config.src, PathBuf::from("contracts"));
3549
3550            let config = Config {
3551                root: jail.directory().into(),
3552                src: "custom-src".into(),
3553                ..Default::default()
3554            };
3555            let figment: Figment = config.into();
3556            let config = figment.extract::<Config>().unwrap();
3557            assert_eq!(config.src, PathBuf::from("custom-src"));
3558
3559            Ok(())
3560        });
3561    }
3562
3563    #[test]
3564    fn test_default_libs() {
3565        figment::Jail::expect_with(|jail| {
3566            let config = Config::load().unwrap();
3567            assert_eq!(config.libs, vec![PathBuf::from("lib")]);
3568
3569            fs::create_dir_all(jail.directory().join("node_modules")).unwrap();
3570            let config = Config::load().unwrap();
3571            assert_eq!(config.libs, vec![PathBuf::from("node_modules")]);
3572
3573            fs::create_dir_all(jail.directory().join("lib")).unwrap();
3574            let config = Config::load().unwrap();
3575            assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
3576
3577            Ok(())
3578        });
3579    }
3580
3581    #[test]
3582    fn test_inheritance_from_default_test_path() {
3583        figment::Jail::expect_with(|jail| {
3584            jail.create_file(
3585                "foundry.toml",
3586                r#"
3587                [profile.default]
3588                test = "defaulttest"
3589                src  = "defaultsrc"
3590                libs = ['lib', 'node_modules']
3591
3592                [profile.custom]
3593                src = "customsrc"
3594            "#,
3595            )?;
3596
3597            let config = Config::load().unwrap();
3598            assert_eq!(config.src, PathBuf::from("defaultsrc"));
3599            assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
3600
3601            jail.set_env("FOUNDRY_PROFILE", "custom");
3602            let config = Config::load().unwrap();
3603            assert_eq!(config.src, PathBuf::from("customsrc"));
3604            assert_eq!(config.test, PathBuf::from("defaulttest"));
3605            assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
3606
3607            Ok(())
3608        });
3609    }
3610
3611    #[test]
3612    fn test_custom_test_path() {
3613        figment::Jail::expect_with(|jail| {
3614            jail.create_file(
3615                "foundry.toml",
3616                r#"
3617                [profile.default]
3618                test = "mytest"
3619            "#,
3620            )?;
3621
3622            let config = Config::load().unwrap();
3623            let paths_config = config.project_paths::<Solc>();
3624            assert_eq!(paths_config.tests, PathBuf::from(r"mytest"));
3625            Ok(())
3626        });
3627    }
3628
3629    #[test]
3630    fn test_remappings() {
3631        figment::Jail::expect_with(|jail| {
3632            jail.create_file(
3633                "foundry.toml",
3634                r#"
3635                [profile.default]
3636                src = "some-source"
3637                out = "some-out"
3638                cache = true
3639            "#,
3640            )?;
3641            let config = Config::load().unwrap();
3642            assert!(config.remappings.is_empty());
3643
3644            jail.create_file(
3645                "remappings.txt",
3646                r"
3647                file-ds-test/=lib/ds-test/
3648                file-other/=lib/other/
3649            ",
3650            )?;
3651
3652            let config = Config::load().unwrap();
3653            assert_eq!(
3654                config.remappings,
3655                vec![
3656                    Remapping::from_str("file-ds-test/=lib/ds-test/").unwrap().into(),
3657                    Remapping::from_str("file-other/=lib/other/").unwrap().into(),
3658                ],
3659            );
3660
3661            jail.set_env("DAPP_REMAPPINGS", "ds-test=lib/ds-test/\nother/=lib/other/");
3662            let config = Config::load().unwrap();
3663
3664            assert_eq!(
3665                config.remappings,
3666                vec![
3667                    // From environment (should have precedence over remapping.txt)
3668                    Remapping::from_str("ds-test=lib/ds-test/").unwrap().into(),
3669                    Remapping::from_str("other/=lib/other/").unwrap().into(),
3670                    // From remapping.txt (should have less precedence than remapping.txt)
3671                    Remapping::from_str("file-ds-test/=lib/ds-test/").unwrap().into(),
3672                    Remapping::from_str("file-other/=lib/other/").unwrap().into(),
3673                ],
3674            );
3675
3676            Ok(())
3677        });
3678    }
3679
3680    #[test]
3681    fn test_remappings_override() {
3682        figment::Jail::expect_with(|jail| {
3683            jail.create_file(
3684                "foundry.toml",
3685                r#"
3686                [profile.default]
3687                src = "some-source"
3688                out = "some-out"
3689                cache = true
3690            "#,
3691            )?;
3692            let config = Config::load().unwrap();
3693            assert!(config.remappings.is_empty());
3694
3695            jail.create_file(
3696                "remappings.txt",
3697                r"
3698                ds-test/=lib/ds-test/
3699                other/=lib/other/
3700            ",
3701            )?;
3702
3703            let config = Config::load().unwrap();
3704            assert_eq!(
3705                config.remappings,
3706                vec![
3707                    Remapping::from_str("ds-test/=lib/ds-test/").unwrap().into(),
3708                    Remapping::from_str("other/=lib/other/").unwrap().into(),
3709                ],
3710            );
3711
3712            jail.set_env("DAPP_REMAPPINGS", "ds-test/=lib/ds-test/src/\nenv-lib/=lib/env-lib/");
3713            let config = Config::load().unwrap();
3714
3715            // Remappings should now be:
3716            // - ds-test from environment (lib/ds-test/src/)
3717            // - other from remappings.txt (lib/other/)
3718            // - env-lib from environment (lib/env-lib/)
3719            assert_eq!(
3720                config.remappings,
3721                vec![
3722                    Remapping::from_str("ds-test/=lib/ds-test/src/").unwrap().into(),
3723                    Remapping::from_str("env-lib/=lib/env-lib/").unwrap().into(),
3724                    Remapping::from_str("other/=lib/other/").unwrap().into(),
3725                ],
3726            );
3727
3728            // contains additional remapping to the source dir
3729            assert_eq!(
3730                config.get_all_remappings().collect::<Vec<_>>(),
3731                vec![
3732                    Remapping::from_str("ds-test/=lib/ds-test/src/").unwrap(),
3733                    Remapping::from_str("env-lib/=lib/env-lib/").unwrap(),
3734                    Remapping::from_str("other/=lib/other/").unwrap(),
3735                ],
3736            );
3737
3738            Ok(())
3739        });
3740    }
3741
3742    #[test]
3743    fn test_can_update_libs() {
3744        figment::Jail::expect_with(|jail| {
3745            jail.create_file(
3746                "foundry.toml",
3747                r#"
3748                [profile.default]
3749                libs = ["node_modules"]
3750            "#,
3751            )?;
3752
3753            let mut config = Config::load().unwrap();
3754            config.libs.push("libs".into());
3755            config.update_libs().unwrap();
3756
3757            let config = Config::load().unwrap();
3758            assert_eq!(config.libs, vec![PathBuf::from("node_modules"), PathBuf::from("libs"),]);
3759            Ok(())
3760        });
3761    }
3762
3763    #[test]
3764    fn test_large_gas_limit() {
3765        figment::Jail::expect_with(|jail| {
3766            let gas = u64::MAX;
3767            jail.create_file(
3768                "foundry.toml",
3769                &format!(
3770                    r#"
3771                [profile.default]
3772                gas_limit = "{gas}"
3773            "#
3774                ),
3775            )?;
3776
3777            let config = Config::load().unwrap();
3778            assert_eq!(
3779                config,
3780                Config {
3781                    gas_limit: gas.into(),
3782                    ..Config::default().normalized_optimizer_settings()
3783                }
3784            );
3785
3786            Ok(())
3787        });
3788    }
3789
3790    #[test]
3791    #[should_panic]
3792    fn test_toml_file_parse_failure() {
3793        figment::Jail::expect_with(|jail| {
3794            jail.create_file(
3795                "foundry.toml",
3796                r#"
3797                [profile.default]
3798                eth_rpc_url = "https://example.com/
3799            "#,
3800            )?;
3801
3802            let _config = Config::load().unwrap();
3803
3804            Ok(())
3805        });
3806    }
3807
3808    #[test]
3809    #[should_panic]
3810    fn test_toml_file_non_existing_config_var_failure() {
3811        figment::Jail::expect_with(|jail| {
3812            jail.set_env("FOUNDRY_CONFIG", "this config does not exist");
3813
3814            let _config = Config::load().unwrap();
3815
3816            Ok(())
3817        });
3818    }
3819
3820    #[test]
3821    fn test_resolve_etherscan_with_chain() {
3822        figment::Jail::expect_with(|jail| {
3823            let env_key = "__BSC_ETHERSCAN_API_KEY";
3824            let env_value = "env value";
3825            jail.create_file(
3826                "foundry.toml",
3827                r#"
3828                [profile.default]
3829
3830                [etherscan]
3831                bsc = { key = "${__BSC_ETHERSCAN_API_KEY}", url = "https://api.bscscan.com/api" }
3832            "#,
3833            )?;
3834
3835            let config = Config::load().unwrap();
3836            assert!(
3837                config
3838                    .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
3839                    .is_err()
3840            );
3841
3842            unsafe {
3843                std::env::set_var(env_key, env_value);
3844            }
3845
3846            assert_eq!(
3847                config
3848                    .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
3849                    .unwrap()
3850                    .unwrap()
3851                    .key,
3852                env_value
3853            );
3854
3855            let mut with_key = config;
3856            with_key.etherscan_api_key = Some("via etherscan_api_key".to_string());
3857
3858            assert_eq!(
3859                with_key
3860                    .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
3861                    .unwrap()
3862                    .unwrap()
3863                    .key,
3864                "via etherscan_api_key"
3865            );
3866
3867            unsafe {
3868                std::env::remove_var(env_key);
3869            }
3870            Ok(())
3871        });
3872    }
3873
3874    #[test]
3875    fn test_resolve_etherscan() {
3876        figment::Jail::expect_with(|jail| {
3877            jail.create_file(
3878                "foundry.toml",
3879                r#"
3880                [profile.default]
3881
3882                [etherscan]
3883                mainnet = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
3884                moonbeam = { key = "${_CONFIG_ETHERSCAN_MOONBEAM}" }
3885            "#,
3886            )?;
3887
3888            let config = Config::load().unwrap();
3889
3890            assert!(config.etherscan.clone().resolved().has_unresolved());
3891
3892            jail.set_env("_CONFIG_ETHERSCAN_MOONBEAM", "123456789");
3893
3894            let configs = config.etherscan.resolved();
3895            assert!(!configs.has_unresolved());
3896
3897            let mb_urls = Moonbeam.etherscan_urls().unwrap();
3898            let mainnet_urls = NamedChain::Mainnet.etherscan_urls().unwrap();
3899            assert_eq!(
3900                configs,
3901                ResolvedEtherscanConfigs::new([
3902                    (
3903                        "mainnet",
3904                        ResolvedEtherscanConfig {
3905                            api_url: mainnet_urls.0.to_string(),
3906                            chain: Some(NamedChain::Mainnet.into()),
3907                            browser_url: Some(mainnet_urls.1.to_string()),
3908                            key: "FX42Z3BBJJEWXWGYV2X1CIPRSCN".to_string(),
3909                        }
3910                    ),
3911                    (
3912                        "moonbeam",
3913                        ResolvedEtherscanConfig {
3914                            api_url: mb_urls.0.to_string(),
3915                            chain: Some(Moonbeam.into()),
3916                            browser_url: Some(mb_urls.1.to_string()),
3917                            key: "123456789".to_string(),
3918                        }
3919                    ),
3920                ])
3921            );
3922
3923            Ok(())
3924        });
3925    }
3926
3927    #[test]
3928    fn test_resolve_etherscan_with_versions() {
3929        figment::Jail::expect_with(|jail| {
3930            jail.create_file(
3931                "foundry.toml",
3932                r#"
3933                [profile.default]
3934
3935                [etherscan]
3936                mainnet = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN", api_version = "v2" }
3937                moonbeam = { key = "${_CONFIG_ETHERSCAN_MOONBEAM}", api_version = "v1" }
3938            "#,
3939            )?;
3940
3941            let config = Config::load().unwrap();
3942
3943            assert!(config.etherscan.clone().resolved().has_unresolved());
3944
3945            jail.set_env("_CONFIG_ETHERSCAN_MOONBEAM", "123456789");
3946
3947            let configs = config.etherscan.resolved();
3948            assert!(!configs.has_unresolved());
3949
3950            let mb_urls = Moonbeam.etherscan_urls().unwrap();
3951            let mainnet_urls = NamedChain::Mainnet.etherscan_urls().unwrap();
3952            assert_eq!(
3953                configs,
3954                ResolvedEtherscanConfigs::new([
3955                    (
3956                        "mainnet",
3957                        ResolvedEtherscanConfig {
3958                            api_url: mainnet_urls.0.to_string(),
3959                            chain: Some(NamedChain::Mainnet.into()),
3960                            browser_url: Some(mainnet_urls.1.to_string()),
3961                            key: "FX42Z3BBJJEWXWGYV2X1CIPRSCN".to_string(),
3962                        }
3963                    ),
3964                    (
3965                        "moonbeam",
3966                        ResolvedEtherscanConfig {
3967                            api_url: mb_urls.0.to_string(),
3968                            chain: Some(Moonbeam.into()),
3969                            browser_url: Some(mb_urls.1.to_string()),
3970                            key: "123456789".to_string(),
3971                        }
3972                    ),
3973                ])
3974            );
3975
3976            Ok(())
3977        });
3978    }
3979
3980    #[test]
3981    fn test_resolve_etherscan_chain_id() {
3982        figment::Jail::expect_with(|jail| {
3983            jail.create_file(
3984                "foundry.toml",
3985                r#"
3986                [profile.default]
3987                chain_id = "sepolia"
3988
3989                [etherscan]
3990                sepolia = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
3991            "#,
3992            )?;
3993
3994            let config = Config::load().unwrap();
3995            let etherscan = config.get_etherscan_config().unwrap().unwrap();
3996            assert_eq!(etherscan.chain, Some(NamedChain::Sepolia.into()));
3997            assert_eq!(etherscan.key, "FX42Z3BBJJEWXWGYV2X1CIPRSCN");
3998
3999            Ok(())
4000        });
4001    }
4002
4003    // any invalid entry invalidates whole [etherscan] sections
4004    #[test]
4005    fn test_resolve_etherscan_with_invalid_name() {
4006        figment::Jail::expect_with(|jail| {
4007            jail.create_file(
4008                "foundry.toml",
4009                r#"
4010                [etherscan]
4011                mainnet = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
4012                an_invalid_name = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
4013            "#,
4014            )?;
4015
4016            let config = Config::load().unwrap();
4017            let etherscan_config = config.get_etherscan_config();
4018            assert!(etherscan_config.is_none());
4019
4020            Ok(())
4021        });
4022    }
4023
4024    #[test]
4025    fn test_resolve_rpc_url() {
4026        figment::Jail::expect_with(|jail| {
4027            jail.create_file(
4028                "foundry.toml",
4029                r#"
4030                [profile.default]
4031                [rpc_endpoints]
4032                optimism = "https://example.com/"
4033                mainnet = "${_CONFIG_MAINNET}"
4034            "#,
4035            )?;
4036            jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
4037
4038            let mut config = Config::load().unwrap();
4039            assert_eq!("http://localhost:8545", config.get_rpc_url_or_localhost_http().unwrap());
4040
4041            config.eth_rpc_url = Some("mainnet".to_string());
4042            assert_eq!(
4043                "https://eth-mainnet.alchemyapi.io/v2/123455",
4044                config.get_rpc_url_or_localhost_http().unwrap()
4045            );
4046
4047            config.eth_rpc_url = Some("optimism".to_string());
4048            assert_eq!("https://example.com/", config.get_rpc_url_or_localhost_http().unwrap());
4049
4050            Ok(())
4051        })
4052    }
4053
4054    #[test]
4055    fn test_resolve_rpc_url_if_etherscan_set() {
4056        figment::Jail::expect_with(|jail| {
4057            jail.create_file(
4058                "foundry.toml",
4059                r#"
4060                [profile.default]
4061                etherscan_api_key = "dummy"
4062                [rpc_endpoints]
4063                optimism = "https://example.com/"
4064            "#,
4065            )?;
4066
4067            let config = Config::load().unwrap();
4068            assert_eq!("http://localhost:8545", config.get_rpc_url_or_localhost_http().unwrap());
4069
4070            Ok(())
4071        })
4072    }
4073
4074    #[test]
4075    fn test_resolve_rpc_url_alias() {
4076        figment::Jail::expect_with(|jail| {
4077            jail.create_file(
4078                "foundry.toml",
4079                r#"
4080                [profile.default]
4081                [rpc_endpoints]
4082                polygonAmoy = "https://polygon-amoy.g.alchemy.com/v2/${_RESOLVE_RPC_ALIAS}"
4083            "#,
4084            )?;
4085            let mut config = Config::load().unwrap();
4086            config.eth_rpc_url = Some("polygonAmoy".to_string());
4087            assert!(config.get_rpc_url().unwrap().is_err());
4088
4089            jail.set_env("_RESOLVE_RPC_ALIAS", "123455");
4090
4091            let mut config = Config::load().unwrap();
4092            config.eth_rpc_url = Some("polygonAmoy".to_string());
4093            assert_eq!(
4094                "https://polygon-amoy.g.alchemy.com/v2/123455",
4095                config.get_rpc_url().unwrap().unwrap()
4096            );
4097
4098            Ok(())
4099        })
4100    }
4101
4102    #[test]
4103    fn test_resolve_rpc_aliases() {
4104        figment::Jail::expect_with(|jail| {
4105            jail.create_file(
4106                "foundry.toml",
4107                r#"
4108               [profile.default]
4109               [etherscan]
4110               arbitrum_alias = { key = "${TEST_RESOLVE_RPC_ALIAS_ARBISCAN}" }
4111               [rpc_endpoints]
4112               arbitrum_alias = "https://arb-mainnet.g.alchemy.com/v2/${TEST_RESOLVE_RPC_ALIAS_ARB_ONE}"
4113            "#,
4114            )?;
4115
4116            jail.set_env("TEST_RESOLVE_RPC_ALIAS_ARB_ONE", "123455");
4117            jail.set_env("TEST_RESOLVE_RPC_ALIAS_ARBISCAN", "123455");
4118
4119            let config = Config::load().unwrap();
4120
4121            let config = config.get_etherscan_config_with_chain(Some(NamedChain::Arbitrum.into()));
4122            assert!(config.is_err());
4123            assert_eq!(
4124                config.unwrap_err().to_string(),
4125                "At least one of `url` or `chain` must be present for Etherscan config with unknown alias `arbitrum_alias`"
4126            );
4127
4128            Ok(())
4129        });
4130    }
4131
4132    #[test]
4133    fn test_resolve_rpc_config() {
4134        figment::Jail::expect_with(|jail| {
4135            jail.create_file(
4136                "foundry.toml",
4137                r#"
4138                [rpc_endpoints]
4139                optimism = "https://example.com/"
4140                mainnet = { endpoint = "${_CONFIG_MAINNET}", retries = 3, retry_backoff = 1000, compute_units_per_second = 1000 }
4141            "#,
4142            )?;
4143            jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
4144
4145            let config = Config::load().unwrap();
4146            assert_eq!(
4147                RpcEndpoints::new([
4148                    (
4149                        "optimism",
4150                        RpcEndpointType::String(RpcEndpointUrl::Url(
4151                            "https://example.com/".to_string()
4152                        ))
4153                    ),
4154                    (
4155                        "mainnet",
4156                        RpcEndpointType::Config(RpcEndpoint {
4157                            endpoint: RpcEndpointUrl::Env("${_CONFIG_MAINNET}".to_string()),
4158                            extra_endpoints: vec![],
4159                            config: RpcEndpointConfig {
4160                                retries: Some(3),
4161                                retry_backoff: Some(1000),
4162                                compute_units_per_second: Some(1000),
4163                            },
4164                            auth: None,
4165                        })
4166                    ),
4167                ]),
4168                config.rpc_endpoints
4169            );
4170
4171            let resolved = config.rpc_endpoints.resolved();
4172            assert_eq!(
4173                RpcEndpoints::new([
4174                    (
4175                        "optimism",
4176                        RpcEndpointType::String(RpcEndpointUrl::Url(
4177                            "https://example.com/".to_string()
4178                        ))
4179                    ),
4180                    (
4181                        "mainnet",
4182                        RpcEndpointType::Config(RpcEndpoint {
4183                            endpoint: RpcEndpointUrl::Env("${_CONFIG_MAINNET}".to_string()),
4184                            extra_endpoints: vec![],
4185                            config: RpcEndpointConfig {
4186                                retries: Some(3),
4187                                retry_backoff: Some(1000),
4188                                compute_units_per_second: Some(1000),
4189                            },
4190                            auth: None,
4191                        })
4192                    ),
4193                ])
4194                .resolved(),
4195                resolved
4196            );
4197            Ok(())
4198        })
4199    }
4200
4201    #[test]
4202    fn test_resolve_auth() {
4203        figment::Jail::expect_with(|jail| {
4204            jail.create_file(
4205                "foundry.toml",
4206                r#"
4207                [profile.default]
4208                eth_rpc_url = "optimism"
4209                [rpc_endpoints]
4210                optimism = "https://example.com/"
4211                mainnet = { endpoint = "${_CONFIG_MAINNET}", retries = 3, retry_backoff = 1000, compute_units_per_second = 1000, auth = "Bearer ${_CONFIG_AUTH}" }
4212            "#,
4213            )?;
4214
4215            let config = Config::load().unwrap();
4216
4217            jail.set_env("_CONFIG_AUTH", "123456");
4218            jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
4219
4220            assert_eq!(
4221                RpcEndpoints::new([
4222                    (
4223                        "optimism",
4224                        RpcEndpointType::String(RpcEndpointUrl::Url(
4225                            "https://example.com/".to_string()
4226                        ))
4227                    ),
4228                    (
4229                        "mainnet",
4230                        RpcEndpointType::Config(RpcEndpoint {
4231                            endpoint: RpcEndpointUrl::Env("${_CONFIG_MAINNET}".to_string()),
4232                            extra_endpoints: vec![],
4233                            config: RpcEndpointConfig {
4234                                retries: Some(3),
4235                                retry_backoff: Some(1000),
4236                                compute_units_per_second: Some(1000)
4237                            },
4238                            auth: Some(RpcAuth::Env("Bearer ${_CONFIG_AUTH}".to_string())),
4239                        })
4240                    ),
4241                ]),
4242                config.rpc_endpoints
4243            );
4244            let resolved = config.rpc_endpoints.resolved();
4245            assert_eq!(
4246                RpcEndpoints::new([
4247                    (
4248                        "optimism",
4249                        RpcEndpointType::String(RpcEndpointUrl::Url(
4250                            "https://example.com/".to_string()
4251                        ))
4252                    ),
4253                    (
4254                        "mainnet",
4255                        RpcEndpointType::Config(RpcEndpoint {
4256                            endpoint: RpcEndpointUrl::Url(
4257                                "https://eth-mainnet.alchemyapi.io/v2/123455".to_string()
4258                            ),
4259                            extra_endpoints: vec![],
4260                            config: RpcEndpointConfig {
4261                                retries: Some(3),
4262                                retry_backoff: Some(1000),
4263                                compute_units_per_second: Some(1000)
4264                            },
4265                            auth: Some(RpcAuth::Raw("Bearer 123456".to_string())),
4266                        })
4267                    ),
4268                ])
4269                .resolved(),
4270                resolved
4271            );
4272
4273            Ok(())
4274        });
4275    }
4276
4277    #[test]
4278    fn test_resolve_endpoints() {
4279        figment::Jail::expect_with(|jail| {
4280            jail.create_file(
4281                "foundry.toml",
4282                r#"
4283                [profile.default]
4284                eth_rpc_url = "optimism"
4285                [rpc_endpoints]
4286                optimism = "https://example.com/"
4287                mainnet = "${_CONFIG_MAINNET}"
4288                mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${_CONFIG_API_KEY1}"
4289                mainnet_3 = "https://eth-mainnet.alchemyapi.io/v2/${_CONFIG_API_KEY1}/${_CONFIG_API_KEY2}"
4290            "#,
4291            )?;
4292
4293            let config = Config::load().unwrap();
4294
4295            assert_eq!(config.get_rpc_url().unwrap().unwrap(), "https://example.com/");
4296
4297            assert!(config.rpc_endpoints.clone().resolved().has_unresolved());
4298
4299            jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
4300            jail.set_env("_CONFIG_API_KEY1", "123456");
4301            jail.set_env("_CONFIG_API_KEY2", "98765");
4302
4303            let endpoints = config.rpc_endpoints.resolved();
4304
4305            assert!(!endpoints.has_unresolved());
4306
4307            assert_eq!(
4308                endpoints,
4309                RpcEndpoints::new([
4310                    ("optimism", RpcEndpointUrl::Url("https://example.com/".to_string())),
4311                    (
4312                        "mainnet",
4313                        RpcEndpointUrl::Url(
4314                            "https://eth-mainnet.alchemyapi.io/v2/123455".to_string()
4315                        )
4316                    ),
4317                    (
4318                        "mainnet_2",
4319                        RpcEndpointUrl::Url(
4320                            "https://eth-mainnet.alchemyapi.io/v2/123456".to_string()
4321                        )
4322                    ),
4323                    (
4324                        "mainnet_3",
4325                        RpcEndpointUrl::Url(
4326                            "https://eth-mainnet.alchemyapi.io/v2/123456/98765".to_string()
4327                        )
4328                    ),
4329                ])
4330                .resolved()
4331            );
4332
4333            Ok(())
4334        });
4335    }
4336
4337    #[test]
4338    fn test_extract_etherscan_config() {
4339        figment::Jail::expect_with(|jail| {
4340            jail.create_file(
4341                "foundry.toml",
4342                r#"
4343                [profile.default]
4344                etherscan_api_key = "optimism"
4345
4346                [etherscan]
4347                optimism = { key = "https://etherscan-optimism.com/" }
4348                amoy = { key = "https://etherscan-amoy.com/" }
4349            "#,
4350            )?;
4351
4352            let mut config = Config::load().unwrap();
4353
4354            let optimism = config.get_etherscan_api_key(Some(NamedChain::Optimism.into()));
4355            assert_eq!(optimism, Some("https://etherscan-optimism.com/".to_string()));
4356
4357            config.etherscan_api_key = Some("amoy".to_string());
4358
4359            let amoy = config.get_etherscan_api_key(Some(NamedChain::PolygonAmoy.into()));
4360            assert_eq!(amoy, Some("https://etherscan-amoy.com/".to_string()));
4361
4362            Ok(())
4363        });
4364    }
4365
4366    #[test]
4367    fn test_extract_etherscan_config_by_chain() {
4368        figment::Jail::expect_with(|jail| {
4369            jail.create_file(
4370                "foundry.toml",
4371                r#"
4372                [profile.default]
4373
4374                [etherscan]
4375                amoy = { key = "https://etherscan-amoy.com/", chain = 80002 }
4376            "#,
4377            )?;
4378
4379            let config = Config::load().unwrap();
4380
4381            let amoy = config
4382                .get_etherscan_config_with_chain(Some(NamedChain::PolygonAmoy.into()))
4383                .unwrap()
4384                .unwrap();
4385            assert_eq!(amoy.key, "https://etherscan-amoy.com/".to_string());
4386
4387            Ok(())
4388        });
4389    }
4390
4391    #[test]
4392    fn test_extract_etherscan_config_by_chain_with_url() {
4393        figment::Jail::expect_with(|jail| {
4394            jail.create_file(
4395                "foundry.toml",
4396                r#"
4397                [profile.default]
4398
4399                [etherscan]
4400                amoy = { key = "https://etherscan-amoy.com/", chain = 80002 , url =  "https://verifier-url.com/"}
4401            "#,
4402            )?;
4403
4404            let config = Config::load().unwrap();
4405
4406            let amoy = config
4407                .get_etherscan_config_with_chain(Some(NamedChain::PolygonAmoy.into()))
4408                .unwrap()
4409                .unwrap();
4410            assert_eq!(amoy.key, "https://etherscan-amoy.com/".to_string());
4411            assert_eq!(amoy.api_url, "https://verifier-url.com/".to_string());
4412
4413            Ok(())
4414        });
4415    }
4416
4417    #[test]
4418    fn test_extract_etherscan_config_by_chain_and_alias() {
4419        figment::Jail::expect_with(|jail| {
4420            jail.create_file(
4421                "foundry.toml",
4422                r#"
4423                [profile.default]
4424                eth_rpc_url = "amoy"
4425
4426                [etherscan]
4427                amoy = { key = "https://etherscan-amoy.com/" }
4428
4429                [rpc_endpoints]
4430                amoy = "https://polygon-amoy.g.alchemy.com/v2/amoy"
4431            "#,
4432            )?;
4433
4434            let config = Config::load().unwrap();
4435
4436            let amoy = config.get_etherscan_config_with_chain(None).unwrap().unwrap();
4437            assert_eq!(amoy.key, "https://etherscan-amoy.com/".to_string());
4438
4439            let amoy_rpc = config.get_rpc_url().unwrap().unwrap();
4440            assert_eq!(amoy_rpc, "https://polygon-amoy.g.alchemy.com/v2/amoy");
4441            Ok(())
4442        });
4443    }
4444
4445    #[test]
4446    fn test_toml_file() {
4447        figment::Jail::expect_with(|jail| {
4448            jail.create_file(
4449                "foundry.toml",
4450                r#"
4451                [profile.default]
4452                src = "some-source"
4453                out = "some-out"
4454                cache = true
4455                eth_rpc_url = "https://example.com/"
4456                verbosity = 3
4457                remappings = ["ds-test=lib/ds-test/"]
4458                via_ir = true
4459                rpc_storage_caching = { chains = [1, "optimism", 999999], endpoints = "all"}
4460                use_literal_content = false
4461                bytecode_hash = "ipfs"
4462                cbor_metadata = true
4463                revert_strings = "strip"
4464                allow_paths = ["allow", "paths"]
4465                build_info_path = "build-info"
4466                always_use_create_2_factory = true
4467
4468                [rpc_endpoints]
4469                optimism = "https://example.com/"
4470                mainnet = "${RPC_MAINNET}"
4471                mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}"
4472                mainnet_3 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}/${ANOTHER_KEY}"
4473            "#,
4474            )?;
4475
4476            let config = Config::load().unwrap();
4477            assert_eq!(
4478                config,
4479                Config {
4480                    src: "some-source".into(),
4481                    out: "some-out".into(),
4482                    cache: true,
4483                    eth_rpc_url: Some("https://example.com/".to_string()),
4484                    remappings: vec![Remapping::from_str("ds-test=lib/ds-test/").unwrap().into()],
4485                    verbosity: 3,
4486                    via_ir: true,
4487                    rpc_storage_caching: StorageCachingConfig {
4488                        chains: CachedChains::Chains(vec![
4489                            Chain::mainnet(),
4490                            Chain::optimism_mainnet(),
4491                            Chain::from_id(999999)
4492                        ]),
4493                        endpoints: CachedEndpoints::All,
4494                    },
4495                    use_literal_content: false,
4496                    bytecode_hash: BytecodeHash::Ipfs,
4497                    cbor_metadata: true,
4498                    revert_strings: Some(RevertStrings::Strip),
4499                    allow_paths: vec![PathBuf::from("allow"), PathBuf::from("paths")],
4500                    rpc_endpoints: RpcEndpoints::new([
4501                        ("optimism", RpcEndpointUrl::Url("https://example.com/".to_string())),
4502                        ("mainnet", RpcEndpointUrl::Env("${RPC_MAINNET}".to_string())),
4503                        (
4504                            "mainnet_2",
4505                            RpcEndpointUrl::Env(
4506                                "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}".to_string()
4507                            )
4508                        ),
4509                        (
4510                            "mainnet_3",
4511                            RpcEndpointUrl::Env(
4512                                "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}/${ANOTHER_KEY}"
4513                                    .to_string()
4514                            )
4515                        ),
4516                    ]),
4517                    build_info_path: Some("build-info".into()),
4518                    always_use_create_2_factory: true,
4519                    ..Config::default().normalized_optimizer_settings()
4520                }
4521            );
4522
4523            Ok(())
4524        });
4525    }
4526
4527    #[test]
4528    fn test_load_remappings() {
4529        figment::Jail::expect_with(|jail| {
4530            jail.create_file(
4531                "foundry.toml",
4532                r"
4533                [profile.default]
4534                remappings = ['nested/=lib/nested/']
4535            ",
4536            )?;
4537
4538            let config = Config::load_with_root(jail.directory()).unwrap();
4539            assert_eq!(
4540                config.remappings,
4541                vec![Remapping::from_str("nested/=lib/nested/").unwrap().into()]
4542            );
4543
4544            Ok(())
4545        });
4546    }
4547
4548    #[test]
4549    fn test_load_full_toml() {
4550        figment::Jail::expect_with(|jail| {
4551            jail.create_file(
4552                "foundry.toml",
4553                r#"
4554                [profile.default]
4555                auto_detect_solc = true
4556                block_base_fee_per_gas = 0
4557                block_coinbase = '0x0000000000000000000000000000000000000000'
4558                block_difficulty = 0
4559                block_prevrandao = '0x0000000000000000000000000000000000000000000000000000000000000000'
4560                block_number = 1
4561                block_timestamp = 1
4562                use_literal_content = false
4563                bytecode_hash = 'ipfs'
4564                cbor_metadata = true
4565                cache = true
4566                cache_path = 'cache'
4567                evm_version = 'london'
4568                extra_output = []
4569                extra_output_files = []
4570                always_use_create_2_factory = false
4571                ffi = false
4572                force = false
4573                gas_limit = 9223372036854775807
4574                gas_price = 0
4575                gas_reports = ['*']
4576                ignored_error_codes = [1878]
4577                ignored_warnings_from = ["something"]
4578                deny = "never"
4579                initial_balance = '0xffffffffffffffffffffffff'
4580                libraries = []
4581                libs = ['lib']
4582                memory_limit = 134217728
4583                names = false
4584                no_storage_caching = false
4585                no_rpc_rate_limit = false
4586                offline = false
4587                optimizer = true
4588                optimizer_runs = 200
4589                out = 'out'
4590                remappings = ['nested/=lib/nested/']
4591                sender = '0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38'
4592                sizes = false
4593                sparse_mode = false
4594                src = 'src'
4595                test = 'test'
4596                tx_origin = '0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38'
4597                verbosity = 0
4598                via_ir = false
4599
4600                [profile.default.rpc_storage_caching]
4601                chains = 'all'
4602                endpoints = 'all'
4603
4604                [rpc_endpoints]
4605                optimism = "https://example.com/"
4606                mainnet = "${RPC_MAINNET}"
4607                mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}"
4608
4609                [fuzz]
4610                runs = 256
4611                seed = '0x3e8'
4612                max_test_rejects = 65536
4613
4614                [invariant]
4615                runs = 256
4616                depth = 500
4617                workers = 1
4618                fail_on_revert = false
4619                call_override = false
4620                shrink_run_limit = 5000
4621            "#,
4622            )?;
4623
4624            let config = Config::load_with_root(jail.directory()).unwrap();
4625
4626            assert_eq!(config.ignored_file_paths, vec![PathBuf::from("something")]);
4627            assert_eq!(config.fuzz.seed, Some(U256::from(1000)));
4628            assert_eq!(
4629                config.remappings,
4630                vec![Remapping::from_str("nested/=lib/nested/").unwrap().into()]
4631            );
4632
4633            assert_eq!(
4634                config.rpc_endpoints,
4635                RpcEndpoints::new([
4636                    ("optimism", RpcEndpointUrl::Url("https://example.com/".to_string())),
4637                    ("mainnet", RpcEndpointUrl::Env("${RPC_MAINNET}".to_string())),
4638                    (
4639                        "mainnet_2",
4640                        RpcEndpointUrl::Env(
4641                            "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}".to_string()
4642                        )
4643                    ),
4644                ]),
4645            );
4646
4647            Ok(())
4648        });
4649    }
4650
4651    #[test]
4652    fn test_solc_req() {
4653        figment::Jail::expect_with(|jail| {
4654            jail.create_file(
4655                "foundry.toml",
4656                r#"
4657                [profile.default]
4658                solc_version = "0.8.12"
4659            "#,
4660            )?;
4661
4662            let config = Config::load().unwrap();
4663            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));
4664
4665            jail.create_file(
4666                "foundry.toml",
4667                r#"
4668                [profile.default]
4669                solc = "0.8.12"
4670            "#,
4671            )?;
4672
4673            let config = Config::load().unwrap();
4674            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));
4675
4676            jail.create_file(
4677                "foundry.toml",
4678                r#"
4679                [profile.default]
4680                solc = "path/to/local/solc"
4681            "#,
4682            )?;
4683
4684            let config = Config::load().unwrap();
4685            assert_eq!(config.solc, Some(SolcReq::Local("path/to/local/solc".into())));
4686
4687            jail.set_env("FOUNDRY_SOLC_VERSION", "0.6.6");
4688            let config = Config::load().unwrap();
4689            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 6, 6))));
4690            Ok(())
4691        });
4692    }
4693
4694    // ensures the newer `solc` takes precedence over `solc_version`
4695    #[test]
4696    fn test_backwards_solc_version() {
4697        figment::Jail::expect_with(|jail| {
4698            jail.create_file(
4699                "foundry.toml",
4700                r#"
4701                [default]
4702                solc = "0.8.12"
4703                solc_version = "0.8.20"
4704            "#,
4705            )?;
4706
4707            let config = Config::load().unwrap();
4708            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));
4709
4710            Ok(())
4711        });
4712
4713        figment::Jail::expect_with(|jail| {
4714            jail.create_file(
4715                "foundry.toml",
4716                r#"
4717                [default]
4718                solc_version = "0.8.20"
4719            "#,
4720            )?;
4721
4722            let config = Config::load().unwrap();
4723            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 20))));
4724
4725            Ok(())
4726        });
4727    }
4728
4729    #[test]
4730    fn test_toml_casing_file() {
4731        figment::Jail::expect_with(|jail| {
4732            jail.create_file(
4733                "foundry.toml",
4734                r#"
4735                [profile.default]
4736                src = "some-source"
4737                out = "some-out"
4738                cache = true
4739                eth-rpc-url = "https://example.com/"
4740                evm-version = "berlin"
4741                auto-detect-solc = false
4742            "#,
4743            )?;
4744
4745            let config = Config::load().unwrap();
4746            assert_eq!(
4747                config,
4748                Config {
4749                    src: "some-source".into(),
4750                    out: "some-out".into(),
4751                    cache: true,
4752                    eth_rpc_url: Some("https://example.com/".to_string()),
4753                    auto_detect_solc: false,
4754                    evm_version: EvmVersion::Berlin,
4755                    ..Config::default().normalized_optimizer_settings()
4756                }
4757            );
4758
4759            Ok(())
4760        });
4761    }
4762
4763    #[test]
4764    fn test_output_selection() {
4765        figment::Jail::expect_with(|jail| {
4766            jail.create_file(
4767                "foundry.toml",
4768                r#"
4769                [profile.default]
4770                extra_output = ["metadata", "ir-optimized"]
4771                extra_output_files = ["metadata"]
4772            "#,
4773            )?;
4774
4775            let config = Config::load().unwrap();
4776
4777            assert_eq!(
4778                config.extra_output,
4779                vec![ContractOutputSelection::Metadata, ContractOutputSelection::IrOptimized]
4780            );
4781            assert_eq!(config.extra_output_files, vec![ContractOutputSelection::Metadata]);
4782
4783            Ok(())
4784        });
4785    }
4786
4787    #[test]
4788    fn test_precedence() {
4789        figment::Jail::expect_with(|jail| {
4790            jail.create_file(
4791                "foundry.toml",
4792                r#"
4793                [profile.default]
4794                src = "mysrc"
4795                out = "myout"
4796                verbosity = 3
4797            "#,
4798            )?;
4799
4800            let config = Config::load().unwrap();
4801            assert_eq!(
4802                config,
4803                Config {
4804                    src: "mysrc".into(),
4805                    out: "myout".into(),
4806                    verbosity: 3,
4807                    ..Config::default().normalized_optimizer_settings()
4808                }
4809            );
4810
4811            jail.set_env("FOUNDRY_SRC", r"other-src");
4812            let config = Config::load().unwrap();
4813            assert_eq!(
4814                config,
4815                Config {
4816                    src: "other-src".into(),
4817                    out: "myout".into(),
4818                    verbosity: 3,
4819                    ..Config::default().normalized_optimizer_settings()
4820                }
4821            );
4822
4823            jail.set_env("FOUNDRY_PROFILE", "foo");
4824            let val: Result<String, _> = Config::figment().extract_inner("profile");
4825            assert!(val.is_err());
4826
4827            Ok(())
4828        });
4829    }
4830
4831    #[test]
4832    fn test_extract_basic() {
4833        figment::Jail::expect_with(|jail| {
4834            jail.create_file(
4835                "foundry.toml",
4836                r#"
4837                [profile.default]
4838                src = "mysrc"
4839                out = "myout"
4840                verbosity = 3
4841                evm_version = 'berlin'
4842
4843                [profile.other]
4844                src = "other-src"
4845            "#,
4846            )?;
4847            let loaded = Config::load().unwrap();
4848            assert_eq!(loaded.evm_version, EvmVersion::Berlin);
4849            let base = loaded.into_basic();
4850            let default = Config::default();
4851            assert_eq!(
4852                base,
4853                BasicConfig {
4854                    profile: Config::DEFAULT_PROFILE,
4855                    src: "mysrc".into(),
4856                    out: "myout".into(),
4857                    libs: default.libs.clone(),
4858                    remappings: default.remappings.clone(),
4859                    network: None,
4860                }
4861            );
4862            jail.set_env("FOUNDRY_PROFILE", r"other");
4863            let base = Config::figment().extract::<BasicConfig>().unwrap();
4864            assert_eq!(
4865                base,
4866                BasicConfig {
4867                    profile: Config::DEFAULT_PROFILE,
4868                    src: "other-src".into(),
4869                    out: "myout".into(),
4870                    libs: default.libs.clone(),
4871                    remappings: default.remappings,
4872                    network: None,
4873                }
4874            );
4875            Ok(())
4876        });
4877    }
4878
4879    #[test]
4880    #[should_panic]
4881    fn test_parse_invalid_fuzz_weight() {
4882        figment::Jail::expect_with(|jail| {
4883            jail.create_file(
4884                "foundry.toml",
4885                r"
4886                [fuzz]
4887                dictionary_weight = 101
4888            ",
4889            )?;
4890            let _config = Config::load().unwrap();
4891            Ok(())
4892        });
4893    }
4894
4895    #[test]
4896    fn test_fallback_provider() {
4897        figment::Jail::expect_with(|jail| {
4898            jail.create_file(
4899                "foundry.toml",
4900                r"
4901                [fuzz]
4902                runs = 1
4903                include_storage = false
4904                dictionary_weight = 99
4905
4906                [invariant]
4907                runs = 420
4908
4909                [profile.ci.fuzz]
4910                dictionary_weight = 5
4911
4912                [profile.ci.invariant]
4913                runs = 400
4914            ",
4915            )?;
4916
4917            let invariant_default = InvariantConfig::default();
4918            let config = Config::load().unwrap();
4919
4920            assert_ne!(config.invariant.runs, config.fuzz.runs);
4921            assert_eq!(config.invariant.runs, 420);
4922
4923            assert_ne!(
4924                config.fuzz.dictionary.include_storage,
4925                invariant_default.dictionary.include_storage
4926            );
4927            assert_eq!(
4928                config.invariant.dictionary.include_storage,
4929                config.fuzz.dictionary.include_storage
4930            );
4931
4932            assert_ne!(
4933                config.fuzz.dictionary.dictionary_weight,
4934                invariant_default.dictionary.dictionary_weight
4935            );
4936            assert_eq!(
4937                config.invariant.dictionary.dictionary_weight,
4938                config.fuzz.dictionary.dictionary_weight
4939            );
4940
4941            jail.set_env("FOUNDRY_PROFILE", "ci");
4942            let ci_config = Config::load().unwrap();
4943            assert_eq!(ci_config.fuzz.runs, 1);
4944            assert_eq!(ci_config.invariant.runs, 400);
4945            assert_eq!(ci_config.fuzz.dictionary.dictionary_weight, 5);
4946            assert_eq!(
4947                ci_config.invariant.dictionary.dictionary_weight,
4948                config.fuzz.dictionary.dictionary_weight
4949            );
4950
4951            Ok(())
4952        })
4953    }
4954
4955    #[test]
4956    fn test_standalone_profile_sections() {
4957        figment::Jail::expect_with(|jail| {
4958            jail.create_file(
4959                "foundry.toml",
4960                r#"
4961                [fuzz]
4962                runs = 100
4963
4964                [invariant]
4965                runs = 120
4966
4967                [symbolic]
4968                enabled = true
4969                max_paths = 12
4970                storage_layout = "generic"
4971
4972                [profile.ci.fuzz]
4973                runs = 420
4974
4975                [profile.ci.invariant]
4976                runs = 500
4977
4978                [profile.ci.symbolic]
4979                max_paths = 34
4980                dump_smt = true
4981            "#,
4982            )?;
4983
4984            let config = Config::load().unwrap();
4985            assert_eq!(config.fuzz.runs, 100);
4986            assert_eq!(config.invariant.runs, 120);
4987            assert!(config.symbolic.enabled);
4988            assert_eq!(config.symbolic.max_paths, 12);
4989            assert_eq!(config.symbolic.storage_layout, SymbolicStorageLayout::Generic);
4990            assert!(!config.symbolic.dump_smt);
4991
4992            jail.set_env("FOUNDRY_PROFILE", "ci");
4993            let config = Config::load().unwrap();
4994            assert_eq!(config.fuzz.runs, 420);
4995            assert_eq!(config.invariant.runs, 500);
4996            assert!(config.symbolic.enabled);
4997            assert_eq!(config.symbolic.max_paths, 34);
4998            assert_eq!(config.symbolic.storage_layout, SymbolicStorageLayout::Generic);
4999            assert!(config.symbolic.dump_smt);
5000
5001            Ok(())
5002        });
5003    }
5004
5005    #[test]
5006    fn can_handle_deviating_dapp_aliases() {
5007        figment::Jail::expect_with(|jail| {
5008            let addr = Address::ZERO;
5009            jail.set_env("DAPP_TEST_NUMBER", 1337);
5010            jail.set_env("DAPP_TEST_ADDRESS", format!("{addr:?}"));
5011            jail.set_env("DAPP_TEST_FUZZ_RUNS", 420);
5012            jail.set_env("DAPP_TEST_DEPTH", 20);
5013            jail.set_env("DAPP_FORK_BLOCK", 100);
5014            jail.set_env("DAPP_BUILD_OPTIMIZE_RUNS", 999);
5015            jail.set_env("DAPP_BUILD_OPTIMIZE", 0);
5016
5017            let config = Config::load().unwrap();
5018
5019            assert_eq!(config.block_number, U256::from(1337));
5020            assert_eq!(config.sender, addr);
5021            assert_eq!(config.fuzz.runs, 420);
5022            assert_eq!(config.invariant.depth, 20);
5023            assert_eq!(config.fork_block_number, Some(100));
5024            assert_eq!(config.optimizer_runs, Some(999));
5025            assert!(!config.optimizer.unwrap());
5026
5027            Ok(())
5028        });
5029    }
5030
5031    #[test]
5032    fn can_parse_libraries() {
5033        figment::Jail::expect_with(|jail| {
5034            jail.set_env(
5035                "DAPP_LIBRARIES",
5036                "[src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6]",
5037            );
5038            let config = Config::load().unwrap();
5039            assert_eq!(
5040                config.libraries,
5041                vec![
5042                    "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
5043                        .to_string()
5044                ]
5045            );
5046
5047            jail.set_env(
5048                "DAPP_LIBRARIES",
5049                "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6",
5050            );
5051            let config = Config::load().unwrap();
5052            assert_eq!(
5053                config.libraries,
5054                vec![
5055                    "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
5056                        .to_string(),
5057                ]
5058            );
5059
5060            jail.set_env(
5061                "DAPP_LIBRARIES",
5062                "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6,src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6",
5063            );
5064            let config = Config::load().unwrap();
5065            assert_eq!(
5066                config.libraries,
5067                vec![
5068                    "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
5069                        .to_string(),
5070                    "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
5071                        .to_string()
5072                ]
5073            );
5074
5075            Ok(())
5076        });
5077    }
5078
5079    #[test]
5080    fn test_parse_many_libraries() {
5081        figment::Jail::expect_with(|jail| {
5082            jail.create_file(
5083                "foundry.toml",
5084                r"
5085                [profile.default]
5086               libraries= [
5087                        './src/SizeAuctionDiscount.sol:Chainlink:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
5088                        './src/SizeAuction.sol:ChainlinkTWAP:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
5089                        './src/SizeAuction.sol:Math:0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c',
5090                        './src/test/ChainlinkTWAP.t.sol:ChainlinkTWAP:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
5091                        './src/SizeAuctionDiscount.sol:Math:0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c',
5092                    ]
5093            ",
5094            )?;
5095            let config = Config::load().unwrap();
5096
5097            let libs = config.parsed_libraries().unwrap().libs;
5098
5099            similar_asserts::assert_eq!(
5100                libs,
5101                BTreeMap::from([
5102                    (
5103                        PathBuf::from("./src/SizeAuctionDiscount.sol"),
5104                        BTreeMap::from([
5105                            (
5106                                "Chainlink".to_string(),
5107                                "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
5108                            ),
5109                            (
5110                                "Math".to_string(),
5111                                "0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c".to_string()
5112                            )
5113                        ])
5114                    ),
5115                    (
5116                        PathBuf::from("./src/SizeAuction.sol"),
5117                        BTreeMap::from([
5118                            (
5119                                "ChainlinkTWAP".to_string(),
5120                                "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
5121                            ),
5122                            (
5123                                "Math".to_string(),
5124                                "0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c".to_string()
5125                            )
5126                        ])
5127                    ),
5128                    (
5129                        PathBuf::from("./src/test/ChainlinkTWAP.t.sol"),
5130                        BTreeMap::from([(
5131                            "ChainlinkTWAP".to_string(),
5132                            "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
5133                        )])
5134                    ),
5135                ])
5136            );
5137
5138            Ok(())
5139        });
5140    }
5141
5142    #[test]
5143    fn config_roundtrip() {
5144        figment::Jail::expect_with(|jail| {
5145            let default = Config::default().normalized_optimizer_settings();
5146            let basic = default.clone().into_basic();
5147            jail.create_file("foundry.toml", &basic.to_string_pretty().unwrap())?;
5148
5149            let mut other = Config::load().unwrap();
5150            clear_warning(&mut other);
5151            assert_eq!(default, other);
5152
5153            let other = other.into_basic();
5154            assert_eq!(basic, other);
5155
5156            jail.create_file("foundry.toml", &default.to_string_pretty().unwrap())?;
5157            let mut other = Config::load().unwrap();
5158            clear_warning(&mut other);
5159            let mut serialized_default = default;
5160            mark_serialized_invariant_provenance(&mut serialized_default);
5161            assert_eq!(serialized_default, other);
5162
5163            Ok(())
5164        });
5165    }
5166
5167    #[test]
5168    fn config_serialization_preserves_context_directory_boundary() {
5169        let remapping = "lib/outer/:inner/=lib/outer/lib/inner/";
5170        let config = Config {
5171            remappings: vec![Remapping::from_str(remapping).unwrap().into()],
5172            ..Default::default()
5173        };
5174
5175        let serialized = toml::Value::try_from(&config).unwrap();
5176        assert_eq!(serialized["remappings"][0].as_str(), Some(remapping));
5177
5178        let serialized = toml::Value::try_from(config.into_basic()).unwrap();
5179        assert_eq!(serialized["remappings"][0].as_str(), Some(remapping));
5180    }
5181
5182    #[cfg(windows)]
5183    #[test]
5184    fn config_serialization_preserves_verbatim_unc_context() {
5185        let remapping = r"\\?\UNC\server\share\project\:inner/=lib/inner/";
5186        let expected = r"\\?\UNC\server\share/project/:inner/=lib/inner/";
5187        let config = Config {
5188            remappings: vec![Remapping::from_str(remapping).unwrap().into()],
5189            ..Default::default()
5190        };
5191
5192        let serialized = toml::Value::try_from(&config).unwrap();
5193        assert_eq!(serialized["remappings"][0].as_str(), Some(expected));
5194
5195        let serialized = toml::Value::try_from(config.into_basic()).unwrap();
5196        assert_eq!(serialized["remappings"][0].as_str(), Some(expected));
5197    }
5198
5199    #[test]
5200    fn test_fs_permissions() {
5201        figment::Jail::expect_with(|jail| {
5202            jail.create_file(
5203                "foundry.toml",
5204                r#"
5205                [profile.default]
5206                fs_permissions = [{ access = "read-write", path = "./"}]
5207            "#,
5208            )?;
5209            let loaded = Config::load().unwrap();
5210
5211            assert_eq!(
5212                loaded.fs_permissions,
5213                FsPermissions::new(vec![PathPermission::read_write("./")])
5214            );
5215
5216            jail.create_file(
5217                "foundry.toml",
5218                r#"
5219                [profile.default]
5220                fs_permissions = [{ access = "none", path = "./"}]
5221            "#,
5222            )?;
5223            let loaded = Config::load().unwrap();
5224            assert_eq!(loaded.fs_permissions, FsPermissions::new(vec![PathPermission::none("./")]));
5225
5226            Ok(())
5227        });
5228    }
5229
5230    #[test]
5231    fn test_optimizer_settings_basic() {
5232        figment::Jail::expect_with(|jail| {
5233            jail.create_file(
5234                "foundry.toml",
5235                r"
5236                [profile.default]
5237                optimizer = true
5238
5239                [profile.default.optimizer_details]
5240                yul = false
5241
5242                [profile.default.optimizer_details.yulDetails]
5243                stackAllocation = true
5244            ",
5245            )?;
5246            let mut loaded = Config::load().unwrap();
5247            clear_warning(&mut loaded);
5248            assert_eq!(
5249                loaded.optimizer_details,
5250                Some(OptimizerDetails {
5251                    yul: Some(false),
5252                    yul_details: Some(YulDetails {
5253                        stack_allocation: Some(true),
5254                        ..Default::default()
5255                    }),
5256                    ..Default::default()
5257                })
5258            );
5259
5260            let s = loaded.to_string_pretty().unwrap();
5261            jail.create_file("foundry.toml", &s)?;
5262            mark_serialized_invariant_provenance(&mut loaded);
5263
5264            let mut reloaded = Config::load().unwrap();
5265            clear_warning(&mut reloaded);
5266            assert_eq!(loaded, reloaded);
5267
5268            Ok(())
5269        });
5270    }
5271
5272    #[test]
5273    fn test_model_checker_settings_basic() {
5274        figment::Jail::expect_with(|jail| {
5275            jail.create_file(
5276                "foundry.toml",
5277                r"
5278                [profile.default]
5279
5280                [profile.default.model_checker]
5281                contracts = { 'a.sol' = [ 'A1', 'A2' ], 'b.sol' = [ 'B1', 'B2' ] }
5282                engine = 'chc'
5283                targets = [ 'assert', 'outOfBounds' ]
5284                timeout = 10000
5285            ",
5286            )?;
5287            let mut loaded = Config::load().unwrap();
5288            clear_warning(&mut loaded);
5289            assert_eq!(
5290                loaded.model_checker,
5291                Some(ModelCheckerSettings {
5292                    contracts: BTreeMap::from([
5293                        ("a.sol".to_string(), vec!["A1".to_string(), "A2".to_string()]),
5294                        ("b.sol".to_string(), vec!["B1".to_string(), "B2".to_string()]),
5295                    ]),
5296                    engine: Some(ModelCheckerEngine::CHC),
5297                    targets: Some(vec![
5298                        ModelCheckerTarget::Assert,
5299                        ModelCheckerTarget::OutOfBounds
5300                    ]),
5301                    timeout: Some(10000),
5302                    invariants: None,
5303                    show_unproved: None,
5304                    div_mod_with_slacks: None,
5305                    solvers: None,
5306                    show_unsupported: None,
5307                    show_proved_safe: None,
5308                })
5309            );
5310
5311            let s = loaded.to_string_pretty().unwrap();
5312            jail.create_file("foundry.toml", &s)?;
5313            mark_serialized_invariant_provenance(&mut loaded);
5314
5315            let mut reloaded = Config::load().unwrap();
5316            clear_warning(&mut reloaded);
5317            assert_eq!(loaded, reloaded);
5318
5319            Ok(())
5320        });
5321    }
5322
5323    #[test]
5324    fn test_model_checker_settings_with_bool_flags() {
5325        figment::Jail::expect_with(|jail| {
5326            jail.create_file(
5327                "foundry.toml",
5328                r"
5329                [profile.default]
5330
5331                [profile.default.model_checker]
5332                engine = 'chc'
5333                show_unproved = true
5334                show_unsupported = true
5335                show_proved_safe = false
5336                div_mod_with_slacks = true
5337            ",
5338            )?;
5339            let mut loaded = Config::load().unwrap();
5340            clear_warning(&mut loaded);
5341
5342            let mc = loaded.model_checker.as_ref().unwrap();
5343            assert_eq!(mc.show_unproved, Some(true));
5344            assert_eq!(mc.show_unsupported, Some(true));
5345            assert_eq!(mc.show_proved_safe, Some(false));
5346            assert_eq!(mc.div_mod_with_slacks, Some(true));
5347
5348            // Test round-trip: serialize and reload
5349            let s = loaded.to_string_pretty().unwrap();
5350            jail.create_file("foundry.toml", &s)?;
5351
5352            let mut reloaded = Config::load().unwrap();
5353            clear_warning(&mut reloaded);
5354
5355            let mc_reloaded = reloaded.model_checker.as_ref().unwrap();
5356            assert_eq!(mc_reloaded.show_unproved, Some(true));
5357            assert_eq!(mc_reloaded.show_unsupported, Some(true));
5358            assert_eq!(mc_reloaded.show_proved_safe, Some(false));
5359            assert_eq!(mc_reloaded.div_mod_with_slacks, Some(true));
5360
5361            Ok(())
5362        });
5363    }
5364
5365    #[test]
5366    fn test_model_checker_settings_relative_paths() {
5367        figment::Jail::expect_with(|jail| {
5368            jail.create_file(
5369                "foundry.toml",
5370                r"
5371                [profile.default]
5372
5373                [profile.default.model_checker]
5374                contracts = { 'a.sol' = [ 'A1', 'A2' ], 'b.sol' = [ 'B1', 'B2' ] }
5375                engine = 'chc'
5376                targets = [ 'assert', 'outOfBounds' ]
5377                timeout = 10000
5378            ",
5379            )?;
5380            let loaded = Config::load().unwrap().sanitized();
5381
5382            // NOTE(onbjerg): We have to canonicalize the path here using dunce because figment will
5383            // canonicalize the jail path using the standard library. The standard library *always*
5384            // transforms Windows paths to some weird extended format, which none of our code base
5385            // does.
5386            let dir = foundry_compilers::utils::canonicalize(jail.directory())
5387                .expect("Could not canonicalize jail path");
5388            assert_eq!(
5389                loaded.model_checker,
5390                Some(ModelCheckerSettings {
5391                    contracts: BTreeMap::from([
5392                        (
5393                            format!("{}", dir.join("a.sol").display()),
5394                            vec!["A1".to_string(), "A2".to_string()]
5395                        ),
5396                        (
5397                            format!("{}", dir.join("b.sol").display()),
5398                            vec!["B1".to_string(), "B2".to_string()]
5399                        ),
5400                    ]),
5401                    engine: Some(ModelCheckerEngine::CHC),
5402                    targets: Some(vec![
5403                        ModelCheckerTarget::Assert,
5404                        ModelCheckerTarget::OutOfBounds
5405                    ]),
5406                    timeout: Some(10000),
5407                    invariants: None,
5408                    show_unproved: None,
5409                    div_mod_with_slacks: None,
5410                    solvers: None,
5411                    show_unsupported: None,
5412                    show_proved_safe: None,
5413                })
5414            );
5415
5416            Ok(())
5417        });
5418    }
5419
5420    #[test]
5421    fn test_fmt_config() {
5422        figment::Jail::expect_with(|jail| {
5423            jail.create_file(
5424                "foundry.toml",
5425                r#"
5426                [fmt]
5427                line_length = 100
5428                tab_width = 2
5429                bracket_spacing = true
5430                style = "space"
5431            "#,
5432            )?;
5433            let loaded = Config::load().unwrap().sanitized();
5434            assert_eq!(
5435                loaded.fmt,
5436                FormatterConfig {
5437                    line_length: 100,
5438                    tab_width: 2,
5439                    bracket_spacing: true,
5440                    style: IndentStyle::Space,
5441                    ..Default::default()
5442                }
5443            );
5444
5445            Ok(())
5446        });
5447    }
5448
5449    #[test]
5450    fn test_lint_config() {
5451        figment::Jail::expect_with(|jail| {
5452            jail.create_file(
5453                "foundry.toml",
5454                r"
5455                [lint]
5456                severity = ['high', 'medium']
5457                exclude_lints = ['incorrect-shift']
5458                ",
5459            )?;
5460            let loaded = Config::load().unwrap().sanitized();
5461            assert_eq!(
5462                loaded.lint,
5463                LinterConfig {
5464                    severity: vec![LintSeverity::High, LintSeverity::Med],
5465                    exclude_lints: vec!["incorrect-shift".into()],
5466                    ..Default::default()
5467                }
5468            );
5469
5470            Ok(())
5471        });
5472    }
5473
5474    #[test]
5475    fn test_invariant_config() {
5476        figment::Jail::expect_with(|jail| {
5477            jail.create_file(
5478                "foundry.toml",
5479                r#"
5480                [invariant]
5481                runs = 512
5482                depth = 10
5483                min_depth = 2
5484                depth_mode = "random"
5485                workers = 4
5486                corpus_random_sequence_weight = 30
5487                payable_value_weight = 12
5488                mutation_weight_cmp = 7
5489            "#,
5490            )?;
5491
5492            let loaded = Config::load().unwrap().sanitized();
5493            assert_eq!(
5494                loaded.invariant,
5495                InvariantConfig {
5496                    runs: 512,
5497                    depth: 10,
5498                    min_depth: 2,
5499                    depth_mode: InvariantDepthMode::Random,
5500                    workers: InvariantWorkers::Fixed(NonZeroUsize::new(4).unwrap()),
5501                    corpus: FuzzCorpusConfig {
5502                        corpus_random_sequence_weight: 30,
5503                        payable_value_weight: 12,
5504                        mutation_weights: FuzzCorpusMutationWeights {
5505                            mutation_weight_cmp: 7,
5506                            ..Default::default()
5507                        },
5508                        ..Default::default()
5509                    },
5510                    corpus_random_sequence_weight_configured: true,
5511                    workers_configured: true,
5512                    failure_persist_dir: Some(PathBuf::from("cache/invariant")),
5513                    ..Default::default()
5514                }
5515            );
5516            assert!(loaded.invariant.corpus_random_sequence_weight_configured);
5517
5518            Ok(())
5519        });
5520    }
5521
5522    #[test]
5523    fn test_invariant_corpus_random_sequence_weight_provenance() {
5524        figment::Jail::expect_with(|jail| {
5525            jail.create_file(
5526                "foundry.toml",
5527                r#"
5528                [invariant]
5529                depth = 10
5530            "#,
5531            )?;
5532
5533            let loaded = Config::load().unwrap();
5534            assert_eq!(
5535                loaded.invariant.corpus.corpus_random_sequence_weight,
5536                FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
5537            );
5538            assert!(!loaded.invariant.corpus_random_sequence_weight_configured);
5539            assert!(!loaded.invariant.workers_configured);
5540
5541            jail.create_file(
5542                "foundry.toml",
5543                r#"
5544                [invariant]
5545                corpus_random_sequence_weight = 10
5546                workers = 4
5547            "#,
5548            )?;
5549
5550            let loaded = Config::load().unwrap();
5551            assert_eq!(
5552                loaded.invariant.corpus.corpus_random_sequence_weight,
5553                FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
5554            );
5555            assert!(loaded.invariant.corpus_random_sequence_weight_configured);
5556            assert_eq!(
5557                loaded.invariant.workers,
5558                InvariantWorkers::Fixed(NonZeroUsize::new(4).unwrap())
5559            );
5560            assert!(loaded.invariant.workers_configured);
5561
5562            Ok(())
5563        });
5564    }
5565
5566    #[test]
5567    fn test_fuzz_corpus_random_sequence_weight_fallback_does_not_mark_invariant_configured() {
5568        figment::Jail::expect_with(|jail| {
5569            jail.create_file(
5570                "foundry.toml",
5571                r#"
5572                [fuzz]
5573                corpus_random_sequence_weight = 10
5574            "#,
5575            )?;
5576
5577            let loaded = Config::load().unwrap();
5578            assert_eq!(
5579                loaded.invariant.corpus.corpus_random_sequence_weight,
5580                FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
5581            );
5582            assert!(!loaded.invariant.corpus_random_sequence_weight_configured);
5583
5584            Ok(())
5585        });
5586    }
5587
5588    #[test]
5589    fn test_standalone_sections_env() {
5590        figment::Jail::expect_with(|jail| {
5591            jail.create_file(
5592                "foundry.toml",
5593                r"
5594                [fuzz]
5595                runs = 100
5596
5597                [invariant]
5598                depth = 1
5599            ",
5600            )?;
5601
5602            jail.set_env("FOUNDRY_FMT_LINE_LENGTH", "95");
5603            jail.set_env("FOUNDRY_FUZZ_DICTIONARY_WEIGHT", "99");
5604            jail.set_env("FOUNDRY_FUZZ_MAX_FUZZ_DICTIONARY_VALUES", "max");
5605            jail.set_env("FOUNDRY_INVARIANT_DEPTH", "5");
5606            jail.set_env("FOUNDRY_INVARIANT_MIN_DEPTH", "2");
5607            jail.set_env("FOUNDRY_INVARIANT_DEPTH_MODE", "random");
5608            jail.set_env("FOUNDRY_INVARIANT_WORKERS", "3");
5609            jail.set_env("FOUNDRY_INVARIANT_CORPUS_RANDOM_SEQUENCE_WEIGHT", "30");
5610            jail.set_env("FOUNDRY_INVARIANT_PAYABLE_VALUE_WEIGHT", "12");
5611            jail.set_env("FOUNDRY_INVARIANT_MUTATION_WEIGHT_CMP", "7");
5612            jail.set_env("FOUNDRY_SYMBOLIC_MAX_PATHS", "64");
5613            jail.set_env("FOUNDRY_SYMBOLIC_DUMP_SMT", "true");
5614
5615            let config = Config::load().unwrap();
5616            assert_eq!(config.fmt.line_length, 95);
5617            assert_eq!(config.fuzz.dictionary.dictionary_weight, 99);
5618            assert_eq!(config.fuzz.dictionary.max_fuzz_dictionary_values, usize::MAX);
5619            assert_eq!(config.invariant.depth, 5);
5620            assert_eq!(config.invariant.min_depth, 2);
5621            assert_eq!(config.invariant.depth_mode, InvariantDepthMode::Random);
5622            assert_eq!(
5623                config.invariant.workers,
5624                InvariantWorkers::Fixed(NonZeroUsize::new(3).unwrap())
5625            );
5626            assert_eq!(config.invariant.corpus.corpus_random_sequence_weight, 30);
5627            assert!(config.invariant.corpus_random_sequence_weight_configured);
5628            assert_eq!(config.invariant.corpus.payable_value_weight, 12);
5629            assert_eq!(config.invariant.corpus.mutation_weights.mutation_weight_cmp, 7);
5630            assert_eq!(config.symbolic.max_paths, 64);
5631            assert!(config.symbolic.dump_smt);
5632
5633            Ok(())
5634        });
5635    }
5636
5637    #[test]
5638    fn test_invariant_workers_env_accepts_auto() {
5639        figment::Jail::expect_with(|jail| {
5640            jail.create_file(
5641                "foundry.toml",
5642                r"
5643                [invariant]
5644                workers = 3
5645            ",
5646            )?;
5647
5648            jail.set_env("FOUNDRY_INVARIANT_WORKERS", "auto");
5649
5650            let config = Config::load().unwrap();
5651            assert_eq!(config.invariant.workers, InvariantWorkers::Auto);
5652
5653            Ok(())
5654        });
5655    }
5656
5657    #[test]
5658    fn test_parse_with_profile() {
5659        let foundry_str = r"
5660            [profile.default]
5661            src = 'src'
5662            out = 'out'
5663            libs = ['lib']
5664
5665            # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
5666        ";
5667        assert_eq!(
5668            parse_with_profile::<BasicConfig>(foundry_str).unwrap().unwrap(),
5669            (
5670                Config::DEFAULT_PROFILE,
5671                BasicConfig {
5672                    profile: Config::DEFAULT_PROFILE,
5673                    src: "src".into(),
5674                    out: "out".into(),
5675                    libs: vec!["lib".into()],
5676                    remappings: vec![],
5677                    network: None,
5678                }
5679            )
5680        );
5681    }
5682
5683    #[test]
5684    fn test_implicit_profile_loads() {
5685        figment::Jail::expect_with(|jail| {
5686            jail.create_file(
5687                "foundry.toml",
5688                r"
5689                [default]
5690                src = 'my-src'
5691                out = 'my-out'
5692            ",
5693            )?;
5694            let loaded = Config::load().unwrap().sanitized();
5695            assert_eq!(loaded.src.file_name().unwrap(), "my-src");
5696            assert_eq!(loaded.out.file_name().unwrap(), "my-out");
5697            assert_eq!(
5698                loaded.warnings,
5699                vec![Warning::UnknownSection {
5700                    unknown_section: Profile::new("default"),
5701                    source: Some("foundry.toml".into())
5702                }]
5703            );
5704
5705            Ok(())
5706        });
5707    }
5708
5709    #[test]
5710    fn hardfork_overrides_spec_id() {
5711        let config = Config {
5712            hardfork: Some(FoundryHardfork::Tempo(TempoHardfork::T3)),
5713            ..Config::default()
5714        };
5715
5716        assert_eq!(config.evm_spec_id::<TempoHardfork>(), TempoHardfork::T3);
5717    }
5718
5719    #[test]
5720    fn solc_settings_include_experimental_setting() {
5721        let config = Config { experimental: true, ..Config::default() };
5722        let settings = config.solc_settings().unwrap();
5723        assert_eq!(settings.settings.experimental, Some(true));
5724        assert!(settings.cli_settings.extra_args.is_empty());
5725
5726        let config = Config {
5727            experimental: false,
5728            extra_args: vec!["--experimental".to_string()],
5729            ..Config::default()
5730        };
5731        let settings = config.solc_settings().unwrap();
5732        assert_eq!(settings.settings.experimental, Some(false));
5733        assert_eq!(settings.cli_settings.extra_args, vec!["--experimental".to_string()]);
5734    }
5735
5736    #[test]
5737    fn solc_settings_include_via_ssa_cfg_setting() {
5738        let config = Config { via_ssa_cfg: true, ..Config::default() };
5739        let settings = config.solc_settings().unwrap();
5740        assert_eq!(settings.settings.via_ssa_cfg, Some(true));
5741        assert!(settings.cli_settings.extra_args.is_empty());
5742
5743        let config = Config {
5744            via_ssa_cfg: false,
5745            extra_args: vec!["--via-ssa-cfg".to_string()],
5746            ..Config::default()
5747        };
5748        let settings = config.solc_settings().unwrap();
5749        assert_eq!(settings.settings.via_ssa_cfg, Some(false));
5750        assert_eq!(settings.cli_settings.extra_args, vec!["--via-ssa-cfg".to_string()]);
5751    }
5752
5753    #[test]
5754    fn tempo_network_defaults_to_latest_tempo_hardfork() {
5755        figment::Jail::expect_with(|jail| {
5756            jail.create_file(
5757                "foundry.toml",
5758                r#"
5759                [profile.default]
5760                network = "tempo"
5761            "#,
5762            )?;
5763
5764            let config = Config::load().unwrap();
5765            assert!(config.networks.is_tempo());
5766            assert_eq!(config.evm_spec_id::<TempoHardfork>(), latest_active_tempo_hardfork());
5767
5768            Ok(())
5769        });
5770    }
5771
5772    #[test]
5773    fn tempo_hardfork_infers_tempo_network() {
5774        figment::Jail::expect_with(|jail| {
5775            jail.create_file(
5776                "foundry.toml",
5777                r#"
5778                [profile.default]
5779                hardfork = "tempo:T3"
5780            "#,
5781            )?;
5782
5783            let config = Config::load().unwrap();
5784            assert_eq!(config.hardfork, Some(FoundryHardfork::Tempo(TempoHardfork::T3)));
5785            assert!(config.networks.is_tempo());
5786
5787            Ok(())
5788        });
5789    }
5790
5791    #[test]
5792    #[cfg(feature = "monad")]
5793    fn namespaced_hardfork_infers_monad_network() {
5794        figment::Jail::expect_with(|jail| {
5795            jail.create_file(
5796                "foundry.toml",
5797                r#"
5798                [profile.default]
5799                hardfork = "monad:MonadNine"
5800            "#,
5801            )?;
5802
5803            let config = Config::load().unwrap();
5804            assert_eq!(
5805                config.hardfork,
5806                Some(FoundryHardfork::Monad(foundry_evm_hardforks::MonadHardfork::MonadNine))
5807            );
5808            assert_eq!(
5809                config.evm_spec_id::<foundry_evm_hardforks::MonadHardfork>(),
5810                foundry_evm_hardforks::MonadHardfork::MonadNine
5811            );
5812            assert_eq!(
5813                config.hardfork.as_ref().and_then(FoundryHardfork::namespace),
5814                Some("monad")
5815            );
5816            assert_eq!(
5817                config.hardfork.as_ref().map(FoundryHardfork::name).as_deref(),
5818                Some("MonadNine")
5819            );
5820            assert!(config.networks.is_monad());
5821
5822            Ok(())
5823        });
5824    }
5825
5826    #[test]
5827    #[cfg(feature = "monad")]
5828    fn network_selectors_reject_profile_merged_hybrid() {
5829        figment::Jail::expect_with(|jail| {
5830            jail.create_file(
5831                "foundry.toml",
5832                r#"
5833                [profile.default]
5834                monad = true
5835
5836                [profile.ci]
5837                celo = true
5838            "#,
5839            )?;
5840            jail.set_env("FOUNDRY_PROFILE", "ci");
5841
5842            let err = Config::load().unwrap_err().to_string();
5843            assert!(err.contains(
5844                "network selectors `celo = true` and `monad = true` conflict; select only one \
5845                 network"
5846            ));
5847
5848            Ok(())
5849        });
5850    }
5851
5852    #[test]
5853    #[cfg(feature = "monad")]
5854    fn network_selectors_reject_canonical_environment_conflict() {
5855        figment::Jail::expect_with(|jail| {
5856            jail.create_file(
5857                "foundry.toml",
5858                r#"
5859                [profile.default]
5860                tempo = true
5861            "#,
5862            )?;
5863            jail.set_env("FOUNDRY_NETWORK", "monad");
5864
5865            let err = Config::load().unwrap_err().to_string();
5866            assert!(err.contains(
5867                "network selectors `network = \"monad\"` and `tempo = true` conflict; select only \
5868                 one network"
5869            ));
5870
5871            Ok(())
5872        });
5873    }
5874
5875    #[test]
5876    #[cfg(feature = "monad")]
5877    fn matching_canonical_and_legacy_network_selectors_remain_valid() {
5878        figment::Jail::expect_with(|jail| {
5879            jail.create_file(
5880                "foundry.toml",
5881                r#"
5882                [profile.default]
5883                network = "monad"
5884                monad = true
5885            "#,
5886            )?;
5887
5888            let config = Config::load().unwrap();
5889            assert!(config.networks.is_monad());
5890            assert_eq!(
5891                config.networks.resolved_network(),
5892                Some(foundry_evm_networks::NetworkVariant::Monad)
5893            );
5894
5895            Ok(())
5896        });
5897    }
5898
5899    #[test]
5900    fn celo_network_accepts_ethereum_hardfork() {
5901        figment::Jail::expect_with(|jail| {
5902            jail.create_file(
5903                "foundry.toml",
5904                r#"
5905                [profile.default]
5906                celo = true
5907                hardfork = "prague"
5908            "#,
5909            )?;
5910
5911            let config = Config::load().unwrap();
5912            assert!(config.networks.is_celo());
5913            assert_eq!(
5914                config.hardfork,
5915                Some(FoundryHardfork::Ethereum(foundry_evm_hardforks::EthereumHardfork::Prague))
5916            );
5917
5918            Ok(())
5919        });
5920    }
5921
5922    #[test]
5923    fn hardfork_rejects_conflicting_network() {
5924        figment::Jail::expect_with(|jail| {
5925            jail.create_file(
5926                "foundry.toml",
5927                r#"
5928                [profile.default]
5929                tempo = true
5930                hardfork = "shanghai"
5931            "#,
5932            )?;
5933
5934            let err = Config::load().unwrap_err();
5935            assert!(
5936                err.to_string()
5937                    .to_lowercase()
5938                    .contains("hardfork `shanghai` conflicts with network config `tempo`")
5939            );
5940
5941            Ok(())
5942        });
5943    }
5944
5945    #[test]
5946    fn test_etherscan_api_key() {
5947        figment::Jail::expect_with(|jail| {
5948            jail.create_file(
5949                "foundry.toml",
5950                r"
5951                [default]
5952            ",
5953            )?;
5954            jail.set_env("ETHERSCAN_API_KEY", "");
5955            let loaded = Config::load().unwrap().sanitized();
5956            assert!(loaded.etherscan_api_key.is_none());
5957
5958            jail.set_env("ETHERSCAN_API_KEY", "DUMMY");
5959            let loaded = Config::load().unwrap().sanitized();
5960            assert_eq!(loaded.etherscan_api_key, Some("DUMMY".into()));
5961
5962            Ok(())
5963        });
5964    }
5965
5966    #[test]
5967    fn test_etherscan_api_key_figment() {
5968        figment::Jail::expect_with(|jail| {
5969            jail.create_file(
5970                "foundry.toml",
5971                r"
5972                [default]
5973                etherscan_api_key = 'DUMMY'
5974            ",
5975            )?;
5976            jail.set_env("ETHERSCAN_API_KEY", "ETHER");
5977
5978            let figment = Config::figment_with_root(jail.directory())
5979                .merge(("etherscan_api_key", "USER_KEY"));
5980
5981            let loaded = Config::from_provider(figment).unwrap();
5982            assert_eq!(loaded.etherscan_api_key, Some("USER_KEY".into()));
5983
5984            Ok(())
5985        });
5986    }
5987
5988    #[test]
5989    fn test_normalize_defaults() {
5990        figment::Jail::expect_with(|jail| {
5991            jail.create_file(
5992                "foundry.toml",
5993                r"
5994                [default]
5995                solc = '0.8.13'
5996            ",
5997            )?;
5998
5999            let loaded = Config::load().unwrap().sanitized();
6000            assert_eq!(loaded.evm_version, EvmVersion::London);
6001
6002            let figment = Config::figment_with_root(jail.directory()).merge(
6003                Serialized::default("evm_version", EvmVersion::Amsterdam)
6004                    .profile(Config::selected_profile()),
6005            );
6006            let loaded = Config::from_provider(figment).unwrap().sanitized();
6007            assert_eq!(loaded.evm_version, EvmVersion::Amsterdam);
6008            Ok(())
6009        });
6010    }
6011
6012    // a test to print the config, mainly used to update the example config in the README
6013    #[expect(clippy::disallowed_macros)]
6014    #[test]
6015    #[ignore]
6016    fn print_config() {
6017        let config = Config {
6018            optimizer_details: Some(OptimizerDetails {
6019                peephole: None,
6020                inliner: None,
6021                jumpdest_remover: None,
6022                order_literals: None,
6023                deduplicate: None,
6024                cse: None,
6025                constant_optimizer: Some(true),
6026                yul: Some(true),
6027                yul_details: Some(YulDetails {
6028                    stack_allocation: None,
6029                    optimizer_steps: Some("dhfoDgvulfnTUtnIf".to_string()),
6030                }),
6031                simple_counter_for_loop_unchecked_increment: None,
6032            }),
6033            ..Default::default()
6034        };
6035        println!("{}", config.to_string_pretty().unwrap());
6036    }
6037
6038    #[test]
6039    fn can_use_impl_figment_macro() {
6040        #[derive(Default, Serialize)]
6041        struct MyArgs {
6042            #[serde(skip_serializing_if = "Option::is_none")]
6043            root: Option<PathBuf>,
6044        }
6045        impl_figment_convert!(MyArgs);
6046
6047        impl Provider for MyArgs {
6048            fn metadata(&self) -> Metadata {
6049                Metadata::default()
6050            }
6051
6052            fn data(&self) -> Result<Map<Profile, Dict>, Error> {
6053                let value = Value::serialize(self)?;
6054                let error = InvalidType(value.to_actual(), "map".into());
6055                let dict = value.into_dict().ok_or(error)?;
6056                Ok(Map::from([(Config::selected_profile(), dict)]))
6057            }
6058        }
6059
6060        let _figment: Figment = From::from(&MyArgs::default());
6061
6062        #[derive(Default)]
6063        struct Outer {
6064            start: MyArgs,
6065            other: MyArgs,
6066            another: MyArgs,
6067        }
6068        impl_figment_convert!(Outer, start, other, another);
6069
6070        let _figment: Figment = From::from(&Outer::default());
6071    }
6072
6073    #[test]
6074    fn list_cached_blocks() -> eyre::Result<()> {
6075        fn fake_block_cache(chain_path: &Path, block_number: &str, size_bytes: usize) {
6076            let block_path = chain_path.join(block_number);
6077            fs::create_dir(block_path.as_path()).unwrap();
6078            let file_path = block_path.join("storage.json");
6079            let mut file = File::create(file_path).unwrap();
6080            writeln!(file, "{}", vec![' '; size_bytes - 1].iter().collect::<String>()).unwrap();
6081        }
6082
6083        fn fake_endpoint_block_cache(
6084            chain_path: &Path,
6085            block_number: &str,
6086            endpoint: &str,
6087            size_bytes: usize,
6088        ) {
6089            let block_path = chain_path.join(block_number);
6090            let file_path = block_path.join(format!("storage-{endpoint}.json"));
6091            let mut file = File::create(file_path).unwrap();
6092            writeln!(file, "{}", vec![' '; size_bytes - 1].iter().collect::<String>()).unwrap();
6093        }
6094
6095        fn fake_block_cache_block_path_as_file(
6096            chain_path: &Path,
6097            block_number: &str,
6098            size_bytes: usize,
6099        ) {
6100            let block_path = chain_path.join(block_number);
6101            let mut file = File::create(block_path).unwrap();
6102            writeln!(file, "{}", vec![' '; size_bytes - 1].iter().collect::<String>()).unwrap();
6103        }
6104
6105        let chain_dir = tempdir()?;
6106
6107        fake_block_cache(chain_dir.path(), "1", 100);
6108        fake_endpoint_block_cache(
6109            chain_dir.path(),
6110            "1",
6111            "0000000000000000000000000000000000000000000000000000000000000000",
6112            50,
6113        );
6114        fake_endpoint_block_cache(chain_dir.path(), "1", "backup", 75);
6115        fake_block_cache(chain_dir.path(), "2", 500);
6116        fake_block_cache_block_path_as_file(chain_dir.path(), "3", 900);
6117        // Pollution file that should not show up in the cached block
6118        let mut pol_file = File::create(chain_dir.path().join("pol.txt")).unwrap();
6119        writeln!(pol_file, "{}", [' '; 10].iter().collect::<String>()).unwrap();
6120
6121        let result = Config::get_cached_blocks(chain_dir.path())?;
6122
6123        assert_eq!(result.len(), 3);
6124        let block1 = &result.iter().find(|x| x.0 == "1").unwrap();
6125        let block2 = &result.iter().find(|x| x.0 == "2").unwrap();
6126        let block3 = &result.iter().find(|x| x.0 == "3").unwrap();
6127
6128        assert_eq!(block1.0, "1");
6129        assert_eq!(block1.1, 150);
6130        assert_eq!(block2.0, "2");
6131        assert_eq!(block2.1, 500);
6132        assert_eq!(block3.0, "3");
6133        assert_eq!(block3.1, 900);
6134
6135        chain_dir.close()?;
6136        Ok(())
6137    }
6138
6139    #[test]
6140    fn list_cached_blocks_ignores_removed_entries() -> eyre::Result<()> {
6141        let chain_dir = tempdir()?;
6142        let block_path = chain_dir.path().join("1");
6143        fs::create_dir(&block_path)?;
6144        File::create(block_path.join("storage.json"))?;
6145
6146        let block = fs::read_dir(chain_dir.path())?.next().unwrap()?;
6147        let cache_file = fs::read_dir(&block_path)?.next().unwrap()?;
6148        fs::remove_dir_all(block_path)?;
6149
6150        assert!(Config::get_cached_block(block)?.is_none());
6151        assert!(Config::get_cache_file_size(cache_file)?.is_none());
6152        Ok(())
6153    }
6154
6155    #[test]
6156    fn ignore_not_found_propagates_other_errors() {
6157        assert!(
6158            Config::ignore_not_found::<()>(Err(io::Error::from(io::ErrorKind::NotFound)))
6159                .unwrap()
6160                .is_none()
6161        );
6162
6163        let err =
6164            Config::ignore_not_found::<()>(Err(io::Error::from(io::ErrorKind::PermissionDenied)))
6165                .unwrap_err();
6166        assert_eq!(
6167            err.downcast_ref::<io::Error>().unwrap().kind(),
6168            io::ErrorKind::PermissionDenied
6169        );
6170    }
6171
6172    #[test]
6173    fn cache_listing_propagates_not_a_directory() -> eyre::Result<()> {
6174        let cache_file = tempfile::NamedTempFile::new()?;
6175
6176        let err = Config::get_cached_blocks(cache_file.path()).unwrap_err();
6177        assert_eq!(err.downcast_ref::<io::Error>().unwrap().kind(), io::ErrorKind::NotADirectory);
6178
6179        let err = Config::get_cached_block_explorer_data(cache_file.path()).unwrap_err();
6180        assert_eq!(err.downcast_ref::<io::Error>().unwrap().kind(), io::ErrorKind::NotADirectory);
6181        Ok(())
6182    }
6183
6184    #[test]
6185    fn list_cached_blocks_uses_replaced_entry_type() -> eyre::Result<()> {
6186        let chain_dir = tempdir()?;
6187        let block_path = chain_dir.path().join("1");
6188        File::create(&block_path)?;
6189        let block = fs::read_dir(chain_dir.path())?.next().unwrap()?;
6190
6191        fs::remove_file(&block_path)?;
6192        fs::create_dir(&block_path)?;
6193        fs::write(block_path.join("storage.json"), [0; 10])?;
6194
6195        assert_eq!(Config::get_cached_block(block)?, Some(("1".to_string(), 10)));
6196        Ok(())
6197    }
6198
6199    #[cfg(unix)]
6200    #[test]
6201    fn list_cached_blocks_ignores_symlinks() -> eyre::Result<()> {
6202        let chain_dir = tempdir()?;
6203        let target_dir = tempdir()?;
6204        fs::write(target_dir.path().join("storage.json"), [0; 10])?;
6205        std::os::unix::fs::symlink(target_dir.path(), chain_dir.path().join("1"))?;
6206
6207        let block_path = chain_dir.path().join("2");
6208        fs::create_dir(&block_path)?;
6209        let target_file = tempfile::NamedTempFile::new()?;
6210        fs::write(target_file.path(), [0; 10])?;
6211        std::os::unix::fs::symlink(target_file.path(), block_path.join("storage.json"))?;
6212
6213        assert!(Config::get_cached_blocks(chain_dir.path())?.is_empty());
6214        Ok(())
6215    }
6216
6217    #[test]
6218    fn list_etherscan_cache_ignores_removed_entries() -> eyre::Result<()> {
6219        let cache_dir = tempdir()?;
6220        let sources_path = cache_dir.path().join("sources");
6221        fs::create_dir(&sources_path)?;
6222        File::create(sources_path.join("metadata.json"))?;
6223
6224        let sources = fs::read_dir(cache_dir.path())?.next().unwrap()?;
6225        let metadata = fs::read_dir(&sources_path)?.next().unwrap()?;
6226        fs::remove_dir_all(sources_path)?;
6227
6228        assert!(Config::get_cached_entry_size(sources)?.is_none());
6229        assert!(Config::get_cached_entry_size(metadata)?.is_none());
6230        Ok(())
6231    }
6232
6233    #[test]
6234    fn list_etherscan_cache() -> eyre::Result<()> {
6235        fn fake_etherscan_cache(chain_path: &Path, address: &str, size_bytes: usize) {
6236            let metadata_path = chain_path.join("sources");
6237            let abi_path = chain_path.join("abi");
6238            let _ = fs::create_dir(metadata_path.as_path());
6239            let _ = fs::create_dir(abi_path.as_path());
6240
6241            let metadata_file_path = metadata_path.join(address);
6242            let mut metadata_file = File::create(metadata_file_path).unwrap();
6243            writeln!(metadata_file, "{}", vec![' '; size_bytes / 2 - 1].iter().collect::<String>())
6244                .unwrap();
6245
6246            let abi_file_path = abi_path.join(address);
6247            let mut abi_file = File::create(abi_file_path).unwrap();
6248            writeln!(abi_file, "{}", vec![' '; size_bytes / 2 - 1].iter().collect::<String>())
6249                .unwrap();
6250        }
6251
6252        let chain_dir = tempdir()?;
6253
6254        fake_etherscan_cache(chain_dir.path(), "1", 100);
6255        fake_etherscan_cache(chain_dir.path(), "2", 500);
6256
6257        let result = Config::get_cached_block_explorer_data(chain_dir.path())?;
6258
6259        assert_eq!(result, 600);
6260
6261        chain_dir.close()?;
6262        Ok(())
6263    }
6264
6265    #[test]
6266    fn test_parse_error_codes() {
6267        figment::Jail::expect_with(|jail| {
6268            jail.create_file(
6269                "foundry.toml",
6270                r#"
6271                [default]
6272                ignored_error_codes = ["license", "unreachable", 1337]
6273            "#,
6274            )?;
6275
6276            let config = Config::load().unwrap();
6277            assert_eq!(
6278                config.ignored_error_codes,
6279                vec![
6280                    SolidityErrorCode::SpdxLicenseNotProvided,
6281                    SolidityErrorCode::Unreachable,
6282                    SolidityErrorCode::Other(1337)
6283                ]
6284            );
6285
6286            Ok(())
6287        });
6288    }
6289
6290    #[test]
6291    fn test_parse_file_paths() {
6292        figment::Jail::expect_with(|jail| {
6293            jail.create_file(
6294                "foundry.toml",
6295                r#"
6296                [default]
6297                ignored_warnings_from = ["something"]
6298            "#,
6299            )?;
6300
6301            let config = Config::load().unwrap();
6302            assert_eq!(config.ignored_file_paths, vec![Path::new("something").to_path_buf()]);
6303
6304            Ok(())
6305        });
6306    }
6307
6308    #[test]
6309    fn test_parse_optimizer_settings() {
6310        figment::Jail::expect_with(|jail| {
6311            jail.create_file(
6312                "foundry.toml",
6313                r"
6314                [default]
6315                [profile.default.optimizer_details]
6316            ",
6317            )?;
6318
6319            let config = Config::load().unwrap();
6320            assert_eq!(config.optimizer_details, Some(OptimizerDetails::default()));
6321
6322            Ok(())
6323        });
6324    }
6325
6326    #[test]
6327    fn test_parse_labels() {
6328        figment::Jail::expect_with(|jail| {
6329            jail.create_file(
6330                "foundry.toml",
6331                r#"
6332                [labels]
6333                0x1F98431c8aD98523631AE4a59f267346ea31F984 = "Uniswap V3: Factory"
6334                0xC36442b4a4522E871399CD717aBDD847Ab11FE88 = "Uniswap V3: Positions NFT"
6335            "#,
6336            )?;
6337
6338            let config = Config::load().unwrap();
6339            assert_eq!(
6340                config.labels,
6341                AddressHashMap::from_iter(vec![
6342                    (
6343                        address!("0x1F98431c8aD98523631AE4a59f267346ea31F984"),
6344                        "Uniswap V3: Factory".to_string()
6345                    ),
6346                    (
6347                        address!("0xC36442b4a4522E871399CD717aBDD847Ab11FE88"),
6348                        "Uniswap V3: Positions NFT".to_string()
6349                    ),
6350                ])
6351            );
6352            assert_eq!(config.tracing.labels, config.labels);
6353            assert_eq!(
6354                config.warnings,
6355                vec![Warning::DeprecatedKey {
6356                    old: "[labels]".to_string(),
6357                    new: "[tracing.labels]".to_string(),
6358                }]
6359            );
6360
6361            Ok(())
6362        });
6363    }
6364
6365    #[test]
6366    fn test_parse_deprecated_profile_labels() {
6367        figment::Jail::expect_with(|jail| {
6368            jail.create_file(
6369                "foundry.toml",
6370                r#"
6371                [profile.default.labels]
6372                0x0000000000000000000000000000000000000001 = "Alice"
6373            "#,
6374            )?;
6375
6376            let config = Config::load().unwrap();
6377            let labels = AddressHashMap::from_iter(vec![(
6378                address!("0x0000000000000000000000000000000000000001"),
6379                "Alice".to_string(),
6380            )]);
6381            assert_eq!(config.labels, labels);
6382            assert_eq!(config.tracing.labels, labels);
6383            assert_eq!(
6384                config.warnings,
6385                vec![Warning::DeprecatedKey {
6386                    old: "labels".to_string(),
6387                    new: "tracing.labels".to_string(),
6388                }]
6389            );
6390
6391            Ok(())
6392        });
6393    }
6394
6395    #[test]
6396    fn test_deprecated_env_labels_use_tracing_section() {
6397        figment::Jail::expect_with(|jail| {
6398            jail.set_env(
6399                "FOUNDRY_LABELS",
6400                r#"{ "0x0000000000000000000000000000000000000001" = "Alice" }"#,
6401            );
6402            jail.set_env(
6403                "FOUNDRY_TRACING_LABELS",
6404                r#"{ "0x0000000000000000000000000000000000000001" = "Bob" }"#,
6405            );
6406
6407            let config = Config::load().unwrap();
6408            assert_eq!(
6409                config.labels,
6410                AddressHashMap::from_iter([(
6411                    address!("0x0000000000000000000000000000000000000001"),
6412                    "Alice".to_string(),
6413                )])
6414            );
6415            assert_eq!(
6416                config.tracing.labels,
6417                AddressHashMap::from_iter([(
6418                    address!("0x0000000000000000000000000000000000000001"),
6419                    "Bob".to_string(),
6420                )])
6421            );
6422
6423            Ok(())
6424        });
6425    }
6426
6427    #[test]
6428    fn test_deprecated_labels_warn_for_inactive_profiles() {
6429        figment::Jail::expect_with(|jail| {
6430            jail.create_file(
6431                "foundry.toml",
6432                r#"
6433                [profile.ci.labels]
6434                0x0000000000000000000000000000000000000001 = "Alice"
6435            "#,
6436            )?;
6437
6438            let config = Config::load().unwrap();
6439            assert_eq!(
6440                config.warnings,
6441                vec![Warning::DeprecatedKey {
6442                    old: "labels".to_string(),
6443                    new: "tracing.labels".to_string(),
6444                }]
6445            );
6446
6447            Ok(())
6448        });
6449    }
6450
6451    #[test]
6452    fn test_tracing_serialization_keeps_global_verbosity() {
6453        let address = address!("0x0000000000000000000000000000000000000001");
6454        let labels = AddressHashMap::from_iter([(address, "Alice".to_string())]);
6455        let config = Config {
6456            tracing: TracingConfig { verbosity: 4, labels: labels.clone(), ..Default::default() },
6457            ..Default::default()
6458        };
6459
6460        let serialized = toml::Value::try_from(&config).unwrap();
6461        let table = serialized.as_table().unwrap();
6462        assert_eq!(table["verbosity"].as_integer(), Some(0));
6463        assert!(!table.contains_key("labels"));
6464        assert_eq!(table["tracing"]["verbosity"].as_integer(), Some(4));
6465
6466        let provided = Figment::from(&config).extract::<Config>().unwrap();
6467        assert_eq!(provided.verbosity, 0);
6468        assert!(provided.labels.is_empty());
6469        assert_eq!(provided.tracing.labels, labels);
6470
6471        let merged = config.merge_inline_provider(("ffi", true)).unwrap();
6472        assert_eq!(merged.tracing.verbosity, 4);
6473        assert_eq!(merged.tracing.labels, config.tracing.labels);
6474    }
6475
6476    #[test]
6477    fn test_legacy_programmatic_labels_survive_serialization() {
6478        let address = address!("0x0000000000000000000000000000000000000001");
6479        let labels = AddressHashMap::from_iter([(address, "Alice".to_string())]);
6480        let config = Config { labels: labels.clone(), ..Default::default() };
6481
6482        let serialized = toml::Value::try_from(&config).unwrap();
6483        assert_eq!(
6484            serialized["labels"].as_table().unwrap().values().next().and_then(|v| v.as_str()),
6485            Some("Alice")
6486        );
6487
6488        let provided = Config::from_provider(&config).unwrap();
6489        assert_eq!(provided.labels, labels);
6490        assert_eq!(provided.tracing.labels, labels);
6491
6492        let merged = config.merge_inline_provider(("ffi", true)).unwrap();
6493        assert_eq!(merged.labels, labels);
6494        assert_eq!(merged.tracing.labels, labels);
6495    }
6496
6497    #[test]
6498    fn test_parse_tracing_section() {
6499        figment::Jail::expect_with(|jail| {
6500            jail.create_file(
6501                "foundry.toml",
6502                r#"
6503                [profile.default]
6504                verbosity = 2
6505
6506                [tracing]
6507                verbosity = 4
6508                disable_labels = true
6509                compact_labels = true
6510                trace_depth = 3
6511                decode_internal = true
6512                external_identification_timeout = 9
6513
6514                [tracing.labels]
6515                0x0000000000000000000000000000000000000002 = "Bob"
6516            "#,
6517            )?;
6518
6519            let config = Config::load().unwrap();
6520            assert_eq!(config.verbosity, 2);
6521            assert_eq!(config.tracing.verbosity, 4);
6522            assert!(config.tracing.disable_labels);
6523            assert_eq!(config.tracing.trace_depth, Some(3));
6524            assert!(config.tracing.decode_internal);
6525            assert!(config.tracing.compact_labels);
6526            assert_eq!(config.tracing.external_identification_timeout, 9);
6527            let labels = AddressHashMap::from_iter(vec![(
6528                address!("0x0000000000000000000000000000000000000002"),
6529                "Bob".to_string(),
6530            )]);
6531            assert!(config.labels.is_empty());
6532            assert_eq!(config.tracing.labels, labels);
6533            assert!(config.warnings.is_empty());
6534
6535            let serialized = config.to_string_pretty().unwrap();
6536            assert!(!serialized.contains("[labels]"));
6537            assert!(serialized.contains("[tracing.labels]"));
6538
6539            jail.create_file("foundry.toml", &serialized)?;
6540            let reloaded = Config::load().unwrap();
6541            assert!(reloaded.labels.is_empty());
6542            assert_eq!(reloaded.tracing.labels, labels);
6543            assert!(reloaded.warnings.is_empty());
6544
6545            Ok(())
6546        });
6547    }
6548
6549    #[test]
6550    fn test_external_identification_timeout_env() {
6551        figment::Jail::expect_with(|jail| {
6552            jail.set_env("FOUNDRY_TRACING_EXTERNAL_IDENTIFICATION_TIMEOUT", "0");
6553
6554            let config = Config::load().unwrap();
6555
6556            assert_eq!(config.tracing.external_identification_timeout, 0);
6557            Ok(())
6558        });
6559    }
6560
6561    #[test]
6562    fn test_global_and_tracing_verbosity_are_independent() {
6563        figment::Jail::expect_with(|jail| {
6564            jail.create_file(
6565                "foundry.toml",
6566                r#"
6567                [profile.default]
6568                verbosity = 4
6569
6570                [tracing]
6571                disable_labels = true
6572            "#,
6573            )?;
6574
6575            let config = Config::load().unwrap();
6576            assert_eq!(config.verbosity, 4);
6577            assert_eq!(config.tracing.verbosity, 0);
6578            assert!(config.tracing.disable_labels);
6579            assert_eq!(config.tracing.external_identification_timeout, 5);
6580
6581            Ok(())
6582        });
6583    }
6584
6585    #[test]
6586    fn test_label_aliases_preserve_provider_precedence() {
6587        let address = address!("0x0000000000000000000000000000000000000001");
6588        let labels = |label: &str| AddressHashMap::from_iter([(address, label.to_string())]);
6589        let provider = |global: &str, local: &str| {
6590            let figment = Config::merge_toml_provider(
6591                Figment::from(Config::default()),
6592                Toml::string(global).nested(),
6593                Config::DEFAULT_PROFILE,
6594            );
6595            Config::merge_toml_provider(
6596                figment,
6597                Toml::string(local).nested(),
6598                Config::DEFAULT_PROFILE,
6599            )
6600        };
6601
6602        let config = Config::from_provider(provider(
6603            r#"[tracing.labels]
6604            0x0000000000000000000000000000000000000001 = "global""#,
6605            r#"[labels]
6606            0x0000000000000000000000000000000000000001 = "local""#,
6607        ))
6608        .unwrap();
6609        assert_eq!(config.tracing.labels, labels("local"));
6610
6611        let config = Config::from_provider(provider(
6612            r#"[labels]
6613            0x0000000000000000000000000000000000000001 = "global""#,
6614            r#"[tracing.labels]
6615            0x0000000000000000000000000000000000000001 = "local""#,
6616        ))
6617        .unwrap();
6618        assert_eq!(config.tracing.labels, labels("local"));
6619    }
6620
6621    #[test]
6622    fn test_malformed_tracing_labels_are_not_replaced() {
6623        let provider = Toml::string(
6624            r#"
6625            [profile.default.labels]
6626            0x0000000000000000000000000000000000000001 = "legacy"
6627
6628            [profile.default.tracing]
6629            labels = "invalid"
6630            "#,
6631        )
6632        .nested();
6633
6634        assert!(Config::from_provider(provider).is_err());
6635    }
6636
6637    #[test]
6638    fn test_parse_vyper() {
6639        figment::Jail::expect_with(|jail| {
6640            jail.create_file(
6641                "foundry.toml",
6642                r#"
6643                [vyper]
6644                optimize = "O1"
6645                path = "/path/to/vyper"
6646                experimental_codegen = true
6647                venom_experimental = false
6648                debug = true
6649                enable_decimals = true
6650
6651                [vyper.venom]
6652                disable_inlining = true
6653                disable_cse = true
6654                disable_sccp = false
6655                disable_load_elimination = true
6656                disable_dead_store_elimination = false
6657                disable_algebraic_optimization = true
6658                disable_branch_optimization = false
6659                disable_assert_elimination = true
6660                disable_mem2var = false
6661                disable_simplify_cfg = true
6662                disable_remove_unused_variables = false
6663                inline_threshold = 15
6664            "#,
6665            )?;
6666
6667            let config = Config::load().unwrap();
6668            assert_eq!(
6669                config.vyper,
6670                VyperConfig {
6671                    optimize: Some(VyperOptimizationMode::O1),
6672                    opt_level: None,
6673                    path: Some("/path/to/vyper".into()),
6674                    experimental_codegen: Some(true),
6675                    venom_experimental: Some(false),
6676                    debug: Some(true),
6677                    enable_decimals: Some(true),
6678                    venom: Some(VyperVenomSettings {
6679                        disable_inlining: Some(true),
6680                        disable_cse: Some(true),
6681                        disable_sccp: Some(false),
6682                        disable_load_elimination: Some(true),
6683                        disable_dead_store_elimination: Some(false),
6684                        disable_algebraic_optimization: Some(true),
6685                        disable_branch_optimization: Some(false),
6686                        disable_assert_elimination: Some(true),
6687                        disable_mem2var: Some(false),
6688                        disable_simplify_cfg: Some(true),
6689                        disable_remove_unused_variables: Some(false),
6690                        inline_threshold: Some(15),
6691                    }),
6692                }
6693            );
6694
6695            Ok(())
6696        });
6697    }
6698
6699    #[test]
6700    fn test_vyper_settings_include_extended_config() {
6701        let config = Config {
6702            vyper: VyperConfig {
6703                opt_level: Some(VyperOptimizationLevel::O3),
6704                experimental_codegen: Some(true),
6705                venom_experimental: Some(false),
6706                debug: Some(true),
6707                enable_decimals: Some(true),
6708                venom: Some(VyperVenomSettings {
6709                    disable_cse: Some(true),
6710                    inline_threshold: Some(15),
6711                    ..Default::default()
6712                }),
6713                ..Default::default()
6714            },
6715            ..Config::default().normalized_optimizer_settings()
6716        };
6717
6718        let settings = config.vyper_settings().unwrap();
6719        assert_eq!(settings.optimize, None);
6720        assert_eq!(settings.opt_level, Some(VyperOptimizationLevel::O3));
6721        assert_eq!(settings.experimental_codegen, Some(true));
6722        assert_eq!(settings.venom_experimental, Some(false));
6723        assert_eq!(settings.debug, Some(true));
6724        assert_eq!(settings.enable_decimals, Some(true));
6725        assert_eq!(
6726            settings.venom,
6727            Some(VyperVenomSettings {
6728                disable_cse: Some(true),
6729                inline_threshold: Some(15),
6730                ..Default::default()
6731            })
6732        );
6733    }
6734
6735    #[test]
6736    fn vyper_opt_level_overrides_optimize() {
6737        let config = Config {
6738            vyper: VyperConfig {
6739                optimize: Some(VyperOptimizationMode::Gas),
6740                opt_level: Some(VyperOptimizationLevel::O3),
6741                ..Default::default()
6742            },
6743            ..Config::default().normalized_optimizer_settings()
6744        };
6745
6746        let settings = config.vyper_settings().unwrap();
6747        assert_eq!(settings.optimize, None);
6748        assert_eq!(settings.opt_level, Some(VyperOptimizationLevel::O3));
6749    }
6750
6751    #[test]
6752    fn test_parse_soldeer() {
6753        figment::Jail::expect_with(|jail| {
6754            jail.create_file(
6755                "foundry.toml",
6756                r#"
6757                [soldeer]
6758                remappings_generate = true
6759                remappings_regenerate = false
6760                remappings_version = true
6761                remappings_prefix = "@"
6762                remappings_location = "txt"
6763                recursive_deps = true
6764            "#,
6765            )?;
6766
6767            let config = Config::load().unwrap();
6768
6769            assert_eq!(
6770                config.soldeer,
6771                Some(SoldeerConfig {
6772                    remappings_generate: true,
6773                    remappings_regenerate: false,
6774                    remappings_version: true,
6775                    remappings_prefix: "@".to_string(),
6776                    remappings_location: RemappingsLocation::Txt,
6777                    recursive_deps: true,
6778                })
6779            );
6780
6781            Ok(())
6782        });
6783    }
6784
6785    // <https://github.com/foundry-rs/foundry/issues/10926>
6786    #[test]
6787    fn test_resolve_mesc_by_chain_id() {
6788        let s = r#"{
6789    "mesc_version": "0.2.1",
6790    "default_endpoint": null,
6791    "endpoints": {
6792        "sophon_50104": {
6793            "name": "sophon_50104",
6794            "url": "https://rpc.sophon.xyz",
6795            "chain_id": "50104",
6796            "endpoint_metadata": {}
6797        }
6798    },
6799    "network_defaults": {
6800    },
6801    "network_names": {},
6802    "profiles": {
6803        "foundry": {
6804            "name": "foundry",
6805            "default_endpoint": "local_ethereum",
6806            "network_defaults": {
6807                "50104": "sophon_50104"
6808            },
6809            "profile_metadata": {},
6810            "use_mesc": true
6811        }
6812    },
6813    "global_metadata": {}
6814}"#;
6815
6816        let config = serde_json::from_str(s).unwrap();
6817        let endpoint = mesc::query::get_endpoint_by_network(&config, "50104", Some("foundry"))
6818            .unwrap()
6819            .unwrap();
6820        assert_eq!(endpoint.url, "https://rpc.sophon.xyz");
6821
6822        let s = r#"{
6823    "mesc_version": "0.2.1",
6824    "default_endpoint": null,
6825    "endpoints": {
6826        "sophon_50104": {
6827            "name": "sophon_50104",
6828            "url": "https://rpc.sophon.xyz",
6829            "chain_id": "50104",
6830            "endpoint_metadata": {}
6831        }
6832    },
6833    "network_defaults": {
6834        "50104": "sophon_50104"
6835    },
6836    "network_names": {},
6837    "profiles": {},
6838    "global_metadata": {}
6839}"#;
6840
6841        let config = serde_json::from_str(s).unwrap();
6842        let endpoint = mesc::query::get_endpoint_by_network(&config, "50104", Some("foundry"))
6843            .unwrap()
6844            .unwrap();
6845        assert_eq!(endpoint.url, "https://rpc.sophon.xyz");
6846    }
6847
6848    #[test]
6849    fn test_get_etherscan_config_with_unknown_chain() {
6850        figment::Jail::expect_with(|jail| {
6851            jail.create_file(
6852                "foundry.toml",
6853                r#"
6854                [etherscan]
6855                mainnet = { chain = 3658348, key = "api-key"}
6856            "#,
6857            )?;
6858            let config = Config::load().unwrap();
6859            let unknown_chain = Chain::from_id(3658348);
6860            let result = config.get_etherscan_config_with_chain(Some(unknown_chain));
6861            assert!(result.is_err());
6862            let error_msg = result.unwrap_err().to_string();
6863            assert!(error_msg.contains("No known Etherscan API URL for chain `3658348`"));
6864            assert!(error_msg.contains("Specify a `url`"));
6865            assert!(error_msg.contains("Verify the chain `3658348` is correct"));
6866
6867            Ok(())
6868        });
6869    }
6870
6871    #[test]
6872    fn test_get_etherscan_config_with_existing_chain_and_url() {
6873        figment::Jail::expect_with(|jail| {
6874            jail.create_file(
6875                "foundry.toml",
6876                r#"
6877                [etherscan]
6878                mainnet = { chain = 1, key = "api-key" }
6879            "#,
6880            )?;
6881            let config = Config::load().unwrap();
6882            let unknown_chain = Chain::from_id(1);
6883            let result = config.get_etherscan_config_with_chain(Some(unknown_chain));
6884            assert!(result.is_ok());
6885            Ok(())
6886        });
6887    }
6888
6889    #[test]
6890    fn test_can_inherit_a_base_toml() {
6891        figment::Jail::expect_with(|jail| {
6892            // Create base config file with optimizer_runs = 800
6893            jail.create_file(
6894                "base-config.toml",
6895                r#"
6896                    [profile.default]
6897                    optimizer_runs = 800
6898
6899                    [invariant]
6900                    runs = 1000
6901
6902                    [rpc_endpoints]
6903                    mainnet = "https://example.com"
6904                    optimism = "https://example-2.com/"
6905                    "#,
6906            )?;
6907
6908            // Create local config that inherits from base-config.toml
6909            jail.create_file(
6910                "foundry.toml",
6911                r#"
6912                    [profile.default]
6913                    extends = "base-config.toml"
6914
6915                    [invariant]
6916                    runs = 333
6917                    depth = 15
6918
6919                    [rpc_endpoints]
6920                    mainnet = "https://test.xyz/rpc"
6921                    "#,
6922            )?;
6923
6924            let config = Config::load().unwrap();
6925            assert_eq!(config.extends, Some(Extends::Path("base-config.toml".to_string())));
6926
6927            // optimizer_runs should be inherited from base-config.toml
6928            assert_eq!(config.optimizer_runs, Some(800));
6929
6930            // invariant settings should be overridden by local config
6931            assert_eq!(config.invariant.runs, 333);
6932            assert_eq!(config.invariant.depth, 15);
6933
6934            // rpc_endpoints.mainnet should be overridden by local config
6935            // optimism should be inherited from base config
6936            let endpoints = config.rpc_endpoints.resolved();
6937            assert!(
6938                endpoints.get("mainnet").unwrap().url().unwrap().contains("https://test.xyz/rpc")
6939            );
6940            assert!(endpoints.get("optimism").unwrap().url().unwrap().contains("example-2.com"));
6941
6942            Ok(())
6943        });
6944    }
6945
6946    #[test]
6947    fn inherited_symbolic_sections_preserve_source_precedence() {
6948        figment::Jail::expect_with(|jail| {
6949            jail.create_file(
6950                "base.toml",
6951                r#"
6952                    [profile.default.symbolic]
6953                    max_paths = 10
6954                    depth = 100
6955                "#,
6956            )?;
6957            jail.create_file(
6958                "foundry.toml",
6959                r#"
6960                    [profile.default]
6961                    extends = "base.toml"
6962
6963                    [symbolic]
6964                    max_paths = 20
6965                "#,
6966            )?;
6967
6968            let config = Config::load().unwrap();
6969            assert_eq!(config.symbolic.max_paths, 20);
6970            assert_eq!(config.symbolic.depth, Some(100));
6971
6972            jail.create_file(
6973                "base.toml",
6974                r#"
6975                    [symbolic]
6976                    max_paths = 30
6977                    depth = 200
6978                "#,
6979            )?;
6980            jail.create_file(
6981                "foundry.toml",
6982                r#"
6983                    [profile.default]
6984                    extends = "base.toml"
6985
6986                    [profile.default.symbolic]
6987                    max_paths = 40
6988                "#,
6989            )?;
6990
6991            let config = Config::load().unwrap();
6992            assert_eq!(config.symbolic.max_paths, 40);
6993            assert_eq!(config.symbolic.depth, Some(200));
6994
6995            Ok(())
6996        });
6997    }
6998
6999    #[test]
7000    fn inherited_symbolic_sections_detect_effective_collisions() {
7001        figment::Jail::expect_with(|jail| {
7002            jail.create_file(
7003                "base.toml",
7004                r#"
7005                    [profile.default.symbolic]
7006                    max_paths = 10
7007                "#,
7008            )?;
7009            jail.create_file(
7010                "foundry.toml",
7011                r#"
7012                    [profile.default]
7013                    extends = { path = "base.toml", strategy = "no-collision" }
7014
7015                    [symbolic]
7016                    max_paths = 20
7017                "#,
7018            )?;
7019
7020            let err = Config::load().unwrap_err().to_string();
7021            assert!(err.contains("Key collision detected"), "unexpected error: {err}");
7022            assert!(err.contains("symbolic"), "unexpected error: {err}");
7023
7024            Ok(())
7025        });
7026    }
7027
7028    #[test]
7029    fn inherited_label_aliases_preserve_source_precedence() {
7030        figment::Jail::expect_with(|jail| {
7031            let address = address!("0x0000000000000000000000000000000000000001");
7032
7033            jail.create_file(
7034                "base.toml",
7035                r#"
7036                    [profile.default.tracing.labels]
7037                    0x0000000000000000000000000000000000000001 = "base"
7038                "#,
7039            )?;
7040            jail.create_file(
7041                "foundry.toml",
7042                r#"
7043                    [profile.default]
7044                    extends = "base.toml"
7045
7046                    [profile.default.labels]
7047                    0x0000000000000000000000000000000000000001 = "local"
7048                "#,
7049            )?;
7050
7051            let config = Config::load().unwrap();
7052            assert_eq!(config.tracing.labels.get(&address).map(String::as_str), Some("local"));
7053
7054            jail.create_file(
7055                "base.toml",
7056                r#"
7057                    [profile.default.labels]
7058                    0x0000000000000000000000000000000000000001 = "base"
7059                "#,
7060            )?;
7061            jail.create_file(
7062                "foundry.toml",
7063                r#"
7064                    [profile.default]
7065                    extends = "base.toml"
7066
7067                    [profile.default.tracing.labels]
7068                    0x0000000000000000000000000000000000000001 = "local"
7069                "#,
7070            )?;
7071
7072            let config = Config::load().unwrap();
7073            assert_eq!(config.tracing.labels.get(&address).map(String::as_str), Some("local"));
7074
7075            jail.create_file(
7076                "base.toml",
7077                r#"
7078                    [profile.default.tracing.labels]
7079                    0x0000000000000000000000000000000000000001 = "base"
7080                "#,
7081            )?;
7082            jail.create_file(
7083                "foundry.toml",
7084                r#"
7085                    [profile.default]
7086                    extends = "base.toml"
7087
7088                    [labels]
7089                    0x0000000000000000000000000000000000000001 = "local"
7090                "#,
7091            )?;
7092
7093            let config = Config::load().unwrap();
7094            assert_eq!(config.tracing.labels.get(&address).map(String::as_str), Some("local"));
7095            assert_eq!(
7096                config.warnings,
7097                vec![Warning::DeprecatedKey {
7098                    old: "[labels]".to_string(),
7099                    new: "[tracing.labels]".to_string(),
7100                }]
7101            );
7102
7103            jail.create_file(
7104                "base.toml",
7105                r#"
7106                    [labels]
7107                    0x0000000000000000000000000000000000000001 = "base"
7108                "#,
7109            )?;
7110            jail.create_file(
7111                "foundry.toml",
7112                r#"
7113                    [profile.default]
7114                    extends = "base.toml"
7115
7116                    [profile.default.tracing.labels]
7117                    0x0000000000000000000000000000000000000001 = "local"
7118                "#,
7119            )?;
7120
7121            let config = Config::load().unwrap();
7122            assert_eq!(config.tracing.labels.get(&address).map(String::as_str), Some("local"));
7123
7124            Ok(())
7125        });
7126    }
7127
7128    #[test]
7129    fn inherited_label_aliases_detect_effective_collisions() {
7130        figment::Jail::expect_with(|jail| {
7131            jail.create_file(
7132                "base.toml",
7133                r#"
7134                    [profile.default.tracing.labels]
7135                    0x0000000000000000000000000000000000000001 = "base"
7136                "#,
7137            )?;
7138            jail.create_file(
7139                "foundry.toml",
7140                r#"
7141                    [profile.default]
7142                    extends = { path = "base.toml", strategy = "no-collision" }
7143
7144                    [labels]
7145                    0x0000000000000000000000000000000000000001 = "local"
7146                "#,
7147            )?;
7148
7149            let err = Config::load().unwrap_err().to_string();
7150            assert_eq!(
7151                err,
7152                "failed to extract foundry config:\n\
7153                 foundry config error: Key collision detected in profile 'default' when extending \
7154                 'base.toml'. Conflicting keys: [\"tracing\"]. Use 'extends.strategy' or \
7155                 'extends_strategy' to specify how to handle conflicts.\n"
7156            );
7157
7158            Ok(())
7159        });
7160    }
7161
7162    #[test]
7163    fn inherited_fuzz_section_remains_invariant_fallback() {
7164        figment::Jail::expect_with(|jail| {
7165            jail.create_file(
7166                "base.toml",
7167                r#"
7168                    [fuzz]
7169                    include_storage = false
7170                    dictionary_weight = 99
7171                "#,
7172            )?;
7173            jail.create_file(
7174                "foundry.toml",
7175                r#"
7176                    [profile.default]
7177                    extends = "base.toml"
7178
7179                    [invariant]
7180                    runs = 420
7181                "#,
7182            )?;
7183
7184            let config = Config::load().unwrap();
7185            assert_eq!(config.invariant.runs, 420);
7186            assert!(!config.invariant.dictionary.include_storage);
7187            assert_eq!(config.invariant.dictionary.dictionary_weight, 99);
7188
7189            Ok(())
7190        });
7191    }
7192
7193    #[test]
7194    fn test_inheritance_validation() {
7195        figment::Jail::expect_with(|jail| {
7196            // Test 1: Base file with 'extends' should fail
7197            jail.create_file(
7198                "base-with-inherit.toml",
7199                r#"
7200                    [profile.default]
7201                    extends = "another.toml"
7202                    optimizer_runs = 800
7203                    "#,
7204            )?;
7205
7206            jail.create_file(
7207                "foundry.toml",
7208                r#"
7209                    [profile.default]
7210                    extends = "base-with-inherit.toml"
7211                    "#,
7212            )?;
7213
7214            // Should fail because base file has 'extends'
7215            let result = Config::load();
7216            assert!(result.is_err());
7217            assert!(result.unwrap_err().to_string().contains("Nested inheritance is not allowed"));
7218
7219            // Test 2: Circular reference should fail
7220            jail.create_file(
7221                "foundry.toml",
7222                r#"
7223                    [profile.default]
7224                    extends = "foundry.toml"
7225                    "#,
7226            )?;
7227
7228            let result = Config::load();
7229            assert!(result.is_err());
7230            assert!(result.unwrap_err().to_string().contains("cannot inherit from itself"));
7231
7232            // Test 3: Non-existent base file should fail
7233            jail.create_file(
7234                "foundry.toml",
7235                r#"
7236                    [profile.default]
7237                    extends = "non-existent.toml"
7238                    "#,
7239            )?;
7240
7241            let result = Config::load();
7242            assert!(result.is_err());
7243            let err_msg = result.unwrap_err().to_string();
7244            assert!(
7245                err_msg.contains("does not exist")
7246                    || err_msg.contains("Failed to resolve inherited config path"),
7247                "Error message: {err_msg}"
7248            );
7249
7250            Ok(())
7251        });
7252    }
7253
7254    #[test]
7255    fn test_complex_inheritance_merging() {
7256        figment::Jail::expect_with(|jail| {
7257            // Create a comprehensive base config
7258            jail.create_file(
7259                "base.toml",
7260                r#"
7261                    [profile.default]
7262                    optimizer = true
7263                    optimizer_runs = 1000
7264                    via_ir = false
7265                    solc = "0.8.19"
7266
7267                    [invariant]
7268                    runs = 500
7269                    depth = 100
7270
7271                    [fuzz]
7272                    runs = 256
7273                    seed = "0x123"
7274
7275                    [rpc_endpoints]
7276                    mainnet = "https://base-mainnet.com"
7277                    optimism = "https://base-optimism.com"
7278                    arbitrum = "https://base-arbitrum.com"
7279                    "#,
7280            )?;
7281
7282            // Create local config that overrides some values
7283            jail.create_file(
7284                "foundry.toml",
7285                r#"
7286                    [profile.default]
7287                    extends = "base.toml"
7288                    optimizer_runs = 200  # Override
7289                    via_ir = true        # Override
7290                    # optimizer and solc are inherited
7291
7292                    [invariant]
7293                    runs = 333  # Override
7294                    # depth is inherited
7295
7296                    # fuzz section is fully inherited
7297
7298                    [rpc_endpoints]
7299                    mainnet = "https://local-mainnet.com"  # Override
7300                    # optimism and arbitrum are inherited
7301                    polygon = "https://local-polygon.com"  # New
7302                    "#,
7303            )?;
7304
7305            let config = Config::load().unwrap();
7306
7307            // Check profile.default values
7308            assert_eq!(config.optimizer, Some(true));
7309            assert_eq!(config.optimizer_runs, Some(200));
7310            assert_eq!(config.via_ir, true);
7311            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 19))));
7312
7313            // Check invariant section
7314            assert_eq!(config.invariant.runs, 333);
7315            assert_eq!(config.invariant.depth, 100);
7316
7317            // Check fuzz section (fully inherited)
7318            assert_eq!(config.fuzz.runs, 256);
7319            assert_eq!(config.fuzz.seed, Some(U256::from(0x123)));
7320
7321            // Check rpc_endpoints
7322            let endpoints = config.rpc_endpoints.resolved();
7323            assert!(endpoints.get("mainnet").unwrap().url().unwrap().contains("local-mainnet"));
7324            assert!(endpoints.get("optimism").unwrap().url().unwrap().contains("base-optimism"));
7325            assert!(endpoints.get("arbitrum").unwrap().url().unwrap().contains("base-arbitrum"));
7326            assert!(endpoints.get("polygon").unwrap().url().unwrap().contains("local-polygon"));
7327
7328            Ok(())
7329        });
7330    }
7331
7332    #[test]
7333    fn test_inheritance_with_different_profiles() {
7334        figment::Jail::expect_with(|jail| {
7335            // Create base config with multiple profiles
7336            jail.create_file(
7337                "base.toml",
7338                r#"
7339                    [profile.default]
7340                    optimizer = true
7341                    optimizer_runs = 200
7342
7343                    [profile.ci]
7344                    optimizer = true
7345                    optimizer_runs = 10000
7346                    via_ir = true
7347
7348                    [profile.dev]
7349                    optimizer = false
7350                    "#,
7351            )?;
7352
7353            // Local config inherits from base - only for default profile
7354            jail.create_file(
7355                "foundry.toml",
7356                r#"
7357                    [profile.default]
7358                    extends = "base.toml"
7359                    verbosity = 3
7360
7361                    [profile.ci]
7362                    optimizer_runs = 5000  # This doesn't inherit from base.toml's ci profile
7363                    "#,
7364            )?;
7365
7366            // Test default profile
7367            let config = Config::load().unwrap();
7368            assert_eq!(config.optimizer, Some(true));
7369            assert_eq!(config.optimizer_runs, Some(200));
7370            assert_eq!(config.verbosity, 3);
7371
7372            // Test CI profile (NO 'extends', so doesn't inherit from base)
7373            jail.set_env("FOUNDRY_PROFILE", "ci");
7374            let config = Config::load().unwrap();
7375            assert_eq!(config.optimizer_runs, Some(5000));
7376            assert_eq!(config.optimizer, Some(true));
7377            // via_ir is not set in local ci profile and there's no 'extends', so default
7378            assert_eq!(config.via_ir, false);
7379
7380            Ok(())
7381        });
7382    }
7383
7384    #[test]
7385    fn test_inheritance_with_env_vars() {
7386        figment::Jail::expect_with(|jail| {
7387            jail.create_file(
7388                "base.toml",
7389                r#"
7390                    [profile.default]
7391                    optimizer_runs = 500
7392                    sender = "0x0000000000000000000000000000000000000001"
7393                    verbosity = 1
7394                    "#,
7395            )?;
7396
7397            jail.create_file(
7398                "foundry.toml",
7399                r#"
7400                    [profile.default]
7401                    extends = "base.toml"
7402                    verbosity = 2
7403                    "#,
7404            )?;
7405
7406            // Environment variables should override both base and local values
7407            jail.set_env("FOUNDRY_OPTIMIZER_RUNS", "999");
7408            jail.set_env("FOUNDRY_VERBOSITY", "4");
7409
7410            let config = Config::load().unwrap();
7411            assert_eq!(config.optimizer_runs, Some(999));
7412            assert_eq!(config.verbosity, 4);
7413            assert_eq!(
7414                config.sender,
7415                "0x0000000000000000000000000000000000000001"
7416                    .parse::<alloy_primitives::Address>()
7417                    .unwrap()
7418            );
7419
7420            Ok(())
7421        });
7422    }
7423
7424    #[test]
7425    fn test_inheritance_with_subdirectories() {
7426        figment::Jail::expect_with(|jail| {
7427            // Create base config in a subdirectory
7428            jail.create_dir("configs")?;
7429            jail.create_file(
7430                "configs/base.toml",
7431                r#"
7432                    [profile.default]
7433                    optimizer_runs = 800
7434                    src = "contracts"
7435                    "#,
7436            )?;
7437
7438            // Reference it with relative path
7439            jail.create_file(
7440                "foundry.toml",
7441                r#"
7442                    [profile.default]
7443                    extends = "configs/base.toml"
7444                    test = "tests"
7445                    "#,
7446            )?;
7447
7448            let config = Config::load().unwrap();
7449            assert_eq!(config.optimizer_runs, Some(800));
7450            assert_eq!(config.src, PathBuf::from("contracts"));
7451            assert_eq!(config.test, PathBuf::from("tests"));
7452
7453            // Test with parent directory reference
7454            jail.create_dir("project")?;
7455            jail.create_file(
7456                "shared-base.toml",
7457                r#"
7458                    [profile.default]
7459                    optimizer_runs = 1500
7460                    "#,
7461            )?;
7462
7463            jail.create_file(
7464                "project/foundry.toml",
7465                r#"
7466                    [profile.default]
7467                    extends = "../shared-base.toml"
7468                    "#,
7469            )?;
7470
7471            std::env::set_current_dir(jail.directory().join("project")).unwrap();
7472            let config = Config::load().unwrap();
7473            assert_eq!(config.optimizer_runs, Some(1500));
7474
7475            Ok(())
7476        });
7477    }
7478
7479    #[test]
7480    fn test_inheritance_with_empty_files() {
7481        figment::Jail::expect_with(|jail| {
7482            // Empty base file
7483            jail.create_file(
7484                "base.toml",
7485                r#"
7486                    [profile.default]
7487                    "#,
7488            )?;
7489
7490            jail.create_file(
7491                "foundry.toml",
7492                r#"
7493                    [profile.default]
7494                    extends = "base.toml"
7495                    optimizer_runs = 300
7496                    "#,
7497            )?;
7498
7499            let config = Config::load().unwrap();
7500            assert_eq!(config.optimizer_runs, Some(300));
7501
7502            // Empty local file (only 'extends')
7503            jail.create_file(
7504                "base2.toml",
7505                r#"
7506                    [profile.default]
7507                    optimizer_runs = 400
7508                    via_ir = true
7509                    "#,
7510            )?;
7511
7512            jail.create_file(
7513                "foundry.toml",
7514                r#"
7515                    [profile.default]
7516                    extends = "base2.toml"
7517                    "#,
7518            )?;
7519
7520            let config = Config::load().unwrap();
7521            assert_eq!(config.optimizer_runs, Some(400));
7522            assert!(config.via_ir);
7523
7524            Ok(())
7525        });
7526    }
7527
7528    #[test]
7529    fn test_inheritance_array_and_table_merging() {
7530        figment::Jail::expect_with(|jail| {
7531            jail.create_file(
7532                "base.toml",
7533                r#"
7534                    [profile.default]
7535                    libs = ["lib", "node_modules"]
7536                    ignored_error_codes = [5667, 1878]
7537                    extra_output = ["metadata", "ir"]
7538
7539                    [profile.default.model_checker]
7540                    engine = "chc"
7541                    timeout = 10000
7542                    targets = ["assert"]
7543
7544                    [profile.default.optimizer_details]
7545                    peephole = true
7546                    inliner = true
7547                    "#,
7548            )?;
7549
7550            jail.create_file(
7551                "foundry.toml",
7552                r#"
7553                    [profile.default]
7554                    extends = "base.toml"
7555                    libs = ["custom-lib"]  # Concatenates with base array
7556                    ignored_error_codes = [2018]  # Concatenates with base array
7557
7558                    [profile.default.model_checker]
7559                    timeout = 5000  # Overrides base value
7560                    # engine and targets are inherited
7561
7562                    [profile.default.optimizer_details]
7563                    jumpdest_remover = true  # Adds new field
7564                    # peephole and inliner are inherited
7565                    "#,
7566            )?;
7567
7568            let config = Config::load().unwrap();
7569
7570            // Arrays are now concatenated with admerge (base + local)
7571            assert_eq!(
7572                config.libs,
7573                vec![
7574                    PathBuf::from("lib"),
7575                    PathBuf::from("node_modules"),
7576                    PathBuf::from("custom-lib")
7577                ]
7578            );
7579            assert_eq!(
7580                config.ignored_error_codes,
7581                vec![
7582                    SolidityErrorCode::UnusedFunctionParameter, // 5667 from base.toml
7583                    SolidityErrorCode::SpdxLicenseNotProvided,  // 1878 from base.toml
7584                    SolidityErrorCode::FunctionStateMutabilityCanBeRestricted  // 2018 from local
7585                ]
7586            );
7587
7588            // Tables are deep-merged
7589            assert_eq!(config.model_checker.as_ref().unwrap().timeout, Some(5000));
7590            assert_eq!(
7591                config.model_checker.as_ref().unwrap().engine,
7592                Some(ModelCheckerEngine::CHC)
7593            );
7594            assert_eq!(
7595                config.model_checker.as_ref().unwrap().targets,
7596                Some(vec![ModelCheckerTarget::Assert])
7597            );
7598
7599            // optimizer_details table is actually merged, not replaced
7600            assert_eq!(config.optimizer_details.as_ref().unwrap().peephole, Some(true));
7601            assert_eq!(config.optimizer_details.as_ref().unwrap().inliner, Some(true));
7602            assert_eq!(config.optimizer_details.as_ref().unwrap().jumpdest_remover, None);
7603
7604            Ok(())
7605        });
7606    }
7607
7608    #[test]
7609    fn test_inheritance_with_special_sections() {
7610        figment::Jail::expect_with(|jail| {
7611            jail.create_file(
7612                "base.toml",
7613                r#"
7614                    [profile.default]
7615                    # Base file should not have 'extends' to avoid nested inheritance
7616
7617                    [labels]
7618                    "0x0000000000000000000000000000000000000001" = "Alice"
7619                    "0x0000000000000000000000000000000000000002" = "Bob"
7620
7621                    [[profile.default.fs_permissions]]
7622                    access = "read"
7623                    path = "./src"
7624
7625                    [[profile.default.fs_permissions]]
7626                    access = "read-write"
7627                    path = "./cache"
7628                    "#,
7629            )?;
7630
7631            jail.create_file(
7632                "foundry.toml",
7633                r#"
7634                    [profile.default]
7635                    extends = "base.toml"
7636
7637                    [labels]
7638                    "0x0000000000000000000000000000000000000002" = "Bob Updated"
7639                    "0x0000000000000000000000000000000000000003" = "Charlie"
7640
7641                    [[profile.default.fs_permissions]]
7642                    access = "read"
7643                    path = "./test"
7644                    "#,
7645            )?;
7646
7647            let config = Config::load().unwrap();
7648
7649            // Labels should be merged
7650            assert_eq!(
7651                config.labels.get(
7652                    &"0x0000000000000000000000000000000000000001"
7653                        .parse::<alloy_primitives::Address>()
7654                        .unwrap()
7655                ),
7656                Some(&"Alice".to_string())
7657            );
7658            assert_eq!(
7659                config.labels.get(
7660                    &"0x0000000000000000000000000000000000000002"
7661                        .parse::<alloy_primitives::Address>()
7662                        .unwrap()
7663                ),
7664                Some(&"Bob Updated".to_string())
7665            );
7666            assert_eq!(
7667                config.labels.get(
7668                    &"0x0000000000000000000000000000000000000003"
7669                        .parse::<alloy_primitives::Address>()
7670                        .unwrap()
7671                ),
7672                Some(&"Charlie".to_string())
7673            );
7674
7675            // fs_permissions array is now concatenated with addmerge (base + local)
7676            assert_eq!(config.fs_permissions.permissions.len(), 3); // 2 from base + 1 from local
7677            // Check that all permissions are present
7678            assert!(
7679                config
7680                    .fs_permissions
7681                    .permissions
7682                    .iter()
7683                    .any(|p| p.path.to_str().unwrap() == "./src")
7684            );
7685            assert!(
7686                config
7687                    .fs_permissions
7688                    .permissions
7689                    .iter()
7690                    .any(|p| p.path.to_str().unwrap() == "./cache")
7691            );
7692            assert!(
7693                config
7694                    .fs_permissions
7695                    .permissions
7696                    .iter()
7697                    .any(|p| p.path.to_str().unwrap() == "./test")
7698            );
7699
7700            Ok(())
7701        });
7702    }
7703
7704    #[test]
7705    fn test_inheritance_with_compilation_settings() {
7706        figment::Jail::expect_with(|jail| {
7707            jail.create_file(
7708                "base.toml",
7709                r#"
7710                    [profile.default]
7711                    solc = "0.8.19"
7712                    evm_version = "paris"
7713                    via_ir = false
7714                    optimizer = true
7715                    optimizer_runs = 200
7716
7717                    [profile.default.optimizer_details]
7718                    peephole = true
7719                    inliner = false
7720                    jumpdest_remover = true
7721                    order_literals = false
7722                    deduplicate = true
7723                    cse = true
7724                    constant_optimizer = true
7725                    yul = true
7726
7727                    [profile.default.optimizer_details.yul_details]
7728                    stack_allocation = true
7729                    optimizer_steps = "dhfoDgvulfnTUtnIf"
7730                    "#,
7731            )?;
7732
7733            jail.create_file(
7734                "foundry.toml",
7735                r#"
7736                    [profile.default]
7737                    extends = "base.toml"
7738                    evm_version = "shanghai"  # Override
7739                    optimizer_runs = 1000  # Override
7740
7741                    [profile.default.optimizer_details]
7742                    inliner = true  # Override
7743                    # Rest inherited
7744                    "#,
7745            )?;
7746
7747            let config = Config::load().unwrap();
7748
7749            // Check compilation settings
7750            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 19))));
7751            assert_eq!(config.evm_version, EvmVersion::Shanghai);
7752            assert_eq!(config.via_ir, false);
7753            assert_eq!(config.optimizer, Some(true));
7754            assert_eq!(config.optimizer_runs, Some(1000));
7755
7756            // Check optimizer details - the table is actually merged
7757            let details = config.optimizer_details.as_ref().unwrap();
7758            assert_eq!(details.peephole, Some(true));
7759            assert_eq!(details.inliner, Some(true));
7760            assert_eq!(details.jumpdest_remover, None);
7761            assert_eq!(details.order_literals, None);
7762            assert_eq!(details.deduplicate, Some(true));
7763            assert_eq!(details.cse, Some(true));
7764            assert_eq!(details.constant_optimizer, None);
7765            assert_eq!(details.yul, Some(true));
7766
7767            // Check yul details - inherited from base
7768            if let Some(yul_details) = details.yul_details.as_ref() {
7769                assert_eq!(yul_details.stack_allocation, Some(true));
7770                assert_eq!(yul_details.optimizer_steps, Some("dhfoDgvulfnTUtnIf".to_string()));
7771            }
7772
7773            Ok(())
7774        });
7775    }
7776
7777    #[test]
7778    fn test_inheritance_with_remappings() {
7779        figment::Jail::expect_with(|jail| {
7780            jail.create_file(
7781                "base.toml",
7782                r#"
7783                    [profile.default]
7784                    remappings = [
7785                        "forge-std/=lib/forge-std/src/",
7786                        "@openzeppelin/=lib/openzeppelin-contracts/",
7787                        "ds-test/=lib/ds-test/src/"
7788                    ]
7789                    auto_detect_remappings = false
7790                    "#,
7791            )?;
7792
7793            jail.create_file(
7794                "foundry.toml",
7795                r#"
7796                    [profile.default]
7797                    extends = "base.toml"
7798                    remappings = [
7799                        "@custom/=lib/custom/",
7800                        "ds-test/=lib/forge-std/lib/ds-test/src/"  # Note: This will be added alongside base remappings
7801                    ]
7802                    "#,
7803            )?;
7804
7805            let config = Config::load().unwrap();
7806
7807            // Remappings array is now concatenated with admerge (base + local)
7808            assert!(config.remappings.iter().any(|r| r.to_string().contains("@custom/")));
7809            assert!(config.remappings.iter().any(|r| r.to_string().contains("ds-test/")));
7810            assert!(config.remappings.iter().any(|r| r.to_string().contains("forge-std/")));
7811            assert!(config.remappings.iter().any(|r| r.to_string().contains("@openzeppelin/")));
7812
7813            // auto_detect_remappings should be inherited
7814            assert!(!config.auto_detect_remappings);
7815
7816            Ok(())
7817        });
7818    }
7819
7820    #[test]
7821    fn test_inheritance_with_multiple_profiles_and_single_file() {
7822        figment::Jail::expect_with(|jail| {
7823            // Create base config with prod and test profiles
7824            jail.create_file(
7825                "base.toml",
7826                r#"
7827                    [profile.prod]
7828                    optimizer = true
7829                    optimizer_runs = 10000
7830                    via_ir = true
7831
7832                    [profile.test]
7833                    optimizer = false
7834
7835                    [profile.test.fuzz]
7836                    runs = 100
7837                    "#,
7838            )?;
7839
7840            // Local config inherits from base for prod profile
7841            jail.create_file(
7842                "foundry.toml",
7843                r#"
7844                    [profile.prod]
7845                    extends = "base.toml"
7846                    evm_version = "shanghai"  # Additional setting
7847
7848                    [profile.test]
7849                    extends = "base.toml"
7850
7851                    [profile.test.fuzz]
7852                    runs = 500  # Override
7853                    "#,
7854            )?;
7855
7856            // Test prod profile
7857            jail.set_env("FOUNDRY_PROFILE", "prod");
7858            let config = Config::load().unwrap();
7859            assert_eq!(config.optimizer, Some(true));
7860            assert_eq!(config.optimizer_runs, Some(10000));
7861            assert_eq!(config.via_ir, true);
7862            assert_eq!(config.evm_version, EvmVersion::Shanghai);
7863
7864            // Test test profile
7865            jail.set_env("FOUNDRY_PROFILE", "test");
7866            let config = Config::load().unwrap();
7867            assert_eq!(config.optimizer, Some(false));
7868            assert_eq!(config.fuzz.runs, 500);
7869
7870            Ok(())
7871        });
7872    }
7873
7874    #[test]
7875    fn test_inheritance_with_multiple_profiles_and_files() {
7876        figment::Jail::expect_with(|jail| {
7877            jail.create_file(
7878                "prod.toml",
7879                r#"
7880                    [profile.prod]
7881                    optimizer = true
7882                    optimizer_runs = 20000
7883                    gas_limit = 50000000
7884                    "#,
7885            )?;
7886            jail.create_file(
7887                "dev.toml",
7888                r#"
7889                    [profile.dev]
7890                    optimizer = true
7891                    optimizer_runs = 333
7892                    gas_limit = 555555
7893                    "#,
7894            )?;
7895
7896            // Local config with only both profiles
7897            jail.create_file(
7898                "foundry.toml",
7899                r#"
7900                    [profile.dev]
7901                    extends = "dev.toml"
7902                    sender = "0x0000000000000000000000000000000000000001"
7903
7904                    [profile.prod]
7905                    extends = "prod.toml"
7906                    sender = "0x0000000000000000000000000000000000000002"
7907                    "#,
7908            )?;
7909
7910            // Test that prod profile correctly inherits even without a default profile
7911            jail.set_env("FOUNDRY_PROFILE", "dev");
7912            let config = Config::load().unwrap();
7913            assert_eq!(config.optimizer, Some(true));
7914            assert_eq!(config.optimizer_runs, Some(333));
7915            assert_eq!(config.gas_limit, 555555.into());
7916            assert_eq!(
7917                config.sender,
7918                "0x0000000000000000000000000000000000000001"
7919                    .parse::<alloy_primitives::Address>()
7920                    .unwrap()
7921            );
7922
7923            // Test that prod profile correctly inherits even without a default profile
7924            jail.set_env("FOUNDRY_PROFILE", "prod");
7925            let config = Config::load().unwrap();
7926            assert_eq!(config.optimizer, Some(true));
7927            assert_eq!(config.optimizer_runs, Some(20000));
7928            assert_eq!(config.gas_limit, 50000000.into());
7929            assert_eq!(
7930                config.sender,
7931                "0x0000000000000000000000000000000000000002"
7932                    .parse::<alloy_primitives::Address>()
7933                    .unwrap()
7934            );
7935
7936            Ok(())
7937        });
7938    }
7939
7940    #[test]
7941    fn test_extends_strategy_extend_arrays() {
7942        figment::Jail::expect_with(|jail| {
7943            // Create base config with arrays
7944            jail.create_file(
7945                "base.toml",
7946                r#"
7947                    [profile.default]
7948                    libs = ["lib", "node_modules"]
7949                    ignored_error_codes = [5667, 1878]
7950                    optimizer_runs = 200
7951                    "#,
7952            )?;
7953
7954            // Local config extends with extend-arrays strategy (concatenates arrays)
7955            jail.create_file(
7956                "foundry.toml",
7957                r#"
7958                    [profile.default]
7959                    extends = "base.toml"
7960                    libs = ["mylib", "customlib"]
7961                    ignored_error_codes = [1234]
7962                    optimizer_runs = 500
7963                    "#,
7964            )?;
7965
7966            let config = Config::load().unwrap();
7967
7968            // Arrays should be concatenated (base + local)
7969            assert_eq!(config.libs.len(), 4);
7970            assert!(config.libs.iter().any(|l| l.to_str() == Some("lib")));
7971            assert!(config.libs.iter().any(|l| l.to_str() == Some("node_modules")));
7972            assert!(config.libs.iter().any(|l| l.to_str() == Some("mylib")));
7973            assert!(config.libs.iter().any(|l| l.to_str() == Some("customlib")));
7974
7975            assert_eq!(config.ignored_error_codes.len(), 3);
7976            assert!(
7977                config.ignored_error_codes.contains(&SolidityErrorCode::UnusedFunctionParameter)
7978            ); // 5667
7979            assert!(
7980                config.ignored_error_codes.contains(&SolidityErrorCode::SpdxLicenseNotProvided)
7981            ); // 1878
7982            assert!(config.ignored_error_codes.contains(&SolidityErrorCode::from(1234u64))); // 1234 - generic
7983
7984            // Non-array values should be replaced
7985            assert_eq!(config.optimizer_runs, Some(500));
7986
7987            Ok(())
7988        });
7989    }
7990
7991    #[test]
7992    fn test_extends_strategy_replace_arrays() {
7993        figment::Jail::expect_with(|jail| {
7994            // Create base config with arrays
7995            jail.create_file(
7996                "base.toml",
7997                r#"
7998                    [profile.default]
7999                    libs = ["lib", "node_modules"]
8000                    ignored_error_codes = [5667, 1878]
8001                    optimizer_runs = 200
8002                    "#,
8003            )?;
8004
8005            // Local config extends with replace-arrays strategy (replaces arrays entirely)
8006            jail.create_file(
8007                "foundry.toml",
8008                r#"
8009                    [profile.default]
8010                    extends = { path = "base.toml", strategy = "replace-arrays" }
8011                    libs = ["mylib", "customlib"]
8012                    ignored_error_codes = [1234]
8013                    optimizer_runs = 500
8014                    "#,
8015            )?;
8016
8017            let config = Config::load().unwrap();
8018
8019            // Arrays should be replaced entirely (only local values)
8020            assert_eq!(config.libs.len(), 2);
8021            assert!(config.libs.iter().any(|l| l.to_str() == Some("mylib")));
8022            assert!(config.libs.iter().any(|l| l.to_str() == Some("customlib")));
8023            assert!(!config.libs.iter().any(|l| l.to_str() == Some("lib")));
8024            assert!(!config.libs.iter().any(|l| l.to_str() == Some("node_modules")));
8025
8026            assert_eq!(config.ignored_error_codes.len(), 1);
8027            assert!(config.ignored_error_codes.contains(&SolidityErrorCode::from(1234u64))); // 1234
8028            assert!(
8029                !config.ignored_error_codes.contains(&SolidityErrorCode::UnusedFunctionParameter)
8030            ); // 5667
8031
8032            // Non-array values should be replaced
8033            assert_eq!(config.optimizer_runs, Some(500));
8034
8035            Ok(())
8036        });
8037    }
8038
8039    #[test]
8040    fn test_extends_strategy_no_collision_success() {
8041        figment::Jail::expect_with(|jail| {
8042            // Create base config
8043            jail.create_file(
8044                "base.toml",
8045                r#"
8046                    [profile.default]
8047                    optimizer = true
8048                    optimizer_runs = 200
8049                    src = "src"
8050                    "#,
8051            )?;
8052
8053            // Local config extends with no-collision strategy and no conflicts
8054            jail.create_file(
8055                "foundry.toml",
8056                r#"
8057                    [profile.default]
8058                    extends = { path = "base.toml", strategy = "no-collision" }
8059                    test = "tests"
8060                    libs = ["lib"]
8061                    "#,
8062            )?;
8063
8064            let config = Config::load().unwrap();
8065
8066            // Values from base should be present
8067            assert_eq!(config.optimizer, Some(true));
8068            assert_eq!(config.optimizer_runs, Some(200));
8069            assert_eq!(config.src, PathBuf::from("src"));
8070
8071            // Values from local should be present
8072            assert_eq!(config.test, PathBuf::from("tests"));
8073            assert_eq!(config.libs.len(), 1);
8074            assert!(config.libs.iter().any(|l| l.to_str() == Some("lib")));
8075
8076            Ok(())
8077        });
8078    }
8079
8080    #[test]
8081    fn test_extends_strategy_no_collision_error() {
8082        figment::Jail::expect_with(|jail| {
8083            // Create base config
8084            jail.create_file(
8085                "base.toml",
8086                r#"
8087                    [profile.default]
8088                    optimizer = true
8089                    optimizer_runs = 200
8090                    libs = ["lib", "node_modules"]
8091                    "#,
8092            )?;
8093
8094            // Local config extends with no-collision strategy but has conflicts
8095            jail.create_file(
8096                "foundry.toml",
8097                r#"
8098                    [profile.default]
8099                    extends = { path = "base.toml", strategy = "no-collision" }
8100                    optimizer_runs = 500
8101                    libs = ["mylib"]
8102                    "#,
8103            )?;
8104
8105            // Loading should fail due to key collision
8106            let result = Config::load();
8107
8108            if let Ok(config) = result {
8109                panic!(
8110                    "Expected error but got config with optimizer_runs: {:?}, libs: {:?}",
8111                    config.optimizer_runs, config.libs
8112                );
8113            }
8114
8115            let err = result.unwrap_err();
8116            let err_str = err.to_string();
8117            assert!(
8118                err_str.contains("Key collision detected") || err_str.contains("collision"),
8119                "Error message doesn't mention collision: {err_str}"
8120            );
8121
8122            Ok(())
8123        });
8124    }
8125
8126    #[test]
8127    fn test_extends_both_syntaxes() {
8128        figment::Jail::expect_with(|jail| {
8129            // Create base config
8130            jail.create_file(
8131                "base.toml",
8132                r#"
8133                    [profile.default]
8134                    libs = ["lib"]
8135                    optimizer = true
8136                    "#,
8137            )?;
8138
8139            // Test 1: Simple string syntax (should use default extend-arrays)
8140            jail.create_file(
8141                "foundry_string.toml",
8142                r#"
8143                    [profile.default]
8144                    extends = "base.toml"
8145                    libs = ["custom"]
8146                    "#,
8147            )?;
8148
8149            // Test 2: Object syntax with explicit strategy
8150            jail.create_file(
8151                "foundry_object.toml",
8152                r#"
8153                    [profile.default]
8154                    extends = { path = "base.toml", strategy = "replace-arrays" }
8155                    libs = ["custom"]
8156                    "#,
8157            )?;
8158
8159            // Test string syntax (default extend-arrays)
8160            jail.set_env("FOUNDRY_CONFIG", "foundry_string.toml");
8161            let config = Config::load().unwrap();
8162            assert_eq!(config.libs.len(), 2); // Should concatenate
8163            assert!(config.libs.iter().any(|l| l.to_str() == Some("lib")));
8164            assert!(config.libs.iter().any(|l| l.to_str() == Some("custom")));
8165
8166            // Test object syntax (replace-arrays)
8167            jail.set_env("FOUNDRY_CONFIG", "foundry_object.toml");
8168            let config = Config::load().unwrap();
8169            assert_eq!(config.libs.len(), 1); // Should replace
8170            assert!(config.libs.iter().any(|l| l.to_str() == Some("custom")));
8171            assert!(!config.libs.iter().any(|l| l.to_str() == Some("lib")));
8172
8173            Ok(())
8174        });
8175    }
8176
8177    #[test]
8178    fn test_extends_strategy_default_is_extend_arrays() {
8179        figment::Jail::expect_with(|jail| {
8180            // Create base config
8181            jail.create_file(
8182                "base.toml",
8183                r#"
8184                    [profile.default]
8185                    libs = ["lib", "node_modules"]
8186                    optimizer = true
8187                    "#,
8188            )?;
8189
8190            // Local config extends without specifying strategy (should default to extend-arrays)
8191            jail.create_file(
8192                "foundry.toml",
8193                r#"
8194                    [profile.default]
8195                    extends = "base.toml"
8196                    libs = ["custom"]
8197                    optimizer = false
8198                    "#,
8199            )?;
8200
8201            // Should work with default extend-arrays strategy
8202            let config = Config::load().unwrap();
8203
8204            // Arrays should be concatenated by default
8205            assert_eq!(config.libs.len(), 3);
8206            assert!(config.libs.iter().any(|l| l.to_str() == Some("lib")));
8207            assert!(config.libs.iter().any(|l| l.to_str() == Some("node_modules")));
8208            assert!(config.libs.iter().any(|l| l.to_str() == Some("custom")));
8209
8210            // Non-array values should be replaced
8211            assert_eq!(config.optimizer, Some(false));
8212
8213            Ok(())
8214        });
8215    }
8216
8217    #[test]
8218    fn test_deprecated_deny_warnings_is_handled() {
8219        figment::Jail::expect_with(|jail| {
8220            jail.create_file(
8221                "foundry.toml",
8222                r#"
8223                [profile.default]
8224                deny_warnings = true
8225                "#,
8226            )?;
8227            let config = Config::load().unwrap();
8228
8229            // Assert that the deprecated flag is correctly interpreted
8230            assert_eq!(config.deny, DenyLevel::Warnings);
8231            Ok(())
8232        });
8233    }
8234
8235    #[test]
8236    fn warns_on_deprecated_keys_in_inactive_profiles() {
8237        figment::Jail::expect_with(|jail| {
8238            jail.create_file(
8239                "foundry.toml",
8240                r#"
8241                [profile.default]
8242                src = "src"
8243
8244                [profile.ci]
8245                deny_warnings = true
8246                "#,
8247            )?;
8248
8249            let cfg = Config::load().unwrap();
8250            assert!(
8251                cfg.warnings.iter().any(|w| matches!(
8252                    w,
8253                    crate::Warning::DeprecatedKey { old, new }
8254                    if old == "deny_warnings" && new == "deny = warnings"
8255                )),
8256                "Expected deprecated key warning for inactive profile, got: {:?}",
8257                cfg.warnings
8258            );
8259            Ok(())
8260        });
8261    }
8262
8263    #[test]
8264    fn warns_on_deprecated_profile_names() {
8265        figment::Jail::expect_with(|jail| {
8266            jail.create_file(
8267                "foundry.toml",
8268                r#"
8269                [profile.cancun]
8270                "#,
8271            )?;
8272
8273            let cfg = Config::load().unwrap();
8274            assert!(
8275                cfg.warnings.iter().any(|w| matches!(
8276                    w,
8277                    crate::Warning::DeprecatedKey { old, new }
8278                    if old == "cancun" && new == "evm_version = Cancun"
8279                )),
8280                "Expected deprecated profile-name warning, got: {:?}",
8281                cfg.warnings
8282            );
8283            Ok(())
8284        });
8285    }
8286
8287    #[test]
8288    fn warns_on_unknown_keys_in_profile() {
8289        figment::Jail::expect_with(|jail| {
8290            jail.create_file(
8291                "foundry.toml",
8292                r#"
8293                [profile.default]
8294                unknown_key_xyz = 123
8295                "#,
8296            )?;
8297
8298            let cfg = Config::load().unwrap();
8299            assert!(cfg.warnings.iter().any(
8300                |w| matches!(w, crate::Warning::UnknownKey { key, .. } if key == "unknown_key_xyz")
8301            ));
8302            Ok(())
8303        });
8304    }
8305
8306    #[test]
8307    fn no_unknown_key_warning_for_network_field() {
8308        // Regression test: `network` is a flattened `Option` field of `NetworkConfigs`. It must
8309        // not trigger an unknown-key warning, regardless of whether it is set.
8310        figment::Jail::expect_with(|jail| {
8311            jail.create_file(
8312                "foundry.toml",
8313                r#"
8314                [profile.default]
8315                network = "tempo"
8316                "#,
8317            )?;
8318
8319            let cfg = Config::load().unwrap();
8320            assert!(
8321                !cfg.warnings.iter().any(
8322                    |w| matches!(w, crate::Warning::UnknownKey { key, .. } if key == "network")
8323                ),
8324                "did not expect UnknownKey warning for `network`, got: {:?}",
8325                cfg.warnings
8326            );
8327            Ok(())
8328        });
8329    }
8330
8331    #[test]
8332    fn no_unknown_key_warning_for_legacy_tempo_alias() {
8333        // Regression test: the legacy `tempo = true` alias must keep working without warnings.
8334        figment::Jail::expect_with(|jail| {
8335            jail.create_file(
8336                "foundry.toml",
8337                r#"
8338                [profile.default]
8339                tempo = true
8340                "#,
8341            )?;
8342
8343            let cfg = Config::load().unwrap();
8344            assert!(
8345                !cfg.warnings
8346                    .iter()
8347                    .any(|w| matches!(w, crate::Warning::UnknownKey { key, .. } if key == "tempo")),
8348                "did not expect UnknownKey warning for `tempo`, got: {:?}",
8349                cfg.warnings
8350            );
8351            Ok(())
8352        });
8353    }
8354
8355    #[test]
8356    #[cfg(feature = "monad")]
8357    fn no_unknown_key_warning_for_legacy_monad_alias() {
8358        figment::Jail::expect_with(|jail| {
8359            jail.create_file(
8360                "foundry.toml",
8361                r#"
8362                [profile.default]
8363                monad = true
8364                "#,
8365            )?;
8366
8367            let cfg = Config::load().unwrap();
8368            assert!(
8369                !cfg.warnings
8370                    .iter()
8371                    .any(|w| matches!(w, crate::Warning::UnknownKey { key, .. } if key == "monad")),
8372                "did not expect UnknownKey warning for `monad`, got: {:?}",
8373                cfg.warnings
8374            );
8375            Ok(())
8376        });
8377    }
8378
8379    #[test]
8380    #[cfg(not(feature = "monad"))]
8381    fn warns_for_monad_alias_without_monad_support() {
8382        figment::Jail::expect_with(|jail| {
8383            jail.create_file(
8384                "foundry.toml",
8385                r#"
8386                [profile.default]
8387                monad = true
8388                "#,
8389            )?;
8390
8391            let cfg = Config::load().unwrap();
8392            assert!(
8393                cfg.warnings
8394                    .iter()
8395                    .any(|w| matches!(w, crate::Warning::UnknownKey { key, .. } if key == "monad")),
8396                "expected UnknownKey warning for `monad`, got: {:?}",
8397                cfg.warnings
8398            );
8399            Ok(())
8400        });
8401    }
8402
8403    #[test]
8404    fn fails_on_ambiguous_version_in_compilation_restrictions() {
8405        figment::Jail::expect_with(|jail| {
8406            jail.create_file(
8407                "foundry.toml",
8408                r#"
8409                [profile.default]
8410                src = "src"
8411
8412                [[profile.default.compilation_restrictions]]
8413                paths = "src/*.sol"
8414                version = "0.8.11"
8415                "#,
8416            )?;
8417
8418            let err = Config::load().expect_err("expected bare version to fail");
8419            let err_msg = err.to_string();
8420            assert!(
8421                err_msg.contains("Invalid version format '0.8.11'")
8422                    && err_msg.contains("Bare version numbers are ambiguous"),
8423                "Expected error about ambiguous version, got: {err_msg}"
8424            );
8425
8426            Ok(())
8427        });
8428    }
8429
8430    #[test]
8431    fn accepts_explicit_version_requirements() {
8432        figment::Jail::expect_with(|jail| {
8433            jail.create_file(
8434                "foundry.toml",
8435                r#"
8436                [profile.default]
8437                src = "src"
8438
8439                [[profile.default.compilation_restrictions]]
8440                paths = "src/*.sol"
8441                version = "=0.8.11"
8442
8443                [[profile.default.compilation_restrictions]]
8444                paths = "test/*.sol"
8445                version = ">=0.8.11"
8446                "#,
8447            )?;
8448
8449            let config = Config::load().expect("should accept explicit version requirements");
8450            assert_eq!(config.compilation_restrictions.len(), 2);
8451
8452            Ok(())
8453        });
8454    }
8455
8456    #[test]
8457    fn warns_on_unknown_keys_in_all_config_sections() {
8458        figment::Jail::expect_with(|jail| {
8459            jail.create_file(
8460                "foundry.toml",
8461                r#"
8462                [profile.default]
8463                src = "src"
8464                unknown_profile_key = "should_warn"
8465
8466                # Standalone sections with unknown keys
8467                [fmt]
8468                line_length = 120
8469                unknown_fmt_key = "should_warn"
8470
8471                [lint]
8472                severity = ["high"]
8473                unknown_lint_key = "should_warn"
8474
8475                [doc]
8476                out = "docs"
8477                unknown_doc_key = "should_warn"
8478
8479                [fuzz]
8480                runs = 256
8481                unknown_fuzz_key = "should_warn"
8482
8483                [invariant]
8484                runs = 256
8485                unknown_invariant_key = "should_warn"
8486
8487                [symbolic]
8488                enabled = true
8489                depth = 128
8490                unknown_symbolic_key = "should_warn"
8491
8492                [mutation]
8493                unknown_mutation_key = "should_warn"
8494
8495                [vyper]
8496                unknown_vyper_key = "should_warn"
8497
8498                [bind_json]
8499                out = "bindings.sol"
8500                unknown_bind_json_key = "should_warn"
8501
8502                # Nested profile sections with unknown keys
8503                [profile.default.fmt]
8504                line_length = 100
8505                unknown_nested_fmt_key = "should_warn"
8506
8507                [profile.default.lint]
8508                severity = ["low"]
8509                unknown_nested_lint_key = "should_warn"
8510
8511                [profile.default.doc]
8512                out = "documentation"
8513                unknown_nested_doc_key = "should_warn"
8514
8515                [profile.default.fuzz]
8516                runs = 512
8517                unknown_nested_fuzz_key = "should_warn"
8518
8519                [profile.default.invariant]
8520                runs = 512
8521                unknown_nested_invariant_key = "should_warn"
8522
8523                [profile.default.symbolic]
8524                max_paths = 512
8525                unknown_nested_symbolic_key = "should_warn"
8526
8527                [profile.default.mutation]
8528                unknown_nested_mutation_key = "should_warn"
8529
8530                [profile.default.vyper]
8531                unknown_nested_vyper_key = "should_warn"
8532
8533                [profile.default.bind_json]
8534                out = "nested_bindings.sol"
8535                unknown_nested_bind_json_key = "should_warn"
8536
8537                # Array sections with unknown keys
8538                [[profile.default.compilation_restrictions]]
8539                paths = "src/*.sol"
8540                unknown_compilation_key = "should_warn"
8541
8542                [[profile.default.additional_compiler_profiles]]
8543                name = "via-ir"
8544                via_ir = true
8545                unknown_compiler_profile_key = "should_warn"
8546                "#,
8547            )?;
8548
8549            let cfg = Config::load().unwrap();
8550
8551            // Expected warnings for profile-level unknown key
8552            assert!(
8553                cfg.warnings.iter().any(|w| matches!(
8554                    w,
8555                    crate::Warning::UnknownKey { key, .. } if key == "unknown_profile_key"
8556                )),
8557                "Expected warning for 'unknown_profile_key' in profile, got: {:?}",
8558                cfg.warnings
8559            );
8560
8561            // Expected warnings for standalone sections
8562            let standalone_expected = [
8563                ("unknown_fmt_key", "fmt"),
8564                ("unknown_lint_key", "lint"),
8565                ("unknown_doc_key", "doc"),
8566                ("unknown_fuzz_key", "fuzz"),
8567                ("unknown_invariant_key", "invariant"),
8568                ("unknown_symbolic_key", "symbolic"),
8569                ("unknown_mutation_key", "mutation"),
8570                ("unknown_vyper_key", "vyper"),
8571                ("unknown_bind_json_key", "bind_json"),
8572            ];
8573
8574            for (expected_key, expected_section) in standalone_expected {
8575                assert!(
8576                    cfg.warnings.iter().any(|w| matches!(
8577                        w,
8578                        crate::Warning::UnknownSectionKey { key, section, .. }
8579                        if key == expected_key && section == expected_section
8580                    )),
8581                    "Expected warning for '{}' in standalone section '{}', got: {:?}",
8582                    expected_key,
8583                    expected_section,
8584                    cfg.warnings
8585                );
8586            }
8587
8588            // Expected warnings for nested profile sections
8589            let nested_expected = [
8590                ("unknown_nested_fmt_key", "fmt"),
8591                ("unknown_nested_lint_key", "lint"),
8592                ("unknown_nested_doc_key", "doc"),
8593                ("unknown_nested_fuzz_key", "fuzz"),
8594                ("unknown_nested_invariant_key", "invariant"),
8595                ("unknown_nested_symbolic_key", "symbolic"),
8596                ("unknown_nested_mutation_key", "mutation"),
8597                ("unknown_nested_vyper_key", "vyper"),
8598                ("unknown_nested_bind_json_key", "bind_json"),
8599            ];
8600
8601            for (expected_key, expected_section) in nested_expected {
8602                assert!(
8603                    cfg.warnings.iter().any(|w| matches!(
8604                        w,
8605                        crate::Warning::UnknownSectionKey { key, section, .. }
8606                        if key == expected_key && section == expected_section
8607                    )),
8608                    "Expected warning for '{}' in nested section '{}', got: {:?}",
8609                    expected_key,
8610                    expected_section,
8611                    cfg.warnings
8612                );
8613            }
8614
8615            // Expected warnings for array item sections
8616            let array_expected = [
8617                ("unknown_compilation_key", "compilation_restrictions"),
8618                ("unknown_compiler_profile_key", "additional_compiler_profiles"),
8619            ];
8620
8621            for (expected_key, expected_section) in array_expected {
8622                assert!(
8623                    cfg.warnings.iter().any(|w| matches!(
8624                        w,
8625                        crate::Warning::UnknownSectionKey { key, section, .. }
8626                        if key == expected_key && section == expected_section
8627                    )),
8628                    "Expected warning for '{}' in array section '{}', got: {:?}",
8629                    expected_key,
8630                    expected_section,
8631                    cfg.warnings
8632                );
8633            }
8634
8635            // Verify total count of unknown key warnings
8636            let unknown_key_warnings: Vec<_> = cfg
8637                .warnings
8638                .iter()
8639                .filter(|w| {
8640                    matches!(w, crate::Warning::UnknownKey { .. })
8641                        || matches!(w, crate::Warning::UnknownSectionKey { .. })
8642                })
8643                .collect();
8644
8645            // 1 profile key + 9 standalone + 9 nested + 2 array = 21 total
8646            assert_eq!(
8647                unknown_key_warnings.len(),
8648                21,
8649                "Expected 21 unknown key warnings (1 profile + 9 standalone + 9 nested + 2 array), got {}: {:?}",
8650                unknown_key_warnings.len(),
8651                unknown_key_warnings
8652            );
8653
8654            Ok(())
8655        });
8656    }
8657
8658    #[test]
8659    fn warns_on_unknown_keys_in_extended_config() {
8660        figment::Jail::expect_with(|jail| {
8661            // Create base config with unknown keys
8662            jail.create_file(
8663                "base.toml",
8664                r#"
8665                [profile.default]
8666                optimizer_runs = 800
8667                unknown_base_profile_key = "should_warn"
8668
8669                [lint]
8670                severity = ["high"]
8671                unknown_base_lint_key = "should_warn"
8672
8673                [fmt]
8674                line_length = 100
8675                unknown_base_fmt_key = "should_warn"
8676                "#,
8677            )?;
8678
8679            // Create local config that extends base with its own unknown keys
8680            jail.create_file(
8681                "foundry.toml",
8682                r#"
8683                [profile.default]
8684                extends = "base.toml"
8685                src = "src"
8686                unknown_local_profile_key = "should_warn"
8687
8688                [lint]
8689                unknown_local_lint_key = "should_warn"
8690
8691                [fuzz]
8692                runs = 512
8693                unknown_local_fuzz_key = "should_warn"
8694
8695                [[profile.default.compilation_restrictions]]
8696                paths = "src/*.sol"
8697                unknown_local_restriction_key = "should_warn"
8698                "#,
8699            )?;
8700
8701            let cfg = Config::load().unwrap();
8702
8703            // Verify base config values are inherited
8704            assert_eq!(cfg.optimizer_runs, Some(800));
8705
8706            // Unknown keys from both base and local configs should be detected.
8707            // Note: Due to how figment merges configs before validation, the source
8708            // will show the local config file for all warnings. This is a known
8709            // limitation - proper source attribution for extended configs would
8710            // require validating each file before the merge.
8711
8712            // Verify all expected unknown keys are detected
8713            let expected_unknown_keys = ["unknown_base_profile_key", "unknown_local_profile_key"];
8714            for expected_key in expected_unknown_keys {
8715                assert!(
8716                    cfg.warnings.iter().any(|w| matches!(
8717                        w,
8718                        crate::Warning::UnknownKey { key, .. } if key == expected_key
8719                    )),
8720                    "Expected warning for '{}', got: {:?}",
8721                    expected_key,
8722                    cfg.warnings
8723                );
8724            }
8725
8726            let expected_section_keys = [
8727                ("unknown_base_lint_key", "lint"),
8728                ("unknown_base_fmt_key", "fmt"),
8729                ("unknown_local_lint_key", "lint"),
8730                ("unknown_local_fuzz_key", "fuzz"),
8731                ("unknown_local_restriction_key", "compilation_restrictions"),
8732            ];
8733            for (expected_key, expected_section) in expected_section_keys {
8734                assert!(
8735                    cfg.warnings.iter().any(|w| matches!(
8736                        w,
8737                        crate::Warning::UnknownSectionKey { key, section, .. }
8738                        if key == expected_key && section == expected_section
8739                    )),
8740                    "Expected warning for '{}' in section '{}', got: {:?}",
8741                    expected_key,
8742                    expected_section,
8743                    cfg.warnings
8744                );
8745            }
8746
8747            // Verify total: 2 profile keys + 5 section keys = 7 warnings
8748            let unknown_warnings: Vec<_> = cfg
8749                .warnings
8750                .iter()
8751                .filter(|w| {
8752                    matches!(w, crate::Warning::UnknownKey { .. })
8753                        || matches!(w, crate::Warning::UnknownSectionKey { .. })
8754                })
8755                .collect();
8756            assert_eq!(
8757                unknown_warnings.len(),
8758                7,
8759                "Expected 7 unknown key warnings, got {}: {:?}",
8760                unknown_warnings.len(),
8761                unknown_warnings
8762            );
8763
8764            Ok(())
8765        });
8766    }
8767
8768    // Test for issue #12844: FOUNDRY_PROFILE=nonexistent should warn and fall back to default.
8769    #[test]
8770    fn warns_on_unknown_profile() {
8771        figment::Jail::expect_with(|jail| {
8772            jail.create_file(
8773                "foundry.toml",
8774                r#"
8775                [profile.default]
8776                src = "src"
8777                "#,
8778            )?;
8779
8780            jail.set_env("FOUNDRY_PROFILE", "nonexistent");
8781            let cfg = Config::load().expect("expected unknown profile to fall back to default");
8782            assert_eq!(cfg.profile, Config::DEFAULT_PROFILE);
8783            assert!(
8784                cfg.warnings.iter().any(|w| matches!(
8785                    w,
8786                    crate::Warning::UnknownProfile { profile } if profile == "nonexistent"
8787                )),
8788                "Expected UnknownProfile warning, got: {:?}",
8789                cfg.warnings
8790            );
8791
8792            Ok(())
8793        });
8794    }
8795
8796    // Test for issue #13316: vyper config keys should not trigger unknown key warnings
8797    #[test]
8798    fn no_false_warnings_for_vyper_config_keys() {
8799        figment::Jail::expect_with(|jail| {
8800            jail.create_file(
8801                "foundry.toml",
8802                r#"
8803                [profile.default]
8804                src = "src"
8805
8806                [vyper]
8807                optimize = "O1"
8808                path = "/usr/bin/vyper"
8809                experimental_codegen = true
8810                venom_experimental = false
8811                debug = true
8812                enable_decimals = true
8813
8814                [vyper.venom]
8815                disable_cse = true
8816                inline_threshold = 15
8817                "#,
8818            )?;
8819
8820            let cfg = Config::load().unwrap();
8821            // None of the valid vyper keys should trigger warnings
8822            let vyper_warnings: Vec<_> = cfg
8823                .warnings
8824                .iter()
8825                .filter(|w| {
8826                    matches!(
8827                        w,
8828                        crate::Warning::UnknownSectionKey { section, .. } if section == "vyper"
8829                    )
8830                })
8831                .collect();
8832
8833            assert!(
8834                vyper_warnings.is_empty(),
8835                "Valid vyper keys should not trigger warnings, got: {vyper_warnings:?}"
8836            );
8837
8838            Ok(())
8839        });
8840    }
8841
8842    // Test for issue #13316: vyper config in profile should not trigger false warnings
8843    #[test]
8844    fn no_false_warnings_for_nested_vyper_config_keys() {
8845        figment::Jail::expect_with(|jail| {
8846            jail.create_file(
8847                "foundry.toml",
8848                r#"
8849                [profile.default]
8850                src = "src"
8851
8852                [profile.default.vyper]
8853                opt_level = "s"
8854                path = "/opt/vyper/bin/vyper"
8855                experimental_codegen = false
8856                debug = true
8857                enable_decimals = true
8858                venom = { disable_sccp = true, disable_mem2var = false }
8859                "#,
8860            )?;
8861
8862            let cfg = Config::load().unwrap();
8863            // None of the valid vyper keys should trigger warnings
8864            let vyper_warnings: Vec<_> = cfg
8865                .warnings
8866                .iter()
8867                .filter(|w| {
8868                    matches!(
8869                        w,
8870                        crate::Warning::UnknownSectionKey { section, .. } if section == "vyper"
8871                    )
8872                })
8873                .collect();
8874
8875            assert!(
8876                vyper_warnings.is_empty(),
8877                "Valid nested vyper keys should not trigger warnings, got: {vyper_warnings:?}"
8878            );
8879
8880            Ok(())
8881        });
8882    }
8883
8884    // Test for issue #13316: inline vyper config format should not trigger false warnings
8885    // This matches the exact format used in https://github.com/pcaversaccio/snekmate
8886    #[test]
8887    fn no_false_warnings_for_inline_vyper_config() {
8888        figment::Jail::expect_with(|jail| {
8889            jail.create_file(
8890                "foundry.toml",
8891                r#"
8892                [profile.default]
8893                src = "src"
8894                vyper = { optimize = "gas", debug = true }
8895
8896                [profile.default-venom]
8897                vyper = { opt_level = "O2", experimental_codegen = true, venom = { disable_cse = true } }
8898
8899                [profile.ci-venom]
8900                vyper = { venom_experimental = true, enable_decimals = true }
8901                "#,
8902            )?;
8903
8904            let cfg = Config::load().unwrap();
8905            let vyper_warnings: Vec<_> = cfg
8906                .warnings
8907                .iter()
8908                .filter(|w| {
8909                    matches!(
8910                        w,
8911                        crate::Warning::UnknownSectionKey { section, .. } if section == "vyper"
8912                    )
8913                })
8914                .collect();
8915
8916            assert!(
8917                vyper_warnings.is_empty(),
8918                "Valid inline vyper config should not trigger warnings, got: {vyper_warnings:?}"
8919            );
8920
8921            Ok(())
8922        });
8923    }
8924
8925    // Test for issue #13316: unknown vyper keys should still warn
8926    #[test]
8927    fn warns_on_unknown_vyper_keys() {
8928        figment::Jail::expect_with(|jail| {
8929            jail.create_file(
8930                "foundry.toml",
8931                r#"
8932                [profile.default]
8933                src = "src"
8934
8935                [vyper]
8936                optimize = "gas"
8937                unknown_vyper_option = true
8938                "#,
8939            )?;
8940
8941            let cfg = Config::load().unwrap();
8942            assert!(
8943                cfg.warnings.iter().any(|w| matches!(
8944                    w,
8945                    crate::Warning::UnknownSectionKey { key, section, .. }
8946                    if key == "unknown_vyper_option" && section == "vyper"
8947                )),
8948                "Unknown vyper key should trigger warning, got: {:?}",
8949                cfg.warnings
8950            );
8951
8952            Ok(())
8953        });
8954    }
8955
8956    // Test for issue #12844: known profile should work
8957    #[test]
8958    fn succeeds_on_known_profile() {
8959        figment::Jail::expect_with(|jail| {
8960            jail.create_file(
8961                "foundry.toml",
8962                r#"
8963                [profile.default]
8964                src = "src"
8965
8966                [profile.ci]
8967                src = "src"
8968                fuzz = { runs = 10000 }
8969                "#,
8970            )?;
8971
8972            jail.set_env("FOUNDRY_PROFILE", "ci");
8973            let config = Config::load().expect("known profile should work");
8974            assert_eq!(config.profile.as_str(), "ci");
8975            assert_eq!(config.fuzz.runs, 10000);
8976
8977            Ok(())
8978        });
8979    }
8980
8981    // Test for issue #12963: nested lib configs should fallback to default profile
8982    // when they don't define the requested profile
8983    #[test]
8984    fn nested_lib_config_falls_back_to_default_profile() {
8985        figment::Jail::expect_with(|jail| {
8986            // Create a lib directory with only default profile
8987            let lib_path = jail.directory().join("lib/mylib");
8988            std::fs::create_dir_all(&lib_path).unwrap();
8989            jail.create_file(
8990                "lib/mylib/foundry.toml",
8991                r#"
8992                [profile.default]
8993                src = "contracts"
8994                "#,
8995            )?;
8996
8997            // Set a profile that doesn't exist in the lib
8998            jail.set_env("FOUNDRY_PROFILE", "ci");
8999
9000            // load_with_root_and_fallback should succeed and fall back to default
9001            let config = Config::load_with_root_and_fallback(&lib_path)
9002                .expect("lib config should load with fallback");
9003            assert_eq!(config.profile, Config::DEFAULT_PROFILE);
9004            assert_eq!(config.src.as_os_str(), "contracts");
9005
9006            Ok(())
9007        });
9008    }
9009
9010    // Test for issue #12963: nested lib configs should use requested profile if it exists
9011    #[test]
9012    fn nested_lib_config_uses_profile_if_exists() {
9013        figment::Jail::expect_with(|jail| {
9014            // Create a lib directory with both default and ci profiles
9015            let lib_path = jail.directory().join("lib/mylib");
9016            std::fs::create_dir_all(&lib_path).unwrap();
9017            jail.create_file(
9018                "lib/mylib/foundry.toml",
9019                r#"
9020                [profile.default]
9021                src = "contracts"
9022
9023                [profile.ci]
9024                src = "contracts"
9025                fuzz = { runs = 5000 }
9026                "#,
9027            )?;
9028
9029            // Set a profile that exists in the lib
9030            jail.set_env("FOUNDRY_PROFILE", "ci");
9031
9032            // load_with_root_and_fallback should use the ci profile
9033            let config = Config::load_with_root_and_fallback(&lib_path)
9034                .expect("lib config should load with profile");
9035            assert_eq!(config.profile.as_str(), "ci");
9036            assert_eq!(config.fuzz.runs, 5000);
9037
9038            Ok(())
9039        });
9040    }
9041
9042    // Test for issue #13170: profile names with hyphens should work correctly
9043    #[test]
9044    fn succeeds_on_hyphenated_profile_name() {
9045        figment::Jail::expect_with(|jail| {
9046            jail.create_file(
9047                "foundry.toml",
9048                r#"
9049                [profile.default]
9050                src = "src"
9051
9052                [profile.ci-venom]
9053                src = "src"
9054                fuzz = { runs = 7500 }
9055
9056                [profile.default-venom]
9057                src = "src"
9058                fuzz = { runs = 8000 }
9059                "#,
9060            )?;
9061
9062            // Test ci-venom profile
9063            jail.set_env("FOUNDRY_PROFILE", "ci-venom");
9064            let config = Config::load().expect("hyphenated profile should work");
9065            assert_eq!(config.profile.as_str(), "ci-venom");
9066            assert_eq!(config.fuzz.runs, 7500);
9067
9068            // Test default-venom profile
9069            jail.set_env("FOUNDRY_PROFILE", "default-venom");
9070            let config = Config::load().expect("hyphenated profile should work");
9071            assert_eq!(config.profile.as_str(), "default-venom");
9072            assert_eq!(config.fuzz.runs, 8000);
9073
9074            // Verify the profiles list contains hyphenated names
9075            assert!(
9076                config.profiles.iter().any(|p| p.as_str() == "ci-venom"),
9077                "profiles should contain 'ci-venom', got: {:?}",
9078                config.profiles
9079            );
9080            assert!(
9081                config.profiles.iter().any(|p| p.as_str() == "default-venom"),
9082                "profiles should contain 'default-venom', got: {:?}",
9083                config.profiles
9084            );
9085
9086            Ok(())
9087        });
9088    }
9089
9090    #[test]
9091    fn standalone_section_name_can_be_used_as_profile_name() {
9092        figment::Jail::expect_with(|jail| {
9093            jail.create_file(
9094                "foundry.toml",
9095                r#"
9096                [profile.symbolic]
9097                eth-rpc-url = "https://example.com/"
9098                "#,
9099            )?;
9100            jail.set_env("FOUNDRY_PROFILE", "symbolic");
9101
9102            let config = Config::load().unwrap();
9103            assert_eq!(config.profile.as_str(), "symbolic");
9104            assert_eq!(config.eth_rpc_url.as_deref(), Some("https://example.com/"));
9105
9106            Ok(())
9107        });
9108    }
9109
9110    // Test for issue #13170: hyphenated profile with nested config keys
9111    #[test]
9112    fn hyphenated_profile_with_nested_sections() {
9113        figment::Jail::expect_with(|jail| {
9114            jail.create_file(
9115                "foundry.toml",
9116                r#"
9117                [profile.default]
9118                src = "src"
9119
9120                [profile.ci-venom]
9121                src = "src"
9122                optimizer_runs = 500
9123
9124                [profile.ci-venom.fuzz]
9125                runs = 10000
9126                max_test_rejects = 350000
9127
9128                [profile.ci-venom.invariant]
9129                runs = 375
9130                depth = 500
9131                "#,
9132            )?;
9133
9134            jail.set_env("FOUNDRY_PROFILE", "ci-venom");
9135            let config =
9136                Config::load().expect("hyphenated profile with nested sections should work");
9137            assert_eq!(config.profile.as_str(), "ci-venom");
9138            assert_eq!(config.optimizer_runs, Some(500));
9139            assert_eq!(config.fuzz.runs, 10000);
9140            assert_eq!(config.fuzz.max_test_rejects, 350000);
9141            assert_eq!(config.invariant.runs, 375);
9142            assert_eq!(config.invariant.depth, 500);
9143
9144            Ok(())
9145        });
9146    }
9147
9148    #[test]
9149    fn coverage_section_in_profile() {
9150        figment::Jail::expect_with(|jail| {
9151            jail.create_file(
9152                "foundry.toml",
9153                r#"
9154                [profile.default.coverage]
9155                report = ["summary", "lcov"]
9156                lcov_version = "2.2.0"
9157                ir_minimum = true
9158                report_file = "out/lcov.info"
9159                include_libs = true
9160                exclude_tests = true
9161                skip_files = ["test/**", "src/mocks/**"]
9162                "#,
9163            )?;
9164            let config = Config::load_with_root(jail.directory()).unwrap();
9165            assert_eq!(
9166                config.coverage.report,
9167                vec![CoverageReportKind::Summary, CoverageReportKind::Lcov]
9168            );
9169            assert_eq!(config.coverage.lcov_version, semver::Version::new(2, 2, 0));
9170            assert!(config.coverage.ir_minimum);
9171            assert_eq!(
9172                config.coverage.report_file.as_deref(),
9173                Some(std::path::Path::new("out/lcov.info"))
9174            );
9175            assert!(config.coverage.include_libs);
9176            assert!(config.coverage.exclude_tests);
9177            assert_eq!(
9178                config.coverage.skip_files,
9179                vec!["test/**".to_string(), "src/mocks/**".to_string()]
9180            );
9181            Ok(())
9182        });
9183    }
9184
9185    #[test]
9186    fn coverage_standalone_section_falls_back_to_default_profile() {
9187        figment::Jail::expect_with(|jail| {
9188            // Standalone `[coverage]` should populate the active profile,
9189            // matching how `[fuzz]` / `[invariant]` work.
9190            jail.create_file(
9191                "foundry.toml",
9192                r#"
9193                [coverage]
9194                skip_files = ["script/**"]
9195                exclude_tests = true
9196                "#,
9197            )?;
9198            let config = Config::load_with_root(jail.directory()).unwrap();
9199            assert_eq!(config.coverage.skip_files, vec!["script/**".to_string()]);
9200            assert!(config.coverage.exclude_tests);
9201            // Untouched fields keep their defaults.
9202            assert_eq!(config.coverage.report, vec![CoverageReportKind::Summary]);
9203            assert_eq!(config.coverage.lcov_version, semver::Version::new(1, 0, 0));
9204            Ok(())
9205        });
9206    }
9207
9208    #[test]
9209    fn coverage_per_profile_overrides_default() {
9210        figment::Jail::expect_with(|jail| {
9211            jail.create_file(
9212                "foundry.toml",
9213                r#"
9214                [profile.default.coverage]
9215                skip_files = ["script/**"]
9216
9217                [profile.ci.coverage]
9218                skip_files = ["test/**", "lib/**"]
9219                exclude_tests = true
9220                "#,
9221            )?;
9222
9223            // Default profile sees the default-profile coverage block.
9224            let config = Config::load_with_root(jail.directory()).unwrap();
9225            assert_eq!(config.coverage.skip_files, vec!["script/**".to_string()]);
9226            assert!(!config.coverage.exclude_tests);
9227
9228            // CI profile sees its own override.
9229            jail.set_env("FOUNDRY_PROFILE", "ci");
9230            let config = Config::load_with_root(jail.directory()).unwrap();
9231            assert_eq!(
9232                config.coverage.skip_files,
9233                vec!["test/**".to_string(), "lib/**".to_string()]
9234            );
9235            assert!(config.coverage.exclude_tests);
9236            Ok(())
9237        });
9238    }
9239}