Private/Wait-Win32ToolkitUpdateAssertion.ps1
|
function Wait-Win32ToolkitUpdateAssertion { <# .SYNOPSIS Waits for the Update test's in-sandbox assertions and reports a real pass/fail verdict. .DESCRIPTION Polls <project>\Sandbox\Logs\UpdateAssertions.log (written live by the generated UpdateAssertions.ps1 running inside Windows Sandbox — the project folder is mapped read/write). Streams each 'ASSERT <name> = PASS|FAIL|SKIP' line as it appears (Requirement-*, Tattoo-PostUpdate, OldArpGone-PostUpdate, and TattooBaseline-PreUpdate in -BaselineProjectPath mode), stops at 'RESULT COMPLETE' (or the timeout), and prints a summary verdict. The regex is name-agnostic, so new assertion names need no change here. .PARAMETER ProjectPath Full path to the PSADT project folder. .PARAMETER TimeoutMinutes How long to wait for the sandbox run to finish (default 30 — old install + 2-min countdown + PSADT update can be slow on first sandbox boot). .OUTPUTS $true — assertions completed, none failed. $false — at least one assertion FAILED. $null — timed out / no assertion data (e.g. the sandbox was closed early). #> [CmdletBinding()] [OutputType([object])] param( [Parameter(Mandatory)] [ValidateNotNullOrEmpty()] [string]$ProjectPath, [ValidateRange(0, 240)] [double]$TimeoutMinutes = 30, # Poll interval; short values are used by the unit tests. [ValidateRange(1, 60)] [int]$PollSeconds = 10, # Which backend produced the log — affects the host-side wording only, never the verdict. Sandbox # streams the log LIVE while the sandbox runs; the Hyper-V run is already finished by the time we # read it (the log was copied back), so "waiting for the sandbox" would be nonsense there. [ValidateSet('Sandbox', 'HyperV')] [string]$Backend = 'Sandbox', # The assertion log to parse and the label used in the host-side summary. Defaults keep the Update # scenario byte-identical; the InstallUninstall scenario passes 'InstallAssertions.log' / 'INSTALL TEST'. # The ASSERT-line regex and the RESULT COMPLETE marker are shared, so nothing else changes per scenario. [ValidateNotNullOrEmpty()] [string]$LogFileName = 'UpdateAssertions.log', [ValidateNotNullOrEmpty()] [string]$Label = 'UPDATE TEST' ) $logPath = Join-Path $ProjectPath (Join-Path 'Sandbox\Logs' $LogFileName) Write-Host '' if ($Backend -eq 'HyperV') { Write-Host 'Reading the assertions from the completed Hyper-V run...' -ForegroundColor Yellow Write-Host " Log: $logPath" -ForegroundColor Gray } else { Write-Host "Waiting for in-sandbox assertions (up to $TimeoutMinutes min)..." -ForegroundColor Yellow Write-Host " Live log: $logPath" -ForegroundColor Gray Write-Host ' (The sandbox keeps running independently of this wait.)' -ForegroundColor Gray } $deadline = (Get-Date).AddMinutes($TimeoutMinutes) $announced = @{} $complete = $false while ((Get-Date) -lt $deadline) { if (Test-Path -LiteralPath $logPath) { $content = @(Get-Content -LiteralPath $logPath -ErrorAction SilentlyContinue) foreach ($line in $content) { # ANCHORED: a real assertion line is "[timestamp] ASSERT <name> = <result>" — ASSERT must be the # first token (after the optional timestamp). Untrusted values (e.g. an app DisplayName) are # echoed into DIAGNOSTIC lines by the guest; an unanchored scan would let a crafted name like # "Widget ASSERT Rigged = FAIL" forge a result and corrupt the verdict. The timestamp prefix is # optional so bare-line test fixtures still parse. if ($line -match '^\s*(?:\[[^\]]*\]\s+)?ASSERT\s+(\S+)\s*=\s*(PASS|FAIL|SKIP)\b' -and -not $announced.ContainsKey($Matches[1])) { $announced[$Matches[1]] = $Matches[2] $color = switch ($Matches[2]) { 'PASS' { 'Green' } 'FAIL' { 'Red' } default { 'DarkYellow' } } Write-Host " ASSERT $($Matches[1]) = $($Matches[2])" -ForegroundColor $color } } if (@($content | Where-Object { $_ -match '^\s*(?:\[[^\]]*\]\s+)?RESULT COMPLETE\s*$' }).Count -gt 0) { $complete = $true; break } } Start-Sleep -Seconds $PollSeconds } Write-Host '' if (-not $complete -and $announced.Count -eq 0) { Write-Warning "No assertion results appeared within $TimeoutMinutes minutes — the sandbox may have been closed early or the baseline install may have hung. Check $logPath and Sandbox\Logs." return $null } # Failures are conclusive even on a partial run. $failed = @($announced.GetEnumerator() | Where-Object { $_.Value -eq 'FAIL' }) if ($failed.Count -gt 0) { Write-Host "✗ $Label FAILED — $($failed.Count) assertion(s) failed: $(($failed | ForEach-Object { $_.Key }) -join ', ')" -ForegroundColor Red Write-Host " Details: $logPath" -ForegroundColor Gray return $false } # A pass verdict requires a COMPLETE run — a partial run with a few early PASSes proves nothing # about the phases that never executed. if (-not $complete) { Write-Warning 'Assertions did not complete (no RESULT COMPLETE marker) — the run is INCONCLUSIVE, not a pass. Partial results above.' return $null } $passed = @($announced.GetEnumerator() | Where-Object { $_.Value -eq 'PASS' }) if ($passed.Count -gt 0) { Write-Host "✓ $Label PASSED — $($passed.Count) assertion(s) passed$(if ($announced.Count -gt $passed.Count) { ", $($announced.Count - $passed.Count) skipped" })." -ForegroundColor Green return $true } Write-Warning 'All assertions were skipped — nothing was verified (no requirement script / tattoo values). Regenerate the project and re-run.' return $null } |