Skip to content

[BUG] dotnet test never finishes on macOS ARM64 — two distinct test-host wedges, both in Mcp* classes #441

Description

@erikdarlingdata

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions