E2E test script — Galaxy (MXAccess) driver: read / write / subscribe / alarms / history
Seven-stage e2e script covering every Galaxy-specific capability surface:
IReadable + IWritable + ISubscribable + IAlarmSource + IHistoryProvider.
Unlike the other drivers there is no per-protocol CLI — Galaxy's proxy
lives in-process with the server + talks to OtOpcUaGalaxyHost over a
named pipe (MXAccess COM is 32-bit-only), so every stage runs through
`otopcua-cli` against the published OPC UA address space.
## Stages
1. Probe — otopcua-cli read on the source NodeId
2. Source read — capture value for downstream comparison
3. Virtual-tag bridge — Phase 7 VirtualTag (source × 2) through
CachedTagUpstreamSource
4. Subscribe-sees-change — data-change events propagate
5. Reverse bridge — opc-ua write → Galaxy; soft-passes if the
attribute's Galaxy-side ACL forbids writes
(`BadUserAccessDenied` / `BadNotWritable`)
6. Alarm fires — scripted-alarm Condition fires with Active
state when source crosses threshold
7. History read — historyread returns samples from the Aveva
Historian → IHistoryProvider path
## Two new helpers in _common.ps1
- `Test-AlarmFiresOnThreshold` — start `otopcua-cli alarms --refresh`
in the background on a Condition NodeId, drive the source change,
assert captured stdout contains `ALARM` + `Active`. Uses the same
Start-Process + temp-file pattern as `Test-SubscribeSeesChange` since
the alarms command runs until Ctrl+C (no built-in --duration).
- `Test-HistoryHasSamples` — call `otopcua-cli historyread` over a
configurable lookback window, parse `N values returned.` marker, fail
if below MinSamples. Works for driver-sourced, virtual, or scripted-
alarm historized nodes.
## Wiring
- `test-all.ps1` picks up the optional `galaxy` sidecar section and
runs the script with the configured NodeIds + wait windows.
- `e2e-config.sample.json` adds a `galaxy` section seeded with the
Phase 7 defaults (`p7-smoke-tag-source` / `-vt-derived` /
`-al-overtemp`) — matches `scripts/smoke/seed-phase-7-smoke.sql`.
- `scripts/e2e/README.md` expected-matrix gains a Galaxy row.
## Prereqs
- OtOpcUaGalaxyHost running (NSSM-wrapped) with the Galaxy + MXAccess
runtime available
- `seed-phase-7-smoke.sql` applied with a live Galaxy attribute
substituted into `dbo.Tag.TagConfig`
- OtOpcUa server running against the `p7-smoke` cluster
- Non-elevated shell (Galaxy.Host pipe ACL denies Admins)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -310,6 +310,109 @@ function Test-SubscribeSeesChange {
|
||||
return @{ Passed = $false; Reason = "change not observed on subscription" }
|
||||
}
|
||||
|
||||
# Test — alarm fires on threshold. Start `otopcua-cli alarms --refresh` on the
|
||||
# alarm Condition NodeId in the background; drive the underlying data change via
|
||||
# `otopcua-cli write` on the input NodeId; wait for the subscription window to
|
||||
# close; assert the captured stdout contains a matching ALARM line (`SourceName`
|
||||
# of the Condition + an Active state). Covers Part 9 alarm propagation through
|
||||
# the server → driver → Condition node path.
|
||||
function Test-AlarmFiresOnThreshold {
|
||||
param(
|
||||
[Parameter(Mandatory)] $OpcUaCli,
|
||||
[Parameter(Mandatory)] [string]$OpcUaUrl,
|
||||
[Parameter(Mandatory)] [string]$AlarmNodeId,
|
||||
[Parameter(Mandatory)] [string]$InputNodeId,
|
||||
[Parameter(Mandatory)] [string]$TriggerValue,
|
||||
[int]$DurationSec = 10,
|
||||
[int]$SettleSec = 2
|
||||
)
|
||||
Write-Header "Alarm fires on threshold"
|
||||
|
||||
$stdout = New-TemporaryFile
|
||||
$stderr = New-TemporaryFile
|
||||
$allArgs = @($OpcUaCli.PrefixArgs) + @(
|
||||
"alarms", "-u", $OpcUaUrl, "-n", $AlarmNodeId, "-i", "500", "--refresh")
|
||||
$proc = Start-Process -FilePath $OpcUaCli.File `
|
||||
-ArgumentList $allArgs `
|
||||
-NoNewWindow -PassThru `
|
||||
-RedirectStandardOutput $stdout.FullName `
|
||||
-RedirectStandardError $stderr.FullName
|
||||
Write-Info "alarm subscription started (pid $($proc.Id)), waiting ${SettleSec}s to settle"
|
||||
Start-Sleep -Seconds $SettleSec
|
||||
|
||||
$w = Invoke-Cli -Cli $OpcUaCli -Args @(
|
||||
"write", "-u", $OpcUaUrl, "-n", $InputNodeId, "-v", $TriggerValue)
|
||||
if ($w.ExitCode -ne 0) {
|
||||
Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item $stdout.FullName, $stderr.FullName -ErrorAction SilentlyContinue
|
||||
Write-Fail "input write failed (exit=$($w.ExitCode))"
|
||||
Write-Host $w.Output
|
||||
return @{ Passed = $false; Reason = "input write failed" }
|
||||
}
|
||||
Write-Info "input write ok, waiting up to ${DurationSec}s for the alarm to surface"
|
||||
|
||||
# otopcua-cli alarms runs until Ctrl+C; terminate it ourselves after the
|
||||
# duration window (no built-in --duration flag on the alarms command).
|
||||
Start-Sleep -Seconds $DurationSec
|
||||
if (-not $proc.HasExited) { Stop-Process -Id $proc.Id -Force }
|
||||
|
||||
$out = (Get-Content $stdout.FullName -Raw) + (Get-Content $stderr.FullName -Raw)
|
||||
Remove-Item $stdout.FullName, $stderr.FullName -ErrorAction SilentlyContinue
|
||||
|
||||
# AlarmsCommand emits `[ts] ALARM <SourceName>` per event + lines for
|
||||
# State: Active,Unacknowledged | Severity | Message. Match on `ALARM` +
|
||||
# `Active` — both need to appear for the alarm to count as fired.
|
||||
if ($out -match "ALARM\b" -and $out -match "Active\b") {
|
||||
Write-Pass "alarm condition fired with Active state"
|
||||
return @{ Passed = $true }
|
||||
}
|
||||
Write-Fail "no Active alarm event observed in ${DurationSec}s"
|
||||
Write-Host $out
|
||||
return @{ Passed = $false; Reason = "no alarm event" }
|
||||
}
|
||||
|
||||
# Test — history-read returns samples. Calls `otopcua-cli historyread` on the
|
||||
# target NodeId for a time window (default 1h back) and asserts the CLI reports
|
||||
# at least one value returned. Works against any historized tag — driver-sourced,
|
||||
# virtual, or scripted-alarm historizing to the Aveva / SQLite sink.
|
||||
function Test-HistoryHasSamples {
|
||||
param(
|
||||
[Parameter(Mandatory)] $OpcUaCli,
|
||||
[Parameter(Mandatory)] [string]$OpcUaUrl,
|
||||
[Parameter(Mandatory)] [string]$NodeId,
|
||||
[int]$LookbackSec = 3600,
|
||||
[int]$MinSamples = 1
|
||||
)
|
||||
Write-Header "History read"
|
||||
|
||||
$end = (Get-Date).ToUniversalTime().ToString("o")
|
||||
$start = (Get-Date).ToUniversalTime().AddSeconds(-$LookbackSec).ToString("o")
|
||||
|
||||
$r = Invoke-Cli -Cli $OpcUaCli -Args @(
|
||||
"historyread", "-u", $OpcUaUrl, "-n", $NodeId,
|
||||
"--start", $start, "--end", $end, "--max", "1000")
|
||||
if ($r.ExitCode -ne 0) {
|
||||
Write-Fail "historyread exit=$($r.ExitCode)"
|
||||
Write-Host $r.Output
|
||||
return @{ Passed = $false; Reason = "historyread failed" }
|
||||
}
|
||||
|
||||
# HistoryReadCommand ends with `N values returned.` — parse and check >= MinSamples.
|
||||
if ($r.Output -match '(\d+)\s+values?\s+returned') {
|
||||
$count = [int]$Matches[1]
|
||||
if ($count -ge $MinSamples) {
|
||||
Write-Pass "$count samples returned (>= $MinSamples)"
|
||||
return @{ Passed = $true }
|
||||
}
|
||||
Write-Fail "only $count samples returned, expected >= $MinSamples — tag may not be historized, or lookback window misses samples"
|
||||
Write-Host $r.Output
|
||||
return @{ Passed = $false; Reason = "insufficient samples" }
|
||||
}
|
||||
Write-Fail "could not parse 'N values returned.' marker from historyread output"
|
||||
Write-Host $r.Output
|
||||
return @{ Passed = $false; Reason = "parse failure" }
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Summary helper — caller passes an array of test results.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user