From a8032f80589c1623ae44effaf5783938eacb466b Mon Sep 17 00:00:00 2001 From: Trent Blackburn Date: Sat, 22 Aug 2026 12:03:49 -0400 Subject: [PATCH] fix: Report real code coverage percentages in Test-PSBuildPester Each JaCoCo counter percentage was computed as [Math]::Truncate([int]$_.covered / $total) and [Math]::Truncate collapses any fraction to 0, so the value was 0 for every coverage level below exactly 100%. Two consequences fell out of the same number: the printed coverage report always read 0.00% (or 100.00%), and the CodeCoverageThreshold comparison failed the build for any nonzero threshold unless coverage was exactly 100% -- coverage gating could not be used at all. The ratio is now kept as a fraction between 0 and 1, matching what the {2:p} format string and the documented ".90 = 90%" threshold expect. It is deliberately not rounded, so a threshold is never met by a value that only rounds up to it. The change is strictly more permissive: a build that passed with a coverage threshold set still passes. Tests bracket the reported value from both sides against a fixture with partial coverage -- a threshold below it must pass, one above it must fail -- which pins the value to a fraction and catches both a truncated 0 and a 0-to-100 scale. The subprocess helper now captures the command's output instead of letting the report lines interleave with the job result object. Closes #138 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_018TJfFJGtUJY5CFu8MRRMYt --- CHANGELOG.md | 9 +++++ PowerShellBuild/Public/Test-PSBuildPester.ps1 | 7 +++- tests/Test-PSBuildPester.tests.ps1 | 40 ++++++++++++++++++- 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd8089f..ad4a6af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,15 @@ and this project adheres to [Semantic Versioning](http://semver.org/). two Pester versions were installed side by side. When no Pester is loaded, the newest installed version (5.0.0 minimum) is imported as before, and a loaded Pester older than 5.0.0 now produces a clear error. +- [**#138**](https://github.com/psake/PowerShellBuild/issues/138) + `Test-PSBuildPester` now reports real code coverage percentages and compares + them against `CodeCoverageThreshold` correctly. Each percentage was passed + through `[Math]::Truncate`, which collapses any fraction to zero, so the + coverage report always printed `0.00%` (or `100.00%`) and every threshold + above zero failed the build unless coverage was exactly 100%. Consumers who + set `$PSBPreference.Test.CodeCoverage.Threshold` could not use coverage + gating at all. The comparison is now strictly more permissive than before, + so a build that passed with a coverage threshold set still passes. ## [0.8.2] 2026-07-08 diff --git a/PowerShellBuild/Public/Test-PSBuildPester.ps1 b/PowerShellBuild/Public/Test-PSBuildPester.ps1 index 156d837..5a3b293 100644 --- a/PowerShellBuild/Public/Test-PSBuildPester.ps1 +++ b/PowerShellBuild/Public/Test-PSBuildPester.ps1 @@ -124,7 +124,12 @@ function Test-PSBuildPester { [xml]$testCoverage = Get-Content $CodeCoverageOutputFile $ccReport = $testCoverage.report.counter.ForEach({ $total = [int]$_.missed + [int]$_.covered - $percent = [Math]::Truncate([int]$_.covered / $total) + # Keep the ratio as a fraction between 0 and 1. [Math]::Truncate collapsed + # every partial result to 0, which both printed 0.00% and failed the + # CodeCoverageThreshold comparison below for any coverage under 100%. + # The value is deliberately not rounded: rounding 0.7996 up to 0.80 would + # pass a 0.80 threshold that the real coverage does not meet. + $percent = [int]$_.covered / $total [PSCustomObject]@{ name = $textInfo.ToTitleCase($_.Type.ToLower()) percent = $percent diff --git a/tests/Test-PSBuildPester.tests.ps1 b/tests/Test-PSBuildPester.tests.ps1 index e3cc9f7..43a3c8a 100644 --- a/tests/Test-PSBuildPester.tests.ps1 +++ b/tests/Test-PSBuildPester.tests.ps1 @@ -61,8 +61,12 @@ Describe 'Test-PSBuildPester' { $threw = $false $errorMessage = $null + # Capture the command's output rather than letting it fall through to the job's + # output stream, where the coverage report lines would be interleaved with the + # result object below. + $commandOutput = @() try { - Test-PSBuildPester @testPSBuildPesterParameters + $commandOutput = @(Test-PSBuildPester @testPSBuildPesterParameters) } catch { $threw = $true $errorMessage = $_.Exception.Message @@ -71,6 +75,7 @@ Describe 'Test-PSBuildPester' { [PSCustomObject]@{ Threw = $threw ErrorMessage = $errorMessage + Output = $commandOutput LoadedPesterVersions = @((Get-Module -Name 'Pester').Version.ToString()) } } -ArgumentList $InnerPesterVersion, $script:builtModulePath, $Path, $AdditionalParameters @@ -220,6 +225,39 @@ Describe 'Coverage target' { [xml]$coverageReport = Get-Content -Path $coverageOutputPath -Raw $coverageReport.report | Should -Not -BeNullOrEmpty } + + # The coverage scenario exercises only Get-Widget while measuring both public fixture + # functions, so measured coverage is always partial: comfortably above 1% and well + # below 99%. Asserting from both sides pins the reported value to a fraction between + # 0 and 1 -- a truncated 0 fails the first test, and a 0-to-100 scale fails the second. + It 'passes when measured coverage is above the code coverage threshold' { + # Regression: #138 + $coverageOutputPath = Join-Path -Path $script:outputPath -ChildPath "coverage-above-threshold-$script:innerVersion.xml" + $additionalParameters = @{ + CodeCoverage = $true + CodeCoverageFiles = @(Join-Path -Path $script:fixturePath -ChildPath 'Public/*.ps1') + CodeCoverageOutputFile = $coverageOutputPath + CodeCoverageThreshold = 0.01 + } + $result = Invoke-TestPSBuildPesterJob -InnerPesterVersion $script:innerVersion -Path $script:coveragePath -AdditionalParameters $additionalParameters + + $result.Threw | Should -BeFalse + } + + It 'fails when measured coverage is below the code coverage threshold' { + # Regression: #138 + $coverageOutputPath = Join-Path -Path $script:outputPath -ChildPath "coverage-below-threshold-$script:innerVersion.xml" + $additionalParameters = @{ + CodeCoverage = $true + CodeCoverageFiles = @(Join-Path -Path $script:fixturePath -ChildPath 'Public/*.ps1') + CodeCoverageOutputFile = $coverageOutputPath + CodeCoverageThreshold = 0.99 + } + $result = Invoke-TestPSBuildPesterJob -InnerPesterVersion $script:innerVersion -Path $script:coveragePath -AdditionalParameters $additionalParameters + + $result.Threw | Should -BeTrue + $result.ErrorMessage | Should -Match 'less than the threshold' + } } # BeforeDiscovery variables are not visible during the run phase, so the discovered version