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, etherscan::EtherscanEnvProvider};
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
53#[cfg(windows)]
54use path_slash::PathBufExt as _;
55
56mod macros;
57
58pub mod utils;
59pub use foundry_evm_hardforks::{
60 ExecutionSpec, FoundryHardfork, FromEvmVersion, evm_spec_id, evm_spec_id_from_str,
61};
62pub use utils::*;
63
64mod endpoints;
65pub use endpoints::{
66 ResolvedRpcEndpoint, ResolvedRpcEndpoints, RpcEndpoint, RpcEndpointUrl, RpcEndpoints,
67 builtin_rpc_url,
68};
69
70mod etherscan;
71pub use etherscan::{EtherscanConfigError, EtherscanConfigs, ResolvedEtherscanConfig};
72
73pub mod resolve;
74pub use resolve::UnresolvedEnvVarError;
75
76pub mod cache;
77use cache::{Cache, ChainCache};
78
79pub mod fmt;
80pub use fmt::FormatterConfig;
81
82pub mod lint;
83pub use lint::{LinterConfig, Severity as LintSeverity};
84
85pub mod fs_permissions;
86use fs_permissions::PathPermission;
87
88pub use fs_permissions::FsPermissions;
89
90pub mod error;
91use error::ExtractConfigError;
92
93pub use error::SolidityErrorCode;
94
95pub mod doc;
96pub use doc::DocConfig;
97
98pub mod filter;
99pub use filter::SkipBuildFilters;
100
101mod warning;
102pub use warning::*;
103
104pub mod fix;
105
106pub use alloy_chains::{Chain, NamedChain};
108pub use figment;
109
110pub mod providers;
111use providers::*;
112
113pub use providers::Remappings;
114
115mod fuzz;
116pub use fuzz::{FuzzConfig, FuzzCorpusConfig, FuzzCorpusMutationWeights, FuzzDictionaryConfig};
117
118mod invariant;
119pub use invariant::{InvariantConfig, InvariantDepthMode, InvariantWorkers};
120
121mod symbolic;
122pub use symbolic::{SymbolicConfig, SymbolicExplorationOrder, SymbolicStorageLayout};
123
124mod coverage;
125pub use coverage::{CoverageConfig, CoverageReportKind, parse_lcov_version};
126
127mod trace;
128pub use trace::TracingConfig;
129
130mod fee;
131pub use fee::Eip1559FeeEstimatePreset;
132
133pub mod mutation;
134pub use mutation::{MutationConfig, MutatorType};
135
136mod inline;
137pub use inline::{InlineConfig, InlineConfigError, NatSpec};
138
139pub mod soldeer;
140use soldeer::{SoldeerConfig, SoldeerDependencyConfig};
141
142mod vyper;
143pub use vyper::VyperConfig;
144
145mod bind_json;
146use bind_json::BindJsonConfig;
147
148mod compilation;
149pub use compilation::{CompilationRestrictions, SettingsOverrides};
150
151pub mod extend;
152use extend::Extends;
153use foundry_evm_networks::NetworkConfigs;
154
155pub use semver;
156
157#[cfg(not(test))]
158static SELECTED_PROFILE: std::sync::OnceLock<Profile> = std::sync::OnceLock::new();
159
160#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
192pub struct Config {
193 #[serde(skip)]
200 pub profile: Profile,
201 #[serde(skip)]
205 pub profiles: Vec<Profile>,
206
207 #[serde(default = "root_default", skip_serializing)]
212 pub root: PathBuf,
213
214 #[serde(default, skip_serializing)]
219 pub extends: Option<Extends>,
220
221 pub src: PathBuf,
225 pub test: PathBuf,
227 pub script: PathBuf,
229 pub out: PathBuf,
231 pub libs: Vec<PathBuf>,
233 #[serde(serialize_with = "remappings_serde::serialize")]
235 pub remappings: Vec<RelativeRemapping>,
236 pub auto_detect_remappings: bool,
238 pub libraries: Vec<String>,
240 pub cache: bool,
242 pub cache_path: PathBuf,
244 pub dynamic_test_linking: bool,
246 pub snapshots: PathBuf,
248 pub gas_snapshot_check: bool,
250 pub gas_snapshot_emit: bool,
252 pub broadcast: PathBuf,
254 pub allow_paths: Vec<PathBuf>,
256 pub include_paths: Vec<PathBuf>,
258 pub skip: Vec<GlobMatcher>,
260 pub force: bool,
262 #[serde(with = "from_str_lowercase")]
264 pub evm_version: EvmVersion,
265 pub hardfork: Option<FoundryHardfork>,
267 pub gas_reports: Vec<String>,
269 pub gas_reports_ignore: Vec<String>,
271 pub gas_reports_include_tests: bool,
273 #[doc(hidden)]
283 pub solc: Option<SolcReq>,
284 pub auto_detect_solc: bool,
286 pub offline: bool,
293 pub optimizer: Option<bool>,
295 pub optimizer_runs: Option<usize>,
306 pub optimizer_details: Option<OptimizerDetails>,
310 pub model_checker: Option<ModelCheckerSettings>,
312 pub verbosity: u8,
314 pub eth_rpc_url: Option<String>,
316 pub eth_rpc_accept_invalid_certs: bool,
318 pub eth_rpc_no_proxy: bool,
323 pub eth_rpc_jwt: Option<String>,
325 pub eth_rpc_timeout: Option<u64>,
327 pub eth_rpc_headers: Option<Vec<String>>,
336 pub eth_rpc_curl: bool,
338 pub etherscan_api_key: Option<String>,
340 #[serde(default, skip_serializing_if = "EtherscanConfigs::is_empty")]
342 pub etherscan: EtherscanConfigs,
343 pub ignored_error_codes: Vec<SolidityErrorCode>,
345 pub ignored_error_codes_from: Vec<(PathBuf, Vec<SolidityErrorCode>)>,
347 #[serde(rename = "ignored_warnings_from")]
349 pub ignored_file_paths: Vec<PathBuf>,
350 pub deny: DenyLevel,
352 #[serde(default, skip_serializing)]
354 pub deny_warnings: bool,
355 #[serde(rename = "match_test")]
357 pub test_pattern: Option<RegexWrapper>,
358 #[serde(rename = "no_match_test")]
360 pub test_pattern_inverse: Option<RegexWrapper>,
361 #[serde(rename = "match_contract")]
363 pub contract_pattern: Option<RegexWrapper>,
364 #[serde(rename = "no_match_contract")]
366 pub contract_pattern_inverse: Option<RegexWrapper>,
367 #[serde(rename = "match_path", with = "from_opt_glob")]
369 pub path_pattern: Option<globset::Glob>,
370 #[serde(rename = "no_match_path", with = "from_opt_glob")]
372 pub path_pattern_inverse: Option<globset::Glob>,
373 #[serde(rename = "no_match_coverage")]
375 pub coverage_pattern_inverse: Option<RegexWrapper>,
376 pub test_failures_file: PathBuf,
378 pub mutation_dir: PathBuf,
380 pub threads: Option<usize>,
382 pub show_progress: bool,
384 pub fuzz: FuzzConfig,
386 pub invariant: InvariantConfig,
388 pub symbolic: SymbolicConfig,
390 pub coverage: CoverageConfig,
392 pub mutation: MutationConfig,
394 pub tracing: TracingConfig,
396 pub ffi: bool,
398 pub live_logs: bool,
400 pub allow_internal_expect_revert: bool,
402 pub always_use_create_2_factory: bool,
404 #[serde(default)]
408 pub eip1559_fee_estimate: Eip1559FeeEstimatePreset,
409 pub prompt_timeout: u64,
411 pub sender: Address,
413 pub tx_origin: Address,
415 pub initial_balance: U256,
417 #[serde(
419 deserialize_with = "crate::deserialize_u64_to_u256",
420 serialize_with = "crate::serialize_u64_or_u256"
421 )]
422 pub block_number: U256,
423 pub fork_block_number: Option<u64>,
425 #[serde(rename = "chain_id", alias = "chain")]
427 pub chain: Option<Chain>,
428 pub gas_limit: GasLimit,
430 pub code_size_limit: Option<usize>,
432 pub gas_price: Option<u64>,
437 pub block_base_fee_per_gas: u64,
439 pub block_coinbase: Address,
441 #[serde(
443 deserialize_with = "crate::deserialize_u64_to_u256",
444 serialize_with = "crate::serialize_u64_or_u256"
445 )]
446 pub block_timestamp: U256,
447 pub block_difficulty: u64,
449 pub block_prevrandao: B256,
451 pub block_gas_limit: Option<GasLimit>,
453 pub memory_limit: u64,
458 #[serde(default)]
475 pub extra_output: Vec<ContractOutputSelection>,
476 #[serde(default)]
487 pub extra_output_files: Vec<ContractOutputSelection>,
488 pub names: bool,
490 pub sizes: bool,
492 pub via_ir: bool,
495 pub via_ssa_cfg: bool,
499 pub experimental: bool,
504 pub ast: bool,
506 pub rpc_storage_caching: StorageCachingConfig,
508 pub no_storage_caching: bool,
511 pub no_rpc_rate_limit: bool,
514 #[serde(default, skip_serializing_if = "RpcEndpoints::is_empty")]
516 pub rpc_endpoints: RpcEndpoints,
517 pub use_literal_content: bool,
519 #[serde(with = "from_str_lowercase")]
523 pub bytecode_hash: BytecodeHash,
524 pub cbor_metadata: bool,
529 #[serde(with = "serde_helpers::display_from_str_opt")]
531 pub revert_strings: Option<RevertStrings>,
532 pub sparse_mode: bool,
537 pub build_info: bool,
540 pub build_info_path: Option<PathBuf>,
542 pub fmt: FormatterConfig,
544 pub lint: LinterConfig,
546 pub doc: DocConfig,
548 pub bind_json: BindJsonConfig,
550 pub fs_permissions: FsPermissions,
554
555 pub isolate: bool,
559
560 pub disable_block_gas_limit: bool,
562
563 pub enable_tx_gas_limit: bool,
565
566 #[serde(default, skip_serializing_if = "AddressHashMap::is_empty")]
568 pub labels: AddressHashMap<String>,
569
570 pub unchecked_cheatcode_artifacts: bool,
573
574 pub decode_external_storage: bool,
580
581 pub create2_library_salt: B256,
583
584 pub create2_deployer: Address,
586
587 pub vyper: VyperConfig,
589
590 pub dependencies: Option<SoldeerDependencyConfig>,
592
593 pub soldeer: Option<SoldeerConfig>,
595
596 pub assertions_revert: bool,
600
601 pub legacy_assertions: bool,
603
604 #[serde(default, skip_serializing_if = "Vec::is_empty")]
606 pub extra_args: Vec<String>,
607
608 #[serde(flatten)]
610 pub networks: NetworkConfigs,
611
612 pub transaction_timeout: u64,
614
615 #[serde(rename = "__warnings", default, skip_serializing)]
617 pub warnings: Vec<Warning>,
618
619 #[serde(default)]
621 pub additional_compiler_profiles: Vec<SettingsOverrides>,
622
623 #[serde(default)]
625 pub compilation_restrictions: Vec<CompilationRestrictions>,
626
627 pub script_execution_protection: bool,
629
630 #[doc(hidden)]
639 #[serde(skip)]
640 pub _non_exhaustive: (),
641}
642
643#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum, Default, Serialize)]
645#[serde(rename_all = "lowercase")]
646pub enum DenyLevel {
647 #[default]
649 Never,
650 Warnings,
652 Notes,
654}
655
656impl<'de> Deserialize<'de> for DenyLevel {
659 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
660 where
661 D: Deserializer<'de>,
662 {
663 struct DenyLevelVisitor;
664
665 impl<'de> de::Visitor<'de> for DenyLevelVisitor {
666 type Value = DenyLevel;
667
668 fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
669 formatter.write_str("one of the following strings: `never`, `warnings`, `notes`")
670 }
671
672 fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E>
673 where
674 E: de::Error,
675 {
676 Ok(DenyLevel::from(value))
677 }
678
679 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
680 where
681 E: de::Error,
682 {
683 DenyLevel::from_str(value).map_err(de::Error::custom)
684 }
685 }
686
687 deserializer.deserialize_any(DenyLevelVisitor)
688 }
689}
690
691impl FromStr for DenyLevel {
692 type Err = String;
693
694 fn from_str(s: &str) -> Result<Self, Self::Err> {
695 match s.to_lowercase().as_str() {
696 "warnings" | "warning" | "w" => Ok(Self::Warnings),
697 "notes" | "note" | "n" => Ok(Self::Notes),
698 "never" | "false" | "f" => Ok(Self::Never),
699 _ => Err(format!(
700 "unknown variant: found `{s}`, expected one of `never`, `warnings`, `notes`"
701 )),
702 }
703 }
704}
705
706impl From<bool> for DenyLevel {
707 fn from(deny: bool) -> Self {
708 if deny { Self::Warnings } else { Self::Never }
709 }
710}
711
712impl DenyLevel {
713 pub const fn warnings(&self) -> bool {
715 match self {
716 Self::Never => false,
717 Self::Warnings | Self::Notes => true,
718 }
719 }
720
721 pub const fn notes(&self) -> bool {
723 match self {
724 Self::Never | Self::Warnings => false,
725 Self::Notes => true,
726 }
727 }
728
729 pub const fn never(&self) -> bool {
731 match self {
732 Self::Never => true,
733 Self::Warnings | Self::Notes => false,
734 }
735 }
736}
737
738pub const STANDALONE_FALLBACK_SECTIONS: &[(&str, &str)] = &[("invariant", "fuzz")];
740
741pub const DEPRECATIONS: &[(&str, &str)] =
745 &[("cancun", "evm_version = Cancun"), ("deny_warnings", "deny = warnings")];
746
747impl Config {
748 pub const DEFAULT_PROFILE: Profile = Profile::Default;
750
751 pub const HARDHAT_PROFILE: Profile = Profile::const_new("hardhat");
753
754 pub const PROFILE_SECTION: &'static str = "profile";
756
757 pub const EXTERNAL_SECTION: &'static str = "external";
759
760 pub const STANDALONE_SECTIONS: &'static [&'static str] = &[
762 "rpc_endpoints",
763 "etherscan",
764 "fmt",
765 "lint",
766 "doc",
767 "fuzz",
768 "invariant",
769 "symbolic",
770 "coverage",
771 "mutation",
772 "tracing",
773 "labels",
774 "dependencies",
775 "soldeer",
776 "vyper",
777 "bind_json",
778 ];
779
780 pub(crate) fn is_standalone_section<T: ?Sized + PartialEq<str>>(section: &T) -> bool {
781 section == Self::PROFILE_SECTION
782 || section == Self::EXTERNAL_SECTION
783 || Self::STANDALONE_SECTIONS.iter().any(|s| section == *s)
784 }
785
786 pub const FILE_NAME: &'static str = "foundry.toml";
788
789 const DEFAULT_SRC: &'static str = "src";
790
791 pub const FOUNDRY_DIR_NAME: &'static str = ".foundry";
793
794 pub const DEFAULT_SENDER: Address = address!("0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38");
798
799 pub const DEFAULT_CREATE2_LIBRARY_SALT: FixedBytes<32> = FixedBytes::<32>::ZERO;
801
802 pub const DEFAULT_CREATE2_DEPLOYER: Address =
804 address!("0x4e59b44847b379578588920ca78fbf26c0b4956c");
805
806 pub fn load() -> Result<Self, ExtractConfigError> {
810 Self::from_provider(Self::figment())
811 }
812
813 pub fn load_with_providers(providers: FigmentProviders) -> Result<Self, ExtractConfigError> {
817 Self::from_provider(Self::default().to_figment(providers))
818 }
819
820 #[track_caller]
824 pub fn load_with_root(root: impl AsRef<Path>) -> Result<Self, ExtractConfigError> {
825 Self::from_provider(Self::figment_with_root(root.as_ref()))
826 }
827
828 #[track_caller]
835 pub fn load_with_root_and_fallback(root: impl AsRef<Path>) -> Result<Self, ExtractConfigError> {
836 let figment = Self::figment_with_root(root.as_ref());
837 Self::from_figment_fallback(Figment::from(figment))
838 }
839
840 #[doc(alias = "try_from")]
855 pub fn from_provider<T: Provider>(provider: T) -> Result<Self, ExtractConfigError> {
856 trace!("load config with provider: {:?}", provider.metadata());
857 Self::from_figment(Figment::from(provider.legacy_labels()))
858 }
859
860 pub fn merge_inline_provider<T: Provider>(&self, provider: T) -> Result<Self, Error> {
863 let provider = Figment::from(provider.legacy_labels()).select(self.profile.clone());
864 let invariant_corpus_random_sequence_weight_configured =
865 self.invariant.corpus_random_sequence_weight_configured
866 || provider.contains("invariant.corpus_random_sequence_weight")
867 || provider
868 .extract_inner::<bool>("invariant.corpus_random_sequence_weight_configured")
869 .unwrap_or(false);
870 let invariant_workers_configured = self.invariant.workers_configured
871 || provider.contains("invariant.workers")
872 || provider.extract_inner::<bool>("invariant.workers_configured").unwrap_or(false)
873 || provider.extract_inner::<InvariantWorkers>("invariant.workers").is_ok();
874 let figment = self.to_figment(FigmentProviders::None).merge(provider);
875 let mut config = figment.extract::<Self>()?;
876 config.profile = self.profile.clone();
877 config.profiles = self.profiles.clone();
878 config.invariant.corpus_random_sequence_weight_configured =
879 invariant_corpus_random_sequence_weight_configured;
880 config.invariant.workers_configured = invariant_workers_configured;
881 config.normalize_hardfork_settings()?;
882
883 Ok(config)
884 }
885
886 #[doc(hidden)]
887 #[deprecated(note = "use `Config::from_provider` instead")]
888 pub fn try_from<T: Provider>(provider: T) -> Result<Self, ExtractConfigError> {
889 Self::from_provider(provider)
890 }
891
892 fn from_figment(figment: Figment) -> Result<Self, ExtractConfigError> {
893 Self::from_figment_inner(figment, true)
894 }
895
896 fn from_figment_fallback(figment: Figment) -> Result<Self, ExtractConfigError> {
899 Self::from_figment_inner(figment, false)
900 }
901
902 fn from_figment_inner(
903 figment: Figment,
904 strict_profile: bool,
905 ) -> Result<Self, ExtractConfigError> {
906 let invariant_corpus_random_sequence_weight_configured = figment
907 .extract_inner::<bool>("invariant.corpus_random_sequence_weight_configured")
908 .unwrap_or_else(|_| {
909 figment_value_is_configured(&figment, "invariant.corpus_random_sequence_weight")
910 });
911 let invariant_workers_configured = figment
912 .extract_inner::<bool>("invariant.workers_configured")
913 .unwrap_or_else(|_| figment_value_is_configured(&figment, "invariant.workers"));
914 let mut config = figment.extract::<Self>().map_err(ExtractConfigError::new)?;
915 config.invariant.corpus_random_sequence_weight_configured =
916 invariant_corpus_random_sequence_weight_configured;
917 config.invariant.workers_configured = invariant_workers_configured;
918 let selected_profile = figment.profile().clone();
919
920 fn add_profile(profiles: &mut Vec<Profile>, profile: &Profile) {
922 if !profiles.contains(profile) {
923 profiles.push(profile.clone());
924 }
925 }
926 let figment = figment.select(Self::PROFILE_SECTION);
927 if let Ok(data) = figment.data()
928 && let Some(profiles) = data.get(&Profile::new(Self::PROFILE_SECTION))
929 {
930 for profile in profiles.keys() {
931 add_profile(&mut config.profiles, &Profile::new(profile));
932 }
933 }
934 add_profile(&mut config.profiles, &Self::DEFAULT_PROFILE);
935
936 if config.profiles.contains(&selected_profile) {
938 config.profile = selected_profile;
939 } else {
940 if strict_profile {
944 config
945 .warnings
946 .push(Warning::UnknownProfile { profile: selected_profile.to_string() });
947 }
948 config.profile = Self::DEFAULT_PROFILE;
949 }
950
951 config.normalize_optimizer_settings();
952 config.normalize_hardfork_settings().map_err(ExtractConfigError::new)?;
953
954 if let Some(runs) = config.optimizer_runs
956 && runs > u32::MAX as usize
957 {
958 return Err(ExtractConfigError::new(Error::from(format!(
959 "`optimizer_runs` value {} exceeds maximum allowed value of {}",
960 runs,
961 u32::MAX
962 ))));
963 }
964
965 Ok(config)
966 }
967
968 fn normalize_hardfork_settings(&mut self) -> Result<(), Error> {
969 self.networks.validate().map_err(Error::from)?;
970 let Some(hardfork) = self.hardfork else { return Ok(()) };
971 self.networks = self.networks.normalize_for_hardfork(hardfork).map_err(Error::from)?;
972 Ok(())
973 }
974
975 fn uses_default_src(&self) -> bool {
976 self.src == Path::new(Self::DEFAULT_SRC)
977 }
978
979 pub fn to_figment(&self, providers: FigmentProviders) -> Figment {
984 if providers.is_none() {
987 return Figment::from(self);
988 }
989
990 let root = self.root.as_path();
991 let profile = Self::selected_profile();
992 let mut figment = Figment::default()
993 .merge(DappHardhatDirProvider { root, detect_src: self.uses_default_src() });
994
995 if let Some(global_toml) = Self::foundry_dir_toml().filter(|p| p.exists()) {
997 figment = Self::merge_toml_provider(
998 figment,
999 TomlFileProvider::new(None, global_toml),
1000 profile.clone(),
1001 );
1002 }
1003 figment = Self::merge_toml_provider(
1005 figment,
1006 TomlFileProvider::new(Some("FOUNDRY_CONFIG"), root.join(Self::FILE_NAME)),
1007 profile.clone(),
1008 );
1009
1010 figment = figment
1012 .merge(
1013 Env::prefixed("DAPP_")
1014 .ignore(&["REMAPPINGS", "LIBRARIES", "FFI", "FS_PERMISSIONS"])
1015 .global()
1016 .legacy_labels(),
1017 )
1018 .merge(
1019 Env::prefixed("DAPP_TEST_")
1020 .ignore(&["CACHE", "FUZZ_RUNS", "DEPTH", "FFI", "FS_PERMISSIONS"])
1021 .global()
1022 .legacy_labels(),
1023 )
1024 .merge(DappEnvCompatProvider)
1025 .merge(EtherscanEnvProvider::default())
1026 .merge(
1027 Env::prefixed("FOUNDRY_")
1028 .ignore(&["PROFILE", "REMAPPINGS", "LIBRARIES", "FFI", "FS_PERMISSIONS"])
1029 .map(|key| {
1030 let key = key.as_str();
1031 if Self::STANDALONE_SECTIONS.iter().any(|section| {
1032 key.starts_with(&format!("{}_", section.to_ascii_uppercase()))
1033 }) {
1034 key.replacen('_', ".", 1).into()
1035 } else {
1036 key.into()
1037 }
1038 })
1039 .global()
1040 .legacy_labels(),
1041 )
1042 .select(profile.clone());
1043
1044 if providers.is_all() {
1046 let remappings = RemappingsProvider {
1050 auto_detect_remappings: figment
1051 .extract_inner::<bool>("auto_detect_remappings")
1052 .unwrap_or(true),
1053 lib_paths: figment
1054 .extract_inner::<Vec<PathBuf>>("libs")
1055 .map(Cow::Owned)
1056 .unwrap_or_else(|_| Cow::Borrowed(&self.libs)),
1057 root,
1058 remappings: figment.extract_inner::<Vec<Remapping>>("remappings"),
1059 };
1060 figment = figment.merge(remappings);
1061 }
1062
1063 let invariant_corpus_random_sequence_weight_configured = self
1064 .invariant
1065 .corpus_random_sequence_weight_configured
1066 || figment
1067 .extract_inner::<bool>("invariant.corpus_random_sequence_weight_configured")
1068 .unwrap_or_else(|_| {
1069 figment_value_is_configured(&figment, "invariant.corpus_random_sequence_weight")
1070 });
1071 let invariant_workers_configured = self.invariant.workers_configured
1072 || figment.extract_inner::<bool>("invariant.workers_configured").unwrap_or_else(|_| {
1073 figment.extract_inner::<InvariantWorkers>("invariant.workers").is_ok()
1074 });
1075
1076 figment = self.normalize_defaults(figment);
1078 if invariant_corpus_random_sequence_weight_configured {
1079 figment = figment.merge(("invariant.corpus_random_sequence_weight_configured", true));
1080 }
1081 if invariant_workers_configured {
1082 figment = figment.merge(("invariant.workers_configured", true));
1083 }
1084
1085 Figment::from(self).merge(figment).select(profile)
1086 }
1087
1088 #[must_use]
1093 pub fn canonic(self) -> Self {
1094 let root = self.root.clone();
1095 self.canonic_at(root)
1096 }
1097
1098 #[must_use]
1116 pub fn canonic_at(mut self, root: impl Into<PathBuf>) -> Self {
1117 let root = canonic(root);
1118
1119 fn p(root: &Path, rem: &Path) -> PathBuf {
1120 canonic(root.join(rem))
1121 }
1122
1123 self.src = p(&root, &self.src);
1124 self.test = p(&root, &self.test);
1125 self.script = p(&root, &self.script);
1126 self.out = p(&root, &self.out);
1127 self.broadcast = p(&root, &self.broadcast);
1128 self.cache_path = p(&root, &self.cache_path);
1129 self.snapshots = p(&root, &self.snapshots);
1130 self.test_failures_file = p(&root, &self.test_failures_file);
1131
1132 if let Some(build_info_path) = self.build_info_path {
1133 self.build_info_path = Some(p(&root, &build_info_path));
1134 }
1135
1136 self.libs = self.libs.into_iter().map(|lib| p(&root, &lib)).collect();
1137
1138 self.remappings = self
1139 .remappings
1140 .into_iter()
1141 .map(|r| relative_remapping_preserving_context_boundary(r.into(), &root))
1142 .collect();
1143
1144 self.allow_paths = self.allow_paths.into_iter().map(|allow| p(&root, &allow)).collect();
1145
1146 self.include_paths = self.include_paths.into_iter().map(|allow| p(&root, &allow)).collect();
1147
1148 self.fs_permissions.join_all(&root);
1149
1150 if let Some(model_checker) = &mut self.model_checker {
1151 model_checker.contracts = std::mem::take(&mut model_checker.contracts)
1152 .into_iter()
1153 .map(|(path, contracts)| {
1154 (format!("{}", p(&root, path.as_ref()).display()), contracts)
1155 })
1156 .collect();
1157 }
1158
1159 self
1160 }
1161
1162 pub fn normalized_evm_version(mut self) -> Self {
1164 self.normalize_evm_version();
1165 self
1166 }
1167
1168 pub const fn normalized_optimizer_settings(mut self) -> Self {
1171 self.normalize_optimizer_settings();
1172 self
1173 }
1174
1175 pub fn normalize_evm_version(&mut self) {
1177 self.evm_version = self.get_normalized_evm_version();
1178 }
1179
1180 pub const fn normalize_optimizer_settings(&mut self) {
1185 match (self.optimizer, self.optimizer_runs) {
1186 (None, None) => {
1188 self.optimizer = Some(false);
1189 self.optimizer_runs = Some(200);
1190 }
1191 (Some(_), None) => self.optimizer_runs = Some(200),
1193 (None, Some(runs)) => self.optimizer = Some(runs > 0),
1195 _ => {}
1196 }
1197 }
1198
1199 pub fn get_normalized_evm_version(&self) -> EvmVersion {
1201 if let Some(version) = self.solc_version()
1202 && let Some(evm_version) = self.evm_version.normalize_version_solc(&version)
1203 {
1204 return evm_version;
1205 }
1206 self.evm_version
1207 }
1208
1209 #[must_use]
1214 pub fn sanitized(self) -> Self {
1215 let mut config = self.canonic();
1216
1217 config.sanitize_remappings();
1218
1219 config.libs.sort_unstable();
1220 config.libs.dedup();
1221
1222 config
1223 }
1224
1225 #[allow(clippy::missing_const_for_fn)]
1229 pub fn sanitize_remappings(&mut self) {
1230 #[cfg(target_os = "windows")]
1231 {
1232 use path_slash::PathBufExt;
1234 self.remappings.iter_mut().for_each(|r| {
1235 r.path.path = r.path.path.to_slash_lossy().into_owned().into();
1236 });
1237 }
1238 }
1239
1240 pub fn install_lib_dir(&self) -> &Path {
1244 self.libs
1245 .iter()
1246 .find(|p| !p.ends_with("node_modules"))
1247 .map(|p| p.as_path())
1248 .unwrap_or_else(|| Path::new("lib"))
1249 }
1250
1251 pub fn project(&self) -> Result<Project<MultiCompiler>, SolcError> {
1266 self.create_project(self.cache, false)
1267 }
1268
1269 pub fn ephemeral_project(&self) -> Result<Project<MultiCompiler>, SolcError> {
1272 self.create_project(false, true)
1273 }
1274
1275 pub fn solar_project(&self) -> Result<Project<MultiCompiler>, SolcError> {
1279 let ui_testing = std::env::var_os("FOUNDRY_LINT_UI_TESTING").is_some();
1280 let mut project = self.create_project(self.cache && !ui_testing, false)?;
1281 project.update_output_selection(|selection| {
1282 *selection = OutputSelection::common_output_selection(["abi".into()]);
1285 });
1286 Ok(project)
1287 }
1288
1289 fn additional_settings(
1291 &self,
1292 base: &MultiCompilerSettings,
1293 ) -> BTreeMap<String, MultiCompilerSettings> {
1294 let mut map = BTreeMap::new();
1295
1296 for profile in &self.additional_compiler_profiles {
1297 let mut settings = base.clone();
1298 profile.apply(&mut settings);
1299 map.insert(profile.name.clone(), settings);
1300 }
1301
1302 map
1303 }
1304
1305 #[expect(clippy::disallowed_macros)]
1307 fn restrictions(
1308 &self,
1309 paths: &ProjectPathsConfig,
1310 ) -> Result<BTreeMap<PathBuf, RestrictionsWithVersion<MultiCompilerRestrictions>>, SolcError>
1311 {
1312 let mut map: BTreeMap<PathBuf, RestrictionsWithVersion<MultiCompilerRestrictions>> =
1313 BTreeMap::new();
1314 if self.compilation_restrictions.is_empty() {
1315 return Ok(BTreeMap::new());
1316 }
1317
1318 let graph = Graph::<MultiCompilerParser>::resolve(paths)?;
1319 let (sources, _) = graph.into_sources();
1320
1321 for res in &self.compilation_restrictions {
1322 for source in sources.keys().filter(|path| {
1323 if res.paths.is_match(path) {
1324 true
1325 } else if let Ok(path) = path.strip_prefix(&paths.root) {
1326 res.paths.is_match(path)
1327 } else {
1328 false
1329 }
1330 }) {
1331 let res: RestrictionsWithVersion<_> =
1332 res.clone().try_into().map_err(SolcError::msg)?;
1333 if map.contains_key(source) {
1334 let value = map.remove(source.as_path()).unwrap();
1335 if let Some(merged) = value.clone().merge(res) {
1336 map.insert(source.clone(), merged);
1337 } else {
1338 eprintln!(
1340 "{}",
1341 yansi::Paint::yellow(&format!(
1342 "Failed to merge compilation restrictions for {}",
1343 source.display()
1344 ))
1345 );
1346 map.insert(source.clone(), value);
1347 }
1348 } else {
1349 map.insert(source.clone(), res);
1350 }
1351 }
1352 }
1353
1354 Ok(map)
1355 }
1356
1357 pub fn create_project(&self, cached: bool, no_artifacts: bool) -> Result<Project, SolcError> {
1361 let settings = self.compiler_settings()?;
1362 let paths = self.project_paths();
1363
1364 let parse_path = |path: &PathBuf| path.strip_prefix("./").unwrap_or(path).to_path_buf();
1366
1367 let mut builder = Project::builder()
1368 .artifacts(self.configured_artifacts_handler())
1369 .additional_settings(self.additional_settings(&settings))
1370 .restrictions(self.restrictions(&paths)?)
1371 .settings(settings)
1372 .paths(paths)
1373 .ignore_error_codes(self.ignored_error_codes.iter().copied().map(Into::into))
1374 .ignore_error_codes_from(self.ignored_error_codes_from.iter().map(|(path, codes)| {
1375 (parse_path(path), codes.iter().copied().map(Into::into).collect())
1376 }))
1377 .ignore_paths(self.ignored_file_paths.iter().map(parse_path).collect())
1378 .set_compiler_severity_filter(if self.deny.warnings() {
1379 Severity::Warning
1380 } else {
1381 Severity::Error
1382 })
1383 .set_offline(self.offline)
1384 .set_cached(cached)
1385 .set_build_info(!no_artifacts && self.build_info)
1386 .set_no_artifacts(no_artifacts);
1387
1388 if !self.skip.is_empty() {
1389 let filter = SkipBuildFilters::new(self.skip.clone(), self.root.clone());
1390 builder = builder.sparse_output(filter);
1391 }
1392
1393 let project = builder.build(self.compiler()?)?;
1394
1395 #[cfg(windows)]
1399 let mut project = project;
1400 #[cfg(windows)]
1401 for remapping in &mut project.paths.remappings {
1402 if let Some(context) = &mut remapping.context
1403 && context.ends_with('/')
1404 {
1405 context.pop();
1406 context.push(std::path::MAIN_SEPARATOR);
1407 }
1408 }
1409
1410 if self.force {
1411 let _ = self.cleanup(&project);
1414 }
1415
1416 Ok(project)
1417 }
1418
1419 pub fn disable_optimizations(&self, project: &mut Project, ir_minimum: bool) {
1421 if ir_minimum {
1422 project.settings.solc.settings = std::mem::take(&mut project.settings.solc.settings)
1425 .with_via_ir_minimum_optimization();
1426
1427 let evm_version = project.settings.solc.evm_version;
1430 let version = self.solc_version().unwrap_or_else(|| Version::new(0, 8, 4));
1431 project.settings.solc.settings.sanitize(&version, SolcLanguage::Solidity);
1432 project.settings.solc.evm_version = evm_version;
1433 } else {
1434 project.settings.solc.optimizer.disable();
1435 project.settings.solc.optimizer.runs = None;
1436 project.settings.solc.optimizer.details = None;
1437 project.settings.solc.via_ir = None;
1438 }
1439 }
1440
1441 pub fn cleanup<C: Compiler, T: ArtifactOutput<CompilerContract = C::CompilerContract>>(
1446 &self,
1447 project: &Project<C, T>,
1448 ) -> Result<Vec<String>, SolcError> {
1449 let mut warnings = Vec::new();
1450
1451 if let Err(err) = project.cleanup() {
1452 warnings.push(format!("failed to clean project artifacts: {err}"));
1453 }
1454
1455 if let Err(err) = fs::remove_file(&self.test_failures_file)
1457 && err.kind() != io::ErrorKind::NotFound
1458 {
1459 warnings.push(format!(
1460 "failed to remove test failures file {}: {err}",
1461 self.test_failures_file.display()
1462 ));
1463 }
1464
1465 let _ = fs::remove_dir_all(project.root().join(&self.mutation_dir));
1467
1468 let mut remove_test_dir = |test_dir: &Option<PathBuf>| {
1470 if let Some(test_dir) = test_dir {
1471 let path = project.root().join(test_dir);
1472 if let Err(err) = fs::remove_dir_all(&path)
1473 && err.kind() != io::ErrorKind::NotFound
1474 {
1475 warnings.push(format!(
1476 "failed to remove test cache directory {}: {err}",
1477 path.display()
1478 ));
1479 }
1480 }
1481 };
1482 remove_test_dir(&self.fuzz.failure_persist_dir);
1483 remove_test_dir(&self.fuzz.corpus.corpus_dir);
1484 remove_test_dir(&self.fuzz.corpus.frontier_dir);
1485 remove_test_dir(&self.invariant.corpus.corpus_dir);
1486 remove_test_dir(&self.invariant.failure_persist_dir);
1487
1488 Ok(warnings)
1489 }
1490
1491 fn ensure_solc(&self) -> Result<Option<Solc>, SolcError> {
1498 if let Some(solc) = &self.solc {
1499 let solc = match solc {
1500 SolcReq::Version(version) => {
1501 if let Some(solc) = Solc::find_svm_installed_version(version)? {
1502 solc
1503 } else {
1504 if self.offline {
1505 return Err(SolcError::msg(format!(
1506 "can't install missing solc {version} in offline mode"
1507 )));
1508 }
1509 Solc::blocking_install(version)?
1510 }
1511 }
1512 SolcReq::Local(solc) => Solc::new(resolve_solc_path(solc)?)?,
1513 };
1514 return Ok(Some(solc));
1515 }
1516
1517 Ok(None)
1518 }
1519
1520 pub fn evm_spec_id<SPEC: FromEvmVersion>(&self) -> SPEC {
1522 self.hardfork.map(Into::into).unwrap_or_else(|| evm_spec_id(self.evm_version))
1523 }
1524
1525 pub const fn is_auto_detect(&self) -> bool {
1530 if self.solc.is_some() {
1531 return false;
1532 }
1533 self.auto_detect_solc
1534 }
1535
1536 pub fn enable_caching(&self, endpoint: &str, chain_id: impl Into<u64>) -> bool {
1538 !self.no_storage_caching
1539 && self.rpc_storage_caching.enable_for_chain_id(chain_id.into())
1540 && self.rpc_storage_caching.enable_for_endpoint(endpoint)
1541 }
1542
1543 pub fn project_paths<L>(&self) -> ProjectPathsConfig<L> {
1558 let mut builder = ProjectPathsConfig::builder()
1559 .cache(self.cache_path.join(SOLIDITY_FILES_CACHE_FILENAME))
1560 .sources(&self.src)
1561 .tests(&self.test)
1562 .scripts(&self.script)
1563 .artifacts(&self.out)
1564 .libs(self.libs.iter())
1565 .remappings(self.project_remappings())
1566 .allowed_path(&self.root)
1567 .allowed_paths(&self.libs)
1568 .allowed_paths(&self.allow_paths)
1569 .include_paths(&self.include_paths);
1570
1571 if let Some(build_info_path) = &self.build_info_path {
1572 builder = builder.build_infos(build_info_path);
1573 }
1574
1575 builder.build_with_root(&self.root)
1576 }
1577
1578 pub fn solc_compiler(&self) -> Result<SolcCompiler, SolcError> {
1580 if let Some(solc) = self.ensure_solc()? {
1581 Ok(SolcCompiler::Specific(solc))
1582 } else {
1583 Ok(SolcCompiler::AutoDetect)
1584 }
1585 }
1586
1587 pub fn solc_version(&self) -> Option<Version> {
1589 self.solc.as_ref().and_then(|solc| solc.try_version().ok())
1590 }
1591
1592 pub fn vyper_compiler(&self) -> Result<Option<Vyper>, SolcError> {
1594 if !self.project_paths::<VyperLanguage>().has_input_files() {
1596 return Ok(None);
1597 }
1598 let vyper = if let Some(path) = &self.vyper.path {
1599 Some(Vyper::new(path)?)
1600 } else {
1601 Vyper::new("vyper").ok()
1602 };
1603 Ok(vyper)
1604 }
1605
1606 pub fn compiler(&self) -> Result<MultiCompiler, SolcError> {
1608 Ok(MultiCompiler { solc: Some(self.solc_compiler()?), vyper: self.vyper_compiler()? })
1609 }
1610
1611 pub fn compiler_settings(&self) -> Result<MultiCompilerSettings, SolcError> {
1613 Ok(MultiCompilerSettings { solc: self.solc_settings()?, vyper: self.vyper_settings()? })
1614 }
1615
1616 pub fn get_all_remappings(&self) -> impl Iterator<Item = Remapping> + '_ {
1618 self.remappings.iter().map(|m| m.clone().into())
1619 }
1620
1621 fn project_remappings(&self) -> Vec<Remapping> {
1623 let remappings = self.get_all_remappings().collect::<Vec<_>>();
1624 let mut adjusted = Vec::with_capacity(remappings.len());
1625
1626 for remapping in &remappings {
1631 adjusted.push(remapping.clone());
1632
1633 let Some(context) = remapping.context.as_deref() else { continue };
1634 if Path::new(context).is_absolute() {
1635 continue;
1636 }
1637 let Ok(context_path) = foundry_compilers::utils::normalize_solidity_import_path(
1638 &self.root,
1639 Path::new(context),
1640 ) else {
1641 continue;
1642 };
1643 #[cfg(windows)]
1647 let context_path = PathBuf::from_slash(context_path.to_string_lossy());
1648 let mut context_path = context_path.display().to_string();
1649 if context.ends_with(['/', '\\']) && !context_path.ends_with(['/', '\\']) {
1650 context_path.push(std::path::MAIN_SEPARATOR);
1651 }
1652
1653 let mut absolute = remapping.clone();
1654 absolute.context = Some(context_path);
1655 if !remappings.contains(&absolute) && !adjusted.contains(&absolute) {
1656 adjusted.push(absolute);
1657 }
1658 }
1659 adjusted
1660 }
1661
1662 pub fn get_rpc_jwt_secret(&self) -> Result<Option<Cow<'_, str>>, UnresolvedEnvVarError> {
1677 Ok(self.eth_rpc_jwt.as_ref().map(|jwt| Cow::Borrowed(jwt.as_str())))
1678 }
1679
1680 pub fn get_rpc_url(&self) -> Option<Result<Cow<'_, str>, UnresolvedEnvVarError>> {
1696 let maybe_alias = self.eth_rpc_url.as_deref()?;
1697 if let Some(alias) = self.get_rpc_url_with_alias(maybe_alias) {
1698 Some(alias)
1699 } else {
1700 Some(Ok(Cow::Borrowed(self.eth_rpc_url.as_deref()?)))
1701 }
1702 }
1703
1704 pub fn get_rpc_url_with_alias(
1729 &self,
1730 maybe_alias: &str,
1731 ) -> Option<Result<Cow<'_, str>, UnresolvedEnvVarError>> {
1732 let mut endpoints = self.rpc_endpoints.clone().resolved();
1733 if let Some(endpoint) = endpoints.remove(maybe_alias) {
1734 return Some(endpoint.url().map(Cow::Owned));
1735 }
1736
1737 if let Some(mesc_url) = self.get_rpc_url_from_mesc(maybe_alias) {
1738 return Some(Ok(Cow::Owned(mesc_url)));
1739 }
1740
1741 if let Some(builtin) = crate::endpoints::builtin_rpc_url(maybe_alias) {
1742 return Some(Ok(Cow::Borrowed(builtin)));
1743 }
1744
1745 None
1746 }
1747
1748 pub fn get_rpc_url_from_mesc(&self, maybe_alias: &str) -> Option<String> {
1750 let mesc_config = mesc::load::load_config_data()
1753 .inspect_err(|err| debug!(%err, "failed to load mesc config"))
1754 .ok()?;
1755
1756 if let Ok(Some(endpoint)) =
1757 mesc::query::get_endpoint_by_query(&mesc_config, maybe_alias, Some("foundry"))
1758 {
1759 return Some(endpoint.url);
1760 }
1761
1762 if maybe_alias.chars().all(|c| c.is_numeric()) {
1763 if let Ok(Some(endpoint)) =
1769 mesc::query::get_endpoint_by_network(&mesc_config, maybe_alias, Some("foundry"))
1770 {
1771 return Some(endpoint.url);
1772 }
1773 }
1774
1775 None
1776 }
1777
1778 pub fn get_rpc_url_or<'a>(
1790 &'a self,
1791 fallback: impl Into<Cow<'a, str>>,
1792 ) -> Result<Cow<'a, str>, UnresolvedEnvVarError> {
1793 if let Some(url) = self.get_rpc_url() { url } else { Ok(fallback.into()) }
1794 }
1795
1796 pub fn get_rpc_url_or_localhost_http(&self) -> Result<Cow<'_, str>, UnresolvedEnvVarError> {
1808 self.get_rpc_url_or("http://localhost:8545")
1809 }
1810
1811 pub fn get_etherscan_config(
1831 &self,
1832 ) -> Option<Result<ResolvedEtherscanConfig, EtherscanConfigError>> {
1833 self.get_etherscan_config_with_chain(None).transpose()
1834 }
1835
1836 pub fn get_etherscan_config_with_chain(
1843 &self,
1844 chain: Option<Chain>,
1845 ) -> Result<Option<ResolvedEtherscanConfig>, EtherscanConfigError> {
1846 self.etherscan.resolve_for(
1847 self.etherscan_alias(),
1848 self.etherscan_api_key.as_deref(),
1849 chain.or(self.chain),
1850 )
1851 }
1852
1853 pub fn etherscan_alias(&self) -> Option<&str> {
1855 self.etherscan_api_key.as_deref().or(self.eth_rpc_url.as_deref())
1856 }
1857
1858 #[expect(clippy::disallowed_macros)]
1864 pub fn get_etherscan_api_key(&self, chain: Option<Chain>) -> Option<String> {
1865 self.get_etherscan_config_with_chain(chain)
1866 .map_err(|e| {
1867 eprintln!(
1869 "{}: failed getting etherscan config: {e}",
1870 yansi::Paint::yellow("Warning"),
1871 );
1872 })
1873 .ok()
1874 .flatten()
1875 .map(|c| c.key)
1876 }
1877
1878 pub fn get_source_dir_remapping(&self) -> Option<Remapping> {
1885 get_dir_remapping(&self.src)
1886 }
1887
1888 pub fn get_test_dir_remapping(&self) -> Option<Remapping> {
1890 if self.root.join(&self.test).exists() { get_dir_remapping(&self.test) } else { None }
1891 }
1892
1893 pub fn get_script_dir_remapping(&self) -> Option<Remapping> {
1895 if self.root.join(&self.script).exists() { get_dir_remapping(&self.script) } else { None }
1896 }
1897
1898 pub fn optimizer(&self) -> Optimizer {
1904 Optimizer {
1905 enabled: self.optimizer,
1906 runs: self.optimizer_runs,
1907 details: self.optimizer_details.clone(),
1910 }
1911 }
1912
1913 pub fn configured_artifacts_handler(&self) -> ConfigurableArtifacts {
1916 let mut extra_output = self.extra_output.clone();
1917
1918 if !extra_output.contains(&ContractOutputSelection::Metadata) {
1924 extra_output.push(ContractOutputSelection::Metadata);
1925 }
1926
1927 ConfigurableArtifacts::new(extra_output, self.extra_output_files.iter().copied())
1928 }
1929
1930 pub fn parsed_libraries(&self) -> Result<Libraries, SolcError> {
1933 Libraries::parse(&self.libraries)
1934 }
1935
1936 pub fn libraries_with_remappings(&self) -> Result<Libraries, SolcError> {
1938 let paths: ProjectPathsConfig = self.project_paths();
1939 Ok(self.parsed_libraries()?.apply(|libs| paths.apply_lib_remappings(libs)))
1940 }
1941
1942 pub fn solc_settings(&self) -> Result<SolcSettings, SolcError> {
1947 let mut model_checker = self.model_checker.clone();
1951 if let Some(model_checker_settings) = &mut model_checker
1952 && model_checker_settings.targets.is_none()
1953 {
1954 model_checker_settings.targets = Some(vec![ModelCheckerTarget::Assert]);
1955 }
1956
1957 let mut settings = Settings {
1958 libraries: self.libraries_with_remappings()?,
1959 optimizer: self.optimizer(),
1960 evm_version: Some(self.evm_version),
1961 metadata: Some(SettingsMetadata {
1962 use_literal_content: Some(self.use_literal_content),
1963 bytecode_hash: Some(self.bytecode_hash),
1964 cbor_metadata: Some(self.cbor_metadata),
1965 }),
1966 debug: self.revert_strings.map(|revert_strings| DebuggingSettings {
1967 revert_strings: Some(revert_strings),
1968 debug_info: Vec::new(),
1970 }),
1971 model_checker,
1972 via_ir: Some(self.via_ir || self.via_ssa_cfg),
1975 via_ssa_cfg: Some(self.via_ssa_cfg),
1976 experimental: Some(self.experimental),
1977 stop_after: None,
1979 remappings: Vec::new(),
1981 output_selection: Default::default(),
1983 }
1984 .with_extra_output(self.configured_artifacts_handler().output_selection());
1985
1986 if self.ast || self.build_info {
1988 settings = settings.with_ast();
1989 }
1990
1991 let cli_settings =
1992 CliSettings { extra_args: self.extra_args.clone(), ..Default::default() };
1993
1994 Ok(SolcSettings { settings, cli_settings })
1995 }
1996
1997 pub fn vyper_settings(&self) -> Result<VyperSettings, SolcError> {
2000 let optimize = if self.vyper.opt_level.is_some() { None } else { self.vyper.optimize };
2003
2004 Ok(VyperSettings {
2005 evm_version: Some(self.evm_version),
2006 optimize,
2007 opt_level: self.vyper.opt_level,
2008 bytecode_metadata: None,
2009 output_selection: OutputSelection::common_output_selection([
2012 "abi".to_string(),
2013 "evm.bytecode".to_string(),
2014 "evm.deployedBytecode".to_string(),
2015 ]),
2016 search_paths: None,
2017 experimental_codegen: self.vyper.experimental_codegen,
2018 debug: self.vyper.debug,
2019 enable_decimals: self.vyper.enable_decimals,
2020 venom_experimental: self.vyper.venom_experimental,
2021 venom: self.vyper.venom.clone(),
2022 })
2023 }
2024
2025 pub fn figment() -> Figment {
2046 Self::default().into()
2047 }
2048
2049 pub fn figment_with_root(root: impl AsRef<Path>) -> Figment {
2061 Self::with_root(root.as_ref()).into()
2062 }
2063
2064 #[doc(hidden)]
2065 #[track_caller]
2066 pub fn figment_with_root_opt(root: Option<&Path>) -> Figment {
2067 let root = match root {
2068 Some(root) => root,
2069 None => &find_project_root(None).expect("could not determine project root"),
2070 };
2071 Self::figment_with_root(root)
2072 }
2073
2074 pub fn with_root(root: impl AsRef<Path>) -> Self {
2083 Self::_with_root(root.as_ref())
2084 }
2085
2086 fn _with_root(root: &Path) -> Self {
2087 let paths = ProjectPathsConfig::builder()
2089 .remappings(Vec::new())
2093 .build_with_root::<()>(root);
2094 let artifacts: PathBuf = paths.artifacts.file_name().unwrap().into();
2095 let mut config = Self::default();
2096 if config.uses_default_src() {
2097 config.src = paths.sources.file_name().unwrap().into();
2098 }
2099 config.root = paths.root;
2100 config.out = artifacts.clone();
2101 config.libs =
2102 paths.libraries.into_iter().map(|lib| lib.file_name().unwrap().into()).collect();
2103 config.fs_permissions = FsPermissions::new([PathPermission::read(artifacts)]);
2104 config
2105 }
2106
2107 pub fn hardhat() -> Self {
2109 Self {
2110 src: "contracts".into(),
2111 out: "artifacts".into(),
2112 libs: vec!["node_modules".into()],
2113 ..Self::default()
2114 }
2115 }
2116
2117 pub fn into_basic(self) -> BasicConfig {
2126 BasicConfig {
2127 profile: self.profile,
2128 src: self.src,
2129 out: self.out,
2130 libs: self.libs,
2131 remappings: self.remappings,
2132 network: self.networks.resolved_network().map(|network| network.name().to_string()),
2133 }
2134 }
2135
2136 pub fn update_at<F>(root: &Path, f: F) -> eyre::Result<()>
2141 where
2142 F: FnOnce(&Self, &mut toml_edit::DocumentMut) -> bool,
2143 {
2144 let config = Self::load_with_root(root)?.sanitized();
2145 config.update(|doc| f(&config, doc))
2146 }
2147
2148 pub fn update<F>(&self, f: F) -> eyre::Result<()>
2153 where
2154 F: FnOnce(&mut toml_edit::DocumentMut) -> bool,
2155 {
2156 let file_path = self.get_config_path();
2157 if !file_path.exists() {
2158 return Ok(());
2159 }
2160 let contents = fs::read_to_string(&file_path)?;
2161 let mut doc = contents.parse::<toml_edit::DocumentMut>()?;
2162 if f(&mut doc) {
2163 fs::write(file_path, doc.to_string())?;
2164 }
2165 Ok(())
2166 }
2167
2168 pub fn update_libs(&self) -> eyre::Result<()> {
2174 self.update(|doc| {
2175 let profile = self.profile.as_str().as_str();
2176 let root = &self.root;
2177 let libs: toml_edit::Value = self
2178 .libs
2179 .iter()
2180 .map(|path| {
2181 let path =
2182 if let Ok(relative) = path.strip_prefix(root) { relative } else { path };
2183 toml_edit::Value::from(&*path.to_string_lossy())
2184 })
2185 .collect();
2186 let libs = toml_edit::value(libs);
2187 doc[Self::PROFILE_SECTION][profile]["libs"] = libs;
2188 true
2189 })
2190 }
2191
2192 pub fn to_string_pretty(&self) -> Result<String, toml::ser::Error> {
2204 let mut value = toml::Value::try_from(self)?;
2206 let value_table = value.as_table_mut().unwrap();
2208 let standalone_sections = Self::STANDALONE_SECTIONS
2210 .iter()
2211 .filter_map(|section| {
2212 let section = section.to_string();
2213 value_table.remove(§ion).map(|value| (section, value))
2214 })
2215 .collect::<Vec<_>>();
2216 let mut wrapping_table = [(
2218 Self::PROFILE_SECTION.into(),
2219 toml::Value::Table([(self.profile.to_string(), value)].into_iter().collect()),
2220 )]
2221 .into_iter()
2222 .collect::<toml::map::Map<_, _>>();
2223 for (section, value) in standalone_sections {
2225 wrapping_table.insert(section, value);
2226 }
2227 toml::to_string_pretty(&toml::Value::Table(wrapping_table))
2229 }
2230
2231 pub fn get_config_path(&self) -> PathBuf {
2233 self.root.join(Self::FILE_NAME)
2234 }
2235
2236 pub fn selected_profile() -> Profile {
2240 #[cfg(test)]
2242 {
2243 Self::force_selected_profile()
2244 }
2245 #[cfg(not(test))]
2246 {
2247 SELECTED_PROFILE.get_or_init(Self::force_selected_profile).clone()
2248 }
2249 }
2250
2251 pub fn try_set_selected_profile(profile: Profile) -> Result<(), Profile> {
2255 #[cfg(test)]
2256 {
2257 let _ = profile;
2258 Ok(())
2259 }
2260 #[cfg(not(test))]
2261 {
2262 match SELECTED_PROFILE.set(profile) {
2263 Ok(()) => Ok(()),
2264 Err(profile) if SELECTED_PROFILE.get() == Some(&profile) => Ok(()),
2265 Err(_) => Err(SELECTED_PROFILE.get().expect("profile initialized").clone()),
2266 }
2267 }
2268 }
2269
2270 fn force_selected_profile() -> Profile {
2271 Profile::from_env_or("FOUNDRY_PROFILE", Self::DEFAULT_PROFILE)
2272 }
2273
2274 pub fn foundry_dir_toml() -> Option<PathBuf> {
2276 Self::foundry_dir().map(|p| p.join(Self::FILE_NAME))
2277 }
2278
2279 pub fn foundry_dir() -> Option<PathBuf> {
2281 dirs::home_dir().map(|p| p.join(Self::FOUNDRY_DIR_NAME))
2282 }
2283
2284 pub fn foundry_cache_dir() -> Option<PathBuf> {
2286 Self::foundry_dir().map(|p| p.join("cache"))
2287 }
2288
2289 pub fn foundry_rpc_cache_dir() -> Option<PathBuf> {
2291 Some(Self::foundry_cache_dir()?.join("rpc"))
2292 }
2293 pub fn foundry_chain_cache_dir(chain_id: impl Into<Chain>) -> Option<PathBuf> {
2295 Some(Self::foundry_rpc_cache_dir()?.join(chain_id.into().to_string()))
2296 }
2297
2298 pub fn foundry_etherscan_cache_dir() -> Option<PathBuf> {
2300 Some(Self::foundry_cache_dir()?.join("etherscan"))
2301 }
2302
2303 pub fn foundry_keystores_dir() -> Option<PathBuf> {
2305 Some(Self::foundry_dir()?.join("keystores"))
2306 }
2307
2308 pub fn foundry_etherscan_chain_cache_dir(chain_id: impl Into<Chain>) -> Option<PathBuf> {
2311 Some(Self::foundry_etherscan_cache_dir()?.join(chain_id.into().to_string()))
2312 }
2313
2314 pub fn foundry_block_cache_dir(chain_id: impl Into<Chain>, block: u64) -> Option<PathBuf> {
2317 Some(Self::foundry_chain_cache_dir(chain_id)?.join(format!("{block}")))
2318 }
2319
2320 pub fn foundry_block_cache_file(chain_id: impl Into<Chain>, block: u64) -> Option<PathBuf> {
2323 Some(Self::foundry_block_cache_dir(chain_id, block)?.join("storage.json"))
2324 }
2325
2326 pub fn data_dir() -> eyre::Result<PathBuf> {
2334 let path = dirs::data_dir().wrap_err("Failed to find data directory")?.join("foundry");
2335 std::fs::create_dir_all(&path).wrap_err("Failed to create module directory")?;
2336 Ok(path)
2337 }
2338
2339 pub fn find_config_file() -> Option<PathBuf> {
2346 fn find(path: &Path) -> Option<PathBuf> {
2347 if path.is_absolute() {
2348 return match path.is_file() {
2349 true => Some(path.to_path_buf()),
2350 false => None,
2351 };
2352 }
2353 let cwd = std::env::current_dir().ok()?;
2354 let mut cwd = cwd.as_path();
2355 loop {
2356 let file_path = cwd.join(path);
2357 if file_path.is_file() {
2358 return Some(file_path);
2359 }
2360 cwd = cwd.parent()?;
2361 }
2362 }
2363 find(Env::var_or("FOUNDRY_CONFIG", Self::FILE_NAME).as_ref())
2364 .or_else(|| Self::foundry_dir_toml().filter(|p| p.exists()))
2365 }
2366
2367 pub fn clean_foundry_cache() -> eyre::Result<Vec<String>> {
2371 if let Some(cache_dir) = Self::foundry_cache_dir() {
2372 let path = cache_dir.as_path();
2373 if let Err(err) = fs::remove_dir_all(path)
2374 && err.kind() != io::ErrorKind::NotFound
2375 {
2376 return Ok(vec![format!(
2377 "failed to remove foundry cache at {}: {err}",
2378 path.display()
2379 )]);
2380 }
2381 } else {
2382 eyre::bail!("failed to get foundry_cache_dir");
2383 }
2384
2385 Ok(vec![])
2386 }
2387
2388 pub fn clean_foundry_chain_cache(chain: Chain) -> eyre::Result<Vec<String>> {
2392 if let Some(cache_dir) = Self::foundry_chain_cache_dir(chain) {
2393 let path = cache_dir.as_path();
2394 if let Err(err) = fs::remove_dir_all(path)
2395 && err.kind() != io::ErrorKind::NotFound
2396 {
2397 return Ok(vec![format!(
2398 "failed to remove foundry cache for chain {chain} at {}: {err}",
2399 path.display()
2400 )]);
2401 }
2402 } else {
2403 eyre::bail!("failed to get foundry_chain_cache_dir");
2404 }
2405
2406 Ok(vec![])
2407 }
2408
2409 pub fn clean_foundry_block_cache(chain: Chain, block: u64) -> eyre::Result<Vec<String>> {
2413 if let Some(cache_dir) = Self::foundry_block_cache_dir(chain, block) {
2414 let path = cache_dir.as_path();
2415 if let Err(err) = fs::remove_dir_all(path)
2416 && err.kind() != io::ErrorKind::NotFound
2417 {
2418 return Ok(vec![format!(
2419 "failed to remove foundry cache for chain {chain} block {block} at {}: {err}",
2420 path.display()
2421 )]);
2422 }
2423 } else {
2424 eyre::bail!("failed to get foundry_block_cache_dir");
2425 }
2426
2427 Ok(vec![])
2428 }
2429
2430 pub fn clean_foundry_etherscan_cache() -> eyre::Result<Vec<String>> {
2434 if let Some(cache_dir) = Self::foundry_etherscan_cache_dir() {
2435 let path = cache_dir.as_path();
2436 if let Err(err) = fs::remove_dir_all(path)
2437 && err.kind() != io::ErrorKind::NotFound
2438 {
2439 return Ok(vec![format!(
2440 "failed to remove foundry etherscan cache at {}: {err}",
2441 path.display()
2442 )]);
2443 }
2444 } else {
2445 eyre::bail!("failed to get foundry_etherscan_cache_dir");
2446 }
2447
2448 Ok(vec![])
2449 }
2450
2451 pub fn clean_foundry_etherscan_chain_cache(chain: Chain) -> eyre::Result<Vec<String>> {
2455 if let Some(cache_dir) = Self::foundry_etherscan_chain_cache_dir(chain) {
2456 let path = cache_dir.as_path();
2457 if let Err(err) = fs::remove_dir_all(path)
2458 && err.kind() != io::ErrorKind::NotFound
2459 {
2460 return Ok(vec![format!(
2461 "failed to remove foundry etherscan cache for chain {chain} at {}: {err}",
2462 path.display()
2463 )]);
2464 }
2465 } else {
2466 eyre::bail!("failed to get foundry_etherscan_cache_dir for chain: {}", chain);
2467 }
2468
2469 Ok(vec![])
2470 }
2471
2472 pub fn list_foundry_cache() -> eyre::Result<Cache> {
2474 if let Some(cache_dir) = Self::foundry_rpc_cache_dir() {
2475 let mut cache = Cache { chains: vec![] };
2476 let Some(entries) = Self::ignore_not_found(cache_dir.read_dir())? else {
2477 return Ok(cache);
2478 };
2479 for entry in entries {
2480 let Some(entry) = Self::ignore_not_found(entry)? else {
2481 continue;
2482 };
2483 let Some(metadata) = Self::ignore_not_found(fs::metadata(entry.path()))? else {
2484 continue;
2485 };
2486 if !metadata.is_dir() {
2487 continue;
2488 }
2489 if let Ok(chain) = Chain::from_str(&entry.file_name().to_string_lossy()) {
2490 cache.chains.push(Self::list_foundry_chain_cache(chain)?);
2491 }
2492 }
2493 Ok(cache)
2494 } else {
2495 eyre::bail!("failed to get foundry_cache_dir");
2496 }
2497 }
2498
2499 pub fn list_foundry_chain_cache(chain: Chain) -> eyre::Result<ChainCache> {
2501 let block_explorer_data_size = match Self::foundry_etherscan_chain_cache_dir(chain) {
2502 Some(cache_dir) => Self::get_cached_block_explorer_data(&cache_dir)?,
2503 None => {
2504 warn!("failed to access foundry_etherscan_chain_cache_dir");
2505 0
2506 }
2507 };
2508
2509 if let Some(cache_dir) = Self::foundry_chain_cache_dir(chain) {
2510 let blocks = Self::get_cached_blocks(&cache_dir)?;
2511 Ok(ChainCache {
2512 name: chain.to_string(),
2513 blocks,
2514 block_explorer: block_explorer_data_size,
2515 })
2516 } else {
2517 eyre::bail!("failed to get foundry_chain_cache_dir");
2518 }
2519 }
2520
2521 fn get_cached_blocks(chain_path: &Path) -> eyre::Result<Vec<(String, u64)>> {
2523 let mut blocks = vec![];
2524 let Some(entries) = Self::ignore_not_found(chain_path.read_dir())? else {
2525 return Ok(blocks);
2526 };
2527 for block in entries {
2528 let Some(block) = Self::ignore_not_found(block)? else {
2529 continue;
2530 };
2531 if let Some(block) = Self::get_cached_block(block)? {
2532 blocks.push(block);
2533 }
2534 }
2535 Ok(blocks)
2536 }
2537
2538 fn get_cached_block(block: fs::DirEntry) -> eyre::Result<Option<(String, u64)>> {
2539 let file_name = block.file_name();
2540 let Some(metadata) = Self::ignore_not_found(fs::symlink_metadata(block.path()))? else {
2541 return Ok(None);
2542 };
2543 let size = if metadata.is_dir() {
2544 let Some(cache_files) = Self::ignore_not_found(block.path().read_dir())? else {
2545 return Ok(None);
2546 };
2547 let mut size = 0;
2548 for cache_file in cache_files {
2549 let Some(cache_file) = Self::ignore_not_found(cache_file)? else {
2550 continue;
2551 };
2552 size += Self::get_cache_file_size(cache_file)?.unwrap_or_default();
2553 }
2554 if size == 0 {
2555 return Ok(None);
2556 }
2557 size
2558 } else if metadata.is_file() && file_name.to_string_lossy().chars().all(char::is_numeric) {
2559 metadata.len()
2560 } else {
2561 return Ok(None);
2562 };
2563 Ok(Some((file_name.to_string_lossy().into_owned(), size)))
2564 }
2565
2566 fn get_cache_file_size(cache_file: fs::DirEntry) -> eyre::Result<Option<u64>> {
2567 let Some(metadata) = Self::ignore_not_found(fs::symlink_metadata(cache_file.path()))?
2568 else {
2569 return Ok(None);
2570 };
2571 let cache_file_name = cache_file.file_name();
2572 let cache_file_name = cache_file_name.to_string_lossy();
2573 if !metadata.is_file()
2574 || (cache_file_name != "storage.json"
2575 && !cache_file_name
2576 .strip_prefix("storage-")
2577 .and_then(|name| name.strip_suffix(".json"))
2578 .is_some_and(|hash| {
2579 hash.len() == 64 && hash.bytes().all(|b| b.is_ascii_hexdigit())
2580 }))
2581 {
2582 return Ok(None);
2583 }
2584 Ok(Some(metadata.len()))
2585 }
2586
2587 fn get_cached_block_explorer_data(chain_path: &Path) -> eyre::Result<u64> {
2589 let Some(entries) = Self::ignore_not_found(fs::read_dir(chain_path))? else {
2590 return Ok(0);
2591 };
2592 Self::get_cached_dir_size(entries)
2593 }
2594
2595 fn get_cached_dir_size(entries: fs::ReadDir) -> eyre::Result<u64> {
2596 let mut size = 0;
2597 for entry in entries {
2598 let Some(entry) = Self::ignore_not_found(entry)? else {
2599 continue;
2600 };
2601 size += Self::get_cached_entry_size(entry)?.unwrap_or_default();
2602 }
2603 Ok(size)
2604 }
2605
2606 fn get_cached_entry_size(entry: fs::DirEntry) -> eyre::Result<Option<u64>> {
2607 let Some(metadata) = Self::ignore_not_found(fs::symlink_metadata(entry.path()))? else {
2608 return Ok(None);
2609 };
2610 if metadata.is_dir() {
2611 let Some(entries) = Self::ignore_not_found(fs::read_dir(entry.path()))? else {
2612 return Ok(None);
2613 };
2614 Ok(Some(Self::get_cached_dir_size(entries)?))
2615 } else {
2616 Ok(Some(metadata.len()))
2617 }
2618 }
2619
2620 fn ignore_not_found<T>(result: io::Result<T>) -> eyre::Result<Option<T>> {
2622 match result {
2623 Ok(value) => Ok(Some(value)),
2624 Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None),
2625 Err(err) => Err(err.into()),
2626 }
2627 }
2628
2629 fn merge_toml_provider(
2630 mut figment: Figment,
2631 toml_provider: impl Provider,
2632 profile: Profile,
2633 ) -> Figment {
2634 figment = figment.select(profile.clone());
2635
2636 figment = {
2638 let warnings = WarningsProvider::for_figment(&toml_provider, &figment);
2639 figment.merge(warnings)
2640 };
2641
2642 let mut profiles = vec![Self::DEFAULT_PROFILE];
2644 if profile != Self::DEFAULT_PROFILE {
2645 profiles.push(profile.clone());
2646 }
2647 let provider = ForcedSnakeCaseData(toml_provider).strict_select(profiles);
2650 let provider = &BackwardsCompatTomlProvider(provider);
2651
2652 if profile != Self::DEFAULT_PROFILE {
2654 figment = figment.merge(provider.rename(Self::DEFAULT_PROFILE, profile.clone()));
2655 }
2656 for standalone_key in Self::STANDALONE_SECTIONS {
2658 if let Some((_, fallback)) =
2659 STANDALONE_FALLBACK_SECTIONS.iter().find(|(key, _)| standalone_key == key)
2660 {
2661 figment = figment.merge(
2662 provider
2663 .fallback(standalone_key, fallback)
2664 .wrap(profile.clone(), standalone_key),
2665 );
2666 } else {
2667 figment = figment.merge(provider.wrap(profile.clone(), standalone_key));
2668 }
2669 }
2670 figment = figment.merge(provider);
2672 figment
2673 }
2674
2675 fn normalize_defaults(&self, mut figment: Figment) -> Figment {
2681 if figment.contains("evm_version") {
2682 return figment;
2683 }
2684
2685 if let Ok(solc) = figment.extract_inner::<SolcReq>("solc")
2687 && let Some(version) = solc
2688 .try_version()
2689 .ok()
2690 .and_then(|version| self.evm_version.normalize_version_solc(&version))
2691 {
2692 let profile = figment.profile().clone();
2693 figment = figment.merge(Serialized::default("evm_version", version).profile(profile));
2694 }
2695
2696 if figment.extract_inner::<bool>("deny_warnings").unwrap_or(false)
2698 && figment.extract_inner("deny") == Ok(DenyLevel::Never)
2699 {
2700 figment = figment.merge(("deny", DenyLevel::Warnings));
2701 }
2702
2703 figment
2704 }
2705}
2706
2707impl From<Config> for Figment {
2708 fn from(c: Config) -> Self {
2709 (&c).into()
2710 }
2711}
2712impl From<&Config> for Figment {
2713 fn from(c: &Config) -> Self {
2714 c.to_figment(FigmentProviders::All)
2715 }
2716}
2717
2718#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2720pub enum FigmentProviders {
2721 #[default]
2723 All,
2724 Cast,
2728 Anvil,
2732 None,
2734}
2735
2736impl FigmentProviders {
2737 pub const fn is_all(&self) -> bool {
2739 matches!(self, Self::All)
2740 }
2741
2742 pub const fn is_cast(&self) -> bool {
2744 matches!(self, Self::Cast)
2745 }
2746
2747 pub const fn is_anvil(&self) -> bool {
2749 matches!(self, Self::Anvil)
2750 }
2751
2752 pub const fn is_none(&self) -> bool {
2754 matches!(self, Self::None)
2755 }
2756}
2757
2758fn figment_value_is_configured(figment: &Figment, key: &str) -> bool {
2759 figment.find_metadata(key).is_some_and(|metadata| metadata.name.as_ref() != "Foundry Config")
2760}
2761
2762#[derive(Clone, Debug, Serialize, Deserialize)]
2764#[serde(transparent)]
2765pub struct RegexWrapper {
2766 #[serde(with = "serde_regex")]
2767 inner: regex::Regex,
2768}
2769
2770impl std::ops::Deref for RegexWrapper {
2771 type Target = regex::Regex;
2772
2773 fn deref(&self) -> &Self::Target {
2774 &self.inner
2775 }
2776}
2777
2778impl std::cmp::PartialEq for RegexWrapper {
2779 fn eq(&self, other: &Self) -> bool {
2780 self.as_str() == other.as_str()
2781 }
2782}
2783
2784impl Eq for RegexWrapper {}
2785
2786impl From<RegexWrapper> for regex::Regex {
2787 fn from(wrapper: RegexWrapper) -> Self {
2788 wrapper.inner
2789 }
2790}
2791
2792impl From<regex::Regex> for RegexWrapper {
2793 fn from(re: Regex) -> Self {
2794 Self { inner: re }
2795 }
2796}
2797
2798mod serde_regex {
2799 use regex::Regex;
2800 use serde::{Deserialize, Deserializer, Serializer};
2801
2802 pub(crate) fn serialize<S>(value: &Regex, serializer: S) -> Result<S::Ok, S::Error>
2803 where
2804 S: Serializer,
2805 {
2806 serializer.serialize_str(value.as_str())
2807 }
2808
2809 pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result<Regex, D::Error>
2810 where
2811 D: Deserializer<'de>,
2812 {
2813 let s = String::deserialize(deserializer)?;
2814 Regex::new(&s).map_err(serde::de::Error::custom)
2815 }
2816}
2817
2818pub(crate) mod from_opt_glob {
2820 use serde::{Deserialize, Deserializer, Serializer};
2821
2822 pub fn serialize<S>(value: &Option<globset::Glob>, serializer: S) -> Result<S::Ok, S::Error>
2823 where
2824 S: Serializer,
2825 {
2826 match value {
2827 Some(glob) => serializer.serialize_str(glob.glob()),
2828 None => serializer.serialize_none(),
2829 }
2830 }
2831
2832 pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<globset::Glob>, D::Error>
2833 where
2834 D: Deserializer<'de>,
2835 {
2836 let s: Option<String> = Option::deserialize(deserializer)?;
2837 if let Some(s) = s {
2838 return Ok(Some(globset::Glob::new(&s).map_err(serde::de::Error::custom)?));
2839 }
2840 Ok(None)
2841 }
2842}
2843
2844pub fn parse_with_profile<T: serde::de::DeserializeOwned>(
2855 s: &str,
2856) -> Result<Option<(Profile, T)>, Error> {
2857 let figment = Config::merge_toml_provider(
2858 Figment::new(),
2859 Toml::string(s).nested(),
2860 Config::DEFAULT_PROFILE,
2861 );
2862 if figment.profiles().any(|p| p == Config::DEFAULT_PROFILE) {
2863 Ok(Some((Config::DEFAULT_PROFILE, figment.select(Config::DEFAULT_PROFILE).extract()?)))
2864 } else {
2865 Ok(None)
2866 }
2867}
2868
2869impl Provider for Config {
2870 fn metadata(&self) -> Metadata {
2871 Metadata::named("Foundry Config")
2872 }
2873
2874 #[track_caller]
2875 fn data(&self) -> Result<Map<Profile, Dict>, figment::Error> {
2876 let mut data = Serialized::defaults(self).data()?;
2877 let root = Value::serialize(self.root.clone())?;
2878 let labels = Value::serialize(&self.labels)?;
2879 if let Some(entry) = data.get_mut(&Self::DEFAULT_PROFILE) {
2880 entry.insert("root".to_string(), root.clone());
2881 entry.insert("labels".to_string(), labels.clone());
2882 normalize_legacy_labels_in_profile(entry);
2883 }
2884 if let Some(entry) = data.get_mut(&self.profile) {
2885 entry.insert("root".to_string(), root);
2886 entry.insert("labels".to_string(), labels);
2887 normalize_legacy_labels_in_profile(entry);
2888 }
2889 Ok(data)
2890 }
2891
2892 fn profile(&self) -> Option<Profile> {
2893 Some(self.profile.clone())
2894 }
2895}
2896
2897impl Default for Config {
2898 fn default() -> Self {
2899 Self {
2900 profile: Self::DEFAULT_PROFILE,
2901 profiles: vec![Self::DEFAULT_PROFILE],
2902 fs_permissions: FsPermissions::new([PathPermission::read("out")]),
2903 isolate: true,
2904 root: root_default(),
2905 extends: None,
2906 src: Self::DEFAULT_SRC.into(),
2907 test: "test".into(),
2908 script: "script".into(),
2909 out: "out".into(),
2910 libs: vec!["lib".into()],
2911 cache: true,
2912 dynamic_test_linking: true,
2913 cache_path: "cache".into(),
2914 broadcast: "broadcast".into(),
2915 snapshots: "snapshots".into(),
2916 gas_snapshot_check: false,
2917 gas_snapshot_emit: true,
2918 allow_paths: vec![],
2919 include_paths: vec![],
2920 force: false,
2921 evm_version: EvmVersion::Osaka,
2922 hardfork: None,
2923 gas_reports: vec!["*".to_string()],
2924 gas_reports_ignore: vec![],
2925 gas_reports_include_tests: false,
2926 solc: None,
2927 vyper: Default::default(),
2928 auto_detect_solc: true,
2929 offline: false,
2930 optimizer: None,
2931 optimizer_runs: None,
2932 optimizer_details: None,
2933 model_checker: None,
2934 extra_output: Default::default(),
2935 extra_output_files: Default::default(),
2936 names: false,
2937 sizes: false,
2938 test_pattern: None,
2939 test_pattern_inverse: None,
2940 contract_pattern: None,
2941 contract_pattern_inverse: None,
2942 path_pattern: None,
2943 path_pattern_inverse: None,
2944 coverage_pattern_inverse: None,
2945 test_failures_file: "cache/test-failures".into(),
2946 mutation_dir: "cache/mutation".into(),
2947 threads: None,
2948 show_progress: false,
2949 fuzz: FuzzConfig::new("cache/fuzz".into()),
2950 invariant: InvariantConfig::new("cache/invariant".into()),
2951 symbolic: SymbolicConfig::default(),
2952 coverage: CoverageConfig::default(),
2953 mutation: MutationConfig::default(),
2954 tracing: TracingConfig::default(),
2955 always_use_create_2_factory: false,
2956 eip1559_fee_estimate: Eip1559FeeEstimatePreset::default(),
2957 ffi: false,
2958 live_logs: false,
2959 allow_internal_expect_revert: false,
2960 prompt_timeout: 120,
2961 sender: Self::DEFAULT_SENDER,
2962 tx_origin: Self::DEFAULT_SENDER,
2963 initial_balance: U256::from((1u128 << 96) - 1),
2964 block_number: U256::from(1),
2965 fork_block_number: None,
2966 chain: None,
2967 gas_limit: (1u64 << 30).into(), code_size_limit: None,
2969 gas_price: None,
2970 block_base_fee_per_gas: 0,
2971 block_coinbase: Address::ZERO,
2972 block_timestamp: U256::from(1),
2973 block_difficulty: 0,
2974 block_prevrandao: Default::default(),
2975 block_gas_limit: None,
2976 disable_block_gas_limit: false,
2977 enable_tx_gas_limit: false,
2978 memory_limit: 1 << 27, eth_rpc_url: None,
2980 eth_rpc_accept_invalid_certs: false,
2981 eth_rpc_no_proxy: false,
2982 eth_rpc_jwt: None,
2983 eth_rpc_timeout: None,
2984 eth_rpc_headers: None,
2985 eth_rpc_curl: false,
2986 etherscan_api_key: None,
2987 verbosity: 0,
2988 remappings: vec![],
2989 auto_detect_remappings: true,
2990 libraries: vec![],
2991 ignored_error_codes: vec![
2992 SolidityErrorCode::SpdxLicenseNotProvided,
2993 SolidityErrorCode::ContractExceeds24576Bytes,
2994 SolidityErrorCode::ContractInitCodeSizeExceeds49152Bytes,
2995 SolidityErrorCode::TransientStorageUsed,
2996 SolidityErrorCode::TransferDeprecated,
2997 SolidityErrorCode::NatspecMemorySafeAssemblyDeprecated,
2998 ],
2999 ignored_error_codes_from: vec![],
3000 ignored_file_paths: vec![],
3001 deny: DenyLevel::Never,
3002 deny_warnings: false,
3003 via_ir: false,
3004 via_ssa_cfg: false,
3005 experimental: false,
3006 ast: false,
3007 rpc_storage_caching: Default::default(),
3008 rpc_endpoints: Default::default(),
3009 etherscan: Default::default(),
3010 no_storage_caching: false,
3011 no_rpc_rate_limit: false,
3012 use_literal_content: false,
3013 bytecode_hash: BytecodeHash::Ipfs,
3014 cbor_metadata: true,
3015 revert_strings: None,
3016 sparse_mode: false,
3017 build_info: false,
3018 build_info_path: None,
3019 fmt: Default::default(),
3020 lint: Default::default(),
3021 doc: Default::default(),
3022 bind_json: Default::default(),
3023 labels: Default::default(),
3024 unchecked_cheatcode_artifacts: false,
3025 decode_external_storage: false,
3026 create2_library_salt: Self::DEFAULT_CREATE2_LIBRARY_SALT,
3027 create2_deployer: Self::DEFAULT_CREATE2_DEPLOYER,
3028 skip: vec![],
3029 dependencies: Default::default(),
3030 soldeer: Default::default(),
3031 assertions_revert: true,
3032 legacy_assertions: false,
3033 warnings: vec![],
3034 extra_args: vec![],
3035 networks: Default::default(),
3036 transaction_timeout: 120,
3037 additional_compiler_profiles: Default::default(),
3038 compilation_restrictions: Default::default(),
3039 script_execution_protection: true,
3040 _non_exhaustive: (),
3041 }
3042 }
3043}
3044
3045#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Deserialize)]
3051pub struct GasLimit(#[serde(deserialize_with = "crate::deserialize_u64_or_max")] pub u64);
3052
3053impl From<u64> for GasLimit {
3054 fn from(gas: u64) -> Self {
3055 Self(gas)
3056 }
3057}
3058
3059impl From<GasLimit> for u64 {
3060 fn from(gas: GasLimit) -> Self {
3061 gas.0
3062 }
3063}
3064
3065impl Serialize for GasLimit {
3066 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
3067 where
3068 S: Serializer,
3069 {
3070 if self.0 == u64::MAX {
3071 serializer.serialize_str("max")
3072 } else if self.0 > i64::MAX as u64 {
3073 serializer.serialize_str(&self.0.to_string())
3074 } else {
3075 serializer.serialize_u64(self.0)
3076 }
3077 }
3078}
3079
3080#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
3082#[serde(untagged)]
3083pub enum SolcReq {
3084 Version(Version),
3087 Local(PathBuf),
3089}
3090
3091fn resolve_solc_path(solc: &Path) -> Result<PathBuf, SolcError> {
3092 if solc.is_file() {
3093 Ok(solc.to_path_buf())
3094 } else {
3095 which::which(solc).map_err(|_| SolcError::msg(format!("`solc` {solc:?} does not exist")))
3096 }
3097}
3098
3099impl SolcReq {
3100 pub fn try_version(&self) -> Result<Version, SolcError> {
3105 match self {
3106 Self::Version(version) => Ok(version.clone()),
3107 Self::Local(path) => Solc::new(resolve_solc_path(path)?).map(|solc| solc.version),
3108 }
3109 }
3110}
3111
3112impl<T: AsRef<str>> From<T> for SolcReq {
3113 fn from(s: T) -> Self {
3114 let s = s.as_ref();
3115 if let Ok(v) = Version::from_str(s) { Self::Version(v) } else { Self::Local(s.into()) }
3116 }
3117}
3118
3119#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
3131pub struct BasicConfig {
3132 #[serde(skip)]
3134 pub profile: Profile,
3135 pub src: PathBuf,
3137 pub out: PathBuf,
3139 pub libs: Vec<PathBuf>,
3141 #[serde(
3143 default,
3144 skip_serializing_if = "Vec::is_empty",
3145 serialize_with = "remappings_serde::serialize"
3146 )]
3147 pub remappings: Vec<RelativeRemapping>,
3148 #[serde(skip)]
3150 pub network: Option<String>,
3151}
3152
3153impl BasicConfig {
3154 pub fn to_string_pretty(&self) -> Result<String, toml::ser::Error> {
3158 let mut profile_body = toml::Value::try_from(self)?;
3159 if let Some(ref network) = self.network
3160 && let toml::Value::Table(ref mut table) = profile_body
3161 {
3162 table.insert("network".to_string(), toml::Value::String(network.clone()));
3163 }
3164
3165 let mut profile_section = toml::value::Table::new();
3166 profile_section.insert(self.profile.to_string(), profile_body);
3167
3168 let mut document = toml::value::Table::new();
3169 document.insert("profile".to_string(), toml::Value::Table(profile_section));
3170
3171 if self.network.as_deref() == Some("tempo") {
3172 let mut endpoints = toml::value::Table::new();
3173 endpoints.insert(
3174 "tempo".to_string(),
3175 toml::Value::String(crate::endpoints::TEMPO_RPC_URL.to_string()),
3176 );
3177 endpoints.insert(
3178 "moderato".to_string(),
3179 toml::Value::String(crate::endpoints::MODERATO_RPC_URL.to_string()),
3180 );
3181 document.insert("rpc_endpoints".to_string(), toml::Value::Table(endpoints));
3182 }
3183
3184 let body = toml::to_string_pretty(&toml::Value::Table(document))?;
3185 Ok(format!(
3186 "{body}\n# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options\n"
3187 ))
3188 }
3189}
3190
3191mod remappings_serde {
3192 use foundry_compilers::artifacts::remappings::RelativeRemapping;
3193 use serde::{Serialize, Serializer};
3194
3195 #[cfg(windows)]
3196 use path_slash::PathExt as _;
3197 #[cfg(windows)]
3198 use std::path::Path;
3199
3200 #[cfg(windows)]
3201 fn slash_context(context: &str) -> String {
3202 let has_trailing_separator =
3203 context.as_bytes().last().is_some_and(|c| *c == b'/' || *c == b'\\');
3204 let mut context = Path::new(context).to_slash_lossy().into_owned();
3205 if has_trailing_separator && !context.ends_with('/') {
3206 context.push('/');
3207 }
3208 context
3209 }
3210
3211 pub fn serialize<S>(remappings: &[RelativeRemapping], serializer: S) -> Result<S::Ok, S::Error>
3212 where
3213 S: Serializer,
3214 {
3215 remappings
3216 .iter()
3217 .map(|remapping| {
3218 let Some(context) = &remapping.context else { return remapping.to_string() };
3219 let mut remapping = remapping.clone();
3220 remapping.context = None;
3221 #[cfg(windows)]
3222 let context = slash_context(context);
3223 #[cfg(not(windows))]
3224 let context = context.as_str();
3225 format!("{context}:{remapping}")
3226 })
3227 .collect::<Vec<_>>()
3228 .serialize(serializer)
3229 }
3230}
3231
3232pub(crate) mod from_str_lowercase {
3233 use serde::{Deserialize, Deserializer, Serializer};
3234 use std::str::FromStr;
3235
3236 pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
3237 where
3238 T: std::fmt::Display,
3239 S: Serializer,
3240 {
3241 serializer.collect_str(&value.to_string().to_lowercase())
3242 }
3243
3244 pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
3245 where
3246 D: Deserializer<'de>,
3247 T: FromStr,
3248 T::Err: std::fmt::Display,
3249 {
3250 String::deserialize(deserializer)?.to_lowercase().parse().map_err(serde::de::Error::custom)
3251 }
3252}
3253
3254fn canonic(path: impl Into<PathBuf>) -> PathBuf {
3255 let path = path.into();
3256 foundry_compilers::utils::canonicalize(&path).unwrap_or(path)
3257}
3258
3259fn root_default() -> PathBuf {
3260 ".".into()
3261}
3262
3263#[cfg(test)]
3264mod tests {
3265 use super::*;
3266 use crate::{
3267 cache::{CachedChains, CachedEndpoints},
3268 endpoints::RpcEndpointType,
3269 etherscan::ResolvedEtherscanConfigs,
3270 fmt::IndentStyle,
3271 };
3272 use NamedChain::Moonbeam;
3273 use endpoints::{RpcAuth, RpcEndpointConfig};
3274 use figment::error::Kind::InvalidType;
3275 use foundry_compilers::artifacts::{
3276 ModelCheckerEngine, YulDetails,
3277 vyper::{VyperOptimizationLevel, VyperOptimizationMode, VyperVenomSettings},
3278 };
3279 use foundry_evm_hardforks::{TempoHardfork, latest_active_tempo_hardfork};
3280 use similar_asserts::assert_eq;
3281 use soldeer_core::remappings::RemappingsLocation;
3282 use std::{
3283 fs::File,
3284 io::{self, Write},
3285 num::NonZeroUsize,
3286 };
3287 use tempfile::tempdir;
3288
3289 #[cfg(feature = "base")]
3290 use foundry_evm_hardforks::BaseUpgrade;
3291
3292 fn clear_warning(config: &mut Config) {
3295 config.warnings = vec![];
3296 }
3297
3298 fn mark_serialized_invariant_provenance(config: &mut Config) {
3299 config.invariant.corpus_random_sequence_weight_configured = true;
3300 config.invariant.workers_configured = true;
3301 }
3302
3303 #[test]
3304 fn project_remappings_alias_relative_filesystem_contexts_in_place() {
3305 let root = tempdir().unwrap();
3306 let dependency = root.path().join("dependency");
3307 let absolute_dependency = root.path().join("absolute-dependency");
3308 fs::create_dir(&dependency).unwrap();
3309 fs::create_dir(&absolute_dependency).unwrap();
3310
3311 let global_before = Remapping {
3312 context: None,
3313 name: "global-before/".into(),
3314 path: "lib/global-before/".into(),
3315 };
3316 let relative = Remapping {
3317 context: Some(format!("dependency{}", std::path::MAIN_SEPARATOR)),
3318 name: "relative/".into(),
3319 path: "lib/relative/".into(),
3320 };
3321 let absolute = Remapping {
3322 context: Some(format!(
3323 "{}{}",
3324 absolute_dependency.display(),
3325 std::path::MAIN_SEPARATOR
3326 )),
3327 name: "absolute/".into(),
3328 path: "lib/absolute/".into(),
3329 };
3330 let missing = Remapping {
3331 context: Some(format!("missing{}", std::path::MAIN_SEPARATOR)),
3332 name: "missing/".into(),
3333 path: "lib/missing/".into(),
3334 };
3335 let global_after = Remapping {
3336 context: None,
3337 name: "global-after/".into(),
3338 path: "lib/global-after/".into(),
3339 };
3340 let mut config = Config::with_root(root.path());
3341 config.remappings = [
3342 global_before.clone(),
3343 relative.clone(),
3344 absolute.clone(),
3345 missing.clone(),
3346 global_after.clone(),
3347 ]
3348 .map(Into::into)
3349 .into();
3350
3351 let mut absolute_alias = relative.clone();
3352 let absolute_context = config.root.join("dependency");
3353 #[cfg(windows)]
3354 let absolute_context = PathBuf::from_slash(absolute_context.to_string_lossy());
3355 let mut absolute_context = absolute_context.display().to_string();
3356 absolute_context.push(std::path::MAIN_SEPARATOR);
3357 absolute_alias.context = Some(absolute_context);
3358 assert_eq!(
3359 config.project_remappings(),
3360 vec![global_before, relative, absolute_alias, absolute, missing, global_after]
3361 );
3362 }
3363
3364 #[test]
3365 fn default_sender() {
3366 assert_eq!(Config::DEFAULT_SENDER, address!("0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38"));
3367 }
3368
3369 #[test]
3370 fn test_caching() {
3371 let mut config = Config::default();
3372 let chain_id = NamedChain::Mainnet;
3373 let url = "https://eth-mainnet.alchemyapi";
3374 assert!(config.enable_caching(url, chain_id));
3375
3376 config.no_storage_caching = true;
3377 assert!(!config.enable_caching(url, chain_id));
3378
3379 config.no_storage_caching = false;
3380 assert!(!config.enable_caching(url, NamedChain::Dev));
3381 }
3382
3383 #[test]
3384 fn test_install_dir() {
3385 figment::Jail::expect_with(|jail| {
3386 let config = Config::load().unwrap();
3387 assert_eq!(config.install_lib_dir(), PathBuf::from("lib"));
3388 jail.create_file(
3389 "foundry.toml",
3390 r"
3391 [profile.default]
3392 libs = ['node_modules', 'lib']
3393 ",
3394 )?;
3395 let config = Config::load().unwrap();
3396 assert_eq!(config.install_lib_dir(), PathBuf::from("lib"));
3397
3398 jail.create_file(
3399 "foundry.toml",
3400 r"
3401 [profile.default]
3402 libs = ['custom', 'node_modules', 'lib']
3403 ",
3404 )?;
3405 let config = Config::load().unwrap();
3406 assert_eq!(config.install_lib_dir(), PathBuf::from("custom"));
3407
3408 Ok(())
3409 });
3410 }
3411
3412 #[test]
3413 fn test_figment_is_default() {
3414 figment::Jail::expect_with(|_| {
3415 let mut default: Config = Config::figment().extract()?;
3416 let default2 = Config::default();
3417 default.profile = default2.profile.clone();
3418 default.profiles = default2.profiles.clone();
3419 assert_eq!(default, default2);
3420 Ok(())
3421 });
3422 }
3423
3424 #[test]
3425 fn figment_profiles() {
3426 figment::Jail::expect_with(|jail| {
3427 jail.create_file(
3428 "foundry.toml",
3429 r"
3430 [foo.baz]
3431 libs = ['node_modules', 'lib']
3432
3433 [profile.default]
3434 libs = ['node_modules', 'lib']
3435
3436 [profile.ci]
3437 libs = ['node_modules', 'lib']
3438
3439 [profile.local]
3440 libs = ['node_modules', 'lib']
3441 ",
3442 )?;
3443
3444 let config = crate::Config::load().unwrap();
3445 let expected: &[figment::Profile] = &["ci".into(), "default".into(), "local".into()];
3446 assert_eq!(config.profiles, expected);
3447
3448 Ok(())
3449 });
3450 }
3451
3452 #[test]
3453 fn test_default_round_trip() {
3454 figment::Jail::expect_with(|_| {
3455 let original = Config::figment();
3456 let roundtrip = Figment::from(Config::from_provider(&original).unwrap());
3457 for figment in &[original, roundtrip] {
3458 let config = Config::from_provider(figment).unwrap();
3459 assert_eq!(config, Config::default().normalized_optimizer_settings());
3460 }
3461 Ok(())
3462 });
3463 }
3464
3465 #[test]
3466 fn ffi_env_disallowed() {
3467 figment::Jail::expect_with(|jail| {
3468 jail.set_env("FOUNDRY_FFI", "true");
3469 jail.set_env("FFI", "true");
3470 jail.set_env("DAPP_FFI", "true");
3471 let config = Config::load().unwrap();
3472 assert!(!config.ffi);
3473
3474 Ok(())
3475 });
3476 }
3477
3478 #[test]
3479 fn test_profile_env() {
3480 figment::Jail::expect_with(|jail| {
3481 jail.set_env("FOUNDRY_PROFILE", "default");
3482 let figment = Config::figment();
3483 assert_eq!(figment.profile(), "default");
3484
3485 jail.set_env("FOUNDRY_PROFILE", "hardhat");
3486 let figment: Figment = Config::hardhat().into();
3487 assert_eq!(figment.profile(), "hardhat");
3488
3489 jail.create_file(
3490 "foundry.toml",
3491 r"
3492 [profile.default]
3493 libs = ['lib']
3494 [profile.local]
3495 libs = ['modules']
3496 ",
3497 )?;
3498 jail.set_env("FOUNDRY_PROFILE", "local");
3499 let config = Config::load().unwrap();
3500 assert_eq!(config.libs, vec![PathBuf::from("modules")]);
3501
3502 Ok(())
3503 });
3504 }
3505
3506 #[test]
3507 fn test_default_test_path() {
3508 figment::Jail::expect_with(|_| {
3509 let config = Config::default();
3510 let paths_config = config.project_paths::<Solc>();
3511 assert_eq!(paths_config.tests, PathBuf::from(r"test"));
3512 Ok(())
3513 });
3514 }
3515
3516 #[test]
3517 fn test_custom_src_skips_directory_auto_detection() {
3518 figment::Jail::expect_with(|jail| {
3519 fs::create_dir(jail.directory().join("contracts")).unwrap();
3520
3521 let config = Config { root: jail.directory().into(), ..Default::default() };
3522 let figment: Figment = config.into();
3523 let config = figment.extract::<Config>().unwrap();
3524 assert_eq!(config.src, PathBuf::from("contracts"));
3525
3526 let config = Config {
3527 root: jail.directory().into(),
3528 src: "custom-src".into(),
3529 ..Default::default()
3530 };
3531 let figment: Figment = config.into();
3532 let config = figment.extract::<Config>().unwrap();
3533 assert_eq!(config.src, PathBuf::from("custom-src"));
3534
3535 Ok(())
3536 });
3537 }
3538
3539 #[test]
3540 fn test_default_libs() {
3541 figment::Jail::expect_with(|jail| {
3542 let config = Config::load().unwrap();
3543 assert_eq!(config.libs, vec![PathBuf::from("lib")]);
3544
3545 fs::create_dir_all(jail.directory().join("node_modules")).unwrap();
3546 let config = Config::load().unwrap();
3547 assert_eq!(config.libs, vec![PathBuf::from("node_modules")]);
3548
3549 fs::create_dir_all(jail.directory().join("lib")).unwrap();
3550 let config = Config::load().unwrap();
3551 assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
3552
3553 Ok(())
3554 });
3555 }
3556
3557 #[test]
3558 fn test_inheritance_from_default_test_path() {
3559 figment::Jail::expect_with(|jail| {
3560 jail.create_file(
3561 "foundry.toml",
3562 r#"
3563 [profile.default]
3564 test = "defaulttest"
3565 src = "defaultsrc"
3566 libs = ['lib', 'node_modules']
3567
3568 [profile.custom]
3569 src = "customsrc"
3570 "#,
3571 )?;
3572
3573 let config = Config::load().unwrap();
3574 assert_eq!(config.src, PathBuf::from("defaultsrc"));
3575 assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
3576
3577 jail.set_env("FOUNDRY_PROFILE", "custom");
3578 let config = Config::load().unwrap();
3579 assert_eq!(config.src, PathBuf::from("customsrc"));
3580 assert_eq!(config.test, PathBuf::from("defaulttest"));
3581 assert_eq!(config.libs, vec![PathBuf::from("lib"), PathBuf::from("node_modules")]);
3582
3583 Ok(())
3584 });
3585 }
3586
3587 #[test]
3588 fn test_custom_test_path() {
3589 figment::Jail::expect_with(|jail| {
3590 jail.create_file(
3591 "foundry.toml",
3592 r#"
3593 [profile.default]
3594 test = "mytest"
3595 "#,
3596 )?;
3597
3598 let config = Config::load().unwrap();
3599 let paths_config = config.project_paths::<Solc>();
3600 assert_eq!(paths_config.tests, PathBuf::from(r"mytest"));
3601 Ok(())
3602 });
3603 }
3604
3605 #[test]
3606 fn test_remappings() {
3607 figment::Jail::expect_with(|jail| {
3608 jail.create_file(
3609 "foundry.toml",
3610 r#"
3611 [profile.default]
3612 src = "some-source"
3613 out = "some-out"
3614 cache = true
3615 "#,
3616 )?;
3617 let config = Config::load().unwrap();
3618 assert!(config.remappings.is_empty());
3619
3620 jail.create_file(
3621 "remappings.txt",
3622 r"
3623 file-ds-test/=lib/ds-test/
3624 file-other/=lib/other/
3625 ",
3626 )?;
3627
3628 let config = Config::load().unwrap();
3629 assert_eq!(
3630 config.remappings,
3631 vec![
3632 Remapping::from_str("file-ds-test/=lib/ds-test/").unwrap().into(),
3633 Remapping::from_str("file-other/=lib/other/").unwrap().into(),
3634 ],
3635 );
3636
3637 jail.set_env("DAPP_REMAPPINGS", "ds-test=lib/ds-test/\nother/=lib/other/");
3638 let config = Config::load().unwrap();
3639
3640 assert_eq!(
3641 config.remappings,
3642 vec![
3643 Remapping::from_str("ds-test=lib/ds-test/").unwrap().into(),
3645 Remapping::from_str("other/=lib/other/").unwrap().into(),
3646 Remapping::from_str("file-ds-test/=lib/ds-test/").unwrap().into(),
3648 Remapping::from_str("file-other/=lib/other/").unwrap().into(),
3649 ],
3650 );
3651
3652 Ok(())
3653 });
3654 }
3655
3656 #[test]
3657 fn test_remappings_override() {
3658 figment::Jail::expect_with(|jail| {
3659 jail.create_file(
3660 "foundry.toml",
3661 r#"
3662 [profile.default]
3663 src = "some-source"
3664 out = "some-out"
3665 cache = true
3666 "#,
3667 )?;
3668 let config = Config::load().unwrap();
3669 assert!(config.remappings.is_empty());
3670
3671 jail.create_file(
3672 "remappings.txt",
3673 r"
3674 ds-test/=lib/ds-test/
3675 other/=lib/other/
3676 ",
3677 )?;
3678
3679 let config = Config::load().unwrap();
3680 assert_eq!(
3681 config.remappings,
3682 vec![
3683 Remapping::from_str("ds-test/=lib/ds-test/").unwrap().into(),
3684 Remapping::from_str("other/=lib/other/").unwrap().into(),
3685 ],
3686 );
3687
3688 jail.set_env("DAPP_REMAPPINGS", "ds-test/=lib/ds-test/src/\nenv-lib/=lib/env-lib/");
3689 let config = Config::load().unwrap();
3690
3691 assert_eq!(
3696 config.remappings,
3697 vec![
3698 Remapping::from_str("ds-test/=lib/ds-test/src/").unwrap().into(),
3699 Remapping::from_str("env-lib/=lib/env-lib/").unwrap().into(),
3700 Remapping::from_str("other/=lib/other/").unwrap().into(),
3701 ],
3702 );
3703
3704 assert_eq!(
3706 config.get_all_remappings().collect::<Vec<_>>(),
3707 vec![
3708 Remapping::from_str("ds-test/=lib/ds-test/src/").unwrap(),
3709 Remapping::from_str("env-lib/=lib/env-lib/").unwrap(),
3710 Remapping::from_str("other/=lib/other/").unwrap(),
3711 ],
3712 );
3713
3714 Ok(())
3715 });
3716 }
3717
3718 #[test]
3719 fn test_can_update_libs() {
3720 figment::Jail::expect_with(|jail| {
3721 jail.create_file(
3722 "foundry.toml",
3723 r#"
3724 [profile.default]
3725 libs = ["node_modules"]
3726 "#,
3727 )?;
3728
3729 let mut config = Config::load().unwrap();
3730 config.libs.push("libs".into());
3731 config.update_libs().unwrap();
3732
3733 let config = Config::load().unwrap();
3734 assert_eq!(config.libs, vec![PathBuf::from("node_modules"), PathBuf::from("libs"),]);
3735 Ok(())
3736 });
3737 }
3738
3739 #[test]
3740 fn test_large_gas_limit() {
3741 figment::Jail::expect_with(|jail| {
3742 let gas = u64::MAX;
3743 jail.create_file(
3744 "foundry.toml",
3745 &format!(
3746 r#"
3747 [profile.default]
3748 gas_limit = "{gas}"
3749 "#
3750 ),
3751 )?;
3752
3753 let config = Config::load().unwrap();
3754 assert_eq!(
3755 config,
3756 Config {
3757 gas_limit: gas.into(),
3758 ..Config::default().normalized_optimizer_settings()
3759 }
3760 );
3761
3762 Ok(())
3763 });
3764 }
3765
3766 #[test]
3767 #[should_panic]
3768 fn test_toml_file_parse_failure() {
3769 figment::Jail::expect_with(|jail| {
3770 jail.create_file(
3771 "foundry.toml",
3772 r#"
3773 [profile.default]
3774 eth_rpc_url = "https://example.com/
3775 "#,
3776 )?;
3777
3778 let _config = Config::load().unwrap();
3779
3780 Ok(())
3781 });
3782 }
3783
3784 #[test]
3785 #[should_panic]
3786 fn test_toml_file_non_existing_config_var_failure() {
3787 figment::Jail::expect_with(|jail| {
3788 jail.set_env("FOUNDRY_CONFIG", "this config does not exist");
3789
3790 let _config = Config::load().unwrap();
3791
3792 Ok(())
3793 });
3794 }
3795
3796 #[test]
3797 fn test_resolve_etherscan_with_chain() {
3798 figment::Jail::expect_with(|jail| {
3799 let env_key = "__BSC_ETHERSCAN_API_KEY";
3800 let env_value = "env value";
3801 jail.create_file(
3802 "foundry.toml",
3803 r#"
3804 [profile.default]
3805
3806 [etherscan]
3807 bsc = { key = "${__BSC_ETHERSCAN_API_KEY}", url = "https://api.bscscan.com/api" }
3808 "#,
3809 )?;
3810
3811 let config = Config::load().unwrap();
3812 assert!(
3813 config
3814 .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
3815 .is_err()
3816 );
3817
3818 unsafe {
3819 std::env::set_var(env_key, env_value);
3820 }
3821
3822 assert_eq!(
3823 config
3824 .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
3825 .unwrap()
3826 .unwrap()
3827 .key,
3828 env_value
3829 );
3830
3831 let mut with_key = config;
3832 with_key.etherscan_api_key = Some("via etherscan_api_key".to_string());
3833
3834 assert_eq!(
3835 with_key
3836 .get_etherscan_config_with_chain(Some(NamedChain::BinanceSmartChain.into()))
3837 .unwrap()
3838 .unwrap()
3839 .key,
3840 "via etherscan_api_key"
3841 );
3842
3843 unsafe {
3844 std::env::remove_var(env_key);
3845 }
3846 Ok(())
3847 });
3848 }
3849
3850 #[test]
3851 fn test_resolve_etherscan() {
3852 figment::Jail::expect_with(|jail| {
3853 jail.create_file(
3854 "foundry.toml",
3855 r#"
3856 [profile.default]
3857
3858 [etherscan]
3859 mainnet = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
3860 moonbeam = { key = "${_CONFIG_ETHERSCAN_MOONBEAM}" }
3861 "#,
3862 )?;
3863
3864 let config = Config::load().unwrap();
3865
3866 assert!(config.etherscan.clone().resolved().has_unresolved());
3867
3868 jail.set_env("_CONFIG_ETHERSCAN_MOONBEAM", "123456789");
3869
3870 let configs = config.etherscan.resolved();
3871 assert!(!configs.has_unresolved());
3872
3873 let mb_urls = Moonbeam.etherscan_urls().unwrap();
3874 let mainnet_urls = NamedChain::Mainnet.etherscan_urls().unwrap();
3875 assert_eq!(
3876 configs,
3877 ResolvedEtherscanConfigs::new([
3878 (
3879 "mainnet",
3880 ResolvedEtherscanConfig {
3881 api_url: mainnet_urls.0.to_string(),
3882 chain: Some(NamedChain::Mainnet.into()),
3883 browser_url: Some(mainnet_urls.1.to_string()),
3884 key: "FX42Z3BBJJEWXWGYV2X1CIPRSCN".to_string(),
3885 }
3886 ),
3887 (
3888 "moonbeam",
3889 ResolvedEtherscanConfig {
3890 api_url: mb_urls.0.to_string(),
3891 chain: Some(Moonbeam.into()),
3892 browser_url: Some(mb_urls.1.to_string()),
3893 key: "123456789".to_string(),
3894 }
3895 ),
3896 ])
3897 );
3898
3899 Ok(())
3900 });
3901 }
3902
3903 #[test]
3904 fn test_resolve_etherscan_with_versions() {
3905 figment::Jail::expect_with(|jail| {
3906 jail.create_file(
3907 "foundry.toml",
3908 r#"
3909 [profile.default]
3910
3911 [etherscan]
3912 mainnet = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN", api_version = "v2" }
3913 moonbeam = { key = "${_CONFIG_ETHERSCAN_MOONBEAM}", api_version = "v1" }
3914 "#,
3915 )?;
3916
3917 let config = Config::load().unwrap();
3918
3919 assert!(config.etherscan.clone().resolved().has_unresolved());
3920
3921 jail.set_env("_CONFIG_ETHERSCAN_MOONBEAM", "123456789");
3922
3923 let configs = config.etherscan.resolved();
3924 assert!(!configs.has_unresolved());
3925
3926 let mb_urls = Moonbeam.etherscan_urls().unwrap();
3927 let mainnet_urls = NamedChain::Mainnet.etherscan_urls().unwrap();
3928 assert_eq!(
3929 configs,
3930 ResolvedEtherscanConfigs::new([
3931 (
3932 "mainnet",
3933 ResolvedEtherscanConfig {
3934 api_url: mainnet_urls.0.to_string(),
3935 chain: Some(NamedChain::Mainnet.into()),
3936 browser_url: Some(mainnet_urls.1.to_string()),
3937 key: "FX42Z3BBJJEWXWGYV2X1CIPRSCN".to_string(),
3938 }
3939 ),
3940 (
3941 "moonbeam",
3942 ResolvedEtherscanConfig {
3943 api_url: mb_urls.0.to_string(),
3944 chain: Some(Moonbeam.into()),
3945 browser_url: Some(mb_urls.1.to_string()),
3946 key: "123456789".to_string(),
3947 }
3948 ),
3949 ])
3950 );
3951
3952 Ok(())
3953 });
3954 }
3955
3956 #[test]
3957 fn test_resolve_etherscan_chain_id() {
3958 figment::Jail::expect_with(|jail| {
3959 jail.create_file(
3960 "foundry.toml",
3961 r#"
3962 [profile.default]
3963 chain_id = "sepolia"
3964
3965 [etherscan]
3966 sepolia = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
3967 "#,
3968 )?;
3969
3970 let config = Config::load().unwrap();
3971 let etherscan = config.get_etherscan_config().unwrap().unwrap();
3972 assert_eq!(etherscan.chain, Some(NamedChain::Sepolia.into()));
3973 assert_eq!(etherscan.key, "FX42Z3BBJJEWXWGYV2X1CIPRSCN");
3974
3975 Ok(())
3976 });
3977 }
3978
3979 #[test]
3981 fn test_resolve_etherscan_with_invalid_name() {
3982 figment::Jail::expect_with(|jail| {
3983 jail.create_file(
3984 "foundry.toml",
3985 r#"
3986 [etherscan]
3987 mainnet = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
3988 an_invalid_name = { key = "FX42Z3BBJJEWXWGYV2X1CIPRSCN" }
3989 "#,
3990 )?;
3991
3992 let config = Config::load().unwrap();
3993 let etherscan_config = config.get_etherscan_config();
3994 assert!(etherscan_config.is_none());
3995
3996 Ok(())
3997 });
3998 }
3999
4000 #[test]
4001 fn test_resolve_rpc_url() {
4002 figment::Jail::expect_with(|jail| {
4003 jail.create_file(
4004 "foundry.toml",
4005 r#"
4006 [profile.default]
4007 [rpc_endpoints]
4008 optimism = "https://example.com/"
4009 mainnet = "${_CONFIG_MAINNET}"
4010 "#,
4011 )?;
4012 jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
4013
4014 let mut config = Config::load().unwrap();
4015 assert_eq!("http://localhost:8545", config.get_rpc_url_or_localhost_http().unwrap());
4016
4017 config.eth_rpc_url = Some("mainnet".to_string());
4018 assert_eq!(
4019 "https://eth-mainnet.alchemyapi.io/v2/123455",
4020 config.get_rpc_url_or_localhost_http().unwrap()
4021 );
4022
4023 config.eth_rpc_url = Some("optimism".to_string());
4024 assert_eq!("https://example.com/", config.get_rpc_url_or_localhost_http().unwrap());
4025
4026 Ok(())
4027 })
4028 }
4029
4030 #[test]
4031 fn test_resolve_rpc_url_if_etherscan_set() {
4032 figment::Jail::expect_with(|jail| {
4033 jail.create_file(
4034 "foundry.toml",
4035 r#"
4036 [profile.default]
4037 etherscan_api_key = "dummy"
4038 [rpc_endpoints]
4039 optimism = "https://example.com/"
4040 "#,
4041 )?;
4042
4043 let config = Config::load().unwrap();
4044 assert_eq!("http://localhost:8545", config.get_rpc_url_or_localhost_http().unwrap());
4045
4046 Ok(())
4047 })
4048 }
4049
4050 #[test]
4051 fn test_resolve_rpc_url_alias() {
4052 figment::Jail::expect_with(|jail| {
4053 jail.create_file(
4054 "foundry.toml",
4055 r#"
4056 [profile.default]
4057 [rpc_endpoints]
4058 polygonAmoy = "https://polygon-amoy.g.alchemy.com/v2/${_RESOLVE_RPC_ALIAS}"
4059 "#,
4060 )?;
4061 let mut config = Config::load().unwrap();
4062 config.eth_rpc_url = Some("polygonAmoy".to_string());
4063 assert!(config.get_rpc_url().unwrap().is_err());
4064
4065 jail.set_env("_RESOLVE_RPC_ALIAS", "123455");
4066
4067 let mut config = Config::load().unwrap();
4068 config.eth_rpc_url = Some("polygonAmoy".to_string());
4069 assert_eq!(
4070 "https://polygon-amoy.g.alchemy.com/v2/123455",
4071 config.get_rpc_url().unwrap().unwrap()
4072 );
4073
4074 Ok(())
4075 })
4076 }
4077
4078 #[test]
4079 fn test_resolve_rpc_aliases() {
4080 figment::Jail::expect_with(|jail| {
4081 jail.create_file(
4082 "foundry.toml",
4083 r#"
4084 [profile.default]
4085 [etherscan]
4086 arbitrum_alias = { key = "${TEST_RESOLVE_RPC_ALIAS_ARBISCAN}" }
4087 [rpc_endpoints]
4088 arbitrum_alias = "https://arb-mainnet.g.alchemy.com/v2/${TEST_RESOLVE_RPC_ALIAS_ARB_ONE}"
4089 "#,
4090 )?;
4091
4092 jail.set_env("TEST_RESOLVE_RPC_ALIAS_ARB_ONE", "123455");
4093 jail.set_env("TEST_RESOLVE_RPC_ALIAS_ARBISCAN", "123455");
4094
4095 let config = Config::load().unwrap();
4096
4097 let config = config.get_etherscan_config_with_chain(Some(NamedChain::Arbitrum.into()));
4098 assert!(config.is_err());
4099 assert_eq!(
4100 config.unwrap_err().to_string(),
4101 "At least one of `url` or `chain` must be present for Etherscan config with unknown alias `arbitrum_alias`"
4102 );
4103
4104 Ok(())
4105 });
4106 }
4107
4108 #[test]
4109 fn test_resolve_rpc_config() {
4110 figment::Jail::expect_with(|jail| {
4111 jail.create_file(
4112 "foundry.toml",
4113 r#"
4114 [rpc_endpoints]
4115 optimism = "https://example.com/"
4116 mainnet = { endpoint = "${_CONFIG_MAINNET}", retries = 3, retry_backoff = 1000, compute_units_per_second = 1000 }
4117 "#,
4118 )?;
4119 jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
4120
4121 let config = Config::load().unwrap();
4122 assert_eq!(
4123 RpcEndpoints::new([
4124 (
4125 "optimism",
4126 RpcEndpointType::String(RpcEndpointUrl::Url(
4127 "https://example.com/".to_string()
4128 ))
4129 ),
4130 (
4131 "mainnet",
4132 RpcEndpointType::Config(RpcEndpoint {
4133 endpoint: RpcEndpointUrl::Env("${_CONFIG_MAINNET}".to_string()),
4134 extra_endpoints: vec![],
4135 config: RpcEndpointConfig {
4136 retries: Some(3),
4137 retry_backoff: Some(1000),
4138 compute_units_per_second: Some(1000),
4139 },
4140 auth: None,
4141 })
4142 ),
4143 ]),
4144 config.rpc_endpoints
4145 );
4146
4147 let resolved = config.rpc_endpoints.resolved();
4148 assert_eq!(
4149 RpcEndpoints::new([
4150 (
4151 "optimism",
4152 RpcEndpointType::String(RpcEndpointUrl::Url(
4153 "https://example.com/".to_string()
4154 ))
4155 ),
4156 (
4157 "mainnet",
4158 RpcEndpointType::Config(RpcEndpoint {
4159 endpoint: RpcEndpointUrl::Env("${_CONFIG_MAINNET}".to_string()),
4160 extra_endpoints: vec![],
4161 config: RpcEndpointConfig {
4162 retries: Some(3),
4163 retry_backoff: Some(1000),
4164 compute_units_per_second: Some(1000),
4165 },
4166 auth: None,
4167 })
4168 ),
4169 ])
4170 .resolved(),
4171 resolved
4172 );
4173 Ok(())
4174 })
4175 }
4176
4177 #[test]
4178 fn test_resolve_auth() {
4179 figment::Jail::expect_with(|jail| {
4180 jail.create_file(
4181 "foundry.toml",
4182 r#"
4183 [profile.default]
4184 eth_rpc_url = "optimism"
4185 [rpc_endpoints]
4186 optimism = "https://example.com/"
4187 mainnet = { endpoint = "${_CONFIG_MAINNET}", retries = 3, retry_backoff = 1000, compute_units_per_second = 1000, auth = "Bearer ${_CONFIG_AUTH}" }
4188 "#,
4189 )?;
4190
4191 let config = Config::load().unwrap();
4192
4193 jail.set_env("_CONFIG_AUTH", "123456");
4194 jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
4195
4196 assert_eq!(
4197 RpcEndpoints::new([
4198 (
4199 "optimism",
4200 RpcEndpointType::String(RpcEndpointUrl::Url(
4201 "https://example.com/".to_string()
4202 ))
4203 ),
4204 (
4205 "mainnet",
4206 RpcEndpointType::Config(RpcEndpoint {
4207 endpoint: RpcEndpointUrl::Env("${_CONFIG_MAINNET}".to_string()),
4208 extra_endpoints: vec![],
4209 config: RpcEndpointConfig {
4210 retries: Some(3),
4211 retry_backoff: Some(1000),
4212 compute_units_per_second: Some(1000)
4213 },
4214 auth: Some(RpcAuth::Env("Bearer ${_CONFIG_AUTH}".to_string())),
4215 })
4216 ),
4217 ]),
4218 config.rpc_endpoints
4219 );
4220 let resolved = config.rpc_endpoints.resolved();
4221 assert_eq!(
4222 RpcEndpoints::new([
4223 (
4224 "optimism",
4225 RpcEndpointType::String(RpcEndpointUrl::Url(
4226 "https://example.com/".to_string()
4227 ))
4228 ),
4229 (
4230 "mainnet",
4231 RpcEndpointType::Config(RpcEndpoint {
4232 endpoint: RpcEndpointUrl::Url(
4233 "https://eth-mainnet.alchemyapi.io/v2/123455".to_string()
4234 ),
4235 extra_endpoints: vec![],
4236 config: RpcEndpointConfig {
4237 retries: Some(3),
4238 retry_backoff: Some(1000),
4239 compute_units_per_second: Some(1000)
4240 },
4241 auth: Some(RpcAuth::Raw("Bearer 123456".to_string())),
4242 })
4243 ),
4244 ])
4245 .resolved(),
4246 resolved
4247 );
4248
4249 Ok(())
4250 });
4251 }
4252
4253 #[test]
4254 fn test_resolve_endpoints() {
4255 figment::Jail::expect_with(|jail| {
4256 jail.create_file(
4257 "foundry.toml",
4258 r#"
4259 [profile.default]
4260 eth_rpc_url = "optimism"
4261 [rpc_endpoints]
4262 optimism = "https://example.com/"
4263 mainnet = "${_CONFIG_MAINNET}"
4264 mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${_CONFIG_API_KEY1}"
4265 mainnet_3 = "https://eth-mainnet.alchemyapi.io/v2/${_CONFIG_API_KEY1}/${_CONFIG_API_KEY2}"
4266 "#,
4267 )?;
4268
4269 let config = Config::load().unwrap();
4270
4271 assert_eq!(config.get_rpc_url().unwrap().unwrap(), "https://example.com/");
4272
4273 assert!(config.rpc_endpoints.clone().resolved().has_unresolved());
4274
4275 jail.set_env("_CONFIG_MAINNET", "https://eth-mainnet.alchemyapi.io/v2/123455");
4276 jail.set_env("_CONFIG_API_KEY1", "123456");
4277 jail.set_env("_CONFIG_API_KEY2", "98765");
4278
4279 let endpoints = config.rpc_endpoints.resolved();
4280
4281 assert!(!endpoints.has_unresolved());
4282
4283 assert_eq!(
4284 endpoints,
4285 RpcEndpoints::new([
4286 ("optimism", RpcEndpointUrl::Url("https://example.com/".to_string())),
4287 (
4288 "mainnet",
4289 RpcEndpointUrl::Url(
4290 "https://eth-mainnet.alchemyapi.io/v2/123455".to_string()
4291 )
4292 ),
4293 (
4294 "mainnet_2",
4295 RpcEndpointUrl::Url(
4296 "https://eth-mainnet.alchemyapi.io/v2/123456".to_string()
4297 )
4298 ),
4299 (
4300 "mainnet_3",
4301 RpcEndpointUrl::Url(
4302 "https://eth-mainnet.alchemyapi.io/v2/123456/98765".to_string()
4303 )
4304 ),
4305 ])
4306 .resolved()
4307 );
4308
4309 Ok(())
4310 });
4311 }
4312
4313 #[test]
4314 fn test_extract_etherscan_config() {
4315 figment::Jail::expect_with(|jail| {
4316 jail.create_file(
4317 "foundry.toml",
4318 r#"
4319 [profile.default]
4320 etherscan_api_key = "optimism"
4321
4322 [etherscan]
4323 optimism = { key = "https://etherscan-optimism.com/" }
4324 amoy = { key = "https://etherscan-amoy.com/" }
4325 "#,
4326 )?;
4327
4328 let mut config = Config::load().unwrap();
4329
4330 let optimism = config.get_etherscan_api_key(Some(NamedChain::Optimism.into()));
4331 assert_eq!(optimism, Some("https://etherscan-optimism.com/".to_string()));
4332
4333 config.etherscan_api_key = Some("amoy".to_string());
4334
4335 let amoy = config.get_etherscan_api_key(Some(NamedChain::PolygonAmoy.into()));
4336 assert_eq!(amoy, Some("https://etherscan-amoy.com/".to_string()));
4337
4338 Ok(())
4339 });
4340 }
4341
4342 #[test]
4343 fn test_extract_etherscan_config_by_chain() {
4344 figment::Jail::expect_with(|jail| {
4345 jail.create_file(
4346 "foundry.toml",
4347 r#"
4348 [profile.default]
4349
4350 [etherscan]
4351 amoy = { key = "https://etherscan-amoy.com/", chain = 80002 }
4352 "#,
4353 )?;
4354
4355 let config = Config::load().unwrap();
4356
4357 let amoy = config
4358 .get_etherscan_config_with_chain(Some(NamedChain::PolygonAmoy.into()))
4359 .unwrap()
4360 .unwrap();
4361 assert_eq!(amoy.key, "https://etherscan-amoy.com/".to_string());
4362
4363 Ok(())
4364 });
4365 }
4366
4367 #[test]
4368 fn test_extract_etherscan_config_by_chain_with_url() {
4369 figment::Jail::expect_with(|jail| {
4370 jail.create_file(
4371 "foundry.toml",
4372 r#"
4373 [profile.default]
4374
4375 [etherscan]
4376 amoy = { key = "https://etherscan-amoy.com/", chain = 80002 , url = "https://verifier-url.com/"}
4377 "#,
4378 )?;
4379
4380 let config = Config::load().unwrap();
4381
4382 let amoy = config
4383 .get_etherscan_config_with_chain(Some(NamedChain::PolygonAmoy.into()))
4384 .unwrap()
4385 .unwrap();
4386 assert_eq!(amoy.key, "https://etherscan-amoy.com/".to_string());
4387 assert_eq!(amoy.api_url, "https://verifier-url.com/".to_string());
4388
4389 Ok(())
4390 });
4391 }
4392
4393 #[test]
4394 fn test_extract_etherscan_config_by_chain_and_alias() {
4395 figment::Jail::expect_with(|jail| {
4396 jail.create_file(
4397 "foundry.toml",
4398 r#"
4399 [profile.default]
4400 eth_rpc_url = "amoy"
4401
4402 [etherscan]
4403 amoy = { key = "https://etherscan-amoy.com/" }
4404
4405 [rpc_endpoints]
4406 amoy = "https://polygon-amoy.g.alchemy.com/v2/amoy"
4407 "#,
4408 )?;
4409
4410 let config = Config::load().unwrap();
4411
4412 let amoy = config.get_etherscan_config_with_chain(None).unwrap().unwrap();
4413 assert_eq!(amoy.key, "https://etherscan-amoy.com/".to_string());
4414
4415 let amoy_rpc = config.get_rpc_url().unwrap().unwrap();
4416 assert_eq!(amoy_rpc, "https://polygon-amoy.g.alchemy.com/v2/amoy");
4417 Ok(())
4418 });
4419 }
4420
4421 #[test]
4422 fn test_toml_file() {
4423 figment::Jail::expect_with(|jail| {
4424 jail.create_file(
4425 "foundry.toml",
4426 r#"
4427 [profile.default]
4428 src = "some-source"
4429 out = "some-out"
4430 cache = true
4431 eth_rpc_url = "https://example.com/"
4432 verbosity = 3
4433 remappings = ["ds-test=lib/ds-test/"]
4434 via_ir = true
4435 rpc_storage_caching = { chains = [1, "optimism", 999999], endpoints = "all"}
4436 use_literal_content = false
4437 bytecode_hash = "ipfs"
4438 cbor_metadata = true
4439 revert_strings = "strip"
4440 allow_paths = ["allow", "paths"]
4441 build_info_path = "build-info"
4442 always_use_create_2_factory = true
4443
4444 [rpc_endpoints]
4445 optimism = "https://example.com/"
4446 mainnet = "${RPC_MAINNET}"
4447 mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}"
4448 mainnet_3 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}/${ANOTHER_KEY}"
4449 "#,
4450 )?;
4451
4452 let config = Config::load().unwrap();
4453 assert_eq!(
4454 config,
4455 Config {
4456 src: "some-source".into(),
4457 out: "some-out".into(),
4458 cache: true,
4459 eth_rpc_url: Some("https://example.com/".to_string()),
4460 remappings: vec![Remapping::from_str("ds-test=lib/ds-test/").unwrap().into()],
4461 verbosity: 3,
4462 via_ir: true,
4463 rpc_storage_caching: StorageCachingConfig {
4464 chains: CachedChains::Chains(vec![
4465 Chain::mainnet(),
4466 Chain::optimism_mainnet(),
4467 Chain::from_id(999999)
4468 ]),
4469 endpoints: CachedEndpoints::All,
4470 },
4471 use_literal_content: false,
4472 bytecode_hash: BytecodeHash::Ipfs,
4473 cbor_metadata: true,
4474 revert_strings: Some(RevertStrings::Strip),
4475 allow_paths: vec![PathBuf::from("allow"), PathBuf::from("paths")],
4476 rpc_endpoints: RpcEndpoints::new([
4477 ("optimism", RpcEndpointUrl::Url("https://example.com/".to_string())),
4478 ("mainnet", RpcEndpointUrl::Env("${RPC_MAINNET}".to_string())),
4479 (
4480 "mainnet_2",
4481 RpcEndpointUrl::Env(
4482 "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}".to_string()
4483 )
4484 ),
4485 (
4486 "mainnet_3",
4487 RpcEndpointUrl::Env(
4488 "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}/${ANOTHER_KEY}"
4489 .to_string()
4490 )
4491 ),
4492 ]),
4493 build_info_path: Some("build-info".into()),
4494 always_use_create_2_factory: true,
4495 ..Config::default().normalized_optimizer_settings()
4496 }
4497 );
4498
4499 Ok(())
4500 });
4501 }
4502
4503 #[test]
4504 fn test_load_remappings() {
4505 figment::Jail::expect_with(|jail| {
4506 jail.create_file(
4507 "foundry.toml",
4508 r"
4509 [profile.default]
4510 remappings = ['nested/=lib/nested/']
4511 ",
4512 )?;
4513
4514 let config = Config::load_with_root(jail.directory()).unwrap();
4515 assert_eq!(
4516 config.remappings,
4517 vec![Remapping::from_str("nested/=lib/nested/").unwrap().into()]
4518 );
4519
4520 Ok(())
4521 });
4522 }
4523
4524 #[test]
4525 fn test_load_full_toml() {
4526 figment::Jail::expect_with(|jail| {
4527 jail.create_file(
4528 "foundry.toml",
4529 r#"
4530 [profile.default]
4531 auto_detect_solc = true
4532 block_base_fee_per_gas = 0
4533 block_coinbase = '0x0000000000000000000000000000000000000000'
4534 block_difficulty = 0
4535 block_prevrandao = '0x0000000000000000000000000000000000000000000000000000000000000000'
4536 block_number = 1
4537 block_timestamp = 1
4538 use_literal_content = false
4539 bytecode_hash = 'ipfs'
4540 cbor_metadata = true
4541 cache = true
4542 cache_path = 'cache'
4543 evm_version = 'london'
4544 extra_output = []
4545 extra_output_files = []
4546 always_use_create_2_factory = false
4547 ffi = false
4548 force = false
4549 gas_limit = 9223372036854775807
4550 gas_price = 0
4551 gas_reports = ['*']
4552 ignored_error_codes = [1878]
4553 ignored_warnings_from = ["something"]
4554 deny = "never"
4555 initial_balance = '0xffffffffffffffffffffffff'
4556 libraries = []
4557 libs = ['lib']
4558 memory_limit = 134217728
4559 names = false
4560 no_storage_caching = false
4561 no_rpc_rate_limit = false
4562 offline = false
4563 optimizer = true
4564 optimizer_runs = 200
4565 out = 'out'
4566 remappings = ['nested/=lib/nested/']
4567 sender = '0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38'
4568 sizes = false
4569 sparse_mode = false
4570 src = 'src'
4571 test = 'test'
4572 tx_origin = '0x1804c8AB1F12E6bbf3894d4083f33e07309d1f38'
4573 verbosity = 0
4574 via_ir = false
4575
4576 [profile.default.rpc_storage_caching]
4577 chains = 'all'
4578 endpoints = 'all'
4579
4580 [rpc_endpoints]
4581 optimism = "https://example.com/"
4582 mainnet = "${RPC_MAINNET}"
4583 mainnet_2 = "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}"
4584
4585 [fuzz]
4586 runs = 256
4587 seed = '0x3e8'
4588 max_test_rejects = 65536
4589
4590 [invariant]
4591 runs = 256
4592 depth = 500
4593 workers = 1
4594 fail_on_revert = false
4595 call_override = false
4596 shrink_run_limit = 5000
4597 "#,
4598 )?;
4599
4600 let config = Config::load_with_root(jail.directory()).unwrap();
4601
4602 assert_eq!(config.ignored_file_paths, vec![PathBuf::from("something")]);
4603 assert_eq!(config.fuzz.seed, Some(U256::from(1000)));
4604 assert_eq!(
4605 config.remappings,
4606 vec![Remapping::from_str("nested/=lib/nested/").unwrap().into()]
4607 );
4608
4609 assert_eq!(
4610 config.rpc_endpoints,
4611 RpcEndpoints::new([
4612 ("optimism", RpcEndpointUrl::Url("https://example.com/".to_string())),
4613 ("mainnet", RpcEndpointUrl::Env("${RPC_MAINNET}".to_string())),
4614 (
4615 "mainnet_2",
4616 RpcEndpointUrl::Env(
4617 "https://eth-mainnet.alchemyapi.io/v2/${API_KEY}".to_string()
4618 )
4619 ),
4620 ]),
4621 );
4622
4623 Ok(())
4624 });
4625 }
4626
4627 #[test]
4628 fn test_solc_req() {
4629 figment::Jail::expect_with(|jail| {
4630 jail.create_file(
4631 "foundry.toml",
4632 r#"
4633 [profile.default]
4634 solc_version = "0.8.12"
4635 "#,
4636 )?;
4637
4638 let config = Config::load().unwrap();
4639 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));
4640
4641 jail.create_file(
4642 "foundry.toml",
4643 r#"
4644 [profile.default]
4645 solc = "0.8.12"
4646 "#,
4647 )?;
4648
4649 let config = Config::load().unwrap();
4650 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));
4651
4652 jail.create_file(
4653 "foundry.toml",
4654 r#"
4655 [profile.default]
4656 solc = "path/to/local/solc"
4657 "#,
4658 )?;
4659
4660 let config = Config::load().unwrap();
4661 assert_eq!(config.solc, Some(SolcReq::Local("path/to/local/solc".into())));
4662
4663 jail.set_env("FOUNDRY_SOLC_VERSION", "0.6.6");
4664 let config = Config::load().unwrap();
4665 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 6, 6))));
4666 Ok(())
4667 });
4668 }
4669
4670 #[test]
4672 fn test_backwards_solc_version() {
4673 figment::Jail::expect_with(|jail| {
4674 jail.create_file(
4675 "foundry.toml",
4676 r#"
4677 [default]
4678 solc = "0.8.12"
4679 solc_version = "0.8.20"
4680 "#,
4681 )?;
4682
4683 let config = Config::load().unwrap();
4684 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 12))));
4685
4686 Ok(())
4687 });
4688
4689 figment::Jail::expect_with(|jail| {
4690 jail.create_file(
4691 "foundry.toml",
4692 r#"
4693 [default]
4694 solc_version = "0.8.20"
4695 "#,
4696 )?;
4697
4698 let config = Config::load().unwrap();
4699 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 20))));
4700
4701 Ok(())
4702 });
4703 }
4704
4705 #[test]
4706 fn test_toml_casing_file() {
4707 figment::Jail::expect_with(|jail| {
4708 jail.create_file(
4709 "foundry.toml",
4710 r#"
4711 [profile.default]
4712 src = "some-source"
4713 out = "some-out"
4714 cache = true
4715 eth-rpc-url = "https://example.com/"
4716 evm-version = "berlin"
4717 auto-detect-solc = false
4718 "#,
4719 )?;
4720
4721 let config = Config::load().unwrap();
4722 assert_eq!(
4723 config,
4724 Config {
4725 src: "some-source".into(),
4726 out: "some-out".into(),
4727 cache: true,
4728 eth_rpc_url: Some("https://example.com/".to_string()),
4729 auto_detect_solc: false,
4730 evm_version: EvmVersion::Berlin,
4731 ..Config::default().normalized_optimizer_settings()
4732 }
4733 );
4734
4735 Ok(())
4736 });
4737 }
4738
4739 #[test]
4740 fn test_output_selection() {
4741 figment::Jail::expect_with(|jail| {
4742 jail.create_file(
4743 "foundry.toml",
4744 r#"
4745 [profile.default]
4746 extra_output = ["metadata", "ir-optimized"]
4747 extra_output_files = ["metadata"]
4748 "#,
4749 )?;
4750
4751 let config = Config::load().unwrap();
4752
4753 assert_eq!(
4754 config.extra_output,
4755 vec![ContractOutputSelection::Metadata, ContractOutputSelection::IrOptimized]
4756 );
4757 assert_eq!(config.extra_output_files, vec![ContractOutputSelection::Metadata]);
4758
4759 Ok(())
4760 });
4761 }
4762
4763 #[test]
4764 fn test_precedence() {
4765 figment::Jail::expect_with(|jail| {
4766 jail.create_file(
4767 "foundry.toml",
4768 r#"
4769 [profile.default]
4770 src = "mysrc"
4771 out = "myout"
4772 verbosity = 3
4773 "#,
4774 )?;
4775
4776 let config = Config::load().unwrap();
4777 assert_eq!(
4778 config,
4779 Config {
4780 src: "mysrc".into(),
4781 out: "myout".into(),
4782 verbosity: 3,
4783 ..Config::default().normalized_optimizer_settings()
4784 }
4785 );
4786
4787 jail.set_env("FOUNDRY_SRC", r"other-src");
4788 let config = Config::load().unwrap();
4789 assert_eq!(
4790 config,
4791 Config {
4792 src: "other-src".into(),
4793 out: "myout".into(),
4794 verbosity: 3,
4795 ..Config::default().normalized_optimizer_settings()
4796 }
4797 );
4798
4799 jail.set_env("FOUNDRY_PROFILE", "foo");
4800 let val: Result<String, _> = Config::figment().extract_inner("profile");
4801 assert!(val.is_err());
4802
4803 Ok(())
4804 });
4805 }
4806
4807 #[test]
4808 fn test_extract_basic() {
4809 figment::Jail::expect_with(|jail| {
4810 jail.create_file(
4811 "foundry.toml",
4812 r#"
4813 [profile.default]
4814 src = "mysrc"
4815 out = "myout"
4816 verbosity = 3
4817 evm_version = 'berlin'
4818
4819 [profile.other]
4820 src = "other-src"
4821 "#,
4822 )?;
4823 let loaded = Config::load().unwrap();
4824 assert_eq!(loaded.evm_version, EvmVersion::Berlin);
4825 let base = loaded.into_basic();
4826 let default = Config::default();
4827 assert_eq!(
4828 base,
4829 BasicConfig {
4830 profile: Config::DEFAULT_PROFILE,
4831 src: "mysrc".into(),
4832 out: "myout".into(),
4833 libs: default.libs.clone(),
4834 remappings: default.remappings.clone(),
4835 network: None,
4836 }
4837 );
4838 jail.set_env("FOUNDRY_PROFILE", r"other");
4839 let base = Config::figment().extract::<BasicConfig>().unwrap();
4840 assert_eq!(
4841 base,
4842 BasicConfig {
4843 profile: Config::DEFAULT_PROFILE,
4844 src: "other-src".into(),
4845 out: "myout".into(),
4846 libs: default.libs.clone(),
4847 remappings: default.remappings,
4848 network: None,
4849 }
4850 );
4851 Ok(())
4852 });
4853 }
4854
4855 #[test]
4856 #[should_panic]
4857 fn test_parse_invalid_fuzz_weight() {
4858 figment::Jail::expect_with(|jail| {
4859 jail.create_file(
4860 "foundry.toml",
4861 r"
4862 [fuzz]
4863 dictionary_weight = 101
4864 ",
4865 )?;
4866 let _config = Config::load().unwrap();
4867 Ok(())
4868 });
4869 }
4870
4871 #[test]
4872 fn test_fallback_provider() {
4873 figment::Jail::expect_with(|jail| {
4874 jail.create_file(
4875 "foundry.toml",
4876 r"
4877 [fuzz]
4878 runs = 1
4879 include_storage = false
4880 dictionary_weight = 99
4881
4882 [invariant]
4883 runs = 420
4884
4885 [profile.ci.fuzz]
4886 dictionary_weight = 5
4887
4888 [profile.ci.invariant]
4889 runs = 400
4890 ",
4891 )?;
4892
4893 let invariant_default = InvariantConfig::default();
4894 let config = Config::load().unwrap();
4895
4896 assert_ne!(config.invariant.runs, config.fuzz.runs);
4897 assert_eq!(config.invariant.runs, 420);
4898
4899 assert_ne!(
4900 config.fuzz.dictionary.include_storage,
4901 invariant_default.dictionary.include_storage
4902 );
4903 assert_eq!(
4904 config.invariant.dictionary.include_storage,
4905 config.fuzz.dictionary.include_storage
4906 );
4907
4908 assert_ne!(
4909 config.fuzz.dictionary.dictionary_weight,
4910 invariant_default.dictionary.dictionary_weight
4911 );
4912 assert_eq!(
4913 config.invariant.dictionary.dictionary_weight,
4914 config.fuzz.dictionary.dictionary_weight
4915 );
4916
4917 jail.set_env("FOUNDRY_PROFILE", "ci");
4918 let ci_config = Config::load().unwrap();
4919 assert_eq!(ci_config.fuzz.runs, 1);
4920 assert_eq!(ci_config.invariant.runs, 400);
4921 assert_eq!(ci_config.fuzz.dictionary.dictionary_weight, 5);
4922 assert_eq!(
4923 ci_config.invariant.dictionary.dictionary_weight,
4924 config.fuzz.dictionary.dictionary_weight
4925 );
4926
4927 Ok(())
4928 })
4929 }
4930
4931 #[test]
4932 fn test_standalone_profile_sections() {
4933 figment::Jail::expect_with(|jail| {
4934 jail.create_file(
4935 "foundry.toml",
4936 r#"
4937 [fuzz]
4938 runs = 100
4939
4940 [invariant]
4941 runs = 120
4942
4943 [symbolic]
4944 enabled = true
4945 max_paths = 12
4946 storage_layout = "generic"
4947
4948 [profile.ci.fuzz]
4949 runs = 420
4950
4951 [profile.ci.invariant]
4952 runs = 500
4953
4954 [profile.ci.symbolic]
4955 max_paths = 34
4956 dump_smt = true
4957 "#,
4958 )?;
4959
4960 let config = Config::load().unwrap();
4961 assert_eq!(config.fuzz.runs, 100);
4962 assert_eq!(config.invariant.runs, 120);
4963 assert!(config.symbolic.enabled);
4964 assert_eq!(config.symbolic.max_paths, 12);
4965 assert_eq!(config.symbolic.storage_layout, SymbolicStorageLayout::Generic);
4966 assert!(!config.symbolic.dump_smt);
4967
4968 jail.set_env("FOUNDRY_PROFILE", "ci");
4969 let config = Config::load().unwrap();
4970 assert_eq!(config.fuzz.runs, 420);
4971 assert_eq!(config.invariant.runs, 500);
4972 assert!(config.symbolic.enabled);
4973 assert_eq!(config.symbolic.max_paths, 34);
4974 assert_eq!(config.symbolic.storage_layout, SymbolicStorageLayout::Generic);
4975 assert!(config.symbolic.dump_smt);
4976
4977 Ok(())
4978 });
4979 }
4980
4981 #[test]
4982 fn can_handle_deviating_dapp_aliases() {
4983 figment::Jail::expect_with(|jail| {
4984 let addr = Address::ZERO;
4985 jail.set_env("DAPP_TEST_NUMBER", 1337);
4986 jail.set_env("DAPP_TEST_ADDRESS", format!("{addr:?}"));
4987 jail.set_env("DAPP_TEST_FUZZ_RUNS", 420);
4988 jail.set_env("DAPP_TEST_DEPTH", 20);
4989 jail.set_env("DAPP_FORK_BLOCK", 100);
4990 jail.set_env("DAPP_BUILD_OPTIMIZE_RUNS", 999);
4991 jail.set_env("DAPP_BUILD_OPTIMIZE", 0);
4992
4993 let config = Config::load().unwrap();
4994
4995 assert_eq!(config.block_number, U256::from(1337));
4996 assert_eq!(config.sender, addr);
4997 assert_eq!(config.fuzz.runs, 420);
4998 assert_eq!(config.invariant.depth, 20);
4999 assert_eq!(config.fork_block_number, Some(100));
5000 assert_eq!(config.optimizer_runs, Some(999));
5001 assert!(!config.optimizer.unwrap());
5002
5003 Ok(())
5004 });
5005 }
5006
5007 #[test]
5008 fn can_parse_libraries() {
5009 figment::Jail::expect_with(|jail| {
5010 jail.set_env(
5011 "DAPP_LIBRARIES",
5012 "[src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6]",
5013 );
5014 let config = Config::load().unwrap();
5015 assert_eq!(
5016 config.libraries,
5017 vec![
5018 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
5019 .to_string()
5020 ]
5021 );
5022
5023 jail.set_env(
5024 "DAPP_LIBRARIES",
5025 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6",
5026 );
5027 let config = Config::load().unwrap();
5028 assert_eq!(
5029 config.libraries,
5030 vec![
5031 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
5032 .to_string(),
5033 ]
5034 );
5035
5036 jail.set_env(
5037 "DAPP_LIBRARIES",
5038 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6,src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6",
5039 );
5040 let config = Config::load().unwrap();
5041 assert_eq!(
5042 config.libraries,
5043 vec![
5044 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
5045 .to_string(),
5046 "src/DssSpell.sol:DssExecLib:0x8De6DDbCd5053d32292AAA0D2105A32d108484a6"
5047 .to_string()
5048 ]
5049 );
5050
5051 Ok(())
5052 });
5053 }
5054
5055 #[test]
5056 fn test_parse_many_libraries() {
5057 figment::Jail::expect_with(|jail| {
5058 jail.create_file(
5059 "foundry.toml",
5060 r"
5061 [profile.default]
5062 libraries= [
5063 './src/SizeAuctionDiscount.sol:Chainlink:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
5064 './src/SizeAuction.sol:ChainlinkTWAP:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
5065 './src/SizeAuction.sol:Math:0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c',
5066 './src/test/ChainlinkTWAP.t.sol:ChainlinkTWAP:0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5',
5067 './src/SizeAuctionDiscount.sol:Math:0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c',
5068 ]
5069 ",
5070 )?;
5071 let config = Config::load().unwrap();
5072
5073 let libs = config.parsed_libraries().unwrap().libs;
5074
5075 similar_asserts::assert_eq!(
5076 libs,
5077 BTreeMap::from([
5078 (
5079 PathBuf::from("./src/SizeAuctionDiscount.sol"),
5080 BTreeMap::from([
5081 (
5082 "Chainlink".to_string(),
5083 "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
5084 ),
5085 (
5086 "Math".to_string(),
5087 "0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c".to_string()
5088 )
5089 ])
5090 ),
5091 (
5092 PathBuf::from("./src/SizeAuction.sol"),
5093 BTreeMap::from([
5094 (
5095 "ChainlinkTWAP".to_string(),
5096 "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
5097 ),
5098 (
5099 "Math".to_string(),
5100 "0x902f6cf364b8d9470d5793a9b2b2e86bddd21e0c".to_string()
5101 )
5102 ])
5103 ),
5104 (
5105 PathBuf::from("./src/test/ChainlinkTWAP.t.sol"),
5106 BTreeMap::from([(
5107 "ChainlinkTWAP".to_string(),
5108 "0xffedba5e171c4f15abaaabc86e8bd01f9b54dae5".to_string()
5109 )])
5110 ),
5111 ])
5112 );
5113
5114 Ok(())
5115 });
5116 }
5117
5118 #[test]
5119 fn config_roundtrip() {
5120 figment::Jail::expect_with(|jail| {
5121 let default = Config::default().normalized_optimizer_settings();
5122 let basic = default.clone().into_basic();
5123 jail.create_file("foundry.toml", &basic.to_string_pretty().unwrap())?;
5124
5125 let mut other = Config::load().unwrap();
5126 clear_warning(&mut other);
5127 assert_eq!(default, other);
5128
5129 let other = other.into_basic();
5130 assert_eq!(basic, other);
5131
5132 jail.create_file("foundry.toml", &default.to_string_pretty().unwrap())?;
5133 let mut other = Config::load().unwrap();
5134 clear_warning(&mut other);
5135 let mut serialized_default = default;
5136 mark_serialized_invariant_provenance(&mut serialized_default);
5137 assert_eq!(serialized_default, other);
5138
5139 Ok(())
5140 });
5141 }
5142
5143 #[test]
5144 fn config_serialization_preserves_context_directory_boundary() {
5145 let remapping = "lib/outer/:inner/=lib/outer/lib/inner/";
5146 let config = Config {
5147 remappings: vec![Remapping::from_str(remapping).unwrap().into()],
5148 ..Default::default()
5149 };
5150
5151 let serialized = toml::Value::try_from(&config).unwrap();
5152 assert_eq!(serialized["remappings"][0].as_str(), Some(remapping));
5153
5154 let serialized = toml::Value::try_from(config.into_basic()).unwrap();
5155 assert_eq!(serialized["remappings"][0].as_str(), Some(remapping));
5156 }
5157
5158 #[cfg(windows)]
5159 #[test]
5160 fn config_serialization_preserves_verbatim_unc_context() {
5161 let remapping = r"\\?\UNC\server\share\project\:inner/=lib/inner/";
5162 let expected = r"\\?\UNC\server\share/project/:inner/=lib/inner/";
5163 let config = Config {
5164 remappings: vec![Remapping::from_str(remapping).unwrap().into()],
5165 ..Default::default()
5166 };
5167
5168 let serialized = toml::Value::try_from(&config).unwrap();
5169 assert_eq!(serialized["remappings"][0].as_str(), Some(expected));
5170
5171 let serialized = toml::Value::try_from(config.into_basic()).unwrap();
5172 assert_eq!(serialized["remappings"][0].as_str(), Some(expected));
5173 }
5174
5175 #[test]
5176 fn test_fs_permissions() {
5177 figment::Jail::expect_with(|jail| {
5178 jail.create_file(
5179 "foundry.toml",
5180 r#"
5181 [profile.default]
5182 fs_permissions = [{ access = "read-write", path = "./"}]
5183 "#,
5184 )?;
5185 let loaded = Config::load().unwrap();
5186
5187 assert_eq!(
5188 loaded.fs_permissions,
5189 FsPermissions::new(vec![PathPermission::read_write("./")])
5190 );
5191
5192 jail.create_file(
5193 "foundry.toml",
5194 r#"
5195 [profile.default]
5196 fs_permissions = [{ access = "none", path = "./"}]
5197 "#,
5198 )?;
5199 let loaded = Config::load().unwrap();
5200 assert_eq!(loaded.fs_permissions, FsPermissions::new(vec![PathPermission::none("./")]));
5201
5202 Ok(())
5203 });
5204 }
5205
5206 #[test]
5207 fn test_optimizer_settings_basic() {
5208 figment::Jail::expect_with(|jail| {
5209 jail.create_file(
5210 "foundry.toml",
5211 r"
5212 [profile.default]
5213 optimizer = true
5214
5215 [profile.default.optimizer_details]
5216 yul = false
5217
5218 [profile.default.optimizer_details.yulDetails]
5219 stackAllocation = true
5220 ",
5221 )?;
5222 let mut loaded = Config::load().unwrap();
5223 clear_warning(&mut loaded);
5224 assert_eq!(
5225 loaded.optimizer_details,
5226 Some(OptimizerDetails {
5227 yul: Some(false),
5228 yul_details: Some(YulDetails {
5229 stack_allocation: Some(true),
5230 ..Default::default()
5231 }),
5232 ..Default::default()
5233 })
5234 );
5235
5236 let s = loaded.to_string_pretty().unwrap();
5237 jail.create_file("foundry.toml", &s)?;
5238 mark_serialized_invariant_provenance(&mut loaded);
5239
5240 let mut reloaded = Config::load().unwrap();
5241 clear_warning(&mut reloaded);
5242 assert_eq!(loaded, reloaded);
5243
5244 Ok(())
5245 });
5246 }
5247
5248 #[test]
5249 fn test_model_checker_settings_basic() {
5250 figment::Jail::expect_with(|jail| {
5251 jail.create_file(
5252 "foundry.toml",
5253 r"
5254 [profile.default]
5255
5256 [profile.default.model_checker]
5257 contracts = { 'a.sol' = [ 'A1', 'A2' ], 'b.sol' = [ 'B1', 'B2' ] }
5258 engine = 'chc'
5259 targets = [ 'assert', 'outOfBounds' ]
5260 timeout = 10000
5261 ",
5262 )?;
5263 let mut loaded = Config::load().unwrap();
5264 clear_warning(&mut loaded);
5265 assert_eq!(
5266 loaded.model_checker,
5267 Some(ModelCheckerSettings {
5268 contracts: BTreeMap::from([
5269 ("a.sol".to_string(), vec!["A1".to_string(), "A2".to_string()]),
5270 ("b.sol".to_string(), vec!["B1".to_string(), "B2".to_string()]),
5271 ]),
5272 engine: Some(ModelCheckerEngine::CHC),
5273 targets: Some(vec![
5274 ModelCheckerTarget::Assert,
5275 ModelCheckerTarget::OutOfBounds
5276 ]),
5277 timeout: Some(10000),
5278 invariants: None,
5279 show_unproved: None,
5280 div_mod_with_slacks: None,
5281 solvers: None,
5282 show_unsupported: None,
5283 show_proved_safe: None,
5284 })
5285 );
5286
5287 let s = loaded.to_string_pretty().unwrap();
5288 jail.create_file("foundry.toml", &s)?;
5289 mark_serialized_invariant_provenance(&mut loaded);
5290
5291 let mut reloaded = Config::load().unwrap();
5292 clear_warning(&mut reloaded);
5293 assert_eq!(loaded, reloaded);
5294
5295 Ok(())
5296 });
5297 }
5298
5299 #[test]
5300 fn test_model_checker_settings_with_bool_flags() {
5301 figment::Jail::expect_with(|jail| {
5302 jail.create_file(
5303 "foundry.toml",
5304 r"
5305 [profile.default]
5306
5307 [profile.default.model_checker]
5308 engine = 'chc'
5309 show_unproved = true
5310 show_unsupported = true
5311 show_proved_safe = false
5312 div_mod_with_slacks = true
5313 ",
5314 )?;
5315 let mut loaded = Config::load().unwrap();
5316 clear_warning(&mut loaded);
5317
5318 let mc = loaded.model_checker.as_ref().unwrap();
5319 assert_eq!(mc.show_unproved, Some(true));
5320 assert_eq!(mc.show_unsupported, Some(true));
5321 assert_eq!(mc.show_proved_safe, Some(false));
5322 assert_eq!(mc.div_mod_with_slacks, Some(true));
5323
5324 let s = loaded.to_string_pretty().unwrap();
5326 jail.create_file("foundry.toml", &s)?;
5327
5328 let mut reloaded = Config::load().unwrap();
5329 clear_warning(&mut reloaded);
5330
5331 let mc_reloaded = reloaded.model_checker.as_ref().unwrap();
5332 assert_eq!(mc_reloaded.show_unproved, Some(true));
5333 assert_eq!(mc_reloaded.show_unsupported, Some(true));
5334 assert_eq!(mc_reloaded.show_proved_safe, Some(false));
5335 assert_eq!(mc_reloaded.div_mod_with_slacks, Some(true));
5336
5337 Ok(())
5338 });
5339 }
5340
5341 #[test]
5342 fn test_model_checker_settings_relative_paths() {
5343 figment::Jail::expect_with(|jail| {
5344 jail.create_file(
5345 "foundry.toml",
5346 r"
5347 [profile.default]
5348
5349 [profile.default.model_checker]
5350 contracts = { 'a.sol' = [ 'A1', 'A2' ], 'b.sol' = [ 'B1', 'B2' ] }
5351 engine = 'chc'
5352 targets = [ 'assert', 'outOfBounds' ]
5353 timeout = 10000
5354 ",
5355 )?;
5356 let loaded = Config::load().unwrap().sanitized();
5357
5358 let dir = foundry_compilers::utils::canonicalize(jail.directory())
5363 .expect("Could not canonicalize jail path");
5364 assert_eq!(
5365 loaded.model_checker,
5366 Some(ModelCheckerSettings {
5367 contracts: BTreeMap::from([
5368 (
5369 format!("{}", dir.join("a.sol").display()),
5370 vec!["A1".to_string(), "A2".to_string()]
5371 ),
5372 (
5373 format!("{}", dir.join("b.sol").display()),
5374 vec!["B1".to_string(), "B2".to_string()]
5375 ),
5376 ]),
5377 engine: Some(ModelCheckerEngine::CHC),
5378 targets: Some(vec![
5379 ModelCheckerTarget::Assert,
5380 ModelCheckerTarget::OutOfBounds
5381 ]),
5382 timeout: Some(10000),
5383 invariants: None,
5384 show_unproved: None,
5385 div_mod_with_slacks: None,
5386 solvers: None,
5387 show_unsupported: None,
5388 show_proved_safe: None,
5389 })
5390 );
5391
5392 Ok(())
5393 });
5394 }
5395
5396 #[test]
5397 fn test_fmt_config() {
5398 figment::Jail::expect_with(|jail| {
5399 jail.create_file(
5400 "foundry.toml",
5401 r#"
5402 [fmt]
5403 line_length = 100
5404 tab_width = 2
5405 bracket_spacing = true
5406 style = "space"
5407 "#,
5408 )?;
5409 let loaded = Config::load().unwrap().sanitized();
5410 assert_eq!(
5411 loaded.fmt,
5412 FormatterConfig {
5413 line_length: 100,
5414 tab_width: 2,
5415 bracket_spacing: true,
5416 style: IndentStyle::Space,
5417 ..Default::default()
5418 }
5419 );
5420
5421 Ok(())
5422 });
5423 }
5424
5425 #[test]
5426 fn test_lint_config() {
5427 figment::Jail::expect_with(|jail| {
5428 jail.create_file(
5429 "foundry.toml",
5430 r"
5431 [lint]
5432 severity = ['high', 'medium']
5433 exclude_lints = ['incorrect-shift']
5434 ",
5435 )?;
5436 let loaded = Config::load().unwrap().sanitized();
5437 assert_eq!(
5438 loaded.lint,
5439 LinterConfig {
5440 severity: vec![LintSeverity::High, LintSeverity::Med],
5441 exclude_lints: vec!["incorrect-shift".into()],
5442 ..Default::default()
5443 }
5444 );
5445
5446 Ok(())
5447 });
5448 }
5449
5450 #[test]
5451 fn test_invariant_config() {
5452 figment::Jail::expect_with(|jail| {
5453 jail.create_file(
5454 "foundry.toml",
5455 r#"
5456 [invariant]
5457 runs = 512
5458 depth = 10
5459 min_depth = 2
5460 depth_mode = "random"
5461 workers = 4
5462 corpus_random_sequence_weight = 30
5463 payable_value_weight = 12
5464 mutation_weight_cmp = 7
5465 "#,
5466 )?;
5467
5468 let loaded = Config::load().unwrap().sanitized();
5469 assert_eq!(
5470 loaded.invariant,
5471 InvariantConfig {
5472 runs: 512,
5473 depth: 10,
5474 min_depth: 2,
5475 depth_mode: InvariantDepthMode::Random,
5476 workers: InvariantWorkers::Fixed(NonZeroUsize::new(4).unwrap()),
5477 corpus: FuzzCorpusConfig {
5478 corpus_random_sequence_weight: 30,
5479 payable_value_weight: 12,
5480 mutation_weights: FuzzCorpusMutationWeights {
5481 mutation_weight_cmp: 7,
5482 ..Default::default()
5483 },
5484 ..Default::default()
5485 },
5486 corpus_random_sequence_weight_configured: true,
5487 workers_configured: true,
5488 failure_persist_dir: Some(PathBuf::from("cache/invariant")),
5489 ..Default::default()
5490 }
5491 );
5492 assert!(loaded.invariant.corpus_random_sequence_weight_configured);
5493
5494 Ok(())
5495 });
5496 }
5497
5498 #[test]
5499 fn test_invariant_corpus_random_sequence_weight_provenance() {
5500 figment::Jail::expect_with(|jail| {
5501 jail.create_file(
5502 "foundry.toml",
5503 r#"
5504 [invariant]
5505 depth = 10
5506 "#,
5507 )?;
5508
5509 let loaded = Config::load().unwrap();
5510 assert_eq!(
5511 loaded.invariant.corpus.corpus_random_sequence_weight,
5512 FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
5513 );
5514 assert!(!loaded.invariant.corpus_random_sequence_weight_configured);
5515 assert!(!loaded.invariant.workers_configured);
5516
5517 jail.create_file(
5518 "foundry.toml",
5519 r#"
5520 [invariant]
5521 corpus_random_sequence_weight = 10
5522 workers = 4
5523 "#,
5524 )?;
5525
5526 let loaded = Config::load().unwrap();
5527 assert_eq!(
5528 loaded.invariant.corpus.corpus_random_sequence_weight,
5529 FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
5530 );
5531 assert!(loaded.invariant.corpus_random_sequence_weight_configured);
5532 assert_eq!(
5533 loaded.invariant.workers,
5534 InvariantWorkers::Fixed(NonZeroUsize::new(4).unwrap())
5535 );
5536 assert!(loaded.invariant.workers_configured);
5537
5538 Ok(())
5539 });
5540 }
5541
5542 #[test]
5543 fn test_fuzz_corpus_random_sequence_weight_fallback_does_not_mark_invariant_configured() {
5544 figment::Jail::expect_with(|jail| {
5545 jail.create_file(
5546 "foundry.toml",
5547 r#"
5548 [fuzz]
5549 corpus_random_sequence_weight = 10
5550 "#,
5551 )?;
5552
5553 let loaded = Config::load().unwrap();
5554 assert_eq!(
5555 loaded.invariant.corpus.corpus_random_sequence_weight,
5556 FuzzCorpusConfig::DEFAULT_CORPUS_RANDOM_SEQUENCE_WEIGHT
5557 );
5558 assert!(!loaded.invariant.corpus_random_sequence_weight_configured);
5559
5560 Ok(())
5561 });
5562 }
5563
5564 #[test]
5565 fn test_standalone_sections_env() {
5566 figment::Jail::expect_with(|jail| {
5567 jail.create_file(
5568 "foundry.toml",
5569 r"
5570 [fuzz]
5571 runs = 100
5572
5573 [invariant]
5574 depth = 1
5575 ",
5576 )?;
5577
5578 jail.set_env("FOUNDRY_FMT_LINE_LENGTH", "95");
5579 jail.set_env("FOUNDRY_FUZZ_DICTIONARY_WEIGHT", "99");
5580 jail.set_env("FOUNDRY_FUZZ_MAX_FUZZ_DICTIONARY_VALUES", "max");
5581 jail.set_env("FOUNDRY_INVARIANT_DEPTH", "5");
5582 jail.set_env("FOUNDRY_INVARIANT_MIN_DEPTH", "2");
5583 jail.set_env("FOUNDRY_INVARIANT_DEPTH_MODE", "random");
5584 jail.set_env("FOUNDRY_INVARIANT_WORKERS", "3");
5585 jail.set_env("FOUNDRY_INVARIANT_CORPUS_RANDOM_SEQUENCE_WEIGHT", "30");
5586 jail.set_env("FOUNDRY_INVARIANT_PAYABLE_VALUE_WEIGHT", "12");
5587 jail.set_env("FOUNDRY_INVARIANT_MUTATION_WEIGHT_CMP", "7");
5588 jail.set_env("FOUNDRY_SYMBOLIC_MAX_PATHS", "64");
5589 jail.set_env("FOUNDRY_SYMBOLIC_DUMP_SMT", "true");
5590
5591 let config = Config::load().unwrap();
5592 assert_eq!(config.fmt.line_length, 95);
5593 assert_eq!(config.fuzz.dictionary.dictionary_weight, 99);
5594 assert_eq!(config.fuzz.dictionary.max_fuzz_dictionary_values, usize::MAX);
5595 assert_eq!(config.invariant.depth, 5);
5596 assert_eq!(config.invariant.min_depth, 2);
5597 assert_eq!(config.invariant.depth_mode, InvariantDepthMode::Random);
5598 assert_eq!(
5599 config.invariant.workers,
5600 InvariantWorkers::Fixed(NonZeroUsize::new(3).unwrap())
5601 );
5602 assert_eq!(config.invariant.corpus.corpus_random_sequence_weight, 30);
5603 assert!(config.invariant.corpus_random_sequence_weight_configured);
5604 assert_eq!(config.invariant.corpus.payable_value_weight, 12);
5605 assert_eq!(config.invariant.corpus.mutation_weights.mutation_weight_cmp, 7);
5606 assert_eq!(config.symbolic.max_paths, 64);
5607 assert!(config.symbolic.dump_smt);
5608
5609 Ok(())
5610 });
5611 }
5612
5613 #[test]
5614 fn test_invariant_workers_env_accepts_auto() {
5615 figment::Jail::expect_with(|jail| {
5616 jail.create_file(
5617 "foundry.toml",
5618 r"
5619 [invariant]
5620 workers = 3
5621 ",
5622 )?;
5623
5624 jail.set_env("FOUNDRY_INVARIANT_WORKERS", "auto");
5625
5626 let config = Config::load().unwrap();
5627 assert_eq!(config.invariant.workers, InvariantWorkers::Auto);
5628
5629 Ok(())
5630 });
5631 }
5632
5633 #[test]
5634 fn test_parse_with_profile() {
5635 let foundry_str = r"
5636 [profile.default]
5637 src = 'src'
5638 out = 'out'
5639 libs = ['lib']
5640
5641 # See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
5642 ";
5643 assert_eq!(
5644 parse_with_profile::<BasicConfig>(foundry_str).unwrap().unwrap(),
5645 (
5646 Config::DEFAULT_PROFILE,
5647 BasicConfig {
5648 profile: Config::DEFAULT_PROFILE,
5649 src: "src".into(),
5650 out: "out".into(),
5651 libs: vec!["lib".into()],
5652 remappings: vec![],
5653 network: None,
5654 }
5655 )
5656 );
5657 }
5658
5659 #[test]
5660 fn test_implicit_profile_loads() {
5661 figment::Jail::expect_with(|jail| {
5662 jail.create_file(
5663 "foundry.toml",
5664 r"
5665 [default]
5666 src = 'my-src'
5667 out = 'my-out'
5668 ",
5669 )?;
5670 let loaded = Config::load().unwrap().sanitized();
5671 assert_eq!(loaded.src.file_name().unwrap(), "my-src");
5672 assert_eq!(loaded.out.file_name().unwrap(), "my-out");
5673 assert_eq!(
5674 loaded.warnings,
5675 vec![Warning::UnknownSection {
5676 unknown_section: Profile::new("default"),
5677 source: Some("foundry.toml".into())
5678 }]
5679 );
5680
5681 Ok(())
5682 });
5683 }
5684
5685 #[test]
5686 fn hardfork_overrides_spec_id() {
5687 let config = Config {
5688 hardfork: Some(FoundryHardfork::Tempo(TempoHardfork::T3)),
5689 ..Config::default()
5690 };
5691
5692 assert_eq!(config.evm_spec_id::<TempoHardfork>(), TempoHardfork::T3);
5693 }
5694
5695 #[test]
5696 fn solc_settings_include_experimental_setting() {
5697 let config = Config { experimental: true, ..Config::default() };
5698 let settings = config.solc_settings().unwrap();
5699 assert_eq!(settings.settings.experimental, Some(true));
5700 assert!(settings.cli_settings.extra_args.is_empty());
5701
5702 let config = Config {
5703 experimental: false,
5704 extra_args: vec!["--experimental".to_string()],
5705 ..Config::default()
5706 };
5707 let settings = config.solc_settings().unwrap();
5708 assert_eq!(settings.settings.experimental, Some(false));
5709 assert_eq!(settings.cli_settings.extra_args, vec!["--experimental".to_string()]);
5710 }
5711
5712 #[test]
5713 fn solc_settings_include_via_ssa_cfg_setting() {
5714 let config = Config { via_ssa_cfg: true, ..Config::default() };
5715 let settings = config.solc_settings().unwrap();
5716 assert_eq!(settings.settings.via_ssa_cfg, Some(true));
5717 assert!(settings.cli_settings.extra_args.is_empty());
5718
5719 let config = Config {
5720 via_ssa_cfg: false,
5721 extra_args: vec!["--via-ssa-cfg".to_string()],
5722 ..Config::default()
5723 };
5724 let settings = config.solc_settings().unwrap();
5725 assert_eq!(settings.settings.via_ssa_cfg, Some(false));
5726 assert_eq!(settings.cli_settings.extra_args, vec!["--via-ssa-cfg".to_string()]);
5727 }
5728
5729 #[test]
5730 fn tempo_network_defaults_to_latest_tempo_hardfork() {
5731 figment::Jail::expect_with(|jail| {
5732 jail.create_file(
5733 "foundry.toml",
5734 r#"
5735 [profile.default]
5736 network = "tempo"
5737 "#,
5738 )?;
5739
5740 let config = Config::load().unwrap();
5741 assert!(config.networks.is_tempo());
5742 assert_eq!(config.evm_spec_id::<TempoHardfork>(), latest_active_tempo_hardfork());
5743
5744 Ok(())
5745 });
5746 }
5747
5748 #[test]
5749 fn tempo_hardfork_infers_tempo_network() {
5750 figment::Jail::expect_with(|jail| {
5751 jail.create_file(
5752 "foundry.toml",
5753 r#"
5754 [profile.default]
5755 hardfork = "tempo:T3"
5756 "#,
5757 )?;
5758
5759 let config = Config::load().unwrap();
5760 assert_eq!(config.hardfork, Some(FoundryHardfork::Tempo(TempoHardfork::T3)));
5761 assert!(config.networks.is_tempo());
5762
5763 Ok(())
5764 });
5765 }
5766
5767 #[cfg(feature = "base")]
5768 #[test]
5769 fn base_upgrade_infers_base_network() {
5770 figment::Jail::expect_with(|jail| {
5771 jail.create_file(
5772 "foundry.toml",
5773 r#"
5774 [profile.default]
5775 hardfork = "base:Beryl"
5776 "#,
5777 )?;
5778
5779 let config = Config::load().unwrap();
5780 assert_eq!(config.hardfork, Some(FoundryHardfork::Base(BaseUpgrade::Beryl)));
5781 assert!(config.networks.is_base());
5782
5783 Ok(())
5784 });
5785 }
5786
5787 #[test]
5788 #[cfg(feature = "monad")]
5789 fn namespaced_hardfork_infers_monad_network() {
5790 figment::Jail::expect_with(|jail| {
5791 jail.create_file(
5792 "foundry.toml",
5793 r#"
5794 [profile.default]
5795 hardfork = "monad:MonadNine"
5796 "#,
5797 )?;
5798
5799 let config = Config::load().unwrap();
5800 assert_eq!(
5801 config.hardfork,
5802 Some(FoundryHardfork::Monad(foundry_evm_hardforks::MonadHardfork::MonadNine))
5803 );
5804 assert_eq!(
5805 config.evm_spec_id::<foundry_evm_hardforks::MonadHardfork>(),
5806 foundry_evm_hardforks::MonadHardfork::MonadNine
5807 );
5808 assert_eq!(
5809 config.hardfork.as_ref().and_then(FoundryHardfork::namespace),
5810 Some("monad")
5811 );
5812 assert_eq!(
5813 config.hardfork.as_ref().map(FoundryHardfork::name).as_deref(),
5814 Some("MonadNine")
5815 );
5816 assert!(config.networks.is_monad());
5817
5818 Ok(())
5819 });
5820 }
5821
5822 #[test]
5823 #[cfg(feature = "monad")]
5824 fn network_selectors_reject_profile_merged_hybrid() {
5825 figment::Jail::expect_with(|jail| {
5826 jail.create_file(
5827 "foundry.toml",
5828 r#"
5829 [profile.default]
5830 monad = true
5831
5832 [profile.ci]
5833 celo = true
5834 "#,
5835 )?;
5836 jail.set_env("FOUNDRY_PROFILE", "ci");
5837
5838 let err = Config::load().unwrap_err().to_string();
5839 assert!(err.contains(
5840 "network selectors `celo = true` and `monad = true` conflict; select only one \
5841 network"
5842 ));
5843
5844 Ok(())
5845 });
5846 }
5847
5848 #[test]
5849 #[cfg(feature = "monad")]
5850 fn network_selectors_reject_canonical_environment_conflict() {
5851 figment::Jail::expect_with(|jail| {
5852 jail.create_file(
5853 "foundry.toml",
5854 r#"
5855 [profile.default]
5856 tempo = true
5857 "#,
5858 )?;
5859 jail.set_env("FOUNDRY_NETWORK", "monad");
5860
5861 let err = Config::load().unwrap_err().to_string();
5862 assert!(err.contains(
5863 "network selectors `network = \"monad\"` and `tempo = true` conflict; select only \
5864 one network"
5865 ));
5866
5867 Ok(())
5868 });
5869 }
5870
5871 #[test]
5872 #[cfg(feature = "monad")]
5873 fn matching_canonical_and_legacy_network_selectors_remain_valid() {
5874 figment::Jail::expect_with(|jail| {
5875 jail.create_file(
5876 "foundry.toml",
5877 r#"
5878 [profile.default]
5879 network = "monad"
5880 monad = true
5881 "#,
5882 )?;
5883
5884 let config = Config::load().unwrap();
5885 assert!(config.networks.is_monad());
5886 assert_eq!(
5887 config.networks.resolved_network(),
5888 Some(foundry_evm_networks::NetworkVariant::Monad)
5889 );
5890
5891 Ok(())
5892 });
5893 }
5894
5895 #[test]
5896 fn celo_network_accepts_ethereum_hardfork() {
5897 figment::Jail::expect_with(|jail| {
5898 jail.create_file(
5899 "foundry.toml",
5900 r#"
5901 [profile.default]
5902 celo = true
5903 hardfork = "prague"
5904 "#,
5905 )?;
5906
5907 let config = Config::load().unwrap();
5908 assert!(config.networks.is_celo());
5909 assert_eq!(
5910 config.hardfork,
5911 Some(FoundryHardfork::Ethereum(foundry_evm_hardforks::EthereumHardfork::Prague))
5912 );
5913
5914 Ok(())
5915 });
5916 }
5917
5918 #[test]
5919 fn hardfork_rejects_conflicting_network() {
5920 figment::Jail::expect_with(|jail| {
5921 jail.create_file(
5922 "foundry.toml",
5923 r#"
5924 [profile.default]
5925 tempo = true
5926 hardfork = "shanghai"
5927 "#,
5928 )?;
5929
5930 let err = Config::load().unwrap_err();
5931 assert!(
5932 err.to_string()
5933 .to_lowercase()
5934 .contains("hardfork `shanghai` conflicts with network config `tempo`")
5935 );
5936
5937 Ok(())
5938 });
5939 }
5940
5941 #[test]
5942 fn test_etherscan_api_key() {
5943 figment::Jail::expect_with(|jail| {
5944 jail.create_file(
5945 "foundry.toml",
5946 r"
5947 [default]
5948 ",
5949 )?;
5950 jail.set_env("ETHERSCAN_API_KEY", "");
5951 let loaded = Config::load().unwrap().sanitized();
5952 assert!(loaded.etherscan_api_key.is_none());
5953
5954 jail.set_env("ETHERSCAN_API_KEY", "DUMMY");
5955 let loaded = Config::load().unwrap().sanitized();
5956 assert_eq!(loaded.etherscan_api_key, Some("DUMMY".into()));
5957
5958 Ok(())
5959 });
5960 }
5961
5962 #[test]
5963 fn test_etherscan_api_key_figment() {
5964 figment::Jail::expect_with(|jail| {
5965 jail.create_file(
5966 "foundry.toml",
5967 r"
5968 [default]
5969 etherscan_api_key = 'DUMMY'
5970 ",
5971 )?;
5972 jail.set_env("ETHERSCAN_API_KEY", "ETHER");
5973
5974 let figment = Config::figment_with_root(jail.directory())
5975 .merge(("etherscan_api_key", "USER_KEY"));
5976
5977 let loaded = Config::from_provider(figment).unwrap();
5978 assert_eq!(loaded.etherscan_api_key, Some("USER_KEY".into()));
5979
5980 Ok(())
5981 });
5982 }
5983
5984 #[test]
5985 fn test_normalize_defaults() {
5986 figment::Jail::expect_with(|jail| {
5987 jail.create_file(
5988 "foundry.toml",
5989 r"
5990 [default]
5991 solc = '0.8.13'
5992 ",
5993 )?;
5994
5995 let loaded = Config::load().unwrap().sanitized();
5996 assert_eq!(loaded.evm_version, EvmVersion::London);
5997
5998 let figment = Config::figment_with_root(jail.directory()).merge(
5999 Serialized::default("evm_version", EvmVersion::Amsterdam)
6000 .profile(Config::selected_profile()),
6001 );
6002 let loaded = Config::from_provider(figment).unwrap().sanitized();
6003 assert_eq!(loaded.evm_version, EvmVersion::Amsterdam);
6004 Ok(())
6005 });
6006 }
6007
6008 #[expect(clippy::disallowed_macros)]
6010 #[test]
6011 #[ignore]
6012 fn print_config() {
6013 let config = Config {
6014 optimizer_details: Some(OptimizerDetails {
6015 peephole: None,
6016 inliner: None,
6017 jumpdest_remover: None,
6018 order_literals: None,
6019 deduplicate: None,
6020 cse: None,
6021 constant_optimizer: Some(true),
6022 yul: Some(true),
6023 yul_details: Some(YulDetails {
6024 stack_allocation: None,
6025 optimizer_steps: Some("dhfoDgvulfnTUtnIf".to_string()),
6026 }),
6027 simple_counter_for_loop_unchecked_increment: None,
6028 }),
6029 ..Default::default()
6030 };
6031 println!("{}", config.to_string_pretty().unwrap());
6032 }
6033
6034 #[test]
6035 fn can_use_impl_figment_macro() {
6036 #[derive(Default, Serialize)]
6037 struct MyArgs {
6038 #[serde(skip_serializing_if = "Option::is_none")]
6039 root: Option<PathBuf>,
6040 }
6041 impl_figment_convert!(MyArgs);
6042
6043 impl Provider for MyArgs {
6044 fn metadata(&self) -> Metadata {
6045 Metadata::default()
6046 }
6047
6048 fn data(&self) -> Result<Map<Profile, Dict>, Error> {
6049 let value = Value::serialize(self)?;
6050 let error = InvalidType(value.to_actual(), "map".into());
6051 let dict = value.into_dict().ok_or(error)?;
6052 Ok(Map::from([(Config::selected_profile(), dict)]))
6053 }
6054 }
6055
6056 let _figment: Figment = From::from(&MyArgs::default());
6057
6058 #[derive(Default)]
6059 struct Outer {
6060 start: MyArgs,
6061 other: MyArgs,
6062 another: MyArgs,
6063 }
6064 impl_figment_convert!(Outer, start, other, another);
6065
6066 let _figment: Figment = From::from(&Outer::default());
6067 }
6068
6069 #[test]
6070 fn list_cached_blocks() -> eyre::Result<()> {
6071 fn fake_block_cache(chain_path: &Path, block_number: &str, size_bytes: usize) {
6072 let block_path = chain_path.join(block_number);
6073 fs::create_dir(block_path.as_path()).unwrap();
6074 let file_path = block_path.join("storage.json");
6075 let mut file = File::create(file_path).unwrap();
6076 writeln!(file, "{}", vec![' '; size_bytes - 1].iter().collect::<String>()).unwrap();
6077 }
6078
6079 fn fake_endpoint_block_cache(
6080 chain_path: &Path,
6081 block_number: &str,
6082 endpoint: &str,
6083 size_bytes: usize,
6084 ) {
6085 let block_path = chain_path.join(block_number);
6086 let file_path = block_path.join(format!("storage-{endpoint}.json"));
6087 let mut file = File::create(file_path).unwrap();
6088 writeln!(file, "{}", vec![' '; size_bytes - 1].iter().collect::<String>()).unwrap();
6089 }
6090
6091 fn fake_block_cache_block_path_as_file(
6092 chain_path: &Path,
6093 block_number: &str,
6094 size_bytes: usize,
6095 ) {
6096 let block_path = chain_path.join(block_number);
6097 let mut file = File::create(block_path).unwrap();
6098 writeln!(file, "{}", vec![' '; size_bytes - 1].iter().collect::<String>()).unwrap();
6099 }
6100
6101 let chain_dir = tempdir()?;
6102
6103 fake_block_cache(chain_dir.path(), "1", 100);
6104 fake_endpoint_block_cache(
6105 chain_dir.path(),
6106 "1",
6107 "0000000000000000000000000000000000000000000000000000000000000000",
6108 50,
6109 );
6110 fake_endpoint_block_cache(chain_dir.path(), "1", "backup", 75);
6111 fake_block_cache(chain_dir.path(), "2", 500);
6112 fake_block_cache_block_path_as_file(chain_dir.path(), "3", 900);
6113 let mut pol_file = File::create(chain_dir.path().join("pol.txt")).unwrap();
6115 writeln!(pol_file, "{}", [' '; 10].iter().collect::<String>()).unwrap();
6116
6117 let result = Config::get_cached_blocks(chain_dir.path())?;
6118
6119 assert_eq!(result.len(), 3);
6120 let block1 = &result.iter().find(|x| x.0 == "1").unwrap();
6121 let block2 = &result.iter().find(|x| x.0 == "2").unwrap();
6122 let block3 = &result.iter().find(|x| x.0 == "3").unwrap();
6123
6124 assert_eq!(block1.0, "1");
6125 assert_eq!(block1.1, 150);
6126 assert_eq!(block2.0, "2");
6127 assert_eq!(block2.1, 500);
6128 assert_eq!(block3.0, "3");
6129 assert_eq!(block3.1, 900);
6130
6131 chain_dir.close()?;
6132 Ok(())
6133 }
6134
6135 #[test]
6136 fn list_cached_blocks_ignores_removed_entries() -> eyre::Result<()> {
6137 let chain_dir = tempdir()?;
6138 let block_path = chain_dir.path().join("1");
6139 fs::create_dir(&block_path)?;
6140 File::create(block_path.join("storage.json"))?;
6141
6142 let block = fs::read_dir(chain_dir.path())?.next().unwrap()?;
6143 let cache_file = fs::read_dir(&block_path)?.next().unwrap()?;
6144 fs::remove_dir_all(block_path)?;
6145
6146 assert!(Config::get_cached_block(block)?.is_none());
6147 assert!(Config::get_cache_file_size(cache_file)?.is_none());
6148 Ok(())
6149 }
6150
6151 #[test]
6152 fn ignore_not_found_propagates_other_errors() {
6153 assert!(
6154 Config::ignore_not_found::<()>(Err(io::Error::from(io::ErrorKind::NotFound)))
6155 .unwrap()
6156 .is_none()
6157 );
6158
6159 let err =
6160 Config::ignore_not_found::<()>(Err(io::Error::from(io::ErrorKind::PermissionDenied)))
6161 .unwrap_err();
6162 assert_eq!(
6163 err.downcast_ref::<io::Error>().unwrap().kind(),
6164 io::ErrorKind::PermissionDenied
6165 );
6166 }
6167
6168 #[test]
6169 fn cache_listing_propagates_not_a_directory() -> eyre::Result<()> {
6170 let cache_file = tempfile::NamedTempFile::new()?;
6171
6172 let err = Config::get_cached_blocks(cache_file.path()).unwrap_err();
6173 assert_eq!(err.downcast_ref::<io::Error>().unwrap().kind(), io::ErrorKind::NotADirectory);
6174
6175 let err = Config::get_cached_block_explorer_data(cache_file.path()).unwrap_err();
6176 assert_eq!(err.downcast_ref::<io::Error>().unwrap().kind(), io::ErrorKind::NotADirectory);
6177 Ok(())
6178 }
6179
6180 #[test]
6181 fn list_cached_blocks_uses_replaced_entry_type() -> eyre::Result<()> {
6182 let chain_dir = tempdir()?;
6183 let block_path = chain_dir.path().join("1");
6184 File::create(&block_path)?;
6185 let block = fs::read_dir(chain_dir.path())?.next().unwrap()?;
6186
6187 fs::remove_file(&block_path)?;
6188 fs::create_dir(&block_path)?;
6189 fs::write(block_path.join("storage.json"), [0; 10])?;
6190
6191 assert_eq!(Config::get_cached_block(block)?, Some(("1".to_string(), 10)));
6192 Ok(())
6193 }
6194
6195 #[cfg(unix)]
6196 #[test]
6197 fn list_cached_blocks_ignores_symlinks() -> eyre::Result<()> {
6198 let chain_dir = tempdir()?;
6199 let target_dir = tempdir()?;
6200 fs::write(target_dir.path().join("storage.json"), [0; 10])?;
6201 std::os::unix::fs::symlink(target_dir.path(), chain_dir.path().join("1"))?;
6202
6203 let block_path = chain_dir.path().join("2");
6204 fs::create_dir(&block_path)?;
6205 let target_file = tempfile::NamedTempFile::new()?;
6206 fs::write(target_file.path(), [0; 10])?;
6207 std::os::unix::fs::symlink(target_file.path(), block_path.join("storage.json"))?;
6208
6209 assert!(Config::get_cached_blocks(chain_dir.path())?.is_empty());
6210 Ok(())
6211 }
6212
6213 #[test]
6214 fn list_etherscan_cache_ignores_removed_entries() -> eyre::Result<()> {
6215 let cache_dir = tempdir()?;
6216 let sources_path = cache_dir.path().join("sources");
6217 fs::create_dir(&sources_path)?;
6218 File::create(sources_path.join("metadata.json"))?;
6219
6220 let sources = fs::read_dir(cache_dir.path())?.next().unwrap()?;
6221 let metadata = fs::read_dir(&sources_path)?.next().unwrap()?;
6222 fs::remove_dir_all(sources_path)?;
6223
6224 assert!(Config::get_cached_entry_size(sources)?.is_none());
6225 assert!(Config::get_cached_entry_size(metadata)?.is_none());
6226 Ok(())
6227 }
6228
6229 #[test]
6230 fn list_etherscan_cache() -> eyre::Result<()> {
6231 fn fake_etherscan_cache(chain_path: &Path, address: &str, size_bytes: usize) {
6232 let metadata_path = chain_path.join("sources");
6233 let abi_path = chain_path.join("abi");
6234 let _ = fs::create_dir(metadata_path.as_path());
6235 let _ = fs::create_dir(abi_path.as_path());
6236
6237 let metadata_file_path = metadata_path.join(address);
6238 let mut metadata_file = File::create(metadata_file_path).unwrap();
6239 writeln!(metadata_file, "{}", vec![' '; size_bytes / 2 - 1].iter().collect::<String>())
6240 .unwrap();
6241
6242 let abi_file_path = abi_path.join(address);
6243 let mut abi_file = File::create(abi_file_path).unwrap();
6244 writeln!(abi_file, "{}", vec![' '; size_bytes / 2 - 1].iter().collect::<String>())
6245 .unwrap();
6246 }
6247
6248 let chain_dir = tempdir()?;
6249
6250 fake_etherscan_cache(chain_dir.path(), "1", 100);
6251 fake_etherscan_cache(chain_dir.path(), "2", 500);
6252
6253 let result = Config::get_cached_block_explorer_data(chain_dir.path())?;
6254
6255 assert_eq!(result, 600);
6256
6257 chain_dir.close()?;
6258 Ok(())
6259 }
6260
6261 #[test]
6262 fn test_parse_error_codes() {
6263 figment::Jail::expect_with(|jail| {
6264 jail.create_file(
6265 "foundry.toml",
6266 r#"
6267 [default]
6268 ignored_error_codes = ["license", "unreachable", 1337]
6269 "#,
6270 )?;
6271
6272 let config = Config::load().unwrap();
6273 assert_eq!(
6274 config.ignored_error_codes,
6275 vec![
6276 SolidityErrorCode::SpdxLicenseNotProvided,
6277 SolidityErrorCode::Unreachable,
6278 SolidityErrorCode::Other(1337)
6279 ]
6280 );
6281
6282 Ok(())
6283 });
6284 }
6285
6286 #[test]
6287 fn test_parse_file_paths() {
6288 figment::Jail::expect_with(|jail| {
6289 jail.create_file(
6290 "foundry.toml",
6291 r#"
6292 [default]
6293 ignored_warnings_from = ["something"]
6294 "#,
6295 )?;
6296
6297 let config = Config::load().unwrap();
6298 assert_eq!(config.ignored_file_paths, vec![Path::new("something").to_path_buf()]);
6299
6300 Ok(())
6301 });
6302 }
6303
6304 #[test]
6305 fn test_parse_optimizer_settings() {
6306 figment::Jail::expect_with(|jail| {
6307 jail.create_file(
6308 "foundry.toml",
6309 r"
6310 [default]
6311 [profile.default.optimizer_details]
6312 ",
6313 )?;
6314
6315 let config = Config::load().unwrap();
6316 assert_eq!(config.optimizer_details, Some(OptimizerDetails::default()));
6317
6318 Ok(())
6319 });
6320 }
6321
6322 #[test]
6323 fn test_parse_labels() {
6324 figment::Jail::expect_with(|jail| {
6325 jail.create_file(
6326 "foundry.toml",
6327 r#"
6328 [labels]
6329 0x1F98431c8aD98523631AE4a59f267346ea31F984 = "Uniswap V3: Factory"
6330 0xC36442b4a4522E871399CD717aBDD847Ab11FE88 = "Uniswap V3: Positions NFT"
6331 "#,
6332 )?;
6333
6334 let config = Config::load().unwrap();
6335 assert_eq!(
6336 config.labels,
6337 AddressHashMap::from_iter(vec![
6338 (
6339 address!("0x1F98431c8aD98523631AE4a59f267346ea31F984"),
6340 "Uniswap V3: Factory".to_string()
6341 ),
6342 (
6343 address!("0xC36442b4a4522E871399CD717aBDD847Ab11FE88"),
6344 "Uniswap V3: Positions NFT".to_string()
6345 ),
6346 ])
6347 );
6348 assert_eq!(config.tracing.labels, config.labels);
6349 assert_eq!(
6350 config.warnings,
6351 vec![Warning::DeprecatedKey {
6352 old: "[labels]".to_string(),
6353 new: "[tracing.labels]".to_string(),
6354 }]
6355 );
6356
6357 Ok(())
6358 });
6359 }
6360
6361 #[test]
6362 fn test_parse_deprecated_profile_labels() {
6363 figment::Jail::expect_with(|jail| {
6364 jail.create_file(
6365 "foundry.toml",
6366 r#"
6367 [profile.default.labels]
6368 0x0000000000000000000000000000000000000001 = "Alice"
6369 "#,
6370 )?;
6371
6372 let config = Config::load().unwrap();
6373 let labels = AddressHashMap::from_iter(vec![(
6374 address!("0x0000000000000000000000000000000000000001"),
6375 "Alice".to_string(),
6376 )]);
6377 assert_eq!(config.labels, labels);
6378 assert_eq!(config.tracing.labels, labels);
6379 assert_eq!(
6380 config.warnings,
6381 vec![Warning::DeprecatedKey {
6382 old: "labels".to_string(),
6383 new: "tracing.labels".to_string(),
6384 }]
6385 );
6386
6387 Ok(())
6388 });
6389 }
6390
6391 #[test]
6392 fn test_deprecated_env_labels_use_tracing_section() {
6393 figment::Jail::expect_with(|jail| {
6394 jail.set_env(
6395 "FOUNDRY_LABELS",
6396 r#"{ "0x0000000000000000000000000000000000000001" = "Alice" }"#,
6397 );
6398 jail.set_env(
6399 "FOUNDRY_TRACING_LABELS",
6400 r#"{ "0x0000000000000000000000000000000000000001" = "Bob" }"#,
6401 );
6402
6403 let config = Config::load().unwrap();
6404 assert_eq!(
6405 config.labels,
6406 AddressHashMap::from_iter([(
6407 address!("0x0000000000000000000000000000000000000001"),
6408 "Alice".to_string(),
6409 )])
6410 );
6411 assert_eq!(
6412 config.tracing.labels,
6413 AddressHashMap::from_iter([(
6414 address!("0x0000000000000000000000000000000000000001"),
6415 "Bob".to_string(),
6416 )])
6417 );
6418
6419 Ok(())
6420 });
6421 }
6422
6423 #[test]
6424 fn test_deprecated_labels_warn_for_inactive_profiles() {
6425 figment::Jail::expect_with(|jail| {
6426 jail.create_file(
6427 "foundry.toml",
6428 r#"
6429 [profile.ci.labels]
6430 0x0000000000000000000000000000000000000001 = "Alice"
6431 "#,
6432 )?;
6433
6434 let config = Config::load().unwrap();
6435 assert_eq!(
6436 config.warnings,
6437 vec![Warning::DeprecatedKey {
6438 old: "labels".to_string(),
6439 new: "tracing.labels".to_string(),
6440 }]
6441 );
6442
6443 Ok(())
6444 });
6445 }
6446
6447 #[test]
6448 fn test_tracing_serialization_keeps_global_verbosity() {
6449 let address = address!("0x0000000000000000000000000000000000000001");
6450 let labels = AddressHashMap::from_iter([(address, "Alice".to_string())]);
6451 let config = Config {
6452 tracing: TracingConfig { verbosity: 4, labels: labels.clone(), ..Default::default() },
6453 ..Default::default()
6454 };
6455
6456 let serialized = toml::Value::try_from(&config).unwrap();
6457 let table = serialized.as_table().unwrap();
6458 assert_eq!(table["verbosity"].as_integer(), Some(0));
6459 assert!(!table.contains_key("labels"));
6460 assert_eq!(table["tracing"]["verbosity"].as_integer(), Some(4));
6461
6462 let provided = Figment::from(&config).extract::<Config>().unwrap();
6463 assert_eq!(provided.verbosity, 0);
6464 assert!(provided.labels.is_empty());
6465 assert_eq!(provided.tracing.labels, labels);
6466
6467 let merged = config.merge_inline_provider(("ffi", true)).unwrap();
6468 assert_eq!(merged.tracing.verbosity, 4);
6469 assert_eq!(merged.tracing.labels, config.tracing.labels);
6470 }
6471
6472 #[test]
6473 fn test_legacy_programmatic_labels_survive_serialization() {
6474 let address = address!("0x0000000000000000000000000000000000000001");
6475 let labels = AddressHashMap::from_iter([(address, "Alice".to_string())]);
6476 let config = Config { labels: labels.clone(), ..Default::default() };
6477
6478 let serialized = toml::Value::try_from(&config).unwrap();
6479 assert_eq!(
6480 serialized["labels"].as_table().unwrap().values().next().and_then(|v| v.as_str()),
6481 Some("Alice")
6482 );
6483
6484 let provided = Config::from_provider(&config).unwrap();
6485 assert_eq!(provided.labels, labels);
6486 assert_eq!(provided.tracing.labels, labels);
6487
6488 let merged = config.merge_inline_provider(("ffi", true)).unwrap();
6489 assert_eq!(merged.labels, labels);
6490 assert_eq!(merged.tracing.labels, labels);
6491 }
6492
6493 #[test]
6494 fn test_parse_tracing_section() {
6495 figment::Jail::expect_with(|jail| {
6496 jail.create_file(
6497 "foundry.toml",
6498 r#"
6499 [profile.default]
6500 verbosity = 2
6501
6502 [tracing]
6503 verbosity = 4
6504 disable_labels = true
6505 compact_labels = true
6506 trace_depth = 3
6507 decode_internal = true
6508 external_identification_timeout = 9
6509
6510 [tracing.labels]
6511 0x0000000000000000000000000000000000000002 = "Bob"
6512 "#,
6513 )?;
6514
6515 let config = Config::load().unwrap();
6516 assert_eq!(config.verbosity, 2);
6517 assert_eq!(config.tracing.verbosity, 4);
6518 assert!(config.tracing.disable_labels);
6519 assert_eq!(config.tracing.trace_depth, Some(3));
6520 assert!(config.tracing.decode_internal);
6521 assert!(config.tracing.compact_labels);
6522 assert_eq!(config.tracing.external_identification_timeout, 9);
6523 let labels = AddressHashMap::from_iter(vec![(
6524 address!("0x0000000000000000000000000000000000000002"),
6525 "Bob".to_string(),
6526 )]);
6527 assert!(config.labels.is_empty());
6528 assert_eq!(config.tracing.labels, labels);
6529 assert!(config.warnings.is_empty());
6530
6531 let serialized = config.to_string_pretty().unwrap();
6532 assert!(!serialized.contains("[labels]"));
6533 assert!(serialized.contains("[tracing.labels]"));
6534
6535 jail.create_file("foundry.toml", &serialized)?;
6536 let reloaded = Config::load().unwrap();
6537 assert!(reloaded.labels.is_empty());
6538 assert_eq!(reloaded.tracing.labels, labels);
6539 assert!(reloaded.warnings.is_empty());
6540
6541 Ok(())
6542 });
6543 }
6544
6545 #[test]
6546 fn test_external_identification_timeout_env() {
6547 figment::Jail::expect_with(|jail| {
6548 jail.set_env("FOUNDRY_TRACING_EXTERNAL_IDENTIFICATION_TIMEOUT", "0");
6549
6550 let config = Config::load().unwrap();
6551
6552 assert_eq!(config.tracing.external_identification_timeout, 0);
6553 Ok(())
6554 });
6555 }
6556
6557 #[test]
6558 fn test_global_and_tracing_verbosity_are_independent() {
6559 figment::Jail::expect_with(|jail| {
6560 jail.create_file(
6561 "foundry.toml",
6562 r#"
6563 [profile.default]
6564 verbosity = 4
6565
6566 [tracing]
6567 disable_labels = true
6568 "#,
6569 )?;
6570
6571 let config = Config::load().unwrap();
6572 assert_eq!(config.verbosity, 4);
6573 assert_eq!(config.tracing.verbosity, 0);
6574 assert!(config.tracing.disable_labels);
6575 assert_eq!(config.tracing.external_identification_timeout, 5);
6576
6577 Ok(())
6578 });
6579 }
6580
6581 #[test]
6582 fn test_label_aliases_preserve_provider_precedence() {
6583 let address = address!("0x0000000000000000000000000000000000000001");
6584 let labels = |label: &str| AddressHashMap::from_iter([(address, label.to_string())]);
6585 let provider = |global: &str, local: &str| {
6586 let figment = Config::merge_toml_provider(
6587 Figment::from(Config::default()),
6588 Toml::string(global).nested(),
6589 Config::DEFAULT_PROFILE,
6590 );
6591 Config::merge_toml_provider(
6592 figment,
6593 Toml::string(local).nested(),
6594 Config::DEFAULT_PROFILE,
6595 )
6596 };
6597
6598 let config = Config::from_provider(provider(
6599 r#"[tracing.labels]
6600 0x0000000000000000000000000000000000000001 = "global""#,
6601 r#"[labels]
6602 0x0000000000000000000000000000000000000001 = "local""#,
6603 ))
6604 .unwrap();
6605 assert_eq!(config.tracing.labels, labels("local"));
6606
6607 let config = Config::from_provider(provider(
6608 r#"[labels]
6609 0x0000000000000000000000000000000000000001 = "global""#,
6610 r#"[tracing.labels]
6611 0x0000000000000000000000000000000000000001 = "local""#,
6612 ))
6613 .unwrap();
6614 assert_eq!(config.tracing.labels, labels("local"));
6615 }
6616
6617 #[test]
6618 fn test_malformed_tracing_labels_are_not_replaced() {
6619 let provider = Toml::string(
6620 r#"
6621 [profile.default.labels]
6622 0x0000000000000000000000000000000000000001 = "legacy"
6623
6624 [profile.default.tracing]
6625 labels = "invalid"
6626 "#,
6627 )
6628 .nested();
6629
6630 assert!(Config::from_provider(provider).is_err());
6631 }
6632
6633 #[test]
6634 fn test_parse_vyper() {
6635 figment::Jail::expect_with(|jail| {
6636 jail.create_file(
6637 "foundry.toml",
6638 r#"
6639 [vyper]
6640 optimize = "O1"
6641 path = "/path/to/vyper"
6642 experimental_codegen = true
6643 venom_experimental = false
6644 debug = true
6645 enable_decimals = true
6646
6647 [vyper.venom]
6648 disable_inlining = true
6649 disable_cse = true
6650 disable_sccp = false
6651 disable_load_elimination = true
6652 disable_dead_store_elimination = false
6653 disable_algebraic_optimization = true
6654 disable_branch_optimization = false
6655 disable_assert_elimination = true
6656 disable_mem2var = false
6657 disable_simplify_cfg = true
6658 disable_remove_unused_variables = false
6659 inline_threshold = 15
6660 "#,
6661 )?;
6662
6663 let config = Config::load().unwrap();
6664 assert_eq!(
6665 config.vyper,
6666 VyperConfig {
6667 optimize: Some(VyperOptimizationMode::O1),
6668 opt_level: None,
6669 path: Some("/path/to/vyper".into()),
6670 experimental_codegen: Some(true),
6671 venom_experimental: Some(false),
6672 debug: Some(true),
6673 enable_decimals: Some(true),
6674 venom: Some(VyperVenomSettings {
6675 disable_inlining: Some(true),
6676 disable_cse: Some(true),
6677 disable_sccp: Some(false),
6678 disable_load_elimination: Some(true),
6679 disable_dead_store_elimination: Some(false),
6680 disable_algebraic_optimization: Some(true),
6681 disable_branch_optimization: Some(false),
6682 disable_assert_elimination: Some(true),
6683 disable_mem2var: Some(false),
6684 disable_simplify_cfg: Some(true),
6685 disable_remove_unused_variables: Some(false),
6686 inline_threshold: Some(15),
6687 }),
6688 }
6689 );
6690
6691 Ok(())
6692 });
6693 }
6694
6695 #[test]
6696 fn test_vyper_settings_include_extended_config() {
6697 let config = Config {
6698 vyper: VyperConfig {
6699 opt_level: Some(VyperOptimizationLevel::O3),
6700 experimental_codegen: Some(true),
6701 venom_experimental: Some(false),
6702 debug: Some(true),
6703 enable_decimals: Some(true),
6704 venom: Some(VyperVenomSettings {
6705 disable_cse: Some(true),
6706 inline_threshold: Some(15),
6707 ..Default::default()
6708 }),
6709 ..Default::default()
6710 },
6711 ..Config::default().normalized_optimizer_settings()
6712 };
6713
6714 let settings = config.vyper_settings().unwrap();
6715 assert_eq!(settings.optimize, None);
6716 assert_eq!(settings.opt_level, Some(VyperOptimizationLevel::O3));
6717 assert_eq!(settings.experimental_codegen, Some(true));
6718 assert_eq!(settings.venom_experimental, Some(false));
6719 assert_eq!(settings.debug, Some(true));
6720 assert_eq!(settings.enable_decimals, Some(true));
6721 assert_eq!(
6722 settings.venom,
6723 Some(VyperVenomSettings {
6724 disable_cse: Some(true),
6725 inline_threshold: Some(15),
6726 ..Default::default()
6727 })
6728 );
6729 }
6730
6731 #[test]
6732 fn vyper_opt_level_overrides_optimize() {
6733 let config = Config {
6734 vyper: VyperConfig {
6735 optimize: Some(VyperOptimizationMode::Gas),
6736 opt_level: Some(VyperOptimizationLevel::O3),
6737 ..Default::default()
6738 },
6739 ..Config::default().normalized_optimizer_settings()
6740 };
6741
6742 let settings = config.vyper_settings().unwrap();
6743 assert_eq!(settings.optimize, None);
6744 assert_eq!(settings.opt_level, Some(VyperOptimizationLevel::O3));
6745 }
6746
6747 #[test]
6748 fn test_parse_soldeer() {
6749 figment::Jail::expect_with(|jail| {
6750 jail.create_file(
6751 "foundry.toml",
6752 r#"
6753 [soldeer]
6754 remappings_generate = true
6755 remappings_regenerate = false
6756 remappings_version = true
6757 remappings_prefix = "@"
6758 remappings_location = "txt"
6759 recursive_deps = true
6760 "#,
6761 )?;
6762
6763 let config = Config::load().unwrap();
6764
6765 assert_eq!(
6766 config.soldeer,
6767 Some(SoldeerConfig {
6768 remappings_generate: true,
6769 remappings_regenerate: false,
6770 remappings_version: true,
6771 remappings_prefix: "@".to_string(),
6772 remappings_location: RemappingsLocation::Txt,
6773 recursive_deps: true,
6774 })
6775 );
6776
6777 Ok(())
6778 });
6779 }
6780
6781 #[test]
6783 fn test_resolve_mesc_by_chain_id() {
6784 let s = r#"{
6785 "mesc_version": "0.2.1",
6786 "default_endpoint": null,
6787 "endpoints": {
6788 "sophon_50104": {
6789 "name": "sophon_50104",
6790 "url": "https://rpc.sophon.xyz",
6791 "chain_id": "50104",
6792 "endpoint_metadata": {}
6793 }
6794 },
6795 "network_defaults": {
6796 },
6797 "network_names": {},
6798 "profiles": {
6799 "foundry": {
6800 "name": "foundry",
6801 "default_endpoint": "local_ethereum",
6802 "network_defaults": {
6803 "50104": "sophon_50104"
6804 },
6805 "profile_metadata": {},
6806 "use_mesc": true
6807 }
6808 },
6809 "global_metadata": {}
6810}"#;
6811
6812 let config = serde_json::from_str(s).unwrap();
6813 let endpoint = mesc::query::get_endpoint_by_network(&config, "50104", Some("foundry"))
6814 .unwrap()
6815 .unwrap();
6816 assert_eq!(endpoint.url, "https://rpc.sophon.xyz");
6817
6818 let s = r#"{
6819 "mesc_version": "0.2.1",
6820 "default_endpoint": null,
6821 "endpoints": {
6822 "sophon_50104": {
6823 "name": "sophon_50104",
6824 "url": "https://rpc.sophon.xyz",
6825 "chain_id": "50104",
6826 "endpoint_metadata": {}
6827 }
6828 },
6829 "network_defaults": {
6830 "50104": "sophon_50104"
6831 },
6832 "network_names": {},
6833 "profiles": {},
6834 "global_metadata": {}
6835}"#;
6836
6837 let config = serde_json::from_str(s).unwrap();
6838 let endpoint = mesc::query::get_endpoint_by_network(&config, "50104", Some("foundry"))
6839 .unwrap()
6840 .unwrap();
6841 assert_eq!(endpoint.url, "https://rpc.sophon.xyz");
6842 }
6843
6844 #[test]
6845 fn test_get_etherscan_config_with_unknown_chain() {
6846 figment::Jail::expect_with(|jail| {
6847 jail.create_file(
6848 "foundry.toml",
6849 r#"
6850 [etherscan]
6851 mainnet = { chain = 3658348, key = "api-key"}
6852 "#,
6853 )?;
6854 let config = Config::load().unwrap();
6855 let unknown_chain = Chain::from_id(3658348);
6856 let result = config.get_etherscan_config_with_chain(Some(unknown_chain));
6857 assert!(result.is_err());
6858 let error_msg = result.unwrap_err().to_string();
6859 assert!(error_msg.contains("No known Etherscan API URL for chain `3658348`"));
6860 assert!(error_msg.contains("Specify a `url`"));
6861 assert!(error_msg.contains("Verify the chain `3658348` is correct"));
6862
6863 Ok(())
6864 });
6865 }
6866
6867 #[test]
6868 fn test_get_etherscan_config_with_existing_chain_and_url() {
6869 figment::Jail::expect_with(|jail| {
6870 jail.create_file(
6871 "foundry.toml",
6872 r#"
6873 [etherscan]
6874 mainnet = { chain = 1, key = "api-key" }
6875 "#,
6876 )?;
6877 let config = Config::load().unwrap();
6878 let unknown_chain = Chain::from_id(1);
6879 let result = config.get_etherscan_config_with_chain(Some(unknown_chain));
6880 assert!(result.is_ok());
6881 Ok(())
6882 });
6883 }
6884
6885 #[test]
6886 fn test_can_inherit_a_base_toml() {
6887 figment::Jail::expect_with(|jail| {
6888 jail.create_file(
6890 "base-config.toml",
6891 r#"
6892 [profile.default]
6893 optimizer_runs = 800
6894
6895 [invariant]
6896 runs = 1000
6897
6898 [rpc_endpoints]
6899 mainnet = "https://example.com"
6900 optimism = "https://example-2.com/"
6901 "#,
6902 )?;
6903
6904 jail.create_file(
6906 "foundry.toml",
6907 r#"
6908 [profile.default]
6909 extends = "base-config.toml"
6910
6911 [invariant]
6912 runs = 333
6913 depth = 15
6914
6915 [rpc_endpoints]
6916 mainnet = "https://test.xyz/rpc"
6917 "#,
6918 )?;
6919
6920 let config = Config::load().unwrap();
6921 assert_eq!(config.extends, Some(Extends::Path("base-config.toml".to_string())));
6922
6923 assert_eq!(config.optimizer_runs, Some(800));
6925
6926 assert_eq!(config.invariant.runs, 333);
6928 assert_eq!(config.invariant.depth, 15);
6929
6930 let endpoints = config.rpc_endpoints.resolved();
6933 assert!(
6934 endpoints.get("mainnet").unwrap().url().unwrap().contains("https://test.xyz/rpc")
6935 );
6936 assert!(endpoints.get("optimism").unwrap().url().unwrap().contains("example-2.com"));
6937
6938 Ok(())
6939 });
6940 }
6941
6942 #[test]
6943 fn inherited_symbolic_sections_preserve_source_precedence() {
6944 figment::Jail::expect_with(|jail| {
6945 jail.create_file(
6946 "base.toml",
6947 r#"
6948 [profile.default.symbolic]
6949 max_paths = 10
6950 depth = 100
6951 "#,
6952 )?;
6953 jail.create_file(
6954 "foundry.toml",
6955 r#"
6956 [profile.default]
6957 extends = "base.toml"
6958
6959 [symbolic]
6960 max_paths = 20
6961 "#,
6962 )?;
6963
6964 let config = Config::load().unwrap();
6965 assert_eq!(config.symbolic.max_paths, 20);
6966 assert_eq!(config.symbolic.depth, Some(100));
6967
6968 jail.create_file(
6969 "base.toml",
6970 r#"
6971 [symbolic]
6972 max_paths = 30
6973 depth = 200
6974 "#,
6975 )?;
6976 jail.create_file(
6977 "foundry.toml",
6978 r#"
6979 [profile.default]
6980 extends = "base.toml"
6981
6982 [profile.default.symbolic]
6983 max_paths = 40
6984 "#,
6985 )?;
6986
6987 let config = Config::load().unwrap();
6988 assert_eq!(config.symbolic.max_paths, 40);
6989 assert_eq!(config.symbolic.depth, Some(200));
6990
6991 Ok(())
6992 });
6993 }
6994
6995 #[test]
6996 fn inherited_symbolic_sections_detect_effective_collisions() {
6997 figment::Jail::expect_with(|jail| {
6998 jail.create_file(
6999 "base.toml",
7000 r#"
7001 [profile.default.symbolic]
7002 max_paths = 10
7003 "#,
7004 )?;
7005 jail.create_file(
7006 "foundry.toml",
7007 r#"
7008 [profile.default]
7009 extends = { path = "base.toml", strategy = "no-collision" }
7010
7011 [symbolic]
7012 max_paths = 20
7013 "#,
7014 )?;
7015
7016 let err = Config::load().unwrap_err().to_string();
7017 assert!(err.contains("Key collision detected"), "unexpected error: {err}");
7018 assert!(err.contains("symbolic"), "unexpected error: {err}");
7019
7020 Ok(())
7021 });
7022 }
7023
7024 #[test]
7025 fn inherited_label_aliases_preserve_source_precedence() {
7026 figment::Jail::expect_with(|jail| {
7027 let address = address!("0x0000000000000000000000000000000000000001");
7028
7029 jail.create_file(
7030 "base.toml",
7031 r#"
7032 [profile.default.tracing.labels]
7033 0x0000000000000000000000000000000000000001 = "base"
7034 "#,
7035 )?;
7036 jail.create_file(
7037 "foundry.toml",
7038 r#"
7039 [profile.default]
7040 extends = "base.toml"
7041
7042 [profile.default.labels]
7043 0x0000000000000000000000000000000000000001 = "local"
7044 "#,
7045 )?;
7046
7047 let config = Config::load().unwrap();
7048 assert_eq!(config.tracing.labels.get(&address).map(String::as_str), Some("local"));
7049
7050 jail.create_file(
7051 "base.toml",
7052 r#"
7053 [profile.default.labels]
7054 0x0000000000000000000000000000000000000001 = "base"
7055 "#,
7056 )?;
7057 jail.create_file(
7058 "foundry.toml",
7059 r#"
7060 [profile.default]
7061 extends = "base.toml"
7062
7063 [profile.default.tracing.labels]
7064 0x0000000000000000000000000000000000000001 = "local"
7065 "#,
7066 )?;
7067
7068 let config = Config::load().unwrap();
7069 assert_eq!(config.tracing.labels.get(&address).map(String::as_str), Some("local"));
7070
7071 jail.create_file(
7072 "base.toml",
7073 r#"
7074 [profile.default.tracing.labels]
7075 0x0000000000000000000000000000000000000001 = "base"
7076 "#,
7077 )?;
7078 jail.create_file(
7079 "foundry.toml",
7080 r#"
7081 [profile.default]
7082 extends = "base.toml"
7083
7084 [labels]
7085 0x0000000000000000000000000000000000000001 = "local"
7086 "#,
7087 )?;
7088
7089 let config = Config::load().unwrap();
7090 assert_eq!(config.tracing.labels.get(&address).map(String::as_str), Some("local"));
7091 assert_eq!(
7092 config.warnings,
7093 vec![Warning::DeprecatedKey {
7094 old: "[labels]".to_string(),
7095 new: "[tracing.labels]".to_string(),
7096 }]
7097 );
7098
7099 jail.create_file(
7100 "base.toml",
7101 r#"
7102 [labels]
7103 0x0000000000000000000000000000000000000001 = "base"
7104 "#,
7105 )?;
7106 jail.create_file(
7107 "foundry.toml",
7108 r#"
7109 [profile.default]
7110 extends = "base.toml"
7111
7112 [profile.default.tracing.labels]
7113 0x0000000000000000000000000000000000000001 = "local"
7114 "#,
7115 )?;
7116
7117 let config = Config::load().unwrap();
7118 assert_eq!(config.tracing.labels.get(&address).map(String::as_str), Some("local"));
7119
7120 Ok(())
7121 });
7122 }
7123
7124 #[test]
7125 fn inherited_label_aliases_detect_effective_collisions() {
7126 figment::Jail::expect_with(|jail| {
7127 jail.create_file(
7128 "base.toml",
7129 r#"
7130 [profile.default.tracing.labels]
7131 0x0000000000000000000000000000000000000001 = "base"
7132 "#,
7133 )?;
7134 jail.create_file(
7135 "foundry.toml",
7136 r#"
7137 [profile.default]
7138 extends = { path = "base.toml", strategy = "no-collision" }
7139
7140 [labels]
7141 0x0000000000000000000000000000000000000001 = "local"
7142 "#,
7143 )?;
7144
7145 let err = Config::load().unwrap_err().to_string();
7146 assert_eq!(
7147 err,
7148 "failed to extract foundry config:\n\
7149 foundry config error: Key collision detected in profile 'default' when extending \
7150 'base.toml'. Conflicting keys: [\"tracing\"]. Use 'extends.strategy' or \
7151 'extends_strategy' to specify how to handle conflicts.\n"
7152 );
7153
7154 Ok(())
7155 });
7156 }
7157
7158 #[test]
7159 fn inherited_fuzz_section_remains_invariant_fallback() {
7160 figment::Jail::expect_with(|jail| {
7161 jail.create_file(
7162 "base.toml",
7163 r#"
7164 [fuzz]
7165 include_storage = false
7166 dictionary_weight = 99
7167 "#,
7168 )?;
7169 jail.create_file(
7170 "foundry.toml",
7171 r#"
7172 [profile.default]
7173 extends = "base.toml"
7174
7175 [invariant]
7176 runs = 420
7177 "#,
7178 )?;
7179
7180 let config = Config::load().unwrap();
7181 assert_eq!(config.invariant.runs, 420);
7182 assert!(!config.invariant.dictionary.include_storage);
7183 assert_eq!(config.invariant.dictionary.dictionary_weight, 99);
7184
7185 Ok(())
7186 });
7187 }
7188
7189 #[test]
7190 fn test_inheritance_validation() {
7191 figment::Jail::expect_with(|jail| {
7192 jail.create_file(
7194 "base-with-inherit.toml",
7195 r#"
7196 [profile.default]
7197 extends = "another.toml"
7198 optimizer_runs = 800
7199 "#,
7200 )?;
7201
7202 jail.create_file(
7203 "foundry.toml",
7204 r#"
7205 [profile.default]
7206 extends = "base-with-inherit.toml"
7207 "#,
7208 )?;
7209
7210 let result = Config::load();
7212 assert!(result.is_err());
7213 assert!(result.unwrap_err().to_string().contains("Nested inheritance is not allowed"));
7214
7215 jail.create_file(
7217 "foundry.toml",
7218 r#"
7219 [profile.default]
7220 extends = "foundry.toml"
7221 "#,
7222 )?;
7223
7224 let result = Config::load();
7225 assert!(result.is_err());
7226 assert!(result.unwrap_err().to_string().contains("cannot inherit from itself"));
7227
7228 jail.create_file(
7230 "foundry.toml",
7231 r#"
7232 [profile.default]
7233 extends = "non-existent.toml"
7234 "#,
7235 )?;
7236
7237 let result = Config::load();
7238 assert!(result.is_err());
7239 let err_msg = result.unwrap_err().to_string();
7240 assert!(
7241 err_msg.contains("does not exist")
7242 || err_msg.contains("Failed to resolve inherited config path"),
7243 "Error message: {err_msg}"
7244 );
7245
7246 Ok(())
7247 });
7248 }
7249
7250 #[test]
7251 fn test_complex_inheritance_merging() {
7252 figment::Jail::expect_with(|jail| {
7253 jail.create_file(
7255 "base.toml",
7256 r#"
7257 [profile.default]
7258 optimizer = true
7259 optimizer_runs = 1000
7260 via_ir = false
7261 solc = "0.8.19"
7262
7263 [invariant]
7264 runs = 500
7265 depth = 100
7266
7267 [fuzz]
7268 runs = 256
7269 seed = "0x123"
7270
7271 [rpc_endpoints]
7272 mainnet = "https://base-mainnet.com"
7273 optimism = "https://base-optimism.com"
7274 arbitrum = "https://base-arbitrum.com"
7275 "#,
7276 )?;
7277
7278 jail.create_file(
7280 "foundry.toml",
7281 r#"
7282 [profile.default]
7283 extends = "base.toml"
7284 optimizer_runs = 200 # Override
7285 via_ir = true # Override
7286 # optimizer and solc are inherited
7287
7288 [invariant]
7289 runs = 333 # Override
7290 # depth is inherited
7291
7292 # fuzz section is fully inherited
7293
7294 [rpc_endpoints]
7295 mainnet = "https://local-mainnet.com" # Override
7296 # optimism and arbitrum are inherited
7297 polygon = "https://local-polygon.com" # New
7298 "#,
7299 )?;
7300
7301 let config = Config::load().unwrap();
7302
7303 assert_eq!(config.optimizer, Some(true));
7305 assert_eq!(config.optimizer_runs, Some(200));
7306 assert_eq!(config.via_ir, true);
7307 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 19))));
7308
7309 assert_eq!(config.invariant.runs, 333);
7311 assert_eq!(config.invariant.depth, 100);
7312
7313 assert_eq!(config.fuzz.runs, 256);
7315 assert_eq!(config.fuzz.seed, Some(U256::from(0x123)));
7316
7317 let endpoints = config.rpc_endpoints.resolved();
7319 assert!(endpoints.get("mainnet").unwrap().url().unwrap().contains("local-mainnet"));
7320 assert!(endpoints.get("optimism").unwrap().url().unwrap().contains("base-optimism"));
7321 assert!(endpoints.get("arbitrum").unwrap().url().unwrap().contains("base-arbitrum"));
7322 assert!(endpoints.get("polygon").unwrap().url().unwrap().contains("local-polygon"));
7323
7324 Ok(())
7325 });
7326 }
7327
7328 #[test]
7329 fn test_inheritance_with_different_profiles() {
7330 figment::Jail::expect_with(|jail| {
7331 jail.create_file(
7333 "base.toml",
7334 r#"
7335 [profile.default]
7336 optimizer = true
7337 optimizer_runs = 200
7338
7339 [profile.ci]
7340 optimizer = true
7341 optimizer_runs = 10000
7342 via_ir = true
7343
7344 [profile.dev]
7345 optimizer = false
7346 "#,
7347 )?;
7348
7349 jail.create_file(
7351 "foundry.toml",
7352 r#"
7353 [profile.default]
7354 extends = "base.toml"
7355 verbosity = 3
7356
7357 [profile.ci]
7358 optimizer_runs = 5000 # This doesn't inherit from base.toml's ci profile
7359 "#,
7360 )?;
7361
7362 let config = Config::load().unwrap();
7364 assert_eq!(config.optimizer, Some(true));
7365 assert_eq!(config.optimizer_runs, Some(200));
7366 assert_eq!(config.verbosity, 3);
7367
7368 jail.set_env("FOUNDRY_PROFILE", "ci");
7370 let config = Config::load().unwrap();
7371 assert_eq!(config.optimizer_runs, Some(5000));
7372 assert_eq!(config.optimizer, Some(true));
7373 assert_eq!(config.via_ir, false);
7375
7376 Ok(())
7377 });
7378 }
7379
7380 #[test]
7381 fn test_inheritance_with_env_vars() {
7382 figment::Jail::expect_with(|jail| {
7383 jail.create_file(
7384 "base.toml",
7385 r#"
7386 [profile.default]
7387 optimizer_runs = 500
7388 sender = "0x0000000000000000000000000000000000000001"
7389 verbosity = 1
7390 "#,
7391 )?;
7392
7393 jail.create_file(
7394 "foundry.toml",
7395 r#"
7396 [profile.default]
7397 extends = "base.toml"
7398 verbosity = 2
7399 "#,
7400 )?;
7401
7402 jail.set_env("FOUNDRY_OPTIMIZER_RUNS", "999");
7404 jail.set_env("FOUNDRY_VERBOSITY", "4");
7405
7406 let config = Config::load().unwrap();
7407 assert_eq!(config.optimizer_runs, Some(999));
7408 assert_eq!(config.verbosity, 4);
7409 assert_eq!(
7410 config.sender,
7411 "0x0000000000000000000000000000000000000001"
7412 .parse::<alloy_primitives::Address>()
7413 .unwrap()
7414 );
7415
7416 Ok(())
7417 });
7418 }
7419
7420 #[test]
7421 fn test_inheritance_with_subdirectories() {
7422 figment::Jail::expect_with(|jail| {
7423 jail.create_dir("configs")?;
7425 jail.create_file(
7426 "configs/base.toml",
7427 r#"
7428 [profile.default]
7429 optimizer_runs = 800
7430 src = "contracts"
7431 "#,
7432 )?;
7433
7434 jail.create_file(
7436 "foundry.toml",
7437 r#"
7438 [profile.default]
7439 extends = "configs/base.toml"
7440 test = "tests"
7441 "#,
7442 )?;
7443
7444 let config = Config::load().unwrap();
7445 assert_eq!(config.optimizer_runs, Some(800));
7446 assert_eq!(config.src, PathBuf::from("contracts"));
7447 assert_eq!(config.test, PathBuf::from("tests"));
7448
7449 jail.create_dir("project")?;
7451 jail.create_file(
7452 "shared-base.toml",
7453 r#"
7454 [profile.default]
7455 optimizer_runs = 1500
7456 "#,
7457 )?;
7458
7459 jail.create_file(
7460 "project/foundry.toml",
7461 r#"
7462 [profile.default]
7463 extends = "../shared-base.toml"
7464 "#,
7465 )?;
7466
7467 std::env::set_current_dir(jail.directory().join("project")).unwrap();
7468 let config = Config::load().unwrap();
7469 assert_eq!(config.optimizer_runs, Some(1500));
7470
7471 Ok(())
7472 });
7473 }
7474
7475 #[test]
7476 fn test_inheritance_with_empty_files() {
7477 figment::Jail::expect_with(|jail| {
7478 jail.create_file(
7480 "base.toml",
7481 r#"
7482 [profile.default]
7483 "#,
7484 )?;
7485
7486 jail.create_file(
7487 "foundry.toml",
7488 r#"
7489 [profile.default]
7490 extends = "base.toml"
7491 optimizer_runs = 300
7492 "#,
7493 )?;
7494
7495 let config = Config::load().unwrap();
7496 assert_eq!(config.optimizer_runs, Some(300));
7497
7498 jail.create_file(
7500 "base2.toml",
7501 r#"
7502 [profile.default]
7503 optimizer_runs = 400
7504 via_ir = true
7505 "#,
7506 )?;
7507
7508 jail.create_file(
7509 "foundry.toml",
7510 r#"
7511 [profile.default]
7512 extends = "base2.toml"
7513 "#,
7514 )?;
7515
7516 let config = Config::load().unwrap();
7517 assert_eq!(config.optimizer_runs, Some(400));
7518 assert!(config.via_ir);
7519
7520 Ok(())
7521 });
7522 }
7523
7524 #[test]
7525 fn test_inheritance_array_and_table_merging() {
7526 figment::Jail::expect_with(|jail| {
7527 jail.create_file(
7528 "base.toml",
7529 r#"
7530 [profile.default]
7531 libs = ["lib", "node_modules"]
7532 ignored_error_codes = [5667, 1878]
7533 extra_output = ["metadata", "ir"]
7534
7535 [profile.default.model_checker]
7536 engine = "chc"
7537 timeout = 10000
7538 targets = ["assert"]
7539
7540 [profile.default.optimizer_details]
7541 peephole = true
7542 inliner = true
7543 "#,
7544 )?;
7545
7546 jail.create_file(
7547 "foundry.toml",
7548 r#"
7549 [profile.default]
7550 extends = "base.toml"
7551 libs = ["custom-lib"] # Concatenates with base array
7552 ignored_error_codes = [2018] # Concatenates with base array
7553
7554 [profile.default.model_checker]
7555 timeout = 5000 # Overrides base value
7556 # engine and targets are inherited
7557
7558 [profile.default.optimizer_details]
7559 jumpdest_remover = true # Adds new field
7560 # peephole and inliner are inherited
7561 "#,
7562 )?;
7563
7564 let config = Config::load().unwrap();
7565
7566 assert_eq!(
7568 config.libs,
7569 vec![
7570 PathBuf::from("lib"),
7571 PathBuf::from("node_modules"),
7572 PathBuf::from("custom-lib")
7573 ]
7574 );
7575 assert_eq!(
7576 config.ignored_error_codes,
7577 vec![
7578 SolidityErrorCode::UnusedFunctionParameter, SolidityErrorCode::SpdxLicenseNotProvided, SolidityErrorCode::FunctionStateMutabilityCanBeRestricted ]
7582 );
7583
7584 assert_eq!(config.model_checker.as_ref().unwrap().timeout, Some(5000));
7586 assert_eq!(
7587 config.model_checker.as_ref().unwrap().engine,
7588 Some(ModelCheckerEngine::CHC)
7589 );
7590 assert_eq!(
7591 config.model_checker.as_ref().unwrap().targets,
7592 Some(vec![ModelCheckerTarget::Assert])
7593 );
7594
7595 assert_eq!(config.optimizer_details.as_ref().unwrap().peephole, Some(true));
7597 assert_eq!(config.optimizer_details.as_ref().unwrap().inliner, Some(true));
7598 assert_eq!(config.optimizer_details.as_ref().unwrap().jumpdest_remover, None);
7599
7600 Ok(())
7601 });
7602 }
7603
7604 #[test]
7605 fn test_inheritance_with_special_sections() {
7606 figment::Jail::expect_with(|jail| {
7607 jail.create_file(
7608 "base.toml",
7609 r#"
7610 [profile.default]
7611 # Base file should not have 'extends' to avoid nested inheritance
7612
7613 [labels]
7614 "0x0000000000000000000000000000000000000001" = "Alice"
7615 "0x0000000000000000000000000000000000000002" = "Bob"
7616
7617 [[profile.default.fs_permissions]]
7618 access = "read"
7619 path = "./src"
7620
7621 [[profile.default.fs_permissions]]
7622 access = "read-write"
7623 path = "./cache"
7624 "#,
7625 )?;
7626
7627 jail.create_file(
7628 "foundry.toml",
7629 r#"
7630 [profile.default]
7631 extends = "base.toml"
7632
7633 [labels]
7634 "0x0000000000000000000000000000000000000002" = "Bob Updated"
7635 "0x0000000000000000000000000000000000000003" = "Charlie"
7636
7637 [[profile.default.fs_permissions]]
7638 access = "read"
7639 path = "./test"
7640 "#,
7641 )?;
7642
7643 let config = Config::load().unwrap();
7644
7645 assert_eq!(
7647 config.labels.get(
7648 &"0x0000000000000000000000000000000000000001"
7649 .parse::<alloy_primitives::Address>()
7650 .unwrap()
7651 ),
7652 Some(&"Alice".to_string())
7653 );
7654 assert_eq!(
7655 config.labels.get(
7656 &"0x0000000000000000000000000000000000000002"
7657 .parse::<alloy_primitives::Address>()
7658 .unwrap()
7659 ),
7660 Some(&"Bob Updated".to_string())
7661 );
7662 assert_eq!(
7663 config.labels.get(
7664 &"0x0000000000000000000000000000000000000003"
7665 .parse::<alloy_primitives::Address>()
7666 .unwrap()
7667 ),
7668 Some(&"Charlie".to_string())
7669 );
7670
7671 assert_eq!(config.fs_permissions.permissions.len(), 3); assert!(
7675 config
7676 .fs_permissions
7677 .permissions
7678 .iter()
7679 .any(|p| p.path.to_str().unwrap() == "./src")
7680 );
7681 assert!(
7682 config
7683 .fs_permissions
7684 .permissions
7685 .iter()
7686 .any(|p| p.path.to_str().unwrap() == "./cache")
7687 );
7688 assert!(
7689 config
7690 .fs_permissions
7691 .permissions
7692 .iter()
7693 .any(|p| p.path.to_str().unwrap() == "./test")
7694 );
7695
7696 Ok(())
7697 });
7698 }
7699
7700 #[test]
7701 fn test_inheritance_with_compilation_settings() {
7702 figment::Jail::expect_with(|jail| {
7703 jail.create_file(
7704 "base.toml",
7705 r#"
7706 [profile.default]
7707 solc = "0.8.19"
7708 evm_version = "paris"
7709 via_ir = false
7710 optimizer = true
7711 optimizer_runs = 200
7712
7713 [profile.default.optimizer_details]
7714 peephole = true
7715 inliner = false
7716 jumpdest_remover = true
7717 order_literals = false
7718 deduplicate = true
7719 cse = true
7720 constant_optimizer = true
7721 yul = true
7722
7723 [profile.default.optimizer_details.yul_details]
7724 stack_allocation = true
7725 optimizer_steps = "dhfoDgvulfnTUtnIf"
7726 "#,
7727 )?;
7728
7729 jail.create_file(
7730 "foundry.toml",
7731 r#"
7732 [profile.default]
7733 extends = "base.toml"
7734 evm_version = "shanghai" # Override
7735 optimizer_runs = 1000 # Override
7736
7737 [profile.default.optimizer_details]
7738 inliner = true # Override
7739 # Rest inherited
7740 "#,
7741 )?;
7742
7743 let config = Config::load().unwrap();
7744
7745 assert_eq!(config.solc, Some(SolcReq::Version(Version::new(0, 8, 19))));
7747 assert_eq!(config.evm_version, EvmVersion::Shanghai);
7748 assert_eq!(config.via_ir, false);
7749 assert_eq!(config.optimizer, Some(true));
7750 assert_eq!(config.optimizer_runs, Some(1000));
7751
7752 let details = config.optimizer_details.as_ref().unwrap();
7754 assert_eq!(details.peephole, Some(true));
7755 assert_eq!(details.inliner, Some(true));
7756 assert_eq!(details.jumpdest_remover, None);
7757 assert_eq!(details.order_literals, None);
7758 assert_eq!(details.deduplicate, Some(true));
7759 assert_eq!(details.cse, Some(true));
7760 assert_eq!(details.constant_optimizer, None);
7761 assert_eq!(details.yul, Some(true));
7762
7763 if let Some(yul_details) = details.yul_details.as_ref() {
7765 assert_eq!(yul_details.stack_allocation, Some(true));
7766 assert_eq!(yul_details.optimizer_steps, Some("dhfoDgvulfnTUtnIf".to_string()));
7767 }
7768
7769 Ok(())
7770 });
7771 }
7772
7773 #[test]
7774 fn test_inheritance_with_remappings() {
7775 figment::Jail::expect_with(|jail| {
7776 jail.create_file(
7777 "base.toml",
7778 r#"
7779 [profile.default]
7780 remappings = [
7781 "forge-std/=lib/forge-std/src/",
7782 "@openzeppelin/=lib/openzeppelin-contracts/",
7783 "ds-test/=lib/ds-test/src/"
7784 ]
7785 auto_detect_remappings = false
7786 "#,
7787 )?;
7788
7789 jail.create_file(
7790 "foundry.toml",
7791 r#"
7792 [profile.default]
7793 extends = "base.toml"
7794 remappings = [
7795 "@custom/=lib/custom/",
7796 "ds-test/=lib/forge-std/lib/ds-test/src/" # Note: This will be added alongside base remappings
7797 ]
7798 "#,
7799 )?;
7800
7801 let config = Config::load().unwrap();
7802
7803 assert!(config.remappings.iter().any(|r| r.to_string().contains("@custom/")));
7805 assert!(config.remappings.iter().any(|r| r.to_string().contains("ds-test/")));
7806 assert!(config.remappings.iter().any(|r| r.to_string().contains("forge-std/")));
7807 assert!(config.remappings.iter().any(|r| r.to_string().contains("@openzeppelin/")));
7808
7809 assert!(!config.auto_detect_remappings);
7811
7812 Ok(())
7813 });
7814 }
7815
7816 #[test]
7817 fn test_inheritance_with_multiple_profiles_and_single_file() {
7818 figment::Jail::expect_with(|jail| {
7819 jail.create_file(
7821 "base.toml",
7822 r#"
7823 [profile.prod]
7824 optimizer = true
7825 optimizer_runs = 10000
7826 via_ir = true
7827
7828 [profile.test]
7829 optimizer = false
7830
7831 [profile.test.fuzz]
7832 runs = 100
7833 "#,
7834 )?;
7835
7836 jail.create_file(
7838 "foundry.toml",
7839 r#"
7840 [profile.prod]
7841 extends = "base.toml"
7842 evm_version = "shanghai" # Additional setting
7843
7844 [profile.test]
7845 extends = "base.toml"
7846
7847 [profile.test.fuzz]
7848 runs = 500 # Override
7849 "#,
7850 )?;
7851
7852 jail.set_env("FOUNDRY_PROFILE", "prod");
7854 let config = Config::load().unwrap();
7855 assert_eq!(config.optimizer, Some(true));
7856 assert_eq!(config.optimizer_runs, Some(10000));
7857 assert_eq!(config.via_ir, true);
7858 assert_eq!(config.evm_version, EvmVersion::Shanghai);
7859
7860 jail.set_env("FOUNDRY_PROFILE", "test");
7862 let config = Config::load().unwrap();
7863 assert_eq!(config.optimizer, Some(false));
7864 assert_eq!(config.fuzz.runs, 500);
7865
7866 Ok(())
7867 });
7868 }
7869
7870 #[test]
7871 fn test_inheritance_with_multiple_profiles_and_files() {
7872 figment::Jail::expect_with(|jail| {
7873 jail.create_file(
7874 "prod.toml",
7875 r#"
7876 [profile.prod]
7877 optimizer = true
7878 optimizer_runs = 20000
7879 gas_limit = 50000000
7880 "#,
7881 )?;
7882 jail.create_file(
7883 "dev.toml",
7884 r#"
7885 [profile.dev]
7886 optimizer = true
7887 optimizer_runs = 333
7888 gas_limit = 555555
7889 "#,
7890 )?;
7891
7892 jail.create_file(
7894 "foundry.toml",
7895 r#"
7896 [profile.dev]
7897 extends = "dev.toml"
7898 sender = "0x0000000000000000000000000000000000000001"
7899
7900 [profile.prod]
7901 extends = "prod.toml"
7902 sender = "0x0000000000000000000000000000000000000002"
7903 "#,
7904 )?;
7905
7906 jail.set_env("FOUNDRY_PROFILE", "dev");
7908 let config = Config::load().unwrap();
7909 assert_eq!(config.optimizer, Some(true));
7910 assert_eq!(config.optimizer_runs, Some(333));
7911 assert_eq!(config.gas_limit, 555555.into());
7912 assert_eq!(
7913 config.sender,
7914 "0x0000000000000000000000000000000000000001"
7915 .parse::<alloy_primitives::Address>()
7916 .unwrap()
7917 );
7918
7919 jail.set_env("FOUNDRY_PROFILE", "prod");
7921 let config = Config::load().unwrap();
7922 assert_eq!(config.optimizer, Some(true));
7923 assert_eq!(config.optimizer_runs, Some(20000));
7924 assert_eq!(config.gas_limit, 50000000.into());
7925 assert_eq!(
7926 config.sender,
7927 "0x0000000000000000000000000000000000000002"
7928 .parse::<alloy_primitives::Address>()
7929 .unwrap()
7930 );
7931
7932 Ok(())
7933 });
7934 }
7935
7936 #[test]
7937 fn test_extends_strategy_extend_arrays() {
7938 figment::Jail::expect_with(|jail| {
7939 jail.create_file(
7941 "base.toml",
7942 r#"
7943 [profile.default]
7944 libs = ["lib", "node_modules"]
7945 ignored_error_codes = [5667, 1878]
7946 optimizer_runs = 200
7947 "#,
7948 )?;
7949
7950 jail.create_file(
7952 "foundry.toml",
7953 r#"
7954 [profile.default]
7955 extends = "base.toml"
7956 libs = ["mylib", "customlib"]
7957 ignored_error_codes = [1234]
7958 optimizer_runs = 500
7959 "#,
7960 )?;
7961
7962 let config = Config::load().unwrap();
7963
7964 assert_eq!(config.libs.len(), 4);
7966 assert!(config.libs.iter().any(|l| l.to_str() == Some("lib")));
7967 assert!(config.libs.iter().any(|l| l.to_str() == Some("node_modules")));
7968 assert!(config.libs.iter().any(|l| l.to_str() == Some("mylib")));
7969 assert!(config.libs.iter().any(|l| l.to_str() == Some("customlib")));
7970
7971 assert_eq!(config.ignored_error_codes.len(), 3);
7972 assert!(
7973 config.ignored_error_codes.contains(&SolidityErrorCode::UnusedFunctionParameter)
7974 ); assert!(
7976 config.ignored_error_codes.contains(&SolidityErrorCode::SpdxLicenseNotProvided)
7977 ); assert!(config.ignored_error_codes.contains(&SolidityErrorCode::from(1234u64))); assert_eq!(config.optimizer_runs, Some(500));
7982
7983 Ok(())
7984 });
7985 }
7986
7987 #[test]
7988 fn test_extends_strategy_replace_arrays() {
7989 figment::Jail::expect_with(|jail| {
7990 jail.create_file(
7992 "base.toml",
7993 r#"
7994 [profile.default]
7995 libs = ["lib", "node_modules"]
7996 ignored_error_codes = [5667, 1878]
7997 optimizer_runs = 200
7998 "#,
7999 )?;
8000
8001 jail.create_file(
8003 "foundry.toml",
8004 r#"
8005 [profile.default]
8006 extends = { path = "base.toml", strategy = "replace-arrays" }
8007 libs = ["mylib", "customlib"]
8008 ignored_error_codes = [1234]
8009 optimizer_runs = 500
8010 "#,
8011 )?;
8012
8013 let config = Config::load().unwrap();
8014
8015 assert_eq!(config.libs.len(), 2);
8017 assert!(config.libs.iter().any(|l| l.to_str() == Some("mylib")));
8018 assert!(config.libs.iter().any(|l| l.to_str() == Some("customlib")));
8019 assert!(!config.libs.iter().any(|l| l.to_str() == Some("lib")));
8020 assert!(!config.libs.iter().any(|l| l.to_str() == Some("node_modules")));
8021
8022 assert_eq!(config.ignored_error_codes.len(), 1);
8023 assert!(config.ignored_error_codes.contains(&SolidityErrorCode::from(1234u64))); assert!(
8025 !config.ignored_error_codes.contains(&SolidityErrorCode::UnusedFunctionParameter)
8026 ); assert_eq!(config.optimizer_runs, Some(500));
8030
8031 Ok(())
8032 });
8033 }
8034
8035 #[test]
8036 fn test_extends_strategy_no_collision_success() {
8037 figment::Jail::expect_with(|jail| {
8038 jail.create_file(
8040 "base.toml",
8041 r#"
8042 [profile.default]
8043 optimizer = true
8044 optimizer_runs = 200
8045 src = "src"
8046 "#,
8047 )?;
8048
8049 jail.create_file(
8051 "foundry.toml",
8052 r#"
8053 [profile.default]
8054 extends = { path = "base.toml", strategy = "no-collision" }
8055 test = "tests"
8056 libs = ["lib"]
8057 "#,
8058 )?;
8059
8060 let config = Config::load().unwrap();
8061
8062 assert_eq!(config.optimizer, Some(true));
8064 assert_eq!(config.optimizer_runs, Some(200));
8065 assert_eq!(config.src, PathBuf::from("src"));
8066
8067 assert_eq!(config.test, PathBuf::from("tests"));
8069 assert_eq!(config.libs.len(), 1);
8070 assert!(config.libs.iter().any(|l| l.to_str() == Some("lib")));
8071
8072 Ok(())
8073 });
8074 }
8075
8076 #[test]
8077 fn test_extends_strategy_no_collision_error() {
8078 figment::Jail::expect_with(|jail| {
8079 jail.create_file(
8081 "base.toml",
8082 r#"
8083 [profile.default]
8084 optimizer = true
8085 optimizer_runs = 200
8086 libs = ["lib", "node_modules"]
8087 "#,
8088 )?;
8089
8090 jail.create_file(
8092 "foundry.toml",
8093 r#"
8094 [profile.default]
8095 extends = { path = "base.toml", strategy = "no-collision" }
8096 optimizer_runs = 500
8097 libs = ["mylib"]
8098 "#,
8099 )?;
8100
8101 let result = Config::load();
8103
8104 if let Ok(config) = result {
8105 panic!(
8106 "Expected error but got config with optimizer_runs: {:?}, libs: {:?}",
8107 config.optimizer_runs, config.libs
8108 );
8109 }
8110
8111 let err = result.unwrap_err();
8112 let err_str = err.to_string();
8113 assert!(
8114 err_str.contains("Key collision detected") || err_str.contains("collision"),
8115 "Error message doesn't mention collision: {err_str}"
8116 );
8117
8118 Ok(())
8119 });
8120 }
8121
8122 #[test]
8123 fn test_extends_both_syntaxes() {
8124 figment::Jail::expect_with(|jail| {
8125 jail.create_file(
8127 "base.toml",
8128 r#"
8129 [profile.default]
8130 libs = ["lib"]
8131 optimizer = true
8132 "#,
8133 )?;
8134
8135 jail.create_file(
8137 "foundry_string.toml",
8138 r#"
8139 [profile.default]
8140 extends = "base.toml"
8141 libs = ["custom"]
8142 "#,
8143 )?;
8144
8145 jail.create_file(
8147 "foundry_object.toml",
8148 r#"
8149 [profile.default]
8150 extends = { path = "base.toml", strategy = "replace-arrays" }
8151 libs = ["custom"]
8152 "#,
8153 )?;
8154
8155 jail.set_env("FOUNDRY_CONFIG", "foundry_string.toml");
8157 let config = Config::load().unwrap();
8158 assert_eq!(config.libs.len(), 2); assert!(config.libs.iter().any(|l| l.to_str() == Some("lib")));
8160 assert!(config.libs.iter().any(|l| l.to_str() == Some("custom")));
8161
8162 jail.set_env("FOUNDRY_CONFIG", "foundry_object.toml");
8164 let config = Config::load().unwrap();
8165 assert_eq!(config.libs.len(), 1); assert!(config.libs.iter().any(|l| l.to_str() == Some("custom")));
8167 assert!(!config.libs.iter().any(|l| l.to_str() == Some("lib")));
8168
8169 Ok(())
8170 });
8171 }
8172
8173 #[test]
8174 fn test_extends_strategy_default_is_extend_arrays() {
8175 figment::Jail::expect_with(|jail| {
8176 jail.create_file(
8178 "base.toml",
8179 r#"
8180 [profile.default]
8181 libs = ["lib", "node_modules"]
8182 optimizer = true
8183 "#,
8184 )?;
8185
8186 jail.create_file(
8188 "foundry.toml",
8189 r#"
8190 [profile.default]
8191 extends = "base.toml"
8192 libs = ["custom"]
8193 optimizer = false
8194 "#,
8195 )?;
8196
8197 let config = Config::load().unwrap();
8199
8200 assert_eq!(config.libs.len(), 3);
8202 assert!(config.libs.iter().any(|l| l.to_str() == Some("lib")));
8203 assert!(config.libs.iter().any(|l| l.to_str() == Some("node_modules")));
8204 assert!(config.libs.iter().any(|l| l.to_str() == Some("custom")));
8205
8206 assert_eq!(config.optimizer, Some(false));
8208
8209 Ok(())
8210 });
8211 }
8212
8213 #[test]
8214 fn test_deprecated_deny_warnings_is_handled() {
8215 figment::Jail::expect_with(|jail| {
8216 jail.create_file(
8217 "foundry.toml",
8218 r#"
8219 [profile.default]
8220 deny_warnings = true
8221 "#,
8222 )?;
8223 let config = Config::load().unwrap();
8224
8225 assert_eq!(config.deny, DenyLevel::Warnings);
8227 Ok(())
8228 });
8229 }
8230
8231 #[test]
8232 fn warns_on_deprecated_keys_in_inactive_profiles() {
8233 figment::Jail::expect_with(|jail| {
8234 jail.create_file(
8235 "foundry.toml",
8236 r#"
8237 [profile.default]
8238 src = "src"
8239
8240 [profile.ci]
8241 deny_warnings = true
8242 "#,
8243 )?;
8244
8245 let cfg = Config::load().unwrap();
8246 assert!(
8247 cfg.warnings.iter().any(|w| matches!(
8248 w,
8249 crate::Warning::DeprecatedKey { old, new }
8250 if old == "deny_warnings" && new == "deny = warnings"
8251 )),
8252 "Expected deprecated key warning for inactive profile, got: {:?}",
8253 cfg.warnings
8254 );
8255 Ok(())
8256 });
8257 }
8258
8259 #[test]
8260 fn warns_on_deprecated_profile_names() {
8261 figment::Jail::expect_with(|jail| {
8262 jail.create_file(
8263 "foundry.toml",
8264 r#"
8265 [profile.cancun]
8266 "#,
8267 )?;
8268
8269 let cfg = Config::load().unwrap();
8270 assert!(
8271 cfg.warnings.iter().any(|w| matches!(
8272 w,
8273 crate::Warning::DeprecatedKey { old, new }
8274 if old == "cancun" && new == "evm_version = Cancun"
8275 )),
8276 "Expected deprecated profile-name warning, got: {:?}",
8277 cfg.warnings
8278 );
8279 Ok(())
8280 });
8281 }
8282
8283 #[test]
8284 fn warns_on_unknown_keys_in_profile() {
8285 figment::Jail::expect_with(|jail| {
8286 jail.create_file(
8287 "foundry.toml",
8288 r#"
8289 [profile.default]
8290 unknown_key_xyz = 123
8291 "#,
8292 )?;
8293
8294 let cfg = Config::load().unwrap();
8295 assert!(cfg.warnings.iter().any(
8296 |w| matches!(w, crate::Warning::UnknownKey { key, .. } if key == "unknown_key_xyz")
8297 ));
8298 Ok(())
8299 });
8300 }
8301
8302 #[test]
8303 fn no_unknown_key_warning_for_network_field() {
8304 figment::Jail::expect_with(|jail| {
8307 jail.create_file(
8308 "foundry.toml",
8309 r#"
8310 [profile.default]
8311 network = "tempo"
8312 "#,
8313 )?;
8314
8315 let cfg = Config::load().unwrap();
8316 assert!(
8317 !cfg.warnings.iter().any(
8318 |w| matches!(w, crate::Warning::UnknownKey { key, .. } if key == "network")
8319 ),
8320 "did not expect UnknownKey warning for `network`, got: {:?}",
8321 cfg.warnings
8322 );
8323 Ok(())
8324 });
8325 }
8326
8327 #[test]
8328 fn no_unknown_key_warning_for_legacy_tempo_alias() {
8329 figment::Jail::expect_with(|jail| {
8331 jail.create_file(
8332 "foundry.toml",
8333 r#"
8334 [profile.default]
8335 tempo = true
8336 "#,
8337 )?;
8338
8339 let cfg = Config::load().unwrap();
8340 assert!(
8341 !cfg.warnings
8342 .iter()
8343 .any(|w| matches!(w, crate::Warning::UnknownKey { key, .. } if key == "tempo")),
8344 "did not expect UnknownKey warning for `tempo`, got: {:?}",
8345 cfg.warnings
8346 );
8347 Ok(())
8348 });
8349 }
8350
8351 #[test]
8352 #[cfg(feature = "monad")]
8353 fn no_unknown_key_warning_for_legacy_monad_alias() {
8354 figment::Jail::expect_with(|jail| {
8355 jail.create_file(
8356 "foundry.toml",
8357 r#"
8358 [profile.default]
8359 monad = true
8360 "#,
8361 )?;
8362
8363 let cfg = Config::load().unwrap();
8364 assert!(
8365 !cfg.warnings
8366 .iter()
8367 .any(|w| matches!(w, crate::Warning::UnknownKey { key, .. } if key == "monad")),
8368 "did not expect UnknownKey warning for `monad`, got: {:?}",
8369 cfg.warnings
8370 );
8371 Ok(())
8372 });
8373 }
8374
8375 #[test]
8376 #[cfg(not(feature = "monad"))]
8377 fn warns_for_monad_alias_without_monad_support() {
8378 figment::Jail::expect_with(|jail| {
8379 jail.create_file(
8380 "foundry.toml",
8381 r#"
8382 [profile.default]
8383 monad = true
8384 "#,
8385 )?;
8386
8387 let cfg = Config::load().unwrap();
8388 assert!(
8389 cfg.warnings
8390 .iter()
8391 .any(|w| matches!(w, crate::Warning::UnknownKey { key, .. } if key == "monad")),
8392 "expected UnknownKey warning for `monad`, got: {:?}",
8393 cfg.warnings
8394 );
8395 Ok(())
8396 });
8397 }
8398
8399 #[test]
8400 fn fails_on_ambiguous_version_in_compilation_restrictions() {
8401 figment::Jail::expect_with(|jail| {
8402 jail.create_file(
8403 "foundry.toml",
8404 r#"
8405 [profile.default]
8406 src = "src"
8407
8408 [[profile.default.compilation_restrictions]]
8409 paths = "src/*.sol"
8410 version = "0.8.11"
8411 "#,
8412 )?;
8413
8414 let err = Config::load().expect_err("expected bare version to fail");
8415 let err_msg = err.to_string();
8416 assert!(
8417 err_msg.contains("Invalid version format '0.8.11'")
8418 && err_msg.contains("Bare version numbers are ambiguous"),
8419 "Expected error about ambiguous version, got: {err_msg}"
8420 );
8421
8422 Ok(())
8423 });
8424 }
8425
8426 #[test]
8427 fn accepts_explicit_version_requirements() {
8428 figment::Jail::expect_with(|jail| {
8429 jail.create_file(
8430 "foundry.toml",
8431 r#"
8432 [profile.default]
8433 src = "src"
8434
8435 [[profile.default.compilation_restrictions]]
8436 paths = "src/*.sol"
8437 version = "=0.8.11"
8438
8439 [[profile.default.compilation_restrictions]]
8440 paths = "test/*.sol"
8441 version = ">=0.8.11"
8442 "#,
8443 )?;
8444
8445 let config = Config::load().expect("should accept explicit version requirements");
8446 assert_eq!(config.compilation_restrictions.len(), 2);
8447
8448 Ok(())
8449 });
8450 }
8451
8452 #[test]
8453 fn warns_on_unknown_keys_in_all_config_sections() {
8454 figment::Jail::expect_with(|jail| {
8455 jail.create_file(
8456 "foundry.toml",
8457 r#"
8458 [profile.default]
8459 src = "src"
8460 unknown_profile_key = "should_warn"
8461
8462 # Standalone sections with unknown keys
8463 [fmt]
8464 line_length = 120
8465 unknown_fmt_key = "should_warn"
8466
8467 [lint]
8468 severity = ["high"]
8469 unknown_lint_key = "should_warn"
8470
8471 [doc]
8472 out = "docs"
8473 unknown_doc_key = "should_warn"
8474
8475 [fuzz]
8476 runs = 256
8477 unknown_fuzz_key = "should_warn"
8478
8479 [invariant]
8480 runs = 256
8481 unknown_invariant_key = "should_warn"
8482
8483 [symbolic]
8484 enabled = true
8485 depth = 128
8486 unknown_symbolic_key = "should_warn"
8487
8488 [mutation]
8489 unknown_mutation_key = "should_warn"
8490
8491 [vyper]
8492 unknown_vyper_key = "should_warn"
8493
8494 [bind_json]
8495 out = "bindings.sol"
8496 unknown_bind_json_key = "should_warn"
8497
8498 # Nested profile sections with unknown keys
8499 [profile.default.fmt]
8500 line_length = 100
8501 unknown_nested_fmt_key = "should_warn"
8502
8503 [profile.default.lint]
8504 severity = ["low"]
8505 unknown_nested_lint_key = "should_warn"
8506
8507 [profile.default.doc]
8508 out = "documentation"
8509 unknown_nested_doc_key = "should_warn"
8510
8511 [profile.default.fuzz]
8512 runs = 512
8513 unknown_nested_fuzz_key = "should_warn"
8514
8515 [profile.default.invariant]
8516 runs = 512
8517 unknown_nested_invariant_key = "should_warn"
8518
8519 [profile.default.symbolic]
8520 max_paths = 512
8521 unknown_nested_symbolic_key = "should_warn"
8522
8523 [profile.default.mutation]
8524 unknown_nested_mutation_key = "should_warn"
8525
8526 [profile.default.vyper]
8527 unknown_nested_vyper_key = "should_warn"
8528
8529 [profile.default.bind_json]
8530 out = "nested_bindings.sol"
8531 unknown_nested_bind_json_key = "should_warn"
8532
8533 # Array sections with unknown keys
8534 [[profile.default.compilation_restrictions]]
8535 paths = "src/*.sol"
8536 unknown_compilation_key = "should_warn"
8537
8538 [[profile.default.additional_compiler_profiles]]
8539 name = "via-ir"
8540 via_ir = true
8541 unknown_compiler_profile_key = "should_warn"
8542 "#,
8543 )?;
8544
8545 let cfg = Config::load().unwrap();
8546
8547 assert!(
8549 cfg.warnings.iter().any(|w| matches!(
8550 w,
8551 crate::Warning::UnknownKey { key, .. } if key == "unknown_profile_key"
8552 )),
8553 "Expected warning for 'unknown_profile_key' in profile, got: {:?}",
8554 cfg.warnings
8555 );
8556
8557 let standalone_expected = [
8559 ("unknown_fmt_key", "fmt"),
8560 ("unknown_lint_key", "lint"),
8561 ("unknown_doc_key", "doc"),
8562 ("unknown_fuzz_key", "fuzz"),
8563 ("unknown_invariant_key", "invariant"),
8564 ("unknown_symbolic_key", "symbolic"),
8565 ("unknown_mutation_key", "mutation"),
8566 ("unknown_vyper_key", "vyper"),
8567 ("unknown_bind_json_key", "bind_json"),
8568 ];
8569
8570 for (expected_key, expected_section) in standalone_expected {
8571 assert!(
8572 cfg.warnings.iter().any(|w| matches!(
8573 w,
8574 crate::Warning::UnknownSectionKey { key, section, .. }
8575 if key == expected_key && section == expected_section
8576 )),
8577 "Expected warning for '{}' in standalone section '{}', got: {:?}",
8578 expected_key,
8579 expected_section,
8580 cfg.warnings
8581 );
8582 }
8583
8584 let nested_expected = [
8586 ("unknown_nested_fmt_key", "fmt"),
8587 ("unknown_nested_lint_key", "lint"),
8588 ("unknown_nested_doc_key", "doc"),
8589 ("unknown_nested_fuzz_key", "fuzz"),
8590 ("unknown_nested_invariant_key", "invariant"),
8591 ("unknown_nested_symbolic_key", "symbolic"),
8592 ("unknown_nested_mutation_key", "mutation"),
8593 ("unknown_nested_vyper_key", "vyper"),
8594 ("unknown_nested_bind_json_key", "bind_json"),
8595 ];
8596
8597 for (expected_key, expected_section) in nested_expected {
8598 assert!(
8599 cfg.warnings.iter().any(|w| matches!(
8600 w,
8601 crate::Warning::UnknownSectionKey { key, section, .. }
8602 if key == expected_key && section == expected_section
8603 )),
8604 "Expected warning for '{}' in nested section '{}', got: {:?}",
8605 expected_key,
8606 expected_section,
8607 cfg.warnings
8608 );
8609 }
8610
8611 let array_expected = [
8613 ("unknown_compilation_key", "compilation_restrictions"),
8614 ("unknown_compiler_profile_key", "additional_compiler_profiles"),
8615 ];
8616
8617 for (expected_key, expected_section) in array_expected {
8618 assert!(
8619 cfg.warnings.iter().any(|w| matches!(
8620 w,
8621 crate::Warning::UnknownSectionKey { key, section, .. }
8622 if key == expected_key && section == expected_section
8623 )),
8624 "Expected warning for '{}' in array section '{}', got: {:?}",
8625 expected_key,
8626 expected_section,
8627 cfg.warnings
8628 );
8629 }
8630
8631 let unknown_key_warnings: Vec<_> = cfg
8633 .warnings
8634 .iter()
8635 .filter(|w| {
8636 matches!(w, crate::Warning::UnknownKey { .. })
8637 || matches!(w, crate::Warning::UnknownSectionKey { .. })
8638 })
8639 .collect();
8640
8641 assert_eq!(
8643 unknown_key_warnings.len(),
8644 21,
8645 "Expected 21 unknown key warnings (1 profile + 9 standalone + 9 nested + 2 array), got {}: {:?}",
8646 unknown_key_warnings.len(),
8647 unknown_key_warnings
8648 );
8649
8650 Ok(())
8651 });
8652 }
8653
8654 #[test]
8655 fn warns_on_unknown_keys_in_extended_config() {
8656 figment::Jail::expect_with(|jail| {
8657 jail.create_file(
8659 "base.toml",
8660 r#"
8661 [profile.default]
8662 optimizer_runs = 800
8663 unknown_base_profile_key = "should_warn"
8664
8665 [lint]
8666 severity = ["high"]
8667 unknown_base_lint_key = "should_warn"
8668
8669 [fmt]
8670 line_length = 100
8671 unknown_base_fmt_key = "should_warn"
8672 "#,
8673 )?;
8674
8675 jail.create_file(
8677 "foundry.toml",
8678 r#"
8679 [profile.default]
8680 extends = "base.toml"
8681 src = "src"
8682 unknown_local_profile_key = "should_warn"
8683
8684 [lint]
8685 unknown_local_lint_key = "should_warn"
8686
8687 [fuzz]
8688 runs = 512
8689 unknown_local_fuzz_key = "should_warn"
8690
8691 [[profile.default.compilation_restrictions]]
8692 paths = "src/*.sol"
8693 unknown_local_restriction_key = "should_warn"
8694 "#,
8695 )?;
8696
8697 let cfg = Config::load().unwrap();
8698
8699 assert_eq!(cfg.optimizer_runs, Some(800));
8701
8702 let expected_unknown_keys = ["unknown_base_profile_key", "unknown_local_profile_key"];
8710 for expected_key in expected_unknown_keys {
8711 assert!(
8712 cfg.warnings.iter().any(|w| matches!(
8713 w,
8714 crate::Warning::UnknownKey { key, .. } if key == expected_key
8715 )),
8716 "Expected warning for '{}', got: {:?}",
8717 expected_key,
8718 cfg.warnings
8719 );
8720 }
8721
8722 let expected_section_keys = [
8723 ("unknown_base_lint_key", "lint"),
8724 ("unknown_base_fmt_key", "fmt"),
8725 ("unknown_local_lint_key", "lint"),
8726 ("unknown_local_fuzz_key", "fuzz"),
8727 ("unknown_local_restriction_key", "compilation_restrictions"),
8728 ];
8729 for (expected_key, expected_section) in expected_section_keys {
8730 assert!(
8731 cfg.warnings.iter().any(|w| matches!(
8732 w,
8733 crate::Warning::UnknownSectionKey { key, section, .. }
8734 if key == expected_key && section == expected_section
8735 )),
8736 "Expected warning for '{}' in section '{}', got: {:?}",
8737 expected_key,
8738 expected_section,
8739 cfg.warnings
8740 );
8741 }
8742
8743 let unknown_warnings: Vec<_> = cfg
8745 .warnings
8746 .iter()
8747 .filter(|w| {
8748 matches!(w, crate::Warning::UnknownKey { .. })
8749 || matches!(w, crate::Warning::UnknownSectionKey { .. })
8750 })
8751 .collect();
8752 assert_eq!(
8753 unknown_warnings.len(),
8754 7,
8755 "Expected 7 unknown key warnings, got {}: {:?}",
8756 unknown_warnings.len(),
8757 unknown_warnings
8758 );
8759
8760 Ok(())
8761 });
8762 }
8763
8764 #[test]
8766 fn warns_on_unknown_profile() {
8767 figment::Jail::expect_with(|jail| {
8768 jail.create_file(
8769 "foundry.toml",
8770 r#"
8771 [profile.default]
8772 src = "src"
8773 "#,
8774 )?;
8775
8776 jail.set_env("FOUNDRY_PROFILE", "nonexistent");
8777 let cfg = Config::load().expect("expected unknown profile to fall back to default");
8778 assert_eq!(cfg.profile, Config::DEFAULT_PROFILE);
8779 assert!(
8780 cfg.warnings.iter().any(|w| matches!(
8781 w,
8782 crate::Warning::UnknownProfile { profile } if profile == "nonexistent"
8783 )),
8784 "Expected UnknownProfile warning, got: {:?}",
8785 cfg.warnings
8786 );
8787
8788 Ok(())
8789 });
8790 }
8791
8792 #[test]
8794 fn no_false_warnings_for_vyper_config_keys() {
8795 figment::Jail::expect_with(|jail| {
8796 jail.create_file(
8797 "foundry.toml",
8798 r#"
8799 [profile.default]
8800 src = "src"
8801
8802 [vyper]
8803 optimize = "O1"
8804 path = "/usr/bin/vyper"
8805 experimental_codegen = true
8806 venom_experimental = false
8807 debug = true
8808 enable_decimals = true
8809
8810 [vyper.venom]
8811 disable_cse = true
8812 inline_threshold = 15
8813 "#,
8814 )?;
8815
8816 let cfg = Config::load().unwrap();
8817 let vyper_warnings: Vec<_> = cfg
8819 .warnings
8820 .iter()
8821 .filter(|w| {
8822 matches!(
8823 w,
8824 crate::Warning::UnknownSectionKey { section, .. } if section == "vyper"
8825 )
8826 })
8827 .collect();
8828
8829 assert!(
8830 vyper_warnings.is_empty(),
8831 "Valid vyper keys should not trigger warnings, got: {vyper_warnings:?}"
8832 );
8833
8834 Ok(())
8835 });
8836 }
8837
8838 #[test]
8840 fn no_false_warnings_for_nested_vyper_config_keys() {
8841 figment::Jail::expect_with(|jail| {
8842 jail.create_file(
8843 "foundry.toml",
8844 r#"
8845 [profile.default]
8846 src = "src"
8847
8848 [profile.default.vyper]
8849 opt_level = "s"
8850 path = "/opt/vyper/bin/vyper"
8851 experimental_codegen = false
8852 debug = true
8853 enable_decimals = true
8854 venom = { disable_sccp = true, disable_mem2var = false }
8855 "#,
8856 )?;
8857
8858 let cfg = Config::load().unwrap();
8859 let vyper_warnings: Vec<_> = cfg
8861 .warnings
8862 .iter()
8863 .filter(|w| {
8864 matches!(
8865 w,
8866 crate::Warning::UnknownSectionKey { section, .. } if section == "vyper"
8867 )
8868 })
8869 .collect();
8870
8871 assert!(
8872 vyper_warnings.is_empty(),
8873 "Valid nested vyper keys should not trigger warnings, got: {vyper_warnings:?}"
8874 );
8875
8876 Ok(())
8877 });
8878 }
8879
8880 #[test]
8883 fn no_false_warnings_for_inline_vyper_config() {
8884 figment::Jail::expect_with(|jail| {
8885 jail.create_file(
8886 "foundry.toml",
8887 r#"
8888 [profile.default]
8889 src = "src"
8890 vyper = { optimize = "gas", debug = true }
8891
8892 [profile.default-venom]
8893 vyper = { opt_level = "O2", experimental_codegen = true, venom = { disable_cse = true } }
8894
8895 [profile.ci-venom]
8896 vyper = { venom_experimental = true, enable_decimals = true }
8897 "#,
8898 )?;
8899
8900 let cfg = Config::load().unwrap();
8901 let vyper_warnings: Vec<_> = cfg
8902 .warnings
8903 .iter()
8904 .filter(|w| {
8905 matches!(
8906 w,
8907 crate::Warning::UnknownSectionKey { section, .. } if section == "vyper"
8908 )
8909 })
8910 .collect();
8911
8912 assert!(
8913 vyper_warnings.is_empty(),
8914 "Valid inline vyper config should not trigger warnings, got: {vyper_warnings:?}"
8915 );
8916
8917 Ok(())
8918 });
8919 }
8920
8921 #[test]
8923 fn warns_on_unknown_vyper_keys() {
8924 figment::Jail::expect_with(|jail| {
8925 jail.create_file(
8926 "foundry.toml",
8927 r#"
8928 [profile.default]
8929 src = "src"
8930
8931 [vyper]
8932 optimize = "gas"
8933 unknown_vyper_option = true
8934 "#,
8935 )?;
8936
8937 let cfg = Config::load().unwrap();
8938 assert!(
8939 cfg.warnings.iter().any(|w| matches!(
8940 w,
8941 crate::Warning::UnknownSectionKey { key, section, .. }
8942 if key == "unknown_vyper_option" && section == "vyper"
8943 )),
8944 "Unknown vyper key should trigger warning, got: {:?}",
8945 cfg.warnings
8946 );
8947
8948 Ok(())
8949 });
8950 }
8951
8952 #[test]
8954 fn succeeds_on_known_profile() {
8955 figment::Jail::expect_with(|jail| {
8956 jail.create_file(
8957 "foundry.toml",
8958 r#"
8959 [profile.default]
8960 src = "src"
8961
8962 [profile.ci]
8963 src = "src"
8964 fuzz = { runs = 10000 }
8965 "#,
8966 )?;
8967
8968 jail.set_env("FOUNDRY_PROFILE", "ci");
8969 let config = Config::load().expect("known profile should work");
8970 assert_eq!(config.profile.as_str(), "ci");
8971 assert_eq!(config.fuzz.runs, 10000);
8972
8973 Ok(())
8974 });
8975 }
8976
8977 #[test]
8980 fn nested_lib_config_falls_back_to_default_profile() {
8981 figment::Jail::expect_with(|jail| {
8982 let lib_path = jail.directory().join("lib/mylib");
8984 std::fs::create_dir_all(&lib_path).unwrap();
8985 jail.create_file(
8986 "lib/mylib/foundry.toml",
8987 r#"
8988 [profile.default]
8989 src = "contracts"
8990 "#,
8991 )?;
8992
8993 jail.set_env("FOUNDRY_PROFILE", "ci");
8995
8996 let config = Config::load_with_root_and_fallback(&lib_path)
8998 .expect("lib config should load with fallback");
8999 assert_eq!(config.profile, Config::DEFAULT_PROFILE);
9000 assert_eq!(config.src.as_os_str(), "contracts");
9001
9002 Ok(())
9003 });
9004 }
9005
9006 #[test]
9008 fn nested_lib_config_uses_profile_if_exists() {
9009 figment::Jail::expect_with(|jail| {
9010 let lib_path = jail.directory().join("lib/mylib");
9012 std::fs::create_dir_all(&lib_path).unwrap();
9013 jail.create_file(
9014 "lib/mylib/foundry.toml",
9015 r#"
9016 [profile.default]
9017 src = "contracts"
9018
9019 [profile.ci]
9020 src = "contracts"
9021 fuzz = { runs = 5000 }
9022 "#,
9023 )?;
9024
9025 jail.set_env("FOUNDRY_PROFILE", "ci");
9027
9028 let config = Config::load_with_root_and_fallback(&lib_path)
9030 .expect("lib config should load with profile");
9031 assert_eq!(config.profile.as_str(), "ci");
9032 assert_eq!(config.fuzz.runs, 5000);
9033
9034 Ok(())
9035 });
9036 }
9037
9038 #[test]
9040 fn succeeds_on_hyphenated_profile_name() {
9041 figment::Jail::expect_with(|jail| {
9042 jail.create_file(
9043 "foundry.toml",
9044 r#"
9045 [profile.default]
9046 src = "src"
9047
9048 [profile.ci-venom]
9049 src = "src"
9050 fuzz = { runs = 7500 }
9051
9052 [profile.default-venom]
9053 src = "src"
9054 fuzz = { runs = 8000 }
9055 "#,
9056 )?;
9057
9058 jail.set_env("FOUNDRY_PROFILE", "ci-venom");
9060 let config = Config::load().expect("hyphenated profile should work");
9061 assert_eq!(config.profile.as_str(), "ci-venom");
9062 assert_eq!(config.fuzz.runs, 7500);
9063
9064 jail.set_env("FOUNDRY_PROFILE", "default-venom");
9066 let config = Config::load().expect("hyphenated profile should work");
9067 assert_eq!(config.profile.as_str(), "default-venom");
9068 assert_eq!(config.fuzz.runs, 8000);
9069
9070 assert!(
9072 config.profiles.iter().any(|p| p.as_str() == "ci-venom"),
9073 "profiles should contain 'ci-venom', got: {:?}",
9074 config.profiles
9075 );
9076 assert!(
9077 config.profiles.iter().any(|p| p.as_str() == "default-venom"),
9078 "profiles should contain 'default-venom', got: {:?}",
9079 config.profiles
9080 );
9081
9082 Ok(())
9083 });
9084 }
9085
9086 #[test]
9087 fn standalone_section_name_can_be_used_as_profile_name() {
9088 figment::Jail::expect_with(|jail| {
9089 jail.create_file(
9090 "foundry.toml",
9091 r#"
9092 [profile.symbolic]
9093 eth-rpc-url = "https://example.com/"
9094 "#,
9095 )?;
9096 jail.set_env("FOUNDRY_PROFILE", "symbolic");
9097
9098 let config = Config::load().unwrap();
9099 assert_eq!(config.profile.as_str(), "symbolic");
9100 assert_eq!(config.eth_rpc_url.as_deref(), Some("https://example.com/"));
9101
9102 Ok(())
9103 });
9104 }
9105
9106 #[test]
9108 fn hyphenated_profile_with_nested_sections() {
9109 figment::Jail::expect_with(|jail| {
9110 jail.create_file(
9111 "foundry.toml",
9112 r#"
9113 [profile.default]
9114 src = "src"
9115
9116 [profile.ci-venom]
9117 src = "src"
9118 optimizer_runs = 500
9119
9120 [profile.ci-venom.fuzz]
9121 runs = 10000
9122 max_test_rejects = 350000
9123
9124 [profile.ci-venom.invariant]
9125 runs = 375
9126 depth = 500
9127 "#,
9128 )?;
9129
9130 jail.set_env("FOUNDRY_PROFILE", "ci-venom");
9131 let config =
9132 Config::load().expect("hyphenated profile with nested sections should work");
9133 assert_eq!(config.profile.as_str(), "ci-venom");
9134 assert_eq!(config.optimizer_runs, Some(500));
9135 assert_eq!(config.fuzz.runs, 10000);
9136 assert_eq!(config.fuzz.max_test_rejects, 350000);
9137 assert_eq!(config.invariant.runs, 375);
9138 assert_eq!(config.invariant.depth, 500);
9139
9140 Ok(())
9141 });
9142 }
9143
9144 #[test]
9145 fn coverage_section_in_profile() {
9146 figment::Jail::expect_with(|jail| {
9147 jail.create_file(
9148 "foundry.toml",
9149 r#"
9150 [profile.default.coverage]
9151 report = ["summary", "lcov"]
9152 lcov_version = "2.2.0"
9153 ir_minimum = true
9154 report_file = "out/lcov.info"
9155 include_libs = true
9156 exclude_tests = true
9157 skip_files = ["test/**", "src/mocks/**"]
9158 "#,
9159 )?;
9160 let config = Config::load_with_root(jail.directory()).unwrap();
9161 assert_eq!(
9162 config.coverage.report,
9163 vec![CoverageReportKind::Summary, CoverageReportKind::Lcov]
9164 );
9165 assert_eq!(config.coverage.lcov_version, semver::Version::new(2, 2, 0));
9166 assert!(config.coverage.ir_minimum);
9167 assert_eq!(
9168 config.coverage.report_file.as_deref(),
9169 Some(std::path::Path::new("out/lcov.info"))
9170 );
9171 assert!(config.coverage.include_libs);
9172 assert!(config.coverage.exclude_tests);
9173 assert_eq!(
9174 config.coverage.skip_files,
9175 vec!["test/**".to_string(), "src/mocks/**".to_string()]
9176 );
9177 Ok(())
9178 });
9179 }
9180
9181 #[test]
9182 fn coverage_standalone_section_falls_back_to_default_profile() {
9183 figment::Jail::expect_with(|jail| {
9184 jail.create_file(
9187 "foundry.toml",
9188 r#"
9189 [coverage]
9190 skip_files = ["script/**"]
9191 exclude_tests = true
9192 "#,
9193 )?;
9194 let config = Config::load_with_root(jail.directory()).unwrap();
9195 assert_eq!(config.coverage.skip_files, vec!["script/**".to_string()]);
9196 assert!(config.coverage.exclude_tests);
9197 assert_eq!(config.coverage.report, vec![CoverageReportKind::Summary]);
9199 assert_eq!(config.coverage.lcov_version, semver::Version::new(1, 0, 0));
9200 Ok(())
9201 });
9202 }
9203
9204 #[test]
9205 fn coverage_per_profile_overrides_default() {
9206 figment::Jail::expect_with(|jail| {
9207 jail.create_file(
9208 "foundry.toml",
9209 r#"
9210 [profile.default.coverage]
9211 skip_files = ["script/**"]
9212
9213 [profile.ci.coverage]
9214 skip_files = ["test/**", "lib/**"]
9215 exclude_tests = true
9216 "#,
9217 )?;
9218
9219 let config = Config::load_with_root(jail.directory()).unwrap();
9221 assert_eq!(config.coverage.skip_files, vec!["script/**".to_string()]);
9222 assert!(!config.coverage.exclude_tests);
9223
9224 jail.set_env("FOUNDRY_PROFILE", "ci");
9226 let config = Config::load_with_root(jail.directory()).unwrap();
9227 assert_eq!(
9228 config.coverage.skip_files,
9229 vec!["test/**".to_string(), "lib/**".to_string()]
9230 );
9231 assert!(config.coverage.exclude_tests);
9232 Ok(())
9233 });
9234 }
9235}