docs: document OPC UA 2.0 compatibility and rollout
This commit is contained in:
@@ -1,185 +1,88 @@
|
||||
# OPC UA Client test fixture
|
||||
|
||||
Coverage map + gap inventory for the OPC UA Client (gateway / aggregation)
|
||||
driver.
|
||||
This is the fixture and command map for the driver/browser upgrade from SDK
|
||||
**1.5.378.182** to exact **2.0.1** (#782). It describes available tests; execution,
|
||||
counts, skips and release decisions belong in [#782](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/782)
|
||||
and the linked [execution evidence](../plans/2026-11-10-opcua-sdk-2-evidence.md).
|
||||
A listed test or historical run is not candidate release evidence.
|
||||
|
||||
**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).
|
||||
## Fixture layers
|
||||
|
||||
## What the fixture is
|
||||
| Layer | Repository location | Scope and limitation |
|
||||
|---|---|---|
|
||||
| Driver unit | `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/` | Faked session contracts: lifecycle/generations, value conversion, per-item errors, policy selection, credentials, namespace remapping, subscription cleanup, discovery, alarms and host-owned reinitialization. No real secure channel. |
|
||||
| Browser unit | `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests/` | Browse attributes/pagination and certificate-identity ownership, including failure cleanup. No external server. |
|
||||
| In-process wire | `tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/` | Real driver against OtOpcUa: `OpcUaClientDriverWireTests`, `OpcUaClientDriverStallWireTests`, `OpcUaClientDriverReconnectWireTests`. Real channels but both sides use this repository's SDK. |
|
||||
| External simulator | `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/` | `OpcUaClientSmokeTests`: real read, varied-type batch read and subscription against opc-plc. Does not prove all upstream products, secured authentication, write or alarm compatibility. |
|
||||
| Browser integration | `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.IntegrationTests/` | Separate fixture-backed browser tests; inspect fixture availability and individual results. |
|
||||
|
||||
**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).
|
||||
The SDK 2 unit boundary cases include `OpcUaClientSdkValueBoundaryTests` and
|
||||
`OpcUaClientContinuationPointReleaseTests`. They preserve CLR value types and
|
||||
null/empty buffers, reject unsupported matrix items without shifting batch results,
|
||||
and release abandoned browse points. Certificate tests cover lifetime through
|
||||
session close; unit mocks alone do not establish trust-chain interoperability.
|
||||
|
||||
**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.
|
||||
The in-process stall proxy holds bytes without closing the session, then resumes;
|
||||
its hard-close case refuses connections. These exercise stale notification rejection,
|
||||
quality transitions and explicit host reinitialization. The driver does not use
|
||||
`SessionReconnectHandler` or `TransferSubscriptions`; historical transfer-based
|
||||
coverage descriptions do not apply to the current lifecycle (#670/#676).
|
||||
|
||||
## What it actually covers
|
||||
The driver has **no upstream HistoryRead capability** (#655). Its tags can be
|
||||
recorded into the local server-wide historian; backend conformance and aggregate
|
||||
oracles are documented in [Historian.md](../Historian.md).
|
||||
|
||||
### Integration (opc-plc Docker, task #215)
|
||||
## Running locally
|
||||
|
||||
- `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
|
||||
Run from the repository root. Build first and use a fresh result directory for
|
||||
**each** project. The [runbook](../plans/2026-11-10-opcua-sdk-2-rollout.md#verification-commands)
|
||||
provides the result helper that preserves test exit status and rejects all-skipped
|
||||
runs. For example, after defining that helper:
|
||||
|
||||
Wire-level surfaces verified: `IDriver` + `IReadable` + `ISubscribable` +
|
||||
`IHostConnectivityProbe` (via the Secure Channel exchange).
|
||||
```bash
|
||||
opc_test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests.csproj
|
||||
opc_test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser.Tests.csproj
|
||||
opc_test tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests.csproj
|
||||
```
|
||||
|
||||
### In-process wire tests (no opc-plc)
|
||||
The simulator compose file is
|
||||
`tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/Docker/docker-compose.yml`.
|
||||
It launches `mcr.microsoft.com/iotedge/opc-plc:2.14.10` with `--ut --aa --alm --pn=50000`.
|
||||
Use the shared-host procedure in [infra/README.md](../../infra/README.md); coordinate
|
||||
shared fixture use before restarting containers. These dev flags do not establish
|
||||
production trust or secure-policy coverage.
|
||||
|
||||
`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. There is no HistoryRead leg: the driver has no history capability (#655).
|
||||
`OpcPlcFixture` probes `OPCUA_SIM_ENDPOINT` (default `opc.tcp://10.100.0.35:50000`)
|
||||
and skips when unavailable. With an available, reserved fixture:
|
||||
|
||||
### Unit
|
||||
```bash
|
||||
export OPCUA_SIM_ENDPOINT=opc.tcp://10.100.0.35:50000
|
||||
opc_test tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests.csproj
|
||||
```
|
||||
|
||||
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):
|
||||
A nonzero suite count does not prove every required scenario ran: inspect individual
|
||||
TRX results and record required skips separately. No tests were run by preparing
|
||||
this documentation.
|
||||
|
||||
- `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
|
||||
## Required compatibility evidence
|
||||
|
||||
Capability surfaces whose contract is verified: `IDriver`, `ITagDiscovery`,
|
||||
`IReadable`, `IWritable`, `ISubscribable`, `IHostConnectivityProbe`,
|
||||
`IAlarmSource`.
|
||||
Release validation must identify both application builds and SDK versions, endpoint
|
||||
policy, token kind, certificate chain, configuration and executed capabilities.
|
||||
Use the baseline client against the candidate server and candidate client against
|
||||
the baseline server, plus the actual ScadaBridge adapter and external upstreams.
|
||||
For secured paths include trusted SignAndEncrypt, API-key allow/deny/revocation,
|
||||
certificate rejection and restart/rebind. Apply only operations the peer implements;
|
||||
a simulator need not implement OtOpcUa's API-key or historian contract.
|
||||
|
||||
## What it does NOT cover
|
||||
For alarms verify typed EventFilter select clauses, result-field indexes, actual
|
||||
ConditionType/SourceNode/EventId identity, refresh, acknowledgements and no replay
|
||||
after an ambiguous transport failure. For all supported paths preserve existing
|
||||
operation statuses, locale behavior and null/empty filter meaning. Constructing an
|
||||
empty SDK 2 `ContentFilter` is an API adaptation, not evidence of a changed filter
|
||||
contract. Record any stricter protocol response and consumer impact before acceptance.
|
||||
|
||||
### 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 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`;
|
||||
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)
|
||||
opc-plc uses the OPC Foundation stack too. Independent upstream behavior, secured
|
||||
authentication variants, large publish queues, real alarm products, packet-loss
|
||||
windows and performance require separate evidence. The rollout runbook links the
|
||||
pending gates; no simulator run substitutes for ScadaBridge, live gateway, Windows,
|
||||
Linux or mixed-version cluster validation.
|
||||
|
||||
@@ -9,8 +9,9 @@ and for the test-harness map read [OpcUaClient-Test-Fixture.md](OpcUaClient-Test
|
||||
A **remote OPC UA server**. This driver runs the *opposite* direction from the
|
||||
usual "server exposes PLC data" flow: it acts as an OPC UA **client**, opens a
|
||||
`Session` against an upstream server, and re-exposes that server's address space
|
||||
through the local OtOpcUa server. Browse, read, write, subscribe, alarm, and
|
||||
history calls are passed through to the upstream endpoint.
|
||||
through the local OtOpcUa server. Browse, read, write, subscribe and alarm
|
||||
operations use the upstream endpoint. HistoryRead uses the local server-wide
|
||||
historian instead; see [History](#history-not-supported-gitea-655).
|
||||
|
||||
It is built on the OPC Foundation UA .NET Standard reference SDK and runs
|
||||
in-process in the OtOpcUa server's .NET 10 AnyCPU host — pure managed, no
|
||||
@@ -20,6 +21,31 @@ out-of-process isolation.
|
||||
> a remote OPC UA endpoint by hand, point the general-purpose
|
||||
> [Client CLI](../Client.CLI.md) at it directly.
|
||||
|
||||
## SDK 2.0.1 compatibility (#782)
|
||||
|
||||
The driver, shared session layer and AdminUI browser target exact SDK **2.0.1**
|
||||
(previous **1.5.378.182**). Their existing abstractions and host-owned reconnect
|
||||
lifecycle remain in place. SDK immutable values are detached at the boundary into
|
||||
existing CLR scalars and one-dimensional arrays, preserving null/empty ByteStrings,
|
||||
UTC timestamps, Guid values, numeric widths, item order and per-item status. Matrix
|
||||
payloads remain unsupported and fail per item; this upgrade adds no matrix or
|
||||
upstream-history capability.
|
||||
|
||||
Certificate-based identities retain their loaded certificate until their owning
|
||||
session closes, including failure cleanup. The browser releases abandoned browse
|
||||
continuation points. Endpoint policy selection, namespace-URI remapping, old-session
|
||||
notification rejection and the existing refusal to replay non-idempotent writes or
|
||||
alarm acknowledgements remain compatibility requirements.
|
||||
|
||||
No application status-code, locale-selection or event-filter semantic change is
|
||||
approved by this upgrade. SDK value wrappers and empty-filter construction are API
|
||||
adaptations; callers must continue to inspect operation and select-clause results
|
||||
by index. Confirm actual client/server combinations with the
|
||||
[fixture map](OpcUaClient-Test-Fixture.md) and
|
||||
[rollout gates](../plans/2026-11-10-opcua-sdk-2-rollout.md) before deployment.
|
||||
Historical SDK 1.5 observations elsewhere on this page describe their original
|
||||
measurements; they are not fresh SDK 2.0.1 interoperability evidence.
|
||||
|
||||
## Project split
|
||||
|
||||
| Project | Target | Role |
|
||||
@@ -365,7 +391,7 @@ They are not part of `DriverHealth` or the
|
||||
## Testing
|
||||
|
||||
- **Unit tests** — `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests/`
|
||||
cover the session lifecycle, namespace remapping, alarm/history passthrough,
|
||||
cover the session lifecycle, namespace remapping, alarm dispatch,
|
||||
and config binding against a faked SDK session.
|
||||
- **Integration fixture** — exercises the driver against a reference OPC UA
|
||||
server (opc-plc) on the shared docker host; see
|
||||
|
||||
Reference in New Issue
Block a user