From 1003d7c6912e32d08b6fee049c2234066ddfa865 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 7 Jul 2026 22:52:00 +0000 Subject: [PATCH 1/2] =?UTF-8?q?[ogar-adapter-csharp]=20isolate=20dotnet=20?= =?UTF-8?q?CLI=20home=20per=20test=20=E2=80=94=20fix=20flaky=20NuGet-migra?= =?UTF-8?q?tion=20shm=20race?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The C# parity tests shell out to `dotnet build`/`dotnet run` but shared the default dotnet home (`~/.dotnet`, or `/tmp/.dotnet` when HOME is unset). Its first-run NuGet-migration step opens a named session mutex under `.../shm/session`; two concurrent `dotnet` processes (parallel test binaries, or parallel CI jobs on one runner) race on the same path and one fails with `mkdir(...) == -1; errno == EEXIST` in NuGet.Common.Migrations.MigrationRunner — an intermittent hard failure with nothing to do with the code under test (observed red on OGAR #179 while main was green on the same commit + same test). Fix: point HOME + DOTNET_CLI_HOME + NUGET_PACKAGES at a per-test unique temp dir (sibling of the existing per-test project dir), so the shm/session dirs never collide. `dotnet()` now takes a home path; both tests create and clean their own home; the availability probe uses a throwaway home. Test-only, no product change; compiles clean (`cargo test -p ogar-adapter-csharp --no-run`). --- crates/ogar-adapter-csharp/tests/parity.rs | 42 ++++++++++++++++------ 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/crates/ogar-adapter-csharp/tests/parity.rs b/crates/ogar-adapter-csharp/tests/parity.rs index 6655456..4793f79 100644 --- a/crates/ogar-adapter-csharp/tests/parity.rs +++ b/crates/ogar-adapter-csharp/tests/parity.rs @@ -42,11 +42,25 @@ 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 CLI home + NuGet caches per invocation-tree. + // Without this, every `dotnet` process shares the default + // `~/.dotnet` (`/tmp/.dotnet` when HOME is unset), whose first-run + // NuGet-migration step opens a named session mutex under + // `.../shm/session`. Two concurrent processes (parallel test + // binaries, or parallel CI jobs on one runner) then race on the + // same path — `mkdir(...) == -1; errno == EEXIST` in + // NuGet.Common.Migrations.MigrationRunner, an intermittent hard + // failure unrelated to the code under test. Pointing HOME + + // DOTNET_CLI_HOME + NUGET_PACKAGES at a per-test dir removes the + // shared path, so the shm/session dirs never collide. + .env("HOME", home) + .env("DOTNET_CLI_HOME", home) + .env("NUGET_PACKAGES", home.join("nuget")); cmd } @@ -61,7 +75,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 +99,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 +114,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 +140,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 +157,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 +170,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 +202,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 +224,5 @@ fn facet_bytes_built_in_rust_decode_correctly_in_csharp() { ); let _ = fs::remove_dir_all(&dir); + let _ = fs::remove_dir_all(&home); } From 70b0783783f3cf9231f74ba5bd0737ca557a861a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Jul 2026 03:46:49 +0000 Subject: [PATCH 2/2] fixup: TMPDIR is the shm-controlling var, not HOME/DOTNET_CLI_HOME MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first pass isolated HOME/DOTNET_CLI_HOME/NUGET_PACKAGES but the failure persisted (error flipped EEXIST->ENOENT, path still /tmp/.dotnet/shm) — proving those vars don't move the .NET named-mutex shared-memory dir. That dir is $TMPDIR/.dotnet/shm (fallback /tmp). Adding TMPDIR= relocates it per invocation, so the two parity tests (and parallel CI jobs) no longer race on one shared shm/session path. Comment corrected to name TMPDIR as the fix. --- crates/ogar-adapter-csharp/tests/parity.rs | 27 +++++++++++++--------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/crates/ogar-adapter-csharp/tests/parity.rs b/crates/ogar-adapter-csharp/tests/parity.rs index 4793f79..5a0c844 100644 --- a/crates/ogar-adapter-csharp/tests/parity.rs +++ b/crates/ogar-adapter-csharp/tests/parity.rs @@ -47,17 +47,22 @@ fn dotnet(home: &Path) -> Command { cmd.env("DOTNET_CLI_TELEMETRY_OPTOUT", "1") .env("DOTNET_SKIP_FIRST_TIME_EXPERIENCE", "1") .env("DOTNET_NOLOGO", "1") - // Isolate the dotnet CLI home + NuGet caches per invocation-tree. - // Without this, every `dotnet` process shares the default - // `~/.dotnet` (`/tmp/.dotnet` when HOME is unset), whose first-run - // NuGet-migration step opens a named session mutex under - // `.../shm/session`. Two concurrent processes (parallel test - // binaries, or parallel CI jobs on one runner) then race on the - // same path — `mkdir(...) == -1; errno == EEXIST` in - // NuGet.Common.Migrations.MigrationRunner, an intermittent hard - // failure unrelated to the code under test. Pointing HOME + - // DOTNET_CLI_HOME + NUGET_PACKAGES at a per-test dir removes the - // shared path, so the shm/session dirs never collide. + // 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"));