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 5df9ddd06..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,34 +84,118 @@ 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()); + 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 { + 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"); + 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(), + )?; + + 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() .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(), + crate::utils::reset_checkout( &project_directory_str, - project.commit.as_deref(), - &format!( - "{} Foundry project {}", - solx_utils::cargo_status_ok("Cloning"), + format!( + "{} Foundry project {} to its post-setup state", + solx_utils::cargo_status_ok("Resetting"), project_name.bright_white().bold() - ), + ) + .as_str(), )?; eprintln!( @@ -119,11 +203,10 @@ pub fn test( 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; } @@ -136,64 +219,6 @@ 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(), - )?; 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..acdcf67c9 100644 --- a/solx-dev/src/test/hardhat/mod.rs +++ b/solx-dev/src/test/hardhat/mod.rs @@ -89,29 +89,143 @@ 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, + )?; + + 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() { + Some("hardhat.config.js") + } else { + 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(), + // 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, - project.commit.as_deref(), - &format!( - "{} Hardhat project {}", - solx_utils::cargo_status_ok("Cloning"), + format!( + "{} Hardhat project {} to its post-setup state", + solx_utils::cargo_status_ok("Resetting"), project_name.bright_white().bold() - ), + ) + .as_str(), )?; eprintln!( @@ -119,11 +233,10 @@ pub fn test( 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; } @@ -136,95 +249,6 @@ 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 { eprintln!( "{} the configuration file {} of Hardhat project {}", @@ -232,6 +256,8 @@ pub fn test( config_file_name.bright_white().bold(), project_name.bright_white().bold(), ); + // Targets the committed literal version, which the checkout + // reset restored. crate::utils::sed_file( project_directory.join(config_file_name).as_path(), &[ @@ -306,6 +332,9 @@ 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"); let npm_test_report_path_str = npm_test_report_path.to_string_lossy(); npm_test_command.env("JUNIT_REPORT", &*npm_test_report_path_str); 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. ///