Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
9.6 KiB
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 — 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/Drivers/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://10.100.0.35:50000 (the shared Docker host; override via
OPCUA_SIM_ENDPOINT). 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/Drivers/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=StepUpRead round-tripOpcUaClientSmokeTests.Client_reads_batch_of_varied_types_from_live_simulator— batch Read of UInt32 / Int32 / Boolean; assertsbool-specific Variant decoding to catch a common attribute-mapping regressionOpcUaClientSmokeTests.Client_subscribe_receives_StepUp_data_changes_from_live_server— realMonitoredItemsubscription againstns=3;s=FastUInt1(ticks every 100 ms); assertsOnDataChangefires within 3 s of subscribe
Wire-level surfaces verified: IDriver + IReadable + ISubscribable +
IHostConnectivityProbe (via the Secure Channel exchange).
In-process wire tests (no opc-plc)
tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/OpcUaClientDriverWireTests drives the real driver
against the in-process OtOpcUa server (ServerEventRig, authenticating with the rig's API-key credential):
read (+ missing node), write through the node-write gateway (+ read-only refusal, + failed-write revert),
subscribe/unsubscribe, host-status Running -> not-Running when the server stops, and discovery. These need
no Docker. The HistoryRead wire leg is deferred (needs an IHistorianDataSource stub on the server).
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—IDriverlifecycleOpcUaClientReadWriteTests— read + write lifecycleOpcUaClientSubscribeAndProbeTests— monitored-item subscription + probe state transitionsOpcUaClientDiscoveryTests—GetEndpoints+ endpoint selectionOpcUaClientAttributeMappingTests— OPC UA node attribute → driver value mappingOpcUaClientSecurityPolicyTests—SignAndEncrypt/Sign/Nonepolicy negotiation contractOpcUaClientCertAuthTests— cert store paths, revocation-list configOpcUaClientReconnectTests— SDK reconnect hook +TransferSubscriptionsacross the disconnect boundaryOpcUaClientFailoverTests— primary → secondary session fallback per driver configOpcUaClientAlarmTests— A&E severity bucket (1–1000 → Low / Medium / High / Critical), subscribe / unsubscribe / ack contractOpcUaClientHistoryTests— 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. Full real-stack exchange (unit tests only)
The unit suite mocks Session.ReadAsync, Session.CreateSubscription,
Session.AddItem, etc. — no UA Secure Channel is opened. The integration
suite (OpcUaClientSmokeTests, task #215) does open a real Secure Channel
against opc-plc and exercises Read + Subscribe end-to-end. What remains
untested even in the integration suite: certificate validation under
non-anonymous security policies, signing/encryption, nonce handling, chunk
assembly, keep-alive cadence — all SDK-internal.
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-sideLdapOpcUaUserAuthenticator) requires a live LDAP round-trip; not tested.
When to trust OpcUaClient tests, when to reach for a server
| Question | Unit tests | Integration (opc-plc) | 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 round-trip work?" | no | yes | yes |
| "Does a real OPC UA subscribe deliver changes?" | no | yes | yes |
| "Does write round-trip work against a live server?" | no | no (not yet exercised) | yes (required) |
| "Does event-filter-based alarm subscription return ConditionType events?" | no | no | yes (required) |
| "Does history read from AVEVA Historian return correct aggregates?" | no | no | yes (required) |
| "Does the SDK's publish queue lose notifications under load?" | no | no | yes (stress) |
Follow-up candidates
The easiest win here is to wire the client driver tests against this
repo's own server. The v2 integration test project
tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/DualEndpointTests.cs
(the v2 replacement for the retired v1 OpcUaServerIntegrationTests) 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:
- Prosys OPC UA Simulation Server — free, Windows-available, scriptable.
- UaExpert Server-Side Simulator — Unified Automation's sample server; good coverage of typed ConditionType events.
- Dedicated historian integration lab — only path for historian-specific coverage.
Key fixture / config files
tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/— unit tests with mockedSessiontests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/OpcPlcFixture.cs— collection fixture; parsesOPCUA_SIM_ENDPOINT(defaultopc.tcp://10.100.0.35:50000), TCP-probes at collection init, recordsSkipReasonwhen unreachabletests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/OpcUaClientSmokeTests.cs— wire-level test suite (3[Fact]methods: read, batch read, subscribe)tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/Docker/docker-compose.yml—mcr.microsoft.com/iotedge/opc-plc:2.14.10with--ut --aa --alm --pn=50000src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs— ctor + session-factory seam tests mock through; implementsIAlarmSource+IHistoryProvider(unique among drivers); does NOT implementIRediscoverabletests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/DualEndpointTests.cs— the v2 dual-endpoint integration harness a future loopback client test could piggyback on (v1OpcUaServerIntegrationTests.csretired with the v1 server project)