@@ -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+
106241if ($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