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