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, io,
49 path::{Path, PathBuf},
50 str::FromStr,
51};
52
53mod macros;
54
55pub mod utils;
56pub use foundry_evm_hardforks::{
57 ExecutionSpec, FoundryHardfork, FromEvmVersion, evm_spec_id, evm_spec_id_from_str,
58};
59pub use utils::*;
60
61mod endpoints;
62pub use endpoints::{
63 ResolvedRpcEndpoint, ResolvedRpcEndpoints, RpcEndpoint, RpcEndpointUrl, RpcEndpoints,
64 builtin_rpc_url,
65};
66
67mod etherscan;
68pub use etherscan::EtherscanConfigError;
69use etherscan::{EtherscanConfigs, EtherscanEnvProvider, ResolvedEtherscanConfig};
70
71pub mod resolve;
72pub use resolve::UnresolvedEnvVarError;
73
74pub mod cache;
75use cache::{Cache, ChainCache};
76
77pub mod fmt;
78pub use fmt::FormatterConfig;
79
80pub mod lint;
81pub use lint::{LinterConfig, Severity as LintSeverity};
82
83pub mod fs_permissions;
84pub use fs_permissions::FsPermissions;
85use fs_permissions::PathPermission;
86
87pub mod error;
88use error::ExtractConfigError;
89pub use error::SolidityErrorCode;
90
91pub mod doc;
92pub use doc::DocConfig;
93
94pub mod filter;
95pub use filter::SkipBuildFilters;
96
97mod warning;
98pub use warning::*;
99
100pub mod fix;
101
102pub use alloy_chains::{Chain, NamedChain};
104pub use figment;
105
106pub mod providers;
107pub use providers::Remappings;
108use providers::*;
109
110mod fuzz;
111pub use fuzz::{FuzzConfig, FuzzCorpusConfig, FuzzDictionaryConfig};
112
113mod invariant;
114pub use invariant::InvariantConfig;
115
116mod inline;
117pub use inline::{InlineConfig, InlineConfigError, NatSpec};
118
119pub mod soldeer;
120use soldeer::{SoldeerConfig, SoldeerDependencyConfig};
121
122mod vyper;
123pub use vyper::VyperConfig;
124
125mod bind_json;
126use bind_json::BindJsonConfig;
127
128mod compilation;
129pub use compilation::{CompilationRestrictions, SettingsOverrides};
130
131pub mod extend;
132use extend::Extends;
133
134use foundry_evm_networks::NetworkConfigs;
135pub use semver;
136
137#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
169pub struct Config {
170 #[serde(skip)]
177 pub profile: Profile,
178 #[serde(skip)]
182 pub profiles: Vec<Profile>,
183
184 #[serde(default = "root_default", skip_serializing)]
189 pub root: PathBuf,
190
191 #[serde(default, skip_serializing)]
196 pub extends: Option<Extends>,
197
198 pub src: PathBuf,
202 pub test: PathBuf,
204 pub script: PathBuf,
206 pub out: PathBuf,
208 pub libs: Vec<PathBuf>,
210 pub remappings: Vec<RelativeRemapping>,
212 pub auto_detect_remappings: bool,
214 pub libraries: Vec<String>,
216 pub cache: bool,
218 pub cache_path: PathBuf,
220 pub dynamic_test_linking: bool,
222 pub snapshots: PathBuf,
224 pub gas_snapshot_check: bool,
226 pub gas_snapshot_emit: bool,
228 pub broadcast: PathBuf,
230 pub allow_paths: Vec<PathBuf>,
232 pub include_paths: Vec<PathBuf>,
234 pub skip: Vec<GlobMatcher>,
236 pub force: bool,
238 #[serde(with = "from_str_lowercase")]
240 pub evm_version: EvmVersion,
241 pub hardfork: Option<FoundryHardfork>,
243 pub gas_reports: Vec<String>,
245 pub gas_reports_ignore: Vec<String>,
247 pub gas_reports_include_tests: bool,
249 #[doc(hidden)]
259 pub solc: Option<SolcReq>,
260 pub auto_detect_solc: bool,
262 pub offline: bool,
269 pub optimizer: Option<bool>,
271 pub optimizer_runs: Option<usize>,
282 pub optimizer_details: Option<OptimizerDetails>,
286 pub model_checker: Option<ModelCheckerSettings>,
288 pub verbosity: u8,
290 pub eth_rpc_url: Option<String>,
292 pub eth_rpc_accept_invalid_certs: bool,
294 pub eth_rpc_no_proxy: bool,
299 pub eth_rpc_jwt: Option<String>,
301 pub eth_rpc_timeout: Option<u64>,
303 pub eth_rpc_headers: Option<Vec<String>>,
312 pub eth_rpc_curl: bool,
314 pub etherscan_api_key: Option<String>,
316 #[serde(default, skip_serializing_if = "EtherscanConfigs::is_empty")]
318 pub etherscan: EtherscanConfigs,
319 pub ignored_error_codes: Vec<SolidityErrorCode>,
321 pub ignored_error_codes_from: Vec<(PathBuf, Vec<SolidityErrorCode>)>,
323 #[serde(rename = "ignored_warnings_from")]
325 pub ignored_file_paths: Vec<PathBuf>,
326 pub deny: DenyLevel,
328 #[serde(default, skip_serializing)]
330 pub deny_warnings: bool,
331 #[serde(rename = "match_test")]
333 pub test_pattern: Option<RegexWrapper>,
334 #[serde(rename = "no_match_test")]
336 pub test_pattern_inverse: Option<RegexWrapper>,
337 #[serde(rename = "match_contract")]
339 pub contract_pattern: Option<RegexWrapper>,
340 #[serde(rename = "no_match_contract")]
342 pub contract_pattern_inverse: Option<RegexWrapper>,
343 #[serde(rename = "match_path", with = "from_opt_glob")]
345 pub path_pattern: Option<globset::Glob>,
346 #[serde(rename = "no_match_path", with = "from_opt_glob")]
348 pub path_pattern_inverse: Option<globset::Glob>,
349 #[serde(rename = "no_match_coverage")]
351 pub coverage_pattern_inverse: Option<RegexWrapper>,
352 pub test_failures_file: PathBuf,
354 pub threads: Option<usize>,
356 pub show_progress: bool,
358 pub fuzz: FuzzConfig,
360 pub invariant: InvariantConfig,
362 pub ffi: bool,
364 pub live_logs: bool,
366 pub allow_internal_expect_revert: bool,
368 pub always_use_create_2_factory: bool,
370 pub prompt_timeout: u64,
372 pub sender: Address,
374 pub tx_origin: Address,
376 pub initial_balance: U256,
378 #[serde(
380 deserialize_with = "crate::deserialize_u64_to_u256",
381 serialize_with = "crate::serialize_u64_or_u256"
382 )]
383 pub block_number: U256,
384 pub fork_block_number: Option<u64>,
386 #[serde(rename = "chain_id", alias = "chain")]
388 pub chain: Option<Chain>,
389 pub gas_limit: GasLimit,
391 pub code_size_limit: Option<usize>,
393 pub gas_price: Option<u64>,
398 pub block_base_fee_per_gas: u64,
400 pub block_coinbase: Address,
402 #[serde(
404 deserialize_with = "crate::deserialize_u64_to_u256",
405 serialize_with = "crate::serialize_u64_or_u256"
406 )]
407 pub block_timestamp: U256,
408 pub block_difficulty: u64,
410 pub block_prevrandao: B256,
412 pub block_gas_limit: Option<GasLimit>,
414 pub memory_limit: u64,
419 #[serde(default)]
436 pub extra_output: Vec<ContractOutputSelection>,
437 #[serde(default)]
448 pub extra_output_files: Vec<ContractOutputSelection>,
449 pub names: bool,
451 pub sizes: bool,
453 pub via_ir: bool,
456 pub ast: bool,
458 pub rpc_storage_caching: StorageCachingConfig,
460 pub no_storage_caching: bool,
463 pub no_rpc_rate_limit: bool,
466 #[serde(default, skip_serializing_if = "RpcEndpoints::is_empty")]
468 pub rpc_endpoints: RpcEndpoints,
469 pub use_literal_content: bool,
471 #[serde(with = "from_str_lowercase")]
475 pub bytecode_hash: BytecodeHash,
476 pub cbor_metadata: bool,
481 #[serde(with = "serde_helpers::display_from_str_opt")]
483 pub revert_strings: Option<RevertStrings>,
484 pub sparse_mode: bool,
489 pub build_info: bool,
492 pub build_info_path: Option<PathBuf>,
494 pub fmt: FormatterConfig,
496 pub lint: LinterConfig,
498 pub doc: DocConfig,
500 pub bind_json: BindJsonConfig,
502 pub fs_permissions: FsPermissions,
506
507 pub isolate: bool,
511
512 pub disable_block_gas_limit: bool,
514
515 pub enable_tx_gas_limit: bool,
517
518 pub labels: AddressHashMap<String>,
520
521 pub unchecked_cheatcode_artifacts: bool,
524
525 pub create2_library_salt: B256,
527
528 pub create2_deployer: Address,
530
531 pub vyper: VyperConfig,
533
534 pub dependencies: Option<SoldeerDependencyConfig>,
536
537 pub soldeer: Option<SoldeerConfig>,
539
540 pub assertions_revert: bool,
544
545 pub legacy_assertions: bool,
547
548 #[serde(default, skip_serializing_if = "Vec::is_empty")]
550 pub extra_args: Vec<String>,
551
552 #[serde(flatten)]
554 pub networks: NetworkConfigs,
555
556 pub transaction_timeout: u64,
558
559 #[serde(rename = "__warnings", default, skip_serializing)]
561 pub warnings: Vec<Warning>,
562
563 #[serde(default)]
565 pub additional_compiler_profiles: Vec<SettingsOverrides>,
566
567 #[serde(default)]
569 pub compilation_restrictions: Vec<CompilationRestrictions>,
570
571 pub script_execution_protection: bool,
573
574 #[doc(hidden)]
583 #[serde(skip)]
584 pub _non_exhaustive: (),
585}
586
587#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum, Default, Serialize)]
589#[serde(rename_all = "lowercase")]
590pub enum DenyLevel {
591 #[default]
593 Never,
594 Warnings,
596 Notes,
598}
599
600impl<'de> Deserialize<'de> for DenyLevel {
603 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
604 where
605 D: Deserializer<'de>,
606 {
607 struct DenyLevelVisitor;
608
609 impl<'de> de::Visitor<'de> for DenyLevelVisitor {
610 type Value = DenyLevel;
611
612 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
613 formatter.write_str("one of the following strings: `never`, `warnings`, `notes`")
614 }
615
616 fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
617 where
618 E: de::Error,
619 {
620 Ok(DenyLevel::from(value))
621 }
622
623 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
624 where
625 E: de::Error,
626 {
627 DenyLevel::from_str(value).map_err(de::Error::custom)
628 }
629 }
630
631 deserializer.deserialize_any(DenyLevelVisitor)
632 }
633}
634
635impl FromStr for DenyLevel {
636 type Err = String;
637
638 fn from_str(s: &str) -> Result<Self, Self::Err> {
639 match s.to_lowercase().as_str() {
640 "warnings" | "warning" | "w" => Ok(Self::Warnings),
641 "notes" | "note" | "n" => Ok(Self::Notes),
642 "never" | "false" | "f" => Ok(Self::Never),
643 _ => Err(format!(
644 "unknown variant: found `{s}`, expected one of `never`, `warnings`, `notes`"
645 )),
646 }
647 }
648}
649
650impl From<bool> for DenyLevel {
651 fn from(deny: bool) -> Self {
652 if deny { Self::Warnings } else { Self::Never }
653 }
654}
655
656impl DenyLevel {
657 pub const fn warnings(&self) -> bool {
659 match self {
660 Self::Never => false,
661 Self::Warnings | Self::Notes => true,
662 }
663 }
664
665 pub const fn notes(&self) -> bool {
667 match self {
668 Self::Never | Self::Warnings => false,
669 Self::Notes => true,
670 }
671 }
672
673 pub const fn never(&self) -> bool {
675 match self {
676 Self::Never => true,
677 Self::Warnings | Self::Notes => false,
678 }
679 }
680}
681
682pub const STANDALONE_FALLBACK_SECTIONS: &[(&str, &str)] = &[("invariant", "fuzz")];
684
685pub const DEPRECATIONS: &[(&str, &str)] =
689 &[("cancun", "evm_version = Cancun"), ("deny_warnings", "deny = warnings")];
690
691impl Config {
692 pub const DEFAULT_PROFILE: Profile = Profile::Default;
694
695 pub const HARDHAT_PROFILE: Profile = Profile::const_new("hardhat");
697
698 pub const PROFILE_SECTION: &'static str = "profile";
700
701 pub const EXTERNAL_SECTION: &'static str = "external";
703
704 pub const STANDALONE_SECTIONS: &'static [&'static str] = &[
706 "rpc_endpoints",
707 "etherscan",
708 "fmt",
709 "lint",
710 "doc",
711 "fuzz",
712 "invariant",
713 "labels",
714 "dependencies",
715 "soldeer",
716 "vyper",
717 "bind_json",
718 ];
719
720 pub(crate) fn is_standalone_section<T: ?Sized + PartialEq<str>>(section: &T) -> bool {
721 section == Self::PROFILE_SECTION
722 || section == Self::EXTERNAL_SECTION
723 || Self::STANDALONE_SECTIONS.iter().any(|s| section == *s)
724 }
725
726 pub const FILE_NAME: &'static str = "foundry.toml";
728
729 pub const FOUNDRY_DIR_NAME: &'static str = ".foundry";
731
732 pub const DEFAULT_SENDER: Address = address!("0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38");
736
737 pub const DEFAULT_CREATE2_LIBRARY_SALT: FixedBytes<32> = FixedBytes::<32>::ZERO;
739
740 pub const DEFAULT_CREATE2_DEPLOYER: Address =
742 address!("0x4e59b44847b379578588920ca78fbf26c0b4956c");
743
744 pub fn load() -> Result<Self, ExtractConfigError> {
748 Self::from_provider(Self::figment())
749 }
750
751 pub fn load_with_providers(providers: FigmentProviders) -> Result<Self, ExtractConfigError> {
755 Self::from_provider(Self::default().to_figment(providers))
756 }
757
758 #[track_caller]
762 pub fn load_with_root(root: impl AsRef<Path>) -> Result<Self, ExtractConfigError> {
763 Self::from_provider(Self::figment_with_root(root.as_ref()))
764 }
765
766 #[track_caller]
773 pub fn load_with_root_and_fallback(root: impl AsRef<Path>) -> Result<Self, ExtractConfigError> {
774 let figment = Self::figment_with_root(root.as_ref());
775 Self::from_figment_fallback(Figment::from(figment))
776 }
777
778 #[doc(alias = "try_from")]
793 pub fn from_provider<T: Provider>(provider: T) -> Result<Self, ExtractConfigError> {
794 trace!("load config with provider: {:?}", provider.metadata());
795 Self::from_figment(Figment::from(provider))
796 }
797
798 pub fn merge_inline_provider<T: Provider>(&self, provider: T) -> Result<Self, Error> {
801 let mut config =
802 self.to_figment(FigmentProviders::None).merge(provider).extract::<Self>()?;
803 config.profile = self.profile.clone();
804 config.profiles = self.profiles.clone();
805 config.normalize_hardfork_settings()?;
806
807 Ok(config)
808 }
809
810 #[doc(hidden)]
811 #[deprecated(note = "use `Config::from_provider` instead")]
812 pub fn try_from<T: Provider>(provider: T) -> Result<Self, ExtractConfigError> {
813 Self::from_provider(provider)
814 }
815
816 fn from_figment(figment: Figment) -> Result<Self, ExtractConfigError> {
817 Self::from_figment_inner(figment, true)
818 }
819
820 fn from_figment_fallback(figment: Figment) -> Result<Self, ExtractConfigError> {
823 Self::from_figment_inner(figment, false)
824 }
825
826 fn from_figment_inner(
827 figment: Figment,
828 strict_profile: bool,
829 ) -> Result<Self, ExtractConfigError> {
830 let mut config = figment.extract::<Self>().map_err(ExtractConfigError::new)?;
831 let selected_profile = figment.profile().clone();
832
833 fn add_profile(profiles: &mut Vec<Profile>, profile: &Profile) {
835 if !profiles.contains(profile) {
836 profiles.push(profile.clone());
837 }
838 }
839 let figment = figment.select(Self::PROFILE_SECTION);
840 if let Ok(data) = figment.data()
841 && let Some(profiles) = data.get(&Profile::new(Self::PROFILE_SECTION))
842 {
843 for profile in profiles.keys() {
844 add_profile(&mut config.profiles, &Profile::new(profile));
845 }
846 }
847 add_profile(&mut config.profiles, &Self::DEFAULT_PROFILE);
848
849 if config.profiles.contains(&selected_profile) {
851 config.profile = selected_profile;
852 } else {
853 if strict_profile {
857 config
858 .warnings
859 .push(Warning::UnknownProfile { profile: selected_profile.to_string() });
860 }
861 config.profile = Self::DEFAULT_PROFILE;
862 }
863
864 config.normalize_optimizer_settings();
865 config.normalize_hardfork_settings().map_err(ExtractConfigError::new)?;
866
867 if let Some(runs) = config.optimizer_runs
869 && runs > u32::MAX as usize
870 {
871 return Err(ExtractConfigError::new(Error::from(format!(
872 "`optimizer_runs` value {} exceeds maximum allowed value of {}",
873 runs,
874 u32::MAX
875 ))));
876 }
877
878 Ok(config)
879 }
880
881 fn normalize_hardfork_settings(&mut self) -> Result<(), Error> {
882 let Some(hardfork) = self.hardfork else { return Ok(()) };
883 self.networks = self.networks.normalize_for_hardfork(hardfork).map_err(Error::from)?;
884 Ok(())
885 }
886
887 pub fn to_figment(&self, providers: FigmentProviders) -> Figment {
892 if providers.is_none() {
895 return Figment::from(self);
896 }
897
898 let root = self.root.as_path();
899 let profile = Self::selected_profile();
900 let mut figment = Figment::default().merge(DappHardhatDirProvider(root));
901
902 if let Some(global_toml) = Self::foundry_dir_toml().filter(|p| p.exists()) {
904 figment = Self::merge_toml_provider(
905 figment,
906 TomlFileProvider::new(None, global_toml),
907 profile.clone(),
908 );
909 }
910 figment = Self::merge_toml_provider(
912 figment,
913 TomlFileProvider::new(Some("FOUNDRY_CONFIG"), root.join(Self::FILE_NAME)),
914 profile.clone(),
915 );
916
917 figment = figment
919 .merge(
920 Env::prefixed("DAPP_")
921 .ignore(&["REMAPPINGS", "LIBRARIES", "FFI", "FS_PERMISSIONS"])
922 .global(),
923 )
924 .merge(
925 Env::prefixed("DAPP_TEST_")
926 .ignore(&["CACHE", "FUZZ_RUNS", "DEPTH", "FFI", "FS_PERMISSIONS"])
927 .global(),
928 )
929 .merge(DappEnvCompatProvider)
930 .merge(EtherscanEnvProvider::default())
931 .merge(
932 Env::prefixed("FOUNDRY_")
933 .ignore(&["PROFILE", "REMAPPINGS", "LIBRARIES", "FFI", "FS_PERMISSIONS"])
934 .map(|key| {
935 let key = key.as_str();
936 if Self::STANDALONE_SECTIONS.iter().any(|section| {
937 key.starts_with(&format!("{}_", section.to_ascii_uppercase()))
938 }) {
939 key.replacen('_', ".", 1).into()
940 } else {
941 key.into()
942 }
943 })
944 .global(),
945 )
946 .select(profile.clone());
947
948 if providers.is_all() {
950 let remappings = RemappingsProvider {
954 auto_detect_remappings: figment
955 .extract_inner::<bool>("auto_detect_remappings")
956 .unwrap_or(true),
957 lib_paths: figment
958 .extract_inner::<Vec<PathBuf>>("libs")
959 .map(Cow::Owned)
960 .unwrap_or_else(|_| Cow::Borrowed(&self.libs)),
961 root,
962 remappings: figment.extract_inner::<Vec<Remapping>>("remappings"),
963 };
964 figment = figment.merge(remappings);
965 }
966
967 figment = self.normalize_defaults(figment);
969
970 Figment::from(self).merge(figment).select(profile)
971 }
972
973 #[must_use]
978 pub fn canonic(self) -> Self {
979 let root = self.root.clone();
980 self.canonic_at(root)
981 }
982
983 #[must_use]
1001 pub fn canonic_at(mut self, root: impl Into<PathBuf>) -> Self {
1002 let root = canonic(root);
1003
1004 fn p(root: &Path, rem: &Path) -> PathBuf {
1005 canonic(root.join(rem))
1006 }
1007
1008 self.src = p(&root, &self.src);
1009 self.test = p(&root, &self.test);
1010 self.script = p(&root, &self.script);
1011 self.out = p(&root, &self.out);
1012 self.broadcast = p(&root, &self.broadcast);
1013 self.cache_path = p(&root, &self.cache_path);
1014 self.snapshots = p(&root, &self.snapshots);
1015 self.test_failures_file = p(&root, &self.test_failures_file);
1016
1017 if let Some(build_info_path) = self.build_info_path {
1018 self.build_info_path = Some(p(&root, &build_info_path));
1019 }
1020
1021 self.libs = self.libs.into_iter().map(|lib| p(&root, &lib)).collect();
1022
1023 self.remappings =
1024 self.remappings.into_iter().map(|r| RelativeRemapping::new(r.into(), &root)).collect();
1025
1026 self.allow_paths = self.allow_paths.into_iter().map(|allow| p(&root, &allow)).collect();
1027
1028 self.include_paths = self.include_paths.into_iter().map(|allow| p(&root, &allow)).collect();
1029
1030 self.fs_permissions.join_all(&root);
1031
1032 if let Some(model_checker) = &mut self.model_checker {
1033 model_checker.contracts = std::mem::take(&mut model_checker.contracts)
1034 .into_iter()
1035 .map(|(path, contracts)| {
1036 (format!("{}", p(&root, path.as_ref()).display()), contracts)
1037 })
1038 .collect();
1039 }
1040
1041 self
1042 }
1043
1044 pub fn normalized_evm_version(mut self) -> Self {
1046 self.normalize_evm_version();
1047 self
1048 }
1049
1050 pub const fn normalized_optimizer_settings(mut self) -> Self {
1053 self.normalize_optimizer_settings();
1054 self
1055 }
1056
1057 pub fn normalize_evm_version(&mut self) {
1059 self.evm_version = self.get_normalized_evm_version();
1060 }
1061
1062 pub const fn normalize_optimizer_settings(&mut self) {
1067 match (self.optimizer, self.optimizer_runs) {
1068 (None, None) => {
1070 self.optimizer = Some(false);
1071 self.optimizer_runs = Some(200);
1072 }
1073 (Some(_), None) => self.optimizer_runs = Some(200),
1075 (None, Some(runs)) => self.optimizer = Some(runs > 0),
1077 _ => {}
1078 }
1079 }
1080
1081 pub fn get_normalized_evm_version(&self) -> EvmVersion {
1083 if let Some(version) = self.solc_version()
1084 && let Some(evm_version) = self.evm_version.normalize_version_solc(&version)
1085 {
1086 return evm_version;
1087 }
1088 self.evm_version
1089 }
1090
1091 #[must_use]
1096 pub fn sanitized(self) -> Self {
1097 let mut config = self.canonic();
1098
1099 config.sanitize_remappings();
1100
1101 config.libs.sort_unstable();
1102 config.libs.dedup();
1103
1104 config
1105 }
1106
1107 #[allow(clippy::missing_const_for_fn)]
1111 pub fn sanitize_remappings(&mut self) {
1112 #[cfg(target_os = "windows")]
1113 {
1114 use path_slash::PathBufExt;
1116 self.remappings.iter_mut().for_each(|r| {
1117 r.path.path = r.path.path.to_slash_lossy().into_owned().into();
1118 });
1119 }
1120 }
1121
1122 pub fn install_lib_dir(&self) -> &Path {
1126 self.libs
1127 .iter()
1128 .find(|p| !p.ends_with("node_modules"))
1129 .map(|p| p.as_path())
1130 .unwrap_or_else(|| Path::new("lib"))
1131 }
1132
1133 pub fn project(&self) -> Result<Project<MultiCompiler>, SolcError> {
1148 self.create_project(self.cache, false)
1149 }
1150
1151 pub fn ephemeral_project(&self) -> Result<Project<MultiCompiler>, SolcError> {
1154 self.create_project(false, true)
1155 }
1156
1157 pub fn solar_project(&self) -> Result<Project<MultiCompiler>, SolcError> {
1161 let ui_testing = std::env::var_os("FOUNDRY_LINT_UI_TESTING").is_some();
1162 let mut project = self.create_project(self.cache && !ui_testing, false)?;
1163 project.update_output_selection(|selection| {
1164 *selection = OutputSelection::common_output_selection(["abi".into()]);
1167 });
1168 Ok(project)
1169 }
1170
1171 fn additional_settings(
1173 &self,
1174 base: &MultiCompilerSettings,
1175 ) -> BTreeMap<String, MultiCompilerSettings> {
1176 let mut map = BTreeMap::new();
1177
1178 for profile in &self.additional_compiler_profiles {
1179 let mut settings = base.clone();
1180 profile.apply(&mut settings);
1181 map.insert(profile.name.clone(), settings);
1182 }
1183
1184 map
1185 }
1186
1187 #[expect(clippy::disallowed_macros)]
1189 fn restrictions(
1190 &self,
1191 paths: &ProjectPathsConfig,
1192 ) -> Result<BTreeMap<PathBuf, RestrictionsWithVersion<MultiCompilerRestrictions>>, SolcError>
1193 {
1194 let mut map: BTreeMap<PathBuf, RestrictionsWithVersion<MultiCompilerRestrictions>> =
1195 BTreeMap::new();
1196 if self.compilation_restrictions.is_empty() {
1197 return Ok(BTreeMap::new());
1198 }
1199
1200 let graph = Graph::<MultiCompilerParser>::resolve(paths)?;
1201 let (sources, _) = graph.into_sources();
1202
1203 for res in &self.compilation_restrictions {
1204 for source in sources.keys().filter(|path| {
1205 if res.paths.is_match(path) {
1206 true
1207 } else if let Ok(path) = path.strip_prefix(&paths.root) {
1208 res.paths.is_match(path)
1209 } else {
1210 false
1211 }
1212 }) {
1213 let res: RestrictionsWithVersion<_> =
1214 res.clone().try_into().map_err(SolcError::msg)?;
1215 if map.contains_key(source) {
1216 let value = map.remove(source.as_path()).unwrap();
1217 if let Some(merged) = value.clone().merge(res) {
1218 map.insert(source.clone(), merged);
1219 } else {
1220 eprintln!(
1222 "{}",
1223 yansi::Paint::yellow(&format!(
1224 "Failed to merge compilation restrictions for {}",
1225 source.display()
1226 ))
1227 );
1228 map.insert(source.clone(), value);
1229 }
1230 } else {
1231 map.insert(source.clone(), res);
1232 }
1233 }
1234 }
1235
1236 Ok(map)
1237 }
1238
1239 pub fn create_project(&self, cached: bool, no_artifacts: bool) -> Result<Project, SolcError> {
1243 let settings = self.compiler_settings()?;
1244 let paths = self.project_paths();
1245
1246 let parse_path = |path: &PathBuf| path.strip_prefix("./").unwrap_or(path).to_path_buf();
1248
1249 let mut builder = Project::builder()
1250 .artifacts(self.configured_artifacts_handler())
1251 .additional_settings(self.additional_settings(&settings))
1252 .restrictions(self.restrictions(&paths)?)
1253 .settings(settings)
1254 .paths(paths)
1255 .ignore_error_codes(self.ignored_error_codes.iter().copied().map(Into::into))
1256 .ignore_error_codes_from(self.ignored_error_codes_from.iter().map(|(path, codes)| {
1257 (parse_path(path), codes.iter().copied().map(Into::into).collect())
1258 }))
1259 .ignore_paths(self.ignored_file_paths.iter().map(parse_path).collect())
1260 .set_compiler_severity_filter(if self.deny.warnings() {
1261 Severity::Warning
1262 } else {
1263 Severity::Error
1264 })
1265 .set_offline(self.offline)
1266 .set_cached(cached)
1267 .set_build_info(!no_artifacts && self.build_info)
1268 .set_no_artifacts(no_artifacts);
1269
1270 if !self.skip.is_empty() {
1271 let filter = SkipBuildFilters::new(self.skip.clone(), self.root.clone());
1272 builder = builder.sparse_output(filter);
1273 }
1274
1275 let project = builder.build(self.compiler()?)?;
1276
1277 if self.force {
1278 let _ = self.cleanup(&project);
1281 }
1282
1283 Ok(project)
1284 }
1285
1286 pub fn disable_optimizations(&self, project: &mut Project, ir_minimum: bool) {
1288 if ir_minimum {
1289 project.settings.solc.settings = std::mem::take(&mut project.settings.solc.settings)
1292 .with_via_ir_minimum_optimization();
1293
1294 let evm_version = project.settings.solc.evm_version;
1297 let version = self.solc_version().unwrap_or_else(|| Version::new(0, 8, 4));
1298 project.settings.solc.settings.sanitize(&version, SolcLanguage::Solidity);
1299 project.settings.solc.evm_version = evm_version;
1300 } else {
1301 project.settings.solc.optimizer.disable();
1302 project.settings.solc.optimizer.runs = None;
1303 project.settings.solc.optimizer.details = None;
1304 project.settings.solc.via_ir = None;
1305 }
1306 }
1307
1308 pub fn cleanup<C: Compiler, T: ArtifactOutput<CompilerContract = C::CompilerContract>>(
1313 &self,
1314 project: &Project<C, T>,
1315 ) -> Result<Vec<String>, SolcError> {
1316 let mut warnings = Vec::new();
1317
1318 if let Err(err) = project.cleanup() {
1319 warnings.push(format!("failed to clean project artifacts: {err}"));
1320 }
1321
1322 if let Err(err) = fs::remove_file(&self.test_failures_file)
1324 && err.kind() != io::ErrorKind::NotFound
1325 {
1326 warnings.push(format!(
1327 "failed to remove test failures file {}: {err}",
1328 self.test_failures_file.display()
1329 ));
1330 }
1331
1332 let mut remove_test_dir = |test_dir: &Option<PathBuf>| {
1334 if let Some(test_dir) = test_dir {
1335 let path = project.root().join(test_dir);
1336 if let Err(err) = fs::remove_dir_all(&path)
1337 && err.kind() != io::ErrorKind::NotFound
1338 {
1339 warnings.push(format!(
1340 "failed to remove test cache directory {}: {err}",
1341 path.display()
1342 ));
1343 }
1344 }
1345 };
1346 remove_test_dir(&self.fuzz.failure_persist_dir);
1347 remove_test_dir(&self.fuzz.corpus.corpus_dir);
1348 remove_test_dir(&self.invariant.corpus.corpus_dir);
1349 remove_test_dir(&self.invariant.failure_persist_dir);
1350
1351 Ok(warnings)
1352 }
1353
1354 fn ensure_solc(&self) -> Result<Option<Solc>, SolcError> {
1361 if let Some(solc) = &self.solc {
1362 let solc = match solc {
1363 SolcReq::Version(version) => {
1364 if let Some(solc) = Solc::find_svm_installed_version(version)? {
1365 solc
1366 } else {
1367 if self.offline {
1368 return Err(SolcError::msg(format!(
1369 "can't install missing solc {version} in offline mode"
1370 )));
1371 }
1372 Solc::blocking_install(version)?
1373 }
1374 }
1375 SolcReq::Local(solc) => {
1376 if !solc.is_file() {
1377 return Err(SolcError::msg(format!(
1378 "`solc` {} does not exist",
1379 solc.display()
1380 )));
1381 }
1382 Solc::new(solc)?
1383 }
1384 };
1385 return Ok(Some(solc));
1386 }
1387
1388 Ok(None)
1389 }
1390
1391 pub fn evm_spec_id<SPEC: FromEvmVersion>(&self) -> SPEC {
1393 self.hardfork.map(Into::into).unwrap_or_else(|| evm_spec_id(self.evm_version))
1394 }
1395
1396 pub const fn is_auto_detect(&self) -> bool {
1401 if self.solc.is_some() {
1402 return false;
1403 }
1404 self.auto_detect_solc
1405 }
1406
1407 pub fn enable_caching(&self, endpoint: &str, chain_id: impl Into<u64>) -> bool {
1409 !self.no_storage_caching
1410 && self.rpc_storage_caching.enable_for_chain_id(chain_id.into())
1411 && self.rpc_storage_caching.enable_for_endpoint(endpoint)
1412 }
1413
1414 pub fn project_paths<L>(&self) -> ProjectPathsConfig<L> {
1429 let mut builder = ProjectPathsConfig::builder()
1430 .cache(self.cache_path.join(SOLIDITY_FILES_CACHE_FILENAME))
1431 .sources(&self.src)
1432 .tests(&self.test)
1433 .scripts(&self.script)
1434 .artifacts(&self.out)
1435 .libs(self.libs.iter())
1436 .remappings(self.get_all_remappings())
1437 .allowed_path(&self.root)
1438 .allowed_paths(&self.libs)
1439 .allowed_paths(&self.allow_paths)
1440 .include_paths(&self.include_paths);
1441
1442 if let Some(build_info_path) = &self.build_info_path {
1443 builder = builder.build_infos(build_info_path);
1444 }
1445
1446 builder.build_with_root(&self.root)
1447 }
1448
1449 pub fn solc_compiler(&self) -> Result<SolcCompiler, SolcError> {
1451 if let Some(solc) = self.ensure_solc()? {
1452 Ok(SolcCompiler::Specific(solc))
1453 } else {
1454 Ok(SolcCompiler::AutoDetect)
1455 }
1456 }
1457
1458 pub fn solc_version(&self) -> Option<Version> {
1460 self.solc.as_ref().and_then(|solc| solc.try_version().ok())
1461 }
1462
1463 pub fn vyper_compiler(&self) -> Result<Option<Vyper>, SolcError> {
1465 if !self.project_paths::<VyperLanguage>().has_input_files() {
1467 return Ok(None);
1468 }
1469 let vyper = if let Some(path) = &self.vyper.path {
1470 Some(Vyper::new(path)?)
1471 } else {
1472 Vyper::new("vyper").ok()
1473 };
1474 Ok(vyper)
1475 }
1476
1477 pub fn compiler(&self) -> Result<MultiCompiler, SolcError> {
1479 Ok(MultiCompiler { solc: Some(self.solc_compiler()?), vyper: self.vyper_compiler()? })
1480 }
1481
1482 pub fn compiler_settings(&self) -> Result<MultiCompilerSettings, SolcError> {
1484 Ok(MultiCompilerSettings { solc: self.solc_settings()?, vyper: self.vyper_settings()? })
1485 }
1486
1487 pub fn get_all_remappings(&self) -> impl Iterator<Item = Remapping> + '_ {
1489 self.remappings.iter().map(|m| m.clone().into())
1490 }
1491
1492 pub fn get_rpc_jwt_secret(&self) -> Result<Option<Cow<'_, str>>, UnresolvedEnvVarError> {
1507 Ok(self.eth_rpc_jwt.as_ref().map(|jwt| Cow::Borrowed(jwt.as_str())))
1508 }
1509
1510 pub fn get_rpc_url(&self) -> Option<Result<Cow<'_, str>, UnresolvedEnvVarError>> {
1526 let maybe_alias = self.eth_rpc_url.as_deref()?;
1527 if let Some(alias) = self.get_rpc_url_with_alias(maybe_alias) {
1528 Some(alias)
1529 } else {
1530 Some(Ok(Cow::Borrowed(self.eth_rpc_url.as_deref()?)))
1531 }
1532 }
1533
1534 pub fn get_rpc_url_with_alias(
1559 &self,
1560 maybe_alias: &str,
1561 ) -> Option<Result<Cow<'_, str>, UnresolvedEnvVarError>> {
1562 let mut endpoints = self.rpc_endpoints.clone().resolved();
1563 if let Some(endpoint) = endpoints.remove(maybe_alias) {
1564 return Some(endpoint.url().map(Cow::Owned));
1565 }
1566
1567 if let Some(mesc_url) = self.get_rpc_url_from_mesc(maybe_alias) {
1568 return Some(Ok(Cow::Owned(mesc_url)));
1569 }
1570
1571 if let Some(builtin) = crate::endpoints::builtin_rpc_url(maybe_alias) {
1572 return Some(Ok(Cow::Borrowed(builtin)));
1573 }
1574
1575 None
1576 }
1577
1578 pub fn get_rpc_url_from_mesc(&self, maybe_alias: &str) -> Option<String> {
1580 let mesc_config = mesc::load::load_config_data()
1583 .inspect_err(|err| debug!(%err, "failed to load mesc config"))
1584 .ok()?;
1585
1586 if let Ok(Some(endpoint)) =
1587 mesc::query::get_endpoint_by_query(&mesc_config, maybe_alias, Some("foundry"))
1588 {
1589 return Some(endpoint.url);
1590 }
1591
1592 if maybe_alias.chars().all(|c| c.is_numeric()) {
1593 if let Ok(Some(endpoint)) =
1599 mesc::query::get_endpoint_by_network(&mesc_config, maybe_alias, Some("foundry"))
1600 {
1601 return Some(endpoint.url);
1602 }
1603 }
1604
1605 None
1606 }
1607
1608 pub fn get_rpc_url_or<'a>(
1620 &'a self,
1621 fallback: impl Into<Cow<'a, str>>,
1622 ) -> Result<Cow<'a, str>, UnresolvedEnvVarError> {
1623 if let Some(url) = self.get_rpc_url() { url } else { Ok(fallback.into()) }
1624 }
1625
1626 pub fn get_rpc_url_or_localhost_http(&self) -> Result<Cow<'_, str>, UnresolvedEnvVarError> {
1638 self.get_rpc_url_or("http://localhost:8545")
1639 }
1640
1641 pub fn get_etherscan_config(
1661 &self,
1662 ) -> Option<Result<ResolvedEtherscanConfig, EtherscanConfigError>> {
1663 self.get_etherscan_config_with_chain(None).transpose()
1664 }
1665
1666 pub fn get_etherscan_config_with_chain(
1673 &self,
1674 chain: Option<Chain>,
1675 ) -> Result<Option<ResolvedEtherscanConfig>, EtherscanConfigError> {
1676 if let Some(maybe_alias) = self.etherscan_api_key.as_ref().or(self.eth_rpc_url.as_ref())
1677 && self.etherscan.contains_key(maybe_alias)
1678 {
1679 return self.etherscan.clone().resolved().remove(maybe_alias).transpose();
1680 }
1681
1682 if let Some(res) = chain
1684 .or(self.chain)
1685 .and_then(|chain| self.etherscan.clone().resolved().find_chain(chain))
1686 {
1687 match (res, self.etherscan_api_key.as_ref()) {
1688 (Ok(mut config), Some(key)) => {
1689 config.key.clone_from(key);
1692 return Ok(Some(config));
1693 }
1694 (Ok(config), None) => return Ok(Some(config)),
1695 (Err(err), None) => return Err(err),
1696 (Err(_), Some(_)) => {
1697 }
1699 }
1700 }
1701
1702 if let Some(key) = self.etherscan_api_key.as_ref() {
1704 return Ok(ResolvedEtherscanConfig::create(
1705 key,
1706 chain.or(self.chain).unwrap_or_default(),
1707 ));
1708 }
1709 Ok(None)
1710 }
1711
1712 #[expect(clippy::disallowed_macros)]
1718 pub fn get_etherscan_api_key(&self, chain: Option<Chain>) -> Option<String> {
1719 self.get_etherscan_config_with_chain(chain)
1720 .map_err(|e| {
1721 eprintln!(
1723 "{}: failed getting etherscan config: {e}",
1724 yansi::Paint::yellow("Warning"),
1725 );
1726 })
1727 .ok()
1728 .flatten()
1729 .map(|c| c.key)
1730 }
1731
1732 pub fn get_source_dir_remapping(&self) -> Option<Remapping> {
1739 get_dir_remapping(&self.src)
1740 }
1741
1742 pub fn get_test_dir_remapping(&self) -> Option<Remapping> {
1744 if self.root.join(&self.test).exists() { get_dir_remapping(&self.test) } else { None }
1745 }
1746
1747 pub fn get_script_dir_remapping(&self) -> Option<Remapping> {
1749 if self.root.join(&self.script).exists() { get_dir_remapping(&self.script) } else { None }
1750 }
1751
1752 pub fn optimizer(&self) -> Optimizer {
1758 Optimizer {
1759 enabled: self.optimizer,
1760 runs: self.optimizer_runs,
1761 details: self.optimizer_details.clone(),
1764 }
1765 }
1766
1767 pub fn configured_artifacts_handler(&self) -> ConfigurableArtifacts {
1770 let mut extra_output = self.extra_output.clone();
1771
1772 if !extra_output.contains(&ContractOutputSelection::Metadata) {
1778 extra_output.push(ContractOutputSelection::Metadata);
1779 }
1780
1781 ConfigurableArtifacts::new(extra_output, self.extra_output_files.iter().copied())
1782 }
1783
1784 pub fn parsed_libraries(&self) -> Result<Libraries, SolcError> {
1787 Libraries::parse(&self.libraries)
1788 }
1789
1790 pub fn libraries_with_remappings(&self) -> Result<Libraries, SolcError> {
1792 let paths: ProjectPathsConfig = self.project_paths();
1793 Ok(self.parsed_libraries()?.apply(|libs| paths.apply_lib_remappings(libs)))
1794 }
1795
1796 pub fn solc_settings(&self) -> Result<SolcSettings, SolcError> {
1801 let mut model_checker = self.model_checker.clone();
1805 if let Some(model_checker_settings) = &mut model_checker
1806 && model_checker_settings.targets.is_none()
1807 {
1808 model_checker_settings.targets = Some(vec![ModelCheckerTarget::Assert]);
1809 }
1810
1811 let mut settings = Settings {
1812 libraries: self.libraries_with_remappings()?,
1813 optimizer: self.optimizer(),
1814 evm_version: Some(self.evm_version),
1815 metadata: Some(SettingsMetadata {
1816 use_literal_content: Some(self.use_literal_content),
1817 bytecode_hash: Some(self.bytecode_hash),
1818 cbor_metadata: Some(self.cbor_metadata),
1819 }),
1820 debug: self.revert_strings.map(|revert_strings| DebuggingSettings {
1821 revert_strings: Some(revert_strings),
1822 debug_info: Vec::new(),
1824 }),
1825 model_checker,
1826 via_ir: Some(self.via_ir),
1827 stop_after: None,
1829 remappings: Vec::new(),
1831 output_selection: Default::default(),
1833 }
1834 .with_extra_output(self.configured_artifacts_handler().output_selection());
1835
1836 if self.ast || self.build_info {
1838 settings = settings.with_ast();
1839 }
1840
1841 let cli_settings =
1842 CliSettings { extra_args: self.extra_args.clone(), ..Default::default() };
1843
1844 Ok(SolcSettings { settings, cli_settings })
1845 }
1846
1847 pub fn vyper_settings(&self) -> Result<VyperSettings, SolcError> {
1850 let optimize = if self.vyper.opt_level.is_some() { None } else { self.vyper.optimize };
1853
1854 Ok(VyperSettings {
1855 evm_version: Some(self.evm_version),
1856 optimize,
1857 opt_level: self.vyper.opt_level,
1858 bytecode_metadata: None,
1859 output_selection: OutputSelection::common_output_selection([
1862 "abi".to_string(),
1863 "evm.bytecode".to_string(),
1864 "evm.deployedBytecode".to_string(),
1865 ]),
1866 search_paths: None,
1867 experimental_codegen: self.vyper.experimental_codegen,
1868 debug: self.vyper.debug,
1869 enable_decimals: self.vyper.enable_decimals,
1870 venom_experimental: self.vyper.venom_experimental,
1871 venom: self.vyper.venom.clone(),
1872 })
1873 }
1874
1875 pub fn figment() -> Figment {
1896 Self::default().into()
1897 }
1898
1899 pub fn figment_with_root(root: impl AsRef<Path>) -> Figment {
1911 Self::with_root(root.as_ref()).into()
1912 }
1913
1914 #[doc(hidden)]
1915 #[track_caller]
1916 pub fn figment_with_root_opt(root: Option<&Path>) -> Figment {
1917 let root = match root {
1918 Some(root) => root,
1919 None => &find_project_root(None).expect("could not determine project root"),
1920 };
1921 Self::figment_with_root(root)
1922 }
1923
1924 pub fn with_root(root: impl AsRef<Path>) -> Self {
1933 Self::_with_root(root.as_ref())
1934 }
1935
1936 fn _with_root(root: &Path) -> Self {
1937 let paths = ProjectPathsConfig::builder().build_with_root::<()>(root);
1939 let artifacts: PathBuf = paths.artifacts.file_name().unwrap().into();
1940 Self {
1941 root: paths.root,
1942 src: paths.sources.file_name().unwrap().into(),
1943 out: artifacts.clone(),
1944 libs: paths.libraries.into_iter().map(|lib| lib.file_name().unwrap().into()).collect(),
1945 fs_permissions: FsPermissions::new([PathPermission::read(artifacts)]),
1946 ..Self::default()
1947 }
1948 }
1949
1950 pub fn hardhat() -> Self {
1952 Self {
1953 src: "contracts".into(),
1954 out: "artifacts".into(),
1955 libs: vec!["node_modules".into()],
1956 ..Self::default()
1957 }
1958 }
1959
1960 pub fn into_basic(self) -> BasicConfig {
1969 BasicConfig {
1970 profile: self.profile,
1971 src: self.src,
1972 out: self.out,
1973 libs: self.libs,
1974 remappings: self.remappings,
1975 network: self.networks.active_network_name().map(String::from),
1976 }
1977 }
1978
1979 pub fn update_at<F>(root: &Path, f: F) -> eyre::Result<()>
1984 where
1985 F: FnOnce(&Self, &mut toml_edit::DocumentMut) -> bool,
1986 {
1987 let config = Self::load_with_root(root)?.sanitized();
1988 config.update(|doc| f(&config, doc))
1989 }
1990
1991 pub fn update<F>(&self, f: F) -> eyre::Result<()>
1996 where
1997 F: FnOnce(&mut toml_edit::DocumentMut) -> bool,
1998 {
1999 let file_path = self.get_config_path();
2000 if !file_path.exists() {
2001 return Ok(());
2002 }
2003 let contents = fs::read_to_string(&file_path)?;
2004 let mut doc = contents.parse::<toml_edit::DocumentMut>()?;
2005 if f(&mut doc) {
2006 fs::write(file_path, doc.to_string())?;
2007 }
2008 Ok(())
2009 }
2010
2011 pub fn update_libs(&self) -> eyre::Result<()> {
2017 self.update(|doc| {
2018 let profile = self.profile.as_str().as_str();
2019 let root = &self.root;
2020 let libs: toml_edit::Value = self
2021 .libs
2022 .iter()
2023 .map(|path| {
2024 let path =
2025 if let Ok(relative) = path.strip_prefix(root) { relative } else { path };
2026 toml_edit::Value::from(&*path.to_string_lossy())
2027 })
2028 .collect();
2029 let libs = toml_edit::value(libs);
2030 doc[Self::PROFILE_SECTION][profile]["libs"] = libs;
2031 true
2032 })
2033 }
2034
2035 pub fn to_string_pretty(&self) -> Result<String, toml::ser::Error> {
2047 let mut value = toml::Value::try_from(self)?;
2049 let value_table = value.as_table_mut().unwrap();
2051 let standalone_sections = Self::STANDALONE_SECTIONS
2053 .iter()
2054 .filter_map(|section| {
2055 let section = section.to_string();
2056 value_table.remove(§ion).map(|value| (section, value))
2057 })
2058 .collect::<Vec<_>>();
2059 let mut wrapping_table = [(
2061 Self::PROFILE_SECTION.into(),
2062 toml::Value::Table([(self.profile.to_string(), value)].into_iter().collect()),
2063 )]
2064 .into_iter()
2065 .collect::<toml::map::Map<_, _>>();
2066 for (section, value) in standalone_sections {
2068 wrapping_table.insert(section, value);
2069 }
2070 toml::to_string_pretty(&toml::Value::Table(wrapping_table))
2072 }
2073
2074 pub fn get_config_path(&self) -> PathBuf {
2076 self.root.join(Self::FILE_NAME)
2077 }
2078
2079 pub fn selected_profile() -> Profile {
2083 #[cfg(test)]
2085 {
2086 Self::force_selected_profile()
2087 }
2088 #[cfg(not(test))]
2089 {
2090 static CACHE: std::sync::OnceLock<Profile> = std::sync::OnceLock::new();
2091 CACHE.get_or_init(Self::force_selected_profile).clone()
2092 }
2093 }
2094
2095 fn force_selected_profile() -> Profile {
2096 Profile::from_env_or("FOUNDRY_PROFILE", Self::DEFAULT_PROFILE)
2097 }
2098
2099 pub fn foundry_dir_toml() -> Option<PathBuf> {
2101 Self::foundry_dir().map(|p| p.join(Self::FILE_NAME))
2102 }
2103
2104 pub fn foundry_dir() -> Option<PathBuf> {
2106 dirs::home_dir().map(|p| p.join(Self::FOUNDRY_DIR_NAME))
2107 }
2108
2109 pub fn foundry_cache_dir() -> Option<PathBuf> {
2111 Self::foundry_dir().map(|p| p.join("cache"))
2112 }
2113
2114 pub fn foundry_rpc_cache_dir() -> Option<PathBuf> {
2116 Some(Self::foundry_cache_dir()?.join("rpc"))
2117 }
2118 pub fn foundry_chain_cache_dir(chain_id: impl Into<Chain>) -> Option<PathBuf> {
2120 Some(Self::foundry_rpc_cache_dir()?.join(chain_id.into().to_string()))
2121 }
2122
2123 pub fn foundry_etherscan_cache_dir() -> Option<PathBuf> {
2125 Some(Self::foundry_cache_dir()?.join("etherscan"))
2126 }
2127
2128 pub fn foundry_keystores_dir() -> Option<PathBuf> {
2130 Some(Self::foundry_dir()?.join("keystores"))
2131 }
2132
2133 pub fn foundry_etherscan_chain_cache_dir(chain_id: impl Into<Chain>) -> Option<PathBuf> {
2136 Some(Self::foundry_etherscan_cache_dir()?.join(chain_id.into().to_string()))
2137 }
2138
2139 pub fn foundry_block_cache_dir(chain_id: impl Into<Chain>, block: u64) -> Option<PathBuf> {
2142 Some(Self::foundry_chain_cache_dir(chain_id)?.join(format!("{block}")))
2143 }
2144
2145 pub fn foundry_block_cache_file(chain_id: impl Into<Chain>, block: u64) -> Option<PathBuf> {
2148 Some(Self::foundry_block_cache_dir(chain_id, block)?.join("storage.json"))
2149 }
2150
2151 pub fn data_dir() -> eyre::Result<PathBuf> {
2159 let path = dirs::data_dir().wrap_err("Failed to find data directory")?.join("foundry");
2160 std::fs::create_dir_all(&path).wrap_err("Failed to create module directory")?;
2161 Ok(path)
2162 }
2163
2164 pub fn find_config_file() -> Option<PathBuf> {
2171 fn find(path: &Path) -> Option<PathBuf> {
2172 if path.is_absolute() {
2173 return match path.is_file() {
2174 true => Some(path.to_path_buf()),
2175 false => None,
2176 };
2177 }
2178 let cwd = std::env::current_dir().ok()?;
2179 let mut cwd = cwd.as_path();
2180 loop {
2181 let file_path = cwd.join(path);
2182 if file_path.is_file() {
2183 return Some(file_path);
2184 }
2185 cwd = cwd.parent()?;
2186 }
2187 }
2188 find(Env::var_or("FOUNDRY_CONFIG", Self::FILE_NAME).as_ref())
2189 .or_else(|| Self::foundry_dir_toml().filter(|p| p.exists()))
2190 }
2191
2192 pub fn clean_foundry_cache() -> eyre::Result<Vec<String>> {
2196 if let Some(cache_dir) = Self::foundry_cache_dir() {
2197 let path = cache_dir.as_path();
2198 if let Err(err) = fs::remove_dir_all(path)
2199 && err.kind() != io::ErrorKind::NotFound
2200 {
2201 return Ok(vec![format!(
2202 "failed to remove foundry cache at {}: {err}",
2203 path.display()
2204 )]);
2205 }
2206 } else {
2207 eyre::bail!("failed to get foundry_cache_dir");
2208 }
2209
2210 Ok(vec![])
2211 }
2212
2213 pub fn clean_foundry_chain_cache(chain: Chain) -> eyre::Result<Vec<String>> {
2217 if let Some(cache_dir) = Self::foundry_chain_cache_dir(chain) {
2218 let path = cache_dir.as_path();
2219 if let Err(err) = fs::remove_dir_all(path)
2220 && err.kind() != io::ErrorKind::NotFound
2221 {
2222 return Ok(vec![format!(
2223 "failed to remove foundry cache for chain {chain} at {}: {err}",
2224 path.display()
2225 )]);
2226 }
2227 } else {
2228 eyre::bail!("failed to get foundry_chain_cache_dir");
2229 }
2230
2231 Ok(vec![])
2232 }
2233
2234 pub fn clean_foundry_block_cache(chain: Chain, block: u64) -> eyre::Result<Vec<String>> {
2238 if let Some(cache_dir) = Self::foundry_block_cache_dir(chain, block) {
2239 let path = cache_dir.as_path();
2240 if let Err(err) = fs::remove_dir_all(path)
2241 && err.kind() != io::ErrorKind::NotFound
2242 {
2243 return Ok(vec![format!(
2244 "failed to remove foundry cache for chain {chain} block {block} at {}: {err}",
2245 path.display()
2246 )]);
2247 }
2248 } else {
2249 eyre::bail!("failed to get foundry_block_cache_dir");
2250 }
2251
2252 Ok(vec![])
2253 }
2254
2255 pub fn clean_foundry_etherscan_cache() -> eyre::Result<Vec<String>> {
2259 if let Some(cache_dir) = Self::foundry_etherscan_cache_dir() {
2260 let path = cache_dir.as_path();
2261 if let Err(err) = fs::remove_dir_all(path)
2262 && err.kind() != io::ErrorKind::NotFound
2263 {
2264 return Ok(vec![format!(
2265 "failed to remove foundry etherscan cache at {}: {err}",
2266 path.display()
2267 )]);
2268 }
2269 } else {
2270 eyre::bail!("failed to get foundry_etherscan_cache_dir");
2271 }
2272
2273 Ok(vec![])
2274 }
2275
2276 pub fn clean_foundry_etherscan_chain_cache(chain: Chain) -> eyre::Result<Vec<String>> {
2280 if let Some(cache_dir) = Self::foundry_etherscan_chain_cache_dir(chain) {
2281 let path = cache_dir.as_path();
2282 if let Err(err) = fs::remove_dir_all(path)
2283 && err.kind() != io::ErrorKind::NotFound
2284 {
2285 return Ok(vec![format!(
2286 "failed to remove foundry etherscan cache for chain {chain} at {}: {err}",
2287 path.display()
2288 )]);
2289 }
2290 } else {
2291 eyre::bail!("failed to get foundry_etherscan_cache_dir for chain: {}", chain);
2292 }
2293
2294 Ok(vec![])
2295 }
2296
2297 pub fn list_foundry_cache() -> eyre::Result<Cache> {
2299 if let Some(cache_dir) = Self::foundry_rpc_cache_dir() {
2300 let mut cache = Cache { chains: vec![] };
2301 if !cache_dir.exists() {
2302 return Ok(cache);
2303 }
2304 if let Ok(entries) = cache_dir.as_path().read_dir() {
2305 for entry in entries.flatten().filter(|x| x.path().is_dir()) {
2306 if let Ok(chain) = Chain::from_str(&entry.file_name().to_string_lossy()) {
2307 cache.chains.push(Self::list_foundry_chain_cache(chain)?);
2308 }
2309 }
2310 Ok(cache)
2311 } else {
2312 eyre::bail!("failed to access foundry_cache_dir");
2313 }
2314 } else {
2315 eyre::bail!("failed to get foundry_cache_dir");
2316 }
2317 }
2318
2319 pub fn list_foundry_chain_cache(chain: Chain) -> eyre::Result<ChainCache> {
2321 let block_explorer_data_size = match Self::foundry_etherscan_chain_cache_dir(chain) {
2322 Some(cache_dir) => Self::get_cached_block_explorer_data(&cache_dir)?,
2323 None => {
2324 warn!("failed to access foundry_etherscan_chain_cache_dir");
2325 0
2326 }
2327 };
2328
2329 if let Some(cache_dir) = Self::foundry_chain_cache_dir(chain) {
2330 let blocks = Self::get_cached_blocks(&cache_dir)?;
2331 Ok(ChainCache {
2332 name: chain.to_string(),
2333 blocks,
2334 block_explorer: block_explorer_data_size,
2335 })
2336 } else {
2337 eyre::bail!("failed to get foundry_chain_cache_dir");
2338 }
2339 }
2340
2341 fn get_cached_blocks(chain_path: &Path) -> eyre::Result<Vec<(String, u64)>> {
2343 let mut blocks = vec![];
2344 if !chain_path.exists() {
2345 return Ok(blocks);
2346 }
2347 for block in chain_path.read_dir()?.flatten() {
2348 let file_type = block.file_type()?;
2349 let file_name = block.file_name();
2350 let filepath = if file_type.is_dir() {
2351 block.path().join("storage.json")
2352 } else if file_type.is_file()
2353 && file_name.to_string_lossy().chars().all(char::is_numeric)
2354 {
2355 block.path()
2356 } else {
2357 continue;
2358 };
2359 blocks.push((file_name.to_string_lossy().into_owned(), fs::metadata(filepath)?.len()));
2360 }
2361 Ok(blocks)
2362 }
2363
2364 fn get_cached_block_explorer_data(chain_path: &Path) -> eyre::Result<u64> {
2366 if !chain_path.exists() {
2367 return Ok(0);
2368 }
2369
2370 fn dir_size_recursive(mut dir: fs::ReadDir) -> eyre::Result<u64> {
2371 dir.try_fold(0, |acc, file| {
2372 let file = file?;
2373 let size = match file.metadata()? {
2374 data if data.is_dir() => dir_size_recursive(fs::read_dir(file.path())?)?,
2375 data => data.len(),
2376 };
2377 Ok(acc + size)
2378 })
2379 }
2380
2381 dir_size_recursive(fs::read_dir(chain_path)?)
2382 }
2383
2384 fn merge_toml_provider(
2385 mut figment: Figment,
2386 toml_provider: impl Provider,
2387 profile: Profile,
2388 ) -> Figment {
2389 figment = figment.select(profile.clone());
2390
2391 figment = {
2393 let warnings = WarningsProvider::for_figment(&toml_provider, &figment);
2394 figment.merge(warnings)
2395 };
2396
2397 let mut profiles = vec![Self::DEFAULT_PROFILE];
2399 if profile != Self::DEFAULT_PROFILE {
2400 profiles.push(profile.clone());
2401 }
2402 let provider = toml_provider.strict_select(profiles);
2403
2404 let provider = &BackwardsCompatTomlProvider(ForcedSnakeCaseData(provider));
2406
2407 if profile != Self::DEFAULT_PROFILE {
2409 figment = figment.merge(provider.rename(Self::DEFAULT_PROFILE, profile.clone()));
2410 }
2411 for standalone_key in Self::STANDALONE_SECTIONS {
2413 if let Some((_, fallback)) =
2414 STANDALONE_FALLBACK_SECTIONS.iter().find(|(key, _)| standalone_key == key)
2415 {
2416 figment = figment.merge(
2417 provider
2418 .fallback(standalone_key, fallback)
2419 .wrap(profile.clone(), standalone_key),
2420 );
2421 } else {
2422 figment = figment.merge(provider.wrap(profile.clone(), standalone_key));
2423 }
2424 }
2425 figment = figment.merge(provider);
2427 figment
2428 }
2429
2430 fn normalize_defaults(&self, mut figment: Figment) -> Figment {
2436 if figment.contains("evm_version") {
2437 return figment;
2438 }
2439
2440 if let Ok(solc) = figment.extract_inner::<SolcReq>("solc")
2442 && let Some(version) = solc
2443 .try_version()
2444 .ok()
2445 .and_then(|version| self.evm_version.normalize_version_solc(&version))
2446 {
2447 figment = figment.merge(("evm_version", version));
2448 }
2449
2450 if figment.extract_inner::<bool>("deny_warnings").unwrap_or(false)
2452 && figment.extract_inner("deny") == Ok(DenyLevel::Never)
2453 {
2454 figment = figment.merge(("deny", DenyLevel::Warnings));
2455 }
2456
2457 figment
2458 }
2459}
2460
2461impl From<Config> for Figment {
2462 fn from(c: Config) -> Self {
2463 (&c).into()
2464 }
2465}
2466impl From<&Config> for Figment {
2467 fn from(c: &Config) -> Self {
2468 c.to_figment(FigmentProviders::All)
2469 }
2470}
2471
2472#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2474pub enum FigmentProviders {
2475 #[default]
2477 All,
2478 Cast,
2482 Anvil,
2486 None,
2488}
2489
2490impl FigmentProviders {
2491 pub const fn is_all(&self) -> bool {
2493 matches!(self, Self::All)
2494 }
2495
2496 pub const fn is_cast(&self) -> bool {
2498 matches!(self, Self::Cast)
2499 }
2500
2501 pub const fn is_anvil(&self) -> bool {
2503 matches!(self, Self::Anvil)
2504 }
2505
2506 pub const fn is_none(&self) -> bool {
2508 matches!(self, Self::None)
2509 }
2510}
2511
2512#[derive(Clone, Debug, Serialize, Deserialize)]
2514#[serde(transparent)]
2515pub struct RegexWrapper {
2516 #[serde(with = "serde_regex")]
2517 inner: regex::Regex,
2518}
2519
2520impl std::ops::Deref for RegexWrapper {
2521 type Target = regex::Regex;
2522
2523 fn deref(&self) -> &Self::Target {
2524 &self.inner
2525 }
2526}
2527
2528impl std::cmp::PartialEq for RegexWrapper {
2529 fn eq(&self, other: &Self) -> bool {
2530 self.as_str() == other.as_str()
2531 }
2532}
2533
2534impl Eq for RegexWrapper {}
2535
2536impl From<RegexWrapper> for regex::Regex {
2537 fn from(wrapper: RegexWrapper) -> Self {
2538 wrapper.inner
2539 }
2540}
2541
2542impl From<regex::Regex> for RegexWrapper {
2543 fn from(re: Regex) -> Self {
2544 Self { inner: re }
2545 }
2546}
2547
2548mod serde_regex {
2549 use regex::Regex;
2550 use serde::{Deserialize, Deserializer, Serializer};
2551
2552 pub(crate) fn serialize<S>(value: &Regex, serializer: S) -> Result<S::Ok, S::Error>
2553 where
2554 S: Serializer,
2555 {
2556 serializer.serialize_str(value.as_str())
2557 }
2558
2559 pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Regex, D::Error>
2560 where
2561 D: Deserializer<'de>,
2562 {
2563 let s = String::deserialize(deserializer)?;
2564 Regex::new(&s).map_err(serde::de::Error::custom)
2565 }
2566}
2567
2568pub(crate) mod from_opt_glob {
2570 use serde::{Deserialize, Deserializer, Serializer};
2571
2572 pub fn serialize<S>(value: &Option<globset::Glob>, serializer: S) -> Result<S::Ok, S::Error>
2573 where
2574 S: Serializer,
2575 {
2576 match value {
2577 Some(glob) => serializer.serialize_str(glob.glob()),
2578 None => serializer.serialize_none(),
2579 }
2580 }
2581
2582 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<globset::Glob>, D::Error>
2583 where
2584 D: Deserializer<'de>,
2585 {
2586 let s: Option<String> = Option::deserialize(deserializer)?;
2587 if let Some(s) = s {
2588 return Ok(Some(globset::Glob::new(&s).map_err(serde::de::Error::custom)?));
2589 }
2590 Ok(None)
2591 }
2592}
2593
2594pub fn parse_with_profile<T: serde::de::DeserializeOwned>(
2605 s: &str,
2606) -> Result<Option<(Profile, T)>, Error> {
2607 let figment = Config::merge_toml_provider(
2608 Figment::new(),
2609 Toml::string(s).nested(),
2610 Config::DEFAULT_PROFILE,
2611 );
2612 if figment.profiles().any(|p| p == Config::DEFAULT_PROFILE) {
2613 Ok(Some((Config::DEFAULT_PROFILE, figment.select(Config::DEFAULT_PROFILE).extract()?)))
2614 } else {
2615 Ok(None)
2616 }
2617}
2618
2619impl Provider for Config {
2620 fn metadata(&self) -> Metadata {
2621 Metadata::named("Foundry Config")
2622 }
2623
2624 #[track_caller]
2625 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
2626 let mut data = Serialized::defaults(self).data()?;
2627 if let Some(entry) = data.get_mut(&self.profile) {
2628 entry.insert("root".to_string(), Value::serialize(self.root.clone())?);
2629 }
2630 Ok(data)
2631 }
2632
2633 fn profile(&self) -> Option<Profile> {
2634 Some(self.profile.clone())
2635 }
2636}
2637
2638impl Default for Config {
2639 fn default() -> Self {
2640 Self {
2641 profile: Self::DEFAULT_PROFILE,
2642 profiles: vec![Self::DEFAULT_PROFILE],
2643 fs_permissions: FsPermissions::new([PathPermission::read("out")]),
2644 isolate: cfg!(feature = "isolate-by-default"),
2645 root: root_default(),
2646 extends: None,
2647 src: "src".into(),
2648 test: "test".into(),
2649 script: "script".into(),
2650 out: "out".into(),
2651 libs: vec!["lib".into()],
2652 cache: true,
2653 dynamic_test_linking: false,
2654 cache_path: "cache".into(),
2655 broadcast: "broadcast".into(),
2656 snapshots: "snapshots".into(),
2657 gas_snapshot_check: false,
2658 gas_snapshot_emit: true,
2659 allow_paths: vec![],
2660 include_paths: vec![],
2661 force: false,
2662 evm_version: EvmVersion::Osaka,
2663 hardfork: None,
2664 gas_reports: vec!["*".to_string()],
2665 gas_reports_ignore: vec![],
2666 gas_reports_include_tests: false,
2667 solc: None,
2668 vyper: Default::default(),
2669 auto_detect_solc: true,
2670 offline: false,
2671 optimizer: None,
2672 optimizer_runs: None,
2673 optimizer_details: None,
2674 model_checker: None,
2675 extra_output: Default::default(),
2676 extra_output_files: Default::default(),
2677 names: false,
2678 sizes: false,
2679 test_pattern: None,
2680 test_pattern_inverse: None,
2681 contract_pattern: None,
2682 contract_pattern_inverse: None,
2683 path_pattern: None,
2684 path_pattern_inverse: None,
2685 coverage_pattern_inverse: None,
2686 test_failures_file: "cache/test-failures".into(),
2687 threads: None,
2688 show_progress: false,
2689 fuzz: FuzzConfig::new("cache/fuzz".into()),
2690 invariant: InvariantConfig::new("cache/invariant".into()),
2691 always_use_create_2_factory: false,
2692 ffi: false,
2693 live_logs: false,
2694 allow_internal_expect_revert: false,
2695 prompt_timeout: 120,
2696 sender: Self::DEFAULT_SENDER,
2697 tx_origin: Self::DEFAULT_SENDER,
2698 initial_balance: U256::from((1u128 << 96) - 1),
2699 block_number: U256::from(1),
2700 fork_block_number: None,
2701 chain: None,
2702 gas_limit: (1u64 << 30).into(), code_size_limit: None,
2704 gas_price: None,
2705 block_base_fee_per_gas: 0,
2706 block_coinbase: Address::ZERO,
2707 block_timestamp: U256::from(1),
2708 block_difficulty: 0,
2709 block_prevrandao: Default::default(),
2710 block_gas_limit: None,
2711 disable_block_gas_limit: false,
2712 enable_tx_gas_limit: false,
2713 memory_limit: 1 << 27, eth_rpc_url: None,
2715 eth_rpc_accept_invalid_certs: false,
2716 eth_rpc_no_proxy: false,
2717 eth_rpc_jwt: None,
2718 eth_rpc_timeout: None,
2719 eth_rpc_headers: None,
2720 eth_rpc_curl: false,
2721 etherscan_api_key: None,
2722 verbosity: 0,
2723 remappings: vec![],
2724 auto_detect_remappings: true,
2725 libraries: vec![],
2726 ignored_error_codes: vec![
2727 SolidityErrorCode::SpdxLicenseNotProvided,
2728 SolidityErrorCode::ContractExceeds24576Bytes,
2729 SolidityErrorCode::ContractInitCodeSizeExceeds49152Bytes,
2730 SolidityErrorCode::TransientStorageUsed,
2731 SolidityErrorCode::TransferDeprecated,
2732 SolidityErrorCode::NatspecMemorySafeAssemblyDeprecated,
2733 ],
2734 ignored_error_codes_from: vec![],
2735 ignored_file_paths: vec![],
2736 deny: DenyLevel::Never,
2737 deny_warnings: false,
2738 via_ir: false,
2739 ast: false,
2740 rpc_storage_caching: Default::default(),
2741 rpc_endpoints: Default::default(),
2742 etherscan: Default::default(),
2743 no_storage_caching: false,
2744 no_rpc_rate_limit: false,
2745 use_literal_content: false,
2746 bytecode_hash: BytecodeHash::Ipfs,
2747 cbor_metadata: true,
2748 revert_strings: None,
2749 sparse_mode: false,
2750 build_info: false,
2751 build_info_path: None,
2752 fmt: Default::default(),
2753 lint: Default::default(),
2754 doc: Default::default(),
2755 bind_json: Default::default(),
2756 labels: Default::default(),
2757 unchecked_cheatcode_artifacts: false,
2758 create2_library_salt: Self::DEFAULT_CREATE2_LIBRARY_SALT,
2759 create2_deployer: Self::DEFAULT_CREATE2_DEPLOYER,
2760 skip: vec![],
2761 dependencies: Default::default(),
2762 soldeer: Default::default(),
2763 assertions_revert: true,
2764 legacy_assertions: false,
2765 warnings: vec![],
2766 extra_args: vec![],
2767 networks: Default::default(),
2768 transaction_timeout: 120,
2769 additional_compiler_profiles: Default::default(),
2770 compilation_restrictions: Default::default(),
2771 script_execution_protection: true,
2772 _non_exhaustive: (),
2773 }
2774 }
2775}
2776
2777#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
2783pub struct GasLimit(#[serde(deserialize_with = "crate::deserialize_u64_or_max")] pub u64);
2784
2785impl From<u64> for GasLimit {
2786 fn from(gas: u64) -> Self {
2787 Self(gas)
2788 }
2789}
2790
2791impl From<GasLimit> for u64 {
2792 fn from(gas: GasLimit) -> Self {
2793 gas.0
2794 }
2795}
2796
2797impl Serialize for GasLimit {
2798 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
2799 where
2800 S: Serializer,
2801 {
2802 if self.0 == u64::MAX {
2803 serializer.serialize_str("max")
2804 } else if self.0 > i64::MAX as u64 {
2805 serializer.serialize_str(&self.0.to_string())
2806 } else {
2807 serializer.serialize_u64(self.0)
2808 }
2809 }
2810}
2811
2812#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2814#[serde(untagged)]
2815pub enum SolcReq {
2816 Version(Version),
2819 Local(PathBuf),
2821}
2822
2823impl SolcReq {
2824 fn try_version(&self) -> Result<Version, SolcError> {
2829 match self {
2830 Self::Version(version) => Ok(version.clone()),
2831 Self::Local(path) => Solc::new(path).map(|solc| solc.version),
2832 }
2833 }
2834}
2835
2836impl<T: AsRef<str>> From<T> for SolcReq {
2837 fn from(s: T) -> Self {
2838 let s = s.as_ref();
2839 if let Ok(v) = Version::from_str(s) { Self::Version(v) } else { Self::Local(s.into()) }
2840 }
2841}
2842
2843#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2855pub struct BasicConfig {
2856 #[serde(skip)]
2858 pub profile: Profile,
2859 pub src: PathBuf,
2861 pub out: PathBuf,
2863 pub libs: Vec<PathBuf>,
2865 #[serde(default, skip_serializing_if = "Vec::is_empty")]
2867 pub remappings: Vec<RelativeRemapping>,
2868 #[serde(skip)]
2870 pub network: Option<String>,
2871}
2872
2873impl BasicConfig {
2874 pub fn to_string_pretty(&self) -> Result<String, toml::ser::Error> {
2878 let mut profile_body = toml::Value::try_from(self)?;
2879 if let Some(ref network) = self.network
2880 && let toml::Value::Table(ref mut table) = profile_body
2881 {
2882 table.insert("network".to_string(), toml::Value::String(network.clone()));
2883 }
2884
2885 let mut profile_section = toml::value::Table::new();
2886 profile_section.insert(self.profile.to_string(), profile_body);
2887
2888 let mut document = toml::value::Table::new();
2889 document.insert("profile".to_string(), toml::Value::Table(profile_section));
2890
2891 if self.network.as_deref() == Some("tempo") {
2892 let mut endpoints = toml::value::Table::new();
2893 endpoints.insert(
2894 "tempo".to_string(),
2895 toml::Value::String("https://rpc.tempo.xyz/".to_string()),
2896 );
2897 endpoints.insert(
2898 "moderato".to_string(),
2899 toml::Value::String("https://rpc.moderato.tempo.xyz/".to_string()),
2900 );
2901 document.insert("rpc_endpoints".to_string(), toml::Value::Table(endpoints));
2902 }
2903
2904 let body = toml::to_string_pretty(&toml::Value::Table(document))?;
2905 Ok(format!(
2906 "{body}\n# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options\n"
2907 ))
2908 }
2909}
2910
2911pub(crate) mod from_str_lowercase {
2912 use serde::{Deserialize, Deserializer, Serializer};
2913 use std::str::FromStr;
2914
2915 pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
2916 where
2917 T: std::fmt::Display,
2918 S: Serializer,
2919 {
2920 serializer.collect_str(&value.to_string().to_lowercase())
2921 }
2922
2923 pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
2924 where
2925 D: Deserializer<'de>,
2926 T: FromStr,
2927 T::Err: std::fmt::Display,
2928 {
2929 String::deserialize(deserializer)?.to_lowercase().parse().map_err(serde::de::Error::custom)
2930 }
2931}
2932
2933fn canonic(path: impl Into<PathBuf>) -> PathBuf {
2934 let path = path.into();
2935 foundry_compilers::utils::canonicalize(&path).unwrap_or(path)
2936}
2937
2938fn root_default() -> PathBuf {
2939 ".".into()
2940}
2941
2942#[cfg(test)]
2943mod tests {
2944 use super::*;
2945 use crate::{
2946 cache::{CachedChains, CachedEndpoints},
2947 endpoints::RpcEndpointType,
2948 etherscan::ResolvedEtherscanConfigs,
2949 fmt::IndentStyle,
2950 };
2951 use NamedChain::Moonbeam;
2952 use endpoints::{RpcAuth, RpcEndpointConfig};
2953 use figment::error::Kind::InvalidType;
2954 use foundry_compilers::artifacts::{
2955 ModelCheckerEngine, YulDetails,
2956 vyper::{VyperOptimizationLevel, VyperOptimizationMode, VyperVenomSettings},
2957 };
2958 use foundry_evm_hardforks::{TempoHardfork, latest_active_tempo_hardfork};
2959 use similar_asserts::assert_eq;
2960 use soldeer_core::remappings::RemappingsLocation;
2961 use std::{fs::File, io::Write};
2962 use tempfile::tempdir;
2963
2964 fn clear_warning(config: &mut Config) {
2967 config.warnings = vec![];
2968 }
2969
2970 #[test]
2971 fn default_sender() {
2972 assert_eq!(Config::DEFAULT_SENDER, address!("0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38"));
2973 }
2974
2975 #[test]
2976 fn test_caching() {
2977 let mut config = Config::default();
2978 let chain_id = NamedChain::Mainnet;
2979 let url = "https://eth-mainnet.alchemyapi";
2980 assert!(config.enable_caching(url, chain_id));
2981
2982 config.no_storage_caching = true;
2983 assert!(!config.enable_caching(url, chain_id));
2984
2985 config.no_storage_caching = false;
2986 assert!(!config.enable_caching(url, NamedChain::Dev));
2987 }
2988
2989 #[test]
2990 fn test_install_dir() {
2991 figment::Jail::expect_with(|jail| {
2992 let config = Config::load().unwrap();
2993 assert_eq!(config.install_lib_dir(), PathBuf::from("lib"));
2994 jail.create_file(
2995 "foundry.toml",
2996 r"
2997 [profile.default]
2998 libs = ['node_modules', 'lib']
2999 ",
3000 )?;
3001 let config = Config::load().unwrap();
3002 assert_eq!(config.install_lib_dir(), PathBuf::from("lib"));
3003
3004 jail.create_file(
3005 "foundry.toml",
3006 r"
3007 [profile.default]
3008 libs = ['custom', 'node_modules', 'lib']
3009 ",
3010 )?;
3011 let config = Config::load().unwrap();
3012 assert_eq!(config.install_lib_dir(), PathBuf::from("custom"));
3013
3014 Ok(())
3015 });
3016 }
3017
3018 #[test]
3019 fn test_figment_is_default() {
3020 figment::Jail::expect_with(|_| {
3021 let mut default: Config = Config::figment().extract()?;
3022 let default2 = Config::default();
3023 default.profile = default2.profile.clone();
3024 default.profiles = default2.profiles.clone();
3025 assert_eq!(default, default2);
3026 Ok(())
3027 });
3028 }
3029
3030 #[test]
3031 fn figment_profiles() {
3032 figment::Jail::expect_with(|jail| {
3033 jail.create_file(
3034 "foundry.toml",
3035 r"
3036 [foo.baz]
3037 libs = ['node_modules', 'lib']
3038
3039 [profile.default]
3040 libs = ['node_modules', 'lib']
3041
3042 [profile.ci]
3043 libs = ['node_modules', 'lib']
3044
3045 [profile.local]
3046 libs = ['node_modules', 'lib']
3047 ",
3048 )?;
3049
3050 let config = crate::Config::load().unwrap();
3051 let expected: &[figment::Profile] = &["ci".into(), "default".into(), "local".into()];
3052 assert_eq!(config.profiles, expected);
3053
3054 Ok(())
3055 });
3056 }
3057
3058 #[test]
3059 fn test_default_round_trip() {
3060 figment::Jail::expect_with(|_| {
3061 let original = Config::figment();
3062 let roundtrip = Figment::from(Config::from_provider(&original).unwrap());
3063 for figment in &[original, roundtrip] {
3064 let config = Config::from_provider(figment).unwrap();
3065 assert_eq!(config, Config::default().normalized_optimizer_settings());
3066 }
3067 Ok(())
3068 });
3069 }
3070
3071 #[test]
3072 fn ffi_env_disallowed() {
3073 figment::Jail::expect_with(|jail| {
3074 jail.set_env("FOUNDRY_FFI", "true");
3075 jail.set_env("FFI", "true");
3076 jail.set_env("DAPP_FFI", "true");
3077 let config = Config::load().unwrap();
3078 assert!(!config.ffi);
3079
3080 Ok(())
3081 });
3082 }
3083
3084 #[test]
3085 fn test_profile_env() {
3086 figment::Jail::expect_with(|jail| {
3087 jail.set_env("FOUNDRY_PROFILE", "default");
3088 let figment = Config::figment();
3089 assert_eq!(figment.profile(), "default");
3090
3091 jail.set_env("FOUNDRY_PROFILE", "hardhat");
3092 let figment: Figment = Config::hardhat().into();
3093 assert_eq!(figment.profile(), "hardhat");
3094
3095 jail.create_file(
3096 "foundry.toml",
3097 r"
3098 [profile.default]
3099 libs = ['lib']
3100 [profile.local]
3101 libs = ['modules']
3102 ",
3103 )?;
3104 jail.set_env("FOUNDRY_PROFILE", "local");
3105 let config = Config::load().unwrap();
3106 assert_eq!(config.libs, vec![PathBuf::from("modules")]);
3107
3108 Ok(())
3109 });
3110 }
3111
3112 #[test]
3113 fn test_default_test_path() {
3114 figment::Jail::expect_with(|_| {
3115 let config = Config::default();
3116 let paths_config = config.project_paths::<Solc>();
3117 assert_eq!(paths_config.tests, PathBuf::from(r"test"));
3118 Ok(())
3119 });
3120 }
3121
3122 #[test]
3123 fn test_default_libs() {
3124 figment::Jail::expect_with(|jail| {
3125 let config = Config::load().unwrap();
3126 assert_eq!(config.libs, vec![PathBuf::from("lib")]);
3127
3128 fs::create_dir_all(jail.directory().join("node_modules")).unwrap();
3129 let config = Config::load().unwrap();
3130 assert_eq!(config.libs, vec![PathBuf::from("node_modules")]);
3131
3132 fs::create_dir_all(jail.directory().join("lib")).unwrap();
3133 let config = Config::load().unwrap();
3134 assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
3135
3136 Ok(())
3137 });
3138 }
3139
3140 #[test]
3141 fn test_inheritance_from_default_test_path() {
3142 figment::Jail::expect_with(|jail| {
3143 jail.create_file(
3144 "foundry.toml",
3145 r#"
3146 [profile.default]
3147 test = "defaulttest"
3148 src = "defaultsrc"
3149 libs = ['lib', 'node_modules']
3150
3151 [profile.custom]
3152 src = "customsrc"
3153 "#,
3154 )?;
3155
3156 let config = Config::load().unwrap();
3157 assert_eq!(config.src, PathBuf::from("defaultsrc"));
3158 assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
3159
3160 jail.set_env("FOUNDRY_PROFILE", "custom");
3161 let config = Config::load().unwrap();
3162 assert_eq!(config.src, PathBuf::from("customsrc"));
3163 assert_eq!(config.test, PathBuf::from("defaulttest"));
3164 assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
3165
3166 Ok(())
3167 });
3168 }
3169
3170 #[test]
3171 fn test_custom_test_path() {
3172 figment::Jail::expect_with(|jail| {
3173 jail.create_file(
3174 "foundry.toml",
3175 r#"
3176 [profile.default]
3177 test = "mytest"
3178 "#,
3179 )?;
3180
3181 let config = Config::load().unwrap();
3182 let paths_config = config.project_paths::<Solc>();
3183 assert_eq!(paths_config.tests, PathBuf::from(r"mytest"));
3184 Ok(())
3185 });
3186 }
3187
3188 #[test]
3189 fn test_remappings() {
3190 figment::Jail::expect_with(|jail| {
3191 jail.create_file(
3192 "foundry.toml",
3193 r#"
3194 [profile.default]
3195 src = "some-source"
3196 out = "some-out"
3197 cache = true
3198 "#,
3199 )?;
3200 let config = Config::load().unwrap();
3201 assert!(config.remappings.is_empty());
3202
3203 jail.create_file(
3204 "remappings.txt",
3205 r"
3206 file-ds-test/=lib/ds-test/
3207 file-other/=lib/other/
3208 ",
3209 )?;
3210
3211 let config = Config::load().unwrap();
3212 assert_eq!(
3213 config.remappings,
3214 vec![
3215 Remapping::from_str("file-ds-test/=lib/ds-test/").unwrap().into(),
3216 Remapping::from_str("file-other/=lib/other/").unwrap().into(),
3217 ],
3218 );
3219
3220 jail.set_env("DAPP_REMAPPINGS", "ds-test=lib/ds-test/\nother/=lib/other/");
3221 let config = Config::load().unwrap();
3222
3223 assert_eq!(
3224 config.remappings,
3225 vec![
3226 Remapping::from_str("ds-test=lib/ds-test/").unwrap().into(),
3228 Remapping::from_str("other/=lib/other/").unwrap().into(),
3229 Remapping::from_str("file-ds-test/=lib/ds-test/").unwrap().into(),
3231 Remapping::from_str("file-other/=lib/other/").unwrap().into(),
3232 ],
3233 );
3234
3235 Ok(())
3236 });
3237 }
3238
3239 #[test]
3240 fn test_remappings_override() {
3241 figment::Jail::expect_with(|jail| {
3242 jail.create_file(
3243 "foundry.toml",
3244 r#"
3245 [profile.default]
3246 src = "some-source"
3247 out = "some-out"
3248 cache = true
3249 "#,
3250 )?;
3251 let config = Config::load().unwrap();
3252 assert!(config.remappings.is_empty());
3253
3254 jail.create_file(
3255 "remappings.txt",
3256 r"
3257 ds-test/=lib/ds-test/
3258 other/=lib/other/
3259 ",
3260 )?;
3261
3262 let config = Config::load().unwrap();
3263 assert_eq!(
3264 config.remappings,
3265 vec![
3266 Remapping::from_str("ds-test/=lib/ds-test/").unwrap().into(),
3267 Remapping::from_str("other/=lib/other/").unwrap().into(),
3268 ],
3269 );
3270
3271 jail.set_env("DAPP_REMAPPINGS", "ds-test/=lib/ds-test/src/\nenv-lib/=lib/env-lib/");
3272 let config = Config::load().unwrap();
3273
3274 assert_eq!(
3279 config.remappings,
3280 vec![
3281 Remapping::from_str("ds-test/=lib/ds-test/src/").unwrap().into(),
3282 Remapping::from_str("env-lib/=lib/env-lib/").unwrap().into(),
3283 Remapping::from_str("other/=lib/other/").unwrap().into(),
3284 ],
3285 );
3286
3287 assert_eq!(
3289 config.get_all_remappings().collect::<Vec<_>>(),
3290 vec![
3291 Remapping::from_str("ds-test/=lib/ds-test/src/").unwrap(),
3292 Remapping::from_str("env-lib/=lib/env-lib/").unwrap(),
3293 Remapping::from_str("other/=lib/other/").unwrap(),
3294 ],
3295 );
3296
3297 Ok(())
3298 });
3299 }
3300
3301 #[test]
3302 fn test_can_update_libs() {
3303 figment::Jail::expect_with(|jail| {
3304 jail.create_file(
3305 "foundry.toml",
3306 r#"
3307 [profile.default]
3308 libs = ["node_modules"]
3309 "#,
3310 )?;
3311
3312 let mut config = Config::load().unwrap();
3313 config.libs.push("libs".into());
3314 config.update_libs().unwrap();
3315
3316 let config = Config::load().unwrap();
3317 assert_eq!(config.libs, vec![PathBuf::from("node_modules"), PathBuf::from("libs"),]);
3318 Ok(())
3319 });
3320 }
3321
3322 #[test]
3323 fn test_large_gas_limit() {
3324 figment::Jail::expect_with(|jail| {
3325 let gas = u64::MAX;
3326 jail.create_file(
3327 "foundry.toml",
3328 &format!(
3329 r#"
3330 [profile.default]
3331 gas_limit = "{gas}"
3332 "#
3333 ),
3334 )?;
3335
3336 let config = Config::load().unwrap();
3337 assert_eq!(
3338 config,
3339 Config {
3340 gas_limit: gas.into(),
3341 ..Config::default().normalized_optimizer_settings()
3342 }
3343 );
3344
3345 Ok(())
3346 });
3347 }
3348
3349 #[test]
3350 #[should_panic]
3351 fn test_toml_file_parse_failure() {
3352 figment::Jail::expect_with(|jail| {
3353 jail.create_file(
3354 "foundry.toml",
3355 r#"
3356 [profile.default]
3357 eth_rpc_url = "https://example.com/
3358 "#,
3359 )?;
3360
3361 let _config = Config::load().unwrap();
3362
3363 Ok(())
3364 });
3365 }
3366
3367 #[test]
3368 #[should_panic]
3369 fn test_toml_file_non_existing_config_var_failure() {
3370 figment::Jail::expect_with(|jail| {
3371 jail.set_env("FOUNDRY_CONFIG", "this config does not exist");
3372
3373 let _config = Config::load().unwrap();
3374
3375 Ok(())
3376 });
3377 }
3378
3379 #[test]
3380 fn test_resolve_etherscan_with_chain() {
3381 figment::Jail::expect_with(|jail| {
3382 let env_key = "__BSC_ETHERSCAN_API_KEY";
3383 let env_value = "env value";
3384 jail.create_file(
3385 "foundry.toml",
3386 r#"
3387 [profile.default]
3388
3389 [etherscan]
3390 bsc = { key = "${__BSC_ETHERSCAN_API_KEY}", url = "https://api.bscscan.com/api" }
3391 "#,
3392 )?;
3393
3394 let config = Config::load().unwrap();
3395 assert!(
3396 config
3397 .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
3398 .is_err()
3399 );
3400
3401 unsafe {
3402 std::env::set_var(env_key, env_value);
3403 }
3404
3405 assert_eq!(
3406 config
3407 .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
3408 .unwrap()
3409 .unwrap()
3410 .key,
3411 env_value
3412 );
3413
3414 let mut with_key = config;
3415 with_key.etherscan_api_key = Some("via etherscan_api_key".to_string());
3416
3417 assert_eq!(
3418 with_key
3419 .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
3420 .unwrap()
3421 .unwrap()
3422 .key,
3423 "via etherscan_api_key"
3424 );
3425
3426 unsafe {
3427 std::env::remove_var(env_key);
3428 }
3429 Ok(())
3430 });
3431 }
3432
3433 #[test]
3434 fn test_resolve_etherscan() {
3435 figment::Jail::expect_with(|jail| {
3436 jail.create_file(
3437 "foundry.toml",
3438 r#"
3439 [profile.default]
3440
3441 [etherscan]
3442 mainnet = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
3443 moonbeam = { key = "${_CONFIG_ETHERSCAN_MOONBEAM}" }
3444 "#,
3445 )?;
3446
3447 let config = Config::load().unwrap();
3448
3449 assert!(config.etherscan.clone().resolved().has_unresolved());
3450
3451 jail.set_env("_CONFIG_ETHERSCAN_MOONBEAM", "123456789");
3452
3453 let configs = config.etherscan.resolved();
3454 assert!(!configs.has_unresolved());
3455
3456 let mb_urls = Moonbeam.etherscan_urls().unwrap();
3457 let mainnet_urls = NamedChain::Mainnet.etherscan_urls().unwrap();
3458 assert_eq!(
3459 configs,
3460 ResolvedEtherscanConfigs::new([
3461 (
3462 "mainnet",
3463 ResolvedEtherscanConfig {
3464 api_url: mainnet_urls.0.to_string(),
3465 chain: Some(NamedChain::Mainnet.into()),
3466 browser_url: Some(mainnet_urls.1.to_string()),
3467 key: "FX42Z3BBJJEWXWGYV2X1CIPRSCN".to_string(),
3468 }
3469 ),
3470 (
3471 "moonbeam",
3472 ResolvedEtherscanConfig {
3473 api_url: mb_urls.0.to_string(),
3474 chain: Some(Moonbeam.into()),
3475 browser_url: Some(mb_urls.1.to_string()),
3476 key: "123456789".to_string(),
3477 }
3478 ),
3479 ])
3480 );
3481
3482 Ok(())
3483 });
3484 }
3485
3486 #[test]
3487 fn test_resolve_etherscan_with_versions() {
3488 figment::Jail::expect_with(|jail| {
3489 jail.create_file(
3490 "foundry.toml",
3491 r#"
3492 [profile.default]
3493
3494 [etherscan]
3495 mainnet = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN", api_version = "v2" }
3496 moonbeam = { key = "${_CONFIG_ETHERSCAN_MOONBEAM}", api_version = "v1" }
3497 "#,
3498 )?;
3499
3500 let config = Config::load().unwrap();
3501
3502 assert!(config.etherscan.clone().resolved().has_unresolved());
3503
3504 jail.set_env("_CONFIG_ETHERSCAN_MOONBEAM", "123456789");
3505
3506 let configs = config.etherscan.resolved();
3507 assert!(!configs.has_unresolved());
3508
3509 let mb_urls = Moonbeam.etherscan_urls().unwrap();
3510 let mainnet_urls = NamedChain::Mainnet.etherscan_urls().unwrap();
3511 assert_eq!(
3512 configs,
3513 ResolvedEtherscanConfigs::new([
3514 (
3515 "mainnet",
3516 ResolvedEtherscanConfig {
3517 api_url: mainnet_urls.0.to_string(),
3518 chain: Some(NamedChain::Mainnet.into()),
3519 browser_url: Some(mainnet_urls.1.to_string()),
3520 key: "FX42Z3BBJJEWXWGYV2X1CIPRSCN".to_string(),
3521 }
3522 ),
3523 (
3524 "moonbeam",
3525 ResolvedEtherscanConfig {
3526 api_url: mb_urls.0.to_string(),
3527 chain: Some(Moonbeam.into()),
3528 browser_url: Some(mb_urls.1.to_string()),
3529 key: "123456789".to_string(),
3530 }
3531 ),
3532 ])
3533 );
3534
3535 Ok(())
3536 });
3537 }
3538
3539 #[test]
3540 fn test_resolve_etherscan_chain_id() {
3541 figment::Jail::expect_with(|jail| {
3542 jail.create_file(
3543 "foundry.toml",
3544 r#"
3545 [profile.default]
3546 chain_id = "sepolia"
3547
3548 [etherscan]
3549 sepolia = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
3550 "#,
3551 )?;
3552
3553 let config = Config::load().unwrap();
3554 let etherscan = config.get_etherscan_config().unwrap().unwrap();
3555 assert_eq!(etherscan.chain, Some(NamedChain::Sepolia.into()));
3556 assert_eq!(etherscan.key, "FX42Z3BBJJEWXWGYV2X1CIPRSCN");
3557
3558 Ok(())
3559 });
3560 }
3561
3562 #[test]
3564 fn test_resolve_etherscan_with_invalid_name() {
3565 figment::Jail::expect_with(|jail| {
3566 jail.create_file(
3567 "foundry.toml",
3568 r#"
3569 [etherscan]
3570 mainnet = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
3571 an_invalid_name = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
3572 "#,
3573 )?;
3574
3575 let config = Config::load().unwrap();
3576 let etherscan_config = config.get_etherscan_config();
3577 assert!(etherscan_config.is_none());
3578
3579 Ok(())
3580 });
3581 }
3582
3583 #[test]
3584 fn test_resolve_rpc_url() {
3585 figment::Jail::expect_with(|jail| {
3586 jail.create_file(
3587 "foundry.toml",
3588 r#"
3589 [profile.default]
3590 [rpc_endpoints]
3591 optimism = "https://example.com/"
3592 mainnet = "${_CONFIG_MAINNET}"
3593 "#,
3594 )?;
3595 jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3596
3597 let mut config = Config::load().unwrap();
3598 assert_eq!("http://localhost:8545", config.get_rpc_url_or_localhost_http().unwrap());
3599
3600 config.eth_rpc_url = Some("mainnet".to_string());
3601 assert_eq!(
3602 "https://eth-mainnet.alchemyapi.io/v2/123455",
3603 config.get_rpc_url_or_localhost_http().unwrap()
3604 );
3605
3606 config.eth_rpc_url = Some("optimism".to_string());
3607 assert_eq!("https://example.com/", config.get_rpc_url_or_localhost_http().unwrap());
3608
3609 Ok(())
3610 })
3611 }
3612
3613 #[test]
3614 fn test_resolve_rpc_url_if_etherscan_set() {
3615 figment::Jail::expect_with(|jail| {
3616 jail.create_file(
3617 "foundry.toml",
3618 r#"
3619 [profile.default]
3620 etherscan_api_key = "dummy"
3621 [rpc_endpoints]
3622 optimism = "https://example.com/"
3623 "#,
3624 )?;
3625
3626 let config = Config::load().unwrap();
3627 assert_eq!("http://localhost:8545", config.get_rpc_url_or_localhost_http().unwrap());
3628
3629 Ok(())
3630 })
3631 }
3632
3633 #[test]
3634 fn test_resolve_rpc_url_alias() {
3635 figment::Jail::expect_with(|jail| {
3636 jail.create_file(
3637 "foundry.toml",
3638 r#"
3639 [profile.default]
3640 [rpc_endpoints]
3641 polygonAmoy = "https://polygon-amoy.g.alchemy.com/v2/${_RESOLVE_RPC_ALIAS}"
3642 "#,
3643 )?;
3644 let mut config = Config::load().unwrap();
3645 config.eth_rpc_url = Some("polygonAmoy".to_string());
3646 assert!(config.get_rpc_url().unwrap().is_err());
3647
3648 jail.set_env("_RESOLVE_RPC_ALIAS", "123455");
3649
3650 let mut config = Config::load().unwrap();
3651 config.eth_rpc_url = Some("polygonAmoy".to_string());
3652 assert_eq!(
3653 "https://polygon-amoy.g.alchemy.com/v2/123455",
3654 config.get_rpc_url().unwrap().unwrap()
3655 );
3656
3657 Ok(())
3658 })
3659 }
3660
3661 #[test]
3662 fn test_resolve_rpc_aliases() {
3663 figment::Jail::expect_with(|jail| {
3664 jail.create_file(
3665 "foundry.toml",
3666 r#"
3667 [profile.default]
3668 [etherscan]
3669 arbitrum_alias = { key = "${TEST_RESOLVE_RPC_ALIAS_ARBISCAN}" }
3670 [rpc_endpoints]
3671 arbitrum_alias = "https://arb-mainnet.g.alchemy.com/v2/${TEST_RESOLVE_RPC_ALIAS_ARB_ONE}"
3672 "#,
3673 )?;
3674
3675 jail.set_env("TEST_RESOLVE_RPC_ALIAS_ARB_ONE", "123455");
3676 jail.set_env("TEST_RESOLVE_RPC_ALIAS_ARBISCAN", "123455");
3677
3678 let config = Config::load().unwrap();
3679
3680 let config = config.get_etherscan_config_with_chain(Some(NamedChain::Arbitrum.into()));
3681 assert!(config.is_err());
3682 assert_eq!(
3683 config.unwrap_err().to_string(),
3684 "At least one of `url` or `chain` must be present for Etherscan config with unknown alias `arbitrum_alias`"
3685 );
3686
3687 Ok(())
3688 });
3689 }
3690
3691 #[test]
3692 fn test_resolve_rpc_config() {
3693 figment::Jail::expect_with(|jail| {
3694 jail.create_file(
3695 "foundry.toml",
3696 r#"
3697 [rpc_endpoints]
3698 optimism = "https://example.com/"
3699 mainnet = { endpoint = "${_CONFIG_MAINNET}", retries = 3, retry_backoff = 1000, compute_units_per_second = 1000 }
3700 "#,
3701 )?;
3702 jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3703
3704 let config = Config::load().unwrap();
3705 assert_eq!(
3706 RpcEndpoints::new([
3707 (
3708 "optimism",
3709 RpcEndpointType::String(RpcEndpointUrl::Url(
3710 "https://example.com/".to_string()
3711 ))
3712 ),
3713 (
3714 "mainnet",
3715 RpcEndpointType::Config(RpcEndpoint {
3716 endpoint: RpcEndpointUrl::Env("${_CONFIG_MAINNET}".to_string()),
3717 extra_endpoints: vec![],
3718 config: RpcEndpointConfig {
3719 retries: Some(3),
3720 retry_backoff: Some(1000),
3721 compute_units_per_second: Some(1000),
3722 },
3723 auth: None,
3724 })
3725 ),
3726 ]),
3727 config.rpc_endpoints
3728 );
3729
3730 let resolved = config.rpc_endpoints.resolved();
3731 assert_eq!(
3732 RpcEndpoints::new([
3733 (
3734 "optimism",
3735 RpcEndpointType::String(RpcEndpointUrl::Url(
3736 "https://example.com/".to_string()
3737 ))
3738 ),
3739 (
3740 "mainnet",
3741 RpcEndpointType::Config(RpcEndpoint {
3742 endpoint: RpcEndpointUrl::Env("${_CONFIG_MAINNET}".to_string()),
3743 extra_endpoints: vec![],
3744 config: RpcEndpointConfig {
3745 retries: Some(3),
3746 retry_backoff: Some(1000),
3747 compute_units_per_second: Some(1000),
3748 },
3749 auth: None,
3750 })
3751 ),
3752 ])
3753 .resolved(),
3754 resolved
3755 );
3756 Ok(())
3757 })
3758 }
3759
3760 #[test]
3761 fn test_resolve_auth() {
3762 figment::Jail::expect_with(|jail| {
3763 jail.create_file(
3764 "foundry.toml",
3765 r#"
3766 [profile.default]
3767 eth_rpc_url = "optimism"
3768 [rpc_endpoints]
3769 optimism = "https://example.com/"
3770 mainnet = { endpoint = "${_CONFIG_MAINNET}", retries = 3, retry_backoff = 1000, compute_units_per_second = 1000, auth = "Bearer ${_CONFIG_AUTH}" }
3771 "#,
3772 )?;
3773
3774 let config = Config::load().unwrap();
3775
3776 jail.set_env("_CONFIG_AUTH", "123456");
3777 jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3778
3779 assert_eq!(
3780 RpcEndpoints::new([
3781 (
3782 "optimism",
3783 RpcEndpointType::String(RpcEndpointUrl::Url(
3784 "https://example.com/".to_string()
3785 ))
3786 ),
3787 (
3788 "mainnet",
3789 RpcEndpointType::Config(RpcEndpoint {
3790 endpoint: RpcEndpointUrl::Env("${_CONFIG_MAINNET}".to_string()),
3791 extra_endpoints: vec![],
3792 config: RpcEndpointConfig {
3793 retries: Some(3),
3794 retry_backoff: Some(1000),
3795 compute_units_per_second: Some(1000)
3796 },
3797 auth: Some(RpcAuth::Env("Bearer ${_CONFIG_AUTH}".to_string())),
3798 })
3799 ),
3800 ]),
3801 config.rpc_endpoints
3802 );
3803 let resolved = config.rpc_endpoints.resolved();
3804 assert_eq!(
3805 RpcEndpoints::new([
3806 (
3807 "optimism",
3808 RpcEndpointType::String(RpcEndpointUrl::Url(
3809 "https://example.com/".to_string()
3810 ))
3811 ),
3812 (
3813 "mainnet",
3814 RpcEndpointType::Config(RpcEndpoint {
3815 endpoint: RpcEndpointUrl::Url(
3816 "https://eth-mainnet.alchemyapi.io/v2/123455".to_string()
3817 ),
3818 extra_endpoints: vec![],
3819 config: RpcEndpointConfig {
3820 retries: Some(3),
3821 retry_backoff: Some(1000),
3822 compute_units_per_second: Some(1000)
3823 },
3824 auth: Some(RpcAuth::Raw("Bearer 123456".to_string())),
3825 })
3826 ),
3827 ])
3828 .resolved(),
3829 resolved
3830 );
3831
3832 Ok(())
3833 });
3834 }
3835
3836 #[test]
3837 fn test_resolve_endpoints() {
3838 figment::Jail::expect_with(|jail| {
3839 jail.create_file(
3840 "foundry.toml",
3841 r#"
3842 [profile.default]
3843 eth_rpc_url = "optimism"
3844 [rpc_endpoints]
3845 optimism = "https://example.com/"
3846 mainnet = "${_CONFIG_MAINNET}"
3847 mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${_CONFIG_API_KEY1}"
3848 mainnet_3 = "https://eth-mainnet.alchemyapi.io/v2/${_CONFIG_API_KEY1}/${_CONFIG_API_KEY2}"
3849 "#,
3850 )?;
3851
3852 let config = Config::load().unwrap();
3853
3854 assert_eq!(config.get_rpc_url().unwrap().unwrap(), "https://example.com/");
3855
3856 assert!(config.rpc_endpoints.clone().resolved().has_unresolved());
3857
3858 jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
3859 jail.set_env("_CONFIG_API_KEY1", "123456");
3860 jail.set_env("_CONFIG_API_KEY2", "98765");
3861
3862 let endpoints = config.rpc_endpoints.resolved();
3863
3864 assert!(!endpoints.has_unresolved());
3865
3866 assert_eq!(
3867 endpoints,
3868 RpcEndpoints::new([
3869 ("optimism", RpcEndpointUrl::Url("https://example.com/".to_string())),
3870 (
3871 "mainnet",
3872 RpcEndpointUrl::Url(
3873 "https://eth-mainnet.alchemyapi.io/v2/123455".to_string()
3874 )
3875 ),
3876 (
3877 "mainnet_2",
3878 RpcEndpointUrl::Url(
3879 "https://eth-mainnet.alchemyapi.io/v2/123456".to_string()
3880 )
3881 ),
3882 (
3883 "mainnet_3",
3884 RpcEndpointUrl::Url(
3885 "https://eth-mainnet.alchemyapi.io/v2/123456/98765".to_string()
3886 )
3887 ),
3888 ])
3889 .resolved()
3890 );
3891
3892 Ok(())
3893 });
3894 }
3895
3896 #[test]
3897 fn test_extract_etherscan_config() {
3898 figment::Jail::expect_with(|jail| {
3899 jail.create_file(
3900 "foundry.toml",
3901 r#"
3902 [profile.default]
3903 etherscan_api_key = "optimism"
3904
3905 [etherscan]
3906 optimism = { key = "https://etherscan-optimism.com/" }
3907 amoy = { key = "https://etherscan-amoy.com/" }
3908 "#,
3909 )?;
3910
3911 let mut config = Config::load().unwrap();
3912
3913 let optimism = config.get_etherscan_api_key(Some(NamedChain::Optimism.into()));
3914 assert_eq!(optimism, Some("https://etherscan-optimism.com/".to_string()));
3915
3916 config.etherscan_api_key = Some("amoy".to_string());
3917
3918 let amoy = config.get_etherscan_api_key(Some(NamedChain::PolygonAmoy.into()));
3919 assert_eq!(amoy, Some("https://etherscan-amoy.com/".to_string()));
3920
3921 Ok(())
3922 });
3923 }
3924
3925 #[test]
3926 fn test_extract_etherscan_config_by_chain() {
3927 figment::Jail::expect_with(|jail| {
3928 jail.create_file(
3929 "foundry.toml",
3930 r#"
3931 [profile.default]
3932
3933 [etherscan]
3934 amoy = { key = "https://etherscan-amoy.com/", chain = 80002 }
3935 "#,
3936 )?;
3937
3938 let config = Config::load().unwrap();
3939
3940 let amoy = config
3941 .get_etherscan_config_with_chain(Some(NamedChain::PolygonAmoy.into()))
3942 .unwrap()
3943 .unwrap();
3944 assert_eq!(amoy.key, "https://etherscan-amoy.com/".to_string());
3945
3946 Ok(())
3947 });
3948 }
3949
3950 #[test]
3951 fn test_extract_etherscan_config_by_chain_with_url() {
3952 figment::Jail::expect_with(|jail| {
3953 jail.create_file(
3954 "foundry.toml",
3955 r#"
3956 [profile.default]
3957
3958 [etherscan]
3959 amoy = { key = "https://etherscan-amoy.com/", chain = 80002 , url = "https://verifier-url.com/"}
3960 "#,
3961 )?;
3962
3963 let config = Config::load().unwrap();
3964
3965 let amoy = config
3966 .get_etherscan_config_with_chain(Some(NamedChain::PolygonAmoy.into()))
3967 .unwrap()
3968 .unwrap();
3969 assert_eq!(amoy.key, "https://etherscan-amoy.com/".to_string());
3970 assert_eq!(amoy.api_url, "https://verifier-url.com/".to_string());
3971
3972 Ok(())
3973 });
3974 }
3975
3976 #[test]
3977 fn test_extract_etherscan_config_by_chain_and_alias() {
3978 figment::Jail::expect_with(|jail| {
3979 jail.create_file(
3980 "foundry.toml",
3981 r#"
3982 [profile.default]
3983 eth_rpc_url = "amoy"
3984
3985 [etherscan]
3986 amoy = { key = "https://etherscan-amoy.com/" }
3987
3988 [rpc_endpoints]
3989 amoy = "https://polygon-amoy.g.alchemy.com/v2/amoy"
3990 "#,
3991 )?;
3992
3993 let config = Config::load().unwrap();
3994
3995 let amoy = config.get_etherscan_config_with_chain(None).unwrap().unwrap();
3996 assert_eq!(amoy.key, "https://etherscan-amoy.com/".to_string());
3997
3998 let amoy_rpc = config.get_rpc_url().unwrap().unwrap();
3999 assert_eq!(amoy_rpc, "https://polygon-amoy.g.alchemy.com/v2/amoy");
4000 Ok(())
4001 });
4002 }
4003
4004 #[test]
4005 fn test_toml_file() {
4006 figment::Jail::expect_with(|jail| {
4007 jail.create_file(
4008 "foundry.toml",
4009 r#"
4010 [profile.default]
4011 src = "some-source"
4012 out = "some-out"
4013 cache = true
4014 eth_rpc_url = "https://example.com/"
4015 verbosity = 3
4016 remappings = ["ds-test=lib/ds-test/"]
4017 via_ir = true
4018 rpc_storage_caching = { chains = [1, "optimism", 999999], endpoints = "all"}
4019 use_literal_content = false
4020 bytecode_hash = "ipfs"
4021 cbor_metadata = true
4022 revert_strings = "strip"
4023 allow_paths = ["allow", "paths"]
4024 build_info_path = "build-info"
4025 always_use_create_2_factory = true
4026
4027 [rpc_endpoints]
4028 optimism = "https://example.com/"
4029 mainnet = "${RPC_MAINNET}"
4030 mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}"
4031 mainnet_3 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}/${ANOTHER_KEY}"
4032 "#,
4033 )?;
4034
4035 let config = Config::load().unwrap();
4036 assert_eq!(
4037 config,
4038 Config {
4039 src: "some-source".into(),
4040 out: "some-out".into(),
4041 cache: true,
4042 eth_rpc_url: Some("https://example.com/".to_string()),
4043 remappings: vec![Remapping::from_str("ds-test=lib/ds-test/").unwrap().into()],
4044 verbosity: 3,
4045 via_ir: true,
4046 rpc_storage_caching: StorageCachingConfig {
4047 chains: CachedChains::Chains(vec![
4048 Chain::mainnet(),
4049 Chain::optimism_mainnet(),
4050 Chain::from_id(999999)
4051 ]),
4052 endpoints: CachedEndpoints::All,
4053 },
4054 use_literal_content: false,
4055 bytecode_hash: BytecodeHash::Ipfs,
4056 cbor_metadata: true,
4057 revert_strings: Some(RevertStrings::Strip),
4058 allow_paths: vec![PathBuf::from("allow"), PathBuf::from("paths")],
4059 rpc_endpoints: RpcEndpoints::new([
4060 ("optimism", RpcEndpointUrl::Url("https://example.com/".to_string())),
4061 ("mainnet", RpcEndpointUrl::Env("${RPC_MAINNET}".to_string())),
4062 (
4063 "mainnet_2",
4064 RpcEndpointUrl::Env(
4065 "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}".to_string()
4066 )
4067 ),
4068 (
4069 "mainnet_3",
4070 RpcEndpointUrl::Env(
4071 "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}/${ANOTHER_KEY}"
4072 .to_string()
4073 )
4074 ),
4075 ]),
4076 build_info_path: Some("build-info".into()),
4077 always_use_create_2_factory: true,
4078 ..Config::default().normalized_optimizer_settings()
4079 }
4080 );
4081
4082 Ok(())
4083 });
4084 }
4085
4086 #[test]
4087 fn test_load_remappings() {
4088 figment::Jail::expect_with(|jail| {
4089 jail.create_file(
4090 "foundry.toml",
4091 r"
4092 [profile.default]
4093 remappings = ['nested/=lib/nested/']
4094 ",
4095 )?;
4096
4097 let config = Config::load_with_root(jail.directory()).unwrap();
4098 assert_eq!(
4099 config.remappings,
4100 vec![Remapping::from_str("nested/=lib/nested/").unwrap().into()]
4101 );
4102
4103 Ok(())
4104 });
4105 }
4106
4107 #[test]
4108 fn test_load_full_toml() {
4109 figment::Jail::expect_with(|jail| {
4110 jail.create_file(
4111 "foundry.toml",
4112 r#"
4113 [profile.default]
4114 auto_detect_solc = true
4115 block_base_fee_per_gas = 0
4116 block_coinbase = '0x0000000000000000000000000000000000000000'
4117 block_difficulty = 0
4118 block_prevrandao = '0x0000000000000000000000000000000000000000000000000000000000000000'
4119 block_number = 1
4120 block_timestamp = 1
4121 use_literal_content = false
4122 bytecode_hash = 'ipfs'
4123 cbor_metadata = true
4124 cache = true
4125 cache_path = 'cache'
4126 evm_version = 'london'
4127 extra_output = []
4128 extra_output_files = []
4129 always_use_create_2_factory = false
4130 ffi = false
4131 force = false
4132 gas_limit = 9223372036854775807
4133 gas_price = 0
4134 gas_reports = ['*']
4135 ignored_error_codes = [1878]
4136 ignored_warnings_from = ["something"]
4137 deny = "never"
4138 initial_balance = '0xffffffffffffffffffffffff'
4139 libraries = []
4140 libs = ['lib']
4141 memory_limit = 134217728
4142 names = false
4143 no_storage_caching = false
4144 no_rpc_rate_limit = false
4145 offline = false
4146 optimizer = true
4147 optimizer_runs = 200
4148 out = 'out'
4149 remappings = ['nested/=lib/nested/']
4150 sender = '0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38'
4151 sizes = false
4152 sparse_mode = false
4153 src = 'src'
4154 test = 'test'
4155 tx_origin = '0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38'
4156 verbosity = 0
4157 via_ir = false
4158
4159 [profile.default.rpc_storage_caching]
4160 chains = 'all'
4161 endpoints = 'all'
4162
4163 [rpc_endpoints]
4164 optimism = "https://example.com/"
4165 mainnet = "${RPC_MAINNET}"
4166 mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}"
4167
4168 [fuzz]
4169 runs = 256
4170 seed = '0x3e8'
4171 max_test_rejects = 65536
4172
4173 [invariant]
4174 runs = 256
4175 depth = 500
4176 fail_on_revert = false
4177 call_override = false
4178 shrink_run_limit = 5000
4179 "#,
4180 )?;
4181
4182 let config = Config::load_with_root(jail.directory()).unwrap();
4183
4184 assert_eq!(config.ignored_file_paths, vec![PathBuf::from("something")]);
4185 assert_eq!(config.fuzz.seed, Some(U256::from(1000)));
4186 assert_eq!(
4187 config.remappings,
4188 vec![Remapping::from_str("nested/=lib/nested/").unwrap().into()]
4189 );
4190
4191 assert_eq!(
4192 config.rpc_endpoints,
4193 RpcEndpoints::new([
4194 ("optimism", RpcEndpointUrl::Url("https://example.com/".to_string())),
4195 ("mainnet", RpcEndpointUrl::Env("${RPC_MAINNET}".to_string())),
4196 (
4197 "mainnet_2",
4198 RpcEndpointUrl::Env(
4199 "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}".to_string()
4200 )
4201 ),
4202 ]),
4203 );
4204
4205 Ok(())
4206 });
4207 }
4208
4209 #[test]
4210 fn test_solc_req() {
4211 figment::Jail::expect_with(|jail| {
4212 jail.create_file(
4213 "foundry.toml",
4214 r#"
4215 [profile.default]
4216 solc_version = "0.8.12"
4217 "#,
4218 )?;
4219
4220 let config = Config::load().unwrap();
4221 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));
4222
4223 jail.create_file(
4224 "foundry.toml",
4225 r#"
4226 [profile.default]
4227 solc = "0.8.12"
4228 "#,
4229 )?;
4230
4231 let config = Config::load().unwrap();
4232 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));
4233
4234 jail.create_file(
4235 "foundry.toml",
4236 r#"
4237 [profile.default]
4238 solc = "path/to/local/solc"
4239 "#,
4240 )?;
4241
4242 let config = Config::load().unwrap();
4243 assert_eq!(config.solc, Some(SolcReq::Local("path/to/local/solc".into())));
4244
4245 jail.set_env("FOUNDRY_SOLC_VERSION", "0.6.6");
4246 let config = Config::load().unwrap();
4247 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 6, 6))));
4248 Ok(())
4249 });
4250 }
4251
4252 #[test]
4254 fn test_backwards_solc_version() {
4255 figment::Jail::expect_with(|jail| {
4256 jail.create_file(
4257 "foundry.toml",
4258 r#"
4259 [default]
4260 solc = "0.8.12"
4261 solc_version = "0.8.20"
4262 "#,
4263 )?;
4264
4265 let config = Config::load().unwrap();
4266 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));
4267
4268 Ok(())
4269 });
4270
4271 figment::Jail::expect_with(|jail| {
4272 jail.create_file(
4273 "foundry.toml",
4274 r#"
4275 [default]
4276 solc_version = "0.8.20"
4277 "#,
4278 )?;
4279
4280 let config = Config::load().unwrap();
4281 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 20))));
4282
4283 Ok(())
4284 });
4285 }
4286
4287 #[test]
4288 fn test_toml_casing_file() {
4289 figment::Jail::expect_with(|jail| {
4290 jail.create_file(
4291 "foundry.toml",
4292 r#"
4293 [profile.default]
4294 src = "some-source"
4295 out = "some-out"
4296 cache = true
4297 eth-rpc-url = "https://example.com/"
4298 evm-version = "berlin"
4299 auto-detect-solc = false
4300 "#,
4301 )?;
4302
4303 let config = Config::load().unwrap();
4304 assert_eq!(
4305 config,
4306 Config {
4307 src: "some-source".into(),
4308 out: "some-out".into(),
4309 cache: true,
4310 eth_rpc_url: Some("https://example.com/".to_string()),
4311 auto_detect_solc: false,
4312 evm_version: EvmVersion::Berlin,
4313 ..Config::default().normalized_optimizer_settings()
4314 }
4315 );
4316
4317 Ok(())
4318 });
4319 }
4320
4321 #[test]
4322 fn test_output_selection() {
4323 figment::Jail::expect_with(|jail| {
4324 jail.create_file(
4325 "foundry.toml",
4326 r#"
4327 [profile.default]
4328 extra_output = ["metadata", "ir-optimized"]
4329 extra_output_files = ["metadata"]
4330 "#,
4331 )?;
4332
4333 let config = Config::load().unwrap();
4334
4335 assert_eq!(
4336 config.extra_output,
4337 vec![ContractOutputSelection::Metadata, ContractOutputSelection::IrOptimized]
4338 );
4339 assert_eq!(config.extra_output_files, vec![ContractOutputSelection::Metadata]);
4340
4341 Ok(())
4342 });
4343 }
4344
4345 #[test]
4346 fn test_precedence() {
4347 figment::Jail::expect_with(|jail| {
4348 jail.create_file(
4349 "foundry.toml",
4350 r#"
4351 [profile.default]
4352 src = "mysrc"
4353 out = "myout"
4354 verbosity = 3
4355 "#,
4356 )?;
4357
4358 let config = Config::load().unwrap();
4359 assert_eq!(
4360 config,
4361 Config {
4362 src: "mysrc".into(),
4363 out: "myout".into(),
4364 verbosity: 3,
4365 ..Config::default().normalized_optimizer_settings()
4366 }
4367 );
4368
4369 jail.set_env("FOUNDRY_SRC", r"other-src");
4370 let config = Config::load().unwrap();
4371 assert_eq!(
4372 config,
4373 Config {
4374 src: "other-src".into(),
4375 out: "myout".into(),
4376 verbosity: 3,
4377 ..Config::default().normalized_optimizer_settings()
4378 }
4379 );
4380
4381 jail.set_env("FOUNDRY_PROFILE", "foo");
4382 let val: Result<String, _> = Config::figment().extract_inner("profile");
4383 assert!(val.is_err());
4384
4385 Ok(())
4386 });
4387 }
4388
4389 #[test]
4390 fn test_extract_basic() {
4391 figment::Jail::expect_with(|jail| {
4392 jail.create_file(
4393 "foundry.toml",
4394 r#"
4395 [profile.default]
4396 src = "mysrc"
4397 out = "myout"
4398 verbosity = 3
4399 evm_version = 'berlin'
4400
4401 [profile.other]
4402 src = "other-src"
4403 "#,
4404 )?;
4405 let loaded = Config::load().unwrap();
4406 assert_eq!(loaded.evm_version, EvmVersion::Berlin);
4407 let base = loaded.into_basic();
4408 let default = Config::default();
4409 assert_eq!(
4410 base,
4411 BasicConfig {
4412 profile: Config::DEFAULT_PROFILE,
4413 src: "mysrc".into(),
4414 out: "myout".into(),
4415 libs: default.libs.clone(),
4416 remappings: default.remappings.clone(),
4417 network: None,
4418 }
4419 );
4420 jail.set_env("FOUNDRY_PROFILE", r"other");
4421 let base = Config::figment().extract::<BasicConfig>().unwrap();
4422 assert_eq!(
4423 base,
4424 BasicConfig {
4425 profile: Config::DEFAULT_PROFILE,
4426 src: "other-src".into(),
4427 out: "myout".into(),
4428 libs: default.libs.clone(),
4429 remappings: default.remappings,
4430 network: None,
4431 }
4432 );
4433 Ok(())
4434 });
4435 }
4436
4437 #[test]
4438 #[should_panic]
4439 fn test_parse_invalid_fuzz_weight() {
4440 figment::Jail::expect_with(|jail| {
4441 jail.create_file(
4442 "foundry.toml",
4443 r"
4444 [fuzz]
4445 dictionary_weight = 101
4446 ",
4447 )?;
4448 let _config = Config::load().unwrap();
4449 Ok(())
4450 });
4451 }
4452
4453 #[test]
4454 fn test_fallback_provider() {
4455 figment::Jail::expect_with(|jail| {
4456 jail.create_file(
4457 "foundry.toml",
4458 r"
4459 [fuzz]
4460 runs = 1
4461 include_storage = false
4462 dictionary_weight = 99
4463
4464 [invariant]
4465 runs = 420
4466
4467 [profile.ci.fuzz]
4468 dictionary_weight = 5
4469
4470 [profile.ci.invariant]
4471 runs = 400
4472 ",
4473 )?;
4474
4475 let invariant_default = InvariantConfig::default();
4476 let config = Config::load().unwrap();
4477
4478 assert_ne!(config.invariant.runs, config.fuzz.runs);
4479 assert_eq!(config.invariant.runs, 420);
4480
4481 assert_ne!(
4482 config.fuzz.dictionary.include_storage,
4483 invariant_default.dictionary.include_storage
4484 );
4485 assert_eq!(
4486 config.invariant.dictionary.include_storage,
4487 config.fuzz.dictionary.include_storage
4488 );
4489
4490 assert_ne!(
4491 config.fuzz.dictionary.dictionary_weight,
4492 invariant_default.dictionary.dictionary_weight
4493 );
4494 assert_eq!(
4495 config.invariant.dictionary.dictionary_weight,
4496 config.fuzz.dictionary.dictionary_weight
4497 );
4498
4499 jail.set_env("FOUNDRY_PROFILE", "ci");
4500 let ci_config = Config::load().unwrap();
4501 assert_eq!(ci_config.fuzz.runs, 1);
4502 assert_eq!(ci_config.invariant.runs, 400);
4503 assert_eq!(ci_config.fuzz.dictionary.dictionary_weight, 5);
4504 assert_eq!(
4505 ci_config.invariant.dictionary.dictionary_weight,
4506 config.fuzz.dictionary.dictionary_weight
4507 );
4508
4509 Ok(())
4510 })
4511 }
4512
4513 #[test]
4514 fn test_standalone_profile_sections() {
4515 figment::Jail::expect_with(|jail| {
4516 jail.create_file(
4517 "foundry.toml",
4518 r"
4519 [fuzz]
4520 runs = 100
4521
4522 [invariant]
4523 runs = 120
4524
4525 [profile.ci.fuzz]
4526 runs = 420
4527
4528 [profile.ci.invariant]
4529 runs = 500
4530 ",
4531 )?;
4532
4533 let config = Config::load().unwrap();
4534 assert_eq!(config.fuzz.runs, 100);
4535 assert_eq!(config.invariant.runs, 120);
4536
4537 jail.set_env("FOUNDRY_PROFILE", "ci");
4538 let config = Config::load().unwrap();
4539 assert_eq!(config.fuzz.runs, 420);
4540 assert_eq!(config.invariant.runs, 500);
4541
4542 Ok(())
4543 });
4544 }
4545
4546 #[test]
4547 fn can_handle_deviating_dapp_aliases() {
4548 figment::Jail::expect_with(|jail| {
4549 let addr = Address::ZERO;
4550 jail.set_env("DAPP_TEST_NUMBER", 1337);
4551 jail.set_env("DAPP_TEST_ADDRESS", format!("{addr:?}"));
4552 jail.set_env("DAPP_TEST_FUZZ_RUNS", 420);
4553 jail.set_env("DAPP_TEST_DEPTH", 20);
4554 jail.set_env("DAPP_FORK_BLOCK", 100);
4555 jail.set_env("DAPP_BUILD_OPTIMIZE_RUNS", 999);
4556 jail.set_env("DAPP_BUILD_OPTIMIZE", 0);
4557
4558 let config = Config::load().unwrap();
4559
4560 assert_eq!(config.block_number, U256::from(1337));
4561 assert_eq!(config.sender, addr);
4562 assert_eq!(config.fuzz.runs, 420);
4563 assert_eq!(config.invariant.depth, 20);
4564 assert_eq!(config.fork_block_number, Some(100));
4565 assert_eq!(config.optimizer_runs, Some(999));
4566 assert!(!config.optimizer.unwrap());
4567
4568 Ok(())
4569 });
4570 }
4571
4572 #[test]
4573 fn can_parse_libraries() {
4574 figment::Jail::expect_with(|jail| {
4575 jail.set_env(
4576 "DAPP_LIBRARIES",
4577 "[src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6]",
4578 );
4579 let config = Config::load().unwrap();
4580 assert_eq!(
4581 config.libraries,
4582 vec![
4583 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4584 .to_string()
4585 ]
4586 );
4587
4588 jail.set_env(
4589 "DAPP_LIBRARIES",
4590 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6",
4591 );
4592 let config = Config::load().unwrap();
4593 assert_eq!(
4594 config.libraries,
4595 vec![
4596 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4597 .to_string(),
4598 ]
4599 );
4600
4601 jail.set_env(
4602 "DAPP_LIBRARIES",
4603 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6,src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6",
4604 );
4605 let config = Config::load().unwrap();
4606 assert_eq!(
4607 config.libraries,
4608 vec![
4609 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4610 .to_string(),
4611 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
4612 .to_string()
4613 ]
4614 );
4615
4616 Ok(())
4617 });
4618 }
4619
4620 #[test]
4621 fn test_parse_many_libraries() {
4622 figment::Jail::expect_with(|jail| {
4623 jail.create_file(
4624 "foundry.toml",
4625 r"
4626 [profile.default]
4627 libraries= [
4628 './src/SizeAuctionDiscount.sol:Chainlink:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
4629 './src/SizeAuction.sol:ChainlinkTWAP:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
4630 './src/SizeAuction.sol:Math:0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c',
4631 './src/test/ChainlinkTWAP.t.sol:ChainlinkTWAP:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
4632 './src/SizeAuctionDiscount.sol:Math:0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c',
4633 ]
4634 ",
4635 )?;
4636 let config = Config::load().unwrap();
4637
4638 let libs = config.parsed_libraries().unwrap().libs;
4639
4640 similar_asserts::assert_eq!(
4641 libs,
4642 BTreeMap::from([
4643 (
4644 PathBuf::from("./src/SizeAuctionDiscount.sol"),
4645 BTreeMap::from([
4646 (
4647 "Chainlink".to_string(),
4648 "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
4649 ),
4650 (
4651 "Math".to_string(),
4652 "0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c".to_string()
4653 )
4654 ])
4655 ),
4656 (
4657 PathBuf::from("./src/SizeAuction.sol"),
4658 BTreeMap::from([
4659 (
4660 "ChainlinkTWAP".to_string(),
4661 "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
4662 ),
4663 (
4664 "Math".to_string(),
4665 "0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c".to_string()
4666 )
4667 ])
4668 ),
4669 (
4670 PathBuf::from("./src/test/ChainlinkTWAP.t.sol"),
4671 BTreeMap::from([(
4672 "ChainlinkTWAP".to_string(),
4673 "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
4674 )])
4675 ),
4676 ])
4677 );
4678
4679 Ok(())
4680 });
4681 }
4682
4683 #[test]
4684 fn config_roundtrip() {
4685 figment::Jail::expect_with(|jail| {
4686 let default = Config::default().normalized_optimizer_settings();
4687 let basic = default.clone().into_basic();
4688 jail.create_file("foundry.toml", &basic.to_string_pretty().unwrap())?;
4689
4690 let mut other = Config::load().unwrap();
4691 clear_warning(&mut other);
4692 assert_eq!(default, other);
4693
4694 let other = other.into_basic();
4695 assert_eq!(basic, other);
4696
4697 jail.create_file("foundry.toml", &default.to_string_pretty().unwrap())?;
4698 let mut other = Config::load().unwrap();
4699 clear_warning(&mut other);
4700 assert_eq!(default, other);
4701
4702 Ok(())
4703 });
4704 }
4705
4706 #[test]
4707 fn test_fs_permissions() {
4708 figment::Jail::expect_with(|jail| {
4709 jail.create_file(
4710 "foundry.toml",
4711 r#"
4712 [profile.default]
4713 fs_permissions = [{ access = "read-write", path = "./"}]
4714 "#,
4715 )?;
4716 let loaded = Config::load().unwrap();
4717
4718 assert_eq!(
4719 loaded.fs_permissions,
4720 FsPermissions::new(vec![PathPermission::read_write("./")])
4721 );
4722
4723 jail.create_file(
4724 "foundry.toml",
4725 r#"
4726 [profile.default]
4727 fs_permissions = [{ access = "none", path = "./"}]
4728 "#,
4729 )?;
4730 let loaded = Config::load().unwrap();
4731 assert_eq!(loaded.fs_permissions, FsPermissions::new(vec![PathPermission::none("./")]));
4732
4733 Ok(())
4734 });
4735 }
4736
4737 #[test]
4738 fn test_optimizer_settings_basic() {
4739 figment::Jail::expect_with(|jail| {
4740 jail.create_file(
4741 "foundry.toml",
4742 r"
4743 [profile.default]
4744 optimizer = true
4745
4746 [profile.default.optimizer_details]
4747 yul = false
4748
4749 [profile.default.optimizer_details.yulDetails]
4750 stackAllocation = true
4751 ",
4752 )?;
4753 let mut loaded = Config::load().unwrap();
4754 clear_warning(&mut loaded);
4755 assert_eq!(
4756 loaded.optimizer_details,
4757 Some(OptimizerDetails {
4758 yul: Some(false),
4759 yul_details: Some(YulDetails {
4760 stack_allocation: Some(true),
4761 ..Default::default()
4762 }),
4763 ..Default::default()
4764 })
4765 );
4766
4767 let s = loaded.to_string_pretty().unwrap();
4768 jail.create_file("foundry.toml", &s)?;
4769
4770 let mut reloaded = Config::load().unwrap();
4771 clear_warning(&mut reloaded);
4772 assert_eq!(loaded, reloaded);
4773
4774 Ok(())
4775 });
4776 }
4777
4778 #[test]
4779 fn test_model_checker_settings_basic() {
4780 figment::Jail::expect_with(|jail| {
4781 jail.create_file(
4782 "foundry.toml",
4783 r"
4784 [profile.default]
4785
4786 [profile.default.model_checker]
4787 contracts = { 'a.sol' = [ 'A1', 'A2' ], 'b.sol' = [ 'B1', 'B2' ] }
4788 engine = 'chc'
4789 targets = [ 'assert', 'outOfBounds' ]
4790 timeout = 10000
4791 ",
4792 )?;
4793 let mut loaded = Config::load().unwrap();
4794 clear_warning(&mut loaded);
4795 assert_eq!(
4796 loaded.model_checker,
4797 Some(ModelCheckerSettings {
4798 contracts: BTreeMap::from([
4799 ("a.sol".to_string(), vec!["A1".to_string(), "A2".to_string()]),
4800 ("b.sol".to_string(), vec!["B1".to_string(), "B2".to_string()]),
4801 ]),
4802 engine: Some(ModelCheckerEngine::CHC),
4803 targets: Some(vec![
4804 ModelCheckerTarget::Assert,
4805 ModelCheckerTarget::OutOfBounds
4806 ]),
4807 timeout: Some(10000),
4808 invariants: None,
4809 show_unproved: None,
4810 div_mod_with_slacks: None,
4811 solvers: None,
4812 show_unsupported: None,
4813 show_proved_safe: None,
4814 })
4815 );
4816
4817 let s = loaded.to_string_pretty().unwrap();
4818 jail.create_file("foundry.toml", &s)?;
4819
4820 let mut reloaded = Config::load().unwrap();
4821 clear_warning(&mut reloaded);
4822 assert_eq!(loaded, reloaded);
4823
4824 Ok(())
4825 });
4826 }
4827
4828 #[test]
4829 fn test_model_checker_settings_with_bool_flags() {
4830 figment::Jail::expect_with(|jail| {
4831 jail.create_file(
4832 "foundry.toml",
4833 r"
4834 [profile.default]
4835
4836 [profile.default.model_checker]
4837 engine = 'chc'
4838 show_unproved = true
4839 show_unsupported = true
4840 show_proved_safe = false
4841 div_mod_with_slacks = true
4842 ",
4843 )?;
4844 let mut loaded = Config::load().unwrap();
4845 clear_warning(&mut loaded);
4846
4847 let mc = loaded.model_checker.as_ref().unwrap();
4848 assert_eq!(mc.show_unproved, Some(true));
4849 assert_eq!(mc.show_unsupported, Some(true));
4850 assert_eq!(mc.show_proved_safe, Some(false));
4851 assert_eq!(mc.div_mod_with_slacks, Some(true));
4852
4853 let s = loaded.to_string_pretty().unwrap();
4855 jail.create_file("foundry.toml", &s)?;
4856
4857 let mut reloaded = Config::load().unwrap();
4858 clear_warning(&mut reloaded);
4859
4860 let mc_reloaded = reloaded.model_checker.as_ref().unwrap();
4861 assert_eq!(mc_reloaded.show_unproved, Some(true));
4862 assert_eq!(mc_reloaded.show_unsupported, Some(true));
4863 assert_eq!(mc_reloaded.show_proved_safe, Some(false));
4864 assert_eq!(mc_reloaded.div_mod_with_slacks, Some(true));
4865
4866 Ok(())
4867 });
4868 }
4869
4870 #[test]
4871 fn test_model_checker_settings_relative_paths() {
4872 figment::Jail::expect_with(|jail| {
4873 jail.create_file(
4874 "foundry.toml",
4875 r"
4876 [profile.default]
4877
4878 [profile.default.model_checker]
4879 contracts = { 'a.sol' = [ 'A1', 'A2' ], 'b.sol' = [ 'B1', 'B2' ] }
4880 engine = 'chc'
4881 targets = [ 'assert', 'outOfBounds' ]
4882 timeout = 10000
4883 ",
4884 )?;
4885 let loaded = Config::load().unwrap().sanitized();
4886
4887 let dir = foundry_compilers::utils::canonicalize(jail.directory())
4892 .expect("Could not canonicalize jail path");
4893 assert_eq!(
4894 loaded.model_checker,
4895 Some(ModelCheckerSettings {
4896 contracts: BTreeMap::from([
4897 (
4898 format!("{}", dir.join("a.sol").display()),
4899 vec!["A1".to_string(), "A2".to_string()]
4900 ),
4901 (
4902 format!("{}", dir.join("b.sol").display()),
4903 vec!["B1".to_string(), "B2".to_string()]
4904 ),
4905 ]),
4906 engine: Some(ModelCheckerEngine::CHC),
4907 targets: Some(vec![
4908 ModelCheckerTarget::Assert,
4909 ModelCheckerTarget::OutOfBounds
4910 ]),
4911 timeout: Some(10000),
4912 invariants: None,
4913 show_unproved: None,
4914 div_mod_with_slacks: None,
4915 solvers: None,
4916 show_unsupported: None,
4917 show_proved_safe: None,
4918 })
4919 );
4920
4921 Ok(())
4922 });
4923 }
4924
4925 #[test]
4926 fn test_fmt_config() {
4927 figment::Jail::expect_with(|jail| {
4928 jail.create_file(
4929 "foundry.toml",
4930 r#"
4931 [fmt]
4932 line_length = 100
4933 tab_width = 2
4934 bracket_spacing = true
4935 style = "space"
4936 "#,
4937 )?;
4938 let loaded = Config::load().unwrap().sanitized();
4939 assert_eq!(
4940 loaded.fmt,
4941 FormatterConfig {
4942 line_length: 100,
4943 tab_width: 2,
4944 bracket_spacing: true,
4945 style: IndentStyle::Space,
4946 ..Default::default()
4947 }
4948 );
4949
4950 Ok(())
4951 });
4952 }
4953
4954 #[test]
4955 fn test_lint_config() {
4956 figment::Jail::expect_with(|jail| {
4957 jail.create_file(
4958 "foundry.toml",
4959 r"
4960 [lint]
4961 severity = ['high', 'medium']
4962 exclude_lints = ['incorrect-shift']
4963 ",
4964 )?;
4965 let loaded = Config::load().unwrap().sanitized();
4966 assert_eq!(
4967 loaded.lint,
4968 LinterConfig {
4969 severity: vec![LintSeverity::High, LintSeverity::Med],
4970 exclude_lints: vec!["incorrect-shift".into()],
4971 ..Default::default()
4972 }
4973 );
4974
4975 Ok(())
4976 });
4977 }
4978
4979 #[test]
4980 fn test_invariant_config() {
4981 figment::Jail::expect_with(|jail| {
4982 jail.create_file(
4983 "foundry.toml",
4984 r"
4985 [invariant]
4986 runs = 512
4987 depth = 10
4988 ",
4989 )?;
4990
4991 let loaded = Config::load().unwrap().sanitized();
4992 assert_eq!(
4993 loaded.invariant,
4994 InvariantConfig {
4995 runs: 512,
4996 depth: 10,
4997 failure_persist_dir: Some(PathBuf::from("cache/invariant")),
4998 ..Default::default()
4999 }
5000 );
5001
5002 Ok(())
5003 });
5004 }
5005
5006 #[test]
5007 fn test_standalone_sections_env() {
5008 figment::Jail::expect_with(|jail| {
5009 jail.create_file(
5010 "foundry.toml",
5011 r"
5012 [fuzz]
5013 runs = 100
5014
5015 [invariant]
5016 depth = 1
5017 ",
5018 )?;
5019
5020 jail.set_env("FOUNDRY_FMT_LINE_LENGTH", "95");
5021 jail.set_env("FOUNDRY_FUZZ_DICTIONARY_WEIGHT", "99");
5022 jail.set_env("FOUNDRY_INVARIANT_DEPTH", "5");
5023
5024 let config = Config::load().unwrap();
5025 assert_eq!(config.fmt.line_length, 95);
5026 assert_eq!(config.fuzz.dictionary.dictionary_weight, 99);
5027 assert_eq!(config.invariant.depth, 5);
5028
5029 Ok(())
5030 });
5031 }
5032
5033 #[test]
5034 fn test_parse_with_profile() {
5035 let foundry_str = r"
5036 [profile.default]
5037 src = 'src'
5038 out = 'out'
5039 libs = ['lib']
5040
5041 # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
5042 ";
5043 assert_eq!(
5044 parse_with_profile::<BasicConfig>(foundry_str).unwrap().unwrap(),
5045 (
5046 Config::DEFAULT_PROFILE,
5047 BasicConfig {
5048 profile: Config::DEFAULT_PROFILE,
5049 src: "src".into(),
5050 out: "out".into(),
5051 libs: vec!["lib".into()],
5052 remappings: vec![],
5053 network: None,
5054 }
5055 )
5056 );
5057 }
5058
5059 #[test]
5060 fn test_implicit_profile_loads() {
5061 figment::Jail::expect_with(|jail| {
5062 jail.create_file(
5063 "foundry.toml",
5064 r"
5065 [default]
5066 src = 'my-src'
5067 out = 'my-out'
5068 ",
5069 )?;
5070 let loaded = Config::load().unwrap().sanitized();
5071 assert_eq!(loaded.src.file_name().unwrap(), "my-src");
5072 assert_eq!(loaded.out.file_name().unwrap(), "my-out");
5073 assert_eq!(
5074 loaded.warnings,
5075 vec![Warning::UnknownSection {
5076 unknown_section: Profile::new("default"),
5077 source: Some("foundry.toml".into())
5078 }]
5079 );
5080
5081 Ok(())
5082 });
5083 }
5084
5085 #[test]
5086 fn hardfork_overrides_spec_id() {
5087 let config = Config {
5088 hardfork: Some(FoundryHardfork::Tempo(TempoHardfork::T3)),
5089 ..Config::default()
5090 };
5091
5092 assert_eq!(config.evm_spec_id::<TempoHardfork>(), TempoHardfork::T3);
5093 }
5094
5095 #[test]
5096 fn tempo_network_defaults_to_latest_tempo_hardfork() {
5097 figment::Jail::expect_with(|jail| {
5098 jail.create_file(
5099 "foundry.toml",
5100 r#"
5101 [profile.default]
5102 network = "tempo"
5103 "#,
5104 )?;
5105
5106 let config = Config::load().unwrap();
5107 assert!(config.networks.is_tempo());
5108 assert_eq!(config.evm_spec_id::<TempoHardfork>(), latest_active_tempo_hardfork());
5109
5110 Ok(())
5111 });
5112 }
5113
5114 #[test]
5115 fn tempo_hardfork_infers_tempo_network() {
5116 figment::Jail::expect_with(|jail| {
5117 jail.create_file(
5118 "foundry.toml",
5119 r#"
5120 [profile.default]
5121 hardfork = "tempo:T3"
5122 "#,
5123 )?;
5124
5125 let config = Config::load().unwrap();
5126 assert_eq!(config.hardfork, Some(FoundryHardfork::Tempo(TempoHardfork::T3)));
5127 assert!(config.networks.is_tempo());
5128
5129 Ok(())
5130 });
5131 }
5132
5133 #[test]
5134 fn hardfork_rejects_conflicting_network() {
5135 figment::Jail::expect_with(|jail| {
5136 jail.create_file(
5137 "foundry.toml",
5138 r#"
5139 [profile.default]
5140 tempo = true
5141 hardfork = "shanghai"
5142 "#,
5143 )?;
5144
5145 let err = Config::load().unwrap_err();
5146 assert!(
5147 err.to_string()
5148 .to_lowercase()
5149 .contains("hardfork `shanghai` conflicts with network config `tempo`")
5150 );
5151
5152 Ok(())
5153 });
5154 }
5155
5156 #[test]
5157 fn test_etherscan_api_key() {
5158 figment::Jail::expect_with(|jail| {
5159 jail.create_file(
5160 "foundry.toml",
5161 r"
5162 [default]
5163 ",
5164 )?;
5165 jail.set_env("ETHERSCAN_API_KEY", "");
5166 let loaded = Config::load().unwrap().sanitized();
5167 assert!(loaded.etherscan_api_key.is_none());
5168
5169 jail.set_env("ETHERSCAN_API_KEY", "DUMMY");
5170 let loaded = Config::load().unwrap().sanitized();
5171 assert_eq!(loaded.etherscan_api_key, Some("DUMMY".into()));
5172
5173 Ok(())
5174 });
5175 }
5176
5177 #[test]
5178 fn test_etherscan_api_key_figment() {
5179 figment::Jail::expect_with(|jail| {
5180 jail.create_file(
5181 "foundry.toml",
5182 r"
5183 [default]
5184 etherscan_api_key = 'DUMMY'
5185 ",
5186 )?;
5187 jail.set_env("ETHERSCAN_API_KEY", "ETHER");
5188
5189 let figment = Config::figment_with_root(jail.directory())
5190 .merge(("etherscan_api_key", "USER_KEY"));
5191
5192 let loaded = Config::from_provider(figment).unwrap();
5193 assert_eq!(loaded.etherscan_api_key, Some("USER_KEY".into()));
5194
5195 Ok(())
5196 });
5197 }
5198
5199 #[test]
5200 fn test_normalize_defaults() {
5201 figment::Jail::expect_with(|jail| {
5202 jail.create_file(
5203 "foundry.toml",
5204 r"
5205 [default]
5206 solc = '0.8.13'
5207 ",
5208 )?;
5209
5210 let loaded = Config::load().unwrap().sanitized();
5211 assert_eq!(loaded.evm_version, EvmVersion::London);
5212 Ok(())
5213 });
5214 }
5215
5216 #[expect(clippy::disallowed_macros)]
5218 #[test]
5219 #[ignore]
5220 fn print_config() {
5221 let config = Config {
5222 optimizer_details: Some(OptimizerDetails {
5223 peephole: None,
5224 inliner: None,
5225 jumpdest_remover: None,
5226 order_literals: None,
5227 deduplicate: None,
5228 cse: None,
5229 constant_optimizer: Some(true),
5230 yul: Some(true),
5231 yul_details: Some(YulDetails {
5232 stack_allocation: None,
5233 optimizer_steps: Some("dhfoDgvulfnTUtnIf".to_string()),
5234 }),
5235 simple_counter_for_loop_unchecked_increment: None,
5236 }),
5237 ..Default::default()
5238 };
5239 println!("{}", config.to_string_pretty().unwrap());
5240 }
5241
5242 #[test]
5243 fn can_use_impl_figment_macro() {
5244 #[derive(Default, Serialize)]
5245 struct MyArgs {
5246 #[serde(skip_serializing_if = "Option::is_none")]
5247 root: Option<PathBuf>,
5248 }
5249 impl_figment_convert!(MyArgs);
5250
5251 impl Provider for MyArgs {
5252 fn metadata(&self) -> Metadata {
5253 Metadata::default()
5254 }
5255
5256 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
5257 let value = Value::serialize(self)?;
5258 let error = InvalidType(value.to_actual(), "map".into());
5259 let dict = value.into_dict().ok_or(error)?;
5260 Ok(Map::from([(Config::selected_profile(), dict)]))
5261 }
5262 }
5263
5264 let _figment: Figment = From::from(&MyArgs::default());
5265
5266 #[derive(Default)]
5267 struct Outer {
5268 start: MyArgs,
5269 other: MyArgs,
5270 another: MyArgs,
5271 }
5272 impl_figment_convert!(Outer, start, other, another);
5273
5274 let _figment: Figment = From::from(&Outer::default());
5275 }
5276
5277 #[test]
5278 fn list_cached_blocks() -> eyre::Result<()> {
5279 fn fake_block_cache(chain_path: &Path, block_number: &str, size_bytes: usize) {
5280 let block_path = chain_path.join(block_number);
5281 fs::create_dir(block_path.as_path()).unwrap();
5282 let file_path = block_path.join("storage.json");
5283 let mut file = File::create(file_path).unwrap();
5284 writeln!(file, "{}", vec![' '; size_bytes - 1].iter().collect::<String>()).unwrap();
5285 }
5286
5287 fn fake_block_cache_block_path_as_file(
5288 chain_path: &Path,
5289 block_number: &str,
5290 size_bytes: usize,
5291 ) {
5292 let block_path = chain_path.join(block_number);
5293 let mut file = File::create(block_path).unwrap();
5294 writeln!(file, "{}", vec![' '; size_bytes - 1].iter().collect::<String>()).unwrap();
5295 }
5296
5297 let chain_dir = tempdir()?;
5298
5299 fake_block_cache(chain_dir.path(), "1", 100);
5300 fake_block_cache(chain_dir.path(), "2", 500);
5301 fake_block_cache_block_path_as_file(chain_dir.path(), "3", 900);
5302 let mut pol_file = File::create(chain_dir.path().join("pol.txt")).unwrap();
5304 writeln!(pol_file, "{}", [' '; 10].iter().collect::<String>()).unwrap();
5305
5306 let result = Config::get_cached_blocks(chain_dir.path())?;
5307
5308 assert_eq!(result.len(), 3);
5309 let block1 = &result.iter().find(|x| x.0 == "1").unwrap();
5310 let block2 = &result.iter().find(|x| x.0 == "2").unwrap();
5311 let block3 = &result.iter().find(|x| x.0 == "3").unwrap();
5312
5313 assert_eq!(block1.0, "1");
5314 assert_eq!(block1.1, 100);
5315 assert_eq!(block2.0, "2");
5316 assert_eq!(block2.1, 500);
5317 assert_eq!(block3.0, "3");
5318 assert_eq!(block3.1, 900);
5319
5320 chain_dir.close()?;
5321 Ok(())
5322 }
5323
5324 #[test]
5325 fn list_etherscan_cache() -> eyre::Result<()> {
5326 fn fake_etherscan_cache(chain_path: &Path, address: &str, size_bytes: usize) {
5327 let metadata_path = chain_path.join("sources");
5328 let abi_path = chain_path.join("abi");
5329 let _ = fs::create_dir(metadata_path.as_path());
5330 let _ = fs::create_dir(abi_path.as_path());
5331
5332 let metadata_file_path = metadata_path.join(address);
5333 let mut metadata_file = File::create(metadata_file_path).unwrap();
5334 writeln!(metadata_file, "{}", vec![' '; size_bytes / 2 - 1].iter().collect::<String>())
5335 .unwrap();
5336
5337 let abi_file_path = abi_path.join(address);
5338 let mut abi_file = File::create(abi_file_path).unwrap();
5339 writeln!(abi_file, "{}", vec![' '; size_bytes / 2 - 1].iter().collect::<String>())
5340 .unwrap();
5341 }
5342
5343 let chain_dir = tempdir()?;
5344
5345 fake_etherscan_cache(chain_dir.path(), "1", 100);
5346 fake_etherscan_cache(chain_dir.path(), "2", 500);
5347
5348 let result = Config::get_cached_block_explorer_data(chain_dir.path())?;
5349
5350 assert_eq!(result, 600);
5351
5352 chain_dir.close()?;
5353 Ok(())
5354 }
5355
5356 #[test]
5357 fn test_parse_error_codes() {
5358 figment::Jail::expect_with(|jail| {
5359 jail.create_file(
5360 "foundry.toml",
5361 r#"
5362 [default]
5363 ignored_error_codes = ["license", "unreachable", 1337]
5364 "#,
5365 )?;
5366
5367 let config = Config::load().unwrap();
5368 assert_eq!(
5369 config.ignored_error_codes,
5370 vec![
5371 SolidityErrorCode::SpdxLicenseNotProvided,
5372 SolidityErrorCode::Unreachable,
5373 SolidityErrorCode::Other(1337)
5374 ]
5375 );
5376
5377 Ok(())
5378 });
5379 }
5380
5381 #[test]
5382 fn test_parse_file_paths() {
5383 figment::Jail::expect_with(|jail| {
5384 jail.create_file(
5385 "foundry.toml",
5386 r#"
5387 [default]
5388 ignored_warnings_from = ["something"]
5389 "#,
5390 )?;
5391
5392 let config = Config::load().unwrap();
5393 assert_eq!(config.ignored_file_paths, vec![Path::new("something").to_path_buf()]);
5394
5395 Ok(())
5396 });
5397 }
5398
5399 #[test]
5400 fn test_parse_optimizer_settings() {
5401 figment::Jail::expect_with(|jail| {
5402 jail.create_file(
5403 "foundry.toml",
5404 r"
5405 [default]
5406 [profile.default.optimizer_details]
5407 ",
5408 )?;
5409
5410 let config = Config::load().unwrap();
5411 assert_eq!(config.optimizer_details, Some(OptimizerDetails::default()));
5412
5413 Ok(())
5414 });
5415 }
5416
5417 #[test]
5418 fn test_parse_labels() {
5419 figment::Jail::expect_with(|jail| {
5420 jail.create_file(
5421 "foundry.toml",
5422 r#"
5423 [labels]
5424 0x1F98431c8aD98523631AE4a59f267346ea31F984 = "Uniswap V3: Factory"
5425 0xC36442b4a4522E871399CD717aBDD847Ab11FE88 = "Uniswap V3: Positions NFT"
5426 "#,
5427 )?;
5428
5429 let config = Config::load().unwrap();
5430 assert_eq!(
5431 config.labels,
5432 AddressHashMap::from_iter(vec![
5433 (
5434 address!("0x1F98431c8aD98523631AE4a59f267346ea31F984"),
5435 "Uniswap V3: Factory".to_string()
5436 ),
5437 (
5438 address!("0xC36442b4a4522E871399CD717aBDD847Ab11FE88"),
5439 "Uniswap V3: Positions NFT".to_string()
5440 ),
5441 ])
5442 );
5443
5444 Ok(())
5445 });
5446 }
5447
5448 #[test]
5449 fn test_parse_vyper() {
5450 figment::Jail::expect_with(|jail| {
5451 jail.create_file(
5452 "foundry.toml",
5453 r#"
5454 [vyper]
5455 optimize = "O1"
5456 path = "/path/to/vyper"
5457 experimental_codegen = true
5458 venom_experimental = false
5459 debug = true
5460 enable_decimals = true
5461
5462 [vyper.venom]
5463 disable_inlining = true
5464 disable_cse = true
5465 disable_sccp = false
5466 disable_load_elimination = true
5467 disable_dead_store_elimination = false
5468 disable_algebraic_optimization = true
5469 disable_branch_optimization = false
5470 disable_assert_elimination = true
5471 disable_mem2var = false
5472 disable_simplify_cfg = true
5473 disable_remove_unused_variables = false
5474 inline_threshold = 15
5475 "#,
5476 )?;
5477
5478 let config = Config::load().unwrap();
5479 assert_eq!(
5480 config.vyper,
5481 VyperConfig {
5482 optimize: Some(VyperOptimizationMode::O1),
5483 opt_level: None,
5484 path: Some("/path/to/vyper".into()),
5485 experimental_codegen: Some(true),
5486 venom_experimental: Some(false),
5487 debug: Some(true),
5488 enable_decimals: Some(true),
5489 venom: Some(VyperVenomSettings {
5490 disable_inlining: Some(true),
5491 disable_cse: Some(true),
5492 disable_sccp: Some(false),
5493 disable_load_elimination: Some(true),
5494 disable_dead_store_elimination: Some(false),
5495 disable_algebraic_optimization: Some(true),
5496 disable_branch_optimization: Some(false),
5497 disable_assert_elimination: Some(true),
5498 disable_mem2var: Some(false),
5499 disable_simplify_cfg: Some(true),
5500 disable_remove_unused_variables: Some(false),
5501 inline_threshold: Some(15),
5502 }),
5503 }
5504 );
5505
5506 Ok(())
5507 });
5508 }
5509
5510 #[test]
5511 fn test_vyper_settings_include_extended_config() {
5512 let config = Config {
5513 vyper: VyperConfig {
5514 opt_level: Some(VyperOptimizationLevel::O3),
5515 experimental_codegen: Some(true),
5516 venom_experimental: Some(false),
5517 debug: Some(true),
5518 enable_decimals: Some(true),
5519 venom: Some(VyperVenomSettings {
5520 disable_cse: Some(true),
5521 inline_threshold: Some(15),
5522 ..Default::default()
5523 }),
5524 ..Default::default()
5525 },
5526 ..Config::default().normalized_optimizer_settings()
5527 };
5528
5529 let settings = config.vyper_settings().unwrap();
5530 assert_eq!(settings.optimize, None);
5531 assert_eq!(settings.opt_level, Some(VyperOptimizationLevel::O3));
5532 assert_eq!(settings.experimental_codegen, Some(true));
5533 assert_eq!(settings.venom_experimental, Some(false));
5534 assert_eq!(settings.debug, Some(true));
5535 assert_eq!(settings.enable_decimals, Some(true));
5536 assert_eq!(
5537 settings.venom,
5538 Some(VyperVenomSettings {
5539 disable_cse: Some(true),
5540 inline_threshold: Some(15),
5541 ..Default::default()
5542 })
5543 );
5544 }
5545
5546 #[test]
5547 fn vyper_opt_level_overrides_optimize() {
5548 let config = Config {
5549 vyper: VyperConfig {
5550 optimize: Some(VyperOptimizationMode::Gas),
5551 opt_level: Some(VyperOptimizationLevel::O3),
5552 ..Default::default()
5553 },
5554 ..Config::default().normalized_optimizer_settings()
5555 };
5556
5557 let settings = config.vyper_settings().unwrap();
5558 assert_eq!(settings.optimize, None);
5559 assert_eq!(settings.opt_level, Some(VyperOptimizationLevel::O3));
5560 }
5561
5562 #[test]
5563 fn test_parse_soldeer() {
5564 figment::Jail::expect_with(|jail| {
5565 jail.create_file(
5566 "foundry.toml",
5567 r#"
5568 [soldeer]
5569 remappings_generate = true
5570 remappings_regenerate = false
5571 remappings_version = true
5572 remappings_prefix = "@"
5573 remappings_location = "txt"
5574 recursive_deps = true
5575 "#,
5576 )?;
5577
5578 let config = Config::load().unwrap();
5579
5580 assert_eq!(
5581 config.soldeer,
5582 Some(SoldeerConfig {
5583 remappings_generate: true,
5584 remappings_regenerate: false,
5585 remappings_version: true,
5586 remappings_prefix: "@".to_string(),
5587 remappings_location: RemappingsLocation::Txt,
5588 recursive_deps: true,
5589 })
5590 );
5591
5592 Ok(())
5593 });
5594 }
5595
5596 #[test]
5598 fn test_resolve_mesc_by_chain_id() {
5599 let s = r#"{
5600 "mesc_version": "0.2.1",
5601 "default_endpoint": null,
5602 "endpoints": {
5603 "sophon_50104": {
5604 "name": "sophon_50104",
5605 "url": "https://rpc.sophon.xyz",
5606 "chain_id": "50104",
5607 "endpoint_metadata": {}
5608 }
5609 },
5610 "network_defaults": {
5611 },
5612 "network_names": {},
5613 "profiles": {
5614 "foundry": {
5615 "name": "foundry",
5616 "default_endpoint": "local_ethereum",
5617 "network_defaults": {
5618 "50104": "sophon_50104"
5619 },
5620 "profile_metadata": {},
5621 "use_mesc": true
5622 }
5623 },
5624 "global_metadata": {}
5625}"#;
5626
5627 let config = serde_json::from_str(s).unwrap();
5628 let endpoint = mesc::query::get_endpoint_by_network(&config, "50104", Some("foundry"))
5629 .unwrap()
5630 .unwrap();
5631 assert_eq!(endpoint.url, "https://rpc.sophon.xyz");
5632
5633 let s = r#"{
5634 "mesc_version": "0.2.1",
5635 "default_endpoint": null,
5636 "endpoints": {
5637 "sophon_50104": {
5638 "name": "sophon_50104",
5639 "url": "https://rpc.sophon.xyz",
5640 "chain_id": "50104",
5641 "endpoint_metadata": {}
5642 }
5643 },
5644 "network_defaults": {
5645 "50104": "sophon_50104"
5646 },
5647 "network_names": {},
5648 "profiles": {},
5649 "global_metadata": {}
5650}"#;
5651
5652 let config = serde_json::from_str(s).unwrap();
5653 let endpoint = mesc::query::get_endpoint_by_network(&config, "50104", Some("foundry"))
5654 .unwrap()
5655 .unwrap();
5656 assert_eq!(endpoint.url, "https://rpc.sophon.xyz");
5657 }
5658
5659 #[test]
5660 fn test_get_etherscan_config_with_unknown_chain() {
5661 figment::Jail::expect_with(|jail| {
5662 jail.create_file(
5663 "foundry.toml",
5664 r#"
5665 [etherscan]
5666 mainnet = { chain = 3658348, key = "api-key"}
5667 "#,
5668 )?;
5669 let config = Config::load().unwrap();
5670 let unknown_chain = Chain::from_id(3658348);
5671 let result = config.get_etherscan_config_with_chain(Some(unknown_chain));
5672 assert!(result.is_err());
5673 let error_msg = result.unwrap_err().to_string();
5674 assert!(error_msg.contains("No known Etherscan API URL for chain `3658348`"));
5675 assert!(error_msg.contains("Specify a `url`"));
5676 assert!(error_msg.contains("Verify the chain `3658348` is correct"));
5677
5678 Ok(())
5679 });
5680 }
5681
5682 #[test]
5683 fn test_get_etherscan_config_with_existing_chain_and_url() {
5684 figment::Jail::expect_with(|jail| {
5685 jail.create_file(
5686 "foundry.toml",
5687 r#"
5688 [etherscan]
5689 mainnet = { chain = 1, key = "api-key" }
5690 "#,
5691 )?;
5692 let config = Config::load().unwrap();
5693 let unknown_chain = Chain::from_id(1);
5694 let result = config.get_etherscan_config_with_chain(Some(unknown_chain));
5695 assert!(result.is_ok());
5696 Ok(())
5697 });
5698 }
5699
5700 #[test]
5701 fn test_can_inherit_a_base_toml() {
5702 figment::Jail::expect_with(|jail| {
5703 jail.create_file(
5705 "base-config.toml",
5706 r#"
5707 [profile.default]
5708 optimizer_runs = 800
5709
5710 [invariant]
5711 runs = 1000
5712
5713 [rpc_endpoints]
5714 mainnet = "https://example.com"
5715 optimism = "https://example-2.com/"
5716 "#,
5717 )?;
5718
5719 jail.create_file(
5721 "foundry.toml",
5722 r#"
5723 [profile.default]
5724 extends = "base-config.toml"
5725
5726 [invariant]
5727 runs = 333
5728 depth = 15
5729
5730 [rpc_endpoints]
5731 mainnet = "https://test.xyz/rpc"
5732 "#,
5733 )?;
5734
5735 let config = Config::load().unwrap();
5736 assert_eq!(config.extends, Some(Extends::Path("base-config.toml".to_string())));
5737
5738 assert_eq!(config.optimizer_runs, Some(800));
5740
5741 assert_eq!(config.invariant.runs, 333);
5743 assert_eq!(config.invariant.depth, 15);
5744
5745 let endpoints = config.rpc_endpoints.resolved();
5748 assert!(
5749 endpoints.get("mainnet").unwrap().url().unwrap().contains("https://test.xyz/rpc")
5750 );
5751 assert!(endpoints.get("optimism").unwrap().url().unwrap().contains("example-2.com"));
5752
5753 Ok(())
5754 });
5755 }
5756
5757 #[test]
5758 fn test_inheritance_validation() {
5759 figment::Jail::expect_with(|jail| {
5760 jail.create_file(
5762 "base-with-inherit.toml",
5763 r#"
5764 [profile.default]
5765 extends = "another.toml"
5766 optimizer_runs = 800
5767 "#,
5768 )?;
5769
5770 jail.create_file(
5771 "foundry.toml",
5772 r#"
5773 [profile.default]
5774 extends = "base-with-inherit.toml"
5775 "#,
5776 )?;
5777
5778 let result = Config::load();
5780 assert!(result.is_err());
5781 assert!(result.unwrap_err().to_string().contains("Nested inheritance is not allowed"));
5782
5783 jail.create_file(
5785 "foundry.toml",
5786 r#"
5787 [profile.default]
5788 extends = "foundry.toml"
5789 "#,
5790 )?;
5791
5792 let result = Config::load();
5793 assert!(result.is_err());
5794 assert!(result.unwrap_err().to_string().contains("cannot inherit from itself"));
5795
5796 jail.create_file(
5798 "foundry.toml",
5799 r#"
5800 [profile.default]
5801 extends = "non-existent.toml"
5802 "#,
5803 )?;
5804
5805 let result = Config::load();
5806 assert!(result.is_err());
5807 let err_msg = result.unwrap_err().to_string();
5808 assert!(
5809 err_msg.contains("does not exist")
5810 || err_msg.contains("Failed to resolve inherited config path"),
5811 "Error message: {err_msg}"
5812 );
5813
5814 Ok(())
5815 });
5816 }
5817
5818 #[test]
5819 fn test_complex_inheritance_merging() {
5820 figment::Jail::expect_with(|jail| {
5821 jail.create_file(
5823 "base.toml",
5824 r#"
5825 [profile.default]
5826 optimizer = true
5827 optimizer_runs = 1000
5828 via_ir = false
5829 solc = "0.8.19"
5830
5831 [invariant]
5832 runs = 500
5833 depth = 100
5834
5835 [fuzz]
5836 runs = 256
5837 seed = "0x123"
5838
5839 [rpc_endpoints]
5840 mainnet = "https://base-mainnet.com"
5841 optimism = "https://base-optimism.com"
5842 arbitrum = "https://base-arbitrum.com"
5843 "#,
5844 )?;
5845
5846 jail.create_file(
5848 "foundry.toml",
5849 r#"
5850 [profile.default]
5851 extends = "base.toml"
5852 optimizer_runs = 200 # Override
5853 via_ir = true # Override
5854 # optimizer and solc are inherited
5855
5856 [invariant]
5857 runs = 333 # Override
5858 # depth is inherited
5859
5860 # fuzz section is fully inherited
5861
5862 [rpc_endpoints]
5863 mainnet = "https://local-mainnet.com" # Override
5864 # optimism and arbitrum are inherited
5865 polygon = "https://local-polygon.com" # New
5866 "#,
5867 )?;
5868
5869 let config = Config::load().unwrap();
5870
5871 assert_eq!(config.optimizer, Some(true));
5873 assert_eq!(config.optimizer_runs, Some(200));
5874 assert_eq!(config.via_ir, true);
5875 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 19))));
5876
5877 assert_eq!(config.invariant.runs, 333);
5879 assert_eq!(config.invariant.depth, 100);
5880
5881 assert_eq!(config.fuzz.runs, 256);
5883 assert_eq!(config.fuzz.seed, Some(U256::from(0x123)));
5884
5885 let endpoints = config.rpc_endpoints.resolved();
5887 assert!(endpoints.get("mainnet").unwrap().url().unwrap().contains("local-mainnet"));
5888 assert!(endpoints.get("optimism").unwrap().url().unwrap().contains("base-optimism"));
5889 assert!(endpoints.get("arbitrum").unwrap().url().unwrap().contains("base-arbitrum"));
5890 assert!(endpoints.get("polygon").unwrap().url().unwrap().contains("local-polygon"));
5891
5892 Ok(())
5893 });
5894 }
5895
5896 #[test]
5897 fn test_inheritance_with_different_profiles() {
5898 figment::Jail::expect_with(|jail| {
5899 jail.create_file(
5901 "base.toml",
5902 r#"
5903 [profile.default]
5904 optimizer = true
5905 optimizer_runs = 200
5906
5907 [profile.ci]
5908 optimizer = true
5909 optimizer_runs = 10000
5910 via_ir = true
5911
5912 [profile.dev]
5913 optimizer = false
5914 "#,
5915 )?;
5916
5917 jail.create_file(
5919 "foundry.toml",
5920 r#"
5921 [profile.default]
5922 extends = "base.toml"
5923 verbosity = 3
5924
5925 [profile.ci]
5926 optimizer_runs = 5000 # This doesn't inherit from base.toml's ci profile
5927 "#,
5928 )?;
5929
5930 let config = Config::load().unwrap();
5932 assert_eq!(config.optimizer, Some(true));
5933 assert_eq!(config.optimizer_runs, Some(200));
5934 assert_eq!(config.verbosity, 3);
5935
5936 jail.set_env("FOUNDRY_PROFILE", "ci");
5938 let config = Config::load().unwrap();
5939 assert_eq!(config.optimizer_runs, Some(5000));
5940 assert_eq!(config.optimizer, Some(true));
5941 assert_eq!(config.via_ir, false);
5943
5944 Ok(())
5945 });
5946 }
5947
5948 #[test]
5949 fn test_inheritance_with_env_vars() {
5950 figment::Jail::expect_with(|jail| {
5951 jail.create_file(
5952 "base.toml",
5953 r#"
5954 [profile.default]
5955 optimizer_runs = 500
5956 sender = "0x0000000000000000000000000000000000000001"
5957 verbosity = 1
5958 "#,
5959 )?;
5960
5961 jail.create_file(
5962 "foundry.toml",
5963 r#"
5964 [profile.default]
5965 extends = "base.toml"
5966 verbosity = 2
5967 "#,
5968 )?;
5969
5970 jail.set_env("FOUNDRY_OPTIMIZER_RUNS", "999");
5972 jail.set_env("FOUNDRY_VERBOSITY", "4");
5973
5974 let config = Config::load().unwrap();
5975 assert_eq!(config.optimizer_runs, Some(999));
5976 assert_eq!(config.verbosity, 4);
5977 assert_eq!(
5978 config.sender,
5979 "0x0000000000000000000000000000000000000001"
5980 .parse::<alloy_primitives::Address>()
5981 .unwrap()
5982 );
5983
5984 Ok(())
5985 });
5986 }
5987
5988 #[test]
5989 fn test_inheritance_with_subdirectories() {
5990 figment::Jail::expect_with(|jail| {
5991 jail.create_dir("configs")?;
5993 jail.create_file(
5994 "configs/base.toml",
5995 r#"
5996 [profile.default]
5997 optimizer_runs = 800
5998 src = "contracts"
5999 "#,
6000 )?;
6001
6002 jail.create_file(
6004 "foundry.toml",
6005 r#"
6006 [profile.default]
6007 extends = "configs/base.toml"
6008 test = "tests"
6009 "#,
6010 )?;
6011
6012 let config = Config::load().unwrap();
6013 assert_eq!(config.optimizer_runs, Some(800));
6014 assert_eq!(config.src, PathBuf::from("contracts"));
6015 assert_eq!(config.test, PathBuf::from("tests"));
6016
6017 jail.create_dir("project")?;
6019 jail.create_file(
6020 "shared-base.toml",
6021 r#"
6022 [profile.default]
6023 optimizer_runs = 1500
6024 "#,
6025 )?;
6026
6027 jail.create_file(
6028 "project/foundry.toml",
6029 r#"
6030 [profile.default]
6031 extends = "../shared-base.toml"
6032 "#,
6033 )?;
6034
6035 std::env::set_current_dir(jail.directory().join("project")).unwrap();
6036 let config = Config::load().unwrap();
6037 assert_eq!(config.optimizer_runs, Some(1500));
6038
6039 Ok(())
6040 });
6041 }
6042
6043 #[test]
6044 fn test_inheritance_with_empty_files() {
6045 figment::Jail::expect_with(|jail| {
6046 jail.create_file(
6048 "base.toml",
6049 r#"
6050 [profile.default]
6051 "#,
6052 )?;
6053
6054 jail.create_file(
6055 "foundry.toml",
6056 r#"
6057 [profile.default]
6058 extends = "base.toml"
6059 optimizer_runs = 300
6060 "#,
6061 )?;
6062
6063 let config = Config::load().unwrap();
6064 assert_eq!(config.optimizer_runs, Some(300));
6065
6066 jail.create_file(
6068 "base2.toml",
6069 r#"
6070 [profile.default]
6071 optimizer_runs = 400
6072 via_ir = true
6073 "#,
6074 )?;
6075
6076 jail.create_file(
6077 "foundry.toml",
6078 r#"
6079 [profile.default]
6080 extends = "base2.toml"
6081 "#,
6082 )?;
6083
6084 let config = Config::load().unwrap();
6085 assert_eq!(config.optimizer_runs, Some(400));
6086 assert!(config.via_ir);
6087
6088 Ok(())
6089 });
6090 }
6091
6092 #[test]
6093 fn test_inheritance_array_and_table_merging() {
6094 figment::Jail::expect_with(|jail| {
6095 jail.create_file(
6096 "base.toml",
6097 r#"
6098 [profile.default]
6099 libs = ["lib", "node_modules"]
6100 ignored_error_codes = [5667, 1878]
6101 extra_output = ["metadata", "ir"]
6102
6103 [profile.default.model_checker]
6104 engine = "chc"
6105 timeout = 10000
6106 targets = ["assert"]
6107
6108 [profile.default.optimizer_details]
6109 peephole = true
6110 inliner = true
6111 "#,
6112 )?;
6113
6114 jail.create_file(
6115 "foundry.toml",
6116 r#"
6117 [profile.default]
6118 extends = "base.toml"
6119 libs = ["custom-lib"] # Concatenates with base array
6120 ignored_error_codes = [2018] # Concatenates with base array
6121
6122 [profile.default.model_checker]
6123 timeout = 5000 # Overrides base value
6124 # engine and targets are inherited
6125
6126 [profile.default.optimizer_details]
6127 jumpdest_remover = true # Adds new field
6128 # peephole and inliner are inherited
6129 "#,
6130 )?;
6131
6132 let config = Config::load().unwrap();
6133
6134 assert_eq!(
6136 config.libs,
6137 vec![
6138 PathBuf::from("lib"),
6139 PathBuf::from("node_modules"),
6140 PathBuf::from("custom-lib")
6141 ]
6142 );
6143 assert_eq!(
6144 config.ignored_error_codes,
6145 vec![
6146 SolidityErrorCode::UnusedFunctionParameter, SolidityErrorCode::SpdxLicenseNotProvided, SolidityErrorCode::FunctionStateMutabilityCanBeRestricted ]
6150 );
6151
6152 assert_eq!(config.model_checker.as_ref().unwrap().timeout, Some(5000));
6154 assert_eq!(
6155 config.model_checker.as_ref().unwrap().engine,
6156 Some(ModelCheckerEngine::CHC)
6157 );
6158 assert_eq!(
6159 config.model_checker.as_ref().unwrap().targets,
6160 Some(vec![ModelCheckerTarget::Assert])
6161 );
6162
6163 assert_eq!(config.optimizer_details.as_ref().unwrap().peephole, Some(true));
6165 assert_eq!(config.optimizer_details.as_ref().unwrap().inliner, Some(true));
6166 assert_eq!(config.optimizer_details.as_ref().unwrap().jumpdest_remover, None);
6167
6168 Ok(())
6169 });
6170 }
6171
6172 #[test]
6173 fn test_inheritance_with_special_sections() {
6174 figment::Jail::expect_with(|jail| {
6175 jail.create_file(
6176 "base.toml",
6177 r#"
6178 [profile.default]
6179 # Base file should not have 'extends' to avoid nested inheritance
6180
6181 [labels]
6182 "0x0000000000000000000000000000000000000001" = "Alice"
6183 "0x0000000000000000000000000000000000000002" = "Bob"
6184
6185 [[profile.default.fs_permissions]]
6186 access = "read"
6187 path = "./src"
6188
6189 [[profile.default.fs_permissions]]
6190 access = "read-write"
6191 path = "./cache"
6192 "#,
6193 )?;
6194
6195 jail.create_file(
6196 "foundry.toml",
6197 r#"
6198 [profile.default]
6199 extends = "base.toml"
6200
6201 [labels]
6202 "0x0000000000000000000000000000000000000002" = "Bob Updated"
6203 "0x0000000000000000000000000000000000000003" = "Charlie"
6204
6205 [[profile.default.fs_permissions]]
6206 access = "read"
6207 path = "./test"
6208 "#,
6209 )?;
6210
6211 let config = Config::load().unwrap();
6212
6213 assert_eq!(
6215 config.labels.get(
6216 &"0x0000000000000000000000000000000000000001"
6217 .parse::<alloy_primitives::Address>()
6218 .unwrap()
6219 ),
6220 Some(&"Alice".to_string())
6221 );
6222 assert_eq!(
6223 config.labels.get(
6224 &"0x0000000000000000000000000000000000000002"
6225 .parse::<alloy_primitives::Address>()
6226 .unwrap()
6227 ),
6228 Some(&"Bob Updated".to_string())
6229 );
6230 assert_eq!(
6231 config.labels.get(
6232 &"0x0000000000000000000000000000000000000003"
6233 .parse::<alloy_primitives::Address>()
6234 .unwrap()
6235 ),
6236 Some(&"Charlie".to_string())
6237 );
6238
6239 assert_eq!(config.fs_permissions.permissions.len(), 3); assert!(
6243 config
6244 .fs_permissions
6245 .permissions
6246 .iter()
6247 .any(|p| p.path.to_str().unwrap() == "./src")
6248 );
6249 assert!(
6250 config
6251 .fs_permissions
6252 .permissions
6253 .iter()
6254 .any(|p| p.path.to_str().unwrap() == "./cache")
6255 );
6256 assert!(
6257 config
6258 .fs_permissions
6259 .permissions
6260 .iter()
6261 .any(|p| p.path.to_str().unwrap() == "./test")
6262 );
6263
6264 Ok(())
6265 });
6266 }
6267
6268 #[test]
6269 fn test_inheritance_with_compilation_settings() {
6270 figment::Jail::expect_with(|jail| {
6271 jail.create_file(
6272 "base.toml",
6273 r#"
6274 [profile.default]
6275 solc = "0.8.19"
6276 evm_version = "paris"
6277 via_ir = false
6278 optimizer = true
6279 optimizer_runs = 200
6280
6281 [profile.default.optimizer_details]
6282 peephole = true
6283 inliner = false
6284 jumpdest_remover = true
6285 order_literals = false
6286 deduplicate = true
6287 cse = true
6288 constant_optimizer = true
6289 yul = true
6290
6291 [profile.default.optimizer_details.yul_details]
6292 stack_allocation = true
6293 optimizer_steps = "dhfoDgvulfnTUtnIf"
6294 "#,
6295 )?;
6296
6297 jail.create_file(
6298 "foundry.toml",
6299 r#"
6300 [profile.default]
6301 extends = "base.toml"
6302 evm_version = "shanghai" # Override
6303 optimizer_runs = 1000 # Override
6304
6305 [profile.default.optimizer_details]
6306 inliner = true # Override
6307 # Rest inherited
6308 "#,
6309 )?;
6310
6311 let config = Config::load().unwrap();
6312
6313 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 19))));
6315 assert_eq!(config.evm_version, EvmVersion::Shanghai);
6316 assert_eq!(config.via_ir, false);
6317 assert_eq!(config.optimizer, Some(true));
6318 assert_eq!(config.optimizer_runs, Some(1000));
6319
6320 let details = config.optimizer_details.as_ref().unwrap();
6322 assert_eq!(details.peephole, Some(true));
6323 assert_eq!(details.inliner, Some(true));
6324 assert_eq!(details.jumpdest_remover, None);
6325 assert_eq!(details.order_literals, None);
6326 assert_eq!(details.deduplicate, Some(true));
6327 assert_eq!(details.cse, Some(true));
6328 assert_eq!(details.constant_optimizer, None);
6329 assert_eq!(details.yul, Some(true));
6330
6331 if let Some(yul_details) = details.yul_details.as_ref() {
6333 assert_eq!(yul_details.stack_allocation, Some(true));
6334 assert_eq!(yul_details.optimizer_steps, Some("dhfoDgvulfnTUtnIf".to_string()));
6335 }
6336
6337 Ok(())
6338 });
6339 }
6340
6341 #[test]
6342 fn test_inheritance_with_remappings() {
6343 figment::Jail::expect_with(|jail| {
6344 jail.create_file(
6345 "base.toml",
6346 r#"
6347 [profile.default]
6348 remappings = [
6349 "forge-std/=lib/forge-std/src/",
6350 "@openzeppelin/=lib/openzeppelin-contracts/",
6351 "ds-test/=lib/ds-test/src/"
6352 ]
6353 auto_detect_remappings = false
6354 "#,
6355 )?;
6356
6357 jail.create_file(
6358 "foundry.toml",
6359 r#"
6360 [profile.default]
6361 extends = "base.toml"
6362 remappings = [
6363 "@custom/=lib/custom/",
6364 "ds-test/=lib/forge-std/lib/ds-test/src/" # Note: This will be added alongside base remappings
6365 ]
6366 "#,
6367 )?;
6368
6369 let config = Config::load().unwrap();
6370
6371 assert!(config.remappings.iter().any(|r| r.to_string().contains("@custom/")));
6373 assert!(config.remappings.iter().any(|r| r.to_string().contains("ds-test/")));
6374 assert!(config.remappings.iter().any(|r| r.to_string().contains("forge-std/")));
6375 assert!(config.remappings.iter().any(|r| r.to_string().contains("@openzeppelin/")));
6376
6377 assert!(!config.auto_detect_remappings);
6379
6380 Ok(())
6381 });
6382 }
6383
6384 #[test]
6385 fn test_inheritance_with_multiple_profiles_and_single_file() {
6386 figment::Jail::expect_with(|jail| {
6387 jail.create_file(
6389 "base.toml",
6390 r#"
6391 [profile.prod]
6392 optimizer = true
6393 optimizer_runs = 10000
6394 via_ir = true
6395
6396 [profile.test]
6397 optimizer = false
6398
6399 [profile.test.fuzz]
6400 runs = 100
6401 "#,
6402 )?;
6403
6404 jail.create_file(
6406 "foundry.toml",
6407 r#"
6408 [profile.prod]
6409 extends = "base.toml"
6410 evm_version = "shanghai" # Additional setting
6411
6412 [profile.test]
6413 extends = "base.toml"
6414
6415 [profile.test.fuzz]
6416 runs = 500 # Override
6417 "#,
6418 )?;
6419
6420 jail.set_env("FOUNDRY_PROFILE", "prod");
6422 let config = Config::load().unwrap();
6423 assert_eq!(config.optimizer, Some(true));
6424 assert_eq!(config.optimizer_runs, Some(10000));
6425 assert_eq!(config.via_ir, true);
6426 assert_eq!(config.evm_version, EvmVersion::Shanghai);
6427
6428 jail.set_env("FOUNDRY_PROFILE", "test");
6430 let config = Config::load().unwrap();
6431 assert_eq!(config.optimizer, Some(false));
6432 assert_eq!(config.fuzz.runs, 500);
6433
6434 Ok(())
6435 });
6436 }
6437
6438 #[test]
6439 fn test_inheritance_with_multiple_profiles_and_files() {
6440 figment::Jail::expect_with(|jail| {
6441 jail.create_file(
6442 "prod.toml",
6443 r#"
6444 [profile.prod]
6445 optimizer = true
6446 optimizer_runs = 20000
6447 gas_limit = 50000000
6448 "#,
6449 )?;
6450 jail.create_file(
6451 "dev.toml",
6452 r#"
6453 [profile.dev]
6454 optimizer = true
6455 optimizer_runs = 333
6456 gas_limit = 555555
6457 "#,
6458 )?;
6459
6460 jail.create_file(
6462 "foundry.toml",
6463 r#"
6464 [profile.dev]
6465 extends = "dev.toml"
6466 sender = "0x0000000000000000000000000000000000000001"
6467
6468 [profile.prod]
6469 extends = "prod.toml"
6470 sender = "0x0000000000000000000000000000000000000002"
6471 "#,
6472 )?;
6473
6474 jail.set_env("FOUNDRY_PROFILE", "dev");
6476 let config = Config::load().unwrap();
6477 assert_eq!(config.optimizer, Some(true));
6478 assert_eq!(config.optimizer_runs, Some(333));
6479 assert_eq!(config.gas_limit, 555555.into());
6480 assert_eq!(
6481 config.sender,
6482 "0x0000000000000000000000000000000000000001"
6483 .parse::<alloy_primitives::Address>()
6484 .unwrap()
6485 );
6486
6487 jail.set_env("FOUNDRY_PROFILE", "prod");
6489 let config = Config::load().unwrap();
6490 assert_eq!(config.optimizer, Some(true));
6491 assert_eq!(config.optimizer_runs, Some(20000));
6492 assert_eq!(config.gas_limit, 50000000.into());
6493 assert_eq!(
6494 config.sender,
6495 "0x0000000000000000000000000000000000000002"
6496 .parse::<alloy_primitives::Address>()
6497 .unwrap()
6498 );
6499
6500 Ok(())
6501 });
6502 }
6503
6504 #[test]
6505 fn test_extends_strategy_extend_arrays() {
6506 figment::Jail::expect_with(|jail| {
6507 jail.create_file(
6509 "base.toml",
6510 r#"
6511 [profile.default]
6512 libs = ["lib", "node_modules"]
6513 ignored_error_codes = [5667, 1878]
6514 optimizer_runs = 200
6515 "#,
6516 )?;
6517
6518 jail.create_file(
6520 "foundry.toml",
6521 r#"
6522 [profile.default]
6523 extends = "base.toml"
6524 libs = ["mylib", "customlib"]
6525 ignored_error_codes = [1234]
6526 optimizer_runs = 500
6527 "#,
6528 )?;
6529
6530 let config = Config::load().unwrap();
6531
6532 assert_eq!(config.libs.len(), 4);
6534 assert!(config.libs.iter().any(|l| l.to_str() == Some("lib")));
6535 assert!(config.libs.iter().any(|l| l.to_str() == Some("node_modules")));
6536 assert!(config.libs.iter().any(|l| l.to_str() == Some("mylib")));
6537 assert!(config.libs.iter().any(|l| l.to_str() == Some("customlib")));
6538
6539 assert_eq!(config.ignored_error_codes.len(), 3);
6540 assert!(
6541 config.ignored_error_codes.contains(&SolidityErrorCode::UnusedFunctionParameter)
6542 ); assert!(
6544 config.ignored_error_codes.contains(&SolidityErrorCode::SpdxLicenseNotProvided)
6545 ); assert!(config.ignored_error_codes.contains(&SolidityErrorCode::from(1234u64))); assert_eq!(config.optimizer_runs, Some(500));
6550
6551 Ok(())
6552 });
6553 }
6554
6555 #[test]
6556 fn test_extends_strategy_replace_arrays() {
6557 figment::Jail::expect_with(|jail| {
6558 jail.create_file(
6560 "base.toml",
6561 r#"
6562 [profile.default]
6563 libs = ["lib", "node_modules"]
6564 ignored_error_codes = [5667, 1878]
6565 optimizer_runs = 200
6566 "#,
6567 )?;
6568
6569 jail.create_file(
6571 "foundry.toml",
6572 r#"
6573 [profile.default]
6574 extends = { path = "base.toml", strategy = "replace-arrays" }
6575 libs = ["mylib", "customlib"]
6576 ignored_error_codes = [1234]
6577 optimizer_runs = 500
6578 "#,
6579 )?;
6580
6581 let config = Config::load().unwrap();
6582
6583 assert_eq!(config.libs.len(), 2);
6585 assert!(config.libs.iter().any(|l| l.to_str() == Some("mylib")));
6586 assert!(config.libs.iter().any(|l| l.to_str() == Some("customlib")));
6587 assert!(!config.libs.iter().any(|l| l.to_str() == Some("lib")));
6588 assert!(!config.libs.iter().any(|l| l.to_str() == Some("node_modules")));
6589
6590 assert_eq!(config.ignored_error_codes.len(), 1);
6591 assert!(config.ignored_error_codes.contains(&SolidityErrorCode::from(1234u64))); assert!(
6593 !config.ignored_error_codes.contains(&SolidityErrorCode::UnusedFunctionParameter)
6594 ); assert_eq!(config.optimizer_runs, Some(500));
6598
6599 Ok(())
6600 });
6601 }
6602
6603 #[test]
6604 fn test_extends_strategy_no_collision_success() {
6605 figment::Jail::expect_with(|jail| {
6606 jail.create_file(
6608 "base.toml",
6609 r#"
6610 [profile.default]
6611 optimizer = true
6612 optimizer_runs = 200
6613 src = "src"
6614 "#,
6615 )?;
6616
6617 jail.create_file(
6619 "foundry.toml",
6620 r#"
6621 [profile.default]
6622 extends = { path = "base.toml", strategy = "no-collision" }
6623 test = "tests"
6624 libs = ["lib"]
6625 "#,
6626 )?;
6627
6628 let config = Config::load().unwrap();
6629
6630 assert_eq!(config.optimizer, Some(true));
6632 assert_eq!(config.optimizer_runs, Some(200));
6633 assert_eq!(config.src, PathBuf::from("src"));
6634
6635 assert_eq!(config.test, PathBuf::from("tests"));
6637 assert_eq!(config.libs.len(), 1);
6638 assert!(config.libs.iter().any(|l| l.to_str() == Some("lib")));
6639
6640 Ok(())
6641 });
6642 }
6643
6644 #[test]
6645 fn test_extends_strategy_no_collision_error() {
6646 figment::Jail::expect_with(|jail| {
6647 jail.create_file(
6649 "base.toml",
6650 r#"
6651 [profile.default]
6652 optimizer = true
6653 optimizer_runs = 200
6654 libs = ["lib", "node_modules"]
6655 "#,
6656 )?;
6657
6658 jail.create_file(
6660 "foundry.toml",
6661 r#"
6662 [profile.default]
6663 extends = { path = "base.toml", strategy = "no-collision" }
6664 optimizer_runs = 500
6665 libs = ["mylib"]
6666 "#,
6667 )?;
6668
6669 let result = Config::load();
6671
6672 if let Ok(config) = result {
6673 panic!(
6674 "Expected error but got config with optimizer_runs: {:?}, libs: {:?}",
6675 config.optimizer_runs, config.libs
6676 );
6677 }
6678
6679 let err = result.unwrap_err();
6680 let err_str = err.to_string();
6681 assert!(
6682 err_str.contains("Key collision detected") || err_str.contains("collision"),
6683 "Error message doesn't mention collision: {err_str}"
6684 );
6685
6686 Ok(())
6687 });
6688 }
6689
6690 #[test]
6691 fn test_extends_both_syntaxes() {
6692 figment::Jail::expect_with(|jail| {
6693 jail.create_file(
6695 "base.toml",
6696 r#"
6697 [profile.default]
6698 libs = ["lib"]
6699 optimizer = true
6700 "#,
6701 )?;
6702
6703 jail.create_file(
6705 "foundry_string.toml",
6706 r#"
6707 [profile.default]
6708 extends = "base.toml"
6709 libs = ["custom"]
6710 "#,
6711 )?;
6712
6713 jail.create_file(
6715 "foundry_object.toml",
6716 r#"
6717 [profile.default]
6718 extends = { path = "base.toml", strategy = "replace-arrays" }
6719 libs = ["custom"]
6720 "#,
6721 )?;
6722
6723 jail.set_env("FOUNDRY_CONFIG", "foundry_string.toml");
6725 let config = Config::load().unwrap();
6726 assert_eq!(config.libs.len(), 2); assert!(config.libs.iter().any(|l| l.to_str() == Some("lib")));
6728 assert!(config.libs.iter().any(|l| l.to_str() == Some("custom")));
6729
6730 jail.set_env("FOUNDRY_CONFIG", "foundry_object.toml");
6732 let config = Config::load().unwrap();
6733 assert_eq!(config.libs.len(), 1); assert!(config.libs.iter().any(|l| l.to_str() == Some("custom")));
6735 assert!(!config.libs.iter().any(|l| l.to_str() == Some("lib")));
6736
6737 Ok(())
6738 });
6739 }
6740
6741 #[test]
6742 fn test_extends_strategy_default_is_extend_arrays() {
6743 figment::Jail::expect_with(|jail| {
6744 jail.create_file(
6746 "base.toml",
6747 r#"
6748 [profile.default]
6749 libs = ["lib", "node_modules"]
6750 optimizer = true
6751 "#,
6752 )?;
6753
6754 jail.create_file(
6756 "foundry.toml",
6757 r#"
6758 [profile.default]
6759 extends = "base.toml"
6760 libs = ["custom"]
6761 optimizer = false
6762 "#,
6763 )?;
6764
6765 let config = Config::load().unwrap();
6767
6768 assert_eq!(config.libs.len(), 3);
6770 assert!(config.libs.iter().any(|l| l.to_str() == Some("lib")));
6771 assert!(config.libs.iter().any(|l| l.to_str() == Some("node_modules")));
6772 assert!(config.libs.iter().any(|l| l.to_str() == Some("custom")));
6773
6774 assert_eq!(config.optimizer, Some(false));
6776
6777 Ok(())
6778 });
6779 }
6780
6781 #[test]
6782 fn test_deprecated_deny_warnings_is_handled() {
6783 figment::Jail::expect_with(|jail| {
6784 jail.create_file(
6785 "foundry.toml",
6786 r#"
6787 [profile.default]
6788 deny_warnings = true
6789 "#,
6790 )?;
6791 let config = Config::load().unwrap();
6792
6793 assert_eq!(config.deny, DenyLevel::Warnings);
6795 Ok(())
6796 });
6797 }
6798
6799 #[test]
6800 fn warns_on_unknown_keys_in_profile() {
6801 figment::Jail::expect_with(|jail| {
6802 jail.create_file(
6803 "foundry.toml",
6804 r#"
6805 [profile.default]
6806 unknown_key_xyz = 123
6807 "#,
6808 )?;
6809
6810 let cfg = Config::load().unwrap();
6811 assert!(cfg.warnings.iter().any(
6812 |w| matches!(w, crate::Warning::UnknownKey { key, .. } if key == "unknown_key_xyz")
6813 ));
6814 Ok(())
6815 });
6816 }
6817
6818 #[test]
6819 fn no_unknown_key_warning_for_network_field() {
6820 figment::Jail::expect_with(|jail| {
6823 jail.create_file(
6824 "foundry.toml",
6825 r#"
6826 [profile.default]
6827 network = "tempo"
6828 "#,
6829 )?;
6830
6831 let cfg = Config::load().unwrap();
6832 assert!(
6833 !cfg.warnings.iter().any(
6834 |w| matches!(w, crate::Warning::UnknownKey { key, .. } if key == "network")
6835 ),
6836 "did not expect UnknownKey warning for `network`, got: {:?}",
6837 cfg.warnings
6838 );
6839 Ok(())
6840 });
6841 }
6842
6843 #[test]
6844 fn no_unknown_key_warning_for_legacy_tempo_alias() {
6845 figment::Jail::expect_with(|jail| {
6847 jail.create_file(
6848 "foundry.toml",
6849 r#"
6850 [profile.default]
6851 tempo = true
6852 "#,
6853 )?;
6854
6855 let cfg = Config::load().unwrap();
6856 assert!(
6857 !cfg.warnings
6858 .iter()
6859 .any(|w| matches!(w, crate::Warning::UnknownKey { key, .. } if key == "tempo")),
6860 "did not expect UnknownKey warning for `tempo`, got: {:?}",
6861 cfg.warnings
6862 );
6863 Ok(())
6864 });
6865 }
6866
6867 #[test]
6868 fn fails_on_ambiguous_version_in_compilation_restrictions() {
6869 figment::Jail::expect_with(|jail| {
6870 jail.create_file(
6871 "foundry.toml",
6872 r#"
6873 [profile.default]
6874 src = "src"
6875
6876 [[profile.default.compilation_restrictions]]
6877 paths = "src/*.sol"
6878 version = "0.8.11"
6879 "#,
6880 )?;
6881
6882 let err = Config::load().expect_err("expected bare version to fail");
6883 let err_msg = err.to_string();
6884 assert!(
6885 err_msg.contains("Invalid version format '0.8.11'")
6886 && err_msg.contains("Bare version numbers are ambiguous"),
6887 "Expected error about ambiguous version, got: {err_msg}"
6888 );
6889
6890 Ok(())
6891 });
6892 }
6893
6894 #[test]
6895 fn accepts_explicit_version_requirements() {
6896 figment::Jail::expect_with(|jail| {
6897 jail.create_file(
6898 "foundry.toml",
6899 r#"
6900 [profile.default]
6901 src = "src"
6902
6903 [[profile.default.compilation_restrictions]]
6904 paths = "src/*.sol"
6905 version = "=0.8.11"
6906
6907 [[profile.default.compilation_restrictions]]
6908 paths = "test/*.sol"
6909 version = ">=0.8.11"
6910 "#,
6911 )?;
6912
6913 let config = Config::load().expect("should accept explicit version requirements");
6914 assert_eq!(config.compilation_restrictions.len(), 2);
6915
6916 Ok(())
6917 });
6918 }
6919
6920 #[test]
6921 fn warns_on_unknown_keys_in_all_config_sections() {
6922 figment::Jail::expect_with(|jail| {
6923 jail.create_file(
6924 "foundry.toml",
6925 r#"
6926 [profile.default]
6927 src = "src"
6928 unknown_profile_key = "should_warn"
6929
6930 # Standalone sections with unknown keys
6931 [fmt]
6932 line_length = 120
6933 unknown_fmt_key = "should_warn"
6934
6935 [lint]
6936 severity = ["high"]
6937 unknown_lint_key = "should_warn"
6938
6939 [doc]
6940 out = "docs"
6941 unknown_doc_key = "should_warn"
6942
6943 [fuzz]
6944 runs = 256
6945 unknown_fuzz_key = "should_warn"
6946
6947 [invariant]
6948 runs = 256
6949 unknown_invariant_key = "should_warn"
6950
6951 [vyper]
6952 unknown_vyper_key = "should_warn"
6953
6954 [bind_json]
6955 out = "bindings.sol"
6956 unknown_bind_json_key = "should_warn"
6957
6958 # Nested profile sections with unknown keys
6959 [profile.default.fmt]
6960 line_length = 100
6961 unknown_nested_fmt_key = "should_warn"
6962
6963 [profile.default.lint]
6964 severity = ["low"]
6965 unknown_nested_lint_key = "should_warn"
6966
6967 [profile.default.doc]
6968 out = "documentation"
6969 unknown_nested_doc_key = "should_warn"
6970
6971 [profile.default.fuzz]
6972 runs = 512
6973 unknown_nested_fuzz_key = "should_warn"
6974
6975 [profile.default.invariant]
6976 runs = 512
6977 unknown_nested_invariant_key = "should_warn"
6978
6979 [profile.default.vyper]
6980 unknown_nested_vyper_key = "should_warn"
6981
6982 [profile.default.bind_json]
6983 out = "nested_bindings.sol"
6984 unknown_nested_bind_json_key = "should_warn"
6985
6986 # Array sections with unknown keys
6987 [[profile.default.compilation_restrictions]]
6988 paths = "src/*.sol"
6989 unknown_compilation_key = "should_warn"
6990
6991 [[profile.default.additional_compiler_profiles]]
6992 name = "via-ir"
6993 via_ir = true
6994 unknown_compiler_profile_key = "should_warn"
6995 "#,
6996 )?;
6997
6998 let cfg = Config::load().unwrap();
6999
7000 assert!(
7002 cfg.warnings.iter().any(|w| matches!(
7003 w,
7004 crate::Warning::UnknownKey { key, .. } if key == "unknown_profile_key"
7005 )),
7006 "Expected warning for 'unknown_profile_key' in profile, got: {:?}",
7007 cfg.warnings
7008 );
7009
7010 let standalone_expected = [
7012 ("unknown_fmt_key", "fmt"),
7013 ("unknown_lint_key", "lint"),
7014 ("unknown_doc_key", "doc"),
7015 ("unknown_fuzz_key", "fuzz"),
7016 ("unknown_invariant_key", "invariant"),
7017 ("unknown_vyper_key", "vyper"),
7018 ("unknown_bind_json_key", "bind_json"),
7019 ];
7020
7021 for (expected_key, expected_section) in standalone_expected {
7022 assert!(
7023 cfg.warnings.iter().any(|w| matches!(
7024 w,
7025 crate::Warning::UnknownSectionKey { key, section, .. }
7026 if key == expected_key && section == expected_section
7027 )),
7028 "Expected warning for '{}' in standalone section '{}', got: {:?}",
7029 expected_key,
7030 expected_section,
7031 cfg.warnings
7032 );
7033 }
7034
7035 let nested_expected = [
7037 ("unknown_nested_fmt_key", "fmt"),
7038 ("unknown_nested_lint_key", "lint"),
7039 ("unknown_nested_doc_key", "doc"),
7040 ("unknown_nested_fuzz_key", "fuzz"),
7041 ("unknown_nested_invariant_key", "invariant"),
7042 ("unknown_nested_vyper_key", "vyper"),
7043 ("unknown_nested_bind_json_key", "bind_json"),
7044 ];
7045
7046 for (expected_key, expected_section) in nested_expected {
7047 assert!(
7048 cfg.warnings.iter().any(|w| matches!(
7049 w,
7050 crate::Warning::UnknownSectionKey { key, section, .. }
7051 if key == expected_key && section == expected_section
7052 )),
7053 "Expected warning for '{}' in nested section '{}', got: {:?}",
7054 expected_key,
7055 expected_section,
7056 cfg.warnings
7057 );
7058 }
7059
7060 let array_expected = [
7062 ("unknown_compilation_key", "compilation_restrictions"),
7063 ("unknown_compiler_profile_key", "additional_compiler_profiles"),
7064 ];
7065
7066 for (expected_key, expected_section) in array_expected {
7067 assert!(
7068 cfg.warnings.iter().any(|w| matches!(
7069 w,
7070 crate::Warning::UnknownSectionKey { key, section, .. }
7071 if key == expected_key && section == expected_section
7072 )),
7073 "Expected warning for '{}' in array section '{}', got: {:?}",
7074 expected_key,
7075 expected_section,
7076 cfg.warnings
7077 );
7078 }
7079
7080 let unknown_key_warnings: Vec<_> = cfg
7082 .warnings
7083 .iter()
7084 .filter(|w| {
7085 matches!(w, crate::Warning::UnknownKey { .. })
7086 || matches!(w, crate::Warning::UnknownSectionKey { .. })
7087 })
7088 .collect();
7089
7090 assert_eq!(
7092 unknown_key_warnings.len(),
7093 17,
7094 "Expected 17 unknown key warnings (1 profile + 7 standalone + 7 nested + 2 array), got {}: {:?}",
7095 unknown_key_warnings.len(),
7096 unknown_key_warnings
7097 );
7098
7099 Ok(())
7100 });
7101 }
7102
7103 #[test]
7104 fn warns_on_unknown_keys_in_extended_config() {
7105 figment::Jail::expect_with(|jail| {
7106 jail.create_file(
7108 "base.toml",
7109 r#"
7110 [profile.default]
7111 optimizer_runs = 800
7112 unknown_base_profile_key = "should_warn"
7113
7114 [lint]
7115 severity = ["high"]
7116 unknown_base_lint_key = "should_warn"
7117
7118 [fmt]
7119 line_length = 100
7120 unknown_base_fmt_key = "should_warn"
7121 "#,
7122 )?;
7123
7124 jail.create_file(
7126 "foundry.toml",
7127 r#"
7128 [profile.default]
7129 extends = "base.toml"
7130 src = "src"
7131 unknown_local_profile_key = "should_warn"
7132
7133 [lint]
7134 unknown_local_lint_key = "should_warn"
7135
7136 [fuzz]
7137 runs = 512
7138 unknown_local_fuzz_key = "should_warn"
7139
7140 [[profile.default.compilation_restrictions]]
7141 paths = "src/*.sol"
7142 unknown_local_restriction_key = "should_warn"
7143 "#,
7144 )?;
7145
7146 let cfg = Config::load().unwrap();
7147
7148 assert_eq!(cfg.optimizer_runs, Some(800));
7150
7151 let expected_unknown_keys = ["unknown_base_profile_key", "unknown_local_profile_key"];
7159 for expected_key in expected_unknown_keys {
7160 assert!(
7161 cfg.warnings.iter().any(|w| matches!(
7162 w,
7163 crate::Warning::UnknownKey { key, .. } if key == expected_key
7164 )),
7165 "Expected warning for '{}', got: {:?}",
7166 expected_key,
7167 cfg.warnings
7168 );
7169 }
7170
7171 let expected_section_keys = [
7172 ("unknown_base_lint_key", "lint"),
7173 ("unknown_base_fmt_key", "fmt"),
7174 ("unknown_local_lint_key", "lint"),
7175 ("unknown_local_fuzz_key", "fuzz"),
7176 ("unknown_local_restriction_key", "compilation_restrictions"),
7177 ];
7178 for (expected_key, expected_section) in expected_section_keys {
7179 assert!(
7180 cfg.warnings.iter().any(|w| matches!(
7181 w,
7182 crate::Warning::UnknownSectionKey { key, section, .. }
7183 if key == expected_key && section == expected_section
7184 )),
7185 "Expected warning for '{}' in section '{}', got: {:?}",
7186 expected_key,
7187 expected_section,
7188 cfg.warnings
7189 );
7190 }
7191
7192 let unknown_warnings: Vec<_> = cfg
7194 .warnings
7195 .iter()
7196 .filter(|w| {
7197 matches!(w, crate::Warning::UnknownKey { .. })
7198 || matches!(w, crate::Warning::UnknownSectionKey { .. })
7199 })
7200 .collect();
7201 assert_eq!(
7202 unknown_warnings.len(),
7203 7,
7204 "Expected 7 unknown key warnings, got {}: {:?}",
7205 unknown_warnings.len(),
7206 unknown_warnings
7207 );
7208
7209 Ok(())
7210 });
7211 }
7212
7213 #[test]
7215 fn warns_on_unknown_profile() {
7216 figment::Jail::expect_with(|jail| {
7217 jail.create_file(
7218 "foundry.toml",
7219 r#"
7220 [profile.default]
7221 src = "src"
7222 "#,
7223 )?;
7224
7225 jail.set_env("FOUNDRY_PROFILE", "nonexistent");
7226 let cfg = Config::load().expect("expected unknown profile to fall back to default");
7227 assert_eq!(cfg.profile, Config::DEFAULT_PROFILE);
7228 assert!(
7229 cfg.warnings.iter().any(|w| matches!(
7230 w,
7231 crate::Warning::UnknownProfile { profile } if profile == "nonexistent"
7232 )),
7233 "Expected UnknownProfile warning, got: {:?}",
7234 cfg.warnings
7235 );
7236
7237 Ok(())
7238 });
7239 }
7240
7241 #[test]
7243 fn no_false_warnings_for_vyper_config_keys() {
7244 figment::Jail::expect_with(|jail| {
7245 jail.create_file(
7246 "foundry.toml",
7247 r#"
7248 [profile.default]
7249 src = "src"
7250
7251 [vyper]
7252 optimize = "O1"
7253 path = "/usr/bin/vyper"
7254 experimental_codegen = true
7255 venom_experimental = false
7256 debug = true
7257 enable_decimals = true
7258
7259 [vyper.venom]
7260 disable_cse = true
7261 inline_threshold = 15
7262 "#,
7263 )?;
7264
7265 let cfg = Config::load().unwrap();
7266 let vyper_warnings: Vec<_> = cfg
7268 .warnings
7269 .iter()
7270 .filter(|w| {
7271 matches!(
7272 w,
7273 crate::Warning::UnknownSectionKey { section, .. } if section == "vyper"
7274 )
7275 })
7276 .collect();
7277
7278 assert!(
7279 vyper_warnings.is_empty(),
7280 "Valid vyper keys should not trigger warnings, got: {vyper_warnings:?}"
7281 );
7282
7283 Ok(())
7284 });
7285 }
7286
7287 #[test]
7289 fn no_false_warnings_for_nested_vyper_config_keys() {
7290 figment::Jail::expect_with(|jail| {
7291 jail.create_file(
7292 "foundry.toml",
7293 r#"
7294 [profile.default]
7295 src = "src"
7296
7297 [profile.default.vyper]
7298 opt_level = "s"
7299 path = "/opt/vyper/bin/vyper"
7300 experimental_codegen = false
7301 debug = true
7302 enable_decimals = true
7303 venom = { disable_sccp = true, disable_mem2var = false }
7304 "#,
7305 )?;
7306
7307 let cfg = Config::load().unwrap();
7308 let vyper_warnings: Vec<_> = cfg
7310 .warnings
7311 .iter()
7312 .filter(|w| {
7313 matches!(
7314 w,
7315 crate::Warning::UnknownSectionKey { section, .. } if section == "vyper"
7316 )
7317 })
7318 .collect();
7319
7320 assert!(
7321 vyper_warnings.is_empty(),
7322 "Valid nested vyper keys should not trigger warnings, got: {vyper_warnings:?}"
7323 );
7324
7325 Ok(())
7326 });
7327 }
7328
7329 #[test]
7332 fn no_false_warnings_for_inline_vyper_config() {
7333 figment::Jail::expect_with(|jail| {
7334 jail.create_file(
7335 "foundry.toml",
7336 r#"
7337 [profile.default]
7338 src = "src"
7339 vyper = { optimize = "gas", debug = true }
7340
7341 [profile.default-venom]
7342 vyper = { opt_level = "O2", experimental_codegen = true, venom = { disable_cse = true } }
7343
7344 [profile.ci-venom]
7345 vyper = { venom_experimental = true, enable_decimals = true }
7346 "#,
7347 )?;
7348
7349 let cfg = Config::load().unwrap();
7350 let vyper_warnings: Vec<_> = cfg
7351 .warnings
7352 .iter()
7353 .filter(|w| {
7354 matches!(
7355 w,
7356 crate::Warning::UnknownSectionKey { section, .. } if section == "vyper"
7357 )
7358 })
7359 .collect();
7360
7361 assert!(
7362 vyper_warnings.is_empty(),
7363 "Valid inline vyper config should not trigger warnings, got: {vyper_warnings:?}"
7364 );
7365
7366 Ok(())
7367 });
7368 }
7369
7370 #[test]
7372 fn warns_on_unknown_vyper_keys() {
7373 figment::Jail::expect_with(|jail| {
7374 jail.create_file(
7375 "foundry.toml",
7376 r#"
7377 [profile.default]
7378 src = "src"
7379
7380 [vyper]
7381 optimize = "gas"
7382 unknown_vyper_option = true
7383 "#,
7384 )?;
7385
7386 let cfg = Config::load().unwrap();
7387 assert!(
7388 cfg.warnings.iter().any(|w| matches!(
7389 w,
7390 crate::Warning::UnknownSectionKey { key, section, .. }
7391 if key == "unknown_vyper_option" && section == "vyper"
7392 )),
7393 "Unknown vyper key should trigger warning, got: {:?}",
7394 cfg.warnings
7395 );
7396
7397 Ok(())
7398 });
7399 }
7400
7401 #[test]
7403 fn succeeds_on_known_profile() {
7404 figment::Jail::expect_with(|jail| {
7405 jail.create_file(
7406 "foundry.toml",
7407 r#"
7408 [profile.default]
7409 src = "src"
7410
7411 [profile.ci]
7412 src = "src"
7413 fuzz = { runs = 10000 }
7414 "#,
7415 )?;
7416
7417 jail.set_env("FOUNDRY_PROFILE", "ci");
7418 let config = Config::load().expect("known profile should work");
7419 assert_eq!(config.profile.as_str(), "ci");
7420 assert_eq!(config.fuzz.runs, 10000);
7421
7422 Ok(())
7423 });
7424 }
7425
7426 #[test]
7429 fn nested_lib_config_falls_back_to_default_profile() {
7430 figment::Jail::expect_with(|jail| {
7431 let lib_path = jail.directory().join("lib/mylib");
7433 std::fs::create_dir_all(&lib_path).unwrap();
7434 jail.create_file(
7435 "lib/mylib/foundry.toml",
7436 r#"
7437 [profile.default]
7438 src = "contracts"
7439 "#,
7440 )?;
7441
7442 jail.set_env("FOUNDRY_PROFILE", "ci");
7444
7445 let config = Config::load_with_root_and_fallback(&lib_path)
7447 .expect("lib config should load with fallback");
7448 assert_eq!(config.profile, Config::DEFAULT_PROFILE);
7449 assert_eq!(config.src.as_os_str(), "contracts");
7450
7451 Ok(())
7452 });
7453 }
7454
7455 #[test]
7457 fn nested_lib_config_uses_profile_if_exists() {
7458 figment::Jail::expect_with(|jail| {
7459 let lib_path = jail.directory().join("lib/mylib");
7461 std::fs::create_dir_all(&lib_path).unwrap();
7462 jail.create_file(
7463 "lib/mylib/foundry.toml",
7464 r#"
7465 [profile.default]
7466 src = "contracts"
7467
7468 [profile.ci]
7469 src = "contracts"
7470 fuzz = { runs = 5000 }
7471 "#,
7472 )?;
7473
7474 jail.set_env("FOUNDRY_PROFILE", "ci");
7476
7477 let config = Config::load_with_root_and_fallback(&lib_path)
7479 .expect("lib config should load with profile");
7480 assert_eq!(config.profile.as_str(), "ci");
7481 assert_eq!(config.fuzz.runs, 5000);
7482
7483 Ok(())
7484 });
7485 }
7486
7487 #[test]
7489 fn succeeds_on_hyphenated_profile_name() {
7490 figment::Jail::expect_with(|jail| {
7491 jail.create_file(
7492 "foundry.toml",
7493 r#"
7494 [profile.default]
7495 src = "src"
7496
7497 [profile.ci-venom]
7498 src = "src"
7499 fuzz = { runs = 7500 }
7500
7501 [profile.default-venom]
7502 src = "src"
7503 fuzz = { runs = 8000 }
7504 "#,
7505 )?;
7506
7507 jail.set_env("FOUNDRY_PROFILE", "ci-venom");
7509 let config = Config::load().expect("hyphenated profile should work");
7510 assert_eq!(config.profile.as_str(), "ci-venom");
7511 assert_eq!(config.fuzz.runs, 7500);
7512
7513 jail.set_env("FOUNDRY_PROFILE", "default-venom");
7515 let config = Config::load().expect("hyphenated profile should work");
7516 assert_eq!(config.profile.as_str(), "default-venom");
7517 assert_eq!(config.fuzz.runs, 8000);
7518
7519 assert!(
7521 config.profiles.iter().any(|p| p.as_str() == "ci-venom"),
7522 "profiles should contain 'ci-venom', got: {:?}",
7523 config.profiles
7524 );
7525 assert!(
7526 config.profiles.iter().any(|p| p.as_str() == "default-venom"),
7527 "profiles should contain 'default-venom', got: {:?}",
7528 config.profiles
7529 );
7530
7531 Ok(())
7532 });
7533 }
7534
7535 #[test]
7537 fn hyphenated_profile_with_nested_sections() {
7538 figment::Jail::expect_with(|jail| {
7539 jail.create_file(
7540 "foundry.toml",
7541 r#"
7542 [profile.default]
7543 src = "src"
7544
7545 [profile.ci-venom]
7546 src = "src"
7547 optimizer_runs = 500
7548
7549 [profile.ci-venom.fuzz]
7550 runs = 10000
7551 max_test_rejects = 350000
7552
7553 [profile.ci-venom.invariant]
7554 runs = 375
7555 depth = 500
7556 "#,
7557 )?;
7558
7559 jail.set_env("FOUNDRY_PROFILE", "ci-venom");
7560 let config =
7561 Config::load().expect("hyphenated profile with nested sections should work");
7562 assert_eq!(config.profile.as_str(), "ci-venom");
7563 assert_eq!(config.optimizer_runs, Some(500));
7564 assert_eq!(config.fuzz.runs, 10000);
7565 assert_eq!(config.fuzz.max_test_rejects, 350000);
7566 assert_eq!(config.invariant.runs, 375);
7567 assert_eq!(config.invariant.depth, 500);
7568
7569 Ok(())
7570 });
7571 }
7572}