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, doc_auto_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, SolcSettings},
41};
42use regex::Regex;
43use revm::primitives::hardfork::SpecId;
44use semver::Version;
45use serde::{Deserialize, Serialize, Serializer};
46use std::{
47    borrow::Cow,
48    collections::BTreeMap,
49    fs,
50    path::{Path, PathBuf},
51    str::FromStr,
52};
53
54mod macros;
55
56pub mod utils;
57pub use utils::*;
58
59mod endpoints;
60pub use endpoints::{
61    ResolvedRpcEndpoint, ResolvedRpcEndpoints, RpcEndpoint, RpcEndpointUrl, RpcEndpoints,
62};
63
64mod etherscan;
65use etherscan::{
66    EtherscanConfigError, EtherscanConfigs, EtherscanEnvProvider, ResolvedEtherscanConfig,
67};
68
69pub mod resolve;
70pub use resolve::UnresolvedEnvVarError;
71
72pub mod cache;
73use cache::{Cache, ChainCache};
74
75pub mod fmt;
76pub use fmt::FormatterConfig;
77
78pub mod lint;
79pub use lint::{LinterConfig, Severity as LintSeverity};
80
81pub mod fs_permissions;
82pub use fs_permissions::FsPermissions;
83use fs_permissions::PathPermission;
84
85pub mod error;
86use error::ExtractConfigError;
87pub use error::SolidityErrorCode;
88
89pub mod doc;
90pub use doc::DocConfig;
91
92pub mod filter;
93pub use filter::SkipBuildFilters;
94
95mod warning;
96pub use warning::*;
97
98pub mod fix;
99
100// reexport so cli types can implement `figment::Provider` to easily merge compiler arguments
101pub use alloy_chains::{Chain, NamedChain};
102pub use figment;
103use foundry_block_explorers::EtherscanApiVersion;
104
105pub mod providers;
106pub use providers::Remappings;
107use providers::*;
108
109mod fuzz;
110pub use fuzz::{FuzzConfig, FuzzCorpusConfig, FuzzDictionaryConfig};
111
112mod invariant;
113pub use invariant::InvariantConfig;
114
115mod inline;
116pub use inline::{InlineConfig, InlineConfigError, NatSpec};
117
118pub mod soldeer;
119use soldeer::{SoldeerConfig, SoldeerDependencyConfig};
120
121mod vyper;
122pub use vyper::VyperConfig;
123
124mod bind_json;
125use bind_json::BindJsonConfig;
126
127mod compilation;
128pub use compilation::{CompilationRestrictions, SettingsOverrides};
129
130pub mod extend;
131use extend::Extends;
132
133pub use semver;
134
135/// Foundry configuration
136///
137/// # Defaults
138///
139/// All configuration values have a default, documented in the [fields](#fields)
140/// section below. [`Config::default()`] returns the default values for
141/// the default profile while [`Config::with_root()`] returns the values based on the given
142/// directory. [`Config::load()`] starts with the default profile and merges various providers into
143/// the config, same for [`Config::load_with_root()`], but there the default values are determined
144/// by [`Config::with_root()`]
145///
146/// # Provider Details
147///
148/// `Config` is a Figment [`Provider`] with the following characteristics:
149///
150///   * **Profile**
151///
152///     The profile is set to the value of the `profile` field.
153///
154///   * **Metadata**
155///
156///     This provider is named `Foundry Config`. It does not specify a
157///     [`Source`](figment::Source) and uses default interpolation.
158///
159///   * **Data**
160///
161///     The data emitted by this provider are the keys and values corresponding
162///     to the fields and values of the structure. The dictionary is emitted to
163///     the "default" meta-profile.
164///
165/// Note that these behaviors differ from those of [`Config::figment()`].
166#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
167pub struct Config {
168    /// The selected profile. **(default: _default_ `default`)**
169    ///
170    /// **Note:** This field is never serialized nor deserialized. When a
171    /// `Config` is merged into a `Figment` as a `Provider`, this profile is
172    /// selected on the `Figment`. When a `Config` is extracted, this field is
173    /// set to the extracting Figment's selected `Profile`.
174    #[serde(skip)]
175    pub profile: Profile,
176    /// The list of all profiles defined in the config.
177    ///
178    /// See `profile`.
179    #[serde(skip)]
180    pub profiles: Vec<Profile>,
181
182    /// The root path where the config detection started from, [`Config::with_root`].
183    // We're skipping serialization here, so it won't be included in the [`Config::to_string()`]
184    // representation, but will be deserialized from the `Figment` so that forge commands can
185    // override it.
186    #[serde(default = "root_default", skip_serializing)]
187    pub root: PathBuf,
188
189    /// Configuration for extending from another foundry.toml (base) file.
190    ///
191    /// Can be either a string path or an object with path and strategy.
192    /// Base files cannot extend (inherit) other files.
193    #[serde(default, skip_serializing)]
194    pub extends: Option<Extends>,
195
196    /// path of the source contracts dir, like `src` or `contracts`
197    pub src: PathBuf,
198    /// path of the test dir
199    pub test: PathBuf,
200    /// path of the script dir
201    pub script: PathBuf,
202    /// path to where artifacts shut be written to
203    pub out: PathBuf,
204    /// all library folders to include, `lib`, `node_modules`
205    pub libs: Vec<PathBuf>,
206    /// `Remappings` to use for this repo
207    pub remappings: Vec<RelativeRemapping>,
208    /// Whether to autodetect remappings by scanning the `libs` folders recursively
209    pub auto_detect_remappings: bool,
210    /// library addresses to link
211    pub libraries: Vec<String>,
212    /// whether to enable cache
213    pub cache: bool,
214    /// whether to dynamically link tests
215    pub dynamic_test_linking: bool,
216    /// where the cache is stored if enabled
217    pub cache_path: PathBuf,
218    /// where the gas snapshots are stored
219    pub snapshots: PathBuf,
220    /// whether to check for differences against previously stored gas snapshots
221    pub gas_snapshot_check: bool,
222    /// whether to emit gas snapshots to disk
223    pub gas_snapshot_emit: bool,
224    /// where the broadcast logs are stored
225    pub broadcast: PathBuf,
226    /// additional solc allow paths for `--allow-paths`
227    pub allow_paths: Vec<PathBuf>,
228    /// additional solc include paths for `--include-path`
229    pub include_paths: Vec<PathBuf>,
230    /// glob patterns to skip
231    pub skip: Vec<GlobMatcher>,
232    /// whether to force a `project.clean()`
233    pub force: bool,
234    /// evm version to use
235    #[serde(with = "from_str_lowercase")]
236    pub evm_version: EvmVersion,
237    /// list of contracts to report gas of
238    pub gas_reports: Vec<String>,
239    /// list of contracts to ignore for gas reports
240    pub gas_reports_ignore: Vec<String>,
241    /// Whether to include gas reports for tests.
242    pub gas_reports_include_tests: bool,
243    /// The Solc instance to use if any.
244    ///
245    /// This takes precedence over `auto_detect_solc`, if a version is set then this overrides
246    /// auto-detection.
247    ///
248    /// **Note** for backwards compatibility reasons this also accepts solc_version from the toml
249    /// file, see `BackwardsCompatTomlProvider`.
250    ///
251    /// Avoid using this field directly; call the related `solc` methods instead.
252    #[doc(hidden)]
253    pub solc: Option<SolcReq>,
254    /// Whether to autodetect the solc compiler version to use.
255    pub auto_detect_solc: bool,
256    /// Offline mode, if set, network access (downloading solc) is disallowed.
257    ///
258    /// Relationship with `auto_detect_solc`:
259    ///    - if `auto_detect_solc = true` and `offline = true`, the required solc version(s) will
260    ///      be auto detected but if the solc version is not installed, it will _not_ try to
261    ///      install it
262    pub offline: bool,
263    /// Whether to activate optimizer
264    pub optimizer: Option<bool>,
265    /// The number of runs specifies roughly how often each opcode of the deployed code will be
266    /// executed across the life-time of the contract. This means it is a trade-off parameter
267    /// between code size (deploy cost) and code execution cost (cost after deployment).
268    /// An `optimizer_runs` parameter of `1` will produce short but expensive code. In contrast, a
269    /// larger `optimizer_runs` parameter will produce longer but more gas efficient code. The
270    /// maximum value of the parameter is `2**32-1`.
271    ///
272    /// A common misconception is that this parameter specifies the number of iterations of the
273    /// optimizer. This is not true: The optimizer will always run as many times as it can
274    /// still improve the code.
275    pub optimizer_runs: Option<usize>,
276    /// Switch optimizer components on or off in detail.
277    /// The "enabled" switch above provides two defaults which can be
278    /// tweaked here. If "details" is given, "enabled" can be omitted.
279    pub optimizer_details: Option<OptimizerDetails>,
280    /// Model checker settings.
281    pub model_checker: Option<ModelCheckerSettings>,
282    /// verbosity to use
283    pub verbosity: u8,
284    /// url of the rpc server that should be used for any rpc calls
285    pub eth_rpc_url: Option<String>,
286    /// Whether to accept invalid certificates for the rpc server.
287    pub eth_rpc_accept_invalid_certs: bool,
288    /// JWT secret that should be used for any rpc calls
289    pub eth_rpc_jwt: Option<String>,
290    /// Timeout that should be used for any rpc calls
291    pub eth_rpc_timeout: Option<u64>,
292    /// Headers that should be used for any rpc calls
293    ///
294    /// # Example
295    ///
296    /// rpc_headers = ["x-custom-header:value", "x-another-header:another-value"]
297    ///
298    /// You can also the ETH_RPC_HEADERS env variable like so:
299    /// `ETH_RPC_HEADERS="x-custom-header:value x-another-header:another-value"`
300    pub eth_rpc_headers: Option<Vec<String>>,
301    /// etherscan API key, or alias for an `EtherscanConfig` in `etherscan` table
302    pub etherscan_api_key: Option<String>,
303    /// etherscan API version
304    pub etherscan_api_version: Option<EtherscanApiVersion>,
305    /// Multiple etherscan api configs and their aliases
306    #[serde(default, skip_serializing_if = "EtherscanConfigs::is_empty")]
307    pub etherscan: EtherscanConfigs,
308    /// list of solidity error codes to always silence in the compiler output
309    pub ignored_error_codes: Vec<SolidityErrorCode>,
310    /// list of file paths to ignore
311    #[serde(rename = "ignored_warnings_from")]
312    pub ignored_file_paths: Vec<PathBuf>,
313    /// When true, compiler warnings are treated as errors
314    pub deny_warnings: bool,
315    /// Only run test functions matching the specified regex pattern.
316    #[serde(rename = "match_test")]
317    pub test_pattern: Option<RegexWrapper>,
318    /// Only run test functions that do not match the specified regex pattern.
319    #[serde(rename = "no_match_test")]
320    pub test_pattern_inverse: Option<RegexWrapper>,
321    /// Only run tests in contracts matching the specified regex pattern.
322    #[serde(rename = "match_contract")]
323    pub contract_pattern: Option<RegexWrapper>,
324    /// Only run tests in contracts that do not match the specified regex pattern.
325    #[serde(rename = "no_match_contract")]
326    pub contract_pattern_inverse: Option<RegexWrapper>,
327    /// Only run tests in source files matching the specified glob pattern.
328    #[serde(rename = "match_path", with = "from_opt_glob")]
329    pub path_pattern: Option<globset::Glob>,
330    /// Only run tests in source files that do not match the specified glob pattern.
331    #[serde(rename = "no_match_path", with = "from_opt_glob")]
332    pub path_pattern_inverse: Option<globset::Glob>,
333    /// Only show coverage for files that do not match the specified regex pattern.
334    #[serde(rename = "no_match_coverage")]
335    pub coverage_pattern_inverse: Option<RegexWrapper>,
336    /// Path where last test run failures are recorded.
337    pub test_failures_file: PathBuf,
338    /// Max concurrent threads to use.
339    pub threads: Option<usize>,
340    /// Whether to show test execution progress.
341    pub show_progress: bool,
342    /// Configuration for fuzz testing
343    pub fuzz: FuzzConfig,
344    /// Configuration for invariant testing
345    pub invariant: InvariantConfig,
346    /// Whether to allow ffi cheatcodes in test
347    pub ffi: bool,
348    /// Whether to allow `expectRevert` for internal functions.
349    pub allow_internal_expect_revert: bool,
350    /// Use the create 2 factory in all cases including tests and non-broadcasting scripts.
351    pub always_use_create_2_factory: bool,
352    /// Sets a timeout in seconds for vm.prompt cheatcodes
353    pub prompt_timeout: u64,
354    /// The address which will be executing all tests
355    pub sender: Address,
356    /// The tx.origin value during EVM execution
357    pub tx_origin: Address,
358    /// the initial balance of each deployed test contract
359    pub initial_balance: U256,
360    /// the block.number value during EVM execution
361    #[serde(
362        deserialize_with = "crate::deserialize_u64_to_u256",
363        serialize_with = "crate::serialize_u64_or_u256"
364    )]
365    pub block_number: U256,
366    /// pins the block number for the state fork
367    pub fork_block_number: Option<u64>,
368    /// The chain name or EIP-155 chain ID.
369    #[serde(rename = "chain_id", alias = "chain")]
370    pub chain: Option<Chain>,
371    /// Block gas limit.
372    pub gas_limit: GasLimit,
373    /// EIP-170: Contract code size limit in bytes. Useful to increase this because of tests.
374    pub code_size_limit: Option<usize>,
375    /// `tx.gasprice` value during EVM execution.
376    ///
377    /// This is an Option, so we can determine in fork mode whether to use the config's gas price
378    /// (if set by user) or the remote client's gas price.
379    pub gas_price: Option<u64>,
380    /// The base fee in a block.
381    pub block_base_fee_per_gas: u64,
382    /// The `block.coinbase` value during EVM execution.
383    pub block_coinbase: Address,
384    /// The `block.timestamp` value during EVM execution.
385    #[serde(
386        deserialize_with = "crate::deserialize_u64_to_u256",
387        serialize_with = "crate::serialize_u64_or_u256"
388    )]
389    pub block_timestamp: U256,
390    /// The `block.difficulty` value during EVM execution.
391    pub block_difficulty: u64,
392    /// Before merge the `block.max_hash`, after merge it is `block.prevrandao`.
393    pub block_prevrandao: B256,
394    /// The `block.gaslimit` value during EVM execution.
395    pub block_gas_limit: Option<GasLimit>,
396    /// The memory limit per EVM execution in bytes.
397    /// If this limit is exceeded, a `MemoryLimitOOG` result is thrown.
398    ///
399    /// The default is 128MiB.
400    pub memory_limit: u64,
401    /// Additional output selection for all contracts, such as "ir", "devdoc", "storageLayout",
402    /// etc.
403    ///
404    /// See the [Solc Compiler Api](https://docs.soliditylang.org/en/latest/using-the-compiler.html#compiler-api) for more information.
405    ///
406    /// The following values are always set because they're required by `forge`:
407    /// ```json
408    /// {
409    ///   "*": [
410    ///       "abi",
411    ///       "evm.bytecode",
412    ///       "evm.deployedBytecode",
413    ///       "evm.methodIdentifiers"
414    ///     ]
415    /// }
416    /// ```
417    #[serde(default)]
418    pub extra_output: Vec<ContractOutputSelection>,
419    /// If set, a separate JSON file will be emitted for every contract depending on the
420    /// selection, eg. `extra_output_files = ["metadata"]` will create a `metadata.json` for
421    /// each contract in the project.
422    ///
423    /// See [Contract Metadata](https://docs.soliditylang.org/en/latest/metadata.html) for more information.
424    ///
425    /// The difference between `extra_output = ["metadata"]` and
426    /// `extra_output_files = ["metadata"]` is that the former will include the
427    /// contract's metadata in the contract's json artifact, whereas the latter will emit the
428    /// output selection as separate files.
429    #[serde(default)]
430    pub extra_output_files: Vec<ContractOutputSelection>,
431    /// Whether to print the names of the compiled contracts.
432    pub names: bool,
433    /// Whether to print the sizes of the compiled contracts.
434    pub sizes: bool,
435    /// If set to true, changes compilation pipeline to go through the Yul intermediate
436    /// representation.
437    pub via_ir: bool,
438    /// Whether to include the AST as JSON in the compiler output.
439    pub ast: bool,
440    /// RPC storage caching settings determines what chains and endpoints to cache
441    pub rpc_storage_caching: StorageCachingConfig,
442    /// Disables storage caching entirely. This overrides any settings made in
443    /// `rpc_storage_caching`
444    pub no_storage_caching: bool,
445    /// Disables rate limiting entirely. This overrides any settings made in
446    /// `compute_units_per_second`
447    pub no_rpc_rate_limit: bool,
448    /// Multiple rpc endpoints and their aliases
449    #[serde(default, skip_serializing_if = "RpcEndpoints::is_empty")]
450    pub rpc_endpoints: RpcEndpoints,
451    /// Whether to store the referenced sources in the metadata as literal data.
452    pub use_literal_content: bool,
453    /// Whether to include the metadata hash.
454    ///
455    /// 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>
456    #[serde(with = "from_str_lowercase")]
457    pub bytecode_hash: BytecodeHash,
458    /// Whether to append the metadata hash to the bytecode.
459    ///
460    /// If this is `false` and the `bytecode_hash` option above is not `None` solc will issue a
461    /// warning.
462    pub cbor_metadata: bool,
463    /// How to treat revert (and require) reason strings.
464    #[serde(with = "serde_helpers::display_from_str_opt")]
465    pub revert_strings: Option<RevertStrings>,
466    /// Whether to compile in sparse mode
467    ///
468    /// If this option is enabled, only the required contracts/files will be selected to be
469    /// included in solc's output selection, see also [`OutputSelection`].
470    pub sparse_mode: bool,
471    /// Generates additional build info json files for every new build, containing the
472    /// `CompilerInput` and `CompilerOutput`.
473    pub build_info: bool,
474    /// The path to the `build-info` directory that contains the build info json files.
475    pub build_info_path: Option<PathBuf>,
476    /// Configuration for `forge fmt`
477    pub fmt: FormatterConfig,
478    /// Configuration for `forge lint`
479    pub lint: LinterConfig,
480    /// Configuration for `forge doc`
481    pub doc: DocConfig,
482    /// Configuration for `forge bind-json`
483    pub bind_json: BindJsonConfig,
484    /// Configures the permissions of cheat codes that touch the file system.
485    ///
486    /// This includes what operations can be executed (read, write)
487    pub fs_permissions: FsPermissions,
488
489    /// Whether to enable call isolation.
490    ///
491    /// Useful for more correct gas accounting and EVM behavior in general.
492    pub isolate: bool,
493
494    /// Whether to disable the block gas limit.
495    pub disable_block_gas_limit: bool,
496
497    /// Address labels
498    pub labels: AddressHashMap<String>,
499
500    /// Whether to enable safety checks for `vm.getCode` and `vm.getDeployedCode` invocations.
501    /// If disabled, it is possible to access artifacts which were not recompiled or cached.
502    pub unchecked_cheatcode_artifacts: bool,
503
504    /// CREATE2 salt to use for the library deployment in scripts.
505    pub create2_library_salt: B256,
506
507    /// The CREATE2 deployer address to use.
508    pub create2_deployer: Address,
509
510    /// Configuration for Vyper compiler
511    pub vyper: VyperConfig,
512
513    /// Soldeer dependencies
514    pub dependencies: Option<SoldeerDependencyConfig>,
515
516    /// Soldeer custom configs
517    pub soldeer: Option<SoldeerConfig>,
518
519    /// Whether failed assertions should revert.
520    ///
521    /// Note that this only applies to native (cheatcode) assertions, invoked on Vm contract.
522    pub assertions_revert: bool,
523
524    /// Whether `failed()` should be invoked to check if the test have failed.
525    pub legacy_assertions: bool,
526
527    /// Optional additional CLI arguments to pass to `solc` binary.
528    #[serde(default, skip_serializing_if = "Vec::is_empty")]
529    pub extra_args: Vec<String>,
530
531    /// Whether to enable Odyssey features.
532    #[serde(alias = "alphanet")]
533    pub odyssey: bool,
534
535    /// Timeout for transactions in seconds.
536    pub transaction_timeout: u64,
537
538    /// Warnings gathered when loading the Config. See [`WarningsProvider`] for more information.
539    #[serde(rename = "__warnings", default, skip_serializing)]
540    pub warnings: Vec<Warning>,
541
542    /// Additional settings profiles to use when compiling.
543    #[serde(default)]
544    pub additional_compiler_profiles: Vec<SettingsOverrides>,
545
546    /// Restrictions on compilation of certain files.
547    #[serde(default)]
548    pub compilation_restrictions: Vec<CompilationRestrictions>,
549
550    /// Whether to enable script execution protection.
551    pub script_execution_protection: bool,
552
553    /// PRIVATE: This structure may grow, As such, constructing this structure should
554    /// _always_ be done using a public constructor or update syntax:
555    ///
556    /// ```ignore
557    /// use foundry_config::Config;
558    ///
559    /// let config = Config { src: "other".into(), ..Default::default() };
560    /// ```
561    #[doc(hidden)]
562    #[serde(skip)]
563    pub _non_exhaustive: (),
564}
565
566/// Mapping of fallback standalone sections. See [`FallbackProfileProvider`].
567pub const STANDALONE_FALLBACK_SECTIONS: &[(&str, &str)] = &[("invariant", "fuzz")];
568
569/// Deprecated keys and their replacements.
570///
571/// See [Warning::DeprecatedKey]
572pub const DEPRECATIONS: &[(&str, &str)] = &[("cancun", "evm_version = Cancun")];
573
574impl Config {
575    /// The default profile: "default"
576    pub const DEFAULT_PROFILE: Profile = Profile::Default;
577
578    /// The hardhat profile: "hardhat"
579    pub const HARDHAT_PROFILE: Profile = Profile::const_new("hardhat");
580
581    /// TOML section for profiles
582    pub const PROFILE_SECTION: &'static str = "profile";
583
584    /// Standalone sections in the config which get integrated into the selected profile
585    pub const STANDALONE_SECTIONS: &'static [&'static str] = &[
586        "rpc_endpoints",
587        "etherscan",
588        "fmt",
589        "lint",
590        "doc",
591        "fuzz",
592        "invariant",
593        "labels",
594        "dependencies",
595        "soldeer",
596        "vyper",
597        "bind_json",
598    ];
599
600    /// File name of config toml file
601    pub const FILE_NAME: &'static str = "foundry.toml";
602
603    /// The name of the directory foundry reserves for itself under the user's home directory: `~`
604    pub const FOUNDRY_DIR_NAME: &'static str = ".foundry";
605
606    /// Default address for tx.origin
607    ///
608    /// `0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38`
609    pub const DEFAULT_SENDER: Address = address!("0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38");
610
611    /// Default salt for create2 library deployments
612    pub const DEFAULT_CREATE2_LIBRARY_SALT: FixedBytes<32> = FixedBytes::<32>::ZERO;
613
614    /// Default create2 deployer
615    pub const DEFAULT_CREATE2_DEPLOYER: Address =
616        address!("0x4e59b44847b379578588920ca78fbf26c0b4956c");
617
618    /// Loads the `Config` from the current directory.
619    ///
620    /// See [`figment`](Self::figment) for more details.
621    pub fn load() -> Result<Self, ExtractConfigError> {
622        Self::from_provider(Self::figment())
623    }
624
625    /// Loads the `Config` with the given `providers` preset.
626    ///
627    /// See [`figment`](Self::figment) for more details.
628    pub fn load_with_providers(providers: FigmentProviders) -> Result<Self, ExtractConfigError> {
629        Self::from_provider(Self::default().to_figment(providers))
630    }
631
632    /// Loads the `Config` from the given root directory.
633    ///
634    /// See [`figment_with_root`](Self::figment_with_root) for more details.
635    #[track_caller]
636    pub fn load_with_root(root: impl AsRef<Path>) -> Result<Self, ExtractConfigError> {
637        Self::from_provider(Self::figment_with_root(root.as_ref()))
638    }
639
640    /// Attempts to extract a `Config` from `provider`, returning the result.
641    ///
642    /// # Example
643    ///
644    /// ```rust
645    /// use figment::providers::{Env, Format, Toml};
646    /// use foundry_config::Config;
647    ///
648    /// // Use foundry's default `Figment`, but allow values from `other.toml`
649    /// // to supersede its values.
650    /// let figment = Config::figment().merge(Toml::file("other.toml").nested());
651    ///
652    /// let config = Config::from_provider(figment);
653    /// ```
654    #[doc(alias = "try_from")]
655    pub fn from_provider<T: Provider>(provider: T) -> Result<Self, ExtractConfigError> {
656        trace!("load config with provider: {:?}", provider.metadata());
657        Self::from_figment(Figment::from(provider))
658    }
659
660    #[doc(hidden)]
661    #[deprecated(note = "use `Config::from_provider` instead")]
662    pub fn try_from<T: Provider>(provider: T) -> Result<Self, ExtractConfigError> {
663        Self::from_provider(provider)
664    }
665
666    fn from_figment(figment: Figment) -> Result<Self, ExtractConfigError> {
667        let mut config = figment.extract::<Self>().map_err(ExtractConfigError::new)?;
668        config.profile = figment.profile().clone();
669
670        // The `"profile"` profile contains all the profiles as keys.
671        let mut add_profile = |profile: &Profile| {
672            if !config.profiles.contains(profile) {
673                config.profiles.push(profile.clone());
674            }
675        };
676        let figment = figment.select(Self::PROFILE_SECTION);
677        if let Ok(data) = figment.data()
678            && let Some(profiles) = data.get(&Profile::new(Self::PROFILE_SECTION))
679        {
680            for profile in profiles.keys() {
681                add_profile(&Profile::new(profile));
682            }
683        }
684        add_profile(&Self::DEFAULT_PROFILE);
685        add_profile(&config.profile);
686
687        config.normalize_optimizer_settings();
688
689        Ok(config)
690    }
691
692    /// Returns the populated [Figment] using the requested [FigmentProviders] preset.
693    ///
694    /// This will merge various providers, such as env,toml,remappings into the figment if
695    /// requested.
696    pub fn to_figment(&self, providers: FigmentProviders) -> Figment {
697        // Note that `Figment::from` here is a method on `Figment` rather than the `From` impl below
698
699        if providers.is_none() {
700            return Figment::from(self);
701        }
702
703        let root = self.root.as_path();
704        let profile = Self::selected_profile();
705        let mut figment = Figment::default().merge(DappHardhatDirProvider(root));
706
707        // merge global foundry.toml file
708        if let Some(global_toml) = Self::foundry_dir_toml().filter(|p| p.exists()) {
709            figment = Self::merge_toml_provider(
710                figment,
711                TomlFileProvider::new(None, global_toml).cached(),
712                profile.clone(),
713            );
714        }
715        // merge local foundry.toml file
716        figment = Self::merge_toml_provider(
717            figment,
718            TomlFileProvider::new(Some("FOUNDRY_CONFIG"), root.join(Self::FILE_NAME)).cached(),
719            profile.clone(),
720        );
721
722        // merge environment variables
723        figment = figment
724            .merge(
725                Env::prefixed("DAPP_")
726                    .ignore(&["REMAPPINGS", "LIBRARIES", "FFI", "FS_PERMISSIONS"])
727                    .global(),
728            )
729            .merge(
730                Env::prefixed("DAPP_TEST_")
731                    .ignore(&["CACHE", "FUZZ_RUNS", "DEPTH", "FFI", "FS_PERMISSIONS"])
732                    .global(),
733            )
734            .merge(DappEnvCompatProvider)
735            .merge(EtherscanEnvProvider::default())
736            .merge(
737                Env::prefixed("FOUNDRY_")
738                    .ignore(&["PROFILE", "REMAPPINGS", "LIBRARIES", "FFI", "FS_PERMISSIONS"])
739                    .map(|key| {
740                        let key = key.as_str();
741                        if Self::STANDALONE_SECTIONS.iter().any(|section| {
742                            key.starts_with(&format!("{}_", section.to_ascii_uppercase()))
743                        }) {
744                            key.replacen('_', ".", 1).into()
745                        } else {
746                            key.into()
747                        }
748                    })
749                    .global(),
750            )
751            .select(profile.clone());
752
753        // only resolve remappings if all providers are requested
754        if providers.is_all() {
755            // we try to merge remappings after we've merged all other providers, this prevents
756            // redundant fs lookups to determine the default remappings that are eventually updated
757            // by other providers, like the toml file
758            let remappings = RemappingsProvider {
759                auto_detect_remappings: figment
760                    .extract_inner::<bool>("auto_detect_remappings")
761                    .unwrap_or(true),
762                lib_paths: figment
763                    .extract_inner::<Vec<PathBuf>>("libs")
764                    .map(Cow::Owned)
765                    .unwrap_or_else(|_| Cow::Borrowed(&self.libs)),
766                root,
767                remappings: figment.extract_inner::<Vec<Remapping>>("remappings"),
768            };
769            figment = figment.merge(remappings);
770        }
771
772        // normalize defaults
773        figment = self.normalize_defaults(figment);
774
775        Figment::from(self).merge(figment).select(profile)
776    }
777
778    /// The config supports relative paths and tracks the root path separately see
779    /// `Config::with_root`
780    ///
781    /// This joins all relative paths with the current root and attempts to make them canonic
782    #[must_use]
783    pub fn canonic(self) -> Self {
784        let root = self.root.clone();
785        self.canonic_at(root)
786    }
787
788    /// Joins all relative paths with the given root so that paths that are defined as:
789    ///
790    /// ```toml
791    /// [profile.default]
792    /// src = "src"
793    /// out = "./out"
794    /// libs = ["lib", "/var/lib"]
795    /// ```
796    ///
797    /// Will be made canonic with the given root:
798    ///
799    /// ```toml
800    /// [profile.default]
801    /// src = "<root>/src"
802    /// out = "<root>/out"
803    /// libs = ["<root>/lib", "/var/lib"]
804    /// ```
805    #[must_use]
806    pub fn canonic_at(mut self, root: impl Into<PathBuf>) -> Self {
807        let root = canonic(root);
808
809        fn p(root: &Path, rem: &Path) -> PathBuf {
810            canonic(root.join(rem))
811        }
812
813        self.src = p(&root, &self.src);
814        self.test = p(&root, &self.test);
815        self.script = p(&root, &self.script);
816        self.out = p(&root, &self.out);
817        self.broadcast = p(&root, &self.broadcast);
818        self.cache_path = p(&root, &self.cache_path);
819        self.snapshots = p(&root, &self.snapshots);
820
821        if let Some(build_info_path) = self.build_info_path {
822            self.build_info_path = Some(p(&root, &build_info_path));
823        }
824
825        self.libs = self.libs.into_iter().map(|lib| p(&root, &lib)).collect();
826
827        self.remappings =
828            self.remappings.into_iter().map(|r| RelativeRemapping::new(r.into(), &root)).collect();
829
830        self.allow_paths = self.allow_paths.into_iter().map(|allow| p(&root, &allow)).collect();
831
832        self.include_paths = self.include_paths.into_iter().map(|allow| p(&root, &allow)).collect();
833
834        self.fs_permissions.join_all(&root);
835
836        if let Some(model_checker) = &mut self.model_checker {
837            model_checker.contracts = std::mem::take(&mut model_checker.contracts)
838                .into_iter()
839                .map(|(path, contracts)| {
840                    (format!("{}", p(&root, path.as_ref()).display()), contracts)
841                })
842                .collect();
843        }
844
845        self
846    }
847
848    /// Normalizes the evm version if a [SolcReq] is set
849    pub fn normalized_evm_version(mut self) -> Self {
850        self.normalize_evm_version();
851        self
852    }
853
854    /// Normalizes optimizer settings.
855    /// See <https://github.com/foundry-rs/foundry/issues/9665>
856    pub fn normalized_optimizer_settings(mut self) -> Self {
857        self.normalize_optimizer_settings();
858        self
859    }
860
861    /// Normalizes the evm version if a [SolcReq] is set to a valid version.
862    pub fn normalize_evm_version(&mut self) {
863        self.evm_version = self.get_normalized_evm_version();
864    }
865
866    /// Normalizes optimizer settings:
867    /// - with default settings, optimizer is set to false and optimizer runs to 200
868    /// - if optimizer is set and optimizer runs not specified, then optimizer runs is set to 200
869    /// - enable optimizer if not explicitly set and optimizer runs set to a value greater than 0
870    pub fn normalize_optimizer_settings(&mut self) {
871        match (self.optimizer, self.optimizer_runs) {
872            // Default: set the optimizer to false and optimizer runs to 200.
873            (None, None) => {
874                self.optimizer = Some(false);
875                self.optimizer_runs = Some(200);
876            }
877            // Set the optimizer runs to 200 if the `optimizer` config set.
878            (Some(_), None) => self.optimizer_runs = Some(200),
879            // Enables optimizer if the `optimizer_runs` has been set with a value greater than 0.
880            (None, Some(runs)) => self.optimizer = Some(runs > 0),
881            _ => {}
882        }
883    }
884
885    /// Returns the normalized [EvmVersion] for the current solc version, or the configured one.
886    pub fn get_normalized_evm_version(&self) -> EvmVersion {
887        if let Some(version) = self.solc_version()
888            && let Some(evm_version) = self.evm_version.normalize_version_solc(&version)
889        {
890            return evm_version;
891        }
892        self.evm_version
893    }
894
895    /// Returns a sanitized version of the Config where are paths are set correctly and potential
896    /// duplicates are resolved
897    ///
898    /// See [`Self::canonic`]
899    #[must_use]
900    pub fn sanitized(self) -> Self {
901        let mut config = self.canonic();
902
903        config.sanitize_remappings();
904
905        config.libs.sort_unstable();
906        config.libs.dedup();
907
908        config
909    }
910
911    /// Cleans up any duplicate `Remapping` and sorts them
912    ///
913    /// On windows this will convert any `\` in the remapping path into a `/`
914    pub fn sanitize_remappings(&mut self) {
915        #[cfg(target_os = "windows")]
916        {
917            // force `/` in remappings on windows
918            use path_slash::PathBufExt;
919            self.remappings.iter_mut().for_each(|r| {
920                r.path.path = r.path.path.to_slash_lossy().into_owned().into();
921            });
922        }
923    }
924
925    /// Returns the directory in which dependencies should be installed
926    ///
927    /// Returns the first dir from `libs` that is not `node_modules` or `lib` if `libs` is empty
928    pub fn install_lib_dir(&self) -> &Path {
929        self.libs
930            .iter()
931            .find(|p| !p.ends_with("node_modules"))
932            .map(|p| p.as_path())
933            .unwrap_or_else(|| Path::new("lib"))
934    }
935
936    /// Serves as the entrypoint for obtaining the project.
937    ///
938    /// Returns the `Project` configured with all `solc` and path related values.
939    ///
940    /// *Note*: this also _cleans_ [`Project::cleanup`] the workspace if `force` is set to true.
941    ///
942    /// # Example
943    ///
944    /// ```
945    /// use foundry_config::Config;
946    /// let config = Config::load_with_root(".")?.sanitized();
947    /// let project = config.project()?;
948    /// # Ok::<_, eyre::Error>(())
949    /// ```
950    pub fn project(&self) -> Result<Project<MultiCompiler>, SolcError> {
951        self.create_project(self.cache, false)
952    }
953
954    /// Same as [`Self::project()`] but sets configures the project to not emit artifacts and ignore
955    /// cache.
956    pub fn ephemeral_project(&self) -> Result<Project<MultiCompiler>, SolcError> {
957        self.create_project(false, true)
958    }
959
960    /// A cached, in-memory project that does not request any artifacts.
961    ///
962    /// Use this when you just want the source graph or the Solar compiler context.
963    pub fn solar_project(&self) -> Result<Project<MultiCompiler>, SolcError> {
964        let mut project = self.project()?;
965        project.update_output_selection(|selection| {
966            *selection = OutputSelection::common_output_selection([]);
967        });
968        Ok(project)
969    }
970
971    /// Builds mapping with additional settings profiles.
972    fn additional_settings(
973        &self,
974        base: &MultiCompilerSettings,
975    ) -> BTreeMap<String, MultiCompilerSettings> {
976        let mut map = BTreeMap::new();
977
978        for profile in &self.additional_compiler_profiles {
979            let mut settings = base.clone();
980            profile.apply(&mut settings);
981            map.insert(profile.name.clone(), settings);
982        }
983
984        map
985    }
986
987    /// Resolves globs and builds a mapping from individual source files to their restrictions
988    #[expect(clippy::disallowed_macros)]
989    fn restrictions(
990        &self,
991        paths: &ProjectPathsConfig,
992    ) -> Result<BTreeMap<PathBuf, RestrictionsWithVersion<MultiCompilerRestrictions>>, SolcError>
993    {
994        let mut map = BTreeMap::new();
995        if self.compilation_restrictions.is_empty() {
996            return Ok(BTreeMap::new());
997        }
998
999        let graph = Graph::<MultiCompilerParser>::resolve(paths)?;
1000        let (sources, _) = graph.into_sources();
1001
1002        for res in &self.compilation_restrictions {
1003            for source in sources.keys().filter(|path| {
1004                if res.paths.is_match(path) {
1005                    true
1006                } else if let Ok(path) = path.strip_prefix(&paths.root) {
1007                    res.paths.is_match(path)
1008                } else {
1009                    false
1010                }
1011            }) {
1012                let res: RestrictionsWithVersion<_> =
1013                    res.clone().try_into().map_err(SolcError::msg)?;
1014                if !map.contains_key(source) {
1015                    map.insert(source.clone(), res);
1016                } else {
1017                    let value = map.remove(source.as_path()).unwrap();
1018                    if let Some(merged) = value.clone().merge(res) {
1019                        map.insert(source.clone(), merged);
1020                    } else {
1021                        // `sh_warn!` is a circular dependency, preventing us from using it here.
1022                        eprintln!(
1023                            "{}",
1024                            yansi::Paint::yellow(&format!(
1025                                "Failed to merge compilation restrictions for {}",
1026                                source.display()
1027                            ))
1028                        );
1029                        map.insert(source.clone(), value);
1030                    }
1031                }
1032            }
1033        }
1034
1035        Ok(map)
1036    }
1037
1038    /// Creates a [`Project`] with the given `cached` and `no_artifacts` flags.
1039    ///
1040    /// Prefer using [`Self::project`] or [`Self::ephemeral_project`] instead.
1041    pub fn create_project(&self, cached: bool, no_artifacts: bool) -> Result<Project, SolcError> {
1042        let settings = self.compiler_settings()?;
1043        let paths = self.project_paths();
1044        let mut builder = Project::builder()
1045            .artifacts(self.configured_artifacts_handler())
1046            .additional_settings(self.additional_settings(&settings))
1047            .restrictions(self.restrictions(&paths)?)
1048            .settings(settings)
1049            .paths(paths)
1050            .ignore_error_codes(self.ignored_error_codes.iter().copied().map(Into::into))
1051            .ignore_paths(self.ignored_file_paths.clone())
1052            .set_compiler_severity_filter(if self.deny_warnings {
1053                Severity::Warning
1054            } else {
1055                Severity::Error
1056            })
1057            .set_offline(self.offline)
1058            .set_cached(cached)
1059            .set_build_info(!no_artifacts && self.build_info)
1060            .set_no_artifacts(no_artifacts);
1061
1062        if !self.skip.is_empty() {
1063            let filter = SkipBuildFilters::new(self.skip.clone(), self.root.clone());
1064            builder = builder.sparse_output(filter);
1065        }
1066
1067        let project = builder.build(self.compiler()?)?;
1068
1069        if self.force {
1070            self.cleanup(&project)?;
1071        }
1072
1073        Ok(project)
1074    }
1075
1076    /// Cleans the project.
1077    pub fn cleanup<C: Compiler, T: ArtifactOutput<CompilerContract = C::CompilerContract>>(
1078        &self,
1079        project: &Project<C, T>,
1080    ) -> Result<(), SolcError> {
1081        project.cleanup()?;
1082
1083        // Remove last test run failures file.
1084        let _ = fs::remove_file(&self.test_failures_file);
1085
1086        // Remove fuzz and invariant cache directories.
1087        let remove_test_dir = |test_dir: &Option<PathBuf>| {
1088            if let Some(test_dir) = test_dir {
1089                let path = project.root().join(test_dir);
1090                if path.exists() {
1091                    let _ = fs::remove_dir_all(&path);
1092                }
1093            }
1094        };
1095        remove_test_dir(&self.fuzz.failure_persist_dir);
1096        remove_test_dir(&self.fuzz.corpus.corpus_dir);
1097        remove_test_dir(&self.invariant.corpus.corpus_dir);
1098        remove_test_dir(&self.invariant.failure_persist_dir);
1099
1100        Ok(())
1101    }
1102
1103    /// Ensures that the configured version is installed if explicitly set
1104    ///
1105    /// If `solc` is [`SolcReq::Version`] then this will download and install the solc version if
1106    /// it's missing, unless the `offline` flag is enabled, in which case an error is thrown.
1107    ///
1108    /// If `solc` is [`SolcReq::Local`] then this will ensure that the path exists.
1109    fn ensure_solc(&self) -> Result<Option<Solc>, SolcError> {
1110        if let Some(solc) = &self.solc {
1111            let solc = match solc {
1112                SolcReq::Version(version) => {
1113                    if let Some(solc) = Solc::find_svm_installed_version(version)? {
1114                        solc
1115                    } else {
1116                        if self.offline {
1117                            return Err(SolcError::msg(format!(
1118                                "can't install missing solc {version} in offline mode"
1119                            )));
1120                        }
1121                        Solc::blocking_install(version)?
1122                    }
1123                }
1124                SolcReq::Local(solc) => {
1125                    if !solc.is_file() {
1126                        return Err(SolcError::msg(format!(
1127                            "`solc` {} does not exist",
1128                            solc.display()
1129                        )));
1130                    }
1131                    Solc::new(solc)?
1132                }
1133            };
1134            return Ok(Some(solc));
1135        }
1136
1137        Ok(None)
1138    }
1139
1140    /// Returns the [SpecId] derived from the configured [EvmVersion]
1141    pub fn evm_spec_id(&self) -> SpecId {
1142        evm_spec_id(self.evm_version, self.odyssey)
1143    }
1144
1145    /// Returns whether the compiler version should be auto-detected
1146    ///
1147    /// Returns `false` if `solc_version` is explicitly set, otherwise returns the value of
1148    /// `auto_detect_solc`
1149    pub fn is_auto_detect(&self) -> bool {
1150        if self.solc.is_some() {
1151            return false;
1152        }
1153        self.auto_detect_solc
1154    }
1155
1156    /// Whether caching should be enabled for the given chain id
1157    pub fn enable_caching(&self, endpoint: &str, chain_id: impl Into<u64>) -> bool {
1158        !self.no_storage_caching
1159            && self.rpc_storage_caching.enable_for_chain_id(chain_id.into())
1160            && self.rpc_storage_caching.enable_for_endpoint(endpoint)
1161    }
1162
1163    /// Returns the `ProjectPathsConfig` sub set of the config.
1164    ///
1165    /// **NOTE**: this uses the paths as they are and does __not__ modify them, see
1166    /// `[Self::sanitized]`
1167    ///
1168    /// # Example
1169    ///
1170    /// ```
1171    /// use foundry_compilers::solc::Solc;
1172    /// use foundry_config::Config;
1173    /// let config = Config::load_with_root(".")?.sanitized();
1174    /// let paths = config.project_paths::<Solc>();
1175    /// # Ok::<_, eyre::Error>(())
1176    /// ```
1177    pub fn project_paths<L>(&self) -> ProjectPathsConfig<L> {
1178        let mut builder = ProjectPathsConfig::builder()
1179            .cache(self.cache_path.join(SOLIDITY_FILES_CACHE_FILENAME))
1180            .sources(&self.src)
1181            .tests(&self.test)
1182            .scripts(&self.script)
1183            .artifacts(&self.out)
1184            .libs(self.libs.iter())
1185            .remappings(self.get_all_remappings())
1186            .allowed_path(&self.root)
1187            .allowed_paths(&self.libs)
1188            .allowed_paths(&self.allow_paths)
1189            .include_paths(&self.include_paths);
1190
1191        if let Some(build_info_path) = &self.build_info_path {
1192            builder = builder.build_infos(build_info_path);
1193        }
1194
1195        builder.build_with_root(&self.root)
1196    }
1197
1198    /// Returns configuration for a compiler to use when setting up a [Project].
1199    pub fn solc_compiler(&self) -> Result<SolcCompiler, SolcError> {
1200        if let Some(solc) = self.ensure_solc()? {
1201            Ok(SolcCompiler::Specific(solc))
1202        } else {
1203            Ok(SolcCompiler::AutoDetect)
1204        }
1205    }
1206
1207    /// Returns the solc version, if any.
1208    pub fn solc_version(&self) -> Option<Version> {
1209        self.solc.as_ref().and_then(|solc| solc.try_version().ok())
1210    }
1211
1212    /// Returns configured [Vyper] compiler.
1213    pub fn vyper_compiler(&self) -> Result<Option<Vyper>, SolcError> {
1214        // Only instantiate Vyper if there are any Vyper files in the project.
1215        if !self.project_paths::<VyperLanguage>().has_input_files() {
1216            return Ok(None);
1217        }
1218        let vyper = if let Some(path) = &self.vyper.path {
1219            Some(Vyper::new(path)?)
1220        } else {
1221            Vyper::new("vyper").ok()
1222        };
1223        Ok(vyper)
1224    }
1225
1226    /// Returns configuration for a compiler to use when setting up a [Project].
1227    pub fn compiler(&self) -> Result<MultiCompiler, SolcError> {
1228        Ok(MultiCompiler { solc: Some(self.solc_compiler()?), vyper: self.vyper_compiler()? })
1229    }
1230
1231    /// Returns configured [MultiCompilerSettings].
1232    pub fn compiler_settings(&self) -> Result<MultiCompilerSettings, SolcError> {
1233        Ok(MultiCompilerSettings { solc: self.solc_settings()?, vyper: self.vyper_settings()? })
1234    }
1235
1236    /// Returns all configured remappings.
1237    pub fn get_all_remappings(&self) -> impl Iterator<Item = Remapping> + '_ {
1238        self.remappings.iter().map(|m| m.clone().into())
1239    }
1240
1241    /// Returns the configured rpc jwt secret
1242    ///
1243    /// Returns:
1244    ///    - The jwt secret, if configured
1245    ///
1246    /// # Example
1247    ///
1248    /// ```
1249    /// use foundry_config::Config;
1250    /// # fn t() {
1251    /// let config = Config::with_root("./");
1252    /// let rpc_jwt = config.get_rpc_jwt_secret().unwrap().unwrap();
1253    /// # }
1254    /// ```
1255    pub fn get_rpc_jwt_secret(&self) -> Result<Option<Cow<'_, str>>, UnresolvedEnvVarError> {
1256        Ok(self.eth_rpc_jwt.as_ref().map(|jwt| Cow::Borrowed(jwt.as_str())))
1257    }
1258
1259    /// Returns the configured rpc url
1260    ///
1261    /// Returns:
1262    ///    - the matching, resolved url of  `rpc_endpoints` if `eth_rpc_url` is an alias
1263    ///    - the `eth_rpc_url` as-is if it isn't an alias
1264    ///
1265    /// # Example
1266    ///
1267    /// ```
1268    /// use foundry_config::Config;
1269    /// # fn t() {
1270    /// let config = Config::with_root("./");
1271    /// let rpc_url = config.get_rpc_url().unwrap().unwrap();
1272    /// # }
1273    /// ```
1274    pub fn get_rpc_url(&self) -> Option<Result<Cow<'_, str>, UnresolvedEnvVarError>> {
1275        let maybe_alias = self.eth_rpc_url.as_ref().or(self.etherscan_api_key.as_ref())?;
1276        if let Some(alias) = self.get_rpc_url_with_alias(maybe_alias) {
1277            Some(alias)
1278        } else {
1279            Some(Ok(Cow::Borrowed(self.eth_rpc_url.as_deref()?)))
1280        }
1281    }
1282
1283    /// Resolves the given alias to a matching rpc url
1284    ///
1285    /// # Returns
1286    ///
1287    /// In order of resolution:
1288    ///
1289    /// - the matching, resolved url of `rpc_endpoints` if `maybe_alias` is an alias
1290    /// - a mesc resolved url if `maybe_alias` is a known alias in mesc
1291    /// - `None` otherwise
1292    ///
1293    /// # Note on mesc
1294    ///
1295    /// The endpoint is queried for in mesc under the `foundry` profile, allowing users to customize
1296    /// endpoints for Foundry specifically.
1297    ///
1298    /// # Example
1299    ///
1300    /// ```
1301    /// use foundry_config::Config;
1302    /// # fn t() {
1303    /// let config = Config::with_root("./");
1304    /// let rpc_url = config.get_rpc_url_with_alias("mainnet").unwrap().unwrap();
1305    /// # }
1306    /// ```
1307    pub fn get_rpc_url_with_alias(
1308        &self,
1309        maybe_alias: &str,
1310    ) -> Option<Result<Cow<'_, str>, UnresolvedEnvVarError>> {
1311        let mut endpoints = self.rpc_endpoints.clone().resolved();
1312        if let Some(endpoint) = endpoints.remove(maybe_alias) {
1313            return Some(endpoint.url().map(Cow::Owned));
1314        }
1315
1316        if let Some(mesc_url) = self.get_rpc_url_from_mesc(maybe_alias) {
1317            return Some(Ok(Cow::Owned(mesc_url)));
1318        }
1319
1320        None
1321    }
1322
1323    /// Attempts to resolve the URL for the given alias from [`mesc`](https://github.com/paradigmxyz/mesc)
1324    pub fn get_rpc_url_from_mesc(&self, maybe_alias: &str) -> Option<String> {
1325        // Note: mesc requires a MESC_PATH in the env, which the user can configure and is expected
1326        // to be part of the shell profile, default is ~/mesc.json
1327        let mesc_config = mesc::load::load_config_data()
1328            .inspect_err(|err| debug!(%err, "failed to load mesc config"))
1329            .ok()?;
1330
1331        if let Ok(Some(endpoint)) =
1332            mesc::query::get_endpoint_by_query(&mesc_config, maybe_alias, Some("foundry"))
1333        {
1334            return Some(endpoint.url);
1335        }
1336
1337        if maybe_alias.chars().all(|c| c.is_numeric()) {
1338            // try to lookup the mesc network by chain id if alias is numeric
1339            // This only succeeds if the chain id has a default:
1340            // "network_defaults": {
1341            //    "50104": "sophon_50104"
1342            // }
1343            if let Ok(Some(endpoint)) =
1344                mesc::query::get_endpoint_by_network(&mesc_config, maybe_alias, Some("foundry"))
1345            {
1346                return Some(endpoint.url);
1347            }
1348        }
1349
1350        None
1351    }
1352
1353    /// Returns the configured rpc, or the fallback url
1354    ///
1355    /// # Example
1356    ///
1357    /// ```
1358    /// use foundry_config::Config;
1359    /// # fn t() {
1360    /// let config = Config::with_root("./");
1361    /// let rpc_url = config.get_rpc_url_or("http://localhost:8545").unwrap();
1362    /// # }
1363    /// ```
1364    pub fn get_rpc_url_or<'a>(
1365        &'a self,
1366        fallback: impl Into<Cow<'a, str>>,
1367    ) -> Result<Cow<'a, str>, UnresolvedEnvVarError> {
1368        if let Some(url) = self.get_rpc_url() { url } else { Ok(fallback.into()) }
1369    }
1370
1371    /// Returns the configured rpc or `"http://localhost:8545"` if no `eth_rpc_url` is set
1372    ///
1373    /// # Example
1374    ///
1375    /// ```
1376    /// use foundry_config::Config;
1377    /// # fn t() {
1378    /// let config = Config::with_root("./");
1379    /// let rpc_url = config.get_rpc_url_or_localhost_http().unwrap();
1380    /// # }
1381    /// ```
1382    pub fn get_rpc_url_or_localhost_http(&self) -> Result<Cow<'_, str>, UnresolvedEnvVarError> {
1383        self.get_rpc_url_or("http://localhost:8545")
1384    }
1385
1386    /// Returns the `EtherscanConfig` to use, if any
1387    ///
1388    /// Returns
1389    ///  - the matching `ResolvedEtherscanConfig` of the `etherscan` table if `etherscan_api_key` is
1390    ///    an alias
1391    ///  - the matching `ResolvedEtherscanConfig` of the `etherscan` table if a `chain` is
1392    ///    configured. an alias
1393    ///  - the Mainnet  `ResolvedEtherscanConfig` if `etherscan_api_key` is set, `None` otherwise
1394    ///
1395    /// # Example
1396    ///
1397    /// ```
1398    /// use foundry_config::Config;
1399    /// # fn t() {
1400    /// let config = Config::with_root("./");
1401    /// let etherscan_config = config.get_etherscan_config().unwrap().unwrap();
1402    /// let client = etherscan_config.into_client().unwrap();
1403    /// # }
1404    /// ```
1405    pub fn get_etherscan_config(
1406        &self,
1407    ) -> Option<Result<ResolvedEtherscanConfig, EtherscanConfigError>> {
1408        self.get_etherscan_config_with_chain(None).transpose()
1409    }
1410
1411    /// Same as [`Self::get_etherscan_config()`] but optionally updates the config with the given
1412    /// `chain`, and `etherscan_api_key`
1413    ///
1414    /// If not matching alias was found, then this will try to find the first entry in the table
1415    /// with a matching chain id. If an etherscan_api_key is already set it will take precedence
1416    /// over the chain's entry in the table.
1417    pub fn get_etherscan_config_with_chain(
1418        &self,
1419        chain: Option<Chain>,
1420    ) -> Result<Option<ResolvedEtherscanConfig>, EtherscanConfigError> {
1421        let default_api_version = self.etherscan_api_version.unwrap_or_default();
1422
1423        if let Some(maybe_alias) = self.etherscan_api_key.as_ref().or(self.eth_rpc_url.as_ref())
1424            && self.etherscan.contains_key(maybe_alias)
1425        {
1426            return self
1427                .etherscan
1428                .clone()
1429                .resolved(default_api_version)
1430                .remove(maybe_alias)
1431                .transpose();
1432        }
1433
1434        // try to find by comparing chain IDs after resolving
1435        if let Some(res) = chain.or(self.chain).and_then(|chain| {
1436            self.etherscan.clone().resolved(default_api_version).find_chain(chain)
1437        }) {
1438            match (res, self.etherscan_api_key.as_ref()) {
1439                (Ok(mut config), Some(key)) => {
1440                    // we update the key, because if an etherscan_api_key is set, it should take
1441                    // precedence over the entry, since this is usually set via env var or CLI args.
1442                    config.key.clone_from(key);
1443                    return Ok(Some(config));
1444                }
1445                (Ok(config), None) => return Ok(Some(config)),
1446                (Err(err), None) => return Err(err),
1447                (Err(_), Some(_)) => {
1448                    // use the etherscan key as fallback
1449                }
1450            }
1451        }
1452
1453        // etherscan fallback via API key
1454        if let Some(key) = self.etherscan_api_key.as_ref() {
1455            match ResolvedEtherscanConfig::create(
1456                key,
1457                chain.or(self.chain).unwrap_or_default(),
1458                default_api_version,
1459            ) {
1460                Some(config) => return Ok(Some(config)),
1461                None => {
1462                    return Err(EtherscanConfigError::UnknownChain(
1463                        String::new(),
1464                        chain.unwrap_or_default(),
1465                    ));
1466                }
1467            }
1468        }
1469        Ok(None)
1470    }
1471
1472    /// Helper function to just get the API key
1473    ///
1474    /// Optionally updates the config with the given `chain`.
1475    ///
1476    /// See also [Self::get_etherscan_config_with_chain]
1477    pub fn get_etherscan_api_key(&self, chain: Option<Chain>) -> Option<String> {
1478        self.get_etherscan_config_with_chain(chain).ok().flatten().map(|c| c.key)
1479    }
1480
1481    /// Helper function to get the API version.
1482    ///
1483    /// See also [Self::get_etherscan_config_with_chain]
1484    pub fn get_etherscan_api_version(&self, chain: Option<Chain>) -> EtherscanApiVersion {
1485        self.get_etherscan_config_with_chain(chain)
1486            .ok()
1487            .flatten()
1488            .map(|c| c.api_version)
1489            .unwrap_or_default()
1490    }
1491
1492    /// Returns the remapping for the project's _src_ directory
1493    ///
1494    /// **Note:** this will add an additional `<src>/=<src path>` remapping here so imports that
1495    /// look like `import {Foo} from "src/Foo.sol";` are properly resolved.
1496    ///
1497    /// 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.
1498    pub fn get_source_dir_remapping(&self) -> Option<Remapping> {
1499        get_dir_remapping(&self.src)
1500    }
1501
1502    /// Returns the remapping for the project's _test_ directory, but only if it exists
1503    pub fn get_test_dir_remapping(&self) -> Option<Remapping> {
1504        if self.root.join(&self.test).exists() { get_dir_remapping(&self.test) } else { None }
1505    }
1506
1507    /// Returns the remapping for the project's _script_ directory, but only if it exists
1508    pub fn get_script_dir_remapping(&self) -> Option<Remapping> {
1509        if self.root.join(&self.script).exists() { get_dir_remapping(&self.script) } else { None }
1510    }
1511
1512    /// Returns the `Optimizer` based on the configured settings
1513    ///
1514    /// Note: optimizer details can be set independently of `enabled`
1515    /// See also: <https://github.com/foundry-rs/foundry/issues/7689>
1516    /// and  <https://github.com/ethereum/solidity/blob/bbb7f58be026fdc51b0b4694a6f25c22a1425586/docs/using-the-compiler.rst?plain=1#L293-L294>
1517    pub fn optimizer(&self) -> Optimizer {
1518        Optimizer {
1519            enabled: self.optimizer,
1520            runs: self.optimizer_runs,
1521            // we always set the details because `enabled` is effectively a specific details profile
1522            // that can still be modified
1523            details: self.optimizer_details.clone(),
1524        }
1525    }
1526
1527    /// returns the [`foundry_compilers::ConfigurableArtifacts`] for this config, that includes the
1528    /// `extra_output` fields
1529    pub fn configured_artifacts_handler(&self) -> ConfigurableArtifacts {
1530        let mut extra_output = self.extra_output.clone();
1531
1532        // Sourcify verification requires solc metadata output. Since, it doesn't
1533        // affect the UX & performance of the compiler, output the metadata files
1534        // by default.
1535        // For more info see: <https://github.com/foundry-rs/foundry/issues/2795>
1536        // Metadata is not emitted as separate file because this breaks typechain support: <https://github.com/foundry-rs/foundry/issues/2969>
1537        if !extra_output.contains(&ContractOutputSelection::Metadata) {
1538            extra_output.push(ContractOutputSelection::Metadata);
1539        }
1540
1541        ConfigurableArtifacts::new(extra_output, self.extra_output_files.iter().copied())
1542    }
1543
1544    /// Parses all libraries in the form of
1545    /// `<file>:<lib>:<addr>`
1546    pub fn parsed_libraries(&self) -> Result<Libraries, SolcError> {
1547        Libraries::parse(&self.libraries)
1548    }
1549
1550    /// Returns all libraries with applied remappings. Same as `self.solc_settings()?.libraries`.
1551    pub fn libraries_with_remappings(&self) -> Result<Libraries, SolcError> {
1552        let paths: ProjectPathsConfig = self.project_paths();
1553        Ok(self.parsed_libraries()?.apply(|libs| paths.apply_lib_remappings(libs)))
1554    }
1555
1556    /// Returns the configured `solc` `Settings` that includes:
1557    /// - all libraries
1558    /// - the optimizer (including details, if configured)
1559    /// - evm version
1560    pub fn solc_settings(&self) -> Result<SolcSettings, SolcError> {
1561        // By default if no targets are specifically selected the model checker uses all targets.
1562        // This might be too much here, so only enable assertion checks.
1563        // If users wish to enable all options they need to do so explicitly.
1564        let mut model_checker = self.model_checker.clone();
1565        if let Some(model_checker_settings) = &mut model_checker
1566            && model_checker_settings.targets.is_none()
1567        {
1568            model_checker_settings.targets = Some(vec![ModelCheckerTarget::Assert]);
1569        }
1570
1571        let mut settings = Settings {
1572            libraries: self.libraries_with_remappings()?,
1573            optimizer: self.optimizer(),
1574            evm_version: Some(self.evm_version),
1575            metadata: Some(SettingsMetadata {
1576                use_literal_content: Some(self.use_literal_content),
1577                bytecode_hash: Some(self.bytecode_hash),
1578                cbor_metadata: Some(self.cbor_metadata),
1579            }),
1580            debug: self.revert_strings.map(|revert_strings| DebuggingSettings {
1581                revert_strings: Some(revert_strings),
1582                // Not used.
1583                debug_info: Vec::new(),
1584            }),
1585            model_checker,
1586            via_ir: Some(self.via_ir),
1587            // Not used.
1588            stop_after: None,
1589            // Set in project paths.
1590            remappings: Vec::new(),
1591            // Set with `with_extra_output` below.
1592            output_selection: Default::default(),
1593        }
1594        .with_extra_output(self.configured_artifacts_handler().output_selection());
1595
1596        // We're keeping AST in `--build-info` for backwards compatibility with HardHat.
1597        if self.ast || self.build_info {
1598            settings = settings.with_ast();
1599        }
1600
1601        let cli_settings =
1602            CliSettings { extra_args: self.extra_args.clone(), ..Default::default() };
1603
1604        Ok(SolcSettings { settings, cli_settings })
1605    }
1606
1607    /// Returns the configured [VyperSettings] that includes:
1608    /// - evm version
1609    pub fn vyper_settings(&self) -> Result<VyperSettings, SolcError> {
1610        Ok(VyperSettings {
1611            evm_version: Some(self.evm_version),
1612            optimize: self.vyper.optimize,
1613            bytecode_metadata: None,
1614            // TODO: We don't yet have a way to deserialize other outputs correctly, so request only
1615            // those for now. It should be enough to run tests and deploy contracts.
1616            output_selection: OutputSelection::common_output_selection([
1617                "abi".to_string(),
1618                "evm.bytecode".to_string(),
1619                "evm.deployedBytecode".to_string(),
1620            ]),
1621            search_paths: None,
1622            experimental_codegen: self.vyper.experimental_codegen,
1623        })
1624    }
1625
1626    /// Returns the default figment
1627    ///
1628    /// The default figment reads from the following sources, in ascending
1629    /// priority order:
1630    ///
1631    ///   1. [`Config::default()`] (see [defaults](#defaults))
1632    ///   2. `foundry.toml` _or_ filename in `FOUNDRY_CONFIG` environment variable
1633    ///   3. `FOUNDRY_` prefixed environment variables
1634    ///
1635    /// The profile selected is the value set in the `FOUNDRY_PROFILE`
1636    /// environment variable. If it is not set, it defaults to `default`.
1637    ///
1638    /// # Example
1639    ///
1640    /// ```rust
1641    /// use foundry_config::Config;
1642    /// use serde::Deserialize;
1643    ///
1644    /// let my_config = Config::figment().extract::<Config>();
1645    /// ```
1646    pub fn figment() -> Figment {
1647        Self::default().into()
1648    }
1649
1650    /// Returns the default figment enhanced with additional context extracted from the provided
1651    /// root, like remappings and directories.
1652    ///
1653    /// # Example
1654    ///
1655    /// ```rust
1656    /// use foundry_config::Config;
1657    /// use serde::Deserialize;
1658    ///
1659    /// let my_config = Config::figment_with_root(".").extract::<Config>();
1660    /// ```
1661    pub fn figment_with_root(root: impl AsRef<Path>) -> Figment {
1662        Self::with_root(root.as_ref()).into()
1663    }
1664
1665    #[doc(hidden)]
1666    #[track_caller]
1667    pub fn figment_with_root_opt(root: Option<&Path>) -> Figment {
1668        let root = match root {
1669            Some(root) => root,
1670            None => &find_project_root(None).expect("could not determine project root"),
1671        };
1672        Self::figment_with_root(root)
1673    }
1674
1675    /// Creates a new Config that adds additional context extracted from the provided root.
1676    ///
1677    /// # Example
1678    ///
1679    /// ```rust
1680    /// use foundry_config::Config;
1681    /// let my_config = Config::with_root(".");
1682    /// ```
1683    pub fn with_root(root: impl AsRef<Path>) -> Self {
1684        Self::_with_root(root.as_ref())
1685    }
1686
1687    fn _with_root(root: &Path) -> Self {
1688        // autodetect paths
1689        let paths = ProjectPathsConfig::builder().build_with_root::<()>(root);
1690        let artifacts: PathBuf = paths.artifacts.file_name().unwrap().into();
1691        Self {
1692            root: paths.root,
1693            src: paths.sources.file_name().unwrap().into(),
1694            out: artifacts.clone(),
1695            libs: paths.libraries.into_iter().map(|lib| lib.file_name().unwrap().into()).collect(),
1696            remappings: paths
1697                .remappings
1698                .into_iter()
1699                .map(|r| RelativeRemapping::new(r, root))
1700                .collect(),
1701            fs_permissions: FsPermissions::new([PathPermission::read(artifacts)]),
1702            ..Self::default()
1703        }
1704    }
1705
1706    /// Returns the default config but with hardhat paths
1707    pub fn hardhat() -> Self {
1708        Self {
1709            src: "contracts".into(),
1710            out: "artifacts".into(),
1711            libs: vec!["node_modules".into()],
1712            ..Self::default()
1713        }
1714    }
1715
1716    /// Returns the default config that uses dapptools style paths
1717    pub fn dapptools() -> Self {
1718        Self {
1719            chain: Some(Chain::from_id(99)),
1720            block_timestamp: U256::ZERO,
1721            block_number: U256::ZERO,
1722            ..Self::default()
1723        }
1724    }
1725
1726    /// Extracts a basic subset of the config, used for initialisations.
1727    ///
1728    /// # Example
1729    ///
1730    /// ```rust
1731    /// use foundry_config::Config;
1732    /// let my_config = Config::with_root(".").into_basic();
1733    /// ```
1734    pub fn into_basic(self) -> BasicConfig {
1735        BasicConfig {
1736            profile: self.profile,
1737            src: self.src,
1738            out: self.out,
1739            libs: self.libs,
1740            remappings: self.remappings,
1741        }
1742    }
1743
1744    /// Updates the `foundry.toml` file for the given `root` based on the provided closure.
1745    ///
1746    /// **Note:** the closure will only be invoked if the `foundry.toml` file exists, See
1747    /// [Self::get_config_path()] and if the closure returns `true`.
1748    pub fn update_at<F>(root: &Path, f: F) -> eyre::Result<()>
1749    where
1750        F: FnOnce(&Self, &mut toml_edit::DocumentMut) -> bool,
1751    {
1752        let config = Self::load_with_root(root)?.sanitized();
1753        config.update(|doc| f(&config, doc))
1754    }
1755
1756    /// Updates the `foundry.toml` file this `Config` ias based on with the provided closure.
1757    ///
1758    /// **Note:** the closure will only be invoked if the `foundry.toml` file exists, See
1759    /// [Self::get_config_path()] and if the closure returns `true`
1760    pub fn update<F>(&self, f: F) -> eyre::Result<()>
1761    where
1762        F: FnOnce(&mut toml_edit::DocumentMut) -> bool,
1763    {
1764        let file_path = self.get_config_path();
1765        if !file_path.exists() {
1766            return Ok(());
1767        }
1768        let contents = fs::read_to_string(&file_path)?;
1769        let mut doc = contents.parse::<toml_edit::DocumentMut>()?;
1770        if f(&mut doc) {
1771            fs::write(file_path, doc.to_string())?;
1772        }
1773        Ok(())
1774    }
1775
1776    /// Sets the `libs` entry inside a `foundry.toml` file but only if it exists
1777    ///
1778    /// # Errors
1779    ///
1780    /// An error if the `foundry.toml` could not be parsed.
1781    pub fn update_libs(&self) -> eyre::Result<()> {
1782        self.update(|doc| {
1783            let profile = self.profile.as_str().as_str();
1784            let root = &self.root;
1785            let libs: toml_edit::Value = self
1786                .libs
1787                .iter()
1788                .map(|path| {
1789                    let path =
1790                        if let Ok(relative) = path.strip_prefix(root) { relative } else { path };
1791                    toml_edit::Value::from(&*path.to_string_lossy())
1792                })
1793                .collect();
1794            let libs = toml_edit::value(libs);
1795            doc[Self::PROFILE_SECTION][profile]["libs"] = libs;
1796            true
1797        })
1798    }
1799
1800    /// Serialize the config type as a String of TOML.
1801    ///
1802    /// This serializes to a table with the name of the profile
1803    ///
1804    /// ```toml
1805    /// [profile.default]
1806    /// src = "src"
1807    /// out = "out"
1808    /// libs = ["lib"]
1809    /// # ...
1810    /// ```
1811    pub fn to_string_pretty(&self) -> Result<String, toml::ser::Error> {
1812        // serializing to value first to prevent `ValueAfterTable` errors
1813        let mut value = toml::Value::try_from(self)?;
1814        // Config map always gets serialized as a table
1815        let value_table = value.as_table_mut().unwrap();
1816        // remove standalone sections from inner table
1817        let standalone_sections = Self::STANDALONE_SECTIONS
1818            .iter()
1819            .filter_map(|section| {
1820                let section = section.to_string();
1821                value_table.remove(&section).map(|value| (section, value))
1822            })
1823            .collect::<Vec<_>>();
1824        // wrap inner table in [profile.<profile>]
1825        let mut wrapping_table = [(
1826            Self::PROFILE_SECTION.into(),
1827            toml::Value::Table([(self.profile.to_string(), value)].into_iter().collect()),
1828        )]
1829        .into_iter()
1830        .collect::<toml::map::Map<_, _>>();
1831        // insert standalone sections
1832        for (section, value) in standalone_sections {
1833            wrapping_table.insert(section, value);
1834        }
1835        // stringify
1836        toml::to_string_pretty(&toml::Value::Table(wrapping_table))
1837    }
1838
1839    /// Returns the path to the `foundry.toml` of this `Config`.
1840    pub fn get_config_path(&self) -> PathBuf {
1841        self.root.join(Self::FILE_NAME)
1842    }
1843
1844    /// Returns the selected profile.
1845    ///
1846    /// If the `FOUNDRY_PROFILE` env variable is not set, this returns the `DEFAULT_PROFILE`.
1847    pub fn selected_profile() -> Profile {
1848        // Can't cache in tests because the env var can change.
1849        #[cfg(test)]
1850        {
1851            Self::force_selected_profile()
1852        }
1853        #[cfg(not(test))]
1854        {
1855            static CACHE: std::sync::OnceLock<Profile> = std::sync::OnceLock::new();
1856            CACHE.get_or_init(Self::force_selected_profile).clone()
1857        }
1858    }
1859
1860    fn force_selected_profile() -> Profile {
1861        Profile::from_env_or("FOUNDRY_PROFILE", Self::DEFAULT_PROFILE)
1862    }
1863
1864    /// Returns the path to foundry's global TOML file: `~/.foundry/foundry.toml`.
1865    pub fn foundry_dir_toml() -> Option<PathBuf> {
1866        Self::foundry_dir().map(|p| p.join(Self::FILE_NAME))
1867    }
1868
1869    /// Returns the path to foundry's config dir: `~/.foundry/`.
1870    pub fn foundry_dir() -> Option<PathBuf> {
1871        dirs::home_dir().map(|p| p.join(Self::FOUNDRY_DIR_NAME))
1872    }
1873
1874    /// Returns the path to foundry's cache dir: `~/.foundry/cache`.
1875    pub fn foundry_cache_dir() -> Option<PathBuf> {
1876        Self::foundry_dir().map(|p| p.join("cache"))
1877    }
1878
1879    /// Returns the path to foundry rpc cache dir: `~/.foundry/cache/rpc`.
1880    pub fn foundry_rpc_cache_dir() -> Option<PathBuf> {
1881        Some(Self::foundry_cache_dir()?.join("rpc"))
1882    }
1883    /// Returns the path to foundry chain's cache dir: `~/.foundry/cache/rpc/<chain>`
1884    pub fn foundry_chain_cache_dir(chain_id: impl Into<Chain>) -> Option<PathBuf> {
1885        Some(Self::foundry_rpc_cache_dir()?.join(chain_id.into().to_string()))
1886    }
1887
1888    /// Returns the path to foundry's etherscan cache dir: `~/.foundry/cache/etherscan`.
1889    pub fn foundry_etherscan_cache_dir() -> Option<PathBuf> {
1890        Some(Self::foundry_cache_dir()?.join("etherscan"))
1891    }
1892
1893    /// Returns the path to foundry's keystores dir: `~/.foundry/keystores`.
1894    pub fn foundry_keystores_dir() -> Option<PathBuf> {
1895        Some(Self::foundry_dir()?.join("keystores"))
1896    }
1897
1898    /// Returns the path to foundry's etherscan cache dir for `chain_id`:
1899    /// `~/.foundry/cache/etherscan/<chain>`
1900    pub fn foundry_etherscan_chain_cache_dir(chain_id: impl Into<Chain>) -> Option<PathBuf> {
1901        Some(Self::foundry_etherscan_cache_dir()?.join(chain_id.into().to_string()))
1902    }
1903
1904    /// Returns the path to the cache dir of the `block` on the `chain`:
1905    /// `~/.foundry/cache/rpc/<chain>/<block>`
1906    pub fn foundry_block_cache_dir(chain_id: impl Into<Chain>, block: u64) -> Option<PathBuf> {
1907        Some(Self::foundry_chain_cache_dir(chain_id)?.join(format!("{block}")))
1908    }
1909
1910    /// Returns the path to the cache file of the `block` on the `chain`:
1911    /// `~/.foundry/cache/rpc/<chain>/<block>/storage.json`
1912    pub fn foundry_block_cache_file(chain_id: impl Into<Chain>, block: u64) -> Option<PathBuf> {
1913        Some(Self::foundry_block_cache_dir(chain_id, block)?.join("storage.json"))
1914    }
1915
1916    /// Returns the path to `foundry`'s data directory inside the user's data directory.
1917    ///
1918    /// | Platform | Value                                         | Example                                          |
1919    /// | -------  | --------------------------------------------- | ------------------------------------------------ |
1920    /// | Linux    | `$XDG_CONFIG_HOME` or `$HOME`/.config/foundry | /home/alice/.config/foundry                      |
1921    /// | macOS    | `$HOME`/Library/Application Support/foundry   | /Users/Alice/Library/Application Support/foundry |
1922    /// | Windows  | `{FOLDERID_RoamingAppData}/foundry`           | C:\Users\Alice\AppData\Roaming/foundry           |
1923    pub fn data_dir() -> eyre::Result<PathBuf> {
1924        let path = dirs::data_dir().wrap_err("Failed to find data directory")?.join("foundry");
1925        std::fs::create_dir_all(&path).wrap_err("Failed to create module directory")?;
1926        Ok(path)
1927    }
1928
1929    /// Returns the path to the `foundry.toml` file, the file is searched for in
1930    /// the current working directory and all parent directories until the root,
1931    /// and the first hit is used.
1932    ///
1933    /// If this search comes up empty, then it checks if a global `foundry.toml` exists at
1934    /// `~/.foundry/foundry.toml`, see [`Self::foundry_dir_toml`].
1935    pub fn find_config_file() -> Option<PathBuf> {
1936        fn find(path: &Path) -> Option<PathBuf> {
1937            if path.is_absolute() {
1938                return match path.is_file() {
1939                    true => Some(path.to_path_buf()),
1940                    false => None,
1941                };
1942            }
1943            let cwd = std::env::current_dir().ok()?;
1944            let mut cwd = cwd.as_path();
1945            loop {
1946                let file_path = cwd.join(path);
1947                if file_path.is_file() {
1948                    return Some(file_path);
1949                }
1950                cwd = cwd.parent()?;
1951            }
1952        }
1953        find(Env::var_or("FOUNDRY_CONFIG", Self::FILE_NAME).as_ref())
1954            .or_else(|| Self::foundry_dir_toml().filter(|p| p.exists()))
1955    }
1956
1957    /// Clears the foundry cache.
1958    pub fn clean_foundry_cache() -> eyre::Result<()> {
1959        if let Some(cache_dir) = Self::foundry_cache_dir() {
1960            let path = cache_dir.as_path();
1961            let _ = fs::remove_dir_all(path);
1962        } else {
1963            eyre::bail!("failed to get foundry_cache_dir");
1964        }
1965
1966        Ok(())
1967    }
1968
1969    /// Clears the foundry cache for `chain`.
1970    pub fn clean_foundry_chain_cache(chain: Chain) -> eyre::Result<()> {
1971        if let Some(cache_dir) = Self::foundry_chain_cache_dir(chain) {
1972            let path = cache_dir.as_path();
1973            let _ = fs::remove_dir_all(path);
1974        } else {
1975            eyre::bail!("failed to get foundry_chain_cache_dir");
1976        }
1977
1978        Ok(())
1979    }
1980
1981    /// Clears the foundry cache for `chain` and `block`.
1982    pub fn clean_foundry_block_cache(chain: Chain, block: u64) -> eyre::Result<()> {
1983        if let Some(cache_dir) = Self::foundry_block_cache_dir(chain, block) {
1984            let path = cache_dir.as_path();
1985            let _ = fs::remove_dir_all(path);
1986        } else {
1987            eyre::bail!("failed to get foundry_block_cache_dir");
1988        }
1989
1990        Ok(())
1991    }
1992
1993    /// Clears the foundry etherscan cache.
1994    pub fn clean_foundry_etherscan_cache() -> eyre::Result<()> {
1995        if let Some(cache_dir) = Self::foundry_etherscan_cache_dir() {
1996            let path = cache_dir.as_path();
1997            let _ = fs::remove_dir_all(path);
1998        } else {
1999            eyre::bail!("failed to get foundry_etherscan_cache_dir");
2000        }
2001
2002        Ok(())
2003    }
2004
2005    /// Clears the foundry etherscan cache for `chain`.
2006    pub fn clean_foundry_etherscan_chain_cache(chain: Chain) -> eyre::Result<()> {
2007        if let Some(cache_dir) = Self::foundry_etherscan_chain_cache_dir(chain) {
2008            let path = cache_dir.as_path();
2009            let _ = fs::remove_dir_all(path);
2010        } else {
2011            eyre::bail!("failed to get foundry_etherscan_cache_dir for chain: {}", chain);
2012        }
2013
2014        Ok(())
2015    }
2016
2017    /// List the data in the foundry cache.
2018    pub fn list_foundry_cache() -> eyre::Result<Cache> {
2019        if let Some(cache_dir) = Self::foundry_rpc_cache_dir() {
2020            let mut cache = Cache { chains: vec![] };
2021            if !cache_dir.exists() {
2022                return Ok(cache);
2023            }
2024            if let Ok(entries) = cache_dir.as_path().read_dir() {
2025                for entry in entries.flatten().filter(|x| x.path().is_dir()) {
2026                    match Chain::from_str(&entry.file_name().to_string_lossy()) {
2027                        Ok(chain) => cache.chains.push(Self::list_foundry_chain_cache(chain)?),
2028                        Err(_) => continue,
2029                    }
2030                }
2031                Ok(cache)
2032            } else {
2033                eyre::bail!("failed to access foundry_cache_dir");
2034            }
2035        } else {
2036            eyre::bail!("failed to get foundry_cache_dir");
2037        }
2038    }
2039
2040    /// List the cached data for `chain`.
2041    pub fn list_foundry_chain_cache(chain: Chain) -> eyre::Result<ChainCache> {
2042        let block_explorer_data_size = match Self::foundry_etherscan_chain_cache_dir(chain) {
2043            Some(cache_dir) => Self::get_cached_block_explorer_data(&cache_dir)?,
2044            None => {
2045                warn!("failed to access foundry_etherscan_chain_cache_dir");
2046                0
2047            }
2048        };
2049
2050        if let Some(cache_dir) = Self::foundry_chain_cache_dir(chain) {
2051            let blocks = Self::get_cached_blocks(&cache_dir)?;
2052            Ok(ChainCache {
2053                name: chain.to_string(),
2054                blocks,
2055                block_explorer: block_explorer_data_size,
2056            })
2057        } else {
2058            eyre::bail!("failed to get foundry_chain_cache_dir");
2059        }
2060    }
2061
2062    /// The path provided to this function should point to a cached chain folder.
2063    fn get_cached_blocks(chain_path: &Path) -> eyre::Result<Vec<(String, u64)>> {
2064        let mut blocks = vec![];
2065        if !chain_path.exists() {
2066            return Ok(blocks);
2067        }
2068        for block in chain_path.read_dir()?.flatten() {
2069            let file_type = block.file_type()?;
2070            let file_name = block.file_name();
2071            let filepath = if file_type.is_dir() {
2072                block.path().join("storage.json")
2073            } else if file_type.is_file()
2074                && file_name.to_string_lossy().chars().all(char::is_numeric)
2075            {
2076                block.path()
2077            } else {
2078                continue;
2079            };
2080            blocks.push((file_name.to_string_lossy().into_owned(), fs::metadata(filepath)?.len()));
2081        }
2082        Ok(blocks)
2083    }
2084
2085    /// The path provided to this function should point to the etherscan cache for a chain.
2086    fn get_cached_block_explorer_data(chain_path: &Path) -> eyre::Result<u64> {
2087        if !chain_path.exists() {
2088            return Ok(0);
2089        }
2090
2091        fn dir_size_recursive(mut dir: fs::ReadDir) -> eyre::Result<u64> {
2092            dir.try_fold(0, |acc, file| {
2093                let file = file?;
2094                let size = match file.metadata()? {
2095                    data if data.is_dir() => dir_size_recursive(fs::read_dir(file.path())?)?,
2096                    data => data.len(),
2097                };
2098                Ok(acc + size)
2099            })
2100        }
2101
2102        dir_size_recursive(fs::read_dir(chain_path)?)
2103    }
2104
2105    fn merge_toml_provider(
2106        mut figment: Figment,
2107        toml_provider: impl Provider,
2108        profile: Profile,
2109    ) -> Figment {
2110        figment = figment.select(profile.clone());
2111
2112        // add warnings
2113        figment = {
2114            let warnings = WarningsProvider::for_figment(&toml_provider, &figment);
2115            figment.merge(warnings)
2116        };
2117
2118        // use [profile.<profile>] as [<profile>]
2119        let mut profiles = vec![Self::DEFAULT_PROFILE];
2120        if profile != Self::DEFAULT_PROFILE {
2121            profiles.push(profile.clone());
2122        }
2123        let provider = toml_provider.strict_select(profiles);
2124
2125        // apply any key fixes
2126        let provider = &BackwardsCompatTomlProvider(ForcedSnakeCaseData(provider));
2127
2128        // merge the default profile as a base
2129        if profile != Self::DEFAULT_PROFILE {
2130            figment = figment.merge(provider.rename(Self::DEFAULT_PROFILE, profile.clone()));
2131        }
2132        // merge special keys into config
2133        for standalone_key in Self::STANDALONE_SECTIONS {
2134            if let Some((_, fallback)) =
2135                STANDALONE_FALLBACK_SECTIONS.iter().find(|(key, _)| standalone_key == key)
2136            {
2137                figment = figment.merge(
2138                    provider
2139                        .fallback(standalone_key, fallback)
2140                        .wrap(profile.clone(), standalone_key),
2141                );
2142            } else {
2143                figment = figment.merge(provider.wrap(profile.clone(), standalone_key));
2144            }
2145        }
2146        // merge the profile
2147        figment = figment.merge(provider);
2148        figment
2149    }
2150
2151    /// Check if any defaults need to be normalized.
2152    ///
2153    /// This normalizes the default `evm_version` if a `solc` was provided in the config.
2154    ///
2155    /// See also <https://github.com/foundry-rs/foundry/issues/7014>
2156    fn normalize_defaults(&self, mut figment: Figment) -> Figment {
2157        // TODO: add a warning if evm_version is provided but incompatible
2158        if figment.contains("evm_version") {
2159            return figment;
2160        }
2161
2162        // Normalize `evm_version` based on the provided solc version.
2163        if let Ok(solc) = figment.extract_inner::<SolcReq>("solc")
2164            && let Some(version) = solc
2165                .try_version()
2166                .ok()
2167                .and_then(|version| self.evm_version.normalize_version_solc(&version))
2168        {
2169            figment = figment.merge(("evm_version", version));
2170        }
2171
2172        figment
2173    }
2174}
2175
2176impl From<Config> for Figment {
2177    fn from(c: Config) -> Self {
2178        (&c).into()
2179    }
2180}
2181impl From<&Config> for Figment {
2182    fn from(c: &Config) -> Self {
2183        c.to_figment(FigmentProviders::All)
2184    }
2185}
2186
2187/// Determines what providers should be used when loading the [`Figment`] for a [`Config`].
2188#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2189pub enum FigmentProviders {
2190    /// Include all providers.
2191    #[default]
2192    All,
2193    /// Only include necessary providers that are useful for cast commands.
2194    ///
2195    /// This will exclude more expensive providers such as remappings.
2196    Cast,
2197    /// Only include necessary providers that are useful for anvil.
2198    ///
2199    /// This will exclude more expensive providers such as remappings.
2200    Anvil,
2201    /// Don't include any providers.
2202    None,
2203}
2204
2205impl FigmentProviders {
2206    /// Returns true if all providers should be included.
2207    pub const fn is_all(&self) -> bool {
2208        matches!(self, Self::All)
2209    }
2210
2211    /// Returns true if this is the cast preset.
2212    pub const fn is_cast(&self) -> bool {
2213        matches!(self, Self::Cast)
2214    }
2215
2216    /// Returns true if this is the anvil preset.
2217    pub const fn is_anvil(&self) -> bool {
2218        matches!(self, Self::Anvil)
2219    }
2220
2221    /// Returns true if no providers should be included.
2222    pub const fn is_none(&self) -> bool {
2223        matches!(self, Self::None)
2224    }
2225}
2226
2227/// Wrapper type for [`regex::Regex`] that implements [`PartialEq`] and [`serde`] traits.
2228#[derive(Clone, Debug, Serialize, Deserialize)]
2229#[serde(transparent)]
2230pub struct RegexWrapper {
2231    #[serde(with = "serde_regex")]
2232    inner: regex::Regex,
2233}
2234
2235impl std::ops::Deref for RegexWrapper {
2236    type Target = regex::Regex;
2237
2238    fn deref(&self) -> &Self::Target {
2239        &self.inner
2240    }
2241}
2242
2243impl std::cmp::PartialEq for RegexWrapper {
2244    fn eq(&self, other: &Self) -> bool {
2245        self.as_str() == other.as_str()
2246    }
2247}
2248
2249impl Eq for RegexWrapper {}
2250
2251impl From<RegexWrapper> for regex::Regex {
2252    fn from(wrapper: RegexWrapper) -> Self {
2253        wrapper.inner
2254    }
2255}
2256
2257impl From<regex::Regex> for RegexWrapper {
2258    fn from(re: Regex) -> Self {
2259        Self { inner: re }
2260    }
2261}
2262
2263mod serde_regex {
2264    use regex::Regex;
2265    use serde::{Deserialize, Deserializer, Serializer};
2266
2267    pub(crate) fn serialize<S>(value: &Regex, serializer: S) -> Result<S::Ok, S::Error>
2268    where
2269        S: Serializer,
2270    {
2271        serializer.serialize_str(value.as_str())
2272    }
2273
2274    pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Regex, D::Error>
2275    where
2276        D: Deserializer<'de>,
2277    {
2278        let s = String::deserialize(deserializer)?;
2279        Regex::new(&s).map_err(serde::de::Error::custom)
2280    }
2281}
2282
2283/// Ser/de `globset::Glob` explicitly to handle `Option<Glob>` properly
2284pub(crate) mod from_opt_glob {
2285    use serde::{Deserialize, Deserializer, Serializer};
2286
2287    pub fn serialize<S>(value: &Option<globset::Glob>, serializer: S) -> Result<S::Ok, S::Error>
2288    where
2289        S: Serializer,
2290    {
2291        match value {
2292            Some(glob) => serializer.serialize_str(glob.glob()),
2293            None => serializer.serialize_none(),
2294        }
2295    }
2296
2297    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<globset::Glob>, D::Error>
2298    where
2299        D: Deserializer<'de>,
2300    {
2301        let s: Option<String> = Option::deserialize(deserializer)?;
2302        if let Some(s) = s {
2303            return Ok(Some(globset::Glob::new(&s).map_err(serde::de::Error::custom)?));
2304        }
2305        Ok(None)
2306    }
2307}
2308
2309/// Parses a config profile
2310///
2311/// All `Profile` date is ignored by serde, however the `Config::to_string_pretty` includes it and
2312/// returns a toml table like
2313///
2314/// ```toml
2315/// #[profile.default]
2316/// src = "..."
2317/// ```
2318/// This ignores the `#[profile.default]` part in the toml
2319pub fn parse_with_profile<T: serde::de::DeserializeOwned>(
2320    s: &str,
2321) -> Result<Option<(Profile, T)>, Error> {
2322    let figment = Config::merge_toml_provider(
2323        Figment::new(),
2324        Toml::string(s).nested(),
2325        Config::DEFAULT_PROFILE,
2326    );
2327    if figment.profiles().any(|p| p == Config::DEFAULT_PROFILE) {
2328        Ok(Some((Config::DEFAULT_PROFILE, figment.select(Config::DEFAULT_PROFILE).extract()?)))
2329    } else {
2330        Ok(None)
2331    }
2332}
2333
2334impl Provider for Config {
2335    fn metadata(&self) -> Metadata {
2336        Metadata::named("Foundry Config")
2337    }
2338
2339    #[track_caller]
2340    fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
2341        let mut data = Serialized::defaults(self).data()?;
2342        if let Some(entry) = data.get_mut(&self.profile) {
2343            entry.insert("root".to_string(), Value::serialize(self.root.clone())?);
2344        }
2345        Ok(data)
2346    }
2347
2348    fn profile(&self) -> Option<Profile> {
2349        Some(self.profile.clone())
2350    }
2351}
2352
2353impl Default for Config {
2354    fn default() -> Self {
2355        Self {
2356            profile: Self::DEFAULT_PROFILE,
2357            profiles: vec![Self::DEFAULT_PROFILE],
2358            fs_permissions: FsPermissions::new([PathPermission::read("out")]),
2359            isolate: cfg!(feature = "isolate-by-default"),
2360            root: root_default(),
2361            extends: None,
2362            src: "src".into(),
2363            test: "test".into(),
2364            script: "script".into(),
2365            out: "out".into(),
2366            libs: vec!["lib".into()],
2367            cache: true,
2368            dynamic_test_linking: false,
2369            cache_path: "cache".into(),
2370            broadcast: "broadcast".into(),
2371            snapshots: "snapshots".into(),
2372            gas_snapshot_check: false,
2373            gas_snapshot_emit: true,
2374            allow_paths: vec![],
2375            include_paths: vec![],
2376            force: false,
2377            evm_version: EvmVersion::Prague,
2378            gas_reports: vec!["*".to_string()],
2379            gas_reports_ignore: vec![],
2380            gas_reports_include_tests: false,
2381            solc: None,
2382            vyper: Default::default(),
2383            auto_detect_solc: true,
2384            offline: false,
2385            optimizer: None,
2386            optimizer_runs: None,
2387            optimizer_details: None,
2388            model_checker: None,
2389            extra_output: Default::default(),
2390            extra_output_files: Default::default(),
2391            names: false,
2392            sizes: false,
2393            test_pattern: None,
2394            test_pattern_inverse: None,
2395            contract_pattern: None,
2396            contract_pattern_inverse: None,
2397            path_pattern: None,
2398            path_pattern_inverse: None,
2399            coverage_pattern_inverse: None,
2400            test_failures_file: "cache/test-failures".into(),
2401            threads: None,
2402            show_progress: false,
2403            fuzz: FuzzConfig::new("cache/fuzz".into()),
2404            invariant: InvariantConfig::new("cache/invariant".into()),
2405            always_use_create_2_factory: false,
2406            ffi: false,
2407            allow_internal_expect_revert: false,
2408            prompt_timeout: 120,
2409            sender: Self::DEFAULT_SENDER,
2410            tx_origin: Self::DEFAULT_SENDER,
2411            initial_balance: U256::from((1u128 << 96) - 1),
2412            block_number: U256::from(1),
2413            fork_block_number: None,
2414            chain: None,
2415            gas_limit: (1u64 << 30).into(), // ~1B
2416            code_size_limit: None,
2417            gas_price: None,
2418            block_base_fee_per_gas: 0,
2419            block_coinbase: Address::ZERO,
2420            block_timestamp: U256::from(1),
2421            block_difficulty: 0,
2422            block_prevrandao: Default::default(),
2423            block_gas_limit: None,
2424            disable_block_gas_limit: false,
2425            memory_limit: 1 << 27, // 2**27 = 128MiB = 134_217_728 bytes
2426            eth_rpc_url: None,
2427            eth_rpc_accept_invalid_certs: false,
2428            eth_rpc_jwt: None,
2429            eth_rpc_timeout: None,
2430            eth_rpc_headers: None,
2431            etherscan_api_key: None,
2432            etherscan_api_version: None,
2433            verbosity: 0,
2434            remappings: vec![],
2435            auto_detect_remappings: true,
2436            libraries: vec![],
2437            ignored_error_codes: vec![
2438                SolidityErrorCode::SpdxLicenseNotProvided,
2439                SolidityErrorCode::ContractExceeds24576Bytes,
2440                SolidityErrorCode::ContractInitCodeSizeExceeds49152Bytes,
2441                SolidityErrorCode::TransientStorageUsed,
2442            ],
2443            ignored_file_paths: vec![],
2444            deny_warnings: false,
2445            via_ir: false,
2446            ast: false,
2447            rpc_storage_caching: Default::default(),
2448            rpc_endpoints: Default::default(),
2449            etherscan: Default::default(),
2450            no_storage_caching: false,
2451            no_rpc_rate_limit: false,
2452            use_literal_content: false,
2453            bytecode_hash: BytecodeHash::Ipfs,
2454            cbor_metadata: true,
2455            revert_strings: None,
2456            sparse_mode: false,
2457            build_info: false,
2458            build_info_path: None,
2459            fmt: Default::default(),
2460            lint: Default::default(),
2461            doc: Default::default(),
2462            bind_json: Default::default(),
2463            labels: Default::default(),
2464            unchecked_cheatcode_artifacts: false,
2465            create2_library_salt: Self::DEFAULT_CREATE2_LIBRARY_SALT,
2466            create2_deployer: Self::DEFAULT_CREATE2_DEPLOYER,
2467            skip: vec![],
2468            dependencies: Default::default(),
2469            soldeer: Default::default(),
2470            assertions_revert: true,
2471            legacy_assertions: false,
2472            warnings: vec![],
2473            extra_args: vec![],
2474            odyssey: false,
2475            transaction_timeout: 120,
2476            additional_compiler_profiles: Default::default(),
2477            compilation_restrictions: Default::default(),
2478            script_execution_protection: true,
2479            _non_exhaustive: (),
2480        }
2481    }
2482}
2483
2484/// Wrapper for the config's `gas_limit` value necessary because toml-rs can't handle larger number
2485/// because integers are stored signed: <https://github.com/alexcrichton/toml-rs/issues/256>
2486///
2487/// Due to this limitation this type will be serialized/deserialized as String if it's larger than
2488/// `i64`
2489#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
2490pub struct GasLimit(#[serde(deserialize_with = "crate::deserialize_u64_or_max")] pub u64);
2491
2492impl From<u64> for GasLimit {
2493    fn from(gas: u64) -> Self {
2494        Self(gas)
2495    }
2496}
2497
2498impl From<GasLimit> for u64 {
2499    fn from(gas: GasLimit) -> Self {
2500        gas.0
2501    }
2502}
2503
2504impl Serialize for GasLimit {
2505    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2506    where
2507        S: Serializer,
2508    {
2509        if self.0 == u64::MAX {
2510            serializer.serialize_str("max")
2511        } else if self.0 > i64::MAX as u64 {
2512            serializer.serialize_str(&self.0.to_string())
2513        } else {
2514            serializer.serialize_u64(self.0)
2515        }
2516    }
2517}
2518
2519/// Variants for selecting the [`Solc`] instance
2520#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2521#[serde(untagged)]
2522pub enum SolcReq {
2523    /// Requires a specific solc version, that's either already installed (via `svm`) or will be
2524    /// auto installed (via `svm`)
2525    Version(Version),
2526    /// Path to an existing local solc installation
2527    Local(PathBuf),
2528}
2529
2530impl SolcReq {
2531    /// Tries to get the solc version from the `SolcReq`
2532    ///
2533    /// If the `SolcReq` is a `Version` it will return the version, if it's a path to a binary it
2534    /// will try to get the version from the binary.
2535    fn try_version(&self) -> Result<Version, SolcError> {
2536        match self {
2537            Self::Version(version) => Ok(version.clone()),
2538            Self::Local(path) => Solc::new(path).map(|solc| solc.version),
2539        }
2540    }
2541}
2542
2543impl<T: AsRef<str>> From<T> for SolcReq {
2544    fn from(s: T) -> Self {
2545        let s = s.as_ref();
2546        if let Ok(v) = Version::from_str(s) { Self::Version(v) } else { Self::Local(s.into()) }
2547    }
2548}
2549
2550/// A subset of the foundry `Config`
2551/// used to initialize a `foundry.toml` file
2552///
2553/// # Example
2554///
2555/// ```rust
2556/// use foundry_config::{BasicConfig, Config};
2557/// use serde::Deserialize;
2558///
2559/// let my_config = Config::figment().extract::<BasicConfig>();
2560/// ```
2561#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2562pub struct BasicConfig {
2563    /// the profile tag: `[profile.default]`
2564    #[serde(skip)]
2565    pub profile: Profile,
2566    /// path of the source contracts dir, like `src` or `contracts`
2567    pub src: PathBuf,
2568    /// path to where artifacts shut be written to
2569    pub out: PathBuf,
2570    /// all library folders to include, `lib`, `node_modules`
2571    pub libs: Vec<PathBuf>,
2572    /// `Remappings` to use for this repo
2573    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2574    pub remappings: Vec<RelativeRemapping>,
2575}
2576
2577impl BasicConfig {
2578    /// Serialize the config as a String of TOML.
2579    ///
2580    /// This serializes to a table with the name of the profile
2581    pub fn to_string_pretty(&self) -> Result<String, toml::ser::Error> {
2582        let s = toml::to_string_pretty(self)?;
2583        Ok(format!(
2584            "\
2585[profile.{}]
2586{s}
2587# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options\n",
2588            self.profile
2589        ))
2590    }
2591}
2592
2593pub(crate) mod from_str_lowercase {
2594    use serde::{Deserialize, Deserializer, Serializer};
2595    use std::str::FromStr;
2596
2597    pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
2598    where
2599        T: std::fmt::Display,
2600        S: Serializer,
2601    {
2602        serializer.collect_str(&value.to_string().to_lowercase())
2603    }
2604
2605    pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
2606    where
2607        D: Deserializer<'de>,
2608        T: FromStr,
2609        T::Err: std::fmt::Display,
2610    {
2611        String::deserialize(deserializer)?.to_lowercase().parse().map_err(serde::de::Error::custom)
2612    }
2613}
2614
2615fn canonic(path: impl Into<PathBuf>) -> PathBuf {
2616    let path = path.into();
2617    foundry_compilers::utils::canonicalize(&path).unwrap_or(path)
2618}
2619
2620fn root_default() -> PathBuf {
2621    ".".into()
2622}
2623
2624#[cfg(test)]
2625mod tests {
2626    use super::*;
2627    use crate::{
2628        cache::{CachedChains, CachedEndpoints},
2629        endpoints::RpcEndpointType,
2630        etherscan::ResolvedEtherscanConfigs,
2631        fmt::IndentStyle,
2632    };
2633    use NamedChain::Moonbeam;
2634    use endpoints::{RpcAuth, RpcEndpointConfig};
2635    use figment::error::Kind::InvalidType;
2636    use foundry_compilers::artifacts::{
2637        ModelCheckerEngine, YulDetails, vyper::VyperOptimizationMode,
2638    };
2639    use similar_asserts::assert_eq;
2640    use soldeer_core::remappings::RemappingsLocation;
2641    use std::{fs::File, io::Write};
2642    use tempfile::tempdir;
2643
2644    // Helper function to clear `__warnings` in config, since it will be populated during loading
2645    // from file, causing testing problem when comparing to those created from `default()`, etc.
2646    fn clear_warning(config: &mut Config) {
2647        config.warnings = vec![];
2648    }
2649
2650    #[test]
2651    fn default_sender() {
2652        assert_eq!(Config::DEFAULT_SENDER, address!("0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38"));
2653    }
2654
2655    #[test]
2656    fn test_caching() {
2657        let mut config = Config::default();
2658        let chain_id = NamedChain::Mainnet;
2659        let url = "https://eth-mainnet.alchemyapi";
2660        assert!(config.enable_caching(url, chain_id));
2661
2662        config.no_storage_caching = true;
2663        assert!(!config.enable_caching(url, chain_id));
2664
2665        config.no_storage_caching = false;
2666        assert!(!config.enable_caching(url, NamedChain::Dev));
2667    }
2668
2669    #[test]
2670    fn test_install_dir() {
2671        figment::Jail::expect_with(|jail| {
2672            let config = Config::load().unwrap();
2673            assert_eq!(config.install_lib_dir(), PathBuf::from("lib"));
2674            jail.create_file(
2675                "foundry.toml",
2676                r"
2677                [profile.default]
2678                libs = ['node_modules', 'lib']
2679            ",
2680            )?;
2681            let config = Config::load().unwrap();
2682            assert_eq!(config.install_lib_dir(), PathBuf::from("lib"));
2683
2684            jail.create_file(
2685                "foundry.toml",
2686                r"
2687                [profile.default]
2688                libs = ['custom', 'node_modules', 'lib']
2689            ",
2690            )?;
2691            let config = Config::load().unwrap();
2692            assert_eq!(config.install_lib_dir(), PathBuf::from("custom"));
2693
2694            Ok(())
2695        });
2696    }
2697
2698    #[test]
2699    fn test_figment_is_default() {
2700        figment::Jail::expect_with(|_| {
2701            let mut default: Config = Config::figment().extract()?;
2702            let default2 = Config::default();
2703            default.profile = default2.profile.clone();
2704            default.profiles = default2.profiles.clone();
2705            assert_eq!(default, default2);
2706            Ok(())
2707        });
2708    }
2709
2710    #[test]
2711    fn figment_profiles() {
2712        figment::Jail::expect_with(|jail| {
2713            jail.create_file(
2714                "foundry.toml",
2715                r"
2716                [foo.baz]
2717                libs = ['node_modules', 'lib']
2718
2719                [profile.default]
2720                libs = ['node_modules', 'lib']
2721
2722                [profile.ci]
2723                libs = ['node_modules', 'lib']
2724
2725                [profile.local]
2726                libs = ['node_modules', 'lib']
2727            ",
2728            )?;
2729
2730            let config = crate::Config::load().unwrap();
2731            let expected: &[figment::Profile] = &["ci".into(), "default".into(), "local".into()];
2732            assert_eq!(config.profiles, expected);
2733
2734            Ok(())
2735        });
2736    }
2737
2738    #[test]
2739    fn test_default_round_trip() {
2740        figment::Jail::expect_with(|_| {
2741            let original = Config::figment();
2742            let roundtrip = Figment::from(Config::from_provider(&original).unwrap());
2743            for figment in &[original, roundtrip] {
2744                let config = Config::from_provider(figment).unwrap();
2745                assert_eq!(config, Config::default().normalized_optimizer_settings());
2746            }
2747            Ok(())
2748        });
2749    }
2750
2751    #[test]
2752    fn ffi_env_disallowed() {
2753        figment::Jail::expect_with(|jail| {
2754            jail.set_env("FOUNDRY_FFI", "true");
2755            jail.set_env("FFI", "true");
2756            jail.set_env("DAPP_FFI", "true");
2757            let config = Config::load().unwrap();
2758            assert!(!config.ffi);
2759
2760            Ok(())
2761        });
2762    }
2763
2764    #[test]
2765    fn test_profile_env() {
2766        figment::Jail::expect_with(|jail| {
2767            jail.set_env("FOUNDRY_PROFILE", "default");
2768            let figment = Config::figment();
2769            assert_eq!(figment.profile(), "default");
2770
2771            jail.set_env("FOUNDRY_PROFILE", "hardhat");
2772            let figment: Figment = Config::hardhat().into();
2773            assert_eq!(figment.profile(), "hardhat");
2774
2775            jail.create_file(
2776                "foundry.toml",
2777                r"
2778                [profile.default]
2779                libs = ['lib']
2780                [profile.local]
2781                libs = ['modules']
2782            ",
2783            )?;
2784            jail.set_env("FOUNDRY_PROFILE", "local");
2785            let config = Config::load().unwrap();
2786            assert_eq!(config.libs, vec![PathBuf::from("modules")]);
2787
2788            Ok(())
2789        });
2790    }
2791
2792    #[test]
2793    fn test_default_test_path() {
2794        figment::Jail::expect_with(|_| {
2795            let config = Config::default();
2796            let paths_config = config.project_paths::<Solc>();
2797            assert_eq!(paths_config.tests, PathBuf::from(r"test"));
2798            Ok(())
2799        });
2800    }
2801
2802    #[test]
2803    fn test_default_libs() {
2804        figment::Jail::expect_with(|jail| {
2805            let config = Config::load().unwrap();
2806            assert_eq!(config.libs, vec![PathBuf::from("lib")]);
2807
2808            fs::create_dir_all(jail.directory().join("node_modules")).unwrap();
2809            let config = Config::load().unwrap();
2810            assert_eq!(config.libs, vec![PathBuf::from("node_modules")]);
2811
2812            fs::create_dir_all(jail.directory().join("lib")).unwrap();
2813            let config = Config::load().unwrap();
2814            assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
2815
2816            Ok(())
2817        });
2818    }
2819
2820    #[test]
2821    fn test_inheritance_from_default_test_path() {
2822        figment::Jail::expect_with(|jail| {
2823            jail.create_file(
2824                "foundry.toml",
2825                r#"
2826                [profile.default]
2827                test = "defaulttest"
2828                src  = "defaultsrc"
2829                libs = ['lib', 'node_modules']
2830
2831                [profile.custom]
2832                src = "customsrc"
2833            "#,
2834            )?;
2835
2836            let config = Config::load().unwrap();
2837            assert_eq!(config.src, PathBuf::from("defaultsrc"));
2838            assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
2839
2840            jail.set_env("FOUNDRY_PROFILE", "custom");
2841            let config = Config::load().unwrap();
2842            assert_eq!(config.src, PathBuf::from("customsrc"));
2843            assert_eq!(config.test, PathBuf::from("defaulttest"));
2844            assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
2845
2846            Ok(())
2847        });
2848    }
2849
2850    #[test]
2851    fn test_custom_test_path() {
2852        figment::Jail::expect_with(|jail| {
2853            jail.create_file(
2854                "foundry.toml",
2855                r#"
2856                [profile.default]
2857                test = "mytest"
2858            "#,
2859            )?;
2860
2861            let config = Config::load().unwrap();
2862            let paths_config = config.project_paths::<Solc>();
2863            assert_eq!(paths_config.tests, PathBuf::from(r"mytest"));
2864            Ok(())
2865        });
2866    }
2867
2868    #[test]
2869    fn test_remappings() {
2870        figment::Jail::expect_with(|jail| {
2871            jail.create_file(
2872                "foundry.toml",
2873                r#"
2874                [profile.default]
2875                src = "some-source"
2876                out = "some-out"
2877                cache = true
2878            "#,
2879            )?;
2880            let config = Config::load().unwrap();
2881            assert!(config.remappings.is_empty());
2882
2883            jail.create_file(
2884                "remappings.txt",
2885                r"
2886                file-ds-test/=lib/ds-test/
2887                file-other/=lib/other/
2888            ",
2889            )?;
2890
2891            let config = Config::load().unwrap();
2892            assert_eq!(
2893                config.remappings,
2894                vec![
2895                    Remapping::from_str("file-ds-test/=lib/ds-test/").unwrap().into(),
2896                    Remapping::from_str("file-other/=lib/other/").unwrap().into(),
2897                ],
2898            );
2899
2900            jail.set_env("DAPP_REMAPPINGS", "ds-test=lib/ds-test/\nother/=lib/other/");
2901            let config = Config::load().unwrap();
2902
2903            assert_eq!(
2904                config.remappings,
2905                vec![
2906                    // From environment (should have precedence over remapping.txt)
2907                    Remapping::from_str("ds-test=lib/ds-test/").unwrap().into(),
2908                    Remapping::from_str("other/=lib/other/").unwrap().into(),
2909                    // From remapping.txt (should have less precedence than remapping.txt)
2910                    Remapping::from_str("file-ds-test/=lib/ds-test/").unwrap().into(),
2911                    Remapping::from_str("file-other/=lib/other/").unwrap().into(),
2912                ],
2913            );
2914
2915            Ok(())
2916        });
2917    }
2918
2919    #[test]
2920    fn test_remappings_override() {
2921        figment::Jail::expect_with(|jail| {
2922            jail.create_file(
2923                "foundry.toml",
2924                r#"
2925                [profile.default]
2926                src = "some-source"
2927                out = "some-out"
2928                cache = true
2929            "#,
2930            )?;
2931            let config = Config::load().unwrap();
2932            assert!(config.remappings.is_empty());
2933
2934            jail.create_file(
2935                "remappings.txt",
2936                r"
2937                ds-test/=lib/ds-test/
2938                other/=lib/other/
2939            ",
2940            )?;
2941
2942            let config = Config::load().unwrap();
2943            assert_eq!(
2944                config.remappings,
2945                vec![
2946                    Remapping::from_str("ds-test/=lib/ds-test/").unwrap().into(),
2947                    Remapping::from_str("other/=lib/other/").unwrap().into(),
2948                ],
2949            );
2950
2951            jail.set_env("DAPP_REMAPPINGS", "ds-test/=lib/ds-test/src/\nenv-lib/=lib/env-lib/");
2952            let config = Config::load().unwrap();
2953
2954            // Remappings should now be:
2955            // - ds-test from environment (lib/ds-test/src/)
2956            // - other from remappings.txt (lib/other/)
2957            // - env-lib from environment (lib/env-lib/)
2958            assert_eq!(
2959                config.remappings,
2960                vec![
2961                    Remapping::from_str("ds-test/=lib/ds-test/src/").unwrap().into(),
2962                    Remapping::from_str("env-lib/=lib/env-lib/").unwrap().into(),
2963                    Remapping::from_str("other/=lib/other/").unwrap().into(),
2964                ],
2965            );
2966
2967            // contains additional remapping to the source dir
2968            assert_eq!(
2969                config.get_all_remappings().collect::<Vec<_>>(),
2970                vec![
2971                    Remapping::from_str("ds-test/=lib/ds-test/src/").unwrap(),
2972                    Remapping::from_str("env-lib/=lib/env-lib/").unwrap(),
2973                    Remapping::from_str("other/=lib/other/").unwrap(),
2974                ],
2975            );
2976
2977            Ok(())
2978        });
2979    }
2980
2981    #[test]
2982    fn test_can_update_libs() {
2983        figment::Jail::expect_with(|jail| {
2984            jail.create_file(
2985                "foundry.toml",
2986                r#"
2987                [profile.default]
2988                libs = ["node_modules"]
2989            "#,
2990            )?;
2991
2992            let mut config = Config::load().unwrap();
2993            config.libs.push("libs".into());
2994            config.update_libs().unwrap();
2995
2996            let config = Config::load().unwrap();
2997            assert_eq!(config.libs, vec![PathBuf::from("node_modules"), PathBuf::from("libs"),]);
2998            Ok(())
2999        });
3000    }
3001
3002    #[test]
3003    fn test_large_gas_limit() {
3004        figment::Jail::expect_with(|jail| {
3005            let gas = u64::MAX;
3006            jail.create_file(
3007                "foundry.toml",
3008                &format!(
3009                    r#"
3010                [profile.default]
3011                gas_limit = "{gas}"
3012            "#
3013                ),
3014            )?;
3015
3016            let config = Config::load().unwrap();
3017            assert_eq!(
3018                config,
3019                Config {
3020                    gas_limit: gas.into(),
3021                    ..Config::default().normalized_optimizer_settings()
3022                }
3023            );
3024
3025            Ok(())
3026        });
3027    }
3028
3029    #[test]
3030    #[should_panic]
3031    fn test_toml_file_parse_failure() {
3032        figment::Jail::expect_with(|jail| {
3033            jail.create_file(
3034                "foundry.toml",
3035                r#"
3036                [profile.default]
3037                eth_rpc_url = "https://example.com/
3038            "#,
3039            )?;
3040
3041            let _config = Config::load().unwrap();
3042
3043            Ok(())
3044        });
3045    }
3046
3047    #[test]
3048    #[should_panic]
3049    fn test_toml_file_non_existing_config_var_failure() {
3050        figment::Jail::expect_with(|jail| {
3051            jail.set_env("FOUNDRY_CONFIG", "this config does not exist");
3052
3053            let _config = Config::load().unwrap();
3054
3055            Ok(())
3056        });
3057    }
3058
3059    #[test]
3060    fn test_resolve_etherscan_with_chain() {
3061        figment::Jail::expect_with(|jail| {
3062            let env_key = "__BSC_ETHERSCAN_API_KEY";
3063            let env_value = "env value";
3064            jail.create_file(
3065                "foundry.toml",
3066                r#"
3067                [profile.default]
3068
3069                [etherscan]
3070                bsc = { key = "${__BSC_ETHERSCAN_API_KEY}", url = "https://api.bscscan.com/api" }
3071            "#,
3072            )?;
3073
3074            let config = Config::load().unwrap();
3075            assert!(
3076                config
3077                    .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
3078                    .is_err()
3079            );
3080
3081            unsafe {
3082                std::env::set_var(env_key, env_value);
3083            }
3084
3085            assert_eq!(
3086                config
3087                    .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
3088                    .unwrap()
3089                    .unwrap()
3090                    .key,
3091                env_value
3092            );
3093
3094            let mut with_key = config;
3095            with_key.etherscan_api_key = Some("via etherscan_api_key".to_string());
3096
3097            assert_eq!(
3098                with_key
3099                    .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
3100                    .unwrap()
3101                    .unwrap()
3102                    .key,
3103                "via etherscan_api_key"
3104            );
3105
3106            unsafe {
3107                std::env::remove_var(env_key);
3108            }
3109            Ok(())
3110        });
3111    }
3112
3113    #[test]
3114    fn test_resolve_etherscan() {
3115        figment::Jail::expect_with(|jail| {
3116            jail.create_file(
3117                "foundry.toml",
3118                r#"
3119                [profile.default]
3120
3121                [etherscan]
3122                mainnet = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
3123                moonbeam = { key = "${_CONFIG_ETHERSCAN_MOONBEAM}" }
3124            "#,
3125            )?;
3126
3127            let config = Config::load().unwrap();
3128
3129            assert!(config.etherscan.clone().resolved(EtherscanApiVersion::V2).has_unresolved());
3130
3131            jail.set_env("_CONFIG_ETHERSCAN_MOONBEAM", "123456789");
3132
3133            let configs = config.etherscan.resolved(EtherscanApiVersion::V2);
3134            assert!(!configs.has_unresolved());
3135
3136            let mb_urls = Moonbeam.etherscan_urls().unwrap();
3137            let mainnet_urls = NamedChain::Mainnet.etherscan_urls().unwrap();
3138            assert_eq!(
3139                configs,
3140                ResolvedEtherscanConfigs::new([
3141                    (
3142                        "mainnet",
3143                        ResolvedEtherscanConfig {
3144                            api_url: mainnet_urls.0.to_string(),
3145                            chain: Some(NamedChain::Mainnet.into()),
3146                            browser_url: Some(mainnet_urls.1.to_string()),
3147                            api_version: EtherscanApiVersion::V2,
3148                            key: "FX42Z3BBJJEWXWGYV2X1CIPRSCN".to_string(),
3149                        }
3150                    ),
3151                    (
3152                        "moonbeam",
3153                        ResolvedEtherscanConfig {
3154                            api_url: mb_urls.0.to_string(),
3155                            chain: Some(Moonbeam.into()),
3156                            browser_url: Some(mb_urls.1.to_string()),
3157                            api_version: EtherscanApiVersion::V2,
3158                            key: "123456789".to_string(),
3159                        }
3160                    ),
3161                ])
3162            );
3163
3164            Ok(())
3165        });
3166    }
3167
3168    #[test]
3169    fn test_resolve_etherscan_with_versions() {
3170        figment::Jail::expect_with(|jail| {
3171            jail.create_file(
3172                "foundry.toml",
3173                r#"
3174                [profile.default]
3175
3176                [etherscan]
3177                mainnet = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN", api_version = "v2" }
3178                moonbeam = { key = "${_CONFIG_ETHERSCAN_MOONBEAM}", api_version = "v1" }
3179            "#,
3180            )?;
3181
3182            let config = Config::load().unwrap();
3183
3184            assert!(config.etherscan.clone().resolved(EtherscanApiVersion::V2).has_unresolved());
3185
3186            jail.set_env("_CONFIG_ETHERSCAN_MOONBEAM", "123456789");
3187
3188            let configs = config.etherscan.resolved(EtherscanApiVersion::V2);
3189            assert!(!configs.has_unresolved());
3190
3191            let mb_urls = Moonbeam.etherscan_urls().unwrap();
3192            let mainnet_urls = NamedChain::Mainnet.etherscan_urls().unwrap();
3193            assert_eq!(
3194                configs,
3195                ResolvedEtherscanConfigs::new([
3196                    (
3197                        "mainnet",
3198                        ResolvedEtherscanConfig {
3199                            api_url: mainnet_urls.0.to_string(),
3200                            chain: Some(NamedChain::Mainnet.into()),
3201                            browser_url: Some(mainnet_urls.1.to_string()),
3202                            api_version: EtherscanApiVersion::V2,
3203                            key: "FX42Z3BBJJEWXWGYV2X1CIPRSCN".to_string(),
3204                        }
3205                    ),
3206                    (
3207                        "moonbeam",
3208                        ResolvedEtherscanConfig {
3209                            api_url: mb_urls.0.to_string(),
3210                            chain: Some(Moonbeam.into()),
3211                            browser_url: Some(mb_urls.1.to_string()),
3212                            api_version: EtherscanApiVersion::V1,
3213                            key: "123456789".to_string(),
3214                        }
3215                    ),
3216                ])
3217            );
3218
3219            Ok(())
3220        });
3221    }
3222
3223    #[test]
3224    fn test_resolve_etherscan_chain_id() {
3225        figment::Jail::expect_with(|jail| {
3226            jail.create_file(
3227                "foundry.toml",
3228                r#"
3229                [profile.default]
3230                chain_id = "sepolia"
3231
3232                [etherscan]
3233                sepolia = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
3234            "#,
3235            )?;
3236
3237            let config = Config::load().unwrap();
3238            let etherscan = config.get_etherscan_config().unwrap().unwrap();
3239            assert_eq!(etherscan.chain, Some(NamedChain::Sepolia.into()));
3240            assert_eq!(etherscan.key, "FX42Z3BBJJEWXWGYV2X1CIPRSCN");
3241
3242            Ok(())
3243        });
3244    }
3245
3246    #[test]
3247    fn test_resolve_rpc_url() {
3248        figment::Jail::expect_with(|jail| {
3249            jail.create_file(
3250                "foundry.toml",
3251                r#"
3252                [profile.default]
3253                [rpc_endpoints]
3254                optimism = "https://example.com/"
3255                mainnet = "${_CONFIG_MAINNET}"
3256            "#,
3257            )?;
3258            jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3259
3260            let mut config = Config::load().unwrap();
3261            assert_eq!("http://localhost:8545", config.get_rpc_url_or_localhost_http().unwrap());
3262
3263            config.eth_rpc_url = Some("mainnet".to_string());
3264            assert_eq!(
3265                "https://eth-mainnet.alchemyapi.io/v2/123455",
3266                config.get_rpc_url_or_localhost_http().unwrap()
3267            );
3268
3269            config.eth_rpc_url = Some("optimism".to_string());
3270            assert_eq!("https://example.com/", config.get_rpc_url_or_localhost_http().unwrap());
3271
3272            Ok(())
3273        })
3274    }
3275
3276    #[test]
3277    fn test_resolve_rpc_url_if_etherscan_set() {
3278        figment::Jail::expect_with(|jail| {
3279            jail.create_file(
3280                "foundry.toml",
3281                r#"
3282                [profile.default]
3283                etherscan_api_key = "dummy"
3284                [rpc_endpoints]
3285                optimism = "https://example.com/"
3286            "#,
3287            )?;
3288
3289            let config = Config::load().unwrap();
3290            assert_eq!("http://localhost:8545", config.get_rpc_url_or_localhost_http().unwrap());
3291
3292            Ok(())
3293        })
3294    }
3295
3296    #[test]
3297    fn test_resolve_rpc_url_alias() {
3298        figment::Jail::expect_with(|jail| {
3299            jail.create_file(
3300                "foundry.toml",
3301                r#"
3302                [profile.default]
3303                [rpc_endpoints]
3304                polygonMumbai = "https://polygon-mumbai.g.alchemy.com/v2/${_RESOLVE_RPC_ALIAS}"
3305            "#,
3306            )?;
3307            let mut config = Config::load().unwrap();
3308            config.eth_rpc_url = Some("polygonMumbai".to_string());
3309            assert!(config.get_rpc_url().unwrap().is_err());
3310
3311            jail.set_env("_RESOLVE_RPC_ALIAS", "123455");
3312
3313            let mut config = Config::load().unwrap();
3314            config.eth_rpc_url = Some("polygonMumbai".to_string());
3315            assert_eq!(
3316                "https://polygon-mumbai.g.alchemy.com/v2/123455",
3317                config.get_rpc_url().unwrap().unwrap()
3318            );
3319
3320            Ok(())
3321        })
3322    }
3323
3324    #[test]
3325    fn test_resolve_rpc_aliases() {
3326        figment::Jail::expect_with(|jail| {
3327            jail.create_file(
3328                "foundry.toml",
3329                r#"
3330               [profile.default]
3331               [etherscan]
3332               arbitrum_alias = { key = "${TEST_RESOLVE_RPC_ALIAS_ARBISCAN}" }
3333               [rpc_endpoints]
3334               arbitrum_alias = "https://arb-mainnet.g.alchemy.com/v2/${TEST_RESOLVE_RPC_ALIAS_ARB_ONE}"
3335            "#,
3336            )?;
3337
3338            jail.set_env("TEST_RESOLVE_RPC_ALIAS_ARB_ONE", "123455");
3339            jail.set_env("TEST_RESOLVE_RPC_ALIAS_ARBISCAN", "123455");
3340
3341            let config = Config::load().unwrap();
3342
3343            let config = config.get_etherscan_config_with_chain(Some(NamedChain::Arbitrum.into()));
3344            assert!(config.is_err());
3345            assert_eq!(
3346                config.unwrap_err().to_string(),
3347                "At least one of `url` or `chain` must be present for Etherscan config with unknown alias `arbitrum_alias`"
3348            );
3349
3350            Ok(())
3351        });
3352    }
3353
3354    #[test]
3355    fn test_resolve_rpc_config() {
3356        figment::Jail::expect_with(|jail| {
3357            jail.create_file(
3358                "foundry.toml",
3359                r#"
3360                [rpc_endpoints]
3361                optimism = "https://example.com/"
3362                mainnet = { endpoint = "${_CONFIG_MAINNET}", retries = 3, retry_backoff = 1000, compute_units_per_second = 1000 }
3363            "#,
3364            )?;
3365            jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3366
3367            let config = Config::load().unwrap();
3368            assert_eq!(
3369                RpcEndpoints::new([
3370                    (
3371                        "optimism",
3372                        RpcEndpointType::String(RpcEndpointUrl::Url(
3373                            "https://example.com/".to_string()
3374                        ))
3375                    ),
3376                    (
3377                        "mainnet",
3378                        RpcEndpointType::Config(RpcEndpoint {
3379                            endpoint: RpcEndpointUrl::Env("${_CONFIG_MAINNET}".to_string()),
3380                            config: RpcEndpointConfig {
3381                                retries: Some(3),
3382                                retry_backoff: Some(1000),
3383                                compute_units_per_second: Some(1000),
3384                            },
3385                            auth: None,
3386                        })
3387                    ),
3388                ]),
3389                config.rpc_endpoints
3390            );
3391
3392            let resolved = config.rpc_endpoints.resolved();
3393            assert_eq!(
3394                RpcEndpoints::new([
3395                    (
3396                        "optimism",
3397                        RpcEndpointType::String(RpcEndpointUrl::Url(
3398                            "https://example.com/".to_string()
3399                        ))
3400                    ),
3401                    (
3402                        "mainnet",
3403                        RpcEndpointType::Config(RpcEndpoint {
3404                            endpoint: RpcEndpointUrl::Env("${_CONFIG_MAINNET}".to_string()),
3405                            config: RpcEndpointConfig {
3406                                retries: Some(3),
3407                                retry_backoff: Some(1000),
3408                                compute_units_per_second: Some(1000),
3409                            },
3410                            auth: None,
3411                        })
3412                    ),
3413                ])
3414                .resolved(),
3415                resolved
3416            );
3417            Ok(())
3418        })
3419    }
3420
3421    #[test]
3422    fn test_resolve_auth() {
3423        figment::Jail::expect_with(|jail| {
3424            jail.create_file(
3425                "foundry.toml",
3426                r#"
3427                [profile.default]
3428                eth_rpc_url = "optimism"
3429                [rpc_endpoints]
3430                optimism = "https://example.com/"
3431                mainnet = { endpoint = "${_CONFIG_MAINNET}", retries = 3, retry_backoff = 1000, compute_units_per_second = 1000, auth = "Bearer ${_CONFIG_AUTH}" }
3432            "#,
3433            )?;
3434
3435            let config = Config::load().unwrap();
3436
3437            jail.set_env("_CONFIG_AUTH", "123456");
3438            jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3439
3440            assert_eq!(
3441                RpcEndpoints::new([
3442                    (
3443                        "optimism",
3444                        RpcEndpointType::String(RpcEndpointUrl::Url(
3445                            "https://example.com/".to_string()
3446                        ))
3447                    ),
3448                    (
3449                        "mainnet",
3450                        RpcEndpointType::Config(RpcEndpoint {
3451                            endpoint: RpcEndpointUrl::Env("${_CONFIG_MAINNET}".to_string()),
3452                            config: RpcEndpointConfig {
3453                                retries: Some(3),
3454                                retry_backoff: Some(1000),
3455                                compute_units_per_second: Some(1000)
3456                            },
3457                            auth: Some(RpcAuth::Env("Bearer ${_CONFIG_AUTH}".to_string())),
3458                        })
3459                    ),
3460                ]),
3461                config.rpc_endpoints
3462            );
3463            let resolved = config.rpc_endpoints.resolved();
3464            assert_eq!(
3465                RpcEndpoints::new([
3466                    (
3467                        "optimism",
3468                        RpcEndpointType::String(RpcEndpointUrl::Url(
3469                            "https://example.com/".to_string()
3470                        ))
3471                    ),
3472                    (
3473                        "mainnet",
3474                        RpcEndpointType::Config(RpcEndpoint {
3475                            endpoint: RpcEndpointUrl::Url(
3476                                "https://eth-mainnet.alchemyapi.io/v2/123455".to_string()
3477                            ),
3478                            config: RpcEndpointConfig {
3479                                retries: Some(3),
3480                                retry_backoff: Some(1000),
3481                                compute_units_per_second: Some(1000)
3482                            },
3483                            auth: Some(RpcAuth::Raw("Bearer 123456".to_string())),
3484                        })
3485                    ),
3486                ])
3487                .resolved(),
3488                resolved
3489            );
3490
3491            Ok(())
3492        });
3493    }
3494
3495    #[test]
3496    fn test_resolve_endpoints() {
3497        figment::Jail::expect_with(|jail| {
3498            jail.create_file(
3499                "foundry.toml",
3500                r#"
3501                [profile.default]
3502                eth_rpc_url = "optimism"
3503                [rpc_endpoints]
3504                optimism = "https://example.com/"
3505                mainnet = "${_CONFIG_MAINNET}"
3506                mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${_CONFIG_API_KEY1}"
3507                mainnet_3 = "https://eth-mainnet.alchemyapi.io/v2/${_CONFIG_API_KEY1}/${_CONFIG_API_KEY2}"
3508            "#,
3509            )?;
3510
3511            let config = Config::load().unwrap();
3512
3513            assert_eq!(config.get_rpc_url().unwrap().unwrap(), "https://example.com/");
3514
3515            assert!(config.rpc_endpoints.clone().resolved().has_unresolved());
3516
3517            jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3518            jail.set_env("_CONFIG_API_KEY1", "123456");
3519            jail.set_env("_CONFIG_API_KEY2", "98765");
3520
3521            let endpoints = config.rpc_endpoints.resolved();
3522
3523            assert!(!endpoints.has_unresolved());
3524
3525            assert_eq!(
3526                endpoints,
3527                RpcEndpoints::new([
3528                    ("optimism", RpcEndpointUrl::Url("https://example.com/".to_string())),
3529                    (
3530                        "mainnet",
3531                        RpcEndpointUrl::Url(
3532                            "https://eth-mainnet.alchemyapi.io/v2/123455".to_string()
3533                        )
3534                    ),
3535                    (
3536                        "mainnet_2",
3537                        RpcEndpointUrl::Url(
3538                            "https://eth-mainnet.alchemyapi.io/v2/123456".to_string()
3539                        )
3540                    ),
3541                    (
3542                        "mainnet_3",
3543                        RpcEndpointUrl::Url(
3544                            "https://eth-mainnet.alchemyapi.io/v2/123456/98765".to_string()
3545                        )
3546                    ),
3547                ])
3548                .resolved()
3549            );
3550
3551            Ok(())
3552        });
3553    }
3554
3555    #[test]
3556    fn test_extract_etherscan_config() {
3557        figment::Jail::expect_with(|jail| {
3558            jail.create_file(
3559                "foundry.toml",
3560                r#"
3561                [profile.default]
3562                etherscan_api_key = "optimism"
3563
3564                [etherscan]
3565                optimism = { key = "https://etherscan-optimism.com/" }
3566                mumbai = { key = "https://etherscan-mumbai.com/" }
3567            "#,
3568            )?;
3569
3570            let mut config = Config::load().unwrap();
3571
3572            let optimism = config.get_etherscan_api_key(Some(NamedChain::Optimism.into()));
3573            assert_eq!(optimism, Some("https://etherscan-optimism.com/".to_string()));
3574
3575            config.etherscan_api_key = Some("mumbai".to_string());
3576
3577            let mumbai = config.get_etherscan_api_key(Some(NamedChain::PolygonMumbai.into()));
3578            assert_eq!(mumbai, Some("https://etherscan-mumbai.com/".to_string()));
3579
3580            Ok(())
3581        });
3582    }
3583
3584    #[test]
3585    fn test_extract_etherscan_config_by_chain() {
3586        figment::Jail::expect_with(|jail| {
3587            jail.create_file(
3588                "foundry.toml",
3589                r#"
3590                [profile.default]
3591
3592                [etherscan]
3593                mumbai = { key = "https://etherscan-mumbai.com/", chain = 80001 }
3594            "#,
3595            )?;
3596
3597            let config = Config::load().unwrap();
3598
3599            let mumbai = config
3600                .get_etherscan_config_with_chain(Some(NamedChain::PolygonMumbai.into()))
3601                .unwrap()
3602                .unwrap();
3603            assert_eq!(mumbai.key, "https://etherscan-mumbai.com/".to_string());
3604
3605            Ok(())
3606        });
3607    }
3608
3609    #[test]
3610    fn test_extract_etherscan_config_by_chain_with_url() {
3611        figment::Jail::expect_with(|jail| {
3612            jail.create_file(
3613                "foundry.toml",
3614                r#"
3615                [profile.default]
3616
3617                [etherscan]
3618                mumbai = { key = "https://etherscan-mumbai.com/", chain = 80001 , url =  "https://verifier-url.com/"}
3619            "#,
3620            )?;
3621
3622            let config = Config::load().unwrap();
3623
3624            let mumbai = config
3625                .get_etherscan_config_with_chain(Some(NamedChain::PolygonMumbai.into()))
3626                .unwrap()
3627                .unwrap();
3628            assert_eq!(mumbai.key, "https://etherscan-mumbai.com/".to_string());
3629            assert_eq!(mumbai.api_url, "https://verifier-url.com/".to_string());
3630
3631            Ok(())
3632        });
3633    }
3634
3635    #[test]
3636    fn test_extract_etherscan_config_by_chain_and_alias() {
3637        figment::Jail::expect_with(|jail| {
3638            jail.create_file(
3639                "foundry.toml",
3640                r#"
3641                [profile.default]
3642                eth_rpc_url = "mumbai"
3643
3644                [etherscan]
3645                mumbai = { key = "https://etherscan-mumbai.com/" }
3646
3647                [rpc_endpoints]
3648                mumbai = "https://polygon-mumbai.g.alchemy.com/v2/mumbai"
3649            "#,
3650            )?;
3651
3652            let config = Config::load().unwrap();
3653
3654            let mumbai = config.get_etherscan_config_with_chain(None).unwrap().unwrap();
3655            assert_eq!(mumbai.key, "https://etherscan-mumbai.com/".to_string());
3656
3657            let mumbai_rpc = config.get_rpc_url().unwrap().unwrap();
3658            assert_eq!(mumbai_rpc, "https://polygon-mumbai.g.alchemy.com/v2/mumbai");
3659            Ok(())
3660        });
3661    }
3662
3663    #[test]
3664    fn test_toml_file() {
3665        figment::Jail::expect_with(|jail| {
3666            jail.create_file(
3667                "foundry.toml",
3668                r#"
3669                [profile.default]
3670                src = "some-source"
3671                out = "some-out"
3672                cache = true
3673                eth_rpc_url = "https://example.com/"
3674                verbosity = 3
3675                remappings = ["ds-test=lib/ds-test/"]
3676                via_ir = true
3677                rpc_storage_caching = { chains = [1, "optimism", 999999], endpoints = "all"}
3678                use_literal_content = false
3679                bytecode_hash = "ipfs"
3680                cbor_metadata = true
3681                revert_strings = "strip"
3682                allow_paths = ["allow", "paths"]
3683                build_info_path = "build-info"
3684                always_use_create_2_factory = true
3685
3686                [rpc_endpoints]
3687                optimism = "https://example.com/"
3688                mainnet = "${RPC_MAINNET}"
3689                mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}"
3690                mainnet_3 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}/${ANOTHER_KEY}"
3691            "#,
3692            )?;
3693
3694            let config = Config::load().unwrap();
3695            assert_eq!(
3696                config,
3697                Config {
3698                    src: "some-source".into(),
3699                    out: "some-out".into(),
3700                    cache: true,
3701                    eth_rpc_url: Some("https://example.com/".to_string()),
3702                    remappings: vec![Remapping::from_str("ds-test=lib/ds-test/").unwrap().into()],
3703                    verbosity: 3,
3704                    via_ir: true,
3705                    rpc_storage_caching: StorageCachingConfig {
3706                        chains: CachedChains::Chains(vec![
3707                            Chain::mainnet(),
3708                            Chain::optimism_mainnet(),
3709                            Chain::from_id(999999)
3710                        ]),
3711                        endpoints: CachedEndpoints::All,
3712                    },
3713                    use_literal_content: false,
3714                    bytecode_hash: BytecodeHash::Ipfs,
3715                    cbor_metadata: true,
3716                    revert_strings: Some(RevertStrings::Strip),
3717                    allow_paths: vec![PathBuf::from("allow"), PathBuf::from("paths")],
3718                    rpc_endpoints: RpcEndpoints::new([
3719                        ("optimism", RpcEndpointUrl::Url("https://example.com/".to_string())),
3720                        ("mainnet", RpcEndpointUrl::Env("${RPC_MAINNET}".to_string())),
3721                        (
3722                            "mainnet_2",
3723                            RpcEndpointUrl::Env(
3724                                "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}".to_string()
3725                            )
3726                        ),
3727                        (
3728                            "mainnet_3",
3729                            RpcEndpointUrl::Env(
3730                                "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}/${ANOTHER_KEY}"
3731                                    .to_string()
3732                            )
3733                        ),
3734                    ]),
3735                    build_info_path: Some("build-info".into()),
3736                    always_use_create_2_factory: true,
3737                    ..Config::default().normalized_optimizer_settings()
3738                }
3739            );
3740
3741            Ok(())
3742        });
3743    }
3744
3745    #[test]
3746    fn test_load_remappings() {
3747        figment::Jail::expect_with(|jail| {
3748            jail.create_file(
3749                "foundry.toml",
3750                r"
3751                [profile.default]
3752                remappings = ['nested/=lib/nested/']
3753            ",
3754            )?;
3755
3756            let config = Config::load_with_root(jail.directory()).unwrap();
3757            assert_eq!(
3758                config.remappings,
3759                vec![Remapping::from_str("nested/=lib/nested/").unwrap().into()]
3760            );
3761
3762            Ok(())
3763        });
3764    }
3765
3766    #[test]
3767    fn test_load_full_toml() {
3768        figment::Jail::expect_with(|jail| {
3769            jail.create_file(
3770                "foundry.toml",
3771                r#"
3772                [profile.default]
3773                auto_detect_solc = true
3774                block_base_fee_per_gas = 0
3775                block_coinbase = '0x0000000000000000000000000000000000000000'
3776                block_difficulty = 0
3777                block_prevrandao = '0x0000000000000000000000000000000000000000000000000000000000000000'
3778                block_number = 1
3779                block_timestamp = 1
3780                use_literal_content = false
3781                bytecode_hash = 'ipfs'
3782                cbor_metadata = true
3783                cache = true
3784                cache_path = 'cache'
3785                evm_version = 'london'
3786                extra_output = []
3787                extra_output_files = []
3788                always_use_create_2_factory = false
3789                ffi = false
3790                force = false
3791                gas_limit = 9223372036854775807
3792                gas_price = 0
3793                gas_reports = ['*']
3794                ignored_error_codes = [1878]
3795                ignored_warnings_from = ["something"]
3796                deny_warnings = false
3797                initial_balance = '0xffffffffffffffffffffffff'
3798                libraries = []
3799                libs = ['lib']
3800                memory_limit = 134217728
3801                names = false
3802                no_storage_caching = false
3803                no_rpc_rate_limit = false
3804                offline = false
3805                optimizer = true
3806                optimizer_runs = 200
3807                out = 'out'
3808                remappings = ['nested/=lib/nested/']
3809                sender = '0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38'
3810                sizes = false
3811                sparse_mode = false
3812                src = 'src'
3813                test = 'test'
3814                tx_origin = '0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38'
3815                verbosity = 0
3816                via_ir = false
3817
3818                [profile.default.rpc_storage_caching]
3819                chains = 'all'
3820                endpoints = 'all'
3821
3822                [rpc_endpoints]
3823                optimism = "https://example.com/"
3824                mainnet = "${RPC_MAINNET}"
3825                mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}"
3826
3827                [fuzz]
3828                runs = 256
3829                seed = '0x3e8'
3830                max_test_rejects = 65536
3831
3832                [invariant]
3833                runs = 256
3834                depth = 500
3835                fail_on_revert = false
3836                call_override = false
3837                shrink_run_limit = 5000
3838            "#,
3839            )?;
3840
3841            let config = Config::load_with_root(jail.directory()).unwrap();
3842
3843            assert_eq!(config.ignored_file_paths, vec![PathBuf::from("something")]);
3844            assert_eq!(config.fuzz.seed, Some(U256::from(1000)));
3845            assert_eq!(
3846                config.remappings,
3847                vec![Remapping::from_str("nested/=lib/nested/").unwrap().into()]
3848            );
3849
3850            assert_eq!(
3851                config.rpc_endpoints,
3852                RpcEndpoints::new([
3853                    ("optimism", RpcEndpointUrl::Url("https://example.com/".to_string())),
3854                    ("mainnet", RpcEndpointUrl::Env("${RPC_MAINNET}".to_string())),
3855                    (
3856                        "mainnet_2",
3857                        RpcEndpointUrl::Env(
3858                            "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}".to_string()
3859                        )
3860                    ),
3861                ]),
3862            );
3863
3864            Ok(())
3865        });
3866    }
3867
3868    #[test]
3869    fn test_solc_req() {
3870        figment::Jail::expect_with(|jail| {
3871            jail.create_file(
3872                "foundry.toml",
3873                r#"
3874                [profile.default]
3875                solc_version = "0.8.12"
3876            "#,
3877            )?;
3878
3879            let config = Config::load().unwrap();
3880            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));
3881
3882            jail.create_file(
3883                "foundry.toml",
3884                r#"
3885                [profile.default]
3886                solc = "0.8.12"
3887            "#,
3888            )?;
3889
3890            let config = Config::load().unwrap();
3891            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));
3892
3893            jail.create_file(
3894                "foundry.toml",
3895                r#"
3896                [profile.default]
3897                solc = "path/to/local/solc"
3898            "#,
3899            )?;
3900
3901            let config = Config::load().unwrap();
3902            assert_eq!(config.solc, Some(SolcReq::Local("path/to/local/solc".into())));
3903
3904            jail.set_env("FOUNDRY_SOLC_VERSION", "0.6.6");
3905            let config = Config::load().unwrap();
3906            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 6, 6))));
3907            Ok(())
3908        });
3909    }
3910
3911    // ensures the newer `solc` takes precedence over `solc_version`
3912    #[test]
3913    fn test_backwards_solc_version() {
3914        figment::Jail::expect_with(|jail| {
3915            jail.create_file(
3916                "foundry.toml",
3917                r#"
3918                [default]
3919                solc = "0.8.12"
3920                solc_version = "0.8.20"
3921            "#,
3922            )?;
3923
3924            let config = Config::load().unwrap();
3925            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));
3926
3927            Ok(())
3928        });
3929
3930        figment::Jail::expect_with(|jail| {
3931            jail.create_file(
3932                "foundry.toml",
3933                r#"
3934                [default]
3935                solc_version = "0.8.20"
3936            "#,
3937            )?;
3938
3939            let config = Config::load().unwrap();
3940            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 20))));
3941
3942            Ok(())
3943        });
3944    }
3945
3946    #[test]
3947    fn test_toml_casing_file() {
3948        figment::Jail::expect_with(|jail| {
3949            jail.create_file(
3950                "foundry.toml",
3951                r#"
3952                [profile.default]
3953                src = "some-source"
3954                out = "some-out"
3955                cache = true
3956                eth-rpc-url = "https://example.com/"
3957                evm-version = "berlin"
3958                auto-detect-solc = false
3959            "#,
3960            )?;
3961
3962            let config = Config::load().unwrap();
3963            assert_eq!(
3964                config,
3965                Config {
3966                    src: "some-source".into(),
3967                    out: "some-out".into(),
3968                    cache: true,
3969                    eth_rpc_url: Some("https://example.com/".to_string()),
3970                    auto_detect_solc: false,
3971                    evm_version: EvmVersion::Berlin,
3972                    ..Config::default().normalized_optimizer_settings()
3973                }
3974            );
3975
3976            Ok(())
3977        });
3978    }
3979
3980    #[test]
3981    fn test_output_selection() {
3982        figment::Jail::expect_with(|jail| {
3983            jail.create_file(
3984                "foundry.toml",
3985                r#"
3986                [profile.default]
3987                extra_output = ["metadata", "ir-optimized"]
3988                extra_output_files = ["metadata"]
3989            "#,
3990            )?;
3991
3992            let config = Config::load().unwrap();
3993
3994            assert_eq!(
3995                config.extra_output,
3996                vec![ContractOutputSelection::Metadata, ContractOutputSelection::IrOptimized]
3997            );
3998            assert_eq!(config.extra_output_files, vec![ContractOutputSelection::Metadata]);
3999
4000            Ok(())
4001        });
4002    }
4003
4004    #[test]
4005    fn test_precedence() {
4006        figment::Jail::expect_with(|jail| {
4007            jail.create_file(
4008                "foundry.toml",
4009                r#"
4010                [profile.default]
4011                src = "mysrc"
4012                out = "myout"
4013                verbosity = 3
4014            "#,
4015            )?;
4016
4017            let config = Config::load().unwrap();
4018            assert_eq!(
4019                config,
4020                Config {
4021                    src: "mysrc".into(),
4022                    out: "myout".into(),
4023                    verbosity: 3,
4024                    ..Config::default().normalized_optimizer_settings()
4025                }
4026            );
4027
4028            jail.set_env("FOUNDRY_SRC", r"other-src");
4029            let config = Config::load().unwrap();
4030            assert_eq!(
4031                config,
4032                Config {
4033                    src: "other-src".into(),
4034                    out: "myout".into(),
4035                    verbosity: 3,
4036                    ..Config::default().normalized_optimizer_settings()
4037                }
4038            );
4039
4040            jail.set_env("FOUNDRY_PROFILE", "foo");
4041            let val: Result<String, _> = Config::figment().extract_inner("profile");
4042            assert!(val.is_err());
4043
4044            Ok(())
4045        });
4046    }
4047
4048    #[test]
4049    fn test_extract_basic() {
4050        figment::Jail::expect_with(|jail| {
4051            jail.create_file(
4052                "foundry.toml",
4053                r#"
4054                [profile.default]
4055                src = "mysrc"
4056                out = "myout"
4057                verbosity = 3
4058                evm_version = 'berlin'
4059
4060                [profile.other]
4061                src = "other-src"
4062            "#,
4063            )?;
4064            let loaded = Config::load().unwrap();
4065            assert_eq!(loaded.evm_version, EvmVersion::Berlin);
4066            let base = loaded.into_basic();
4067            let default = Config::default();
4068            assert_eq!(
4069                base,
4070                BasicConfig {
4071                    profile: Config::DEFAULT_PROFILE,
4072                    src: "mysrc".into(),
4073                    out: "myout".into(),
4074                    libs: default.libs.clone(),
4075                    remappings: default.remappings.clone(),
4076                }
4077            );
4078            jail.set_env("FOUNDRY_PROFILE", r"other");
4079            let base = Config::figment().extract::<BasicConfig>().unwrap();
4080            assert_eq!(
4081                base,
4082                BasicConfig {
4083                    profile: Config::DEFAULT_PROFILE,
4084                    src: "other-src".into(),
4085                    out: "myout".into(),
4086                    libs: default.libs.clone(),
4087                    remappings: default.remappings,
4088                }
4089            );
4090            Ok(())
4091        });
4092    }
4093
4094    #[test]
4095    #[should_panic]
4096    fn test_parse_invalid_fuzz_weight() {
4097        figment::Jail::expect_with(|jail| {
4098            jail.create_file(
4099                "foundry.toml",
4100                r"
4101                [fuzz]
4102                dictionary_weight = 101
4103            ",
4104            )?;
4105            let _config = Config::load().unwrap();
4106            Ok(())
4107        });
4108    }
4109
4110    #[test]
4111    fn test_fallback_provider() {
4112        figment::Jail::expect_with(|jail| {
4113            jail.create_file(
4114                "foundry.toml",
4115                r"
4116                [fuzz]
4117                runs = 1
4118                include_storage = false
4119                dictionary_weight = 99
4120
4121                [invariant]
4122                runs = 420
4123
4124                [profile.ci.fuzz]
4125                dictionary_weight = 5
4126
4127                [profile.ci.invariant]
4128                runs = 400
4129            ",
4130            )?;
4131
4132            let invariant_default = InvariantConfig::default();
4133            let config = Config::load().unwrap();
4134
4135            assert_ne!(config.invariant.runs, config.fuzz.runs);
4136            assert_eq!(config.invariant.runs, 420);
4137
4138            assert_ne!(
4139                config.fuzz.dictionary.include_storage,
4140                invariant_default.dictionary.include_storage
4141            );
4142            assert_eq!(
4143                config.invariant.dictionary.include_storage,
4144                config.fuzz.dictionary.include_storage
4145            );
4146
4147            assert_ne!(
4148                config.fuzz.dictionary.dictionary_weight,
4149                invariant_default.dictionary.dictionary_weight
4150            );
4151            assert_eq!(
4152                config.invariant.dictionary.dictionary_weight,
4153                config.fuzz.dictionary.dictionary_weight
4154            );
4155
4156            jail.set_env("FOUNDRY_PROFILE", "ci");
4157            let ci_config = Config::load().unwrap();
4158            assert_eq!(ci_config.fuzz.runs, 1);
4159            assert_eq!(ci_config.invariant.runs, 400);
4160            assert_eq!(ci_config.fuzz.dictionary.dictionary_weight, 5);
4161            assert_eq!(
4162                ci_config.invariant.dictionary.dictionary_weight,
4163                config.fuzz.dictionary.dictionary_weight
4164            );
4165
4166            Ok(())
4167        })
4168    }
4169
4170    #[test]
4171    fn test_standalone_profile_sections() {
4172        figment::Jail::expect_with(|jail| {
4173            jail.create_file(
4174                "foundry.toml",
4175                r"
4176                [fuzz]
4177                runs = 100
4178
4179                [invariant]
4180                runs = 120
4181
4182                [profile.ci.fuzz]
4183                runs = 420
4184
4185                [profile.ci.invariant]
4186                runs = 500
4187            ",
4188            )?;
4189
4190            let config = Config::load().unwrap();
4191            assert_eq!(config.fuzz.runs, 100);
4192            assert_eq!(config.invariant.runs, 120);
4193
4194            jail.set_env("FOUNDRY_PROFILE", "ci");
4195            let config = Config::load().unwrap();
4196            assert_eq!(config.fuzz.runs, 420);
4197            assert_eq!(config.invariant.runs, 500);
4198
4199            Ok(())
4200        });
4201    }
4202
4203    #[test]
4204    fn can_handle_deviating_dapp_aliases() {
4205        figment::Jail::expect_with(|jail| {
4206            let addr = Address::ZERO;
4207            jail.set_env("DAPP_TEST_NUMBER", 1337);
4208            jail.set_env("DAPP_TEST_ADDRESS", format!("{addr:?}"));
4209            jail.set_env("DAPP_TEST_FUZZ_RUNS", 420);
4210            jail.set_env("DAPP_TEST_DEPTH", 20);
4211            jail.set_env("DAPP_FORK_BLOCK", 100);
4212            jail.set_env("DAPP_BUILD_OPTIMIZE_RUNS", 999);
4213            jail.set_env("DAPP_BUILD_OPTIMIZE", 0);
4214
4215            let config = Config::load().unwrap();
4216
4217            assert_eq!(config.block_number, U256::from(1337));
4218            assert_eq!(config.sender, addr);
4219            assert_eq!(config.fuzz.runs, 420);
4220            assert_eq!(config.invariant.depth, 20);
4221            assert_eq!(config.fork_block_number, Some(100));
4222            assert_eq!(config.optimizer_runs, Some(999));
4223            assert!(!config.optimizer.unwrap());
4224
4225            Ok(())
4226        });
4227    }
4228
4229    #[test]
4230    fn can_parse_libraries() {
4231        figment::Jail::expect_with(|jail| {
4232            jail.set_env(
4233                "DAPP_LIBRARIES",
4234                "[src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6]",
4235            );
4236            let config = Config::load().unwrap();
4237            assert_eq!(
4238                config.libraries,
4239                vec![
4240                    "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4241                        .to_string()
4242                ]
4243            );
4244
4245            jail.set_env(
4246                "DAPP_LIBRARIES",
4247                "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6",
4248            );
4249            let config = Config::load().unwrap();
4250            assert_eq!(
4251                config.libraries,
4252                vec![
4253                    "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4254                        .to_string(),
4255                ]
4256            );
4257
4258            jail.set_env(
4259                "DAPP_LIBRARIES",
4260                "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6,src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6",
4261            );
4262            let config = Config::load().unwrap();
4263            assert_eq!(
4264                config.libraries,
4265                vec![
4266                    "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4267                        .to_string(),
4268                    "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4269                        .to_string()
4270                ]
4271            );
4272
4273            Ok(())
4274        });
4275    }
4276
4277    #[test]
4278    fn test_parse_many_libraries() {
4279        figment::Jail::expect_with(|jail| {
4280            jail.create_file(
4281                "foundry.toml",
4282                r"
4283                [profile.default]
4284               libraries= [
4285                        './src/SizeAuctionDiscount.sol:Chainlink:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
4286                        './src/SizeAuction.sol:ChainlinkTWAP:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
4287                        './src/SizeAuction.sol:Math:0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c',
4288                        './src/test/ChainlinkTWAP.t.sol:ChainlinkTWAP:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
4289                        './src/SizeAuctionDiscount.sol:Math:0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c',
4290                    ]
4291            ",
4292            )?;
4293            let config = Config::load().unwrap();
4294
4295            let libs = config.parsed_libraries().unwrap().libs;
4296
4297            similar_asserts::assert_eq!(
4298                libs,
4299                BTreeMap::from([
4300                    (
4301                        PathBuf::from("./src/SizeAuctionDiscount.sol"),
4302                        BTreeMap::from([
4303                            (
4304                                "Chainlink".to_string(),
4305                                "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
4306                            ),
4307                            (
4308                                "Math".to_string(),
4309                                "0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c".to_string()
4310                            )
4311                        ])
4312                    ),
4313                    (
4314                        PathBuf::from("./src/SizeAuction.sol"),
4315                        BTreeMap::from([
4316                            (
4317                                "ChainlinkTWAP".to_string(),
4318                                "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
4319                            ),
4320                            (
4321                                "Math".to_string(),
4322                                "0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c".to_string()
4323                            )
4324                        ])
4325                    ),
4326                    (
4327                        PathBuf::from("./src/test/ChainlinkTWAP.t.sol"),
4328                        BTreeMap::from([(
4329                            "ChainlinkTWAP".to_string(),
4330                            "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
4331                        )])
4332                    ),
4333                ])
4334            );
4335
4336            Ok(())
4337        });
4338    }
4339
4340    #[test]
4341    fn config_roundtrip() {
4342        figment::Jail::expect_with(|jail| {
4343            let default = Config::default().normalized_optimizer_settings();
4344            let basic = default.clone().into_basic();
4345            jail.create_file("foundry.toml", &basic.to_string_pretty().unwrap())?;
4346
4347            let mut other = Config::load().unwrap();
4348            clear_warning(&mut other);
4349            assert_eq!(default, other);
4350
4351            let other = other.into_basic();
4352            assert_eq!(basic, other);
4353
4354            jail.create_file("foundry.toml", &default.to_string_pretty().unwrap())?;
4355            let mut other = Config::load().unwrap();
4356            clear_warning(&mut other);
4357            assert_eq!(default, other);
4358
4359            Ok(())
4360        });
4361    }
4362
4363    #[test]
4364    fn test_fs_permissions() {
4365        figment::Jail::expect_with(|jail| {
4366            jail.create_file(
4367                "foundry.toml",
4368                r#"
4369                [profile.default]
4370                fs_permissions = [{ access = "read-write", path = "./"}]
4371            "#,
4372            )?;
4373            let loaded = Config::load().unwrap();
4374
4375            assert_eq!(
4376                loaded.fs_permissions,
4377                FsPermissions::new(vec![PathPermission::read_write("./")])
4378            );
4379
4380            jail.create_file(
4381                "foundry.toml",
4382                r#"
4383                [profile.default]
4384                fs_permissions = [{ access = "none", path = "./"}]
4385            "#,
4386            )?;
4387            let loaded = Config::load().unwrap();
4388            assert_eq!(loaded.fs_permissions, FsPermissions::new(vec![PathPermission::none("./")]));
4389
4390            Ok(())
4391        });
4392    }
4393
4394    #[test]
4395    fn test_optimizer_settings_basic() {
4396        figment::Jail::expect_with(|jail| {
4397            jail.create_file(
4398                "foundry.toml",
4399                r"
4400                [profile.default]
4401                optimizer = true
4402
4403                [profile.default.optimizer_details]
4404                yul = false
4405
4406                [profile.default.optimizer_details.yulDetails]
4407                stackAllocation = true
4408            ",
4409            )?;
4410            let mut loaded = Config::load().unwrap();
4411            clear_warning(&mut loaded);
4412            assert_eq!(
4413                loaded.optimizer_details,
4414                Some(OptimizerDetails {
4415                    yul: Some(false),
4416                    yul_details: Some(YulDetails {
4417                        stack_allocation: Some(true),
4418                        ..Default::default()
4419                    }),
4420                    ..Default::default()
4421                })
4422            );
4423
4424            let s = loaded.to_string_pretty().unwrap();
4425            jail.create_file("foundry.toml", &s)?;
4426
4427            let mut reloaded = Config::load().unwrap();
4428            clear_warning(&mut reloaded);
4429            assert_eq!(loaded, reloaded);
4430
4431            Ok(())
4432        });
4433    }
4434
4435    #[test]
4436    fn test_model_checker_settings_basic() {
4437        figment::Jail::expect_with(|jail| {
4438            jail.create_file(
4439                "foundry.toml",
4440                r"
4441                [profile.default]
4442
4443                [profile.default.model_checker]
4444                contracts = { 'a.sol' = [ 'A1', 'A2' ], 'b.sol' = [ 'B1', 'B2' ] }
4445                engine = 'chc'
4446                targets = [ 'assert', 'outOfBounds' ]
4447                timeout = 10000
4448            ",
4449            )?;
4450            let mut loaded = Config::load().unwrap();
4451            clear_warning(&mut loaded);
4452            assert_eq!(
4453                loaded.model_checker,
4454                Some(ModelCheckerSettings {
4455                    contracts: BTreeMap::from([
4456                        ("a.sol".to_string(), vec!["A1".to_string(), "A2".to_string()]),
4457                        ("b.sol".to_string(), vec!["B1".to_string(), "B2".to_string()]),
4458                    ]),
4459                    engine: Some(ModelCheckerEngine::CHC),
4460                    targets: Some(vec![
4461                        ModelCheckerTarget::Assert,
4462                        ModelCheckerTarget::OutOfBounds
4463                    ]),
4464                    timeout: Some(10000),
4465                    invariants: None,
4466                    show_unproved: None,
4467                    div_mod_with_slacks: None,
4468                    solvers: None,
4469                    show_unsupported: None,
4470                    show_proved_safe: None,
4471                })
4472            );
4473
4474            let s = loaded.to_string_pretty().unwrap();
4475            jail.create_file("foundry.toml", &s)?;
4476
4477            let mut reloaded = Config::load().unwrap();
4478            clear_warning(&mut reloaded);
4479            assert_eq!(loaded, reloaded);
4480
4481            Ok(())
4482        });
4483    }
4484
4485    #[test]
4486    fn test_model_checker_settings_relative_paths() {
4487        figment::Jail::expect_with(|jail| {
4488            jail.create_file(
4489                "foundry.toml",
4490                r"
4491                [profile.default]
4492
4493                [profile.default.model_checker]
4494                contracts = { 'a.sol' = [ 'A1', 'A2' ], 'b.sol' = [ 'B1', 'B2' ] }
4495                engine = 'chc'
4496                targets = [ 'assert', 'outOfBounds' ]
4497                timeout = 10000
4498            ",
4499            )?;
4500            let loaded = Config::load().unwrap().sanitized();
4501
4502            // NOTE(onbjerg): We have to canonicalize the path here using dunce because figment will
4503            // canonicalize the jail path using the standard library. The standard library *always*
4504            // transforms Windows paths to some weird extended format, which none of our code base
4505            // does.
4506            let dir = foundry_compilers::utils::canonicalize(jail.directory())
4507                .expect("Could not canonicalize jail path");
4508            assert_eq!(
4509                loaded.model_checker,
4510                Some(ModelCheckerSettings {
4511                    contracts: BTreeMap::from([
4512                        (
4513                            format!("{}", dir.join("a.sol").display()),
4514                            vec!["A1".to_string(), "A2".to_string()]
4515                        ),
4516                        (
4517                            format!("{}", dir.join("b.sol").display()),
4518                            vec!["B1".to_string(), "B2".to_string()]
4519                        ),
4520                    ]),
4521                    engine: Some(ModelCheckerEngine::CHC),
4522                    targets: Some(vec![
4523                        ModelCheckerTarget::Assert,
4524                        ModelCheckerTarget::OutOfBounds
4525                    ]),
4526                    timeout: Some(10000),
4527                    invariants: None,
4528                    show_unproved: None,
4529                    div_mod_with_slacks: None,
4530                    solvers: None,
4531                    show_unsupported: None,
4532                    show_proved_safe: None,
4533                })
4534            );
4535
4536            Ok(())
4537        });
4538    }
4539
4540    #[test]
4541    fn test_fmt_config() {
4542        figment::Jail::expect_with(|jail| {
4543            jail.create_file(
4544                "foundry.toml",
4545                r#"
4546                [fmt]
4547                line_length = 100
4548                tab_width = 2
4549                bracket_spacing = true
4550                style = "space"
4551            "#,
4552            )?;
4553            let loaded = Config::load().unwrap().sanitized();
4554            assert_eq!(
4555                loaded.fmt,
4556                FormatterConfig {
4557                    line_length: 100,
4558                    tab_width: 2,
4559                    bracket_spacing: true,
4560                    style: IndentStyle::Space,
4561                    ..Default::default()
4562                }
4563            );
4564
4565            Ok(())
4566        });
4567    }
4568
4569    #[test]
4570    fn test_lint_config() {
4571        figment::Jail::expect_with(|jail| {
4572            jail.create_file(
4573                "foundry.toml",
4574                r"
4575                [lint]
4576                severity = ['high', 'medium']
4577                exclude_lints = ['incorrect-shift']
4578                ",
4579            )?;
4580            let loaded = Config::load().unwrap().sanitized();
4581            assert_eq!(
4582                loaded.lint,
4583                LinterConfig {
4584                    severity: vec![LintSeverity::High, LintSeverity::Med],
4585                    exclude_lints: vec!["incorrect-shift".into()],
4586                    ..Default::default()
4587                }
4588            );
4589
4590            Ok(())
4591        });
4592    }
4593
4594    #[test]
4595    fn test_invariant_config() {
4596        figment::Jail::expect_with(|jail| {
4597            jail.create_file(
4598                "foundry.toml",
4599                r"
4600                [invariant]
4601                runs = 512
4602                depth = 10
4603            ",
4604            )?;
4605
4606            let loaded = Config::load().unwrap().sanitized();
4607            assert_eq!(
4608                loaded.invariant,
4609                InvariantConfig {
4610                    runs: 512,
4611                    depth: 10,
4612                    failure_persist_dir: Some(PathBuf::from("cache/invariant")),
4613                    ..Default::default()
4614                }
4615            );
4616
4617            Ok(())
4618        });
4619    }
4620
4621    #[test]
4622    fn test_standalone_sections_env() {
4623        figment::Jail::expect_with(|jail| {
4624            jail.create_file(
4625                "foundry.toml",
4626                r"
4627                [fuzz]
4628                runs = 100
4629
4630                [invariant]
4631                depth = 1
4632            ",
4633            )?;
4634
4635            jail.set_env("FOUNDRY_FMT_LINE_LENGTH", "95");
4636            jail.set_env("FOUNDRY_FUZZ_DICTIONARY_WEIGHT", "99");
4637            jail.set_env("FOUNDRY_INVARIANT_DEPTH", "5");
4638
4639            let config = Config::load().unwrap();
4640            assert_eq!(config.fmt.line_length, 95);
4641            assert_eq!(config.fuzz.dictionary.dictionary_weight, 99);
4642            assert_eq!(config.invariant.depth, 5);
4643
4644            Ok(())
4645        });
4646    }
4647
4648    #[test]
4649    fn test_parse_with_profile() {
4650        let foundry_str = r"
4651            [profile.default]
4652            src = 'src'
4653            out = 'out'
4654            libs = ['lib']
4655
4656            # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
4657        ";
4658        assert_eq!(
4659            parse_with_profile::<BasicConfig>(foundry_str).unwrap().unwrap(),
4660            (
4661                Config::DEFAULT_PROFILE,
4662                BasicConfig {
4663                    profile: Config::DEFAULT_PROFILE,
4664                    src: "src".into(),
4665                    out: "out".into(),
4666                    libs: vec!["lib".into()],
4667                    remappings: vec![]
4668                }
4669            )
4670        );
4671    }
4672
4673    #[test]
4674    fn test_implicit_profile_loads() {
4675        figment::Jail::expect_with(|jail| {
4676            jail.create_file(
4677                "foundry.toml",
4678                r"
4679                [default]
4680                src = 'my-src'
4681                out = 'my-out'
4682            ",
4683            )?;
4684            let loaded = Config::load().unwrap().sanitized();
4685            assert_eq!(loaded.src.file_name().unwrap(), "my-src");
4686            assert_eq!(loaded.out.file_name().unwrap(), "my-out");
4687            assert_eq!(
4688                loaded.warnings,
4689                vec![Warning::UnknownSection {
4690                    unknown_section: Profile::new("default"),
4691                    source: Some("foundry.toml".into())
4692                }]
4693            );
4694
4695            Ok(())
4696        });
4697    }
4698
4699    #[test]
4700    fn test_etherscan_api_key() {
4701        figment::Jail::expect_with(|jail| {
4702            jail.create_file(
4703                "foundry.toml",
4704                r"
4705                [default]
4706            ",
4707            )?;
4708            jail.set_env("ETHERSCAN_API_KEY", "");
4709            let loaded = Config::load().unwrap().sanitized();
4710            assert!(loaded.etherscan_api_key.is_none());
4711
4712            jail.set_env("ETHERSCAN_API_KEY", "DUMMY");
4713            let loaded = Config::load().unwrap().sanitized();
4714            assert_eq!(loaded.etherscan_api_key, Some("DUMMY".into()));
4715
4716            Ok(())
4717        });
4718    }
4719
4720    #[test]
4721    fn test_etherscan_api_key_figment() {
4722        figment::Jail::expect_with(|jail| {
4723            jail.create_file(
4724                "foundry.toml",
4725                r"
4726                [default]
4727                etherscan_api_key = 'DUMMY'
4728            ",
4729            )?;
4730            jail.set_env("ETHERSCAN_API_KEY", "ETHER");
4731
4732            let figment = Config::figment_with_root(jail.directory())
4733                .merge(("etherscan_api_key", "USER_KEY"));
4734
4735            let loaded = Config::from_provider(figment).unwrap();
4736            assert_eq!(loaded.etherscan_api_key, Some("USER_KEY".into()));
4737
4738            Ok(())
4739        });
4740    }
4741
4742    #[test]
4743    fn test_normalize_defaults() {
4744        figment::Jail::expect_with(|jail| {
4745            jail.create_file(
4746                "foundry.toml",
4747                r"
4748                [default]
4749                solc = '0.8.13'
4750            ",
4751            )?;
4752
4753            let loaded = Config::load().unwrap().sanitized();
4754            assert_eq!(loaded.evm_version, EvmVersion::London);
4755            Ok(())
4756        });
4757    }
4758
4759    // a test to print the config, mainly used to update the example config in the README
4760    #[expect(clippy::disallowed_macros)]
4761    #[test]
4762    #[ignore]
4763    fn print_config() {
4764        let config = Config {
4765            optimizer_details: Some(OptimizerDetails {
4766                peephole: None,
4767                inliner: None,
4768                jumpdest_remover: None,
4769                order_literals: None,
4770                deduplicate: None,
4771                cse: None,
4772                constant_optimizer: Some(true),
4773                yul: Some(true),
4774                yul_details: Some(YulDetails {
4775                    stack_allocation: None,
4776                    optimizer_steps: Some("dhfoDgvulfnTUtnIf".to_string()),
4777                }),
4778                simple_counter_for_loop_unchecked_increment: None,
4779            }),
4780            ..Default::default()
4781        };
4782        println!("{}", config.to_string_pretty().unwrap());
4783    }
4784
4785    #[test]
4786    fn can_use_impl_figment_macro() {
4787        #[derive(Default, Serialize)]
4788        struct MyArgs {
4789            #[serde(skip_serializing_if = "Option::is_none")]
4790            root: Option<PathBuf>,
4791        }
4792        impl_figment_convert!(MyArgs);
4793
4794        impl Provider for MyArgs {
4795            fn metadata(&self) -> Metadata {
4796                Metadata::default()
4797            }
4798
4799            fn data(&self) -> Result<Map<Profile, Dict>, Error> {
4800                let value = Value::serialize(self)?;
4801                let error = InvalidType(value.to_actual(), "map".into());
4802                let dict = value.into_dict().ok_or(error)?;
4803                Ok(Map::from([(Config::selected_profile(), dict)]))
4804            }
4805        }
4806
4807        let _figment: Figment = From::from(&MyArgs::default());
4808
4809        #[derive(Default)]
4810        struct Outer {
4811            start: MyArgs,
4812            other: MyArgs,
4813            another: MyArgs,
4814        }
4815        impl_figment_convert!(Outer, start, other, another);
4816
4817        let _figment: Figment = From::from(&Outer::default());
4818    }
4819
4820    #[test]
4821    fn list_cached_blocks() -> eyre::Result<()> {
4822        fn fake_block_cache(chain_path: &Path, block_number: &str, size_bytes: usize) {
4823            let block_path = chain_path.join(block_number);
4824            fs::create_dir(block_path.as_path()).unwrap();
4825            let file_path = block_path.join("storage.json");
4826            let mut file = File::create(file_path).unwrap();
4827            writeln!(file, "{}", vec![' '; size_bytes - 1].iter().collect::<String>()).unwrap();
4828        }
4829
4830        fn fake_block_cache_block_path_as_file(
4831            chain_path: &Path,
4832            block_number: &str,
4833            size_bytes: usize,
4834        ) {
4835            let block_path = chain_path.join(block_number);
4836            let mut file = File::create(block_path).unwrap();
4837            writeln!(file, "{}", vec![' '; size_bytes - 1].iter().collect::<String>()).unwrap();
4838        }
4839
4840        let chain_dir = tempdir()?;
4841
4842        fake_block_cache(chain_dir.path(), "1", 100);
4843        fake_block_cache(chain_dir.path(), "2", 500);
4844        fake_block_cache_block_path_as_file(chain_dir.path(), "3", 900);
4845        // Pollution file that should not show up in the cached block
4846        let mut pol_file = File::create(chain_dir.path().join("pol.txt")).unwrap();
4847        writeln!(pol_file, "{}", [' '; 10].iter().collect::<String>()).unwrap();
4848
4849        let result = Config::get_cached_blocks(chain_dir.path())?;
4850
4851        assert_eq!(result.len(), 3);
4852        let block1 = &result.iter().find(|x| x.0 == "1").unwrap();
4853        let block2 = &result.iter().find(|x| x.0 == "2").unwrap();
4854        let block3 = &result.iter().find(|x| x.0 == "3").unwrap();
4855
4856        assert_eq!(block1.0, "1");
4857        assert_eq!(block1.1, 100);
4858        assert_eq!(block2.0, "2");
4859        assert_eq!(block2.1, 500);
4860        assert_eq!(block3.0, "3");
4861        assert_eq!(block3.1, 900);
4862
4863        chain_dir.close()?;
4864        Ok(())
4865    }
4866
4867    #[test]
4868    fn list_etherscan_cache() -> eyre::Result<()> {
4869        fn fake_etherscan_cache(chain_path: &Path, address: &str, size_bytes: usize) {
4870            let metadata_path = chain_path.join("sources");
4871            let abi_path = chain_path.join("abi");
4872            let _ = fs::create_dir(metadata_path.as_path());
4873            let _ = fs::create_dir(abi_path.as_path());
4874
4875            let metadata_file_path = metadata_path.join(address);
4876            let mut metadata_file = File::create(metadata_file_path).unwrap();
4877            writeln!(metadata_file, "{}", vec![' '; size_bytes / 2 - 1].iter().collect::<String>())
4878                .unwrap();
4879
4880            let abi_file_path = abi_path.join(address);
4881            let mut abi_file = File::create(abi_file_path).unwrap();
4882            writeln!(abi_file, "{}", vec![' '; size_bytes / 2 - 1].iter().collect::<String>())
4883                .unwrap();
4884        }
4885
4886        let chain_dir = tempdir()?;
4887
4888        fake_etherscan_cache(chain_dir.path(), "1", 100);
4889        fake_etherscan_cache(chain_dir.path(), "2", 500);
4890
4891        let result = Config::get_cached_block_explorer_data(chain_dir.path())?;
4892
4893        assert_eq!(result, 600);
4894
4895        chain_dir.close()?;
4896        Ok(())
4897    }
4898
4899    #[test]
4900    fn test_parse_error_codes() {
4901        figment::Jail::expect_with(|jail| {
4902            jail.create_file(
4903                "foundry.toml",
4904                r#"
4905                [default]
4906                ignored_error_codes = ["license", "unreachable", 1337]
4907            "#,
4908            )?;
4909
4910            let config = Config::load().unwrap();
4911            assert_eq!(
4912                config.ignored_error_codes,
4913                vec![
4914                    SolidityErrorCode::SpdxLicenseNotProvided,
4915                    SolidityErrorCode::Unreachable,
4916                    SolidityErrorCode::Other(1337)
4917                ]
4918            );
4919
4920            Ok(())
4921        });
4922    }
4923
4924    #[test]
4925    fn test_parse_file_paths() {
4926        figment::Jail::expect_with(|jail| {
4927            jail.create_file(
4928                "foundry.toml",
4929                r#"
4930                [default]
4931                ignored_warnings_from = ["something"]
4932            "#,
4933            )?;
4934
4935            let config = Config::load().unwrap();
4936            assert_eq!(config.ignored_file_paths, vec![Path::new("something").to_path_buf()]);
4937
4938            Ok(())
4939        });
4940    }
4941
4942    #[test]
4943    fn test_parse_optimizer_settings() {
4944        figment::Jail::expect_with(|jail| {
4945            jail.create_file(
4946                "foundry.toml",
4947                r"
4948                [default]
4949                [profile.default.optimizer_details]
4950            ",
4951            )?;
4952
4953            let config = Config::load().unwrap();
4954            assert_eq!(config.optimizer_details, Some(OptimizerDetails::default()));
4955
4956            Ok(())
4957        });
4958    }
4959
4960    #[test]
4961    fn test_parse_labels() {
4962        figment::Jail::expect_with(|jail| {
4963            jail.create_file(
4964                "foundry.toml",
4965                r#"
4966                [labels]
4967                0x1F98431c8aD98523631AE4a59f267346ea31F984 = "Uniswap V3: Factory"
4968                0xC36442b4a4522E871399CD717aBDD847Ab11FE88 = "Uniswap V3: Positions NFT"
4969            "#,
4970            )?;
4971
4972            let config = Config::load().unwrap();
4973            assert_eq!(
4974                config.labels,
4975                AddressHashMap::from_iter(vec![
4976                    (
4977                        address!("0x1F98431c8aD98523631AE4a59f267346ea31F984"),
4978                        "Uniswap V3: Factory".to_string()
4979                    ),
4980                    (
4981                        address!("0xC36442b4a4522E871399CD717aBDD847Ab11FE88"),
4982                        "Uniswap V3: Positions NFT".to_string()
4983                    ),
4984                ])
4985            );
4986
4987            Ok(())
4988        });
4989    }
4990
4991    #[test]
4992    fn test_parse_vyper() {
4993        figment::Jail::expect_with(|jail| {
4994            jail.create_file(
4995                "foundry.toml",
4996                r#"
4997                [vyper]
4998                optimize = "codesize"
4999                path = "/path/to/vyper"
5000                experimental_codegen = true
5001            "#,
5002            )?;
5003
5004            let config = Config::load().unwrap();
5005            assert_eq!(
5006                config.vyper,
5007                VyperConfig {
5008                    optimize: Some(VyperOptimizationMode::Codesize),
5009                    path: Some("/path/to/vyper".into()),
5010                    experimental_codegen: Some(true),
5011                }
5012            );
5013
5014            Ok(())
5015        });
5016    }
5017
5018    #[test]
5019    fn test_parse_soldeer() {
5020        figment::Jail::expect_with(|jail| {
5021            jail.create_file(
5022                "foundry.toml",
5023                r#"
5024                [soldeer]
5025                remappings_generate = true
5026                remappings_regenerate = false
5027                remappings_version = true
5028                remappings_prefix = "@"
5029                remappings_location = "txt"
5030                recursive_deps = true
5031            "#,
5032            )?;
5033
5034            let config = Config::load().unwrap();
5035
5036            assert_eq!(
5037                config.soldeer,
5038                Some(SoldeerConfig {
5039                    remappings_generate: true,
5040                    remappings_regenerate: false,
5041                    remappings_version: true,
5042                    remappings_prefix: "@".to_string(),
5043                    remappings_location: RemappingsLocation::Txt,
5044                    recursive_deps: true,
5045                })
5046            );
5047
5048            Ok(())
5049        });
5050    }
5051
5052    // <https://github.com/foundry-rs/foundry/issues/10926>
5053    #[test]
5054    fn test_resolve_mesc_by_chain_id() {
5055        let s = r#"{
5056    "mesc_version": "0.2.1",
5057    "default_endpoint": null,
5058    "endpoints": {
5059        "sophon_50104": {
5060            "name": "sophon_50104",
5061            "url": "https://rpc.sophon.xyz",
5062            "chain_id": "50104",
5063            "endpoint_metadata": {}
5064        }
5065    },
5066    "network_defaults": {
5067    },
5068    "network_names": {},
5069    "profiles": {
5070        "foundry": {
5071            "name": "foundry",
5072            "default_endpoint": "local_ethereum",
5073            "network_defaults": {
5074                "50104": "sophon_50104"
5075            },
5076            "profile_metadata": {},
5077            "use_mesc": true
5078        }
5079    },
5080    "global_metadata": {}
5081}"#;
5082
5083        let config = serde_json::from_str(s).unwrap();
5084        let endpoint = mesc::query::get_endpoint_by_network(&config, "50104", Some("foundry"))
5085            .unwrap()
5086            .unwrap();
5087        assert_eq!(endpoint.url, "https://rpc.sophon.xyz");
5088
5089        let s = r#"{
5090    "mesc_version": "0.2.1",
5091    "default_endpoint": null,
5092    "endpoints": {
5093        "sophon_50104": {
5094            "name": "sophon_50104",
5095            "url": "https://rpc.sophon.xyz",
5096            "chain_id": "50104",
5097            "endpoint_metadata": {}
5098        }
5099    },
5100    "network_defaults": {
5101        "50104": "sophon_50104"
5102    },
5103    "network_names": {},
5104    "profiles": {},
5105    "global_metadata": {}
5106}"#;
5107
5108        let config = serde_json::from_str(s).unwrap();
5109        let endpoint = mesc::query::get_endpoint_by_network(&config, "50104", Some("foundry"))
5110            .unwrap()
5111            .unwrap();
5112        assert_eq!(endpoint.url, "https://rpc.sophon.xyz");
5113    }
5114
5115    #[test]
5116    fn test_get_etherscan_config_with_unknown_chain() {
5117        figment::Jail::expect_with(|jail| {
5118            jail.create_file(
5119                "foundry.toml",
5120                r#"
5121                [etherscan]
5122                mainnet = { chain = 3658348, key = "api-key"}
5123            "#,
5124            )?;
5125            let config = Config::load().unwrap();
5126            let unknown_chain = Chain::from_id(3658348);
5127            let result = config.get_etherscan_config_with_chain(Some(unknown_chain));
5128            assert!(result.is_err());
5129            let error_msg = result.unwrap_err().to_string();
5130            assert!(error_msg.contains("No known Etherscan API URL for chain `3658348`"));
5131            assert!(error_msg.contains("Specify a `url`"));
5132            assert!(error_msg.contains("Verify the chain `3658348` is correct"));
5133
5134            Ok(())
5135        });
5136    }
5137
5138    #[test]
5139    fn test_get_etherscan_config_with_existing_chain_and_url() {
5140        figment::Jail::expect_with(|jail| {
5141            jail.create_file(
5142                "foundry.toml",
5143                r#"
5144                [etherscan]
5145                mainnet = { chain = 1, key = "api-key" }
5146            "#,
5147            )?;
5148            let config = Config::load().unwrap();
5149            let unknown_chain = Chain::from_id(1);
5150            let result = config.get_etherscan_config_with_chain(Some(unknown_chain));
5151            assert!(result.is_ok());
5152            Ok(())
5153        });
5154    }
5155
5156    #[test]
5157    fn test_can_inherit_a_base_toml() {
5158        figment::Jail::expect_with(|jail| {
5159            // Create base config file with optimizer_runs = 800
5160            jail.create_file(
5161                "base-config.toml",
5162                r#"
5163                    [profile.default]
5164                    optimizer_runs = 800
5165
5166                    [invariant]
5167                    runs = 1000
5168
5169                    [rpc_endpoints]
5170                    mainnet = "https://example.com"
5171                    optimism = "https://example-2.com/"
5172                    "#,
5173            )?;
5174
5175            // Create local config that inherits from base-config.toml
5176            jail.create_file(
5177                "foundry.toml",
5178                r#"
5179                    [profile.default]
5180                    extends = "base-config.toml"
5181
5182                    [invariant]
5183                    runs = 333
5184                    depth = 15
5185
5186                    [rpc_endpoints]
5187                    mainnet = "https://reth-ethereum.ithaca.xyz/rpc"
5188                    "#,
5189            )?;
5190
5191            let config = Config::load().unwrap();
5192            assert_eq!(config.extends, Some(Extends::Path("base-config.toml".to_string())));
5193
5194            // optimizer_runs should be inherited from base-config.toml
5195            assert_eq!(config.optimizer_runs, Some(800));
5196
5197            // invariant settings should be overridden by local config
5198            assert_eq!(config.invariant.runs, 333);
5199            assert_eq!(config.invariant.depth, 15);
5200
5201            // rpc_endpoints.mainnet should be overridden by local config
5202            // optimism should be inherited from base config
5203            let endpoints = config.rpc_endpoints.resolved();
5204            assert!(
5205                endpoints
5206                    .get("mainnet")
5207                    .unwrap()
5208                    .url()
5209                    .unwrap()
5210                    .contains("reth-ethereum.ithaca.xyz")
5211            );
5212            assert!(endpoints.get("optimism").unwrap().url().unwrap().contains("example-2.com"));
5213
5214            Ok(())
5215        });
5216    }
5217
5218    #[test]
5219    fn test_inheritance_validation() {
5220        figment::Jail::expect_with(|jail| {
5221            // Test 1: Base file with 'extends' should fail
5222            jail.create_file(
5223                "base-with-inherit.toml",
5224                r#"
5225                    [profile.default]
5226                    extends = "another.toml"
5227                    optimizer_runs = 800
5228                    "#,
5229            )?;
5230
5231            jail.create_file(
5232                "foundry.toml",
5233                r#"
5234                    [profile.default]
5235                    extends = "base-with-inherit.toml"
5236                    "#,
5237            )?;
5238
5239            // Should fail because base file has 'extends'
5240            let result = Config::load();
5241            assert!(result.is_err());
5242            assert!(result.unwrap_err().to_string().contains("Nested inheritance is not allowed"));
5243
5244            // Test 2: Circular reference should fail
5245            jail.create_file(
5246                "foundry.toml",
5247                r#"
5248                    [profile.default]
5249                    extends = "foundry.toml"
5250                    "#,
5251            )?;
5252
5253            let result = Config::load();
5254            assert!(result.is_err());
5255            assert!(result.unwrap_err().to_string().contains("cannot inherit from itself"));
5256
5257            // Test 3: Non-existent base file should fail
5258            jail.create_file(
5259                "foundry.toml",
5260                r#"
5261                    [profile.default]
5262                    extends = "non-existent.toml"
5263                    "#,
5264            )?;
5265
5266            let result = Config::load();
5267            assert!(result.is_err());
5268            let err_msg = result.unwrap_err().to_string();
5269            assert!(
5270                err_msg.contains("does not exist")
5271                    || err_msg.contains("Failed to resolve inherited config path"),
5272                "Error message: {err_msg}"
5273            );
5274
5275            Ok(())
5276        });
5277    }
5278
5279    #[test]
5280    fn test_complex_inheritance_merging() {
5281        figment::Jail::expect_with(|jail| {
5282            // Create a comprehensive base config
5283            jail.create_file(
5284                "base.toml",
5285                r#"
5286                    [profile.default]
5287                    optimizer = true
5288                    optimizer_runs = 1000
5289                    via_ir = false
5290                    solc = "0.8.19"
5291
5292                    [invariant]
5293                    runs = 500
5294                    depth = 100
5295
5296                    [fuzz]
5297                    runs = 256
5298                    seed = "0x123"
5299
5300                    [rpc_endpoints]
5301                    mainnet = "https://base-mainnet.com"
5302                    optimism = "https://base-optimism.com"
5303                    arbitrum = "https://base-arbitrum.com"
5304                    "#,
5305            )?;
5306
5307            // Create local config that overrides some values
5308            jail.create_file(
5309                "foundry.toml",
5310                r#"
5311                    [profile.default]
5312                    extends = "base.toml"
5313                    optimizer_runs = 200  # Override
5314                    via_ir = true        # Override
5315                    # optimizer and solc are inherited
5316
5317                    [invariant]
5318                    runs = 333  # Override
5319                    # depth is inherited
5320
5321                    # fuzz section is fully inherited
5322
5323                    [rpc_endpoints]
5324                    mainnet = "https://local-mainnet.com"  # Override
5325                    # optimism and arbitrum are inherited
5326                    polygon = "https://local-polygon.com"  # New
5327                    "#,
5328            )?;
5329
5330            let config = Config::load().unwrap();
5331
5332            // Check profile.default values
5333            assert_eq!(config.optimizer, Some(true));
5334            assert_eq!(config.optimizer_runs, Some(200));
5335            assert_eq!(config.via_ir, true);
5336            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 19))));
5337
5338            // Check invariant section
5339            assert_eq!(config.invariant.runs, 333);
5340            assert_eq!(config.invariant.depth, 100);
5341
5342            // Check fuzz section (fully inherited)
5343            assert_eq!(config.fuzz.runs, 256);
5344            assert_eq!(config.fuzz.seed, Some(U256::from(0x123)));
5345
5346            // Check rpc_endpoints
5347            let endpoints = config.rpc_endpoints.resolved();
5348            assert!(endpoints.get("mainnet").unwrap().url().unwrap().contains("local-mainnet"));
5349            assert!(endpoints.get("optimism").unwrap().url().unwrap().contains("base-optimism"));
5350            assert!(endpoints.get("arbitrum").unwrap().url().unwrap().contains("base-arbitrum"));
5351            assert!(endpoints.get("polygon").unwrap().url().unwrap().contains("local-polygon"));
5352
5353            Ok(())
5354        });
5355    }
5356
5357    #[test]
5358    fn test_inheritance_with_different_profiles() {
5359        figment::Jail::expect_with(|jail| {
5360            // Create base config with multiple profiles
5361            jail.create_file(
5362                "base.toml",
5363                r#"
5364                    [profile.default]
5365                    optimizer = true
5366                    optimizer_runs = 200
5367
5368                    [profile.ci]
5369                    optimizer = true
5370                    optimizer_runs = 10000
5371                    via_ir = true
5372
5373                    [profile.dev]
5374                    optimizer = false
5375                    "#,
5376            )?;
5377
5378            // Local config inherits from base - only for default profile
5379            jail.create_file(
5380                "foundry.toml",
5381                r#"
5382                    [profile.default]
5383                    extends = "base.toml"
5384                    verbosity = 3
5385
5386                    [profile.ci]
5387                    optimizer_runs = 5000  # This doesn't inherit from base.toml's ci profile
5388                    "#,
5389            )?;
5390
5391            // Test default profile
5392            let config = Config::load().unwrap();
5393            assert_eq!(config.optimizer, Some(true));
5394            assert_eq!(config.optimizer_runs, Some(200));
5395            assert_eq!(config.verbosity, 3);
5396
5397            // Test CI profile (NO 'extends', so doesn't inherit from base)
5398            jail.set_env("FOUNDRY_PROFILE", "ci");
5399            let config = Config::load().unwrap();
5400            assert_eq!(config.optimizer_runs, Some(5000));
5401            assert_eq!(config.optimizer, Some(true));
5402            // via_ir is not set in local ci profile and there's no 'extends', so default
5403            assert_eq!(config.via_ir, false);
5404
5405            Ok(())
5406        });
5407    }
5408
5409    #[test]
5410    fn test_inheritance_with_env_vars() {
5411        figment::Jail::expect_with(|jail| {
5412            jail.create_file(
5413                "base.toml",
5414                r#"
5415                    [profile.default]
5416                    optimizer_runs = 500
5417                    sender = "0x0000000000000000000000000000000000000001"
5418                    verbosity = 1
5419                    "#,
5420            )?;
5421
5422            jail.create_file(
5423                "foundry.toml",
5424                r#"
5425                    [profile.default]
5426                    extends = "base.toml"
5427                    verbosity = 2
5428                    "#,
5429            )?;
5430
5431            // Environment variables should override both base and local values
5432            jail.set_env("FOUNDRY_OPTIMIZER_RUNS", "999");
5433            jail.set_env("FOUNDRY_VERBOSITY", "4");
5434
5435            let config = Config::load().unwrap();
5436            assert_eq!(config.optimizer_runs, Some(999));
5437            assert_eq!(config.verbosity, 4);
5438            assert_eq!(
5439                config.sender,
5440                "0x0000000000000000000000000000000000000001"
5441                    .parse::<alloy_primitives::Address>()
5442                    .unwrap()
5443            );
5444
5445            Ok(())
5446        });
5447    }
5448
5449    #[test]
5450    fn test_inheritance_with_subdirectories() {
5451        figment::Jail::expect_with(|jail| {
5452            // Create base config in a subdirectory
5453            jail.create_dir("configs")?;
5454            jail.create_file(
5455                "configs/base.toml",
5456                r#"
5457                    [profile.default]
5458                    optimizer_runs = 800
5459                    src = "contracts"
5460                    "#,
5461            )?;
5462
5463            // Reference it with relative path
5464            jail.create_file(
5465                "foundry.toml",
5466                r#"
5467                    [profile.default]
5468                    extends = "configs/base.toml"
5469                    test = "tests"
5470                    "#,
5471            )?;
5472
5473            let config = Config::load().unwrap();
5474            assert_eq!(config.optimizer_runs, Some(800));
5475            assert_eq!(config.src, PathBuf::from("contracts"));
5476            assert_eq!(config.test, PathBuf::from("tests"));
5477
5478            // Test with parent directory reference
5479            jail.create_dir("project")?;
5480            jail.create_file(
5481                "shared-base.toml",
5482                r#"
5483                    [profile.default]
5484                    optimizer_runs = 1500
5485                    "#,
5486            )?;
5487
5488            jail.create_file(
5489                "project/foundry.toml",
5490                r#"
5491                    [profile.default]
5492                    extends = "../shared-base.toml"
5493                    "#,
5494            )?;
5495
5496            std::env::set_current_dir(jail.directory().join("project")).unwrap();
5497            let config = Config::load().unwrap();
5498            assert_eq!(config.optimizer_runs, Some(1500));
5499
5500            Ok(())
5501        });
5502    }
5503
5504    #[test]
5505    fn test_inheritance_with_empty_files() {
5506        figment::Jail::expect_with(|jail| {
5507            // Empty base file
5508            jail.create_file(
5509                "base.toml",
5510                r#"
5511                    [profile.default]
5512                    "#,
5513            )?;
5514
5515            jail.create_file(
5516                "foundry.toml",
5517                r#"
5518                    [profile.default]
5519                    extends = "base.toml"
5520                    optimizer_runs = 300
5521                    "#,
5522            )?;
5523
5524            let config = Config::load().unwrap();
5525            assert_eq!(config.optimizer_runs, Some(300));
5526
5527            // Empty local file (only 'extends')
5528            jail.create_file(
5529                "base2.toml",
5530                r#"
5531                    [profile.default]
5532                    optimizer_runs = 400
5533                    via_ir = true
5534                    "#,
5535            )?;
5536
5537            jail.create_file(
5538                "foundry.toml",
5539                r#"
5540                    [profile.default]
5541                    extends = "base2.toml"
5542                    "#,
5543            )?;
5544
5545            let config = Config::load().unwrap();
5546            assert_eq!(config.optimizer_runs, Some(400));
5547            assert!(config.via_ir);
5548
5549            Ok(())
5550        });
5551    }
5552
5553    #[test]
5554    fn test_inheritance_array_and_table_merging() {
5555        figment::Jail::expect_with(|jail| {
5556            jail.create_file(
5557                "base.toml",
5558                r#"
5559                    [profile.default]
5560                    libs = ["lib", "node_modules"]
5561                    ignored_error_codes = [5667, 1878]
5562                    extra_output = ["metadata", "ir"]
5563
5564                    [profile.default.model_checker]
5565                    engine = "chc"
5566                    timeout = 10000
5567                    targets = ["assert"]
5568
5569                    [profile.default.optimizer_details]
5570                    peephole = true
5571                    inliner = true
5572                    "#,
5573            )?;
5574
5575            jail.create_file(
5576                "foundry.toml",
5577                r#"
5578                    [profile.default]
5579                    extends = "base.toml"
5580                    libs = ["custom-lib"]  # Concatenates with base array
5581                    ignored_error_codes = [2018]  # Concatenates with base array
5582
5583                    [profile.default.model_checker]
5584                    timeout = 5000  # Overrides base value
5585                    # engine and targets are inherited
5586
5587                    [profile.default.optimizer_details]
5588                    jumpdest_remover = true  # Adds new field
5589                    # peephole and inliner are inherited
5590                    "#,
5591            )?;
5592
5593            let config = Config::load().unwrap();
5594
5595            // Arrays are now concatenated with admerge (base + local)
5596            assert_eq!(
5597                config.libs,
5598                vec![
5599                    PathBuf::from("lib"),
5600                    PathBuf::from("node_modules"),
5601                    PathBuf::from("custom-lib")
5602                ]
5603            );
5604            assert_eq!(
5605                config.ignored_error_codes,
5606                vec![
5607                    SolidityErrorCode::UnusedFunctionParameter, // 5667 from base.toml
5608                    SolidityErrorCode::SpdxLicenseNotProvided,  // 1878 from base.toml
5609                    SolidityErrorCode::FunctionStateMutabilityCanBeRestricted  // 2018 from local
5610                ]
5611            );
5612
5613            // Tables are deep-merged
5614            assert_eq!(config.model_checker.as_ref().unwrap().timeout, Some(5000));
5615            assert_eq!(
5616                config.model_checker.as_ref().unwrap().engine,
5617                Some(ModelCheckerEngine::CHC)
5618            );
5619            assert_eq!(
5620                config.model_checker.as_ref().unwrap().targets,
5621                Some(vec![ModelCheckerTarget::Assert])
5622            );
5623
5624            // optimizer_details table is actually merged, not replaced
5625            assert_eq!(config.optimizer_details.as_ref().unwrap().peephole, Some(true));
5626            assert_eq!(config.optimizer_details.as_ref().unwrap().inliner, Some(true));
5627            assert_eq!(config.optimizer_details.as_ref().unwrap().jumpdest_remover, None);
5628
5629            Ok(())
5630        });
5631    }
5632
5633    #[test]
5634    fn test_inheritance_with_special_sections() {
5635        figment::Jail::expect_with(|jail| {
5636            jail.create_file(
5637                "base.toml",
5638                r#"
5639                    [profile.default]
5640                    # Base file should not have 'extends' to avoid nested inheritance
5641
5642                    [labels]
5643                    "0x0000000000000000000000000000000000000001" = "Alice"
5644                    "0x0000000000000000000000000000000000000002" = "Bob"
5645
5646                    [[profile.default.fs_permissions]]
5647                    access = "read"
5648                    path = "./src"
5649
5650                    [[profile.default.fs_permissions]]
5651                    access = "read-write"
5652                    path = "./cache"
5653                    "#,
5654            )?;
5655
5656            jail.create_file(
5657                "foundry.toml",
5658                r#"
5659                    [profile.default]
5660                    extends = "base.toml"
5661
5662                    [labels]
5663                    "0x0000000000000000000000000000000000000002" = "Bob Updated"
5664                    "0x0000000000000000000000000000000000000003" = "Charlie"
5665
5666                    [[profile.default.fs_permissions]]
5667                    access = "read"
5668                    path = "./test"
5669                    "#,
5670            )?;
5671
5672            let config = Config::load().unwrap();
5673
5674            // Labels should be merged
5675            assert_eq!(
5676                config.labels.get(
5677                    &"0x0000000000000000000000000000000000000001"
5678                        .parse::<alloy_primitives::Address>()
5679                        .unwrap()
5680                ),
5681                Some(&"Alice".to_string())
5682            );
5683            assert_eq!(
5684                config.labels.get(
5685                    &"0x0000000000000000000000000000000000000002"
5686                        .parse::<alloy_primitives::Address>()
5687                        .unwrap()
5688                ),
5689                Some(&"Bob Updated".to_string())
5690            );
5691            assert_eq!(
5692                config.labels.get(
5693                    &"0x0000000000000000000000000000000000000003"
5694                        .parse::<alloy_primitives::Address>()
5695                        .unwrap()
5696                ),
5697                Some(&"Charlie".to_string())
5698            );
5699
5700            // fs_permissions array is now concatenated with addmerge (base + local)
5701            assert_eq!(config.fs_permissions.permissions.len(), 3); // 2 from base + 1 from local
5702            // Check that all permissions are present
5703            assert!(
5704                config
5705                    .fs_permissions
5706                    .permissions
5707                    .iter()
5708                    .any(|p| p.path.to_str().unwrap() == "./src")
5709            );
5710            assert!(
5711                config
5712                    .fs_permissions
5713                    .permissions
5714                    .iter()
5715                    .any(|p| p.path.to_str().unwrap() == "./cache")
5716            );
5717            assert!(
5718                config
5719                    .fs_permissions
5720                    .permissions
5721                    .iter()
5722                    .any(|p| p.path.to_str().unwrap() == "./test")
5723            );
5724
5725            Ok(())
5726        });
5727    }
5728
5729    #[test]
5730    fn test_inheritance_with_compilation_settings() {
5731        figment::Jail::expect_with(|jail| {
5732            jail.create_file(
5733                "base.toml",
5734                r#"
5735                    [profile.default]
5736                    solc = "0.8.19"
5737                    evm_version = "paris"
5738                    via_ir = false
5739                    optimizer = true
5740                    optimizer_runs = 200
5741
5742                    [profile.default.optimizer_details]
5743                    peephole = true
5744                    inliner = false
5745                    jumpdest_remover = true
5746                    order_literals = false
5747                    deduplicate = true
5748                    cse = true
5749                    constant_optimizer = true
5750                    yul = true
5751
5752                    [profile.default.optimizer_details.yul_details]
5753                    stack_allocation = true
5754                    optimizer_steps = "dhfoDgvulfnTUtnIf"
5755                    "#,
5756            )?;
5757
5758            jail.create_file(
5759                "foundry.toml",
5760                r#"
5761                    [profile.default]
5762                    extends = "base.toml"
5763                    evm_version = "shanghai"  # Override
5764                    optimizer_runs = 1000  # Override
5765
5766                    [profile.default.optimizer_details]
5767                    inliner = true  # Override
5768                    # Rest inherited
5769                    "#,
5770            )?;
5771
5772            let config = Config::load().unwrap();
5773
5774            // Check compilation settings
5775            assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 19))));
5776            assert_eq!(config.evm_version, EvmVersion::Shanghai);
5777            assert_eq!(config.via_ir, false);
5778            assert_eq!(config.optimizer, Some(true));
5779            assert_eq!(config.optimizer_runs, Some(1000));
5780
5781            // Check optimizer details - the table is actually merged
5782            let details = config.optimizer_details.as_ref().unwrap();
5783            assert_eq!(details.peephole, Some(true));
5784            assert_eq!(details.inliner, Some(true));
5785            assert_eq!(details.jumpdest_remover, None);
5786            assert_eq!(details.order_literals, None);
5787            assert_eq!(details.deduplicate, Some(true));
5788            assert_eq!(details.cse, Some(true));
5789            assert_eq!(details.constant_optimizer, None);
5790            assert_eq!(details.yul, Some(true));
5791
5792            // Check yul details - inherited from base
5793            if let Some(yul_details) = details.yul_details.as_ref() {
5794                assert_eq!(yul_details.stack_allocation, Some(true));
5795                assert_eq!(yul_details.optimizer_steps, Some("dhfoDgvulfnTUtnIf".to_string()));
5796            }
5797
5798            Ok(())
5799        });
5800    }
5801
5802    #[test]
5803    fn test_inheritance_with_remappings() {
5804        figment::Jail::expect_with(|jail| {
5805            jail.create_file(
5806                "base.toml",
5807                r#"
5808                    [profile.default]
5809                    remappings = [
5810                        "forge-std/=lib/forge-std/src/",
5811                        "@openzeppelin/=lib/openzeppelin-contracts/",
5812                        "ds-test/=lib/ds-test/src/"
5813                    ]
5814                    auto_detect_remappings = false
5815                    "#,
5816            )?;
5817
5818            jail.create_file(
5819                "foundry.toml",
5820                r#"
5821                    [profile.default]
5822                    extends = "base.toml"
5823                    remappings = [
5824                        "@custom/=lib/custom/",
5825                        "ds-test/=lib/forge-std/lib/ds-test/src/"  # Note: This will be added alongside base remappings
5826                    ]
5827                    "#,
5828            )?;
5829
5830            let config = Config::load().unwrap();
5831
5832            // Remappings array is now concatenated with admerge (base + local)
5833            assert!(config.remappings.iter().any(|r| r.to_string().contains("@custom/")));
5834            assert!(config.remappings.iter().any(|r| r.to_string().contains("ds-test/")));
5835            assert!(config.remappings.iter().any(|r| r.to_string().contains("forge-std/")));
5836            assert!(config.remappings.iter().any(|r| r.to_string().contains("@openzeppelin/")));
5837
5838            // auto_detect_remappings should be inherited
5839            assert!(!config.auto_detect_remappings);
5840
5841            Ok(())
5842        });
5843    }
5844
5845    #[test]
5846    fn test_inheritance_with_multiple_profiles_and_single_file() {
5847        figment::Jail::expect_with(|jail| {
5848            // Create base config with prod and test profiles
5849            jail.create_file(
5850                "base.toml",
5851                r#"
5852                    [profile.prod]
5853                    optimizer = true
5854                    optimizer_runs = 10000
5855                    via_ir = true
5856
5857                    [profile.test]
5858                    optimizer = false
5859
5860                    [profile.test.fuzz]
5861                    runs = 100
5862                    "#,
5863            )?;
5864
5865            // Local config inherits from base for prod profile
5866            jail.create_file(
5867                "foundry.toml",
5868                r#"
5869                    [profile.prod]
5870                    extends = "base.toml"
5871                    evm_version = "shanghai"  # Additional setting
5872
5873                    [profile.test]
5874                    extends = "base.toml"
5875
5876                    [profile.test.fuzz]
5877                    runs = 500  # Override
5878                    "#,
5879            )?;
5880
5881            // Test prod profile
5882            jail.set_env("FOUNDRY_PROFILE", "prod");
5883            let config = Config::load().unwrap();
5884            assert_eq!(config.optimizer, Some(true));
5885            assert_eq!(config.optimizer_runs, Some(10000));
5886            assert_eq!(config.via_ir, true);
5887            assert_eq!(config.evm_version, EvmVersion::Shanghai);
5888
5889            // Test test profile
5890            jail.set_env("FOUNDRY_PROFILE", "test");
5891            let config = Config::load().unwrap();
5892            assert_eq!(config.optimizer, Some(false));
5893            assert_eq!(config.fuzz.runs, 500);
5894
5895            Ok(())
5896        });
5897    }
5898
5899    #[test]
5900    fn test_inheritance_with_multiple_profiles_and_files() {
5901        figment::Jail::expect_with(|jail| {
5902            jail.create_file(
5903                "prod.toml",
5904                r#"
5905                    [profile.prod]
5906                    optimizer = true
5907                    optimizer_runs = 20000
5908                    gas_limit = 50000000
5909                    "#,
5910            )?;
5911            jail.create_file(
5912                "dev.toml",
5913                r#"
5914                    [profile.dev]
5915                    optimizer = true
5916                    optimizer_runs = 333
5917                    gas_limit = 555555
5918                    "#,
5919            )?;
5920
5921            // Local config with only both profiles
5922            jail.create_file(
5923                "foundry.toml",
5924                r#"
5925                    [profile.dev]
5926                    extends = "dev.toml"
5927                    sender = "0x0000000000000000000000000000000000000001"
5928
5929                    [profile.prod]
5930                    extends = "prod.toml"
5931                    sender = "0x0000000000000000000000000000000000000002"
5932                    "#,
5933            )?;
5934
5935            // Test that prod profile correctly inherits even without a default profile
5936            jail.set_env("FOUNDRY_PROFILE", "dev");
5937            let config = Config::load().unwrap();
5938            assert_eq!(config.optimizer, Some(true));
5939            assert_eq!(config.optimizer_runs, Some(333));
5940            assert_eq!(config.gas_limit, 555555.into());
5941            assert_eq!(
5942                config.sender,
5943                "0x0000000000000000000000000000000000000001"
5944                    .parse::<alloy_primitives::Address>()
5945                    .unwrap()
5946            );
5947
5948            // Test that prod profile correctly inherits even without a default profile
5949            jail.set_env("FOUNDRY_PROFILE", "prod");
5950            let config = Config::load().unwrap();
5951            assert_eq!(config.optimizer, Some(true));
5952            assert_eq!(config.optimizer_runs, Some(20000));
5953            assert_eq!(config.gas_limit, 50000000.into());
5954            assert_eq!(
5955                config.sender,
5956                "0x0000000000000000000000000000000000000002"
5957                    .parse::<alloy_primitives::Address>()
5958                    .unwrap()
5959            );
5960
5961            Ok(())
5962        });
5963    }
5964
5965    #[test]
5966    fn test_extends_strategy_extend_arrays() {
5967        figment::Jail::expect_with(|jail| {
5968            // Create base config with arrays
5969            jail.create_file(
5970                "base.toml",
5971                r#"
5972                    [profile.default]
5973                    libs = ["lib", "node_modules"]
5974                    ignored_error_codes = [5667, 1878]
5975                    optimizer_runs = 200
5976                    "#,
5977            )?;
5978
5979            // Local config extends with extend-arrays strategy (concatenates arrays)
5980            jail.create_file(
5981                "foundry.toml",
5982                r#"
5983                    [profile.default]
5984                    extends = "base.toml"
5985                    libs = ["mylib", "customlib"]
5986                    ignored_error_codes = [1234]
5987                    optimizer_runs = 500
5988                    "#,
5989            )?;
5990
5991            let config = Config::load().unwrap();
5992
5993            // Arrays should be concatenated (base + local)
5994            assert_eq!(config.libs.len(), 4);
5995            assert!(config.libs.iter().any(|l| l.to_str() == Some("lib")));
5996            assert!(config.libs.iter().any(|l| l.to_str() == Some("node_modules")));
5997            assert!(config.libs.iter().any(|l| l.to_str() == Some("mylib")));
5998            assert!(config.libs.iter().any(|l| l.to_str() == Some("customlib")));
5999
6000            assert_eq!(config.ignored_error_codes.len(), 3);
6001            assert!(
6002                config.ignored_error_codes.contains(&SolidityErrorCode::UnusedFunctionParameter)
6003            ); // 5667
6004            assert!(
6005                config.ignored_error_codes.contains(&SolidityErrorCode::SpdxLicenseNotProvided)
6006            ); // 1878
6007            assert!(config.ignored_error_codes.contains(&SolidityErrorCode::from(1234u64))); // 1234 - generic
6008
6009            // Non-array values should be replaced
6010            assert_eq!(config.optimizer_runs, Some(500));
6011
6012            Ok(())
6013        });
6014    }
6015
6016    #[test]
6017    fn test_extends_strategy_replace_arrays() {
6018        figment::Jail::expect_with(|jail| {
6019            // Create base config with arrays
6020            jail.create_file(
6021                "base.toml",
6022                r#"
6023                    [profile.default]
6024                    libs = ["lib", "node_modules"]
6025                    ignored_error_codes = [5667, 1878]
6026                    optimizer_runs = 200
6027                    "#,
6028            )?;
6029
6030            // Local config extends with replace-arrays strategy (replaces arrays entirely)
6031            jail.create_file(
6032                "foundry.toml",
6033                r#"
6034                    [profile.default]
6035                    extends = { path = "base.toml", strategy = "replace-arrays" }
6036                    libs = ["mylib", "customlib"]
6037                    ignored_error_codes = [1234]
6038                    optimizer_runs = 500
6039                    "#,
6040            )?;
6041
6042            let config = Config::load().unwrap();
6043
6044            // Arrays should be replaced entirely (only local values)
6045            assert_eq!(config.libs.len(), 2);
6046            assert!(config.libs.iter().any(|l| l.to_str() == Some("mylib")));
6047            assert!(config.libs.iter().any(|l| l.to_str() == Some("customlib")));
6048            assert!(!config.libs.iter().any(|l| l.to_str() == Some("lib")));
6049            assert!(!config.libs.iter().any(|l| l.to_str() == Some("node_modules")));
6050
6051            assert_eq!(config.ignored_error_codes.len(), 1);
6052            assert!(config.ignored_error_codes.contains(&SolidityErrorCode::from(1234u64))); // 1234
6053            assert!(
6054                !config.ignored_error_codes.contains(&SolidityErrorCode::UnusedFunctionParameter)
6055            ); // 5667
6056
6057            // Non-array values should be replaced
6058            assert_eq!(config.optimizer_runs, Some(500));
6059
6060            Ok(())
6061        });
6062    }
6063
6064    #[test]
6065    fn test_extends_strategy_no_collision_success() {
6066        figment::Jail::expect_with(|jail| {
6067            // Create base config
6068            jail.create_file(
6069                "base.toml",
6070                r#"
6071                    [profile.default]
6072                    optimizer = true
6073                    optimizer_runs = 200
6074                    src = "src"
6075                    "#,
6076            )?;
6077
6078            // Local config extends with no-collision strategy and no conflicts
6079            jail.create_file(
6080                "foundry.toml",
6081                r#"
6082                    [profile.default]
6083                    extends = { path = "base.toml", strategy = "no-collision" }
6084                    test = "tests"
6085                    libs = ["lib"]
6086                    "#,
6087            )?;
6088
6089            let config = Config::load().unwrap();
6090
6091            // Values from base should be present
6092            assert_eq!(config.optimizer, Some(true));
6093            assert_eq!(config.optimizer_runs, Some(200));
6094            assert_eq!(config.src, PathBuf::from("src"));
6095
6096            // Values from local should be present
6097            assert_eq!(config.test, PathBuf::from("tests"));
6098            assert_eq!(config.libs.len(), 1);
6099            assert!(config.libs.iter().any(|l| l.to_str() == Some("lib")));
6100
6101            Ok(())
6102        });
6103    }
6104
6105    #[test]
6106    fn test_extends_strategy_no_collision_error() {
6107        figment::Jail::expect_with(|jail| {
6108            // Create base config
6109            jail.create_file(
6110                "base.toml",
6111                r#"
6112                    [profile.default]
6113                    optimizer = true
6114                    optimizer_runs = 200
6115                    libs = ["lib", "node_modules"]
6116                    "#,
6117            )?;
6118
6119            // Local config extends with no-collision strategy but has conflicts
6120            jail.create_file(
6121                "foundry.toml",
6122                r#"
6123                    [profile.default]
6124                    extends = { path = "base.toml", strategy = "no-collision" }
6125                    optimizer_runs = 500
6126                    libs = ["mylib"]
6127                    "#,
6128            )?;
6129
6130            // Loading should fail due to key collision
6131            let result = Config::load();
6132
6133            if let Ok(config) = result {
6134                panic!(
6135                    "Expected error but got config with optimizer_runs: {:?}, libs: {:?}",
6136                    config.optimizer_runs, config.libs
6137                );
6138            }
6139
6140            let err = result.unwrap_err();
6141            let err_str = err.to_string();
6142            assert!(
6143                err_str.contains("Key collision detected") || err_str.contains("collision"),
6144                "Error message doesn't mention collision: {err_str}"
6145            );
6146
6147            Ok(())
6148        });
6149    }
6150
6151    #[test]
6152    fn test_extends_both_syntaxes() {
6153        figment::Jail::expect_with(|jail| {
6154            // Create base config
6155            jail.create_file(
6156                "base.toml",
6157                r#"
6158                    [profile.default]
6159                    libs = ["lib"]
6160                    optimizer = true
6161                    "#,
6162            )?;
6163
6164            // Test 1: Simple string syntax (should use default extend-arrays)
6165            jail.create_file(
6166                "foundry_string.toml",
6167                r#"
6168                    [profile.default]
6169                    extends = "base.toml"
6170                    libs = ["custom"]
6171                    "#,
6172            )?;
6173
6174            // Test 2: Object syntax with explicit strategy
6175            jail.create_file(
6176                "foundry_object.toml",
6177                r#"
6178                    [profile.default]
6179                    extends = { path = "base.toml", strategy = "replace-arrays" }
6180                    libs = ["custom"]
6181                    "#,
6182            )?;
6183
6184            // Test string syntax (default extend-arrays)
6185            jail.set_env("FOUNDRY_CONFIG", "foundry_string.toml");
6186            let config = Config::load().unwrap();
6187            assert_eq!(config.libs.len(), 2); // Should concatenate
6188            assert!(config.libs.iter().any(|l| l.to_str() == Some("lib")));
6189            assert!(config.libs.iter().any(|l| l.to_str() == Some("custom")));
6190
6191            // Test object syntax (replace-arrays)
6192            jail.set_env("FOUNDRY_CONFIG", "foundry_object.toml");
6193            let config = Config::load().unwrap();
6194            assert_eq!(config.libs.len(), 1); // Should replace
6195            assert!(config.libs.iter().any(|l| l.to_str() == Some("custom")));
6196            assert!(!config.libs.iter().any(|l| l.to_str() == Some("lib")));
6197
6198            Ok(())
6199        });
6200    }
6201
6202    #[test]
6203    fn test_extends_strategy_default_is_extend_arrays() {
6204        figment::Jail::expect_with(|jail| {
6205            // Create base config
6206            jail.create_file(
6207                "base.toml",
6208                r#"
6209                    [profile.default]
6210                    libs = ["lib", "node_modules"]
6211                    optimizer = true
6212                    "#,
6213            )?;
6214
6215            // Local config extends without specifying strategy (should default to extend-arrays)
6216            jail.create_file(
6217                "foundry.toml",
6218                r#"
6219                    [profile.default]
6220                    extends = "base.toml"
6221                    libs = ["custom"]
6222                    optimizer = false
6223                    "#,
6224            )?;
6225
6226            // Should work with default extend-arrays strategy
6227            let config = Config::load().unwrap();
6228
6229            // Arrays should be concatenated by default
6230            assert_eq!(config.libs.len(), 3);
6231            assert!(config.libs.iter().any(|l| l.to_str() == Some("lib")));
6232            assert!(config.libs.iter().any(|l| l.to_str() == Some("node_modules")));
6233            assert!(config.libs.iter().any(|l| l.to_str() == Some("custom")));
6234
6235            // Non-array values should be replaced
6236            assert_eq!(config.optimizer, Some(false));
6237
6238            Ok(())
6239        });
6240    }
6241}