fix: 修复 Sense 独立验收脚本隔离与诊断 (#145)

This commit is contained in:
QiuSW
2026-08-29 20:00:04 +08:00
parent 6702b8a5b9
commit 1c6b30fac0
3 changed files with 163 additions and 16 deletions
+8
View File
@@ -15,6 +15,14 @@
E2E 入口从 PowerShell 7 调用时会自动转入 Windows PowerShell 5.1 执行本地 HTTP 回归;源码打包仍显式使用冻结要求的 PowerShell 7。这样与 Windows 交付脚本的宿主一致,也避开当前机器 PowerShell 7 HTTP 客户端对本地 Go/MediaMTX 响应的兼容问题。 E2E 入口从 PowerShell 7 调用时会自动转入 Windows PowerShell 5.1 执行本地 HTTP 回归;源码打包仍显式使用冻结要求的 PowerShell 7。这样与 Windows 交付脚本的宿主一致,也避开当前机器 PowerShell 7 HTTP 客户端对本地 Go/MediaMTX 响应的兼容问题。
默认入口只复制 Git 已跟踪的 `Sense/` 源码到系统临时目录,因此正常开发工作区中已有的 `node_modules`、`dist`、本地配置、日志和其他未跟踪文件不会进入验收副本。`-PreparedPackageRoot` 也会先把指定包复制到本次临时目录,运行时配置、浏览器脚本、截图和日志不会写回原包或源码树。
Sense 进程提前退出或 HTTP 就绪超时时,脚本返回非零并输出阶段、退出状态、临时日志位置和经过过滤、截断的日志摘要;数据库连接、密码、token、Cookie、JWT 和 credential key 不得出现在诊断中。默认无论成功或失败都会清理所属进程和临时目录;`-KeepTemporary` 仅用于排错,仍会停止进程,但保留目录可能包含随机运行时秘密,必须限制访问并在排错后安全删除。
为避免 Windows 首次扫描临时复制的 MediaMTX 二进制占用产品固定的就绪窗口,E2E 会先在同一动态端口和临时配置上启动一次包内 MediaMTX,确认 Control API 可用并完全停止,再由 Sense 以 managed 模式启动并完成生命周期验收。预检失败会单独报告 `MediaMTX preflight` 阶段,不会被误报为 Sense HTTP 超时。
HTTP、RTSP、HLS、Control API 和 ONVIF 动态端口使用 TCP 绑定探测;WebRTC 本地 UDP 端口必须使用 UDP socket 实际绑定探测,不得用 TCP 空闲结果代替,避免落入 Windows 的 UDP 排除或占用范围。
## 回归矩阵 ## 回归矩阵
| 范围 | 自动化证据 | 判定 | | 范围 | 自动化证据 | 判定 |
@@ -49,4 +49,18 @@ foreach ($file in $fixtureFiles) {
if ($file.Extension -eq '.json') { [void]($content | ConvertFrom-Json); $passed++ } if ($file.Extension -eq '.json') { [void]($content | ConvertFrom-Json); $passed++ }
} }
$e2eScript = Read-Utf8 (Join-Path $senseRoot 'tests\e2e\run-isolated-e2e.ps1')
Assert-True ($e2eScript.Contains('Copy-TrackedSenseSource $repositoryRoot $senseCopy')) 'Sense E2E no longer copies only tracked source'
Assert-True (-not $e2eScript.Contains('Copy-Item -LiteralPath $sourceSense -Destination $senseCopy -Recurse')) 'Sense E2E regressed to recursive source-tree copying'
Assert-True ($e2eScript.Contains("`$browserScript = Join-Path `$PSScriptRoot 'browser-smoke.cjs'")) 'browser smoke script no longer runs from its tracked location'
Assert-True ($e2eScript.Contains("`$packageRoot = Join-Path `$temporary 'prepared-package'")) 'prepared package no longer runs from an isolated temporary copy'
Assert-True ($e2eScript.Contains("-Process `$process -Stage 'Sense HTTP'")) 'Sense HTTP readiness no longer observes the package process'
Assert-True ($e2eScript.Contains('Get-SafeLogSummary')) 'Sense readiness diagnostics no longer use log redaction'
Assert-True ($e2eScript.Contains("-Stage 'MediaMTX preflight'")) 'Sense E2E no longer preflights the copied MediaMTX package and config'
Assert-True ($e2eScript.Contains('function Get-FreeUdpPort')) 'Sense E2E no longer probes WebRTC UDP ports with the UDP protocol'
Assert-True ($e2eScript.Contains('do { $webrtcUDPort = Get-FreeUdpPort }')) 'Sense E2E WebRTC UDP port regressed to TCP-only discovery'
& powershell.exe -NoProfile -File (Join-Path $senseRoot 'tests\e2e\run-isolated-e2e.ps1') -HarnessSelfTest
if ($LASTEXITCODE -ne 0) { throw 'Sense E2E harness self-test failed' }
Write-Host "Sense compatibility regression passed: $passed assertions." Write-Host "Sense compatibility regression passed: $passed assertions."
+141 -16
View File
@@ -3,11 +3,13 @@ param(
[string]$MediaMTX = 'C:\Users\ila20\Desktop\mediamtx\mediamtx.exe', [string]$MediaMTX = 'C:\Users\ila20\Desktop\mediamtx\mediamtx.exe',
[string]$Browser = 'C:\Program Files\Google\Chrome\Application\chrome.exe', [string]$Browser = 'C:\Program Files\Google\Chrome\Application\chrome.exe',
[string]$PreparedPackageRoot = '', [string]$PreparedPackageRoot = '',
[switch]$HarnessSelfTest,
[switch]$KeepTemporary [switch]$KeepTemporary
) )
if ($PSVersionTable.PSEdition -eq 'Core') { if ($PSVersionTable.PSEdition -eq 'Core') {
$legacyArguments = @('-NoProfile', '-File', $PSCommandPath, '-PostgresBin', $PostgresBin, '-MediaMTX', $MediaMTX, '-Browser', $Browser) $legacyArguments = @('-NoProfile', '-File', $PSCommandPath, '-PostgresBin', $PostgresBin, '-MediaMTX', $MediaMTX, '-Browser', $Browser)
if (-not [string]::IsNullOrWhiteSpace($PreparedPackageRoot)) { $legacyArguments += @('-PreparedPackageRoot', $PreparedPackageRoot) } if (-not [string]::IsNullOrWhiteSpace($PreparedPackageRoot)) { $legacyArguments += @('-PreparedPackageRoot', $PreparedPackageRoot) }
if ($HarnessSelfTest) { $legacyArguments += '-HarnessSelfTest' }
if ($KeepTemporary) { $legacyArguments += '-KeepTemporary' } if ($KeepTemporary) { $legacyArguments += '-KeepTemporary' }
& powershell.exe @legacyArguments & powershell.exe @legacyArguments
exit $LASTEXITCODE exit $LASTEXITCODE
@@ -34,8 +36,10 @@ $fixtureStatus = Join-Path $temporary 'fixture-status.json'
$server = $null $server = $null
$fixture = $null $fixture = $null
$publisher = $null $publisher = $null
$preflightMedia = $null
$pgStarted = $false $pgStarted = $false
$savedEnvironment = @{} $savedEnvironment = @{}
$sensitiveValues = @()
function Get-FreePort { function Get-FreePort {
$listener = [Net.Sockets.TcpListener]::new([Net.IPAddress]::Loopback, 0) $listener = [Net.Sockets.TcpListener]::new([Net.IPAddress]::Loopback, 0)
@@ -61,15 +65,68 @@ function Set-TestEnvironment([string]$Name, [string]$Value) {
} }
[Environment]::SetEnvironmentVariable($Name, $Value, 'Process') [Environment]::SetEnvironmentVariable($Name, $Value, 'Process')
} }
function Wait-Http([string]$Uri, [int]$Attempts = 120) { function Get-FreeUdpPort {
$client = [Net.Sockets.UdpClient]::new([Net.IPEndPoint]::new([Net.IPAddress]::Loopback, 0))
try { return ([Net.IPEndPoint]$client.Client.LocalEndPoint).Port } finally { $client.Dispose() }
}
function Copy-TrackedSenseSource([string]$RepositoryRoot, [string]$Destination) {
$tracked = @(& git -C $RepositoryRoot ls-files -- 'Sense')
if ($LASTEXITCODE -ne 0 -or $tracked.Count -eq 0) { throw 'Could not enumerate tracked Sense source files' }
$sensePrefix = 'Sense\'
foreach ($relative in $tracked) {
$normalized = ([string]$relative).Replace('/', '\')
if (-not $normalized.StartsWith($sensePrefix, [StringComparison]::Ordinal)) {
throw "Unexpected tracked path outside Sense: $relative"
}
$source = Join-Path $RepositoryRoot $normalized
if (-not (Test-Path -LiteralPath $source -PathType Leaf)) { throw "Tracked Sense source is missing: $relative" }
$target = Join-Path $Destination $normalized.Substring($sensePrefix.Length)
$parent = Split-Path -Parent $target
if (-not (Test-Path -LiteralPath $parent)) { [void](New-Item -ItemType Directory -Path $parent -Force) }
Copy-Item -LiteralPath $source -Destination $target
}
}
function Get-SafeLogSummary([string[]]$Paths, [int]$MaximumCharacters = 2000) {
$parts = New-Object System.Collections.Generic.List[string]
foreach ($path in @($Paths)) {
if ([string]::IsNullOrWhiteSpace($path) -or -not (Test-Path -LiteralPath $path -PathType Leaf)) { continue }
$text = [string](@(Get-Content -LiteralPath $path -Tail 20 -ErrorAction SilentlyContinue) -join ' | ')
foreach ($secret in @($script:sensitiveValues)) {
if (-not [string]::IsNullOrWhiteSpace($secret) -and $secret.Length -ge 4) { $text = $text.Replace($secret, '<redacted>') }
}
$text = $text -replace '(?i)((?:password|token|secret|credential(?:_key)?|database(?:_url)?|cookie|authorization)["'']?\s*[:=]\s*["'']?)[^\s,;"'']+', '$1<redacted>'
$text = $text -replace '(?i)(postgres(?:ql)?://)[^\s]+', '$1<redacted>'
if ($text.Length -gt $MaximumCharacters) { $text = $text.Substring($text.Length - $MaximumCharacters) }
if (-not [string]::IsNullOrWhiteSpace($text)) { $parts.Add("$([IO.Path]::GetFileName($path)): $text") }
}
if ($parts.Count -eq 0) { return '<no log output>' }
return ($parts -join ' || ')
}
function Wait-Http {
param(
[string]$Uri,
[int]$Attempts = 120,
$Process = $null,
[string]$Stage = 'HTTP endpoint',
[string[]]$LogPaths = @()
)
for ($attempt = 0; $attempt -lt $Attempts; $attempt++) { for ($attempt = 0; $attempt -lt $Attempts; $attempt++) {
if ($null -ne $Process -and $Process.HasExited) {
try { $Process.WaitForExit(); $Process.Refresh() } catch {}
$exitCode = try { [string]$Process.ExitCode } catch { 'unknown' }
if ([string]::IsNullOrWhiteSpace($exitCode)) { $exitCode = 'unknown' }
$summary = Get-SafeLogSummary $LogPaths
throw "$Stage process exited before readiness: exit_code=$exitCode; log_files=$($LogPaths -join ','); summary=$summary"
}
try { try {
$response = Invoke-WebRequest -UseBasicParsing -Uri $Uri -TimeoutSec 1 $response = Invoke-WebRequest -UseBasicParsing -Uri $Uri -TimeoutSec 1
if ($response.StatusCode -eq 200) { return } if ($response.StatusCode -eq 200) { return }
} catch {} } catch {}
Start-Sleep -Milliseconds 500 Start-Sleep -Milliseconds 500
} }
throw "HTTP endpoint did not become ready: $Uri" $processState = if ($null -eq $Process) { 'not-observed' } elseif ($Process.HasExited) { "exited:$($Process.ExitCode)" } else { 'running' }
$summary = Get-SafeLogSummary $LogPaths
throw "$Stage did not become ready: uri=$Uri; process_state=$processState; log_files=$($LogPaths -join ','); summary=$summary"
} }
function Wait-Tcp([int]$Port, [bool]$Open, [int]$Attempts = 120) { function Wait-Tcp([int]$Port, [bool]$Open, [int]$Attempts = 120) {
for ($attempt = 0; $attempt -lt $Attempts; $attempt++) { for ($attempt = 0; $attempt -lt $Attempts; $attempt++) {
@@ -104,13 +161,62 @@ function Invoke-SenseJson {
function Start-SensePackage([string]$PackageRoot) { function Start-SensePackage([string]$PackageRoot) {
$launcher = Join-Path $PackageRoot 'start-sense.bat' $launcher = Join-Path $PackageRoot 'start-sense.bat'
$process = Start-Process -FilePath 'cmd.exe' -ArgumentList '/d', '/c', "`"$launcher`"" -WorkingDirectory $PackageRoot -RedirectStandardOutput $runtimeLog -RedirectStandardError $runtimeError -WindowStyle Hidden -PassThru $process = Start-Process -FilePath 'cmd.exe' -ArgumentList '/d', '/c', "`"$launcher`"" -WorkingDirectory $PackageRoot -RedirectStandardOutput $runtimeLog -RedirectStandardError $runtimeError -WindowStyle Hidden -PassThru
Wait-Http "$script:baseUrl/" Wait-Http -Uri "$script:baseUrl/" -Process $process -Stage 'Sense HTTP' -LogPaths @($runtimeLog, $runtimeError)
return $process return $process
} }
function Stop-ProcessTree($Process) { function Stop-ProcessTree($Process) {
if ($Process -and -not $Process.HasExited) { & taskkill.exe /PID $Process.Id /T /F 2>$null | Out-Null } if ($Process -and -not $Process.HasExited) { & taskkill.exe /PID $Process.Id /T /F 2>$null | Out-Null }
} }
function Invoke-HarnessSelfTest {
$root = Join-Path ([IO.Path]::GetTempPath()) ('sense-e2e-selftest-' + [guid]::NewGuid().ToString('N'))
$originalSensitiveValues = @($script:sensitiveValues)
try {
$fixtureRepository = Join-Path $root 'repository'
$trackedSource = Join-Path $fixtureRepository 'Sense\tracked.txt'
$ignoredSource = Join-Path $fixtureRepository 'Sense\ui\node_modules\ignored.txt'
[void](New-Item -ItemType Directory -Path (Split-Path -Parent $trackedSource) -Force)
[void](New-Item -ItemType Directory -Path (Split-Path -Parent $ignoredSource) -Force)
[IO.File]::WriteAllText($trackedSource, 'tracked', (New-Object Text.UTF8Encoding($false)))
[IO.File]::WriteAllText($ignoredSource, 'ignored', (New-Object Text.UTF8Encoding($false)))
& git -C $fixtureRepository init --quiet
& git -C $fixtureRepository add -- 'Sense/tracked.txt'
if ($LASTEXITCODE -ne 0) { throw 'Harness self-test could not prepare tracked source' }
$copy = Join-Path $root 'copy'
Copy-TrackedSenseSource $fixtureRepository $copy
if (-not (Test-Path -LiteralPath (Join-Path $copy 'tracked.txt'))) { throw 'Harness self-test did not copy tracked source' }
if (Test-Path -LiteralPath (Join-Path $copy 'ui\node_modules\ignored.txt')) { throw 'Harness self-test copied ignored node_modules content' }
$udpPort = Get-FreeUdpPort
$udpProbe = [Net.Sockets.UdpClient]::new()
try { $udpProbe.Client.Bind([Net.IPEndPoint]::new([Net.IPAddress]::Loopback, $udpPort)) } finally { $udpProbe.Dispose() }
$diagnosticLog = Join-Path $root 'sense.err.log'
[IO.File]::WriteAllText($diagnosticLog, 'SENSE_JWT_SECRET=unit-secret-value', (New-Object Text.UTF8Encoding($false)))
$script:sensitiveValues = @('unit-secret-value')
$exited = [pscustomobject]@{ HasExited = $true; ExitCode = 23 }
$earlyFailure = ''
try { Wait-Http -Uri 'http://127.0.0.1:1/' -Attempts 3 -Process $exited -Stage 'Self-test early exit' -LogPaths @($diagnosticLog) } catch { $earlyFailure = $_.Exception.Message }
if ($earlyFailure -notmatch 'exit_code=23' -or $earlyFailure.Contains('unit-secret-value') -or $earlyFailure -notmatch '<redacted>') {
throw "Harness self-test early-exit diagnostic was unsafe or incomplete: $earlyFailure"
}
$timeoutFailure = ''
try { Wait-Http -Uri 'http://127.0.0.1:1/' -Attempts 1 -Stage 'Self-test timeout' -LogPaths @($diagnosticLog) } catch { $timeoutFailure = $_.Exception.Message }
if ($timeoutFailure -notmatch 'process_state=not-observed' -or $timeoutFailure.Contains('unit-secret-value') -or $timeoutFailure -notmatch '<redacted>') {
throw "Harness self-test timeout diagnostic was unsafe or incomplete: $timeoutFailure"
}
Write-Host 'Sense E2E harness self-test passed: tracked copy, UDP bind, early exit, timeout and redaction.'
} finally {
$script:sensitiveValues = $originalSensitiveValues
if (Test-Path -LiteralPath $root) { Remove-Item -LiteralPath $root -Recurse -Force }
}
}
if ($HarnessSelfTest) {
Invoke-HarnessSelfTest
exit 0
}
try { try {
foreach ($required in @( foreach ($required in @(
(Join-Path $PostgresBin 'initdb.exe'), (Join-Path $PostgresBin 'pg_ctl.exe'), (Join-Path $PostgresBin 'initdb.exe'), (Join-Path $PostgresBin 'pg_ctl.exe'),
@@ -121,25 +227,32 @@ try {
} }
$ffmpeg = (Get-Command ffmpeg.exe -ErrorAction Stop).Source $ffmpeg = (Get-Command ffmpeg.exe -ErrorAction Stop).Source
if ([string]::IsNullOrWhiteSpace($PreparedPackageRoot)) { if ([string]::IsNullOrWhiteSpace($PreparedPackageRoot)) {
New-Item -ItemType Directory -Path $repoCopy | Out-Null New-Item -ItemType Directory -Path $senseCopy -Force | Out-Null
Copy-Item -LiteralPath $sourceSense -Destination $senseCopy -Recurse Copy-TrackedSenseSource $repositoryRoot $senseCopy
& git -C $repoCopy init --quiet & git -C $repoCopy init --quiet
& git -C $repoCopy config user.name 'Sense E2E' & git -C $repoCopy config user.name 'Sense E2E'
& git -C $repoCopy config user.email 'sense-e2e@invalid.local' & git -C $repoCopy config user.email 'sense-e2e@invalid.local'
& git -C $repoCopy commit --allow-empty --quiet -m 'temporary acceptance source' & git -C $repoCopy config core.autocrlf false
& git -C $repoCopy add -- Sense
if ($LASTEXITCODE -ne 0) { throw 'temporary acceptance source staging failed' }
& git -C $repoCopy commit --quiet -m 'temporary acceptance source'
if ($LASTEXITCODE -ne 0) { throw 'temporary acceptance source commit failed' }
Write-Host 'Building Sense Windows package in an isolated temporary copy...' Write-Host 'Building Sense Windows package in an isolated temporary copy...'
& pwsh.exe -NoProfile -File (Join-Path $senseCopy 'scripts\build\build-windows.ps1') -MediaMTXPath $MediaMTX & pwsh.exe -NoProfile -File (Join-Path $senseCopy 'scripts\build\build-windows.ps1') -MediaMTXPath $MediaMTX
if ($LASTEXITCODE -ne 0) { throw 'isolated Windows package build failed' } if ($LASTEXITCODE -ne 0) { throw 'isolated Windows package build failed' }
$packageRoot = Join-Path $senseCopy 'dist\sense-windows-amd64' $packageRoot = Join-Path $senseCopy 'dist\sense-windows-amd64'
} else { } else {
$packageRoot = [IO.Path]::GetFullPath($PreparedPackageRoot) $preparedInput = [IO.Path]::GetFullPath($PreparedPackageRoot)
if (-not (Test-Path -LiteralPath (Join-Path $packageRoot 'sense.exe'))) { throw 'prepared Sense package is invalid' } if (-not (Test-Path -LiteralPath (Join-Path $preparedInput 'sense.exe'))) { throw 'prepared Sense package is invalid' }
$senseCopy = [IO.Path]::GetFullPath((Join-Path $packageRoot '..\..')) $packageRoot = Join-Path $temporary 'prepared-package'
Write-Host "Using prepared isolated package: $packageRoot" Copy-Item -LiteralPath $preparedInput -Destination $packageRoot -Recurse
Write-Host "Using temporary copy of prepared package: $packageRoot"
} }
$pgPort, $sensePort, $rtspPort, $hlsPort, $webrtcPort, $webrtcUDPort, $mediaAPIPort, $onvifPort = Get-UniqueFreePorts 8 $tcpPorts = @(Get-UniqueFreePorts 7)
$pgPort, $sensePort, $rtspPort, $hlsPort, $webrtcPort, $mediaAPIPort, $onvifPort = $tcpPorts
do { $webrtcUDPort = Get-FreeUdpPort } while ($tcpPorts -contains $webrtcUDPort)
$script:baseUrl = "http://127.0.0.1:$sensePort" $script:baseUrl = "http://127.0.0.1:$sensePort"
Write-Host "Initializing isolated PostgreSQL on port $pgPort..." Write-Host "Initializing isolated PostgreSQL on port $pgPort..."
& (Join-Path $PostgresBin 'initdb.exe') -D $pgData -U sense_e2e -A trust --encoding=UTF8 --no-locale | Out-Null & (Join-Path $PostgresBin 'initdb.exe') -D $pgData -U sense_e2e -A trust --encoding=UTF8 --no-locale | Out-Null
@@ -162,6 +275,16 @@ try {
'rtmp: false', 'srt: false', 'moq: false', 'metrics: false', 'paths:', ' fixture:' 'rtmp: false', 'srt: false', 'moq: false', 'metrics: false', 'paths:', ' fixture:'
) -join "`n" ) -join "`n"
[IO.File]::WriteAllText((Join-Path $packageRoot 'config\mediamtx.yml'), $mediaConfig, (New-Object Text.UTF8Encoding($false))) [IO.File]::WriteAllText((Join-Path $packageRoot 'config\mediamtx.yml'), $mediaConfig, (New-Object Text.UTF8Encoding($false)))
$preflightOut = Join-Path $temporary 'mediamtx-preflight.out.log'
$preflightError = Join-Path $temporary 'mediamtx-preflight.err.log'
$preflightBinary = Join-Path $packageRoot 'bin\mediamtx.exe'
$preflightConfig = Join-Path $packageRoot 'config\mediamtx.yml'
$preflightMedia = Start-Process -FilePath $preflightBinary -ArgumentList $preflightConfig -WorkingDirectory (Split-Path -Parent $preflightConfig) -RedirectStandardOutput $preflightOut -RedirectStandardError $preflightError -WindowStyle Hidden -PassThru
Wait-Http -Uri "http://127.0.0.1:$mediaAPIPort/v3/config/global/get" -Process $preflightMedia -Stage 'MediaMTX preflight' -LogPaths @($preflightOut, $preflightError) -Attempts 60
Stop-ProcessTree $preflightMedia
Wait-Tcp -Port $mediaAPIPort -Open $false -Attempts 40
$preflightMedia = $null
Write-Host 'MediaMTX package/config preflight passed before managed Sense startup.'
$jwt = New-RandomText 48 $jwt = New-RandomText 48
$bootstrap = New-RandomText 48 $bootstrap = New-RandomText 48
@@ -173,6 +296,7 @@ try {
$cameraUser = 'fixture_' + (New-RandomText 8) $cameraUser = 'fixture_' + (New-RandomText 8)
$cameraPassword = New-RandomText 24 $cameraPassword = New-RandomText 24
$database = "host=127.0.0.1 port=$pgPort user=sense_e2e dbname=sense_e2e sslmode=disable" $database = "host=127.0.0.1 port=$pgPort user=sense_e2e dbname=sense_e2e sslmode=disable"
$script:sensitiveValues = @($jwt, $bootstrap, $adminPassword, $credentialKey, $cameraUser, $cameraPassword, $database)
$environment = @{ $environment = @{
SENSE_HOST = '127.0.0.1'; SENSE_PORT = "$sensePort"; SENSE_DATABASE_URL = $database; SENSE_HOST = '127.0.0.1'; SENSE_PORT = "$sensePort"; SENSE_DATABASE_URL = $database;
SENSE_JWT_SECRET = $jwt; SENSE_BOOTSTRAP_TOKEN = $bootstrap; SENSE_JWT_SECRET = $jwt; SENSE_BOOTSTRAP_TOKEN = $bootstrap;
@@ -198,7 +322,7 @@ try {
if (-not (Test-Path $fixtureStatus)) { throw 'ONVIF fixture did not become ready' } if (-not (Test-Path $fixtureStatus)) { throw 'ONVIF fixture did not become ready' }
$server = Start-SensePackage $packageRoot $server = Start-SensePackage $packageRoot
Wait-Http "http://127.0.0.1:$mediaAPIPort/v3/config/global/get" Wait-Http -Uri "http://127.0.0.1:$mediaAPIPort/v3/config/global/get" -Process $server -Stage 'MediaMTX API' -LogPaths @($runtimeLog, $runtimeError)
$publisherArguments = @( $publisherArguments = @(
'-hide_banner', '-loglevel', 'error', '-re', '-f', 'lavfi', '-i', 'testsrc=size=640x360:rate=10', '-hide_banner', '-loglevel', 'error', '-re', '-f', 'lavfi', '-i', 'testsrc=size=640x360:rate=10',
'-c:v', 'libx264', '-preset', 'ultrafast', '-tune', 'zerolatency', '-f', 'rtsp', '-rtsp_transport', 'tcp', '-c:v', 'libx264', '-preset', 'ultrafast', '-tune', 'zerolatency', '-f', 'rtsp', '-rtsp_transport', 'tcp',
@@ -214,6 +338,7 @@ try {
if ([int]$bootstrapResponse.code -ne 200) { throw 'administrator bootstrap failed' } if ([int]$bootstrapResponse.code -ne 200) { throw 'administrator bootstrap failed' }
$login = Invoke-SenseJson POST '/api/v1/login' @{ username = 'acceptance-admin'; password = $adminPassword } $login = Invoke-SenseJson POST '/api/v1/login' @{ username = 'acceptance-admin'; password = $adminPassword }
$token = [string]$login.token $token = [string]$login.token
$script:sensitiveValues += $token
if ($token.Length -lt 20) { throw 'login did not return a usable token' } if ($token.Length -lt 20) { throw 'login did not return a usable token' }
$unauthorized = Invoke-SenseJson GET '/api/v1/devices' $null '' 401 $unauthorized = Invoke-SenseJson GET '/api/v1/devices' $null '' 401
@@ -265,13 +390,12 @@ try {
$area = @($areas.data.list | Where-Object id -eq $areaCreated.data.id)[0] $area = @($areas.data.list | Where-Object id -eq $areaCreated.data.id)[0]
if (-not $area.needsRecalibration) { throw 'resolution change did not mark the area for recalibration' } if (-not $area.needsRecalibration) { throw 'resolution change did not mark the area for recalibration' }
$browserScript = Join-Path $senseCopy 'ui\sense-browser-smoke.cjs' $browserScript = Join-Path $PSScriptRoot 'browser-smoke.cjs'
Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'browser-smoke.cjs') -Destination $browserScript
foreach ($item in @{ foreach ($item in @{
SENSE_E2E_BASE_URL = $baseUrl; SENSE_E2E_TOKEN = $token; SENSE_E2E_BROWSER = $Browser; SENSE_E2E_BASE_URL = $baseUrl; SENSE_E2E_TOKEN = $token; SENSE_E2E_BROWSER = $Browser;
SENSE_E2E_SCREENSHOT = (Join-Path $temporary 'sense-browser.png') SENSE_E2E_SCREENSHOT = (Join-Path $temporary 'sense-browser.png')
}.GetEnumerator()) { Set-TestEnvironment $item.Key $item.Value } }.GetEnumerator()) { Set-TestEnvironment $item.Key $item.Value }
Push-Location (Join-Path $senseCopy 'ui') Push-Location $temporary
try { & node.exe $browserScript } finally { Pop-Location } try { & node.exe $browserScript } finally { Pop-Location }
if ($LASTEXITCODE -ne 0) { throw 'browser GoAdmin shell smoke failed' } if ($LASTEXITCODE -ne 0) { throw 'browser GoAdmin shell smoke failed' }
@@ -299,7 +423,7 @@ try {
$plainCredentialCount = (& $psql -X -h 127.0.0.1 -p $pgPort -U sense_e2e -d sense_e2e -tAc "select count(*) from sense_device_credentials where position(convert_to('$cameraPassword','UTF8') in ciphertext) > 0;").Trim() $plainCredentialCount = (& $psql -X -h 127.0.0.1 -p $pgPort -U sense_e2e -d sense_e2e -tAc "select count(*) from sense_device_credentials where position(convert_to('$cameraPassword','UTF8') in ciphertext) > 0;").Trim()
if ([int]$plainCredentialCount -ne 0) { throw 'camera credential appeared in plaintext storage' } if ([int]$plainCredentialCount -ne 0) { throw 'camera credential appeared in plaintext storage' }
foreach ($log in @($runtimeLog, $runtimeError, $fixtureLog, $fixtureError, $ffmpegLog, $ffmpegError)) { foreach ($log in @($runtimeLog, $runtimeError, $fixtureLog, $fixtureError, $ffmpegLog, $ffmpegError, $preflightOut, $preflightError)) {
if (Test-Path $log) { if (Test-Path $log) {
$text = [string](Get-Content -LiteralPath $log -Raw -ErrorAction SilentlyContinue) $text = [string](Get-Content -LiteralPath $log -Raw -ErrorAction SilentlyContinue)
if ($null -eq $text) { $text = '' } if ($null -eq $text) { $text = '' }
@@ -311,6 +435,7 @@ try {
Stop-ProcessTree $publisher Stop-ProcessTree $publisher
Stop-ProcessTree $fixture Stop-ProcessTree $fixture
Stop-ProcessTree $server Stop-ProcessTree $server
Stop-ProcessTree $preflightMedia
if ($pgStarted) { if ($pgStarted) {
$pgStopArguments = "-D `"$pgData`" -m fast stop" $pgStopArguments = "-D `"$pgData`" -m fast stop"
[void](Start-Process -FilePath (Join-Path $PostgresBin 'pg_ctl.exe') -ArgumentList $pgStopArguments -RedirectStandardOutput (Join-Path $temporary 'pg-stop.log') -RedirectStandardError (Join-Path $temporary 'pg-stop.err.log') -WindowStyle Hidden -PassThru) [void](Start-Process -FilePath (Join-Path $PostgresBin 'pg_ctl.exe') -ArgumentList $pgStopArguments -RedirectStandardOutput (Join-Path $temporary 'pg-stop.log') -RedirectStandardError (Join-Path $temporary 'pg-stop.err.log') -WindowStyle Hidden -PassThru)