diff --git a/crates/ogar-adapter-csharp/tests/parity.rs b/crates/ogar-adapter-csharp/tests/parity.rs index 6655456..5a0c844 100644 --- a/crates/ogar-adapter-csharp/tests/parity.rs +++ b/crates/ogar-adapter-csharp/tests/parity.rs @@ -42,11 +42,30 @@ fn unique_tmp_dir(tag: &str) -> PathBuf { dir } -fn dotnet() -> Command { +fn dotnet(home: &Path) -> Command { let mut cmd = Command::new("dotnet"); cmd.env("DOTNET_CLI_TELEMETRY_OPTOUT", "1") .env("DOTNET_SKIP_FIRST_TIME_EXPERIENCE", "1") - .env("DOTNET_NOLOGO", "1"); + .env("DOTNET_NOLOGO", "1") + // Isolate the dotnet temp/home tree per invocation. The flaky + // failure is the .NET runtime's cross-process NAMED-MUTEX shared + // memory, which NuGet's first-run MigrationRunner opens. On Linux + // that shm lives at `$TMPDIR/.dotnet/shm` (falling back to + // `/tmp/.dotnet/shm` when TMPDIR is unset) — NOT under HOME or + // DOTNET_CLI_HOME. Two concurrent `dotnet` processes (the two + // parity tests run on separate threads in one binary; CI also runs + // jobs in parallel) then race on the one shared `.../shm/session` + // path: one loses with `mkdir(...) == EEXIST` or `stat(...) == + // ENOENT` in NuGet.Common.Migrations.MigrationRunner — an + // intermittent hard failure with nothing to do with the code under + // test. **`TMPDIR` is the controlling variable** (it relocates the + // shm dir); HOME/DOTNET_CLI_HOME/NUGET_PACKAGES are additionally + // isolated as hygiene. Each test owns its own `home`, so the shm + // dirs never collide. + .env("TMPDIR", home) + .env("HOME", home) + .env("DOTNET_CLI_HOME", home) + .env("NUGET_PACKAGES", home.join("nuget")); cmd } @@ -61,7 +80,9 @@ fn dotnet() -> Command { /// rather than silently skip. CI images therefore MUST provision the /// `net8.0` SDK (pure BCL, no NuGet — see the module doc). fn dotnet_available() -> bool { - let present = dotnet().arg("--version").output().is_ok(); + let probe_home = unique_tmp_dir("probe-home"); + let present = dotnet(&probe_home).arg("--version").output().is_ok(); + let _ = fs::remove_dir_all(&probe_home); assert!( present || std::env::var_os("CI").is_none(), "`dotnet` not found but `CI` is set — the C# parity loop requires the \ @@ -83,8 +104,8 @@ fn scaffold_project(dir: &Path, program_cs: &str) { fs::write(dir.join("Program.cs"), program_cs).expect("write Program.cs"); } -fn dotnet_build(dir: &Path) { - let build = dotnet() +fn dotnet_build(dir: &Path, home: &Path) { + let build = dotnet(home) .current_dir(dir) .args(["build", "-v", "q"]) .output() @@ -98,8 +119,8 @@ fn dotnet_build(dir: &Path) { ); } -fn dotnet_run(dir: &Path) -> String { - let run = dotnet() +fn dotnet_run(dir: &Path, home: &Path) -> String { + let run = dotnet(home) .current_dir(dir) .args(["run", "-v", "q", "--no-build"]) .output() @@ -124,13 +145,14 @@ fn emitted_library_builds_and_dump_matches_ground_truth() { return; } let dir = unique_tmp_dir("build-dump"); + let home = unique_tmp_dir("build-dump-home"); scaffold_project( &dir, &format!("using {NAMESPACE};\nConsole.Write(OgarCapabilitySurface.Dump());\n"), ); - dotnet_build(&dir); - let dump = dotnet_run(&dir); + dotnet_build(&dir, &home); + let dump = dotnet_run(&dir, &home); // Compare the dump against the Rust-side ogar_vocab ground truth // exactly like the Python loop — the SAME comparison function @@ -140,6 +162,7 @@ fn emitted_library_builds_and_dump_matches_ground_truth() { ogar_adapter_python::ground_truth::assert_dump_matches("csharp", &dump); let _ = fs::remove_dir_all(&dir); + let _ = fs::remove_dir_all(&home); } #[test] @@ -152,6 +175,7 @@ fn facet_bytes_built_in_rust_decode_correctly_in_csharp() { return; } let dir = unique_tmp_dir("facet"); + let home = unique_tmp_dir("facet-home"); // Rust builds a known 16-byte facet BY HAND, straight from the // documented layout (facet.rs / ruff_spo_address::Facet doc): @@ -183,8 +207,8 @@ fn facet_bytes_built_in_rust_decode_correctly_in_csharp() { ); scaffold_project(&dir, &program_cs); - dotnet_build(&dir); - let got = dotnet_run(&dir); + dotnet_build(&dir, &home); + let got = dotnet_run(&dir, &home); let expected = format!( "{classid:08X}\n{}\n{}", @@ -205,4 +229,5 @@ fn facet_bytes_built_in_rust_decode_correctly_in_csharp() { ); let _ = fs::remove_dir_all(&dir); + let _ = fs::remove_dir_all(&home); }