Files
lmxopcua/docs/drivers/OpcUaClient-Test-Fixture.md
2026-04-26 09:46:33 -04:00

211 lines
9.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# OPC UA Client test fixture
Coverage map + gap inventory for the OPC UA Client (gateway / aggregation)
driver.
**TL;DR:** Wire-level coverage now exists via
[opc-plc](https://github.com/Azure-Samples/iot-edge-opc-plc) — Microsoft
Industrial IoT's OPC UA PLC simulator running in Docker (task #215). Real
Secure Channel, real Session, real MonitoredItem exchange against an
independent server implementation. Unit tests still carry the exhaustive
capability matrix (cert auth / security policies / reconnect / failover /
attribute mapping). Gaps remaining: upstream-server-specific quirks
(historian aggregates, typed ConditionType events, SDK-publish-queue edge
behavior under load) — opc-plc uses the same OPCFoundation stack internally
so fully-independent-stack coverage needs `open62541/open62541` as a second
image (follow-up).
## What the fixture is
**Integration layer** (task #215):
`tests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/` stands up
`mcr.microsoft.com/iotedge/opc-plc:2.14.10` via `Docker/docker-compose.yml`
on `opc.tcp://localhost:50000`. `OpcPlcFixture` probes the port at
collection init + skips tests with a clear message when the container's
not running (matches the Modbus/pymodbus + S7/python-snap7 skip pattern).
Docker is the launcher — no PowerShell wrapper needed because opc-plc
ships pre-containerized. Compose-file flags: `--ut` (unsecured transport
advertised), `--aa` (auto-accept client certs — opc-plc's cert trust store
resets on each spin-up), `--alm` (alarm simulation for IAlarmSource
follow-up coverage), `--pn=50000` (port).
**Unit layer**:
`tests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/` is still the primary
coverage. Tests inject fakes through the driver's construction path; the
OPCFoundation.NetStandard `Session` surface is wrapped behind an interface
the tests mock.
## What it actually covers
### Integration (opc-plc Docker, task #215)
- `OpcUaClientSmokeTests.Client_connects_and_reads_StepUp_node_through_real_OPC_UA_stack`
full Secure Channel + Session + `ns=3;s=StepUp` Read round-trip
- `OpcUaClientSmokeTests.Client_reads_batch_of_varied_types_from_live_simulator`
batch Read of UInt32 / Int32 / Boolean; asserts `bool`-specific Variant
decoding to catch a common attribute-mapping regression
- `OpcUaClientSmokeTests.Client_subscribe_receives_StepUp_data_changes_from_live_server`
real `MonitoredItem` subscription against `ns=3;s=FastUInt1` (ticks every
100 ms); asserts `OnDataChange` fires within 3 s of subscribe
- `OpcUaClientReverseConnectSmokeTests.Driver_accepts_reverse_connect_from_opc_plc_rc_simulator`
reverse-connect (server-initiated) coverage. Driver binds
`opc.tcp://0.0.0.0:4844`, the `opc-plc-rc` docker service dials in via
`--rc opc.tcp://host.docker.internal:4844`, and a Read round-trips over
the inbound socket. Gated on `OPCUA_RC_SIM=1` because the simulator
requires `host.docker.internal` resolution which not every CI runner
exposes.
Wire-level surfaces verified: `IDriver` + `IReadable` + `ISubscribable` +
`IHostConnectivityProbe` (via the Secure Channel exchange).
### Unit
The surface is broad because `OpcUaClientDriver` is the richest-capability
driver in the fleet (it's a gateway for another OPC UA server, so it
mirrors the full capability matrix):
- `OpcUaClientDriverScaffoldTests``IDriver` lifecycle
- `OpcUaClientReadWriteTests` — read + write lifecycle
- `OpcUaClientSubscribeAndProbeTests` — monitored-item subscription + probe
state transitions
- `OpcUaClientDiscoveryTests``GetEndpoints` + endpoint selection
- `OpcUaClientAttributeMappingTests` — OPC UA node attribute → driver value
mapping
- `OpcUaClientSecurityPolicyTests``SignAndEncrypt` / `Sign` / `None`
policy negotiation contract
- `OpcUaClientCertAuthTests` — cert store paths, revocation-list config
- `OpcUaClientReconnectTests` — SDK reconnect hook + `TransferSubscriptions`
across the disconnect boundary
- `OpcUaClientFailoverTests` — primary → secondary session fallback per
driver config
- `OpcUaClientAlarmTests` — A&E severity bucket (11000 → Low / Medium /
High / Critical), subscribe / unsubscribe / ack contract
- `OpcUaClientHistoryTests` — historical data read + interpolation contract
Capability surfaces whose contract is verified: `IDriver`, `ITagDiscovery`,
`IReadable`, `IWritable`, `ISubscribable`, `IHostConnectivityProbe`,
`IAlarmSource`, `IHistoryProvider`.
## What it does NOT cover
### 1. Real stack exchange
No UA Secure Channel is ever opened. Every test mocks `Session.ReadAsync`,
`Session.CreateSubscription`, `Session.AddItem`, etc. — the SDK itself is
trusted. Certificate validation, signing, nonce handling, chunk assembly,
keep-alive cadence — all SDK-internal and untested here.
### 2. Subscription transfer across reconnect
Contract test: "after a simulated reconnect, `TransferSubscriptions` is
called with the right handles." Real behavior: SDK re-publishes against the
new channel and some events can be lost depending on publish-queue state.
The lossy window is not characterized.
### 3. Large-scale subscription stress
100+ monitored items with heterogeneous publish intervals under a single
session — the shape that breaks publish-queue-size tuning in the wild — is
not exercised.
### 4. Real historian mappings
`IHistoryProvider.ReadRawAsync` + `ReadProcessedAsync` +
`ReadAtTimeAsync` + `ReadEventsAsync` are contract-mocked. Against a real
historian (AVEVA Historian, Prosys historian, Kepware LocalHistorian) each
has specific interpolation + bad-quality-handling quirks the contract test
doesn't see.
### 5. Real A&E events
Alarm subscription is mocked via filtered monitored items; the actual
`EventFilter` select-clause behavior against a server that exposes typed
ConditionType events (non-base `BaseEventType`) is not verified.
### 6. Authentication variants
- Anonymous, UserName/Password, X509 cert tokens — each is contract-tested
but not exchanged against a server that actually enforces each.
- LDAP-backed `UserName` (matching this repo's server-side
`LdapUserAuthenticator`) requires a live LDAP round-trip; not tested.
## When to trust OpcUaClient tests, when to reach for a server
| Question | Unit tests | Real upstream server |
| --- | --- | --- |
| "Does severity 750 bucket as High?" | yes | yes |
| "Does the driver call `TransferSubscriptions` after reconnect?" | yes | yes |
| "Does a real OPC UA read/write round-trip work?" | no | yes (required) |
| "Does event-filter-based alarm subscription return ConditionType events?" | no | yes (required) |
| "Does history read from AVEVA Historian return correct aggregates?" | no | yes (required) |
| "Does the SDK's publish queue lose notifications under load?" | no | yes (stress) |
## Follow-up candidates
The easiest win here is to **wire the client driver tests against this
repo's own server**. The integration test project
`tests/ZB.MOM.WW.OtOpcUa.Server.Tests/OpcUaServerIntegrationTests.cs`
already stands up a real OPC UA server on a non-default port with a seeded
FakeDriver. An `OpcUaClientLiveLoopbackTests` that connects the client
driver to that server would give:
- Real Secure Channel negotiation
- Real Session / Subscription / MonitoredItem exchange
- Real read/write round-trip
- Real certificate validation (the integration test already sets up PKI)
It wouldn't cover upstream-server-specific quirks (AVEVA Historian, Kepware,
Prosys), but it would cover 80% of the SDK surface the driver sits on top
of.
Beyond that:
1. **Prosys OPC UA Simulation Server** — free, Windows-available, scriptable.
2. **UaExpert Server-Side Simulator** — Unified Automation's sample server;
good coverage of typed ConditionType events.
3. **Dedicated historian integration lab** — only path for
historian-specific coverage.
## HistoryRead aggregate coverage
PR-13 (issue #285) extended `HistoryAggregateType` from 5 to ~30 values
matching the OPC UA Part 13 §5 catalog. The mapping itself
(`OpcUaClientDriver.MapAggregateToNodeId`) is unit-tested via
`OpcUaClientAggregateMappingTests`:
- The full enum is swept with `Enum.GetValues<HistoryAggregateType>()`
every value must resolve to a non-null namespace-0 numeric `NodeId`.
- The 25 new aggregates each assert against a reflection-resolved
`Opc.Ua.ObjectIds.AggregateFunction_*` field by name, so a future SDK
upgrade that renames a constant trips the test loudly.
- The original 5 ordinals stay pinned to their pre-PR-13 NodeIds so existing
config files / persisted enums keep working.
This is **the well-known-NodeId test path** — the standard Part 13 NodeIds
are stable across SDK versions; round-tripping each one against a live
upstream is the integration suite's job and doesn't add coverage to the
mapping table itself.
`OpcUaClientAggregateSweepTests` is the integration counterpart. It loops
every enum value against a real opc-plc upstream and asserts the wire path
doesn't crash even when the simulator returns
`BadAggregateNotSupported` for an aggregate it doesn't honour. opc-plc's
default profile doesn't enable HistoryRead on the well-known nodes, so the
test currently `Assert.Skip`s — re-enables when the fixture image is
upgraded to a history-sim profile (`--useslowtypes --ut=10` or similar) and
a known-good historized NodeId is wired into `OpcPlcProfile`.
## Key fixture / config files
- `tests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/` — unit tests with
mocked `Session`
- `src/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs` — ctor +
session-factory seam tests mock through
- `tests/ZB.MOM.WW.OtOpcUa.Server.Tests/OpcUaServerIntegrationTests.cs`
the server-side integration harness a future loopback client test could
piggyback on
- `tests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/OpcUaClientAggregateMappingTests.cs`
— Part 13 aggregate enum-to-NodeId mapping coverage (PR-13)
- `tests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/OpcUaClientAggregateSweepTests.cs`
— wire-side aggregate sweep against opc-plc (build-only scaffold; PR-13)