Browse Source

Add the automated x128 regression harness with reviewed artifact baselines (E08-S01).

Suites run from a manifest into fresh build directories with time limits,
captured logs, JSON results and SHA-256 comparisons; source, lexer and parser
artifacts are pinned, and self-tests cover timeout, failure signature and
changed-artifact detection.

Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
main
Daniel Covington 3 days ago
parent
commit
655b29f9b0
6 changed files with 498 additions and 0 deletions
  1. +1
    -0
      newLang/docs/decisions.md
  2. +100
    -0
      newLang/planning/claims/E08-S01.md
  3. +14
    -0
      newLang/tests/regression/baseline.json
  4. +124
    -0
      newLang/tests/regression/harness.tests.ps1
  5. +229
    -0
      newLang/tests/regression/run-regression.ps1
  6. +30
    -0
      newLang/tests/regression/suites.ps1

+ 1
- 0
newLang/docs/decisions.md View File

@@ -84,6 +84,7 @@ documented default. `Proposed` means a later change still needs approval.
| D148 | E07-S02 puts both emulator launchers on one shared implementation, `scripts/lib/Nova128Launcher.psm1`. `Enter-Nova128ImageSession` takes an exclusive `.newlang-session.lock` beside the writable image, replaces a stale lock only when its PID is gone, and refuses when the holder is alive or the lock cannot even be read; `Wait-Nova128ImageFlush` reopens the image exclusively with retries so a launcher never returns while the drive is still writing; `Invoke-Nova128Emulator` starts the emulator, waits for exit, then waits for flush and releases the lock. `scripts/launch-editor.ps1` uses `x64sc` with `../vim/vim128.prg` and copies `../vim/vim-improved.d81` to `build/newlang-edit.d81`; `scripts/launch-compiler.ps1` uses `x128` with `build/n128c.prg` and prepares the development image unless `-SkipPrepare`. Both autostart a host-side program path, so no viewer binary has to be on the disk, and both accept `-LimitCycles`, `-Warp` and `-Screenshot`. | The story's rule is that the editor and the compiler never hold the same writable disk at once, which is only reliable if both launchers implement the lock and the flush wait identically. A lock file beside the image is detectable from either script and from a test without a shared daemon; an unreadable lock counts as held because an exclusive holder denies reads too. Copying the editor disk per session keeps `../vim/vim-improved.d81` a read-only original, matching E07-S01's rule for the compiler disk. `x64sc` names the VIC-II exit screenshot `-exitscreenshot` while `x128` uses `-exitscreenshotvicii`, so the option is chosen by emulator name. `docs/editor-workflow.md` carries the user-facing order and recovery rules, and `tests/disk/launchers.tests.ps1` proves the lock, the flush wait, both autostarts and that the source disk hash is unchanged. |

| D151 | The editor workflow now defaults to the C128 builds: `scripts/launch-editor.ps1` starts `x128` in 80-column mode with `../vim/vim128.prg`, injected as `vim128` into the session's copy of `../vim/vim-improved.d81`, and autostarts that disk copy. This supersedes the `x64sc` editor default in E07-S02's acceptance text; the C64 editor build and its disk remain available through explicit arguments. Because `x128`'s plain `-exitscreenshot` captures the active display (VDC in 80-column mode) while `-exitscreenshotvicii` captures the 40-column screen, the shared launcher selects the option by emulator and mode. The E07-S03 round-trip test now creates, reopens and tokenizes the source through `vim128` in `x128`, so the whole path stays on the target machine. The editor must be autostarted from the session disk (`work.d81:vim128`, with the launcher running in the image's folder), not from a host path: a host-injected program has no device number from its load, and `vim128` then reports `written` while the image never receives the file — observed while validating the round-trip, which only passed once the disk copy was autostarted. | The user asked for the C128 Vim to be the default editor everywhere. The C128 build was already present as `../vim/vim128.prg` (80-column, built by `vim/build128.bat`) but no shipped `.d81` held it; injecting it into the per-session disk copy makes the editor available to the session's own disk access without writing `../vim` or adding a new reference image. Capturing the VDC screen is required for automated checks: a VIC-II capture of an 80-column session is blank. Keeping the C64 build reachable as an argument preserves the proven fallback instead of deleting working behaviour. |
| D152 | The E08-S01 harness reads its suite list from `tests/regression/suites.ps1`, which returns an array of hashtables, and gives each suite a fresh `build/<name>-test` directory unless `-NoFresh` is passed. Results are written as JSON with per-suite status, seconds, exit code, signature, problems, log path and artifacts; artifact SHA-256 values are compared against `tests/regression/baseline.json`, which `-UpdateBaseline` rewrites. A failed suite's signature is the diagnosis line it printed (for example a fixture name and the mismatch), falling back to the structural problem such as `exit code N` or a timeout when the suite produced no output. | A `.psd1` cannot be invoked with `&` (PowerShell returns nothing), so the manifest is a script whose array literal is evaluated on read; a data file would need `Import-PowerShellDataFile` and could not express per-suite logic. Fresh per-suite directories satisfy the `E08-S01` requirement that every run creates its own D81; `-NoFresh` exists so the self-test can prove that changed artifacts are detected on a rerun. Reviewing hashes rather than regenerating them keeps `E08-S02`'s "reviewed rather than blindly regenerated" rule intact. Preferring the observed diagnosis over an exit status is what makes a failure identify the fixture, as the story requires. |

