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