fixed Vagrant tests for Windows

This commit is contained in:
2026-03-23 21:43:06 -05:00
parent 18ede809e2
commit d33ea69eab
9 changed files with 495 additions and 247 deletions
+2 -1
View File
@@ -2,7 +2,8 @@
"permissions": {
"allow": [
"WebFetch(domain:community.chocolatey.org)",
"WebFetch(domain:github.com)"
"WebFetch(domain:github.com)",
"WebFetch(domain:jrsoftware.org)"
]
}
}
@@ -63,6 +63,13 @@ jobs:
- name: Build Windows executable
run: poetry run pyinstaller scripts/spec_scripts/android-file-handler-windows.spec
- name: Install Inno Setup 6.7.1
shell: pwsh
run: |
choco install innosetup --version 6.7.1 -y --no-progress
# Refresh PATH so ISCC.exe is available immediately
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
- name: Build Windows installer (Inno Setup)
shell: pwsh
run: |
+3 -1
View File
@@ -15,4 +15,6 @@ pkg*
# Python cache files
__pycache__/
*.py[cod]
*$py.class
*$py.class
.vagrant/
vagrant_test_results/
+1 -1
View File
@@ -101,7 +101,7 @@ See [scripts/docker/README.md](scripts/docker/README.md) for detailed Docker bui
poetry run pyinstaller scripts/spec_scripts/android-file-handler-windows.spec
# Windows installer (Inno Setup, after PyInstaller build)
# Inno Setup 6 is pre-installed on GitHub Actions windows-latest runners
# Inno Setup 6.7.1 is installed by the CI/CD workflow via Chocolatey (pinned version)
& "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" scripts\windows\android-file-handler-setup.iss /DMyAppVersion=0.1.1
# Linux packages use distro-specific spec files:
@@ -20,6 +20,10 @@
AppId={{8F2B3A7E-4D1C-4E8F-9A2B-6C7D8E9F0A1B}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
; AppVerName controls the registry DisplayName value. Without it Inno Setup
; defaults to "AppName version AppVersion" which breaks exact-name lookups.
; Windows convention: DisplayName = product name, DisplayVersion = version.
AppVerName={#MyAppName}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
@@ -31,7 +35,8 @@ OutputDir=..\..\dist
OutputBaseFilename=android-file-handler-setup
SetupIconFile=..\..\icon_media\robot_files_256.ico
UninstallDisplayIcon={app}\{#MyAppExeName}
Compression=lzma2/ultra64
Compression=lzma2/normal
LZMADictionarySize=16384
SolidCompression=yes
WizardStyle=modern
ArchitecturesInstallIn64BitMode=x64compatible
+149 -113
View File
@@ -4,8 +4,8 @@
# 1. Build the installer locally (PyInstaller + Inno Setup)
# 2. Ensure the Vagrant VM is running and has a "clean" snapshot
# 3. Restore to the clean snapshot
# 4. Push the installer to the VM
# 5. Run the test script inside the VM via WinRM
# 4. Transfer build assets to the VM and compile the installer (Inno Setup)
# 5. Run the test script inside the VM via PSSession
# 6. Retrieve screenshots and results to the host
# 7. Print results summary to CLI
# 8. Revert VM back to clean snapshot
@@ -17,10 +17,10 @@
#
# Prerequisites:
# - Vagrant and Hyper-V enabled on the host
# - Inno Setup 6 installed (default path or in PATH)
# - Poetry environment set up
# - The VM created and "clean" snapshot saved:
# cd vagrant && vagrant up --provider=hyperv && vagrant snapshot save clean
# Note: Inno Setup 6.7.1 is installed inside the VM by provision.ps1 — no host install needed.
[CmdletBinding()]
param(
@@ -33,7 +33,8 @@ param(
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path
$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path
$vmSession = $null # PSSession — closed in finally block
$VagrantDir = Join-Path $RepoRoot "vagrant"
$Timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
@@ -51,21 +52,22 @@ function Write-Step {
Push-Location $VagrantDir
try {
# ── Step 1: Build the installer locally ──────────────────────────────────
# ── Step 1: Build the Windows executable (host, PyInstaller only) ────────
# Inno Setup runs inside the VM — no host install of Inno Setup required.
$installerPath = Join-Path $RepoRoot "dist\android-file-handler-setup.exe"
$pyinstallerExe = Join-Path $RepoRoot "dist\android-file-handler-windows.exe"
$AppVersion = ""
if ($SkipBuild) {
Write-Step "Skipping build (-SkipBuild specified)"
if (-not (Test-Path $installerPath)) {
Write-Host "ERROR: -SkipBuild was specified but installer not found at $installerPath" -ForegroundColor Red
Write-Step "Skipping PyInstaller build (-SkipBuild specified)"
if (-not (Test-Path $pyinstallerExe)) {
Write-Host "ERROR: -SkipBuild was specified but exe not found at $pyinstallerExe" -ForegroundColor Red
exit 1
}
}
else {
Write-Step "Building installer locally (PyInstaller + Inno Setup)"
Write-Step "Building Windows executable (PyInstaller)"
# PyInstaller — compile the exe
Push-Location $RepoRoot
try {
Write-Host " Running PyInstaller..." -ForegroundColor Yellow
@@ -75,45 +77,27 @@ try {
exit 1
}
$pyinstallerExe = Join-Path $RepoRoot "dist\android-file-handler-windows.exe"
if (-not (Test-Path $pyinstallerExe)) {
Write-Host "ERROR: PyInstaller output not found at $pyinstallerExe" -ForegroundColor Red
exit 1
}
Write-Host " PyInstaller output: $pyinstallerExe" -ForegroundColor Green
# Inno Setup — compile the installer
Write-Host " Running Inno Setup..." -ForegroundColor Yellow
$isccPaths = @(
"C:\Program Files (x86)\Inno Setup 6\ISCC.exe",
"C:\Program Files\Inno Setup 6\ISCC.exe"
)
$iscc = $isccPaths | Where-Object { Test-Path $_ } | Select-Object -First 1
if (-not $iscc) {
$iscc = Get-Command ISCC.exe -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source
}
if (-not $iscc) {
Write-Host "ERROR: Inno Setup (ISCC.exe) not found. Install Inno Setup 6 or add it to PATH." -ForegroundColor Red
exit 1
}
& $iscc "scripts\windows\android-file-handler-setup.iss"
if ($LASTEXITCODE -ne 0) {
Write-Host "ERROR: Inno Setup failed with exit code $LASTEXITCODE" -ForegroundColor Red
exit 1
}
if (-not (Test-Path $installerPath)) {
Write-Host "ERROR: Installer not found at $installerPath after Inno Setup" -ForegroundColor Red
exit 1
}
Write-Host " Installer built: $installerPath" -ForegroundColor Green
}
finally {
Pop-Location
}
}
# Capture version to pass to Inno Setup inside the VM
Push-Location $RepoRoot
try {
$AppVersion = (poetry version -s).Trim()
Write-Host " App version: $AppVersion" -ForegroundColor DarkGray
}
finally {
Pop-Location
}
# ── Step 2: Verify VM and snapshot exist ─────────────────────────────────
Write-Step "Checking Vagrant VM status"
@@ -137,51 +121,98 @@ try {
Write-Step "Restoring VM to '$SnapshotName' snapshot"
vagrant snapshot restore $SnapshotName
# ── Step 4: Push installer to the VM ─────────────────────────────────────
# ── Establish direct PSSession ────────────────────────────────────────────
# vagrant winrm uses a subprocess whose args are subject to the Windows
# command-line length limit (~32 KB). Large file transfers exceed that limit.
# A direct PSSession has no such restriction and supports
# Copy-Item -ToSession / -FromSession for binary files of any size.
Write-Step "Pushing installer to the VM"
Write-Step "Establishing PSSession to VM"
$remoteInstallerDir = "C:\vagrant_installer"
$remoteInstallerPath = "$remoteInstallerDir\android-file-handler-setup.exe"
vagrant winrm --shell powershell --command `
"New-Item -ItemType Directory -Path '$remoteInstallerDir' -Force | Out-Null"
# Base64-encode the installer, transfer via WinRM, decode on the VM
$installerBytes = [IO.File]::ReadAllBytes($installerPath)
$installerSizeMB = [math]::Round($installerBytes.Length / 1MB, 1)
Write-Host " Installer size: ${installerSizeMB} MB — transferring via WinRM..." -ForegroundColor Yellow
# Split into chunks to avoid WinRM message size limits (< 2MB per command)
$chunkSize = 1 * 1024 * 1024 # 1 MB chunks
$totalChunks = [math]::Ceiling($installerBytes.Length / $chunkSize)
vagrant winrm --shell powershell --command `
"if (Test-Path '$remoteInstallerPath') { Remove-Item '$remoteInstallerPath' -Force }"
for ($idx = 0; $idx -lt $totalChunks; $idx++) {
$offset = $idx * $chunkSize
$length = [math]::Min($chunkSize, $installerBytes.Length - $offset)
$chunk = New-Object byte[] $length
[Array]::Copy($installerBytes, $offset, $chunk, 0, $length)
$b64Chunk = [Convert]::ToBase64String($chunk)
$appendScript = if ($idx -eq 0) {
"[IO.File]::WriteAllBytes('$remoteInstallerPath', [Convert]::FromBase64String('$b64Chunk'))"
}
else {
"`$bytes = [Convert]::FromBase64String('$b64Chunk'); " +
"`$stream = [IO.File]::Open('$remoteInstallerPath', 'Append'); " +
"`$stream.Write(`$bytes, 0, `$bytes.Length); `$stream.Close()"
}
vagrant winrm --shell powershell --command $appendScript
Write-Host " Chunk $($idx + 1)/$totalChunks transferred" -ForegroundColor DarkGray
# Ensure host WinRM client is configured to allow Basic authentication
# This is required for the connection to succeed when using -Authentication Basic
if ((Get-Item WSMan:\localhost\Client\Auth\Basic).Value -ne "true") {
Write-Host " Enabling WinRM Client Basic authentication on host..." -ForegroundColor Yellow
Set-Item WSMan:\localhost\Client\Auth\Basic -Value $true -Force
}
# Verify transfer
$remoteSize = vagrant winrm --shell powershell --command `
"(Get-Item '$remoteInstallerPath').Length"
Write-Host " Remote file size: $($remoteSize.Trim()) bytes (expected: $($installerBytes.Length))" -ForegroundColor DarkGray
$remoteBuildRoot = "C:\vagrant_build"
$remoteInstallerPath = "$remoteBuildRoot\dist\android-file-handler-setup.exe"
$remoteResultsDir = "C:\vagrant_test_results"
# vagrant winrm-config reports the plaintext port (5985) used by Vagrant itself.
# We extract the host and port, then derive the HTTPS port (typically +1).
# If the direct IP fails, we fallback to the forwarded port on localhost.
$winrmConfig = & vagrant winrm-config 2>$null | Out-String
$vmHost = if ($winrmConfig -match 'HostName\s+(\S+)') { $Matches[1] } else { '127.0.0.1' }
$vmPort = if ($winrmConfig -match 'Port\s+(\d+)') { [int]$Matches[1] + 1 } else { 5986 }
# If we have a direct IP but it's not reachable, fallback to the forwarded port
if ($vmHost -ne '127.0.0.1' -and -not (Test-NetConnection -ComputerName $vmHost -Port $vmPort -InformationLevel Quiet)) {
Write-Host " Direct IP $vmHost`:$vmPort unreachable. Falling back to localhost..." -ForegroundColor Yellow
$vmHost = '127.0.0.1'
$vmPort = 55986 # The host-side forwarded port for 5986
}
# Verify connectivity before trying PSSession
if (-not (Test-NetConnection -ComputerName $vmHost -Port $vmPort -InformationLevel Quiet)) {
Write-Host "ERROR: Could not reach VM at $vmHost`:$vmPort. Ensure 'vagrant up' and provisioning completed successfully." -ForegroundColor Red
exit 1
}
$vagrantCred = New-Object PSCredential 'vagrant',
(ConvertTo-SecureString 'vagrant' -AsPlainText -Force)
# SkipCACheck / SkipCNCheck accept the self-signed cert created by provision.ps1.
$sessionOpts = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck
$vmSession = New-PSSession -ComputerName $vmHost -Port $vmPort `
-Credential $vagrantCred -Authentication Basic `
-UseSSL -SessionOption $sessionOpts
Write-Host " Connected to VM at $vmHost`:$vmPort" -ForegroundColor DarkGray
# ── Step 4: Transfer build assets to VM and compile installer ────────────
Write-Step "Transferring build assets to VM"
Invoke-Command -Session $vmSession -ScriptBlock {
param($root)
@("$root\dist", "$root\scripts\windows", "$root\icon_media") |
ForEach-Object { New-Item -ItemType Directory -Force -Path $_ | Out-Null }
} -ArgumentList $remoteBuildRoot
$transfers = @(
@{ L = Join-Path $RepoRoot "dist\android-file-handler-windows.exe"; R = "$remoteBuildRoot\dist\android-file-handler-windows.exe" },
@{ L = Join-Path $RepoRoot "scripts\windows\android-file-handler-setup.iss"; R = "$remoteBuildRoot\scripts\windows\android-file-handler-setup.iss" },
@{ L = Join-Path $RepoRoot "LICENSE.txt"; R = "$remoteBuildRoot\LICENSE.txt" },
@{ L = Join-Path $RepoRoot "icon_media\robot_files_256.ico"; R = "$remoteBuildRoot\icon_media\robot_files_256.ico" },
@{ L = Join-Path $RepoRoot "vagrant\test_installer.ps1"; R = "$remoteBuildRoot\test_installer.ps1" }
)
foreach ($t in $transfers) {
Copy-Item -Path $t.L -Destination $t.R -ToSession $vmSession -Force
$sizeMB = [math]::Round((Get-Item $t.L).Length / 1MB, 1)
Write-Host " Sent: $(Split-Path -Leaf $t.L) (${sizeMB} MB)" -ForegroundColor DarkGray
}
# Unblock transferred files — Copy-Item -ToSession can mark files with a
# Zone.Identifier ADS that execution policy may block even under -Scope Process Bypass.
Invoke-Command -Session $vmSession -ScriptBlock {
param($root)
Get-ChildItem -Path $root -Recurse -File |
ForEach-Object { Unblock-File -Path $_.FullName -ErrorAction SilentlyContinue }
} -ArgumentList $remoteBuildRoot
Write-Host " Files unblocked on VM." -ForegroundColor DarkGray
Write-Step "Building installer inside the VM (Inno Setup 6.7.1)"
Invoke-Command -Session $vmSession -ScriptBlock {
param($buildRoot, $version)
& 'C:\Program Files (x86)\Inno Setup 6\ISCC.exe' `
"$buildRoot\scripts\windows\android-file-handler-setup.iss" `
"/DMyAppVersion=$version"
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
} -ArgumentList $remoteBuildRoot, $AppVersion
Write-Host " Installer built: $remoteInstallerPath" -ForegroundColor Green
# ── Step 5: Run tests inside the VM ──────────────────────────────────────
@@ -189,12 +220,34 @@ try {
$testExitCode = 0
try {
vagrant winrm --shell powershell --command `
"Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process -Force; & C:\vagrant_project\vagrant\test_installer.ps1 -InstallerPath 'C:\vagrant_installer\android-file-handler-setup.exe'"
Invoke-Command -Session $vmSession -ScriptBlock {
param($buildRoot, $installer)
Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process -Force
& "$buildRoot\test_installer.ps1" -InstallerPath $installer
# Propagate the test script's exit code as output so the host can inspect it.
$LASTEXITCODE
} -ArgumentList $remoteBuildRoot, $remoteInstallerPath |
ForEach-Object {
if ($_ -is [int]) { $testExitCode = $_ }
}
}
catch {
$testExitCode = 1
Write-Host "Test script returned failures." -ForegroundColor Yellow
Write-Host "Test script encountered an error:" -ForegroundColor Yellow
Write-Host " $($_.Exception.Message)" -ForegroundColor Red
Write-Host " Category : $($_.CategoryInfo.Category)" -ForegroundColor Red
Write-Host " FullError: $_" -ForegroundColor Red
}
# Re-establish the PSSession if the test run left it in a non-Opened state.
# An unhandled exception inside Invoke-Command can terminate the remote runspace.
if ($vmSession.State -ne 'Opened') {
Write-Host " PSSession is '$($vmSession.State)' — re-establishing for results retrieval..." -ForegroundColor Yellow
Remove-PSSession $vmSession -ErrorAction SilentlyContinue
$vmSession = New-PSSession -ComputerName $vmHost -Port $vmPort `
-Credential $vagrantCred -Authentication Basic `
-UseSSL -SessionOption $sessionOpts
Write-Host " Session re-established." -ForegroundColor DarkGray
}
# ── Step 6: Retrieve results and screenshots ─────────────────────────────
@@ -205,36 +258,18 @@ try {
New-Item -ItemType Directory -Path $LocalResultsDir -Force | Out-Null
}
# Copy results from shared folder on VM via vagrant scp or direct file copy
# The test writes to C:\vagrant_test_results inside the VM.
# We retrieve via WinRM by reading files.
try {
# Get list of result files
$remoteFiles = vagrant winrm --shell powershell --command `
"if (Test-Path 'C:\vagrant_test_results') { Get-ChildItem 'C:\vagrant_test_results' -Name }" 2>&1 |
Where-Object { $_ -is [string] -and $_.Trim() -ne "" }
$hasResults = Invoke-Command -Session $vmSession -ScriptBlock {
param($dir) Test-Path $dir
} -ArgumentList $remoteResultsDir
foreach ($fileName in $remoteFiles) {
$trimmedName = $fileName.Trim()
if (-not $trimmedName) { continue }
$remotePath = "C:\vagrant_test_results\$trimmedName"
$localPath = Join-Path $LocalResultsDir $trimmedName
if ($trimmedName -match '\.csv$') {
# Text file — read content and save
$content = vagrant winrm --shell powershell --command `
"Get-Content -Path '$remotePath' -Raw"
$content | Out-File -FilePath $localPath -Encoding utf8
Write-Host " Retrieved: $trimmedName" -ForegroundColor DarkGray
}
elseif ($trimmedName -match '\.(png|jpg|bmp)$') {
# Binary file — base64 encode, transfer, decode
$b64 = vagrant winrm --shell powershell --command `
"[Convert]::ToBase64String([IO.File]::ReadAllBytes('$remotePath'))"
[IO.File]::WriteAllBytes($localPath, [Convert]::FromBase64String($b64.Trim()))
Write-Host " Retrieved: $trimmedName" -ForegroundColor DarkGray
}
if ($hasResults) {
Copy-Item -Path "$remoteResultsDir\*" -Destination $LocalResultsDir `
-FromSession $vmSession -Recurse -Force
Write-Host " Retrieved results from $remoteResultsDir" -ForegroundColor DarkGray
}
else {
Write-Host " No results directory found on VM." -ForegroundColor Yellow
}
}
catch {
@@ -250,8 +285,8 @@ try {
$results = Import-Csv $csvPath
$results | Format-Table -AutoSize
$passCount = ($results | Where-Object { $_.Status -eq "PASS" }).Count
$failCount = ($results | Where-Object { $_.Status -eq "FAIL" }).Count
$passCount = @($results | Where-Object { $_.Status -eq "PASS" }).Count
$failCount = @($results | Where-Object { $_.Status -eq "FAIL" }).Count
Write-Host " Passed: $passCount | Failed: $failCount | Total: $($passCount + $failCount)" -ForegroundColor $(if ($failCount -gt 0) { "Red" } else { "Green" })
}
else {
@@ -294,5 +329,6 @@ try {
}
finally {
if ($vmSession) { Remove-PSSession $vmSession -ErrorAction SilentlyContinue }
Pop-Location
}
+12 -12
View File
@@ -22,31 +22,31 @@ Vagrant.configure("2") do |config|
config.winrm.username = "vagrant"
config.winrm.password = "vagrant"
config.winrm.transport = :plaintext
config.winrm.transport = :plaintext # Vagrant's own comms (boot check, provision)
config.winrm.basic_auth_only = true
config.vm.guest = :windows
config.vm.boot_timeout = 900
config.vm.graceful_halt_timeout = 120
config.vm.network "forwarded_port", guest: 5985, host: 55985
# Automatically select the Default Switch for Hyper-V to avoid interactive prompt
config.vm.network "public_network", bridge: "Default Switch"
config.vm.network "forwarded_port", guest: 5985, host: 55985 # Vagrant plaintext WinRM
config.vm.network "forwarded_port", guest: 5986, host: 55986 # PSSession HTTPS WinRM
config.vm.provider "hyperv" do |hv|
hv.vmname = "afh-win11-test"
hv.memory = 4096
hv.maxmemory = 4096
hv.cpus = 2
hv.memory = 8192
hv.maxmemory = 8192
hv.cpus = 4
hv.enable_enhanced_session_mode = true
hv.linked_clone = true
end
# SMB synced folder — Hyper-V does not support VirtualBox shared folders.
# Vagrant will prompt for host credentials on first `vagrant up` to set up the SMB share.
config.vm.synced_folder "..", "/vagrant_project",
type: "smb",
smb_username: ENV["VAGRANT_SMB_USERNAME"],
smb_password: ENV["VAGRANT_SMB_PASSWORD"],
mount_options: ["ro"]
# Disable default Vagrant synced folder to prevent SMB credential prompts.
# Files are transferred directly via PSSession by run_vagrant_tests.ps1.
config.vm.synced_folder ".", "/vagrant", disabled: true
config.vm.provision "shell",
path: "provision.ps1",
+66
View File
@@ -12,6 +12,19 @@ Set-ItemProperty -Path $RegPath -Name AutoAdminLogon -Value "1"
Set-ItemProperty -Path $RegPath -Name DefaultUserName -Value "vagrant"
Set-ItemProperty -Path $RegPath -Name DefaultPassword -Value "vagrant"
# --- Allow script execution (required for test suite) ---
# Write the LocalMachine execution policy directly via the registry instead of using
# Set-ExecutionPolicy. Vagrant runs provision.ps1 under "powershell -ExecutionPolicy Bypass",
# which sets a Process-scope policy. When a Process-scope policy is active,
# Set-ExecutionPolicy -Scope LocalMachine throws a terminating SecurityException
# ("overridden by a policy defined at a more specific scope") that cannot be suppressed
# with -ErrorAction. Writing the registry key bypasses that cmdlet restriction entirely.
# The value takes effect in all subsequent WinRM PSSessions (which have no Process-scope
# override) — exactly the sessions that run the test suite.
$psMachinePolicyPath = 'HKLM:\SOFTWARE\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell'
Set-ItemProperty -Path $psMachinePolicyPath -Name 'ExecutionPolicy' -Value 'Unrestricted' -Force
Write-Host " Execution policy set to Unrestricted (LocalMachine)." -ForegroundColor DarkGray
# --- Install Chocolatey (package manager) ---
if (-not (Get-Command choco -ErrorAction SilentlyContinue)) {
Write-Host "Installing Chocolatey..." -ForegroundColor Yellow
@@ -20,10 +33,63 @@ if (-not (Get-Command choco -ErrorAction SilentlyContinue)) {
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
}
# --- Configure HTTPS WinRM listener ---
# HTTP WinRM (plaintext) causes PowerShell PSSession to reject unencrypted
# traffic. A self-signed cert on port 5986 satisfies WinRM's security policy
# without needing a domain CA. The 10-year expiry covers long-lived test boxes.
Write-Host "Configuring HTTPS WinRM listener..." -ForegroundColor Yellow
# Ensure network is Private so WinRM HTTPS works correctly
Get-NetConnectionProfile | Set-NetConnectionProfile -NetworkCategory Private
# Allow local accounts to perform administrative tasks via WinRM
# (Required for the 'vagrant' user to have full admin rights over PSSession)
$policyPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System"
Set-ItemProperty -Path $policyPath -Name LocalAccountTokenFilterPolicy -Value 1 -Force
# Create certificate for HTTPS listener
$cert = New-SelfSignedCertificate `
-DnsName "afh-test", "localhost" `
-CertStoreLocation "Cert:\LocalMachine\My" `
-KeyAlgorithm RSA `
-KeyLength 2048 `
-NotAfter (Get-Date).AddYears(10)
# Check if an HTTPS listener already exists
$listeners = winrm enumerate winrm/config/listener
$hasHttps = $listeners -match "Transport = HTTPS"
if ($hasHttps) {
# If the thumbprint matches, we're already good
if ($listeners -match $cert.Thumbprint) {
Write-Host " HTTPS listener already correctly configured." -ForegroundColor Green
} else {
Write-Host " Updating existing HTTPS listener with new certificate..." -ForegroundColor Gray
winrm delete winrm/config/Listener?Address=*+Transport=HTTPS
winrm create winrm/config/Listener?Address=*+Transport=HTTPS `
"@{Hostname=`"afh-test`";CertificateThumbprint=`"$($cert.Thumbprint)`"}"
}
} else {
Write-Host " Creating new HTTPS listener..." -ForegroundColor Gray
winrm create winrm/config/Listener?Address=*+Transport=HTTPS `
"@{Hostname=`"afh-test`";CertificateThumbprint=`"$($cert.Thumbprint)`"}"
}
winrm set winrm/config/service/Auth '@{Basic="true"}'
netsh advfirewall firewall add rule `
name="WinRM HTTPS" protocol=TCP dir=in localport=5986 action=allow profile=any | Out-Null
Write-Host " HTTPS WinRM listener ready on port 5986" -ForegroundColor Green
# --- Install nircmd for screenshot capture ---
Write-Host "Installing nircmd for screenshot capture..." -ForegroundColor Yellow
choco install nircmd -y --no-progress
# --- Install Inno Setup 6.7.1 (pinned) ---
Write-Host "Installing Inno Setup 6.7.1..." -ForegroundColor Yellow
choco install innosetup --version 6.7.1 -y --no-progress
# Refresh PATH
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
+249 -118
View File
@@ -4,8 +4,9 @@
# Exit codes: 0 = all passed, 1 = one or more failures.
param(
[string]$InstallerPath = "C:\vagrant_project\dist\android-file-handler-setup.exe",
[string]$ScreenshotDir = "C:\vagrant_test_results"
[string]$InstallerPath = "C:\vagrant_build\dist\android-file-handler-setup.exe",
[string]$ScreenshotDir = "C:\vagrant_test_results",
[string]$AppVersion = ""
)
Set-StrictMode -Version Latest
@@ -22,7 +23,7 @@ function Write-TestResult {
$status = if ($Pass) { "PASS" } else { "FAIL" }
$color = if ($Pass) { "Green" } else { "Red" }
$msg = "[$status] $Name"
if ($Detail) { $msg += " — $Detail" }
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 }
@@ -60,7 +61,7 @@ $DefaultInstDir = Join-Path $env:ProgramFiles $AppName
Write-Host ""
Write-Host "=============================================" -ForegroundColor Cyan
Write-Host " Android File Handler — Vagrant Test Suite " -ForegroundColor Cyan
Write-Host " Android File Handler - Vagrant Test Suite " -ForegroundColor Cyan
Write-Host "=============================================" -ForegroundColor Cyan
Write-Host ""
@@ -68,134 +69,264 @@ if (-not (Test-Path $ScreenshotDir)) {
New-Item -ItemType Directory -Path $ScreenshotDir -Force | Out-Null
}
# ── Test 1: Installer exists ────────────────────────────────────────────────
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.
$installerExists = Test-Path $InstallerPath
Write-TestResult -Name "Installer file exists" -Pass $installerExists -Detail $InstallerPath
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 $installerExists) {
Write-Host "`nFATAL: Installer not found at $InstallerPath. Build the installer first." -ForegroundColor Red
Write-Host " poetry run pyinstaller scripts\spec_scripts\android-file-handler-windows.spec"
Write-Host " iscc scripts\windows\android-file-handler-setup.iss"
exit 1
}
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
}
# ── Test 2: Silent install ──────────────────────────────────────────────────
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--- Running silent install ---" -ForegroundColor Yellow
$installProc = Start-Process -FilePath $InstallerPath `
-ArgumentList "/VERYSILENT", "/SUPPRESSMSGBOXES", "/NORESTART", "/SP-" `
-Wait -PassThru
Write-Host "`n--- Building installer inside the VM (Inno Setup 6.7.1) ---" -ForegroundColor Yellow
$installOk = $installProc.ExitCode -eq 0
Write-TestResult -Name "Silent install completed" -Pass $installOk -Detail "Exit code: $($installProc.ExitCode)"
@("$buildRoot\dist", "$buildRoot\scripts\windows", "$buildRoot\icon_media") |
ForEach-Object { New-Item -ItemType Directory -Force -Path $_ | Out-Null }
# ── Test 3: Install directory created ────────────────────────────────────────
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
$dirExists = Test-Path $DefaultInstDir
Write-TestResult -Name "Install directory exists" -Pass $dirExists -Detail $DefaultInstDir
$isccArgs = @("$buildRoot\scripts\windows\android-file-handler-setup.iss")
if ($AppVersion) { $isccArgs += "/DMyAppVersion=$AppVersion" }
# ── 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 ────────────────────────────────────────
$uninstallKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"
$registryEntry = Get-ChildItem $uninstallKey -ErrorAction SilentlyContinue |
Where-Object { (Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue).DisplayName -eq $AppName }
$registryOk = $null -ne $registryEntry
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
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);
& $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
}
"@
$hwnd = [Win32]::FindWindow($null, $AppName)
$windowFound = $hwnd -ne [IntPtr]::Zero
Write-TestResult -Name "Main window found by title" -Pass $windowFound -Detail "HWND: $hwnd"
# Take screenshot of the running application
Start-Sleep -Seconds 2
$screenshotFile = Take-Screenshot -Name "app_running"
Write-Host " Screenshot saved: $screenshotFile" -ForegroundColor DarkGray
# ── Test 1: Installer exists ────────────────────────────────────────────────
# Gracefully close
if (-not $appProc.HasExited) {
$appProc.CloseMainWindow() | Out-Null
Start-Sleep -Seconds 3
if (-not $appProc.HasExited) {
$appProc.Kill()
$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
}
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"
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
}
}
# ── 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
$dirGone = -not (Test-Path $DefaultInstDir)
Write-TestResult -Name "Install directory removed after uninstall" -Pass $dirGone
}
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"
}
# ── 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
$csvPath = Join-Path $ScreenshotDir "test_results.csv"
$Script:Results | Export-Csv -Path $csvPath -NoTypeInformation
Write-Host "Results saved to $csvPath" -ForegroundColor DarkGray
exit $(if ($Script:Failed -gt 0) { 1 } else { 0 })