Microsoft.Testing.Extensions.CodeCoverage: line hits missing from cobertura output for passing tests when test process spawns a child dotnet process via Process.Start in Azure Dev Ops Pipeline
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 5/5
- Tiempo estimado
- Más de una semana
- Aptitud para principiantes
- 35/100
- Tipo de issue
- Error
- Claridad
- Bastante claro
- Estado de actividad
- Tranquilo
- Stack tecnológico
- azure, csharp
- Área
- devtools, testing-qa
Línea de trabajo
Comienza con el esquema de reproducción mínima y ejecuta el comando dotnet test indicado usando Microsoft.Testing.Extensions.CodeCoverage 18.6.2, comparando la salida de Cobertura con y sin el proceso hijo de Process.Start. Investiga la herencia del entorno del profiler y la salida de cobertura informada; se considera terminado cuando los tests que pasan conservan los aciertos de línea para clases no relacionadas sin requerir la solución alternativa de un proyecto separado.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
Environment
- OS: Ubuntu 24.04.4 LTS (Linux CI agent), Docker container based on mcr.microsoft.com/dotnet/sdk:10.0-noble
- CPU/RAM: 12-core Ryzen, 32 GB RAM
- .NET SDK: 10.0.203 (also reproduces with .NET 9 SDK side-by-side installed)
- Test framework: xUnit v3 via xunit.v3.mtp-v2
- MTP coverage extension: Microsoft.Testing.Extensions.CodeCoverage 18.6.2
- Test runner: dotnet test ... --coverage --coverage-output-format cobertura --coverage-output
coverage.cobertura.xml --coverage-settings .runsettings - Reproduces both via Azure DevOps DotNetCoreCLI@2 task and raw pwsh dotnet test invocation (verified — task
wrapper is not the cause).
Summary (revised)
A test class containing [Fact]s that spawn another dotnet .dll as a subprocess via
Process.Start is correlated with the parent test process's published cobertura output dropping line hits for many
classes that the same tests still pass against. The drop pattern is binary at the class level, not gradient at
the method level.
Concretely, on a single Linux CI run reproducing the issue (Microsoft.Testing.Extensions.CodeCoverage 18.6.2,
.NET 10.0.203, xUnit v3 via xunit.v3.mtp-v2), 73 classes that were ≥99% covered in the prior run dropped to ≤9%
in the current run, distributed:
┌───────────────────────────┬────────────────┐
│ Post-drop coverage bucket │ Class count │
├───────────────────────────┼────────────────┤
│ 0–9% │ 73 │
├───────────────────────────┼────────────────┤
│ 10–19% │ 12 │
├───────────────────────────┼────────────────┤
│ 20–29% │ 4 │
├───────────────────────────┼────────────────┤
│ 30–39% │ 0 │
├───────────────────────────┼────────────────┤
│ 40–89% │ 14 (scattered) │
├───────────────────────────┼────────────────┤
│ 90–99% │ 3 │
└───────────────────────────┴────────────────┘
51 of the 73 classes went from ≥99% to exactly 0% — every line uncovered, no partial coverage. Test results are
unchanged: ≥99% of tests passed, including tests known to exercise the now-zero classes.
The bimodal distribution (most affected classes either keep full coverage or are fully zeroed, with a small
intermediate tail) is inconsistent with a per-method hit-count race, which would produce a gradient. It is
consistent with per-class instrumentation registration being lost at JIT time. Smaller classes (most of the
zeroed set are 1–30 coverable lines) JIT in a single brief window; if the perturbation overlaps that window,
every method on the class misses instrumentation. Larger classes (e.g., one we examined has 392 coverable lines
across many methods) JIT method-by-method over a longer interval and end up with partial coverage — some methods
registered, others not.
Cobertura lines-valid, method counts, and per-class metadata (sequence points, complexity) are identical between
healthy and broken runs. Only lines-covered differs. PDB-driven instrumentation enumeration is fine; runtime hit
recording is what fails.
Effects observed on real CI runs:
- Overall coverage drops from a stable baseline of ~89% to ~73–78%.
- Specific classes that are well-covered by tests that pass show 0% line coverage in the published Cobertura
output. - Cobertura lines-valid / method counts are identical to a healthy run; only lines-covered differs. Per-class
metadata (sequence points, complexity) is fully present, so PDB scanning/instrumentation enumeration is fine —
only runtime hit recording is dropped. - Test results are clean (>99% passed). Tests covering the affected classes did execute and assert successfully —
they just didn't get credit.
Minimal Repro Sketch
public sealed class SubprocessSmokeFixture
{
public string ChildDllPath { get; } = /* path to any built net10.0 console app dll */;
public async Task<int> RunAsync(params string[] args)
{
var psi = new ProcessStartInfo("dotnet")
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true,
};
psi.ArgumentList.Add(ChildDllPath);
foreach (var a in args) psi.ArgumentList.Add(a);
using var p = new Process { StartInfo = psi };
p.Start();
await p.WaitForExitAsync();
return p.ExitCode;
}
}
public sealed class SmokeTests : IClassFixture
{
private readonly SubprocessSmokeFixture _spawner;
public SmokeTests(SubprocessSmokeFixture s) => _spawner = s;
[Fact] public async Task RunsChildSubprocess() => Assert.Equal(0, await _spawner.RunAsync("--version"));
[Fact] public async Task RunsChildSubprocessAgain() => Assert.Equal(0, await _spawner.RunAsync("--help"));
}
Add a few hundred unrelated tests in the same project that exercise unrelated production code. Run with dotnet
test --coverage. The unrelated production code's class-level coverage will be partially zeroed despite all tests
passing.
What We Verified
- Tests run and pass — confirmed via TRX. Test result counts and outcomes are identical between healthy and
broken runs. - Static IL not modified — captured SHA256 of every project assembly in the test bin before vs. after the test
step on Linux CI. All hashes identical. So MS Code Coverage on Linux CI uses dynamic / JIT-time profiler
instrumentation, not static rewrite. Hit recording depends on the runtime profiler — and that's what's failing. - Excluding the subprocess-spawning test classes restores coverage to baseline (deterministic).
- Child inherits profiler env vars by default. Captured the env of two PIDs in the test process tree:
- Parent test orchestrator: no profiler env vars set.
- Test host (the child of the orchestrator that loads tests): CORECLR_ENABLE_PROFILING=1,
CORECLR_PROFILER={324F817A-7420-4E6D-B3C1-143FBED6D855}, CORECLR_PROFILER_PATH_64=…/libInstrumentationEngine.so,
plus MicrosoftInstrumentationEngine_ConfigPath64_VanguardInstrumentationProfiler and
MicrosoftInstrumentationEngine_DisableCodeSignatureValidation=1.
- When the test host spawns dotnet … via Process.Start, the child inherits these by default. The child loads
the same profiler shim and instrumentation method as the parent, contending for shared bin-dir state
(runtimes/linux-x64/native/Cov_x64.config is the same file). - Scrubbing profiler env in child psi.Environment helps but doesn't fully fix it.
Removed in child's psi.Environment before Process.Start:
CORECLR_ENABLE_PROFILING (set to "0")
CORECLR_PROFILER
CORECLR_PROFILER_PATH / 32 / 64
DOTNET_STARTUP_HOOKS
MicrosoftInstrumentationEngine*
Microsoft_VisualStudio_TraceDataCollector*
MicrosoftCodeCoverage_*
MTPCC_*
VSTEST_* - With this scrub: regression goes from ~16pp → ~6pp. So env inheritance accounts for ~10pp of the bug; the
remaining ~6pp comes from Process.Start itself. (Likely fork()/exec() on Linux briefly perturbs profiler
thread/signal state, or threadpool callbacks for OutputDataReceived/ErrorDataReceived async readers compete with
profiler hit-recording threads. Speculation; we don't have a definitive mechanism for the residual.) - DotNetCoreCLI@2 Azure DevOps task is not the cause. Replaced with raw pwsh dotnet test, identical
reproduction. - Reproducibility: consistent on the Azure DevOps Linux agent. We could not reproduce in a manually constructed
Linux Docker container with the same SDK + workloads on a high-spec workstation — likely a timing-window artifact
rather than an environmental difference (we ran the local container only a handful of times; CI runs every
commit).
Expected Behavior
A test that spawns a dotnet child process should not affect the coverage profiler's hit recording in the parent
test host process for unrelated classes.
Workaround We Implemented
In our subprocess fixture, scrub profiler-related env from the child ProcessStartInfo before Start():
private static void ScrubProfilerEnv(ProcessStartInfo psi)
{
psi.Environment["CORECLR_ENABLE_PROFILING"] = "0";
psi.Environment.Remove("CORECLR_PROFILER");
psi.Environment.Remove("CORECLR_PROFILER_PATH");
psi.Environment.Remove("CORECLR_PROFILER_PATH_32");
psi.Environment.Remove("CORECLR_PROFILER_PATH_64");
psi.Environment.Remove("DOTNET_STARTUP_HOOKS");
string[] prefixes =
{
"MicrosoftInstrumentationEngine_",
"MicrosoftTestPlatform_",
"Microsoft_VisualStudio_TraceDataCollector_",
"MicrosoftCodeCoverage_",
"MTPCC_",
"VSTEST_"
};
var keys = psi.Environment.Keys
.Where(k => prefixes.Any(p => k.StartsWith(p, StringComparison.Ordinal)))
.ToList();
foreach (var key in keys) psi.Environment.Remove(key);
}
This recovers ~10pp of the regression. The remaining ~6pp we've worked around by moving subprocess-spawning tests
into a separate test project (separate dotnet test invocation, separate test host process), which removes them
from the same coverage-instrumented host.
- Lenguaje dominante
- C#
- Estrellas
- 125
- Forks
- 17
- Merge medio
- 1 h 15 min
- PR fusionados (30 d)
- 2
Preparar el entorno
Este proyecto no incluye contenedor de desarrollo, Dockerfile ni guía de contribución, así que la configuración corre por tu cuenta: empieza por su README y consulta nuestra guía para la primera contribución para los pasos generales.
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de microsoft/codecoverage
-
Dificultad 4/5 3-5 días Aptitud para principiantes 45/100
microsoft/codecoverage#253 ·
-
Dificultad 3/5 1-2 días Aptitud para principiantes 65/100
microsoft/codecoverage#252 ·
-
Dificultad 4/5 3-5 días Aptitud para principiantes 52/100
microsoft/codecoverage#251 ·
-
[BUG] Intermittent Windows 0xC0000005 crashes in MTP test hosts with CodeCoverage on .NET 10Abierto
Dificultad 4/5 3-5 días Aptitud para principiantes 35/100
microsoft/codecoverage#249 · 1 comentario · 1 reacción ·
-
False positive branch coverageAbierto
Dificultad 3/5 1-2 días Aptitud para principiantes 66/100
microsoft/codecoverage#218 ·
Todos los issues de microsoft/codecoverage
Issues similares
-
bug component/other
Dificultad 2/5 1-3 horas Aptitud para principiantes 73/100
umbraco/Umbraco.AI#511 ·
Los mantenedores suelen responder en 1 día
-
sev:L
Dificultad 1/5 Menos de una hora Aptitud para principiantes 72/100
Systemorph/MeshWeaver#6233 ·
Los mantenedores suelen responder en 1 día
-
[Bug]:Abiertobug needs response
Dificultad 2/5 1-3 horas Aptitud para principiantes 66/100
Adyen/adyen-dotnet-api-library#1874 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 66/100
CommunityToolkit/Aspire#2231 ·
Los mantenedores suelen responder en 1 día
-
BUG: `ExecutablePluginV2` is not set up correctly and FL uses `ExecutablePlugin` (v1) insteadAbiertobug
Dificultad 1/5 Menos de una hora Aptitud para principiantes 90/100
Flow-Launcher/Flow.Launcher#4697 ·
Los mantenedores suelen responder en 4 días