Auto: ablegacy-12 — auto-demote on comm failure

Closes #255
This commit is contained in:
Joseph Doherty
2026-04-26 08:44:53 -04:00
parent 8ee65a75d2
commit 1e3053c0d8
18 changed files with 1160 additions and 31 deletions

View File

@@ -39,6 +39,21 @@
client may have bumped it by more, so the comparison is `>=`). NodeId form:
ns=<n>;s=AbLegacy/<gateway>/_Diagnostics/RequestCount. Mirrors the
-SystemConnectionStatusNodeId knob on test-abcip.ps1.
.PARAMETER DiagnosticsDemoteCountNodeId
Optional NodeId for the synthetic _Diagnostics/<host>/DemoteCount variable
emitted by AB Legacy discovery (PR ablegacy-12 / #255). When supplied, the
script runs the auto-demote assertion: kills the simulator container so
reads start failing, hammers the user-tag BridgeNodeId at least
FailureThreshold times to trip the demotion, then reads the diagnostic
counter and asserts the value increased by >= 1. NodeId form:
ns=<n>;s=AbLegacy/<gateway>/_Diagnostics/DemoteCount. The simulator
must support `docker stop otopcua-ab-server-slc500` for the kill stage.
.PARAMETER FailureThresholdForDemote
Failure threshold the server is configured with (default 3). The
demote assertion writes/reads N+1 times against the killed simulator
to guarantee the threshold trips even if some reads beat the kill.
#>
param(
@@ -47,7 +62,9 @@ param(
[string]$Address = "N7:5",
[string]$OpcUaUrl = "opc.tcp://localhost:4840",
[Parameter(Mandatory)] [string]$BridgeNodeId,
[string]$DiagnosticsRequestCountNodeId
[string]$DiagnosticsRequestCountNodeId,
[string]$DiagnosticsDemoteCountNodeId,
[int]$FailureThresholdForDemote = 3
)
$ErrorActionPreference = "Stop"
@@ -245,5 +262,67 @@ finally {
Remove-Item -Path $importJsonPath -ErrorAction SilentlyContinue
}
# PR ablegacy-12 / #255 — auto-demote round-trip. Kill the simulator container,
# hammer the bridge NodeId past the failure threshold, then assert the
# DemoteCount diagnostic incremented. Restart the simulator at the end so the
# next run gets a clean baseline. Gated on -DiagnosticsDemoteCountNodeId so
# environments without docker-side control of the simulator can opt out.
if ($DiagnosticsDemoteCountNodeId) {
Write-Header "AutoDemote (kill simulator + observe DemoteCount from $DiagnosticsDemoteCountNodeId)"
$baselineDemoteOut = & $opcUaCli.File @($opcUaCli.PrefixArgs) `
@("read", "-u", $OpcUaUrl, "-n", $DiagnosticsDemoteCountNodeId) 2>&1
$baselineDemote = 0
if (($baselineDemoteOut -join "`n") -match '(\d+)') { $baselineDemote = [int64]$Matches[1] }
# Best-effort container kill — prefer the slc500 profile name; fall back to
# micrologix / plc5 in case the operator pointed the e2e at a different family.
$simContainers = @("otopcua-ab-server-slc500", "otopcua-ab-server-micrologix", "otopcua-ab-server-plc5")
$killed = $false
foreach ($c in $simContainers) {
$stop = docker stop $c 2>$null
if ($LASTEXITCODE -eq 0 -and $stop) {
Write-Host "Stopped $c"
$killed = $true
break
}
}
if (-not $killed) {
Write-Fail "AutoDemote: no ab_server container found via 'docker stop' — skipping demote assertion"
$results += @{ Passed = $false; Reason = "no simulator container to kill" }
}
else {
# Hammer past the threshold. Each read against a now-unreachable simulator
# surfaces BadCommunicationError; FailureThreshold consecutive ones trip
# the demotion. We add 2 extra to absorb timing slack (one read may be
# in-flight when the kill lands).
$hammerCount = $FailureThresholdForDemote + 2
for ($i = 0; $i -lt $hammerCount; $i++) {
& $opcUaCli.File @($opcUaCli.PrefixArgs) `
@("read", "-u", $OpcUaUrl, "-n", $BridgeNodeId) 2>&1 | Out-Null
}
Start-Sleep -Seconds 1
$afterDemoteOut = & $opcUaCli.File @($opcUaCli.PrefixArgs) `
@("read", "-u", $OpcUaUrl, "-n", $DiagnosticsDemoteCountNodeId) 2>&1
$afterDemote = 0
if (($afterDemoteOut -join "`n") -match '(\d+)') { $afterDemote = [int64]$Matches[1] }
$deltaDemote = $afterDemote - $baselineDemote
if ($deltaDemote -ge 1) {
Write-Pass "AutoDemote DemoteCount delta $deltaDemote >= 1 after $hammerCount failed reads"
$results += @{ Passed = $true }
} else {
Write-Fail "AutoDemote DemoteCount delta $deltaDemote < 1 (baseline=$baselineDemote after=$afterDemote)"
$results += @{ Passed = $false; Reason = "demote delta $deltaDemote" }
}
# Restart the simulator so subsequent test runs have a clean baseline.
# Best-effort — if docker-compose isn't on the path the operator can
# bring it back manually via the Docker/docker-compose.yml profile.
try { docker start (docker ps -aq -f "name=otopcua-ab-server-") | Out-Null } catch { }
}
}
Write-Summary -Title "AB Legacy e2e" -Results $results
if ($results | Where-Object { -not $_.Passed }) { exit 1 }