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:
@@ -133,6 +133,7 @@ section to skip it.
|
||||
| Modbus | — | **PASS** (pymodbus fixture) |
|
||||
| AB CIP | — | **PASS** (ab_server fixture) |
|
||||
| AB Legacy | — | **PASS** (ab_server SLC500/MicroLogix/PLC-5 profiles; `/1,0` cip-path required for the Docker fixture) |
|
||||
| Galaxy | — | **PASS** (requires OtOpcUaGalaxyHost + a live Galaxy; 7 stages including alarms + history) |
|
||||
| S7 | — | **PASS** (python-snap7 fixture) |
|
||||
| FOCAS | `FOCAS_TRUST_WIRE=1` | **SKIP** (no public simulator — task #222 lab rig) |
|
||||
| TwinCAT | `TWINCAT_TRUST_WIRE=1` | **SKIP** (needs XAR or standalone Router — task #221) |
|
||||
|
||||
@@ -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.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -49,6 +49,17 @@
|
||||
"bridgeNodeId": "ns=2;s=TwinCAT/MAIN_iCounter"
|
||||
},
|
||||
|
||||
"galaxy": {
|
||||
"$comment": "Galaxy (MXAccess) driver. Has no per-driver CLI — all stages go through otopcua-cli against the published NodeIds. Seven stages: probe / source read / virtual-tag bridge / subscribe-sees-change / reverse write / alarm fires / history read. Requires OtOpcUaGalaxyHost running + seed-phase-7-smoke.sql applied with a real Galaxy attribute substituted into dbo.Tag.TagConfig.",
|
||||
"sourceNodeId": "ns=2;s=p7-smoke-tag-source",
|
||||
"virtualNodeId": "ns=2;s=p7-smoke-vt-derived",
|
||||
"alarmNodeId": "ns=2;s=p7-smoke-al-overtemp",
|
||||
"alarmTriggerValue": "75",
|
||||
"changeWaitSec": 10,
|
||||
"alarmWaitSec": 10,
|
||||
"historyLookbackSec": 3600
|
||||
},
|
||||
|
||||
"phase7": {
|
||||
"$comment": "Virtual tags + scripted alarms. The VirtualNodeId must resolve to a server-side virtual tag whose script reads the modbus InputNodeId and writes VT = input * 2. The AlarmNodeId is the ConditionId of a scripted alarm that fires when VT > 100.",
|
||||
"modbusEndpoint": "127.0.0.1:5502",
|
||||
|
||||
@@ -172,6 +172,23 @@ else { $summary["twincat"] = "SKIP (no config entry)" }
|
||||
# Phase 7 virtual tags + scripted alarms
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
$galaxy = Get-Or $config "galaxy"
|
||||
if ($galaxy) {
|
||||
Write-Header "== GALAXY =="
|
||||
Run-Suite "galaxy" {
|
||||
& "$PSScriptRoot/test-galaxy.ps1" `
|
||||
-OpcUaUrl (Get-Or $galaxy "opcUaUrl" $OpcUaUrl) `
|
||||
-SourceNodeId $galaxy["sourceNodeId"] `
|
||||
-VirtualNodeId (Get-Or $galaxy "virtualNodeId" "") `
|
||||
-AlarmNodeId (Get-Or $galaxy "alarmNodeId" "") `
|
||||
-AlarmTriggerValue (Get-Or $galaxy "alarmTriggerValue" "75") `
|
||||
-ChangeWaitSec (Get-Or $galaxy "changeWaitSec" 10) `
|
||||
-AlarmWaitSec (Get-Or $galaxy "alarmWaitSec" 10) `
|
||||
-HistoryLookbackSec (Get-Or $galaxy "historyLookbackSec" 3600)
|
||||
}
|
||||
}
|
||||
else { $summary["galaxy"] = "SKIP (no config entry)" }
|
||||
|
||||
$phase7 = Get-Or $config "phase7"
|
||||
if ($phase7) {
|
||||
Write-Header "== PHASE 7 virtual tags + scripted alarms =="
|
||||
|
||||
278
scripts/e2e/test-galaxy.ps1
Normal file
278
scripts/e2e/test-galaxy.ps1
Normal file
@@ -0,0 +1,278 @@
|
||||
#Requires -Version 7.0
|
||||
<#
|
||||
.SYNOPSIS
|
||||
End-to-end CLI test for the Galaxy (MXAccess) driver — read, write, subscribe,
|
||||
alarms, and history through a running OtOpcUa server.
|
||||
|
||||
.DESCRIPTION
|
||||
Unlike the other e2e scripts there is no `otopcua-galaxy-cli` — the Galaxy
|
||||
driver proxy lives in-process with the server + talks to `OtOpcUaGalaxyHost`
|
||||
over a named pipe (MXAccess is 32-bit COM, can't ship in the .NET 10 process).
|
||||
Every stage therefore goes through `otopcua-cli` against the published OPC UA
|
||||
address space.
|
||||
|
||||
Seven stages:
|
||||
|
||||
1. Probe — otopcua-cli connect + read the source NodeId; confirms
|
||||
the whole Galaxy.Host → Proxy → server → client chain is
|
||||
up
|
||||
2. Source read — otopcua-cli read returns a Good value for the source
|
||||
attribute; proves IReadable.ReadAsync is dispatching
|
||||
through the IPC bridge
|
||||
3. Virtual-tag bridge — `otopcua-cli read` on the VirtualTag NodeId; confirms
|
||||
the Phase 7 CachedTagUpstreamSource is bridging the
|
||||
driver-sourced input into the scripting engine
|
||||
4. Subscribe-sees-change — subscribe to the source NodeId in the background;
|
||||
Galaxy pushes a data-change event within N seconds
|
||||
(Galaxy's underlying attribute must be actively
|
||||
changing — production Galaxies typically have
|
||||
scan-driven updates; for idle galaxies, widen
|
||||
-ChangeWaitSec or drive the write stage below first)
|
||||
5. Reverse bridge — `otopcua-cli write` to a writable Galaxy attribute;
|
||||
read it back. Gracefully becomes INFO-only if the
|
||||
attribute's Galaxy-side AccessLevel forbids writes
|
||||
(BadUserAccessDenied / BadNotWritable)
|
||||
6. Alarm fires — subscribe to the scripted-alarm Condition NodeId,
|
||||
drive the source tag above its threshold, confirm an
|
||||
Active alarm event surfaces. Exercises the Part 9
|
||||
alarm-condition propagation path
|
||||
7. History read — historyread on the source tag over the last hour;
|
||||
confirms Aveva Historian → IHistoryProvider dispatch
|
||||
returns samples
|
||||
|
||||
The Phase 7 seed (`scripts/smoke/seed-phase-7-smoke.sql`) already plants the
|
||||
right shape — one Galaxy DriverInstance, one source Tag, one VirtualTag
|
||||
(source × 2), one ScriptedAlarm (source > 50). Substitute the real Galaxy
|
||||
attribute FullName into `dbo.Tag.TagConfig` before running.
|
||||
|
||||
.PARAMETER OpcUaUrl
|
||||
OtOpcUa server endpoint. Default opc.tcp://localhost:4840.
|
||||
|
||||
.PARAMETER SourceNodeId
|
||||
NodeId of the driver-sourced Galaxy tag (numeric, writable preferred).
|
||||
Default matches the Phase 7 seed — `ns=2;s=p7-smoke-tag-source`.
|
||||
|
||||
.PARAMETER VirtualNodeId
|
||||
NodeId of the VirtualTag computed as Source × 2 (Phase 7 scripting).
|
||||
Default matches the Phase 7 seed — `ns=2;s=p7-smoke-vt-derived`.
|
||||
|
||||
.PARAMETER AlarmNodeId
|
||||
NodeId of the scripted-alarm Condition (fires when Source > 50).
|
||||
Default matches the Phase 7 seed — `ns=2;s=p7-smoke-al-overtemp`.
|
||||
|
||||
.PARAMETER AlarmTriggerValue
|
||||
Value written to -SourceNodeId to push it over the alarm threshold.
|
||||
Default 75 (well above the seeded 50-threshold).
|
||||
|
||||
.PARAMETER ChangeWaitSec
|
||||
Seconds the subscribe-sees-change stage waits for a natural data change.
|
||||
Default 10. Idle galaxies may need this extended or the stage will fail
|
||||
with "subscribe did not observe...".
|
||||
|
||||
.PARAMETER AlarmWaitSec
|
||||
Seconds the alarm-fires stage waits after triggering the write. Default 10.
|
||||
|
||||
.PARAMETER HistoryLookbackSec
|
||||
Seconds back from now to query history. Default 3600 (1 h).
|
||||
|
||||
.EXAMPLE
|
||||
# Against the default Phase-7 smoke seed + live Galaxy + OtOpcUa server
|
||||
./scripts/e2e/test-galaxy.ps1
|
||||
|
||||
.EXAMPLE
|
||||
# Custom NodeIds from a non-smoke cluster
|
||||
./scripts/e2e/test-galaxy.ps1 `
|
||||
-SourceNodeId "ns=2;s=Reactor1.Temperature" `
|
||||
-VirtualNodeId "ns=2;s=Reactor1.TempDoubled" `
|
||||
-AlarmNodeId "ns=2;s=Reactor1.OverTemp" `
|
||||
-AlarmTriggerValue 120
|
||||
#>
|
||||
|
||||
param(
|
||||
[string]$OpcUaUrl = "opc.tcp://localhost:4840",
|
||||
[string]$SourceNodeId = "ns=2;s=p7-smoke-tag-source",
|
||||
[string]$VirtualNodeId = "ns=2;s=p7-smoke-vt-derived",
|
||||
[string]$AlarmNodeId = "ns=2;s=p7-smoke-al-overtemp",
|
||||
[string]$AlarmTriggerValue = "75",
|
||||
[int]$ChangeWaitSec = 10,
|
||||
[int]$AlarmWaitSec = 10,
|
||||
[int]$HistoryLookbackSec = 3600
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
. "$PSScriptRoot/_common.ps1"
|
||||
|
||||
$opcUaCli = Get-CliInvocation `
|
||||
-ProjectFolder "src/ZB.MOM.WW.OtOpcUa.Client.CLI" `
|
||||
-ExeName "otopcua-cli"
|
||||
|
||||
$results = @()
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stage 1 — Probe. The probe is an otopcua-cli read against the source NodeId;
|
||||
# success implies Galaxy.Host is up + the pipe ACL lets the server connect +
|
||||
# the Proxy is tracking the tag + the server published it.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Write-Header "Probe"
|
||||
$probe = Invoke-Cli -Cli $opcUaCli -Args @("read", "-u", $OpcUaUrl, "-n", $SourceNodeId)
|
||||
if ($probe.ExitCode -eq 0 -and $probe.Output -match "Status:\s+0x00000000") {
|
||||
Write-Pass "source NodeId readable (Galaxy pipe → proxy → server → client chain up)"
|
||||
$results += @{ Passed = $true }
|
||||
} else {
|
||||
Write-Fail "probe read failed (exit=$($probe.ExitCode))"
|
||||
Write-Host $probe.Output
|
||||
$results += @{ Passed = $false; Reason = "probe failed" }
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stage 2 — Source read. Captures the current value for the later virtual-tag
|
||||
# comparison + confirms read dispatch works end-to-end. Failure here without a
|
||||
# stage-1 failure would be unusual — probe already reads.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Write-Header "Source read"
|
||||
$sourceRead = Invoke-Cli -Cli $opcUaCli -Args @("read", "-u", $OpcUaUrl, "-n", $SourceNodeId)
|
||||
$sourceValue = $null
|
||||
if ($sourceRead.ExitCode -eq 0 -and $sourceRead.Output -match "Value:\s+([^\r\n]+)") {
|
||||
$sourceValue = $Matches[1].Trim()
|
||||
Write-Pass "source value = $sourceValue"
|
||||
$results += @{ Passed = $true }
|
||||
} else {
|
||||
Write-Fail "source read failed"
|
||||
Write-Host $sourceRead.Output
|
||||
$results += @{ Passed = $false; Reason = "source read failed" }
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stage 3 — Virtual-tag bridge. Reads the Phase 7 VirtualTag (source × 2). Not
|
||||
# strictly driver-specific, but exercises the CachedTagUpstreamSource bridge
|
||||
# (the seam most likely to silently stop working after a Galaxy-side change).
|
||||
# Skip if the VirtualNodeId param is empty (non-Phase-7 clusters).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
if ([string]::IsNullOrEmpty($VirtualNodeId)) {
|
||||
Write-Header "Virtual-tag bridge"
|
||||
Write-Skip "VirtualNodeId not supplied — skipping Phase 7 bridge check"
|
||||
} else {
|
||||
Write-Header "Virtual-tag bridge"
|
||||
$vtRead = Invoke-Cli -Cli $opcUaCli -Args @("read", "-u", $OpcUaUrl, "-n", $VirtualNodeId)
|
||||
if ($vtRead.ExitCode -eq 0 -and $vtRead.Output -match "Value:\s+([^\r\n]+)") {
|
||||
$vtValue = $Matches[1].Trim()
|
||||
Write-Pass "virtual-tag value = $vtValue (source was $sourceValue)"
|
||||
$results += @{ Passed = $true }
|
||||
} else {
|
||||
Write-Fail "virtual-tag read failed"
|
||||
Write-Host $vtRead.Output
|
||||
$results += @{ Passed = $false; Reason = "virtual-tag read failed" }
|
||||
}
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stage 4 — Subscribe-sees-change. otopcua-cli subscribe in the background;
|
||||
# wait N seconds for Galaxy to push any data-change event on the source node.
|
||||
# This is optimistic — if the Galaxy attribute is idle, widen -ChangeWaitSec.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Write-Header "Subscribe sees change"
|
||||
$stdout = New-TemporaryFile
|
||||
$stderr = New-TemporaryFile
|
||||
$subArgs = @($opcUaCli.PrefixArgs) + @(
|
||||
"subscribe", "-u", $OpcUaUrl, "-n", $SourceNodeId,
|
||||
"-i", "500", "--duration", "$ChangeWaitSec")
|
||||
$subProc = Start-Process -FilePath $opcUaCli.File `
|
||||
-ArgumentList $subArgs -NoNewWindow -PassThru `
|
||||
-RedirectStandardOutput $stdout.FullName `
|
||||
-RedirectStandardError $stderr.FullName
|
||||
Write-Info "subscription started (pid $($subProc.Id)) for ${ChangeWaitSec}s"
|
||||
$subProc.WaitForExit(($ChangeWaitSec + 5) * 1000) | Out-Null
|
||||
if (-not $subProc.HasExited) { Stop-Process -Id $subProc.Id -Force }
|
||||
$subOut = (Get-Content $stdout.FullName -Raw) + (Get-Content $stderr.FullName -Raw)
|
||||
Remove-Item $stdout.FullName, $stderr.FullName -ErrorAction SilentlyContinue
|
||||
|
||||
# Any `=` followed by `(Good)` line after the initial subscribe-confirmation
|
||||
# indicates at least one data-change tick arrived.
|
||||
$changeLines = ($subOut -split "`n") | Where-Object { $_ -match "=\s+.*\(Good\)" }
|
||||
if ($changeLines.Count -gt 0) {
|
||||
Write-Pass "$($changeLines.Count) data-change events observed"
|
||||
$results += @{ Passed = $true }
|
||||
} else {
|
||||
Write-Fail "no data-change events in ${ChangeWaitSec}s — Galaxy attribute may be idle; rerun with -ChangeWaitSec larger, or trigger a change first"
|
||||
Write-Host $subOut
|
||||
$results += @{ Passed = $false; Reason = "no data-change" }
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stage 5 — Reverse bridge (OPC UA write → Galaxy). Galaxy attributes with
|
||||
# AccessLevel > FreeAccess often reject anonymous writes; record as INFO when
|
||||
# that's the case rather than failing the whole script.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Write-Header "Reverse bridge (OPC UA write)"
|
||||
$writeValue = [int]$AlarmTriggerValue # reuse the alarm trigger value — two stages for one write
|
||||
$w = Invoke-Cli -Cli $opcUaCli -Args @(
|
||||
"write", "-u", $OpcUaUrl, "-n", $SourceNodeId, "-v", "$writeValue")
|
||||
if ($w.ExitCode -ne 0) {
|
||||
# Connection/protocol failure — still a test failure.
|
||||
Write-Fail "write CLI exit=$($w.ExitCode)"
|
||||
Write-Host $w.Output
|
||||
$results += @{ Passed = $false; Reason = "write failed" }
|
||||
} elseif ($w.Output -match "Write failed:\s*0x801F0000") {
|
||||
Write-Info "BadUserAccessDenied — attribute's Galaxy-side ACL blocks writes for this session. Not a bug; grant WriteOperate or run against a writable attribute."
|
||||
$results += @{ Passed = $true; Reason = "acl-expected" }
|
||||
} elseif ($w.Output -match "Write failed:\s*0x80390000|BadNotWritable") {
|
||||
Write-Info "BadNotWritable — attribute is read-only at the Galaxy layer (status attributes, @-prefixed meta, etc)."
|
||||
$results += @{ Passed = $true; Reason = "readonly-expected" }
|
||||
} elseif ($w.Output -match "Write successful") {
|
||||
# Read back — Galaxy poll interval + MXAccess advise may need a second or two to settle.
|
||||
Start-Sleep -Seconds 2
|
||||
$r = Invoke-Cli -Cli $opcUaCli -Args @("read", "-u", $OpcUaUrl, "-n", $SourceNodeId)
|
||||
if ($r.Output -match "Value:\s+$([Regex]::Escape("$writeValue"))\b") {
|
||||
Write-Pass "write propagated — source reads back $writeValue"
|
||||
$results += @{ Passed = $true }
|
||||
} else {
|
||||
Write-Fail "write reported success but read-back did not reflect $writeValue"
|
||||
Write-Host $r.Output
|
||||
$results += @{ Passed = $false; Reason = "write-readback mismatch" }
|
||||
}
|
||||
} else {
|
||||
Write-Fail "unexpected write response"
|
||||
Write-Host $w.Output
|
||||
$results += @{ Passed = $false; Reason = "unexpected write response" }
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stage 6 — Alarm fires. Uses the helper from _common.ps1. If stage 5 already
|
||||
# wrote the trigger value the alarm may already be active; that's fine — the
|
||||
# Part 9 ConditionRefresh in the alarms CLI replays the current state so the
|
||||
# subscribe window still captures the Active event.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
if ([string]::IsNullOrEmpty($AlarmNodeId)) {
|
||||
Write-Header "Alarm fires on threshold"
|
||||
Write-Skip "AlarmNodeId not supplied — skipping alarm check"
|
||||
} else {
|
||||
$results += Test-AlarmFiresOnThreshold `
|
||||
-OpcUaCli $opcUaCli `
|
||||
-OpcUaUrl $OpcUaUrl `
|
||||
-AlarmNodeId $AlarmNodeId `
|
||||
-InputNodeId $SourceNodeId `
|
||||
-TriggerValue $AlarmTriggerValue `
|
||||
-DurationSec $AlarmWaitSec
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Stage 7 — History read. historyread against the source tag over the last N
|
||||
# seconds. Failure modes the skip pattern catches: tag not historized in the
|
||||
# Galaxy attribute's historization profile, or the lookback window misses the
|
||||
# sample cadence.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
$results += Test-HistoryHasSamples `
|
||||
-OpcUaCli $opcUaCli `
|
||||
-OpcUaUrl $OpcUaUrl `
|
||||
-NodeId $SourceNodeId `
|
||||
-LookbackSec $HistoryLookbackSec
|
||||
|
||||
Write-Summary -Title "Galaxy e2e" -Results $results
|
||||
if ($results | Where-Object { -not $_.Passed }) { exit 1 }
|
||||
Reference in New Issue
Block a user