From c13aed235bdddb5e02f750acb3e9f51d2dc51d47 Mon Sep 17 00:00:00 2001 From: KellyMichels Date: Thu, 13 Aug 2026 15:42:21 -0500 Subject: [PATCH] test(coverage): run the Pester suite with code coverage tests/Invoke-Coverage.ps1 runs the suite with Pester's profiler-based coverage collector and writes both JaCoCo XML and a coverage-summary.json in the same shape vitest and jest emit, so one reader handles every tool in the fleet. CodeCoverage.Path is every *.ps1 in the repo root, including the ones no test touches. They report 0% and drag the total down, which is correct - measuring only the already-tested files answers "how well covered is the covered code". The same mistake in vitest form had evo.www reporting 93%. Baseline: 222 tests pass, 7.0% of commands (219/3,125 across 24 scripts). Pester counts commands, not statements; the collector labels it as such rather than filing it under a unit it is not. --- .gitignore | 3 ++ tests/Invoke-Coverage.ps1 | 91 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 tests/Invoke-Coverage.ps1 diff --git a/.gitignore b/.gitignore index bc7a0e2..e22a1d2 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ desktop.ini # Internal working notes (superseded by TOKEN_SAVINGS.md) md/ + +# Pester coverage output (regenerated; never committed) +coverage/ diff --git a/tests/Invoke-Coverage.ps1 b/tests/Invoke-Coverage.ps1 new file mode 100644 index 0000000..ea9c13e --- /dev/null +++ b/tests/Invoke-Coverage.ps1 @@ -0,0 +1,91 @@ +# Evomedia.net Token Savers — https://github.com/evomedia-net/evo.zscripts +# Created by Kelly Michels · dev@evomedia.net +# Licensed under the MIT License. See LICENSE. + +# Invoke-Coverage.ps1 — run the Pester suite with code coverage. +# +# .\tests\Invoke-Coverage.ps1 run everything, print the summary +# .\tests\Invoke-Coverage.ps1 -Quiet just the numbers (for tooling) +# +# Writes coverage/ : +# pester-coverage.xml JaCoCo, for anything that reads a standard format +# coverage-summary.json the same shape vitest/jest emit, so the fleet +# coverage collector reads all three identically +# +# WHY EVERY SCRIPT IS MEASURED, NOT JUST THE TESTED ONES +# ------------------------------------------------------ +# CodeCoverage.Path covers every top-level *.ps1, including the ones no test +# touches. They report 0% and drag the total down, which is correct: the +# alternative measures only the files that already have tests and answers "how +# well covered is the covered code" - a question nobody is asking. The same +# mistake in vitest form made www look 91% covered when it was 17%. +# +# Pester counts *commands*, not statements. The collector labels it accordingly; +# the two are not interchangeable and averaging them would be nonsense. + +[CmdletBinding(PositionalBinding = $false)] +param([switch]$Quiet) + +$ErrorActionPreference = "Stop" + +$RepoRoot = Split-Path -Parent $PSScriptRoot +$OutDir = Join-Path $RepoRoot "coverage" +$Jacoco = Join-Path $OutDir "pester-coverage.xml" +$Summary = Join-Path $OutDir "coverage-summary.json" + +Import-Module Pester -MinimumVersion 5.0 -ErrorAction Stop + +# Every script in the repo root. ZKiller/ZKillOnly and friends are included +# deliberately - see the header. +$targets = @(Get-ChildItem -Path $RepoRoot -Filter "*.ps1" -File | Select-Object -ExpandProperty FullName) +if (-not $targets) { Write-Error "No *.ps1 found in $RepoRoot"; exit 1 } + +if (Test-Path $OutDir) { Remove-Item $OutDir -Recurse -Force } +New-Item -ItemType Directory -Force -Path $OutDir | Out-Null + +$cfg = New-PesterConfiguration +$cfg.Run.Path = Join-Path $RepoRoot "tests" +$cfg.Run.PassThru = $true +$cfg.Output.Verbosity = if ($Quiet) { "None" } else { "Normal" } +$cfg.CodeCoverage.Enabled = $true +$cfg.CodeCoverage.Path = $targets +$cfg.CodeCoverage.OutputFormat = "JaCoCo" +$cfg.CodeCoverage.OutputPath = $Jacoco +$cfg.CodeCoverage.UseBreakpoints = $false # the profiler-based collector; far faster +$cfg.Should.ErrorAction = "Continue" + +$r = Invoke-Pester -Configuration $cfg + +$analyzed = [int]$r.CodeCoverage.CommandsAnalyzedCount +$executed = [int]$r.CodeCoverage.CommandsExecutedCount +$pct = if ($analyzed -gt 0) { [math]::Round(($executed / $analyzed) * 100, 2) } else { 0 } + +# Same shape as vitest/jest json-summary. "statements" is Pester's command +# count; the key is reused so one reader handles every tool, and the unit is +# named in the collector rather than pretended away here. +$json = [ordered]@{ + total = [ordered]@{ + statements = [ordered]@{ pct = $pct; covered = $executed; total = $analyzed } + } + tests = [ordered]@{ + passed = [int]$r.PassedCount + failed = [int]$r.FailedCount + skipped = [int]$r.SkippedCount + total = [int]$r.TotalCount + } + unit = "commands" +} | ConvertTo-Json -Depth 6 + +# WriteAllText, not Set-Content: Set-Content -Encoding utf8 on 5.1 writes a BOM, +# and a BOM makes python's json.load throw on the first character. +[IO.File]::WriteAllText($Summary, $json, (New-Object Text.UTF8Encoding $false)) + +if (-not $Quiet) { + Write-Host "" + Write-Host "Coverage: $pct% ($executed/$analyzed commands across $($targets.Count) scripts)" -ForegroundColor Cyan + Write-Host " $Jacoco" + Write-Host " $Summary" + Write-Host "" +} + +exit $(if ($r.FailedCount -gt 0) { 1 } else { 0 })