The installer inferred that question from whatever the server happened to look like: a MySQL service exists, the database has tables, the site exists, the venv exists. None of those record who created them. A retry after a failed first install was therefore taken for an upgrade of somebody else's working system, which produced two dead ends on exactly the retry the wizard invites: stage 3 demanded a mandatory backup of a database its own failed attempt had written, and then refused to prune tables it had created minutes earlier, because core migration 7d05 seeds access protocols owned by the computers plugin and any profile without that plugin hit the refusal every single time. An install record at ProgramData\ShopDB-Flask\install-state.json answers it instead. It is written when provisioning STARTS rather than when it finishes, because the run that dies halfway is precisely the run whose retry needs it, and it records what this installer created as it goes, so a crashed run no longer leaves the next one guessing from the machine. During unfinished first provisioning the pre-migration backup becomes advisory and prune may force, since every row present was written by an earlier attempt of the same install. On an established install both stay exactly as they were. The classification is deliberately asymmetric: an install predating this record carries a version stamp and probably real data, so it is treated as established and keeps the mandatory backup. Guessing "first run" there would arm prune --force against live tables. Get-CreatedItems comma-protects its return. A zero-length array returned from a PowerShell function unrolls to $null, and $null.Count is fatal under StrictMode 2.0 - the same fault that made bundle verification fail on every install earlier. The harness caught it before it shipped. Tests: deploy/windows/installer/tests/test-install-state.ps1 exercises new servers, retries, completed installs, unrecorded-but-stamped installs, records naming another directory, corrupt records, and persistence across a crash. tests/test_installer_state.py runs it wherever pwsh exists and asserts the invariants as text everywhere else. Both were confirmed to fail when the prune gate or the comma protection is removed. pytest.ini stops collection walking into deploy/windows/installer/bundle, which is build output holding a complete second copy of the application. Importing every plugin twice made SQLAlchemy refuse a redefined table and the whole suite fail to collect, on a tree with nothing wrong in it, purely because an installer had been built first. It surfaced only when the bundle grew from four plugins to thirteen.
131 lines
5.1 KiB
PowerShell
131 lines
5.1 KiB
PowerShell
# Tests the installer's durable install record.
|
|
#
|
|
# The record answers one question: "is this a re-run of MY install?". Getting it
|
|
# wrong is destructive in one direction - answering "first provisioning" for a
|
|
# server holding real data lets prune-schema --force loose on it - and a dead
|
|
# end in the other, which is what it was built to fix.
|
|
#
|
|
# Run directly: pwsh -File test-install-state.ps1
|
|
# pytest runs it through tests/test_installer_state.py wherever pwsh exists.
|
|
|
|
Set-StrictMode -Version 2.0
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
$Here = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
$Installer = Join-Path (Split-Path -Parent $Here) 'shopdb-install.ps1'
|
|
if (-not (Test-Path $Installer)) { throw "installer not found at $Installer" }
|
|
|
|
$Sandbox = Join-Path ([System.IO.Path]::GetTempPath()) ('shopdb-state-' + [System.Guid]::NewGuid().ToString('N'))
|
|
|
|
# Load ONLY the state block, so the rest of the installer does not run. Bounded
|
|
# by the same markers the installer uses, and it fails loudly if either moves
|
|
# rather than silently testing nothing.
|
|
$lines = Get-Content $Installer
|
|
$start = ($lines | Select-String -Pattern '^\$script:StateFile ' | Select-Object -First 1)
|
|
$end = ($lines | Select-String -Pattern '^function Invoke-Native' | Select-Object -First 1)
|
|
if (-not $start -or -not $end) { throw 'could not locate the install-record block in shopdb-install.ps1' }
|
|
$block = $lines[($start.LineNumber - 1)..($end.LineNumber - 2)] -join "`n"
|
|
if ($block -notmatch 'function Test-FirstProvisioning') { throw 'extracted block does not contain the state functions' }
|
|
|
|
# Stand-ins for the surrounding script.
|
|
$script:Created = New-Object System.Collections.ArrayList
|
|
function Write-Log { param($m, $l = 'INFO') }
|
|
function Protect-File { param([string] $Path, [switch] $Directory) }
|
|
function Test-SamePath {
|
|
param([string] $A, [string] $B)
|
|
if (-not $A) { return -not $B }
|
|
return ($A.TrimEnd('\', '/').ToLowerInvariant() -eq $B.TrimEnd('\', '/').ToLowerInvariant())
|
|
}
|
|
if (-not $env:ProgramData) { $env:ProgramData = [System.IO.Path]::GetTempPath() }
|
|
|
|
Invoke-Expression $block
|
|
|
|
$script:Failures = 0
|
|
function Check {
|
|
param([string] $Name, $Expected, $Actual)
|
|
if ($Expected -eq $Actual) {
|
|
Write-Host (" PASS " + $Name)
|
|
} else {
|
|
Write-Host (" FAIL {0}: expected [{1}], got [{2}]" -f $Name, $Expected, $Actual)
|
|
$script:Failures++
|
|
}
|
|
}
|
|
|
|
function Reset-Case {
|
|
param([switch] $Stamped)
|
|
if (Test-Path $Sandbox) { Remove-Item $Sandbox -Recurse -Force }
|
|
New-Item -ItemType Directory -Path $Sandbox -Force | Out-Null
|
|
$script:AppRoot = $Sandbox
|
|
$script:StateFile = Join-Path $Sandbox 'install-state.json'
|
|
$script:InstallState = $null
|
|
if ($Stamped) { Set-Content (Join-Path $Sandbox '.installed-version') '0.7.0' }
|
|
}
|
|
|
|
Write-Host 'brand new server'
|
|
Reset-Case
|
|
Initialize-InstallState
|
|
Check 'first provisioning' $true (Test-FirstProvisioning)
|
|
Check 'record written' $true (Test-Path $script:StateFile)
|
|
|
|
Write-Host 'retry of a failed first install'
|
|
$script:InstallState = $null
|
|
Initialize-InstallState
|
|
Check 'still first provisioning' $true (Test-FirstProvisioning)
|
|
|
|
Write-Host 'genuine upgrade after a completed install'
|
|
Complete-InstallState
|
|
$script:InstallState = $null
|
|
Initialize-InstallState
|
|
Check 'no longer first provisioning' $false (Test-FirstProvisioning)
|
|
|
|
Write-Host 'install predating the record - must take the safe side'
|
|
Reset-Case -Stamped
|
|
Initialize-InstallState
|
|
Check 'treated as established' $false (Test-FirstProvisioning)
|
|
|
|
Write-Host 'record naming a different directory'
|
|
Reset-Case
|
|
Initialize-InstallState
|
|
$other = Get-Content $script:StateFile -Raw | ConvertFrom-Json
|
|
$other.approot = 'C:\somewhere-else'
|
|
($other | ConvertTo-Json -Depth 6) | Set-Content $script:StateFile
|
|
$script:InstallState = $null
|
|
Check 'foreign record rejected' $null (Get-InstallState)
|
|
|
|
Write-Host 'corrupt record'
|
|
Reset-Case
|
|
Set-Content $script:StateFile '{ this is not json'
|
|
$script:InstallState = $null
|
|
Check 'corrupt record rejected' $null (Get-InstallState)
|
|
Reset-Case
|
|
Set-Content $script:StateFile '{ this is not json'
|
|
Initialize-InstallState
|
|
Check 'recovers and rewrites' $true ($null -ne $script:InstallState)
|
|
|
|
Write-Host 'created items survive a crash'
|
|
Reset-Case
|
|
Initialize-InstallState
|
|
Track 'site' 'shopdb-flask'
|
|
Track 'apppool' 'shopdbflask'
|
|
Track 'site' 'shopdb-flask'
|
|
$script:InstallState = $null
|
|
Initialize-InstallState
|
|
Check 'site remembered' 'shopdb-flask' ((Get-CreatedItems 'site') -join ',')
|
|
Check 'apppool remembered' 'shopdbflask' ((Get-CreatedItems 'apppool') -join ',')
|
|
# A zero-length array returned from a function unrolls to $null, and $null.Count
|
|
# is fatal under StrictMode. This is the check that catches losing the comma.
|
|
Check 'unknown kind is an empty array' 0 ((Get-CreatedItems 'firewall')).Count
|
|
|
|
Write-Host 'no record at all'
|
|
$script:InstallState = $null
|
|
Check 'empty list, not null' 0 ((Get-CreatedItems 'site')).Count
|
|
Check 'not first provisioning' $false (Test-FirstProvisioning)
|
|
|
|
if (Test-Path $Sandbox) { Remove-Item $Sandbox -Recurse -Force }
|
|
if ($script:Failures -gt 0) {
|
|
Write-Host ("{0} check(s) failed" -f $script:Failures)
|
|
exit 1
|
|
}
|
|
Write-Host 'all checks passed'
|
|
exit 0
|