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