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