diff --git a/Cargo.lock b/Cargo.lock index 9fb3f8266db95..ee7af3a9562b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4910,8 +4910,10 @@ dependencies = [ "parking_lot", "rand 0.9.2", "regex", + "reqwest", "serde_json", "snapbox", + "svm-rs", "tempfile", "tokio", "tracing", diff --git a/Cargo.toml b/Cargo.toml index 51c58f761ba8e..8646fb4403b59 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -215,6 +215,9 @@ foundry-compilers = { version = "0.19.1", default-features = false, features = [ foundry-fork-db = "0.18" solang-parser = { version = "=0.3.9", package = "foundry-solang-parser" } solar = { package = "solar-compiler", version = "=0.1.7", default-features = false } +svm = { package = "svm-rs", version = "0.5", default-features = false, features = [ + "rustls", +] } ## alloy alloy-consensus = { version = "1.0.23", default-features = false } diff --git a/crates/forge/Cargo.toml b/crates/forge/Cargo.toml index ede355791a820..c7a4b63ad45d6 100644 --- a/crates/forge/Cargo.toml +++ b/crates/forge/Cargo.toml @@ -111,9 +111,7 @@ mockall = "0.13" globset = "0.4" paste = "1.0" similar-asserts.workspace = true -svm = { package = "svm-rs", version = "0.5", default-features = false, features = [ - "rustls", -] } +svm.workspace = true tempfile.workspace = true alloy-signer-local.workspace = true diff --git a/crates/forge/tests/cli/verify.rs b/crates/forge/tests/cli/verify.rs index 65e951b1d73de..00de79d08c9ca 100644 --- a/crates/forge/tests/cli/verify.rs +++ b/crates/forge/tests/cli/verify.rs @@ -70,13 +70,12 @@ contract Verify is Unique { ); } -#[expect(clippy::disallowed_macros)] fn parse_verification_result(cmd: &mut TestCommand, retries: u32) -> eyre::Result<()> { // Give Etherscan some time to verify the contract. Retry::new(retries, Duration::from_secs(30)).run(|| -> eyre::Result<()> { let output = cmd.execute(); let out = String::from_utf8_lossy(&output.stdout); - println!("{out}"); + test_debug!("{out}"); if out.contains("Contract successfully verified") { return Ok(()); } @@ -155,11 +154,10 @@ fn deploy_contract( .unwrap_or_else(|| panic!("Failed to parse deployer {output}")) } -#[expect(clippy::disallowed_macros)] fn verify_on_chain(info: Option, prj: TestProject, mut cmd: TestCommand) { // only execute if keys present if let Some(info) = info { - println!("verifying on {}", info.chain); + test_debug!("verifying on {}", info.chain); let contract_path = "src/Verify.sol:Verify"; let address = deploy_contract(&info, contract_path, prj, &mut cmd); @@ -186,11 +184,10 @@ fn verify_on_chain(info: Option, prj: TestProject, mut cmd: Te } } -#[expect(clippy::disallowed_macros)] fn guess_constructor_args(info: Option, prj: TestProject, mut cmd: TestCommand) { // only execute if keys present if let Some(info) = info { - println!("verifying on {}", info.chain); + test_debug!("verifying on {}", info.chain); add_unique(&prj); add_verify_target_with_constructor(&prj); @@ -227,12 +224,11 @@ fn guess_constructor_args(info: Option, prj: TestProject, mut } } -#[expect(clippy::disallowed_macros)] /// Executes create --verify on the given chain fn create_verify_on_chain(info: Option, prj: TestProject, mut cmd: TestCommand) { // only execute if keys present if let Some(info) = info { - println!("verifying on {}", info.chain); + test_debug!("verifying on {}", info.chain); add_single_verify_target_file(&prj); let contract_path = "src/Verify.sol:Verify"; diff --git a/crates/forge/tests/it/cheats.rs b/crates/forge/tests/it/cheats.rs index 02a0625f795d5..1271f534cdc5d 100644 --- a/crates/forge/tests/it/cheats.rs +++ b/crates/forge/tests/it/cheats.rs @@ -3,14 +3,14 @@ use crate::{ config::*, test_helpers::{ ForgeTestData, ForgeTestProfile, RE_PATH_SEPARATOR, TEST_DATA_DEFAULT, - TEST_DATA_MULTI_VERSION, TEST_DATA_PARIS, get_compiled, + TEST_DATA_MULTI_VERSION, TEST_DATA_PARIS, }, }; use alloy_primitives::U256; use foundry_cli::utils::install_crypto_provider; use foundry_compilers::artifacts::output_selection::ContractOutputSelection; use foundry_config::{FsPermissions, fs_permissions::PathPermission}; -use foundry_test_utils::{Filter, init_tracing}; +use foundry_test_utils::{Filter, init_tracing, util::get_compiled}; /// Executes all cheat code tests but not fork cheat codes or tests that require isolation mode or /// specific seed. diff --git a/crates/forge/tests/it/test_helpers.rs b/crates/forge/tests/it/test_helpers.rs index 59139600f78a6..dc23a95d58a35 100644 --- a/crates/forge/tests/it/test_helpers.rs +++ b/crates/forge/tests/it/test_helpers.rs @@ -5,10 +5,9 @@ use alloy_primitives::U256; use forge::{MultiContractRunner, MultiContractRunnerBuilder}; use foundry_cli::utils::install_crypto_provider; use foundry_compilers::{ - Project, ProjectCompileOutput, SolcConfig, Vyper, + Project, ProjectCompileOutput, SolcConfig, artifacts::{EvmVersion, Libraries, Settings}, compilers::multi::MultiCompiler, - utils::RuntimeOrHandle, }; use foundry_config::{ Config, FsPermissions, FuzzConfig, FuzzCorpusConfig, FuzzDictionaryConfig, InvariantConfig, @@ -16,21 +15,19 @@ use foundry_config::{ }; use foundry_evm::{constants::CALLER, opts::EvmOpts}; use foundry_test_utils::{ - fd_lock, init_tracing, + init_tracing, rpc::{next_http_archive_rpc_url, next_rpc_endpoint}, - test_debug, + util::get_compiled, }; use revm::primitives::hardfork::SpecId; use std::{ env, fmt, - io::Write, path::{Path, PathBuf}, sync::{Arc, LazyLock}, }; pub const RE_PATH_SEPARATOR: &str = "/"; const TESTDATA: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../testdata"); -static VYPER: LazyLock = LazyLock::new(|| std::env::temp_dir().join("vyper")); /// Profile for the tests group. Used to configure separate configurations for test runs. pub enum ForgeTestProfile { @@ -256,84 +253,6 @@ impl ForgeTestData { } } -/// Installs Vyper if it's not already present. -pub fn get_vyper() -> Vyper { - if let Ok(vyper) = Vyper::new("vyper") { - return vyper; - } - if let Ok(vyper) = Vyper::new(&*VYPER) { - return vyper; - } - RuntimeOrHandle::new().block_on(async { - #[cfg(target_family = "unix")] - use std::{fs::Permissions, os::unix::fs::PermissionsExt}; - - let suffix = match svm::platform() { - svm::Platform::MacOsAarch64 => "darwin", - svm::Platform::LinuxAmd64 => "linux", - svm::Platform::WindowsAmd64 => "windows.exe", - platform => panic!( - "unsupported platform {platform:?} for installing vyper, \ - install it manually and add it to $PATH" - ), - }; - let url = format!("https://github.com/vyperlang/vyper/releases/download/v0.4.3/vyper.0.4.3+commit.bff19ea2.{suffix}"); - - test_debug!("downloading vyper from {url}"); - let res = reqwest::Client::builder().build().unwrap().get(url).send().await.unwrap(); - - assert!(res.status().is_success()); - - let bytes = res.bytes().await.unwrap(); - - std::fs::write(&*VYPER, bytes).unwrap(); - - #[cfg(target_family = "unix")] - std::fs::set_permissions(&*VYPER, Permissions::from_mode(0o755)).unwrap(); - - Vyper::new(&*VYPER).unwrap() - }) -} - -#[tracing::instrument] -pub fn get_compiled(project: &mut Project) -> ProjectCompileOutput { - let lock_file_path = project.sources_path().join(".lock"); - // Compile only once per test run. - // We need to use a file lock because `cargo-nextest` runs tests in different processes. - // This is similar to [`foundry_test_utils::util::initialize`], see its comments for more - // details. - let mut lock = fd_lock::new_lock(&lock_file_path); - let read = lock.read().unwrap(); - let out; - - let mut write = None; - if !project.cache_path().exists() || std::fs::read(&lock_file_path).unwrap() != b"1" { - drop(read); - write = Some(lock.write().unwrap()); - test_debug!("cache miss for {}", lock_file_path.display()); - } else { - test_debug!("cache hit for {}", lock_file_path.display()); - } - - if project.compiler.vyper.is_none() { - project.compiler.vyper = Some(get_vyper()); - } - - test_debug!("compiling {}", lock_file_path.display()); - out = project.compile().unwrap(); - test_debug!("compiled {}", lock_file_path.display()); - - if out.has_compiler_errors() { - panic!("Compiled with errors:\n{out}"); - } - - if let Some(ref mut write) = write { - write.write_all(b"1").unwrap(); - } - - out -} - /// Default data for the tests group. pub static TEST_DATA_DEFAULT: LazyLock = LazyLock::new(|| ForgeTestData::new(ForgeTestProfile::Default)); diff --git a/crates/test-utils/Cargo.toml b/crates/test-utils/Cargo.toml index c8f4184af73c0..041409fa78227 100644 --- a/crates/test-utils/Cargo.toml +++ b/crates/test-utils/Cargo.toml @@ -33,6 +33,8 @@ rand.workspace = true snapbox = { version = "0.6", features = ["json", "regex", "term-svg"] } tempfile.workspace = true ui_test = "0.30.2" +reqwest.workspace = true +svm.workspace = true # Pinned dependencies. See /Cargo.toml. [target.'cfg(any())'.dependencies] diff --git a/crates/test-utils/src/fd_lock.rs b/crates/test-utils/src/fd_lock.rs index 1c5a479cd5efd..a131648f325a6 100644 --- a/crates/test-utils/src/fd_lock.rs +++ b/crates/test-utils/src/fd_lock.rs @@ -19,3 +19,9 @@ pub fn new_lock(lock_path: impl AsRef) -> RwLock { } new_lock(lock_path.as_ref()) } + +pub(crate) const LOCK_TOKEN: &[u8] = b"1"; + +pub(crate) fn lock_exists(lock_path: &Path) -> bool { + std::fs::read(lock_path).is_ok_and(|b| b == LOCK_TOKEN) +} diff --git a/crates/test-utils/src/util.rs b/crates/test-utils/src/util.rs index de910cb9a48ae..b2da96b03a7c3 100644 --- a/crates/test-utils/src/util.rs +++ b/crates/test-utils/src/util.rs @@ -1,12 +1,14 @@ use crate::init_tracing; use eyre::{Result, WrapErr}; use foundry_compilers::{ - ArtifactOutput, ConfigurableArtifacts, PathStyle, ProjectPathsConfig, + ArtifactOutput, ConfigurableArtifacts, PathStyle, Project, ProjectCompileOutput, + ProjectPathsConfig, Vyper, artifacts::Contract, cache::CompilerCache, compilers::multi::MultiCompiler, project_util::{TempProject, copy_dir}, solc::SolcSettings, + utils::RuntimeOrHandle, }; use foundry_config::Config; use parking_lot::Mutex; @@ -174,7 +176,7 @@ impl ExtTester { if self.rev.is_empty() { let mut git = Command::new("git"); git.current_dir(root).args(["log", "-n", "1"]); - println!("$ {git:?}"); + test_debug!("$ {git:?}"); let output = git.output().unwrap(); if !output.status.success() { panic!("git log failed: {output:?}"); @@ -185,7 +187,7 @@ impl ExtTester { } else { let mut git = Command::new("git"); git.current_dir(root).args(["checkout", self.rev]); - println!("$ {git:?}"); + test_debug!("$ {git:?}"); let status = git.status().unwrap(); if !status.success() { panic!("git checkout failed: {status}"); @@ -199,10 +201,10 @@ impl ExtTester { for install_command in &self.install_commands { let mut install_cmd = Command::new(&install_command[0]); install_cmd.args(&install_command[1..]).current_dir(root); - println!("cd {root}; {install_cmd:?}"); + test_debug!("cd {root}; {install_cmd:?}"); match install_cmd.status() { Ok(s) => { - println!("\n\n{install_cmd:?}: {s}"); + test_debug!("\n\n{install_cmd:?}: {s}"); if s.success() { break; } @@ -259,9 +261,8 @@ impl ExtTester { /// test can initialize the template at a time. /// /// This sets the project's solc version to the [`SOLC_VERSION`]. -#[expect(clippy::disallowed_macros)] pub fn initialize(target: &Path) { - println!("initializing {}", target.display()); + test_debug!("initializing {}", target.display()); let tpath = TEMPLATE_PATH.as_path(); pretty_err(tpath, fs::create_dir_all(tpath)); @@ -269,7 +270,7 @@ pub fn initialize(target: &Path) { // Initialize the global template if necessary. let mut lock = crate::fd_lock::new_lock(TEMPLATE_LOCK.as_path()); let mut _read = lock.read().unwrap(); - if fs::read(&*TEMPLATE_LOCK).unwrap() != b"1" { + if !crate::fd_lock::lock_exists(TEMPLATE_LOCK.as_path()) { // We are the first to acquire the lock: // - initialize a new empty temp project; // - run `forge init`; @@ -280,16 +281,14 @@ pub fn initialize(target: &Path) { // Release the read lock and acquire a write lock, initializing the lock file. drop(_read); - let mut write = lock.write().unwrap(); - let mut data = String::new(); - write.read_to_string(&mut data).unwrap(); - - if data != "1" { + let mut data = Vec::new(); + write.read_to_end(&mut data).unwrap(); + if data != crate::fd_lock::LOCK_TOKEN { // Initialize and build. let (prj, mut cmd) = setup_forge("template", foundry_compilers::PathStyle::Dapptools); - println!("- initializing template dir in {}", prj.root().display()); + test_debug!("- initializing template dir in {}", prj.root().display()); cmd.args(["init", "--force"]).assert_success(); prj.write_config(Config { @@ -317,7 +316,7 @@ pub fn initialize(target: &Path) { // Update lockfile to mark that template is initialized. write.set_len(0).unwrap(); write.seek(std::io::SeekFrom::Start(0)).unwrap(); - write.write_all(b"1").unwrap(); + write.write_all(crate::fd_lock::LOCK_TOKEN).unwrap(); } // Release the write lock and acquire a new read lock. @@ -325,22 +324,114 @@ pub fn initialize(target: &Path) { _read = lock.read().unwrap(); } - println!("- copying template dir from {}", tpath.display()); + test_debug!("- copying template dir from {}", tpath.display()); pretty_err(target, fs::create_dir_all(target)); pretty_err(target, copy_dir(tpath, target)); } +/// Compile the project with a lock for the cache. +pub fn get_compiled(project: &mut Project) -> ProjectCompileOutput { + let lock_file_path = project.sources_path().join(".lock"); + // We need to use a file lock because `cargo-nextest` runs tests in different processes. + // This is similar to `initialize`, see its comments for more details. + let mut lock = crate::fd_lock::new_lock(&lock_file_path); + let read = lock.read().unwrap(); + let out; + + let mut write = None; + if !project.cache_path().exists() || !crate::fd_lock::lock_exists(&lock_file_path) { + drop(read); + write = Some(lock.write().unwrap()); + test_debug!("cache miss for {}", lock_file_path.display()); + } else { + test_debug!("cache hit for {}", lock_file_path.display()); + } + + if project.compiler.vyper.is_none() { + project.compiler.vyper = Some(get_vyper()); + } + + test_debug!("compiling {}", lock_file_path.display()); + out = project.compile().unwrap(); + test_debug!("compiled {}", lock_file_path.display()); + + if out.has_compiler_errors() { + panic!("Compiled with errors:\n{out}"); + } + + if let Some(write) = &mut write { + write.write_all(crate::fd_lock::LOCK_TOKEN).unwrap(); + } + + out +} + +/// Installs Vyper if it's not already present. +pub fn get_vyper() -> Vyper { + static VYPER: LazyLock = LazyLock::new(|| std::env::temp_dir().join("vyper")); + + if let Ok(vyper) = Vyper::new("vyper") { + return vyper; + } + if let Ok(vyper) = Vyper::new(&*VYPER) { + return vyper; + } + return RuntimeOrHandle::new().block_on(install()); + + async fn install() -> Vyper { + #[cfg(target_family = "unix")] + use std::{fs::Permissions, os::unix::fs::PermissionsExt}; + + let path = VYPER.as_path(); + let mut file = File::create(path).unwrap(); + if let Err(e) = file.try_lock() { + if let fs::TryLockError::WouldBlock = e { + file.lock().unwrap(); + assert!(path.exists()); + return Vyper::new(path).unwrap(); + } + file.lock().unwrap(); + } + + let suffix = match svm::platform() { + svm::Platform::MacOsAarch64 => "darwin", + svm::Platform::LinuxAmd64 => "linux", + svm::Platform::WindowsAmd64 => "windows.exe", + platform => panic!( + "unsupported platform {platform:?} for installing vyper, \ + install it manually and add it to $PATH" + ), + }; + let url = format!( + "https://github.com/vyperlang/vyper/releases/download/v0.4.3/vyper.0.4.3+commit.bff19ea2.{suffix}" + ); + + test_debug!("downloading vyper from {url}"); + let res = reqwest::Client::builder().build().unwrap().get(url).send().await.unwrap(); + + assert!(res.status().is_success()); + + let bytes = res.bytes().await.unwrap(); + + file.write_all(&bytes).unwrap(); + + #[cfg(target_family = "unix")] + file.set_permissions(Permissions::from_mode(0o755)).unwrap(); + + Vyper::new(path).unwrap() + } +} + /// Clones a remote repository into the specified directory. Panics if the command fails. pub fn clone_remote(repo_url: &str, target_dir: &str) { let mut cmd = Command::new("git"); cmd.args(["clone", "--recursive", "--shallow-submodules"]); cmd.args([repo_url, target_dir]); - println!("{cmd:?}"); + test_debug!("{cmd:?}"); let status = cmd.status().unwrap(); if !status.success() { panic!("git clone failed: {status}"); } - println!(); } /// Setup an empty test project and return a command pointing to the forge @@ -1036,7 +1127,7 @@ impl TestCommand { #[track_caller] pub fn try_execute(&mut self) -> std::io::Result { - println!("executing {:?}", self.cmd); + test_debug!("executing {:?}", self.cmd); let mut child = self.cmd.stdout(Stdio::piped()).stderr(Stdio::piped()).stdin(Stdio::piped()).spawn()?; if let Some(fun) = self.stdin_fun.take() {