Commit Graph

4 Commits

Author SHA1 Message Date
Joseph Doherty
2666a598ae Task #253 follow-up — fix test-all.ps1 StrictMode crash on missing JSON keys
Running `test-all.ps1` end-to-end with a partial sidecar (only modbus/
abcip/s7 populated, no focas/twincat/phase7) crashed:

    [FAIL] modbus runner crashed: The property 'opcUaUrl' cannot be
    found on this object. Verify that the property exists.

Root cause: `_common.ps1` sets `Set-StrictMode -Version 3.0`, which
turns missing-property access on PSCustomObject into a throw. Every
`$config.<driver>.<optional-field> ?? $default` and `if
($config.<missing-section>)` check is therefore unsafe against a
normal JSON where optional fields are omitted.

Fix: switch to `ConvertFrom-Json -AsHashtable` and add a `Get-Or`
helper. Hashtables tolerate `.ContainsKey()` / indexer access even
under StrictMode, so the per-driver sections now read:

    $modbus = Get-Or $config "modbus"
    if ($modbus) {
        ... -OpcUaUrl (Get-Or $modbus "opcUaUrl" $OpcUaUrl) ...
    }

Verified end-to-end with live docker-compose fixtures:
 - Modbus / AB CIP / S7 each run to completion, report 2/5 PASS (the
   driver-only stages) and FAIL the 3 server-bridge stages (expected —
   server-side factory wiring is blocked on #209).
 - The FINAL MATRIX header renders cleanly with SKIP rows for the
   drivers not present in the sidecar + FAIL rows for the present ones.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 10:55:15 -04:00
Joseph Doherty
fe981b0b7f Task #253 follow-up — driver-side e2e debug: port fixes + HR[200] scratch register
Ran the driver CLIs against the live docker-compose fixtures to debug
what actually works. Two real bugs surfaced:

1. `e2e-config.sample.json` pointed at the wrong simulator ports:
     - Modbus: 5502 → **5020** (pymodbus compose binding)
     - S7:      102 → **1102** (python-snap7, non-priv port)
     - AbCip:   no port → now explicit **:44818**
   `test-modbus.ps1` default `-ModbusHost` also shipped with 5502;
   fixed to 5020.

2. Modbus loopback was off-by-2 because pymodbus `standard.json` makes
   HR[100] an auto-increment register (value ticks on every poll).
   Switched `test-modbus.ps1` to **HR[200]** (scratch range in the
   profile) + updated sample config `bridgeNodeId` to match.

Also fixed the AbCip probe: `-t @raw_cpu_type` was rejected by the
driver's TagPath parser ("malformed TagPath"). Probe now uses the
user-supplied `-TagPath` for every family. Works against both
ab_server and real controllers.

Verified driver-side against live containers:
 - Modbus  5020:  probe ✓, HR[200] write+read round-trip ✓
 - AB CIP  44818: probe ✓, TestDINT write+read round-trip ✓
 - S7      1102:  probe ✓, DB1.DBW0 write+read round-trip ✓

## Known blocker (stages 3-5)

README now flags — and the 4 child issues under umbrella #209 track —
that `src/ZB.MOM.WW.OtOpcUa.Server/Program.cs:98-104` only registers
Galaxy + FOCAS driver factories. `DriverInstanceBootstrapper` silently
skips any `DriverType` without a registered factory, so stages 3-5
(anything crossing the OPC UA server) can't work today even with a
perfect Config DB seed. Issues #210-213 scope the factory + seed SQL
work per driver.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 10:31:55 -04:00
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
Joseph Doherty
8d92e00e38 Task #253 — E2E CLI test scripts + FOCAS test-client CLI
The driver-layer integration tests confirm the driver sees the PLC, and
the Client.CLI tests confirm the client sees the server. Nothing glued
them end-to-end until this PR.

- scripts/e2e/_common.ps1: shared helpers — CLI invocation (published-
  binary OR `dotnet run` fallback), Test-Probe / Test-DriverLoopback /
  Test-ServerBridge (all return @{Passed;Reason} hashtables).
- scripts/e2e/test-<modbus|abcip|ablegacy|s7|focas|twincat>.ps1: per-
  driver three-stage script (probe → driver-loopback → server-bridge).
  AB Legacy / FOCAS / TwinCAT are gated behind *_TRUST_WIRE env vars
  since they need real hardware (#222) or a licensed runtime (#221).
- scripts/e2e/test-phase7-virtualtags.ps1: writes a Modbus HR, reads
  the server-side VirtualTag (VT = input * 2) back via OPC UA, triggers
  + clears a scripted alarm. Exercises the Phase 7 CachedTagUpstreamSource
  + ScriptedAlarmEngine path.
- scripts/e2e/test-all.ps1: reads e2e-config.json sidecar, runs each
  present driver, prints a FINAL MATRIX (PASS/FAIL/SKIP). Missing
  sections SKIP rather than fail hard.
- scripts/e2e/e2e-config.sample.json: commented sample — each dev's
  NodeIds are local-seed-specific so e2e-config.json is .gitignore-d.
- scripts/e2e/README.md: full walkthrough — prereqs, three-stage design,
  env-var gates, expected matrix, why this is separate from `dotnet test`.

Tasks #249-#251 shipped Modbus/AbCip/AbLegacy/S7/TwinCAT CLIs but left
FOCAS out. Since test-focas.ps1 needs it, the 6th CLI ships here:

- src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli: probe/read/write/subscribe
  commands, AssemblyName `otopcua-focas-cli`. WriteCommand.ParseValue
  handles the full FocasDataType enum (Bit/Byte/Int16/Int32/Float32/
  Float64/String — no UInt variants; the FOCAS protocol exposes signed
  PMC + Fanuc-Float only). Default DataType is Int16 to match the PMC
  register convention.

Full-solution build clean (0 errors). FOCAS CLI wired into
ZB.MOM.WW.OtOpcUa.slnx. No .Tests project for the FOCAS CLI yet —
symmetric with how ProbeCommand has no unit-testable pure logic in the
other 5 CLIs either; WriteCommand.ParseValue parity will land in a
follow-up to keep this PR scoped.

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