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