dotnet test does not finish on macOS ARM64. It wedges partway through and the run never returns. CI is ubuntu-latest and is unaffected, which is why this has stayed invisible — it only costs whoever runs the suite locally, and it costs them a core.
Splitting this out of #437, where it turned up.
Two distinct signatures, not one
This is the part that took the longest to see, and it matters because 5f84116 documents only the first.
1. The GC-suspension livelock — the one 5f84116 describes. Spins at ~107-108% CPU. Sampling a wedged host shows 16 frames of SuspendEE / CheckActivationSafePoint, plus a .NET BGC thread. Reproduced under xunit.v3 4.0.0 / Microsoft.Testing.Platform.
2. A blocked wedge, which is something else. Under the current runner (VSTest, xunit.v3 3.2.2) I isolated a hang to McpPlanPathPolicyTests where every thread sits in ThreadNativeWait / Monitor_Wait, 0% CPU, no SuspendEE anywhere in the sample. That is not a livelock, it is a deadlock, and the two want different fixes.
I could not narrow #2 past the class. It survives excluding OpenAsync_TimesOutUnresponsiveNativeRoots and OpenAsync_ValidatesAndReturnsTheSameOpenedHandle, so it is not simply the infinite-Task.Delay stub or the symlink-swap test.
A lead worth chasing first
The wedged host had a live child process:
39273 PlanViewer.Core.Tests <- wedged, 107% CPU
└ 39275 dotnet planview.dll mcp serve <- real MCP server, spawned by the tests
The suite starts real planview mcp serve subprocesses, and both hangs are in Mcp* classes. CI logs show the same thing from the other end — every run ends with the runner reaping orphans:
Terminate orphan process: pid (8721) (dotnet)
Terminate orphan process: pid (8722) (dotnet)
...
A child process that outlives, or fails to complete, its handshake would explain a 0%-CPU all-threads-blocked wedge much more naturally than a runtime bug does. I would look there before looking at CoreCLR.
No test-runner timeout stops signature 1
Measured against a genuinely wedged host, not assumed:
| mechanism |
setting |
outcome |
| MTP hangdump |
--hangdump-timeout 90s |
ran 2m55s at 107% CPU, never fired |
| MTP session timeout |
--timeout 60s |
ran 8 minutes, never fired |
MTP does put the timeout in a separate controller process (confirmed in the process tree) — that was the property 5f84116 argued was essential — and it still cannot win. Same reason the commit gives for xUnit's own timeouts: the diagnostics the platform needs are served by the execution engine that is suspended.
--blame-hang-timeout under VSTest did not fire on it either.
Honest gap: I have not shown the current TestSessionTimeout watchdog kills a livelocked host. 5f84116 verified it at 1ms on a healthy run, which proves the property is wired up, not that it can terminate a wedge. Both runners may be equally powerless here. Worth testing deliberately before trusting the 15-minute backstop.
Reproducing
dotnet test tests/PlanViewer.Core.Tests with no filter, on macOS ARM64 (.NET 10.0.10, Apple silicon). Not every run wedges; it took several attempts to land each signature. Reproduces on unmodified dev — the first run I did, before touching anything, hung this way.
Filtered runs are fine, which is the current workaround: everything except Mcp*/PlanRepl* passes (279 tests).
Why it is worth fixing rather than documenting
CONTRIBUTING.md tells contributors to run dotnet test. On a Mac that command does not return, and leaves a process pinning a core after they Ctrl-C the runner.
dotnet testdoes not finish on macOS ARM64. It wedges partway through and the run never returns. CI isubuntu-latestand is unaffected, which is why this has stayed invisible — it only costs whoever runs the suite locally, and it costs them a core.Splitting this out of #437, where it turned up.
Two distinct signatures, not one
This is the part that took the longest to see, and it matters because
5f84116documents only the first.1. The GC-suspension livelock — the one
5f84116describes. Spins at ~107-108% CPU. Sampling a wedged host shows 16 frames ofSuspendEE/CheckActivationSafePoint, plus a.NET BGCthread. Reproduced under xunit.v3 4.0.0 / Microsoft.Testing.Platform.2. A blocked wedge, which is something else. Under the current runner (VSTest, xunit.v3 3.2.2) I isolated a hang to
McpPlanPathPolicyTestswhere every thread sits inThreadNativeWait/Monitor_Wait, 0% CPU, noSuspendEEanywhere in the sample. That is not a livelock, it is a deadlock, and the two want different fixes.I could not narrow #2 past the class. It survives excluding
OpenAsync_TimesOutUnresponsiveNativeRootsandOpenAsync_ValidatesAndReturnsTheSameOpenedHandle, so it is not simply the infinite-Task.Delaystub or the symlink-swap test.A lead worth chasing first
The wedged host had a live child process:
The suite starts real
planview mcp servesubprocesses, and both hangs are inMcp*classes. CI logs show the same thing from the other end — every run ends with the runner reaping orphans:A child process that outlives, or fails to complete, its handshake would explain a 0%-CPU all-threads-blocked wedge much more naturally than a runtime bug does. I would look there before looking at CoreCLR.
No test-runner timeout stops signature 1
Measured against a genuinely wedged host, not assumed:
--hangdump-timeout 90s--timeout 60sMTP does put the timeout in a separate controller process (confirmed in the process tree) — that was the property
5f84116argued was essential — and it still cannot win. Same reason the commit gives for xUnit's own timeouts: the diagnostics the platform needs are served by the execution engine that is suspended.--blame-hang-timeoutunder VSTest did not fire on it either.Honest gap: I have not shown the current
TestSessionTimeoutwatchdog kills a livelocked host.5f84116verified it at 1ms on a healthy run, which proves the property is wired up, not that it can terminate a wedge. Both runners may be equally powerless here. Worth testing deliberately before trusting the 15-minute backstop.Reproducing
dotnet test tests/PlanViewer.Core.Testswith no filter, on macOS ARM64 (.NET 10.0.10, Apple silicon). Not every run wedges; it took several attempts to land each signature. Reproduces on unmodifieddev— the first run I did, before touching anything, hung this way.Filtered runs are fine, which is the current workaround: everything except
Mcp*/PlanRepl*passes (279 tests).Why it is worth fixing rather than documenting
CONTRIBUTING.mdtells contributors to rundotnet test. On a Mac that command does not return, and leaves a process pinning a core after they Ctrl-C the runner.