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
8 changes: 4 additions & 4 deletions .github/workflows/ci-pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
release_version_guard:
name: validate-release-version
needs: [build]
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
timeout-minutes: 5
permissions:
contents: read
Expand Down Expand Up @@ -49,7 +49,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, windows-2025, macos-26]
os: [ubuntu-26.04, windows-2025, macos-26]
project_name: [core, core-coverage, mtp]
include:
- project_name: core
Expand Down Expand Up @@ -88,7 +88,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, windows-2025, macos-26]
os: [ubuntu-26.04, windows-2025, macos-26]
framework: [net9.0, net10.0]
permissions:
contents: read
Expand All @@ -113,7 +113,7 @@ jobs:
if: ${{ github.event_name != 'pull_request' && needs.build.result == 'success' && needs.test.result == 'success' && needs.pack.result == 'success' && needs.package_validation.result == 'success' }}
name: publish-nuget
needs: [build, test, pack, package_validation]
runs-on: ubuntu-24.04
runs-on: ubuntu-26.04
timeout-minutes: 15
environment: Production
permissions:
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ 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.1] - 2026-10-20

This is a patch release that fixes module unload mutex collisions between unrelated coverage sessions and updates the Microsoft runtime pins and the `System.Security.Cryptography.Pkcs` override used by coverlet.core, with no new capabilities.

### Changed

- Update the central Microsoft .NET 10 and .NET 9 runtime pins from 10.0.12 and 9.0.20 to 10.0.13 and 9.0.21,
- Update the `System.Security.Cryptography.Pkcs` override in coverlet.core from 8.0.1 to 8.0.2.

### Fixed

- Fix unrelated coverage sessions sharing the module unload mutex by naming it from the full hit file name instead of the extension-stripped name, with regression coverage for the legacy assembly-prefix collision.

## [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.
Expand Down Expand Up @@ -84,5 +97,6 @@ This is a minor release focused on the Codebelt.Coverlet.MTP fork with narrowed
- .vscode/settings.json configuration,
- .devcontainer legacy configuration.

