diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b7f5319..bf08416 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,27 +69,53 @@ jobs: - name: Build solution run: dotnet build ./src/NEventStore.Persistence.MongoDB.Core.sln -c Release --no-restore /p:ContinuousIntegrationBuild=True + - name: Test dependency verification guards + shell: pwsh + run: ./scripts/test-verify-driver-dependencies.ps1 + + - name: Verify production and default test restore + shell: pwsh + run: ./scripts/verify-driver-dependencies.ps1 -TestDriverVersion '3.13.0' + + - name: Pack and verify published dependency contract + if: runner.os == 'Windows' + shell: pwsh + run: | + nuget pack ./src/.nuget/NEventStore.Persistence.MongoDB.nuspec -properties "version=${{ steps.gitversion.outputs.semver }};configuration=Release" -OutputDirectory artifacts + if ($LASTEXITCODE -ne 0) { throw 'NuGet pack failed.' } + $package = Get-ChildItem artifacts -Filter '*.nupkg' | Select-Object -First 1 + ./scripts/verify-driver-dependencies.ps1 -PackagePath $package.FullName -RestoreConsumer + # Tests run on Linux because this repository needs a live MongoDB instance and # GitHub Actions service containers are only available on Linux runners. - # The matrix is therefore per target framework instead of per OS. + # Each framework/driver pair restores and builds in a separate job. test-modern-tfm-linux: - name: Test (Linux, ${{ matrix.tfm }}) + name: Test (Linux, ${{ matrix.tfm }}, Driver ${{ matrix.driver }}, MongoDB ${{ matrix.mongodb }}) needs: build runs-on: ubuntu-latest strategy: fail-fast: false matrix: - # Run each modern TFM independently so failures are isolated and easy to read. + # Pin both lanes; update the current 3.x pin in reviewed changes. + driver: ["3.0.0", "3.13.0"] + mongodb: ["7.0.45"] tfm: - net8.0 - net9.0 - net10.0 + include: + # Retain current-server coverage with the current driver only. + - tfm: net10.0 + driver: "3.13.0" + mongodb: "9.0.4" services: # The test project reads NEventStore.MongoDB from the environment and expects # a real MongoDB server. This sidecar provides that dependency for each matrix leg. mongodb: - image: mongo:9.0.4 + # MongoDB 7 supports both lanes and avoids the MongoDB 8/Linux 6.19 + # startup incompatibility: https://jira.mongodb.org/browse/SERVER-121912 + image: mongo:${{ matrix.mongodb }} ports: - 27017:27017 options: >- @@ -127,15 +153,22 @@ jobs: restore-keys: | nuget-${{ runner.os }}- - - name: Run tests for ${{ matrix.tfm }} - # Only the test project is executed here. It pulls in the production project and - # linked acceptance coverage from the NEventStore submodule. - run: dotnet test ./src/NEventStore.Persistence.MongoDB.Tests/NEventStore.Persistence.MongoDB.Core.Tests.csproj -c Release -f ${{ matrix.tfm }} --logger "trx;LogFileName=test-results-${{ matrix.tfm }}.trx" + - name: Restore selected test driver + run: dotnet restore ./src/NEventStore.Persistence.MongoDB.Tests/NEventStore.Persistence.MongoDB.Core.Tests.csproj -p:MongoDbDriverTestVersion=${{ matrix.driver }} + + - name: Verify restored driver and BSON versions + shell: pwsh + run: ./scripts/verify-driver-dependencies.ps1 -TestDriverVersion '${{ matrix.driver }}' + + - name: Run tests for ${{ matrix.tfm }} with Driver ${{ matrix.driver }} + # Both driver lanes execute the same complete acceptance and regression suite. + run: dotnet test ./src/NEventStore.Persistence.MongoDB.Tests/NEventStore.Persistence.MongoDB.Core.Tests.csproj -c Release -f ${{ matrix.tfm }} --no-restore -p:MongoDbDriverTestVersion=${{ matrix.driver }} --logger "trx;LogFileName=test-results-${{ matrix.tfm }}-${{ matrix.driver }}-${{ matrix.mongodb }}.trx" - name: Upload test results + if: ${{ always() }} uses: actions/upload-artifact@v7.0.2 with: - name: test-results-${{ matrix.tfm }} - path: "**/test-results-${{ matrix.tfm }}.trx" + name: test-results-${{ matrix.tfm }}-${{ matrix.driver }}-${{ matrix.mongodb }} + path: "**/test-results-${{ matrix.tfm }}-${{ matrix.driver }}-${{ matrix.mongodb }}.trx" if-no-files-found: error retention-days: 14 diff --git a/Changelog.md b/Changelog.md index c7c8381..f95ac5f 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,8 @@ ## Unreleased +- Run the same acceptance and regression suite with pinned MongoDB.Driver 3.0.0 and 3.13.0 test dependencies in isolated CI jobs on MongoDB 7.0.45, retaining a net10.0/current-driver lane on MongoDB 9.0.4. Verify restored Driver/BSON versions and the packed dependency contract across supported frameworks. Production dependencies and the published `[3.0.0,4.0.0)` range are unchanged; no API, schema or migration change. [#93](https://github.com/NEventStore/NEventStore.Persistence.MongoDB/issues/93) + - Guard recycle-bin checkpoint reads and deletion together in synchronous and asynchronous cleanup. Disposed engines fail before collection access, failed reads never reach deletion, and acquired cursors are disposed. Preserve async cancellation, query shape and the strict checkpoint cutoff. No public API, stored-schema or MongoDB minimum-version change; no data migration required. [#107](https://github.com/NEventStore/NEventStore.Persistence.MongoDB/issues/107) - Translate MongoDB driver failures from all nine lazy synchronous reads at cursor acquisition and subsequent batches, preserving streaming, cursor disposal, and serializer exception identity. Async reads, public signatures, stored data, CI, and the MongoDB 4.2 minimum are unchanged. [#91](https://github.com/NEventStore/NEventStore.Persistence.MongoDB/issues/91) diff --git a/README.md b/README.md index a333822..9d66b2f 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,31 @@ To build the project locally on a Windows Machine: NEventStore.MongoDB="mongodb://localhost:50002/NEventStore" ``` +### MongoDB driver coverage + +The production reference stays at MongoDB.Driver 3.0.0 and the published range stays +`[3.0.0,4.0.0)`. Only the test project accepts `MongoDbDriverTestVersion`; its default +is the pinned current stable 3.x release, 3.13.0. CI runs the same full suite for +3.0.0 and 3.13.0 on net8.0, net9.0 and net10.0 in separate jobs with MongoDB 7.0.45. One additional net10.0/3.13.0 job retains +MongoDB 9.0.4 coverage without pairing that server with the baseline driver. +MongoDB 7 also avoids the [MongoDB 8/Linux 6.19 startup incompatibility](https://jira.mongodb.org/browse/SERVER-121912). +.NET Framework 4.7.2 is compiled but not executed. This matrix does not establish +compatibility with every MongoDB server or every intervening driver release. + +Run either lane locally (do not reuse `--no-restore` or `--no-build` outputs when +switching versions): + +```powershell +dotnet test ./src/NEventStore.Persistence.MongoDB.Tests/NEventStore.Persistence.MongoDB.Core.Tests.csproj -c Release -f net8.0 -p:MongoDbDriverTestVersion=3.0.0 +dotnet test ./src/NEventStore.Persistence.MongoDB.Tests/NEventStore.Persistence.MongoDB.Core.Tests.csproj -c Release -f net8.0 -p:MongoDbDriverTestVersion=3.13.0 +``` + +When a new stable 3.x driver is released, update the test-project default and the +CI current-lane/verification pins together, then review both lanes' results. Keep +the baseline and production package contract unchanged. Do not use floating versions. +CI checks the resolved Driver and BSON packages for every target and verifies the +packed package's dependency ranges and consumer restore without publishing it. + ## Run Benchmarks (locally) - Build benchmark project: diff --git a/scripts/test-verify-driver-dependencies.ps1 b/scripts/test-verify-driver-dependencies.ps1 new file mode 100644 index 0000000..9a13e0a --- /dev/null +++ b/scripts/test-verify-driver-dependencies.ps1 @@ -0,0 +1,82 @@ +# Synthetic regression checks; no MongoDB, NuGet restore, or Pester required. +$ErrorActionPreference = 'Stop' +$verify = Join-Path $PSScriptRoot 'verify-driver-dependencies.ps1' +$temp = Join-Path ([IO.Path]::GetTempPath()) ([guid]::NewGuid().ToString()) +New-Item -ItemType Directory $temp | Out-Null +$testPath = Join-Path $temp 'tests.json' +$libraryPath = Join-Path $temp 'library.json' +$packagePath = Join-Path $temp 'fixture.nupkg' +$testTargets = @('.NETFramework,Version=v4.7.2', 'net8.0', 'net9.0', 'net10.0') +$libraryTargets = $testTargets + @('netstandard2.1', 'net6.0') +function Write-Assets($path, $targets, $version) { + $data = @{ targets = @{} } + foreach ($target in $targets) { + $data.targets[$target] = @{ + "MongoDB.Driver/$version" = @{ type = 'package' } + "MongoDB.Bson/$version" = @{ type = 'package' } + } + } + $data | ConvertTo-Json -Depth 10 | Set-Content $path +} +function Assert-Fails($label, [scriptblock]$action, $message) { + try { & $action } catch { + if ($_.Exception.Message -notlike "*$message*") { throw } + Write-Host "PASS: $label" + return + } + throw "Expected failure: $label" +} +function Check-Assets($version) { + & $verify -TestDriverVersion $version -TestAssetsPath $testPath -LibraryAssetsPath $libraryPath +} +function Write-Package($targets, $range) { + $groups = foreach ($target in $targets) { + "" + } + $xml = "NEventStore.Persistence.MongoDB1.0.0$($groups -join '')" + $zip = [IO.Compression.ZipFile]::Open($packagePath, 'Create') + try { + $writer = [IO.StreamWriter]::new($zip.CreateEntry('NEventStore.Persistence.MongoDB.nuspec').Open()) + try { $writer.Write($xml) } finally { $writer.Dispose() } + } finally { $zip.Dispose() } +} +try { + Write-Assets $libraryPath $libraryTargets '3.0.0' + foreach ($version in @('3.0.0', '3.9.0')) { + Write-Assets $testPath $testTargets $version + Check-Assets $version + } + Assert-Fails 'test downgrade' { Check-Assets '3.0.0' } 'MongoDB.Driver' + $data = Get-Content $testPath -Raw | ConvertFrom-Json -AsHashtable + $data.targets['net10.0'].Remove('MongoDB.Bson/3.9.0') + $data | ConvertTo-Json -Depth 10 | Set-Content $testPath + Assert-Fails 'missing BSON' { Check-Assets '3.9.0' } 'MongoDB.Bson' + Write-Assets $testPath @('net8.0') '3.9.0' + Assert-Fails 'missing test frameworks' { Check-Assets '3.9.0' } 'target frameworks' + Write-Assets $testPath ($testTargets + 'net8.0/linux-x64') '3.9.0' + Check-Assets '3.9.0' + $data = Get-Content $testPath -Raw | ConvertFrom-Json -AsHashtable + $data.targets['net8.0/linux-x64'].Remove('MongoDB.Bson/3.9.0') + $data.targets['net8.0/linux-x64']['MongoDB.Bson/3.0.0'] = @{ type = 'package' } + $data | ConvertTo-Json -Depth 10 | Set-Content $testPath + Assert-Fails 'BSON skew in RID target' { Check-Assets '3.9.0' } 'MongoDB.Bson' + Write-Assets $testPath $testTargets '3.9.0' + Write-Assets $libraryPath $libraryTargets '3.9.0' + Assert-Fails 'production upgraded' { Check-Assets '3.9.0' } 'MongoDB.Driver' + Write-Assets $libraryPath @() '3.0.0' + Assert-Fails 'empty production targets' { Check-Assets '3.9.0' } 'target frameworks' + Write-Package @('.NETFramework4.7.2', '.NETStandard2.1', 'net6.0', 'net8.0', 'net9.0', 'net10.0') '[3.0.0,4.0.0)' + & $verify -PackagePath $packagePath + Remove-Item $packagePath + Write-Package $libraryTargets '[3.0.0, 4.0.0)' + & $verify -PackagePath $packagePath + Remove-Item $packagePath + Write-Package $libraryTargets '[3.9.0,4.0.0)' + Assert-Fails 'raised package floor' { & $verify -PackagePath $packagePath } 'MongoDB.Driver' + Remove-Item $packagePath + Write-Package @('net8.0') '[3.0.0,4.0.0)' + Assert-Fails 'missing package frameworks' { & $verify -PackagePath $packagePath } 'target frameworks' + Write-Host 'All dependency verifier regression checks passed.' +} finally { + Remove-Item $temp -Recurse -Force +} diff --git a/scripts/verify-driver-dependencies.ps1 b/scripts/verify-driver-dependencies.ps1 new file mode 100644 index 0000000..e143b77 --- /dev/null +++ b/scripts/verify-driver-dependencies.ps1 @@ -0,0 +1,113 @@ +[CmdletBinding(DefaultParameterSetName = 'Assets')] +param( + [Parameter(Mandatory, ParameterSetName = 'Assets')] + [string]$TestDriverVersion, + [Parameter(ParameterSetName = 'Assets')] + [string]$TestAssetsPath = "$PSScriptRoot/../src/NEventStore.Persistence.MongoDB.Tests/obj/project.assets.json", + [Parameter(ParameterSetName = 'Assets')] + [string]$LibraryAssetsPath = "$PSScriptRoot/../src/NEventStore.Persistence.MongoDB/obj/project.assets.json", + [Parameter(Mandatory, ParameterSetName = 'Package')] + [string]$PackagePath, + [Parameter(ParameterSetName = 'Package')] + [switch]$RestoreConsumer +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +# Explicit compatibility contract: changing supported targets requires updating this guard. +$testFrameworks = @('net472', 'net8.0', 'net9.0', 'net10.0') +$libraryFrameworks = $testFrameworks + @('netstandard2.1', 'net6.0') + +function Normalize-Framework([string]$framework) { + $framework = $framework.Split('/')[0].ToLowerInvariant().Replace(',version=v', '').TrimStart('.') + if ($framework.StartsWith('netframework')) { return 'net' + $framework.Substring(12).Replace('.', '') } + return $framework.Replace('netcoreapp', 'net') +} + +function Assert-Frameworks($actual, $expected, $source) { + $normalized = @($actual | ForEach-Object { Normalize-Framework $_ } | Sort-Object -Unique) + if (Compare-Object $normalized ($expected | Sort-Object -Unique)) { + throw "$source target frameworks: expected $($expected -join ', '); found $($normalized -join ', ')" + } +} + +function Assert-Assets($path, $version, $frameworks) { + $assets = Get-Content -LiteralPath $path -Raw | ConvertFrom-Json -AsHashtable + if (-not $assets.targets -or $assets.targets.Count -eq 0) { throw "$path has no target frameworks" } + Assert-Frameworks @($assets.targets.Keys) $frameworks $path + foreach ($target in $assets.targets.Keys) { + foreach ($package in @('MongoDB.Driver', 'MongoDB.Bson')) { + $matches = @($assets.targets[$target].Keys | Where-Object { $_.StartsWith("$package/", [StringComparison]::OrdinalIgnoreCase) }) + if ($matches.Count -ne 1 -or $matches[0] -cne "$package/$version" -or $assets.targets[$target][$matches[0]].type -ne 'package') { + throw "$path [$target]: expected $package/$version; found $($matches -join ', ')" + } + } + } + Write-Host "Verified $path`: MongoDB.Driver and MongoDB.Bson $version in every target." +} + +if ($PSCmdlet.ParameterSetName -eq 'Assets') { + Assert-Assets $TestAssetsPath $TestDriverVersion $testFrameworks + Assert-Assets $LibraryAssetsPath '3.0.0' $libraryFrameworks +} else { + $zip = [IO.Compression.ZipFile]::OpenRead((Resolve-Path -LiteralPath $PackagePath).Path) + try { + $entries = @($zip.Entries | Where-Object { $_.FullName -like '*.nuspec' }) + if ($entries.Count -ne 1) { throw "$PackagePath must contain exactly one nuspec" } + $reader = [IO.StreamReader]::new($entries[0].Open()) + try { [xml]$nuspec = $reader.ReadToEnd() } finally { $reader.Dispose() } + if ($nuspec.package.metadata.id -ne 'NEventStore.Persistence.MongoDB') { throw 'Unexpected package identity' } + $groups = @($nuspec.package.metadata.dependencies.group) + Assert-Frameworks @($groups | ForEach-Object { $_.targetFramework }) $libraryFrameworks $PackagePath + foreach ($group in $groups) { + $drivers = @($group.dependency | Where-Object { $_.id -eq 'MongoDB.Driver' }) + if ($drivers.Count -ne 1 -or ($drivers[0].version -replace '\s', '') -ne '[3.0.0,4.0.0)') { + throw "$PackagePath [$($group.targetFramework)]: expected MongoDB.Driver [3.0.0,4.0.0)" + } + } + Write-Host "Verified $PackagePath`: MongoDB.Driver [3.0.0,4.0.0) for all supported frameworks." + } finally { $zip.Dispose() } +} + +# Restore the actual package as a consumer, isolated from repository MSBuild props +# and the global package cache so a previously published package cannot mask it. +if ($RestoreConsumer) { + $temp = Join-Path ([IO.Path]::GetTempPath()) ([guid]::NewGuid().ToString()) + New-Item -ItemType Directory $temp | Out-Null + try { + $project = Join-Path $temp 'Consumer.csproj' + $version = [Security.SecurityElement]::Escape($nuspec.package.metadata.version) + if ([string]::IsNullOrWhiteSpace($version)) { throw 'Packed nuspec has no version' } + @" + + $($libraryFrameworks -join ';') + + +"@ | Set-Content $project + $source = New-Item -ItemType Directory "$temp/feed" + Copy-Item -LiteralPath $PackagePath -Destination $source.FullName + $escapedSource = [Security.SecurityElement]::Escape($source.FullName) + $config = Join-Path $temp 'NuGet.Config' + # Exact ID mapping takes precedence over the wildcard, so the adapter + # can only come from this build, never the public feed. + @" + + + + + + + + + + + + +"@ | Set-Content $config + & dotnet restore $project --configfile $config --packages "$temp/packages" --verbosity minimal + if ($LASTEXITCODE -ne 0) { throw "Packed consumer restore failed with exit code $LASTEXITCODE" } + Assert-Assets "$temp/obj/project.assets.json" '3.0.0' $libraryFrameworks + } finally { + Remove-Item $temp -Recurse -Force + } +} diff --git a/src/NEventStore.Persistence.MongoDB.Tests/NEventStore.Persistence.MongoDB.Core.Tests.csproj b/src/NEventStore.Persistence.MongoDB.Tests/NEventStore.Persistence.MongoDB.Core.Tests.csproj index 4eee8a1..2bc145b 100644 --- a/src/NEventStore.Persistence.MongoDB.Tests/NEventStore.Persistence.MongoDB.Core.Tests.csproj +++ b/src/NEventStore.Persistence.MongoDB.Tests/NEventStore.Persistence.MongoDB.Core.Tests.csproj @@ -7,6 +7,8 @@ NEventStore.Persistence.MongoDB.Tests false true + + 3.13.0 @@ -22,7 +24,7 @@ - +