diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index 10e0edb8b..01c73e172 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -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 @@ -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 @@ -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 @@ -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: diff --git a/CHANGELOG.md b/CHANGELOG.md index 1982c2289..3aa74aa89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. @@ -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 diff --git a/Directory.Packages.props b/Directory.Packages.props index 8fae233d5..cfd9a4250 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -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. --> - 10.0.12 - 9.0.20 + 10.0.13 + 9.0.21 $(MicrosoftNet10RuntimeVersion) $(MicrosoftNet9RuntimeVersion) 18.10.1 diff --git a/src/coverlet.core/Instrumentation/ModuleTrackerTemplate.cs b/src/coverlet.core/Instrumentation/ModuleTrackerTemplate.cs index dd9ae76f3..403999551 100644 --- a/src/coverlet.core/Instrumentation/ModuleTrackerTemplate.cs +++ b/src/coverlet.core/Instrumentation/ModuleTrackerTemplate.cs @@ -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(); diff --git a/src/coverlet.core/coverlet.core.csproj b/src/coverlet.core/coverlet.core.csproj index 20b2b601c..8522f42c8 100644 --- a/src/coverlet.core/coverlet.core.csproj +++ b/src/coverlet.core/coverlet.core.csproj @@ -30,7 +30,7 @@ - + diff --git a/test/coverlet.MTP.validation.tests/PackageLayoutTests.cs b/test/coverlet.MTP.validation.tests/PackageLayoutTests.cs index 9a7ec4d92..6601a9ae5 100644 --- a/test/coverlet.MTP.validation.tests/PackageLayoutTests.cs +++ b/test/coverlet.MTP.validation.tests/PackageLayoutTests.cs @@ -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); } diff --git a/test/coverlet.MTP.validation.tests/TestInfrastructure/MtpPackageVersions.cs b/test/coverlet.MTP.validation.tests/TestInfrastructure/MtpPackageVersions.cs index 0210cf1d8..c24cab9d3 100644 --- a/test/coverlet.MTP.validation.tests/TestInfrastructure/MtpPackageVersions.cs +++ b/test/coverlet.MTP.validation.tests/TestInfrastructure/MtpPackageVersions.cs @@ -7,12 +7,12 @@ namespace Coverlet.MTP.validation.tests; /// /// Reads package versions from Directory.Packages.props 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. /// 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); /// Gets the value of MicrosoftTestingPlatformVersion from Directory.Packages.props. @@ -21,7 +21,13 @@ internal static class MtpPackageVersions /// Gets the value of XunitV3Version from Directory.Packages.props. internal static string XunitV3 => s_versions.Value.XunitV3; - private static (string MicrosoftTestingPlatform, string XunitV3) ReadVersionsFromProps() + /// Gets the value of MicrosoftNet9RuntimeVersion from Directory.Packages.props. + internal static string MicrosoftNet9Runtime => s_versions.Value.MicrosoftNet9Runtime; + + /// Gets the value of MicrosoftNet10RuntimeVersion from Directory.Packages.props. + 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)) @@ -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); diff --git a/test/coverlet.MTP.validation.tests/coverlet.MTP.validation.tests.csproj b/test/coverlet.MTP.validation.tests/coverlet.MTP.validation.tests.csproj index e5c92d036..c0b6cec7e 100644 --- a/test/coverlet.MTP.validation.tests/coverlet.MTP.validation.tests.csproj +++ b/test/coverlet.MTP.validation.tests/coverlet.MTP.validation.tests.csproj @@ -16,6 +16,7 @@ + diff --git a/test/coverlet.core.coverage.tests/Instrumentation/ModuleTrackerTemplateTests.cs b/test/coverlet.core.coverage.tests/Instrumentation/ModuleTrackerTemplateTests.cs index f3ad5b269..3f9d3ee4b 100644 --- a/test/coverlet.core.coverage.tests/Instrumentation/ModuleTrackerTemplateTests.cs +++ b/test/coverlet.core.coverage.tests/Instrumentation/ModuleTrackerTemplateTests.cs @@ -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]; @@ -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() {