Skip to main content

foundry_config/
lib.rs

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