Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions src/functions/Coverage.ps1
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
function Enter-CoverageAnalysis {
function Enter-CoverageAnalysis {
[CmdletBinding()]
param (
[object[]] $CodeCoverage,
Expand Down Expand Up @@ -1228,10 +1228,13 @@ function Add-JaCoCoCounter {
})
}

function Start-TraceScript ($Breakpoints) {
# Translate the breakpoints into the coordinates the tracer records hits at. Split out of
# Start-TraceScript so a caller that already has the points can reuse them instead of doing this
# again: it walks the Ast of every analyzed file, which is the expensive part of a coverage run.
function Get-TracerPoint ($Breakpoints) {

$points = [Collections.Generic.List[Pester.Tracing.CodeCoveragePoint]]@()
foreach ($breakpoint in $breakpoints) {
foreach ($breakpoint in $Breakpoints) {
$location = $breakpoint.BreakpointLocation

$hitColumn = $location.Column
Expand All @@ -1256,7 +1259,18 @@ function Start-TraceScript ($Breakpoints) {
$points.Add([Pester.Tracing.CodeCoveragePoint]::Create($location.Script, $hitLine, $hitColumn, $location.Line, $location.Column, $breakpoint.Command))
}

$tracer = [Pester.Tracing.CodeCoverageTracer]::Create($points)
, $points
}

function Start-TraceScript ($Breakpoints, $Points) {

# Points are the already translated breakpoints. test.ps1 passes them in, so that the child
# processes it starts do not each redo the translation for the same source tree.
if ($null -eq $Points) {
$Points = Get-TracerPoint -Breakpoints $Breakpoints
}

$tracer = [Pester.Tracing.CodeCoverageTracer]::Create($Points)

# detect if profiler is imported and running and in that case just add us as a second tracer
# to not disturb the profiling session
Expand Down
38 changes: 33 additions & 5 deletions test.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,13 @@ if ($CC) {
$env:PESTER_CC_IN_CC = 1
# Tests that spawn a child process via Invoke-InNewProcess (e.g. the Output and InNewProcess
# tests) execute Pester code the parent tracer cannot see. Point those children at a shared
# drop folder and the same coverage target; each child traces itself and writes the coordinates
# it hit there, and we merge them into $measure below before the report is generated.
# drop folder and hand them the tracer points we compute below; each child traces itself and
# writes the coordinates it hit there, and we merge them into $measure below before the report
# is generated.
$ccChildDir = Join-Path ([System.IO.Path]::GetTempPath()) "pester-cc-child-$PID"
if (Test-Path $ccChildDir) { Remove-Item $ccChildDir -Recurse -Force }
$null = New-Item -ItemType Directory -Path $ccChildDir -Force
$env:PESTER_CC_CHILD_OUTPUT = $ccChildDir
$env:PESTER_CC_CHILD_TARGET = if ($Inline) { "$PSScriptRoot/bin/Pester*" } else { "$PSScriptRoot/src/*" }
$sw = [System.Diagnostics.Stopwatch]::StartNew()
$here = {}
$bp = Set-PSBreakpoint -Script $PSCommandPath -Line $here.StartPosition.StartLine -Action {}
Expand All @@ -81,8 +81,25 @@ if ($CC) {
else {
$breakpoints = & $Enter_CoverageAnalysis -CodeCoverage "$PSScriptRoot/src/*" -UseBreakpoints $false
}
$Get_TracerPoint = & (Get-Module Pester) { Get-Command Get-TracerPoint }
$points = & $Get_TracerPoint -Breakpoints $breakpoints

# Write the points out once for the children to read. Analyzing the source tree to arrive at
# this list is the expensive part of a coverage run (about 10k points, inlined or not), and
# every child used to redo it. A child only ever reports back path + 'line:column', so it needs the
# coordinates and nothing else; the command text is left out because only the parent's report
# uses it.
$ccPointsFile = Join-Path ([System.IO.Path]::GetTempPath()) "pester-cc-points-$PID.tsv"
$tab = [char] 9
$pointLines = [System.Collections.Generic.List[string]]::new($points.Count)
foreach ($point in $points) {
$pointLines.Add($point.Path + $tab + $point.Line + $tab + $point.Column + $tab + $point.BpLine + $tab + $point.BpColumn)
}
[System.IO.File]::WriteAllLines($ccPointsFile, $pointLines)
$env:PESTER_CC_CHILD_POINTS = $ccPointsFile

$Start_TraceScript = & (Get-Module Pester) { Get-Command Start-TraceScript }
$patched, $tracer = & $Start_TraceScript $breakpoints
$patched, $tracer = & $Start_TraceScript -Points $points
}

# remove pester because we will be reimporting it in multiple other places
Expand Down Expand Up @@ -232,6 +249,14 @@ if ($CC) {
}
Write-Host "Merged code coverage from $($childFiles.Count) child process run(s), marking $mergedPoints additional point(s) as hit."

# The children collect coverage without being able to report a failure, they fall back to a
# plain run so the test itself still passes. So if the plumbing between us breaks, the only
# symptom is coverage quietly dropping. Say so instead. P tests are the ones that spawn
# children, so only check when they ran.
if (-not $SkipPTests -and 0 -eq $childFiles.Count) {
throw "Code coverage from child processes is missing, expected at least one file in '$ccChildDir'. Invoke-InNewProcess in tst/PTestHelpers.psm1 did not collect it."
}

$coverageReport = & $Get_CoverageReport -CommandCoverage $breakpoints -Measure $measure
}
finally {
Expand All @@ -241,8 +266,11 @@ if ($CC) {
if ($ccChildDir -and (Test-Path $ccChildDir)) {
Remove-Item $ccChildDir -Recurse -Force -ErrorAction SilentlyContinue
}
if ($ccPointsFile -and (Test-Path $ccPointsFile)) {
Remove-Item $ccPointsFile -Force -ErrorAction SilentlyContinue
}
$env:PESTER_CC_CHILD_OUTPUT = $null
$env:PESTER_CC_CHILD_TARGET = $null
$env:PESTER_CC_CHILD_POINTS = $null
}

[xml] $jaCoCoReport = [xml] (& $Get_JaCoCoReportXml -CommandCoverage $breakpoints -TotalMilliseconds $sw.ElapsedMilliseconds -CoverageReport $coverageReport -ReportRoot $PSScriptRoot)
Expand Down
19 changes: 14 additions & 5 deletions tst/PTestHelpers.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,31 @@
# that executes here in the child process is invisible to it. When the parent asks for it
# (via PESTER_CC_CHILD_* env vars, which we inherit) we trace this child the same way and
# dump the coordinates we hit, so the parent can merge them into the single coverage report.
# The parent hands us the tracer points in a file. Deriving them means analyzing every file
# in the source tree, which took longer than the test itself, and the result is the same in
# every child, so the parent does it once for all of us.
# Note: this scriptblock is stringified and passed to the child as -Command, and on Windows
# PowerShell (legacy native argument passing) only the user ScriptBlock is quote-escaped
# below, so keep this block free of double quotes to avoid mangling the child command line.
# Coverage collection is best-effort: any failure here must fall back to a plain run so the
# test behaves exactly as without coverage.
$ccDir = $env:PESTER_CC_CHILD_OUTPUT
$ccTarget = $env:PESTER_CC_CHILD_TARGET
$ccPointsFile = $env:PESTER_CC_CHILD_POINTS
$ccTracer = $null
$ccPatched = $false
if ($ccDir -and $ccTarget) {
if ($ccDir -and $ccPointsFile -and (Test-Path $ccPointsFile)) {
try {
$pesterModule = Get-Module Pester
$enter = & $pesterModule { Get-Command Enter-CoverageAnalysis }
$start = & $pesterModule { Get-Command Start-TraceScript }
$bps = & $enter -CodeCoverage $ccTarget -UseBreakpoints $false
$ccPatched, $ccTracer = & $start $bps
$tab = [char] 9
$ccPoints = [System.Collections.Generic.List[Pester.Tracing.CodeCoveragePoint]]::new()
foreach ($pointLine in [System.IO.File]::ReadAllLines($ccPointsFile)) {
if ([string]::IsNullOrWhiteSpace($pointLine)) { continue }
$f = $pointLine.Split($tab)
# command text is empty, only the parent report uses it and the parent has its own
$ccPoints.Add([Pester.Tracing.CodeCoveragePoint]::Create($f[0], [int] $f[1], [int] $f[2], [int] $f[3], [int] $f[4], [string]::Empty))
}
$ccPatched, $ccTracer = & $start -Points $ccPoints
}
catch {
$ccTracer = $null
Expand Down
42 changes: 42 additions & 0 deletions tst/functions/Coverage.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -1441,6 +1441,48 @@ InPesterModuleScope {
}
}

Describe 'Get-TracerPoint' {
BeforeAll {
$tracerScriptPath = Join-Path -Path (Get-PSDrive TestDrive).Root -ChildPath TracerPointScript.ps1
Set-Content -Path $tracerScriptPath -Value @'
function Get-Number {
$a = 1
return $a
}
'@
$tracerBreakpoints = Enter-CoverageAnalysis -CodeCoverage $tracerScriptPath -UseBreakpoints $false
}

It 'produces the points Start-TraceScript would build itself' {
$points = Get-TracerPoint -Breakpoints $tracerBreakpoints
$points.Count | Should -Be $tracerBreakpoints.Count
$points[0].Path | Should -Be $tracerScriptPath
}

It 'keeps the hit coordinates through the text form test.ps1 sends to its child processes' {
# test.ps1 writes the points to a file and the children rebuild them from it, dropping
# the command text. The tracer looks up hits by path and 'line:column', so that trip has
# to leave those untouched, otherwise the children report coordinates the parent cannot
# merge and their coverage is lost without any error.
$points = Get-TracerPoint -Breakpoints $tracerBreakpoints
$tab = [char] 9
$rebuilt = [System.Collections.Generic.List[Pester.Tracing.CodeCoveragePoint]]::new()
foreach ($point in $points) {
$row = @($point.Path, $point.Line, $point.Column, $point.BpLine, $point.BpColumn) -join $tab
$f = $row.Split($tab)
$rebuilt.Add([Pester.Tracing.CodeCoveragePoint]::Create($f[0], [int] $f[1], [int] $f[2], [int] $f[3], [int] $f[4], [string]::Empty))
}

$original = [Pester.Tracing.CodeCoverageTracer]::Create($points)
$child = [Pester.Tracing.CodeCoverageTracer]::Create($rebuilt)

@($child.Hits.Keys) | Should -Be @($original.Hits.Keys)
foreach ($path in $original.Hits.Keys) {
@($child.Hits[$path].Keys) | Should -Be @($original.Hits[$path].Keys)
}
}
}

Describe 'Resolve-CodeCoverageConfiguration report root resolution (#2923)' {
# A relative ReportRoot (or its Run.RepoRoot fallback) must be captured against the
# location Invoke-Pester was called from, during configuration validation, not against
Expand Down
Loading