Files
lmxopcua/scripts/e2e/README.md
Joseph Doherty a9b585ac5b Task #253 follow-up — bidirectional + subscribe-sees-change e2e stages
The original three-stage design (probe / driver-loopback / forward-
bridge) only proved driver-write → server-read. It missed:

 - OPC UA write → server → driver → PLC (the reverse direction)
 - server-side data-change notifications actually firing (a stale
   subscription can still let a read-after-the-fact return the new
   value and look fine)

Extend _common.ps1 with two helpers:

 - Test-OpcUaWriteBridge: otopcua-cli write the NodeId -> wait 3s ->
   driver CLI read the PLC side, assert equality.
 - Test-SubscribeSeesChange: Start-Process otopcua-cli subscribe in the
   background with --duration N, settle 2s, driver-side write, wait for
   the subscription window to close, assert captured stdout contains
   the new value.

Wire both into test-modbus / test-abcip / test-ablegacy / test-s7 /
test-focas / test-twincat after the existing forward-bridge stage.
Update README to describe the five-stage design + note that the
published NodeId must be writable for stages 4 + 5.

Also prepend UTF-8 BOM to every script in scripts/e2e so Windows
PowerShell 5.1 parsers agree on em-dash byte sequences the way
PowerShell 7 already does. The scripts still #Requires -Version 7.0 —
the BOM is purely defensive for IDE / CI step parsers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 10:08:52 -04:00

6.5 KiB

E2E CLI test scripts

End-to-end black-box tests that drive each protocol through its driver CLI and verify the resulting OPC UA address-space state through otopcua-cli. They answer one question per driver:

If I poke the real PLC through the driver, does the running OtOpcUa server see the change?

This is the acceptance gate v1 was missing — the driver-level integration tests (tests/.../IntegrationTests/) confirm the driver sees the PLC, and the OPC UA Client.CLI.Tests confirm the client sees the server — but nothing glued them end-to-end. These scripts close that loop.

Five-stage test per driver

Every per-driver script runs the same five tests. The goal is to prove both directions across the bridge plus subscription delivery — forward-only coverage would miss writable-flag drops, IWritable dispatch bugs, and broken data-change notification paths where a fresh read still returns the right value.

  1. probe — driver CLI opens a session + reads a sentinel. Confirms the simulator / PLC is reachable and speaking the protocol.
  2. Driver loopback — write a random value via the driver CLI, read it back via the same CLI. Confirms the driver round-trips without involving the OPC UA server. A failure here is a driver bug, not a server-bridge bug.
  3. Forward bridge (driver → server → client) — write a different random value via the driver CLI, wait --ServerPollDelaySec (default 3s), read the OPC UA NodeId the server publishes that tag at via otopcua-cli read. Confirms reads propagate from PLC to OPC UA client.
  4. Reverse bridge (client → server → driver) — write a fresh random value via otopcua-cli write against the same NodeId, wait --DriverPollDelaySec (default 3s), read the PLC-side via the driver CLI. Confirms writes propagate the other way — catches writable-flag drops, ACL misconfiguration, and IWritable dispatch bugs the forward test can't see.
  5. Subscribe-sees-change — start otopcua-cli subscribe --duration N in the background, give it --SettleSec (default 2s) to attach, write a random value via the driver CLI, wait for the subscription window to close, and assert the captured output mentions the new value. Confirms the server's monitored-item + data-change path actually fires — not just that a fresh read returns the new value.

The OtOpcUa server must already be running with a config that (a) binds a driver instance to the same PLC the script points at, and (b) publishes the address the script writes under a NodeId the script knows. Those NodeIds live in e2e-config.json (see below). The published tag must be writable — stages 4 + 5 will fail against a read-only tag.

Prereqs

  1. OtOpcUa server running on opc.tcp://localhost:4840 (or pass -OpcUaUrl to override). The server's Config DB must define a driver instance per protocol you want to test, bound to the matching simulator endpoint.
  2. Per-driver simulators running. See docs/v2/test-data-sources.md for the simulator matrix — pymodbus / ab_server / python-snap7 / opc-plc cover Modbus / AB / S7 / OPC UA Client. FOCAS and TwinCAT have no public simulator; they are gated with env-var skip flags below.
  3. PowerShell 7+. The runner uses null-coalescing + Set-StrictMode; the Windows-PowerShell-5.1 shell will not parse test-all.ps1.
  4. .NET 10 SDK. Each script either runs dotnet run --project src/ZB.MOM.WW.OtOpcUa.Driver.<Name>.Cli directly, or if $env:OTOPCUA_CLI_BIN points at a publish folder, runs the pre-built otopcua-*.exe from there (faster for repeat loops).

Running

One protocol at a time

./scripts/e2e/test-modbus.ps1 `
  -ModbusHost    127.0.0.1:5502 `
  -BridgeNodeId "ns=2;s=Modbus/HR100"

Every per-protocol script takes the driver endpoint, the address to write, and the OPC UA NodeId the server exposes it at.

Full matrix

./scripts/e2e/test-all.ps1 `
  -ConfigFile ./scripts/e2e/e2e-config.json

The runner reads the sidecar JSON, invokes each driver's script with the parameters from that section, and prints a FINAL MATRIX showing PASS / FAIL / SKIP per driver. Any driver absent from the sidecar is SKIP-ed rather than failing hard — useful on dev boxes that only have one simulator up.

Sidecar format

Copy e2e-config.sample.jsone2e-config.json and fill in the NodeIds from your server's Config DB. The file is .gitignore-d (each dev's NodeIds are specific to their local seed). Omit a driver section to skip it.

Expected pass/fail matrix (default config)

Driver Gate Default state on a clean dev box
Modbus PASS (pymodbus fixture)
AB CIP PASS (ab_server fixture)
AB Legacy AB_LEGACY_TRUST_WIRE=1 SKIP (ab_server PCCC path upstream-broken — task #222)
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)
Phase 7 PASS if the Modbus instance seeds a VT_DoubledHR100 virtual tag + AlarmHigh scripted alarm

Set the *_TRUST_WIRE env vars to 1 when you've pointed the script at real hardware or a properly-configured simulator.

Output

Each step prints one of:

  • [PASS] ... — step succeeded
  • [FAIL] ... — step failed, stdout of the failing CLI is echoed below for diagnosis
  • [SKIP] ... — step short-circuited (env-var gate)
  • [INFO] ... — progress note (e.g., "waiting 3s for server-side poll")

The runner ends with a coloured summary per driver:

==================== FINAL MATRIX ====================
  modbus     PASS
  abcip      PASS
  ablegacy   SKIP (no config entry)
  s7         PASS
  focas      SKIP (no config entry)
  twincat    SKIP (no config entry)
  phase7     PASS
All present suites passed.

Non-zero exit if any present suite failed. SKIPs do not fail the run.

Why this is separate from dotnet test

dotnet test covers driver-layer + server-layer correctness in isolation — mocks + in-process test hosts. These e2e scripts cover the integration seam that unit tests can't cover by design: a live OPC UA server process, a live simulator, and the wire between them. Run them before a v2 release-readiness sign-off, after a driver-layer change that could plausibly affect the NodeManager contract, and before any "it works on my box" handoff to QA.