This commit is contained in:
2026-06-12 20:56:48 -05:00
parent 8e1819caa7
commit 65a939027d
31 changed files with 1411 additions and 155 deletions
+30 -8
View File
@@ -16,8 +16,8 @@ Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
# Coverage-harness orchestrator. Drives the Vagrant Windows VM through every
# scenario directory under vm\<scenario>\install.toml using the per-scenario
# guest scripts under scripts\windows-vm\coverage\<scenario>.ps1.
# scenario directory under vm\<scenario>\<AppName>-install.toml using the
# per-scenario guest scripts under scripts\windows-vm\coverage\<scenario>.ps1.
#
# All install/uninstall side effects happen INSIDE the VM. The host only:
# 1. Builds covenant-setup.exe (release).
@@ -73,6 +73,21 @@ function Open-HyperVViewer {
Start-Process -FilePath $vmConnect -ArgumentList @("localhost", $VmName) | Out-Null
}
# The engine enforces the <AppName>-install.toml naming convention, so each
# scenario directory carries its own manifest file name; discover it instead
# of hardcoding install.toml.
function Get-ScenarioManifest {
param([Parameter(Mandatory)][string]$ScenarioDir)
$candidates = @(Get-ChildItem -LiteralPath $ScenarioDir -Filter "*install.toml" -File | Sort-Object Name)
if ($candidates.Count -eq 0) {
throw "Scenario manifest (*install.toml) not found in: $ScenarioDir"
}
if ($candidates.Count -gt 1) {
throw "Multiple *install.toml manifests found in: $ScenarioDir"
}
return $candidates[0]
}
$repoRoot = Split-Path -Parent $PSScriptRoot
$releaseExe = Join-Path $repoRoot "target\release\covenant-setup.exe"
$outputRoot = Join-Path $repoRoot "dist\vagrant-coverage"
@@ -98,10 +113,7 @@ if (-not (Test-Path -LiteralPath $releaseExe)) {
# Stage the payload tree for each scenario manifest that references
# payload\covenant-setup.exe (relative to the manifest dir).
foreach ($scenario in $Scenarios) {
$manifest = Join-Path $repoRoot "vm\$scenario\install.toml"
if (-not (Test-Path -LiteralPath $manifest)) {
throw "Scenario manifest not found: $manifest"
}
$manifest = (Get-ScenarioManifest -ScenarioDir (Join-Path $repoRoot "vm\$scenario")).FullName
$payloadDir = Join-Path $repoRoot "vm\$scenario\payload"
$manifestText = Get-Content -LiteralPath $manifest -Raw
if ($manifestText -match 'payload\\\\covenant-setup\.exe' -or $manifestText -match 'payload[\\/]covenant-setup\.exe') {
@@ -122,7 +134,16 @@ try {
Open-HyperVViewer -VmName $VmName
}
$waitForShellCommand = "for (`$i = 0; `$i -lt 90; `$i++) { if (Get-Process -Name explorer -ErrorAction SilentlyContinue) { exit 0 }; Start-Sleep -Seconds 2 }; Write-Error 'Explorer shell did not start in time.'; exit 1"
$waitForShellCommand = @(
"for (`$i = 0; `$i -lt 90; `$i++) { if (Get-Process -Name explorer -ErrorAction SilentlyContinue) { exit 0 }; Start-Sleep -Seconds 2 }"
"`$winlogon = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon'"
"Write-Output ('consoleUser=' + (Get-CimInstance Win32_ComputerSystem).UserName)"
"Write-Output ('computerName=' + `$env:COMPUTERNAME)"
"foreach (`$name in 'AutoAdminLogon','ForceAutoLogon','DefaultUserName','DefaultDomainName','AutoLogonCount') { `$value = `$winlogon.PSObject.Properties[`$name]; Write-Output (`$name + '=' + `$(if (`$value) { `$value.Value } else { '<absent>' })) }"
"Write-Output ('DefaultPassword=' + `$(if (`$winlogon.PSObject.Properties['DefaultPassword']) { '<set>' } else { '<absent>' }))"
"Write-Error 'Explorer shell did not start in time. Auto-logon state is printed above.'"
"exit 1"
) -join "; "
Invoke-Vagrant -Arguments @("winrm", "-s", "powershell", "-c", $waitForShellCommand)
# Prepare guest layout.
@@ -156,7 +177,8 @@ try {
Write-Host ""
Write-Host "[coverage] -> $scenario" -ForegroundColor Cyan
$remoteScript = Join-Path $guestScriptRoot "$scenario.ps1"
$remoteManifest = Join-Path $guestScenarioRoot "$scenario\install.toml"
$manifestName = (Get-ScenarioManifest -ScenarioDir (Join-Path $repoRoot "vm\$scenario")).Name
$remoteManifest = Join-Path $guestScenarioRoot "$scenario\$manifestName"
$remoteWork = Join-Path $guestWorkRoot $scenario
$logRel = "$scenario\guest.log"
$remoteLog = Join-Path $guestWorkRoot $logRel
+43 -2
View File
@@ -1,7 +1,7 @@
[CmdletBinding()]
param(
[string]$Provider = "hyperv",
[string]$ManifestPath = "vm\self-test\install.toml",
[string]$ManifestPath = "vm\self-test\CovenantSetupSelfTest-install.toml",
[string]$OutputRoot = "dist\vagrant-self-test",
[string]$VmName = $(if ($env:COVENANT_VM_NAME) { $env:COVENANT_VM_NAME } else { "covenant-setup-windows" }),
[string]$GuestUsername = $(if ($env:COVENANT_WINRM_USERNAME) { $env:COVENANT_WINRM_USERNAME } else { "vagrant" }),
@@ -244,7 +244,16 @@ try {
Open-HyperVViewer -VmName $VmName
}
$waitForShellCommand = "for (`$i = 0; `$i -lt 90; `$i++) { if (Get-Process -Name explorer -ErrorAction SilentlyContinue) { exit 0 }; Start-Sleep -Seconds 2 }; Write-Error 'Explorer shell did not start in time.'; exit 1"
$waitForShellCommand = @(
"for (`$i = 0; `$i -lt 90; `$i++) { if (Get-Process -Name explorer -ErrorAction SilentlyContinue) { exit 0 }; Start-Sleep -Seconds 2 }"
"`$winlogon = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon'"
"Write-Output ('consoleUser=' + (Get-CimInstance Win32_ComputerSystem).UserName)"
"Write-Output ('computerName=' + `$env:COMPUTERNAME)"
"foreach (`$name in 'AutoAdminLogon','ForceAutoLogon','DefaultUserName','DefaultDomainName','AutoLogonCount') { `$value = `$winlogon.PSObject.Properties[`$name]; Write-Output (`$name + '=' + `$(if (`$value) { `$value.Value } else { '<absent>' })) }"
"Write-Output ('DefaultPassword=' + `$(if (`$winlogon.PSObject.Properties['DefaultPassword']) { '<set>' } else { '<absent>' }))"
"Write-Error 'Explorer shell did not start in time. Auto-logon state is printed above.'"
"exit 1"
) -join "; "
Invoke-Vagrant -Arguments @("winrm", "-s", "powershell", "-c", $waitForShellCommand)
Invoke-Vagrant -Arguments @("winrm", "-s", "powershell", "-c", "New-Item -ItemType Directory -Force -Path '$guestRoot' | Out-Null; New-Item -ItemType Directory -Force -Path '$guestScriptRoot' | Out-Null")
@@ -257,6 +266,38 @@ try {
) -join "; "
Invoke-Vagrant -Arguments @("winrm", "-s", "powershell", "-c", $clearGuestTraceCommand)
Invoke-Vagrant -Arguments @("upload", $installerPath, $guestInstallerPath)
# Zip the ui folder, excluding bin/obj directories to keep it small
$uiZipPath = Join-Path $outputRootAbs "ui.zip"
Remove-Item -LiteralPath $uiZipPath -Force -ErrorAction SilentlyContinue
$zipScript = @(
"& {"
" `$uiPath = '$repoRoot\ui'"
" `$zipPath = '$uiZipPath'"
" `$tempDir = Join-Path '$outputRootAbs' 'ui-temp'"
" Remove-Item -LiteralPath `$tempDir -Recurse -Force -ErrorAction SilentlyContinue"
" New-Item -ItemType Directory -Force -Path `$tempDir | Out-Null"
" Get-ChildItem -Path `$uiPath -Recurse | Where-Object { `$_.FullName -notmatch '\\(bin|obj|TestResults|\.vs)($|\\)' } | ForEach-Object {"
" `$relative = `$_.FullName.Substring(`$uiPath.Length + 1)"
" `$target = Join-Path `$tempDir `$relative"
" if (`$_.PSIsContainer) {"
" New-Item -ItemType Directory -Force -Path `$target | Out-Null"
" } else {"
" `$targetParent = Split-Path -Parent `$target"
" New-Item -ItemType Directory -Force -Path `$targetParent -ErrorAction SilentlyContinue | Out-Null"
" Copy-Item -LiteralPath `$_.FullName -Destination `$target -Force"
" }"
" }"
" Compress-Archive -Path (Join-Path `$tempDir '*') -DestinationPath `$zipPath -Force"
" Remove-Item -LiteralPath `$tempDir -Recurse -Force -ErrorAction SilentlyContinue"
"}"
) -join "`n"
powershell.exe -NoProfile -Command $zipScript
Invoke-Vagrant -Arguments @("upload", $uiZipPath, "$guestRoot\ui.zip")
Invoke-Vagrant -Arguments @("winrm", "-s", "powershell", "-c", "if (Test-Path -LiteralPath '$guestRoot\ui') { Remove-Item -LiteralPath '$guestRoot\ui' -Recurse -Force -ErrorAction SilentlyContinue }; Expand-Archive -Path '$guestRoot\ui.zip' -DestinationPath '$guestRoot\ui' -Force")
Invoke-Vagrant -Arguments @("upload", (Join-Path $repoRoot "scripts\windows-vm\Invoke-InteractiveInstaller.ps1"), (Join-Path $guestScriptRoot "Invoke-InteractiveInstaller.ps1"))
Invoke-Vagrant -Arguments @("upload", (Join-Path $repoRoot "scripts\windows-vm\Start-InteractiveSelfInstall.ps1"), (Join-Path $guestScriptRoot "Start-InteractiveSelfInstall.ps1"))
@@ -129,51 +129,95 @@ try {
$env:COVENANT_SETUP_TRACE_DIR = $TracePath
Write-TraceEvent -Phase "trace_environment_set" -Detail @{ name = "COVENANT_SETUP_TRACE_DIR"; value = $TracePath }
$installer = Start-Process -FilePath $InstallerPath -ArgumentList $InstallerArguments -PassThru
Write-TraceEvent -Phase "installer_process_started" -Detail @{ pid = $installer.Id; operationName = $OperationName }
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
$lastPoll = Get-Date "2000-01-01"
while (-not $installer.HasExited) {
if ((Get-Date) -ge $deadline) {
Export-InstallerDiagnostics -Reason "installer_timeout" -InstallerProcess $installer
Stop-Process -Id $installer.Id -Force -ErrorAction SilentlyContinue
throw "Installer timed out after $TimeoutSeconds seconds."
$testProjectDir = "C:\Users\vagrant\AppData\Local\Temp\covenant-setup-smoke\ui\Covenant.Setup.Ui.Tests"
if (Test-Path -LiteralPath "$testProjectDir\Covenant.Setup.Ui.Tests.csproj") {
$env:COVENANT_REAL_INSTALLER_PATH = $InstallerPath
$env:COVENANT_REAL_INSTALLER_ARGS = $InstallerArguments -join ' '
Write-TraceEvent -Phase "running_ui_automation_tests" -Detail @{
installerPath = $InstallerPath
arguments = $InstallerArguments
}
$dotnetPath = "C:\Users\vagrant\dotnet\dotnet.exe"
if (-not (Test-Path -LiteralPath $dotnetPath)) {
$dotnetPath = "dotnet"
}
Write-TraceEvent -Phase "executing_dotnet_test" -Detail @{
dotnetPath = $dotnetPath
project = "$testProjectDir\Covenant.Setup.Ui.Tests.csproj"
}
$stdoutLog = Join-Path $TracePath "dotnet-test.log"
$stderrLog = Join-Path $TracePath "dotnet-test-error.log"
$process = Start-Process -FilePath $dotnetPath -ArgumentList @("test", "$testProjectDir\Covenant.Setup.Ui.Tests.csproj", "--filter", "FullyQualifiedName=Covenant.Setup.Ui.Tests.InstallerUiAutomationTests.TestRealInstallUninstallFlow") -PassThru -NoNewWindow -Wait -RedirectStandardOutput $stdoutLog -RedirectStandardError $stderrLog
Write-TraceEvent -Phase "dotnet_test_finished" -Detail @{ exitCode = $process.ExitCode }
$result = [ordered]@{
success = ($process.ExitCode -eq 0)
exitCode = [int]$process.ExitCode
installerPath = $InstallerPath
installerArgs = $InstallerArguments
operationName = $OperationName
tracePath = $TracePath
startedAt = $startedAt.ToString("o")
finishedAt = (Get-Date).ToString("o")
}
$result | ConvertTo-Json | Set-Content -LiteralPath $ResultPath -Encoding UTF8
if ($process.ExitCode -ne 0) {
exit $process.ExitCode
}
}
else {
$installer = Start-Process -FilePath $InstallerPath -ArgumentList $InstallerArguments -PassThru
Write-TraceEvent -Phase "installer_process_started" -Detail @{ pid = $installer.Id; operationName = $OperationName }
if (((Get-Date) - $lastPoll).TotalSeconds -ge 10) {
$lastPoll = Get-Date
$installer.Refresh()
Write-TraceEvent -Phase "installer_still_running" -Detail @{
pid = $installer.Id
operationName = $OperationName
elapsedSeconds = [int]((Get-Date) - $startedAt).TotalSeconds
responding = $installer.Responding
mainWindow = $installer.MainWindowTitle
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
$lastPoll = Get-Date "2000-01-01"
while (-not $installer.HasExited) {
if ((Get-Date) -ge $deadline) {
Export-InstallerDiagnostics -Reason "installer_timeout" -InstallerProcess $installer
Stop-Process -Id $installer.Id -Force -ErrorAction SilentlyContinue
throw "Installer timed out after $TimeoutSeconds seconds."
}
if (((Get-Date) - $lastPoll).TotalSeconds -ge 10) {
$lastPoll = Get-Date
$installer.Refresh()
Write-TraceEvent -Phase "installer_still_running" -Detail @{
pid = $installer.Id
operationName = $OperationName
elapsedSeconds = [int]((Get-Date) - $startedAt).TotalSeconds
responding = $installer.Responding
mainWindow = $installer.MainWindowTitle
}
}
Start-Sleep -Seconds 2
$installer.Refresh()
}
Start-Sleep -Seconds 2
$installer.Refresh()
}
Write-TraceEvent -Phase "installer_process_exited" -Detail @{ pid = $installer.Id; exitCode = $installer.ExitCode; operationName = $OperationName }
Export-InstallerDiagnostics -Reason "installer_exit" -InstallerProcess $installer
Write-TraceEvent -Phase "installer_process_exited" -Detail @{ pid = $installer.Id; exitCode = $installer.ExitCode; operationName = $OperationName }
Export-InstallerDiagnostics -Reason "installer_exit" -InstallerProcess $installer
$result = [ordered]@{
success = ($installer.ExitCode -eq 0)
exitCode = [int]$installer.ExitCode
installerPath = $InstallerPath
installerArgs = $InstallerArguments
operationName = $OperationName
tracePath = $TracePath
startedAt = $startedAt.ToString("o")
finishedAt = (Get-Date).ToString("o")
}
$result | ConvertTo-Json | Set-Content -LiteralPath $ResultPath -Encoding UTF8
$result = [ordered]@{
success = ($installer.ExitCode -eq 0)
exitCode = [int]$installer.ExitCode
installerPath = $InstallerPath
installerArgs = $InstallerArguments
operationName = $OperationName
tracePath = $TracePath
startedAt = $startedAt.ToString("o")
finishedAt = (Get-Date).ToString("o")
}
$result | ConvertTo-Json | Set-Content -LiteralPath $ResultPath -Encoding UTF8
if ($installer.ExitCode -ne 0) {
exit $installer.ExitCode
if ($installer.ExitCode -ne 0) {
exit $installer.ExitCode
}
}
}
catch {
@@ -321,6 +321,43 @@ try {
timeoutSeconds = $TimeoutSeconds
}
# Install VC++ Redistributable if missing
$vcredistInstalled = $false
$vcredistKey = "HKLM:\SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64"
if (Test-Path -Path $vcredistKey) {
$vcredistVal = Get-ItemProperty -Path $vcredistKey -Name "Installed" -ErrorAction SilentlyContinue
if ($vcredistVal -and $vcredistVal.Installed -eq 1) {
$vcredistInstalled = $true
}
}
if (-not $vcredistInstalled) {
Write-TraceEvent -Phase "vcredist_install_start"
try {
$vcredistPath = Join-Path $env:TEMP "vc_redist.x64.exe"
Remove-Item -Path $vcredistPath -Force -ErrorAction SilentlyContinue
Write-Host "Downloading VC++ Redistributable..."
$oldProgress = $ProgressPreference
$ProgressPreference = 'SilentlyContinue'
Invoke-WebRequest -Uri "https://aka.ms/vs/17/release/vc_redist.x64.exe" -OutFile $vcredistPath
$ProgressPreference = $oldProgress
Write-Host "Installing VC++ Redistributable..."
$proc = Start-Process -FilePath $vcredistPath -ArgumentList "/install", "/quiet", "/norestart" -PassThru -Wait
if ($proc.ExitCode -ne 0 -and $proc.ExitCode -ne 3010) { # 3010 is success but reboot required
throw "vc_redist installation failed with exit code $($proc.ExitCode)"
}
Write-TraceEvent -Phase "vcredist_install_success" -Detail @{ exitCode = $proc.ExitCode }
}
catch {
Write-TraceEvent -Phase "vcredist_install_error" -Detail @{ error = $_.Exception.Message }
Write-Warning "Failed to install VC++ Redistributable: $($_.Exception.Message)"
}
}
else {
Write-TraceEvent -Phase "vcredist_already_installed"
}
if (-not (Test-Path -LiteralPath $InstallerPath)) {
throw "Installer not found in guest: $InstallerPath"
}