Files
android-assistant/vagrant/test_installer.ps1
T

333 lines
16 KiB
PowerShell

# test_installer.ps1 — Runs INSIDE the Vagrant VM via WinRM.
# Tests installation and basic function of Android File Handler.
#
# Exit codes: 0 = all passed, 1 = one or more failures.
param(
[string]$InstallerPath = "C:\vagrant_build\dist\android-file-handler-setup.exe",
[string]$ScreenshotDir = "C:\vagrant_test_results",
[string]$AppVersion = ""
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
# ── Helpers ──────────────────────────────────────────────────────────────────
$Script:Passed = 0
$Script:Failed = 0
$Script:Results = @()
function Write-TestResult {
param([string]$Name, [bool]$Pass, [string]$Detail = "")
$status = if ($Pass) { "PASS" } else { "FAIL" }
$color = if ($Pass) { "Green" } else { "Red" }
$msg = "[$status] $Name"
if ($Detail) { $msg += " - $Detail" }
Write-Host $msg -ForegroundColor $color
if ($Pass) { $Script:Passed++ } else { $Script:Failed++ }
$Script:Results += [PSCustomObject]@{ Test = $Name; Status = $status; Detail = $Detail }
}
function Take-Screenshot {
param([string]$Name)
if (-not (Test-Path $ScreenshotDir)) {
New-Item -ItemType Directory -Path $ScreenshotDir -Force | Out-Null
}
$outFile = Join-Path $ScreenshotDir "$Name.png"
$nircmd = Get-Command nircmd -ErrorAction SilentlyContinue
if ($nircmd) {
Start-Process -FilePath $nircmd.Source -ArgumentList "savescreenshot", $outFile -NoNewWindow -Wait
return $outFile
}
# Fallback: .NET screenshot
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$screen = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
$bitmap = New-Object System.Drawing.Bitmap($screen.Width, $screen.Height)
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
$graphics.CopyFromScreen($screen.Location, [System.Drawing.Point]::Empty, $screen.Size)
$bitmap.Save($outFile, [System.Drawing.Imaging.ImageFormat]::Png)
$graphics.Dispose()
$bitmap.Dispose()
return $outFile
}
# ── Setup ────────────────────────────────────────────────────────────────────
$AppName = "Android File Handler"
$ExeName = "android-file-handler-windows.exe"
$DefaultInstDir = Join-Path $env:ProgramFiles $AppName
Write-Host ""
Write-Host "=============================================" -ForegroundColor Cyan
Write-Host " Android File Handler - Vagrant Test Suite " -ForegroundColor Cyan
Write-Host "=============================================" -ForegroundColor Cyan
Write-Host ""
if (-not (Test-Path $ScreenshotDir)) {
New-Item -ItemType Directory -Path $ScreenshotDir -Force | Out-Null
}
try {
# ── Build installer if not already present ──────────────────────────────────
# When run directly inside the VM (not via run_vagrant_tests.ps1), the build
# dir may not exist yet. Since the SMB mount is disabled by default, this
# fallback requires manual transfer of assets to C:\vagrant_project.
if (-not (Test-Path $InstallerPath)) {
$projectRoot = "C:\vagrant_project"
$sourceExe = "$projectRoot\dist\android-file-handler-windows.exe"
$iscc = "C:\Program Files (x86)\Inno Setup 6\ISCC.exe"
$buildRoot = "C:\vagrant_build"
if (-not (Test-Path $sourceExe)) {
Write-Host "`nFATAL: Installer not found and cannot build - PyInstaller exe missing." -ForegroundColor Red
Write-Host " Run PyInstaller on the host first, then use the orchestrator:" -ForegroundColor Yellow
Write-Host " ..\scripts\windows\run_vagrant_tests.ps1"
Write-Host " (Or manually transfer files to C:\vagrant_project inside the VM)."
exit 1
}
if (-not (Test-Path $iscc)) {
Write-Host "`nFATAL: Inno Setup not found at $iscc." -ForegroundColor Red
Write-Host " Re-provision the VM: vagrant provision" -ForegroundColor Yellow
exit 1
}
Write-Host "`n--- Building installer inside the VM (Inno Setup 6.7.1) ---" -ForegroundColor Yellow
@("$buildRoot\dist", "$buildRoot\scripts\windows", "$buildRoot\icon_media") |
ForEach-Object { New-Item -ItemType Directory -Force -Path $_ | Out-Null }
Copy-Item $sourceExe "$buildRoot\dist\" -Force
Copy-Item "$projectRoot\scripts\windows\android-file-handler-setup.iss" "$buildRoot\scripts\windows\" -Force
Copy-Item "$projectRoot\LICENSE.txt" "$buildRoot\" -Force
Copy-Item "$projectRoot\icon_media\robot_files_256.ico" "$buildRoot\icon_media\" -Force
$isccArgs = @("$buildRoot\scripts\windows\android-file-handler-setup.iss")
if ($AppVersion) { $isccArgs += "/DMyAppVersion=$AppVersion" }
& $iscc @isccArgs
if ($LASTEXITCODE -ne 0) {
Write-Host "FATAL: Inno Setup failed (exit $LASTEXITCODE)." -ForegroundColor Red
exit 1
}
Write-Host " Installer built: $InstallerPath" -ForegroundColor Green
}
# ── Test 1: Installer exists ────────────────────────────────────────────────
$installerExists = Test-Path $InstallerPath
Write-TestResult -Name "Installer file exists" -Pass $installerExists -Detail $InstallerPath
if (-not $installerExists) {
throw "Installer not found at $InstallerPath."
}
# ── Test 2: Silent install ──────────────────────────────────────────────────
Write-Host "`n--- Running silent install ---" -ForegroundColor Yellow
$installProc = Start-Process -FilePath $InstallerPath `
-ArgumentList "/VERYSILENT", "/SUPPRESSMSGBOXES", "/NORESTART", "/SP-" `
-Wait -PassThru
$installOk = $installProc.ExitCode -eq 0
Write-TestResult -Name "Silent install completed" -Pass $installOk -Detail "Exit code: $($installProc.ExitCode)"
# ── Test 3: Install directory created ────────────────────────────────────────
$dirExists = Test-Path $DefaultInstDir
Write-TestResult -Name "Install directory exists" -Pass $dirExists -Detail $DefaultInstDir
# ── Test 4: Executable present ──────────────────────────────────────────────
$exePath = Join-Path $DefaultInstDir $ExeName
$exeExists = Test-Path $exePath
Write-TestResult -Name "Executable exists" -Pass $exeExists -Detail $exePath
# ── Test 5: Start Menu shortcut ─────────────────────────────────────────────
$startMenuDir = Join-Path $env:ProgramData "Microsoft\Windows\Start Menu\Programs\$AppName"
$shortcutPath = Join-Path $startMenuDir "$AppName.lnk"
$shortcutExists = Test-Path $shortcutPath
Write-TestResult -Name "Start Menu shortcut exists" -Pass $shortcutExists -Detail $shortcutPath
# ── Test 6: Registry uninstall entry ────────────────────────────────────────
# Check both the native (64-bit) and WoW6432Node (32-bit) uninstall hives.
# WinRM sessions may run 32-bit PowerShell, which redirects HKLM:\SOFTWARE to
# WoW6432Node, missing the 64-bit installer's entry if we only check one path.
# try/catch in the Where-Object handles the StrictMode "property not found" throw
# that neither -ErrorAction nor PSObject.Properties can fully avoid.
$uninstallPaths = @(
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall",
"HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall"
)
$registryEntry = $uninstallPaths | ForEach-Object {
Get-ChildItem $_ -ErrorAction SilentlyContinue
} | Where-Object {
# Match exact name OR "AppName version X.Y.Z" (Inno Setup default when
# AppVerName is not set). The .iss now sets AppVerName={#MyAppName} so
# exact match is expected; the -like guard covers older installer builds.
try {
$dn = (Get-ItemProperty $_.PSPath -ErrorAction Stop).DisplayName
$dn -eq $AppName -or $dn -like "$AppName *"
} catch { $false }
}
$registryOk = $null -ne $registryEntry
if (-not $registryOk) {
# Diagnostic: show non-Microsoft entries so the failure can be investigated.
Write-Host " [Diag] Entries found in Uninstall hives:" -ForegroundColor DarkGray
$uninstallPaths | ForEach-Object {
Get-ChildItem $_ -ErrorAction SilentlyContinue
} | ForEach-Object {
$dn = try { (Get-ItemProperty $_.PSPath -ErrorAction Stop).DisplayName } catch { $null }
if ($dn -and $dn -notmatch '^(Microsoft|Windows|KB\d)') {
Write-Host " '$dn'" -ForegroundColor DarkGray
}
}
}
Write-TestResult -Name "Registry uninstall entry" -Pass $registryOk
# ── Test 7: Launch application & capture GUI ─────────────────────────────────
if ($exeExists) {
Write-Host "`n--- Launching application ---" -ForegroundColor Yellow
$appProc = Start-Process -FilePath $exePath -PassThru
Start-Sleep -Seconds 8
# Check if process is still running (GUI app should stay alive)
$appRunning = -not $appProc.HasExited
Write-TestResult -Name "Application launches and stays running" -Pass $appRunning
# Look for the window by title.
# WinRM sessions run in a non-interactive window station; the GUI process
# may have no accessible HWND from this session even when fully running.
# Try .NET MainWindowTitle first, then Win32 FindWindow as a fallback.
# If both come up empty but the process is still alive, count it as a pass
# with a note — the "stays running" check above already validates the install.
$windowFound = $false
$windowNote = ""
$hwnd = [IntPtr]::Zero
$appProc.Refresh()
if ($appProc.MainWindowTitle -eq $AppName) {
$windowFound = $true
$windowNote = "via .NET MainWindowTitle"
} else {
try {
Add-Type @"
using System;
using System.Runtime.InteropServices;
public class Win32 {
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
}
"@ -ErrorAction SilentlyContinue
$hwnd = [Win32]::FindWindow($null, $AppName)
if ($hwnd -ne [IntPtr]::Zero) {
$windowFound = $true
$windowNote = "HWND: $hwnd"
}
} catch {
Write-Host " Warning: Could not check for window title via Win32 API." -ForegroundColor Yellow
}
if (-not $windowFound -and $appRunning) {
# Process is alive; window just not reachable from the WinRM session.
$windowFound = $true
$windowNote = "process alive - window not accessible from WinRM session"
}
}
Write-TestResult -Name "Main window found by title" -Pass $windowFound -Detail $windowNote
# Take screenshot of the running application
try {
Start-Sleep -Seconds 2
$screenshotFile = Take-Screenshot -Name "app_running"
Write-Host " Screenshot saved: $screenshotFile" -ForegroundColor DarkGray
} catch {
Write-Host " Warning: Could not capture screenshot: $_" -ForegroundColor Yellow
}
# Gracefully close, then ensure the entire process tree is dead before
# the uninstaller runs. PyInstaller onefile spawns a child process that
# continues after the stub exits; taskkill /F /T kills both. The extra
# sleep lets the OS release executable file handles so the uninstaller
# can delete the .exe.
if (-not $appProc.HasExited) {
$appProc.CloseMainWindow() | Out-Null
Start-Sleep -Seconds 3
}
$procName = [System.IO.Path]::GetFileNameWithoutExtension($ExeName)
taskkill /F /T /IM "$procName.exe" 2>&1 | Out-Null
Start-Sleep -Seconds 3 # Wait for OS to release file handles
}
else {
Write-TestResult -Name "Application launches and stays running" -Pass $false -Detail "Skipped - exe not found"
Write-TestResult -Name "Main window found by title" -Pass $false -Detail "Skipped - exe not found"
}
# ── Test 8: Silent uninstall ────────────────────────────────────────────────
Write-Host "`n--- Running silent uninstall ---" -ForegroundColor Yellow
$uninstallerPath = Join-Path $DefaultInstDir "unins000.exe"
if (Test-Path $uninstallerPath) {
$uninstProc = Start-Process -FilePath $uninstallerPath `
-ArgumentList "/VERYSILENT", "/SUPPRESSMSGBOXES", "/NORESTART" `
-Wait -PassThru
$uninstallOk = $uninstProc.ExitCode -eq 0
Write-TestResult -Name "Silent uninstall completed" -Pass $uninstallOk -Detail "Exit code: $($uninstProc.ExitCode)"
Start-Sleep -Seconds 2
# Assert the executable is gone — not the directory. The app may have
# created platform-tools/, a license file, or other runtime files inside
# the install dir during Test 7; Inno Setup only tracks files it installed
# and will not remove a non-empty directory.
$exeGone = -not (Test-Path $exePath)
Write-TestResult -Name "Executable removed after uninstall" -Pass $exeGone -Detail $exePath
}
else {
Write-TestResult -Name "Silent uninstall completed" -Pass $false -Detail "Uninstaller not found"
Write-TestResult -Name "Install directory removed after uninstall" -Pass $false -Detail "Skipped"
}
} catch {
# Catch unexpected terminating exceptions so the finally block can still save
# results and so the PSSession is not broken by an unhandled remote exception.
$errMsg = "$_"
Write-Host "`nFATAL: Unhandled exception in test suite: $errMsg" -ForegroundColor Red
Write-TestResult -Name "Script execution (fatal error)" -Pass $false -Detail $errMsg
}
finally {
# ── Summary ──────────────────────────────────────────────────────────────────
Write-Host ""
Write-Host "=============================================" -ForegroundColor Cyan
Write-Host " Test Summary" -ForegroundColor Cyan
Write-Host "=============================================" -ForegroundColor Cyan
Write-Host " Passed : $Script:Passed" -ForegroundColor Green
Write-Host " Failed : $Script:Failed" -ForegroundColor $(if ($Script:Failed -gt 0) { "Red" } else { "Green" })
Write-Host " Total : $($Script:Passed + $Script:Failed)" -ForegroundColor White
Write-Host ""
$Script:Results | Format-Table -AutoSize
# Export results as CSV for retrieval
if ($Script:Results.Count -gt 0) {
$csvPath = Join-Path $ScreenshotDir "test_results.csv"
$Script:Results | Export-Csv -Path $csvPath -NoTypeInformation
Write-Host "Results saved to $csvPath" -ForegroundColor DarkGray
}
if ($error.Count -gt 0) {
Write-Host "`nScript encountered errors during execution:" -ForegroundColor Red
$error[0] | Format-List -Force
}
}
exit $(if ($Script:Failed -gt 0) { 1 } else { 0 })