2026-08-07 17:52:50 +08:00
|
|
|
[CmdletBinding()]
|
|
|
|
|
param(
|
|
|
|
|
[string]$PgRoot = 'D:\pgsql17'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
|
Set-StrictMode -Version Latest
|
|
|
|
|
|
|
|
|
|
$repoRoot = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot '..')).Path
|
|
|
|
|
$pgRootResolved = (Resolve-Path -LiteralPath $PgRoot).Path
|
|
|
|
|
$bin = Join-Path $pgRootResolved 'bin'
|
|
|
|
|
$initdb = Join-Path $bin 'initdb.exe'
|
|
|
|
|
$pgCtl = Join-Path $bin 'pg_ctl.exe'
|
|
|
|
|
$psql = Join-Path $bin 'psql.exe'
|
|
|
|
|
$createdb = Join-Path $bin 'createdb.exe'
|
|
|
|
|
foreach ($required in @($initdb, $pgCtl, $psql, $createdb)) {
|
|
|
|
|
if (-not (Test-Path -LiteralPath $required -PathType Leaf)) {
|
|
|
|
|
throw "PostgreSQL executable is missing under the selected PgRoot."
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$postgresExe = Join-Path $bin 'postgres.exe'
|
|
|
|
|
$version = (& $postgresExe --version 2>&1 | Out-String).Trim()
|
|
|
|
|
if ($LASTEXITCODE -ne 0 -or $version -notmatch 'PostgreSQL\) 17\.10$') {
|
2026-08-07 18:23:24 +08:00
|
|
|
throw 'PostgreSQL integration requires the frozen PostgreSQL 17.10 binaries.'
|
2026-08-07 17:52:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$existing5432 = @(
|
|
|
|
|
Get-NetTCPConnection -State Listen -LocalPort 5432 -ErrorAction SilentlyContinue |
|
|
|
|
|
Select-Object -ExpandProperty OwningProcess -Unique |
|
|
|
|
|
Sort-Object
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
$listener = [System.Net.Sockets.TcpListener]::new(
|
|
|
|
|
[System.Net.IPAddress]::Loopback,
|
|
|
|
|
0
|
|
|
|
|
)
|
|
|
|
|
$listener.Start()
|
|
|
|
|
$port = ([System.Net.IPEndPoint]$listener.LocalEndpoint).Port
|
|
|
|
|
$listener.Stop()
|
|
|
|
|
|
|
|
|
|
$systemTemp = [IO.Path]::GetFullPath([IO.Path]::GetTempPath())
|
2026-08-07 23:00:03 +08:00
|
|
|
$clusterName = 'yovision-t012-pg-' + [guid]::NewGuid().ToString('N')
|
2026-08-07 17:52:50 +08:00
|
|
|
$dataDir = Join-Path $systemTemp $clusterName
|
|
|
|
|
$logPath = Join-Path $systemTemp ($clusterName + '.log')
|
|
|
|
|
$started = $false
|
|
|
|
|
$stopped = $false
|
|
|
|
|
$hadSenseDSN = Test-Path Env:YOVISION_TEST_POSTGRES_DSN
|
|
|
|
|
$previousSenseDSN = if ($hadSenseDSN) { (Get-Item Env:YOVISION_TEST_POSTGRES_DSN).Value } else { $null }
|
|
|
|
|
$hadAdminDSN = Test-Path Env:YOVISION_TEST_POSTGRES_ADMIN_DSN
|
|
|
|
|
$previousAdminDSN = if ($hadAdminDSN) { (Get-Item Env:YOVISION_TEST_POSTGRES_ADMIN_DSN).Value } else { $null }
|
2026-08-10 23:53:11 +08:00
|
|
|
$hadBellDSN = Test-Path Env:YOVISION_TEST_BELL_POSTGRES_DSN
|
|
|
|
|
$previousBellDSN = if ($hadBellDSN) { (Get-Item Env:YOVISION_TEST_BELL_POSTGRES_DSN).Value } else { $null }
|
2026-08-07 17:52:50 +08:00
|
|
|
|
|
|
|
|
function Invoke-Checked {
|
|
|
|
|
param(
|
|
|
|
|
[Parameter(Mandatory)] [string]$FilePath,
|
|
|
|
|
[Parameter(ValueFromRemainingArguments)] [string[]]$Arguments
|
|
|
|
|
)
|
|
|
|
|
& $FilePath @Arguments
|
|
|
|
|
if ($LASTEXITCODE -ne 0) {
|
|
|
|
|
throw "Command failed with exit code $LASTEXITCODE."
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
New-Item -ItemType Directory -Path $dataDir | Out-Null
|
|
|
|
|
Invoke-Checked $initdb '-D' $dataDir '-U' 'postgres' '-A' 'trust' '--encoding=UTF8' '--no-locale' '--no-sync'
|
|
|
|
|
$serverOptions = "-h 127.0.0.1 -p $port -c listen_addresses=127.0.0.1"
|
|
|
|
|
Invoke-Checked $pgCtl '-D' $dataDir '-l' $logPath '-o' $serverOptions '-w' 'start'
|
|
|
|
|
$started = $true
|
|
|
|
|
|
|
|
|
|
$adminRootDSN = "postgres://postgres@127.0.0.1:$port/postgres?sslmode=disable"
|
2026-08-07 23:00:03 +08:00
|
|
|
$databaseName = 'yovision_t012'
|
2026-08-07 17:52:50 +08:00
|
|
|
$adminDatabaseDSN = "postgres://postgres@127.0.0.1:$port/${databaseName}?sslmode=disable"
|
2026-08-07 23:00:03 +08:00
|
|
|
$senseDSN = "postgres://yovision_t012_sense@127.0.0.1:$port/${databaseName}?sslmode=disable"
|
2026-08-07 17:52:50 +08:00
|
|
|
|
|
|
|
|
Invoke-Checked $psql '-X' '-v' 'ON_ERROR_STOP=1' '-d' $adminRootDSN '-f' (Join-Path $repoRoot 'deploy\postgres\001_roles.sql')
|
|
|
|
|
Invoke-Checked $createdb '-h' '127.0.0.1' '-p' ([string]$port) '-U' 'postgres' $databaseName
|
|
|
|
|
|
|
|
|
|
foreach ($pass in 1..2) {
|
2026-08-07 18:23:24 +08:00
|
|
|
foreach ($name in @(
|
|
|
|
|
'002_bell.sql',
|
|
|
|
|
'003_sense.sql',
|
|
|
|
|
'004_privileges.sql',
|
|
|
|
|
'005_area_policy.sql',
|
|
|
|
|
'006_device_operation_outbox.sql',
|
2026-08-07 20:54:41 +08:00
|
|
|
'007_privileges_area_audit.sql',
|
|
|
|
|
'008_control_api.sql',
|
2026-08-07 23:00:03 +08:00
|
|
|
'009_privileges_control_api.sql',
|
|
|
|
|
'010_reconcile_safety.sql',
|
2026-08-10 23:53:11 +08:00
|
|
|
'011_privileges_reconcile_safety.sql',
|
|
|
|
|
'012_bell_events.sql',
|
|
|
|
|
'013_privileges_bell_events.sql'
|
2026-08-07 18:23:24 +08:00
|
|
|
)) {
|
2026-08-07 17:52:50 +08:00
|
|
|
Invoke-Checked $psql '-X' '-v' 'ON_ERROR_STOP=1' '-d' $adminDatabaseDSN '-f' (Join-Path $repoRoot "deploy\postgres\$name")
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-08-07 23:00:03 +08:00
|
|
|
Invoke-Checked $psql '-X' '-v' 'ON_ERROR_STOP=1' '-d' $adminRootDSN '-c' 'CREATE ROLE yovision_t012_sense LOGIN IN ROLE sense_app'
|
2026-08-10 23:53:11 +08:00
|
|
|
Invoke-Checked $psql '-X' '-v' 'ON_ERROR_STOP=1' '-d' $adminRootDSN '-c' 'CREATE ROLE yovision_t015_bell LOGIN IN ROLE bell_runtime'
|
2026-08-07 17:52:50 +08:00
|
|
|
Invoke-Checked $psql '-X' '-v' 'ON_ERROR_STOP=1' '-d' $adminDatabaseDSN '-f' (Join-Path $repoRoot 'deploy\postgres\tests\assertions.sql')
|
|
|
|
|
|
|
|
|
|
$env:YOVISION_TEST_POSTGRES_DSN = $senseDSN
|
|
|
|
|
$env:YOVISION_TEST_POSTGRES_ADMIN_DSN = $adminDatabaseDSN
|
2026-08-10 23:53:11 +08:00
|
|
|
$bellDSN = "postgres://yovision_t015_bell@127.0.0.1:$port/${databaseName}?sslmode=disable"
|
|
|
|
|
$env:YOVISION_TEST_BELL_POSTGRES_DSN = $bellDSN
|
2026-08-07 17:52:50 +08:00
|
|
|
Invoke-Checked 'go' '-C' (Join-Path $repoRoot 'Sense') 'test' './internal/store' '-run' '^TestPostgres' '-count=1'
|
2026-08-10 23:53:11 +08:00
|
|
|
Invoke-Checked 'go' '-C' (Join-Path $repoRoot 'Bell') 'test' './internal/store' '-run' '^TestPostgres' '-count=1'
|
2026-08-07 17:52:50 +08:00
|
|
|
}
|
|
|
|
|
finally {
|
|
|
|
|
if ($started) {
|
|
|
|
|
& $pgCtl '-D' $dataDir '-m' 'fast' '-w' 'stop'
|
|
|
|
|
$stopped = $LASTEXITCODE -eq 0
|
|
|
|
|
}
|
|
|
|
|
if (-not $started -or $stopped) {
|
|
|
|
|
$resolvedData = [IO.Path]::GetFullPath($dataDir)
|
|
|
|
|
if (-not $resolvedData.StartsWith($systemTemp, [StringComparison]::OrdinalIgnoreCase) -or
|
2026-08-07 23:00:03 +08:00
|
|
|
[IO.Path]::GetFileName($resolvedData) -notlike 'yovision-t012-pg-*') {
|
2026-08-07 17:52:50 +08:00
|
|
|
throw "Refusing to clean unexpected temporary path."
|
|
|
|
|
}
|
|
|
|
|
if (Test-Path -LiteralPath $resolvedData) {
|
|
|
|
|
Remove-Item -LiteralPath $resolvedData -Recurse -Force
|
|
|
|
|
}
|
|
|
|
|
if (Test-Path -LiteralPath $logPath) {
|
|
|
|
|
Remove-Item -LiteralPath $logPath -Force
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
Write-Warning 'Temporary PostgreSQL did not stop; its data directory was retained for manual recovery.'
|
|
|
|
|
}
|
|
|
|
|
if ($hadSenseDSN) {
|
|
|
|
|
$env:YOVISION_TEST_POSTGRES_DSN = $previousSenseDSN
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
Remove-Item Env:YOVISION_TEST_POSTGRES_DSN -ErrorAction SilentlyContinue
|
|
|
|
|
}
|
|
|
|
|
if ($hadAdminDSN) {
|
|
|
|
|
$env:YOVISION_TEST_POSTGRES_ADMIN_DSN = $previousAdminDSN
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
Remove-Item Env:YOVISION_TEST_POSTGRES_ADMIN_DSN -ErrorAction SilentlyContinue
|
|
|
|
|
}
|
2026-08-10 23:53:11 +08:00
|
|
|
if ($hadBellDSN) {
|
|
|
|
|
$env:YOVISION_TEST_BELL_POSTGRES_DSN = $previousBellDSN
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
Remove-Item Env:YOVISION_TEST_BELL_POSTGRES_DSN -ErrorAction SilentlyContinue
|
|
|
|
|
}
|
2026-08-07 17:52:50 +08:00
|
|
|
$after5432 = @(
|
|
|
|
|
Get-NetTCPConnection -State Listen -LocalPort 5432 -ErrorAction SilentlyContinue |
|
|
|
|
|
Select-Object -ExpandProperty OwningProcess -Unique |
|
|
|
|
|
Sort-Object
|
|
|
|
|
)
|
|
|
|
|
if (($existing5432 -join ',') -ne ($after5432 -join ',')) {
|
|
|
|
|
throw 'The existing PostgreSQL listener on port 5432 changed during the isolated test.'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Write-Output "PostgreSQL $version isolated integration passed; temporary port $port was cleaned."
|