diff --git a/crates/cheatcodes/assets/cheatcodes.json b/crates/cheatcodes/assets/cheatcodes.json index a1b3ca6a9fbe3..725b73301b81b 100644 --- a/crates/cheatcodes/assets/cheatcodes.json +++ b/crates/cheatcodes/assets/cheatcodes.json @@ -4127,7 +4127,7 @@ { "func": { "id": "deployCode_0", - "description": "Deploys a contract from an artifact file. Takes in the relative path to the json file or the path to the\nartifact in the form of :: where and parts are optional.\nReverts if the target artifact contains unlinked library placeholders.", + "description": "Deploys a contract from an artifact file. Takes in the relative path to the json file or the path to the\nartifact in the form of :: or :: where and\n/ parts are optional.\nReverts if the target artifact contains unlinked library placeholders.", "declaration": "function deployCode(string calldata artifactPath) external returns (address deployedAddress);", "visibility": "external", "mutability": "", @@ -4147,7 +4147,7 @@ { "func": { "id": "deployCode_1", - "description": "Deploys a contract from an artifact file. Takes in the relative path to the json file or the path to the\nartifact in the form of :: where and parts are optional.\nReverts if the target artifact contains unlinked library placeholders.\nAdditionally accepts abi-encoded constructor arguments.", + "description": "Deploys a contract from an artifact file. Takes in the relative path to the json file or the path to the\nartifact in the form of :: or :: where and\n/ parts are optional.\nReverts if the target artifact contains unlinked library placeholders.\nAdditionally accepts abi-encoded constructor arguments.", "declaration": "function deployCode(string calldata artifactPath, bytes calldata constructorArgs) external returns (address deployedAddress);", "visibility": "external", "mutability": "", @@ -4167,7 +4167,7 @@ { "func": { "id": "deployCode_2", - "description": "Deploys a contract from an artifact file. Takes in the relative path to the json file or the path to the\nartifact in the form of :: where and parts are optional.\nReverts if the target artifact contains unlinked library placeholders.\nAdditionally accepts `msg.value`.", + "description": "Deploys a contract from an artifact file. Takes in the relative path to the json file or the path to the\nartifact in the form of :: or :: where and\n/ parts are optional.\nReverts if the target artifact contains unlinked library placeholders.\nAdditionally accepts `msg.value`.", "declaration": "function deployCode(string calldata artifactPath, uint256 value) external returns (address deployedAddress);", "visibility": "external", "mutability": "", @@ -4187,7 +4187,7 @@ { "func": { "id": "deployCode_3", - "description": "Deploys a contract from an artifact file. Takes in the relative path to the json file or the path to the\nartifact in the form of :: where and parts are optional.\nReverts if the target artifact contains unlinked library placeholders.\nAdditionally accepts abi-encoded constructor arguments and `msg.value`.", + "description": "Deploys a contract from an artifact file. Takes in the relative path to the json file or the path to the\nartifact in the form of :: or :: where and\n/ parts are optional.\nReverts if the target artifact contains unlinked library placeholders.\nAdditionally accepts abi-encoded constructor arguments and `msg.value`.", "declaration": "function deployCode(string calldata artifactPath, bytes calldata constructorArgs, uint256 value) external returns (address deployedAddress);", "visibility": "external", "mutability": "", @@ -4207,7 +4207,7 @@ { "func": { "id": "deployCode_4", - "description": "Deploys a contract from an artifact file, using the CREATE2 salt. Takes in the relative path to the json file or the path to the\nartifact in the form of :: where and parts are optional.\nReverts if the target artifact contains unlinked library placeholders.", + "description": "Deploys a contract from an artifact file, using the CREATE2 salt. Takes in the relative path to the json file or the path to the\nartifact in the form of :: or :: where and\n/ parts are optional.\nReverts if the target artifact contains unlinked library placeholders.", "declaration": "function deployCode(string calldata artifactPath, bytes32 salt) external returns (address deployedAddress);", "visibility": "external", "mutability": "", @@ -4227,7 +4227,7 @@ { "func": { "id": "deployCode_5", - "description": "Deploys a contract from an artifact file, using the CREATE2 salt. Takes in the relative path to the json file or the path to the\nartifact in the form of :: where and parts are optional.\nReverts if the target artifact contains unlinked library placeholders.\nAdditionally accepts abi-encoded constructor arguments.", + "description": "Deploys a contract from an artifact file, using the CREATE2 salt. Takes in the relative path to the json file or the path to the\nartifact in the form of :: or :: where and\n/ parts are optional.\nReverts if the target artifact contains unlinked library placeholders.\nAdditionally accepts abi-encoded constructor arguments.", "declaration": "function deployCode(string calldata artifactPath, bytes calldata constructorArgs, bytes32 salt) external returns (address deployedAddress);", "visibility": "external", "mutability": "", @@ -4247,7 +4247,7 @@ { "func": { "id": "deployCode_6", - "description": "Deploys a contract from an artifact file, using the CREATE2 salt. Takes in the relative path to the json file or the path to the\nartifact in the form of :: where and parts are optional.\nReverts if the target artifact contains unlinked library placeholders.\nAdditionally accepts `msg.value`.", + "description": "Deploys a contract from an artifact file, using the CREATE2 salt. Takes in the relative path to the json file or the path to the\nartifact in the form of :: or :: where and\n/ parts are optional.\nReverts if the target artifact contains unlinked library placeholders.\nAdditionally accepts `msg.value`.", "declaration": "function deployCode(string calldata artifactPath, uint256 value, bytes32 salt) external returns (address deployedAddress);", "visibility": "external", "mutability": "", @@ -4267,7 +4267,7 @@ { "func": { "id": "deployCode_7", - "description": "Deploys a contract from an artifact file, using the CREATE2 salt. Takes in the relative path to the json file or the path to the\nartifact in the form of :: where and parts are optional.\nReverts if the target artifact contains unlinked library placeholders.\nAdditionally accepts abi-encoded constructor arguments and `msg.value`.", + "description": "Deploys a contract from an artifact file, using the CREATE2 salt. Takes in the relative path to the json file or the path to the\nartifact in the form of :: or :: where and\n/ parts are optional.\nReverts if the target artifact contains unlinked library placeholders.\nAdditionally accepts abi-encoded constructor arguments and `msg.value`.", "declaration": "function deployCode(string calldata artifactPath, bytes calldata constructorArgs, uint256 value, bytes32 salt) external returns (address deployedAddress);", "visibility": "external", "mutability": "", @@ -6347,7 +6347,7 @@ { "func": { "id": "getCode", - "description": "Gets the creation bytecode from an artifact file. Takes in the relative path to the json file or the path to the\nartifact in the form of :: where and parts are optional.", + "description": "Gets the creation bytecode from an artifact file. Takes in the relative path to the json file or the path to the\nartifact in the form of :: or :: where and\n/ parts are optional. Use to select artifacts compiled with a specific profile\nfrom foundry.toml.", "declaration": "function getCode(string calldata artifactPath) external view returns (bytes memory creationBytecode);", "visibility": "external", "mutability": "view", @@ -6367,7 +6367,7 @@ { "func": { "id": "getDeployedCode", - "description": "Gets the deployed bytecode from an artifact file. Takes in the relative path to the json file or the path to the\nartifact in the form of :: where and parts are optional.", + "description": "Gets the deployed bytecode from an artifact file. Takes in the relative path to the json file or the path to the\nartifact in the form of :: or :: where and\n/ parts are optional.", "declaration": "function getDeployedCode(string calldata artifactPath) external view returns (bytes memory runtimeBytecode);", "visibility": "external", "mutability": "view", diff --git a/crates/cheatcodes/spec/src/vm.rs b/crates/cheatcodes/spec/src/vm.rs index f20c234a67701..674f8389325ff 100644 --- a/crates/cheatcodes/spec/src/vm.rs +++ b/crates/cheatcodes/spec/src/vm.rs @@ -1998,18 +1998,22 @@ interface Vm { function getArtifactPathByDeployedCode(bytes calldata deployedCode) external view returns (string memory path); /// Gets the creation bytecode from an artifact file. Takes in the relative path to the json file or the path to the - /// artifact in the form of :: where and parts are optional. + /// artifact in the form of :: or :: where and + /// / parts are optional. Use to select artifacts compiled with a specific profile + /// from foundry.toml. #[cheatcode(group = Filesystem)] function getCode(string calldata artifactPath) external view returns (bytes memory creationBytecode); /// Deploys a contract from an artifact file. Takes in the relative path to the json file or the path to the - /// artifact in the form of :: where and parts are optional. + /// artifact in the form of :: or :: where and + /// / parts are optional. /// Reverts if the target artifact contains unlinked library placeholders. #[cheatcode(group = Filesystem)] function deployCode(string calldata artifactPath) external returns (address deployedAddress); /// Deploys a contract from an artifact file. Takes in the relative path to the json file or the path to the - /// artifact in the form of :: where and parts are optional. + /// artifact in the form of :: or :: where and + /// / parts are optional. /// Reverts if the target artifact contains unlinked library placeholders. /// /// Additionally accepts abi-encoded constructor arguments. @@ -2017,7 +2021,8 @@ interface Vm { function deployCode(string calldata artifactPath, bytes calldata constructorArgs) external returns (address deployedAddress); /// Deploys a contract from an artifact file. Takes in the relative path to the json file or the path to the - /// artifact in the form of :: where and parts are optional. + /// artifact in the form of :: or :: where and + /// / parts are optional. /// Reverts if the target artifact contains unlinked library placeholders. /// /// Additionally accepts `msg.value`. @@ -2025,7 +2030,8 @@ interface Vm { function deployCode(string calldata artifactPath, uint256 value) external returns (address deployedAddress); /// Deploys a contract from an artifact file. Takes in the relative path to the json file or the path to the - /// artifact in the form of :: where and parts are optional. + /// artifact in the form of :: or :: where and + /// / parts are optional. /// Reverts if the target artifact contains unlinked library placeholders. /// /// Additionally accepts abi-encoded constructor arguments and `msg.value`. @@ -2033,13 +2039,15 @@ interface Vm { function deployCode(string calldata artifactPath, bytes calldata constructorArgs, uint256 value) external returns (address deployedAddress); /// Deploys a contract from an artifact file, using the CREATE2 salt. Takes in the relative path to the json file or the path to the - /// artifact in the form of :: where and parts are optional. + /// artifact in the form of :: or :: where and + /// / parts are optional. /// Reverts if the target artifact contains unlinked library placeholders. #[cheatcode(group = Filesystem)] function deployCode(string calldata artifactPath, bytes32 salt) external returns (address deployedAddress); /// Deploys a contract from an artifact file, using the CREATE2 salt. Takes in the relative path to the json file or the path to the - /// artifact in the form of :: where and parts are optional. + /// artifact in the form of :: or :: where and + /// / parts are optional. /// Reverts if the target artifact contains unlinked library placeholders. /// /// Additionally accepts abi-encoded constructor arguments. @@ -2047,7 +2055,8 @@ interface Vm { function deployCode(string calldata artifactPath, bytes calldata constructorArgs, bytes32 salt) external returns (address deployedAddress); /// Deploys a contract from an artifact file, using the CREATE2 salt. Takes in the relative path to the json file or the path to the - /// artifact in the form of :: where and parts are optional. + /// artifact in the form of :: or :: where and + /// / parts are optional. /// Reverts if the target artifact contains unlinked library placeholders. /// /// Additionally accepts `msg.value`. @@ -2055,7 +2064,8 @@ interface Vm { function deployCode(string calldata artifactPath, uint256 value, bytes32 salt) external returns (address deployedAddress); /// Deploys a contract from an artifact file, using the CREATE2 salt. Takes in the relative path to the json file or the path to the - /// artifact in the form of :: where and parts are optional. + /// artifact in the form of :: or :: where and + /// / parts are optional. /// Reverts if the target artifact contains unlinked library placeholders. /// /// Additionally accepts abi-encoded constructor arguments and `msg.value`. @@ -2063,7 +2073,8 @@ interface Vm { function deployCode(string calldata artifactPath, bytes calldata constructorArgs, uint256 value, bytes32 salt) external returns (address deployedAddress); /// Gets the deployed bytecode from an artifact file. Takes in the relative path to the json file or the path to the - /// artifact in the form of :: where and parts are optional. + /// artifact in the form of :: or :: where and + /// / parts are optional. #[cheatcode(group = Filesystem)] function getDeployedCode(string calldata artifactPath) external view returns (bytes memory runtimeBytecode); diff --git a/crates/cheatcodes/src/fs.rs b/crates/cheatcodes/src/fs.rs index f2f11d91ad163..ff4dd8e83b22d 100644 --- a/crates/cheatcodes/src/fs.rs +++ b/crates/cheatcodes/src/fs.rs @@ -30,6 +30,76 @@ use std::{ }; use walkdir::WalkDir; +/// Parsed artifact path components. +#[derive(Debug, Default, PartialEq, Eq)] +struct ParsedArtifactPath<'a> { + file: Option, + contract_name: Option<&'a str>, + version: Option, + profile: Option<&'a str>, +} + +/// Parses an artifact path string into its components. +/// +/// Supports the following formats: +/// - `path/to/contract.sol` +/// - `path/to/contract.sol:ContractName` +/// - `path/to/contract.sol:ContractName:0.8.23` +/// - `path/to/contract.sol:ContractName:profile` +/// - `path/to/contract.sol:0.8.23` +/// - `path/to/contract.sol:profile` +/// - `ContractName` +/// - `ContractName:0.8.23` +/// - `ContractName:profile` +fn parse_artifact_path(path: &str) -> std::result::Result, String> { + let mut parts = path.split(':'); + + let mut file = None; + let mut contract_name = None; + let mut version = None; + let mut profile = None; + + let path_or_name = parts.next().unwrap(); + if path_or_name.contains('.') { + file = Some(PathBuf::from(path_or_name)); + if let Some(name_or_version_or_profile) = parts.next() { + if name_or_version_or_profile.contains('.') + || Version::parse(name_or_version_or_profile).is_ok() + { + version = Some(name_or_version_or_profile); + } else { + contract_name = Some(name_or_version_or_profile); + if let Some(version_or_profile) = parts.next() { + if version_or_profile.contains('.') + || Version::parse(version_or_profile).is_ok() + { + version = Some(version_or_profile); + } else { + profile = Some(version_or_profile); + } + } + } + } + } else { + contract_name = Some(path_or_name); + if let Some(version_or_profile) = parts.next() { + if version_or_profile.contains('.') || Version::parse(version_or_profile).is_ok() { + version = Some(version_or_profile); + } else { + profile = Some(version_or_profile); + } + } + } + + let version = if let Some(version) = version { + Some(Version::parse(version).map_err(|e| format!("failed parsing version: {e}"))?) + } else { + None + }; + + Ok(ParsedArtifactPath { file, contract_name, version, profile }) +} + impl Cheatcode for existsCall { fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { path } = self; @@ -455,9 +525,12 @@ fn deploy_code( /// - `path/to/contract.sol` /// - `path/to/contract.sol:ContractName` /// - `path/to/contract.sol:ContractName:0.8.23` +/// - `path/to/contract.sol:ContractName:profile` /// - `path/to/contract.sol:0.8.23` +/// - `path/to/contract.sol:profile` /// - `ContractName` /// - `ContractName:0.8.23` +/// - `ContractName:profile` /// /// This function is safe to use with contracts that have library dependencies. /// `alloy_json_abi::ContractObject` validates bytecode during JSON parsing and will @@ -470,62 +543,55 @@ fn get_artifact_code( let path = if path.ends_with(".json") { PathBuf::from(path) } else { - let mut parts = path.split(':'); - - let mut file = None; - let mut contract_name = None; - let mut version = None; - - let path_or_name = parts.next().unwrap(); - if path_or_name.contains('.') { - file = Some(PathBuf::from(path_or_name)); - if let Some(name_or_version) = parts.next() { - if name_or_version.contains('.') { - version = Some(name_or_version); - } else { - contract_name = Some(name_or_version); - version = parts.next(); - } - } - } else { - contract_name = Some(path_or_name); - version = parts.next(); - } - - let version = if let Some(version) = version { - Some(Version::parse(version).map_err(|e| fmt_err!("failed parsing version: {e}"))?) - } else { - None - }; + let parsed = parse_artifact_path(path) + .map_err(|e| fmt_err!("failed to parse artifact path: {e}"))?; + let ParsedArtifactPath { file, contract_name, version, profile } = parsed; // Use available artifacts list if present if let Some(artifacts) = &state.config.available_artifacts { - let filtered = artifacts - .iter() - .filter(|(id, _)| { - // name might be in the form of "Counter.0.8.23" - let id_name = id.name.split('.').next().unwrap(); - - if let Some(path) = &file - && !id.source.ends_with(path) - { - return false; - } - if let Some(name) = contract_name - && id_name != name - { - return false; - } - if let Some(ref version) = version - && (id.version.minor != version.minor - || id.version.major != version.major - || id.version.patch != version.patch) - { - return false; - } - true - }) - .collect::>(); + let ambiguous_file_profile = + file.is_some() && version.is_none() && profile.is_none() && contract_name.is_some(); + let filter_artifacts = |treat_ambiguous_as_profile: bool| -> Vec<_> { + artifacts + .iter() + .filter(|(id, _)| { + // name might be in the form of "Counter.0.8.23" + let id_name = id.name.split('.').next().unwrap(); + + if let Some(path) = &file + && !id.source.ends_with(path) + { + return false; + } + if let Some(ref version) = version + && (id.version.minor != version.minor + || id.version.major != version.major + || id.version.patch != version.patch) + { + return false; + } + if let Some(profile) = profile + && id.profile != profile + { + return false; + } + if let Some(name) = contract_name { + if treat_ambiguous_as_profile && ambiguous_file_profile { + return id.profile == name; + } + + return id_name == name; + } + + true + }) + .collect() + }; + + let mut filtered = filter_artifacts(false); + if filtered.is_empty() && ambiguous_file_profile { + filtered = filter_artifacts(true); + } let artifact = match &filtered[..] { [] => None, @@ -539,18 +605,24 @@ fn get_artifact_code( .running_artifact .as_ref() .and_then(|running| { - // Firstly filter by version - filtered.retain(|(id, _)| id.version == running.version); - - // Return artifact if only one matched - if filtered.len() == 1 { - return Some(filtered[0]); + // Only filter by running version if user did NOT specify a version + if version.is_none() { + filtered.retain(|(id, _)| id.version == running.version); + + // Return artifact if only one matched + if filtered.len() == 1 { + return Some(filtered[0]); + } } - // Try filtering by profile as well - filtered.retain(|(id, _)| id.profile == running.profile); + // Only filter by running profile if user did NOT specify a profile + if profile.is_none() { + filtered.retain(|(id, _)| id.profile == running.profile); - (filtered.len() == 1).then(|| filtered[0]) + return (filtered.len() == 1).then(|| filtered[0]); + } + + None }) .ok_or_else(|| fmt_err!("multiple matching artifacts found")), ) @@ -962,6 +1034,11 @@ mod tests { use super::*; use crate::CheatsConfig; use alloy_primitives::{address, b256}; + use foundry_common::ContractsByArtifact; + use foundry_compilers::{ + ArtifactId, + artifacts::{BytecodeObject, CompactBytecode, CompactContractBytecode}, + }; use foundry_evm_core::evm::TempoEvmNetwork; use std::{env, fs as stdfs, sync::Arc}; @@ -1038,6 +1115,182 @@ mod tests { assert!(err.contains("expected bytecode, found unlinked bytecode with placeholder")); } + #[test] + fn test_parse_artifact_path_file_only() { + let parsed = super::parse_artifact_path("path/to/Contract.sol").unwrap(); + assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol"))); + assert_eq!(parsed.contract_name, None); + assert_eq!(parsed.version, None); + assert_eq!(parsed.profile, None); + } + + #[test] + fn test_parse_artifact_path_file_and_contract() { + let parsed = super::parse_artifact_path("path/to/Contract.sol:MyContract").unwrap(); + assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol"))); + assert_eq!(parsed.contract_name, Some("MyContract")); + assert_eq!(parsed.version, None); + assert_eq!(parsed.profile, None); + } + + #[test] + fn test_parse_artifact_path_file_contract_version() { + let parsed = super::parse_artifact_path("path/to/Contract.sol:MyContract:0.8.23").unwrap(); + assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol"))); + assert_eq!(parsed.contract_name, Some("MyContract")); + assert_eq!(parsed.version, Some(semver::Version::new(0, 8, 23))); + assert_eq!(parsed.profile, None); + } + + #[test] + fn test_parse_artifact_path_file_contract_profile() { + let parsed = + super::parse_artifact_path("path/to/Contract.sol:MyContract:optimized").unwrap(); + assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol"))); + assert_eq!(parsed.contract_name, Some("MyContract")); + assert_eq!(parsed.version, None); + assert_eq!(parsed.profile, Some("optimized")); + } + + #[test] + fn test_parse_artifact_path_file_and_version() { + let parsed = super::parse_artifact_path("path/to/Contract.sol:0.8.18").unwrap(); + assert_eq!(parsed.file, Some(PathBuf::from("path/to/Contract.sol"))); + assert_eq!(parsed.contract_name, None); + assert_eq!(parsed.version, Some(semver::Version::new(0, 8, 18))); + assert_eq!(parsed.profile, None); + } + + #[test] + fn test_parse_artifact_path_file_and_profile() { + // The parser keeps the two-part file form ambiguous. Artifact lookup can resolve this + // segment as a profile when no contract name matches. + let parsed = super::parse_artifact_path("Contract.sol:paris").unwrap(); + assert_eq!(parsed.file, Some(PathBuf::from("Contract.sol"))); + assert_eq!(parsed.contract_name, Some("paris")); + assert_eq!(parsed.version, None); + assert_eq!(parsed.profile, None); + } + + fn test_artifact( + source: &str, + name: &str, + profile: &str, + bytecode: Bytes, + ) -> (ArtifactId, CompactContractBytecode) { + ( + ArtifactId { + path: PathBuf::from(format!("{source}/{name}.json")), + name: name.to_owned(), + source: PathBuf::from(source), + version: Version::new(0, 8, 30), + build_id: String::new(), + profile: profile.to_owned(), + }, + CompactContractBytecode { + abi: Some(Default::default()), + bytecode: Some(CompactBytecode { + object: BytecodeObject::Bytecode(bytecode), + source_map: None, + link_references: Default::default(), + }), + deployed_bytecode: None, + }, + ) + } + + #[test] + fn test_get_artifact_code_resolves_file_profile_ambiguity() { + let default_bytecode = Bytes::from_static(&[0x60, 0x01]); + let paris_bytecode = Bytes::from_static(&[0x60, 0x02]); + let source = "src/GetCodeProfile.t.sol"; + let artifacts = ContractsByArtifact::new([ + test_artifact(source, "GetCodeProfile", "default", default_bytecode), + test_artifact(source, "GetCodeProfile", "paris", paris_bytecode.clone()), + ]); + let config = CheatsConfig { + available_artifacts: Some(artifacts), + root: PathBuf::from(&env!("CARGO_MANIFEST_DIR")), + ..Default::default() + }; + let cheats: Cheatcodes = Cheatcodes::new(Arc::new(config)); + + let bytecode = + super::get_artifact_code(&cheats, "src/GetCodeProfile.t.sol:paris", false).unwrap(); + + assert_eq!(bytecode, paris_bytecode); + } + + #[test] + fn test_get_artifact_code_prefers_contract_name_over_file_profile_ambiguity() { + let profile_bytecode = Bytes::from_static(&[0x60, 0x02]); + let contract_bytecode = Bytes::from_static(&[0x60, 0x03]); + let source = "src/GetCodeProfile.t.sol"; + let artifacts = ContractsByArtifact::new([ + test_artifact(source, "GetCodeProfile", "paris", profile_bytecode), + test_artifact(source, "paris", "default", contract_bytecode.clone()), + ]); + let config = CheatsConfig { + available_artifacts: Some(artifacts), + root: PathBuf::from(&env!("CARGO_MANIFEST_DIR")), + ..Default::default() + }; + let cheats: Cheatcodes = Cheatcodes::new(Arc::new(config)); + + let bytecode = + super::get_artifact_code(&cheats, "src/GetCodeProfile.t.sol:paris", false).unwrap(); + + assert_eq!(bytecode, contract_bytecode); + } + + #[test] + fn test_parse_artifact_path_contract_only() { + let parsed = super::parse_artifact_path("MyContract").unwrap(); + assert_eq!(parsed.file, None); + assert_eq!(parsed.contract_name, Some("MyContract")); + assert_eq!(parsed.version, None); + assert_eq!(parsed.profile, None); + } + + #[test] + fn test_parse_artifact_path_contract_and_version() { + let parsed = super::parse_artifact_path("MyContract:0.8.23").unwrap(); + assert_eq!(parsed.file, None); + assert_eq!(parsed.contract_name, Some("MyContract")); + assert_eq!(parsed.version, Some(semver::Version::new(0, 8, 23))); + assert_eq!(parsed.profile, None); + } + + #[test] + fn test_parse_artifact_path_contract_and_profile() { + let parsed = super::parse_artifact_path("MyContract:optimized").unwrap(); + assert_eq!(parsed.file, None); + assert_eq!(parsed.contract_name, Some("MyContract")); + assert_eq!(parsed.version, None); + assert_eq!(parsed.profile, Some("optimized")); + } + + #[test] + fn test_parse_artifact_path_profile_names() { + // Test various profile name patterns + for profile in ["v1", "v2", "paris", "optimized", "default", "prod", "dev"] { + let path = format!("MyContract:{profile}"); + let parsed = super::parse_artifact_path(&path).unwrap(); + assert_eq!(parsed.contract_name, Some("MyContract")); + assert_eq!(parsed.profile, Some(profile)); + assert_eq!(parsed.version, None); + } + } + + #[test] + fn test_parse_artifact_path_invalid_version() { + // Invalid semver should be treated as profile + let parsed = super::parse_artifact_path("MyContract:invalid").unwrap(); + assert_eq!(parsed.contract_name, Some("MyContract")); + assert_eq!(parsed.profile, Some("invalid")); + assert_eq!(parsed.version, None); + } + fn unique_temp_dir(prefix: &str) -> PathBuf { env::temp_dir().join(format!( "foundry-cheatcodes-{prefix}-{}", diff --git a/testdata/foundry.toml b/testdata/foundry.toml index 6307668074860..4ffa18d3b8e38 100644 --- a/testdata/foundry.toml +++ b/testdata/foundry.toml @@ -19,11 +19,11 @@ ignored_error_codes = [ extra_output = ["storageLayout"] additional_compiler_profiles = [ - # paris + # paris - EVM version paris for testing profile selection { name = "paris", evm_version = "paris" }, ] compilation_restrictions = [ - # paris + # paris - files in paris/ directory use paris EVM version { paths = "paris/**", evm_version = "paris" }, ] diff --git a/testdata/multi-version/cheats/GetCode.t.sol b/testdata/multi-version/cheats/GetCode.t.sol index ecb78876cb053..e1b09c75e8b91 100644 --- a/testdata/multi-version/cheats/GetCode.t.sol +++ b/testdata/multi-version/cheats/GetCode.t.sol @@ -15,8 +15,15 @@ contract GetCodeTest is Test { } function testGetCodeByNameMultiVersion() public { - assertEq(vm.getCode("Counter"), type(Counter).creationCode); - require(keccak256(vm.getCode("Counter")) != keccak256(vm.getCode("Counter:0.8.17")), "Invalid artifact"); - assertEq(vm.getCode("Counter"), vm.getCode("Counter:0.8.18")); + // Use full path to avoid ambiguity with other Counter contracts in testdata + assertEq(vm.getCode("multi-version/Counter.sol:Counter"), type(Counter).creationCode); + require( + keccak256(vm.getCode("multi-version/Counter.sol:Counter")) + != keccak256(vm.getCode("multi-version/Counter.sol:Counter:0.8.17")), + "Invalid artifact" + ); + assertEq( + vm.getCode("multi-version/Counter.sol:Counter"), vm.getCode("multi-version/Counter.sol:Counter:0.8.18") + ); } } diff --git a/testdata/multi-version/cheats/GetCode17.t.sol b/testdata/multi-version/cheats/GetCode17.t.sol index 3cdd42ef3c78a..43d5add93186f 100644 --- a/testdata/multi-version/cheats/GetCode17.t.sol +++ b/testdata/multi-version/cheats/GetCode17.t.sol @@ -16,8 +16,13 @@ contract GetCodeTest17 is Test { } function testGetCodeByNameMultiVersion() public { - assertEq(vm.getCode("Counter"), type(Counter).creationCode); - require(keccak256(vm.getCode("Counter")) != keccak256(vm.getCode("Counter:0.8.18")), "Invalid artifact"); - assertEq(vm.getCode("Counter.sol"), vm.getCode("Counter:0.8.17")); + // Use full path to avoid ambiguity with other Counter contracts in testdata + assertEq(vm.getCode("multi-version/Counter.sol:Counter"), type(Counter).creationCode); + require( + keccak256(vm.getCode("multi-version/Counter.sol:Counter")) + != keccak256(vm.getCode("multi-version/Counter.sol:Counter:0.8.18")), + "Invalid artifact" + ); + assertEq(vm.getCode("multi-version/Counter.sol"), vm.getCode("multi-version/Counter.sol:Counter:0.8.17")); } } diff --git a/testdata/paris/cheats/GetCodeProfile.t.sol b/testdata/paris/cheats/GetCodeProfile.t.sol new file mode 100644 index 0000000000000..3c61610b42575 --- /dev/null +++ b/testdata/paris/cheats/GetCodeProfile.t.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.18; + +import "utils/Test.sol"; + +/// @notice A contract used to test profile-based artifact selection. +contract ParisProfileContract { + uint256 public value = 42; + + function getValue() public view returns (uint256) { + return value; + } +} + +/// @notice Tests vm.getCode with compilation profile parameter. +/// The paris directory uses the "paris" profile which has evm_version = "paris". +/// This test verifies that the profile can be used to select artifacts. +contract GetCodeProfileTest is Test { + function testGetCodeByProfile() public { + // Get code for a contract compiled with the paris profile + // The format is: path:ContractName:profile + bytes memory code = vm.getCode("paris/cheats/GetCodeProfile.t.sol:ParisProfileContract:paris"); + assertGt(code.length, 0, "should get bytecode for paris profile"); + assertEq(code, type(ParisProfileContract).creationCode, "bytecode should match"); + } + + function testGetCodeByContractNameAndProfile() public { + // Get code by contract name and profile + bytes memory code = vm.getCode("ParisProfileContract:paris"); + assertGt(code.length, 0, "should get bytecode for paris profile by name"); + assertEq(code, type(ParisProfileContract).creationCode, "bytecode should match"); + } + + function testGetDeployedCodeByProfile() public { + // Get deployed code by profile + bytes memory code = vm.getDeployedCode("ParisProfileContract:paris"); + assertGt(code.length, 0, "should get deployed bytecode for paris profile"); + } + + function testDeployCodeByProfile() public { + // Deploy using profile + address deployed = vm.deployCode("ParisProfileContract:paris"); + assertGt(deployed.code.length, 0, "should deploy contract"); + } + + function testGetCodeWrongProfile() public { + // Trying to get code with non-existent profile should fail + vm._expectCheatcodeRevert("no matching artifact found"); + vm.getCode("ParisProfileContract:nonexistent"); + } + + function testGetCodeDefaultProfileFails() public { + // The paris contract is ONLY compiled with paris profile, + // so requesting "default" profile should fail + vm._expectCheatcodeRevert("no matching artifact found"); + vm.getCode("ParisProfileContract:default"); + } +}