Skip to content

Commit 7b3c005

Browse files
authored
Merge pull request #208 from flashcatcloud/fix/install-ps1-test
Run install.ps1 behavior tests on Windows
2 parents 2ef1996 + 1a9ab15 commit 7b3c005

3 files changed

Lines changed: 197 additions & 5 deletions

File tree

‎.github/workflows/install-scripts.yml‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ on:
88
- install.sh
99
- install.ps1
1010
- tests/install_sh_sudo_test.sh
11+
- tests/install_ps1_test.ps1
1112
- .github/workflows/install-scripts.yml
1213
pull_request:
1314
paths:
1415
- install.sh
1516
- install.ps1
1617
- tests/install_sh_sudo_test.sh
18+
- tests/install_ps1_test.ps1
1719
- .github/workflows/install-scripts.yml
1820

1921
permissions:
@@ -34,6 +36,23 @@ jobs:
3436
- name: installer behavior tests
3537
run: sh tests/install_sh_sudo_test.sh
3638

39+
windows:
40+
name: install.ps1 behavior (windows)
41+
runs-on: windows-latest
42+
steps:
43+
- uses: actions/checkout@v7
44+
- uses: actions/setup-go@v7
45+
with:
46+
go-version-file: "go.mod"
47+
cache: false
48+
- name: installer behavior tests (Windows PowerShell 5.1)
49+
shell: powershell
50+
run: ./tests/install_ps1_test.ps1
51+
- name: installer behavior tests (PowerShell 7)
52+
if: ${{ !cancelled() }}
53+
shell: pwsh
54+
run: ./tests/install_ps1_test.ps1
55+
3756
mirror:
3857
name: mirror install scripts
3958
runs-on: ubuntu-latest

‎install.ps1‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -156,11 +156,13 @@ try {
156156

157157
$DestPath = Join-Path $InstallDir $InstalledName
158158
if (Test-Path $DestPath) {
159-
# A running .exe can't be overwritten but can be renamed: move it aside
160-
# so `update` can replace the binary it is running from.
161-
$OldPath = "$DestPath.old"
162-
Remove-Item -Path $OldPath -Force -ErrorAction SilentlyContinue
163-
Move-Item -Path $DestPath -Destination $OldPath -Force
159+
# A running .exe can't be overwritten or deleted but can be renamed:
160+
# move it aside under a unique name so `update` can replace the binary
161+
# it is running from. Copies moved aside earlier are deleted; one that
162+
# is still running stays locked and is left for a later install.
163+
Get-ChildItem -Path $InstallDir -Filter "$InstalledName.*.old" -File |
164+
Remove-Item -Force -ErrorAction SilentlyContinue
165+
Move-Item -Path $DestPath -Destination "$DestPath.$([System.Guid]::NewGuid().ToString('N')).old"
164166
}
165167
Move-Item -Path $BinaryPath -Destination $DestPath -Force
166168

‎tests/install_ps1_test.ps1‎

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
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

Comments
 (0)