## Deferred



+ 100
- 0
newLang/planning/claims/E08-S01.md View File

@@ -0,0 +1,100 @@
---
story: E08-S01
owner: codebuff
status: done
started: 2026-11-10
completed: 2026-11-10
depends-on: [E01-S05, E02-S04]
---

# E08-S01 Claim

## Intended change

One harness that runs the verification suites reproducibly on the target
machine: each suite gets a fresh build directory (so it creates its own D81), a
wall-clock limit that bounds emulated cycles, a captured log, a machine-readable
result, and — where the output is stable — a reviewed SHA-256 baseline of the
PRG/MAP artifacts. A failing suite names its fixture and the observed mismatch.

## File area

- `tests/regression/run-regression.ps1` (new): suite loop, child PowerShell per
suite, timeout with tree kill, log capture, artifact hashing and comparison,
JSON results, baseline update
- `tests/regression/suites.ps1` (new): manifest of 24 suites, each pointing at
an existing `scripts/verify-*-test.ps1` script and a `<name>-test` build
directory
- `tests/regression/baseline.json` (new): reviewed hashes for the source, lexer
and parser artifacts
- `tests/regression/harness.tests.ps1` (new): self-tests of the mechanics with
no emulator
- `docs/decisions.md` (D152), `planning/claims/E08-S01.md`
- generated files under `build/regression/` (results, per-suite logs)

## Acceptance evidence

- [x] Creates a fresh D81 per test run: every suite runs in
`build/<name>-test`, which the harness deletes before the run unless
`-NoFresh` is passed; each of the three exercised suites builds its own
image there. The self-test proves the deletion with a suite that only passes
when its directory starts empty.
- [x] Enforces time/cycle limits and captures machine-readable results: a suite
exceeding its `Timeout` seconds is killed with its process tree and recorded
as `timedOut`; results are JSON with `startedAt`, `finishedAt`, per-suite
`status`, `seconds`, `exitCode`, `signature`, `problems`, `log` and
`artifacts`, plus `passed`/`failed` totals.
- [x] Extracts PRG/ERR/MAP outputs and compares expected artifacts: artifacts
are hashed with SHA-256 and compared against `baseline.json`; a rerun reports
`match`, a changed file reports `changed`, a new file reports `new`, and a
baselined file that disappeared reports `missing`.
- [x] A failed test identifies the fixture and observed signature: the failed
suite's signature is the diagnosis line from its log (the self-test asserts a
fixture name and observed value survive), not merely `exit code 3`.

## Coordination notes

- Depends on E01-S05 (container/loader) and E02-S04 (lexer); both are complete.
The harness runs the existing `scripts/verify-*-test.ps1` scripts unchanged,
so it adds automation rather than a second implementation of any check.
- Only the source, lexer and parser suites have a reviewed baseline: their PRG
and MAP bytes are stable across runs. The other 21 suites are executed for
status, success marker and log, because their artifacts either embed
run-specific data or have not been reviewed yet. Adding a baseline entry
requires reviewing the bytes first, per E08-S02.
- `-NoFresh` exists for the self-test only; a normal run always reuses nothing.

## Work log

