Skip to main content

Concurrency

What it is​

ProtoTest scopes the execution context to the async flow. Proto.Context is per test and flow-local. Two parallel tests do not share clients, resources or trace. That is why the runner's own parallelism is safe to use.

Inside a test​

The context flows into anything a test awaits. Two patterns matter:

  • Fan-out that keeps the flow works. A Task.WhenAll over tasks you start in the test body keeps the ambient context, so the operations those tasks record land on the test's trace.
  • Suppressed or abandoned flow loses the context. ExecutionContext.SuppressFlow(), a detached Thread, or work started after the context was cleared run without Proto.Context. Reading it throws, and anything they record lands on the run trace instead of the test's. Keep fan-out on the test's flow, or pass the ProtoExecutionContext explicitly to the delegate that needs it.

One test owns one context while it is active. A nested lifecycle cannot start on the same flow.

Exercised parallelism​

Recorded 2026-09-24 on a 16-core developer machine against a suite with Postgres, RabbitMQ and real browsers, using NUnit.NumberOfTestWorkers. Quote these numbers only after re-running your own suite on your own hardware:

WorkersResultDuration
853 passed, 6 skipped6 s
3253 passed, 6 skipped8 s
641 failed, then passed on rerun21 s

The suite was stable up to roughly twice the core count. At four times the cores the run oversubscribed and produced one failure that never reproduced. Its evidence was lost to a harness limitation: the suite writes a single trace file per run, so the passing rerun overwrote the failed run's trace before it could be read. There is no framework finding behind that row, only an unreadable artifact.

Re-run 2026-09-29 on a 16-core machine (dotnet test -- NUnit.NumberOfTestWorkers=64):

  • tests/ProtoTest.Core.Tests: 380 passed, three runs, about 4 s each.
  • samples/Northstar.ProtoTest with container Postgres, container RabbitMQ and a headless Chromium journey: 15 passed, 4 skipped, two runs (6 s and 2 s).

Beyond this range, expect resource pressure rather than a ProtoTest-specific limit: port and container exhaustion, browser memory, and contention on run-scoped infrastructure.

Per-runner knobs​

ProtoTest does not schedule tests. Each runner brings its own switch:

RunnerKnob
NUnitdotnet test -- NUnit.NumberOfTestWorkers=8, or [assembly: LevelOfParallelism(8)] (tests/NUnitParallelization.cs links both into every parallel NUnit project)
xUnit v2[assembly: CollectionBehavior(MaxParallelThreads = 8)]; collections run in parallel by default
xUnit v3[assembly: Xunit.v3.Parallelization(MaxThreads = 8)]; parallel by default, like v2
MSTest[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]
TUnitparallel by default; [NotInParallel] keeps a test off the parallel path, with an optional constraint key to serialize only against the tests that share it

The runner pages name the same switches where they change behavior: NUnit, xUnit v2, xUnit v3, MSTest, TUnit.

What the trace shows​

  • Operations recorded on the test's flow land on that test's record, whichever task recorded them.
  • Operations from a suppressed or abandoned flow land on the run trace, without a test to attribute them to. They are still correlated by trace id.
  • Each test's record carries its own id, so two parallel tests writing similar-looking requests stay distinguishable.

Limits​

  • The context is flow-local. Anything that escapes the flow cannot read Proto.Context; pass the context explicitly or correlate by trace id with ProtoHost.FindTraceWriter.
  • One context per active test per flow. Starting a second test on the same flow throws.
  • Parallelism belongs to the runner. ProtoTest does not schedule tests; configure workers with your runner's own options.
  • Shared run-scoped pieces are shared. Containers, a per-run fake or a shared client serve every parallel test, so plan for concurrent use. See Clients and Infrastructure.
  • The measured ceiling is resource pressure. Oversubscribing workers costs ports, containers, browser memory and database connections, not ProtoTest correctness.