From 280a0e635562e20bbc752815140e007618daa204 Mon Sep 17 00:00:00 2001 From: Bas van Gijzel Date: Sun, 12 Jul 2026 22:02:55 +0000 Subject: [PATCH 1/2] perf(test-harness): clone and install each project once per run The Foundry and Hardhat runners re-cloned and re-installed every project for each (compiler x codegen) pair: 4x per project on the PR matrix, 8x in full mode. The only toolchain-dependent inputs are the pragma and config-version seds, whose patterns match their own output, so the checkout is reused and only the seds re-run per toolchain. Three guards replace what the fresh clone provided implicitly: - the sed glob skips node_modules, keeping dependency pragmas untouched now that installs precede the per-toolchain sed; - the Hardhat config file is restored from a pristine snapshot before each sed, since that sed consumes the committed literal version; - Hardhat's cache and artifacts directories and any stale junit report are deleted per toolchain, as its cache cannot distinguish two solx binaries reporting the same base solc version, and a leftover report would mask a test run that produced none. --- solx-dev/src/test/foundry/mod.rs | 153 +++++++++--------- solx-dev/src/test/hardhat/mod.rs | 262 ++++++++++++++++++------------- 2 files changed, 238 insertions(+), 177 deletions(-) diff --git a/solx-dev/src/test/foundry/mod.rs b/solx-dev/src/test/foundry/mod.rs index 5df9ddd06..3825c5d01 100644 --- a/solx-dev/src/test/foundry/mod.rs +++ b/solx-dev/src/test/foundry/mod.rs @@ -89,31 +89,88 @@ pub fn test( let mut project_directory = crate::utils::absolute_path(projects_directory.as_path())?; project_directory.push(project_name.as_str()); + crate::utils::remove(project_directory.as_path(), project_name.as_str())?; + + let project_directory_str = project_directory.to_string_lossy(); + crate::utils::clone_repository( + project.url.as_str(), + &project_directory_str, + project.commit.as_deref(), + &format!( + "{} Foundry project {}", + solx_utils::cargo_status_ok("Cloning"), + project_name.bright_white().bold() + ), + )?; + + if project.requires_yarn { + crate::utils::exists("npm")?; + + let build_system = "yarn"; + let yarn_version = config.build_systems.get(build_system).ok_or_else(|| { + anyhow::anyhow!("Foundry test configuration missing `build_systems.{build_system}`") + })?; + let npm_spec = format!("{build_system}@{yarn_version}"); + let mut npm_install_yarn = Command::new("npm"); + npm_install_yarn.current_dir(project_directory.as_path()); + npm_install_yarn.arg("install"); + npm_install_yarn.args(["--loglevel", "error"]); + npm_install_yarn.arg("--force"); + npm_install_yarn.arg("--yes"); + npm_install_yarn.arg("--global"); + npm_install_yarn.arg(&npm_spec); + crate::utils::command_with_retries( + &mut npm_install_yarn, + format!( + "{} build system {} for Foundry project {project_name}", + solx_utils::cargo_status_ok("Installing"), + build_system.bright_yellow().bold() + ) + .as_str(), + 16, + )?; + let mut yarn_install_command = Command::new(build_system); + yarn_install_command.args(["--cwd", &*project_directory_str]); + yarn_install_command.arg("install"); + yarn_install_command.arg("--silent"); + crate::utils::command_with_retries( + &mut yarn_install_command, + format!( + "{} dependencies for Foundry project {project_name}", + solx_utils::cargo_status_ok("Installing") + ) + .as_str(), + 16, + )?; + } + + let config_file_name = "foundry.toml"; + let mut forge_config_fix_command = Command::new("forge"); + forge_config_fix_command.current_dir(project_directory.as_path()); + forge_config_fix_command.arg("config"); + forge_config_fix_command.arg("--fix"); + crate::utils::command( + &mut forge_config_fix_command, + format!( + "{} the configuration file {} of Foundry project {}", + solx_utils::cargo_status_ok("Fixing"), + config_file_name.bright_white().bold(), + project_name.bright_white().bold(), + ) + .as_str(), + )?; + for ((identifier, compiler), codegen) in config .compilers .iter() .filter(|(_identifier, compiler)| !compiler.disabled) .cartesian_product(crate::test::CODEGENS) { - crate::utils::remove(project_directory.as_path(), project_name.as_str())?; - let solidity_version = compiler .solidity_version .as_deref() .unwrap_or(solidity_version.as_str()); - let project_directory_str = project_directory.to_string_lossy(); - crate::utils::clone_repository( - project.url.as_str(), - &project_directory_str, - project.commit.as_deref(), - &format!( - "{} Foundry project {}", - solx_utils::cargo_status_ok("Cloning"), - project_name.bright_white().bold() - ), - )?; - eprintln!( "{} pragmas in Foundry project {}", solx_utils::cargo_status_ok("Fixing"), @@ -127,6 +184,14 @@ pub fn test( if !solidity_file.is_file() { continue; } + // Dependencies keep their own pragmas, as they did when the + // install followed the sed in every fresh clone. + if solidity_file + .components() + .any(|component| component.as_os_str() == "node_modules") + { + continue; + } crate::utils::sed_file( solidity_file.as_path(), &[ @@ -136,64 +201,8 @@ pub fn test( )?; } - if project.requires_yarn { - crate::utils::exists("npm")?; - - let build_system = "yarn"; - let yarn_version = config.build_systems.get(build_system).ok_or_else(|| { - anyhow::anyhow!( - "Foundry test configuration missing `build_systems.{build_system}`" - ) - })?; - let npm_spec = format!("{build_system}@{yarn_version}"); - let mut npm_install_yarn = Command::new("npm"); - npm_install_yarn.current_dir(project_directory.as_path()); - npm_install_yarn.arg("install"); - npm_install_yarn.args(["--loglevel", "error"]); - npm_install_yarn.arg("--force"); - npm_install_yarn.arg("--yes"); - npm_install_yarn.arg("--global"); - npm_install_yarn.arg(&npm_spec); - crate::utils::command_with_retries( - &mut npm_install_yarn, - format!( - "{} build system {} for Foundry project {project_name}", - solx_utils::cargo_status_ok("Installing"), - build_system.bright_yellow().bold() - ) - .as_str(), - 16, - )?; - let mut yarn_install_command = Command::new(build_system); - yarn_install_command.args(["--cwd", &*project_directory_str]); - yarn_install_command.arg("install"); - yarn_install_command.arg("--silent"); - crate::utils::command_with_retries( - &mut yarn_install_command, - format!( - "{} dependencies for Foundry project {project_name}", - solx_utils::cargo_status_ok("Installing") - ) - .as_str(), - 16, - )?; - } - - let config_file_name = "foundry.toml"; - let mut forge_config_fix_command = Command::new("forge"); - forge_config_fix_command.current_dir(project_directory.as_path()); - forge_config_fix_command.arg("config"); - forge_config_fix_command.arg("--fix"); - crate::utils::command( - &mut forge_config_fix_command, - format!( - "{} the configuration file {} of Foundry project {}", - solx_utils::cargo_status_ok("Fixing"), - config_file_name.bright_white().bold(), - project_name.bright_white().bold(), - ) - .as_str(), - )?; + // These patterns match their own output, so re-running them re-pins + // the version for each compiler without resetting the checkout. crate::utils::sed_file( project_directory.join(config_file_name).as_path(), &[ diff --git a/solx-dev/src/test/hardhat/mod.rs b/solx-dev/src/test/hardhat/mod.rs index dcd9ff81a..033318358 100644 --- a/solx-dev/src/test/hardhat/mod.rs +++ b/solx-dev/src/test/hardhat/mod.rs @@ -89,31 +89,136 @@ pub fn test( let mut project_directory = crate::utils::absolute_path(projects_directory.as_path())?; project_directory.push(project_name.as_str()); + crate::utils::remove(project_directory.as_path(), project_name.as_str())?; + + let project_directory_str = project_directory.to_string_lossy(); + crate::utils::clone_repository( + project.url.as_str(), + &project_directory_str, + project.commit.as_deref(), + &format!( + "{} Hardhat project {}", + solx_utils::cargo_status_ok("Cloning"), + project_name.bright_white().bold() + ), + )?; + + let build_system = project.build_system.to_string(); + if let Some(version) = config.build_systems.get(&project.build_system) { + let npm_spec = format!("{build_system}@{version}"); + let mut npm_install_build_system = Command::new("npm"); + npm_install_build_system.current_dir(project_directory.as_path()); + npm_install_build_system.args(["--loglevel", "error"]); + npm_install_build_system.arg("--force"); + npm_install_build_system.arg("--yes"); + npm_install_build_system.arg("install"); + npm_install_build_system.arg("--global"); + npm_install_build_system.arg(&npm_spec); + crate::utils::command_with_retries( + &mut npm_install_build_system, + format!( + "{} build system {} for Hardhat project {project_name}", + solx_utils::cargo_status_ok("Installing"), + build_system.bright_yellow().bold() + ) + .as_str(), + 16, + )?; + } else if project.build_system != BuildSystem::Npm { + anyhow::bail!("Hardhat test configuration missing `build_systems.{build_system}`"); + } + let mut build_system_install_command = Command::new(build_system.as_str()); + build_system_install_command.current_dir(project_directory.as_path()); + match project.build_system { + BuildSystem::Npm => { + build_system_install_command.args(["--loglevel", "error"]); + build_system_install_command.arg("--force"); + build_system_install_command.arg("--yes"); + } + BuildSystem::Pnpm => { + build_system_install_command.arg("--ignore-scripts"); + } + _ => {} + } + build_system_install_command.arg("install"); + crate::utils::command_with_retries( + &mut build_system_install_command, + format!( + "{} dependencies for Hardhat project {project_name}", + solx_utils::cargo_status_ok("Installing") + ) + .as_str(), + 16, + )?; + + let mut dependency_override_command = Command::new(build_system.as_str()); + dependency_override_command.current_dir(project_directory.as_path()); + match project.build_system { + BuildSystem::Npm => { + dependency_override_command.args(["--loglevel", "error"]); + dependency_override_command.arg("--force"); + dependency_override_command.arg("--yes"); + } + BuildSystem::Yarn => { + dependency_override_command.arg("--silent"); + } + BuildSystem::Pnpm => { + dependency_override_command.arg("--ignore-scripts"); + } + _ => {} + } + dependency_override_command.arg("install"); + dependency_override_command.args(project.dependencies.as_slice()); + dependency_override_command.arg("--save-dev"); + crate::utils::command_with_retries( + &mut dependency_override_command, + format!( + "{} dependences with {} for Hardhat project {project_name}", + solx_utils::cargo_status_ok("Overriding"), + project + .dependencies + .iter() + .map(|dependency| dependency.bright_yellow().bold()) + .join(", ") + ) + .as_str(), + 16, + )?; + + let config_file_name = if project_directory.join("hardhat.config.ts").exists() { + Some("hardhat.config.ts") + } else if project_directory.join("hardhat.config.js").exists() { + Some("hardhat.config.js") + } else { + None + }; + // The config sed consumes the committed literal version, so each + // toolchain needs the pristine file back before applying its own. + let config_file_snapshot = match config_file_name { + Some(config_file_name) => { + let config_file_path = project_directory.join(config_file_name); + Some( + std::fs::read_to_string(config_file_path.as_path()).map_err(|error| { + anyhow::anyhow!( + "Reading Hardhat configuration file {config_file_path:?}: {error}" + ) + })?, + ) + } + None => None, + }; + for ((identifier, compiler), codegen) in config .compilers .iter() .filter(|(_identifier, compiler)| !compiler.disabled) .cartesian_product(crate::test::CODEGENS) { - crate::utils::remove(project_directory.as_path(), project_name.as_str())?; - let solidity_version = compiler .solidity_version .as_deref() .unwrap_or(solidity_version.as_str()); - let project_directory_str = project_directory.to_string_lossy(); - crate::utils::clone_repository( - project.url.as_str(), - &project_directory_str, - project.commit.as_deref(), - &format!( - "{} Hardhat project {}", - solx_utils::cargo_status_ok("Cloning"), - project_name.bright_white().bold() - ), - )?; - eprintln!( "{} pragmas in Hardhat project {}", solx_utils::cargo_status_ok("Fixing"), @@ -127,6 +232,14 @@ pub fn test( if !solidity_file.is_file() { continue; } + // Dependencies keep their own pragmas, as they did when the + // install followed the sed in every fresh clone. + if solidity_file + .components() + .any(|component| component.as_os_str() == "node_modules") + { + continue; + } crate::utils::sed_file( solidity_file.as_path(), &[ @@ -136,104 +249,25 @@ pub fn test( )?; } - let build_system = project.build_system.to_string(); - if let Some(version) = config.build_systems.get(&project.build_system) { - let npm_spec = format!("{build_system}@{version}"); - let mut npm_install_build_system = Command::new("npm"); - npm_install_build_system.current_dir(project_directory.as_path()); - npm_install_build_system.args(["--loglevel", "error"]); - npm_install_build_system.arg("--force"); - npm_install_build_system.arg("--yes"); - npm_install_build_system.arg("install"); - npm_install_build_system.arg("--global"); - npm_install_build_system.arg(&npm_spec); - crate::utils::command_with_retries( - &mut npm_install_build_system, - format!( - "{} build system {} for Hardhat project {project_name}", - solx_utils::cargo_status_ok("Installing"), - build_system.bright_yellow().bold() - ) - .as_str(), - 16, - )?; - } else if project.build_system != BuildSystem::Npm { - anyhow::bail!("Hardhat test configuration missing `build_systems.{build_system}`"); - } - let mut build_system_install_command = Command::new(build_system.as_str()); - build_system_install_command.current_dir(project_directory.as_path()); - match project.build_system { - BuildSystem::Npm => { - build_system_install_command.args(["--loglevel", "error"]); - build_system_install_command.arg("--force"); - build_system_install_command.arg("--yes"); - } - BuildSystem::Pnpm => { - build_system_install_command.arg("--ignore-scripts"); - } - _ => {} - } - build_system_install_command.arg("install"); - crate::utils::command_with_retries( - &mut build_system_install_command, - format!( - "{} dependencies for Hardhat project {project_name}", - solx_utils::cargo_status_ok("Installing") - ) - .as_str(), - 16, - )?; - - let mut dependency_override_command = Command::new(build_system.as_str()); - dependency_override_command.current_dir(project_directory.as_path()); - match project.build_system { - BuildSystem::Npm => { - dependency_override_command.args(["--loglevel", "error"]); - dependency_override_command.arg("--force"); - dependency_override_command.arg("--yes"); - } - BuildSystem::Yarn => { - dependency_override_command.arg("--silent"); - } - BuildSystem::Pnpm => { - dependency_override_command.arg("--ignore-scripts"); - } - _ => {} - } - dependency_override_command.arg("install"); - dependency_override_command.args(project.dependencies.as_slice()); - dependency_override_command.arg("--save-dev"); - crate::utils::command_with_retries( - &mut dependency_override_command, - format!( - "{} dependences with {} for Hardhat project {project_name}", - solx_utils::cargo_status_ok("Overriding"), - project - .dependencies - .iter() - .map(|dependency| dependency.bright_yellow().bold()) - .join(", ") - ) - .as_str(), - 16, - )?; - - let config_file_name = if project_directory.join("hardhat.config.ts").exists() { - Some("hardhat.config.ts") - } else if project_directory.join("hardhat.config.js").exists() { - Some("hardhat.config.js") - } else { - None - }; - if let Some(config_file_name) = config_file_name { + if let (Some(config_file_name), Some(config_file_snapshot)) = + (config_file_name, config_file_snapshot.as_deref()) + { eprintln!( "{} the configuration file {} of Hardhat project {}", solx_utils::cargo_status_ok("Fixing"), config_file_name.bright_white().bold(), project_name.bright_white().bold(), ); + let config_file_path = project_directory.join(config_file_name); + std::fs::write(config_file_path.as_path(), config_file_snapshot).map_err( + |error| { + anyhow::anyhow!( + "Restoring Hardhat configuration file {config_file_path:?}: {error}" + ) + }, + )?; crate::utils::sed_file( - project_directory.join(config_file_name).as_path(), + config_file_path.as_path(), &[ format!(r#"s/version:\s*["']0.8.30["']/version: "{solidity_version}"/g"#) .as_str(), @@ -250,6 +284,17 @@ pub fn test( let toolchain_name = crate::test::toolchain_name(compiler.name.as_str(), codegen); compiler_shim.reset()?; + // Hardhat cannot tell two solx binaries reporting the same base solc + // version apart, so its cache must go before every compilation. + for stale_directory in ["cache", "artifacts"] { + let stale_path = project_directory.join(stale_directory); + if stale_path.exists() { + std::fs::remove_dir_all(stale_path.as_path()).map_err(|error| { + anyhow::anyhow!("Removing Hardhat directory {stale_path:?}: {error}") + })?; + } + } + let mut npm_compile_command = Command::new("npm"); npm_compile_command.current_dir(&*project_directory_str); npm_compile_command.arg("run"); @@ -307,6 +352,13 @@ pub fn test( npm_test_command.env(key, value); } let npm_test_report_path = project_directory.join("junit-report.json"); + // A report left by the previous toolchain would silently stand in + // for a test run that failed to produce one. + if npm_test_report_path.exists() { + std::fs::remove_file(npm_test_report_path.as_path()).map_err(|error| { + anyhow::anyhow!("Removing stale test report {npm_test_report_path:?}: {error}") + })?; + } let npm_test_report_path_str = npm_test_report_path.to_string_lossy(); npm_test_command.env("JUNIT_REPORT", &*npm_test_report_path_str); if toolchain_name.contains("solx") { From 6f725270987ba8d277cda1ffe885e89ead125478 Mon Sep 17 00:00:00 2001 From: Bas van Gijzel Date: Mon, 20 Jul 2026 08:47:00 +0000 Subject: [PATCH 2/2] fix(test-harness): reset reused checkouts with git instead of point guards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The clone-once reuse re-established per-toolchain isolation piecemeal: a config snapshot/restore, a cache+artifacts purge, and a junit-report deletion on the Hardhat side, and nothing on the Foundry side — where forge's persisted invariant counterexamples (cache/invariant) leaked into the next toolchain's run and could distort the failure-count gate in either direction. Each project checkout is now committed once after setup, and every toolchain iteration starts from that commit via a git reset: checkout of tracked files (submodules included) plus clean of untracked and ignored outputs, keeping node_modules. One primitive covers all per-toolchain state — caches, artifacts, reports, persisted fuzz failures, sed'ed sources and configs — and newly discovered stateful outputs are cleaned without needing their own deletion entry. The seds no longer need to match their own output. Also: - the pragma sed enumerates `git ls-files --recurse-submodules` instead of glob-walking the entire tree per toolchain, which traversed node_modules only to filter it out afterwards; - yarn is installed globally once per run instead of once per requires_yarn project. --- Cargo.lock | 1 - solx-dev/Cargo.toml | 1 - solx-dev/src/test/foundry/mod.rs | 100 +++++++++++++++++------------ solx-dev/src/test/hardhat/mod.rs | 93 ++++++++++----------------- solx-dev/src/utils.rs | 107 +++++++++++++++++++++++++++++++ 5 files changed, 200 insertions(+), 102 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6a63adacb..60a4830d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4477,7 +4477,6 @@ dependencies = [ "clap", "colored", "fs_extra", - "glob", "hex", "itertools 0.15.0", "path-slash", diff --git a/solx-dev/Cargo.toml b/solx-dev/Cargo.toml index 40754aabd..79cd65bf0 100644 --- a/solx-dev/Cargo.toml +++ b/solx-dev/Cargo.toml @@ -21,7 +21,6 @@ serde.workspace = true sha2.workspace = true serde_json.workspace = true regex.workspace = true -glob.workspace = true hex.workspace = true itertools.workspace = true toml = "1.0" diff --git a/solx-dev/src/test/foundry/mod.rs b/solx-dev/src/test/foundry/mod.rs index 3825c5d01..0caf1f645 100644 --- a/solx-dev/src/test/foundry/mod.rs +++ b/solx-dev/src/test/foundry/mod.rs @@ -74,7 +74,7 @@ pub fn test( ); } - for (project_name, project) in config + let projects: Vec<_> = config .projects .into_iter() .filter(|(project_name, project)| { @@ -84,7 +84,39 @@ pub fn test( .iter() .any(|element| project_name.contains(element))) }) + .collect(); + + if projects + .iter() + .any(|(_project_name, project)| project.requires_yarn) { + crate::utils::exists("npm")?; + + let build_system = "yarn"; + let yarn_version = config.build_systems.get(build_system).ok_or_else(|| { + anyhow::anyhow!("Foundry test configuration missing `build_systems.{build_system}`") + })?; + let npm_spec = format!("{build_system}@{yarn_version}"); + let mut npm_install_yarn = Command::new("npm"); + npm_install_yarn.arg("install"); + npm_install_yarn.args(["--loglevel", "error"]); + npm_install_yarn.arg("--force"); + npm_install_yarn.arg("--yes"); + npm_install_yarn.arg("--global"); + npm_install_yarn.arg(&npm_spec); + crate::utils::command_with_retries( + &mut npm_install_yarn, + format!( + "{} build system {} for Foundry projects", + solx_utils::cargo_status_ok("Installing"), + build_system.bright_yellow().bold() + ) + .as_str(), + 16, + )?; + } + + for (project_name, project) in projects { attempted_projects.push(project_name.clone()); let mut project_directory = crate::utils::absolute_path(projects_directory.as_path())?; project_directory.push(project_name.as_str()); @@ -104,32 +136,7 @@ pub fn test( )?; if project.requires_yarn { - crate::utils::exists("npm")?; - - let build_system = "yarn"; - let yarn_version = config.build_systems.get(build_system).ok_or_else(|| { - anyhow::anyhow!("Foundry test configuration missing `build_systems.{build_system}`") - })?; - let npm_spec = format!("{build_system}@{yarn_version}"); - let mut npm_install_yarn = Command::new("npm"); - npm_install_yarn.current_dir(project_directory.as_path()); - npm_install_yarn.arg("install"); - npm_install_yarn.args(["--loglevel", "error"]); - npm_install_yarn.arg("--force"); - npm_install_yarn.arg("--yes"); - npm_install_yarn.arg("--global"); - npm_install_yarn.arg(&npm_spec); - crate::utils::command_with_retries( - &mut npm_install_yarn, - format!( - "{} build system {} for Foundry project {project_name}", - solx_utils::cargo_status_ok("Installing"), - build_system.bright_yellow().bold() - ) - .as_str(), - 16, - )?; - let mut yarn_install_command = Command::new(build_system); + let mut yarn_install_command = Command::new("yarn"); yarn_install_command.args(["--cwd", &*project_directory_str]); yarn_install_command.arg("install"); yarn_install_command.arg("--silent"); @@ -160,6 +167,16 @@ pub fn test( .as_str(), )?; + crate::utils::commit_checkout( + &project_directory_str, + format!( + "{} the post-setup state of Foundry project {}", + solx_utils::cargo_status_ok("Committing"), + project_name.bright_white().bold() + ) + .as_str(), + )?; + for ((identifier, compiler), codegen) in config .compilers .iter() @@ -171,27 +188,28 @@ pub fn test( .as_deref() .unwrap_or(solidity_version.as_str()); + crate::utils::reset_checkout( + &project_directory_str, + format!( + "{} Foundry project {} to its post-setup state", + solx_utils::cargo_status_ok("Resetting"), + project_name.bright_white().bold() + ) + .as_str(), + )?; + eprintln!( "{} pragmas in Foundry project {}", solx_utils::cargo_status_ok("Fixing"), project_name.bright_white().bold() ); - for solidity_file in - glob::glob(format!("{}/**/*.sol", project_directory.to_string_lossy()).as_str()) - .expect("Always valid") - .filter_map(Result::ok) - { + // Tracked files only: dependencies under node_modules keep their + // own pragmas. + for solidity_file in crate::utils::git_tracked_files(&project_directory_str, "*.sol")? { + let solidity_file = project_directory.join(solidity_file); if !solidity_file.is_file() { continue; } - // Dependencies keep their own pragmas, as they did when the - // install followed the sed in every fresh clone. - if solidity_file - .components() - .any(|component| component.as_os_str() == "node_modules") - { - continue; - } crate::utils::sed_file( solidity_file.as_path(), &[ @@ -201,8 +219,6 @@ pub fn test( )?; } - // These patterns match their own output, so re-running them re-pins - // the version for each compiler without resetting the checkout. crate::utils::sed_file( project_directory.join(config_file_name).as_path(), &[ diff --git a/solx-dev/src/test/hardhat/mod.rs b/solx-dev/src/test/hardhat/mod.rs index 033318358..acdcf67c9 100644 --- a/solx-dev/src/test/hardhat/mod.rs +++ b/solx-dev/src/test/hardhat/mod.rs @@ -185,6 +185,16 @@ pub fn test( 16, )?; + crate::utils::commit_checkout( + &project_directory_str, + format!( + "{} the post-setup state of Hardhat project {}", + solx_utils::cargo_status_ok("Committing"), + project_name.bright_white().bold() + ) + .as_str(), + )?; + let config_file_name = if project_directory.join("hardhat.config.ts").exists() { Some("hardhat.config.ts") } else if project_directory.join("hardhat.config.js").exists() { @@ -192,21 +202,6 @@ pub fn test( } else { None }; - // The config sed consumes the committed literal version, so each - // toolchain needs the pristine file back before applying its own. - let config_file_snapshot = match config_file_name { - Some(config_file_name) => { - let config_file_path = project_directory.join(config_file_name); - Some( - std::fs::read_to_string(config_file_path.as_path()).map_err(|error| { - anyhow::anyhow!( - "Reading Hardhat configuration file {config_file_path:?}: {error}" - ) - })?, - ) - } - None => None, - }; for ((identifier, compiler), codegen) in config .compilers @@ -219,27 +214,32 @@ pub fn test( .as_deref() .unwrap_or(solidity_version.as_str()); + // The reset also forces Hardhat to recompile: its cache cannot + // tell two solx binaries reporting the same base solc version + // apart, and would otherwise reuse the previous toolchain's + // artifacts. + crate::utils::reset_checkout( + &project_directory_str, + format!( + "{} Hardhat project {} to its post-setup state", + solx_utils::cargo_status_ok("Resetting"), + project_name.bright_white().bold() + ) + .as_str(), + )?; + eprintln!( "{} pragmas in Hardhat project {}", solx_utils::cargo_status_ok("Fixing"), project_name.bright_white().bold() ); - for solidity_file in - glob::glob(format!("{}/**/*.sol", project_directory.to_string_lossy()).as_str()) - .expect("Always valid") - .filter_map(Result::ok) - { + // Tracked files only: dependencies under node_modules keep their + // own pragmas. + for solidity_file in crate::utils::git_tracked_files(&project_directory_str, "*.sol")? { + let solidity_file = project_directory.join(solidity_file); if !solidity_file.is_file() { continue; } - // Dependencies keep their own pragmas, as they did when the - // install followed the sed in every fresh clone. - if solidity_file - .components() - .any(|component| component.as_os_str() == "node_modules") - { - continue; - } crate::utils::sed_file( solidity_file.as_path(), &[ @@ -249,25 +249,17 @@ pub fn test( )?; } - if let (Some(config_file_name), Some(config_file_snapshot)) = - (config_file_name, config_file_snapshot.as_deref()) - { + if let Some(config_file_name) = config_file_name { eprintln!( "{} the configuration file {} of Hardhat project {}", solx_utils::cargo_status_ok("Fixing"), config_file_name.bright_white().bold(), project_name.bright_white().bold(), ); - let config_file_path = project_directory.join(config_file_name); - std::fs::write(config_file_path.as_path(), config_file_snapshot).map_err( - |error| { - anyhow::anyhow!( - "Restoring Hardhat configuration file {config_file_path:?}: {error}" - ) - }, - )?; + // Targets the committed literal version, which the checkout + // reset restored. crate::utils::sed_file( - config_file_path.as_path(), + project_directory.join(config_file_name).as_path(), &[ format!(r#"s/version:\s*["']0.8.30["']/version: "{solidity_version}"/g"#) .as_str(), @@ -284,17 +276,6 @@ pub fn test( let toolchain_name = crate::test::toolchain_name(compiler.name.as_str(), codegen); compiler_shim.reset()?; - // Hardhat cannot tell two solx binaries reporting the same base solc - // version apart, so its cache must go before every compilation. - for stale_directory in ["cache", "artifacts"] { - let stale_path = project_directory.join(stale_directory); - if stale_path.exists() { - std::fs::remove_dir_all(stale_path.as_path()).map_err(|error| { - anyhow::anyhow!("Removing Hardhat directory {stale_path:?}: {error}") - })?; - } - } - let mut npm_compile_command = Command::new("npm"); npm_compile_command.current_dir(&*project_directory_str); npm_compile_command.arg("run"); @@ -351,14 +332,10 @@ pub fn test( for (key, value) in project.env.iter() { npm_test_command.env(key, value); } + // The checkout reset removed any report left by the previous + // toolchain, which would silently stand in for a test run that + // failed to produce one. let npm_test_report_path = project_directory.join("junit-report.json"); - // A report left by the previous toolchain would silently stand in - // for a test run that failed to produce one. - if npm_test_report_path.exists() { - std::fs::remove_file(npm_test_report_path.as_path()).map_err(|error| { - anyhow::anyhow!("Removing stale test report {npm_test_report_path:?}: {error}") - })?; - } let npm_test_report_path_str = npm_test_report_path.to_string_lossy(); npm_test_command.env("JUNIT_REPORT", &*npm_test_report_path_str); if toolchain_name.contains("solx") { diff --git a/solx-dev/src/utils.rs b/solx-dev/src/utils.rs index 67d8d1614..4b19caeed 100644 --- a/solx-dev/src/utils.rs +++ b/solx-dev/src/utils.rs @@ -195,6 +195,113 @@ pub fn clone_repository( Ok(()) } +/// +/// Commits the project checkout after setup, so that `reset_checkout` can +/// restore it before each toolchain runs. +/// +/// `git add --all` respects the project's own .gitignore, so installed +/// dependency trees stay untracked. +/// +pub fn commit_checkout(directory: &str, description: &str) -> anyhow::Result<()> { + let mut add_command = Command::new("git"); + add_command.args(["-C", directory, "add", "--all"]); + command(&mut add_command, description)?; + + let mut commit_command = Command::new("git"); + commit_command.args([ + "-C", + directory, + "-c", + "user.name=solx-dev", + "-c", + "user.email=solx-dev@localhost", + "-c", + "commit.gpgsign=false", + "commit", + "--quiet", + "--no-verify", + "--allow-empty", + "--message", + "solx-dev: project setup", + ]); + command(&mut commit_command, description) +} + +/// +/// Resets the checkout to the commit made by `commit_checkout`: restores all +/// tracked files (submodules included) and removes every untracked and +/// ignored output — build caches, artifacts, reports, persisted fuzz +/// failures — keeping only `node_modules`, which is never modified. +/// +pub fn reset_checkout(directory: &str, description: &str) -> anyhow::Result<()> { + let mut checkout_command = Command::new("git"); + checkout_command.args(["-C", directory, "checkout", "--", "."]); + command(&mut checkout_command, description)?; + + let mut submodule_command = Command::new("git"); + submodule_command.args([ + "-C", + directory, + "submodule", + "--quiet", + "foreach", + "--recursive", + "git", + "checkout", + "--", + ".", + ]); + command(&mut submodule_command, description)?; + + let mut clean_command = Command::new("git"); + clean_command.args([ + "-C", + directory, + "clean", + "--force", + "-d", + "-x", + "--quiet", + "-e", + "node_modules", + ]); + command(&mut clean_command, description) +} + +/// +/// Lists the tracked files matching `pathspec`, including submodule contents. +/// +pub fn git_tracked_files(directory: &str, pathspec: &str) -> anyhow::Result> { + let mut command = Command::new("git"); + command.args([ + "-C", + directory, + "ls-files", + "--recurse-submodules", + "--", + pathspec, + ]); + + let output = command + .output() + .map_err(|error| anyhow::anyhow!("{command:?} process spawning error: {error:?}"))?; + if output.status.code() != Some(solx_utils::EXIT_CODE_SUCCESS) { + anyhow::bail!( + "{command:?} subprocess failed {}:\n{}", + match output.status.code() { + Some(code) => format!("with exit code {code:?}"), + None => "without exit code".to_owned(), + }, + String::from_utf8_lossy(output.stderr.as_slice()), + ); + } + + Ok(String::from_utf8_lossy(output.stdout.as_slice()) + .lines() + .map(PathBuf::from) + .collect()) +} + /// /// Removes the project directory after building and testing. ///