@@ -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).
6382fn 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};\n Console.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