Skip to content

Fix silent zero coverage on .NET Framework since 8.0.0 - #1985

Merged
Bertk merged 1 commit into
coverlet-coverage:masterfrom
tobiwae:fix/1984-net48-tracker-template
Jul 15, 2026
Merged

Bertk merged 1 commit into
coverlet-coverage:masterfrom
tobiwae:fix/1984-net48-tracker-template

Conversation

@tobiwae

@tobiwae tobiwae commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Fixes #1984.

Problem

Instrumenter.AddCustomModuleTrackerToModule copies the ModuleTrackerTemplate IL from the
running coverlet.core assembly (typeof(ModuleTrackerTemplate).Assembly.Location). Since 8.0.0
coverlet.core targets net8.0+, so the C# compiler lowers the template's string interpolations to
DefaultInterpolatedStringHandler — a type that does not exist on .NET Framework.

When that IL is injected into a .NET Framework module, the tracker's UnloadModule (the
AppDomain.ProcessExit / DomainUnload flush handler) fails to JIT at process exit. The CLR
swallows exceptions thrown from unload handlers, so the test run passes green while the coverage
report is silently 0%
, with no error. The #1818 scope-remap doesn't save it — it points the
reference at mscorlib, where the type still doesn't exist.

Affected drivers: coverlet.console and coverlet.collector (both run the net8.0+ core).
coverlet.msbuild is unaffected because it runs the netstandard2.0 core, where the same
interpolations lower to string.Format.

Fix

Add a small coverlet.template project that compiles ModuleTrackerTemplate to netstandard2.0
(linking the existing source file — single source of truth), embed that assembly into coverlet.core,
and read the injected template IL from the embedded resource instead of from the running build. The
injected IL is then .NET Framework-safe regardless of which TFM coverlet.core runs as. No new file
ships and there is no type collision (the reference uses ReferenceOutputAssembly="false").

Verification

  1. Regression test — new TestInstrument_NetFrameworkAssembly_TrackerDoesNotReferenceInterpolatedStringHandler
    ([Trait("Category", "NetFramework")]) instruments the net-framework sample and asserts the injected
    tracker references no DefaultInterpolatedStringHandler. It fails on master and passes with this
    change. (The existing [BUG] TypeInitializationException when targeting .NET Framework #1818 test can't catch this — the scope-remap makes its "not System.Runtime"
    assertion pass while the type is still missing.)

  2. No regressions — full InstrumenterTests suite green (55/55).

  3. End-to-end — built coverlet.console from this branch and ran it over a stock SDK-style net48
    library + NUnit tests via nunit3-console, on the same sample used in [BUG] Silent zero coverage on .NET Framework 4.8 (console & collector) since 8.0.0 #1984 (Windows x64, .NET SDK 10):

    coverlet.console Tests Coverage collected
    10.0.1 (shipped) 7/7 passed 0 / 13 sequence points (0%) — silent bug
    this branch 7/7 passed 13 / 13 (100%)

A minimal standalone reproduction is attached to #1984.

The instrumenter copied the ModuleTrackerTemplate IL from the running coverlet.core build. When that build targets net8.0+, the template's string interpolations lower to DefaultInterpolatedStringHandler, a type absent on .NET Framework, so the injected UnloadModule flush handler fails to JIT at process exit and coverage is silently lost while tests still pass.

Compile the template to a dedicated netstandard2.0 assembly, embed it in coverlet.core, and read the injected IL from that resource so it stays net4x-safe on every driver. Adds a regression test asserting the injected .NET Framework tracker does not reference DefaultInterpolatedStringHandler.

Fixes #1984
@tobiwae

tobiwae commented Jul 14, 2026

Copy link
Copy Markdown
Contributor Author

@dotnet-policy-service agree

@Bertk

Bertk commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Thank you for the PR. 👍

@Bertk
Bertk merged commit 79dff97 into coverlet-coverage:master Jul 15, 2026
10 checks passed
@tobiwae
tobiwae deleted the fix/1984-net48-tracker-template branch July 25, 2026 07:54
@Bertk
Bertk requested a review from Copilot August 2, 2026 12:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a regression where .NET Framework targets could report silent 0% coverage (tests pass green, but no hits are flushed) when driven by coverlet.console or coverlet.collector, by ensuring the injected ModuleTrackerTemplate IL is sourced from a netstandard2.0-compiled template rather than the running net8.0+ coverlet.core build.

Changes:

  • Add a new coverlet.template project that compiles ModuleTrackerTemplate for netstandard2.0 (linked source) so the resulting IL avoids net6+ only types like DefaultInterpolatedStringHandler.
  • Embed the coverlet.template.dll output into coverlet.core and update Instrumenter to read the template IL from the embedded resource stream.
  • Add a regression test asserting the injected tracker in a .NET Framework module does not reference DefaultInterpolatedStringHandler, and document the fix in the changelog.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs Adds a regression test verifying the injected tracker IL does not reference DefaultInterpolatedStringHandler for .NET Framework modules.
src/coverlet.template/coverlet.template.csproj Introduces a netstandard2.0-only project that links ModuleTrackerTemplate.cs to produce a framework-safe IL template.
src/coverlet.core/Instrumentation/Instrumenter.cs Switches tracker template sourcing from on-disk assembly location to an embedded coverlet.template.dll resource stream.
src/coverlet.core/coverlet.core.csproj Embeds the coverlet.template build output as a manifest resource in coverlet.core.
Documentation/Changelog.md Adds an “Unreleased” entry describing the .NET Framework silent-zero-coverage fix.
coverlet.slnx Adds src/coverlet.template/coverlet.template.csproj to the solution.

Comment thread src/coverlet.core/Instrumentation/Instrumenter.cs
This was referenced Sep 27, 2026
This was referenced Sep 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Silent zero coverage on .NET Framework 4.8 (console & collector) since 8.0.0

3 participants