e08855fb9d
Adds deferment.md — a source-verified inventory of new-driver status, all 17
open issues, in-code deferrals, open live gates and plan bookkeeping. Every
claim was checked against src/ and git, not against documentation.
Headline finding: three subsystems are authored, persisted and shipped but
never executed —
* Node ACLs: IPermissionEvaluator/TriePermissionEvaluator have zero
production consumers and OtOpcUaNodeManager never references them, yet
ClusterAcls.razor authors NodeAcl rows and ConfigComposer.cs:51 ships them
in every artifact. An authored deny rule has no effect.
* IRediscoverable / IHostConnectivityProbe raise into the void (#518/#507);
nothing ever writes a DriverHostStatus row.
* DriverTypeRegistry is vestigial, so no factory passes a tier and every
driver runs Tier A with the Tier-C protections dormant.
Also records the Calculation driver as picker-visible but unauthorable
(DriverConfigModal has no case) — the "registered but unauthorable" class
recurring after the Sql picker defect — and notes that no parity test guards
DriverConfigModal/DeviceModal, which is why it survived review.
Documentation corrections (source-verified):
* ReadWriteOperations.md claimed "a denied read never hits the driver" via
four types that do not exist in src/. Bannered + struck through; a security
review reading that page would have concluded a per-node ACL gate exists.
* CLAUDE.md: the Change Detection sentence was false (DriverHost consumes
nothing); the mesh Phase 4 and Phase 5 live gates and the auto-down 1-vs-1
gate had all PASSED; the ScriptedAlarmState table was already dropped.
* driver-expansion tracking: Modbus RTU and SQL poll are merged, not
pending — its command table would have made someone rebuild two shipped
drivers in a fresh worktree.
* drivers/README.md: dead DriverTypeRegistry paragraph, retired
SystemPlatform namespace kind, missing Sql + Calculation rows, missing
Modbus RTU-over-TCP transport.
* TwinCAT.md/Galaxy.md promised an address-space rebuild that never happens.
* Historian.md gained the #491 unproven-value-capture pointer.
* IncrementalSync.md and AddressSpace.md bannered as wholesale v2-era (the
rewrite is recorded as still owed, not done here); four v2 status docs
bannered as historical; three wrong-name-for-a-live-type fixes.
Left deliberately untouched: the secrets NoOpSecretReplicator line, which
makes a security claim and needs the real behaviour identified rather than a
rename.
187 lines
9.1 KiB
Markdown
187 lines
9.1 KiB
Markdown
# 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/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=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
|
||
|
||
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 (1–1000 → 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. 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-side
|
||
`LdapOpcUaUserAuthenticator`) 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:
|
||
|
||
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.
|
||
|
||
## Key fixture / config files
|
||
|
||
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/` — unit tests with
|
||
mocked `Session`
|
||
- `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/OpcPlcFixture.cs`
|
||
— collection fixture; parses `OPCUA_SIM_ENDPOINT` (default
|
||
`opc.tcp://10.100.0.35:50000`), TCP-probes at collection init, records
|
||
`SkipReason` when unreachable
|
||
- `tests/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.10` with `--ut --aa --alm --pn=50000`
|
||
- `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs` — ctor +
|
||
session-factory seam tests mock through; implements `IAlarmSource` +
|
||
`IHistoryProvider` (unique among drivers); does NOT implement `IRediscoverable`
|
||
- `tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/DualEndpointTests.cs` —
|
||
the v2 dual-endpoint integration harness a future loopback client test could
|
||
piggyback on (v1 `OpcUaServerIntegrationTests.cs` retired with the v1 server project)
|