From 09e08f7392d21fdddd65a2de802fb7376fcf68a7 Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Mon, 28 Sep 2026 22:43:53 +0200 Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=92=AC=20add=2010.1.0=20release=20not?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Synchronize the changelog with upstream coverlet 10.1.0 fixes that have been ported to this fork's narrowed MTP and core package surface. --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4ae87a34..1982c2289 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [10.1.0] - 2026-09-27 + +This release synchronizes the Codebelt.Coverlet.MTP fork with upstream coverlet 10.1.0 fixes that apply to the narrowed MTP/core package surface while preserving the fork's multi-targeted package layout. + +### Fixed + +- Honor the Microsoft Testing Platform `--config-file` argument in coverlet.MTP instead of falling back directly to legacy `coverlet.mtp.appsettings.json` discovery. +- Resolve shared-framework assemblies that are missing from `compileLibraries` by falling back to `NetCoreSharedFrameworkResolver`, preventing silently empty coverage for modules affected by newer SDK dependency context behavior. +- Add regression coverage for `ExcludeByAttribute=CompilerGeneratedAttribute` so async state-machine methods remain instrumented. + ## [10.0.1] - 2026-09-20 This is a minor release focused on the Codebelt.Coverlet.MTP fork with narrowed scope, enhanced coverage analysis, improved testing infrastructure, and build reliability improvements. @@ -74,4 +84,5 @@ This is a minor release focused on the Codebelt.Coverlet.MTP fork with narrowed - .vscode/settings.json configuration, - .devcontainer legacy configuration. +[10.1.0]: https://github.com/codebeltnet/coverlet/compare/v10.0.1...v10.1.0 [10.0.1]: https://github.com/codebeltnet/coverlet/compare/v10.0.0...v10.0.1 From caf278576ac22e6556ddeecc48088caf8a051cdd Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Mon, 28 Sep 2026 22:43:59 +0200 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=92=AC=20add=20repository=20fork=20gu?= =?UTF-8?q?idance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add AGENTS.md with upstream synchronization workflow and fork-specific constraints to guide future maintenance. Update solution file to include this guidance document. --- AGENTS.md | 33 +++++++++++++++++++++++++++++++++ coverlet.slnx | 1 + 2 files changed, 34 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..8eee83066 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,33 @@ +# Repository guidance + +This fork is intentionally narrower than `coverlet-coverage/coverlet`. Preserve the Codebelt package surface unless a task explicitly says otherwise. + +## Upstream sync workflow + +When synchronizing from `coverlet-coverage/coverlet`, keep the sync selective: + +1. Fetch upstream without tags to avoid importing upstream release tags into this fork: + + ```powershell + git fetch --no-tags https://github.com/coverlet-coverage/coverlet.git master:refs/remotes/coverlet-upstream/master + ``` + +2. Compare from the shared base: + + ```powershell + git merge-base HEAD coverlet-upstream/master + git log --oneline --left-right --cherry-pick HEAD...coverlet-upstream/master + ``` + +3. Port only changes that apply to the retained package surface: `src\coverlet.core`, `src\coverlet.MTP`, and the directly related tests. +4. Do not reintroduce upstream surfaces removed by this fork, including legacy console, collector, msbuild packages, legacy Azure Pipelines, broad upstream documentation, or `version.json`. +5. Preserve `Codebelt.Coverlet.MTP` package identity, `Coverlet.MTP` assembly identity, and the multi-targeted `netstandard2.0;net9.0;net10.0` layout. +6. Keep versioning on MinVer `v*` tags and the existing release workflow guards. Do not restore upstream Nerdbank.GitVersioning files. +7. Adapt release notes to this fork's root `CHANGELOG.md`; describe only changes that were actually ported. +8. If an upstream tag was accidentally fetched and points at an upstream commit, delete that local tag before release preparation: + + ```powershell + git tag -d v + ``` + +Before running tests, ask for explicit approval. Build-only validation is acceptable when it does not execute tests. diff --git a/coverlet.slnx b/coverlet.slnx index b4c224b34..efdf89746 100644 --- a/coverlet.slnx +++ b/coverlet.slnx @@ -4,6 +4,7 @@ + From 1ac82d6079899f49996c750a7036f0069b082bef Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Mon, 28 Sep 2026 22:44:05 +0200 Subject: [PATCH 3/5] =?UTF-8?q?=F0=9F=90=9B=20honor=20--config-file=20argu?= =?UTF-8?q?ment=20in=20MTP=20collector?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add support for the Microsoft Testing Platform --config-file command-line argument in the coverlet.MTP collector, falling back to the legacy coverlet.mtp.appsettings.json discovery only when no config file is explicitly specified. --- src/coverlet.MTP/Collector/CollectorExtension.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/coverlet.MTP/Collector/CollectorExtension.cs b/src/coverlet.MTP/Collector/CollectorExtension.cs index f42690c53..c431b4995 100644 --- a/src/coverlet.MTP/Collector/CollectorExtension.cs +++ b/src/coverlet.MTP/Collector/CollectorExtension.cs @@ -124,8 +124,18 @@ Task ITestHostProcessLifetimeHandler.BeforeTestHostProcessStartAsync(Cancellatio return Task.CompletedTask; } - // Load configuration file settings (coverlet.mtp.appsettings.json) - CoverletMTPSettings? configFileSettings = LoadConfigurationFileSettings(_testModulePath!); + CoverletMTPSettings? configFileSettings; + + if (_commandLineOptions.TryGetOptionArgumentList("--config-file", out string[]? configFilePaths)) + { + // If a config file was passed via the command-line, we'll use that. + configFileSettings = TestConfigParser.ParseFromFile(configFilePaths[0], _testModulePath!, _fileSystem); + } + else + { + // If not, we'll try loading up a legacy one (coverlet.mtp.appsettings.json) instead. + configFileSettings = LoadConfigurationFileSettings(_testModulePath!); + } // Create merged configuration: command-line options take precedence over config file. // testAssemblyName is passed so the dynamic exclude-filter logic can omit the test assembly itself. From bc23e71cdd20e55f5011df9f1fc4f06075d922f0 Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Mon, 28 Sep 2026 22:44:11 +0200 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=90=9B=20resolve=20shared-framework?= =?UTF-8?q?=20assemblies=20missing=20from=20compileLibraries?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix assembly resolution to fall back to NetCoreSharedFrameworkResolver when an assembly is completely absent from all *.deps.json compileLibraries (occurs when FrameworkReference is declared transitively in recent SDK versions). Prevents silently empty coverage reports for modules affected by this dependency context behavior. --- .../Instrumentation/CecilAssemblyResolver.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/coverlet.core/Instrumentation/CecilAssemblyResolver.cs b/src/coverlet.core/Instrumentation/CecilAssemblyResolver.cs index 8efd013b8..6df84950b 100644 --- a/src/coverlet.core/Instrumentation/CecilAssemblyResolver.cs +++ b/src/coverlet.core/Instrumentation/CecilAssemblyResolver.cs @@ -400,6 +400,29 @@ internal AssemblyDefinition TryWithCustomResolverOnDotNetCore(AssemblyNameRefere return asm.Value; } + // The assembly can be missing from every *.deps.json's compileLibraries yet still be + // physically present in a shared framework (e.g. Microsoft.AspNetCore.App). This happens + // when the FrameworkReference that pulls it in is declared transitively - on a referenced + // project/package - rather than directly on the instrumented module; recent SDKs + // (observed starting with the 10.0.4xx feature band) stop listing such assemblies in + // compileLibraries even though NetCoreSharedFrameworkResolver (already part of + // _compositeResolver, see constructor) can still find them on disk. Ask it directly by + // name before giving up. + try + { + var sharedFrameworkLibrary = new CompilationLibrary("package", name.Name, "0.0.0.0", null, [name.Name], [], false); + string sharedFrameworkPath = sharedFrameworkLibrary.ResolveReferencePaths(_compositeResolver.Value).FirstOrDefault(); + if (!string.IsNullOrEmpty(sharedFrameworkPath)) + { + _logger.LogVerbose($"'{name}' not listed in any compileLibraries, resolved via shared framework fallback: '{sharedFrameworkPath}'"); + return AssemblyDefinition.ReadAssembly(sharedFrameworkPath, new ReaderParameters() { AssemblyResolver = this }); + } + } + catch (Exception ex) + { + _logger.LogVerbose($"TryWithCustomResolverOnDotNetCore shared framework fallback exception: {ex}"); + } + throw new CecilAssemblyResolutionException($"AssemblyResolutionException for '{name}'. Try to add true to test projects or pass '/p:CopyLocalLockFileAssemblies=true' option to the 'dotnet test' command-line", new AssemblyResolutionException(name)); } } From b9ea944b6d7961278e3ca227bb94cb2ee6e3ec75 Mon Sep 17 00:00:00 2001 From: aicia-bot Date: Mon, 28 Sep 2026 22:44:18 +0200 Subject: [PATCH 5/5] =?UTF-8?q?=E2=9C=85=20add=20async=20state=20machine?= =?UTF-8?q?=20regression=20tests=20with=20exclude=20attributes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add regression tests for issue #1727 to ensure that async state-machine methods remain instrumented when ExcludeByAttribute=CompilerGeneratedAttribute is configured. Includes unit test for shared-framework assembly resolution fallback. Updates test infrastructure to support excludeAttributes parameter. --- .../Coverage/CoverageTests.AsyncAwait.cs | 44 ++++++++++++++++ .../Coverage/InstrumenterHelper.cs | 5 +- .../Instrumentation/InstrumenterTests.cs | 50 +++++++++++++++++++ 3 files changed, 97 insertions(+), 2 deletions(-) diff --git a/test/coverlet.core.coverage.tests/Coverage/CoverageTests.AsyncAwait.cs b/test/coverlet.core.coverage.tests/Coverage/CoverageTests.AsyncAwait.cs index 525962bbc..e0c450fd5 100644 --- a/test/coverlet.core.coverage.tests/Coverage/CoverageTests.AsyncAwait.cs +++ b/test/coverlet.core.coverage.tests/Coverage/CoverageTests.AsyncAwait.cs @@ -865,5 +865,49 @@ public void AsyncAwait_Issue1337_TryCatchFinallyWithRealAsyncInFinally_NoPhantom File.Delete(path); } } + + [Fact] + public void AsyncAwait_Issue_1727_ExcludeByCompilerGeneratedAttribute_StateMachineStillInstrumented() + { + // Issue #1727: with CompilerGeneratedAttribute + // (the coverlet setting commonly recommended for ASP.NET projects to silence Razor/ + // top-level-statement noise), every async method's compiler-generated state machine + // class -- which the compiler always stamps with [CompilerGenerated] -- used to be + // excluded outright, so async methods vanished from the coverage report with no error. + // Fixed by #1940 (IsCompilerGeneratedStateMachineType check in Instrumenter.IsTypeExcluded): + // state machine types now ignore CompilerGeneratedAttribute/GeneratedCodeAttribute + // specifically, while still honoring any other user-configured exclusion attribute. + // This test reruns the ConfigureAwait() scenario from the "AsyncAwait" test above with + // that exclude attribute set and expects the SAME lines covered as the unfiltered run + // (a plain, Moq-free sample is used deliberately: reusing a Moq-backed sample class, + // e.g. Issue_669_1, from a second test in the same process trips an unrelated Castle + // DynamicProxy "Invalid assembly public key" collision -- TestInstrumentationHelper.Run + // reloads a fresh copy of the test assembly per call, and Moq's proxy-type cache does + // not expect the same mocked interface to be reloaded from two different assembly + // copies within one test host process). + string path = Path.GetTempFileName(); + try + { + FunctionExecutor.Run(async (string[] pathSerialize) => + { + CoveragePrepareResult coveragePrepareResult = await TestInstrumentationHelper.Run(async instance => + { + int res = await (Task)instance.ConfigureAwait(); + }, + persistPrepareResultToFile: pathSerialize[0], + excludeAttributes: new[] { "CompilerGeneratedAttribute" }); + + return 0; + }, [path]); + + TestInstrumentationHelper.GetCoverageResult(path) + .Document("Instrumentation.AsyncAwait.cs") + .AssertLinesCovered(BuildConfiguration.Debug, (89, 1), (90, 1)); + } + finally + { + File.Delete(path); + } + } } } diff --git a/test/coverlet.core.coverage.tests/Coverage/InstrumenterHelper.cs b/test/coverlet.core.coverage.tests/Coverage/InstrumenterHelper.cs index 222f9c1d9..7bdf377f7 100644 --- a/test/coverlet.core.coverage.tests/Coverage/InstrumenterHelper.cs +++ b/test/coverlet.core.coverage.tests/Coverage/InstrumenterHelper.cs @@ -77,7 +77,8 @@ public static async Task Run(Func callM string persistPrepareResultToFile = null, bool disableRestoreModules = false, bool skipAutoProps = false, - string assemblyLocation = null) + string assemblyLocation = null, + string[] excludeAttributes = null) { if (persistPrepareResultToFile is null) { @@ -112,7 +113,7 @@ public static async Task Run(Func callM "[coverlet.*]*" }).ToArray(), ExcludedSourceFiles = Array.Empty(), - ExcludeAttributes = Array.Empty(), + ExcludeAttributes = excludeAttributes ?? Array.Empty(), IncludeTestAssembly = true, SingleHit = false, MergeWith = string.Empty, diff --git a/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs b/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs index 711090b0a..c94a31a24 100644 --- a/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs +++ b/test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs @@ -809,6 +809,56 @@ public void TestInstrument_NetstandardAwareAssemblyResolver_SiblingRuntimeConfig } } + [Fact] + public void TestInstrument_NetstandardAwareAssemblyResolver_MissingFromCompileLibrariesFallsBackToSharedFramework() + { + // Regression test for https://github.com/coverlet-coverage/coverlet/issues/2026: an assembly + // can be completely absent from every *.deps.json's compileLibraries (e.g. when the + // FrameworkReference that pulls it in is declared transitively - on a referenced + // project/package - rather than directly on the instrumented module; recent SDKs, observed + // starting with the 10.0.4xx feature band, stop listing such assemblies there) yet still be + // physically present in a shared framework directory (e.g. Microsoft.AspNetCore.App). + // Before the fix, TryWithCustomResolverOnDotNetCore gave up the moment the compileLibraries + // lookup missed, without ever asking NetCoreSharedFrameworkResolver directly - even though + // that resolver (already part of _compositeResolver, see constructor) could find it on disk. + string tempDirectory = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + Directory.CreateDirectory(tempDirectory); + try + { + // No *.deps.json is written here on purpose: TryWithCustomResolverOnDotNetCore's + // Directory.GetFiles(..., "*.deps.json") scan then finds nothing, so its "libraries" + // dictionary stays empty and the initial lookup misses - exactly the state a caller sees + // when their assembly isn't listed in any compileLibraries at all. + string modulePath = Path.Combine(tempDirectory, "module.without.deps.json.dll"); + + string runtimeVersion = new DirectoryInfo(Path.GetDirectoryName(typeof(object).Assembly.Location)!).Name; + string runtimeConfigFile = Path.Combine(tempDirectory, "testhost.runtimeconfig.json"); + File.WriteAllText(runtimeConfigFile, + "{\n" + + " \"runtimeOptions\": {\n" + + " \"tfm\": \"net8.0\",\n" + + " \"framework\": {\n" + + " \"name\": \"Microsoft.NETCore.App\",\n" + + $" \"version\": \"{runtimeVersion}\"\n" + + " }\n" + + " }\n" + + "}\n"); + + var netstandardResolver = new NetstandardAwareAssemblyResolver(modulePath, _mockLogger.Object); + AssemblyName textJsonAssembly = typeof(System.Text.Json.JsonSerializer).Assembly.GetName(); + + AssemblyDefinition asm = netstandardResolver.TryWithCustomResolverOnDotNetCore( + new AssemblyNameReference(textJsonAssembly.Name, textJsonAssembly.Version)); + + Assert.NotNull(asm); + Assert.Equal(textJsonAssembly.Name, asm.Name.Name); + } + finally + { + Directory.Delete(tempDirectory, true); + } + } + [Fact] public void TestReachabilityHelper() {