Tuning tman for dotnet test

.NET

Restore and build are quiet; testhost processes are famous orphans.

capstart atwhat it answers
stall15mhang backstop — no output and no CPU/IO activity for this long
max-time20mwall-clock budget — the only cap that bounds a run that is working but will never finish
max-memunset — opt in only if it has hurt youRSS ceiling across the process tree
max-parallel2slots per name-and-directory bucket

These are starting points derived from how dotnet test behaves, not measurements of your machine. Every one of them should be replaced by a number you took from your own runs — the last section on this page is how.

.tman.kdl
alias "test" {
    command "dotnet"
    args "test" "tests/YourProject.Tests/YourProject.Tests.csproj"
    max-time "20m"
}

A dotnet test invocation is three phases wearing one command: restore, build, then the actual run. The first two are near-silent and can dominate the wall clock on a cold cache or a large solution, so the stall backstop has to be sized for the build, not for the tests.

testhost is the orphan

The test run happens in a separate testhost process. When the parent dies badly — an agent that gave up, a terminal that closed, a machine that suspended — that child routinely survives, holding a lock on the build output and, if the suite starts one, a port. This is the textbook case for the reaper: every tman command, including tman list, kills live children whose runner has died.

shell
tman list          # the sweep runs first, then you see what is left
tman clean         # same sweep, on demand, printing the counts

Point the alias at the test project, not at the solution root. Run from a directory holding an application .csproj, dotnet test resolves that project, finds no tests, and can exit 0 in a few seconds having run nothing — a green that means nothing at all. An explicit path to the test project is the fix, and a build-time guard that refuses the ambiguous invocation is worth adding if the mistake has been made once.

Separate the build from the test

If restore and build dominate, split them into their own alias with their own caps. dotnet build is supposed to saturate cores and can want several GB, so it wants a wide budget and no memory ceiling; the test run afterwards wants a tighter one. dotnet test --no-build then measures what you actually care about.

Turn the starting point into a real number

Everything above is derived from how this tool behaves, not from your machine. The numbers that matter come from your own runs, and getting them takes one command:

shell
tman status --json | jq '.runs[] | {name, exit, duration_ms}'

Set max-time from the slowest successful run you are willing to wait for, with real headroom — a cap that fires on healthy work teaches everyone to raise the cap, and after a few rounds of that it bounds nothing. Set stall from the longest stretch the command legitimately produces no output and no CPU, then double it.

Exit 124, 125, and 126 are findings, not flakes. 124 is a wall-clock timeout, 125 a stall, 126 a resource cull. Widening the cap that fired is the right response only after you know why it fired — the first two times a suite trips max-mem, the interesting question is what is holding the memory.

Frequently asked

How do I clean up leftover testhost processes?

Any tman command does it. Every invocation, including `tman list`, runs a housekeeping sweep first that kills live children whose runner has died and prunes expired records. `tman clean` runs the same sweep on demand and prints what it did.

Why does dotnet test exit 0 without running tests?

It resolved the wrong project. Run from a directory holding an application .csproj rather than a test project, it finds no tests and reports success in a few seconds. Always give the alias an explicit path to the test project — and consider a build target that refuses the ambiguous invocation outright.

Sources