From c96eae0f6dfa48a16322b8998159bed465c3a9de Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 13:06:43 +0000 Subject: [PATCH 1/4] Start T64: one shared copy_tree for test suites Assisted-by: Claude Code:claude-opus-5-5 From 79aa884a55fded76f9326a97f323de895b2e8cc1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 13:08:12 +0000 Subject: [PATCH 2/4] Share one copy_tree across the test suites Twenty-eight suites and shared test modules each carried their own recursive directory copier. They now use common::copy_tree, from the new tests/common/fs_tree.rs; modules whose includers don't load common pull that file in by #[path], as with oracle.rs. The two missing-source copies (vex_e2e_common/uv.rs, in_process_redirect_pdm) become an is_dir() guard at each call site. The shared copy follows symlinks to directories, as yarn_berry_common's did; every other copy panicked there, so no passing test changes. Assisted-by: Claude Code:claude-opus-5-5 --- .../socket-patch-cli/tests/common/fs_tree.rs | 24 +++++++++++ crates/socket-patch-cli/tests/common/mod.rs | 7 ++++ .../tests/composer_e2e_common/mod.rs | 19 +++------ .../tests/docker_e2e_vendor_composer.rs | 5 ++- .../tests/e2e_nuget_dotnet_build.rs | 21 +++------- .../tests/e2e_redirect_bun_build.rs | 15 +------ .../tests/e2e_redirect_cargo_build.rs | 23 +++-------- .../tests/e2e_redirect_cargo_shapes.rs | 21 ++-------- .../tests/e2e_redirect_composer_build.rs | 5 ++- .../tests/e2e_redirect_pnpm_build.rs | 21 +++------- .../tests/e2e_redirect_yarn_berry_build.rs | 17 +------- .../tests/e2e_vendor_bun_build.rs | 15 +------ .../tests/e2e_vendor_cargo_build.rs | 31 ++++---------- .../tests/e2e_vendor_composer_build.rs | 2 +- .../tests/e2e_vendor_gem_build.rs | 41 +++++++------------ .../tests/e2e_vendor_pnpm_build.rs | 27 ++++-------- .../tests/e2e_vendor_pypi_build.rs | 27 ++++-------- .../tests/e2e_vendor_yarn_berry_build.rs | 15 +------ .../tests/e2e_vendor_yarn_classic_build.rs | 19 ++------- .../tests/e2e_vendor_yarn_classic_dev_flow.rs | 19 ++------- .../tests/e2e_vex_build/main.rs | 3 ++ .../tests/e2e_vex_build/poetry.rs | 16 +------- .../tests/e2e_yarn4_pnpm_linker_build.rs | 21 ++-------- .../tests/e2e_yarn4_workspaces_build.rs | 17 +------- .../tests/in_process_redirect_pdm.rs | 27 +++++------- .../tests/maven_build_common/mod.rs | 19 +++------ .../tests/mode_migration_bun.rs | 19 ++------- .../tests/mode_migration_cargo.rs | 21 ++-------- .../tests/mode_migration_npm.rs | 15 +------ .../tests/prebuilt_common/mod.rs | 19 +++------ .../socket-patch-cli/tests/vendor_jvm_cli.rs | 14 +------ .../tests/vex_e2e_common/uv.rs | 33 ++++++--------- .../tests/vlt_vendor_common/mod.rs | 19 +++------ .../tests/yarn_berry_common/mod.rs | 22 +++------- 34 files changed, 178 insertions(+), 461 deletions(-) create mode 100644 crates/socket-patch-cli/tests/common/fs_tree.rs diff --git a/crates/socket-patch-cli/tests/common/fs_tree.rs b/crates/socket-patch-cli/tests/common/fs_tree.rs new file mode 100644 index 000000000..af7e48d2b --- /dev/null +++ b/crates/socket-patch-cli/tests/common/fs_tree.rs @@ -0,0 +1,24 @@ +//! Recursive directory copy for test fixtures. `common/mod.rs` re-exports +//! it; shared modules whose includers don't all declare `mod common` pull +//! this file in on its own with `#[path = "../common/fs_tree.rs"] mod +//! fs_tree;`. + +#![allow(dead_code)] + +use std::path::Path; + +/// Copy `src` into `dst`, creating `dst`. Symlinks are followed: a link to +/// a directory is copied as a directory, a link to a file as the file. +/// Panics if `src` is not a readable directory. +pub fn copy_tree(src: &Path, dst: &Path) { + std::fs::create_dir_all(dst).unwrap(); + for entry in std::fs::read_dir(src).unwrap() { + let entry = entry.unwrap(); + let to = dst.join(entry.file_name()); + if entry.path().is_dir() { + copy_tree(&entry.path(), &to); + } else { + std::fs::copy(entry.path(), &to).unwrap(); + } + } +} diff --git a/crates/socket-patch-cli/tests/common/mod.rs b/crates/socket-patch-cli/tests/common/mod.rs index 1cf239401..1458e3a96 100644 --- a/crates/socket-patch-cli/tests/common/mod.rs +++ b/crates/socket-patch-cli/tests/common/mod.rs @@ -48,6 +48,13 @@ pub mod oracle; pub use oracle::{binary, git_sha256}; +/// Recursive fixture copy. Shared modules whose includers don't all +/// declare `mod common` pull it in with +/// `#[path = "../common/fs_tree.rs"] mod fs_tree;`. +pub mod fs_tree; + +pub use fs_tree::copy_tree; + // ── Binary discovery + invocation ───────────────────────────────────── /// Quick check whether `cmd` is on PATH. Used to soft-skip diff --git a/crates/socket-patch-cli/tests/composer_e2e_common/mod.rs b/crates/socket-patch-cli/tests/composer_e2e_common/mod.rs index 1b8d9b581..df74f61b0 100644 --- a/crates/socket-patch-cli/tests/composer_e2e_common/mod.rs +++ b/crates/socket-patch-cli/tests/composer_e2e_common/mod.rs @@ -51,6 +51,10 @@ #![allow(dead_code)] +#[allow(clippy::duplicate_mod)] +#[path = "../common/fs_tree.rs"] +mod fs_tree; + use std::path::Path; use std::process::{Command, Output}; @@ -320,19 +324,6 @@ pub fn installed_packages(proj: &Path) -> Vec { .clone() } -pub fn copy_dir_recursive(src: &Path, dst: &Path) { - std::fs::create_dir_all(dst).unwrap(); - for entry in std::fs::read_dir(src).unwrap() { - let entry = entry.unwrap(); - let to = dst.join(entry.file_name()); - if entry.file_type().unwrap().is_dir() { - copy_dir_recursive(&entry.path(), &to); - } else { - std::fs::copy(entry.path(), &to).unwrap(); - } - } -} - /// A fresh checkout of `proj` at `dst`: ONLY the committable files /// (composer.json, composer.lock, `.socket/`) — never `vendor/`. pub fn fresh_checkout(proj: &Path, dst: &Path) { @@ -340,7 +331,7 @@ pub fn fresh_checkout(proj: &Path, dst: &Path) { std::fs::copy(proj.join("composer.json"), dst.join("composer.json")).unwrap(); std::fs::copy(proj.join("composer.lock"), dst.join("composer.lock")).unwrap(); if proj.join(".socket").is_dir() { - copy_dir_recursive(&proj.join(".socket"), &dst.join(".socket")); + fs_tree::copy_tree(&proj.join(".socket"), &dst.join(".socket")); } assert!( !dst.join("vendor").exists(), diff --git a/crates/socket-patch-cli/tests/docker_e2e_vendor_composer.rs b/crates/socket-patch-cli/tests/docker_e2e_vendor_composer.rs index 82b89bbd3..553ba9be6 100644 --- a/crates/socket-patch-cli/tests/docker_e2e_vendor_composer.rs +++ b/crates/socket-patch-cli/tests/docker_e2e_vendor_composer.rs @@ -46,6 +46,9 @@ #![cfg(feature = "docker-e2e")] +#[allow(clippy::duplicate_mod)] +#[path = "common/fs_tree.rs"] +mod fs_tree; #[path = "common/cache_env.rs"] mod cache_env; #[path = "composer_e2e_common/mod.rs"] @@ -437,7 +440,7 @@ fn assert_manifestless_vex_from_host(host_dir: &std::path::Path) { let purl = format!("pkg:composer/psr/log@{psr_ver}"); let copy = tempfile::tempdir().expect("tempdir"); let fresh = copy.path().join("fresh"); - composer_e2e_common::copy_dir_recursive(&host_dir.join("fresh"), &fresh); + fs_tree::copy_tree(&host_dir.join("fresh"), &fresh); let patched = std::fs::read(fresh.join("vendor/psr/log/src/LoggerInterface.php")) .expect("the container-installed patched file"); let vulns: &[(&str, &[&str])] = &[(GHSA, &["CVE-2024-66666"])]; diff --git a/crates/socket-patch-cli/tests/e2e_nuget_dotnet_build.rs b/crates/socket-patch-cli/tests/e2e_nuget_dotnet_build.rs index 8b5831646..d764818cc 100644 --- a/crates/socket-patch-cli/tests/e2e_nuget_dotnet_build.rs +++ b/crates/socket-patch-cli/tests/e2e_nuget_dotnet_build.rs @@ -51,6 +51,10 @@ //! done //! ``` +#[allow(clippy::duplicate_mod)] +#[path = "common/fs_tree.rs"] +mod fs_tree; + #[path = "vex_e2e_common/mod.rs"] mod vex_e2e_common; @@ -308,7 +312,7 @@ fn fresh_checkout(from: &Path, to: &Path) { } // v5 hosted mode writes no `.socket/`: the lock pins are its whole state. if from.join(".socket").is_dir() { - copy_tree(&from.join(".socket"), &to.join(".socket")); + fs_tree::copy_tree(&from.join(".socket"), &to.join(".socket")); } strip_manifest(to); let blobs = to.join(".socket/blobs"); @@ -317,19 +321,6 @@ fn fresh_checkout(from: &Path, to: &Path) { } } -fn copy_tree(from: &Path, to: &Path) { - std::fs::create_dir_all(to).unwrap(); - for entry in std::fs::read_dir(from).unwrap() { - let entry = entry.unwrap(); - let dst = to.join(entry.file_name()); - if entry.file_type().unwrap().is_dir() { - copy_tree(&entry.path(), &dst); - } else { - std::fs::copy(entry.path(), dst).unwrap(); - } - } -} - /// The patch server's rebuild: the upstream nupkg with `FILE_KEY` replaced /// and the package signature dropped (its digest no longer matches). fn patched_nupkg(upstream: &[u8], patched: &[u8]) -> Vec { @@ -1190,7 +1181,7 @@ fn nuget_vendored_solution_member_and_named_locks_restore() { // Fresh checkout of the committable files, cold cache, each project. let checkout = sb.dir("solution-checkout"); std::fs::copy(root.join("nuget.config"), checkout.join("nuget.config")).unwrap(); - copy_tree(&root.join(".socket"), &checkout.join(".socket")); + fs_tree::copy_tree(&root.join(".socket"), &checkout.join(".socket")); strip_manifest(&checkout); let blobs = checkout.join(".socket/blobs"); if blobs.exists() { diff --git a/crates/socket-patch-cli/tests/e2e_redirect_bun_build.rs b/crates/socket-patch-cli/tests/e2e_redirect_bun_build.rs index 80e42bff0..f51705d4c 100644 --- a/crates/socket-patch-cli/tests/e2e_redirect_bun_build.rs +++ b/crates/socket-patch-cli/tests/e2e_redirect_bun_build.rs @@ -337,19 +337,6 @@ fn sri(bytes: &[u8]) -> String { format!("sha512-{}", sha512_sri_b64(bytes)) } -fn copy_dir_recursive(src: &Path, dst: &Path) { - std::fs::create_dir_all(dst).unwrap(); - for entry in std::fs::read_dir(src).unwrap() { - let entry = entry.unwrap(); - let to = dst.join(entry.file_name()); - if entry.file_type().unwrap().is_dir() { - copy_dir_recursive(&entry.path(), &to); - } else { - std::fs::copy(entry.path(), &to).unwrap(); - } - } -} - /// A gzipped npm tarball from `(entry name under package/, bytes, mode)` /// triples, built with the tar crate so the suite has no system-`tar` /// dependency (Windows runners included). @@ -1149,7 +1136,7 @@ fn fresh_checkout(fx: &BunRedirectFixture, name: &str) -> PathBuf { std::fs::copy(fx.proj.join("bunfig.toml"), fresh.join("bunfig.toml")).unwrap(); } if fx.proj.join(".socket").is_dir() { - copy_dir_recursive(&fx.proj.join(".socket"), &fresh.join(".socket")); + common::copy_tree(&fx.proj.join(".socket"), &fresh.join(".socket")); } fresh } diff --git a/crates/socket-patch-cli/tests/e2e_redirect_cargo_build.rs b/crates/socket-patch-cli/tests/e2e_redirect_cargo_build.rs index 600843061..a0a5f89bc 100644 --- a/crates/socket-patch-cli/tests/e2e_redirect_cargo_build.rs +++ b/crates/socket-patch-cli/tests/e2e_redirect_cargo_build.rs @@ -136,19 +136,6 @@ fn sha256_hex(bytes: &[u8]) -> String { hex::encode(Sha256::digest(bytes)) } -fn copy_dir_recursive(src: &Path, dst: &Path) { - std::fs::create_dir_all(dst).unwrap(); - for entry in std::fs::read_dir(src).unwrap() { - let entry = entry.unwrap(); - let to = dst.join(entry.file_name()); - if entry.file_type().unwrap().is_dir() { - copy_dir_recursive(&entry.path(), &to); - } else { - std::fs::copy(entry.path(), &to).unwrap(); - } - } -} - /// The locked version of `name` in Cargo.lock (first `[[package]]` match). fn locked_version(lock_text: &str, name: &str) -> Option { let needle = format!("name = \"{name}\""); @@ -225,7 +212,7 @@ fn build_patched_crate( ) -> Vec { let leaf = format!("{DEP}-{version}"); let pkg_dir = stage_root.join(&leaf); - copy_dir_recursive(crate_dir, &pkg_dir); + common::copy_tree(crate_dir, &pkg_dir); let _ = std::fs::remove_file(pkg_dir.join(".cargo-checksum.json")); std::fs::write(pkg_dir.join("src/lib.rs"), patched).unwrap(); @@ -670,11 +657,11 @@ fn fresh_checkout_cargo_fetch(fx: &RedirectFixture) -> (PathBuf, PathBuf, Output std::fs::create_dir_all(&fresh).unwrap(); std::fs::copy(fx.proj.join("Cargo.toml"), fresh.join("Cargo.toml")).unwrap(); std::fs::copy(fx.proj.join("Cargo.lock"), fresh.join("Cargo.lock")).unwrap(); - copy_dir_recursive(&fx.proj.join(".cargo"), &fresh.join(".cargo")); - copy_dir_recursive(&fx.proj.join("src"), &fresh.join("src")); + common::copy_tree(&fx.proj.join(".cargo"), &fresh.join(".cargo")); + common::copy_tree(&fx.proj.join("src"), &fresh.join("src")); // v5 hosted mode writes nothing under `.socket/`; carry it when present. if fx.proj.join(".socket").is_dir() { - copy_dir_recursive(&fx.proj.join(".socket"), &fresh.join(".socket")); + common::copy_tree(&fx.proj.join(".socket"), &fresh.join(".socket")); } let fresh_home = fx.tmp.path().join("fresh-cargo-home"); @@ -842,7 +829,7 @@ impl ManifestlessHosted { std::fs::create_dir_all(&revert_home).unwrap(); std::fs::write(revert.join("Cargo.toml"), &self.baseline_toml).unwrap(); std::fs::write(revert.join("Cargo.lock"), &self.baseline_lock).unwrap(); - copy_dir_recursive(&fresh.join("src"), &revert.join("src")); + common::copy_tree(&fresh.join("src"), &revert.join("src")); let fetch = cargo(&revert, &["fetch", "--locked"], &revert_home); assert!( fetch.status.success(), diff --git a/crates/socket-patch-cli/tests/e2e_redirect_cargo_shapes.rs b/crates/socket-patch-cli/tests/e2e_redirect_cargo_shapes.rs index e8631fac6..a33cca165 100644 --- a/crates/socket-patch-cli/tests/e2e_redirect_cargo_shapes.rs +++ b/crates/socket-patch-cli/tests/e2e_redirect_cargo_shapes.rs @@ -227,7 +227,7 @@ async fn mount_crates_index_mirror(pristine_lock: &str, patches: &[Patch]) -> Mo fn build_crate(stage: &Path, crate_dir: &Path, leaf: &str, patched: &[u8]) -> Vec { let pkg = stage.join(leaf); - copy_tree(crate_dir, &pkg); + common::copy_tree(crate_dir, &pkg); let _ = std::fs::remove_file(pkg.join(".cargo-checksum.json")); std::fs::write(pkg.join("src/lib.rs"), patched).unwrap(); let mut bytes = Vec::new(); @@ -240,19 +240,6 @@ fn build_crate(stage: &Path, crate_dir: &Path, leaf: &str, patched: &[u8]) -> Ve bytes } -fn copy_tree(src: &Path, dst: &Path) { - std::fs::create_dir_all(dst).unwrap(); - for entry in std::fs::read_dir(src).unwrap() { - let entry = entry.unwrap(); - let to = dst.join(entry.file_name()); - if entry.file_type().unwrap().is_dir() { - copy_tree(&entry.path(), &to); - } else { - std::fs::copy(entry.path(), &to).unwrap(); - } - } -} - /// Every project file except build output and the `.socket/` ledger dir. fn snapshot(root: &Path) -> BTreeMap> { fn walk(root: &Path, dir: &Path, out: &mut BTreeMap>) { @@ -640,7 +627,7 @@ async fn run_shape(shape: Shape) -> Option<()> { std::fs::copy(proj.join(&rel), to).unwrap(); } if proj.join(".socket").is_dir() { - copy_tree(&proj.join(".socket"), &fresh.join(".socket")); + common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); } let fresh_home = tmp.path().join("fresh-home"); std::fs::create_dir_all(&fresh_home).unwrap(); @@ -734,7 +721,7 @@ async fn run_shape(shape: Shape) -> Option<()> { ("SOCKET_PATCH_SERVER_URL", uri.as_str()), ]; let post_scan = tmp.path().join("post-scan"); - copy_tree(&proj, &post_scan); + common::copy_tree(&proj, &post_scan); let mut orders = vec![shape.patches.clone()]; if shape.patches.len() > 1 { orders.push(shape.patches.iter().rev().copied().collect()); @@ -742,7 +729,7 @@ async fn run_shape(shape: Shape) -> Option<()> { for (n, order) in orders.iter().enumerate() { if n > 0 { std::fs::remove_dir_all(&proj).unwrap(); - copy_tree(&post_scan, &proj); + common::copy_tree(&post_scan, &proj); } for patch in order { let purl = patch.purl(); diff --git a/crates/socket-patch-cli/tests/e2e_redirect_composer_build.rs b/crates/socket-patch-cli/tests/e2e_redirect_composer_build.rs index df9354d36..c8716a425 100644 --- a/crates/socket-patch-cli/tests/e2e_redirect_composer_build.rs +++ b/crates/socket-patch-cli/tests/e2e_redirect_composer_build.rs @@ -53,6 +53,9 @@ //! release. `#[ignore]`-gated like the vendored twin: the unpinned `test` job //! skips it; the e2e job runs it with a pinned toolchain via `--ignored`. +#[allow(clippy::duplicate_mod)] +#[path = "common/fs_tree.rs"] +mod fs_tree; #[path = "common/hermetic.rs"] mod hermetic; #[path = "common/rollback_json.rs"] @@ -638,7 +641,7 @@ fn assert_manifestless_hosted_vex(fx: &Fixture, fresh: &Path, tag: &str) { std::fs::copy(fresh.join("composer.json"), reverted.join("composer.json")).unwrap(); std::fs::write(reverted.join("composer.lock"), &fx.registry_lock).unwrap(); if fresh.join(".socket").is_dir() { - composer_e2e_common::copy_dir_recursive(&fresh.join(".socket"), &reverted.join(".socket")); + fs_tree::copy_tree(&fresh.join(".socket"), &reverted.join(".socket")); } let install = composer( &reverted, diff --git a/crates/socket-patch-cli/tests/e2e_redirect_pnpm_build.rs b/crates/socket-patch-cli/tests/e2e_redirect_pnpm_build.rs index ebaf1109f..fac884751 100644 --- a/crates/socket-patch-cli/tests/e2e_redirect_pnpm_build.rs +++ b/crates/socket-patch-cli/tests/e2e_redirect_pnpm_build.rs @@ -70,6 +70,10 @@ use socket_patch_core::hash::git_sha256::compute_git_sha256_from_bytes; use wiremock::matchers::{method, path, path_regex}; use wiremock::{Mock, MockServer, ResponseTemplate}; +#[allow(clippy::duplicate_mod)] +#[path = "common/fs_tree.rs"] +mod fs_tree; + #[path = "common/cache_env.rs"] mod cache_env; #[path = "common/hermetic.rs"] @@ -309,19 +313,6 @@ fn sha512_sri_b64(bytes: &[u8]) -> String { base64::engine::general_purpose::STANDARD.encode(Sha512::digest(bytes)) } -fn copy_dir_recursive(src: &Path, dst: &Path) { - std::fs::create_dir_all(dst).unwrap(); - for entry in std::fs::read_dir(src).unwrap() { - let entry = entry.unwrap(); - let to = dst.join(entry.file_name()); - if entry.file_type().unwrap().is_dir() { - copy_dir_recursive(&entry.path(), &to); - } else { - std::fs::copy(entry.path(), &to).unwrap(); - } - } -} - fn hosted_url_for(base: &str) -> String { format!("{base}/patch/npm/{DEP}/{DEP_VERSION}/{TOKEN}/{UUID}/{DEP}-{DEP_VERSION}.tgz") } @@ -853,7 +844,7 @@ fn fresh_checkout_install_env( } // v5 hosted mode writes nothing under `.socket/`; carry it when present. if fx.proj.join(".socket").is_dir() { - copy_dir_recursive(&fx.proj.join(".socket"), &fresh.join(".socket")); + fs_tree::copy_tree(&fx.proj.join(".socket"), &fresh.join(".socket")); } // Dead registry: the only reachable artifact URL is the wiremock hosted // tarball, so a successful install can only have come from it. The retry @@ -2204,7 +2195,7 @@ async fn pnpm_pinned_matrix_workspace_peer_instances() { // Manifest-less VEX over the fresh workspace: both peer instances are // hash-verified, from the ledger and then from the lockfile alone. if proj.join(".socket").is_dir() { - copy_dir_recursive(&proj.join(".socket"), &fresh.join(".socket")); + fs_tree::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); } assert_manifestless_hosted_vex( &fresh, diff --git a/crates/socket-patch-cli/tests/e2e_redirect_yarn_berry_build.rs b/crates/socket-patch-cli/tests/e2e_redirect_yarn_berry_build.rs index 39c6368bf..61d1a440f 100644 --- a/crates/socket-patch-cli/tests/e2e_redirect_yarn_berry_build.rs +++ b/crates/socket-patch-cli/tests/e2e_redirect_yarn_berry_build.rs @@ -159,24 +159,11 @@ fn run_socket(cwd: &Path, args: &[&str]) -> (i32, String, String) { ) } -fn copy_dir_recursive(src: &Path, dst: &Path) { - std::fs::create_dir_all(dst).unwrap(); - for entry in std::fs::read_dir(src).unwrap() { - let entry = entry.unwrap(); - let to = dst.join(entry.file_name()); - if entry.file_type().unwrap().is_dir() { - copy_dir_recursive(&entry.path(), &to); - } else { - std::fs::copy(entry.path(), &to).unwrap(); - } - } -} - /// Build a patched npm tarball (`package/` prefix, marker-prepended index.js) /// from the installed dep directory. fn build_patched_tgz(installed_dir: &Path, patched_index: &[u8], out_tgz: &Path) { let stage = out_tgz.parent().unwrap().join("tarstage"); - copy_dir_recursive(installed_dir, &stage.join("package")); + common::copy_tree(installed_dir, &stage.join("package")); std::fs::write(stage.join("package").join("index.js"), patched_index).unwrap(); let tar = Command::new("tar") .args(["-czf", out_tgz.to_str().unwrap(), "package"]) @@ -658,7 +645,7 @@ fn fresh_checkout_yarn_install(fx: &BerryRedirectFixture) -> (PathBuf, Output) { std::fs::write(fresh.join(".yarnrc.yml"), fresh_yarnrc(fx)).unwrap(); // v5 hosted mode may leave no `.socket/` at all (no ledger, no manifest). if fx.proj.join(".socket").is_dir() { - copy_dir_recursive(&fx.proj.join(".socket"), &fresh.join(".socket")); + common::copy_tree(&fx.proj.join(".socket"), &fresh.join(".socket")); } let fresh_global = fx.tmp.path().join("fresh-yarn-global"); let ci = corepack( diff --git a/crates/socket-patch-cli/tests/e2e_vendor_bun_build.rs b/crates/socket-patch-cli/tests/e2e_vendor_bun_build.rs index a76f1a1a6..9fcc54d0a 100644 --- a/crates/socket-patch-cli/tests/e2e_vendor_bun_build.rs +++ b/crates/socket-patch-cli/tests/e2e_vendor_bun_build.rs @@ -305,19 +305,6 @@ fn sri(bytes: &[u8]) -> String { format!("sha512-{}", b64(&Sha512::digest(bytes))) } -fn copy_dir_recursive(src: &Path, dst: &Path) { - std::fs::create_dir_all(dst).unwrap(); - for entry in std::fs::read_dir(src).unwrap() { - let entry = entry.unwrap(); - let to = dst.join(entry.file_name()); - if entry.file_type().unwrap().is_dir() { - copy_dir_recursive(&entry.path(), &to); - } else { - std::fs::copy(entry.path(), &to).unwrap(); - } - } -} - /// A gzipped npm tarball from `(entry name under package/, bytes, mode)` /// triples, built with the tar crate so the suite has no system-`tar` /// dependency (Windows runners included). @@ -826,7 +813,7 @@ fn fresh_checkout(fx: &BunProject, name: &str) -> PathBuf { if fx.proj.join("bunfig.toml").is_file() { std::fs::copy(fx.proj.join("bunfig.toml"), fresh.join("bunfig.toml")).unwrap(); } - copy_dir_recursive(&fx.proj.join(".socket"), &fresh.join(".socket")); + common::copy_tree(&fx.proj.join(".socket"), &fresh.join(".socket")); fresh } diff --git a/crates/socket-patch-cli/tests/e2e_vendor_cargo_build.rs b/crates/socket-patch-cli/tests/e2e_vendor_cargo_build.rs index 7b19e468f..36f8ed0e1 100644 --- a/crates/socket-patch-cli/tests/e2e_vendor_cargo_build.rs +++ b/crates/socket-patch-cli/tests/e2e_vendor_cargo_build.rs @@ -271,19 +271,6 @@ fn stage_patch(proj: &Path, purl: &str, file_key: &str, before: &[u8], after: &[ std::fs::write(socket.join("blobs").join(git_sha256(after)), after).unwrap(); } -fn copy_dir_recursive(src: &Path, dst: &Path) { - std::fs::create_dir_all(dst).unwrap(); - for entry in std::fs::read_dir(src).unwrap() { - let entry = entry.unwrap(); - let to = dst.join(entry.file_name()); - if entry.file_type().unwrap().is_dir() { - copy_dir_recursive(&entry.path(), &to); - } else { - std::fs::copy(entry.path(), &to).unwrap(); - } - } -} - /// `manifest` without its `[patch.crates-io]` table (the header through the /// next table header or EOF, plus the blank line before it) — a hand revert /// of the vendored wiring. @@ -551,8 +538,8 @@ impl ManifestlessVendored { strip_patch_table(&std::fs::read_to_string(fresh.join("Cargo.toml")).unwrap()), ) .unwrap(); - copy_dir_recursive(&fresh.join("src"), &revert.join("src")); - copy_dir_recursive(&fresh.join(".socket"), &revert.join(".socket")); + common::copy_tree(&fresh.join("src"), &revert.join("src")); + common::copy_tree(&fresh.join(".socket"), &revert.join(".socket")); std::fs::write( revert.join("src/main.rs"), "fn main() { println!(\"baseline\"); }\n", @@ -773,8 +760,8 @@ fn cargo_vendor_fresh_checkout_locked_offline_build_and_revert() { std::fs::create_dir_all(&fresh).unwrap(); std::fs::copy(proj.join("Cargo.toml"), fresh.join("Cargo.toml")).unwrap(); std::fs::copy(&lock_path, fresh.join("Cargo.lock")).unwrap(); - copy_dir_recursive(&proj.join("src"), &fresh.join("src")); - copy_dir_recursive(&proj.join(".socket"), &fresh.join(".socket")); + common::copy_tree(&proj.join("src"), &fresh.join("src")); + common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); let fresh_home = tmp.path().join("fresh-cargo-home"); std::fs::create_dir_all(&fresh_home).unwrap(); @@ -1230,8 +1217,8 @@ async fn cargo_get_uuid_vendored_fresh_checkout_locked_build() { std::fs::create_dir_all(&fresh).unwrap(); std::fs::copy(proj.join("Cargo.toml"), fresh.join("Cargo.toml")).unwrap(); std::fs::copy(proj.join("Cargo.lock"), fresh.join("Cargo.lock")).unwrap(); - copy_dir_recursive(&proj.join("src"), &fresh.join("src")); - copy_dir_recursive(&proj.join(".socket"), &fresh.join(".socket")); + common::copy_tree(&proj.join("src"), &fresh.join("src")); + common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); let fresh_home = tmp.path().join("fresh-cargo-home"); std::fs::create_dir_all(&fresh_home).unwrap(); @@ -1342,7 +1329,7 @@ fn fresh_checkout(proj: &Path, tmp: &Path, tag: &str) -> (PathBuf, PathBuf) { } for dir in ["src", ".socket", ".cargo"] { if proj.join(dir).exists() { - copy_dir_recursive(&proj.join(dir), &fresh.join(dir)); + common::copy_tree(&proj.join(dir), &fresh.join(dir)); } } let home = tmp.join(format!("fresh-home-{tag}")); @@ -2629,7 +2616,7 @@ fn cargo_url_spelled_crates_io_patch_table_is_refused() { return; }; // A user's same-version fork of cfg-if under the URL spelling. - copy_dir_recursive(&crate_dir, &proj.join("fork-cfg-if")); + common::copy_tree(&crate_dir, &proj.join("fork-cfg-if")); let manifest = format!( "{}\n[patch.\"https://github.com/rust-lang/crates.io-index\"]\n{DEP} = {{ path = \"fork-cfg-if\" }}\n", std::fs::read_to_string(proj.join("Cargo.toml")).unwrap() @@ -2713,7 +2700,7 @@ fn cargo_ancestor_config_patch_cannot_silently_shadow_the_vendored_copy() { // A monorepo-level config adds the user's same-version, unpatched fork // under the bare crate name. let fork = tmp.path().join(format!("forks/{DEP}-{version}")); - copy_dir_recursive(&crate_dir, &fork); + common::copy_tree(&crate_dir, &fork); std::fs::create_dir_all(tmp.path().join(".cargo")).unwrap(); std::fs::write( tmp.path().join(".cargo/config.toml"), diff --git a/crates/socket-patch-cli/tests/e2e_vendor_composer_build.rs b/crates/socket-patch-cli/tests/e2e_vendor_composer_build.rs index a210e834d..d6b3693e5 100644 --- a/crates/socket-patch-cli/tests/e2e_vendor_composer_build.rs +++ b/crates/socket-patch-cli/tests/e2e_vendor_composer_build.rs @@ -467,7 +467,7 @@ fn assert_manifestless_vendored_vex( std::fs::create_dir_all(&reverted).unwrap(); std::fs::copy(fresh.join("composer.json"), reverted.join("composer.json")).unwrap(); std::fs::write(reverted.join("composer.lock"), registry_lock).unwrap(); - composer_e2e_common::copy_dir_recursive(&fresh.join(".socket"), &reverted.join(".socket")); + common::copy_tree(&fresh.join(".socket"), &reverted.join(".socket")); let install = composer( &reverted, &["install"], diff --git a/crates/socket-patch-cli/tests/e2e_vendor_gem_build.rs b/crates/socket-patch-cli/tests/e2e_vendor_gem_build.rs index 6872c38e5..16ab969bd 100644 --- a/crates/socket-patch-cli/tests/e2e_vendor_gem_build.rs +++ b/crates/socket-patch-cli/tests/e2e_vendor_gem_build.rs @@ -182,19 +182,6 @@ fn stage_patch_with_vuln(proj: &Path, purl: &str, file_key: &str, before: &[u8], std::fs::write(socket.join("blobs").join(git_sha256(after)), after).unwrap(); } -fn copy_dir_recursive(src: &Path, dst: &Path) { - std::fs::create_dir_all(dst).unwrap(); - for entry in std::fs::read_dir(src).unwrap() { - let entry = entry.unwrap(); - let to = dst.join(entry.file_name()); - if entry.file_type().unwrap().is_dir() { - copy_dir_recursive(&entry.path(), &to); - } else { - std::fs::copy(entry.path(), &to).unwrap(); - } - } -} - /// The plain resolved version of `name` from the lock's 4-space GEM spec line /// (` rack (3.1.16)`); platform-suffixed spec lines never match (their /// parenthesized token does not start the version with a digit-only form we @@ -341,8 +328,8 @@ fn vendored_manifestless_vex_matrix(v: &Vendored<'_>) { std::fs::create_dir_all(©).unwrap(); std::fs::copy(fresh.join("Gemfile"), copy.join("Gemfile")).unwrap(); std::fs::write(copy.join(lock_name), v.pristine_lock).unwrap(); - copy_dir_recursive(&fresh.join(".socket"), ©.join(".socket")); - copy_dir_recursive(v.committed_bundle, ©.join(".bundle")); + common::copy_tree(&fresh.join(".socket"), ©.join(".socket")); + common::copy_tree(v.committed_bundle, ©.join(".bundle")); for f in std::fs::read_dir(&saved).unwrap() { let f = f.unwrap(); std::fs::copy(f.path(), copy.join(".socket/vendor").join(f.file_name())).unwrap(); @@ -363,7 +350,7 @@ fn vendored_manifestless_vex_matrix(v: &Vendored<'_>) { // Bundler <= 2.0 persisted `BUNDLE_FROZEN: "true"` into the copy's // `.bundle/config` on that frozen attempt; a developer's unfrozen // install runs from the committed config. - copy_dir_recursive(v.committed_bundle, ©.join(".bundle")); + common::copy_tree(v.committed_bundle, ©.join(".bundle")); let install = bundle(©, &["install"], false); assert!( install.status.success(), @@ -612,8 +599,8 @@ fn gem_vendor_fresh_checkout_bundle_install_and_revert() { std::fs::create_dir_all(&fresh).unwrap(); std::fs::copy(&gemfile_path, fresh.join("Gemfile")).unwrap(); std::fs::copy(&lock_path, fresh.join("Gemfile.lock")).unwrap(); - copy_dir_recursive(&proj.join(".socket"), &fresh.join(".socket")); - copy_dir_recursive(&proj.join(".bundle"), &fresh.join(".bundle")); + common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); + common::copy_tree(&proj.join(".bundle"), &fresh.join(".bundle")); assert!( !fresh.join("vendor").exists(), "fresh checkout must not carry an installed tree (test bug)" @@ -913,8 +900,8 @@ fn gem_vendor_transitive_dep_fresh_checkout_and_revert() { std::fs::create_dir_all(&fresh).unwrap(); std::fs::copy(&gemfile_path, fresh.join("Gemfile")).unwrap(); std::fs::copy(&lock_path, fresh.join("Gemfile.lock")).unwrap(); - copy_dir_recursive(&proj.join(".socket"), &fresh.join(".socket")); - copy_dir_recursive(&proj.join(".bundle"), &fresh.join(".bundle")); + common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); + common::copy_tree(&proj.join(".bundle"), &fresh.join(".bundle")); assert!( !fresh.join("vendor").exists(), "fresh checkout must not carry an installed tree (test bug)" @@ -1306,8 +1293,8 @@ async fn gem_get_uuid_vendored_fresh_checkout_bundle_install() { std::fs::create_dir_all(&fresh).unwrap(); std::fs::copy(&gemfile_path, fresh.join("Gemfile")).unwrap(); std::fs::copy(&lock_path, fresh.join("Gemfile.lock")).unwrap(); - copy_dir_recursive(&proj.join(".socket"), &fresh.join(".socket")); - copy_dir_recursive(&proj.join(".bundle"), &fresh.join(".bundle")); + common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); + common::copy_tree(&proj.join(".bundle"), &fresh.join(".bundle")); assert!( !fresh.join("vendor").exists(), "fresh checkout must not carry an installed tree (test bug)" @@ -1768,8 +1755,8 @@ fn gem_vendor_second_gem_section_fresh_checkout_and_revert() { std::fs::create_dir_all(&fresh).unwrap(); std::fs::copy(&gemfile_path, fresh.join("Gemfile")).unwrap(); std::fs::copy(&lock_path, fresh.join("Gemfile.lock")).unwrap(); - copy_dir_recursive(&proj.join(".socket"), &fresh.join(".socket")); - copy_dir_recursive(&proj.join(".bundle"), &fresh.join(".bundle")); + common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); + common::copy_tree(&proj.join(".bundle"), &fresh.join(".bundle")); let lock_wired = std::fs::read(&lock_path).unwrap(); let ci = bundle(&fresh, &["install"], true); assert!( @@ -2213,7 +2200,7 @@ fn gem_vendor_crlf_lock_fresh_checkout_and_revert() { std::fs::create_dir_all(&pristine).unwrap(); std::fs::copy(&gemfile_path, pristine.join("Gemfile")).unwrap(); std::fs::copy(&lock_path, pristine.join("Gemfile.lock")).unwrap(); - copy_dir_recursive(&proj.join(".bundle"), &pristine.join(".bundle")); + common::copy_tree(&proj.join(".bundle"), &pristine.join(".bundle")); let frozen_crlf_ok = bundle(&pristine, &["install"], true).status.success(); let cwd = proj.to_str().unwrap(); let vendor = |extra: &[&str]| { @@ -2262,8 +2249,8 @@ fn gem_vendor_crlf_lock_fresh_checkout_and_revert() { std::fs::create_dir_all(&fresh).unwrap(); std::fs::copy(&gemfile_path, fresh.join("Gemfile")).unwrap(); std::fs::copy(&lock_path, fresh.join("Gemfile.lock")).unwrap(); - copy_dir_recursive(&proj.join(".socket"), &fresh.join(".socket")); - copy_dir_recursive(&proj.join(".bundle"), &fresh.join(".bundle")); + common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); + common::copy_tree(&proj.join(".bundle"), &fresh.join(".bundle")); let ci = bundle(&fresh, &["install"], frozen_crlf_ok); assert!( ci.status.success(), diff --git a/crates/socket-patch-cli/tests/e2e_vendor_pnpm_build.rs b/crates/socket-patch-cli/tests/e2e_vendor_pnpm_build.rs index f3f3b4b0e..4d76f08a1 100644 --- a/crates/socket-patch-cli/tests/e2e_vendor_pnpm_build.rs +++ b/crates/socket-patch-cli/tests/e2e_vendor_pnpm_build.rs @@ -317,19 +317,6 @@ fn parse_envelope(stdout: &str) -> serde_json::Value { .unwrap_or_else(|e| panic!("vendor --json output is not JSON: {e}\nstdout:\n{stdout}")) } -fn copy_dir_recursive(src: &Path, dst: &Path) { - std::fs::create_dir_all(dst).unwrap(); - for entry in std::fs::read_dir(src).unwrap() { - let entry = entry.unwrap(); - let to = dst.join(entry.file_name()); - if entry.file_type().unwrap().is_dir() { - copy_dir_recursive(&entry.path(), &to); - } else { - std::fs::copy(entry.path(), &to).unwrap(); - } - } -} - // ── the capstone ────────────────────────────────────────────────────── #[tokio::test(flavor = "multi_thread")] @@ -692,7 +679,7 @@ async fn run_pnpm_capstone(pm: &str, driver: VendorDriver) { if ws_created { std::fs::copy(&ws_path, fresh.join("pnpm-workspace.yaml")).unwrap(); } - copy_dir_recursive(&proj.join(".socket"), &fresh.join(".socket")); + common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); let fresh_store = tmp.path().join("fresh-pnpm-store"); let mut ci = corepack( @@ -1769,7 +1756,7 @@ fn run_two_document_capstone( for file in ["package.json", "pnpm-lock.yaml", "pnpm-workspace.yaml"] { std::fs::copy(proj.join(file), fresh.join(file)).unwrap(); } - copy_dir_recursive(&proj.join(".socket"), &fresh.join(".socket")); + common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); let fresh_store = tmp.path().join("fresh-store"); let ci = corepack( &fresh, @@ -1923,8 +1910,8 @@ fn pnpm_vendor_names_a_file_copy_of_the_vendored_release() { // Fresh checkout, empty store, frozen: a gets the vendored bytes, b // keeps the fork, exactly as the warning says. let fresh = tmp.path().join("fresh"); - copy_dir_recursive(&proj.join(".socket"), &fresh.join(".socket")); - copy_dir_recursive(&proj.join("forks"), &fresh.join("forks")); + common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); + common::copy_tree(&proj.join("forks"), &fresh.join("forks")); for file in [ "package.json", "pnpm-lock.yaml", @@ -2245,7 +2232,7 @@ fn run_legacy_capstone_for(pm: &str, lock_head: &str, proj_dir: &str, dep: &str, std::fs::create_dir_all(&fresh).unwrap(); std::fs::copy(&pkg_path, fresh.join("package.json")).unwrap(); std::fs::copy(&lock_path, fresh.join("pnpm-lock.yaml")).unwrap(); - copy_dir_recursive(&proj.join(".socket"), &fresh.join(".socket")); + common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); std::fs::write(fresh.join(".npmrc"), "registry=http://127.0.0.1:1/\n").unwrap(); let fresh_store = tmp.path().join("store-fresh"); @@ -2525,7 +2512,7 @@ fn pnpm_vendor_keeps_user_workspace_overrides_authoritative() { for file in ["package.json", "pnpm-lock.yaml", "pnpm-workspace.yaml"] { std::fs::copy(proj.join(file), fresh.join(file)).unwrap(); } - copy_dir_recursive(&proj.join(".socket"), &fresh.join(".socket")); + common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); let fresh_store = tmp.path().join("fresh-pnpm-store"); let ci = corepack( &fresh, @@ -2867,7 +2854,7 @@ async fn run_build_approval_capstone(pm: &str, approval: &str, carried: bool) { for file in ["package.json", "pnpm-lock.yaml", "pnpm-workspace.yaml"] { std::fs::copy(proj.join(file), fresh.join(file)).unwrap(); } - copy_dir_recursive(&proj.join(".socket"), &fresh.join(".socket")); + common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); std::fs::write(fresh.join(".npmrc"), "registry=http://127.0.0.1:1/\n").unwrap(); let fresh_store = tmp.path().join("fresh-pnpm-store"); let ci = corepack( diff --git a/crates/socket-patch-cli/tests/e2e_vendor_pypi_build.rs b/crates/socket-patch-cli/tests/e2e_vendor_pypi_build.rs index 24fe51166..7df2fcd06 100644 --- a/crates/socket-patch-cli/tests/e2e_vendor_pypi_build.rs +++ b/crates/socket-patch-cli/tests/e2e_vendor_pypi_build.rs @@ -452,7 +452,7 @@ fn assert_fresh_checkout_frozen_offline( std::fs::create_dir_all(&fresh).unwrap(); std::fs::copy(proj.join("pyproject.toml"), fresh.join("pyproject.toml")).unwrap(); std::fs::copy(proj.join("uv.lock"), fresh.join("uv.lock")).unwrap(); - copy_dir_recursive(&proj.join(".socket"), &fresh.join(".socket")); + common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); let fresh_cache = tmp.join("fresh-uv-cache"); let mut fresh_env: Vec<(&str, &str)> = vec![("UV_CACHE_DIR", fresh_cache.to_str().unwrap())]; @@ -529,19 +529,6 @@ fn bake_leak_guards() { ); } -fn copy_dir_recursive(src: &Path, dst: &Path) { - std::fs::create_dir_all(dst).unwrap(); - for entry in std::fs::read_dir(src).unwrap() { - let entry = entry.unwrap(); - let to = dst.join(entry.file_name()); - if entry.file_type().unwrap().is_dir() { - copy_dir_recursive(&entry.path(), &to); - } else { - std::fs::copy(entry.path(), &to).unwrap(); - } - } -} - /// The shared manifest-less VEX matrix (`uv_vex::manifestless_matrix`) over /// `/fresh`, the fresh checkout [`assert_fresh_checkout_frozen_offline`] /// just installed PATCHED: manifest deleted → attested `(vendored)`; ledgers @@ -1684,7 +1671,7 @@ fn pip_requirements_vendor_fresh_checkout_no_index_and_revert() { fresh.join("requirements.txt"), ) .unwrap(); - copy_dir_recursive(&proj.join(".socket"), &fresh.join(".socket")); + common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); let fresh_venv = fresh.join(".venv"); let mkvenv = tool(Path::new(python), &fresh, &["-m", "venv", ".venv"], &[]); @@ -1831,7 +1818,7 @@ fn pip_requirements_revert_after_the_vendor_comment_is_edited() { let requirements_before = std::fs::read(proj.join("requirements.txt")).unwrap(); let blobs = proj.join(".socket/blobs"); let blobs_before = tmp.path().join("blobs"); - copy_dir_recursive(&blobs, &blobs_before); + common::copy_tree(&blobs, &blobs_before); let edits: [(&str, fn(&str) -> String); 3] = [ ("respaced", |l| { @@ -1847,7 +1834,7 @@ fn pip_requirements_revert_after_the_vendor_comment_is_edited() { ]; for (edit, apply_edit) in edits { if !blobs.exists() { - copy_dir_recursive(&blobs_before, &blobs); + common::copy_tree(&blobs_before, &blobs); } let (code, stdout, stderr) = run_vendored(&VendorDriver::VendorOffline, &proj); assert_eq!( @@ -1951,7 +1938,7 @@ fn pip_vendored_requirements_evaluate_environment_markers() { fresh.join("requirements.txt"), ) .unwrap(); - copy_dir_recursive(&project.join(".socket"), &fresh.join(".socket")); + common::copy_tree(&project.join(".socket"), &fresh.join(".socket")); assert_tool_ok( &tool(Path::new(python), &fresh, &["-m", "venv", ".venv"], &[]), "create fresh venv", @@ -2073,7 +2060,7 @@ fn pip_vendored_requirements_marker_split_across_includes() { for file in ["requirements.txt", "base.txt"] { std::fs::copy(project.join(file), fresh.join(file)).unwrap(); } - copy_dir_recursive(&project.join(".socket"), &fresh.join(".socket")); + common::copy_tree(&project.join(".socket"), &fresh.join(".socket")); assert_tool_ok( &tool(Path::new(python), &fresh, &["-m", "venv", ".venv"], &[]), "create fresh venv", @@ -2181,7 +2168,7 @@ fn pip_vendored_requirements_marker_split_rewrites_matching_branch() { fresh.join("requirements.txt"), ) .unwrap(); - copy_dir_recursive(&project.join(".socket"), &fresh.join(".socket")); + common::copy_tree(&project.join(".socket"), &fresh.join(".socket")); assert_tool_ok( &tool(Path::new(python), &fresh, &["-m", "venv", ".venv"], &[]), "create fresh venv", diff --git a/crates/socket-patch-cli/tests/e2e_vendor_yarn_berry_build.rs b/crates/socket-patch-cli/tests/e2e_vendor_yarn_berry_build.rs index 14fa7489c..acec2c9b3 100644 --- a/crates/socket-patch-cli/tests/e2e_vendor_yarn_berry_build.rs +++ b/crates/socket-patch-cli/tests/e2e_vendor_yarn_berry_build.rs @@ -220,19 +220,6 @@ fn parse_envelope(stdout: &str) -> serde_json::Value { .unwrap_or_else(|e| panic!("vendor --json output is not JSON: {e}\nstdout:\n{stdout}")) } -fn copy_dir_recursive(src: &Path, dst: &Path) { - std::fs::create_dir_all(dst).unwrap(); - for entry in std::fs::read_dir(src).unwrap() { - let entry = entry.unwrap(); - let to = dst.join(entry.file_name()); - if entry.file_type().unwrap().is_dir() { - copy_dir_recursive(&entry.path(), &to); - } else { - std::fs::copy(entry.path(), &to).unwrap(); - } - } -} - // ── the capstone ────────────────────────────────────────────────────── #[tokio::test(flavor = "multi_thread")] @@ -557,7 +544,7 @@ async fn run_berry_capstone(driver: VendorDriver, yarnrc_extra: &str) { std::fs::copy(&pkg_path, fresh.join("package.json")).unwrap(); std::fs::copy(&lock_path, fresh.join("yarn.lock")).unwrap(); std::fs::copy(proj.join(".yarnrc.yml"), fresh.join(".yarnrc.yml")).unwrap(); - copy_dir_recursive(&proj.join(".socket"), &fresh.join(".socket")); + common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); let fresh_global = tmp.path().join("fresh-yarn-global"); let ci = corepack( diff --git a/crates/socket-patch-cli/tests/e2e_vendor_yarn_classic_build.rs b/crates/socket-patch-cli/tests/e2e_vendor_yarn_classic_build.rs index dc9181854..010ceecc9 100644 --- a/crates/socket-patch-cli/tests/e2e_vendor_yarn_classic_build.rs +++ b/crates/socket-patch-cli/tests/e2e_vendor_yarn_classic_build.rs @@ -164,19 +164,6 @@ fn stage_patch(proj: &Path, purl: &str, file_key: &str, before: &[u8], after: &[ std::fs::write(socket.join("blobs").join(git_sha256(after)), after).unwrap(); } -fn copy_dir_recursive(src: &Path, dst: &Path) { - std::fs::create_dir_all(dst).unwrap(); - for entry in std::fs::read_dir(src).unwrap() { - let entry = entry.unwrap(); - let to = dst.join(entry.file_name()); - if entry.file_type().unwrap().is_dir() { - copy_dir_recursive(&entry.path(), &to); - } else { - std::fs::copy(entry.path(), &to).unwrap(); - } - } -} - // ── the capstone ────────────────────────────────────────────────────── #[test] @@ -391,7 +378,7 @@ fn yarn_classic_vendor_fresh_checkout_frozen_offline_install_and_revert() { std::fs::create_dir_all(&fresh).unwrap(); std::fs::copy(proj.join("package.json"), fresh.join("package.json")).unwrap(); std::fs::copy(&lock_path, fresh.join("yarn.lock")).unwrap(); - copy_dir_recursive(&proj.join(".socket"), &fresh.join(".socket")); + common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); let fresh_cache = tmp.path().join("fresh-yarn-cache"); let ci = corepack( @@ -453,7 +440,7 @@ fn yarn_classic_vendor_fresh_checkout_frozen_offline_install_and_revert() { // 7. MANIFEST-LESS VEX over the really-installed fresh checkout (a copy, // so the idempotency/revert legs below still see the vendored proj). let vex_dir = tmp.path().join("fresh-vex"); - copy_dir_recursive(&fresh, &vex_dir); + common::copy_tree(&fresh, &vex_dir); let api = vex_e2e_common::PatchApi::start(vec![( UUID.to_string(), vex_e2e_common::patch_view( @@ -758,7 +745,7 @@ fn yarn_classic_detached_scan_vendored_fresh_checkout_manifestless_vex() { std::fs::create_dir_all(&fresh).unwrap(); std::fs::copy(proj.join("package.json"), fresh.join("package.json")).unwrap(); std::fs::copy(proj.join("yarn.lock"), fresh.join("yarn.lock")).unwrap(); - copy_dir_recursive(&proj.join(".socket"), &fresh.join(".socket")); + common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); let fresh_cache = tmp.path().join("fresh-yarn-cache"); let ci = corepack( &fresh, diff --git a/crates/socket-patch-cli/tests/e2e_vendor_yarn_classic_dev_flow.rs b/crates/socket-patch-cli/tests/e2e_vendor_yarn_classic_dev_flow.rs index 01bb85ff0..54aaa1c82 100644 --- a/crates/socket-patch-cli/tests/e2e_vendor_yarn_classic_dev_flow.rs +++ b/crates/socket-patch-cli/tests/e2e_vendor_yarn_classic_dev_flow.rs @@ -182,19 +182,6 @@ fn stage_patch(proj: &Path, purl: &str, file_key: &str, before: &[u8], after: &[ std::fs::write(socket.join("blobs").join(git_sha256(after)), after).unwrap(); } -fn copy_dir_recursive(src: &Path, dst: &Path) { - std::fs::create_dir_all(dst).unwrap(); - for entry in std::fs::read_dir(src).unwrap() { - let entry = entry.unwrap(); - let to = dst.join(entry.file_name()); - if entry.file_type().unwrap().is_dir() { - copy_dir_recursive(&entry.path(), &to); - } else { - std::fs::copy(entry.path(), &to).unwrap(); - } - } -} - /// The contiguous lockfile block (header line through the following blank /// line) whose header starts with `"@`. Yarn classic writes one block /// per resolution with headers like `resolve@^1.20.0:` or @@ -350,7 +337,7 @@ fn yarn_classic_vendored_lock_survives_dev_install_resave() { std::fs::create_dir_all(&fresh).unwrap(); std::fs::copy(proj.join("package.json"), fresh.join("package.json")).unwrap(); std::fs::copy(&lock_path, fresh.join("yarn.lock")).unwrap(); - copy_dir_recursive(&proj.join(".socket"), &fresh.join(".socket")); + common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); let fresh_cache = tmp.path().join("fresh-yarn-cache"); let dev = corepack( @@ -692,7 +679,7 @@ fn yarn_classic_drift_skipped_revert_keeps_artifacts_and_recovers() { // entry is no longer wired. let api = patch_api(&purl, &patched); let drift = tmp.path().join("drift-vex"); - copy_dir_recursive(&proj, &drift); + common::copy_tree(&proj, &drift); vex_e2e_common::strip_manifest(&drift); let out = vex_e2e_common::run_vex( &vex_e2e_common::binary(), @@ -748,7 +735,7 @@ fn yarn_classic_drift_skipped_revert_keeps_artifacts_and_recovers() { // 5b. The recovered (re-wired, re-installed) state attests again with no // manifest and no ledger — lockfile + kept tarball + patch API. let recovered = tmp.path().join("recovered-vex"); - copy_dir_recursive(&proj, &recovered); + common::copy_tree(&proj, &recovered); vex_e2e_common::strip_manifest(&recovered); vex_e2e_common::strip_ledgers(&recovered); let out = vex_e2e_common::run_vex( diff --git a/crates/socket-patch-cli/tests/e2e_vex_build/main.rs b/crates/socket-patch-cli/tests/e2e_vex_build/main.rs index b2d5a5ea0..53bd14e7a 100644 --- a/crates/socket-patch-cli/tests/e2e_vex_build/main.rs +++ b/crates/socket-patch-cli/tests/e2e_vex_build/main.rs @@ -15,6 +15,9 @@ //! The hermetic twins of these suites are the same-named modules of //! `tests/e2e_vex_lockfile/`. +#[cfg(unix)] +#[path = "../common/fs_tree.rs"] +mod fs_tree; #[path = "../common/rollback_json.rs"] mod rollback_json; diff --git a/crates/socket-patch-cli/tests/e2e_vex_build/poetry.rs b/crates/socket-patch-cli/tests/e2e_vex_build/poetry.rs index 6401c6355..a810492c2 100644 --- a/crates/socket-patch-cli/tests/e2e_vex_build/poetry.rs +++ b/crates/socket-patch-cli/tests/e2e_vex_build/poetry.rs @@ -53,7 +53,7 @@ //! shares one HTTP-cache lock under the home directory and wedges parallel //! runs) and an in-project `.venv`. -use crate::vex_e2e_common; +use crate::{fs_tree, vex_e2e_common}; use std::ffi::OsString; use std::path::{Path, PathBuf}; @@ -277,19 +277,7 @@ fn fresh_checkout(from: &Path, to: &Path) { std::fs::copy(from.join(file), to.join(file)).unwrap(); } if from.join(".socket").is_dir() { - copy_dir(&from.join(".socket"), &to.join(".socket")); - } -} - -fn copy_dir(src: &Path, dst: &Path) { - std::fs::create_dir_all(dst).unwrap(); - for entry in std::fs::read_dir(src).unwrap().flatten() { - let target = dst.join(entry.file_name()); - if entry.file_type().unwrap().is_dir() { - copy_dir(&entry.path(), &target); - } else { - std::fs::copy(entry.path(), &target).unwrap(); - } + fs_tree::copy_tree(&from.join(".socket"), &to.join(".socket")); } } diff --git a/crates/socket-patch-cli/tests/e2e_yarn4_pnpm_linker_build.rs b/crates/socket-patch-cli/tests/e2e_yarn4_pnpm_linker_build.rs index c1a32baf3..5622d52e1 100644 --- a/crates/socket-patch-cli/tests/e2e_yarn4_pnpm_linker_build.rs +++ b/crates/socket-patch-cli/tests/e2e_yarn4_pnpm_linker_build.rs @@ -150,19 +150,6 @@ fn run_socket(cwd: &Path, args: &[&str]) -> (i32, String, String) { ) } -fn copy_dir_recursive(src: &Path, dst: &Path) { - std::fs::create_dir_all(dst).unwrap(); - for entry in std::fs::read_dir(src).unwrap() { - let entry = entry.unwrap(); - let to = dst.join(entry.file_name()); - if entry.file_type().unwrap().is_dir() { - copy_dir_recursive(&entry.path(), &to); - } else { - std::fs::copy(entry.path(), &to).unwrap(); - } - } -} - /// The pnpm-linker layout invariant: `node_modules/` is a symlink and /// the backing store entry lives under `node_modules/.store/-…`. This /// is the assertion that makes these capstones about the LAYOUT rather than @@ -243,11 +230,11 @@ fn stage_patch(proj: &Path, purl: &str, before: &[u8], after: &[u8]) { /// Build a patched npm tarball (`package/` prefix, marker-prepended index.js) /// from the installed dep directory (read through the store symlink — -/// copy_dir_recursive reads file contents, so the layout is flattened into a +/// copy_tree reads file contents, so the layout is flattened into a /// regular `package/` tree exactly as a registry tarball would carry it). fn build_patched_tgz(installed_dir: &Path, patched_index: &[u8], out_tgz: &Path) { let stage = out_tgz.parent().unwrap().join("tarstage"); - copy_dir_recursive(installed_dir, &stage.join("package")); + common::copy_tree(installed_dir, &stage.join("package")); std::fs::write(stage.join("package").join("index.js"), patched_index).unwrap(); let tar = Command::new("tar") .args(["-czf", out_tgz.to_str().unwrap(), "package"]) @@ -363,7 +350,7 @@ fn fresh_checkout_install(tmp: &Path, proj: &Path, yarnrc: &str) -> (PathBuf, Ou std::fs::write(fresh.join(".yarnrc.yml"), yarnrc).unwrap(); // v5 hosted mode may leave no `.socket/` at all (no ledger, no manifest). if proj.join(".socket").is_dir() { - copy_dir_recursive(&proj.join(".socket"), &fresh.join(".socket")); + common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); } let fresh_global = tmp.join("fresh-yarn-global"); let ci = corepack( @@ -998,7 +985,7 @@ fn yarn4_pnpm_linker_agent_apply_patches_bundled_copy_inside_store_package() { "module.exports = require.resolve('is-number');\n", ) .unwrap(); - copy_dir_recursive( + common::copy_tree( ®istry_copy, &stage.join("node_modules").join("is-number"), ); diff --git a/crates/socket-patch-cli/tests/e2e_yarn4_workspaces_build.rs b/crates/socket-patch-cli/tests/e2e_yarn4_workspaces_build.rs index d4eb34e0e..ff7010a22 100644 --- a/crates/socket-patch-cli/tests/e2e_yarn4_workspaces_build.rs +++ b/crates/socket-patch-cli/tests/e2e_yarn4_workspaces_build.rs @@ -150,19 +150,6 @@ fn run_socket(cwd: &Path, args: &[&str]) -> (i32, String, String) { ) } -fn copy_dir_recursive(src: &Path, dst: &Path) { - std::fs::create_dir_all(dst).unwrap(); - for entry in std::fs::read_dir(src).unwrap() { - let entry = entry.unwrap(); - let to = dst.join(entry.file_name()); - if entry.file_type().unwrap().is_dir() { - copy_dir_recursive(&entry.path(), &to); - } else { - std::fs::copy(entry.path(), &to).unwrap(); - } - } -} - /// Write the workspace fixture: root (no deps) + packages/app (left-pad). fn write_workspace_fixture(proj: &Path) { std::fs::create_dir_all(proj.join("packages/app")).unwrap(); @@ -247,7 +234,7 @@ fn stage_patch(proj: &Path, purl: &str, before: &[u8], after: &[u8]) { /// from the installed dep directory. fn build_patched_tgz(installed_dir: &Path, patched_index: &[u8], out_tgz: &Path) { let stage = out_tgz.parent().unwrap().join("tarstage"); - copy_dir_recursive(installed_dir, &stage.join("package")); + common::copy_tree(installed_dir, &stage.join("package")); std::fs::write(stage.join("package").join("index.js"), patched_index).unwrap(); let tar = Command::new("tar") .args(["-czf", out_tgz.to_str().unwrap(), "package"]) @@ -371,7 +358,7 @@ fn fresh_checkout_install(tmp: &Path, proj: &Path, yarnrc: &str) -> (PathBuf, Ou std::fs::write(fresh.join(".yarnrc.yml"), yarnrc).unwrap(); // v5 hosted mode may leave no `.socket/` at all (no ledger, no manifest). if proj.join(".socket").is_dir() { - copy_dir_recursive(&proj.join(".socket"), &fresh.join(".socket")); + common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); } let fresh_global = tmp.join("fresh-yarn-global"); let ci = corepack( diff --git a/crates/socket-patch-cli/tests/in_process_redirect_pdm.rs b/crates/socket-patch-cli/tests/in_process_redirect_pdm.rs index 10d5b0a3a..4ed40f332 100644 --- a/crates/socket-patch-cli/tests/in_process_redirect_pdm.rs +++ b/crates/socket-patch-cli/tests/in_process_redirect_pdm.rs @@ -30,6 +30,10 @@ use socket_patch_core::hash::git_sha256::compute_git_sha256_from_bytes; use wiremock::matchers::{method, path, path_regex}; use wiremock::{Mock, MockServer, ResponseTemplate}; +#[allow(clippy::duplicate_mod)] +#[path = "common/fs_tree.rs"] +mod fs_tree; + #[path = "common/hermetic.rs"] mod hermetic; #[path = "vex_e2e_common/mod.rs"] @@ -303,7 +307,9 @@ fn manifestless_vex_steps(root: &Path, pristine_lock: &str) { let copy = tmp.path(); write_project(copy, &read(&root.join("pdm.lock"))); std::fs::copy(root.join("pyproject.toml"), copy.join("pyproject.toml")).unwrap(); - copy_dir(&root.join(".socket"), ©.join(".socket")); + if root.join(".socket").is_dir() { + fs_tree::copy_tree(&root.join(".socket"), ©.join(".socket")); + } strip_manifest(copy); assert_no_ledger(copy); @@ -345,7 +351,9 @@ fn manifestless_vex_steps(root: &Path, pristine_lock: &str) { let reverted = tmp2.path(); write_project(reverted, pristine_lock); std::fs::copy(root.join("pyproject.toml"), reverted.join("pyproject.toml")).unwrap(); - copy_dir(©.join(".socket"), &reverted.join(".socket")); + if copy.join(".socket").is_dir() { + fs_tree::copy_tree(©.join(".socket"), &reverted.join(".socket")); + } strip_manifest(reverted); for no_verify in [false, true] { let out = run_vex( @@ -379,21 +387,6 @@ fn legacy_record(view: &serde_json::Value) -> serde_json::Value { record } -fn copy_dir(from: &Path, to: &Path) { - if !from.is_dir() { - return; - } - std::fs::create_dir_all(to).unwrap(); - for entry in std::fs::read_dir(from).unwrap().flatten() { - let target = to.join(entry.file_name()); - if entry.path().is_dir() { - copy_dir(&entry.path(), &target); - } else { - std::fs::copy(entry.path(), target).unwrap(); - } - } -} - /// Lock-only hosted redirect regression: before the `pdm.lock` inventory /// reader, a fresh checkout with nothing installed surfaced no package to the /// batch API, so `redirected` was 0 and nothing was attested. diff --git a/crates/socket-patch-cli/tests/maven_build_common/mod.rs b/crates/socket-patch-cli/tests/maven_build_common/mod.rs index 5eca098ce..01ea050a8 100644 --- a/crates/socket-patch-cli/tests/maven_build_common/mod.rs +++ b/crates/socket-patch-cli/tests/maven_build_common/mod.rs @@ -25,6 +25,10 @@ #![allow(dead_code)] +#[allow(clippy::duplicate_mod)] +#[path = "../common/fs_tree.rs"] +mod fs_tree; + use std::ffi::OsString; use std::io::{Read as _, Write as _}; use std::path::{Path, PathBuf}; @@ -506,19 +510,6 @@ pub fn sha1_hex(bytes: &[u8]) -> String { // ── files ─────────────────────────────────────────────────────────────── -pub fn copy_dir_recursive(src: &Path, dst: &Path) { - std::fs::create_dir_all(dst).unwrap(); - for entry in std::fs::read_dir(src).unwrap() { - let entry = entry.unwrap(); - let to = dst.join(entry.file_name()); - if entry.file_type().unwrap().is_dir() { - copy_dir_recursive(&entry.path(), &to); - } else { - std::fs::copy(entry.path(), &to).unwrap(); - } - } -} - /// A "fresh checkout" of `proj` in `dst`: only the committable files — /// `pom.xml`, `.mvn/` and `.socket/` (never `target/`). pub fn fresh_checkout(proj: &Path, dst: &Path) { @@ -526,7 +517,7 @@ pub fn fresh_checkout(proj: &Path, dst: &Path) { std::fs::copy(proj.join("pom.xml"), dst.join("pom.xml")).unwrap(); for dir in [".mvn", ".socket"] { if proj.join(dir).is_dir() { - copy_dir_recursive(&proj.join(dir), &dst.join(dir)); + fs_tree::copy_tree(&proj.join(dir), &dst.join(dir)); } } assert!(!dst.join("target").exists()); diff --git a/crates/socket-patch-cli/tests/mode_migration_bun.rs b/crates/socket-patch-cli/tests/mode_migration_bun.rs index c25b4b68d..ad012211e 100644 --- a/crates/socket-patch-cli/tests/mode_migration_bun.rs +++ b/crates/socket-patch-cli/tests/mode_migration_bun.rs @@ -483,19 +483,6 @@ fn read_json(proj: &Path, rel: &str) -> Value { serde_json::from_str(&text).unwrap_or_else(|e| panic!("{rel} is not JSON: {e}\n{text}")) } -fn copy_dir_recursive(src: &Path, dst: &Path) { - std::fs::create_dir_all(dst).unwrap(); - for entry in std::fs::read_dir(src).unwrap() { - let entry = entry.unwrap(); - let to = dst.join(entry.file_name()); - if entry.file_type().unwrap().is_dir() { - copy_dir_recursive(&entry.path(), &to); - } else { - std::fs::copy(entry.path(), &to).unwrap(); - } - } -} - /// Every regular file under `root` (relative `/`-joined path → bytes), /// `node_modules` excluded — the write-free oracle for the dry-run legs. /// `.socket/apply.lock` is deliberately NOT excluded: every run removes its @@ -771,9 +758,9 @@ fn copy_project(from: &Path, to: &Path) { std::fs::copy(from.join("package.json"), to.join("package.json")).unwrap(); std::fs::copy(from.join("bun.lock"), to.join("bun.lock")).unwrap(); if from.join(".socket").is_dir() { - copy_dir_recursive(&from.join(".socket"), &to.join(".socket")); + common::copy_tree(&from.join(".socket"), &to.join(".socket")); } - copy_dir_recursive(&from.join("node_modules"), &to.join("node_modules")); + common::copy_tree(&from.join("node_modules"), &to.join("node_modules")); } /// ONLY the committable files (package.json, bun.lock, `.socket/` when it @@ -783,7 +770,7 @@ fn fresh_checkout(from: &Path, to: &Path) { std::fs::copy(from.join("package.json"), to.join("package.json")).unwrap(); std::fs::copy(from.join("bun.lock"), to.join("bun.lock")).unwrap(); if from.join(".socket").is_dir() { - copy_dir_recursive(&from.join(".socket"), &to.join(".socket")); + common::copy_tree(&from.join(".socket"), &to.join(".socket")); } } diff --git a/crates/socket-patch-cli/tests/mode_migration_cargo.rs b/crates/socket-patch-cli/tests/mode_migration_cargo.rs index d20e66300..8d1e4683b 100644 --- a/crates/socket-patch-cli/tests/mode_migration_cargo.rs +++ b/crates/socket-patch-cli/tests/mode_migration_cargo.rs @@ -155,19 +155,6 @@ fn stage_patch(proj: &Path, purl: &str, before: &[u8], after: &[u8]) { std::fs::write(socket.join("blobs").join(git_sha256(after)), after).unwrap(); } -fn copy_dir_recursive(src: &Path, dst: &Path) { - std::fs::create_dir_all(dst).unwrap(); - for entry in std::fs::read_dir(src).unwrap() { - let entry = entry.unwrap(); - let to = dst.join(entry.file_name()); - if entry.file_type().unwrap().is_dir() { - copy_dir_recursive(&entry.path(), &to); - } else { - std::fs::copy(entry.path(), &to).unwrap(); - } - } -} - fn locked_version(lock_text: &str, name: &str) -> Option { let needle = format!("name = \"{name}\""); let mut lines = lock_text.lines(); @@ -223,7 +210,7 @@ fn build_patched_crate( ) -> Vec { let leaf = format!("{DEP}-{version}"); let pkg_dir = stage_root.join(&leaf); - copy_dir_recursive(crate_dir, &pkg_dir); + common::copy_tree(crate_dir, &pkg_dir); let _ = std::fs::remove_file(pkg_dir.join(".cargo-checksum.json")); std::fs::write(pkg_dir.join("src/lib.rs"), patched).unwrap(); let mut bytes = Vec::new(); @@ -639,11 +626,11 @@ fn fresh_checkout(proj: &Path, tmp: &Path, tag: &str) -> (PathBuf, PathBuf) { std::fs::copy(proj.join("Cargo.toml"), fresh.join("Cargo.toml")).unwrap(); std::fs::copy(proj.join("Cargo.lock"), fresh.join("Cargo.lock")).unwrap(); if proj.join(".cargo").exists() { - copy_dir_recursive(&proj.join(".cargo"), &fresh.join(".cargo")); + common::copy_tree(&proj.join(".cargo"), &fresh.join(".cargo")); } - copy_dir_recursive(&proj.join("src"), &fresh.join("src")); + common::copy_tree(&proj.join("src"), &fresh.join("src")); if proj.join(".socket").exists() { - copy_dir_recursive(&proj.join(".socket"), &fresh.join(".socket")); + common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); } let home = tmp.join(format!("fresh-home-{tag}")); std::fs::create_dir_all(&home).unwrap(); diff --git a/crates/socket-patch-cli/tests/mode_migration_npm.rs b/crates/socket-patch-cli/tests/mode_migration_npm.rs index 7e2013645..b5cd2e0d6 100644 --- a/crates/socket-patch-cli/tests/mode_migration_npm.rs +++ b/crates/socket-patch-cli/tests/mode_migration_npm.rs @@ -117,19 +117,6 @@ fn run_socket_env(cwd: &Path, args: &[&str], env: &[(&str, &str)]) -> (i32, Stri ) } -fn copy_dir_recursive(src: &Path, dst: &Path) { - std::fs::create_dir_all(dst).unwrap(); - for entry in std::fs::read_dir(src).unwrap() { - let entry = entry.unwrap(); - let to = dst.join(entry.file_name()); - if entry.file_type().unwrap().is_dir() { - copy_dir_recursive(&entry.path(), &to); - } else { - std::fs::copy(entry.path(), &to).unwrap(); - } - } -} - /// Write `.socket/manifest.json` + the after-hash blob so `vendor --offline` /// runs fully offline (npm-family file keys carry the `package/` prefix). fn stage_patch(proj: &Path, before: &[u8], after: &[u8]) { @@ -596,7 +583,7 @@ fn fresh_checkout(proj: &Path, tmp: &Path, tag: &str, berry: bool) -> PathBuf { if berry { std::fs::copy(proj.join(".yarnrc.yml"), fresh.join(".yarnrc.yml")).unwrap(); } - copy_dir_recursive(&proj.join(".socket"), &fresh.join(".socket")); + common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); fresh } diff --git a/crates/socket-patch-cli/tests/prebuilt_common/mod.rs b/crates/socket-patch-cli/tests/prebuilt_common/mod.rs index 4e740107e..1033c8375 100644 --- a/crates/socket-patch-cli/tests/prebuilt_common/mod.rs +++ b/crates/socket-patch-cli/tests/prebuilt_common/mod.rs @@ -15,6 +15,10 @@ use socket_patch_core::vendor::test_support::service_fixture::{ use wiremock::matchers::{method, path, path_regex}; use wiremock::{Mock, MockServer, ResponseTemplate}; +#[allow(clippy::duplicate_mod)] +#[path = "../common/fs_tree.rs"] +mod fs_tree; + #[path = "../common/jvm_env.rs"] pub mod jvm_env; @@ -535,7 +539,7 @@ pub async fn mount_view_from_source( let (name, version) = package.rsplit_once('@').unwrap(); let name = name.replace("%40", "@"); let layout = package_layout(purl); - copy_tree(installed.unwrap_or(layout.path()), &source); + fs_tree::copy_tree(installed.unwrap_or(layout.path()), &source); if let Some(gemspec) = gemspec { std::fs::create_dir_all(tmp.path().join("specifications")).unwrap(); std::fs::write( @@ -719,19 +723,6 @@ fn fabricate_files21_named( dir } -fn copy_tree(from: &Path, to: &Path) { - std::fs::create_dir_all(to).unwrap(); - for entry in std::fs::read_dir(from).unwrap() { - let entry = entry.unwrap(); - let dest = to.join(entry.file_name()); - if entry.file_type().unwrap().is_dir() { - copy_tree(&entry.path(), &dest); - } else { - std::fs::copy(entry.path(), dest).unwrap(); - } - } -} - pub async fn mount_download(server: &MockServer, purl: &str, uuid: &str, leaf: &str, bytes: &[u8]) { let record: PatchRecord = serde_json::from_value(serde_json::json!({ "uuid": uuid, "exportedAt": "2026-01-01T00:00:00Z", "files": {}, diff --git a/crates/socket-patch-cli/tests/vendor_jvm_cli.rs b/crates/socket-patch-cli/tests/vendor_jvm_cli.rs index 496488eb9..412d26bf4 100644 --- a/crates/socket-patch-cli/tests/vendor_jvm_cli.rs +++ b/crates/socket-patch-cli/tests/vendor_jvm_cli.rs @@ -1413,18 +1413,6 @@ fn gradle_vendor_429_repair_restores_missing_gitattributes() { ok(root, &["vendor", "--check"]); } -fn copy_dir(from: &Path, to: &Path) { - std::fs::create_dir_all(to).unwrap(); - for e in std::fs::read_dir(from).unwrap() { - let e = e.unwrap(); - if e.file_type().unwrap().is_dir() { - copy_dir(&e.path(), &to.join(e.file_name())); - } else { - std::fs::copy(e.path(), to.join(e.file_name())).unwrap(); - } - } -} - /// #428 repair: a JVM ledger entry under a subproject of the Gradle build /// is not repaired there (the real build would never read the restored /// tree); nothing is written. @@ -1435,7 +1423,7 @@ fn gradle_vendor_428_repair_from_subproject_refuses() { fixture(root, Shape::Gradle, &[("foo", FOO_UUID)]); std::fs::create_dir_all(root.join("proj/.git")).unwrap(); ok(root, &["vendor"]); - copy_dir(&root.join("proj/.socket"), &root.join("proj/app/.socket")); + common::copy_tree(&root.join("proj/.socket"), &root.join("proj/app/.socket")); std::fs::remove_file( root.join("proj/app/.socket/vendor/gradle/org/example/foo/1.0/foo-1.0.jar"), ) diff --git a/crates/socket-patch-cli/tests/vex_e2e_common/uv.rs b/crates/socket-patch-cli/tests/vex_e2e_common/uv.rs index 35913958a..e9910cd03 100644 --- a/crates/socket-patch-cli/tests/vex_e2e_common/uv.rs +++ b/crates/socket-patch-cli/tests/vex_e2e_common/uv.rs @@ -71,6 +71,10 @@ #![allow(dead_code)] +#[allow(clippy::duplicate_mod)] +#[path = "../common/fs_tree.rs"] +mod fs_tree; + #[path = "../prebuilt_common/mod.rs"] mod prebuilt_common; @@ -497,23 +501,6 @@ impl Report<'_> { // ── filesystem ───────────────────────────────────────────────────────── -/// Copy `src` recursively to `dst`; a missing `src` copies nothing (a -/// hosted flow commits no `.socket/` in v5). -pub fn copy_tree(src: &Path, dst: &Path) { - if !src.is_dir() { - return; - } - std::fs::create_dir_all(dst).unwrap(); - for entry in std::fs::read_dir(src).unwrap().flatten() { - let to = dst.join(entry.file_name()); - if entry.file_type().unwrap().is_dir() { - copy_tree(&entry.path(), &to); - } else { - std::fs::copy(entry.path(), &to).unwrap(); - } - } -} - /// `/lib/python3.X/site-packages` (or `Lib/site-packages`). pub fn site_packages(venv: &Path) -> Option { if cfg!(windows) { @@ -1449,7 +1436,9 @@ pub fn run_lane(suite: &str, uv: &Uv, mode: Mode, lane: Lane) { let fresh = tmp.path().join("fresh"); std::fs::create_dir_all(&fresh).unwrap(); restore(&fresh, &wired); - copy_tree(&proj.join(".socket"), &fresh.join(".socket")); + if proj.join(".socket").is_dir() { + fs_tree::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); + } let _ = std::fs::remove_dir_all(fresh.join(".socket/blobs")); let _ = std::fs::remove_file(fresh.join("embedded.vex.json")); strip_manifest(&fresh); @@ -1857,7 +1846,9 @@ fn script_sibling_revert(uv: &Uv, report: &Report<'_>, proj: &Path, tmp: &Path) for f in [SCRIPT, "tool.py.lock"] { std::fs::copy(proj.join(f), dir.join(f)).unwrap(); } - copy_tree(&proj.join(".socket"), &dir.join(".socket")); + if proj.join(".socket").is_dir() { + fs_tree::copy_tree(&proj.join(".socket"), &dir.join(".socket")); + } let cache = tmp.join("sibling-cache"); let out = uv.run_py(&dir, &["add", "--script", SCRIPT, "idna==3.7"], &cache); if !ok(&out) { @@ -2224,7 +2215,9 @@ pub fn production_manifestless(p: &Production<'_>) { for f in ["pyproject.toml", "uv.lock"] { std::fs::copy(p.proj.join(f), fresh.join(f)).unwrap(); } - copy_tree(&p.proj.join(".socket"), &fresh.join(".socket")); + if p.proj.join(".socket").is_dir() { + fs_tree::copy_tree(&p.proj.join(".socket"), &fresh.join(".socket")); + } strip_manifest(&fresh); let sync = |dir: &Path, cache: &Path, offline: bool| { let mut cmd = Command::new(p.uv); diff --git a/crates/socket-patch-cli/tests/vlt_vendor_common/mod.rs b/crates/socket-patch-cli/tests/vlt_vendor_common/mod.rs index 652d4bc4f..ea59dd30f 100644 --- a/crates/socket-patch-cli/tests/vlt_vendor_common/mod.rs +++ b/crates/socket-patch-cli/tests/vlt_vendor_common/mod.rs @@ -18,6 +18,10 @@ #![allow(dead_code)] +#[allow(clippy::duplicate_mod)] +#[path = "../common/fs_tree.rs"] +mod fs_tree; + #[path = "../common/hermetic.rs"] mod hermetic; #[path = "../prebuilt_common/mod.rs"] @@ -74,19 +78,6 @@ pub fn file_id_v1(rel: &str) -> String { format!("file~{}", rel.replace('_', "__").replace('/', "+")) } -pub fn copy_dir(src: &Path, dst: &Path) { - std::fs::create_dir_all(dst).unwrap(); - for entry in std::fs::read_dir(src).unwrap() { - let entry = entry.unwrap(); - let to = dst.join(entry.file_name()); - if entry.file_type().unwrap().is_dir() { - copy_dir(&entry.path(), &to); - } else { - std::fs::copy(entry.path(), &to).unwrap(); - } - } -} - /// Every file under `dir` (relative, forward-slashed) with its bytes. pub fn snapshot(dir: &Path) -> std::collections::BTreeMap> { fn walk(root: &Path, dir: &Path, out: &mut std::collections::BTreeMap>) { @@ -338,7 +329,7 @@ pub fn dep_ids(lock: &str, name: &str, version: &str) -> Vec { /// A capture's project copied to `root`, the target installed in vlt's /// store (every instance) and its patch staged at [`FIXTURE_UUID`]. pub fn capture_project(root: &Path, case: &Case) { - copy_dir(&case.project_dir(), root); + fs_tree::copy_tree(&case.project_dir(), root); let lock = std::fs::read_to_string(root.join(VLT_LOCK)).unwrap(); let (name, version) = case.coords(); for id in dep_ids(&lock, &name, &version) { diff --git a/crates/socket-patch-cli/tests/yarn_berry_common/mod.rs b/crates/socket-patch-cli/tests/yarn_berry_common/mod.rs index dffab3915..ad325f465 100644 --- a/crates/socket-patch-cli/tests/yarn_berry_common/mod.rs +++ b/crates/socket-patch-cli/tests/yarn_berry_common/mod.rs @@ -68,6 +68,10 @@ #![allow(dead_code)] +#[allow(clippy::duplicate_mod)] +#[path = "../common/fs_tree.rs"] +mod fs_tree; + use std::io::Write as _; use std::path::{Path, PathBuf}; use std::process::Output; @@ -519,7 +523,7 @@ impl<'a> BerryVexFlow<'a> { } std::fs::write(fresh.join(".yarnrc.yml"), self.yarnrc).unwrap(); if socket && self.proj.join(".socket").is_dir() { - copy_dir(&self.proj.join(".socket"), &fresh.join(".socket")); + fs_tree::copy_tree(&self.proj.join(".socket"), &fresh.join(".socket")); } fresh } @@ -597,20 +601,6 @@ impl<'a> BerryVexFlow<'a> { } } -/// Recursive copy (files and dirs; symlinks are followed). -pub fn copy_dir(src: &Path, dst: &Path) { - std::fs::create_dir_all(dst).unwrap(); - for entry in std::fs::read_dir(src).unwrap() { - let entry = entry.unwrap(); - let to = dst.join(entry.file_name()); - if entry.path().is_dir() { - copy_dir(&entry.path(), &to); - } else { - std::fs::copy(entry.path(), &to).unwrap(); - } - } -} - /// A single-member `.tgz` whose `package/index.js` holds `bytes` — the /// tampered stand-in for a committed vendored artifact. fn single_member_tgz(bytes: &[u8]) -> Vec { @@ -873,7 +863,7 @@ pub fn run_manifestless_vex_matrix(flow: &BerryVexFlow<'_>) -> Vec { std::fs::write(reverted.join(rel), bytes).unwrap(); } if flow.registry_cache.is_dir() { - copy_dir(&flow.registry_cache, &reverted.join(".yarn/cache")); + fs_tree::copy_tree(&flow.registry_cache, &reverted.join(".yarn/cache")); } flow.install(&reverted, &["install", "--immutable"], "reverted install"); assert_eq!( From f81c50cd1c3b035932c1e8e08236974a664f7de1 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 13:21:13 +0000 Subject: [PATCH 3/4] Share the scan suites' mock public proxy e2e_cargo, e2e_composer, e2e_maven and e2e_nuget each defined a byte-identical start_proxy and assert_proxy_served_scans. Both now live in tests/common/scan_proxy.rs. Assisted-by: Claude Code:claude-opus-5-5 --- crates/socket-patch-cli/tests/common/mod.rs | 3 +- .../tests/common/scan_proxy.rs | 34 +++++++++++++++++++ crates/socket-patch-cli/tests/e2e_cargo.rs | 33 +----------------- crates/socket-patch-cli/tests/e2e_composer.rs | 33 +----------------- crates/socket-patch-cli/tests/e2e_maven.rs | 33 +----------------- crates/socket-patch-cli/tests/e2e_nuget.rs | 33 +----------------- .../tests/e2e_redirect_bun_build.rs | 2 +- .../tests/e2e_redirect_cargo_build.rs | 10 +++--- .../tests/e2e_redirect_cargo_shapes.rs | 8 ++--- .../tests/e2e_redirect_yarn_berry_build.rs | 4 +-- .../tests/e2e_vendor_bun_build.rs | 2 +- .../tests/e2e_vendor_cargo_build.rs | 18 +++++----- .../tests/e2e_vendor_composer_build.rs | 2 +- .../tests/e2e_vendor_gem_build.rs | 28 +++++++-------- .../tests/e2e_vendor_pnpm_build.rs | 14 ++++---- .../tests/e2e_vendor_pypi_build.rs | 14 ++++---- .../tests/e2e_vendor_yarn_berry_build.rs | 2 +- .../tests/e2e_vendor_yarn_classic_build.rs | 6 ++-- .../tests/e2e_vendor_yarn_classic_dev_flow.rs | 6 ++-- .../tests/e2e_yarn4_pnpm_linker_build.rs | 6 ++-- .../tests/e2e_yarn4_workspaces_build.rs | 4 +-- .../tests/mode_migration_bun.rs | 6 ++-- .../tests/mode_migration_cargo.rs | 8 ++--- .../tests/mode_migration_npm.rs | 2 +- .../socket-patch-cli/tests/vendor_jvm_cli.rs | 2 +- 25 files changed, 112 insertions(+), 201 deletions(-) create mode 100644 crates/socket-patch-cli/tests/common/scan_proxy.rs diff --git a/crates/socket-patch-cli/tests/common/mod.rs b/crates/socket-patch-cli/tests/common/mod.rs index 1458e3a96..ef3496037 100644 --- a/crates/socket-patch-cli/tests/common/mod.rs +++ b/crates/socket-patch-cli/tests/common/mod.rs @@ -53,7 +53,8 @@ pub use oracle::{binary, git_sha256}; /// `#[path = "../common/fs_tree.rs"] mod fs_tree;`. pub mod fs_tree; -pub use fs_tree::copy_tree; +/// The mock public proxy the hermetic `scan` suites answer with. +pub mod scan_proxy; // ── Binary discovery + invocation ───────────────────────────────────── diff --git a/crates/socket-patch-cli/tests/common/scan_proxy.rs b/crates/socket-patch-cli/tests/common/scan_proxy.rs new file mode 100644 index 000000000..ab5a1b727 --- /dev/null +++ b/crates/socket-patch-cli/tests/common/scan_proxy.rs @@ -0,0 +1,34 @@ +//! The mock Socket public proxy the hermetic `scan` suites (`e2e_cargo`, +//! `e2e_composer`, `e2e_maven`, `e2e_nuget`) pin with `--proxy-url`. + +use wiremock::matchers::{method, path}; +use wiremock::{Mock, MockServer, ResponseTemplate}; + +/// Start a mock Socket public proxy answering the scan's `POST /patch/batch` +/// with an empty (no-patch) result, so no scan ever leaves localhost. +pub async fn start_proxy() -> MockServer { + let server = MockServer::start().await; + Mock::given(method("POST")) + .and(path("/patch/batch")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "packages": [], + "canAccessPaidPatches": false, + }))) + .mount(&server) + .await; + server +} + +/// Hermeticity guard: every scan in a test must have routed its patch lookup +/// through the in-test proxy. Fewer recorded requests than scans means at +/// least one binary invocation talked to the live API (or skipped the lookup +/// outright) despite the pinning. +pub async fn assert_proxy_served_scans(server: &MockServer, scans: usize) { + let requests = server.received_requests().await.unwrap_or_default(); + assert!( + requests.len() >= scans, + "expected all {scans} scan invocations to hit the in-test proxy; \ + recorded only {} request(s)", + requests.len() + ); +} diff --git a/crates/socket-patch-cli/tests/e2e_cargo.rs b/crates/socket-patch-cli/tests/e2e_cargo.rs index c9b2b00ac..d2767d920 100644 --- a/crates/socket-patch-cli/tests/e2e_cargo.rs +++ b/crates/socket-patch-cli/tests/e2e_cargo.rs @@ -20,29 +20,12 @@ use common::{binary, git_sha256, hermetic}; use std::path::{Path, PathBuf}; use std::process::Output; -use wiremock::matchers::{method, path}; -use wiremock::{Mock, MockServer, ResponseTemplate}; +use common::scan_proxy::{assert_proxy_served_scans, start_proxy}; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- -/// Start a mock Socket public proxy answering the scan's `POST /patch/batch` -/// with an empty (no-patch) result, so no scan in this file ever leaves -/// localhost. -async fn start_proxy() -> MockServer { - let server = MockServer::start().await; - Mock::given(method("POST")) - .and(path("/patch/batch")) - .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ - "packages": [], - "canAccessPaidPatches": false, - }))) - .mount(&server) - .await; - server -} - /// Run the binary as a blocking subprocess (off the async runtime so the /// in-test proxy can service its requests concurrently), pinned to `proxy_url`. /// @@ -105,20 +88,6 @@ async fn scan_json(cwd: &Path, proxy_url: &str) -> serde_json::Value { value } -/// Hermeticity guard: every scan in a test must have routed its patch lookup -/// through the in-test proxy. Fewer recorded requests than scans means at -/// least one binary invocation talked to the live API (or skipped the lookup -/// outright) despite the pinning. -async fn assert_proxy_served_scans(server: &MockServer, scans: usize) { - let requests = server.received_requests().await.unwrap_or_default(); - assert!( - requests.len() >= scans, - "expected all {scans} scan invocations to hit the in-test proxy; \ - recorded only {} request(s)", - requests.len() - ); -} - // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- diff --git a/crates/socket-patch-cli/tests/e2e_composer.rs b/crates/socket-patch-cli/tests/e2e_composer.rs index c148bcba8..e2ee81da6 100644 --- a/crates/socket-patch-cli/tests/e2e_composer.rs +++ b/crates/socket-patch-cli/tests/e2e_composer.rs @@ -18,29 +18,12 @@ use common::{binary, hermetic}; use std::process::Output; -use wiremock::matchers::{method, path}; -use wiremock::{Mock, MockServer, ResponseTemplate}; +use common::scan_proxy::{assert_proxy_served_scans, start_proxy}; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- -/// Start a mock Socket public proxy answering the scan's `POST /patch/batch` -/// with an empty (no-patch) result, so no scan in this file ever leaves -/// localhost. Same shape as the e2e_nuget/e2e_gem harnesses. -async fn start_proxy() -> MockServer { - let server = MockServer::start().await; - Mock::given(method("POST")) - .and(path("/patch/batch")) - .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ - "packages": [], - "canAccessPaidPatches": false, - }))) - .mount(&server) - .await; - server -} - /// Run the binary as a blocking subprocess (off the async runtime so the /// in-test proxy can service its requests concurrently), pinned to /// `proxy_url`. The child gets the hermetic `SOCKET_*` environment (no @@ -72,20 +55,6 @@ async fn run(args: &[&str], cwd: &std::path::Path, proxy_url: &str) -> Output { .expect("socket-patch subprocess task panicked") } -/// Hermeticity guard: every scan in a test must have routed its patch lookup -/// through the in-test proxy. Fewer recorded requests than scans means a -/// binary invocation talked to the live API (or skipped the lookup) despite -/// the pinning. -async fn assert_proxy_served_scans(server: &MockServer, scans: usize) { - let requests = server.received_requests().await.unwrap_or_default(); - assert!( - requests.len() >= scans, - "expected all {scans} scan invocations to hit the in-test proxy; \ - recorded only {} request(s)", - requests.len() - ); -} - /// Run `socket-patch scan --json ...`, assert the process succeeded, and /// return the parsed JSON envelope from stdout. /// diff --git a/crates/socket-patch-cli/tests/e2e_maven.rs b/crates/socket-patch-cli/tests/e2e_maven.rs index a455fcc85..cf7687b94 100644 --- a/crates/socket-patch-cli/tests/e2e_maven.rs +++ b/crates/socket-patch-cli/tests/e2e_maven.rs @@ -20,29 +20,12 @@ use common::hermetic; use std::path::Path; use std::process::Output; -use wiremock::matchers::{method, path}; -use wiremock::{Mock, MockServer, ResponseTemplate}; +use common::scan_proxy::{assert_proxy_served_scans, start_proxy}; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- -/// Start a mock Socket public proxy answering the scan's `POST /patch/batch` -/// with an empty (no-patch) result, so no scan in this file ever leaves -/// localhost. -async fn start_proxy() -> MockServer { - let server = MockServer::start().await; - Mock::given(method("POST")) - .and(path("/patch/batch")) - .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ - "packages": [], - "canAccessPaidPatches": false, - }))) - .mount(&server) - .await; - server -} - /// Run the binary as a blocking subprocess (off the async runtime so the /// in-test proxy can service its requests concurrently), pinned to `proxy_url`. /// @@ -84,20 +67,6 @@ async fn run(args: &[&str], cwd: &Path, m2_repo: &Path, proxy_url: &str) -> Outp .expect("socket-patch subprocess task panicked") } -/// Hermeticity guard: every scan in a test must have routed its patch lookup -/// through the in-test proxy. Fewer recorded requests than scans means at -/// least one binary invocation talked to the live API (or skipped the lookup -/// outright) despite the pinning. -async fn assert_proxy_served_scans(server: &MockServer, scans: usize) { - let requests = server.received_requests().await.unwrap_or_default(); - assert!( - requests.len() >= scans, - "expected all {scans} scan invocations to hit the in-test proxy; \ - recorded only {} request(s)", - requests.len() - ); -} - // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- diff --git a/crates/socket-patch-cli/tests/e2e_nuget.rs b/crates/socket-patch-cli/tests/e2e_nuget.rs index 9e7584efc..0db4fed5b 100644 --- a/crates/socket-patch-cli/tests/e2e_nuget.rs +++ b/crates/socket-patch-cli/tests/e2e_nuget.rs @@ -20,29 +20,12 @@ use common::{binary, hermetic}; use std::path::Path; use std::process::Output; -use wiremock::matchers::{method, path}; -use wiremock::{Mock, MockServer, ResponseTemplate}; +use common::scan_proxy::{assert_proxy_served_scans, start_proxy}; // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- -/// Start a mock Socket public proxy answering the scan's `POST /patch/batch` -/// with an empty (no-patch) result, so no scan in this file ever leaves -/// localhost. -async fn start_proxy() -> MockServer { - let server = MockServer::start().await; - Mock::given(method("POST")) - .and(path("/patch/batch")) - .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ - "packages": [], - "canAccessPaidPatches": false, - }))) - .mount(&server) - .await; - server -} - /// Run the binary as a blocking subprocess (off the async runtime so the /// in-test proxy can service its requests concurrently), pinned to `proxy_url`. /// @@ -155,20 +138,6 @@ async fn assert_json_scanned( ); } -/// Hermeticity guard: every scan in a test must have routed its patch lookup -/// through the in-test proxy. Fewer recorded requests than scans means at -/// least one binary invocation talked to the live API (or skipped the lookup -/// outright) despite the pinning. -async fn assert_proxy_served_scans(server: &MockServer, scans: usize) { - let requests = server.received_requests().await.unwrap_or_default(); - assert!( - requests.len() >= scans, - "expected all {scans} scan invocations to hit the in-test proxy; \ - recorded only {} request(s)", - requests.len() - ); -} - // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- diff --git a/crates/socket-patch-cli/tests/e2e_redirect_bun_build.rs b/crates/socket-patch-cli/tests/e2e_redirect_bun_build.rs index f51705d4c..4926412a5 100644 --- a/crates/socket-patch-cli/tests/e2e_redirect_bun_build.rs +++ b/crates/socket-patch-cli/tests/e2e_redirect_bun_build.rs @@ -1136,7 +1136,7 @@ fn fresh_checkout(fx: &BunRedirectFixture, name: &str) -> PathBuf { std::fs::copy(fx.proj.join("bunfig.toml"), fresh.join("bunfig.toml")).unwrap(); } if fx.proj.join(".socket").is_dir() { - common::copy_tree(&fx.proj.join(".socket"), &fresh.join(".socket")); + common::fs_tree::copy_tree(&fx.proj.join(".socket"), &fresh.join(".socket")); } fresh } diff --git a/crates/socket-patch-cli/tests/e2e_redirect_cargo_build.rs b/crates/socket-patch-cli/tests/e2e_redirect_cargo_build.rs index a0a5f89bc..bb2b8f514 100644 --- a/crates/socket-patch-cli/tests/e2e_redirect_cargo_build.rs +++ b/crates/socket-patch-cli/tests/e2e_redirect_cargo_build.rs @@ -212,7 +212,7 @@ fn build_patched_crate( ) -> Vec { let leaf = format!("{DEP}-{version}"); let pkg_dir = stage_root.join(&leaf); - common::copy_tree(crate_dir, &pkg_dir); + common::fs_tree::copy_tree(crate_dir, &pkg_dir); let _ = std::fs::remove_file(pkg_dir.join(".cargo-checksum.json")); std::fs::write(pkg_dir.join("src/lib.rs"), patched).unwrap(); @@ -657,11 +657,11 @@ fn fresh_checkout_cargo_fetch(fx: &RedirectFixture) -> (PathBuf, PathBuf, Output std::fs::create_dir_all(&fresh).unwrap(); std::fs::copy(fx.proj.join("Cargo.toml"), fresh.join("Cargo.toml")).unwrap(); std::fs::copy(fx.proj.join("Cargo.lock"), fresh.join("Cargo.lock")).unwrap(); - common::copy_tree(&fx.proj.join(".cargo"), &fresh.join(".cargo")); - common::copy_tree(&fx.proj.join("src"), &fresh.join("src")); + common::fs_tree::copy_tree(&fx.proj.join(".cargo"), &fresh.join(".cargo")); + common::fs_tree::copy_tree(&fx.proj.join("src"), &fresh.join("src")); // v5 hosted mode writes nothing under `.socket/`; carry it when present. if fx.proj.join(".socket").is_dir() { - common::copy_tree(&fx.proj.join(".socket"), &fresh.join(".socket")); + common::fs_tree::copy_tree(&fx.proj.join(".socket"), &fresh.join(".socket")); } let fresh_home = fx.tmp.path().join("fresh-cargo-home"); @@ -829,7 +829,7 @@ impl ManifestlessHosted { std::fs::create_dir_all(&revert_home).unwrap(); std::fs::write(revert.join("Cargo.toml"), &self.baseline_toml).unwrap(); std::fs::write(revert.join("Cargo.lock"), &self.baseline_lock).unwrap(); - common::copy_tree(&fresh.join("src"), &revert.join("src")); + common::fs_tree::copy_tree(&fresh.join("src"), &revert.join("src")); let fetch = cargo(&revert, &["fetch", "--locked"], &revert_home); assert!( fetch.status.success(), diff --git a/crates/socket-patch-cli/tests/e2e_redirect_cargo_shapes.rs b/crates/socket-patch-cli/tests/e2e_redirect_cargo_shapes.rs index a33cca165..7f6a35ac7 100644 --- a/crates/socket-patch-cli/tests/e2e_redirect_cargo_shapes.rs +++ b/crates/socket-patch-cli/tests/e2e_redirect_cargo_shapes.rs @@ -227,7 +227,7 @@ async fn mount_crates_index_mirror(pristine_lock: &str, patches: &[Patch]) -> Mo fn build_crate(stage: &Path, crate_dir: &Path, leaf: &str, patched: &[u8]) -> Vec { let pkg = stage.join(leaf); - common::copy_tree(crate_dir, &pkg); + common::fs_tree::copy_tree(crate_dir, &pkg); let _ = std::fs::remove_file(pkg.join(".cargo-checksum.json")); std::fs::write(pkg.join("src/lib.rs"), patched).unwrap(); let mut bytes = Vec::new(); @@ -627,7 +627,7 @@ async fn run_shape(shape: Shape) -> Option<()> { std::fs::copy(proj.join(&rel), to).unwrap(); } if proj.join(".socket").is_dir() { - common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); + common::fs_tree::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); } let fresh_home = tmp.path().join("fresh-home"); std::fs::create_dir_all(&fresh_home).unwrap(); @@ -721,7 +721,7 @@ async fn run_shape(shape: Shape) -> Option<()> { ("SOCKET_PATCH_SERVER_URL", uri.as_str()), ]; let post_scan = tmp.path().join("post-scan"); - common::copy_tree(&proj, &post_scan); + common::fs_tree::copy_tree(&proj, &post_scan); let mut orders = vec![shape.patches.clone()]; if shape.patches.len() > 1 { orders.push(shape.patches.iter().rev().copied().collect()); @@ -729,7 +729,7 @@ async fn run_shape(shape: Shape) -> Option<()> { for (n, order) in orders.iter().enumerate() { if n > 0 { std::fs::remove_dir_all(&proj).unwrap(); - common::copy_tree(&post_scan, &proj); + common::fs_tree::copy_tree(&post_scan, &proj); } for patch in order { let purl = patch.purl(); diff --git a/crates/socket-patch-cli/tests/e2e_redirect_yarn_berry_build.rs b/crates/socket-patch-cli/tests/e2e_redirect_yarn_berry_build.rs index 61d1a440f..7f9b6066f 100644 --- a/crates/socket-patch-cli/tests/e2e_redirect_yarn_berry_build.rs +++ b/crates/socket-patch-cli/tests/e2e_redirect_yarn_berry_build.rs @@ -163,7 +163,7 @@ fn run_socket(cwd: &Path, args: &[&str]) -> (i32, String, String) { /// from the installed dep directory. fn build_patched_tgz(installed_dir: &Path, patched_index: &[u8], out_tgz: &Path) { let stage = out_tgz.parent().unwrap().join("tarstage"); - common::copy_tree(installed_dir, &stage.join("package")); + common::fs_tree::copy_tree(installed_dir, &stage.join("package")); std::fs::write(stage.join("package").join("index.js"), patched_index).unwrap(); let tar = Command::new("tar") .args(["-czf", out_tgz.to_str().unwrap(), "package"]) @@ -645,7 +645,7 @@ fn fresh_checkout_yarn_install(fx: &BerryRedirectFixture) -> (PathBuf, Output) { std::fs::write(fresh.join(".yarnrc.yml"), fresh_yarnrc(fx)).unwrap(); // v5 hosted mode may leave no `.socket/` at all (no ledger, no manifest). if fx.proj.join(".socket").is_dir() { - common::copy_tree(&fx.proj.join(".socket"), &fresh.join(".socket")); + common::fs_tree::copy_tree(&fx.proj.join(".socket"), &fresh.join(".socket")); } let fresh_global = fx.tmp.path().join("fresh-yarn-global"); let ci = corepack( diff --git a/crates/socket-patch-cli/tests/e2e_vendor_bun_build.rs b/crates/socket-patch-cli/tests/e2e_vendor_bun_build.rs index 9fcc54d0a..1daeb312d 100644 --- a/crates/socket-patch-cli/tests/e2e_vendor_bun_build.rs +++ b/crates/socket-patch-cli/tests/e2e_vendor_bun_build.rs @@ -813,7 +813,7 @@ fn fresh_checkout(fx: &BunProject, name: &str) -> PathBuf { if fx.proj.join("bunfig.toml").is_file() { std::fs::copy(fx.proj.join("bunfig.toml"), fresh.join("bunfig.toml")).unwrap(); } - common::copy_tree(&fx.proj.join(".socket"), &fresh.join(".socket")); + common::fs_tree::copy_tree(&fx.proj.join(".socket"), &fresh.join(".socket")); fresh } diff --git a/crates/socket-patch-cli/tests/e2e_vendor_cargo_build.rs b/crates/socket-patch-cli/tests/e2e_vendor_cargo_build.rs index 36f8ed0e1..6a25b3ca1 100644 --- a/crates/socket-patch-cli/tests/e2e_vendor_cargo_build.rs +++ b/crates/socket-patch-cli/tests/e2e_vendor_cargo_build.rs @@ -538,8 +538,8 @@ impl ManifestlessVendored { strip_patch_table(&std::fs::read_to_string(fresh.join("Cargo.toml")).unwrap()), ) .unwrap(); - common::copy_tree(&fresh.join("src"), &revert.join("src")); - common::copy_tree(&fresh.join(".socket"), &revert.join(".socket")); + common::fs_tree::copy_tree(&fresh.join("src"), &revert.join("src")); + common::fs_tree::copy_tree(&fresh.join(".socket"), &revert.join(".socket")); std::fs::write( revert.join("src/main.rs"), "fn main() { println!(\"baseline\"); }\n", @@ -760,8 +760,8 @@ fn cargo_vendor_fresh_checkout_locked_offline_build_and_revert() { std::fs::create_dir_all(&fresh).unwrap(); std::fs::copy(proj.join("Cargo.toml"), fresh.join("Cargo.toml")).unwrap(); std::fs::copy(&lock_path, fresh.join("Cargo.lock")).unwrap(); - common::copy_tree(&proj.join("src"), &fresh.join("src")); - common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); + common::fs_tree::copy_tree(&proj.join("src"), &fresh.join("src")); + common::fs_tree::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); let fresh_home = tmp.path().join("fresh-cargo-home"); std::fs::create_dir_all(&fresh_home).unwrap(); @@ -1217,8 +1217,8 @@ async fn cargo_get_uuid_vendored_fresh_checkout_locked_build() { std::fs::create_dir_all(&fresh).unwrap(); std::fs::copy(proj.join("Cargo.toml"), fresh.join("Cargo.toml")).unwrap(); std::fs::copy(proj.join("Cargo.lock"), fresh.join("Cargo.lock")).unwrap(); - common::copy_tree(&proj.join("src"), &fresh.join("src")); - common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); + common::fs_tree::copy_tree(&proj.join("src"), &fresh.join("src")); + common::fs_tree::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); let fresh_home = tmp.path().join("fresh-cargo-home"); std::fs::create_dir_all(&fresh_home).unwrap(); @@ -1329,7 +1329,7 @@ fn fresh_checkout(proj: &Path, tmp: &Path, tag: &str) -> (PathBuf, PathBuf) { } for dir in ["src", ".socket", ".cargo"] { if proj.join(dir).exists() { - common::copy_tree(&proj.join(dir), &fresh.join(dir)); + common::fs_tree::copy_tree(&proj.join(dir), &fresh.join(dir)); } } let home = tmp.join(format!("fresh-home-{tag}")); @@ -2616,7 +2616,7 @@ fn cargo_url_spelled_crates_io_patch_table_is_refused() { return; }; // A user's same-version fork of cfg-if under the URL spelling. - common::copy_tree(&crate_dir, &proj.join("fork-cfg-if")); + common::fs_tree::copy_tree(&crate_dir, &proj.join("fork-cfg-if")); let manifest = format!( "{}\n[patch.\"https://github.com/rust-lang/crates.io-index\"]\n{DEP} = {{ path = \"fork-cfg-if\" }}\n", std::fs::read_to_string(proj.join("Cargo.toml")).unwrap() @@ -2700,7 +2700,7 @@ fn cargo_ancestor_config_patch_cannot_silently_shadow_the_vendored_copy() { // A monorepo-level config adds the user's same-version, unpatched fork // under the bare crate name. let fork = tmp.path().join(format!("forks/{DEP}-{version}")); - common::copy_tree(&crate_dir, &fork); + common::fs_tree::copy_tree(&crate_dir, &fork); std::fs::create_dir_all(tmp.path().join(".cargo")).unwrap(); std::fs::write( tmp.path().join(".cargo/config.toml"), diff --git a/crates/socket-patch-cli/tests/e2e_vendor_composer_build.rs b/crates/socket-patch-cli/tests/e2e_vendor_composer_build.rs index d6b3693e5..d80dc8f8b 100644 --- a/crates/socket-patch-cli/tests/e2e_vendor_composer_build.rs +++ b/crates/socket-patch-cli/tests/e2e_vendor_composer_build.rs @@ -467,7 +467,7 @@ fn assert_manifestless_vendored_vex( std::fs::create_dir_all(&reverted).unwrap(); std::fs::copy(fresh.join("composer.json"), reverted.join("composer.json")).unwrap(); std::fs::write(reverted.join("composer.lock"), registry_lock).unwrap(); - common::copy_tree(&fresh.join(".socket"), &reverted.join(".socket")); + common::fs_tree::copy_tree(&fresh.join(".socket"), &reverted.join(".socket")); let install = composer( &reverted, &["install"], diff --git a/crates/socket-patch-cli/tests/e2e_vendor_gem_build.rs b/crates/socket-patch-cli/tests/e2e_vendor_gem_build.rs index 16ab969bd..329a27176 100644 --- a/crates/socket-patch-cli/tests/e2e_vendor_gem_build.rs +++ b/crates/socket-patch-cli/tests/e2e_vendor_gem_build.rs @@ -328,8 +328,8 @@ fn vendored_manifestless_vex_matrix(v: &Vendored<'_>) { std::fs::create_dir_all(©).unwrap(); std::fs::copy(fresh.join("Gemfile"), copy.join("Gemfile")).unwrap(); std::fs::write(copy.join(lock_name), v.pristine_lock).unwrap(); - common::copy_tree(&fresh.join(".socket"), ©.join(".socket")); - common::copy_tree(v.committed_bundle, ©.join(".bundle")); + common::fs_tree::copy_tree(&fresh.join(".socket"), ©.join(".socket")); + common::fs_tree::copy_tree(v.committed_bundle, ©.join(".bundle")); for f in std::fs::read_dir(&saved).unwrap() { let f = f.unwrap(); std::fs::copy(f.path(), copy.join(".socket/vendor").join(f.file_name())).unwrap(); @@ -350,7 +350,7 @@ fn vendored_manifestless_vex_matrix(v: &Vendored<'_>) { // Bundler <= 2.0 persisted `BUNDLE_FROZEN: "true"` into the copy's // `.bundle/config` on that frozen attempt; a developer's unfrozen // install runs from the committed config. - common::copy_tree(v.committed_bundle, ©.join(".bundle")); + common::fs_tree::copy_tree(v.committed_bundle, ©.join(".bundle")); let install = bundle(©, &["install"], false); assert!( install.status.success(), @@ -599,8 +599,8 @@ fn gem_vendor_fresh_checkout_bundle_install_and_revert() { std::fs::create_dir_all(&fresh).unwrap(); std::fs::copy(&gemfile_path, fresh.join("Gemfile")).unwrap(); std::fs::copy(&lock_path, fresh.join("Gemfile.lock")).unwrap(); - common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); - common::copy_tree(&proj.join(".bundle"), &fresh.join(".bundle")); + common::fs_tree::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); + common::fs_tree::copy_tree(&proj.join(".bundle"), &fresh.join(".bundle")); assert!( !fresh.join("vendor").exists(), "fresh checkout must not carry an installed tree (test bug)" @@ -900,8 +900,8 @@ fn gem_vendor_transitive_dep_fresh_checkout_and_revert() { std::fs::create_dir_all(&fresh).unwrap(); std::fs::copy(&gemfile_path, fresh.join("Gemfile")).unwrap(); std::fs::copy(&lock_path, fresh.join("Gemfile.lock")).unwrap(); - common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); - common::copy_tree(&proj.join(".bundle"), &fresh.join(".bundle")); + common::fs_tree::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); + common::fs_tree::copy_tree(&proj.join(".bundle"), &fresh.join(".bundle")); assert!( !fresh.join("vendor").exists(), "fresh checkout must not carry an installed tree (test bug)" @@ -1293,8 +1293,8 @@ async fn gem_get_uuid_vendored_fresh_checkout_bundle_install() { std::fs::create_dir_all(&fresh).unwrap(); std::fs::copy(&gemfile_path, fresh.join("Gemfile")).unwrap(); std::fs::copy(&lock_path, fresh.join("Gemfile.lock")).unwrap(); - common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); - common::copy_tree(&proj.join(".bundle"), &fresh.join(".bundle")); + common::fs_tree::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); + common::fs_tree::copy_tree(&proj.join(".bundle"), &fresh.join(".bundle")); assert!( !fresh.join("vendor").exists(), "fresh checkout must not carry an installed tree (test bug)" @@ -1755,8 +1755,8 @@ fn gem_vendor_second_gem_section_fresh_checkout_and_revert() { std::fs::create_dir_all(&fresh).unwrap(); std::fs::copy(&gemfile_path, fresh.join("Gemfile")).unwrap(); std::fs::copy(&lock_path, fresh.join("Gemfile.lock")).unwrap(); - common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); - common::copy_tree(&proj.join(".bundle"), &fresh.join(".bundle")); + common::fs_tree::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); + common::fs_tree::copy_tree(&proj.join(".bundle"), &fresh.join(".bundle")); let lock_wired = std::fs::read(&lock_path).unwrap(); let ci = bundle(&fresh, &["install"], true); assert!( @@ -2200,7 +2200,7 @@ fn gem_vendor_crlf_lock_fresh_checkout_and_revert() { std::fs::create_dir_all(&pristine).unwrap(); std::fs::copy(&gemfile_path, pristine.join("Gemfile")).unwrap(); std::fs::copy(&lock_path, pristine.join("Gemfile.lock")).unwrap(); - common::copy_tree(&proj.join(".bundle"), &pristine.join(".bundle")); + common::fs_tree::copy_tree(&proj.join(".bundle"), &pristine.join(".bundle")); let frozen_crlf_ok = bundle(&pristine, &["install"], true).status.success(); let cwd = proj.to_str().unwrap(); let vendor = |extra: &[&str]| { @@ -2249,8 +2249,8 @@ fn gem_vendor_crlf_lock_fresh_checkout_and_revert() { std::fs::create_dir_all(&fresh).unwrap(); std::fs::copy(&gemfile_path, fresh.join("Gemfile")).unwrap(); std::fs::copy(&lock_path, fresh.join("Gemfile.lock")).unwrap(); - common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); - common::copy_tree(&proj.join(".bundle"), &fresh.join(".bundle")); + common::fs_tree::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); + common::fs_tree::copy_tree(&proj.join(".bundle"), &fresh.join(".bundle")); let ci = bundle(&fresh, &["install"], frozen_crlf_ok); assert!( ci.status.success(), diff --git a/crates/socket-patch-cli/tests/e2e_vendor_pnpm_build.rs b/crates/socket-patch-cli/tests/e2e_vendor_pnpm_build.rs index 4d76f08a1..32829123e 100644 --- a/crates/socket-patch-cli/tests/e2e_vendor_pnpm_build.rs +++ b/crates/socket-patch-cli/tests/e2e_vendor_pnpm_build.rs @@ -679,7 +679,7 @@ async fn run_pnpm_capstone(pm: &str, driver: VendorDriver) { if ws_created { std::fs::copy(&ws_path, fresh.join("pnpm-workspace.yaml")).unwrap(); } - common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); + common::fs_tree::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); let fresh_store = tmp.path().join("fresh-pnpm-store"); let mut ci = corepack( @@ -1756,7 +1756,7 @@ fn run_two_document_capstone( for file in ["package.json", "pnpm-lock.yaml", "pnpm-workspace.yaml"] { std::fs::copy(proj.join(file), fresh.join(file)).unwrap(); } - common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); + common::fs_tree::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); let fresh_store = tmp.path().join("fresh-store"); let ci = corepack( &fresh, @@ -1910,8 +1910,8 @@ fn pnpm_vendor_names_a_file_copy_of_the_vendored_release() { // Fresh checkout, empty store, frozen: a gets the vendored bytes, b // keeps the fork, exactly as the warning says. let fresh = tmp.path().join("fresh"); - common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); - common::copy_tree(&proj.join("forks"), &fresh.join("forks")); + common::fs_tree::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); + common::fs_tree::copy_tree(&proj.join("forks"), &fresh.join("forks")); for file in [ "package.json", "pnpm-lock.yaml", @@ -2232,7 +2232,7 @@ fn run_legacy_capstone_for(pm: &str, lock_head: &str, proj_dir: &str, dep: &str, std::fs::create_dir_all(&fresh).unwrap(); std::fs::copy(&pkg_path, fresh.join("package.json")).unwrap(); std::fs::copy(&lock_path, fresh.join("pnpm-lock.yaml")).unwrap(); - common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); + common::fs_tree::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); std::fs::write(fresh.join(".npmrc"), "registry=http://127.0.0.1:1/\n").unwrap(); let fresh_store = tmp.path().join("store-fresh"); @@ -2512,7 +2512,7 @@ fn pnpm_vendor_keeps_user_workspace_overrides_authoritative() { for file in ["package.json", "pnpm-lock.yaml", "pnpm-workspace.yaml"] { std::fs::copy(proj.join(file), fresh.join(file)).unwrap(); } - common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); + common::fs_tree::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); let fresh_store = tmp.path().join("fresh-pnpm-store"); let ci = corepack( &fresh, @@ -2854,7 +2854,7 @@ async fn run_build_approval_capstone(pm: &str, approval: &str, carried: bool) { for file in ["package.json", "pnpm-lock.yaml", "pnpm-workspace.yaml"] { std::fs::copy(proj.join(file), fresh.join(file)).unwrap(); } - common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); + common::fs_tree::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); std::fs::write(fresh.join(".npmrc"), "registry=http://127.0.0.1:1/\n").unwrap(); let fresh_store = tmp.path().join("fresh-pnpm-store"); let ci = corepack( diff --git a/crates/socket-patch-cli/tests/e2e_vendor_pypi_build.rs b/crates/socket-patch-cli/tests/e2e_vendor_pypi_build.rs index 7df2fcd06..26be5d28a 100644 --- a/crates/socket-patch-cli/tests/e2e_vendor_pypi_build.rs +++ b/crates/socket-patch-cli/tests/e2e_vendor_pypi_build.rs @@ -452,7 +452,7 @@ fn assert_fresh_checkout_frozen_offline( std::fs::create_dir_all(&fresh).unwrap(); std::fs::copy(proj.join("pyproject.toml"), fresh.join("pyproject.toml")).unwrap(); std::fs::copy(proj.join("uv.lock"), fresh.join("uv.lock")).unwrap(); - common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); + common::fs_tree::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); let fresh_cache = tmp.join("fresh-uv-cache"); let mut fresh_env: Vec<(&str, &str)> = vec![("UV_CACHE_DIR", fresh_cache.to_str().unwrap())]; @@ -1671,7 +1671,7 @@ fn pip_requirements_vendor_fresh_checkout_no_index_and_revert() { fresh.join("requirements.txt"), ) .unwrap(); - common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); + common::fs_tree::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); let fresh_venv = fresh.join(".venv"); let mkvenv = tool(Path::new(python), &fresh, &["-m", "venv", ".venv"], &[]); @@ -1818,7 +1818,7 @@ fn pip_requirements_revert_after_the_vendor_comment_is_edited() { let requirements_before = std::fs::read(proj.join("requirements.txt")).unwrap(); let blobs = proj.join(".socket/blobs"); let blobs_before = tmp.path().join("blobs"); - common::copy_tree(&blobs, &blobs_before); + common::fs_tree::copy_tree(&blobs, &blobs_before); let edits: [(&str, fn(&str) -> String); 3] = [ ("respaced", |l| { @@ -1834,7 +1834,7 @@ fn pip_requirements_revert_after_the_vendor_comment_is_edited() { ]; for (edit, apply_edit) in edits { if !blobs.exists() { - common::copy_tree(&blobs_before, &blobs); + common::fs_tree::copy_tree(&blobs_before, &blobs); } let (code, stdout, stderr) = run_vendored(&VendorDriver::VendorOffline, &proj); assert_eq!( @@ -1938,7 +1938,7 @@ fn pip_vendored_requirements_evaluate_environment_markers() { fresh.join("requirements.txt"), ) .unwrap(); - common::copy_tree(&project.join(".socket"), &fresh.join(".socket")); + common::fs_tree::copy_tree(&project.join(".socket"), &fresh.join(".socket")); assert_tool_ok( &tool(Path::new(python), &fresh, &["-m", "venv", ".venv"], &[]), "create fresh venv", @@ -2060,7 +2060,7 @@ fn pip_vendored_requirements_marker_split_across_includes() { for file in ["requirements.txt", "base.txt"] { std::fs::copy(project.join(file), fresh.join(file)).unwrap(); } - common::copy_tree(&project.join(".socket"), &fresh.join(".socket")); + common::fs_tree::copy_tree(&project.join(".socket"), &fresh.join(".socket")); assert_tool_ok( &tool(Path::new(python), &fresh, &["-m", "venv", ".venv"], &[]), "create fresh venv", @@ -2168,7 +2168,7 @@ fn pip_vendored_requirements_marker_split_rewrites_matching_branch() { fresh.join("requirements.txt"), ) .unwrap(); - common::copy_tree(&project.join(".socket"), &fresh.join(".socket")); + common::fs_tree::copy_tree(&project.join(".socket"), &fresh.join(".socket")); assert_tool_ok( &tool(Path::new(python), &fresh, &["-m", "venv", ".venv"], &[]), "create fresh venv", diff --git a/crates/socket-patch-cli/tests/e2e_vendor_yarn_berry_build.rs b/crates/socket-patch-cli/tests/e2e_vendor_yarn_berry_build.rs index acec2c9b3..8967c724b 100644 --- a/crates/socket-patch-cli/tests/e2e_vendor_yarn_berry_build.rs +++ b/crates/socket-patch-cli/tests/e2e_vendor_yarn_berry_build.rs @@ -544,7 +544,7 @@ async fn run_berry_capstone(driver: VendorDriver, yarnrc_extra: &str) { std::fs::copy(&pkg_path, fresh.join("package.json")).unwrap(); std::fs::copy(&lock_path, fresh.join("yarn.lock")).unwrap(); std::fs::copy(proj.join(".yarnrc.yml"), fresh.join(".yarnrc.yml")).unwrap(); - common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); + common::fs_tree::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); let fresh_global = tmp.path().join("fresh-yarn-global"); let ci = corepack( diff --git a/crates/socket-patch-cli/tests/e2e_vendor_yarn_classic_build.rs b/crates/socket-patch-cli/tests/e2e_vendor_yarn_classic_build.rs index 010ceecc9..b3e73b995 100644 --- a/crates/socket-patch-cli/tests/e2e_vendor_yarn_classic_build.rs +++ b/crates/socket-patch-cli/tests/e2e_vendor_yarn_classic_build.rs @@ -378,7 +378,7 @@ fn yarn_classic_vendor_fresh_checkout_frozen_offline_install_and_revert() { std::fs::create_dir_all(&fresh).unwrap(); std::fs::copy(proj.join("package.json"), fresh.join("package.json")).unwrap(); std::fs::copy(&lock_path, fresh.join("yarn.lock")).unwrap(); - common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); + common::fs_tree::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); let fresh_cache = tmp.path().join("fresh-yarn-cache"); let ci = corepack( @@ -440,7 +440,7 @@ fn yarn_classic_vendor_fresh_checkout_frozen_offline_install_and_revert() { // 7. MANIFEST-LESS VEX over the really-installed fresh checkout (a copy, // so the idempotency/revert legs below still see the vendored proj). let vex_dir = tmp.path().join("fresh-vex"); - common::copy_tree(&fresh, &vex_dir); + common::fs_tree::copy_tree(&fresh, &vex_dir); let api = vex_e2e_common::PatchApi::start(vec![( UUID.to_string(), vex_e2e_common::patch_view( @@ -745,7 +745,7 @@ fn yarn_classic_detached_scan_vendored_fresh_checkout_manifestless_vex() { std::fs::create_dir_all(&fresh).unwrap(); std::fs::copy(proj.join("package.json"), fresh.join("package.json")).unwrap(); std::fs::copy(proj.join("yarn.lock"), fresh.join("yarn.lock")).unwrap(); - common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); + common::fs_tree::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); let fresh_cache = tmp.path().join("fresh-yarn-cache"); let ci = corepack( &fresh, diff --git a/crates/socket-patch-cli/tests/e2e_vendor_yarn_classic_dev_flow.rs b/crates/socket-patch-cli/tests/e2e_vendor_yarn_classic_dev_flow.rs index 54aaa1c82..5c4b42da6 100644 --- a/crates/socket-patch-cli/tests/e2e_vendor_yarn_classic_dev_flow.rs +++ b/crates/socket-patch-cli/tests/e2e_vendor_yarn_classic_dev_flow.rs @@ -337,7 +337,7 @@ fn yarn_classic_vendored_lock_survives_dev_install_resave() { std::fs::create_dir_all(&fresh).unwrap(); std::fs::copy(proj.join("package.json"), fresh.join("package.json")).unwrap(); std::fs::copy(&lock_path, fresh.join("yarn.lock")).unwrap(); - common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); + common::fs_tree::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); let fresh_cache = tmp.path().join("fresh-yarn-cache"); let dev = corepack( @@ -679,7 +679,7 @@ fn yarn_classic_drift_skipped_revert_keeps_artifacts_and_recovers() { // entry is no longer wired. let api = patch_api(&purl, &patched); let drift = tmp.path().join("drift-vex"); - common::copy_tree(&proj, &drift); + common::fs_tree::copy_tree(&proj, &drift); vex_e2e_common::strip_manifest(&drift); let out = vex_e2e_common::run_vex( &vex_e2e_common::binary(), @@ -735,7 +735,7 @@ fn yarn_classic_drift_skipped_revert_keeps_artifacts_and_recovers() { // 5b. The recovered (re-wired, re-installed) state attests again with no // manifest and no ledger — lockfile + kept tarball + patch API. let recovered = tmp.path().join("recovered-vex"); - common::copy_tree(&proj, &recovered); + common::fs_tree::copy_tree(&proj, &recovered); vex_e2e_common::strip_manifest(&recovered); vex_e2e_common::strip_ledgers(&recovered); let out = vex_e2e_common::run_vex( diff --git a/crates/socket-patch-cli/tests/e2e_yarn4_pnpm_linker_build.rs b/crates/socket-patch-cli/tests/e2e_yarn4_pnpm_linker_build.rs index 5622d52e1..5c0c9ff2d 100644 --- a/crates/socket-patch-cli/tests/e2e_yarn4_pnpm_linker_build.rs +++ b/crates/socket-patch-cli/tests/e2e_yarn4_pnpm_linker_build.rs @@ -234,7 +234,7 @@ fn stage_patch(proj: &Path, purl: &str, before: &[u8], after: &[u8]) { /// regular `package/` tree exactly as a registry tarball would carry it). fn build_patched_tgz(installed_dir: &Path, patched_index: &[u8], out_tgz: &Path) { let stage = out_tgz.parent().unwrap().join("tarstage"); - common::copy_tree(installed_dir, &stage.join("package")); + common::fs_tree::copy_tree(installed_dir, &stage.join("package")); std::fs::write(stage.join("package").join("index.js"), patched_index).unwrap(); let tar = Command::new("tar") .args(["-czf", out_tgz.to_str().unwrap(), "package"]) @@ -350,7 +350,7 @@ fn fresh_checkout_install(tmp: &Path, proj: &Path, yarnrc: &str) -> (PathBuf, Ou std::fs::write(fresh.join(".yarnrc.yml"), yarnrc).unwrap(); // v5 hosted mode may leave no `.socket/` at all (no ledger, no manifest). if proj.join(".socket").is_dir() { - common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); + common::fs_tree::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); } let fresh_global = tmp.join("fresh-yarn-global"); let ci = corepack( @@ -985,7 +985,7 @@ fn yarn4_pnpm_linker_agent_apply_patches_bundled_copy_inside_store_package() { "module.exports = require.resolve('is-number');\n", ) .unwrap(); - common::copy_tree( + common::fs_tree::copy_tree( ®istry_copy, &stage.join("node_modules").join("is-number"), ); diff --git a/crates/socket-patch-cli/tests/e2e_yarn4_workspaces_build.rs b/crates/socket-patch-cli/tests/e2e_yarn4_workspaces_build.rs index ff7010a22..6a697341f 100644 --- a/crates/socket-patch-cli/tests/e2e_yarn4_workspaces_build.rs +++ b/crates/socket-patch-cli/tests/e2e_yarn4_workspaces_build.rs @@ -234,7 +234,7 @@ fn stage_patch(proj: &Path, purl: &str, before: &[u8], after: &[u8]) { /// from the installed dep directory. fn build_patched_tgz(installed_dir: &Path, patched_index: &[u8], out_tgz: &Path) { let stage = out_tgz.parent().unwrap().join("tarstage"); - common::copy_tree(installed_dir, &stage.join("package")); + common::fs_tree::copy_tree(installed_dir, &stage.join("package")); std::fs::write(stage.join("package").join("index.js"), patched_index).unwrap(); let tar = Command::new("tar") .args(["-czf", out_tgz.to_str().unwrap(), "package"]) @@ -358,7 +358,7 @@ fn fresh_checkout_install(tmp: &Path, proj: &Path, yarnrc: &str) -> (PathBuf, Ou std::fs::write(fresh.join(".yarnrc.yml"), yarnrc).unwrap(); // v5 hosted mode may leave no `.socket/` at all (no ledger, no manifest). if proj.join(".socket").is_dir() { - common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); + common::fs_tree::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); } let fresh_global = tmp.join("fresh-yarn-global"); let ci = corepack( diff --git a/crates/socket-patch-cli/tests/mode_migration_bun.rs b/crates/socket-patch-cli/tests/mode_migration_bun.rs index ad012211e..20cf48fe6 100644 --- a/crates/socket-patch-cli/tests/mode_migration_bun.rs +++ b/crates/socket-patch-cli/tests/mode_migration_bun.rs @@ -758,9 +758,9 @@ fn copy_project(from: &Path, to: &Path) { std::fs::copy(from.join("package.json"), to.join("package.json")).unwrap(); std::fs::copy(from.join("bun.lock"), to.join("bun.lock")).unwrap(); if from.join(".socket").is_dir() { - common::copy_tree(&from.join(".socket"), &to.join(".socket")); + common::fs_tree::copy_tree(&from.join(".socket"), &to.join(".socket")); } - common::copy_tree(&from.join("node_modules"), &to.join("node_modules")); + common::fs_tree::copy_tree(&from.join("node_modules"), &to.join("node_modules")); } /// ONLY the committable files (package.json, bun.lock, `.socket/` when it @@ -770,7 +770,7 @@ fn fresh_checkout(from: &Path, to: &Path) { std::fs::copy(from.join("package.json"), to.join("package.json")).unwrap(); std::fs::copy(from.join("bun.lock"), to.join("bun.lock")).unwrap(); if from.join(".socket").is_dir() { - common::copy_tree(&from.join(".socket"), &to.join(".socket")); + common::fs_tree::copy_tree(&from.join(".socket"), &to.join(".socket")); } } diff --git a/crates/socket-patch-cli/tests/mode_migration_cargo.rs b/crates/socket-patch-cli/tests/mode_migration_cargo.rs index 8d1e4683b..f0875d2e0 100644 --- a/crates/socket-patch-cli/tests/mode_migration_cargo.rs +++ b/crates/socket-patch-cli/tests/mode_migration_cargo.rs @@ -210,7 +210,7 @@ fn build_patched_crate( ) -> Vec { let leaf = format!("{DEP}-{version}"); let pkg_dir = stage_root.join(&leaf); - common::copy_tree(crate_dir, &pkg_dir); + common::fs_tree::copy_tree(crate_dir, &pkg_dir); let _ = std::fs::remove_file(pkg_dir.join(".cargo-checksum.json")); std::fs::write(pkg_dir.join("src/lib.rs"), patched).unwrap(); let mut bytes = Vec::new(); @@ -626,11 +626,11 @@ fn fresh_checkout(proj: &Path, tmp: &Path, tag: &str) -> (PathBuf, PathBuf) { std::fs::copy(proj.join("Cargo.toml"), fresh.join("Cargo.toml")).unwrap(); std::fs::copy(proj.join("Cargo.lock"), fresh.join("Cargo.lock")).unwrap(); if proj.join(".cargo").exists() { - common::copy_tree(&proj.join(".cargo"), &fresh.join(".cargo")); + common::fs_tree::copy_tree(&proj.join(".cargo"), &fresh.join(".cargo")); } - common::copy_tree(&proj.join("src"), &fresh.join("src")); + common::fs_tree::copy_tree(&proj.join("src"), &fresh.join("src")); if proj.join(".socket").exists() { - common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); + common::fs_tree::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); } let home = tmp.join(format!("fresh-home-{tag}")); std::fs::create_dir_all(&home).unwrap(); diff --git a/crates/socket-patch-cli/tests/mode_migration_npm.rs b/crates/socket-patch-cli/tests/mode_migration_npm.rs index b5cd2e0d6..c040708c6 100644 --- a/crates/socket-patch-cli/tests/mode_migration_npm.rs +++ b/crates/socket-patch-cli/tests/mode_migration_npm.rs @@ -583,7 +583,7 @@ fn fresh_checkout(proj: &Path, tmp: &Path, tag: &str, berry: bool) -> PathBuf { if berry { std::fs::copy(proj.join(".yarnrc.yml"), fresh.join(".yarnrc.yml")).unwrap(); } - common::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); + common::fs_tree::copy_tree(&proj.join(".socket"), &fresh.join(".socket")); fresh } diff --git a/crates/socket-patch-cli/tests/vendor_jvm_cli.rs b/crates/socket-patch-cli/tests/vendor_jvm_cli.rs index 412d26bf4..4046d00d3 100644 --- a/crates/socket-patch-cli/tests/vendor_jvm_cli.rs +++ b/crates/socket-patch-cli/tests/vendor_jvm_cli.rs @@ -1423,7 +1423,7 @@ fn gradle_vendor_428_repair_from_subproject_refuses() { fixture(root, Shape::Gradle, &[("foo", FOO_UUID)]); std::fs::create_dir_all(root.join("proj/.git")).unwrap(); ok(root, &["vendor"]); - common::copy_tree(&root.join("proj/.socket"), &root.join("proj/app/.socket")); + common::fs_tree::copy_tree(&root.join("proj/.socket"), &root.join("proj/app/.socket")); std::fs::remove_file( root.join("proj/app/.socket/vendor/gradle/org/example/foo/1.0/foo-1.0.jar"), ) From ef8e862bf6baa77229d630a795f9b24b2cdbd166 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Nov 2026 13:27:28 +0000 Subject: [PATCH 4/4] Keep copy_tree from following dir symlinks 27 of the 28 replaced copiers checked entry.file_type(), which does not follow symlinks; only yarn_berry_common followed them. Following them lets a link cycle in an installed tree recurse until the stack overflows. The shared copy now checks file_type() like the majority. yarn_berry_common copies .socket and the yarn zip cache, which hold no directory links. The module doc no longer claims a re-export. Assisted-by: Claude Code:claude-opus-5-5 --- crates/socket-patch-cli/tests/common/fs_tree.rs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/crates/socket-patch-cli/tests/common/fs_tree.rs b/crates/socket-patch-cli/tests/common/fs_tree.rs index af7e48d2b..ab0f9b385 100644 --- a/crates/socket-patch-cli/tests/common/fs_tree.rs +++ b/crates/socket-patch-cli/tests/common/fs_tree.rs @@ -1,21 +1,22 @@ -//! Recursive directory copy for test fixtures. `common/mod.rs` re-exports -//! it; shared modules whose includers don't all declare `mod common` pull -//! this file in on its own with `#[path = "../common/fs_tree.rs"] mod -//! fs_tree;`. +//! Recursive directory copy for test fixtures, reached as +//! `common::fs_tree::copy_tree`. Shared modules whose includers don't all +//! declare `mod common` pull this file in on its own with +//! `#[path = "../common/fs_tree.rs"] mod fs_tree;`. #![allow(dead_code)] use std::path::Path; -/// Copy `src` into `dst`, creating `dst`. Symlinks are followed: a link to -/// a directory is copied as a directory, a link to a file as the file. -/// Panics if `src` is not a readable directory. +/// Copy `src` into `dst`, creating `dst`. A symlink to a file is copied as +/// the file; a symlink to a directory panics rather than being followed, +/// so a link cycle in an installed tree can't recurse forever. Panics if +/// `src` is not a readable directory. pub fn copy_tree(src: &Path, dst: &Path) { std::fs::create_dir_all(dst).unwrap(); for entry in std::fs::read_dir(src).unwrap() { let entry = entry.unwrap(); let to = dst.join(entry.file_name()); - if entry.path().is_dir() { + if entry.file_type().unwrap().is_dir() { copy_tree(&entry.path(), &to); } else { std::fs::copy(entry.path(), &to).unwrap();