[10.1.1]: https://github.com/codebeltnet/coverlet/compare/v10.1.0...v10.1.1
[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
4 changes: 2 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
versions are not uniform across packages. Test projects stay on the newest generation for every TFM because their
tooling (Microsoft.CodeAnalysis, ReportGenerator.Core) already requires the 10.0.x packages.
-->
<MicrosoftNet10RuntimeVersion>10.0.12</MicrosoftNet10RuntimeVersion>
<MicrosoftNet9RuntimeVersion>9.0.20</MicrosoftNet9RuntimeVersion>
<MicrosoftNet10RuntimeVersion>10.0.13</MicrosoftNet10RuntimeVersion>
<MicrosoftNet9RuntimeVersion>9.0.21</MicrosoftNet9RuntimeVersion>
<MicrosoftNetRuntimeVersion>$(MicrosoftNet10RuntimeVersion)</MicrosoftNetRuntimeVersion>
<MicrosoftNetRuntimeVersion Condition="'$(TargetFramework)' == 'net9.0' AND '$(IsTestProject)' != 'true'">$(MicrosoftNet9RuntimeVersion)</MicrosoftNetRuntimeVersion>
<MicrosoftNETTestSdkVersion>18.10.1</MicrosoftNETTestSdkVersion>
Expand Down
4 changes: 3 additions & 1 deletion src/coverlet.core/Instrumentation/ModuleTrackerTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,9 @@ public static void UnloadModule(object sender, EventArgs e)
// The same module can be unloaded concurrently (different AppDomains, or ProcessExit racing an
// in-proc-collector call). A global mutex serialises access; FlushHitFile is cleared inside the
// lock so a waiting caller sees the updated value and skips a redundant write.
using var mutex = new Mutex(true, Path.GetFileNameWithoutExtension(HitsFilePath) + "_Mutex", out bool createdNew);
// Hit files have no extension. Preserve the full name, including the session GUID,
// so unrelated modules and runtimes never share a mutex through an assembly-name prefix.
using var mutex = new Mutex(true, Path.GetFileName(HitsFilePath) + "_Mutex", out bool createdNew);
if (!createdNew)
{
mutex.WaitOne();
Expand Down
2 changes: 1 addition & 1 deletion src/coverlet.core/coverlet.core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
<PackageReference Include="Microsoft.Extensions.FileSystemGlobbing" VersionOverride="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyModel" VersionOverride="8.0.2" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" VersionOverride="8.0.1" />
<PackageReference Include="System.Security.Cryptography.Pkcs" VersionOverride="8.0.1" />
<PackageReference Include="System.Security.Cryptography.Pkcs" VersionOverride="8.0.2" />
<PackageReference Include="System.Text.Json" VersionOverride="8.0.6" />
</ItemGroup>

Expand Down
12 changes: 6 additions & 6 deletions test/coverlet.MTP.validation.tests/PackageLayoutTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,14 @@ public void NuspecContainsFrameworkAlignedDependencyGroups()
AssertDependency(dependencyGroups["netstandard2.0"], "System.Buffers", "4.6.1");
AssertDependency(dependencyGroups["netstandard2.0"], "System.Memory", "4.6.3");

AssertDependency(dependencyGroups["net9.0"], "Microsoft.Extensions.Configuration", "9.0.20");
AssertDependency(dependencyGroups["net9.0"], "Microsoft.Extensions.Configuration.Json", "9.0.20");
AssertDependency(dependencyGroups["net9.0"], "Microsoft.Extensions.DependencyInjection", "9.0.20");
AssertDependency(dependencyGroups["net9.0"], "Microsoft.Extensions.Configuration", MtpPackageVersions.MicrosoftNet9Runtime);
AssertDependency(dependencyGroups["net9.0"], "Microsoft.Extensions.Configuration.Json", MtpPackageVersions.MicrosoftNet9Runtime);
AssertDependency(dependencyGroups["net9.0"], "Microsoft.Extensions.DependencyInjection", MtpPackageVersions.MicrosoftNet9Runtime);
AssertDependency(dependencyGroups["net9.0"], "Microsoft.Testing.Platform", MtpPackageVersions.MicrosoftTestingPlatform);

AssertDependency(dependencyGroups["net10.0"], "Microsoft.Extensions.Configuration", "10.0.12");
AssertDependency(dependencyGroups["net10.0"], "Microsoft.Extensions.Configuration.Json", "10.0.12");
AssertDependency(dependencyGroups["net10.0"], "Microsoft.Extensions.DependencyInjection", "10.0.12");
AssertDependency(dependencyGroups["net10.0"], "Microsoft.Extensions.Configuration", MtpPackageVersions.MicrosoftNet10Runtime);
AssertDependency(dependencyGroups["net10.0"], "Microsoft.Extensions.Configuration.Json", MtpPackageVersions.MicrosoftNet10Runtime);
AssertDependency(dependencyGroups["net10.0"], "Microsoft.Extensions.DependencyInjection", MtpPackageVersions.MicrosoftNet10Runtime);
AssertDependency(dependencyGroups["net10.0"], "Microsoft.Testing.Platform", MtpPackageVersions.MicrosoftTestingPlatform);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ namespace Coverlet.MTP.validation.tests;

/// <summary>
/// Reads package versions from <c>Directory.Packages.props</c> at the repository root so that
/// generated test projects always reference the same versions as the central package-management file.
/// generated test projects and package assertions use the same versions as the central package-management file.
/// Values are resolved lazily and cached after the first read.
/// </summary>
internal static class MtpPackageVersions
{
private static readonly Lazy<(string MicrosoftTestingPlatform, string XunitV3)> s_versions =
private static readonly Lazy<(string MicrosoftTestingPlatform, string XunitV3, string MicrosoftNet9Runtime, string MicrosoftNet10Runtime)> s_versions =
new(ReadVersionsFromProps);

/// <summary>Gets the value of <c>MicrosoftTestingPlatformVersion</c> from <c>Directory.Packages.props</c>.</summary>
Expand All @@ -21,7 +21,13 @@ internal static class MtpPackageVersions
/// <summary>Gets the value of <c>XunitV3Version</c> from <c>Directory.Packages.props</c>.</summary>
internal static string XunitV3 => s_versions.Value.XunitV3;

private static (string MicrosoftTestingPlatform, string XunitV3) ReadVersionsFromProps()
/// <summary>Gets the value of <c>MicrosoftNet9RuntimeVersion</c> from <c>Directory.Packages.props</c>.</summary>
internal static string MicrosoftNet9Runtime => s_versions.Value.MicrosoftNet9Runtime;

/// <summary>Gets the value of <c>MicrosoftNet10RuntimeVersion</c> from <c>Directory.Packages.props</c>.</summary>
internal static string MicrosoftNet10Runtime => s_versions.Value.MicrosoftNet10Runtime;

private static (string MicrosoftTestingPlatform, string XunitV3, string MicrosoftNet9Runtime, string MicrosoftNet10Runtime) ReadVersionsFromProps()
{
string? dir = AppContext.BaseDirectory;
while (!string.IsNullOrEmpty(dir))
Expand All @@ -44,7 +50,15 @@ private static (string MicrosoftTestingPlatform, string XunitV3) ReadVersionsFro
?? throw new InvalidOperationException(
$"Property 'XunitV3Version' not found in '{candidate}'.");

return (mtp, xunit);
string net9Runtime = GetProp("MicrosoftNet9RuntimeVersion")
?? throw new InvalidOperationException(
$"Property 'MicrosoftNet9RuntimeVersion' not found in '{candidate}'.");

string net10Runtime = GetProp("MicrosoftNet10RuntimeVersion")
?? throw new InvalidOperationException(
$"Property 'MicrosoftNet10RuntimeVersion' not found in '{candidate}'.");

return (mtp, xunit, net9Runtime, net10Runtime);
}

dir = Path.GetDirectoryName(dir);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="System.Security.Cryptography.Pkcs" Version="$(MicrosoftNetRuntimeVersion)" />
<PackageReference Include="xunit.v3.mtp-v2" Version="$(XunitV3Version)" />
<PackageReference Include="Microsoft.Testing.Platform" Version="$(MicrosoftTestingPlatformVersion)" />
<PackageReference Include="Microsoft.Testing.Platform.MSBuild" Version="$(MicrosoftTestingPlatformVersion)" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ public void MutexBlocksMultipleWriters()
{
using var ctx = new TrackerContext();
using var mutex = new Mutex(
true, Path.GetFileNameWithoutExtension(ModuleTrackerTemplate.HitsFilePath) + "_Mutex", out bool createdNew);
true, Path.GetFileName(ModuleTrackerTemplate.HitsFilePath) + "_Mutex", out bool createdNew);
Assert.True(createdNew);

ModuleTrackerTemplate.HitsArray = [0, 1, 2, 3];
Expand All @@ -249,6 +249,42 @@ public void MutexBlocksMultipleWriters()

}

[Fact]
public void CoverageSessionDoesNotShareLegacyAssemblyPrefixMutex()
{
FunctionExecutor.Run(() =>
{
using var ctx = new TrackerContext();
string assemblyPrefix = "CoverageSession" + Guid.NewGuid().ToString("N");
ModuleTrackerTemplate.HitsFilePath = Path.Combine(Path.GetTempPath(), assemblyPrefix + ".Core_" + Guid.NewGuid());
string otherSessionHitsFilePath = Path.Combine(Path.GetTempPath(), assemblyPrefix + ".Data_" + Guid.NewGuid());

// Older collectors discard the last namespace segment and session GUID.
// A writer must not share that mutex with an unrelated coverage session.
using var otherSessionMutex = new Mutex(true, Path.GetFileNameWithoutExtension(otherSessionHitsFilePath) + "_Mutex", out bool createdNew);
Assert.True(createdNew);

ModuleTrackerTemplate.HitsArray = [1, 2, 3];
var unloadTask = Task.Run(() => ModuleTrackerTemplate.UnloadModule(null, null));
bool completedWhileOtherSessionOwnedMutex;
try
{
#pragma warning disable xUnit1031 // Keep acquisition and release on the mutex-owning thread.
completedWhileOtherSessionOwnedMutex = unloadTask.Wait(TimeSpan.FromSeconds(5));
#pragma warning restore xUnit1031 // Keep acquisition and release on the mutex-owning thread.
}
finally
{
otherSessionMutex.ReleaseMutex();
}

unloadTask.GetAwaiter().GetResult();
Assert.True(completedWhileOtherSessionOwnedMutex, "An unrelated coverage session blocked the writer.");
Assert.Equal(new[] { 1, 2, 3 }, ReadHitsFile());
return s_success;
});
}

[Fact]
public void LockFileHeldDuringWriteAndReleasedAfter()
{
Expand Down
Loading