224 lines
16 KiB
PowerShell
224 lines
16 KiB
PowerShell
# Isolated startup-script regression: never starts a service or reads project data.
|
|
$ErrorActionPreference = 'Stop'
|
|
$repo = Split-Path $PSScriptRoot -Parent
|
|
$fixture = Join-Path ([IO.Path]::GetTempPath()) ('linkport-start-test-' + [Guid]::NewGuid().ToString('N') + ' with spaces')
|
|
$callerDirectory = (Get-Location).Path
|
|
$previousPause = $env:LINKPORT_START_NO_PAUSE
|
|
$previousEnvironment = @{}
|
|
$heldProcesses = @()
|
|
foreach ($name in @('LINKPORT_DATA', 'LINKPORT_START_TEST_DATA_PATH', 'LINKPORT_START_TEST_RESOLVE_FAILURE')) {
|
|
$previousEnvironment[$name] = [Environment]::GetEnvironmentVariable($name, 'Process')
|
|
}
|
|
function Assert-StartTest([bool]$Condition, [string]$Message) { if (!$Condition) { throw $Message } }
|
|
try {
|
|
. (Join-Path $PSScriptRoot 'version.ps1')
|
|
$beforeBuildTime = [DateTime]::UtcNow.AddSeconds(-1)
|
|
$developmentInfo = Get-linkportBuildInfo -Development
|
|
$releaseInfo = Get-linkportBuildInfo
|
|
Assert-StartTest ($developmentInfo.BuildTime -eq 'unknown') 'Development timestamp invalidates the Go executable cache'
|
|
Assert-StartTest ($developmentInfo.Version -eq $releaseInfo.Version -and $developmentInfo.Commit -eq $releaseInfo.Commit) 'Development build lost version or commit metadata'
|
|
$releaseTime = [DateTimeOffset]::Parse($releaseInfo.BuildTime).UtcDateTime
|
|
Assert-StartTest ($releaseTime -ge $beforeBuildTime -and $releaseTime -le [DateTime]::UtcNow) 'Release build lost its actual build timestamp'
|
|
New-Item -ItemType Directory -Path (Join-Path $fixture 'scripts'), (Join-Path $fixture 'data') -Force | Out-Null
|
|
Copy-Item -LiteralPath (Join-Path $repo 'start-dev.cmd') -Destination $fixture
|
|
Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'start-dev.ps1') -Destination (Join-Path $fixture 'scripts')
|
|
Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'build-lock.ps1') -Destination (Join-Path $fixture 'scripts')
|
|
. (Join-Path $fixture 'scripts/build-lock.ps1')
|
|
$firstLock = Enter-LinkPortBuildLock
|
|
try {
|
|
$rejected = $false
|
|
try { $secondLock = Enter-LinkPortBuildLock; $secondLock.Dispose() } catch { $rejected = $true }
|
|
Assert-StartTest $rejected 'Concurrent build was not rejected'
|
|
} finally { $firstLock.Dispose() }
|
|
$nextLock = Enter-LinkPortBuildLock
|
|
$nextLock.Dispose()
|
|
[IO.File]::WriteAllText((Join-Path $fixture 'data/sentinel'), 'fixture data must stay unchanged')
|
|
$fakeBuild = @'
|
|
param([switch]$SkipTests, [switch]$RebuildWeb, [string]$OutputPath, [switch]$Development)
|
|
$root = Split-Path $PSScriptRoot -Parent
|
|
@{skipTests=[bool]$SkipTests;rebuildWeb=[bool]$RebuildWeb;development=[bool]$Development;output=$OutputPath;workingDirectory=(Get-Location).Path} | ConvertTo-Json | Set-Content (Join-Path $root 'arguments.json')
|
|
if (Test-Path (Join-Path $root 'fail-build')) { exit 9 }
|
|
New-Item -ItemType Directory -Force (Split-Path $OutputPath -Parent) | Out-Null
|
|
if (Test-Path -LiteralPath (Join-Path $root 'fixture.exe')) {
|
|
Copy-Item -LiteralPath (Join-Path $root 'fixture.exe') -Destination $OutputPath -Force
|
|
} else {
|
|
[IO.File]::WriteAllText($OutputPath, 'fixture executable placeholder')
|
|
}
|
|
exit 0
|
|
'@
|
|
[IO.File]::WriteAllText((Join-Path $fixture 'scripts/build.ps1'), $fakeBuild)
|
|
$env:LINKPORT_START_NO_PAUSE = '1'
|
|
$launcher = Join-Path $fixture 'start-dev.cmd'
|
|
& $launcher -BuildOnly
|
|
Assert-StartTest ($LASTEXITCODE -eq 0) 'Default build-only failed'
|
|
$arguments = Get-Content -Raw (Join-Path $fixture 'arguments.json') | ConvertFrom-Json
|
|
Assert-StartTest ($arguments.skipTests -and !$arguments.rebuildWeb -and $arguments.development) 'Fast flags not forwarded'
|
|
Assert-StartTest ($arguments.workingDirectory -eq $fixture) 'CMD did not use the entry directory'
|
|
Assert-StartTest ((Get-Location).Path -eq $callerDirectory) 'CMD changed the caller directory'
|
|
Assert-StartTest ($arguments.output -eq (Join-Path $fixture 'bin/dev-build/linkport.exe')) 'Build output not isolated'
|
|
Assert-StartTest (!(Test-Path (Join-Path $fixture 'bin/linkport.exe'))) 'Build-only installed a binary'
|
|
Assert-StartTest (!(Test-Path (Join-Path $fixture 'bin/backups'))) 'Build-only touched backups'
|
|
& $launcher -BuildOnly -FullCheck -RebuildWeb
|
|
Assert-StartTest ($LASTEXITCODE -eq 0) 'Full build-only failed'
|
|
$arguments = Get-Content -Raw (Join-Path $fixture 'arguments.json') | ConvertFrom-Json
|
|
Assert-StartTest (!$arguments.skipTests -and $arguments.rebuildWeb -and $arguments.development) 'Full-check/force flags not forwarded'
|
|
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $fixture 'scripts/start-dev.ps1') -BuildOnly
|
|
Assert-StartTest ($LASTEXITCODE -eq 0) 'Direct PowerShell build-only failed'
|
|
$arguments = Get-Content -Raw (Join-Path $fixture 'arguments.json') | ConvertFrom-Json
|
|
Assert-StartTest ($arguments.workingDirectory -eq $fixture) 'PowerShell did not use the entry directory'
|
|
Assert-StartTest ((Get-Location).Path -eq $callerDirectory) 'PowerShell changed the caller directory'
|
|
[IO.File]::WriteAllText((Join-Path $fixture 'fail-build'), 'simulate failure with old staged executable still present')
|
|
$ErrorActionPreference = 'Continue'
|
|
& $launcher -BuildOnly 2>$null | Out-Null
|
|
$failedExit = $LASTEXITCODE
|
|
$ErrorActionPreference = 'Stop'
|
|
Assert-StartTest ($failedExit -ne 0) 'Failed build reused old staged executable'
|
|
Assert-StartTest ((Get-Location).Path -eq $callerDirectory) 'Failed launch changed the caller directory'
|
|
Assert-StartTest (!(Test-Path (Join-Path $fixture 'bin/backups'))) 'Failed build touched backups'
|
|
Assert-StartTest (([IO.File]::ReadAllText((Join-Path $fixture 'data/sentinel'))) -eq 'fixture data must stay unchanged') 'Fixture data changed'
|
|
Remove-Item -LiteralPath (Join-Path $fixture 'fail-build')
|
|
$fakeProgram = @'
|
|
using System;
|
|
using System.IO;
|
|
public static class LinkPortStartFixture {
|
|
public static int Main(string[] arguments) {
|
|
if (arguments.Length == 1 && arguments[0] == "hold") {
|
|
File.WriteAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ready.txt"), "ready");
|
|
System.Threading.Thread.Sleep(System.Threading.Timeout.Infinite);
|
|
return 0;
|
|
}
|
|
if (arguments.Length == 1 && arguments[0] == "data-path") {
|
|
if (Environment.GetEnvironmentVariable("LINKPORT_START_TEST_RESOLVE_FAILURE") == "1") return 9;
|
|
Console.WriteLine(Environment.GetEnvironmentVariable("LINKPORT_START_TEST_DATA_PATH"));
|
|
return 0;
|
|
}
|
|
File.WriteAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "launched-data.txt"), Environment.GetEnvironmentVariable("LINKPORT_DATA") ?? "<unset>");
|
|
return 0;
|
|
}
|
|
}
|
|
'@
|
|
Add-Type -TypeDefinition $fakeProgram -OutputAssembly (Join-Path $fixture 'fixture.exe') -OutputType ConsoleApplication
|
|
$legacyDirectory = Join-Path $fixture 'legacy data'
|
|
New-Item -ItemType Directory -Path (Join-Path $legacyDirectory 'audit'), (Join-Path $legacyDirectory 'logs'), (Join-Path $legacyDirectory 'tls') -Force | Out-Null
|
|
$backupFiles = @('linkport.sqlite', 'linkport.sqlite-wal', 'linkport.sqlite-shm', 'linkport.sqlite-journal', 'master-key', 'master-key.dpapi', 'tls\key.pem', 'settings.json')
|
|
$excludedFiles = @('audit\day.sqlite', 'audit\day.sqlite-wal', 'linkport.log', 'linkport.log.1', 'logs\runtime.txt')
|
|
foreach ($relativePath in ($backupFiles + $excludedFiles)) {
|
|
[IO.File]::WriteAllText((Join-Path $legacyDirectory $relativePath), "fixture $relativePath")
|
|
}
|
|
$env:LINKPORT_DATA = ''
|
|
$env:LINKPORT_START_TEST_DATA_PATH = Join-Path $legacyDirectory 'linkport.sqlite'
|
|
$foreignDirectory = Join-Path $fixture 'unowned'
|
|
New-Item -ItemType Directory -Path $foreignDirectory, (Join-Path $fixture 'bin') -Force | Out-Null
|
|
$heldPaths = @((Join-Path $fixture 'linkport.exe'), (Join-Path $fixture 'bin/linkport.exe'), (Join-Path $foreignDirectory 'linkport.exe'))
|
|
foreach ($heldPath in $heldPaths) {
|
|
Copy-Item -LiteralPath (Join-Path $fixture 'fixture.exe') -Destination $heldPath
|
|
$heldProcesses += Start-Process -FilePath $heldPath -ArgumentList 'hold' -WindowStyle Hidden -PassThru
|
|
$readyPath = Join-Path (Split-Path $heldPath -Parent) 'ready.txt'
|
|
$readyDeadline = [DateTime]::UtcNow.AddSeconds(10)
|
|
while (!(Test-Path -LiteralPath $readyPath) -and [DateTime]::UtcNow -lt $readyDeadline) { Start-Sleep -Milliseconds 50 }
|
|
Assert-StartTest (Test-Path -LiteralPath $readyPath) 'Fixture process did not start'
|
|
}
|
|
& $launcher
|
|
Assert-StartTest ($LASTEXITCODE -eq 0) 'Source launch failed'
|
|
Assert-StartTest ($heldProcesses[0].WaitForExit(1000) -and $heldProcesses[1].WaitForExit(1000)) 'Launcher did not stop both owned executable paths'
|
|
Assert-StartTest (!$heldProcesses[2].HasExited) 'Launcher stopped an unowned linkport executable'
|
|
$launchRecord = Join-Path $fixture 'bin/launched-data.txt'
|
|
Assert-StartTest (([IO.File]::ReadAllText($launchRecord)) -eq '<unset>') 'Launcher forced a database path'
|
|
$backups = @(Get-ChildItem -LiteralPath (Join-Path $fixture 'bin/backups') -Directory)
|
|
Assert-StartTest ($backups.Count -eq 1) 'Source launch did not create exactly one backup'
|
|
foreach ($relativePath in $backupFiles) {
|
|
$backupFile = Join-Path $backups[0].FullName (Join-Path 'legacy data' $relativePath)
|
|
Assert-StartTest (([IO.File]::ReadAllText($backupFile)) -eq "fixture $relativePath") 'Selected non-log data was not backed up'
|
|
}
|
|
foreach ($relativePath in $excludedFiles) {
|
|
Assert-StartTest (!(Test-Path -LiteralPath (Join-Path $backups[0].FullName (Join-Path 'legacy data' $relativePath)))) 'Log data was included in the startup backup'
|
|
Assert-StartTest (([IO.File]::ReadAllText((Join-Path $legacyDirectory $relativePath))) -eq "fixture $relativePath") 'Original log data changed'
|
|
}
|
|
foreach ($excludedDirectory in @('audit', 'logs')) {
|
|
Assert-StartTest (!(Test-Path -LiteralPath (Join-Path $backups[0].FullName (Join-Path 'legacy data' $excludedDirectory)))) 'Excluded log directory was traversed or copied'
|
|
}
|
|
Assert-StartTest (!(Test-Path -LiteralPath (Join-Path $backups[0].FullName 'data'))) 'Launcher backed up the unselected local directory'
|
|
$env:LINKPORT_DATA = '.\custom data\selected.sqlite'
|
|
$env:LINKPORT_START_TEST_DATA_PATH = Join-Path $fixture 'custom data/selected.sqlite'
|
|
New-Item -ItemType Directory -Path (Split-Path $env:LINKPORT_START_TEST_DATA_PATH -Parent) -Force | Out-Null
|
|
[IO.File]::WriteAllText($env:LINKPORT_START_TEST_DATA_PATH, 'explicit database fixture')
|
|
& $launcher
|
|
Assert-StartTest ($LASTEXITCODE -eq 0) 'Explicit-path launch failed'
|
|
Assert-StartTest (([IO.File]::ReadAllText($launchRecord)) -eq $env:LINKPORT_DATA) 'Launcher replaced the explicit database path'
|
|
$backups = @(Get-ChildItem -LiteralPath (Join-Path $fixture 'bin/backups') -Directory)
|
|
Assert-StartTest ($backups.Count -eq 2) 'Explicit-path launch did not create a backup'
|
|
$env:LINKPORT_START_TEST_RESOLVE_FAILURE = '1'
|
|
$ErrorActionPreference = 'Continue'
|
|
& $launcher 2>$null | Out-Null
|
|
$failedExit = $LASTEXITCODE
|
|
$ErrorActionPreference = 'Stop'
|
|
Assert-StartTest ($failedExit -ne 0) 'Database resolution failure was ignored'
|
|
$env:LINKPORT_START_TEST_RESOLVE_FAILURE = ''
|
|
$env:LINKPORT_START_TEST_DATA_PATH = Join-Path $fixture 'linkport.sqlite'
|
|
$ErrorActionPreference = 'Continue'
|
|
& $launcher 2>$null | Out-Null
|
|
$failedExit = $LASTEXITCODE
|
|
$ErrorActionPreference = 'Stop'
|
|
Assert-StartTest ($failedExit -ne 0) 'Recursive backup destination was accepted'
|
|
$backups = @(Get-ChildItem -LiteralPath (Join-Path $fixture 'bin/backups') -Directory)
|
|
Assert-StartTest ($backups.Count -eq 2) 'Rejected launch created a backup'
|
|
$env:LINKPORT_START_TEST_DATA_PATH = Join-Path $legacyDirectory 'linkport.sqlite'
|
|
$hashProvider = [Security.Cryptography.SHA256]::Create()
|
|
try {
|
|
$lockHash = ([BitConverter]::ToString($hashProvider.ComputeHash([Text.Encoding]::UTF8.GetBytes(([IO.Path]::GetFullPath($env:LINKPORT_START_TEST_DATA_PATH)).ToLowerInvariant())))).Replace('-', '').ToLowerInvariant()
|
|
} finally { $hashProvider.Dispose() }
|
|
$busyLock = New-Object Threading.Mutex($false, "Local\linkport-$lockHash")
|
|
try {
|
|
$ErrorActionPreference = 'Continue'
|
|
& $launcher 2>$null | Out-Null
|
|
$failedExit = $LASTEXITCODE
|
|
$ErrorActionPreference = 'Stop'
|
|
Assert-StartTest ($failedExit -ne 0) 'Launcher backed up data held by another local instance'
|
|
$backups = @(Get-ChildItem -LiteralPath (Join-Path $fixture 'bin/backups') -Directory)
|
|
Assert-StartTest ($backups.Count -eq 2) 'Busy data directory created a backup'
|
|
} finally {
|
|
$busyLock.Dispose()
|
|
}
|
|
$releaseDirectory = Join-Path $fixture 'release with spaces'
|
|
New-Item -ItemType Directory -Path (Join-Path $releaseDirectory 'scripts') -Force | Out-Null
|
|
Copy-Item -LiteralPath (Join-Path $repo 'start-dev.cmd') -Destination $releaseDirectory
|
|
Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'start-dev.ps1') -Destination (Join-Path $releaseDirectory 'scripts')
|
|
Copy-Item -LiteralPath (Join-Path $fixture 'fixture.exe') -Destination (Join-Path $releaseDirectory 'linkport.exe')
|
|
$env:LINKPORT_DATA = ''
|
|
$env:LINKPORT_START_TEST_DATA_PATH = Join-Path $legacyDirectory 'linkport.sqlite'
|
|
& (Join-Path $releaseDirectory 'start-dev.cmd')
|
|
Assert-StartTest ($LASTEXITCODE -eq 0) 'Release launch failed'
|
|
Assert-StartTest (([IO.File]::ReadAllText((Join-Path $releaseDirectory 'launched-data.txt'))) -eq '<unset>') 'Release launcher forced a database path'
|
|
$releaseBackups = @(Get-ChildItem -LiteralPath (Join-Path $releaseDirectory 'bin/backups') -Directory)
|
|
Assert-StartTest ($releaseBackups.Count -eq 1) 'Release launch did not back up the selected directory'
|
|
foreach ($relativePath in $backupFiles) {
|
|
Assert-StartTest (([IO.File]::ReadAllText((Join-Path $releaseBackups[0].FullName (Join-Path 'legacy data' $relativePath)))) -eq "fixture $relativePath") 'Release startup did not preserve non-log backup data'
|
|
}
|
|
foreach ($relativePath in $excludedFiles) {
|
|
Assert-StartTest (!(Test-Path -LiteralPath (Join-Path $releaseBackups[0].FullName (Join-Path 'legacy data' $relativePath)))) 'Release startup backed up log data'
|
|
}
|
|
Assert-StartTest ((Get-Location).Path -eq $callerDirectory) 'Launch changed the caller directory'
|
|
Assert-StartTest (([IO.File]::ReadAllText((Join-Path $fixture 'data/sentinel'))) -eq 'fixture data must stay unchanged') 'Unselected local data changed'
|
|
Write-Host 'Startup script tests passed: relative entry paths, caller-directory preservation, build locking, build flags/isolation, owned-process stopping and unowned-process preservation, unchanged data override, selected-directory backups excluding logs while preserving source logs, source/release startup and resolution/recursive-backup/busy-data failure protection.'
|
|
} finally {
|
|
foreach ($heldProcess in $heldProcesses) {
|
|
try {
|
|
if (!$heldProcess.HasExited) {
|
|
$heldPath = [IO.Path]::GetFullPath($heldProcess.Path)
|
|
$fixturePrefix = [IO.Path]::GetFullPath($fixture).TrimEnd('\') + '\'
|
|
if (!$heldPath.StartsWith($fixturePrefix, [StringComparison]::OrdinalIgnoreCase)) { throw 'Unexpected fixture process path' }
|
|
$heldProcess.Kill()
|
|
if (!$heldProcess.WaitForExit(10000)) { throw 'Fixture process did not exit' }
|
|
}
|
|
} finally { $heldProcess.Dispose() }
|
|
}
|
|
$env:LINKPORT_START_NO_PAUSE = $previousPause
|
|
foreach ($name in $previousEnvironment.Keys) {
|
|
[Environment]::SetEnvironmentVariable($name, $previousEnvironment[$name], 'Process')
|
|
}
|
|
$resolved = [IO.Path]::GetFullPath($fixture)
|
|
$tempRoot = [IO.Path]::GetFullPath([IO.Path]::GetTempPath())
|
|
if (!$resolved.StartsWith($tempRoot, [StringComparison]::OrdinalIgnoreCase) -or (Split-Path $resolved -Leaf) -notlike 'linkport-start-test-*') { throw 'Unexpected test cleanup path' }
|
|
if (Test-Path -LiteralPath $resolved) { Remove-Item -LiteralPath $resolved -Recurse -Force }
|
|
}
|