feat: 建立 Sense Windows 交付包 (#70)
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
@echo off
|
||||
powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0run-tests.ps1"
|
||||
exit /b %errorlevel%
|
||||
@@ -0,0 +1,88 @@
|
||||
Set-StrictMode -Version 3.0
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$senseRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..'))
|
||||
. (Join-Path $senseRoot 'scripts\runtime\sense-common.ps1')
|
||||
$script:passed = 0
|
||||
|
||||
function Assert-True([bool]$Condition, [string]$Message) {
|
||||
if (-not $Condition) { throw "ASSERT FAILED: $Message" }
|
||||
$script:passed++
|
||||
}
|
||||
function Assert-Equal($Expected, $Actual, [string]$Message) {
|
||||
if ($Expected -cne $Actual) { throw "ASSERT FAILED: $Message; expected [$Expected], got [$Actual]" }
|
||||
$script:passed++
|
||||
}
|
||||
function Assert-Throws([scriptblock]$Action, [string]$Pattern, [string]$Message) {
|
||||
try { & $Action; throw "ASSERT FAILED: $Message; no error was raised" } catch {
|
||||
if ($_.Exception.Message -notmatch $Pattern) { throw "ASSERT FAILED: $Message; unexpected error: $($_.Exception.Message)" }
|
||||
}
|
||||
$script:passed++
|
||||
}
|
||||
|
||||
$temporary = Join-Path ([IO.Path]::GetTempPath()) ("sense-package-tests-" + [guid]::NewGuid().ToString('N'))
|
||||
$listener = $null
|
||||
$oldValues = @{}
|
||||
foreach ($name in $script:SenseAllowedEnvironment) {
|
||||
$oldValues[$name] = [Environment]::GetEnvironmentVariable($name, 'Process')
|
||||
[Environment]::SetEnvironmentVariable($name, $null, 'Process')
|
||||
}
|
||||
try {
|
||||
New-Item -ItemType Directory -Path (Join-Path $temporary 'config'), (Join-Path $temporary 'web'), (Join-Path $temporary 'bin') | Out-Null
|
||||
[IO.File]::WriteAllText((Join-Path $temporary 'web\index.html'), 'sense', (New-Object Text.UTF8Encoding($false)))
|
||||
[IO.File]::WriteAllText((Join-Path $temporary 'bin\mediamtx.exe'), 'fixture', (New-Object Text.UTF8Encoding($false)))
|
||||
[IO.File]::WriteAllText((Join-Path $temporary 'config\mediamtx.yml'), 'api: true', (New-Object Text.UTF8Encoding($false)))
|
||||
|
||||
$listener = New-Object Net.Sockets.TcpListener([Net.IPAddress]::Loopback, 0)
|
||||
$listener.Start()
|
||||
$dbPort = ([Net.IPEndPoint]$listener.LocalEndpoint).Port
|
||||
$marker = Join-Path $temporary 'must-not-exist.txt'
|
||||
$envText = @(
|
||||
'SENSE_HOST=127.0.0.1',
|
||||
'SENSE_PORT=18070',
|
||||
"SENSE_DATABASE_URL=host=127.0.0.1 port=$dbPort user=sense password=p#&;=x dbname=sense sslmode=disable",
|
||||
"SENSE_JWT_SECRET=`$(Set-Content -LiteralPath '$marker' hacked)-literal-secret-1234567890",
|
||||
'SENSE_MEDIAMTX_MODE=managed',
|
||||
'SENSE_MEDIAMTX_BINARY=bin\mediamtx.exe',
|
||||
'SENSE_MEDIAMTX_CONFIG=config\mediamtx.yml',
|
||||
'SENSE_MEDIAMTX_API=http://127.0.0.1:9997',
|
||||
'SENSE_WEB_ROOT=web'
|
||||
) -join "`n"
|
||||
[IO.File]::WriteAllText((Join-Path $temporary 'config\sense.env'), $envText, (New-Object Text.UTF8Encoding($false)))
|
||||
[IO.File]::WriteAllText((Join-Path $temporary 'config\sense.demo.env'), $envText.Replace('SENSE_DATABASE_URL=', 'SENSE_DEMO_DATABASE_URL=').Replace('dbname=sense ', 'dbname=sense_demo '), (New-Object Text.UTF8Encoding($false)))
|
||||
|
||||
[Environment]::SetEnvironmentVariable('SENSE_PORT', '18071', 'Process')
|
||||
$state = Initialize-SenseRuntime -PackageRoot $temporary -Mode production
|
||||
Assert-Equal 18071 $state.Port 'non-empty process environment must override sense.env'
|
||||
Assert-True (-not (Test-Path -LiteralPath $marker)) 'sense.env content must never execute'
|
||||
$generatedSettings = Get-Content -LiteralPath $state.SettingsPath -Raw
|
||||
Assert-True ((Get-SenseEnvironmentValue -Name 'SENSE_DATABASE_URL').Contains('p#&;=x')) 'special characters must survive env parsing'
|
||||
Assert-True ($generatedSettings.Contains('password=p#\u0026;=x')) 'special characters must be safely JSON-escaped in YAML'
|
||||
Assert-True (-not ((Get-SenseDatabaseInfo (Get-SenseEnvironmentValue -Name 'SENSE_DATABASE_URL')).Sanitized.Contains('password='))) 'PostgreSQL tool arguments must not contain password'
|
||||
|
||||
[Environment]::SetEnvironmentVariable('SENSE_PORT', $null, 'Process')
|
||||
foreach ($name in @('SENSE_DATABASE_URL','SENSE_JWT_SECRET','SENSE_MEDIAMTX_MODE','SENSE_MEDIAMTX_BINARY','SENSE_MEDIAMTX_CONFIG','SENSE_MEDIAMTX_API','SENSE_WEB_ROOT')) { [Environment]::SetEnvironmentVariable($name, $null, 'Process') }
|
||||
$demo = Initialize-SenseRuntime -PackageRoot $temporary -Mode demo
|
||||
Assert-Equal 'sense_demo' $demo.Database.Database 'demo must use its dedicated database variable'
|
||||
|
||||
$bad = Join-Path $temporary 'config\bad.env'
|
||||
[IO.File]::WriteAllText($bad, 'SENSE_UNKNOWN=value', (New-Object Text.UTF8Encoding($false)))
|
||||
Assert-Throws { Import-SenseEnvironment -Path $bad } 'Unsupported Sense configuration key' 'unknown keys must be rejected'
|
||||
[IO.File]::WriteAllText((Join-Path $temporary 'config\sense.demo.env'), $envText.Replace('SENSE_DATABASE_URL=', 'SENSE_DEMO_DATABASE_URL='), (New-Object Text.UTF8Encoding($false)))
|
||||
foreach ($name in $script:SenseAllowedEnvironment) { [Environment]::SetEnvironmentVariable($name, $null, 'Process') }
|
||||
Assert-Throws { Initialize-SenseRuntime -PackageRoot $temporary -Mode demo } 'database name containing demo or test' 'demo must reject production database names'
|
||||
|
||||
foreach ($file in Get-ChildItem -LiteralPath (Join-Path $senseRoot 'scripts') -Recurse -Filter '*.ps1') {
|
||||
[void][scriptblock]::Create((Get-Content -LiteralPath $file.FullName -Raw))
|
||||
$script:passed++
|
||||
}
|
||||
Write-Host "Sense package tests passed: $script:passed assertions."
|
||||
} finally {
|
||||
if ($listener) { $listener.Stop() }
|
||||
foreach ($name in $script:SenseAllowedEnvironment) { [Environment]::SetEnvironmentVariable($name, $oldValues[$name], 'Process') }
|
||||
if (Test-Path -LiteralPath $temporary) {
|
||||
$resolved = [IO.Path]::GetFullPath($temporary)
|
||||
if (-not $resolved.StartsWith([IO.Path]::GetTempPath(), [StringComparison]::OrdinalIgnoreCase)) { throw "Unsafe temporary test path: $resolved" }
|
||||
Remove-Item -LiteralPath $resolved -Recurse -Force
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
param([Parameter(Mandatory = $true)][string]$PackageRoot)
|
||||
Set-StrictMode -Version 3.0
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$root = [IO.Path]::GetFullPath($PackageRoot)
|
||||
$start = Join-Path $root 'start-sense.bat'
|
||||
$stop = Join-Path $root 'stop-sense.bat'
|
||||
$port = [int]$env:SENSE_PORT
|
||||
$launcher = Start-Process -FilePath 'cmd.exe' -ArgumentList @('/d', '/c', "`"$start`" -SkipMigration") -WorkingDirectory (Split-Path $root -Parent) -WindowStyle Hidden -PassThru
|
||||
try {
|
||||
$ready = $false
|
||||
for ($attempt = 0; $attempt -lt 60; $attempt++) {
|
||||
$client = New-Object Net.Sockets.TcpClient
|
||||
try {
|
||||
$task = $client.ConnectAsync('127.0.0.1', $port)
|
||||
if ($task.Wait(500) -and $client.Connected) { $ready = $true; break }
|
||||
} catch {} finally { $client.Dispose() }
|
||||
Start-Sleep -Milliseconds 500
|
||||
}
|
||||
if (-not $ready) { throw 'start-sense.bat did not open the configured HTTP port.' }
|
||||
& $stop
|
||||
if ($LASTEXITCODE -ne 0) { throw 'stop-sense.bat failed.' }
|
||||
Start-Sleep -Seconds 1
|
||||
$probe = New-Object Net.Sockets.TcpClient
|
||||
try {
|
||||
$task = $probe.ConnectAsync('127.0.0.1', $port)
|
||||
if ($task.Wait(500) -and $probe.Connected) { throw 'Sense port is still open after stop-sense.bat.' }
|
||||
} catch [Net.Sockets.SocketException] {} finally { $probe.Dispose() }
|
||||
Write-Host 'Sense start/stop wrapper smoke passed from an external working directory.'
|
||||
} finally {
|
||||
if (-not $launcher.HasExited) { & taskkill.exe /PID $launcher.Id /T /F | Out-Null }
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
param([Parameter(Mandatory = $true)][string]$PackageRoot)
|
||||
Set-StrictMode -Version 3.0
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$root = [IO.Path]::GetFullPath($PackageRoot)
|
||||
$port = [int]$env:SENSE_PORT
|
||||
$mediaUri = [Uri]$env:SENSE_MEDIAMTX_API
|
||||
$stdout = Join-Path ([IO.Path]::GetTempPath()) ("sense-smoke-$PID.out")
|
||||
$stderr = Join-Path ([IO.Path]::GetTempPath()) ("sense-smoke-$PID.err")
|
||||
$server = $null
|
||||
$succeeded = $false
|
||||
try {
|
||||
& (Join-Path $root 'migrate-sense.bat')
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Package migration entry failed.' }
|
||||
if (-not [IO.Path]::IsPathRooted($env:SENSE_MEDIAMTX_BINARY)) { $env:SENSE_MEDIAMTX_BINARY = [IO.Path]::GetFullPath((Join-Path $root $env:SENSE_MEDIAMTX_BINARY)) }
|
||||
if (-not [IO.Path]::IsPathRooted($env:SENSE_MEDIAMTX_CONFIG)) { $env:SENSE_MEDIAMTX_CONFIG = [IO.Path]::GetFullPath((Join-Path $root $env:SENSE_MEDIAMTX_CONFIG)) }
|
||||
if (-not [IO.Path]::IsPathRooted($env:SENSE_WEB_ROOT)) { $env:SENSE_WEB_ROOT = [IO.Path]::GetFullPath((Join-Path $root $env:SENSE_WEB_ROOT)) }
|
||||
$settings = Join-Path $root 'data\runtime\settings.yml'
|
||||
$server = Start-Process -FilePath (Join-Path $root 'sense.exe') -ArgumentList 'server', '-c', $settings -WorkingDirectory $root -RedirectStandardOutput $stdout -RedirectStandardError $stderr -WindowStyle Hidden -PassThru
|
||||
$response = $null
|
||||
for ($attempt = 0; $attempt -lt 80; $attempt++) {
|
||||
$server.Refresh()
|
||||
if ($server.HasExited) {
|
||||
Get-Content -LiteralPath $stdout -Tail 120 -ErrorAction SilentlyContinue | Out-Host
|
||||
Get-Content -LiteralPath $stderr -Tail 120 -ErrorAction SilentlyContinue | Out-Host
|
||||
throw "Sense exited before HTTP readiness with code $($server.ExitCode)."
|
||||
}
|
||||
try {
|
||||
$response = Invoke-WebRequest -UseBasicParsing -Uri "http://127.0.0.1:$port/" -TimeoutSec 1
|
||||
if ($response.StatusCode -eq 200 -and $response.Content.Contains('<title>')) { break }
|
||||
} catch {}
|
||||
Start-Sleep -Milliseconds 500
|
||||
}
|
||||
if (-not $response -or $response.StatusCode -ne 200 -or -not $response.Content.Contains('<title>')) {
|
||||
throw 'Sense package did not serve the GoAdmin UI before the smoke timeout.'
|
||||
}
|
||||
$media = $null
|
||||
for ($attempt = 0; $attempt -lt 20; $attempt++) {
|
||||
try {
|
||||
$media = Invoke-RestMethod -Uri "http://$($mediaUri.Host):$($mediaUri.Port)/v3/config/global/get" -TimeoutSec 1
|
||||
if ($null -ne $media) { break }
|
||||
} catch {}
|
||||
Start-Sleep -Milliseconds 500
|
||||
}
|
||||
if ($null -eq $media) { throw 'MediaMTX Control API returned no data.' }
|
||||
Write-Host "Sense package smoke passed: web=200, SPA=true, MediaMTX=true, port=$port."
|
||||
$succeeded = $true
|
||||
} finally {
|
||||
if ($server -and -not $server.HasExited) {
|
||||
& taskkill.exe /PID $server.Id /T /F | Out-Null
|
||||
}
|
||||
Remove-Item -LiteralPath $stdout, $stderr -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
Reference in New Issue
Block a user