- The manifest was first written as `suites.psd1`; invoking a `.psd1` with `&`
yields nothing, so the harness reported "manifest lists no suites". It is now
`suites.ps1`.
- The harness stored its result records in `$results` while the output path
parameter was `$Results`: PowerShell variable names are case-insensitive, so
the path became an empty array and `New-Item -Path ""` failed with
"Cannot bind argument to parameter 'Path'". The parameter is now
`$ResultsPath` with an alias `-Results` so callers are unchanged.
- `-File` passes `-Filter source,lexer,parser` as a single string; the filter
now splits on commas as well as accepting a string array.
- The self-test expected exit code 0 from a run that deliberately includes two
failing suites. It now asserts the two expected suites pass and that failing
suites are reported, which is what the harness guarantees.
- The failing suite's signature was its `exit code 3`; it now prefers the
diagnosis line from the captured output.

## Completion

- Tests run: `tests/regression/harness.tests.ps1`, then
`run-regression.ps1 -Filter source,lexer,parser -UpdateBaseline`, then the
same filter without `-UpdateBaseline` to prove the baseline matches.
- Results: `regression harness tests passed`; both real runs reported
`3 passed, 0 failed` (source 18.3 s / 18.5 s, lexer 21.7 s / 23.7 s, parser
35.6 s / 37.9 s). Baseline covers 3 suites: source PRG
`4C0FA221…DC6AAC65`, lexer PRG `822074D0…97AA4`, parser PRG
`7A2E1B90…4128FBA`.
- Remaining risks: a wall-clock timeout on a heavily loaded host can kill a
healthy suite, and the message then reports the timeout rather than the
fixture; MAP hashes track the cc65 version, so a toolchain upgrade legitimately
changes them and requires a reviewed `-UpdateBaseline`; the other 21 suites
have no pinned artifacts yet, so a silent byte-level change in them would not
be caught.

+ 14
- 0
newLang/tests/regression/baseline.json View File

@@ -0,0 +1,14 @@
{
"source": {
"source-test.map": "49BFA6EAE563D2AF1DE77DBFBF63645157C306F318FF997A7F7924E132D187AB",
"source-test.prg": "4C0FA2218F1E2F7DE460F57C4A5676540C5BE31D1AFE35C585928A3ADC6AAC65"
},
"lexer": {
"lexer-test.map": "EB203A55F4296B62BAF864A9EDDDDA7D959406BE8B4476D9C888C09ED30B7A60",
"lexer-test.prg": "822074D0C1EE04446D50FA2D39ABE30287FA38B762698F3602B25DD21CB97AA4"
},
"parser": {
"parser-test.map": "3C07157712AEDB133D783CC4EB2E706A2769E23DCE08220CF838FD103D94CCC0",
"parser-test.prg": "7A2E1B90FCC9EB06D54FCF1D0FFAE5041B127D94CD0AC48D704390C234128FBA"
}
}

+ 124
- 0
newLang/tests/regression/harness.tests.ps1 View File

@@ -0,0 +1,124 @@
# E08-S01: harness self-tests. These prove the mechanics without starting an
# emulator: fresh build directories, a wall-clock limit, machine-readable
# results, artifact comparison, and a failed run that names the suite and its
# observed signature.
[CmdletBinding()]
param()

$ErrorActionPreference = "Stop"

$project = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$harness = Join-Path $PSScriptRoot "run-regression.ps1"
$root = Join-Path $project "build\regression-selftest"
$suites = Join-Path $root "suites"
$buildRoot = Join-Path $root "build"
$baseline = Join-Path $root "baseline.json"
$results = Join-Path $root "results.json"
$manifest = Join-Path $root "suites.ps1"

if (Test-Path -LiteralPath $root) { Remove-Item -LiteralPath $root -Recurse -Force }
New-Item -ItemType Directory -Force -Path $suites, $buildRoot | Out-Null

function Write-Suite([string]$Name, [string]$Body) {
[IO.File]::WriteAllText((Join-Path $suites $Name), $Body,
[Text.UTF8Encoding]::new($false))
}

