diff --git a/crates/socket-patch-cli/tests/cli_global_args.rs b/crates/socket-patch-cli/tests/cli_global_args.rs index c5c3cd9..6a111b7 100644 --- a/crates/socket-patch-cli/tests/cli_global_args.rs +++ b/crates/socket-patch-cli/tests/cli_global_args.rs @@ -242,6 +242,7 @@ fn global_flag_cases_cover_every_global_field() { /// shipped unguarded). Derive the long-flag set from clap itself and demand a /// case for each — this cannot drift. #[test] +#[serial_test::parallel] fn global_flag_cases_cover_every_global_long_flag() { use clap::CommandFactory; @@ -273,6 +274,7 @@ fn global_flag_cases_cover_every_global_long_flag() { /// `GlobalArgs`, or forgets to add it here), this fails loudly instead of /// silently leaving the new command untested. #[test] +#[serial_test::parallel] fn all_subcommands_are_covered() { use clap::CommandFactory; diff --git a/crates/socket-patch-core/tests/crawler_cargo_e2e.rs b/crates/socket-patch-core/tests/crawler_cargo_e2e.rs index 318c959..a7dbf84 100644 --- a/crates/socket-patch-core/tests/crawler_cargo_e2e.rs +++ b/crates/socket-patch-core/tests/crawler_cargo_e2e.rs @@ -44,6 +44,7 @@ async fn stage_vendor_crate(src: &Path, name: &str, version: &str) -> std::path: // ── parse_cargo_toml_name_version ────────────────────────────── #[test] +#[serial_test::parallel] fn parse_cargo_toml_well_formed() { let toml = "[package]\nname = \"serde\"\nversion = \"1.0.200\"\nedition = \"2021\"\n"; assert_eq!( @@ -53,18 +54,21 @@ fn parse_cargo_toml_well_formed() { } #[test] +#[serial_test::parallel] fn parse_cargo_toml_missing_name_returns_none() { let toml = "[package]\nversion = \"1.0.200\"\n"; assert_eq!(parse_cargo_toml_name_version(toml), None); } #[test] +#[serial_test::parallel] fn parse_cargo_toml_missing_version_returns_none() { let toml = "[package]\nname = \"serde\"\n"; assert_eq!(parse_cargo_toml_name_version(toml), None); } #[test] +#[serial_test::parallel] fn parse_cargo_toml_malformed_returns_none() { let toml = "this is not toml at all"; assert_eq!(parse_cargo_toml_name_version(toml), None); @@ -75,6 +79,7 @@ fn parse_cargo_toml_malformed_returns_none() { /// picked up. Covers the "left package section" early-break arm /// (cargo_crawler.rs:34-36). #[test] +#[serial_test::parallel] fn parse_cargo_toml_stops_at_next_section() { let toml = "[package]\nname = \"foo\"\nversion = \"1.0.0\"\n\n[dependencies]\nname = \"bar\"\n"; assert_eq!( @@ -86,6 +91,7 @@ fn parse_cargo_toml_stops_at_next_section() { /// Parser must ignore key=value lines that appear BEFORE [package] /// (e.g. inside an earlier [profile.release] table). #[test] +#[serial_test::parallel] fn parse_cargo_toml_ignores_lines_before_package_section() { let toml = "[profile.release]\nname = \"wrong\"\n\n[package]\nname = \"foo\"\nversion = \"1.0.0\"\n"; @@ -98,6 +104,7 @@ fn parse_cargo_toml_ignores_lines_before_package_section() { /// CargoCrawler's `Default` impl forwards to `new`. Exercise both /// for symmetry. #[test] +#[serial_test::parallel] fn cargo_crawler_default_and_new_construct_cleanly() { let _a = CargoCrawler; let _b = CargoCrawler::new(); @@ -153,6 +160,7 @@ async fn cargo_home_fallback_to_home_dot_cargo() { // ── find_by_purls ────────────────────────────────────────────── #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_registry_layout_finds_crate() { let tmp = tempfile::tempdir().unwrap(); let pkg = stage_registry_crate(tmp.path(), "serde", "1.0.200").await; @@ -172,6 +180,7 @@ async fn find_by_purls_registry_layout_finds_crate() { } #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_vendor_layout_finds_crate() { let tmp = tempfile::tempdir().unwrap(); let pkg = stage_vendor_crate(tmp.path(), "serde", "1.0.200").await; @@ -192,6 +201,7 @@ async fn find_by_purls_vendor_layout_finds_crate() { } #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_vendor_version_mismatch_returns_empty() { let tmp = tempfile::tempdir().unwrap(); stage_vendor_crate(tmp.path(), "serde", "1.0.200").await; @@ -205,6 +215,7 @@ async fn find_by_purls_vendor_version_mismatch_returns_empty() { } #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_no_match_returns_empty() { let tmp = tempfile::tempdir().unwrap(); let crawler = CargoCrawler; @@ -216,6 +227,7 @@ async fn find_by_purls_no_match_returns_empty() { } #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_invalid_purl_skipped() { let tmp = tempfile::tempdir().unwrap(); let crawler = CargoCrawler; @@ -229,6 +241,7 @@ async fn find_by_purls_invalid_purl_skipped() { // ── crawl_all ───────────────────────────────────────────────── #[tokio::test] +#[serial_test::parallel] async fn crawl_all_via_registry_layout() { let tmp = tempfile::tempdir().unwrap(); stage_registry_crate(tmp.path(), "serde", "1.0.200").await; @@ -267,6 +280,7 @@ async fn crawl_all_via_registry_layout() { } #[tokio::test] +#[serial_test::parallel] async fn crawl_all_empty_src_returns_empty() { let tmp = tempfile::tempdir().unwrap(); let crawler = CargoCrawler; @@ -282,6 +296,7 @@ async fn crawl_all_empty_src_returns_empty() { // ── get_crate_source_paths ───────────────────────────────────── #[tokio::test] +#[serial_test::parallel] async fn get_crate_source_paths_with_global_prefix_passthrough() { let tmp = tempfile::tempdir().unwrap(); let crawler = CargoCrawler; @@ -295,6 +310,7 @@ async fn get_crate_source_paths_with_global_prefix_passthrough() { } #[tokio::test] +#[serial_test::parallel] async fn get_crate_source_paths_with_vendor_dir_returns_vendor() { let tmp = tempfile::tempdir().unwrap(); let vendor = tmp.path().join("vendor"); @@ -321,6 +337,7 @@ async fn get_crate_source_paths_with_vendor_dir_returns_vendor() { /// manifest (e.g. a Composer/Go project) must NOT be claimed by the /// cargo crawler. #[tokio::test] +#[serial_test::parallel] async fn get_crate_source_paths_vendor_without_cargo_manifest_is_empty() { let tmp = tempfile::tempdir().unwrap(); tokio::fs::create_dir(tmp.path().join("vendor")) @@ -339,6 +356,7 @@ async fn get_crate_source_paths_vendor_without_cargo_manifest_is_empty() { } #[tokio::test] +#[serial_test::parallel] async fn get_crate_source_paths_no_cargo_project_returns_empty() { let tmp = tempfile::tempdir().unwrap(); // No Cargo.toml, no Cargo.lock, no vendor. @@ -357,6 +375,7 @@ async fn get_crate_source_paths_no_cargo_project_returns_empty() { /// parsing `-` from the directory name. Exercises /// `parse_dir_name_version` (cargo_crawler.rs:357-372). #[tokio::test] +#[serial_test::parallel] async fn crawl_all_falls_back_to_dir_name_when_workspace_version() { let tmp = tempfile::tempdir().unwrap(); // - directory; Cargo.toml has workspace version. @@ -382,6 +401,7 @@ async fn crawl_all_falls_back_to_dir_name_when_workspace_version() { } #[tokio::test] +#[serial_test::parallel] async fn crawl_all_skips_dir_without_cargo_toml() { let tmp = tempfile::tempdir().unwrap(); // Directory shaped like a crate but no Cargo.toml — must be skipped. @@ -402,6 +422,7 @@ async fn crawl_all_skips_dir_without_cargo_toml() { /// version, find_by_purls compares dir name. Exercises the /// fallback arm in `verify_crate_at_path` (L335-L348). #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_verify_fallback_via_dir_name() { let tmp = tempfile::tempdir().unwrap(); let pkg = tmp.path().join("workspace-crate-0.1.0"); @@ -434,6 +455,7 @@ async fn find_by_purls_verify_fallback_via_dir_name() { /// parsing — but `parse_cargo_toml_name_version` itself must return /// None up front. #[test] +#[serial_test::parallel] fn parse_cargo_toml_version_workspace_returns_none() { let toml = "[package]\nname = \"foo\"\nversion.workspace = true\n"; assert_eq!(parse_cargo_toml_name_version(toml), None); @@ -448,6 +470,7 @@ fn parse_cargo_toml_version_workspace_returns_none() { /// Exercises the `n == name && v == version` false arm /// (cargo_crawler.rs:349). #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_verify_fallback_dir_name_mismatch_returns_empty() { let tmp = tempfile::tempdir().unwrap(); let pkg = tmp.path().join("sha-1"); @@ -472,6 +495,7 @@ async fn find_by_purls_verify_fallback_dir_name_mismatch_returns_empty() { /// Hidden directory entries inside the crate source root must be /// skipped by `scan_crate_source` (line 274). #[tokio::test] +#[serial_test::parallel] async fn crawl_all_skips_hidden_dirs() { let tmp = tempfile::tempdir().unwrap(); // Stage a hidden dir that looks like a registry crate — must be skipped. @@ -505,6 +529,7 @@ async fn crawl_all_skips_hidden_dirs() { /// been recorded in `seen` (line 310-311). Drive this by staging two /// registry dirs for the same crate — the second one is deduped. #[tokio::test] +#[serial_test::parallel] async fn crawl_all_dedups_same_purl() { let tmp = tempfile::tempdir().unwrap(); // Two physical dirs with identical Cargo.toml -> same purl. @@ -572,6 +597,7 @@ async fn get_crate_source_paths_local_cargo_toml_falls_back_to_registry() { /// `scan_crate_source` must skip plain-file entries inside the source /// path — covers `!ft.is_dir()` continue arm (cargo_crawler.rs:266). #[tokio::test] +#[serial_test::parallel] async fn crawl_all_skips_top_level_files() { let tmp = tempfile::tempdir().unwrap(); stage_registry_crate(tmp.path(), "real-crate", "1.0.0").await; @@ -596,6 +622,7 @@ async fn crawl_all_skips_top_level_files() { /// followed by digit), so the chain short-circuits at line 304 and /// the package is silently skipped. #[tokio::test] +#[serial_test::parallel] async fn crawl_all_skips_crate_with_unparseable_toml_and_no_version_dir_name() { let tmp = tempfile::tempdir().unwrap(); let bad = tmp.path().join("no-version-suffix"); @@ -625,6 +652,7 @@ mod common; /// it. Skipped under root because chmod has no effect on uid 0. #[cfg(unix)] #[tokio::test] +#[serial_test::parallel] async fn crawl_all_handles_unreadable_src_path() { if common::uid_is_root() { eprintln!("SKIP: chmod 000 is a no-op under root"); @@ -658,6 +686,7 @@ async fn crawl_all_handles_unreadable_src_path() { /// dir-name-fallback `is_some_and` short-circuit on `None` /// (cargo_crawler.rs:346-349). #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_verify_fails_when_both_parsers_fail() { let tmp = tempfile::tempdir().unwrap(); let bad = tmp.path().join("foo"); diff --git a/crates/socket-patch-core/tests/crawler_composer_e2e.rs b/crates/socket-patch-core/tests/crawler_composer_e2e.rs index 2b59754..cfd6eaa 100644 --- a/crates/socket-patch-core/tests/crawler_composer_e2e.rs +++ b/crates/socket-patch-core/tests/crawler_composer_e2e.rs @@ -10,12 +10,14 @@ use socket_patch_core::crawlers::types::CrawlerOptions; use socket_patch_core::crawlers::ComposerCrawler; #[test] +#[serial_test::parallel] fn parse_composer_home_output_well_formed() { let p = parse_composer_home_output("/Users/foo/.composer\n").unwrap(); assert_eq!(p, std::path::PathBuf::from("/Users/foo/.composer")); } #[test] +#[serial_test::parallel] fn parse_composer_home_output_empty_returns_none() { assert_eq!(parse_composer_home_output(""), None); assert_eq!(parse_composer_home_output(" \n "), None); @@ -65,6 +67,7 @@ async fn stage_composer_project(root: &Path, vendor_name: &str, pkg_name: &str, // ── find_by_purls ────────────────────────────────────────────── #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_finds_package_in_vendor() { let tmp = tempfile::tempdir().unwrap(); stage_composer_project(tmp.path(), "monolog", "monolog", "3.5.0").await; @@ -89,6 +92,7 @@ async fn find_by_purls_finds_package_in_vendor() { } #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_no_installed_json_returns_empty() { let tmp = tempfile::tempdir().unwrap(); let vendor = tmp.path().join("vendor"); @@ -133,6 +137,7 @@ async fn find_by_purls_no_installed_json_returns_empty() { } #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_invalid_purl_skipped() { let tmp = tempfile::tempdir().unwrap(); stage_composer_project(tmp.path(), "monolog", "monolog", "3.5.0").await; @@ -149,6 +154,7 @@ async fn find_by_purls_invalid_purl_skipped() { } #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_version_mismatch_returns_empty() { let tmp = tempfile::tempdir().unwrap(); stage_composer_project(tmp.path(), "monolog", "monolog", "3.5.0").await; @@ -167,6 +173,7 @@ async fn find_by_purls_version_mismatch_returns_empty() { // ── crawl_all ───────────────────────────────────────────────── #[tokio::test] +#[serial_test::parallel] async fn crawl_all_via_installed_json_returns_packages() { let tmp = tempfile::tempdir().unwrap(); stage_composer_project(tmp.path(), "monolog", "monolog", "3.5.0").await; @@ -190,6 +197,7 @@ async fn crawl_all_via_installed_json_returns_packages() { } #[tokio::test] +#[serial_test::parallel] async fn crawl_all_with_corrupt_installed_json_returns_empty() { let tmp = tempfile::tempdir().unwrap(); let vendor = tmp.path().join("vendor"); @@ -238,6 +246,7 @@ async fn crawl_all_with_corrupt_installed_json_returns_empty() { // ── get_vendor_paths ────────────────────────────────────────── #[tokio::test] +#[serial_test::parallel] async fn get_vendor_paths_with_global_prefix_passthrough() { let tmp = tempfile::tempdir().unwrap(); let crawler = ComposerCrawler; @@ -251,6 +260,7 @@ async fn get_vendor_paths_with_global_prefix_passthrough() { } #[tokio::test] +#[serial_test::parallel] async fn get_vendor_paths_local_no_vendor_returns_empty() { let tmp = tempfile::tempdir().unwrap(); let crawler = ComposerCrawler; @@ -262,6 +272,7 @@ async fn get_vendor_paths_local_no_vendor_returns_empty() { } #[tokio::test] +#[serial_test::parallel] async fn get_vendor_paths_local_no_installed_json_returns_empty() { let tmp = tempfile::tempdir().unwrap(); let vendor = tmp.path().join("vendor"); @@ -283,6 +294,7 @@ async fn get_vendor_paths_local_no_installed_json_returns_empty() { } #[tokio::test] +#[serial_test::parallel] async fn get_vendor_paths_local_no_composer_marker_returns_empty() { let tmp = tempfile::tempdir().unwrap(); let vendor = tmp.path().join("vendor"); @@ -305,6 +317,7 @@ async fn get_vendor_paths_local_no_composer_marker_returns_empty() { } #[tokio::test] +#[serial_test::parallel] async fn get_vendor_paths_local_full_setup_returns_vendor() { let tmp = tempfile::tempdir().unwrap(); let vendor = tmp.path().join("vendor"); @@ -581,6 +594,7 @@ mod common; /// rather than panicking. #[cfg(unix)] #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_handles_unreadable_installed_json() { if common::uid_is_root() { eprintln!("SKIP: chmod 000 is a no-op under root"); @@ -635,6 +649,7 @@ async fn find_by_purls_handles_unreadable_installed_json() { /// vendor paths sharing the same installed package — exercises the /// `seen.contains` early-continue arm. #[tokio::test] +#[serial_test::parallel] async fn crawl_all_dedups_across_vendor_paths() { let tmp = tempfile::tempdir().unwrap(); let custom_vendor = tmp.path().join("custom-vendor"); @@ -668,6 +683,7 @@ async fn crawl_all_dedups_across_vendor_paths() { } #[tokio::test] +#[serial_test::parallel] async fn get_vendor_paths_local_with_lock_marker_also_works() { let tmp = tempfile::tempdir().unwrap(); let vendor = tmp.path().join("vendor"); diff --git a/crates/socket-patch-core/tests/crawler_deno_e2e.rs b/crates/socket-patch-core/tests/crawler_deno_e2e.rs index de46fd8..6c58255 100644 --- a/crates/socket-patch-core/tests/crawler_deno_e2e.rs +++ b/crates/socket-patch-core/tests/crawler_deno_e2e.rs @@ -54,6 +54,7 @@ async fn stage_jsr_pkg(root: &Path, scope: &str, name: &str, version: &str) -> s // ── find_by_purls ────────────────────────────────────────────── #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_finds_jsr_package() { let tmp = tempfile::tempdir().unwrap(); let pkg = stage_jsr_pkg(tmp.path(), "@std", "path", "0.220.0").await; @@ -77,6 +78,7 @@ async fn find_by_purls_finds_jsr_package() { } #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_no_match_returns_empty() { let tmp = tempfile::tempdir().unwrap(); // Cache is NOT empty: a *different* package is present. This proves @@ -97,6 +99,7 @@ async fn find_by_purls_no_match_returns_empty() { } #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_non_jsr_purl_skipped() { let tmp = tempfile::tempdir().unwrap(); // Stage a tree that an *ecosystem-blind* parser (one that ignored @@ -121,6 +124,7 @@ async fn find_by_purls_non_jsr_purl_skipped() { /// regression that drops/ignores the scope segment when joining the /// path (which would let `@other/path` satisfy a `@std/path` query). #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_wrong_scope_not_resolved() { let tmp = tempfile::tempdir().unwrap(); // Same name + version, but under `@other`, not the queried `@std`. @@ -140,6 +144,7 @@ async fn find_by_purls_wrong_scope_not_resolved() { // ── crawl_all ───────────────────────────────────────────────── #[tokio::test] +#[serial_test::parallel] async fn crawl_all_enumerates_jsr_packages() { let tmp = tempfile::tempdir().unwrap(); let std_path = stage_jsr_pkg(tmp.path(), "@std", "path", "0.220.0").await; @@ -205,6 +210,7 @@ async fn crawl_all_global_via_deno_dir_env_scans_cache() { /// regression that adds a fourth descent level and emits phantom /// packages like `pkg:jsr/@std/path@src`. #[tokio::test] +#[serial_test::parallel] async fn crawl_all_does_not_recurse_below_version_layer() { let tmp = tempfile::tempdir().unwrap(); let pkg = stage_jsr_pkg(tmp.path(), "@std", "path", "0.220.0").await; @@ -230,6 +236,7 @@ async fn crawl_all_does_not_recurse_below_version_layer() { } #[tokio::test] +#[serial_test::parallel] async fn crawl_all_skips_dirs_not_starting_with_at() { let tmp = tempfile::tempdir().unwrap(); // Legitimate scope. @@ -267,6 +274,7 @@ async fn crawl_all_skips_dirs_not_starting_with_at() { // ── get_jsr_cache_paths ──────────────────────────────────────── #[tokio::test] +#[serial_test::parallel] async fn get_jsr_cache_paths_global_prefix_passthrough() { let tmp = tempfile::tempdir().unwrap(); let crawler = DenoCrawler; diff --git a/crates/socket-patch-core/tests/crawler_go_e2e.rs b/crates/socket-patch-core/tests/crawler_go_e2e.rs index 6d6b46c..cca1a08 100644 --- a/crates/socket-patch-core/tests/crawler_go_e2e.rs +++ b/crates/socket-patch-core/tests/crawler_go_e2e.rs @@ -29,6 +29,7 @@ async fn stage_go_module(cache: &Path, module_path: &str, version: &str) -> std: // ── encode_module_path / decode_module_path ───────────────────── #[test] +#[serial_test::parallel] fn encode_module_path_lowercases_uppercase() { // Per Go module proxy spec, uppercase letters get encoded as // `!` so the filesystem lookup is unambiguous on @@ -38,12 +39,14 @@ fn encode_module_path_lowercases_uppercase() { } #[test] +#[serial_test::parallel] fn encode_module_path_no_uppercase_passthrough() { let encoded = encode_module_path("github.com/gin-gonic/gin"); assert_eq!(encoded, "github.com/gin-gonic/gin"); } #[test] +#[serial_test::parallel] fn decode_module_path_inverts_encode() { let encoded = encode_module_path("github.com/Sirupsen/logrus"); // Pin the intermediate encoding too, so a buggy encode that happens to @@ -54,6 +57,7 @@ fn decode_module_path_inverts_encode() { } #[test] +#[serial_test::parallel] fn decode_module_path_no_bang_passthrough() { assert_eq!( decode_module_path("github.com/gin-gonic/gin"), @@ -64,6 +68,7 @@ fn decode_module_path_no_bang_passthrough() { // ── parse_go_mod_module ──────────────────────────────────────── #[test] +#[serial_test::parallel] fn parse_go_mod_well_formed() { let content = "module github.com/gin-gonic/gin\n\ngo 1.21\n"; assert_eq!( @@ -73,12 +78,14 @@ fn parse_go_mod_well_formed() { } #[test] +#[serial_test::parallel] fn parse_go_mod_missing_module_returns_none() { let content = "go 1.21\n"; assert_eq!(parse_go_mod_module(content), None); } #[test] +#[serial_test::parallel] fn parse_go_mod_empty_returns_none() { assert_eq!(parse_go_mod_module(""), None); } @@ -86,6 +93,7 @@ fn parse_go_mod_empty_returns_none() { // ── find_by_purls ────────────────────────────────────────────── #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_finds_module_in_cache() { let tmp = tempfile::tempdir().unwrap(); let pkg = stage_go_module(tmp.path(), "github.com/gin-gonic/gin", "v1.9.1").await; @@ -108,6 +116,7 @@ async fn find_by_purls_finds_module_in_cache() { } #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_no_match_returns_empty() { let tmp = tempfile::tempdir().unwrap(); let crawler = GoCrawler; @@ -119,6 +128,7 @@ async fn find_by_purls_no_match_returns_empty() { } #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_invalid_purl_skipped() { let tmp = tempfile::tempdir().unwrap(); let crawler = GoCrawler; @@ -132,6 +142,7 @@ async fn find_by_purls_invalid_purl_skipped() { // ── get_module_cache_paths ───────────────────────────────────── #[tokio::test] +#[serial_test::parallel] async fn get_module_cache_paths_with_global_prefix_passthrough() { let tmp = tempfile::tempdir().unwrap(); let crawler = GoCrawler; @@ -198,6 +209,7 @@ mod common; /// `scan_dir_recursive` short-circuits when read_dir returns Err. #[cfg(unix)] #[tokio::test] +#[serial_test::parallel] async fn crawl_all_handles_unreadable_cache_path() { if common::uid_is_root() { eprintln!("SKIP: chmod 000 is a no-op under root"); @@ -227,6 +239,7 @@ async fn crawl_all_handles_unreadable_cache_path() { // `default_constructed_unit_structs` lint is deliberately allowed here. #[allow(clippy::default_constructed_unit_structs)] #[tokio::test] +#[serial_test::parallel] async fn go_crawler_default_and_new_construct_cleanly() { let tmp = tempfile::tempdir().unwrap(); let pkg = stage_go_module(tmp.path(), "github.com/gin-gonic/gin", "v1.9.1").await; @@ -256,12 +269,14 @@ async fn go_crawler_default_and_new_construct_cleanly() { /// A `module` directive with no path (`module`) must not match — the /// guard at line 61 (`!rest.is_empty()`) keeps it from being returned. #[test] +#[serial_test::parallel] fn parse_go_mod_module_directive_with_empty_path_returns_none() { assert_eq!(parse_go_mod_module("module\n"), None); } /// Quoted module path with whitespace — the strip-quotes branch. #[test] +#[serial_test::parallel] fn parse_go_mod_module_quoted_path() { assert_eq!( parse_go_mod_module(r#"module "github.com/foo/bar""#), @@ -274,6 +289,7 @@ fn parse_go_mod_module_quoted_path() { /// `decode_module_path` preserves it rather than silently dropping a byte, /// so decoding an unexpected/corrupt directory name never loses path data. #[test] +#[serial_test::parallel] fn decode_module_path_trailing_bang_is_preserved() { assert_eq!(decode_module_path("github.com/foo!"), "github.com/foo!"); } @@ -284,6 +300,7 @@ fn decode_module_path_trailing_bang_is_preserved() { /// IS present and IS matched) proves the empty result is selective, not a /// blanket "find nothing" regression. #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_module_dir_missing_returns_empty() { let tmp = tempfile::tempdir().unwrap(); // Stage v1.9.1 but NOT the requested v9.9.9. diff --git a/crates/socket-patch-core/tests/crawler_maven_e2e.rs b/crates/socket-patch-core/tests/crawler_maven_e2e.rs index f558687..5649b70 100644 --- a/crates/socket-patch-core/tests/crawler_maven_e2e.rs +++ b/crates/socket-patch-core/tests/crawler_maven_e2e.rs @@ -47,6 +47,7 @@ async fn stage_maven_pkg( // ── parse_pom_group_artifact_version ─────────────────────────── #[test] +#[serial_test::parallel] fn parse_pom_well_formed_extracts_coordinates() { let pom = r#" @@ -66,6 +67,7 @@ fn parse_pom_well_formed_extracts_coordinates() { } #[test] +#[serial_test::parallel] fn parse_pom_missing_group_id_returns_none() { let pom = r#" @@ -76,6 +78,7 @@ fn parse_pom_missing_group_id_returns_none() { } #[test] +#[serial_test::parallel] fn parse_pom_missing_version_returns_none() { let pom = r#" @@ -86,12 +89,14 @@ fn parse_pom_missing_version_returns_none() { } #[test] +#[serial_test::parallel] fn parse_pom_malformed_xml_returns_none() { let pom = "this is not XML at all"; assert_eq!(parse_pom_group_artifact_version(pom), None); } #[test] +#[serial_test::parallel] fn parse_pom_empty_string_returns_none() { assert_eq!(parse_pom_group_artifact_version(""), None); } @@ -100,6 +105,7 @@ fn parse_pom_empty_string_returns_none() { /// exercise the `in_parent` arm that records `parent_group_id` and the /// final `group_id.or(parent_group_id)` fallback (maven_crawler.rs:124). #[test] +#[serial_test::parallel] fn parse_pom_parent_groupid_fallback() { let pom = r#" @@ -126,6 +132,7 @@ fn parse_pom_parent_groupid_fallback() { /// reference — the parser must bail out instead of treating the /// literal placeholder as a value (line 100). #[test] +#[serial_test::parallel] fn parse_pom_property_reference_groupid_returns_none() { let pom = r#" @@ -137,6 +144,7 @@ fn parse_pom_property_reference_groupid_returns_none() { } #[test] +#[serial_test::parallel] fn parse_pom_property_reference_artifactid_returns_none() { let pom = r#" @@ -148,6 +156,7 @@ fn parse_pom_property_reference_artifactid_returns_none() { } #[test] +#[serial_test::parallel] fn parse_pom_property_reference_version_returns_none() { let pom = r#" @@ -162,6 +171,7 @@ fn parse_pom_property_reference_version_returns_none() { /// reference — must NOT be accepted as a fallback groupId (line 86-87 /// skip arm). #[test] +#[serial_test::parallel] fn parse_pom_missing_artifact_id_returns_none() { let pom = r#" @@ -176,6 +186,7 @@ fn parse_pom_missing_artifact_id_returns_none() { /// can't extract a value, and the function returns None. Drives /// `extract_xml_value` line 16 (close-tag not found on same line). #[test] +#[serial_test::parallel] fn parse_pom_split_tag_returns_none() { let pom = r#" @@ -191,6 +202,7 @@ fn parse_pom_split_tag_returns_none() { /// `MavenCrawler::default()` should forward to `new()`. #[test] +#[serial_test::parallel] fn maven_crawler_default_and_new_construct_cleanly() { let _a = MavenCrawler; let _b = MavenCrawler::new(); @@ -317,6 +329,7 @@ async fn get_maven_repo_paths_global_mode_no_m2_returns_empty() { /// `find_by_purls` for a version directory that contains a non-`.pom` /// file but no `.pom` — exercise the `has_pom_file` return-false arm. #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_version_dir_without_pom_returns_empty() { let tmp = tempfile::tempdir().unwrap(); let group_path = "org/apache/commons"; @@ -343,6 +356,7 @@ async fn find_by_purls_version_dir_without_pom_returns_empty() { } #[test] +#[serial_test::parallel] fn parse_pom_parent_property_reference_groupid_skipped() { let pom = r#" @@ -361,6 +375,7 @@ fn parse_pom_parent_property_reference_groupid_skipped() { // ── find_by_purls ────────────────────────────────────────────── #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_finds_package_in_m2_layout() { let tmp = tempfile::tempdir().unwrap(); let pkg_dir = @@ -388,6 +403,7 @@ async fn find_by_purls_finds_package_in_m2_layout() { } #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_no_match_returns_empty() { let tmp = tempfile::tempdir().unwrap(); let crawler = MavenCrawler; @@ -402,6 +418,7 @@ async fn find_by_purls_no_match_returns_empty() { } #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_invalid_purl_skipped() { let tmp = tempfile::tempdir().unwrap(); let crawler = MavenCrawler; @@ -415,6 +432,7 @@ async fn find_by_purls_invalid_purl_skipped() { // ── crawl_all ───────────────────────────────────────────────── #[tokio::test] +#[serial_test::parallel] async fn crawl_all_discovers_packages_in_repo() { let tmp = tempfile::tempdir().unwrap(); stage_maven_pkg(tmp.path(), "org.apache.commons", "commons-lang3", "3.12.0").await; @@ -454,6 +472,7 @@ async fn crawl_all_discovers_packages_in_repo() { } #[tokio::test] +#[serial_test::parallel] async fn crawl_all_with_empty_repo_returns_empty() { let tmp = tempfile::tempdir().unwrap(); let crawler = MavenCrawler; @@ -469,6 +488,7 @@ async fn crawl_all_with_empty_repo_returns_empty() { // ── get_maven_repo_paths ─────────────────────────────────────── #[tokio::test] +#[serial_test::parallel] async fn get_maven_repo_paths_with_global_prefix_returns_only_prefix() { let tmp = tempfile::tempdir().unwrap(); let crawler = MavenCrawler; diff --git a/crates/socket-patch-core/tests/crawler_npm_e2e.rs b/crates/socket-patch-core/tests/crawler_npm_e2e.rs index dea364d..7e8c25a 100644 --- a/crates/socket-patch-core/tests/crawler_npm_e2e.rs +++ b/crates/socket-patch-core/tests/crawler_npm_e2e.rs @@ -37,6 +37,7 @@ async fn stage_npm_pkg(node_modules: &Path, name: &str, version: &str) { // ── parse_package_name ───────────────────────────────────────── #[test] +#[serial_test::parallel] fn parse_package_name_unscoped() { let (ns, name) = parse_package_name("lodash"); assert_eq!(ns, None); @@ -44,6 +45,7 @@ fn parse_package_name_unscoped() { } #[test] +#[serial_test::parallel] fn parse_package_name_scoped() { let (ns, name) = parse_package_name("@types/node"); assert_eq!(ns.as_deref(), Some("@types")); @@ -51,6 +53,7 @@ fn parse_package_name_scoped() { } #[test] +#[serial_test::parallel] fn parse_package_name_at_only_no_slash() { // `@foo` with no `/` — treated as unscoped. let (ns, name) = parse_package_name("@oops"); @@ -61,12 +64,14 @@ fn parse_package_name_at_only_no_slash() { // ── build_npm_purl ───────────────────────────────────────────── #[test] +#[serial_test::parallel] fn build_npm_purl_unscoped() { let purl = build_npm_purl(None, "lodash", "4.17.21"); assert_eq!(purl, "pkg:npm/lodash@4.17.21"); } #[test] +#[serial_test::parallel] fn build_npm_purl_scoped() { let purl = build_npm_purl(Some("@types"), "node", "20.0.0"); assert_eq!(purl, "pkg:npm/@types/node@20.0.0"); @@ -75,6 +80,7 @@ fn build_npm_purl_scoped() { // ── read_package_json ────────────────────────────────────────── #[tokio::test] +#[serial_test::parallel] async fn read_package_json_well_formed() { let tmp = tempfile::tempdir().unwrap(); let pkg = tmp.path().join("package.json"); @@ -87,6 +93,7 @@ async fn read_package_json_well_formed() { } #[tokio::test] +#[serial_test::parallel] async fn read_package_json_missing_returns_none() { let tmp = tempfile::tempdir().unwrap(); let result = read_package_json(&tmp.path().join("nope.json")).await; @@ -94,6 +101,7 @@ async fn read_package_json_missing_returns_none() { } #[tokio::test] +#[serial_test::parallel] async fn read_package_json_malformed_returns_none() { let tmp = tempfile::tempdir().unwrap(); let pkg = tmp.path().join("package.json"); @@ -104,6 +112,7 @@ async fn read_package_json_malformed_returns_none() { } #[tokio::test] +#[serial_test::parallel] async fn read_package_json_missing_name_returns_none() { let tmp = tempfile::tempdir().unwrap(); let pkg = tmp.path().join("package.json"); @@ -116,6 +125,7 @@ async fn read_package_json_missing_name_returns_none() { } #[tokio::test] +#[serial_test::parallel] async fn read_package_json_missing_version_returns_none() { let tmp = tempfile::tempdir().unwrap(); let pkg = tmp.path().join("package.json"); @@ -130,6 +140,7 @@ async fn read_package_json_missing_version_returns_none() { /// Both fields present but empty strings — parse succeeds but the /// downstream is_empty guard must reject. #[tokio::test] +#[serial_test::parallel] async fn read_package_json_empty_name_returns_none() { let tmp = tempfile::tempdir().unwrap(); let pkg = tmp.path().join("package.json"); @@ -140,6 +151,7 @@ async fn read_package_json_empty_name_returns_none() { } #[tokio::test] +#[serial_test::parallel] async fn read_package_json_empty_version_returns_none() { let tmp = tempfile::tempdir().unwrap(); let pkg = tmp.path().join("package.json"); @@ -152,6 +164,7 @@ async fn read_package_json_empty_version_returns_none() { // ── NpmCrawler construction ──────────────────────────────────── #[test] +#[serial_test::parallel] fn npm_crawler_new_and_default_construct_cleanly() { let _a = NpmCrawler::new(); let _b = NpmCrawler; @@ -162,6 +175,7 @@ fn npm_crawler_new_and_default_construct_cleanly() { /// `global_prefix` always takes precedence over discovery, even when /// `global` flag is also set. #[tokio::test] +#[serial_test::parallel] async fn get_node_modules_paths_global_prefix_passthrough() { let tmp = tempfile::tempdir().unwrap(); let custom = tmp.path().join("custom-nm"); @@ -182,6 +196,7 @@ async fn get_node_modules_paths_global_prefix_passthrough() { /// test env may have npm/yarn/pnpm/bun installed, we just assert the /// call returns Ok (it can return any set of real or empty paths). #[tokio::test] +#[serial_test::parallel] async fn get_node_modules_paths_global_mode_no_prefix() { let tmp = tempfile::tempdir().unwrap(); let crawler = NpmCrawler; @@ -208,6 +223,7 @@ async fn get_node_modules_paths_global_mode_no_prefix() { /// same parser. #[cfg(unix)] #[test] +#[serial_test::parallel] fn parse_bun_bin_output_well_formed_unix() { let parsed = parse_bun_bin_output("/home/foo/.bun/bin\n"); assert_eq!( @@ -217,6 +233,7 @@ fn parse_bun_bin_output_well_formed_unix() { } #[test] +#[serial_test::parallel] fn parse_bun_bin_output_empty_returns_none() { assert_eq!(parse_bun_bin_output(""), None); assert_eq!(parse_bun_bin_output(" \n "), None); @@ -224,6 +241,7 @@ fn parse_bun_bin_output_empty_returns_none() { /// Root-only path has no parent — must yield None instead of panicking. #[test] +#[serial_test::parallel] fn parse_bun_bin_output_root_path_returns_none() { assert_eq!(parse_bun_bin_output("/"), None); } @@ -290,6 +308,7 @@ fn get_bun_global_prefix_returns_none_when_bun_not_on_path() { /// path. This covers the "binary present, returned valid output" /// arm without needing npm on PATH. #[test] +#[serial_test::parallel] fn get_npm_global_prefix_with_mock_runner_returns_path() { let runner = common::MockCommandRunner::new().with_response( "npm", @@ -301,6 +320,7 @@ fn get_npm_global_prefix_with_mock_runner_returns_path() { } #[test] +#[serial_test::parallel] fn get_npm_global_prefix_with_mock_runner_empty_stdout_returns_err() { let runner = common::MockCommandRunner::new().with_response("npm", &["root", "-g"], Some("")); assert!(get_npm_global_prefix_with(&runner).is_err()); @@ -310,6 +330,7 @@ fn get_npm_global_prefix_with_mock_runner_empty_stdout_returns_err() { // `parse_bun_bin_output_well_formed_unix` above. #[cfg(unix)] #[test] +#[serial_test::parallel] fn get_yarn_global_prefix_with_mock_runner_success() { let runner = common::MockCommandRunner::new().with_response( "yarn", @@ -323,6 +344,7 @@ fn get_yarn_global_prefix_with_mock_runner_success() { } #[test] +#[serial_test::parallel] fn get_pnpm_global_prefix_with_mock_runner_success() { let runner = common::MockCommandRunner::new().with_response( "pnpm", @@ -339,6 +361,7 @@ fn get_pnpm_global_prefix_with_mock_runner_success() { // `parse_bun_bin_output_well_formed_unix` above. #[cfg(unix)] #[test] +#[serial_test::parallel] fn get_bun_global_prefix_with_mock_runner_success() { let runner = common::MockCommandRunner::new().with_response( "bun", @@ -354,6 +377,7 @@ fn get_bun_global_prefix_with_mock_runner_success() { // ── parse_npm_root_output ────────────────────────────────────── #[test] +#[serial_test::parallel] fn parse_npm_root_output_well_formed() { assert_eq!( parse_npm_root_output("/usr/local/lib/node_modules\n").as_deref(), @@ -362,6 +386,7 @@ fn parse_npm_root_output_well_formed() { } #[test] +#[serial_test::parallel] fn parse_npm_root_output_empty_returns_none() { assert_eq!(parse_npm_root_output(""), None); assert_eq!(parse_npm_root_output(" \n "), None); @@ -375,6 +400,7 @@ fn parse_npm_root_output_empty_returns_none() { /// `_unix`-style tests above. #[cfg(unix)] #[test] +#[serial_test::parallel] fn parse_yarn_dir_output_appends_node_modules() { let parsed = parse_yarn_dir_output("/Users/foo/.yarn/global\n"); assert_eq!( @@ -384,6 +410,7 @@ fn parse_yarn_dir_output_appends_node_modules() { } #[test] +#[serial_test::parallel] fn parse_yarn_dir_output_empty_returns_none() { assert_eq!(parse_yarn_dir_output(""), None); assert_eq!(parse_yarn_dir_output("\n \n"), None); @@ -392,6 +419,7 @@ fn parse_yarn_dir_output_empty_returns_none() { // ── parse_pnpm_root_output ───────────────────────────────────── #[test] +#[serial_test::parallel] fn parse_pnpm_root_output_returns_trimmed_path() { let parsed = parse_pnpm_root_output("/home/foo/.local/share/pnpm/global/5/node_modules\n"); assert_eq!( @@ -401,6 +429,7 @@ fn parse_pnpm_root_output_returns_trimmed_path() { } #[test] +#[serial_test::parallel] fn parse_pnpm_root_output_empty_returns_none() { assert_eq!(parse_pnpm_root_output(""), None); assert_eq!(parse_pnpm_root_output(" \n "), None); @@ -409,6 +438,7 @@ fn parse_pnpm_root_output_empty_returns_none() { // ── find_by_purls ────────────────────────────────────────────── #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_unscoped_package() { let tmp = tempfile::tempdir().unwrap(); let nm = tmp.path().join("node_modules"); @@ -437,6 +467,7 @@ async fn find_by_purls_unscoped_package() { } #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_scoped_package() { let tmp = tempfile::tempdir().unwrap(); let nm = tmp.path().join("node_modules"); @@ -463,6 +494,7 @@ async fn find_by_purls_scoped_package() { } #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_version_mismatch_returns_empty() { let tmp = tempfile::tempdir().unwrap(); let nm = tmp.path().join("node_modules"); @@ -483,6 +515,7 @@ async fn find_by_purls_version_mismatch_returns_empty() { /// in, so keying by a stripped/reconstructed PURL would silently drop every /// qualified PURL. #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_resolves_qualified_purl_keyed_by_input() { let tmp = tempfile::tempdir().unwrap(); let nm = tmp.path().join("node_modules"); @@ -517,6 +550,7 @@ async fn find_by_purls_resolves_qualified_purl_keyed_by_input() { /// qualifiers WITHOUT an `@`, so they cannot catch a strip-order /// regression — this pins it. #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_qualifier_containing_at_does_not_corrupt_version() { let tmp = tempfile::tempdir().unwrap(); let nm = tmp.path().join("node_modules"); @@ -551,6 +585,7 @@ async fn find_by_purls_qualifier_containing_at_does_not_corrupt_version() { /// PURL with no `@` (no version separator) must be rejected via the /// `rfind('@')?` arm (line 707). #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_purl_without_at_skipped() { let tmp = tempfile::tempdir().unwrap(); let nm = tmp.path().join("node_modules"); @@ -565,6 +600,7 @@ async fn find_by_purls_purl_without_at_skipped() { /// PURL with `@` but an empty version (`pkg:npm/lodash@`) — covers the /// `version.is_empty()` arm at line 711-712. #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_purl_with_empty_version_skipped() { let tmp = tempfile::tempdir().unwrap(); let nm = tmp.path().join("node_modules"); @@ -579,6 +615,7 @@ async fn find_by_purls_purl_with_empty_version_skipped() { /// PURL with scope marker but no slash (`pkg:npm/@foo@1.0`) — covers /// the `find('/')?` arm at line 716. #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_scoped_purl_without_slash_skipped() { let tmp = tempfile::tempdir().unwrap(); let nm = tmp.path().join("node_modules"); @@ -593,6 +630,7 @@ async fn find_by_purls_scoped_purl_without_slash_skipped() { /// Scoped PURL with empty name after slash (`pkg:npm/@scope/@1.0`) — /// covers the `if name.is_empty()` arm at line 719-720. #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_scoped_purl_with_empty_name_skipped() { let tmp = tempfile::tempdir().unwrap(); let nm = tmp.path().join("node_modules"); @@ -605,6 +643,7 @@ async fn find_by_purls_scoped_purl_with_empty_name_skipped() { } #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_invalid_purl_skipped() { let tmp = tempfile::tempdir().unwrap(); let crawler = NpmCrawler; @@ -618,6 +657,7 @@ async fn find_by_purls_invalid_purl_skipped() { // ── crawl_all ───────────────────────────────────────────────── #[tokio::test] +#[serial_test::parallel] async fn crawl_all_discovers_unscoped_and_scoped() { let tmp = tempfile::tempdir().unwrap(); let nm = tmp.path().join("node_modules"); @@ -654,6 +694,7 @@ async fn crawl_all_discovers_unscoped_and_scoped() { } #[tokio::test] +#[serial_test::parallel] async fn crawl_all_skips_dirs_without_package_json() { let tmp = tempfile::tempdir().unwrap(); let nm = tmp.path().join("node_modules"); @@ -672,6 +713,7 @@ async fn crawl_all_skips_dirs_without_package_json() { /// looking for nested `node_modules`, while skipping hidden dirs and /// well-known build-output dirs. #[tokio::test] +#[serial_test::parallel] async fn crawl_all_recurses_into_workspace_packages() { let tmp = tempfile::tempdir().unwrap(); // Root has no node_modules but a workspace subdir does. @@ -700,6 +742,7 @@ async fn crawl_all_recurses_into_workspace_packages() { } #[tokio::test] +#[serial_test::parallel] async fn crawl_all_skips_hidden_and_skip_dirs() { let tmp = tempfile::tempdir().unwrap(); // Hidden dirs and SKIP_DIRS entries (dist/build/coverage/tmp/...) are skipped. @@ -751,6 +794,7 @@ mod common; /// `scan_node_modules` short-circuits when read_dir returns Err. #[cfg(unix)] #[tokio::test] +#[serial_test::parallel] async fn crawl_all_handles_unreadable_node_modules() { if common::uid_is_root() { eprintln!("SKIP: chmod 000 is a no-op under root"); @@ -778,6 +822,7 @@ async fn crawl_all_handles_unreadable_node_modules() { /// while leaving a readable one alongside. #[cfg(unix)] #[tokio::test] +#[serial_test::parallel] async fn crawl_all_handles_unreadable_workspace_dir() { if common::uid_is_root() { eprintln!("SKIP: chmod 000 is a no-op under root"); @@ -814,6 +859,7 @@ async fn crawl_all_handles_unreadable_workspace_dir() { /// the hidden-and-file-entries skip arms inside `scan_scoped_packages` /// and `scan_nested_node_modules`. Covers L552, 581-604, 619-665. #[tokio::test] +#[serial_test::parallel] async fn crawl_all_handles_nested_and_messy_scope_dir() { let tmp = tempfile::tempdir().unwrap(); let nm = tmp.path().join("node_modules"); @@ -897,6 +943,7 @@ async fn crawl_all_handles_nested_and_messy_scope_dir() { } #[tokio::test] +#[serial_test::parallel] async fn crawl_all_discovers_deeply_nested_transitive_deps() { // The npm crawler recurses `node_modules` at UNBOUNDED depth, so a patch // targeting a deeply-nested *transitive* dependency is discovered — and thus @@ -943,6 +990,7 @@ async fn crawl_all_discovers_deeply_nested_transitive_deps() { } #[tokio::test] +#[serial_test::parallel] async fn crawl_all_skips_dirs_with_corrupt_package_json() { let tmp = tempfile::tempdir().unwrap(); let nm = tmp.path().join("node_modules"); @@ -969,6 +1017,7 @@ async fn crawl_all_skips_dirs_with_corrupt_package_json() { /// that behavior. #[cfg(unix)] #[tokio::test] +#[serial_test::parallel] async fn crawl_all_does_not_recurse_through_symlinked_nested_package() { use std::os::unix::fs::symlink; @@ -1015,6 +1064,7 @@ async fn crawl_all_does_not_recurse_through_symlinked_nested_package() { /// invisible to `scan` and unpatchable by `apply`. Same class as the /// `strip_bom` fixes in `package_json/detect.rs`. #[tokio::test] +#[serial_test::parallel] async fn read_package_json_tolerates_utf8_bom() { let tmp = tempfile::tempdir().unwrap(); let nm = tmp.path().join("node_modules"); @@ -1062,6 +1112,7 @@ async fn read_package_json_tolerates_utf8_bom() { /// mismatch policy applying the full patched blob of `foo` over `bar`). /// The probe must require the on-disk name to match the PURL identity. #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_rejects_alias_dir_with_matching_version() { let tmp = tempfile::tempdir().unwrap(); let nm = tmp.path().join("node_modules"); @@ -1114,6 +1165,7 @@ async fn find_by_purls_rejects_alias_dir_with_matching_version() { /// classic hoisting-conflict layout) was scannable yet unpatchable: apply /// reported "No packages found that match available patches". #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_resolves_nested_only_install() { let tmp = tempfile::tempdir().unwrap(); let nm = tmp.path().join("node_modules"); @@ -1163,6 +1215,7 @@ async fn find_by_purls_resolves_nested_only_install() { /// readers. #[cfg(unix)] #[tokio::test] +#[serial_test::parallel] async fn read_package_json_rejects_fifo_without_hanging() { let tmp = tempfile::tempdir().unwrap(); let nm = tmp.path().join("node_modules"); @@ -1232,6 +1285,7 @@ async fn read_package_json_rejects_fifo_without_hanging() { /// copy must win (shallowest-first), preserving the pre-existing behavior /// for everything resolvable at the root. #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_prefers_root_copy_over_nested_duplicate() { let tmp = tempfile::tempdir().unwrap(); let nm = tmp.path().join("node_modules"); diff --git a/crates/socket-patch-core/tests/crawler_nuget_e2e.rs b/crates/socket-patch-core/tests/crawler_nuget_e2e.rs index f7cd037..a749fe6 100644 --- a/crates/socket-patch-core/tests/crawler_nuget_e2e.rs +++ b/crates/socket-patch-core/tests/crawler_nuget_e2e.rs @@ -60,6 +60,7 @@ async fn stage_legacy_pkg(root: &Path, name: &str, version: &str) -> std::path:: // ── find_by_purls ────────────────────────────────────────────── #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_global_cache_layout_finds_package() { let tmp = tempfile::tempdir().unwrap(); let pkg_dir = stage_global_cache_pkg(tmp.path(), "Newtonsoft.Json", "13.0.3").await; @@ -77,6 +78,7 @@ async fn find_by_purls_global_cache_layout_finds_package() { } #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_legacy_layout_finds_package() { let tmp = tempfile::tempdir().unwrap(); let pkg_dir = stage_legacy_pkg(tmp.path(), "Newtonsoft.Json", "13.0.3").await; @@ -102,6 +104,7 @@ async fn find_by_purls_legacy_layout_finds_package() { /// folds names. On case-sensitive filesystems (Linux ext4), the /// case-insensitive scan branch fires. #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_case_insensitive_legacy_layout() { let tmp = tempfile::tempdir().unwrap(); let staged = stage_legacy_pkg(tmp.path(), "newtonsoft.json", "13.0.3").await; @@ -136,6 +139,7 @@ async fn find_by_purls_case_insensitive_legacy_layout() { } #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_no_match_returns_empty() { let tmp = tempfile::tempdir().unwrap(); // Empty dir — no packages. @@ -148,6 +152,7 @@ async fn find_by_purls_no_match_returns_empty() { } #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_invalid_purl_skipped() { let tmp = tempfile::tempdir().unwrap(); stage_global_cache_pkg(tmp.path(), "Newtonsoft.Json", "13.0.3").await; @@ -162,6 +167,7 @@ async fn find_by_purls_invalid_purl_skipped() { // ── crawl_all (scan_package_dir) ─────────────────────────────── #[tokio::test] +#[serial_test::parallel] async fn crawl_all_discovers_global_cache_layout() { let tmp = tempfile::tempdir().unwrap(); stage_global_cache_pkg(tmp.path(), "Newtonsoft.Json", "13.0.3").await; @@ -204,6 +210,7 @@ async fn crawl_all_discovers_global_cache_layout() { } #[tokio::test] +#[serial_test::parallel] async fn crawl_all_discovers_legacy_layout() { let tmp = tempfile::tempdir().unwrap(); stage_legacy_pkg(tmp.path(), "Newtonsoft.Json", "13.0.3").await; @@ -240,6 +247,7 @@ async fn crawl_all_discovers_legacy_layout() { } #[tokio::test] +#[serial_test::parallel] async fn crawl_all_skips_hidden_directories() { let tmp = tempfile::tempdir().unwrap(); // Real package. @@ -419,6 +427,7 @@ async fn get_nuget_package_paths_with_sln_falls_back_to_global() { // ── verify_nuget_package indirectly via find_by_purls ─────────── #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_rejects_dir_without_nuspec_or_lib() { let tmp = tempfile::tempdir().unwrap(); // Create a global-cache-shaped dir but with neither .nuspec nor lib/ — verify fails. @@ -441,6 +450,7 @@ async fn find_by_purls_rejects_dir_without_nuspec_or_lib() { } #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_with_lib_dir_marker_succeeds() { let tmp = tempfile::tempdir().unwrap(); let pkg_dir = tmp.path().join("newtonsoft.json").join("13.0.3"); @@ -469,6 +479,7 @@ mod common; /// `scan_package_dir` short-circuits when read_dir returns Err. #[cfg(unix)] #[tokio::test] +#[serial_test::parallel] async fn crawl_all_handles_unreadable_pkg_path() { if common::uid_is_root() { eprintln!("SKIP: chmod 000 is a no-op under root"); @@ -497,6 +508,7 @@ async fn crawl_all_handles_unreadable_pkg_path() { /// nuget_crawler.rs:236. #[cfg(unix)] #[tokio::test] +#[serial_test::parallel] async fn crawl_all_handles_unreadable_version_dir() { if common::uid_is_root() { eprintln!("SKIP: chmod 000 is a no-op under root"); @@ -543,6 +555,7 @@ async fn crawl_all_handles_unreadable_version_dir() { /// the `if !ft.is_dir()` continue arm at L183. Drive this by staging /// a plain file alongside a valid global-cache package. #[tokio::test] +#[serial_test::parallel] async fn crawl_all_skips_files_at_top_level() { let tmp = tempfile::tempdir().unwrap(); // Stage a real package so the scan actually runs. @@ -569,6 +582,7 @@ async fn crawl_all_skips_files_at_top_level() { /// `scan_package_dir` short-circuits when the package dir doesn't /// exist — covers `read_dir(...).await` Err arm at L169. #[tokio::test] +#[serial_test::parallel] async fn crawl_all_missing_pkg_path_returns_empty() { let tmp = tempfile::tempdir().unwrap(); let crawler = NuGetCrawler; @@ -585,6 +599,7 @@ async fn crawl_all_missing_pkg_path_returns_empty() { // ── NuGetCrawler construction ───────────────────────────────── #[test] +#[serial_test::parallel] fn nuget_crawler_default_and_new_construct_cleanly() { let _a = NuGetCrawler; let _b = NuGetCrawler::new(); diff --git a/crates/socket-patch-core/tests/crawler_python_e2e.rs b/crates/socket-patch-core/tests/crawler_python_e2e.rs index 3ac8560..f80a9bd 100644 --- a/crates/socket-patch-core/tests/crawler_python_e2e.rs +++ b/crates/socket-patch-core/tests/crawler_python_e2e.rs @@ -22,6 +22,7 @@ use socket_patch_core::crawlers::types::CrawlerOptions; use socket_patch_core::crawlers::PythonCrawler; #[test] +#[serial_test::parallel] fn parse_python_site_packages_output_well_formed() { let stdout = "/usr/local/lib/python3.11/site-packages\n/usr/local/lib/python3.11/dist-packages\n"; @@ -34,12 +35,14 @@ fn parse_python_site_packages_output_well_formed() { } #[test] +#[serial_test::parallel] fn parse_python_site_packages_output_empty_returns_empty() { assert!(parse_python_site_packages_output("").is_empty()); assert!(parse_python_site_packages_output("\n \n").is_empty()); } #[test] +#[serial_test::parallel] fn parse_python_site_packages_output_trims_and_skips_blanks() { let stdout = " /a/b \n\n \n/c/d\n"; let paths = parse_python_site_packages_output(stdout); @@ -53,6 +56,7 @@ fn parse_python_site_packages_output_trims_and_skips_blanks() { /// the first-match-wins arm. Lets tests exercise the success arm /// without needing python3 on the host's PATH. #[test] +#[serial_test::parallel] fn find_python_command_with_mock_runner_prefers_python3() { let runner = common::MockCommandRunner::new().with_response( "python3", @@ -65,6 +69,7 @@ fn find_python_command_with_mock_runner_prefers_python3() { /// When `python3` is not present but `python` is, the helper should /// fall through to the second candidate. #[test] +#[serial_test::parallel] fn find_python_command_with_mock_runner_falls_through_to_python() { let runner = common::MockCommandRunner::new().with_response( "python", @@ -77,6 +82,7 @@ fn find_python_command_with_mock_runner_falls_through_to_python() { /// When none of `python3`/`python`/`py` are present, the helper /// returns None. #[test] +#[serial_test::parallel] fn find_python_command_with_mock_runner_none_when_no_binary() { let runner = common::MockCommandRunner::new(); assert_eq!(find_python_command_with(&runner), None); @@ -101,6 +107,7 @@ async fn stage_python_layout(root: &Path, py_ver: &str) -> std::path::PathBuf { /// `python3.`. Covers the wildcard arm + the `name.starts_with` /// filter. #[tokio::test] +#[serial_test::parallel] async fn find_python_dirs_python3_wildcard_matches_versions() { let tmp = tempfile::tempdir().unwrap(); let p1 = stage_python_layout(tmp.path(), "3.11").await; @@ -125,6 +132,7 @@ async fn find_python_dirs_python3_wildcard_matches_versions() { /// `*` generic wildcard matches every directory entry. Covers the /// generic wildcard branch (L142-L160 of python_crawler.rs). #[tokio::test] +#[serial_test::parallel] async fn find_python_dirs_star_wildcard_matches_all() { let tmp = tempfile::tempdir().unwrap(); tokio::fs::create_dir_all( @@ -153,6 +161,7 @@ async fn find_python_dirs_star_wildcard_matches_all() { /// `*` wildcard skips non-directory entries (regular files). Covers /// the `if !ft.is_dir() { continue; }` arm. #[tokio::test] +#[serial_test::parallel] async fn find_python_dirs_star_wildcard_skips_files() { let tmp = tempfile::tempdir().unwrap(); // A regular file at the wildcard position must NOT cause issues. @@ -177,6 +186,7 @@ async fn find_python_dirs_star_wildcard_skips_files() { /// `find_python_dirs` against a non-existent base path returns empty /// — the early-return arm. #[tokio::test] +#[serial_test::parallel] async fn find_python_dirs_nonexistent_base_returns_empty() { let tmp = tempfile::tempdir().unwrap(); let absent = tmp.path().join("does-not-exist"); @@ -187,6 +197,7 @@ async fn find_python_dirs_nonexistent_base_returns_empty() { /// `find_python_dirs` with empty segments returns the base path /// itself (terminal-recursion arm). #[tokio::test] +#[serial_test::parallel] async fn find_python_dirs_empty_segments_returns_base() { let tmp = tempfile::tempdir().unwrap(); let result = find_python_dirs(tmp.path(), &[]).await; @@ -197,6 +208,7 @@ async fn find_python_dirs_empty_segments_returns_base() { /// Literal segment branch: non-wildcard segment is treated as a /// literal subdir. #[tokio::test] +#[serial_test::parallel] async fn find_python_dirs_literal_segment_descends() { let tmp = tempfile::tempdir().unwrap(); let target = tmp.path().join("literal_subdir").join("more"); @@ -689,6 +701,7 @@ async fn get_site_packages_paths_no_marker_no_venv_returns_empty() { /// Well-formed METADATA returns (name, version). #[tokio::test] +#[serial_test::parallel] async fn read_python_metadata_well_formed() { let tmp = tempfile::tempdir().unwrap(); let dist_info = tmp.path().join("requests-2.28.0.dist-info"); @@ -707,6 +720,7 @@ async fn read_python_metadata_well_formed() { /// Missing METADATA file → fall back to the `-.dist-info` /// directory name so a partially-written install stays discoverable. #[tokio::test] +#[serial_test::parallel] async fn read_python_metadata_missing_file_falls_back_to_dir_name() { let tmp = tempfile::tempdir().unwrap(); let dist_info = tmp.path().join("requests-2.28.0.dist-info"); @@ -720,6 +734,7 @@ async fn read_python_metadata_missing_file_falls_back_to_dir_name() { /// METADATA missing Name field → headers are unusable, so fall back to the /// directory name rather than dropping the package. #[tokio::test] +#[serial_test::parallel] async fn read_python_metadata_missing_name_falls_back_to_dir_name() { let tmp = tempfile::tempdir().unwrap(); let dist_info = tmp.path().join("requests-2.28.0.dist-info"); @@ -742,6 +757,7 @@ mod common; /// unreadable. Drives the python_crawler.rs:530 read_dir Err arm. #[cfg(unix)] #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_handles_unreadable_site_packages() { if common::uid_is_root() { eprintln!("SKIP: chmod 000 is a no-op under root"); @@ -766,6 +782,7 @@ async fn find_by_purls_handles_unreadable_site_packages() { /// unreadable — drives python_crawler.rs:584 read_dir Err arm. #[cfg(unix)] #[tokio::test] +#[serial_test::parallel] async fn crawl_all_handles_unreadable_site_packages() { if common::uid_is_root() { eprintln!("SKIP: chmod 000 is a no-op under root"); @@ -790,6 +807,7 @@ async fn crawl_all_handles_unreadable_site_packages() { /// `PythonCrawler::default()` should forward to `new()`. #[test] +#[serial_test::parallel] fn python_crawler_default_and_new_construct_cleanly() { let _a = PythonCrawler; let _b = PythonCrawler::new(); @@ -809,6 +827,7 @@ async fn stage_dist_info(site_packages: &Path, raw_name: &str, version: &str) { } #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_matches_canonicalized_name() { let tmp = tempfile::tempdir().unwrap(); // PEP 503 canonicalization: "Requests" -> "requests" @@ -837,6 +856,7 @@ async fn find_by_purls_matches_canonicalized_name() { } #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_strips_qualifiers() { let tmp = tempfile::tempdir().unwrap(); stage_dist_info(tmp.path(), "requests", "2.28.0").await; @@ -867,6 +887,7 @@ async fn find_by_purls_strips_qualifiers() { /// package silently fails to match. Twin of the strip_purl_qualifiers /// subpath fix in utils::purl. #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_strips_subpath() { let tmp = tempfile::tempdir().unwrap(); stage_dist_info(tmp.path(), "requests", "2.28.0").await; @@ -896,6 +917,7 @@ async fn find_by_purls_strips_subpath() { /// — reported "not installed", patch skipped. Twin of the npm crawler's /// percent-decode handling. #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_percent_decodes_encoded_version() { let tmp = tempfile::tempdir().unwrap(); stage_dist_info(tmp.path(), "torch", "2.1.0+cpu").await; @@ -920,6 +942,7 @@ async fn find_by_purls_percent_decodes_encoded_version() { } #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_empty_purls_returns_empty() { let tmp = tempfile::tempdir().unwrap(); stage_dist_info(tmp.path(), "requests", "2.28.0").await; @@ -930,6 +953,7 @@ async fn find_by_purls_empty_purls_returns_empty() { } #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_missing_site_packages_returns_empty() { let tmp = tempfile::tempdir().unwrap(); let crawler = PythonCrawler; @@ -945,6 +969,7 @@ async fn find_by_purls_missing_site_packages_returns_empty() { } #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_invalid_purl_skipped() { let tmp = tempfile::tempdir().unwrap(); stage_dist_info(tmp.path(), "requests", "2.28.0").await; @@ -958,6 +983,7 @@ async fn find_by_purls_invalid_purl_skipped() { } #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_version_mismatch_returns_empty() { let tmp = tempfile::tempdir().unwrap(); stage_dist_info(tmp.path(), "requests", "2.28.0").await; @@ -971,6 +997,7 @@ async fn find_by_purls_version_mismatch_returns_empty() { } #[tokio::test] +#[serial_test::parallel] async fn crawl_all_via_site_packages_finds_dist_info_packages() { let tmp = tempfile::tempdir().unwrap(); stage_dist_info(tmp.path(), "Requests", "2.28.0").await; @@ -1014,6 +1041,7 @@ async fn crawl_all_via_site_packages_finds_dist_info_packages() { } #[tokio::test] +#[serial_test::parallel] async fn crawl_all_with_unparseable_dist_info_skips() { let tmp = tempfile::tempdir().unwrap(); // No version segment in the directory name, so neither the (empty) @@ -1039,6 +1067,7 @@ async fn crawl_all_with_unparseable_dist_info_skips() { /// `get_site_packages_paths` with `global_prefix` set returns just that /// prefix — exercises the early-return arm at python_crawler.rs:473-474. #[tokio::test] +#[serial_test::parallel] async fn get_site_packages_paths_with_global_prefix_passthrough() { let tmp = tempfile::tempdir().unwrap(); let custom = tmp.path().join("custom-sp"); @@ -1064,6 +1093,7 @@ async fn get_site_packages_paths_with_global_prefix_passthrough() { /// (`2.28.0`) so the result proves the blank-line break fired: a `2.28.0` /// result would mean the break leaked the trailing header. #[tokio::test] +#[serial_test::parallel] async fn read_python_metadata_stops_at_blank_line_then_falls_back() { let tmp = tempfile::tempdir().unwrap(); let dist = tmp.path().join("requests-9.9.9.dist-info"); @@ -1084,6 +1114,7 @@ async fn read_python_metadata_stops_at_blank_line_then_falls_back() { /// METADATA missing Version field → headers unusable, fall back to the /// directory name. #[tokio::test] +#[serial_test::parallel] async fn read_python_metadata_missing_version_falls_back_to_dir_name() { let tmp = tempfile::tempdir().unwrap(); let dist_info = tmp.path().join("requests-2.28.0.dist-info"); diff --git a/crates/socket-patch-core/tests/crawler_ruby_e2e.rs b/crates/socket-patch-core/tests/crawler_ruby_e2e.rs index 876457a..2eccfab 100644 --- a/crates/socket-patch-core/tests/crawler_ruby_e2e.rs +++ b/crates/socket-patch-core/tests/crawler_ruby_e2e.rs @@ -12,6 +12,7 @@ use socket_patch_core::crawlers::types::CrawlerOptions; use socket_patch_core::crawlers::RubyCrawler; #[test] +#[serial_test::parallel] fn parse_gem_env_output_well_formed() { assert_eq!( parse_gem_env_output("/Users/foo/.gem/ruby/3.2.0\n").as_deref(), @@ -20,6 +21,7 @@ fn parse_gem_env_output_well_formed() { } #[test] +#[serial_test::parallel] fn parse_gem_env_output_empty_returns_none() { assert_eq!(parse_gem_env_output(""), None); assert_eq!(parse_gem_env_output(" \n "), None); @@ -66,6 +68,7 @@ fn install_fake_gem(bin_dir: &Path, gemdir: &Path) { // ── find_by_purls ────────────────────────────────────────────── #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_finds_gem_in_gem_path() { let tmp = tempfile::tempdir().unwrap(); let pkg_dir = stage_gem(tmp.path(), "rails", "7.1.0").await; @@ -85,6 +88,7 @@ async fn find_by_purls_finds_gem_in_gem_path() { } #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_accepts_gem_with_gemspec_only() { let tmp = tempfile::tempdir().unwrap(); // Stage with .gemspec but NO lib/ directory (alternate marker). @@ -110,6 +114,7 @@ async fn find_by_purls_accepts_gem_with_gemspec_only() { } #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_rejects_dir_without_lib_or_gemspec() { let tmp = tempfile::tempdir().unwrap(); let pkg_dir = tmp.path().join("rails-7.1.0"); @@ -125,6 +130,7 @@ async fn find_by_purls_rejects_dir_without_lib_or_gemspec() { } #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_no_match_returns_empty() { let tmp = tempfile::tempdir().unwrap(); let crawler = RubyCrawler; @@ -136,6 +142,7 @@ async fn find_by_purls_no_match_returns_empty() { } #[tokio::test] +#[serial_test::parallel] async fn find_by_purls_invalid_purl_skipped() { let tmp = tempfile::tempdir().unwrap(); // Stage a gem dir that WOULD match `rails@7.1.0` on disk. The only @@ -171,6 +178,7 @@ async fn find_by_purls_invalid_purl_skipped() { // ── crawl_all ───────────────────────────────────────────────── #[tokio::test] +#[serial_test::parallel] async fn crawl_all_discovers_gems_in_path() { let tmp = tempfile::tempdir().unwrap(); stage_gem(tmp.path(), "rails", "7.1.0").await; @@ -208,6 +216,7 @@ async fn crawl_all_discovers_gems_in_path() { // ── get_gem_paths ────────────────────────────────────────────── #[tokio::test] +#[serial_test::parallel] async fn get_gem_paths_with_global_prefix_returns_only_prefix() { let tmp = tempfile::tempdir().unwrap(); let crawler = RubyCrawler; @@ -221,6 +230,7 @@ async fn get_gem_paths_with_global_prefix_returns_only_prefix() { } #[tokio::test] +#[serial_test::parallel] async fn get_gem_paths_vendor_bundle_takes_precedence_over_global() { let tmp = tempfile::tempdir().unwrap(); // Build a vendor/bundle/ruby//gems layout. Bundler's scan @@ -247,6 +257,7 @@ async fn get_gem_paths_vendor_bundle_takes_precedence_over_global() { } #[tokio::test] +#[serial_test::parallel] async fn get_gem_paths_no_gemfile_returns_empty() { let tmp = tempfile::tempdir().unwrap(); // No Gemfile, no Gemfile.lock, no vendor/bundle. @@ -379,6 +390,7 @@ mod common; /// drives ruby_crawler.rs:270 read_dir Err arm. #[cfg(unix)] #[tokio::test] +#[serial_test::parallel] async fn crawl_all_handles_unreadable_gem_dir() { if common::uid_is_root() { eprintln!("SKIP: chmod 000 is a no-op under root"); @@ -404,6 +416,7 @@ async fn crawl_all_handles_unreadable_gem_dir() { /// `RubyCrawler::default()` should forward to `new()`. #[test] +#[serial_test::parallel] fn ruby_crawler_default_and_new_construct_cleanly() { let _a = RubyCrawler; let _b = RubyCrawler::new();