Skip to content

mtp runner lacks on option to test each mutant in its own process #3742

Description

@gem4511

Describe the bug

With "test-runner": "mtp", mutants that are provably equivalent (no test can possibly detect them) are intermittently reported Killed when the code-under-test holds process-global state — e.g. any CSLA BusinessBase, whose validation rules are registered once per process and kept in a static cache.

Two independent levers each drive the false-kills to zero:

  • running the same solution at --concurrency 1, and
  • removing the process-global state — an otherwise-identical stateless codebase never misattributes, even at ~8,600 mutants / 17 min.

This looks like a per-mutant isolation problem: the mtp shared test-server is reused across concurrently-evaluated mutants, so a genuine failure produced by one mutant is attributed to another — here, an equivalent one → false "Killed". Consequences: the mutation score is inflated (equivalent mutants counted as detected) and the per-mutant survivor set is not reproducible between identical runs.

Minimal repro: https://github.com/gem4511/stryker-mtp-isolation-repro — two runnable solutions (a CSLA case and a stateless control):

solution process-global state concurrency provably-equivalent "canary" mutants reported Killed
CSLA (100 business objects) CSLA per-type rule cache default ~9–18 of 200 (varies run-to-run)
CSLA same --concurrency 1 0 of 200
stateless control (100 functions) none default 0 of 200
stateless, scaled ~8,600 mutants / 17 min none default 0 of ~1,700

Each "canary" is a covered-but-unobservable mutation. In the CSLA case it is the removal of context.AddSuccessResult(true) from a single-rule success branch — equivalent because CSLA clears that rule's prior broken result on every re-run regardless. The README includes a one-command proof (delete every canary line, dotnet test → all 300 pass), so the canaries are demonstrably unkillable and any Killed count > 0 is a false positive.

Notes that rule out the obvious alternative explanations:

  • Tests are stateless and run with [assembly: CollectionBehavior(DisableTestParallelization = true)], so this is not intra-suite test parallelism.
  • The repro sets "additional-timeout": 30000, and the misattributed mutants are reported Killed, not Timeout — so this is not the "timeouts are scored as killed" behavior.

Expected behavior

Each mutant is evaluated in isolation. A mutant that no test can detect (equivalent) must be reported Survived, deterministically, regardless of --concurrency.

Logs

The repro reproduces in ~30 s, so running it directly may be easiest — but I'm happy to attach -L logs from a run if useful; just let me know.

Desktop (please complete the following information):

  • OS: Windows 11
  • Type of project: core (net10.0)
  • Framework Version: .NET SDK 10.0.301; xunit.v3 3.2.2; Microsoft.NET.Test.Sdk 18.5.1; runner mtp
  • Stryker Version: 4.16.0

Additional context

The CSLA half of the mechanism is documented, not guessed. CSLA registers each business-object type's rules once per process and caches them in static state — Rockford Lhotka: "Validation rules are loaded once per-AppDomain per business object type. They are kept in a static cache … they are never unloaded — that is intentional" (performance). In source, BusinessRuleManager holds a static Lazy<ConcurrentDictionary<…>> keyed by Type, and BusinessBase.InitializeBusinessRules() calls AddBusinessRules() exactly once per type under an Initialized-guarded double-checked lock. So CSLA's caching is intentional and thread-safe for registration; the isolation gap appears to be on the runner side — a shared test-server reused across concurrently-evaluated mutants. (Consistent with mtp not yet doing per-test coverage, i.e. the server being heavily shared.)

The precise leak across the shared server is the one inferred step; the CSLA static cache and the two levers (stateless → 0, --concurrency 1 → 0) are directly demonstrated in the repro. Happy to run any diagnostic you suggest.

Possible mitigation: per-mutant process isolation (avoid reusing one test-server across concurrently-evaluated mutants). CSLA exposes no public API to reset its rule cache, so cache-clearing is not a clean option. --concurrency 1 restores correct results today, at a large wall-clock cost.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions