mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-27 15:17:18 +09:00
Full VM installs and Test Sandbox full mode now record a short post-install runtime observation (60s settle + 120s sampling) with an explicit measured/unavailable/failed/interrupted status that never rewrites install success. Adds the standalone scripts/measure-runtime.py sampler, the measure-installed-runtime.py harness wrapper, E2E report rendering, results-API summaries, CI coverage and METRICS.md accounting boundaries.
469 lines
23 KiB
PowerShell
469 lines
23 KiB
PowerShell
#requires -Version 5.1
|
|
<#
|
|
.SYNOPSIS
|
|
One-click full-stack E2E test for ServerKit on Windows. Sequential edition.
|
|
|
|
.DESCRIPTION
|
|
For each distro, in turn: launch VM via Multipass, upload local working
|
|
tree, run installer, run pytest harness, collect results. Aggregates into
|
|
a single HTML report.
|
|
|
|
Sequential by design — Start-Job's separate runspace makes parameter
|
|
passing and command resolution fragile enough that parallel is not worth
|
|
the debugging cost.
|
|
|
|
.PARAMETER Distros
|
|
Distros to test. Default: ubuntu22, ubuntu24, debian12.
|
|
|
|
.PARAMETER Only
|
|
Comma-separated subset (e.g. -Only "ubuntu24").
|
|
|
|
.PARAMETER Keep
|
|
Don't delete VMs at end.
|
|
|
|
.PARAMETER ReuseVm
|
|
Skip launch + install; use an already-running sk-test-* VM. Useful for
|
|
re-running just the harness after an install succeeded.
|
|
#>
|
|
[CmdletBinding()]
|
|
param(
|
|
# Default suite: Ubuntu LTSes via Multipass + Debian 12 / Rocky 9
|
|
# via Vagrant + Hyper-V. Fedora is *not* in the default — Vagrant
|
|
# Cloud's generic/fedora boxes stop at fedora39 (EOL Nov 2024) and no
|
|
# newer pre-built Hyper-V box exists. Use `-Only fedora` to opt in
|
|
# once you've pinned a box you trust. Extra opt-in homelab targets
|
|
# (not in the default either): alma9, debian11, opensuse15.
|
|
[string[]] $Distros = @('ubuntu22','ubuntu24','debian12','rocky9'),
|
|
[string] $Only,
|
|
[switch] $Keep,
|
|
[string] $ReuseVm,
|
|
[int] $Cpus = 2,
|
|
[int] $MemoryGB = 4,
|
|
[int] $DiskGB = 15
|
|
)
|
|
|
|
# NB: keep ErrorActionPreference at Continue. Multipass writes harmless
|
|
# warnings to stderr (e.g. "cannot set permissions" when transferring to
|
|
# NTFS) and 'Stop' turns those into fatal script errors.
|
|
$ErrorActionPreference = 'Continue'
|
|
$ProgressPreference = 'SilentlyContinue'
|
|
|
|
# DistroMap drives both dispatch and image/box selection.
|
|
# backend: 'multipass' uses the Canonical Ubuntu images; 'vagrant' uses
|
|
# the Hyper-V provider with generic/* boxes. The login user differs by
|
|
# backend (ubuntu vs vagrant), which matters for chown on the harness dir.
|
|
$DistroMap = @{
|
|
'ubuntu22' = @{ backend = 'multipass'; image = '22.04'; user = 'ubuntu' }
|
|
'ubuntu24' = @{ backend = 'multipass'; image = '24.04'; user = 'ubuntu' }
|
|
'debian12' = @{ backend = 'vagrant'; image = 'generic/debian12'; user = 'vagrant' }
|
|
# fedora39 went EOL Nov 2024; use fedora40 which is supported.
|
|
'fedora' = @{ backend = 'vagrant'; image = 'generic/fedora40'; user = 'vagrant' }
|
|
'rocky9' = @{ backend = 'vagrant'; image = 'generic/rocky9'; user = 'vagrant' }
|
|
# Opt-in extras (homelab-ish coverage; all have generic/* Hyper-V boxes):
|
|
'alma9' = @{ backend = 'vagrant'; image = 'generic/alma9'; user = 'vagrant' }
|
|
'debian11' = @{ backend = 'vagrant'; image = 'generic/debian11'; user = 'vagrant' }
|
|
'opensuse15' = @{ backend = 'vagrant'; image = 'generic/opensuse15'; user = 'vagrant' }
|
|
|
|
# NO debian13 (trixie) — not an oversight. As of 2026-08 no Debian 13 box
|
|
# publishes a Hyper-V provider, and this harness is Hyper-V only:
|
|
# generic/debian13 does not exist (the generic/* line stops at debian12)
|
|
# debian/trixie64 libvirt only
|
|
# bento/debian-13 parallels, virtualbox, vmware_desktop
|
|
# Multipass is Ubuntu images only, so it cannot cover it either. Adding a
|
|
# name here would just fail at launch.
|
|
#
|
|
# Trixie is covered instead by the `python-provisioning` job in
|
|
# .github/workflows/scripts-ci.yml, which runs provision_python against the
|
|
# real debian:13 repos in a container. That catches the whole class of
|
|
# "a new distro release dropped the Python we ask for" (issue #99) without
|
|
# needing a VM. What is still NOT covered on trixie is the full end-to-end
|
|
# install: systemd unit, nginx vhost, live API harness. Re-check box
|
|
# availability periodically and add it here the moment one lands.
|
|
}
|
|
|
|
if ($Only) {
|
|
# Select from the full DistroMap (not just the default suite) so opt-in
|
|
# distros like fedora/alma9 work, in the order the user listed them.
|
|
$requested = $Only -split ','
|
|
$Distros = @($requested | Where-Object { $DistroMap.ContainsKey($_) })
|
|
}
|
|
|
|
$RepoRoot = (Resolve-Path "$PSScriptRoot\..\..").Path
|
|
$RunId = Get-Date -Format 'yyyyMMdd-HHmmss'
|
|
$OutDir = Join-Path $PSScriptRoot "output\$RunId"
|
|
$null = New-Item -ItemType Directory -Force -Path $OutDir
|
|
|
|
# Seed state.json immediately so any early refresh of the report shows
|
|
# "running" rather than treating an empty dir as a failed run.
|
|
Set-Content -Path (Join-Path $OutDir 'state.json') -Value '{"running":true,"vms":{}}' -Encoding utf8
|
|
|
|
# --- locate backends -----------------------------------------------------
|
|
function Find-Exe {
|
|
param([string[]] $Paths, [string] $Cmd)
|
|
foreach ($p in $Paths) { if (Test-Path $p) { return $p } }
|
|
return (Get-Command $Cmd -ErrorAction SilentlyContinue).Source
|
|
}
|
|
|
|
$MpExe = Find-Exe -Cmd 'multipass' -Paths @(
|
|
'C:\Program Files\Multipass\bin\multipass.exe',
|
|
"$env:LOCALAPPDATA\Programs\Multipass\bin\multipass.exe"
|
|
)
|
|
$VgExe = Find-Exe -Cmd 'vagrant' -Paths @(
|
|
'C:\Program Files\Vagrant\bin\vagrant.exe',
|
|
"$env:LOCALAPPDATA\Programs\Vagrant\bin\vagrant.exe"
|
|
)
|
|
|
|
# Filter distros down to whichever backends are actually installed.
|
|
$missing = @()
|
|
$Distros = $Distros | Where-Object {
|
|
$entry = $DistroMap[$_]
|
|
if (-not $entry) { return $false }
|
|
if ($entry.backend -eq 'multipass' -and -not $MpExe) { $missing += "$_ (needs Multipass)"; return $false }
|
|
if ($entry.backend -eq 'vagrant' -and -not $VgExe) { $missing += "$_ (needs Vagrant)"; return $false }
|
|
$true
|
|
}
|
|
|
|
if (-not $Distros -or $Distros.Count -eq 0) {
|
|
Write-Host "No runnable distros. Install Multipass and/or Vagrant first." -ForegroundColor Red
|
|
if ($missing) { $missing | ForEach-Object { Write-Host " skipped: $_" -ForegroundColor Yellow } }
|
|
exit 2
|
|
}
|
|
|
|
# Vagrant + Hyper-V requires the process itself to run elevated. Being in
|
|
# the Hyper-V Administrators group is necessary but not sufficient.
|
|
$needsAdmin = $Distros | Where-Object { $DistroMap[$_].backend -eq 'vagrant' }
|
|
if ($needsAdmin) {
|
|
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(
|
|
[Security.Principal.WindowsBuiltInRole]::Administrator)
|
|
if (-not $isAdmin) {
|
|
Write-Host "" -ForegroundColor Red
|
|
Write-Host "Vagrant + Hyper-V requires an elevated PowerShell." -ForegroundColor Red
|
|
Write-Host "Requested distros that need it: $($needsAdmin -join ', ')" -ForegroundColor Red
|
|
Write-Host "" -ForegroundColor Red
|
|
Write-Host "Either:" -ForegroundColor Yellow
|
|
Write-Host " 1. Right-click PowerShell -> Run as Administrator, then re-run this script." -ForegroundColor Yellow
|
|
Write-Host " 2. Or run with -Only ubuntu22,ubuntu24 to skip Vagrant distros." -ForegroundColor Yellow
|
|
exit 2
|
|
}
|
|
}
|
|
|
|
Write-Host "========================================" -ForegroundColor Cyan
|
|
Write-Host " ServerKit E2E Test - run $RunId" -ForegroundColor Cyan
|
|
if ($MpExe) { Write-Host " Multipass: $MpExe" -ForegroundColor Cyan }
|
|
if ($VgExe) { Write-Host " Vagrant: $VgExe" -ForegroundColor Cyan }
|
|
Write-Host " Distros: $($Distros -join ', ')" -ForegroundColor Cyan
|
|
if ($missing) { $missing | ForEach-Object { Write-Host " skipped: $_" -ForegroundColor Yellow } }
|
|
Write-Host " Output: $OutDir" -ForegroundColor Cyan
|
|
Write-Host "========================================" -ForegroundColor Cyan
|
|
|
|
# NB: 'mp' is a built-in PS alias for Move-ItemProperty, so we must NOT
|
|
# define a helper function named Mp. Use $MpExe directly everywhere.
|
|
|
|
# --- live state + report regen ------------------------------------------
|
|
$script:State = @{ running = $true; vms = @{} }
|
|
function Save-State {
|
|
$json = $script:State | ConvertTo-Json -Depth 5 -Compress
|
|
Set-Content -Path (Join-Path $OutDir 'state.json') -Value $json -Encoding utf8
|
|
if (Get-Command python -ErrorAction SilentlyContinue) {
|
|
& python (Join-Path $PSScriptRoot 'report.py') $OutDir 2>&1 | Out-Null
|
|
}
|
|
}
|
|
function Set-VmStage {
|
|
param([string]$Vm, [string]$Stage)
|
|
if (-not $script:State.vms.ContainsKey($Vm)) { $script:State.vms[$Vm] = @{} }
|
|
$script:State.vms[$Vm].stage = $Stage
|
|
Save-State
|
|
}
|
|
|
|
function Test-VmInstall {
|
|
param([string]$Vm, [string]$VmOut, [string]$Tarball, [string]$VmInstallScript, [string]$HarnessDir)
|
|
|
|
New-Item -ItemType Directory -Force -Path $VmOut | Out-Null
|
|
$statusFile = Join-Path $VmOut 'install-status'
|
|
|
|
Set-VmStage -Vm $Vm -Stage 'transferring source'
|
|
Write-Host " [$Vm] transferring source ($([math]::Round((Get-Item $Tarball).Length/1MB,1)) MB)..." -ForegroundColor DarkCyan
|
|
& $MpExe transfer $Tarball "${Vm}:/tmp/serverkit-src.tar.gz" 2>&1 | Out-Null
|
|
& $MpExe transfer $VmInstallScript "${Vm}:/tmp/vm-install.sh" 2>&1 | Out-Null
|
|
& $MpExe exec $Vm -- sudo mkdir -p /opt/serverkit-src 2>&1 | Out-Null
|
|
& $MpExe exec $Vm -- sudo tar -xzf /tmp/serverkit-src.tar.gz -C /opt/serverkit-src 2>&1 | Out-Null
|
|
& $MpExe exec $Vm -- sudo chmod +x /tmp/vm-install.sh 2>&1 | Out-Null
|
|
|
|
Set-VmStage -Vm $Vm -Stage 'installing (npm ci + build, ~10 min)'
|
|
Write-Host " [$Vm] running install (this is the long part)..." -ForegroundColor DarkCyan
|
|
& $MpExe exec $Vm -- sh -c "sudo bash /tmp/vm-install.sh > /tmp/vm-install-stdout.log 2>&1; rc=`$?; echo EXIT_RC=`$rc; exit `$rc" `
|
|
| Out-File (Join-Path $VmOut 'install.log') -Encoding utf8
|
|
$installRC = $LASTEXITCODE
|
|
|
|
& $MpExe transfer "${Vm}:/var/log/serverkit-test-install.log" "$VmOut\vm-install.log" 2>&1 | Out-Null
|
|
& $MpExe transfer "${Vm}:/tmp/serverkit-install-status" $statusFile 2>&1 | Out-Null
|
|
& $MpExe transfer "${Vm}:/tmp/serverkit-runtime.json" (Join-Path $VmOut 'runtime.json') 2>&1 | Out-Null
|
|
$rawStatus = if (Test-Path $statusFile) { Get-Content $statusFile -Raw -ErrorAction SilentlyContinue } else { $null }
|
|
if (-not $rawStatus -or "$rawStatus".Trim() -eq '') {
|
|
$fallback = if ($installRC -eq 0) { 'OK' } else { 'FAIL' }
|
|
Set-Content -Path $statusFile -Value $fallback -Encoding ascii
|
|
}
|
|
& $MpExe exec $Vm -- sudo journalctl -u serverkit --no-pager -n 500 2>&1 `
|
|
| Out-File (Join-Path $VmOut 'journalctl.log') -Encoding utf8
|
|
|
|
$rawStatus = Get-Content $statusFile -Raw -ErrorAction SilentlyContinue
|
|
$status = if ($rawStatus) { "$rawStatus".Trim() } else { 'FAIL' }
|
|
Write-Host " [$Vm] install status: $status" -ForegroundColor $(if ($status -eq 'OK') { 'Green' } else { 'Red' })
|
|
Save-State # regenerate so install log + journalctl show up
|
|
|
|
if ($status -ne 'OK') {
|
|
Set-VmStage -Vm $Vm -Stage 'install failed'
|
|
return $status
|
|
}
|
|
|
|
Set-VmStage -Vm $Vm -Stage 'running pytest'
|
|
Write-Host " [$Vm] running pytest harness..." -ForegroundColor DarkCyan
|
|
& $MpExe exec $Vm -- sudo mkdir -p /opt/serverkit-test 2>&1 | Out-Null
|
|
& $MpExe exec $Vm -- sudo chown ubuntu:ubuntu /opt/serverkit-test 2>&1 | Out-Null
|
|
Get-ChildItem $HarnessDir -File | ForEach-Object {
|
|
& $MpExe transfer $_.FullName "${Vm}:/opt/serverkit-test/$($_.Name)" 2>&1 | Out-Null
|
|
}
|
|
& $MpExe exec $Vm -- sudo /opt/serverkit/venv/bin/pip install -r /opt/serverkit-test/requirements.txt 2>&1 `
|
|
| Out-File (Join-Path $VmOut 'harness-deps.log') -Encoding utf8
|
|
& $MpExe exec $Vm -- sh -c "sudo /opt/serverkit/venv/bin/python -m pytest /opt/serverkit-test --json-report --json-report-file=/opt/serverkit-test/pytest-report.json -v > /tmp/pytest-stdout.log 2>&1; rc=`$?; echo EXIT_RC=`$rc; exit `$rc" `
|
|
| Out-File (Join-Path $VmOut 'pytest.log') -Encoding utf8
|
|
& $MpExe transfer "${Vm}:/tmp/pytest-stdout.log" (Join-Path $VmOut 'pytest-stdout.log') 2>&1 | Out-Null
|
|
& $MpExe transfer "${Vm}:/opt/serverkit-test/pytest-report.json" (Join-Path $VmOut 'pytest-report.json') 2>&1 | Out-Null
|
|
|
|
Set-VmStage -Vm $Vm -Stage 'done'
|
|
return $status
|
|
}
|
|
|
|
function Test-VagrantInstall {
|
|
# Vagrant equivalent of Test-VmInstall. Uses 'vagrant upload' for file
|
|
# transfer (added in 2.2.7) and 'vagrant ssh -c' for exec. Each VM
|
|
# gets its own VAGRANT_CWD so multiple distros can coexist.
|
|
param([string]$Vm, [string]$VmOut, [string]$Tarball, [string]$VmInstallScript, [string]$HarnessDir, [string]$Box, [string]$User)
|
|
|
|
New-Item -ItemType Directory -Force -Path $VmOut | Out-Null
|
|
$statusFile = Join-Path $VmOut 'install-status'
|
|
|
|
# Per-VM Vagrant working dir holding the Vagrantfile + .vagrant state.
|
|
$vagrantSrc = Join-Path $PSScriptRoot 'vagrant\Vagrantfile'
|
|
$vagrantCwd = Join-Path $OutDir "vagrant-state\$Vm"
|
|
New-Item -ItemType Directory -Force -Path $vagrantCwd | Out-Null
|
|
Copy-Item $vagrantSrc (Join-Path $vagrantCwd 'Vagrantfile') -Force
|
|
|
|
$env:VAGRANT_CWD = $vagrantCwd
|
|
$env:SK_BOX = $Box
|
|
$env:SK_VM_NAME = $Vm
|
|
$env:SK_CPUS = "$Cpus"
|
|
$env:SK_MEMORY_MB = "$($MemoryGB * 1024)"
|
|
|
|
Set-VmStage -Vm $Vm -Stage 'launching VM (vagrant up, ~5-10 min first time)'
|
|
Write-Host " [$Vm] vagrant up... (full output below; also saved to vagrant-up.log)" -ForegroundColor DarkCyan
|
|
# Tee so the operator can see download progress + any prompts live,
|
|
# and the log file still gets a copy for later inspection. Pipe to
|
|
# Out-Host at the end so the pipeline output is CONSUMED — otherwise
|
|
# it leaks into this function's return stream and corrupts $status.
|
|
& $VgExe up --provider=hyperv 2>&1 | Tee-Object -FilePath (Join-Path $VmOut 'vagrant-up.log') | Out-Host
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Set-Content -Path $statusFile -Value 'LAUNCH_FAILED' -Encoding ascii
|
|
Set-VmStage -Vm $Vm -Stage 'launch failed'
|
|
return 'LAUNCH_FAILED'
|
|
}
|
|
|
|
Set-VmStage -Vm $Vm -Stage 'transferring source'
|
|
Write-Host " [$Vm] transferring source..." -ForegroundColor DarkCyan
|
|
& $VgExe upload $Tarball /tmp/serverkit-src.tar.gz 2>&1 | Out-Null
|
|
& $VgExe upload $VmInstallScript /tmp/vm-install.sh 2>&1 | Out-Null
|
|
& $VgExe ssh -c "sudo mkdir -p /opt/serverkit-src && sudo tar -xzf /tmp/serverkit-src.tar.gz -C /opt/serverkit-src && sudo chmod +x /tmp/vm-install.sh" 2>&1 | Out-Null
|
|
|
|
Set-VmStage -Vm $Vm -Stage 'installing (npm ci + build, ~10 min)'
|
|
Write-Host " [$Vm] running install..." -ForegroundColor DarkCyan
|
|
& $VgExe ssh -c "sudo bash /tmp/vm-install.sh > /tmp/vm-install-stdout.log 2>&1; rc=`$?; echo EXIT_RC=`$rc; exit `$rc" `
|
|
2>&1 | Out-File (Join-Path $VmOut 'install.log') -Encoding utf8
|
|
$installRC = $LASTEXITCODE
|
|
|
|
# Pull canonical logs back. vagrant scp would need a plugin; we
|
|
# cat-then-redirect via vagrant ssh, which is universal.
|
|
& $VgExe ssh -c "sudo cat /var/log/serverkit-test-install.log" 2>$null `
|
|
| Out-File (Join-Path $VmOut 'vm-install.log') -Encoding utf8
|
|
& $VgExe ssh -c "sudo cat /tmp/serverkit-install-status" 2>$null `
|
|
| Out-File $statusFile -Encoding ascii
|
|
& $VgExe ssh -c "sudo cat /tmp/serverkit-runtime.json" 2>$null `
|
|
| Out-File (Join-Path $VmOut 'runtime.json') -Encoding utf8
|
|
# Get-Content -Raw returns $null for empty/missing files; guard before
|
|
# calling .Trim() so a failed status capture doesn't crash the run.
|
|
$rawStatus = if (Test-Path $statusFile) { Get-Content $statusFile -Raw -ErrorAction SilentlyContinue } else { $null }
|
|
if (-not $rawStatus -or "$rawStatus".Trim() -eq '') {
|
|
$fallback = if ($installRC -eq 0) { 'OK' } else { 'FAIL' }
|
|
Set-Content -Path $statusFile -Value $fallback -Encoding ascii
|
|
}
|
|
& $VgExe ssh -c "sudo journalctl -u serverkit --no-pager -n 500" 2>$null `
|
|
| Out-File (Join-Path $VmOut 'journalctl.log') -Encoding utf8
|
|
|
|
$rawStatus = Get-Content $statusFile -Raw -ErrorAction SilentlyContinue
|
|
$status = if ($rawStatus) { "$rawStatus".Trim() } else { 'FAIL' }
|
|
Write-Host " [$Vm] install status: $status" -ForegroundColor $(if ($status -eq 'OK') { 'Green' } else { 'Red' })
|
|
Save-State
|
|
|
|
if ($status -ne 'OK') {
|
|
Set-VmStage -Vm $Vm -Stage 'install failed'
|
|
return $status
|
|
}
|
|
|
|
Set-VmStage -Vm $Vm -Stage 'running pytest'
|
|
Write-Host " [$Vm] running pytest harness..." -ForegroundColor DarkCyan
|
|
& $VgExe ssh -c "sudo mkdir -p /opt/serverkit-test && sudo chown ${User}:${User} /opt/serverkit-test" 2>&1 | Out-Null
|
|
Get-ChildItem $HarnessDir -File | ForEach-Object {
|
|
& $VgExe upload $_.FullName "/opt/serverkit-test/$($_.Name)" 2>&1 | Out-Null
|
|
}
|
|
& $VgExe ssh -c "sudo /opt/serverkit/venv/bin/pip install -r /opt/serverkit-test/requirements.txt" 2>&1 `
|
|
| Out-File (Join-Path $VmOut 'harness-deps.log') -Encoding utf8
|
|
& $VgExe ssh -c "sudo /opt/serverkit/venv/bin/python -m pytest /opt/serverkit-test --json-report --json-report-file=/opt/serverkit-test/pytest-report.json -v > /tmp/pytest-stdout.log 2>&1; rc=`$?; echo EXIT_RC=`$rc; exit `$rc" `
|
|
2>&1 | Out-File (Join-Path $VmOut 'pytest.log') -Encoding utf8
|
|
& $VgExe ssh -c "sudo cat /tmp/pytest-stdout.log" 2>$null | Out-File (Join-Path $VmOut 'pytest-stdout.log') -Encoding utf8
|
|
& $VgExe ssh -c "sudo cat /opt/serverkit-test/pytest-report.json" 2>$null | Out-File (Join-Path $VmOut 'pytest-report.json') -Encoding utf8
|
|
|
|
Set-VmStage -Vm $Vm -Stage 'done'
|
|
return $status
|
|
}
|
|
|
|
function Remove-VagrantVm {
|
|
param([string]$Vm)
|
|
$vagrantCwd = Join-Path $OutDir "vagrant-state\$Vm"
|
|
if (-not (Test-Path $vagrantCwd)) { return }
|
|
$env:VAGRANT_CWD = $vagrantCwd
|
|
& $VgExe destroy -f 2>&1 | Out-Null
|
|
}
|
|
|
|
# --- pack source --------------------------------------------------------
|
|
# Pin to Windows' bsdtar when present: if this script is launched from a
|
|
# Git Bash session, /usr/bin (GNU tar) shadows System32 on PATH, and GNU
|
|
# tar reads "C:\..." as a remote host:path and fails with
|
|
# "Cannot connect to C: resolve failed".
|
|
$TarExe = "$env:SystemRoot\System32\tar.exe"
|
|
if (-not (Test-Path $TarExe)) { $TarExe = 'tar' }
|
|
$Tarball = Join-Path $OutDir 'serverkit-src.tar.gz'
|
|
Write-Host "`n[1/4] Packing local working tree -> $Tarball" -ForegroundColor Yellow
|
|
Push-Location $RepoRoot
|
|
try {
|
|
& $TarExe --exclude='.git' `
|
|
--exclude='venv' `
|
|
--exclude='.venv' `
|
|
--exclude='.venv-wsl' `
|
|
--exclude='node_modules' `
|
|
--exclude='dist' `
|
|
--exclude='__pycache__' `
|
|
--exclude='instance' `
|
|
--exclude='target' `
|
|
--exclude='build' `
|
|
--exclude='*.exe' `
|
|
--exclude='*.msi' `
|
|
--exclude='scripts/test/output' `
|
|
-czf $Tarball -C $RepoRoot .
|
|
if ($LASTEXITCODE -ne 0) { throw "tar failed (exit $LASTEXITCODE)" }
|
|
} finally {
|
|
Pop-Location
|
|
}
|
|
Write-Host (" Tarball: {0:N1} MB" -f ((Get-Item $Tarball).Length / 1MB))
|
|
|
|
# --- main loop ----------------------------------------------------------
|
|
$results = @()
|
|
$liveVMs = @()
|
|
$vmInstallScript = Join-Path $PSScriptRoot 'vm-install.sh'
|
|
$cloudInit = Join-Path $PSScriptRoot 'cloud-init\base.yaml'
|
|
$harnessDir = Join-Path $PSScriptRoot 'harness'
|
|
|
|
# Pre-populate stages and seed the live report, then open it in the browser.
|
|
foreach ($d in $Distros) {
|
|
$vmName = if ($ReuseVm) { $ReuseVm } else { "sk-test-$d-$RunId" }
|
|
$script:State.vms[$vmName] = @{ stage = 'pending' }
|
|
if ($ReuseVm) { break }
|
|
}
|
|
Save-State
|
|
$reportPath = Join-Path $OutDir 'report.html'
|
|
if (Test-Path $reportPath) {
|
|
Write-Host "`nLive report: $reportPath (opening in browser)" -ForegroundColor Cyan
|
|
Start-Process $reportPath
|
|
}
|
|
|
|
if ($ReuseVm) {
|
|
Write-Host "`n[2-3/4] Reusing VM: $ReuseVm" -ForegroundColor Yellow
|
|
$vmOut = Join-Path $OutDir $ReuseVm
|
|
# Reuse only supports Multipass for now (Vagrant VMs are tied to a
|
|
# specific VAGRANT_CWD that doesn't exist outside the original run).
|
|
$status = Test-VmInstall -Vm $ReuseVm -VmOut $vmOut -Tarball $Tarball `
|
|
-VmInstallScript $vmInstallScript -HarnessDir $harnessDir
|
|
$results += @{ Name = $ReuseVm; Status = $status }
|
|
$liveVMs += @{ Name = $ReuseVm; Backend = 'multipass' }
|
|
} else {
|
|
Write-Host "`n[2/4] Launching + testing $($Distros.Count) VM(s) sequentially..." -ForegroundColor Yellow
|
|
foreach ($d in $Distros) {
|
|
$vmName = "sk-test-$d-$RunId"
|
|
$entry = $DistroMap[$d]
|
|
$vmOut = Join-Path $OutDir $vmName
|
|
|
|
Write-Host "`n--- $d ($($entry.backend)) ---" -ForegroundColor Yellow
|
|
|
|
if ($entry.backend -eq 'multipass') {
|
|
Write-Host " Launching $vmName from image $($entry.image)..." -ForegroundColor DarkCyan
|
|
& $MpExe launch $entry.image --name $vmName --cpus $Cpus --memory "${MemoryGB}G" --disk "${DiskGB}G" --cloud-init $cloudInit
|
|
if ($LASTEXITCODE -ne 0) {
|
|
Write-Host " [FAIL] $vmName launch failed" -ForegroundColor Red
|
|
$results += @{ Name = $vmName; Status = 'LAUNCH_FAILED' }
|
|
continue
|
|
}
|
|
Write-Host " [OK] $vmName launched" -ForegroundColor Green
|
|
$liveVMs += @{ Name = $vmName; Backend = 'multipass' }
|
|
$status = Test-VmInstall -Vm $vmName -VmOut $vmOut -Tarball $Tarball `
|
|
-VmInstallScript $vmInstallScript -HarnessDir $harnessDir
|
|
$results += @{ Name = $vmName; Status = $status }
|
|
}
|
|
elseif ($entry.backend -eq 'vagrant') {
|
|
Write-Host " Bringing up $vmName via Vagrant (box: $($entry.image))..." -ForegroundColor DarkCyan
|
|
$liveVMs += @{ Name = $vmName; Backend = 'vagrant' }
|
|
$status = Test-VagrantInstall -Vm $vmName -VmOut $vmOut -Tarball $Tarball `
|
|
-VmInstallScript $vmInstallScript -HarnessDir $harnessDir `
|
|
-Box $entry.image -User $entry.user
|
|
$results += @{ Name = $vmName; Status = $status }
|
|
}
|
|
}
|
|
}
|
|
|
|
# --- report -------------------------------------------------------------
|
|
Write-Host "`n[4/4] Finalizing HTML report..." -ForegroundColor Yellow
|
|
$script:State.running = $false
|
|
Save-State
|
|
$reportHtml = Join-Path $OutDir 'report.html'
|
|
if (Test-Path $reportHtml) {
|
|
Write-Host " Report: $reportHtml" -ForegroundColor Green
|
|
} else {
|
|
Write-Host " python not on PATH. Raw output: $OutDir" -ForegroundColor Yellow
|
|
$reportHtml = $null
|
|
}
|
|
|
|
# --- teardown -----------------------------------------------------------
|
|
if ($Keep -or $ReuseVm) {
|
|
Write-Host "`nVMs left running:" -ForegroundColor Yellow
|
|
$liveVMs | ForEach-Object { Write-Host " - $($_.Name) ($($_.Backend))" }
|
|
} else {
|
|
Write-Host "`nTearing down VMs..." -ForegroundColor Yellow
|
|
foreach ($vm in $liveVMs) {
|
|
if ($vm.Backend -eq 'multipass') {
|
|
& $MpExe delete $vm.Name 2>&1 | Out-Null
|
|
} elseif ($vm.Backend -eq 'vagrant') {
|
|
Remove-VagrantVm -Vm $vm.Name
|
|
}
|
|
}
|
|
if ($MpExe) { & $MpExe purge 2>&1 | Out-Null }
|
|
}
|
|
|
|
# --- summary ------------------------------------------------------------
|
|
$failed = @($results | Where-Object { $_.Status -ne 'OK' })
|
|
Write-Host ""
|
|
if ($failed.Count -eq 0 -and $results.Count -gt 0) {
|
|
Write-Host "ALL GREEN - $($results.Count) VM(s) passed." -ForegroundColor Green
|
|
exit 0
|
|
} else {
|
|
Write-Host "FAILURES on $($failed.Count) VM(s):" -ForegroundColor Red
|
|
$failed | ForEach-Object { Write-Host " - $($_.Name): $($_.Status)" -ForegroundColor Red }
|
|
exit 1
|
|
}
|