From 726ba79fdf5462826c7163522d24aa257ee419ed Mon Sep 17 00:00:00 2001 From: Frank Tovar Date: Sun, 8 Feb 2026 21:52:37 +0100 Subject: [PATCH] Enforce explicit test coverage thresholds in CI --- .github/workflows/ci.yml | 5 ++++- README.md | 3 ++- TESTS.md | 7 ++++++- scripts/check-coverage.ps1 | 43 ++++++++++++++++++++++++++++++++++++++ scripts/ci-local.ps1 | 8 +++++-- 5 files changed, 61 insertions(+), 5 deletions(-) create mode 100644 scripts/check-coverage.ps1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5600d54..63d1931 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,4 +40,7 @@ jobs: run: dotnet build GameList.sln --no-restore -warnaserror - name: Test - run: dotnet test GameList.Tests/GameList.Tests.csproj --no-build --verbosity normal + run: dotnet test GameList.Tests/GameList.Tests.csproj --no-build --verbosity normal --collect:"XPlat Code Coverage" + + - name: Enforce coverage thresholds + run: pwsh ./scripts/check-coverage.ps1 -MinLineRate 0.90 -MinBranchRate 0.70 diff --git a/README.md b/README.md index 473aba1..46bb622 100644 --- a/README.md +++ b/README.md @@ -60,4 +60,5 @@ GitHub Actions workflow: `.github/workflows/ci.yml` - Restores dependencies - Runs frontend lint and format checks - Builds with warnings treated as errors -- Runs `GameList.Tests` +- Runs `GameList.Tests` with coverage collection +- Enforces minimum coverage thresholds (line 90%, branch 70%) diff --git a/TESTS.md b/TESTS.md index 2383387..0c12478 100644 --- a/TESTS.md +++ b/TESTS.md @@ -94,7 +94,12 @@ stateDiagram-v2 - Security middleware tests validate response headers and rate-limiting behavior on auth/admin routes. - Frontend regression guard tests assert modal/admin JS no longer interpolate untrusted values in vulnerable patterns. +## Coverage Policy +- CI and local script enforce Cobertura thresholds from test coverage collection. +- Minimum line coverage: 90%. +- Minimum branch coverage: 70%. + ## Execution Notes - Use named test data builders for players/suggestions to keep cases small and isolated. - Reset in-memory DB per test to avoid cross-contamination; assert timestamps using time providers or approximate windows. -- Cover success + failure for every endpoint status path to reach 100% line/branch coverage. +- Cover success + failure for endpoint status paths and critical helper branches to stay above enforced thresholds. diff --git a/scripts/check-coverage.ps1 b/scripts/check-coverage.ps1 new file mode 100644 index 0000000..05d0789 --- /dev/null +++ b/scripts/check-coverage.ps1 @@ -0,0 +1,43 @@ +param( + [double]$MinLineRate = 0.90, + [double]$MinBranchRate = 0.70, + [string]$ResultsRoot = "GameList.Tests/TestResults" +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +if (-not (Test-Path $ResultsRoot)) { + throw "Coverage results folder not found: $ResultsRoot" +} + +$coverageFile = Get-ChildItem -Path $ResultsRoot -Recurse -Filter "coverage.cobertura.xml" | + Sort-Object LastWriteTimeUtc -Descending | + Select-Object -First 1 + +if ($null -eq $coverageFile) { + throw "No coverage.cobertura.xml found under $ResultsRoot" +} + +[xml]$xml = Get-Content -Path $coverageFile.FullName +$coverage = $xml.coverage + +if ($null -eq $coverage) { + throw "Coverage XML is missing root coverage node: $($coverageFile.FullName)" +} + +[double]$lineRate = [double]$coverage.'line-rate' +[double]$branchRate = [double]$coverage.'branch-rate' + +$linePercent = [Math]::Round($lineRate * 100, 2) +$branchPercent = [Math]::Round($branchRate * 100, 2) +$minLinePercent = [Math]::Round($MinLineRate * 100, 2) +$minBranchPercent = [Math]::Round($MinBranchRate * 100, 2) + +Write-Host "Coverage source: $($coverageFile.FullName)" +Write-Host ("Line coverage: {0}% (required >= {1}%)" -f $linePercent, $minLinePercent) +Write-Host ("Branch coverage: {0}% (required >= {1}%)" -f $branchPercent, $minBranchPercent) + +if ($lineRate -lt $MinLineRate -or $branchRate -lt $MinBranchRate) { + throw "Coverage thresholds failed." +} diff --git a/scripts/ci-local.ps1 b/scripts/ci-local.ps1 index 42fe9a4..5d4a90b 100644 --- a/scripts/ci-local.ps1 +++ b/scripts/ci-local.ps1 @@ -53,13 +53,17 @@ try { Invoke-Step -Name "Run tests" -Action { if ($SkipBuild) { - dotnet test GameList.Tests/GameList.Tests.csproj --verbosity normal + dotnet test GameList.Tests/GameList.Tests.csproj --verbosity normal --collect:"XPlat Code Coverage" } else { - dotnet test GameList.Tests/GameList.Tests.csproj --no-build --verbosity normal + dotnet test GameList.Tests/GameList.Tests.csproj --no-build --verbosity normal --collect:"XPlat Code Coverage" } } + Invoke-Step -Name "Enforce coverage thresholds" -Action { + pwsh ./scripts/check-coverage.ps1 -MinLineRate 0.90 -MinBranchRate 0.70 + } + Write-Host "CI checks passed." } finally {