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