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