Skip to content

Commit e851b52

Browse files
authored
Merge pull request #181 from AdaWorldAPI/claude/csharp-parity-dotnet-home-isolation
[ogar-adapter-csharp] isolate dotnet CLI home per test — fix flaky NuGet-migration shm race
2 parents da03b22 + 70b0783 commit e851b52

1 file changed

Lines changed: 36 additions & 11 deletions

File tree

crates/ogar-adapter-csharp/tests/parity.rs

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,30 @@ fn unique_tmp_dir(tag: &str) -> PathBuf {
4242
dir
4343
}
4444

45-
fn dotnet() -> Command {
45+
fn dotnet(home: &Path) -> Command {
4646
let mut cmd = Command::new("dotnet");
4747
cmd.env("DOTNET_CLI_TELEMETRY_OPTOUT", "1")
4848
.env("DOTNET_SKIP_FIRST_TIME_EXPERIENCE", "1")
49-
.env("DOTNET_NOLOGO", "1");
49+
.env("DOTNET_NOLOGO", "1")
50+
// Isolate the dotnet temp/home tree per invocation. The flaky
51+
// failure is the .NET runtime's cross-process NAMED-MUTEX shared
52+
// memory, which NuGet's first-run MigrationRunner opens. On Linux
53+
// that shm lives at `$TMPDIR/.dotnet/shm` (falling back to
54+
// `/tmp/.dotnet/shm` when TMPDIR is unset) — NOT under HOME or
55+
// DOTNET_CLI_HOME. Two concurrent `dotnet` processes (the two
56+
// parity tests run on separate threads in one binary; CI also runs
57+
// jobs in parallel) then race on the one shared `.../shm/session<N>`
58+
// path: one loses with `mkdir(...) == EEXIST` or `stat(...) ==
59+
// ENOENT` in NuGet.Common.Migrations.MigrationRunner — an
60+
// intermittent hard failure with nothing to do with the code under
61+
// test. **`TMPDIR` is the controlling variable** (it relocates the
62+
// shm dir); HOME/DOTNET_CLI_HOME/NUGET_PACKAGES are additionally
63+
// isolated as hygiene. Each test owns its own `home`, so the shm
64+
// dirs never collide.
65+
.env("TMPDIR", home)
66+
.env("HOME", home)
67+
.env("DOTNET_CLI_HOME", home)
68+
.env("NUGET_PACKAGES", home.join("nuget"));
5069
cmd
5170
}
5271

@@ -61,7 +80,9 @@ fn dotnet() -> Command {
6180
/// rather than silently skip. CI images therefore MUST provision the
6281
/// `net8.0` SDK (pure BCL, no NuGet — see the module doc).
6382
fn dotnet_available() -> bool {
64-
let present = dotnet().arg("--version").output().is_ok();
83+
let probe_home = unique_tmp_dir("probe-home");
84+
let present = dotnet(&probe_home).arg("--version").output().is_ok();
85+
let _ = fs::remove_dir_all(&probe_home);
6586
assert!(
6687
present || std::env::var_os("CI").is_none(),
6788
"`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) {
83104
fs::write(dir.join("Program.cs"), program_cs).expect("write Program.cs");
84105
}
85106

86-
fn dotnet_build(dir: &Path) {
87-
let build = dotnet()
107+
fn dotnet_build(dir: &Path, home: &Path) {
108+
let build = dotnet(home)
88109
.current_dir(dir)
89110
.args(["build", "-v", "q"])
90111
.output()
@@ -98,8 +119,8 @@ fn dotnet_build(dir: &Path) {
98119
);
99120
}
100121

101-
fn dotnet_run(dir: &Path) -> String {
102-
let run = dotnet()
122+
fn dotnet_run(dir: &Path, home: &Path) -> String {
123+
let run = dotnet(home)
103124
.current_dir(dir)
104125
.args(["run", "-v", "q", "--no-build"])
105126
.output()
@@ -124,13 +145,14 @@ fn emitted_library_builds_and_dump_matches_ground_truth() {
124145
return;
125146
}
126147
let dir = unique_tmp_dir("build-dump");
148+
let home = unique_tmp_dir("build-dump-home");
127149
scaffold_project(
128150
&dir,
129151
&format!("using {NAMESPACE};\nConsole.Write(OgarCapabilitySurface.Dump());\n"),
130152
);
131153

132-
dotnet_build(&dir);
133-
let dump = dotnet_run(&dir);
154+
dotnet_build(&dir, &home);
155+
let dump = dotnet_run(&dir, &home);
134156

135157
// Compare the dump against the Rust-side ogar_vocab ground truth
136158
// exactly like the Python loop — the SAME comparison function
@@ -140,6 +162,7 @@ fn emitted_library_builds_and_dump_matches_ground_truth() {
140162
ogar_adapter_python::ground_truth::assert_dump_matches("csharp", &dump);
141163

142164
let _ = fs::remove_dir_all(&dir);
165+
let _ = fs::remove_dir_all(&home);
143166
}
144167

145168
#[test]
@@ -152,6 +175,7 @@ fn facet_bytes_built_in_rust_decode_correctly_in_csharp() {
152175
return;
153176
}
154177
let dir = unique_tmp_dir("facet");
178+
let home = unique_tmp_dir("facet-home");
155179

156180
// Rust builds a known 16-byte facet BY HAND, straight from the
157181
// documented layout (facet.rs / ruff_spo_address::Facet doc):
@@ -183,8 +207,8 @@ fn facet_bytes_built_in_rust_decode_correctly_in_csharp() {
183207
);
184208
scaffold_project(&dir, &program_cs);
185209

186-
dotnet_build(&dir);
187-
let got = dotnet_run(&dir);
210+
dotnet_build(&dir, &home);
211+
let got = dotnet_run(&dir, &home);
188212

189213
let expected = format!(
190214
"{classid:08X}\n{}\n{}",
@@ -205,4 +229,5 @@ fn facet_bytes_built_in_rust_decode_correctly_in_csharp() {
205229
);
206230

207231
let _ = fs::remove_dir_all(&dir);
232+
let _ = fs::remove_dir_all(&home);
208233
}

0 commit comments

Comments
 (0)