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};
42use regex::Regex;
43use revm::primitives::hardfork::SpecId;
44use semver::Version;
45use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
46use std::{
47 borrow::Cow,
48 collections::BTreeMap,
49 fs,
50 path::{Path, PathBuf},
51 str::FromStr,
52};
53
54mod macros;
55
56pub mod utils;
57pub use utils::*;
58
59mod endpoints;
60pub use endpoints::{
61 ResolvedRpcEndpoint, ResolvedRpcEndpoints, RpcEndpoint, RpcEndpointUrl, RpcEndpoints,
62};
63
64mod etherscan;
65pub use etherscan::EtherscanConfigError;
66use etherscan::{EtherscanConfigs, EtherscanEnvProvider, ResolvedEtherscanConfig};
67
68pub mod resolve;
69pub use resolve::UnresolvedEnvVarError;
70
71pub mod cache;
72use cache::{Cache, ChainCache};
73
74pub mod fmt;
75pub use fmt::FormatterConfig;
76
77pub mod lint;
78pub use lint::{LinterConfig, Severity as LintSeverity};
79
80pub mod fs_permissions;
81pub use fs_permissions::FsPermissions;
82use fs_permissions::PathPermission;
83
84pub mod error;
85use error::ExtractConfigError;
86pub use error::SolidityErrorCode;
87
88pub mod doc;
89pub use doc::DocConfig;
90
91pub mod filter;
92pub use filter::SkipBuildFilters;
93
94mod warning;
95pub use warning::*;
96
97pub mod fix;
98
99pub use alloy_chains::{Chain, NamedChain};
101pub use figment;
102
103pub mod providers;
104pub use providers::Remappings;
105use providers::*;
106
107mod fuzz;
108pub use fuzz::{FuzzConfig, FuzzCorpusConfig, FuzzDictionaryConfig};
109
110mod invariant;
111pub use invariant::InvariantConfig;
112
113mod inline;
114pub use inline::{InlineConfig, InlineConfigError, NatSpec};
115
116pub mod soldeer;
117use soldeer::{SoldeerConfig, SoldeerDependencyConfig};
118
119mod vyper;
120pub use vyper::VyperConfig;
121
122mod bind_json;
123use bind_json::BindJsonConfig;
124
125mod compilation;
126pub use compilation::{CompilationRestrictions, SettingsOverrides};
127
128pub mod extend;
129use extend::Extends;
130
131use foundry_evm_networks::NetworkConfigs;
132pub use semver;
133
134#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
166pub struct Config {
167 #[serde(skip)]
174 pub profile: Profile,
175 #[serde(skip)]
179 pub profiles: Vec<Profile>,
180
181 #[serde(default = "root_default", skip_serializing)]
186 pub root: PathBuf,
187
188 #[serde(default, skip_serializing)]
193 pub extends: Option<Extends>,
194
195 pub src: PathBuf,
199 pub test: PathBuf,
201 pub script: PathBuf,
203 pub out: PathBuf,
205 pub libs: Vec<PathBuf>,
207 pub remappings: Vec<RelativeRemapping>,
209 pub auto_detect_remappings: bool,
211 pub libraries: Vec<String>,
213 pub cache: bool,
215 pub cache_path: PathBuf,
217 pub dynamic_test_linking: bool,
219 pub snapshots: PathBuf,
221 pub gas_snapshot_check: bool,
223 pub gas_snapshot_emit: bool,
225 pub broadcast: PathBuf,
227 pub allow_paths: Vec<PathBuf>,
229 pub include_paths: Vec<PathBuf>,
231 pub skip: Vec<GlobMatcher>,
233 pub force: bool,
235 #[serde(with = "from_str_lowercase")]
237 pub evm_version: EvmVersion,
238 pub gas_reports: Vec<String>,
240 pub gas_reports_ignore: Vec<String>,
242 pub gas_reports_include_tests: bool,
244 #[doc(hidden)]
254 pub solc: Option<SolcReq>,
255 pub auto_detect_solc: bool,
257 pub offline: bool,
264 pub optimizer: Option<bool>,
266 pub optimizer_runs: Option<usize>,
277 pub optimizer_details: Option<OptimizerDetails>,
281 pub model_checker: Option<ModelCheckerSettings>,
283 pub verbosity: u8,
285 pub eth_rpc_url: Option<String>,
287 pub eth_rpc_accept_invalid_certs: bool,
289 pub eth_rpc_no_proxy: bool,
294 pub eth_rpc_jwt: Option<String>,
296 pub eth_rpc_timeout: Option<u64>,
298 pub eth_rpc_headers: Option<Vec<String>>,
307 pub etherscan_api_key: Option<String>,
309 #[serde(default, skip_serializing_if = "EtherscanConfigs::is_empty")]
311 pub etherscan: EtherscanConfigs,
312 pub ignored_error_codes: Vec<SolidityErrorCode>,
314 #[serde(rename = "ignored_warnings_from")]
316 pub ignored_file_paths: Vec<PathBuf>,
317 pub deny: DenyLevel,
319 #[serde(default, skip_serializing)]
321 pub deny_warnings: bool,
322 #[serde(rename = "match_test")]
324 pub test_pattern: Option<RegexWrapper>,
325 #[serde(rename = "no_match_test")]
327 pub test_pattern_inverse: Option<RegexWrapper>,
328 #[serde(rename = "match_contract")]
330 pub contract_pattern: Option<RegexWrapper>,
331 #[serde(rename = "no_match_contract")]
333 pub contract_pattern_inverse: Option<RegexWrapper>,
334 #[serde(rename = "match_path", with = "from_opt_glob")]
336 pub path_pattern: Option<globset::Glob>,
337 #[serde(rename = "no_match_path", with = "from_opt_glob")]
339 pub path_pattern_inverse: Option<globset::Glob>,
340 #[serde(rename = "no_match_coverage")]
342 pub coverage_pattern_inverse: Option<RegexWrapper>,
343 pub test_failures_file: PathBuf,
345 pub threads: Option<usize>,
347 pub show_progress: bool,
349 pub fuzz: FuzzConfig,
351 pub invariant: InvariantConfig,
353 pub ffi: bool,
355 pub allow_internal_expect_revert: bool,
357 pub always_use_create_2_factory: bool,
359 pub prompt_timeout: u64,
361 pub sender: Address,
363 pub tx_origin: Address,
365 pub initial_balance: U256,
367 #[serde(
369 deserialize_with = "crate::deserialize_u64_to_u256",
370 serialize_with = "crate::serialize_u64_or_u256"
371 )]
372 pub block_number: U256,
373 pub fork_block_number: Option<u64>,
375 #[serde(rename = "chain_id", alias = "chain")]
377 pub chain: Option<Chain>,
378 pub gas_limit: GasLimit,
380 pub code_size_limit: Option<usize>,
382 pub gas_price: Option<u64>,
387 pub block_base_fee_per_gas: u64,
389 pub block_coinbase: Address,
391 #[serde(
393 deserialize_with = "crate::deserialize_u64_to_u256",
394 serialize_with = "crate::serialize_u64_or_u256"
395 )]
396 pub block_timestamp: U256,
397 pub block_difficulty: u64,
399 pub block_prevrandao: B256,
401 pub block_gas_limit: Option<GasLimit>,
403 pub memory_limit: u64,
408 #[serde(default)]
425 pub extra_output: Vec<ContractOutputSelection>,
426 #[serde(default)]
437 pub extra_output_files: Vec<ContractOutputSelection>,
438 pub names: bool,
440 pub sizes: bool,
442 pub via_ir: bool,
445 pub ast: bool,
447 pub rpc_storage_caching: StorageCachingConfig,
449 pub no_storage_caching: bool,
452 pub no_rpc_rate_limit: bool,
455 #[serde(default, skip_serializing_if = "RpcEndpoints::is_empty")]
457 pub rpc_endpoints: RpcEndpoints,
458 pub use_literal_content: bool,
460 #[serde(with = "from_str_lowercase")]
464 pub bytecode_hash: BytecodeHash,
465 pub cbor_metadata: bool,
470 #[serde(with = "serde_helpers::display_from_str_opt")]
472 pub revert_strings: Option<RevertStrings>,
473 pub sparse_mode: bool,
478 pub build_info: bool,
481 pub build_info_path: Option<PathBuf>,
483 pub fmt: FormatterConfig,
485 pub lint: LinterConfig,
487 pub doc: DocConfig,
489 pub bind_json: BindJsonConfig,
491 pub fs_permissions: FsPermissions,
495
496 pub isolate: bool,
500
501 pub disable_block_gas_limit: bool,
503
504 pub enable_tx_gas_limit: bool,
506
507 pub labels: AddressHashMap<String>,
509
510 pub unchecked_cheatcode_artifacts: bool,
513
514 pub create2_library_salt: B256,
516
517 pub create2_deployer: Address,
519
520 pub vyper: VyperConfig,
522
523 pub dependencies: Option<SoldeerDependencyConfig>,
525
526 pub soldeer: Option<SoldeerConfig>,
528
529 pub assertions_revert: bool,
533
534 pub legacy_assertions: bool,
536
537 #[serde(default, skip_serializing_if = "Vec::is_empty")]
539 pub extra_args: Vec<String>,
540
541 #[serde(flatten)]
543 pub networks: NetworkConfigs,
544
545 pub transaction_timeout: u64,
547
548 #[serde(rename = "__warnings", default, skip_serializing)]
550 pub warnings: Vec<Warning>,
551
552 #[serde(default)]
554 pub additional_compiler_profiles: Vec<SettingsOverrides>,
555
556 #[serde(default)]
558 pub compilation_restrictions: Vec<CompilationRestrictions>,
559
560 pub script_execution_protection: bool,
562
563 #[doc(hidden)]
572 #[serde(skip)]
573 pub _non_exhaustive: (),
574}
575
576#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum, Default, Serialize)]
578#[serde(rename_all = "lowercase")]
579pub enum DenyLevel {
580 #[default]
582 Never,
583 Warnings,
585 Notes,
587}
588
589impl<'de> Deserialize<'de> for DenyLevel {
592 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
593 where
594 D: Deserializer<'de>,
595 {
596 struct DenyLevelVisitor;
597
598 impl<'de> de::Visitor<'de> for DenyLevelVisitor {
599 type Value = DenyLevel;
600
601 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
602 formatter.write_str("one of the following strings: `never`, `warnings`, `notes`")
603 }
604
605 fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
606 where
607 E: de::Error,
608 {
609 Ok(DenyLevel::from(value))
610 }
611
612 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
613 where
614 E: de::Error,
615 {
616 DenyLevel::from_str(value).map_err(de::Error::custom)
617 }
618 }
619
620 deserializer.deserialize_any(DenyLevelVisitor)
621 }
622}
623
624impl FromStr for DenyLevel {
625 type Err = String;
626
627 fn from_str(s: &str) -> Result<Self, Self::Err> {
628 match s.to_lowercase().as_str() {
629 "warnings" | "warning" | "w" => Ok(Self::Warnings),
630 "notes" | "note" | "n" => Ok(Self::Notes),
631 "never" | "false" | "f" => Ok(Self::Never),
632 _ => Err(format!(
633 "unknown variant: found `{s}`, expected one of `never`, `warnings`, `notes`"
634 )),
635 }
636 }
637}
638
639impl From<bool> for DenyLevel {
640 fn from(deny: bool) -> Self {
641 if deny { Self::Warnings } else { Self::Never }
642 }
643}
644
645impl DenyLevel {
646 pub fn warnings(&self) -> bool {
648 match self {
649 Self::Never => false,
650 Self::Warnings | Self::Notes => true,
651 }
652 }
653
654 pub fn notes(&self) -> bool {
656 match self {
657 Self::Never | Self::Warnings => false,
658 Self::Notes => true,
659 }
660 }
661
662 pub fn never(&self) -> bool {
664 match self {
665 Self::Never => true,
666 Self::Warnings | Self::Notes => false,
667 }
668 }
669}
670
671pub const STANDALONE_FALLBACK_SECTIONS: &[(&str, &str)] = &[("invariant", "fuzz")];
673
674pub const DEPRECATIONS: &[(&str, &str)] =
678 &[("cancun", "evm_version = Cancun"), ("deny_warnings", "deny = warnings")];
679
680impl Config {
681 pub const DEFAULT_PROFILE: Profile = Profile::Default;
683
684 pub const HARDHAT_PROFILE: Profile = Profile::const_new("hardhat");
686
687 pub const PROFILE_SECTION: &'static str = "profile";
689
690 pub const EXTERNAL_SECTION: &'static str = "external";
692
693 pub const STANDALONE_SECTIONS: &'static [&'static str] = &[
695 "rpc_endpoints",
696 "etherscan",
697 "fmt",
698 "lint",
699 "doc",
700 "fuzz",
701 "invariant",
702 "labels",
703 "dependencies",
704 "soldeer",
705 "vyper",
706 "bind_json",
707 ];
708
709 pub(crate) fn is_standalone_section<T: ?Sized + PartialEq<str>>(section: &T) -> bool {
710 section == Self::PROFILE_SECTION
711 || section == Self::EXTERNAL_SECTION
712 || Self::STANDALONE_SECTIONS.iter().any(|s| section == *s)
713 }
714
715 pub const FILE_NAME: &'static str = "foundry.toml";
717
718 pub const FOUNDRY_DIR_NAME: &'static str = ".foundry";
720
721 pub const DEFAULT_SENDER: Address = address!("0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38");
725
726 pub const DEFAULT_CREATE2_LIBRARY_SALT: FixedBytes<32> = FixedBytes::<32>::ZERO;
728
729 pub const DEFAULT_CREATE2_DEPLOYER: Address =
731 address!("0x4e59b44847b379578588920ca78fbf26c0b4956c");
732
733 pub fn load() -> Result<Self, ExtractConfigError> {
737 Self::from_provider(Self::figment())
738 }
739
740 pub fn load_with_providers(providers: FigmentProviders) -> Result<Self, ExtractConfigError> {
744 Self::from_provider(Self::default().to_figment(providers))
745 }
746
747 #[track_caller]
751 pub fn load_with_root(root: impl AsRef<Path>) -> Result<Self, ExtractConfigError> {
752 Self::from_provider(Self::figment_with_root(root.as_ref()))
753 }
754
755 #[track_caller]
762 pub fn load_with_root_and_fallback(root: impl AsRef<Path>) -> Result<Self, ExtractConfigError> {
763 let figment = Self::figment_with_root(root.as_ref());
764 Self::from_figment_fallback(Figment::from(figment))
765 }
766
767 #[doc(alias = "try_from")]
782 pub fn from_provider<T: Provider>(provider: T) -> Result<Self, ExtractConfigError> {
783 trace!("load config with provider: {:?}", provider.metadata());
784 Self::from_figment(Figment::from(provider))
785 }
786
787 #[doc(hidden)]
788 #[deprecated(note = "use `Config::from_provider` instead")]
789 pub fn try_from<T: Provider>(provider: T) -> Result<Self, ExtractConfigError> {
790 Self::from_provider(provider)
791 }
792
793 fn from_figment(figment: Figment) -> Result<Self, ExtractConfigError> {
794 Self::from_figment_inner(figment, true)
795 }
796
797 fn from_figment_fallback(figment: Figment) -> Result<Self, ExtractConfigError> {
800 Self::from_figment_inner(figment, false)
801 }
802
803 fn from_figment_inner(
804 figment: Figment,
805 strict_profile: bool,
806 ) -> Result<Self, ExtractConfigError> {
807 let mut config = figment.extract::<Self>().map_err(ExtractConfigError::new)?;
808 let selected_profile = figment.profile().clone();
809
810 fn add_profile(profiles: &mut Vec<Profile>, profile: &Profile) {
812 if !profiles.contains(profile) {
813 profiles.push(profile.clone());
814 }
815 }
816 let figment = figment.select(Self::PROFILE_SECTION);
817 if let Ok(data) = figment.data()
818 && let Some(profiles) = data.get(&Profile::new(Self::PROFILE_SECTION))
819 {
820 for profile in profiles.keys() {
821 add_profile(&mut config.profiles, &Profile::new(profile));
822 }
823 }
824 add_profile(&mut config.profiles, &Self::DEFAULT_PROFILE);
825
826 if config.profiles.contains(&selected_profile) {
828 config.profile = selected_profile;
829 } else if strict_profile {
830 return Err(ExtractConfigError::new(Error::from(format!(
831 "selected profile `{selected_profile}` does not exist"
832 ))));
833 } else {
834 config.profile = Self::DEFAULT_PROFILE;
836 }
837
838 config.normalize_optimizer_settings();
839
840 Ok(config)
841 }
842
843 pub fn to_figment(&self, providers: FigmentProviders) -> Figment {
848 if providers.is_none() {
851 return Figment::from(self);
852 }
853
854 let root = self.root.as_path();
855 let profile = Self::selected_profile();
856 let mut figment = Figment::default().merge(DappHardhatDirProvider(root));
857
858 if let Some(global_toml) = Self::foundry_dir_toml().filter(|p| p.exists()) {
860 figment = Self::merge_toml_provider(
861 figment,
862 TomlFileProvider::new(None, global_toml),
863 profile.clone(),
864 );
865 }
866 figment = Self::merge_toml_provider(
868 figment,
869 TomlFileProvider::new(Some("FOUNDRY_CONFIG"), root.join(Self::FILE_NAME)),
870 profile.clone(),
871 );
872
873 figment = figment
875 .merge(
876 Env::prefixed("DAPP_")
877 .ignore(&["REMAPPINGS", "LIBRARIES", "FFI", "FS_PERMISSIONS"])
878 .global(),
879 )
880 .merge(
881 Env::prefixed("DAPP_TEST_")
882 .ignore(&["CACHE", "FUZZ_RUNS", "DEPTH", "FFI", "FS_PERMISSIONS"])
883 .global(),
884 )
885 .merge(DappEnvCompatProvider)
886 .merge(EtherscanEnvProvider::default())
887 .merge(
888 Env::prefixed("FOUNDRY_")
889 .ignore(&["PROFILE", "REMAPPINGS", "LIBRARIES", "FFI", "FS_PERMISSIONS"])
890 .map(|key| {
891 let key = key.as_str();
892 if Self::STANDALONE_SECTIONS.iter().any(|section| {
893 key.starts_with(&format!("{}_", section.to_ascii_uppercase()))
894 }) {
895 key.replacen('_', ".", 1).into()
896 } else {
897 key.into()
898 }
899 })
900 .global(),
901 )
902 .select(profile.clone());
903
904 if providers.is_all() {
906 let remappings = RemappingsProvider {
910 auto_detect_remappings: figment
911 .extract_inner::<bool>("auto_detect_remappings")
912 .unwrap_or(true),
913 lib_paths: figment
914 .extract_inner::<Vec<PathBuf>>("libs")
915 .map(Cow::Owned)
916 .unwrap_or_else(|_| Cow::Borrowed(&self.libs)),
917 root,
918 remappings: figment.extract_inner::<Vec<Remapping>>("remappings"),
919 };
920 figment = figment.merge(remappings);
921 }
922
923 figment = self.normalize_defaults(figment);
925
926 Figment::from(self).merge(figment).select(profile)
927 }
928
929 #[must_use]
934 pub fn canonic(self) -> Self {
935 let root = self.root.clone();
936 self.canonic_at(root)
937 }
938
939 #[must_use]
957 pub fn canonic_at(mut self, root: impl Into<PathBuf>) -> Self {
958 let root = canonic(root);
959
960 fn p(root: &Path, rem: &Path) -> PathBuf {
961 canonic(root.join(rem))
962 }
963
964 self.src = p(&root, &self.src);
965 self.test = p(&root, &self.test);
966 self.script = p(&root, &self.script);
967 self.out = p(&root, &self.out);
968 self.broadcast = p(&root, &self.broadcast);
969 self.cache_path = p(&root, &self.cache_path);
970 self.snapshots = p(&root, &self.snapshots);
971 self.test_failures_file = p(&root, &self.test_failures_file);
972
973 if let Some(build_info_path) = self.build_info_path {
974 self.build_info_path = Some(p(&root, &build_info_path));
975 }
976
977 self.libs = self.libs.into_iter().map(|lib| p(&root, &lib)).collect();
978
979 self.remappings =
980 self.remappings.into_iter().map(|r| RelativeRemapping::new(r.into(), &root)).collect();
981
982 self.allow_paths = self.allow_paths.into_iter().map(|allow| p(&root, &allow)).collect();
983
984 self.include_paths = self.include_paths.into_iter().map(|allow| p(&root, &allow)).collect();
985
986 self.fs_permissions.join_all(&root);
987
988 if let Some(model_checker) = &mut self.model_checker {
989 model_checker.contracts = std::mem::take(&mut model_checker.contracts)
990 .into_iter()
991 .map(|(path, contracts)| {
992 (format!("{}", p(&root, path.as_ref()).display()), contracts)
993 })
994 .collect();
995 }
996
997 self
998 }
999
1000 pub fn normalized_evm_version(mut self) -> Self {
1002 self.normalize_evm_version();
1003 self
1004 }
1005
1006 pub fn normalized_optimizer_settings(mut self) -> Self {
1009 self.normalize_optimizer_settings();
1010 self
1011 }
1012
1013 pub fn normalize_evm_version(&mut self) {
1015 self.evm_version = self.get_normalized_evm_version();
1016 }
1017
1018 pub fn normalize_optimizer_settings(&mut self) {
1023 match (self.optimizer, self.optimizer_runs) {
1024 (None, None) => {
1026 self.optimizer = Some(false);
1027 self.optimizer_runs = Some(200);
1028 }
1029 (Some(_), None) => self.optimizer_runs = Some(200),
1031 (None, Some(runs)) => self.optimizer = Some(runs > 0),
1033 _ => {}
1034 }
1035 }
1036
1037 pub fn get_normalized_evm_version(&self) -> EvmVersion {
1039 if let Some(version) = self.solc_version()
1040 && let Some(evm_version) = self.evm_version.normalize_version_solc(&version)
1041 {
1042 return evm_version;
1043 }
1044 self.evm_version
1045 }
1046
1047 #[must_use]
1052 pub fn sanitized(self) -> Self {
1053 let mut config = self.canonic();
1054
1055 config.sanitize_remappings();
1056
1057 config.libs.sort_unstable();
1058 config.libs.dedup();
1059
1060 config
1061 }
1062
1063 pub fn sanitize_remappings(&mut self) {
1067 #[cfg(target_os = "windows")]
1068 {
1069 use path_slash::PathBufExt;
1071 self.remappings.iter_mut().for_each(|r| {
1072 r.path.path = r.path.path.to_slash_lossy().into_owned().into();
1073 });
1074 }
1075 }
1076
1077 pub fn install_lib_dir(&self) -> &Path {
1081 self.libs
1082 .iter()
1083 .find(|p| !p.ends_with("node_modules"))
1084 .map(|p| p.as_path())
1085 .unwrap_or_else(|| Path::new("lib"))
1086 }
1087
1088 pub fn project(&self) -> Result<Project<MultiCompiler>, SolcError> {
1103 self.create_project(self.cache, false)
1104 }
1105
1106 pub fn ephemeral_project(&self) -> Result<Project<MultiCompiler>, SolcError> {
1109 self.create_project(false, true)
1110 }
1111
1112 pub fn solar_project(&self) -> Result<Project<MultiCompiler>, SolcError> {
1116 let ui_testing = std::env::var_os("FOUNDRY_LINT_UI_TESTING").is_some();
1117 let mut project = self.create_project(self.cache && !ui_testing, false)?;
1118 project.update_output_selection(|selection| {
1119 *selection = OutputSelection::common_output_selection(["abi".into()]);
1122 });
1123 Ok(project)
1124 }
1125
1126 fn additional_settings(
1128 &self,
1129 base: &MultiCompilerSettings,
1130 ) -> BTreeMap<String, MultiCompilerSettings> {
1131 let mut map = BTreeMap::new();
1132
1133 for profile in &self.additional_compiler_profiles {
1134 let mut settings = base.clone();
1135 profile.apply(&mut settings);
1136 map.insert(profile.name.clone(), settings);
1137 }
1138
1139 map
1140 }
1141
1142 #[expect(clippy::disallowed_macros)]
1144 fn restrictions(
1145 &self,
1146 paths: &ProjectPathsConfig,
1147 ) -> Result<BTreeMap<PathBuf, RestrictionsWithVersion<MultiCompilerRestrictions>>, SolcError>
1148 {
1149 let mut map = BTreeMap::new();
1150 if self.compilation_restrictions.is_empty() {
1151 return Ok(BTreeMap::new());
1152 }
1153
1154 let graph = Graph::<MultiCompilerParser>::resolve(paths)?;
1155 let (sources, _) = graph.into_sources();
1156
1157 for res in &self.compilation_restrictions {
1158 for source in sources.keys().filter(|path| {
1159 if res.paths.is_match(path) {
1160 true
1161 } else if let Ok(path) = path.strip_prefix(&paths.root) {
1162 res.paths.is_match(path)
1163 } else {
1164 false
1165 }
1166 }) {
1167 let res: RestrictionsWithVersion<_> =
1168 res.clone().try_into().map_err(SolcError::msg)?;
1169 if !map.contains_key(source) {
1170 map.insert(source.clone(), res);
1171 } else {
1172 let value = map.remove(source.as_path()).unwrap();
1173 if let Some(merged) = value.clone().merge(res) {
1174 map.insert(source.clone(), merged);
1175 } else {
1176 eprintln!(
1178 "{}",
1179 yansi::Paint::yellow(&format!(
1180 "Failed to merge compilation restrictions for {}",
1181 source.display()
1182 ))
1183 );
1184 map.insert(source.clone(), value);
1185 }
1186 }
1187 }
1188 }
1189
1190 Ok(map)
1191 }
1192
1193 pub fn create_project(&self, cached: bool, no_artifacts: bool) -> Result<Project, SolcError> {
1197 let settings = self.compiler_settings()?;
1198 let paths = self.project_paths();
1199 let mut builder = Project::builder()
1200 .artifacts(self.configured_artifacts_handler())
1201 .additional_settings(self.additional_settings(&settings))
1202 .restrictions(self.restrictions(&paths)?)
1203 .settings(settings)
1204 .paths(paths)
1205 .ignore_error_codes(self.ignored_error_codes.iter().copied().map(Into::into))
1206 .ignore_paths(
1207 self.ignored_file_paths
1208 .iter()
1209 .map(|path| {
1210 path.strip_prefix("./").unwrap_or(path).to_path_buf()
1212 })
1213 .collect::<Vec<_>>(),
1214 )
1215 .set_compiler_severity_filter(if self.deny.warnings() {
1216 Severity::Warning
1217 } else {
1218 Severity::Error
1219 })
1220 .set_offline(self.offline)
1221 .set_cached(cached)
1222 .set_build_info(!no_artifacts && self.build_info)
1223 .set_no_artifacts(no_artifacts);
1224
1225 if !self.skip.is_empty() {
1226 let filter = SkipBuildFilters::new(self.skip.clone(), self.root.clone());
1227 builder = builder.sparse_output(filter);
1228 }
1229
1230 let project = builder.build(self.compiler()?)?;
1231
1232 if self.force {
1233 self.cleanup(&project)?;
1234 }
1235
1236 Ok(project)
1237 }
1238
1239 pub fn disable_optimizations(&self, project: &mut Project, ir_minimum: bool) {
1241 if ir_minimum {
1242 project.settings.solc.settings = std::mem::take(&mut project.settings.solc.settings)
1245 .with_via_ir_minimum_optimization();
1246
1247 let evm_version = project.settings.solc.evm_version;
1250 let version = self.solc_version().unwrap_or_else(|| Version::new(0, 8, 4));
1251 project.settings.solc.settings.sanitize(&version, SolcLanguage::Solidity);
1252 project.settings.solc.evm_version = evm_version;
1253 } else {
1254 project.settings.solc.optimizer.disable();
1255 project.settings.solc.optimizer.runs = None;
1256 project.settings.solc.optimizer.details = None;
1257 project.settings.solc.via_ir = None;
1258 }
1259 }
1260
1261 pub fn cleanup<C: Compiler, T: ArtifactOutput<CompilerContract = C::CompilerContract>>(
1263 &self,
1264 project: &Project<C, T>,
1265 ) -> Result<(), SolcError> {
1266 project.cleanup()?;
1267
1268 let _ = fs::remove_file(&self.test_failures_file);
1270
1271 let remove_test_dir = |test_dir: &Option<PathBuf>| {
1273 if let Some(test_dir) = test_dir {
1274 let path = project.root().join(test_dir);
1275 if path.exists() {
1276 let _ = fs::remove_dir_all(&path);
1277 }
1278 }
1279 };
1280 remove_test_dir(&self.fuzz.failure_persist_dir);
1281 remove_test_dir(&self.fuzz.corpus.corpus_dir);
1282 remove_test_dir(&self.invariant.corpus.corpus_dir);
1283 remove_test_dir(&self.invariant.failure_persist_dir);
1284
1285 Ok(())
1286 }
1287
1288 fn ensure_solc(&self) -> Result<Option<Solc>, SolcError> {
1295 if let Some(solc) = &self.solc {
1296 let solc = match solc {
1297 SolcReq::Version(version) => {
1298 if let Some(solc) = Solc::find_svm_installed_version(version)? {
1299 solc
1300 } else {
1301 if self.offline {
1302 return Err(SolcError::msg(format!(
1303 "can't install missing solc {version} in offline mode"
1304 )));
1305 }
1306 Solc::blocking_install(version)?
1307 }
1308 }
1309 SolcReq::Local(solc) => {
1310 if !solc.is_file() {
1311 return Err(SolcError::msg(format!(
1312 "`solc` {} does not exist",
1313 solc.display()
1314 )));
1315 }
1316 Solc::new(solc)?
1317 }
1318 };
1319 return Ok(Some(solc));
1320 }
1321
1322 Ok(None)
1323 }
1324
1325 pub fn evm_spec_id(&self) -> SpecId {
1327 evm_spec_id(self.evm_version)
1328 }
1329
1330 pub fn is_auto_detect(&self) -> bool {
1335 if self.solc.is_some() {
1336 return false;
1337 }
1338 self.auto_detect_solc
1339 }
1340
1341 pub fn enable_caching(&self, endpoint: &str, chain_id: impl Into<u64>) -> bool {
1343 !self.no_storage_caching
1344 && self.rpc_storage_caching.enable_for_chain_id(chain_id.into())
1345 && self.rpc_storage_caching.enable_for_endpoint(endpoint)
1346 }
1347
1348 pub fn project_paths<L>(&self) -> ProjectPathsConfig<L> {
1363 let mut builder = ProjectPathsConfig::builder()
1364 .cache(self.cache_path.join(SOLIDITY_FILES_CACHE_FILENAME))
1365 .sources(&self.src)
1366 .tests(&self.test)
1367 .scripts(&self.script)
1368 .artifacts(&self.out)
1369 .libs(self.libs.iter())
1370 .remappings(self.get_all_remappings())
1371 .allowed_path(&self.root)
1372 .allowed_paths(&self.libs)
1373 .allowed_paths(&self.allow_paths)
1374 .include_paths(&self.include_paths);
1375
1376 if let Some(build_info_path) = &self.build_info_path {
1377 builder = builder.build_infos(build_info_path);
1378 }
1379
1380 builder.build_with_root(&self.root)
1381 }
1382
1383 pub fn solc_compiler(&self) -> Result<SolcCompiler, SolcError> {
1385 if let Some(solc) = self.ensure_solc()? {
1386 Ok(SolcCompiler::Specific(solc))
1387 } else {
1388 Ok(SolcCompiler::AutoDetect)
1389 }
1390 }
1391
1392 pub fn solc_version(&self) -> Option<Version> {
1394 self.solc.as_ref().and_then(|solc| solc.try_version().ok())
1395 }
1396
1397 pub fn vyper_compiler(&self) -> Result<Option<Vyper>, SolcError> {
1399 if !self.project_paths::<VyperLanguage>().has_input_files() {
1401 return Ok(None);
1402 }
1403 let vyper = if let Some(path) = &self.vyper.path {
1404 Some(Vyper::new(path)?)
1405 } else {
1406 Vyper::new("vyper").ok()
1407 };
1408 Ok(vyper)
1409 }
1410
1411 pub fn compiler(&self) -> Result<MultiCompiler, SolcError> {
1413 Ok(MultiCompiler { solc: Some(self.solc_compiler()?), vyper: self.vyper_compiler()? })
1414 }
1415
1416 pub fn compiler_settings(&self) -> Result<MultiCompilerSettings, SolcError> {
1418 Ok(MultiCompilerSettings { solc: self.solc_settings()?, vyper: self.vyper_settings()? })
1419 }
1420
1421 pub fn get_all_remappings(&self) -> impl Iterator<Item = Remapping> + '_ {
1423 self.remappings.iter().map(|m| m.clone().into())
1424 }
1425
1426 pub fn get_rpc_jwt_secret(&self) -> Result<Option<Cow<'_, str>>, UnresolvedEnvVarError> {
1441 Ok(self.eth_rpc_jwt.as_ref().map(|jwt| Cow::Borrowed(jwt.as_str())))
1442 }
1443
1444 pub fn get_rpc_url(&self) -> Option<Result<Cow<'_, str>, UnresolvedEnvVarError>> {
1460 let maybe_alias = self.eth_rpc_url.as_deref()?;
1461 if let Some(alias) = self.get_rpc_url_with_alias(maybe_alias) {
1462 Some(alias)
1463 } else {
1464 Some(Ok(Cow::Borrowed(self.eth_rpc_url.as_deref()?)))
1465 }
1466 }
1467
1468 pub fn get_rpc_url_with_alias(
1493 &self,
1494 maybe_alias: &str,
1495 ) -> Option<Result<Cow<'_, str>, UnresolvedEnvVarError>> {
1496 let mut endpoints = self.rpc_endpoints.clone().resolved();
1497 if let Some(endpoint) = endpoints.remove(maybe_alias) {
1498 return Some(endpoint.url().map(Cow::Owned));
1499 }
1500
1501 if let Some(mesc_url) = self.get_rpc_url_from_mesc(maybe_alias) {
1502 return Some(Ok(Cow::Owned(mesc_url)));
1503 }
1504
1505 None
1506 }
1507
1508 pub fn get_rpc_url_from_mesc(&self, maybe_alias: &str) -> Option<String> {
1510 let mesc_config = mesc::load::load_config_data()
1513 .inspect_err(|err| debug!(%err, "failed to load mesc config"))
1514 .ok()?;
1515
1516 if let Ok(Some(endpoint)) =
1517 mesc::query::get_endpoint_by_query(&mesc_config, maybe_alias, Some("foundry"))
1518 {
1519 return Some(endpoint.url);
1520 }
1521
1522 if maybe_alias.chars().all(|c| c.is_numeric()) {
1523 if let Ok(Some(endpoint)) =
1529 mesc::query::get_endpoint_by_network(&mesc_config, maybe_alias, Some("foundry"))
1530 {
1531 return Some(endpoint.url);
1532 }
1533 }
1534
1535 None
1536 }
1537
1538 pub fn get_rpc_url_or<'a>(
1550 &'a self,
1551 fallback: impl Into<Cow<'a, str>>,
1552 ) -> Result<Cow<'a, str>, UnresolvedEnvVarError> {
1553 if let Some(url) = self.get_rpc_url() { url } else { Ok(fallback.into()) }
1554 }
1555
1556 pub fn get_rpc_url_or_localhost_http(&self) -> Result<Cow<'_, str>, UnresolvedEnvVarError> {
1568 self.get_rpc_url_or("http://localhost:8545")
1569 }
1570
1571 pub fn get_etherscan_config(
1591 &self,
1592 ) -> Option<Result<ResolvedEtherscanConfig, EtherscanConfigError>> {
1593 self.get_etherscan_config_with_chain(None).transpose()
1594 }
1595
1596 pub fn get_etherscan_config_with_chain(
1603 &self,
1604 chain: Option<Chain>,
1605 ) -> Result<Option<ResolvedEtherscanConfig>, EtherscanConfigError> {
1606 if let Some(maybe_alias) = self.etherscan_api_key.as_ref().or(self.eth_rpc_url.as_ref())
1607 && self.etherscan.contains_key(maybe_alias)
1608 {
1609 return self.etherscan.clone().resolved().remove(maybe_alias).transpose();
1610 }
1611
1612 if let Some(res) = chain
1614 .or(self.chain)
1615 .and_then(|chain| self.etherscan.clone().resolved().find_chain(chain))
1616 {
1617 match (res, self.etherscan_api_key.as_ref()) {
1618 (Ok(mut config), Some(key)) => {
1619 config.key.clone_from(key);
1622 return Ok(Some(config));
1623 }
1624 (Ok(config), None) => return Ok(Some(config)),
1625 (Err(err), None) => return Err(err),
1626 (Err(_), Some(_)) => {
1627 }
1629 }
1630 }
1631
1632 if let Some(key) = self.etherscan_api_key.as_ref() {
1634 return Ok(ResolvedEtherscanConfig::create(
1635 key,
1636 chain.or(self.chain).unwrap_or_default(),
1637 ));
1638 }
1639 Ok(None)
1640 }
1641
1642 #[expect(clippy::disallowed_macros)]
1648 pub fn get_etherscan_api_key(&self, chain: Option<Chain>) -> Option<String> {
1649 self.get_etherscan_config_with_chain(chain)
1650 .map_err(|e| {
1651 eprintln!(
1653 "{}: failed getting etherscan config: {e}",
1654 yansi::Paint::yellow("Warning"),
1655 );
1656 })
1657 .ok()
1658 .flatten()
1659 .map(|c| c.key)
1660 }
1661
1662 pub fn get_source_dir_remapping(&self) -> Option<Remapping> {
1669 get_dir_remapping(&self.src)
1670 }
1671
1672 pub fn get_test_dir_remapping(&self) -> Option<Remapping> {
1674 if self.root.join(&self.test).exists() { get_dir_remapping(&self.test) } else { None }
1675 }
1676
1677 pub fn get_script_dir_remapping(&self) -> Option<Remapping> {
1679 if self.root.join(&self.script).exists() { get_dir_remapping(&self.script) } else { None }
1680 }
1681
1682 pub fn optimizer(&self) -> Optimizer {
1688 Optimizer {
1689 enabled: self.optimizer,
1690 runs: self.optimizer_runs,
1691 details: self.optimizer_details.clone(),
1694 }
1695 }
1696
1697 pub fn configured_artifacts_handler(&self) -> ConfigurableArtifacts {
1700 let mut extra_output = self.extra_output.clone();
1701
1702 if !extra_output.contains(&ContractOutputSelection::Metadata) {
1708 extra_output.push(ContractOutputSelection::Metadata);
1709 }
1710
1711 ConfigurableArtifacts::new(extra_output, self.extra_output_files.iter().copied())
1712 }
1713
1714 pub fn parsed_libraries(&self) -> Result<Libraries, SolcError> {
1717 Libraries::parse(&self.libraries)
1718 }
1719
1720 pub fn libraries_with_remappings(&self) -> Result<Libraries, SolcError> {
1722 let paths: ProjectPathsConfig = self.project_paths();
1723 Ok(self.parsed_libraries()?.apply(|libs| paths.apply_lib_remappings(libs)))
1724 }
1725
1726 pub fn solc_settings(&self) -> Result<SolcSettings, SolcError> {
1731 let mut model_checker = self.model_checker.clone();
1735 if let Some(model_checker_settings) = &mut model_checker
1736 && model_checker_settings.targets.is_none()
1737 {
1738 model_checker_settings.targets = Some(vec![ModelCheckerTarget::Assert]);
1739 }
1740
1741 let mut settings = Settings {
1742 libraries: self.libraries_with_remappings()?,
1743 optimizer: self.optimizer(),
1744 evm_version: Some(self.evm_version),
1745 metadata: Some(SettingsMetadata {
1746 use_literal_content: Some(self.use_literal_content),
1747 bytecode_hash: Some(self.bytecode_hash),
1748 cbor_metadata: Some(self.cbor_metadata),
1749 }),
1750 debug: self.revert_strings.map(|revert_strings| DebuggingSettings {
1751 revert_strings: Some(revert_strings),
1752 debug_info: Vec::new(),
1754 }),
1755 model_checker,
1756 via_ir: Some(self.via_ir),
1757 stop_after: None,
1759 remappings: Vec::new(),
1761 output_selection: Default::default(),
1763 }
1764 .with_extra_output(self.configured_artifacts_handler().output_selection());
1765
1766 if self.ast || self.build_info {
1768 settings = settings.with_ast();
1769 }
1770
1771 let cli_settings =
1772 CliSettings { extra_args: self.extra_args.clone(), ..Default::default() };
1773
1774 Ok(SolcSettings { settings, cli_settings })
1775 }
1776
1777 pub fn vyper_settings(&self) -> Result<VyperSettings, SolcError> {
1780 Ok(VyperSettings {
1781 evm_version: Some(self.evm_version),
1782 optimize: self.vyper.optimize,
1783 bytecode_metadata: None,
1784 output_selection: OutputSelection::common_output_selection([
1787 "abi".to_string(),
1788 "evm.bytecode".to_string(),
1789 "evm.deployedBytecode".to_string(),
1790 ]),
1791 search_paths: None,
1792 experimental_codegen: self.vyper.experimental_codegen,
1793 })
1794 }
1795
1796 pub fn figment() -> Figment {
1817 Self::default().into()
1818 }
1819
1820 pub fn figment_with_root(root: impl AsRef<Path>) -> Figment {
1832 Self::with_root(root.as_ref()).into()
1833 }
1834
1835 #[doc(hidden)]
1836 #[track_caller]
1837 pub fn figment_with_root_opt(root: Option<&Path>) -> Figment {
1838 let root = match root {
1839 Some(root) => root,
1840 None => &find_project_root(None).expect("could not determine project root"),
1841 };
1842 Self::figment_with_root(root)
1843 }
1844
1845 pub fn with_root(root: impl AsRef<Path>) -> Self {
1854 Self::_with_root(root.as_ref())
1855 }
1856
1857 fn _with_root(root: &Path) -> Self {
1858 let paths = ProjectPathsConfig::builder().build_with_root::<()>(root);
1860 let artifacts: PathBuf = paths.artifacts.file_name().unwrap().into();
1861 Self {
1862 root: paths.root,
1863 src: paths.sources.file_name().unwrap().into(),
1864 out: artifacts.clone(),
1865 libs: paths.libraries.into_iter().map(|lib| lib.file_name().unwrap().into()).collect(),
1866 remappings: paths
1867 .remappings
1868 .into_iter()
1869 .map(|r| RelativeRemapping::new(r, root))
1870 .collect(),
1871 fs_permissions: FsPermissions::new([PathPermission::read(artifacts)]),
1872 ..Self::default()
1873 }
1874 }
1875
1876 pub fn hardhat() -> Self {
1878 Self {
1879 src: "contracts".into(),
1880 out: "artifacts".into(),
1881 libs: vec!["node_modules".into()],
1882 ..Self::default()
1883 }
1884 }
1885
1886 pub fn into_basic(self) -> BasicConfig {
1895 BasicConfig {
1896 profile: self.profile,
1897 src: self.src,
1898 out: self.out,
1899 libs: self.libs,
1900 remappings: self.remappings,
1901 }
1902 }
1903
1904 pub fn update_at<F>(root: &Path, f: F) -> eyre::Result<()>
1909 where
1910 F: FnOnce(&Self, &mut toml_edit::DocumentMut) -> bool,
1911 {
1912 let config = Self::load_with_root(root)?.sanitized();
1913 config.update(|doc| f(&config, doc))
1914 }
1915
1916 pub fn update<F>(&self, f: F) -> eyre::Result<()>
1921 where
1922 F: FnOnce(&mut toml_edit::DocumentMut) -> bool,
1923 {
1924 let file_path = self.get_config_path();
1925 if !file_path.exists() {
1926 return Ok(());
1927 }
1928 let contents = fs::read_to_string(&file_path)?;
1929 let mut doc = contents.parse::<toml_edit::DocumentMut>()?;
1930 if f(&mut doc) {
1931 fs::write(file_path, doc.to_string())?;
1932 }
1933 Ok(())
1934 }
1935
1936 pub fn update_libs(&self) -> eyre::Result<()> {
1942 self.update(|doc| {
1943 let profile = self.profile.as_str().as_str();
1944 let root = &self.root;
1945 let libs: toml_edit::Value = self
1946 .libs
1947 .iter()
1948 .map(|path| {
1949 let path =
1950 if let Ok(relative) = path.strip_prefix(root) { relative } else { path };
1951 toml_edit::Value::from(&*path.to_string_lossy())
1952 })
1953 .collect();
1954 let libs = toml_edit::value(libs);
1955 doc[Self::PROFILE_SECTION][profile]["libs"] = libs;
1956 true
1957 })
1958 }
1959
1960 pub fn to_string_pretty(&self) -> Result<String, toml::ser::Error> {
1972 let mut value = toml::Value::try_from(self)?;
1974 let value_table = value.as_table_mut().unwrap();
1976 let standalone_sections = Self::STANDALONE_SECTIONS
1978 .iter()
1979 .filter_map(|section| {
1980 let section = section.to_string();
1981 value_table.remove(§ion).map(|value| (section, value))
1982 })
1983 .collect::<Vec<_>>();
1984 let mut wrapping_table = [(
1986 Self::PROFILE_SECTION.into(),
1987 toml::Value::Table([(self.profile.to_string(), value)].into_iter().collect()),
1988 )]
1989 .into_iter()
1990 .collect::<toml::map::Map<_, _>>();
1991 for (section, value) in standalone_sections {
1993 wrapping_table.insert(section, value);
1994 }
1995 toml::to_string_pretty(&toml::Value::Table(wrapping_table))
1997 }
1998
1999 pub fn get_config_path(&self) -> PathBuf {
2001 self.root.join(Self::FILE_NAME)
2002 }
2003
2004 pub fn selected_profile() -> Profile {
2008 #[cfg(test)]
2010 {
2011 Self::force_selected_profile()
2012 }
2013 #[cfg(not(test))]
2014 {
2015 static CACHE: std::sync::OnceLock<Profile> = std::sync::OnceLock::new();
2016 CACHE.get_or_init(Self::force_selected_profile).clone()
2017 }
2018 }
2019
2020 fn force_selected_profile() -> Profile {
2021 Profile::from_env_or("FOUNDRY_PROFILE", Self::DEFAULT_PROFILE)
2022 }
2023
2024 pub fn foundry_dir_toml() -> Option<PathBuf> {
2026 Self::foundry_dir().map(|p| p.join(Self::FILE_NAME))
2027 }
2028
2029 pub fn foundry_dir() -> Option<PathBuf> {
2031 dirs::home_dir().map(|p| p.join(Self::FOUNDRY_DIR_NAME))
2032 }
2033
2034 pub fn foundry_cache_dir() -> Option<PathBuf> {
2036 Self::foundry_dir().map(|p| p.join("cache"))
2037 }
2038
2039 pub fn foundry_rpc_cache_dir() -> Option<PathBuf> {
2041 Some(Self::foundry_cache_dir()?.join("rpc"))
2042 }
2043 pub fn foundry_chain_cache_dir(chain_id: impl Into<Chain>) -> Option<PathBuf> {
2045 Some(Self::foundry_rpc_cache_dir()?.join(chain_id.into().to_string()))
2046 }
2047
2048 pub fn foundry_etherscan_cache_dir() -> Option<PathBuf> {
2050 Some(Self::foundry_cache_dir()?.join("etherscan"))
2051 }
2052
2053 pub fn foundry_keystores_dir() -> Option<PathBuf> {
2055 Some(Self::foundry_dir()?.join("keystores"))
2056 }
2057
2058 pub fn foundry_etherscan_chain_cache_dir(chain_id: impl Into<Chain>) -> Option<PathBuf> {
2061 Some(Self::foundry_etherscan_cache_dir()?.join(chain_id.into().to_string()))
2062 }
2063
2064 pub fn foundry_block_cache_dir(chain_id: impl Into<Chain>, block: u64) -> Option<PathBuf> {
2067 Some(Self::foundry_chain_cache_dir(chain_id)?.join(format!("{block}")))
2068 }
2069
2070 pub fn foundry_block_cache_file(chain_id: impl Into<Chain>, block: u64) -> Option<PathBuf> {
2073 Some(Self::foundry_block_cache_dir(chain_id, block)?.join("storage.json"))
2074 }
2075
2076 pub fn data_dir() -> eyre::Result<PathBuf> {
2084 let path = dirs::data_dir().wrap_err("Failed to find data directory")?.join("foundry");
2085 std::fs::create_dir_all(&path).wrap_err("Failed to create module directory")?;
2086 Ok(path)
2087 }
2088
2089 pub fn find_config_file() -> Option<PathBuf> {
2096 fn find(path: &Path) -> Option<PathBuf> {
2097 if path.is_absolute() {
2098 return match path.is_file() {
2099 true => Some(path.to_path_buf()),
2100 false => None,
2101 };
2102 }
2103 let cwd = std::env::current_dir().ok()?;
2104 let mut cwd = cwd.as_path();
2105 loop {
2106 let file_path = cwd.join(path);
2107 if file_path.is_file() {
2108 return Some(file_path);
2109 }
2110 cwd = cwd.parent()?;
2111 }
2112 }
2113 find(Env::var_or("FOUNDRY_CONFIG", Self::FILE_NAME).as_ref())
2114 .or_else(|| Self::foundry_dir_toml().filter(|p| p.exists()))
2115 }
2116
2117 pub fn clean_foundry_cache() -> eyre::Result<()> {
2119 if let Some(cache_dir) = Self::foundry_cache_dir() {
2120 let path = cache_dir.as_path();
2121 let _ = fs::remove_dir_all(path);
2122 } else {
2123 eyre::bail!("failed to get foundry_cache_dir");
2124 }
2125
2126 Ok(())
2127 }
2128
2129 pub fn clean_foundry_chain_cache(chain: Chain) -> eyre::Result<()> {
2131 if let Some(cache_dir) = Self::foundry_chain_cache_dir(chain) {
2132 let path = cache_dir.as_path();
2133 let _ = fs::remove_dir_all(path);
2134 } else {
2135 eyre::bail!("failed to get foundry_chain_cache_dir");
2136 }
2137
2138 Ok(())
2139 }
2140
2141 pub fn clean_foundry_block_cache(chain: Chain, block: u64) -> eyre::Result<()> {
2143 if let Some(cache_dir) = Self::foundry_block_cache_dir(chain, block) {
2144 let path = cache_dir.as_path();
2145 let _ = fs::remove_dir_all(path);
2146 } else {
2147 eyre::bail!("failed to get foundry_block_cache_dir");
2148 }
2149
2150 Ok(())
2151 }
2152
2153 pub fn clean_foundry_etherscan_cache() -> eyre::Result<()> {
2155 if let Some(cache_dir) = Self::foundry_etherscan_cache_dir() {
2156 let path = cache_dir.as_path();
2157 let _ = fs::remove_dir_all(path);
2158 } else {
2159 eyre::bail!("failed to get foundry_etherscan_cache_dir");
2160 }
2161
2162 Ok(())
2163 }
2164
2165 pub fn clean_foundry_etherscan_chain_cache(chain: Chain) -> eyre::Result<()> {
2167 if let Some(cache_dir) = Self::foundry_etherscan_chain_cache_dir(chain) {
2168 let path = cache_dir.as_path();
2169 let _ = fs::remove_dir_all(path);
2170 } else {
2171 eyre::bail!("failed to get foundry_etherscan_cache_dir for chain: {}", chain);
2172 }
2173
2174 Ok(())
2175 }
2176
2177 pub fn list_foundry_cache() -> eyre::Result<Cache> {
2179 if let Some(cache_dir) = Self::foundry_rpc_cache_dir() {
2180 let mut cache = Cache { chains: vec![] };
2181 if !cache_dir.exists() {
2182 return Ok(cache);
2183 }
2184 if let Ok(entries) = cache_dir.as_path().read_dir() {
2185 for entry in entries.flatten().filter(|x| x.path().is_dir()) {
2186 match Chain::from_str(&entry.file_name().to_string_lossy()) {
2187 Ok(chain) => cache.chains.push(Self::list_foundry_chain_cache(chain)?),
2188 Err(_) => continue,
2189 }
2190 }
2191 Ok(cache)
2192 } else {
2193 eyre::bail!("failed to access foundry_cache_dir");
2194 }
2195 } else {
2196 eyre::bail!("failed to get foundry_cache_dir");
2197 }
2198 }
2199
2200 pub fn list_foundry_chain_cache(chain: Chain) -> eyre::Result<ChainCache> {
2202 let block_explorer_data_size = match Self::foundry_etherscan_chain_cache_dir(chain) {
2203 Some(cache_dir) => Self::get_cached_block_explorer_data(&cache_dir)?,
2204 None => {
2205 warn!("failed to access foundry_etherscan_chain_cache_dir");
2206 0
2207 }
2208 };
2209
2210 if let Some(cache_dir) = Self::foundry_chain_cache_dir(chain) {
2211 let blocks = Self::get_cached_blocks(&cache_dir)?;
2212 Ok(ChainCache {
2213 name: chain.to_string(),
2214 blocks,
2215 block_explorer: block_explorer_data_size,
2216 })
2217 } else {
2218 eyre::bail!("failed to get foundry_chain_cache_dir");
2219 }
2220 }
2221
2222 fn get_cached_blocks(chain_path: &Path) -> eyre::Result<Vec<(String, u64)>> {
2224 let mut blocks = vec![];
2225 if !chain_path.exists() {
2226 return Ok(blocks);
2227 }
2228 for block in chain_path.read_dir()?.flatten() {
2229 let file_type = block.file_type()?;
2230 let file_name = block.file_name();
2231 let filepath = if file_type.is_dir() {
2232 block.path().join("storage.json")
2233 } else if file_type.is_file()
2234 && file_name.to_string_lossy().chars().all(char::is_numeric)
2235 {
2236 block.path()
2237 } else {
2238 continue;
2239 };
2240 blocks.push((file_name.to_string_lossy().into_owned(), fs::metadata(filepath)?.len()));
2241 }
2242 Ok(blocks)
2243 }
2244
2245 fn get_cached_block_explorer_data(chain_path: &Path) -> eyre::Result<u64> {
2247 if !chain_path.exists() {
2248 return Ok(0);
2249 }
2250
2251 fn dir_size_recursive(mut dir: fs::ReadDir) -> eyre::Result<u64> {
2252 dir.try_fold(0, |acc, file| {
2253 let file = file?;
2254 let size = match file.metadata()? {
2255 data if data.is_dir() => dir_size_recursive(fs::read_dir(file.path())?)?,
2256 data => data.len(),
2257 };
2258 Ok(acc + size)
2259 })
2260 }
2261
2262 dir_size_recursive(fs::read_dir(chain_path)?)
2263 }
2264
2265 fn merge_toml_provider(
2266 mut figment: Figment,
2267 toml_provider: impl Provider,
2268 profile: Profile,
2269 ) -> Figment {
2270 figment = figment.select(profile.clone());
2271
2272 figment = {
2274 let warnings = WarningsProvider::for_figment(&toml_provider, &figment);
2275 figment.merge(warnings)
2276 };
2277
2278 let mut profiles = vec![Self::DEFAULT_PROFILE];
2280 if profile != Self::DEFAULT_PROFILE {
2281 profiles.push(profile.clone());
2282 }
2283 let provider = toml_provider.strict_select(profiles);
2284
2285 let provider = &BackwardsCompatTomlProvider(ForcedSnakeCaseData(provider));
2287
2288 if profile != Self::DEFAULT_PROFILE {
2290 figment = figment.merge(provider.rename(Self::DEFAULT_PROFILE, profile.clone()));
2291 }
2292 for standalone_key in Self::STANDALONE_SECTIONS {
2294 if let Some((_, fallback)) =
2295 STANDALONE_FALLBACK_SECTIONS.iter().find(|(key, _)| standalone_key == key)
2296 {
2297 figment = figment.merge(
2298 provider
2299 .fallback(standalone_key, fallback)
2300 .wrap(profile.clone(), standalone_key),
2301 );
2302 } else {
2303 figment = figment.merge(provider.wrap(profile.clone(), standalone_key));
2304 }
2305 }
2306 figment = figment.merge(provider);
2308 figment
2309 }
2310
2311 fn normalize_defaults(&self, mut figment: Figment) -> Figment {
2317 if figment.contains("evm_version") {
2318 return figment;
2319 }
2320
2321 if let Ok(solc) = figment.extract_inner::<SolcReq>("solc")
2323 && let Some(version) = solc
2324 .try_version()
2325 .ok()
2326 .and_then(|version| self.evm_version.normalize_version_solc(&version))
2327 {
2328 figment = figment.merge(("evm_version", version));
2329 }
2330
2331 if self.deny_warnings
2333 && let Ok(DenyLevel::Never) = figment.extract_inner("deny")
2334 {
2335 figment = figment.merge(("deny", DenyLevel::Warnings));
2336 }
2337
2338 figment
2339 }
2340}
2341
2342impl From<Config> for Figment {
2343 fn from(c: Config) -> Self {
2344 (&c).into()
2345 }
2346}
2347impl From<&Config> for Figment {
2348 fn from(c: &Config) -> Self {
2349 c.to_figment(FigmentProviders::All)
2350 }
2351}
2352
2353#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2355pub enum FigmentProviders {
2356 #[default]
2358 All,
2359 Cast,
2363 Anvil,
2367 None,
2369}
2370
2371impl FigmentProviders {
2372 pub const fn is_all(&self) -> bool {
2374 matches!(self, Self::All)
2375 }
2376
2377 pub const fn is_cast(&self) -> bool {
2379 matches!(self, Self::Cast)
2380 }
2381
2382 pub const fn is_anvil(&self) -> bool {
2384 matches!(self, Self::Anvil)
2385 }
2386
2387 pub const fn is_none(&self) -> bool {
2389 matches!(self, Self::None)
2390 }
2391}
2392
2393#[derive(Clone, Debug, Serialize, Deserialize)]
2395#[serde(transparent)]
2396pub struct RegexWrapper {
2397 #[serde(with = "serde_regex")]
2398 inner: regex::Regex,
2399}
2400
2401impl std::ops::Deref for RegexWrapper {
2402 type Target = regex::Regex;
2403
2404 fn deref(&self) -> &Self::Target {
2405 &self.inner
2406 }
2407}
2408
2409impl std::cmp::PartialEq for RegexWrapper {
2410 fn eq(&self, other: &Self) -> bool {
2411 self.as_str() == other.as_str()
2412 }
2413}
2414
2415impl Eq for RegexWrapper {}
2416
2417impl From<RegexWrapper> for regex::Regex {
2418 fn from(wrapper: RegexWrapper) -> Self {
2419 wrapper.inner
2420 }
2421}
2422
2423impl From<regex::Regex> for RegexWrapper {
2424 fn from(re: Regex) -> Self {
2425 Self { inner: re }
2426 }
2427}
2428
2429mod serde_regex {
2430 use regex::Regex;
2431 use serde::{Deserialize, Deserializer, Serializer};
2432
2433 pub(crate) fn serialize<S>(value: &Regex, serializer: S) -> Result<S::Ok, S::Error>
2434 where
2435 S: Serializer,
2436 {
2437 serializer.serialize_str(value.as_str())
2438 }
2439
2440 pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Regex, D::Error>
2441 where
2442 D: Deserializer<'de>,
2443 {
2444 let s = String::deserialize(deserializer)?;
2445 Regex::new(&s).map_err(serde::de::Error::custom)
2446 }
2447}
2448
2449pub(crate) mod from_opt_glob {
2451 use serde::{Deserialize, Deserializer, Serializer};
2452
2453 pub fn serialize<S>(value: &Option<globset::Glob>, serializer: S) -> Result<S::Ok, S::Error>
2454 where
2455 S: Serializer,
2456 {
2457 match value {
2458 Some(glob) => serializer.serialize_str(glob.glob()),
2459 None => serializer.serialize_none(),
2460 }
2461 }
2462
2463 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<globset::Glob>, D::Error>
2464 where
2465 D: Deserializer<'de>,
2466 {
2467 let s: Option<String> = Option::deserialize(deserializer)?;
2468 if let Some(s) = s {
2469 return Ok(Some(globset::Glob::new(&s).map_err(serde::de::Error::custom)?));
2470 }
2471 Ok(None)
2472 }
2473}
2474
2475pub fn parse_with_profile<T: serde::de::DeserializeOwned>(
2486 s: &str,
2487) -> Result<Option<(Profile, T)>, Error> {
2488 let figment = Config::merge_toml_provider(
2489 Figment::new(),
2490 Toml::string(s).nested(),
2491 Config::DEFAULT_PROFILE,
2492 );
2493 if figment.profiles().any(|p| p == Config::DEFAULT_PROFILE) {
2494 Ok(Some((Config::DEFAULT_PROFILE, figment.select(Config::DEFAULT_PROFILE).extract()?)))
2495 } else {
2496 Ok(None)
2497 }
2498}
2499
2500impl Provider for Config {
2501 fn metadata(&self) -> Metadata {
2502 Metadata::named("Foundry Config")
2503 }
2504
2505 #[track_caller]
2506 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
2507 let mut data = Serialized::defaults(self).data()?;
2508 if let Some(entry) = data.get_mut(&self.profile) {
2509 entry.insert("root".to_string(), Value::serialize(self.root.clone())?);
2510 }
2511 Ok(data)
2512 }
2513
2514 fn profile(&self) -> Option<Profile> {
2515 Some(self.profile.clone())
2516 }
2517}
2518
2519impl Default for Config {
2520 fn default() -> Self {
2521 Self {
2522 profile: Self::DEFAULT_PROFILE,
2523 profiles: vec![Self::DEFAULT_PROFILE],
2524 fs_permissions: FsPermissions::new([PathPermission::read("out")]),
2525 isolate: cfg!(feature = "isolate-by-default"),
2526 root: root_default(),
2527 extends: None,
2528 src: "src".into(),
2529 test: "test".into(),
2530 script: "script".into(),
2531 out: "out".into(),
2532 libs: vec!["lib".into()],
2533 cache: true,
2534 dynamic_test_linking: false,
2535 cache_path: "cache".into(),
2536 broadcast: "broadcast".into(),
2537 snapshots: "snapshots".into(),
2538 gas_snapshot_check: false,
2539 gas_snapshot_emit: true,
2540 allow_paths: vec![],
2541 include_paths: vec![],
2542 force: false,
2543 evm_version: EvmVersion::Osaka,
2544 gas_reports: vec!["*".to_string()],
2545 gas_reports_ignore: vec![],
2546 gas_reports_include_tests: false,
2547 solc: None,
2548 vyper: Default::default(),
2549 auto_detect_solc: true,
2550 offline: false,
2551 optimizer: None,
2552 optimizer_runs: None,
2553 optimizer_details: None,
2554 model_checker: None,
2555 extra_output: Default::default(),
2556 extra_output_files: Default::default(),
2557 names: false,
2558 sizes: false,
2559 test_pattern: None,
2560 test_pattern_inverse: None,
2561 contract_pattern: None,
2562 contract_pattern_inverse: None,
2563 path_pattern: None,
2564 path_pattern_inverse: None,
2565 coverage_pattern_inverse: None,
2566 test_failures_file: "cache/test-failures".into(),
2567 threads: None,
2568 show_progress: false,
2569 fuzz: FuzzConfig::new("cache/fuzz".into()),
2570 invariant: InvariantConfig::new("cache/invariant".into()),
2571 always_use_create_2_factory: false,
2572 ffi: false,
2573 allow_internal_expect_revert: false,
2574 prompt_timeout: 120,
2575 sender: Self::DEFAULT_SENDER,
2576 tx_origin: Self::DEFAULT_SENDER,
2577 initial_balance: U256::from((1u128 << 96) - 1),
2578 block_number: U256::from(1),
2579 fork_block_number: None,
2580 chain: None,
2581 gas_limit: (1u64 << 30).into(), code_size_limit: None,
2583 gas_price: None,
2584 block_base_fee_per_gas: 0,
2585 block_coinbase: Address::ZERO,
2586 block_timestamp: U256::from(1),
2587 block_difficulty: 0,
2588 block_prevrandao: Default::default(),
2589 block_gas_limit: None,
2590 disable_block_gas_limit: false,
2591 enable_tx_gas_limit: false,
2592 memory_limit: 1 << 27, eth_rpc_url: None,
2594 eth_rpc_accept_invalid_certs: false,
2595 eth_rpc_no_proxy: false,
2596 eth_rpc_jwt: None,
2597 eth_rpc_timeout: None,
2598 eth_rpc_headers: None,
2599 etherscan_api_key: None,
2600 verbosity: 0,
2601 remappings: vec![],
2602 auto_detect_remappings: true,
2603 libraries: vec![],
2604 ignored_error_codes: vec![
2605 SolidityErrorCode::SpdxLicenseNotProvided,
2606 SolidityErrorCode::ContractExceeds24576Bytes,
2607 SolidityErrorCode::ContractInitCodeSizeExceeds49152Bytes,
2608 SolidityErrorCode::TransientStorageUsed,
2609 SolidityErrorCode::TransferDeprecated,
2610 SolidityErrorCode::NatspecMemorySafeAssemblyDeprecated,
2611 ],
2612 ignored_file_paths: vec![],
2613 deny: DenyLevel::Never,
2614 deny_warnings: false,
2615 via_ir: false,
2616 ast: false,
2617 rpc_storage_caching: Default::default(),
2618 rpc_endpoints: Default::default(),
2619 etherscan: Default::default(),
2620 no_storage_caching: false,
2621 no_rpc_rate_limit: false,
2622 use_literal_content: false,
2623 bytecode_hash: BytecodeHash::Ipfs,
2624 cbor_metadata: true,
2625 revert_strings: None,
2626 sparse_mode: false,
2627 build_info: false,
2628 build_info_path: None,
2629 fmt: Default::default(),
2630 lint: Default::default(),
2631 doc: Default::default(),
2632 bind_json: Default::default(),
2633 labels: Default::default(),
2634 unchecked_cheatcode_artifacts: false,
2635 create2_library_salt: Self::DEFAULT_CREATE2_LIBRARY_SALT,
2636 create2_deployer: Self::DEFAULT_CREATE2_DEPLOYER,
2637 skip: vec![],
2638 dependencies: Default::default(),
2639 soldeer: Default::default(),
2640 assertions_revert: true,
2641 legacy_assertions: false,
2642 warnings: vec![],
2643 extra_args: vec![],
2644 networks: Default::default(),
2645 transaction_timeout: 120,
2646 additional_compiler_profiles: Default::default(),
2647 compilation_restrictions: Default::default(),
2648 script_execution_protection: true,
2649 _non_exhaustive: (),
2650 }
2651 }
2652}
2653
2654#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
2660pub struct GasLimit(#[serde(deserialize_with = "crate::deserialize_u64_or_max")] pub u64);
2661
2662impl From<u64> for GasLimit {
2663 fn from(gas: u64) -> Self {
2664 Self(gas)
2665 }
2666}
2667
2668impl From<GasLimit> for u64 {
2669 fn from(gas: GasLimit) -> Self {
2670 gas.0
2671 }
2672}
2673
2674impl Serialize for GasLimit {
2675 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2676 where
2677 S: Serializer,
2678 {
2679 if self.0 == u64::MAX {
2680 serializer.serialize_str("max")
2681 } else if self.0 > i64::MAX as u64 {
2682 serializer.serialize_str(&self.0.to_string())
2683 } else {
2684 serializer.serialize_u64(self.0)
2685 }
2686 }
2687}
2688
2689#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2691#[serde(untagged)]
2692pub enum SolcReq {
2693 Version(Version),
2696 Local(PathBuf),
2698}
2699
2700impl SolcReq {
2701 fn try_version(&self) -> Result<Version, SolcError> {
2706 match self {
2707 Self::Version(version) => Ok(version.clone()),
2708 Self::Local(path) => Solc::new(path).map(|solc| solc.version),
2709 }
2710 }
2711}
2712
2713impl<T: AsRef<str>> From<T> for SolcReq {
2714 fn from(s: T) -> Self {
2715 let s = s.as_ref();
2716 if let Ok(v) = Version::from_str(s) { Self::Version(v) } else { Self::Local(s.into()) }
2717 }
2718}
2719
2720#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2732pub struct BasicConfig {
2733 #[serde(skip)]
2735 pub profile: Profile,
2736 pub src: PathBuf,
2738 pub out: PathBuf,
2740 pub libs: Vec<PathBuf>,
2742 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2744 pub remappings: Vec<RelativeRemapping>,
2745}
2746
2747impl BasicConfig {
2748 pub fn to_string_pretty(&self) -> Result<String, toml::ser::Error> {
2752 let s = toml::to_string_pretty(self)?;
2753 Ok(format!(
2754 "\
2755[profile.{}]
2756{s}
2757# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options\n",
2758 self.profile
2759 ))
2760 }
2761}
2762
2763pub(crate) mod from_str_lowercase {
2764 use serde::{Deserialize, Deserializer, Serializer};
2765 use std::str::FromStr;
2766
2767 pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
2768 where
2769 T: std::fmt::Display,
2770 S: Serializer,
2771 {
2772 serializer.collect_str(&value.to_string().to_lowercase())
2773 }
2774
2775 pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
2776 where
2777 D: Deserializer<'de>,
2778 T: FromStr,
2779 T::Err: std::fmt::Display,
2780 {
2781 String::deserialize(deserializer)?.to_lowercase().parse().map_err(serde::de::Error::custom)
2782 }
2783}
2784
2785fn canonic(path: impl Into<PathBuf>) -> PathBuf {
2786 let path = path.into();
2787 foundry_compilers::utils::canonicalize(&path).unwrap_or(path)
2788}
2789
2790fn root_default() -> PathBuf {
2791 ".".into()
2792}
2793
2794#[cfg(test)]
2795mod tests {
2796 use super::*;
2797 use crate::{
2798 cache::{CachedChains, CachedEndpoints},
2799 endpoints::RpcEndpointType,
2800 etherscan::ResolvedEtherscanConfigs,
2801 fmt::IndentStyle,
2802 };
2803 use NamedChain::Moonbeam;
2804 use endpoints::{RpcAuth, RpcEndpointConfig};
2805 use figment::error::Kind::InvalidType;
2806 use foundry_compilers::artifacts::{
2807 ModelCheckerEngine, YulDetails, vyper::VyperOptimizationMode,
2808 };
2809 use similar_asserts::assert_eq;
2810 use soldeer_core::remappings::RemappingsLocation;
2811 use std::{fs::File, io::Write};
2812 use tempfile::tempdir;
2813
2814 fn clear_warning(config: &mut Config) {
2817 config.warnings = vec![];
2818 }
2819
2820 #[test]
2821 fn default_sender() {
2822 assert_eq!(Config::DEFAULT_SENDER, address!("0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38"));
2823 }
2824
2825 #[test]
2826 fn test_caching() {
2827 let mut config = Config::default();
2828 let chain_id = NamedChain::Mainnet;
2829 let url = "https://eth-mainnet.alchemyapi";
2830 assert!(config.enable_caching(url, chain_id));
2831
2832 config.no_storage_caching = true;
2833 assert!(!config.enable_caching(url, chain_id));
2834
2835 config.no_storage_caching = false;
2836 assert!(!config.enable_caching(url, NamedChain::Dev));
2837 }
2838
2839 #[test]
2840 fn test_install_dir() {
2841 figment::Jail::expect_with(|jail| {
2842 let config = Config::load().unwrap();
2843 assert_eq!(config.install_lib_dir(), PathBuf::from("lib"));
2844 jail.create_file(
2845 "foundry.toml",
2846 r"
2847 [profile.default]
2848 libs = ['node_modules', 'lib']
2849 ",
2850 )?;
2851 let config = Config::load().unwrap();
2852 assert_eq!(config.install_lib_dir(), PathBuf::from("lib"));
2853
2854 jail.create_file(
2855 "foundry.toml",
2856 r"
2857 [profile.default]
2858 libs = ['custom', 'node_modules', 'lib']
2859 ",
2860 )?;
2861 let config = Config::load().unwrap();
2862 assert_eq!(config.install_lib_dir(), PathBuf::from("custom"));
2863
2864 Ok(())
2865 });
2866 }
2867
2868 #[test]
2869 fn test_figment_is_default() {
2870 figment::Jail::expect_with(|_| {
2871 let mut default: Config = Config::figment().extract()?;
2872 let default2 = Config::default();
2873 default.profile = default2.profile.clone();
2874 default.profiles = default2.profiles.clone();
2875 assert_eq!(default, default2);
2876 Ok(())
2877 });
2878 }
2879
2880 #[test]
2881 fn figment_profiles() {
2882 figment::Jail::expect_with(|jail| {
2883 jail.create_file(
2884 "foundry.toml",
2885 r"
2886 [foo.baz]
2887 libs = ['node_modules', 'lib']
2888
2889 [profile.default]
2890 libs = ['node_modules', 'lib']
2891
2892 [profile.ci]
2893 libs = ['node_modules', 'lib']
2894
2895 [profile.local]
2896 libs = ['node_modules', 'lib']
2897 ",
2898 )?;
2899
2900 let config = crate::Config::load().unwrap();
2901 let expected: &[figment::Profile] = &["ci".into(), "default".into(), "local".into()];
2902 assert_eq!(config.profiles, expected);
2903
2904 Ok(())
2905 });
2906 }
2907
2908 #[test]
2909 fn test_default_round_trip() {
2910 figment::Jail::expect_with(|_| {
2911 let original = Config::figment();
2912 let roundtrip = Figment::from(Config::from_provider(&original).unwrap());
2913 for figment in &[original, roundtrip] {
2914 let config = Config::from_provider(figment).unwrap();
2915 assert_eq!(config, Config::default().normalized_optimizer_settings());
2916 }
2917 Ok(())
2918 });
2919 }
2920
2921 #[test]
2922 fn ffi_env_disallowed() {
2923 figment::Jail::expect_with(|jail| {
2924 jail.set_env("FOUNDRY_FFI", "true");
2925 jail.set_env("FFI", "true");
2926 jail.set_env("DAPP_FFI", "true");
2927 let config = Config::load().unwrap();
2928 assert!(!config.ffi);
2929
2930 Ok(())
2931 });
2932 }
2933
2934 #[test]
2935 fn test_profile_env() {
2936 figment::Jail::expect_with(|jail| {
2937 jail.set_env("FOUNDRY_PROFILE", "default");
2938 let figment = Config::figment();
2939 assert_eq!(figment.profile(), "default");
2940
2941 jail.set_env("FOUNDRY_PROFILE", "hardhat");
2942 let figment: Figment = Config::hardhat().into();
2943 assert_eq!(figment.profile(), "hardhat");
2944
2945 jail.create_file(
2946 "foundry.toml",
2947 r"
2948 [profile.default]
2949 libs = ['lib']
2950 [profile.local]
2951 libs = ['modules']
2952 ",
2953 )?;
2954 jail.set_env("FOUNDRY_PROFILE", "local");
2955 let config = Config::load().unwrap();
2956 assert_eq!(config.libs, vec![PathBuf::from("modules")]);
2957
2958 Ok(())
2959 });
2960 }
2961
2962 #[test]
2963 fn test_default_test_path() {
2964 figment::Jail::expect_with(|_| {
2965 let config = Config::default();
2966 let paths_config = config.project_paths::<Solc>();
2967 assert_eq!(paths_config.tests, PathBuf::from(r"test"));
2968 Ok(())
2969 });
2970 }
2971
2972 #[test]
2973 fn test_default_libs() {
2974 figment::Jail::expect_with(|jail| {
2975 let config = Config::load().unwrap();
2976 assert_eq!(config.libs, vec![PathBuf::from("lib")]);
2977
2978 fs::create_dir_all(jail.directory().join("node_modules")).unwrap();
2979 let config = Config::load().unwrap();
2980 assert_eq!(config.libs, vec![PathBuf::from("node_modules")]);
2981
2982 fs::create_dir_all(jail.directory().join("lib")).unwrap();
2983 let config = Config::load().unwrap();
2984 assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
2985
2986 Ok(())
2987 });
2988 }
2989
2990 #[test]
2991 fn test_inheritance_from_default_test_path() {
2992 figment::Jail::expect_with(|jail| {
2993 jail.create_file(
2994 "foundry.toml",
2995 r#"
2996 [profile.default]
2997 test = "defaulttest"
2998 src = "defaultsrc"
2999 libs = ['lib', 'node_modules']
3000
3001 [profile.custom]
3002 src = "customsrc"
3003 "#,
3004 )?;
3005
3006 let config = Config::load().unwrap();
3007 assert_eq!(config.src, PathBuf::from("defaultsrc"));
3008 assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
3009
3010 jail.set_env("FOUNDRY_PROFILE", "custom");
3011 let config = Config::load().unwrap();
3012 assert_eq!(config.src, PathBuf::from("customsrc"));
3013 assert_eq!(config.test, PathBuf::from("defaulttest"));
3014 assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
3015
3016 Ok(())
3017 });
3018 }
3019
3020 #[test]
3021 fn test_custom_test_path() {
3022 figment::Jail::expect_with(|jail| {
3023 jail.create_file(
3024 "foundry.toml",
3025 r#"
3026 [profile.default]
3027 test = "mytest"
3028 "#,
3029 )?;
3030
3031 let config = Config::load().unwrap();
3032 let paths_config = config.project_paths::<Solc>();
3033 assert_eq!(paths_config.tests, PathBuf::from(r"mytest"));
3034 Ok(())
3035 });
3036 }
3037
3038 #[test]
3039 fn test_remappings() {
3040 figment::Jail::expect_with(|jail| {
3041 jail.create_file(
3042 "foundry.toml",
3043 r#"
3044 [profile.default]
3045 src = "some-source"
3046 out = "some-out"
3047 cache = true
3048 "#,
3049 )?;
3050 let config = Config::load().unwrap();
3051 assert!(config.remappings.is_empty());
3052
3053 jail.create_file(
3054 "remappings.txt",
3055 r"
3056 file-ds-test/=lib/ds-test/
3057 file-other/=lib/other/
3058 ",
3059 )?;
3060
3061 let config = Config::load().unwrap();
3062 assert_eq!(
3063 config.remappings,
3064 vec![
3065 Remapping::from_str("file-ds-test/=lib/ds-test/").unwrap().into(),
3066 Remapping::from_str("file-other/=lib/other/").unwrap().into(),
3067 ],
3068 );
3069
3070 jail.set_env("DAPP_REMAPPINGS", "ds-test=lib/ds-test/\nother/=lib/other/");
3071 let config = Config::load().unwrap();
3072
3073 assert_eq!(
3074 config.remappings,
3075 vec![
3076 Remapping::from_str("ds-test=lib/ds-test/").unwrap().into(),
3078 Remapping::from_str("other/=lib/other/").unwrap().into(),
3079 Remapping::from_str("file-ds-test/=lib/ds-test/").unwrap().into(),
3081 Remapping::from_str("file-other/=lib/other/").unwrap().into(),
3082 ],
3083 );
3084
3085 Ok(())
3086 });
3087 }
3088
3089 #[test]
3090 fn test_remappings_override() {
3091 figment::Jail::expect_with(|jail| {
3092 jail.create_file(
3093 "foundry.toml",
3094 r#"
3095 [profile.default]
3096 src = "some-source"
3097 out = "some-out"
3098 cache = true
3099 "#,
3100 )?;
3101 let config = Config::load().unwrap();
3102 assert!(config.remappings.is_empty());
3103
3104 jail.create_file(
3105 "remappings.txt",
3106 r"
3107 ds-test/=lib/ds-test/
3108 other/=lib/other/
3109 ",
3110 )?;
3111
3112 let config = Config::load().unwrap();
3113 assert_eq!(
3114 config.remappings,
3115 vec![
3116 Remapping::from_str("ds-test/=lib/ds-test/").unwrap().into(),
3117 Remapping::from_str("other/=lib/other/").unwrap().into(),
3118 ],
3119 );
3120
3121 jail.set_env("DAPP_REMAPPINGS", "ds-test/=lib/ds-test/src/\nenv-lib/=lib/env-lib/");
3122 let config = Config::load().unwrap();
3123
3124 assert_eq!(
3129 config.remappings,
3130 vec![
3131 Remapping::from_str("ds-test/=lib/ds-test/src/").unwrap().into(),
3132 Remapping::from_str("env-lib/=lib/env-lib/").unwrap().into(),
3133 Remapping::from_str("other/=lib/other/").unwrap().into(),
3134 ],
3135 );
3136
3137 assert_eq!(
3139 config.get_all_remappings().collect::<Vec<_>>(),
3140 vec![
3141 Remapping::from_str("ds-test/=lib/ds-test/src/").unwrap(),
3142 Remapping::from_str("env-lib/=lib/env-lib/").unwrap(),
3143 Remapping::from_str("other/=lib/other/").unwrap(),
3144 ],
3145 );
3146
3147 Ok(())
3148 });
3149 }
3150
3151 #[test]
3152 fn test_can_update_libs() {
3153 figment::Jail::expect_with(|jail| {
3154 jail.create_file(
3155 "foundry.toml",
3156 r#"
3157 [profile.default]
3158 libs = ["node_modules"]
3159 "#,
3160 )?;
3161
3162 let mut config = Config::load().unwrap();
3163 config.libs.push("libs".into());
3164 config.update_libs().unwrap();
3165
3166 let config = Config::load().unwrap();
3167 assert_eq!(config.libs, vec![PathBuf::from("node_modules"), PathBuf::from("libs"),]);
3168 Ok(())
3169 });
3170 }
3171
3172 #[test]
3173 fn test_large_gas_limit() {
3174 figment::Jail::expect_with(|jail| {
3175 let gas = u64::MAX;
3176 jail.create_file(
3177 "foundry.toml",
3178 &format!(
3179 r#"
3180 [profile.default]
3181 gas_limit = "{gas}"
3182 "#
3183 ),
3184 )?;
3185
3186 let config = Config::load().unwrap();
3187 assert_eq!(
3188 config,
3189 Config {
3190 gas_limit: gas.into(),
3191 ..Config::default().normalized_optimizer_settings()
3192 }
3193 );
3194
3195 Ok(())
3196 });
3197 }
3198
3199 #[test]
3200 #[should_panic]
3201 fn test_toml_file_parse_failure() {
3202 figment::Jail::expect_with(|jail| {
3203 jail.create_file(
3204 "foundry.toml",
3205 r#"
3206 [profile.default]
3207 eth_rpc_url = "https://example.com/
3208 "#,
3209 )?;
3210
3211 let _config = Config::load().unwrap();
3212
3213 Ok(())
3214 });
3215 }
3216
3217 #[test]
3218 #[should_panic]
3219 fn test_toml_file_non_existing_config_var_failure() {
3220 figment::Jail::expect_with(|jail| {
3221 jail.set_env("FOUNDRY_CONFIG", "this config does not exist");
3222
3223 let _config = Config::load().unwrap();
3224
3225 Ok(())
3226 });
3227 }
3228
3229 #[test]
3230 fn test_resolve_etherscan_with_chain() {
3231 figment::Jail::expect_with(|jail| {
3232 let env_key = "__BSC_ETHERSCAN_API_KEY";
3233 let env_value = "env value";
3234 jail.create_file(
3235 "foundry.toml",
3236 r#"
3237 [profile.default]
3238
3239 [etherscan]
3240 bsc = { key = "${__BSC_ETHERSCAN_API_KEY}", url = "https://api.bscscan.com/api" }
3241 "#,
3242 )?;
3243
3244 let config = Config::load().unwrap();
3245 assert!(
3246 config
3247 .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
3248 .is_err()
3249 );
3250
3251 unsafe {
3252 std::env::set_var(env_key, env_value);
3253 }
3254
3255 assert_eq!(
3256 config
3257 .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
3258 .unwrap()
3259 .unwrap()
3260 .key,
3261 env_value
3262 );
3263
3264 let mut with_key = config;
3265 with_key.etherscan_api_key = Some("via etherscan_api_key".to_string());
3266
3267 assert_eq!(
3268 with_key
3269 .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
3270 .unwrap()
3271 .unwrap()
3272 .key,
3273 "via etherscan_api_key"
3274 );
3275
3276 unsafe {
3277 std::env::remove_var(env_key);
3278 }
3279 Ok(())
3280 });
3281 }
3282
3283 #[test]
3284 fn test_resolve_etherscan() {
3285 figment::Jail::expect_with(|jail| {
3286 jail.create_file(
3287 "foundry.toml",
3288 r#"
3289 [profile.default]
3290
3291 [etherscan]
3292 mainnet = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
3293 moonbeam = { key = "${_CONFIG_ETHERSCAN_MOONBEAM}" }
3294 "#,
3295 )?;
3296
3297 let config = Config::load().unwrap();
3298
3299 assert!(config.etherscan.clone().resolved().has_unresolved());
3300
3301 jail.set_env("_CONFIG_ETHERSCAN_MOONBEAM", "123456789");
3302
3303 let configs = config.etherscan.resolved();
3304 assert!(!configs.has_unresolved());
3305
3306 let mb_urls = Moonbeam.etherscan_urls().unwrap();
3307 let mainnet_urls = NamedChain::Mainnet.etherscan_urls().unwrap();
3308 assert_eq!(
3309 configs,
3310 ResolvedEtherscanConfigs::new([
3311 (
3312 "mainnet",
3313 ResolvedEtherscanConfig {
3314 api_url: mainnet_urls.0.to_string(),
3315 chain: Some(NamedChain::Mainnet.into()),
3316 browser_url: Some(mainnet_urls.1.to_string()),
3317 key: "FX42Z3BBJJEWXWGYV2X1CIPRSCN".to_string(),
3318 }
3319 ),
3320 (
3321 "moonbeam",
3322 ResolvedEtherscanConfig {
3323 api_url: mb_urls.0.to_string(),
3324 chain: Some(Moonbeam.into()),
3325 browser_url: Some(mb_urls.1.to_string()),
3326 key: "123456789".to_string(),
3327 }
3328 ),
3329 ])
3330 );
3331
3332 Ok(())
3333 });
3334 }
3335
3336 #[test]
3337 fn test_resolve_etherscan_with_versions() {
3338 figment::Jail::expect_with(|jail| {
3339 jail.create_file(
3340 "foundry.toml",
3341 r#"
3342 [profile.default]
3343
3344 [etherscan]
3345 mainnet = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN", api_version = "v2" }
3346 moonbeam = { key = "${_CONFIG_ETHERSCAN_MOONBEAM}", api_version = "v1" }
3347 "#,
3348 )?;
3349
3350 let config = Config::load().unwrap();
3351
3352 assert!(config.etherscan.clone().resolved().has_unresolved());
3353
3354 jail.set_env("_CONFIG_ETHERSCAN_MOONBEAM", "123456789");
3355
3356 let configs = config.etherscan.resolved();
3357 assert!(!configs.has_unresolved());
3358
3359 let mb_urls = Moonbeam.etherscan_urls().unwrap();
3360 let mainnet_urls = NamedChain::Mainnet.etherscan_urls().unwrap();
3361 assert_eq!(
3362 configs,
3363 ResolvedEtherscanConfigs::new([
3364 (
3365 "mainnet",
3366 ResolvedEtherscanConfig {
3367 api_url: mainnet_urls.0.to_string(),
3368 chain: Some(NamedChain::Mainnet.into()),
3369 browser_url: Some(mainnet_urls.1.to_string()),
3370 key: "FX42Z3BBJJEWXWGYV2X1CIPRSCN".to_string(),
3371 }
3372 ),
3373 (
3374 "moonbeam",
3375 ResolvedEtherscanConfig {
3376 api_url: mb_urls.0.to_string(),
3377 chain: Some(Moonbeam.into()),
3378 browser_url: Some(mb_urls.1.to_string()),
3379 key: "123456789".to_string(),
3380 }
3381 ),
3382 ])
3383 );
3384
3385 Ok(())
3386 });
3387 }
3388
3389 #[test]
3390 fn test_resolve_etherscan_chain_id() {
3391 figment::Jail::expect_with(|jail| {
3392 jail.create_file(
3393 "foundry.toml",
3394 r#"
3395 [profile.default]
3396 chain_id = "sepolia"
3397
3398 [etherscan]
3399 sepolia = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
3400 "#,
3401 )?;
3402
3403 let config = Config::load().unwrap();
3404 let etherscan = config.get_etherscan_config().unwrap().unwrap();
3405 assert_eq!(etherscan.chain, Some(NamedChain::Sepolia.into()));
3406 assert_eq!(etherscan.key, "FX42Z3BBJJEWXWGYV2X1CIPRSCN");
3407
3408 Ok(())
3409 });
3410 }
3411
3412 #[test]
3414 fn test_resolve_etherscan_with_invalid_name() {
3415 figment::Jail::expect_with(|jail| {
3416 jail.create_file(
3417 "foundry.toml",
3418 r#"
3419 [etherscan]
3420 mainnet = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
3421 an_invalid_name = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
3422 "#,
3423 )?;
3424
3425 let config = Config::load().unwrap();
3426 let etherscan_config = config.get_etherscan_config();
3427 assert!(etherscan_config.is_none());
3428
3429 Ok(())
3430 });
3431 }
3432
3433 #[test]
3434 fn test_resolve_rpc_url() {
3435 figment::Jail::expect_with(|jail| {
3436 jail.create_file(
3437 "foundry.toml",
3438 r#"
3439 [profile.default]
3440 [rpc_endpoints]
3441 optimism = "https://example.com/"
3442 mainnet = "${_CONFIG_MAINNET}"
3443 "#,
3444 )?;
3445 jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3446
3447 let mut config = Config::load().unwrap();
3448 assert_eq!("http://localhost:8545", config.get_rpc_url_or_localhost_http().unwrap());
3449
3450 config.eth_rpc_url = Some("mainnet".to_string());
3451 assert_eq!(
3452 "https://eth-mainnet.alchemyapi.io/v2/123455",
3453 config.get_rpc_url_or_localhost_http().unwrap()
3454 );
3455
3456 config.eth_rpc_url = Some("optimism".to_string());
3457 assert_eq!("https://example.com/", config.get_rpc_url_or_localhost_http().unwrap());
3458
3459 Ok(())
3460 })
3461 }
3462
3463 #[test]
3464 fn test_resolve_rpc_url_if_etherscan_set() {
3465 figment::Jail::expect_with(|jail| {
3466 jail.create_file(
3467 "foundry.toml",
3468 r#"
3469 [profile.default]
3470 etherscan_api_key = "dummy"
3471 [rpc_endpoints]
3472 optimism = "https://example.com/"
3473 "#,
3474 )?;
3475
3476 let config = Config::load().unwrap();
3477 assert_eq!("http://localhost:8545", config.get_rpc_url_or_localhost_http().unwrap());
3478
3479 Ok(())
3480 })
3481 }
3482
3483 #[test]
3484 fn test_resolve_rpc_url_alias() {
3485 figment::Jail::expect_with(|jail| {
3486 jail.create_file(
3487 "foundry.toml",
3488 r#"
3489 [profile.default]
3490 [rpc_endpoints]
3491 polygonAmoy = "https://polygon-amoy.g.alchemy.com/v2/${_RESOLVE_RPC_ALIAS}"
3492 "#,
3493 )?;
3494 let mut config = Config::load().unwrap();
3495 config.eth_rpc_url = Some("polygonAmoy".to_string());
3496 assert!(config.get_rpc_url().unwrap().is_err());
3497
3498 jail.set_env("_RESOLVE_RPC_ALIAS", "123455");
3499
3500 let mut config = Config::load().unwrap();
3501 config.eth_rpc_url = Some("polygonAmoy".to_string());
3502 assert_eq!(
3503 "https://polygon-amoy.g.alchemy.com/v2/123455",
3504 config.get_rpc_url().unwrap().unwrap()
3505 );
3506
3507 Ok(())
3508 })
3509 }
3510
3511 #[test]
3512 fn test_resolve_rpc_aliases() {
3513 figment::Jail::expect_with(|jail| {
3514 jail.create_file(
3515 "foundry.toml",
3516 r#"
3517 [profile.default]
3518 [etherscan]
3519 arbitrum_alias = { key = "${TEST_RESOLVE_RPC_ALIAS_ARBISCAN}" }
3520 [rpc_endpoints]
3521 arbitrum_alias = "https://arb-mainnet.g.alchemy.com/v2/${TEST_RESOLVE_RPC_ALIAS_ARB_ONE}"
3522 "#,
3523 )?;
3524
3525 jail.set_env("TEST_RESOLVE_RPC_ALIAS_ARB_ONE", "123455");
3526 jail.set_env("TEST_RESOLVE_RPC_ALIAS_ARBISCAN", "123455");
3527
3528 let config = Config::load().unwrap();
3529
3530 let config = config.get_etherscan_config_with_chain(Some(NamedChain::Arbitrum.into()));
3531 assert!(config.is_err());
3532 assert_eq!(
3533 config.unwrap_err().to_string(),
3534 "At least one of `url` or `chain` must be present for Etherscan config with unknown alias `arbitrum_alias`"
3535 );
3536
3537 Ok(())
3538 });
3539 }
3540
3541 #[test]
3542 fn test_resolve_rpc_config() {
3543 figment::Jail::expect_with(|jail| {
3544 jail.create_file(
3545 "foundry.toml",
3546 r#"
3547 [rpc_endpoints]
3548 optimism = "https://example.com/"
3549 mainnet = { endpoint = "${_CONFIG_MAINNET}", retries = 3, retry_backoff = 1000, compute_units_per_second = 1000 }
3550 "#,
3551 )?;
3552 jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3553
3554 let config = Config::load().unwrap();
3555 assert_eq!(
3556 RpcEndpoints::new([
3557 (
3558 "optimism",
3559 RpcEndpointType::String(RpcEndpointUrl::Url(
3560 "https://example.com/".to_string()
3561 ))
3562 ),
3563 (
3564 "mainnet",
3565 RpcEndpointType::Config(RpcEndpoint {
3566 endpoint: RpcEndpointUrl::Env("${_CONFIG_MAINNET}".to_string()),
3567 config: RpcEndpointConfig {
3568 retries: Some(3),
3569 retry_backoff: Some(1000),
3570 compute_units_per_second: Some(1000),
3571 },
3572 auth: None,
3573 })
3574 ),
3575 ]),
3576 config.rpc_endpoints
3577 );
3578
3579 let resolved = config.rpc_endpoints.resolved();
3580 assert_eq!(
3581 RpcEndpoints::new([
3582 (
3583 "optimism",
3584 RpcEndpointType::String(RpcEndpointUrl::Url(
3585 "https://example.com/".to_string()
3586 ))
3587 ),
3588 (
3589 "mainnet",
3590 RpcEndpointType::Config(RpcEndpoint {
3591 endpoint: RpcEndpointUrl::Env("${_CONFIG_MAINNET}".to_string()),
3592 config: RpcEndpointConfig {
3593 retries: Some(3),
3594 retry_backoff: Some(1000),
3595 compute_units_per_second: Some(1000),
3596 },
3597 auth: None,
3598 })
3599 ),
3600 ])
3601 .resolved(),
3602 resolved
3603 );
3604 Ok(())
3605 })
3606 }
3607
3608 #[test]
3609 fn test_resolve_auth() {
3610 figment::Jail::expect_with(|jail| {
3611 jail.create_file(
3612 "foundry.toml",
3613 r#"
3614 [profile.default]
3615 eth_rpc_url = "optimism"
3616 [rpc_endpoints]
3617 optimism = "https://example.com/"
3618 mainnet = { endpoint = "${_CONFIG_MAINNET}", retries = 3, retry_backoff = 1000, compute_units_per_second = 1000, auth = "Bearer ${_CONFIG_AUTH}" }
3619 "#,
3620 )?;
3621
3622 let config = Config::load().unwrap();
3623
3624 jail.set_env("_CONFIG_AUTH", "123456");
3625 jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3626
3627 assert_eq!(
3628 RpcEndpoints::new([
3629 (
3630 "optimism",
3631 RpcEndpointType::String(RpcEndpointUrl::Url(
3632 "https://example.com/".to_string()
3633 ))
3634 ),
3635 (
3636 "mainnet",
3637 RpcEndpointType::Config(RpcEndpoint {
3638 endpoint: RpcEndpointUrl::Env("${_CONFIG_MAINNET}".to_string()),
3639 config: RpcEndpointConfig {
3640 retries: Some(3),
3641 retry_backoff: Some(1000),
3642 compute_units_per_second: Some(1000)
3643 },
3644 auth: Some(RpcAuth::Env("Bearer ${_CONFIG_AUTH}".to_string())),
3645 })
3646 ),
3647 ]),
3648 config.rpc_endpoints
3649 );
3650 let resolved = config.rpc_endpoints.resolved();
3651 assert_eq!(
3652 RpcEndpoints::new([
3653 (
3654 "optimism",
3655 RpcEndpointType::String(RpcEndpointUrl::Url(
3656 "https://example.com/".to_string()
3657 ))
3658 ),
3659 (
3660 "mainnet",
3661 RpcEndpointType::Config(RpcEndpoint {
3662 endpoint: RpcEndpointUrl::Url(
3663 "https://eth-mainnet.alchemyapi.io/v2/123455".to_string()
3664 ),
3665 config: RpcEndpointConfig {
3666 retries: Some(3),
3667 retry_backoff: Some(1000),
3668 compute_units_per_second: Some(1000)
3669 },
3670 auth: Some(RpcAuth::Raw("Bearer 123456".to_string())),
3671 })
3672 ),
3673 ])
3674 .resolved(),
3675 resolved
3676 );
3677
3678 Ok(())
3679 });
3680 }
3681
3682 #[test]
3683 fn test_resolve_endpoints() {
3684 figment::Jail::expect_with(|jail| {
3685 jail.create_file(
3686 "foundry.toml",
3687 r#"
3688 [profile.default]
3689 eth_rpc_url = "optimism"
3690 [rpc_endpoints]
3691 optimism = "https://example.com/"
3692 mainnet = "${_CONFIG_MAINNET}"
3693 mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${_CONFIG_API_KEY1}"
3694 mainnet_3 = "https://eth-mainnet.alchemyapi.io/v2/${_CONFIG_API_KEY1}/${_CONFIG_API_KEY2}"
3695 "#,
3696 )?;
3697
3698 let config = Config::load().unwrap();
3699
3700 assert_eq!(config.get_rpc_url().unwrap().unwrap(), "https://example.com/");
3701
3702 assert!(config.rpc_endpoints.clone().resolved().has_unresolved());
3703
3704 jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3705 jail.set_env("_CONFIG_API_KEY1", "123456");
3706 jail.set_env("_CONFIG_API_KEY2", "98765");
3707
3708 let endpoints = config.rpc_endpoints.resolved();
3709
3710 assert!(!endpoints.has_unresolved());
3711
3712 assert_eq!(
3713 endpoints,
3714 RpcEndpoints::new([
3715 ("optimism", RpcEndpointUrl::Url("https://example.com/".to_string())),
3716 (
3717 "mainnet",
3718 RpcEndpointUrl::Url(
3719 "https://eth-mainnet.alchemyapi.io/v2/123455".to_string()
3720 )
3721 ),
3722 (
3723 "mainnet_2",
3724 RpcEndpointUrl::Url(
3725 "https://eth-mainnet.alchemyapi.io/v2/123456".to_string()
3726 )
3727 ),
3728 (
3729 "mainnet_3",
3730 RpcEndpointUrl::Url(
3731 "https://eth-mainnet.alchemyapi.io/v2/123456/98765".to_string()
3732 )
3733 ),
3734 ])
3735 .resolved()
3736 );
3737
3738 Ok(())
3739 });
3740 }
3741
3742 #[test]
3743 fn test_extract_etherscan_config() {
3744 figment::Jail::expect_with(|jail| {
3745 jail.create_file(
3746 "foundry.toml",
3747 r#"
3748 [profile.default]
3749 etherscan_api_key = "optimism"
3750
3751 [etherscan]
3752 optimism = { key = "https://etherscan-optimism.com/" }
3753 amoy = { key = "https://etherscan-amoy.com/" }
3754 "#,
3755 )?;
3756
3757 let mut config = Config::load().unwrap();
3758
3759 let optimism = config.get_etherscan_api_key(Some(NamedChain::Optimism.into()));
3760 assert_eq!(optimism, Some("https://etherscan-optimism.com/".to_string()));
3761
3762 config.etherscan_api_key = Some("amoy".to_string());
3763
3764 let amoy = config.get_etherscan_api_key(Some(NamedChain::PolygonAmoy.into()));
3765 assert_eq!(amoy, Some("https://etherscan-amoy.com/".to_string()));
3766
3767 Ok(())
3768 });
3769 }
3770
3771 #[test]
3772 fn test_extract_etherscan_config_by_chain() {
3773 figment::Jail::expect_with(|jail| {
3774 jail.create_file(
3775 "foundry.toml",
3776 r#"
3777 [profile.default]
3778
3779 [etherscan]
3780 amoy = { key = "https://etherscan-amoy.com/", chain = 80002 }
3781 "#,
3782 )?;
3783
3784 let config = Config::load().unwrap();
3785
3786 let amoy = config
3787 .get_etherscan_config_with_chain(Some(NamedChain::PolygonAmoy.into()))
3788 .unwrap()
3789 .unwrap();
3790 assert_eq!(amoy.key, "https://etherscan-amoy.com/".to_string());
3791
3792 Ok(())
3793 });
3794 }
3795
3796 #[test]
3797 fn test_extract_etherscan_config_by_chain_with_url() {
3798 figment::Jail::expect_with(|jail| {
3799 jail.create_file(
3800 "foundry.toml",
3801 r#"
3802 [profile.default]
3803
3804 [etherscan]
3805 amoy = { key = "https://etherscan-amoy.com/", chain = 80002 , url = "https://verifier-url.com/"}
3806 "#,
3807 )?;
3808
3809 let config = Config::load().unwrap();
3810
3811 let amoy = config
3812 .get_etherscan_config_with_chain(Some(NamedChain::PolygonAmoy.into()))
3813 .unwrap()
3814 .unwrap();
3815 assert_eq!(amoy.key, "https://etherscan-amoy.com/".to_string());
3816 assert_eq!(amoy.api_url, "https://verifier-url.com/".to_string());
3817
3818 Ok(())
3819 });
3820 }
3821
3822 #[test]
3823 fn test_extract_etherscan_config_by_chain_and_alias() {
3824 figment::Jail::expect_with(|jail| {
3825 jail.create_file(
3826 "foundry.toml",
3827 r#"
3828 [profile.default]
3829 eth_rpc_url = "amoy"
3830
3831 [etherscan]
3832 amoy = { key = "https://etherscan-amoy.com/" }
3833
3834 [rpc_endpoints]
3835 amoy = "https://polygon-amoy.g.alchemy.com/v2/amoy"
3836 "#,
3837 )?;
3838
3839 let config = Config::load().unwrap();
3840
3841 let amoy = config.get_etherscan_config_with_chain(None).unwrap().unwrap();
3842 assert_eq!(amoy.key, "https://etherscan-amoy.com/".to_string());
3843
3844 let amoy_rpc = config.get_rpc_url().unwrap().unwrap();
3845 assert_eq!(amoy_rpc, "https://polygon-amoy.g.alchemy.com/v2/amoy");
3846 Ok(())
3847 });
3848 }
3849
3850 #[test]
3851 fn test_toml_file() {
3852 figment::Jail::expect_with(|jail| {
3853 jail.create_file(
3854 "foundry.toml",
3855 r#"
3856 [profile.default]
3857 src = "some-source"
3858 out = "some-out"
3859 cache = true
3860 eth_rpc_url = "https://example.com/"
3861 verbosity = 3
3862 remappings = ["ds-test=lib/ds-test/"]
3863 via_ir = true
3864 rpc_storage_caching = { chains = [1, "optimism", 999999], endpoints = "all"}
3865 use_literal_content = false
3866 bytecode_hash = "ipfs"
3867 cbor_metadata = true
3868 revert_strings = "strip"
3869 allow_paths = ["allow", "paths"]
3870 build_info_path = "build-info"
3871 always_use_create_2_factory = true
3872
3873 [rpc_endpoints]
3874 optimism = "https://example.com/"
3875 mainnet = "${RPC_MAINNET}"
3876 mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}"
3877 mainnet_3 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}/${ANOTHER_KEY}"
3878 "#,
3879 )?;
3880
3881 let config = Config::load().unwrap();
3882 assert_eq!(
3883 config,
3884 Config {
3885 src: "some-source".into(),
3886 out: "some-out".into(),
3887 cache: true,
3888 eth_rpc_url: Some("https://example.com/".to_string()),
3889 remappings: vec![Remapping::from_str("ds-test=lib/ds-test/").unwrap().into()],
3890 verbosity: 3,
3891 via_ir: true,
3892 rpc_storage_caching: StorageCachingConfig {
3893 chains: CachedChains::Chains(vec![
3894 Chain::mainnet(),
3895 Chain::optimism_mainnet(),
3896 Chain::from_id(999999)
3897 ]),
3898 endpoints: CachedEndpoints::All,
3899 },
3900 use_literal_content: false,
3901 bytecode_hash: BytecodeHash::Ipfs,
3902 cbor_metadata: true,
3903 revert_strings: Some(RevertStrings::Strip),
3904 allow_paths: vec![PathBuf::from("allow"), PathBuf::from("paths")],
3905 rpc_endpoints: RpcEndpoints::new([
3906 ("optimism", RpcEndpointUrl::Url("https://example.com/".to_string())),
3907 ("mainnet", RpcEndpointUrl::Env("${RPC_MAINNET}".to_string())),
3908 (
3909 "mainnet_2",
3910 RpcEndpointUrl::Env(
3911 "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}".to_string()
3912 )
3913 ),
3914 (
3915 "mainnet_3",
3916 RpcEndpointUrl::Env(
3917 "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}/${ANOTHER_KEY}"
3918 .to_string()
3919 )
3920 ),
3921 ]),
3922 build_info_path: Some("build-info".into()),
3923 always_use_create_2_factory: true,
3924 ..Config::default().normalized_optimizer_settings()
3925 }
3926 );
3927
3928 Ok(())
3929 });
3930 }
3931
3932 #[test]
3933 fn test_load_remappings() {
3934 figment::Jail::expect_with(|jail| {
3935 jail.create_file(
3936 "foundry.toml",
3937 r"
3938 [profile.default]
3939 remappings = ['nested/=lib/nested/']
3940 ",
3941 )?;
3942
3943 let config = Config::load_with_root(jail.directory()).unwrap();
3944 assert_eq!(
3945 config.remappings,
3946 vec![Remapping::from_str("nested/=lib/nested/").unwrap().into()]
3947 );
3948
3949 Ok(())
3950 });
3951 }
3952
3953 #[test]
3954 fn test_load_full_toml() {
3955 figment::Jail::expect_with(|jail| {
3956 jail.create_file(
3957 "foundry.toml",
3958 r#"
3959 [profile.default]
3960 auto_detect_solc = true
3961 block_base_fee_per_gas = 0
3962 block_coinbase = '0x0000000000000000000000000000000000000000'
3963 block_difficulty = 0
3964 block_prevrandao = '0x0000000000000000000000000000000000000000000000000000000000000000'
3965 block_number = 1
3966 block_timestamp = 1
3967 use_literal_content = false
3968 bytecode_hash = 'ipfs'
3969 cbor_metadata = true
3970 cache = true
3971 cache_path = 'cache'
3972 evm_version = 'london'
3973 extra_output = []
3974 extra_output_files = []
3975 always_use_create_2_factory = false
3976 ffi = false
3977 force = false
3978 gas_limit = 9223372036854775807
3979 gas_price = 0
3980 gas_reports = ['*']
3981 ignored_error_codes = [1878]
3982 ignored_warnings_from = ["something"]
3983 deny = "never"
3984 initial_balance = '0xffffffffffffffffffffffff'
3985 libraries = []
3986 libs = ['lib']
3987 memory_limit = 134217728
3988 names = false
3989 no_storage_caching = false
3990 no_rpc_rate_limit = false
3991 offline = false
3992 optimizer = true
3993 optimizer_runs = 200
3994 out = 'out'
3995 remappings = ['nested/=lib/nested/']
3996 sender = '0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38'
3997 sizes = false
3998 sparse_mode = false
3999 src = 'src'
4000 test = 'test'
4001 tx_origin = '0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38'
4002 verbosity = 0
4003 via_ir = false
4004
4005 [profile.default.rpc_storage_caching]
4006 chains = 'all'
4007 endpoints = 'all'
4008
4009 [rpc_endpoints]
4010 optimism = "https://example.com/"
4011 mainnet = "${RPC_MAINNET}"
4012 mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}"
4013
4014 [fuzz]
4015 runs = 256
4016 seed = '0x3e8'
4017 max_test_rejects = 65536
4018
4019 [invariant]
4020 runs = 256
4021 depth = 500
4022 fail_on_revert = false
4023 call_override = false
4024 shrink_run_limit = 5000
4025 "#,
4026 )?;
4027
4028 let config = Config::load_with_root(jail.directory()).unwrap();
4029
4030 assert_eq!(config.ignored_file_paths, vec![PathBuf::from("something")]);
4031 assert_eq!(config.fuzz.seed, Some(U256::from(1000)));
4032 assert_eq!(
4033 config.remappings,
4034 vec![Remapping::from_str("nested/=lib/nested/").unwrap().into()]
4035 );
4036
4037 assert_eq!(
4038 config.rpc_endpoints,
4039 RpcEndpoints::new([
4040 ("optimism", RpcEndpointUrl::Url("https://example.com/".to_string())),
4041 ("mainnet", RpcEndpointUrl::Env("${RPC_MAINNET}".to_string())),
4042 (
4043 "mainnet_2",
4044 RpcEndpointUrl::Env(
4045 "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}".to_string()
4046 )
4047 ),
4048 ]),
4049 );
4050
4051 Ok(())
4052 });
4053 }
4054
4055 #[test]
4056 fn test_solc_req() {
4057 figment::Jail::expect_with(|jail| {
4058 jail.create_file(
4059 "foundry.toml",
4060 r#"
4061 [profile.default]
4062 solc_version = "0.8.12"
4063 "#,
4064 )?;
4065
4066 let config = Config::load().unwrap();
4067 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));
4068
4069 jail.create_file(
4070 "foundry.toml",
4071 r#"
4072 [profile.default]
4073 solc = "0.8.12"
4074 "#,
4075 )?;
4076
4077 let config = Config::load().unwrap();
4078 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));
4079
4080 jail.create_file(
4081 "foundry.toml",
4082 r#"
4083 [profile.default]
4084 solc = "path/to/local/solc"
4085 "#,
4086 )?;
4087
4088 let config = Config::load().unwrap();
4089 assert_eq!(config.solc, Some(SolcReq::Local("path/to/local/solc".into())));
4090
4091 jail.set_env("FOUNDRY_SOLC_VERSION", "0.6.6");
4092 let config = Config::load().unwrap();
4093 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 6, 6))));
4094 Ok(())
4095 });
4096 }
4097
4098 #[test]
4100 fn test_backwards_solc_version() {
4101 figment::Jail::expect_with(|jail| {
4102 jail.create_file(
4103 "foundry.toml",
4104 r#"
4105 [default]
4106 solc = "0.8.12"
4107 solc_version = "0.8.20"
4108 "#,
4109 )?;
4110
4111 let config = Config::load().unwrap();
4112 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));
4113
4114 Ok(())
4115 });
4116
4117 figment::Jail::expect_with(|jail| {
4118 jail.create_file(
4119 "foundry.toml",
4120 r#"
4121 [default]
4122 solc_version = "0.8.20"
4123 "#,
4124 )?;
4125
4126 let config = Config::load().unwrap();
4127 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 20))));
4128
4129 Ok(())
4130 });
4131 }
4132
4133 #[test]
4134 fn test_toml_casing_file() {
4135 figment::Jail::expect_with(|jail| {
4136 jail.create_file(
4137 "foundry.toml",
4138 r#"
4139 [profile.default]
4140 src = "some-source"
4141 out = "some-out"
4142 cache = true
4143 eth-rpc-url = "https://example.com/"
4144 evm-version = "berlin"
4145 auto-detect-solc = false
4146 "#,
4147 )?;
4148
4149 let config = Config::load().unwrap();
4150 assert_eq!(
4151 config,
4152 Config {
4153 src: "some-source".into(),
4154 out: "some-out".into(),
4155 cache: true,
4156 eth_rpc_url: Some("https://example.com/".to_string()),
4157 auto_detect_solc: false,
4158 evm_version: EvmVersion::Berlin,
4159 ..Config::default().normalized_optimizer_settings()
4160 }
4161 );
4162
4163 Ok(())
4164 });
4165 }
4166
4167 #[test]
4168 fn test_output_selection() {
4169 figment::Jail::expect_with(|jail| {
4170 jail.create_file(
4171 "foundry.toml",
4172 r#"
4173 [profile.default]
4174 extra_output = ["metadata", "ir-optimized"]
4175 extra_output_files = ["metadata"]
4176 "#,
4177 )?;
4178
4179 let config = Config::load().unwrap();
4180
4181 assert_eq!(
4182 config.extra_output,
4183 vec![ContractOutputSelection::Metadata, ContractOutputSelection::IrOptimized]
4184 );
4185 assert_eq!(config.extra_output_files, vec![ContractOutputSelection::Metadata]);
4186
4187 Ok(())
4188 });
4189 }
4190
4191 #[test]
4192 fn test_precedence() {
4193 figment::Jail::expect_with(|jail| {
4194 jail.create_file(
4195 "foundry.toml",
4196 r#"
4197 [profile.default]
4198 src = "mysrc"
4199 out = "myout"
4200 verbosity = 3
4201 "#,
4202 )?;
4203
4204 let config = Config::load().unwrap();
4205 assert_eq!(
4206 config,
4207 Config {
4208 src: "mysrc".into(),
4209 out: "myout".into(),
4210 verbosity: 3,
4211 ..Config::default().normalized_optimizer_settings()
4212 }
4213 );
4214
4215 jail.set_env("FOUNDRY_SRC", r"other-src");
4216 let config = Config::load().unwrap();
4217 assert_eq!(
4218 config,
4219 Config {
4220 src: "other-src".into(),
4221 out: "myout".into(),
4222 verbosity: 3,
4223 ..Config::default().normalized_optimizer_settings()
4224 }
4225 );
4226
4227 jail.set_env("FOUNDRY_PROFILE", "foo");
4228 let val: Result<String, _> = Config::figment().extract_inner("profile");
4229 assert!(val.is_err());
4230
4231 Ok(())
4232 });
4233 }
4234
4235 #[test]
4236 fn test_extract_basic() {
4237 figment::Jail::expect_with(|jail| {
4238 jail.create_file(
4239 "foundry.toml",
4240 r#"
4241 [profile.default]
4242 src = "mysrc"
4243 out = "myout"
4244 verbosity = 3
4245 evm_version = 'berlin'
4246
4247 [profile.other]
4248 src = "other-src"
4249 "#,
4250 )?;
4251 let loaded = Config::load().unwrap();
4252 assert_eq!(loaded.evm_version, EvmVersion::Berlin);
4253 let base = loaded.into_basic();
4254 let default = Config::default();
4255 assert_eq!(
4256 base,
4257 BasicConfig {
4258 profile: Config::DEFAULT_PROFILE,
4259 src: "mysrc".into(),
4260 out: "myout".into(),
4261 libs: default.libs.clone(),
4262 remappings: default.remappings.clone(),
4263 }
4264 );
4265 jail.set_env("FOUNDRY_PROFILE", r"other");
4266 let base = Config::figment().extract::<BasicConfig>().unwrap();
4267 assert_eq!(
4268 base,
4269 BasicConfig {
4270 profile: Config::DEFAULT_PROFILE,
4271 src: "other-src".into(),
4272 out: "myout".into(),
4273 libs: default.libs.clone(),
4274 remappings: default.remappings,
4275 }
4276 );
4277 Ok(())
4278 });
4279 }
4280
4281 #[test]
4282 #[should_panic]
4283 fn test_parse_invalid_fuzz_weight() {
4284 figment::Jail::expect_with(|jail| {
4285 jail.create_file(
4286 "foundry.toml",
4287 r"
4288 [fuzz]
4289 dictionary_weight = 101
4290 ",
4291 )?;
4292 let _config = Config::load().unwrap();
4293 Ok(())
4294 });
4295 }
4296
4297 #[test]
4298 fn test_fallback_provider() {
4299 figment::Jail::expect_with(|jail| {
4300 jail.create_file(
4301 "foundry.toml",
4302 r"
4303 [fuzz]
4304 runs = 1
4305 include_storage = false
4306 dictionary_weight = 99
4307
4308 [invariant]
4309 runs = 420
4310
4311 [profile.ci.fuzz]
4312 dictionary_weight = 5
4313
4314 [profile.ci.invariant]
4315 runs = 400
4316 ",
4317 )?;
4318
4319 let invariant_default = InvariantConfig::default();
4320 let config = Config::load().unwrap();
4321
4322 assert_ne!(config.invariant.runs, config.fuzz.runs);
4323 assert_eq!(config.invariant.runs, 420);
4324
4325 assert_ne!(
4326 config.fuzz.dictionary.include_storage,
4327 invariant_default.dictionary.include_storage
4328 );
4329 assert_eq!(
4330 config.invariant.dictionary.include_storage,
4331 config.fuzz.dictionary.include_storage
4332 );
4333
4334 assert_ne!(
4335 config.fuzz.dictionary.dictionary_weight,
4336 invariant_default.dictionary.dictionary_weight
4337 );
4338 assert_eq!(
4339 config.invariant.dictionary.dictionary_weight,
4340 config.fuzz.dictionary.dictionary_weight
4341 );
4342
4343 jail.set_env("FOUNDRY_PROFILE", "ci");
4344 let ci_config = Config::load().unwrap();
4345 assert_eq!(ci_config.fuzz.runs, 1);
4346 assert_eq!(ci_config.invariant.runs, 400);
4347 assert_eq!(ci_config.fuzz.dictionary.dictionary_weight, 5);
4348 assert_eq!(
4349 ci_config.invariant.dictionary.dictionary_weight,
4350 config.fuzz.dictionary.dictionary_weight
4351 );
4352
4353 Ok(())
4354 })
4355 }
4356
4357 #[test]
4358 fn test_standalone_profile_sections() {
4359 figment::Jail::expect_with(|jail| {
4360 jail.create_file(
4361 "foundry.toml",
4362 r"
4363 [fuzz]
4364 runs = 100
4365
4366 [invariant]
4367 runs = 120
4368
4369 [profile.ci.fuzz]
4370 runs = 420
4371
4372 [profile.ci.invariant]
4373 runs = 500
4374 ",
4375 )?;
4376
4377 let config = Config::load().unwrap();
4378 assert_eq!(config.fuzz.runs, 100);
4379 assert_eq!(config.invariant.runs, 120);
4380
4381 jail.set_env("FOUNDRY_PROFILE", "ci");
4382 let config = Config::load().unwrap();
4383 assert_eq!(config.fuzz.runs, 420);
4384 assert_eq!(config.invariant.runs, 500);
4385
4386 Ok(())
4387 });
4388 }
4389
4390 #[test]
4391 fn can_handle_deviating_dapp_aliases() {
4392 figment::Jail::expect_with(|jail| {
4393 let addr = Address::ZERO;
4394 jail.set_env("DAPP_TEST_NUMBER", 1337);
4395 jail.set_env("DAPP_TEST_ADDRESS", format!("{addr:?}"));
4396 jail.set_env("DAPP_TEST_FUZZ_RUNS", 420);
4397 jail.set_env("DAPP_TEST_DEPTH", 20);
4398 jail.set_env("DAPP_FORK_BLOCK", 100);
4399 jail.set_env("DAPP_BUILD_OPTIMIZE_RUNS", 999);
4400 jail.set_env("DAPP_BUILD_OPTIMIZE", 0);
4401
4402 let config = Config::load().unwrap();
4403
4404 assert_eq!(config.block_number, U256::from(1337));
4405 assert_eq!(config.sender, addr);
4406 assert_eq!(config.fuzz.runs, 420);
4407 assert_eq!(config.invariant.depth, 20);
4408 assert_eq!(config.fork_block_number, Some(100));
4409 assert_eq!(config.optimizer_runs, Some(999));
4410 assert!(!config.optimizer.unwrap());
4411
4412 Ok(())
4413 });
4414 }
4415
4416 #[test]
4417 fn can_parse_libraries() {
4418 figment::Jail::expect_with(|jail| {
4419 jail.set_env(
4420 "DAPP_LIBRARIES",
4421 "[src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6]",
4422 );
4423 let config = Config::load().unwrap();
4424 assert_eq!(
4425 config.libraries,
4426 vec![
4427 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4428 .to_string()
4429 ]
4430 );
4431
4432 jail.set_env(
4433 "DAPP_LIBRARIES",
4434 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6",
4435 );
4436 let config = Config::load().unwrap();
4437 assert_eq!(
4438 config.libraries,
4439 vec![
4440 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4441 .to_string(),
4442 ]
4443 );
4444
4445 jail.set_env(
4446 "DAPP_LIBRARIES",
4447 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6,src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6",
4448 );
4449 let config = Config::load().unwrap();
4450 assert_eq!(
4451 config.libraries,
4452 vec![
4453 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4454 .to_string(),
4455 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4456 .to_string()
4457 ]
4458 );
4459
4460 Ok(())
4461 });
4462 }
4463
4464 #[test]
4465 fn test_parse_many_libraries() {
4466 figment::Jail::expect_with(|jail| {
4467 jail.create_file(
4468 "foundry.toml",
4469 r"
4470 [profile.default]
4471 libraries= [
4472 './src/SizeAuctionDiscount.sol:Chainlink:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
4473 './src/SizeAuction.sol:ChainlinkTWAP:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
4474 './src/SizeAuction.sol:Math:0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c',
4475 './src/test/ChainlinkTWAP.t.sol:ChainlinkTWAP:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
4476 './src/SizeAuctionDiscount.sol:Math:0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c',
4477 ]
4478 ",
4479 )?;
4480 let config = Config::load().unwrap();
4481
4482 let libs = config.parsed_libraries().unwrap().libs;
4483
4484 similar_asserts::assert_eq!(
4485 libs,
4486 BTreeMap::from([
4487 (
4488 PathBuf::from("./src/SizeAuctionDiscount.sol"),
4489 BTreeMap::from([
4490 (
4491 "Chainlink".to_string(),
4492 "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
4493 ),
4494 (
4495 "Math".to_string(),
4496 "0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c".to_string()
4497 )
4498 ])
4499 ),
4500 (
4501 PathBuf::from("./src/SizeAuction.sol"),
4502 BTreeMap::from([
4503 (
4504 "ChainlinkTWAP".to_string(),
4505 "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
4506 ),
4507 (
4508 "Math".to_string(),
4509 "0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c".to_string()
4510 )
4511 ])
4512 ),
4513 (
4514 PathBuf::from("./src/test/ChainlinkTWAP.t.sol"),
4515 BTreeMap::from([(
4516 "ChainlinkTWAP".to_string(),
4517 "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
4518 )])
4519 ),
4520 ])
4521 );
4522
4523 Ok(())
4524 });
4525 }
4526
4527 #[test]
4528 fn config_roundtrip() {
4529 figment::Jail::expect_with(|jail| {
4530 let default = Config::default().normalized_optimizer_settings();
4531 let basic = default.clone().into_basic();
4532 jail.create_file("foundry.toml", &basic.to_string_pretty().unwrap())?;
4533
4534 let mut other = Config::load().unwrap();
4535 clear_warning(&mut other);
4536 assert_eq!(default, other);
4537
4538 let other = other.into_basic();
4539 assert_eq!(basic, other);
4540
4541 jail.create_file("foundry.toml", &default.to_string_pretty().unwrap())?;
4542 let mut other = Config::load().unwrap();
4543 clear_warning(&mut other);
4544 assert_eq!(default, other);
4545
4546 Ok(())
4547 });
4548 }
4549
4550 #[test]
4551 fn test_fs_permissions() {
4552 figment::Jail::expect_with(|jail| {
4553 jail.create_file(
4554 "foundry.toml",
4555 r#"
4556 [profile.default]
4557 fs_permissions = [{ access = "read-write", path = "./"}]
4558 "#,
4559 )?;
4560 let loaded = Config::load().unwrap();
4561
4562 assert_eq!(
4563 loaded.fs_permissions,
4564 FsPermissions::new(vec![PathPermission::read_write("./")])
4565 );
4566
4567 jail.create_file(
4568 "foundry.toml",
4569 r#"
4570 [profile.default]
4571 fs_permissions = [{ access = "none", path = "./"}]
4572 "#,
4573 )?;
4574 let loaded = Config::load().unwrap();
4575 assert_eq!(loaded.fs_permissions, FsPermissions::new(vec![PathPermission::none("./")]));
4576
4577 Ok(())
4578 });
4579 }
4580
4581 #[test]
4582 fn test_optimizer_settings_basic() {
4583 figment::Jail::expect_with(|jail| {
4584 jail.create_file(
4585 "foundry.toml",
4586 r"
4587 [profile.default]
4588 optimizer = true
4589
4590 [profile.default.optimizer_details]
4591 yul = false
4592
4593 [profile.default.optimizer_details.yulDetails]
4594 stackAllocation = true
4595 ",
4596 )?;
4597 let mut loaded = Config::load().unwrap();
4598 clear_warning(&mut loaded);
4599 assert_eq!(
4600 loaded.optimizer_details,
4601 Some(OptimizerDetails {
4602 yul: Some(false),
4603 yul_details: Some(YulDetails {
4604 stack_allocation: Some(true),
4605 ..Default::default()
4606 }),
4607 ..Default::default()
4608 })
4609 );
4610
4611 let s = loaded.to_string_pretty().unwrap();
4612 jail.create_file("foundry.toml", &s)?;
4613
4614 let mut reloaded = Config::load().unwrap();
4615 clear_warning(&mut reloaded);
4616 assert_eq!(loaded, reloaded);
4617
4618 Ok(())
4619 });
4620 }
4621
4622 #[test]
4623 fn test_model_checker_settings_basic() {
4624 figment::Jail::expect_with(|jail| {
4625 jail.create_file(
4626 "foundry.toml",
4627 r"
4628 [profile.default]
4629
4630 [profile.default.model_checker]
4631 contracts = { 'a.sol' = [ 'A1', 'A2' ], 'b.sol' = [ 'B1', 'B2' ] }
4632 engine = 'chc'
4633 targets = [ 'assert', 'outOfBounds' ]
4634 timeout = 10000
4635 ",
4636 )?;
4637 let mut loaded = Config::load().unwrap();
4638 clear_warning(&mut loaded);
4639 assert_eq!(
4640 loaded.model_checker,
4641 Some(ModelCheckerSettings {
4642 contracts: BTreeMap::from([
4643 ("a.sol".to_string(), vec!["A1".to_string(), "A2".to_string()]),
4644 ("b.sol".to_string(), vec!["B1".to_string(), "B2".to_string()]),
4645 ]),
4646 engine: Some(ModelCheckerEngine::CHC),
4647 targets: Some(vec![
4648 ModelCheckerTarget::Assert,
4649 ModelCheckerTarget::OutOfBounds
4650 ]),
4651 timeout: Some(10000),
4652 invariants: None,
4653 show_unproved: None,
4654 div_mod_with_slacks: None,
4655 solvers: None,
4656 show_unsupported: None,
4657 show_proved_safe: None,
4658 })
4659 );
4660
4661 let s = loaded.to_string_pretty().unwrap();
4662 jail.create_file("foundry.toml", &s)?;
4663
4664 let mut reloaded = Config::load().unwrap();
4665 clear_warning(&mut reloaded);
4666 assert_eq!(loaded, reloaded);
4667
4668 Ok(())
4669 });
4670 }
4671
4672 #[test]
4673 fn test_model_checker_settings_with_bool_flags() {
4674 figment::Jail::expect_with(|jail| {
4675 jail.create_file(
4676 "foundry.toml",
4677 r"
4678 [profile.default]
4679
4680 [profile.default.model_checker]
4681 engine = 'chc'
4682 show_unproved = true
4683 show_unsupported = true
4684 show_proved_safe = false
4685 div_mod_with_slacks = true
4686 ",
4687 )?;
4688 let mut loaded = Config::load().unwrap();
4689 clear_warning(&mut loaded);
4690
4691 let mc = loaded.model_checker.as_ref().unwrap();
4692 assert_eq!(mc.show_unproved, Some(true));
4693 assert_eq!(mc.show_unsupported, Some(true));
4694 assert_eq!(mc.show_proved_safe, Some(false));
4695 assert_eq!(mc.div_mod_with_slacks, Some(true));
4696
4697 let s = loaded.to_string_pretty().unwrap();
4699 jail.create_file("foundry.toml", &s)?;
4700
4701 let mut reloaded = Config::load().unwrap();
4702 clear_warning(&mut reloaded);
4703
4704 let mc_reloaded = reloaded.model_checker.as_ref().unwrap();
4705 assert_eq!(mc_reloaded.show_unproved, Some(true));
4706 assert_eq!(mc_reloaded.show_unsupported, Some(true));
4707 assert_eq!(mc_reloaded.show_proved_safe, Some(false));
4708 assert_eq!(mc_reloaded.div_mod_with_slacks, Some(true));
4709
4710 Ok(())
4711 });
4712 }
4713
4714 #[test]
4715 fn test_model_checker_settings_relative_paths() {
4716 figment::Jail::expect_with(|jail| {
4717 jail.create_file(
4718 "foundry.toml",
4719 r"
4720 [profile.default]
4721
4722 [profile.default.model_checker]
4723 contracts = { 'a.sol' = [ 'A1', 'A2' ], 'b.sol' = [ 'B1', 'B2' ] }
4724 engine = 'chc'
4725 targets = [ 'assert', 'outOfBounds' ]
4726 timeout = 10000
4727 ",
4728 )?;
4729 let loaded = Config::load().unwrap().sanitized();
4730
4731 let dir = foundry_compilers::utils::canonicalize(jail.directory())
4736 .expect("Could not canonicalize jail path");
4737 assert_eq!(
4738 loaded.model_checker,
4739 Some(ModelCheckerSettings {
4740 contracts: BTreeMap::from([
4741 (
4742 format!("{}", dir.join("a.sol").display()),
4743 vec!["A1".to_string(), "A2".to_string()]
4744 ),
4745 (
4746 format!("{}", dir.join("b.sol").display()),
4747 vec!["B1".to_string(), "B2".to_string()]
4748 ),
4749 ]),
4750 engine: Some(ModelCheckerEngine::CHC),
4751 targets: Some(vec![
4752 ModelCheckerTarget::Assert,
4753 ModelCheckerTarget::OutOfBounds
4754 ]),
4755 timeout: Some(10000),
4756 invariants: None,
4757 show_unproved: None,
4758 div_mod_with_slacks: None,
4759 solvers: None,
4760 show_unsupported: None,
4761 show_proved_safe: None,
4762 })
4763 );
4764
4765 Ok(())
4766 });
4767 }
4768
4769 #[test]
4770 fn test_fmt_config() {
4771 figment::Jail::expect_with(|jail| {
4772 jail.create_file(
4773 "foundry.toml",
4774 r#"
4775 [fmt]
4776 line_length = 100
4777 tab_width = 2
4778 bracket_spacing = true
4779 style = "space"
4780 "#,
4781 )?;
4782 let loaded = Config::load().unwrap().sanitized();
4783 assert_eq!(
4784 loaded.fmt,
4785 FormatterConfig {
4786 line_length: 100,
4787 tab_width: 2,
4788 bracket_spacing: true,
4789 style: IndentStyle::Space,
4790 ..Default::default()
4791 }
4792 );
4793
4794 Ok(())
4795 });
4796 }
4797
4798 #[test]
4799 fn test_lint_config() {
4800 figment::Jail::expect_with(|jail| {
4801 jail.create_file(
4802 "foundry.toml",
4803 r"
4804 [lint]
4805 severity = ['high', 'medium']
4806 exclude_lints = ['incorrect-shift']
4807 ",
4808 )?;
4809 let loaded = Config::load().unwrap().sanitized();
4810 assert_eq!(
4811 loaded.lint,
4812 LinterConfig {
4813 severity: vec![LintSeverity::High, LintSeverity::Med],
4814 exclude_lints: vec!["incorrect-shift".into()],
4815 ..Default::default()
4816 }
4817 );
4818
4819 Ok(())
4820 });
4821 }
4822
4823 #[test]
4824 fn test_invariant_config() {
4825 figment::Jail::expect_with(|jail| {
4826 jail.create_file(
4827 "foundry.toml",
4828 r"
4829 [invariant]
4830 runs = 512
4831 depth = 10
4832 ",
4833 )?;
4834
4835 let loaded = Config::load().unwrap().sanitized();
4836 assert_eq!(
4837 loaded.invariant,
4838 InvariantConfig {
4839 runs: 512,
4840 depth: 10,
4841 failure_persist_dir: Some(PathBuf::from("cache/invariant")),
4842 ..Default::default()
4843 }
4844 );
4845
4846 Ok(())
4847 });
4848 }
4849
4850 #[test]
4851 fn test_standalone_sections_env() {
4852 figment::Jail::expect_with(|jail| {
4853 jail.create_file(
4854 "foundry.toml",
4855 r"
4856 [fuzz]
4857 runs = 100
4858
4859 [invariant]
4860 depth = 1
4861 ",
4862 )?;
4863
4864 jail.set_env("FOUNDRY_FMT_LINE_LENGTH", "95");
4865 jail.set_env("FOUNDRY_FUZZ_DICTIONARY_WEIGHT", "99");
4866 jail.set_env("FOUNDRY_INVARIANT_DEPTH", "5");
4867
4868 let config = Config::load().unwrap();
4869 assert_eq!(config.fmt.line_length, 95);
4870 assert_eq!(config.fuzz.dictionary.dictionary_weight, 99);
4871 assert_eq!(config.invariant.depth, 5);
4872
4873 Ok(())
4874 });
4875 }
4876
4877 #[test]
4878 fn test_parse_with_profile() {
4879 let foundry_str = r"
4880 [profile.default]
4881 src = 'src'
4882 out = 'out'
4883 libs = ['lib']
4884
4885 # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
4886 ";
4887 assert_eq!(
4888 parse_with_profile::<BasicConfig>(foundry_str).unwrap().unwrap(),
4889 (
4890 Config::DEFAULT_PROFILE,
4891 BasicConfig {
4892 profile: Config::DEFAULT_PROFILE,
4893 src: "src".into(),
4894 out: "out".into(),
4895 libs: vec!["lib".into()],
4896 remappings: vec![]
4897 }
4898 )
4899 );
4900 }
4901
4902 #[test]
4903 fn test_implicit_profile_loads() {
4904 figment::Jail::expect_with(|jail| {
4905 jail.create_file(
4906 "foundry.toml",
4907 r"
4908 [default]
4909 src = 'my-src'
4910 out = 'my-out'
4911 ",
4912 )?;
4913 let loaded = Config::load().unwrap().sanitized();
4914 assert_eq!(loaded.src.file_name().unwrap(), "my-src");
4915 assert_eq!(loaded.out.file_name().unwrap(), "my-out");
4916 assert_eq!(
4917 loaded.warnings,
4918 vec![Warning::UnknownSection {
4919 unknown_section: Profile::new("default"),
4920 source: Some("foundry.toml".into())
4921 }]
4922 );
4923
4924 Ok(())
4925 });
4926 }
4927
4928 #[test]
4929 fn test_etherscan_api_key() {
4930 figment::Jail::expect_with(|jail| {
4931 jail.create_file(
4932 "foundry.toml",
4933 r"
4934 [default]
4935 ",
4936 )?;
4937 jail.set_env("ETHERSCAN_API_KEY", "");
4938 let loaded = Config::load().unwrap().sanitized();
4939 assert!(loaded.etherscan_api_key.is_none());
4940
4941 jail.set_env("ETHERSCAN_API_KEY", "DUMMY");
4942 let loaded = Config::load().unwrap().sanitized();
4943 assert_eq!(loaded.etherscan_api_key, Some("DUMMY".into()));
4944
4945 Ok(())
4946 });
4947 }
4948
4949 #[test]
4950 fn test_etherscan_api_key_figment() {
4951 figment::Jail::expect_with(|jail| {
4952 jail.create_file(
4953 "foundry.toml",
4954 r"
4955 [default]
4956 etherscan_api_key = 'DUMMY'
4957 ",
4958 )?;
4959 jail.set_env("ETHERSCAN_API_KEY", "ETHER");
4960
4961 let figment = Config::figment_with_root(jail.directory())
4962 .merge(("etherscan_api_key", "USER_KEY"));
4963
4964 let loaded = Config::from_provider(figment).unwrap();
4965 assert_eq!(loaded.etherscan_api_key, Some("USER_KEY".into()));
4966
4967 Ok(())
4968 });
4969 }
4970
4971 #[test]
4972 fn test_normalize_defaults() {
4973 figment::Jail::expect_with(|jail| {
4974 jail.create_file(
4975 "foundry.toml",
4976 r"
4977 [default]
4978 solc = '0.8.13'
4979 ",
4980 )?;
4981
4982 let loaded = Config::load().unwrap().sanitized();
4983 assert_eq!(loaded.evm_version, EvmVersion::London);
4984 Ok(())
4985 });
4986 }
4987
4988 #[expect(clippy::disallowed_macros)]
4990 #[test]
4991 #[ignore]
4992 fn print_config() {
4993 let config = Config {
4994 optimizer_details: Some(OptimizerDetails {
4995 peephole: None,
4996 inliner: None,
4997 jumpdest_remover: None,
4998 order_literals: None,
4999 deduplicate: None,
5000 cse: None,
5001 constant_optimizer: Some(true),
5002 yul: Some(true),
5003 yul_details: Some(YulDetails {
5004 stack_allocation: None,
5005 optimizer_steps: Some("dhfoDgvulfnTUtnIf".to_string()),
5006 }),
5007 simple_counter_for_loop_unchecked_increment: None,
5008 }),
5009 ..Default::default()
5010 };
5011 println!("{}", config.to_string_pretty().unwrap());
5012 }
5013
5014 #[test]
5015 fn can_use_impl_figment_macro() {
5016 #[derive(Default, Serialize)]
5017 struct MyArgs {
5018 #[serde(skip_serializing_if = "Option::is_none")]
5019 root: Option<PathBuf>,
5020 }
5021 impl_figment_convert!(MyArgs);
5022
5023 impl Provider for MyArgs {
5024 fn metadata(&self) -> Metadata {
5025 Metadata::default()
5026 }
5027
5028 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
5029 let value = Value::serialize(self)?;
5030 let error = InvalidType(value.to_actual(), "map".into());
5031 let dict = value.into_dict().ok_or(error)?;
5032 Ok(Map::from([(Config::selected_profile(), dict)]))
5033 }
5034 }
5035
5036 let _figment: Figment = From::from(&MyArgs::default());
5037
5038 #[derive(Default)]
5039 struct Outer {
5040 start: MyArgs,
5041 other: MyArgs,
5042 another: MyArgs,
5043 }
5044 impl_figment_convert!(Outer, start, other, another);
5045
5046 let _figment: Figment = From::from(&Outer::default());
5047 }
5048
5049 #[test]
5050 fn list_cached_blocks() -> eyre::Result<()> {
5051 fn fake_block_cache(chain_path: &Path, block_number: &str, size_bytes: usize) {
5052 let block_path = chain_path.join(block_number);
5053 fs::create_dir(block_path.as_path()).unwrap();
5054 let file_path = block_path.join("storage.json");
5055 let mut file = File::create(file_path).unwrap();
5056 writeln!(file, "{}", vec![' '; size_bytes - 1].iter().collect::<String>()).unwrap();
5057 }
5058
5059 fn fake_block_cache_block_path_as_file(
5060 chain_path: &Path,
5061 block_number: &str,
5062 size_bytes: usize,
5063 ) {
5064 let block_path = chain_path.join(block_number);
5065 let mut file = File::create(block_path).unwrap();
5066 writeln!(file, "{}", vec![' '; size_bytes - 1].iter().collect::<String>()).unwrap();
5067 }
5068
5069 let chain_dir = tempdir()?;
5070
5071 fake_block_cache(chain_dir.path(), "1", 100);
5072 fake_block_cache(chain_dir.path(), "2", 500);
5073 fake_block_cache_block_path_as_file(chain_dir.path(), "3", 900);
5074 let mut pol_file = File::create(chain_dir.path().join("pol.txt")).unwrap();
5076 writeln!(pol_file, "{}", [' '; 10].iter().collect::<String>()).unwrap();
5077
5078 let result = Config::get_cached_blocks(chain_dir.path())?;
5079
5080 assert_eq!(result.len(), 3);
5081 let block1 = &result.iter().find(|x| x.0 == "1").unwrap();
5082 let block2 = &result.iter().find(|x| x.0 == "2").unwrap();
5083 let block3 = &result.iter().find(|x| x.0 == "3").unwrap();
5084
5085 assert_eq!(block1.0, "1");
5086 assert_eq!(block1.1, 100);
5087 assert_eq!(block2.0, "2");
5088 assert_eq!(block2.1, 500);
5089 assert_eq!(block3.0, "3");
5090 assert_eq!(block3.1, 900);
5091
5092 chain_dir.close()?;
5093 Ok(())
5094 }
5095
5096 #[test]
5097 fn list_etherscan_cache() -> eyre::Result<()> {
5098 fn fake_etherscan_cache(chain_path: &Path, address: &str, size_bytes: usize) {
5099 let metadata_path = chain_path.join("sources");
5100 let abi_path = chain_path.join("abi");
5101 let _ = fs::create_dir(metadata_path.as_path());
5102 let _ = fs::create_dir(abi_path.as_path());
5103
5104 let metadata_file_path = metadata_path.join(address);
5105 let mut metadata_file = File::create(metadata_file_path).unwrap();
5106 writeln!(metadata_file, "{}", vec![' '; size_bytes / 2 - 1].iter().collect::<String>())
5107 .unwrap();
5108
5109 let abi_file_path = abi_path.join(address);
5110 let mut abi_file = File::create(abi_file_path).unwrap();
5111 writeln!(abi_file, "{}", vec![' '; size_bytes / 2 - 1].iter().collect::<String>())
5112 .unwrap();
5113 }
5114
5115 let chain_dir = tempdir()?;
5116
5117 fake_etherscan_cache(chain_dir.path(), "1", 100);
5118 fake_etherscan_cache(chain_dir.path(), "2", 500);
5119
5120 let result = Config::get_cached_block_explorer_data(chain_dir.path())?;
5121
5122 assert_eq!(result, 600);
5123
5124 chain_dir.close()?;
5125 Ok(())
5126 }
5127
5128 #[test]
5129 fn test_parse_error_codes() {
5130 figment::Jail::expect_with(|jail| {
5131 jail.create_file(
5132 "foundry.toml",
5133 r#"
5134 [default]
5135 ignored_error_codes = ["license", "unreachable", 1337]
5136 "#,
5137 )?;
5138
5139 let config = Config::load().unwrap();
5140 assert_eq!(
5141 config.ignored_error_codes,
5142 vec![
5143 SolidityErrorCode::SpdxLicenseNotProvided,
5144 SolidityErrorCode::Unreachable,
5145 SolidityErrorCode::Other(1337)
5146 ]
5147 );
5148
5149 Ok(())
5150 });
5151 }
5152
5153 #[test]
5154 fn test_parse_file_paths() {
5155 figment::Jail::expect_with(|jail| {
5156 jail.create_file(
5157 "foundry.toml",
5158 r#"
5159 [default]
5160 ignored_warnings_from = ["something"]
5161 "#,
5162 )?;
5163
5164 let config = Config::load().unwrap();
5165 assert_eq!(config.ignored_file_paths, vec![Path::new("something").to_path_buf()]);
5166
5167 Ok(())
5168 });
5169 }
5170
5171 #[test]
5172 fn test_parse_optimizer_settings() {
5173 figment::Jail::expect_with(|jail| {
5174 jail.create_file(
5175 "foundry.toml",
5176 r"
5177 [default]
5178 [profile.default.optimizer_details]
5179 ",
5180 )?;
5181
5182 let config = Config::load().unwrap();
5183 assert_eq!(config.optimizer_details, Some(OptimizerDetails::default()));
5184
5185 Ok(())
5186 });
5187 }
5188
5189 #[test]
5190 fn test_parse_labels() {
5191 figment::Jail::expect_with(|jail| {
5192 jail.create_file(
5193 "foundry.toml",
5194 r#"
5195 [labels]
5196 0x1F98431c8aD98523631AE4a59f267346ea31F984 = "Uniswap V3: Factory"
5197 0xC36442b4a4522E871399CD717aBDD847Ab11FE88 = "Uniswap V3: Positions NFT"
5198 "#,
5199 )?;
5200
5201 let config = Config::load().unwrap();
5202 assert_eq!(
5203 config.labels,
5204 AddressHashMap::from_iter(vec![
5205 (
5206 address!("0x1F98431c8aD98523631AE4a59f267346ea31F984"),
5207 "Uniswap V3: Factory".to_string()
5208 ),
5209 (
5210 address!("0xC36442b4a4522E871399CD717aBDD847Ab11FE88"),
5211 "Uniswap V3: Positions NFT".to_string()
5212 ),
5213 ])
5214 );
5215
5216 Ok(())
5217 });
5218 }
5219
5220 #[test]
5221 fn test_parse_vyper() {
5222 figment::Jail::expect_with(|jail| {
5223 jail.create_file(
5224 "foundry.toml",
5225 r#"
5226 [vyper]
5227 optimize = "codesize"
5228 path = "/path/to/vyper"
5229 experimental_codegen = true
5230 "#,
5231 )?;
5232
5233 let config = Config::load().unwrap();
5234 assert_eq!(
5235 config.vyper,
5236 VyperConfig {
5237 optimize: Some(VyperOptimizationMode::Codesize),
5238 path: Some("/path/to/vyper".into()),
5239 experimental_codegen: Some(true),
5240 }
5241 );
5242
5243 Ok(())
5244 });
5245 }
5246
5247 #[test]
5248 fn test_parse_soldeer() {
5249 figment::Jail::expect_with(|jail| {
5250 jail.create_file(
5251 "foundry.toml",
5252 r#"
5253 [soldeer]
5254 remappings_generate = true
5255 remappings_regenerate = false
5256 remappings_version = true
5257 remappings_prefix = "@"
5258 remappings_location = "txt"
5259 recursive_deps = true
5260 "#,
5261 )?;
5262
5263 let config = Config::load().unwrap();
5264
5265 assert_eq!(
5266 config.soldeer,
5267 Some(SoldeerConfig {
5268 remappings_generate: true,
5269 remappings_regenerate: false,
5270 remappings_version: true,
5271 remappings_prefix: "@".to_string(),
5272 remappings_location: RemappingsLocation::Txt,
5273 recursive_deps: true,
5274 })
5275 );
5276
5277 Ok(())
5278 });
5279 }
5280
5281 #[test]
5283 fn test_resolve_mesc_by_chain_id() {
5284 let s = r#"{
5285 "mesc_version": "0.2.1",
5286 "default_endpoint": null,
5287 "endpoints": {
5288 "sophon_50104": {
5289 "name": "sophon_50104",
5290 "url": "https://rpc.sophon.xyz",
5291 "chain_id": "50104",
5292 "endpoint_metadata": {}
5293 }
5294 },
5295 "network_defaults": {
5296 },
5297 "network_names": {},
5298 "profiles": {
5299 "foundry": {
5300 "name": "foundry",
5301 "default_endpoint": "local_ethereum",
5302 "network_defaults": {
5303 "50104": "sophon_50104"
5304 },
5305 "profile_metadata": {},
5306 "use_mesc": true
5307 }
5308 },
5309 "global_metadata": {}
5310}"#;
5311
5312 let config = serde_json::from_str(s).unwrap();
5313 let endpoint = mesc::query::get_endpoint_by_network(&config, "50104", Some("foundry"))
5314 .unwrap()
5315 .unwrap();
5316 assert_eq!(endpoint.url, "https://rpc.sophon.xyz");
5317
5318 let s = r#"{
5319 "mesc_version": "0.2.1",
5320 "default_endpoint": null,
5321 "endpoints": {
5322 "sophon_50104": {
5323 "name": "sophon_50104",
5324 "url": "https://rpc.sophon.xyz",
5325 "chain_id": "50104",
5326 "endpoint_metadata": {}
5327 }
5328 },
5329 "network_defaults": {
5330 "50104": "sophon_50104"
5331 },
5332 "network_names": {},
5333 "profiles": {},
5334 "global_metadata": {}
5335}"#;
5336
5337 let config = serde_json::from_str(s).unwrap();
5338 let endpoint = mesc::query::get_endpoint_by_network(&config, "50104", Some("foundry"))
5339 .unwrap()
5340 .unwrap();
5341 assert_eq!(endpoint.url, "https://rpc.sophon.xyz");
5342 }
5343
5344 #[test]
5345 fn test_get_etherscan_config_with_unknown_chain() {
5346 figment::Jail::expect_with(|jail| {
5347 jail.create_file(
5348 "foundry.toml",
5349 r#"
5350 [etherscan]
5351 mainnet = { chain = 3658348, key = "api-key"}
5352 "#,
5353 )?;
5354 let config = Config::load().unwrap();
5355 let unknown_chain = Chain::from_id(3658348);
5356 let result = config.get_etherscan_config_with_chain(Some(unknown_chain));
5357 assert!(result.is_err());
5358 let error_msg = result.unwrap_err().to_string();
5359 assert!(error_msg.contains("No known Etherscan API URL for chain `3658348`"));
5360 assert!(error_msg.contains("Specify a `url`"));
5361 assert!(error_msg.contains("Verify the chain `3658348` is correct"));
5362
5363 Ok(())
5364 });
5365 }
5366
5367 #[test]
5368 fn test_get_etherscan_config_with_existing_chain_and_url() {
5369 figment::Jail::expect_with(|jail| {
5370 jail.create_file(
5371 "foundry.toml",
5372 r#"
5373 [etherscan]
5374 mainnet = { chain = 1, key = "api-key" }
5375 "#,
5376 )?;
5377 let config = Config::load().unwrap();
5378 let unknown_chain = Chain::from_id(1);
5379 let result = config.get_etherscan_config_with_chain(Some(unknown_chain));
5380 assert!(result.is_ok());
5381 Ok(())
5382 });
5383 }
5384
5385 #[test]
5386 fn test_can_inherit_a_base_toml() {
5387 figment::Jail::expect_with(|jail| {
5388 jail.create_file(
5390 "base-config.toml",
5391 r#"
5392 [profile.default]
5393 optimizer_runs = 800
5394
5395 [invariant]
5396 runs = 1000
5397
5398 [rpc_endpoints]
5399 mainnet = "https://example.com"
5400 optimism = "https://example-2.com/"
5401 "#,
5402 )?;
5403
5404 jail.create_file(
5406 "foundry.toml",
5407 r#"
5408 [profile.default]
5409 extends = "base-config.toml"
5410
5411 [invariant]
5412 runs = 333
5413 depth = 15
5414
5415 [rpc_endpoints]
5416 mainnet = "https://test.xyz/rpc"
5417 "#,
5418 )?;
5419
5420 let config = Config::load().unwrap();
5421 assert_eq!(config.extends, Some(Extends::Path("base-config.toml".to_string())));
5422
5423 assert_eq!(config.optimizer_runs, Some(800));
5425
5426 assert_eq!(config.invariant.runs, 333);
5428 assert_eq!(config.invariant.depth, 15);
5429
5430 let endpoints = config.rpc_endpoints.resolved();
5433 assert!(
5434 endpoints.get("mainnet").unwrap().url().unwrap().contains("https://test.xyz/rpc")
5435 );
5436 assert!(endpoints.get("optimism").unwrap().url().unwrap().contains("example-2.com"));
5437
5438 Ok(())
5439 });
5440 }
5441
5442 #[test]
5443 fn test_inheritance_validation() {
5444 figment::Jail::expect_with(|jail| {
5445 jail.create_file(
5447 "base-with-inherit.toml",
5448 r#"
5449 [profile.default]
5450 extends = "another.toml"
5451 optimizer_runs = 800
5452 "#,
5453 )?;
5454
5455 jail.create_file(
5456 "foundry.toml",
5457 r#"
5458 [profile.default]
5459 extends = "base-with-inherit.toml"
5460 "#,
5461 )?;
5462
5463 let result = Config::load();
5465 assert!(result.is_err());
5466 assert!(result.unwrap_err().to_string().contains("Nested inheritance is not allowed"));
5467
5468 jail.create_file(
5470 "foundry.toml",
5471 r#"
5472 [profile.default]
5473 extends = "foundry.toml"
5474 "#,
5475 )?;
5476
5477 let result = Config::load();
5478 assert!(result.is_err());
5479 assert!(result.unwrap_err().to_string().contains("cannot inherit from itself"));
5480
5481 jail.create_file(
5483 "foundry.toml",
5484 r#"
5485 [profile.default]
5486 extends = "non-existent.toml"
5487 "#,
5488 )?;
5489
5490 let result = Config::load();
5491 assert!(result.is_err());
5492 let err_msg = result.unwrap_err().to_string();
5493 assert!(
5494 err_msg.contains("does not exist")
5495 || err_msg.contains("Failed to resolve inherited config path"),
5496 "Error message: {err_msg}"
5497 );
5498
5499 Ok(())
5500 });
5501 }
5502
5503 #[test]
5504 fn test_complex_inheritance_merging() {
5505 figment::Jail::expect_with(|jail| {
5506 jail.create_file(
5508 "base.toml",
5509 r#"
5510 [profile.default]
5511 optimizer = true
5512 optimizer_runs = 1000
5513 via_ir = false
5514 solc = "0.8.19"
5515
5516 [invariant]
5517 runs = 500
5518 depth = 100
5519
5520 [fuzz]
5521 runs = 256
5522 seed = "0x123"
5523
5524 [rpc_endpoints]
5525 mainnet = "https://base-mainnet.com"
5526 optimism = "https://base-optimism.com"
5527 arbitrum = "https://base-arbitrum.com"
5528 "#,
5529 )?;
5530
5531 jail.create_file(
5533 "foundry.toml",
5534 r#"
5535 [profile.default]
5536 extends = "base.toml"
5537 optimizer_runs = 200 # Override
5538 via_ir = true # Override
5539 # optimizer and solc are inherited
5540
5541 [invariant]
5542 runs = 333 # Override
5543 # depth is inherited
5544
5545 # fuzz section is fully inherited
5546
5547 [rpc_endpoints]
5548 mainnet = "https://local-mainnet.com" # Override
5549 # optimism and arbitrum are inherited
5550 polygon = "https://local-polygon.com" # New
5551 "#,
5552 )?;
5553
5554 let config = Config::load().unwrap();
5555
5556 assert_eq!(config.optimizer, Some(true));
5558 assert_eq!(config.optimizer_runs, Some(200));
5559 assert_eq!(config.via_ir, true);
5560 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 19))));
5561
5562 assert_eq!(config.invariant.runs, 333);
5564 assert_eq!(config.invariant.depth, 100);
5565
5566 assert_eq!(config.fuzz.runs, 256);
5568 assert_eq!(config.fuzz.seed, Some(U256::from(0x123)));
5569
5570 let endpoints = config.rpc_endpoints.resolved();
5572 assert!(endpoints.get("mainnet").unwrap().url().unwrap().contains("local-mainnet"));
5573 assert!(endpoints.get("optimism").unwrap().url().unwrap().contains("base-optimism"));
5574 assert!(endpoints.get("arbitrum").unwrap().url().unwrap().contains("base-arbitrum"));
5575 assert!(endpoints.get("polygon").unwrap().url().unwrap().contains("local-polygon"));
5576
5577 Ok(())
5578 });
5579 }
5580
5581 #[test]
5582 fn test_inheritance_with_different_profiles() {
5583 figment::Jail::expect_with(|jail| {
5584 jail.create_file(
5586 "base.toml",
5587 r#"
5588 [profile.default]
5589 optimizer = true
5590 optimizer_runs = 200
5591
5592 [profile.ci]
5593 optimizer = true
5594 optimizer_runs = 10000
5595 via_ir = true
5596
5597 [profile.dev]
5598 optimizer = false
5599 "#,
5600 )?;
5601
5602 jail.create_file(
5604 "foundry.toml",
5605 r#"
5606 [profile.default]
5607 extends = "base.toml"
5608 verbosity = 3
5609
5610 [profile.ci]
5611 optimizer_runs = 5000 # This doesn't inherit from base.toml's ci profile
5612 "#,
5613 )?;
5614
5615 let config = Config::load().unwrap();
5617 assert_eq!(config.optimizer, Some(true));
5618 assert_eq!(config.optimizer_runs, Some(200));
5619 assert_eq!(config.verbosity, 3);
5620
5621 jail.set_env("FOUNDRY_PROFILE", "ci");
5623 let config = Config::load().unwrap();
5624 assert_eq!(config.optimizer_runs, Some(5000));
5625 assert_eq!(config.optimizer, Some(true));
5626 assert_eq!(config.via_ir, false);
5628
5629 Ok(())
5630 });
5631 }
5632
5633 #[test]
5634 fn test_inheritance_with_env_vars() {
5635 figment::Jail::expect_with(|jail| {
5636 jail.create_file(
5637 "base.toml",
5638 r#"
5639 [profile.default]
5640 optimizer_runs = 500
5641 sender = "0x0000000000000000000000000000000000000001"
5642 verbosity = 1
5643 "#,
5644 )?;
5645
5646 jail.create_file(
5647 "foundry.toml",
5648 r#"
5649 [profile.default]
5650 extends = "base.toml"
5651 verbosity = 2
5652 "#,
5653 )?;
5654
5655 jail.set_env("FOUNDRY_OPTIMIZER_RUNS", "999");
5657 jail.set_env("FOUNDRY_VERBOSITY", "4");
5658
5659 let config = Config::load().unwrap();
5660 assert_eq!(config.optimizer_runs, Some(999));
5661 assert_eq!(config.verbosity, 4);
5662 assert_eq!(
5663 config.sender,
5664 "0x0000000000000000000000000000000000000001"
5665 .parse::<alloy_primitives::Address>()
5666 .unwrap()
5667 );
5668
5669 Ok(())
5670 });
5671 }
5672
5673 #[test]
5674 fn test_inheritance_with_subdirectories() {
5675 figment::Jail::expect_with(|jail| {
5676 jail.create_dir("configs")?;
5678 jail.create_file(
5679 "configs/base.toml",
5680 r#"
5681 [profile.default]
5682 optimizer_runs = 800
5683 src = "contracts"
5684 "#,
5685 )?;
5686
5687 jail.create_file(
5689 "foundry.toml",
5690 r#"
5691 [profile.default]
5692 extends = "configs/base.toml"
5693 test = "tests"
5694 "#,
5695 )?;
5696
5697 let config = Config::load().unwrap();
5698 assert_eq!(config.optimizer_runs, Some(800));
5699 assert_eq!(config.src, PathBuf::from("contracts"));
5700 assert_eq!(config.test, PathBuf::from("tests"));
5701
5702 jail.create_dir("project")?;
5704 jail.create_file(
5705 "shared-base.toml",
5706 r#"
5707 [profile.default]
5708 optimizer_runs = 1500
5709 "#,
5710 )?;
5711
5712 jail.create_file(
5713 "project/foundry.toml",
5714 r#"
5715 [profile.default]
5716 extends = "../shared-base.toml"
5717 "#,
5718 )?;
5719
5720 std::env::set_current_dir(jail.directory().join("project")).unwrap();
5721 let config = Config::load().unwrap();
5722 assert_eq!(config.optimizer_runs, Some(1500));
5723
5724 Ok(())
5725 });
5726 }
5727
5728 #[test]
5729 fn test_inheritance_with_empty_files() {
5730 figment::Jail::expect_with(|jail| {
5731 jail.create_file(
5733 "base.toml",
5734 r#"
5735 [profile.default]
5736 "#,
5737 )?;
5738
5739 jail.create_file(
5740 "foundry.toml",
5741 r#"
5742 [profile.default]
5743 extends = "base.toml"
5744 optimizer_runs = 300
5745 "#,
5746 )?;
5747
5748 let config = Config::load().unwrap();
5749 assert_eq!(config.optimizer_runs, Some(300));
5750
5751 jail.create_file(
5753 "base2.toml",
5754 r#"
5755 [profile.default]
5756 optimizer_runs = 400
5757 via_ir = true
5758 "#,
5759 )?;
5760
5761 jail.create_file(
5762 "foundry.toml",
5763 r#"
5764 [profile.default]
5765 extends = "base2.toml"
5766 "#,
5767 )?;
5768
5769 let config = Config::load().unwrap();
5770 assert_eq!(config.optimizer_runs, Some(400));
5771 assert!(config.via_ir);
5772
5773 Ok(())
5774 });
5775 }
5776
5777 #[test]
5778 fn test_inheritance_array_and_table_merging() {
5779 figment::Jail::expect_with(|jail| {
5780 jail.create_file(
5781 "base.toml",
5782 r#"
5783 [profile.default]
5784 libs = ["lib", "node_modules"]
5785 ignored_error_codes = [5667, 1878]
5786 extra_output = ["metadata", "ir"]
5787
5788 [profile.default.model_checker]
5789 engine = "chc"
5790 timeout = 10000
5791 targets = ["assert"]
5792
5793 [profile.default.optimizer_details]
5794 peephole = true
5795 inliner = true
5796 "#,
5797 )?;
5798
5799 jail.create_file(
5800 "foundry.toml",
5801 r#"
5802 [profile.default]
5803 extends = "base.toml"
5804 libs = ["custom-lib"] # Concatenates with base array
5805 ignored_error_codes = [2018] # Concatenates with base array
5806
5807 [profile.default.model_checker]
5808 timeout = 5000 # Overrides base value
5809 # engine and targets are inherited
5810
5811 [profile.default.optimizer_details]
5812 jumpdest_remover = true # Adds new field
5813 # peephole and inliner are inherited
5814 "#,
5815 )?;
5816
5817 let config = Config::load().unwrap();
5818
5819 assert_eq!(
5821 config.libs,
5822 vec![
5823 PathBuf::from("lib"),
5824 PathBuf::from("node_modules"),
5825 PathBuf::from("custom-lib")
5826 ]
5827 );
5828 assert_eq!(
5829 config.ignored_error_codes,
5830 vec![
5831 SolidityErrorCode::UnusedFunctionParameter, SolidityErrorCode::SpdxLicenseNotProvided, SolidityErrorCode::FunctionStateMutabilityCanBeRestricted ]
5835 );
5836
5837 assert_eq!(config.model_checker.as_ref().unwrap().timeout, Some(5000));
5839 assert_eq!(
5840 config.model_checker.as_ref().unwrap().engine,
5841 Some(ModelCheckerEngine::CHC)
5842 );
5843 assert_eq!(
5844 config.model_checker.as_ref().unwrap().targets,
5845 Some(vec![ModelCheckerTarget::Assert])
5846 );
5847
5848 assert_eq!(config.optimizer_details.as_ref().unwrap().peephole, Some(true));
5850 assert_eq!(config.optimizer_details.as_ref().unwrap().inliner, Some(true));
5851 assert_eq!(config.optimizer_details.as_ref().unwrap().jumpdest_remover, None);
5852
5853 Ok(())
5854 });
5855 }
5856
5857 #[test]
5858 fn test_inheritance_with_special_sections() {
5859 figment::Jail::expect_with(|jail| {
5860 jail.create_file(
5861 "base.toml",
5862 r#"
5863 [profile.default]
5864 # Base file should not have 'extends' to avoid nested inheritance
5865
5866 [labels]
5867 "0x0000000000000000000000000000000000000001" = "Alice"
5868 "0x0000000000000000000000000000000000000002" = "Bob"
5869
5870 [[profile.default.fs_permissions]]
5871 access = "read"
5872 path = "./src"
5873
5874 [[profile.default.fs_permissions]]
5875 access = "read-write"
5876 path = "./cache"
5877 "#,
5878 )?;
5879
5880 jail.create_file(
5881 "foundry.toml",
5882 r#"
5883 [profile.default]
5884 extends = "base.toml"
5885
5886 [labels]
5887 "0x0000000000000000000000000000000000000002" = "Bob Updated"
5888 "0x0000000000000000000000000000000000000003" = "Charlie"
5889
5890 [[profile.default.fs_permissions]]
5891 access = "read"
5892 path = "./test"
5893 "#,
5894 )?;
5895
5896 let config = Config::load().unwrap();
5897
5898 assert_eq!(
5900 config.labels.get(
5901 &"0x0000000000000000000000000000000000000001"
5902 .parse::<alloy_primitives::Address>()
5903 .unwrap()
5904 ),
5905 Some(&"Alice".to_string())
5906 );
5907 assert_eq!(
5908 config.labels.get(
5909 &"0x0000000000000000000000000000000000000002"
5910 .parse::<alloy_primitives::Address>()
5911 .unwrap()
5912 ),
5913 Some(&"Bob Updated".to_string())
5914 );
5915 assert_eq!(
5916 config.labels.get(
5917 &"0x0000000000000000000000000000000000000003"
5918 .parse::<alloy_primitives::Address>()
5919 .unwrap()
5920 ),
5921 Some(&"Charlie".to_string())
5922 );
5923
5924 assert_eq!(config.fs_permissions.permissions.len(), 3); assert!(
5928 config
5929 .fs_permissions
5930 .permissions
5931 .iter()
5932 .any(|p| p.path.to_str().unwrap() == "./src")
5933 );
5934 assert!(
5935 config
5936 .fs_permissions
5937 .permissions
5938 .iter()
5939 .any(|p| p.path.to_str().unwrap() == "./cache")
5940 );
5941 assert!(
5942 config
5943 .fs_permissions
5944 .permissions
5945 .iter()
5946 .any(|p| p.path.to_str().unwrap() == "./test")
5947 );
5948
5949 Ok(())
5950 });
5951 }
5952
5953 #[test]
5954 fn test_inheritance_with_compilation_settings() {
5955 figment::Jail::expect_with(|jail| {
5956 jail.create_file(
5957 "base.toml",
5958 r#"
5959 [profile.default]
5960 solc = "0.8.19"
5961 evm_version = "paris"
5962 via_ir = false
5963 optimizer = true
5964 optimizer_runs = 200
5965
5966 [profile.default.optimizer_details]
5967 peephole = true
5968 inliner = false
5969 jumpdest_remover = true
5970 order_literals = false
5971 deduplicate = true
5972 cse = true
5973 constant_optimizer = true
5974 yul = true
5975
5976 [profile.default.optimizer_details.yul_details]
5977 stack_allocation = true
5978 optimizer_steps = "dhfoDgvulfnTUtnIf"
5979 "#,
5980 )?;
5981
5982 jail.create_file(
5983 "foundry.toml",
5984 r#"
5985 [profile.default]
5986 extends = "base.toml"
5987 evm_version = "shanghai" # Override
5988 optimizer_runs = 1000 # Override
5989
5990 [profile.default.optimizer_details]
5991 inliner = true # Override
5992 # Rest inherited
5993 "#,
5994 )?;
5995
5996 let config = Config::load().unwrap();
5997
5998 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 19))));
6000 assert_eq!(config.evm_version, EvmVersion::Shanghai);
6001 assert_eq!(config.via_ir, false);
6002 assert_eq!(config.optimizer, Some(true));
6003 assert_eq!(config.optimizer_runs, Some(1000));
6004
6005 let details = config.optimizer_details.as_ref().unwrap();
6007 assert_eq!(details.peephole, Some(true));
6008 assert_eq!(details.inliner, Some(true));
6009 assert_eq!(details.jumpdest_remover, None);
6010 assert_eq!(details.order_literals, None);
6011 assert_eq!(details.deduplicate, Some(true));
6012 assert_eq!(details.cse, Some(true));
6013 assert_eq!(details.constant_optimizer, None);
6014 assert_eq!(details.yul, Some(true));
6015
6016 if let Some(yul_details) = details.yul_details.as_ref() {
6018 assert_eq!(yul_details.stack_allocation, Some(true));
6019 assert_eq!(yul_details.optimizer_steps, Some("dhfoDgvulfnTUtnIf".to_string()));
6020 }
6021
6022 Ok(())
6023 });
6024 }
6025
6026 #[test]
6027 fn test_inheritance_with_remappings() {
6028 figment::Jail::expect_with(|jail| {
6029 jail.create_file(
6030 "base.toml",
6031 r#"
6032 [profile.default]
6033 remappings = [
6034 "forge-std/=lib/forge-std/src/",
6035 "@openzeppelin/=lib/openzeppelin-contracts/",
6036 "ds-test/=lib/ds-test/src/"
6037 ]
6038 auto_detect_remappings = false
6039 "#,
6040 )?;
6041
6042 jail.create_file(
6043 "foundry.toml",
6044 r#"
6045 [profile.default]
6046 extends = "base.toml"
6047 remappings = [
6048 "@custom/=lib/custom/",
6049 "ds-test/=lib/forge-std/lib/ds-test/src/" # Note: This will be added alongside base remappings
6050 ]
6051 "#,
6052 )?;
6053
6054 let config = Config::load().unwrap();
6055
6056 assert!(config.remappings.iter().any(|r| r.to_string().contains("@custom/")));
6058 assert!(config.remappings.iter().any(|r| r.to_string().contains("ds-test/")));
6059 assert!(config.remappings.iter().any(|r| r.to_string().contains("forge-std/")));
6060 assert!(config.remappings.iter().any(|r| r.to_string().contains("@openzeppelin/")));
6061
6062 assert!(!config.auto_detect_remappings);
6064
6065 Ok(())
6066 });
6067 }
6068
6069 #[test]
6070 fn test_inheritance_with_multiple_profiles_and_single_file() {
6071 figment::Jail::expect_with(|jail| {
6072 jail.create_file(
6074 "base.toml",
6075 r#"
6076 [profile.prod]
6077 optimizer = true
6078 optimizer_runs = 10000
6079 via_ir = true
6080
6081 [profile.test]
6082 optimizer = false
6083
6084 [profile.test.fuzz]
6085 runs = 100
6086 "#,
6087 )?;
6088
6089 jail.create_file(
6091 "foundry.toml",
6092 r#"
6093 [profile.prod]
6094 extends = "base.toml"
6095 evm_version = "shanghai" # Additional setting
6096
6097 [profile.test]
6098 extends = "base.toml"
6099
6100 [profile.test.fuzz]
6101 runs = 500 # Override
6102 "#,
6103 )?;
6104
6105 jail.set_env("FOUNDRY_PROFILE", "prod");
6107 let config = Config::load().unwrap();
6108 assert_eq!(config.optimizer, Some(true));
6109 assert_eq!(config.optimizer_runs, Some(10000));
6110 assert_eq!(config.via_ir, true);
6111 assert_eq!(config.evm_version, EvmVersion::Shanghai);
6112
6113 jail.set_env("FOUNDRY_PROFILE", "test");
6115 let config = Config::load().unwrap();
6116 assert_eq!(config.optimizer, Some(false));
6117 assert_eq!(config.fuzz.runs, 500);
6118
6119 Ok(())
6120 });
6121 }
6122
6123 #[test]
6124 fn test_inheritance_with_multiple_profiles_and_files() {
6125 figment::Jail::expect_with(|jail| {
6126 jail.create_file(
6127 "prod.toml",
6128 r#"
6129 [profile.prod]
6130 optimizer = true
6131 optimizer_runs = 20000
6132 gas_limit = 50000000
6133 "#,
6134 )?;
6135 jail.create_file(
6136 "dev.toml",
6137 r#"
6138 [profile.dev]
6139 optimizer = true
6140 optimizer_runs = 333
6141 gas_limit = 555555
6142 "#,
6143 )?;
6144
6145 jail.create_file(
6147 "foundry.toml",
6148 r#"
6149 [profile.dev]
6150 extends = "dev.toml"
6151 sender = "0x0000000000000000000000000000000000000001"
6152
6153 [profile.prod]
6154 extends = "prod.toml"
6155 sender = "0x0000000000000000000000000000000000000002"
6156 "#,
6157 )?;
6158
6159 jail.set_env("FOUNDRY_PROFILE", "dev");
6161 let config = Config::load().unwrap();
6162 assert_eq!(config.optimizer, Some(true));
6163 assert_eq!(config.optimizer_runs, Some(333));
6164 assert_eq!(config.gas_limit, 555555.into());
6165 assert_eq!(
6166 config.sender,
6167 "0x0000000000000000000000000000000000000001"
6168 .parse::<alloy_primitives::Address>()
6169 .unwrap()
6170 );
6171
6172 jail.set_env("FOUNDRY_PROFILE", "prod");
6174 let config = Config::load().unwrap();
6175 assert_eq!(config.optimizer, Some(true));
6176 assert_eq!(config.optimizer_runs, Some(20000));
6177 assert_eq!(config.gas_limit, 50000000.into());
6178 assert_eq!(
6179 config.sender,
6180 "0x0000000000000000000000000000000000000002"
6181 .parse::<alloy_primitives::Address>()
6182 .unwrap()
6183 );
6184
6185 Ok(())
6186 });
6187 }
6188
6189 #[test]
6190 fn test_extends_strategy_extend_arrays() {
6191 figment::Jail::expect_with(|jail| {
6192 jail.create_file(
6194 "base.toml",
6195 r#"
6196 [profile.default]
6197 libs = ["lib", "node_modules"]
6198 ignored_error_codes = [5667, 1878]
6199 optimizer_runs = 200
6200 "#,
6201 )?;
6202
6203 jail.create_file(
6205 "foundry.toml",
6206 r#"
6207 [profile.default]
6208 extends = "base.toml"
6209 libs = ["mylib", "customlib"]
6210 ignored_error_codes = [1234]
6211 optimizer_runs = 500
6212 "#,
6213 )?;
6214
6215 let config = Config::load().unwrap();
6216
6217 assert_eq!(config.libs.len(), 4);
6219 assert!(config.libs.iter().any(|l| l.to_str() == Some("lib")));
6220 assert!(config.libs.iter().any(|l| l.to_str() == Some("node_modules")));
6221 assert!(config.libs.iter().any(|l| l.to_str() == Some("mylib")));
6222 assert!(config.libs.iter().any(|l| l.to_str() == Some("customlib")));
6223
6224 assert_eq!(config.ignored_error_codes.len(), 3);
6225 assert!(
6226 config.ignored_error_codes.contains(&SolidityErrorCode::UnusedFunctionParameter)
6227 ); assert!(
6229 config.ignored_error_codes.contains(&SolidityErrorCode::SpdxLicenseNotProvided)
6230 ); assert!(config.ignored_error_codes.contains(&SolidityErrorCode::from(1234u64))); assert_eq!(config.optimizer_runs, Some(500));
6235
6236 Ok(())
6237 });
6238 }
6239
6240 #[test]
6241 fn test_extends_strategy_replace_arrays() {
6242 figment::Jail::expect_with(|jail| {
6243 jail.create_file(
6245 "base.toml",
6246 r#"
6247 [profile.default]
6248 libs = ["lib", "node_modules"]
6249 ignored_error_codes = [5667, 1878]
6250 optimizer_runs = 200
6251 "#,
6252 )?;
6253
6254 jail.create_file(
6256 "foundry.toml",
6257 r#"
6258 [profile.default]
6259 extends = { path = "base.toml", strategy = "replace-arrays" }
6260 libs = ["mylib", "customlib"]
6261 ignored_error_codes = [1234]
6262 optimizer_runs = 500
6263 "#,
6264 )?;
6265
6266 let config = Config::load().unwrap();
6267
6268 assert_eq!(config.libs.len(), 2);
6270 assert!(config.libs.iter().any(|l| l.to_str() == Some("mylib")));
6271 assert!(config.libs.iter().any(|l| l.to_str() == Some("customlib")));
6272 assert!(!config.libs.iter().any(|l| l.to_str() == Some("lib")));
6273 assert!(!config.libs.iter().any(|l| l.to_str() == Some("node_modules")));
6274
6275 assert_eq!(config.ignored_error_codes.len(), 1);
6276 assert!(config.ignored_error_codes.contains(&SolidityErrorCode::from(1234u64))); assert!(
6278 !config.ignored_error_codes.contains(&SolidityErrorCode::UnusedFunctionParameter)
6279 ); assert_eq!(config.optimizer_runs, Some(500));
6283
6284 Ok(())
6285 });
6286 }
6287
6288 #[test]
6289 fn test_extends_strategy_no_collision_success() {
6290 figment::Jail::expect_with(|jail| {
6291 jail.create_file(
6293 "base.toml",
6294 r#"
6295 [profile.default]
6296 optimizer = true
6297 optimizer_runs = 200
6298 src = "src"
6299 "#,
6300 )?;
6301
6302 jail.create_file(
6304 "foundry.toml",
6305 r#"
6306 [profile.default]
6307 extends = { path = "base.toml", strategy = "no-collision" }
6308 test = "tests"
6309 libs = ["lib"]
6310 "#,
6311 )?;
6312
6313 let config = Config::load().unwrap();
6314
6315 assert_eq!(config.optimizer, Some(true));
6317 assert_eq!(config.optimizer_runs, Some(200));
6318 assert_eq!(config.src, PathBuf::from("src"));
6319
6320 assert_eq!(config.test, PathBuf::from("tests"));
6322 assert_eq!(config.libs.len(), 1);
6323 assert!(config.libs.iter().any(|l| l.to_str() == Some("lib")));
6324
6325 Ok(())
6326 });
6327 }
6328
6329 #[test]
6330 fn test_extends_strategy_no_collision_error() {
6331 figment::Jail::expect_with(|jail| {
6332 jail.create_file(
6334 "base.toml",
6335 r#"
6336 [profile.default]
6337 optimizer = true
6338 optimizer_runs = 200
6339 libs = ["lib", "node_modules"]
6340 "#,
6341 )?;
6342
6343 jail.create_file(
6345 "foundry.toml",
6346 r#"
6347 [profile.default]
6348 extends = { path = "base.toml", strategy = "no-collision" }
6349 optimizer_runs = 500
6350 libs = ["mylib"]
6351 "#,
6352 )?;
6353
6354 let result = Config::load();
6356
6357 if let Ok(config) = result {
6358 panic!(
6359 "Expected error but got config with optimizer_runs: {:?}, libs: {:?}",
6360 config.optimizer_runs, config.libs
6361 );
6362 }
6363
6364 let err = result.unwrap_err();
6365 let err_str = err.to_string();
6366 assert!(
6367 err_str.contains("Key collision detected") || err_str.contains("collision"),
6368 "Error message doesn't mention collision: {err_str}"
6369 );
6370
6371 Ok(())
6372 });
6373 }
6374
6375 #[test]
6376 fn test_extends_both_syntaxes() {
6377 figment::Jail::expect_with(|jail| {
6378 jail.create_file(
6380 "base.toml",
6381 r#"
6382 [profile.default]
6383 libs = ["lib"]
6384 optimizer = true
6385 "#,
6386 )?;
6387
6388 jail.create_file(
6390 "foundry_string.toml",
6391 r#"
6392 [profile.default]
6393 extends = "base.toml"
6394 libs = ["custom"]
6395 "#,
6396 )?;
6397
6398 jail.create_file(
6400 "foundry_object.toml",
6401 r#"
6402 [profile.default]
6403 extends = { path = "base.toml", strategy = "replace-arrays" }
6404 libs = ["custom"]
6405 "#,
6406 )?;
6407
6408 jail.set_env("FOUNDRY_CONFIG", "foundry_string.toml");
6410 let config = Config::load().unwrap();
6411 assert_eq!(config.libs.len(), 2); assert!(config.libs.iter().any(|l| l.to_str() == Some("lib")));
6413 assert!(config.libs.iter().any(|l| l.to_str() == Some("custom")));
6414
6415 jail.set_env("FOUNDRY_CONFIG", "foundry_object.toml");
6417 let config = Config::load().unwrap();
6418 assert_eq!(config.libs.len(), 1); assert!(config.libs.iter().any(|l| l.to_str() == Some("custom")));
6420 assert!(!config.libs.iter().any(|l| l.to_str() == Some("lib")));
6421
6422 Ok(())
6423 });
6424 }
6425
6426 #[test]
6427 fn test_extends_strategy_default_is_extend_arrays() {
6428 figment::Jail::expect_with(|jail| {
6429 jail.create_file(
6431 "base.toml",
6432 r#"
6433 [profile.default]
6434 libs = ["lib", "node_modules"]
6435 optimizer = true
6436 "#,
6437 )?;
6438
6439 jail.create_file(
6441 "foundry.toml",
6442 r#"
6443 [profile.default]
6444 extends = "base.toml"
6445 libs = ["custom"]
6446 optimizer = false
6447 "#,
6448 )?;
6449
6450 let config = Config::load().unwrap();
6452
6453 assert_eq!(config.libs.len(), 3);
6455 assert!(config.libs.iter().any(|l| l.to_str() == Some("lib")));
6456 assert!(config.libs.iter().any(|l| l.to_str() == Some("node_modules")));
6457 assert!(config.libs.iter().any(|l| l.to_str() == Some("custom")));
6458
6459 assert_eq!(config.optimizer, Some(false));
6461
6462 Ok(())
6463 });
6464 }
6465
6466 #[test]
6467 fn test_deprecated_deny_warnings_is_handled() {
6468 figment::Jail::expect_with(|jail| {
6469 jail.create_file(
6470 "foundry.toml",
6471 r#"
6472 [profile.default]
6473 deny_warnings = true
6474 "#,
6475 )?;
6476 let config = Config::load().unwrap();
6477
6478 assert_eq!(config.deny, DenyLevel::Warnings);
6480 Ok(())
6481 });
6482 }
6483
6484 #[test]
6485 fn warns_on_unknown_keys_in_profile() {
6486 figment::Jail::expect_with(|jail| {
6487 jail.create_file(
6488 "foundry.toml",
6489 r#"
6490 [profile.default]
6491 unknown_key_xyz = 123
6492 "#,
6493 )?;
6494
6495 let cfg = Config::load().unwrap();
6496 assert!(cfg.warnings.iter().any(
6497 |w| matches!(w, crate::Warning::UnknownKey { key, .. } if key == "unknown_key_xyz")
6498 ));
6499 Ok(())
6500 });
6501 }
6502
6503 #[test]
6504 fn fails_on_ambiguous_version_in_compilation_restrictions() {
6505 figment::Jail::expect_with(|jail| {
6506 jail.create_file(
6507 "foundry.toml",
6508 r#"
6509 [profile.default]
6510 src = "src"
6511
6512 [[profile.default.compilation_restrictions]]
6513 paths = "src/*.sol"
6514 version = "0.8.11"
6515 "#,
6516 )?;
6517
6518 let err = Config::load().expect_err("expected bare version to fail");
6519 let err_msg = err.to_string();
6520 assert!(
6521 err_msg.contains("Invalid version format '0.8.11'")
6522 && err_msg.contains("Bare version numbers are ambiguous"),
6523 "Expected error about ambiguous version, got: {err_msg}"
6524 );
6525
6526 Ok(())
6527 });
6528 }
6529
6530 #[test]
6531 fn accepts_explicit_version_requirements() {
6532 figment::Jail::expect_with(|jail| {
6533 jail.create_file(
6534 "foundry.toml",
6535 r#"
6536 [profile.default]
6537 src = "src"
6538
6539 [[profile.default.compilation_restrictions]]
6540 paths = "src/*.sol"
6541 version = "=0.8.11"
6542
6543 [[profile.default.compilation_restrictions]]
6544 paths = "test/*.sol"
6545 version = ">=0.8.11"
6546 "#,
6547 )?;
6548
6549 let config = Config::load().expect("should accept explicit version requirements");
6550 assert_eq!(config.compilation_restrictions.len(), 2);
6551
6552 Ok(())
6553 });
6554 }
6555
6556 #[test]
6557 fn warns_on_unknown_keys_in_all_config_sections() {
6558 figment::Jail::expect_with(|jail| {
6559 jail.create_file(
6560 "foundry.toml",
6561 r#"
6562 [profile.default]
6563 src = "src"
6564 unknown_profile_key = "should_warn"
6565
6566 # Standalone sections with unknown keys
6567 [fmt]
6568 line_length = 120
6569 unknown_fmt_key = "should_warn"
6570
6571 [lint]
6572 severity = ["high"]
6573 unknown_lint_key = "should_warn"
6574
6575 [doc]
6576 out = "docs"
6577 unknown_doc_key = "should_warn"
6578
6579 [fuzz]
6580 runs = 256
6581 unknown_fuzz_key = "should_warn"
6582
6583 [invariant]
6584 runs = 256
6585 unknown_invariant_key = "should_warn"
6586
6587 [vyper]
6588 unknown_vyper_key = "should_warn"
6589
6590 [bind_json]
6591 out = "bindings.sol"
6592 unknown_bind_json_key = "should_warn"
6593
6594 # Nested profile sections with unknown keys
6595 [profile.default.fmt]
6596 line_length = 100
6597 unknown_nested_fmt_key = "should_warn"
6598
6599 [profile.default.lint]
6600 severity = ["low"]
6601 unknown_nested_lint_key = "should_warn"
6602
6603 [profile.default.doc]
6604 out = "documentation"
6605 unknown_nested_doc_key = "should_warn"
6606
6607 [profile.default.fuzz]
6608 runs = 512
6609 unknown_nested_fuzz_key = "should_warn"
6610
6611 [profile.default.invariant]
6612 runs = 512
6613 unknown_nested_invariant_key = "should_warn"
6614
6615 [profile.default.vyper]
6616 unknown_nested_vyper_key = "should_warn"
6617
6618 [profile.default.bind_json]
6619 out = "nested_bindings.sol"
6620 unknown_nested_bind_json_key = "should_warn"
6621
6622 # Array sections with unknown keys
6623 [[profile.default.compilation_restrictions]]
6624 paths = "src/*.sol"
6625 unknown_compilation_key = "should_warn"
6626
6627 [[profile.default.additional_compiler_profiles]]
6628 name = "via-ir"
6629 via_ir = true
6630 unknown_compiler_profile_key = "should_warn"
6631 "#,
6632 )?;
6633
6634 let cfg = Config::load().unwrap();
6635
6636 assert!(
6638 cfg.warnings.iter().any(|w| matches!(
6639 w,
6640 crate::Warning::UnknownKey { key, .. } if key == "unknown_profile_key"
6641 )),
6642 "Expected warning for 'unknown_profile_key' in profile, got: {:?}",
6643 cfg.warnings
6644 );
6645
6646 let standalone_expected = [
6648 ("unknown_fmt_key", "fmt"),
6649 ("unknown_lint_key", "lint"),
6650 ("unknown_doc_key", "doc"),
6651 ("unknown_fuzz_key", "fuzz"),
6652 ("unknown_invariant_key", "invariant"),
6653 ("unknown_vyper_key", "vyper"),
6654 ("unknown_bind_json_key", "bind_json"),
6655 ];
6656
6657 for (expected_key, expected_section) in standalone_expected {
6658 assert!(
6659 cfg.warnings.iter().any(|w| matches!(
6660 w,
6661 crate::Warning::UnknownSectionKey { key, section, .. }
6662 if key == expected_key && section == expected_section
6663 )),
6664 "Expected warning for '{}' in standalone section '{}', got: {:?}",
6665 expected_key,
6666 expected_section,
6667 cfg.warnings
6668 );
6669 }
6670
6671 let nested_expected = [
6673 ("unknown_nested_fmt_key", "fmt"),
6674 ("unknown_nested_lint_key", "lint"),
6675 ("unknown_nested_doc_key", "doc"),
6676 ("unknown_nested_fuzz_key", "fuzz"),
6677 ("unknown_nested_invariant_key", "invariant"),
6678 ("unknown_nested_vyper_key", "vyper"),
6679 ("unknown_nested_bind_json_key", "bind_json"),
6680 ];
6681
6682 for (expected_key, expected_section) in nested_expected {
6683 assert!(
6684 cfg.warnings.iter().any(|w| matches!(
6685 w,
6686 crate::Warning::UnknownSectionKey { key, section, .. }
6687 if key == expected_key && section == expected_section
6688 )),
6689 "Expected warning for '{}' in nested section '{}', got: {:?}",
6690 expected_key,
6691 expected_section,
6692 cfg.warnings
6693 );
6694 }
6695
6696 let array_expected = [
6698 ("unknown_compilation_key", "compilation_restrictions"),
6699 ("unknown_compiler_profile_key", "additional_compiler_profiles"),
6700 ];
6701
6702 for (expected_key, expected_section) in array_expected {
6703 assert!(
6704 cfg.warnings.iter().any(|w| matches!(
6705 w,
6706 crate::Warning::UnknownSectionKey { key, section, .. }
6707 if key == expected_key && section == expected_section
6708 )),
6709 "Expected warning for '{}' in array section '{}', got: {:?}",
6710 expected_key,
6711 expected_section,
6712 cfg.warnings
6713 );
6714 }
6715
6716 let unknown_key_warnings: Vec<_> = cfg
6718 .warnings
6719 .iter()
6720 .filter(|w| {
6721 matches!(w, crate::Warning::UnknownKey { .. })
6722 || matches!(w, crate::Warning::UnknownSectionKey { .. })
6723 })
6724 .collect();
6725
6726 assert_eq!(
6728 unknown_key_warnings.len(),
6729 17,
6730 "Expected 17 unknown key warnings (1 profile + 7 standalone + 7 nested + 2 array), got {}: {:?}",
6731 unknown_key_warnings.len(),
6732 unknown_key_warnings
6733 );
6734
6735 Ok(())
6736 });
6737 }
6738
6739 #[test]
6740 fn warns_on_unknown_keys_in_extended_config() {
6741 figment::Jail::expect_with(|jail| {
6742 jail.create_file(
6744 "base.toml",
6745 r#"
6746 [profile.default]
6747 optimizer_runs = 800
6748 unknown_base_profile_key = "should_warn"
6749
6750 [lint]
6751 severity = ["high"]
6752 unknown_base_lint_key = "should_warn"
6753
6754 [fmt]
6755 line_length = 100
6756 unknown_base_fmt_key = "should_warn"
6757 "#,
6758 )?;
6759
6760 jail.create_file(
6762 "foundry.toml",
6763 r#"
6764 [profile.default]
6765 extends = "base.toml"
6766 src = "src"
6767 unknown_local_profile_key = "should_warn"
6768
6769 [lint]
6770 unknown_local_lint_key = "should_warn"
6771
6772 [fuzz]
6773 runs = 512
6774 unknown_local_fuzz_key = "should_warn"
6775
6776 [[profile.default.compilation_restrictions]]
6777 paths = "src/*.sol"
6778 unknown_local_restriction_key = "should_warn"
6779 "#,
6780 )?;
6781
6782 let cfg = Config::load().unwrap();
6783
6784 assert_eq!(cfg.optimizer_runs, Some(800));
6786
6787 let expected_unknown_keys = ["unknown_base_profile_key", "unknown_local_profile_key"];
6795 for expected_key in expected_unknown_keys {
6796 assert!(
6797 cfg.warnings.iter().any(|w| matches!(
6798 w,
6799 crate::Warning::UnknownKey { key, .. } if key == expected_key
6800 )),
6801 "Expected warning for '{}', got: {:?}",
6802 expected_key,
6803 cfg.warnings
6804 );
6805 }
6806
6807 let expected_section_keys = [
6808 ("unknown_base_lint_key", "lint"),
6809 ("unknown_base_fmt_key", "fmt"),
6810 ("unknown_local_lint_key", "lint"),
6811 ("unknown_local_fuzz_key", "fuzz"),
6812 ("unknown_local_restriction_key", "compilation_restrictions"),
6813 ];
6814 for (expected_key, expected_section) in expected_section_keys {
6815 assert!(
6816 cfg.warnings.iter().any(|w| matches!(
6817 w,
6818 crate::Warning::UnknownSectionKey { key, section, .. }
6819 if key == expected_key && section == expected_section
6820 )),
6821 "Expected warning for '{}' in section '{}', got: {:?}",
6822 expected_key,
6823 expected_section,
6824 cfg.warnings
6825 );
6826 }
6827
6828 let unknown_warnings: Vec<_> = cfg
6830 .warnings
6831 .iter()
6832 .filter(|w| {
6833 matches!(w, crate::Warning::UnknownKey { .. })
6834 || matches!(w, crate::Warning::UnknownSectionKey { .. })
6835 })
6836 .collect();
6837 assert_eq!(
6838 unknown_warnings.len(),
6839 7,
6840 "Expected 7 unknown key warnings, got {}: {:?}",
6841 unknown_warnings.len(),
6842 unknown_warnings
6843 );
6844
6845 Ok(())
6846 });
6847 }
6848
6849 #[test]
6851 fn fails_on_unknown_profile() {
6852 figment::Jail::expect_with(|jail| {
6853 jail.create_file(
6854 "foundry.toml",
6855 r#"
6856 [profile.default]
6857 src = "src"
6858 "#,
6859 )?;
6860
6861 jail.set_env("FOUNDRY_PROFILE", "nonexistent");
6862 let err = Config::load().expect_err("expected unknown profile to fail");
6863 let err_msg = err.to_string();
6864 assert!(
6865 err_msg.contains("selected profile `nonexistent` does not exist"),
6866 "Expected error about nonexistent profile, got: {err_msg}"
6867 );
6868
6869 Ok(())
6870 });
6871 }
6872
6873 #[test]
6875 fn succeeds_on_known_profile() {
6876 figment::Jail::expect_with(|jail| {
6877 jail.create_file(
6878 "foundry.toml",
6879 r#"
6880 [profile.default]
6881 src = "src"
6882
6883 [profile.ci]
6884 src = "src"
6885 fuzz = { runs = 10000 }
6886 "#,
6887 )?;
6888
6889 jail.set_env("FOUNDRY_PROFILE", "ci");
6890 let config = Config::load().expect("known profile should work");
6891 assert_eq!(config.profile.as_str(), "ci");
6892 assert_eq!(config.fuzz.runs, 10000);
6893
6894 Ok(())
6895 });
6896 }
6897
6898 #[test]
6901 fn nested_lib_config_falls_back_to_default_profile() {
6902 figment::Jail::expect_with(|jail| {
6903 let lib_path = jail.directory().join("lib/mylib");
6905 std::fs::create_dir_all(&lib_path).unwrap();
6906 jail.create_file(
6907 "lib/mylib/foundry.toml",
6908 r#"
6909 [profile.default]
6910 src = "contracts"
6911 "#,
6912 )?;
6913
6914 jail.set_env("FOUNDRY_PROFILE", "ci");
6916
6917 let config = Config::load_with_root_and_fallback(&lib_path)
6919 .expect("lib config should load with fallback");
6920 assert_eq!(config.profile, Config::DEFAULT_PROFILE);
6921 assert_eq!(config.src.as_os_str(), "contracts");
6922
6923 Ok(())
6924 });
6925 }
6926
6927 #[test]
6929 fn nested_lib_config_uses_profile_if_exists() {
6930 figment::Jail::expect_with(|jail| {
6931 let lib_path = jail.directory().join("lib/mylib");
6933 std::fs::create_dir_all(&lib_path).unwrap();
6934 jail.create_file(
6935 "lib/mylib/foundry.toml",
6936 r#"
6937 [profile.default]
6938 src = "contracts"
6939
6940 [profile.ci]
6941 src = "contracts"
6942 fuzz = { runs = 5000 }
6943 "#,
6944 )?;
6945
6946 jail.set_env("FOUNDRY_PROFILE", "ci");
6948
6949 let config = Config::load_with_root_and_fallback(&lib_path)
6951 .expect("lib config should load with profile");
6952 assert_eq!(config.profile.as_str(), "ci");
6953 assert_eq!(config.fuzz.runs, 5000);
6954
6955 Ok(())
6956 });
6957 }
6958}