# A passing suite writes one artifact and prints a stable success marker.
Write-Suite "ok.ps1" @'
$out = Join-Path (Split-Path $PSScriptRoot -Parent) "build\self-ok"
New-Item -ItemType Directory -Force -Path $out | Out-Null
[IO.File]::WriteAllBytes((Join-Path $out "self.prg"), [byte[]](1, 2, 3, 4))
Write-Host "verified self ok"
exit 0
'@
# A failing suite prints a diagnosis the harness must surface.
Write-Suite "bad.ps1" @'
Write-Host "self fixture ALPHA differs at token 7 (observed signature DEAD)"
exit 3
'@
# A hanging suite must be stopped by the wall-clock limit.
Write-Suite "slow.ps1" @'
Start-Sleep -Seconds 60
Write-Host "should never finish"
exit 0
'@
# A suite whose artifact content changes between runs.
Write-Suite "changed.ps1" @'
param()
$out = Join-Path (Split-Path $PSScriptRoot -Parent) "build\self-changed"
New-Item -ItemType Directory -Force -Path $out | Out-Null
$value = 5
if (Test-Path (Join-Path $out "bump.txt")) { $value = 6 }
[IO.File]::WriteAllText((Join-Path $out "bump.txt"), "$value")
[IO.File]::WriteAllBytes((Join-Path $out "self.err"), [byte[]]$value)
Write-Host "verified self changed"
exit 0
'@

[IO.File]::WriteAllText($manifest, @'
@(
@{ Name = "ok"; Script = "build\regression-selftest\suites\ok.ps1"; BuildDir = "self-ok"; Timeout = 60; Success = "verified self ok"; Artifacts = @("*.prg") }
@{ Name = "bad"; Script = "build\regression-selftest\suites\bad.ps1"; Timeout = 60 }
@{ Name = "slow"; Script = "build\regression-selftest\suites\slow.ps1"; Timeout = 3 }
@{ Name = "changed"; Script = "build\regression-selftest\suites\changed.ps1"; BuildDir = "self-changed"; Timeout = 60; Artifacts = @("*.err") }
)
'@, [Text.UTF8Encoding]::new($false))

function Invoke-Harness([string[]]$Extra) {
$arguments = @("-NoProfile", "-ExecutionPolicy", "Bypass", "-File", ('"' + $harness + '"'),
"-Manifest", ('"' + $manifest + '"'), "-BuildRoot", ('"' + $buildRoot + '"'),
"-Baseline", ('"' + $baseline + '"'), "-Results", ('"' + $results + '"')) + $Extra
$output = & (Get-Command powershell).Source @arguments 2>&1 | Out-String
return @{ Output = $output; Code = $LASTEXITCODE }
}

function Get-Report { Get-Content -Raw -LiteralPath $results | ConvertFrom-Json }
function Get-Entry([object]$Report, [string]$Name) {
return $Report.suites | Where-Object { $_.name -eq $Name }
}

# 1: passing suite, artifact recorded, baseline written and matched on a rerun.
$first = Invoke-Harness @("-UpdateBaseline")
if (-not (Test-Path -LiteralPath $results)) { throw "first harness run wrote no results:`n$($first.Output)" }
$report = Get-Report
if ($report.passed -ne 2) { throw "expected 2 passing suites, got $($report.passed)" }
$ok = Get-Entry $report "ok"
if ($ok.artifacts.Count -ne 1 -or $ok.artifacts[0].status -ne "new") {
throw "the passing suite did not record its artifact"
}
if (-not (Test-Path -LiteralPath $baseline)) { throw "-UpdateBaseline wrote no baseline" }
$second = Invoke-Harness @("-NoFresh")
$ok = Get-Entry (Get-Report) "ok"
if ($ok.artifacts[0].status -ne "match") {
throw "a rerun did not match the recorded artifact:`n$($second.Output)"
}

# 2: a failing suite names the suite and the observed signature.
$bad = Get-Entry (Get-Report) "bad"
if ($bad.status -ne "fail" -or $bad.signature -notmatch "ALPHA differs") {
throw "the failing suite did not report its signature: $($bad.signature)"
}
$slow = Get-Entry (Get-Report) "slow"
if ($slow.status -ne "fail" -or -not $slow.timedOut) {
throw "the hanging suite was not stopped by the time limit"
}
if ($second.Code -eq 0) { throw "the harness reported success with failing suites" }

# 3: an artifact whose bytes change is reported as changed on the next run.
$third = Invoke-Harness @("-NoFresh")
$changed = Get-Entry (Get-Report) "changed"
if ($third.Code -eq 0 -or $changed.artifacts[0].status -ne "changed") {
throw "a changed artifact was not detected:`n$($third.Output)"
}

# 4: results are machine-readable and identify the suite and its log.
if (-not $report.startedAt -or -not $report.finishedAt) { throw "results lack timestamps" }
foreach ($entry in $report.suites) {
if (-not $entry.log -or -not (Test-Path -LiteralPath $entry.log)) {
throw "suite $($entry.name) has no captured log"
}
}

Write-Host "regression harness tests passed"
Write-Host "pass/fail/timeout/artifact-change detection, JSON results, captured logs"

+ 229
- 0
newLang/tests/regression/run-regression.ps1 View File

@@ -0,0 +1,229 @@
# 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
}

