Skip to content

Commit 0230af8

Browse files
gh-197: Parallelize tests\test.ps1.
Closes #197.
1 parent 63c9f04 commit 0230af8

1 file changed

Lines changed: 168 additions & 33 deletions

File tree

tests/test.ps1

Lines changed: 168 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -62,52 +62,187 @@ Write-Host 'Running tests...'
6262
# Record the wall-clock time when the first test case is invoked.
6363
$testStart = Get-Date
6464

65-
$failures = @()
66-
$total = 0
67-
68-
foreach ($case in $passingCases) {
69-
$total += 1
70-
$result = Invoke-TestCase $case
71-
Write-Host "==> $($case): " -NoNewline
72-
if ($result.ExitCode -ne 0) {
73-
Write-Host 'fail' -ForegroundColor Red
74-
$failures += [pscustomobject]@{
75-
Path = $case
76-
Type = 'UnexpectedFailure'
77-
ExitCode = $result.ExitCode
78-
Output = $result.Output
79-
}
80-
} else {
81-
Write-Host 'pass' -ForegroundColor Green
65+
# Combine passing and failing cases and sort them alphabetically.
66+
$allCases = @()
67+
foreach ($case in $passingCases) { $allCases += [pscustomobject]@{ RelativePath = $case; ExpectedPass = $true } }
68+
foreach ($case in $failingCases) { $allCases += [pscustomobject]@{ RelativePath = $case; ExpectedPass = $false } }
69+
$allCases = $allCases | Sort-Object RelativePath
70+
71+
# Mark tests that touch shared prefix roots (lib/ext) as exclusive so they
72+
# run with exclusive access and do not race with other tests.
73+
$casesWithFlags = @()
74+
foreach ($c in $allCases) {
75+
$casePath = Join-Path $casesDir $c.RelativePath
76+
$content = ''
77+
try { $content = Get-Content -Path $casePath -Raw -ErrorAction Stop } catch { $content = '' }
78+
79+
# If the test references lib\std, lib\usr, ext\std or ext\usr (or
80+
# explicitly stages probes), treat it as exclusive to avoid races.
81+
$isExclusive = $false
82+
if ($content -match 'lib\\std|lib\\usr|ext\\std|ext\\usr|Add-StagedProbe|Copy-Item -Path') {
83+
$isExclusive = $true
8284
}
85+
86+
$casesWithFlags += [pscustomobject]@{ RelativePath = $c.RelativePath; ExpectedPass = $c.ExpectedPass; Exclusive = $isExclusive }
87+
}
88+
89+
$allCases = $casesWithFlags
90+
91+
$total = $allCases.Count
92+
93+
# If there are no tests, behave as before and exit cleanly.
94+
if ($total -eq 0) {
95+
$testEnd = Get-Date
96+
$elapsed = $testEnd - $testStart
97+
$elapsedSeconds = '{0:N3}' -f $elapsed.TotalSeconds
98+
Write-Host "Ran 0 tests in $elapsedSeconds seconds, with 0/0 tests passing, 0/0 tests failing." -ForegroundColor Green
99+
exit 0
83100
}
84101

85-
foreach ($case in $failingCases) {
86-
$total += 1
87-
$result = Invoke-TestCase $case
88-
Write-Host "==> $($case): " -NoNewline
89-
if ($result.ExitCode -eq 0) {
90-
Write-Host 'fail' -ForegroundColor Red
91-
$failures += [pscustomobject]@{
92-
Path = $case
93-
Type = 'UnexpectedSuccess'
94-
ExitCode = $result.ExitCode
95-
Output = $result.Output
102+
# Determine number of worker threads: logical processors - 1, or 1 minimum.
103+
$maxThreads = [Math]::Max(1, [Environment]::ProcessorCount - 1)
104+
105+
# Prepare concurrent queues for tasks and results.
106+
$taskQueue = [System.Collections.Concurrent.ConcurrentQueue[object]]::new()
107+
$resultsQueue = [System.Collections.Concurrent.ConcurrentQueue[object]]::new()
108+
foreach ($c in $allCases) { $taskQueue.Enqueue($c) }
109+
110+
# Determine PowerShell executable path for running .ps1 cases.
111+
$powerShellExe = if ($PSVersionTable.PSEdition -eq 'Core') { Join-Path $PSHOME 'pwsh.exe' } else { Join-Path $PSHOME 'powershell.exe' }
112+
113+
# Create a runspace pool and start worker runspaces.
114+
115+
# Reader/writer lock used so "exclusive" tests run alone and non-exclusive
116+
# tests may run concurrently.
117+
$rwLock = New-Object System.Threading.ReaderWriterLockSlim
118+
119+
$runspacePool = [runspacefactory]::CreateRunspacePool(1, $maxThreads)
120+
$runspacePool.Open()
121+
122+
$workerScript = @'
123+
param($queue, $exePath, $casesDir, $powerShellExe, $resultsQueue, $rwLock)
124+
125+
while ($true) {
126+
$item = $null
127+
if (-not $queue.TryDequeue([ref]$item)) { break }
128+
129+
$casePath = Join-Path $casesDir $item.RelativePath
130+
if (-not (Test-Path $casePath)) {
131+
$resultsQueue.Enqueue([pscustomobject]@{ Path = $item.RelativePath; ExitCode = -1; Output = "Test case not found: $($item.RelativePath)"; ExpectedPass = $item.ExpectedPass })
132+
continue
133+
}
134+
135+
$extension = [IO.Path]::GetExtension($casePath)
136+
137+
# Acquire a read lock for normal tests, a write lock for exclusive tests.
138+
if ($item.Exclusive) {
139+
$rwLock.EnterWriteLock()
140+
} else {
141+
$rwLock.EnterReadLock()
142+
}
143+
144+
try {
145+
$output = @()
146+
if ($extension -ieq '.ps1') {
147+
if (-not (Test-Path $powerShellExe)) { throw "PowerShell executable not found at: $powerShellExe" }
148+
$powerShellCommand = "& { `$ErrorActionPreference = 'Stop'; & `$args[0] }"
149+
$output = & $powerShellExe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command $powerShellCommand $casePath 2>&1
150+
} else {
151+
$output = & $exePath $casePath 2>&1
152+
}
153+
154+
$exitCode = [int]$LASTEXITCODE
155+
$resultsQueue.Enqueue([pscustomobject]@{
156+
Path = $item.RelativePath
157+
ExitCode = $exitCode
158+
Output = (($output | ForEach-Object { $_.ToString() }) -join [Environment]::NewLine)
159+
ExpectedPass = $item.ExpectedPass
160+
})
161+
}
162+
finally {
163+
if ($item.Exclusive) {
164+
$rwLock.ExitWriteLock()
165+
} else {
166+
$rwLock.ExitReadLock()
167+
}
168+
}
169+
}
170+
'@
171+
172+
$workers = @()
173+
for ($i = 0; $i -lt $maxThreads; $i++) {
174+
$ps = [PowerShell]::Create()
175+
$ps.RunspacePool = $runspacePool
176+
$ps.AddScript($workerScript) | Out-Null
177+
[void]$ps.AddArgument($taskQueue)
178+
[void]$ps.AddArgument($exePath)
179+
[void]$ps.AddArgument($casesDir)
180+
[void]$ps.AddArgument($powerShellExe)
181+
[void]$ps.AddArgument($resultsQueue)
182+
[void]$ps.AddArgument($rwLock)
183+
$async = $ps.BeginInvoke()
184+
$workers += [pscustomobject]@{ PowerShell = $ps; Async = $async }
185+
}
186+
187+
# Show a single-line progress indicator that updates on each completed test.
188+
$lastDisplayedLength = 0
189+
while ($true) {
190+
$completed = $resultsQueue.Count
191+
if ($completed -gt 0 -or $completed -eq 0) {
192+
$remaining = $total - $completed
193+
$message = "${completed}/${total} test completed, ${remaining} remaining."
194+
$pad = if ($message.Length -lt $lastDisplayedLength) { ' ' * ($lastDisplayedLength - $message.Length) } else { '' }
195+
$lastDisplayedLength = $message.Length
196+
Write-Host -NoNewline "`r$message$pad"
197+
}
198+
if ($completed -ge $total) { break }
199+
Start-Sleep -Milliseconds 100
200+
}
201+
202+
# Ensure a newline after the single-line progress.
203+
Write-Host ''
204+
205+
# Wait for workers to finish and dispose runspaces.
206+
foreach ($w in $workers) {
207+
try { $w.PowerShell.EndInvoke($w.Async) } catch { }
208+
$w.PowerShell.Dispose()
209+
}
210+
$runspacePool.Close()
211+
$runspacePool.Dispose()
212+
213+
# Collect results and determine failures.
214+
$failures = @()
215+
$results = @()
216+
$res = $null
217+
while ($resultsQueue.TryDequeue([ref]$res)) { $results += $res }
218+
219+
foreach ($r in $results) {
220+
if ($r.ExpectedPass -and $r.ExitCode -ne 0) {
221+
$failures += [pscustomobject]@{
222+
Path = $r.Path
223+
Type = 'UnexpectedFailure'
224+
ExitCode = $r.ExitCode
225+
Output = $r.Output
226+
}
227+
} elseif (-not $r.ExpectedPass -and $r.ExitCode -eq 0) {
228+
$failures += [pscustomobject]@{
229+
Path = $r.Path
230+
Type = 'UnexpectedSuccess'
231+
ExitCode = $r.ExitCode
232+
Output = $r.Output
233+
}
96234
}
97-
} else {
98-
Write-Host 'pass' -ForegroundColor Green
99-
}
100235
}
101236

102237
$testEnd = Get-Date
103238
$elapsed = $testEnd - $testStart
104239
$elapsedSeconds = '{0:N3}' -f $elapsed.TotalSeconds
105-
Write-Host ''
240+
106241
if ($failures.Count -gt 0) {
107242
Write-Host 'FAILURES:' -ForegroundColor Red
108243
foreach ($f in $failures) {
109244
Write-Host " - $($f.Path): $($f.Type) (ExitCode: $($f.ExitCode))"
110-
if ($f.Output) { Write-Host $f.Output -ForegroundColor Red}
245+
if ($f.Output) { Write-Host $f.Output -ForegroundColor Red }
111246
}
112247
Write-Host ''
113248
Write-Host "Ran $total tests in $elapsedSeconds seconds, with $($total - $failures.Count)/$($total) tests passing, $($failures.Count)/$($total) tests failing." -ForegroundColor Red

0 commit comments

Comments
 (0)