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
18 changes: 18 additions & 0 deletions src/functions/Pester.Parallel.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,24 @@ function Invoke-TestInParallel {
# Keep only well-formed worker results (defensive against stray pipeline output).
$results = @($results | & $SafeCommands['Where-Object'] { $_ -is [System.Management.Automation.PSCustomObject] -and $null -ne $_.PSObject.Properties['Containers'] })

# The filter above cannot tell stray output from a worker that died before it returned its
# result object, so on its own it would drop a whole test file and let the run report success
# with fewer files than it was given. Losing results silently is worse than failing, so compare
# what came back against what was sent and name the files that went missing. The worker's own
# error was already surfaced by Invoke-InRunspacePool, this says which file it cost us.
if ($results.Count -ne $work.Count) {
$returnedPaths = @{}
foreach ($r in $results) {
if ($null -ne $r.PSObject.Properties['Path']) { $returnedPaths[$r.Path] = $true }
}

$missing = @(foreach ($w in $work) {
if (-not $returnedPaths.ContainsKey($w.Path)) { $w.Path }
})

throw "Parallel run lost the results of $($missing.Count) of $($work.Count) file(s), the worker(s) running them did not return a result. See the errors above for why. Lost: $($missing -join ', ')"
}

# Restore the original discovery order so replay and the merged run are deterministic
# regardless of which worker finished first.
$order = @{}
Expand Down
51 changes: 51 additions & 0 deletions tst/Pester.RSpec.Parallel.ts.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,57 @@ Describe 'Second' {
}
}

b "Lost worker results" {
t "fails the run instead of returning fewer files than it was given" {
# A worker that dies before returning its result object leaves nothing for the
# well-formed-result filter to keep, so without the count check the run would report
# success for the files that did come back and never mention the one that did not.
# Losing a test file silently is worse than failing, so this must throw and name it.
$folder = Join-Path ([IO.Path]::GetTempPath()) ([Guid]::NewGuid().Guid)
$null = New-Item -ItemType Directory -Path $folder -Force
foreach ($name in 'A', 'B', 'C') {
Set-Content -Path (Join-Path $folder "$name.Tests.ps1") -Value "Describe '$name' { It 'i' { 1 | Should -Be 1 } }"
}

try {
# Stand in for a worker that died: hand Invoke-TestInParallel a runspace-pool
# runner that drops B's result on the floor and returns the other two.
$err = & (Get-Module Pester) {
param ($Root)

$original = ${function:Invoke-InRunspacePool}
${function:Invoke-InRunspacePool} = {
param ($InputObject, $ScriptBlock, $ThrottleLimit, $ItemParameterName = 'item', $Parameters = @{})
foreach ($i in $InputObject) {
if ($i.Path -like '*B.Tests.ps1') { continue }
[PSCustomObject]@{ Path = $i.Path; Containers = @(); Tape = @(); Coverage = $null }
}
}

try {
$c = [PesterConfiguration]::Default
$c.Run.Path = $Root
$c.Run.Parallel = $true
$c.Output.Verbosity = 'None'
$containers = @(Find-File -Path $Root -Extension '.Tests.ps1' | ForEach-Object { New-BlockContainerObject -File $_ })

try {
$null = Invoke-TestInParallel -BlockContainer $containers -Configuration $c
$null
}
catch { $_.Exception.Message }
}
finally { ${function:Invoke-InRunspacePool} = $original }
} $folder

$err | Verify-NotNull
$err | Verify-Like '*lost the results of 1 of 3 file(s)*'
$err | Verify-Like '*B.Tests.ps1*'
}
finally { Remove-Item -Path $folder -Recurse -Force }
}
}

b "Invoke-InRunspacePool" {
# The parallelism primitive Run.Parallel is built on. It replaces ForEach-Object -Parallel,
# which does not exist on Windows PowerShell 5.1, so these run on both editions and are the
Expand Down
Loading