|
| 1 | +# Behavior tests for install.ps1. Hermetic: release archives are built locally |
| 2 | +# from a stub program and served by an Invoke-WebRequest stand-in, so no |
| 3 | +# network is touched. Needs Windows and Go on PATH. |
| 4 | +$ErrorActionPreference = "Stop" |
| 5 | + |
| 6 | +$Root = Split-Path -Parent $PSScriptRoot |
| 7 | +$InstallScript = Join-Path $Root "install.ps1" |
| 8 | +$TmpDir = Join-Path ([System.IO.Path]::GetTempPath()) "install-ps1-test-$([System.Guid]::NewGuid().ToString('N'))" |
| 9 | +$Fixtures = Join-Path $TmpDir "fixtures" |
| 10 | +New-Item -ItemType Directory -Path $Fixtures -Force | Out-Null |
| 11 | + |
| 12 | +$Arch = switch ($env:PROCESSOR_ARCHITECTURE) { |
| 13 | + "AMD64" { "x86_64" } |
| 14 | + "ARM64" { "arm64" } |
| 15 | + default { throw "unsupported architecture: $env:PROCESSOR_ARCHITECTURE" } |
| 16 | +} |
| 17 | +$Archive = "flashduty-cli_Windows_${Arch}.zip" |
| 18 | + |
| 19 | +# Stub CLI: prints its build version; `sleep` keeps it running so the |
| 20 | +# installed .exe is locked while a reinstall happens. |
| 21 | +$StubSource = Join-Path $TmpDir "stub.go" |
| 22 | +Set-Content -Path $StubSource -Encoding ascii -Value @' |
| 23 | +package main |
| 24 | +
|
| 25 | +import ( |
| 26 | + "fmt" |
| 27 | + "os" |
| 28 | + "time" |
| 29 | +) |
| 30 | +
|
| 31 | +var version = "dev" |
| 32 | +
|
| 33 | +func main() { |
| 34 | + if len(os.Args) > 1 && os.Args[1] == "sleep" { |
| 35 | + time.Sleep(10 * time.Minute) |
| 36 | + } |
| 37 | + fmt.Println("stub " + version) |
| 38 | +} |
| 39 | +'@ |
| 40 | + |
| 41 | +# Each release is <fixtures>/<version>/{archive, checksums.txt}, the layout |
| 42 | +# install.ps1 downloads from <MIRROR_URL>/releases/download/<version>/. |
| 43 | +function New-Release($Version) { |
| 44 | + $dir = Join-Path $Fixtures $Version |
| 45 | + $build = Join-Path $dir "build" |
| 46 | + New-Item -ItemType Directory -Path $build -Force | Out-Null |
| 47 | + & go build -ldflags "-X main.version=$Version" -o (Join-Path $build "flashduty-cli.exe") $StubSource |
| 48 | + if ($LASTEXITCODE -ne 0) { throw "go build failed for $Version" } |
| 49 | + $zip = Join-Path $dir $Archive |
| 50 | + Compress-Archive -Path (Join-Path $build "flashduty-cli.exe") -DestinationPath $zip -Force |
| 51 | + $sum = (Get-FileHash -Path $zip -Algorithm SHA256).Hash.ToLower() |
| 52 | + Set-Content -Path (Join-Path $dir "checksums.txt") -Encoding ascii -Value "$sum $Archive" |
| 53 | +} |
| 54 | + |
| 55 | +# Stand-in for the cmdlet: functions take precedence over cmdlets, so the |
| 56 | +# installer run below in this session downloads from the local fixtures. |
| 57 | +function Invoke-WebRequest { |
| 58 | + param([string]$Uri, [string]$OutFile, [switch]$UseBasicParsing) |
| 59 | + if ($Uri -notmatch '/releases/download/([^/]+)/([^/]+)$') { throw "unexpected URL: $Uri" } |
| 60 | + $src = Join-Path (Join-Path $Fixtures $Matches[1]) $Matches[2] |
| 61 | + if (-not (Test-Path $src)) { throw "no fixture for URL: $Uri" } |
| 62 | + Copy-Item -Path $src -Destination $OutFile -Force |
| 63 | +} |
| 64 | + |
| 65 | +function Invoke-Installer($InstallDir, $Version, $InstalledName) { |
| 66 | + $env:MIRROR_URL = "https://mirror.example/flashduty-cli" |
| 67 | + $env:FLASHDUTY_VERSION = $Version |
| 68 | + $env:FLASHDUTY_INSTALL_DIR = $InstallDir |
| 69 | + $env:INSTALLED_NAME = $InstalledName |
| 70 | + & $InstallScript | Out-Null |
| 71 | +} |
| 72 | + |
| 73 | +function Assert-Runs($Exe, $Version) { |
| 74 | + $out = (& $Exe | Out-String).Trim() |
| 75 | + if ($out -ne "stub $Version") { throw "$Exe printed '$out', want 'stub $Version'" } |
| 76 | +} |
| 77 | + |
| 78 | +function Assert-ExactName($Dir, $Name) { |
| 79 | + $names = @(Get-ChildItem -Path $Dir -File | Where-Object { $_.Name -notlike "*.old" } | ForEach-Object { $_.Name }) |
| 80 | + if ($names.Count -ne 1 -or $names[0] -cne $Name) { |
| 81 | + throw "files in ${Dir}: [$($names -join ', ')], want exactly [$Name]" |
| 82 | + } |
| 83 | +} |
| 84 | + |
| 85 | +function Get-OldCopies($Dir, $Name) { |
| 86 | + @(Get-ChildItem -Path $Dir -File -Filter "$Name*.old") |
| 87 | +} |
| 88 | + |
| 89 | +function Test-Case($Name, [scriptblock]$Body) { |
| 90 | + try { |
| 91 | + & $Body |
| 92 | + Write-Host "PASS: $Name" |
| 93 | + } catch { |
| 94 | + Write-Host "FAIL: $Name -- $_" |
| 95 | + $script:Failed++ |
| 96 | + } |
| 97 | +} |
| 98 | + |
| 99 | +$Failed = 0 |
| 100 | +$Sleepers = @() |
| 101 | +$SavedUserPath = [Environment]::GetEnvironmentVariable("Path", "User") |
| 102 | +try { |
| 103 | + New-Release "v1.0.0" |
| 104 | + New-Release "v2.0.0" |
| 105 | + New-Release "v3.0.0" |
| 106 | + |
| 107 | + Test-Case "default name installs flashduty.exe" { |
| 108 | + $dir = Join-Path $TmpDir "default" |
| 109 | + Invoke-Installer $dir "v1.0.0" "" |
| 110 | + Assert-ExactName $dir "flashduty.exe" |
| 111 | + Assert-Runs (Join-Path $dir "flashduty.exe") "v1.0.0" |
| 112 | + } |
| 113 | + |
| 114 | + Test-Case "INSTALLED_NAME=fduty installs fduty.exe" { |
| 115 | + $dir = Join-Path $TmpDir "fduty" |
| 116 | + Invoke-Installer $dir "v1.0.0" "fduty" |
| 117 | + Assert-ExactName $dir "fduty.exe" |
| 118 | + Assert-Runs (Join-Path $dir "fduty.exe") "v1.0.0" |
| 119 | + } |
| 120 | + |
| 121 | + Test-Case "INSTALLED_NAME=FDUTY.EXE installs FDUTY.exe" { |
| 122 | + $dir = Join-Path $TmpDir "fduty-upper" |
| 123 | + Invoke-Installer $dir "v1.0.0" "FDUTY.EXE" |
| 124 | + Assert-ExactName $dir "FDUTY.exe" |
| 125 | + Assert-Runs (Join-Path $dir "FDUTY.exe") "v1.0.0" |
| 126 | + } |
| 127 | + |
| 128 | + $runDir = Join-Path $TmpDir "running" |
| 129 | + $runExe = Join-Path $runDir "flashduty.exe" |
| 130 | + |
| 131 | + Test-Case "reinstall replaces the installed exe while it is running" { |
| 132 | + Invoke-Installer $runDir "v1.0.0" "" |
| 133 | + $script:Sleepers += Start-Process -FilePath $runExe -ArgumentList "sleep" -PassThru -WindowStyle Hidden |
| 134 | + Start-Sleep -Seconds 1 |
| 135 | + Invoke-Installer $runDir "v2.0.0" "" |
| 136 | + Assert-Runs $runExe "v2.0.0" |
| 137 | + if ($script:Sleepers[-1].HasExited) { throw "running v1.0.0 process exited during reinstall" } |
| 138 | + if ((Get-OldCopies $runDir "flashduty.exe").Count -ne 1) { throw "want one moved-aside copy of the running exe" } |
| 139 | + } |
| 140 | + |
| 141 | + Test-Case "reinstall while the moved-aside copy is still running" { |
| 142 | + # The v1.0.0 process from the previous case still runs from the |
| 143 | + # moved-aside file, which therefore cannot be deleted or replaced. |
| 144 | + $script:Sleepers += Start-Process -FilePath $runExe -ArgumentList "sleep" -PassThru -WindowStyle Hidden |
| 145 | + Start-Sleep -Seconds 1 |
| 146 | + Invoke-Installer $runDir "v3.0.0" "" |
| 147 | + Assert-Runs $runExe "v3.0.0" |
| 148 | + Assert-ExactName $runDir "flashduty.exe" |
| 149 | + } |
| 150 | + |
| 151 | + Test-Case "a stale moved-aside copy from an earlier install is cleaned up" { |
| 152 | + $dir = Join-Path $TmpDir "stale" |
| 153 | + Invoke-Installer $dir "v1.0.0" "" |
| 154 | + Invoke-Installer $dir "v2.0.0" "" |
| 155 | + Invoke-Installer $dir "v3.0.0" "" |
| 156 | + Assert-Runs (Join-Path $dir "flashduty.exe") "v3.0.0" |
| 157 | + $old = Get-OldCopies $dir "flashduty.exe" |
| 158 | + if ($old.Count -ne 1) { throw "want one moved-aside copy after three installs, got $($old.Count): $($old.Name -join ', ')" } |
| 159 | + } |
| 160 | +} finally { |
| 161 | + $Sleepers | Where-Object { -not $_.HasExited } | Stop-Process -Force -ErrorAction SilentlyContinue |
| 162 | + [Environment]::SetEnvironmentVariable("Path", $SavedUserPath, "User") |
| 163 | + Start-Sleep -Seconds 1 |
| 164 | + Remove-Item -Path $TmpDir -Recurse -Force -ErrorAction SilentlyContinue |
| 165 | +} |
| 166 | + |
| 167 | +if ($Failed -gt 0) { |
| 168 | + Write-Host "$Failed case(s) failed" |
| 169 | + exit 1 |
| 170 | +} |
| 171 | +Write-Host "all install.ps1 cases passed" |
0 commit comments