Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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<version>
```

Before running tests, ask for explicit approval. Build-only validation is acceptable when it does not execute tests.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions coverlet.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<File Path=".github/copilot-instructions.md" />
<File Path=".github/workflows/ci-pipeline.yml" />
<File Path=".gitignore" />
<File Path="AGENTS.md" />
<File Path="CONTRIBUTING.md" />
<File Path="Directory.Build.props" />
<File Path="Directory.Build.targets" />
Expand Down
14 changes: 12 additions & 2 deletions src/coverlet.MTP/Collector/CollectorExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
23 changes: 23 additions & 0 deletions src/coverlet.core/Instrumentation/CecilAssemblyResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 <PreserveCompilationContext>true</PreserveCompilationContext> to test projects </PropertyGroup> or pass '/p:CopyLocalLockFileAssemblies=true' option to the 'dotnet test' command-line", new AssemblyResolutionException(name));
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -865,5 +865,49 @@ public void AsyncAwait_Issue1337_TryCatchFinallyWithRealAsyncInFinally_NoPhantom
File.Delete(path);
}
}

[Fact]
public void AsyncAwait_Issue_1727_ExcludeByCompilerGeneratedAttribute_StateMachineStillInstrumented()
{
// Issue #1727: with <ExcludeByAttribute>CompilerGeneratedAttribute</ExcludeByAttribute>
// (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<AsyncAwait>(async instance =>
{
int res = await (Task<int>)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);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ public static async Task<CoveragePrepareResult> Run<T>(Func<dynamic, Task> callM
string persistPrepareResultToFile = null,
bool disableRestoreModules = false,
bool skipAutoProps = false,
string assemblyLocation = null)
string assemblyLocation = null,
string[] excludeAttributes = null)
{
if (persistPrepareResultToFile is null)
{
Expand Down Expand Up @@ -112,7 +113,7 @@ public static async Task<CoveragePrepareResult> Run<T>(Func<dynamic, Task> callM
"[coverlet.*]*"
}).ToArray(),
ExcludedSourceFiles = Array.Empty<string>(),
ExcludeAttributes = Array.Empty<string>(),
ExcludeAttributes = excludeAttributes ?? Array.Empty<string>(),
IncludeTestAssembly = true,
SingleHit = false,
MergeWith = string.Empty,
Expand Down
50 changes: 50 additions & 0 deletions test/coverlet.core.tests/Instrumentation/InstrumenterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
Loading