|
- # E08-S01: automated x128 regression harness.
- #
- # Runs each suite from a manifest in its own child PowerShell, with a fresh
- # build directory (so every suite creates its own D81), a wall-clock limit that
- # also bounds emulated cycles, a captured log, and a machine-readable results
- # file. When a suite declares artifacts, their SHA-256 values are compared with
- # a reviewed baseline; -UpdateBaseline rewrites that baseline.
- [CmdletBinding()]
- param(
- [string]$Manifest = "",
- [string]$BuildRoot = "",
- [string]$Baseline = "",
- [Alias("Results")][string]$ResultsPath = "",
- [string[]]$Filter = @(),
- [switch]$UpdateBaseline,
- [switch]$NoFresh
- )
-
- $ErrorActionPreference = "Stop"
-
- $project = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
- if ($Manifest -eq "") { $Manifest = Join-Path $PSScriptRoot "suites.ps1" }
- if ($BuildRoot -eq "") { $BuildRoot = Join-Path $project "build" }
- if ($Baseline -eq "") { $Baseline = Join-Path $PSScriptRoot "baseline.json" }
- if ($ResultsPath -eq "") { $ResultsPath = Join-Path $BuildRoot "regression\results.json" }
-
- foreach ($path in @($Manifest)) {
- if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { throw "manifest not found: $path" }
- }
- $suites = @(& $Manifest)
- if ($suites.Count -eq 0) { throw "manifest lists no suites: $Manifest" }
- # -File invocation passes "source,lexer" as one string, so accept both forms.
- $wanted = @()
- foreach ($item in $Filter) { $wanted += @($item -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }) }
- if ($wanted.Count -gt 0) {
- $suites = @($suites | Where-Object { $wanted -contains $_.Name })
- if ($suites.Count -eq 0) { throw "filter matched no suite: $($wanted -join ', ')" }
- }
-
- $logDirectory = Join-Path $BuildRoot "regression"
- New-Item -ItemType Directory -Force -Path $logDirectory | Out-Null
- $baselineData = @{}
- if (Test-Path -LiteralPath $Baseline -PathType Leaf) {
- $raw = Get-Content -Raw -LiteralPath $Baseline | ConvertFrom-Json
- foreach ($property in $raw.PSObject.Properties) {
- $entries = @{}
- foreach ($artifact in $property.Value.PSObject.Properties) {
- $entries[$artifact.Name] = [string]$artifact.Value
- }
- $baselineData[$property.Name] = $entries
- }
- }
-
- # The first line that looks like a diagnosis, so a failed suite names the
- # fixture and what was observed instead of only an exit code.
- function Get-Signature {
- param([AllowEmptyString()][string]$Text)
- $lines = @($Text -split "`r?`n" | Where-Object { $_.Trim() -ne "" })
- if ($lines.Count -eq 0) { return "" }
- $hit = $lines | Where-Object { $_ -match "(?i)fail|differs|error|exception|missing|did not|refus" } |
- Select-Object -First 1
- if (-not $hit) { $hit = $lines | Select-Object -Last 1 }
- $hit = $hit.Trim()
- if ($hit.Length -gt 240) { $hit = $hit.Substring(0, 240) }
- return $hit
- }
-
- function Get-ArtifactFiles {
- param([string]$Directory, [string[]]$Patterns)
- if (-not $Directory -or $Patterns.Count -eq 0) { return @() }
- if (-not (Test-Path -LiteralPath $Directory -PathType Container)) { return @() }
- $extensions = @($Patterns | ForEach-Object { $_.ToLowerInvariant() })
- $files = Get-ChildItem -LiteralPath $Directory -File -Recurse |
- Where-Object { $extensions -contains ("*" + $_.Extension.ToLowerInvariant()) }
- return @($files | Sort-Object FullName)
- }
-
- $results = @()
- $startedAt = (Get-Date).ToString("o")
-
- foreach ($suite in $suites) {
- $name = [string]$suite.Name
- $script = [string]$suite.Script
- if (-not [IO.Path]::IsPathRooted($script)) { $script = Join-Path $project $script }
- $timeout = if ($suite.ContainsKey("Timeout")) { [int]$suite.Timeout } else { 300 }
- $buildDirectory = $null
- if ($suite.ContainsKey("BuildDir") -and $suite.BuildDir) {
- $buildDirectory = Join-Path $BuildRoot ([string]$suite.BuildDir)
- }
-
- $problems = @()
- if (-not (Test-Path -LiteralPath $script -PathType Leaf)) {
- $problems += ("suite script missing: $script")
- }
- if ($problems.Count -eq 0 -and $buildDirectory -and -not $NoFresh) {
- if (Test-Path -LiteralPath $buildDirectory) {
- Remove-Item -LiteralPath $buildDirectory -Recurse -Force
- }
- }
-
- $log = Join-Path $logDirectory ("$name.log")
- $errorLog = Join-Path $logDirectory ("$name.err.log")
- foreach ($path in @($log, $errorLog)) {
- if (Test-Path -LiteralPath $path) { Remove-Item -LiteralPath $path -Force }
- }
-
- $seconds = 0
- $exitCode = -1
- $timedOut = $false
- $text = ""
- if ($problems.Count -eq 0) {
- $started = Get-Date
- $psi = [Diagnostics.ProcessStartInfo]::new()
- $psi.FileName = (Get-Command powershell).Source
- $psi.Arguments = @("-NoProfile", "-ExecutionPolicy", "Bypass", "-File", ('"' + $script + '"')) -join ' '
- $psi.UseShellExecute = $false
- $psi.CreateNoWindow = $true
- $psi.RedirectStandardOutput = $true
- $psi.RedirectStandardError = $true
- $process = [Diagnostics.Process]::Start($psi)
- $outTask = $process.StandardOutput.ReadToEndAsync()
- $errTask = $process.StandardError.ReadToEndAsync()
- if (-not $process.WaitForExit($timeout * 1000)) {
- $timedOut = $true
- & taskkill /PID $process.Id /T /F 2>&1 | Out-Null
- if (-not $process.WaitForExit(10000)) { $process.Kill() }
- }
- $seconds = [Math]::Round(((Get-Date) - $started).TotalSeconds, 1)
- $exitCode = if ($timedOut) { -1 } else { $process.ExitCode }
- $text = ($outTask.Result + "`n" + $errTask.Result)
- [IO.File]::WriteAllText($log, $text)
- if ($timedOut) {
- $problems += ("timed out after $timeout s (cycle budget exhausted)")
- } elseif ($exitCode -ne 0) {
- $problems += ("exit code $exitCode")
- }
- if (-not $timedOut -and $suite.ContainsKey("Success") -and $suite.Success -and
- $text -notmatch [string]$suite.Success) {
- $problems += ("missing success marker '$($suite.Success)'")
- }
- }
-
- # Artifact capture and comparison.
- $artifacts = @()
- $patterns = @()
- if ($suite.ContainsKey("Artifacts")) { $patterns = @($suite.Artifacts) }
- if ($buildDirectory -and $patterns.Count -gt 0) {
- $expected = if ($baselineData.ContainsKey($name)) { $baselineData[$name] } else { @{} }
- foreach ($file in (Get-ArtifactFiles -Directory $buildDirectory -Patterns $patterns)) {
- $relative = $file.FullName.Substring($buildDirectory.Length).TrimStart('\', '/')
- $hash = (Get-FileHash -LiteralPath $file.FullName -Algorithm SHA256).Hash
- $state = if (-not $expected.ContainsKey($relative)) {
- "new"
- } elseif ($expected[$relative] -ne $hash) {
- "changed"
- } else {
- "match"
- }
- if (-not $UpdateBaseline -and $state -ne "match" -and $baselineData.ContainsKey($name)) {
- $problems += ("artifact ${relative}: $state")
- }
- $artifacts += ([ordered]@{
- path = $relative; sha256 = $hash; status = $state
- })
- }
- foreach ($relative in $expected.Keys) {
- if (-not ($artifacts | Where-Object { $_.path -eq $relative })) {
- if (-not $UpdateBaseline) { $problems += ("artifact ${relative}: missing") }
- $artifacts += ([ordered]@{ path = $relative; sha256 = ""; status = "missing" })
- }
- }
- }
-
- $status = if ($problems.Count -eq 0) { "pass" } else { "fail" }
- # A bare exit status says nothing about the fixture; prefer the diagnosis
- # the suite printed, and fall back to the structural problem when it
- # produced no output at all.
- $observed = Get-Signature $text
- $signature = if ($problems.Count -eq 0) { $observed }
- elseif ($problems[0] -match "^exit code" -and $observed) { $observed }
- else { $problems[0] }
- $results += ([ordered]@{
- name = $name
- status = $status
- seconds = $seconds
- exitCode = $exitCode
- timedOut = $timedOut
- signature = $signature
- problems = @($problems)
- log = $log
- artifacts = @($artifacts)
- })
- Write-Host (" {0,-14} {1,-4} {2,6}s {3}" -f $name, $status, $seconds, $signature)
- }
-
- $finishedAt = (Get-Date).ToString("o")
- $report = [ordered]@{
- startedAt = $startedAt
- finishedAt = $finishedAt
- manifest = $Manifest
- suites = @($results)
- passed = @($results | Where-Object { $_.status -eq "pass" }).Count
- failed = @($results | Where-Object { $_.status -eq "fail" }).Count
- }
- $resultsDirectory = Split-Path $ResultsPath -Parent
- New-Item -ItemType Directory -Force -Path $resultsDirectory | Out-Null
- $report | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $ResultsPath
-
- if ($UpdateBaseline) {
- $fresh = [ordered]@{}
- foreach ($entry in $results) {
- if ($entry.status -ne "pass" -or $entry.artifacts.Count -eq 0) { continue }
- $map = [ordered]@{}
- foreach ($artifact in $entry.artifacts) {
- $map[$artifact.path] = $artifact.sha256
- }
- $fresh[$entry.name] = $map
- }
- $fresh | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $Baseline
- Write-Host ("baseline written: {0} ({1} suites)" -f $Baseline, $fresh.Count)
- }
-
- Write-Host ("regression: {0} passed, {1} failed; results {2}" -f $report.passed, $report.failed, $ResultsPath)
- if ($report.failed -gt 0) {
- foreach ($entry in ($results | Where-Object { $_.status -eq "fail" })) {
- Write-Host (" {0}: {1}" -f $entry.name, ($entry.problems -join "; "))
- }
- exit 1
- }
|