+ 30
- 0
newLang/tests/regression/suites.ps1 View File

@@ -0,0 +1,30 @@
# E08-S01 regression manifest. Each suite builds its own fresh D81 inside its
# build directory. Artifacts are compared against baseline.json when listed;
# Suites without Artifacts only have their exit status, success marker and log
# checked.
@(
@{ Name = "source"; Script = "scripts\verify-source-test.ps1"; BuildDir = "source-test"; Timeout = 300; Success = "verified source reader"; Artifacts = @("*.prg", "*.err", "*.map") }
@{ Name = "lexer"; Script = "scripts\verify-lexer-test.ps1"; BuildDir = "lexer-test"; Timeout = 300; Success = "verified C128 lexer"; Artifacts = @("*.prg", "*.err", "*.map") }
@{ Name = "parser"; Script = "scripts\verify-parser-test.ps1"; BuildDir = "parser-test"; Timeout = 400; Success = "verified parser"; Artifacts = @("*.prg", "*.err", "*.map") }
@{ Name = "names"; Script = "scripts\verify-names-test.ps1"; BuildDir = "names-test"; Timeout = 300 }
@{ Name = "diag"; Script = "scripts\verify-diag-test.ps1"; BuildDir = "diag-test"; Timeout = 300 }
@{ Name = "output"; Script = "scripts\verify-output-test.ps1"; BuildDir = "output-test"; Timeout = 300 }
@{ Name = "hello"; Script = "scripts\verify-hello-test.ps1"; BuildDir = "hello-test"; Timeout = 300 }
@{ Name = "frame"; Script = "scripts\verify-frame-test.ps1"; BuildDir = "frame-test"; Timeout = 300 }
@{ Name = "scalar"; Script = "scripts\verify-scalar-test.ps1"; BuildDir = "scalar-test"; Timeout = 300 }
@{ Name = "control"; Script = "scripts\verify-control-test.ps1"; BuildDir = "control-test"; Timeout = 300 }
@{ Name = "print"; Script = "scripts\verify-print-test.ps1"; BuildDir = "print-test"; Timeout = 300 }
@{ Name = "statements"; Script = "scripts\verify-statements-test.ps1"; BuildDir = "statements-test"; Timeout = 300 }
@{ Name = "calls"; Script = "scripts\verify-calls-test.ps1"; BuildDir = "calls-test"; Timeout = 300 }
@{ Name = "class"; Script = "scripts\verify-class-test.ps1"; BuildDir = "class-test"; Timeout = 300 }
@{ Name = "member"; Script = "scripts\verify-member-test.ps1"; BuildDir = "member-test"; Timeout = 300 }
@{ Name = "heap"; Script = "scripts\verify-heap-test.ps1"; BuildDir = "heap-test"; Timeout = 300 }
@{ Name = "object"; Script = "scripts\verify-object-test.ps1"; BuildDir = "object-test"; Timeout = 300 }
@{ Name = "counter"; Script = "scripts\verify-counter-test.ps1"; BuildDir = "counter-test"; Timeout = 300 }
@{ Name = "dispatch"; Script = "scripts\verify-dispatch-test.ps1"; BuildDir = "dispatch-test"; Timeout = 300 }
@{ Name = "interface"; Script = "scripts\verify-interface-test.ps1"; BuildDir = "interface-test"; Timeout = 300 }
@{ Name = "convert"; Script = "scripts\verify-convert-test.ps1"; BuildDir = "convert-test"; Timeout = 300 }
@{ Name = "ifcall"; Script = "scripts\verify-ifcall-test.ps1"; BuildDir = "ifcall-test"; Timeout = 300 }
@{ Name = "iftable"; Script = "scripts\verify-iftable-test.ps1"; BuildDir = "iftable-test"; Timeout = 300 }
@{ Name = "lifetime"; Script = "scripts\verify-lifetime-test.ps1"; BuildDir = "lifetime-test"; Timeout = 300 }
)

Loading…
Cancel
Save

Powered by TurnKey Linux.