Fix silent zero coverage on .NET Framework since 8.0.0 - #1985
Merged
Bertk merged 1 commit intoJul 15, 2026
Merged
Conversation
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
Contributor
Author
|
@dotnet-policy-service agree |
Bertk
approved these changes
Jul 15, 2026
Collaborator
|
Thank you for the PR. 👍 |
Contributor
There was a problem hiding this comment.
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.templateproject that compilesModuleTrackerTemplatefornetstandard2.0(linked source) so the resulting IL avoids net6+ only types likeDefaultInterpolatedStringHandler. - Embed the
coverlet.template.dlloutput intocoverlet.coreand updateInstrumenterto 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. |
This was referenced Sep 27, 2026
This was referenced Sep 30, 2026
Open
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1984.
Problem
Instrumenter.AddCustomModuleTrackerToModulecopies theModuleTrackerTemplateIL from therunning coverlet.core assembly (
typeof(ModuleTrackerTemplate).Assembly.Location). Since 8.0.0coverlet.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(theAppDomain.ProcessExit/DomainUnloadflush handler) fails to JIT at process exit. The CLRswallows 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.consoleandcoverlet.collector(both run the net8.0+ core).coverlet.msbuildis unaffected because it runs the netstandard2.0 core, where the sameinterpolations lower to
string.Format.Fix
Add a small
coverlet.templateproject that compilesModuleTrackerTemplateto 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
Regression test — new
TestInstrument_NetFrameworkAssembly_TrackerDoesNotReferenceInterpolatedStringHandler(
[Trait("Category", "NetFramework")]) instruments the net-framework sample and asserts the injectedtracker references no
DefaultInterpolatedStringHandler. It fails onmasterand passes with thischange. (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.)
No regressions — full
InstrumenterTestssuite green (55/55).End-to-end — built
coverlet.consolefrom this branch and ran it over a stock SDK-style net48library + 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):A minimal standalone reproduction is attached to #1984.