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