From 642a8dab11f3c7345a8a7abda99511f42b72f940 Mon Sep 17 00:00:00 2001 From: Jakub Jares Date: Mon, 29 Jun 2026 19:28:47 +0200 Subject: [PATCH 1/2] Fix parallel run total durations Parallel runs summed container durations, which overstates the run because the files overlap. Use the wall-clock duration (RunEnd - RunStart) for Run.Duration and leave the per-phase totals blank in parallel mode. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Main.ps1 | 5 +++-- src/Pester.RSpec.ps1 | 20 +++++++++++++----- tst/Pester.RSpec.Parallel.ts.ps1 | 36 ++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 7 deletions(-) diff --git a/src/Main.ps1 b/src/Main.ps1 index fb1b81051..b830cc256 100644 --- a/src/Main.ps1 +++ b/src/Main.ps1 @@ -663,7 +663,8 @@ function Invoke-Pester { # If every file opted out with #pester:no-parallel, the run is effectively sequential, # so fall through to the sequential path - which fires the framework's own global # plugin steps at the correct interleaved points. - if ($useParallel -and $parallelSupported -and $allFileContainers -and -not $coverageEnabled -and -not $skipRemainingRunScope -and 0 -lt $parallelContainers.Count) { + $ranInParallel = $useParallel -and $parallelSupported -and $allFileContainers -and -not $coverageEnabled -and -not $skipRemainingRunScope -and 0 -lt $parallelContainers.Count + if ($ranInParallel) { $foldedContainers = [System.Collections.Generic.List[object]]@() $hasNonParallel = 0 -lt $nonParallelContainers.Count @@ -866,7 +867,7 @@ function Invoke-Pester { $run.Containers.Add($i) } - PostProcess-RSpecTestRun -TestRun $run + PostProcess-RSpecTestRun -TestRun $run -Parallel:$ranInParallel $steps = $Plugins.End if ($null -ne $steps -and 0 -lt @($steps).Count) { diff --git a/src/Pester.RSpec.ps1 b/src/Pester.RSpec.ps1 index c882187d0..3e09d3bc9 100644 --- a/src/Pester.RSpec.ps1 +++ b/src/Pester.RSpec.ps1 @@ -178,7 +178,7 @@ function Add-RSpecBlockObjectProperties ($BlockObject) { } } -function PostProcess-RspecTestRun ($TestRun) { +function PostProcess-RspecTestRun ($TestRun, [switch] $Parallel) { $discoveryOnly = $PesterPreference.Run.SkipRun.Value Fold-Run $Run -OnTest { @@ -276,10 +276,20 @@ function PostProcess-RspecTestRun ($TestRun) { $TestRun.FailedContainers.Add($b) } - $TestRun.Duration += $b.Duration - $TestRun.UserDuration += $b.UserDuration - $TestRun.FrameworkDuration += $b.FrameworkDuration - $TestRun.DiscoveryDuration += $b.DiscoveryDuration + # Container durations are summed for a sequential run, where they don't overlap. In a + # parallel run the files execute simultaneously, so summing overstates the totals - the + # real wall-clock duration (RunEnd - RunStart) is used instead and the per-phase totals + # are left blank because they would require excluding overlapping time between containers. + if (-not $Parallel) { + $TestRun.Duration += $b.Duration + $TestRun.UserDuration += $b.UserDuration + $TestRun.FrameworkDuration += $b.FrameworkDuration + $TestRun.DiscoveryDuration += $b.DiscoveryDuration + } + } + + if ($Parallel) { + $TestRun.Duration = [DateTime]::Now - $TestRun.ExecutedAt } $TestRun.PassedCount = $TestRun.Passed.Count diff --git a/tst/Pester.RSpec.Parallel.ts.ps1 b/tst/Pester.RSpec.Parallel.ts.ps1 index 3aee40ed4..0e4dd40fa 100644 --- a/tst/Pester.RSpec.Parallel.ts.ps1 +++ b/tst/Pester.RSpec.Parallel.ts.ps1 @@ -103,6 +103,42 @@ i -PassThru:$PassThru { } } + b "Run duration in parallel mode" { + t "uses wall-clock duration and leaves per-phase totals blank instead of summing containers" { + # Two files that each sleep run sequentially would total ~1.2s; in parallel the wall-clock + # is closer to a single file, so summing container durations overstates the run. (#2794) + $folder = Join-Path ([IO.Path]::GetTempPath()) ([Guid]::NewGuid().Guid) + $null = New-Item -ItemType Directory -Path $folder -Force + Set-Content -Path (Join-Path $folder 'Slow1.Tests.ps1') -Value @' +Describe 'Slow1' { BeforeAll { Start-Sleep -Milliseconds 600 }; It 'p' { 1 | Should -Be 1 } } +'@ + Set-Content -Path (Join-Path $folder 'Slow2.Tests.ps1') -Value @' +Describe 'Slow2' { BeforeAll { Start-Sleep -Milliseconds 600 }; It 'p' { 1 | Should -Be 1 } } +'@ + try { + $c = [PesterConfiguration]::Default + $c.Run.Path = $folder + $c.Run.Parallel = $true + $c.Run.PassThru = $true + $c.Output.Verbosity = 'None' + $r = Invoke-Pester -Configuration $c + + if ($PSVersionTable.PSVersion.Major -ge 7) { + $containerSum = [TimeSpan]::Zero + foreach ($container in $r.Containers) { $containerSum += $container.Duration } + # Wall-clock run duration is less than the naive sum of the two slow files. + ($r.Duration -lt $containerSum) | Verify-True + ($r.Duration -gt [TimeSpan]::Zero) | Verify-True + # Per-phase totals are left blank because containers overlap. + $r.UserDuration | Verify-Equal ([TimeSpan]::Zero) + $r.FrameworkDuration | Verify-Equal ([TimeSpan]::Zero) + $r.DiscoveryDuration | Verify-Equal ([TimeSpan]::Zero) + } + } + finally { Remove-Item -Path $folder -Recurse -Force } + } + } + b "Run.BeforeContainer" { t "runs the repo-root Pester.BeforeContainer.ps1 before each file in a sequential run" { $folder = New-BeforeContainerTestFolder From 1477e10b73e140cc63797c3cfee66b41a9d0194e Mon Sep 17 00:00:00 2001 From: Jakub Jares Date: Mon, 29 Jun 2026 20:22:11 +0200 Subject: [PATCH 2/2] Sum per-phase durations in parallel instead of blanking them The per-phase totals are cumulative measurements of user, framework and discovery time, so they stay valid when files overlap - they just add up to the total work done across the overlapping workers. Only the wall-clock run total needs to be measured fresh, because summing the overlapping container durations overstates it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Pester.RSpec.ps1 | 21 +++++++++++---------- tst/Pester.RSpec.Parallel.ts.ps1 | 13 +++++++------ 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/Pester.RSpec.ps1 b/src/Pester.RSpec.ps1 index 3e09d3bc9..b04939622 100644 --- a/src/Pester.RSpec.ps1 +++ b/src/Pester.RSpec.ps1 @@ -276,19 +276,20 @@ function PostProcess-RspecTestRun ($TestRun, [switch] $Parallel) { $TestRun.FailedContainers.Add($b) } - # Container durations are summed for a sequential run, where they don't overlap. In a - # parallel run the files execute simultaneously, so summing overstates the totals - the - # real wall-clock duration (RunEnd - RunStart) is used instead and the per-phase totals - # are left blank because they would require excluding overlapping time between containers. - if (-not $Parallel) { - $TestRun.Duration += $b.Duration - $TestRun.UserDuration += $b.UserDuration - $TestRun.FrameworkDuration += $b.FrameworkDuration - $TestRun.DiscoveryDuration += $b.DiscoveryDuration - } + # Sum the per-phase durations across containers. These are cumulative measurements of the + # time spent in user code, in the framework and in discovery, so they are valid in both + # sequential and parallel runs - in a parallel run they add up to the total work done + # across the overlapping workers. + $TestRun.Duration += $b.Duration + $TestRun.UserDuration += $b.UserDuration + $TestRun.FrameworkDuration += $b.FrameworkDuration + $TestRun.DiscoveryDuration += $b.DiscoveryDuration } if ($Parallel) { + # The summed container Duration is the total work done, which overstates a parallel run + # because the files overlap in time. Measure the actual elapsed wall-clock for the run + # total instead; the per-phase totals stay as the cumulative work across the workers. $TestRun.Duration = [DateTime]::Now - $TestRun.ExecutedAt } diff --git a/tst/Pester.RSpec.Parallel.ts.ps1 b/tst/Pester.RSpec.Parallel.ts.ps1 index 0e4dd40fa..a02f8e5fc 100644 --- a/tst/Pester.RSpec.Parallel.ts.ps1 +++ b/tst/Pester.RSpec.Parallel.ts.ps1 @@ -104,7 +104,7 @@ i -PassThru:$PassThru { } b "Run duration in parallel mode" { - t "uses wall-clock duration and leaves per-phase totals blank instead of summing containers" { + t "uses the measured wall-clock for the total and still sums the per-phase work" { # Two files that each sleep run sequentially would total ~1.2s; in parallel the wall-clock # is closer to a single file, so summing container durations overstates the run. (#2794) $folder = Join-Path ([IO.Path]::GetTempPath()) ([Guid]::NewGuid().Guid) @@ -126,13 +126,14 @@ Describe 'Slow2' { BeforeAll { Start-Sleep -Milliseconds 600 }; It 'p' { 1 | Sho if ($PSVersionTable.PSVersion.Major -ge 7) { $containerSum = [TimeSpan]::Zero foreach ($container in $r.Containers) { $containerSum += $container.Duration } - # Wall-clock run duration is less than the naive sum of the two slow files. + # Wall-clock run duration is measured, so it is less than the naive sum of the + # two slow files that overlap in parallel. ($r.Duration -lt $containerSum) | Verify-True ($r.Duration -gt [TimeSpan]::Zero) | Verify-True - # Per-phase totals are left blank because containers overlap. - $r.UserDuration | Verify-Equal ([TimeSpan]::Zero) - $r.FrameworkDuration | Verify-Equal ([TimeSpan]::Zero) - $r.DiscoveryDuration | Verify-Equal ([TimeSpan]::Zero) + # Per-phase totals are still measured (summed across the overlapping workers), + # not blanked, so they add up to the total container work. + ($r.UserDuration -gt [TimeSpan]::Zero) | Verify-True + ($r.DiscoveryDuration + $r.UserDuration + $r.FrameworkDuration) | Verify-Equal $containerSum } } finally { Remove-Item -Path $folder -Recurse -Force }