Skip to main content

foundry_config/
lib.rs

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