Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/OpcUaClientSmokeTests.cs
T
Joseph Doherty 4624141a5d test+docs: retire the stale EquipmentTagsDarkBatch4 skips, guard the deploy-time
tag gate, and fix three silently-broken OpcUaClient integration tests

Continues working through deferment.md's remaining items.

SKIPPED TESTS (13 -> 0 for this reason). The skip said "dark until Batch 4";
Batch 4 shipped as v3.0 and RETIRED the equipment-tag path rather than lighting
it, so every one of them was waiting on something that had already happened and
gone the other way. Resolved individually rather than in bulk:
  - REVIVED onto the raw/UNS shape (6): four DriverHostActor primary-gate cases,
    the cluster-scoping rebuild, and the real-SDK dual-namespace materialisation
    E2E. The behaviour never went dark, only the v2 seeding did.
  - FOLDED into a live parity test (3): DeploymentArtifactRawUnsParityTests
    already compared RawTagPlan record-equal and RawTagPlan carries array /
    historize / alarm intent, so widening its corpus by two tags covers what
    three empty placeholder suites had been promising. Those suites are deleted.
  - DELETED (4): subjects genuinely retired -- equipment-namespace EquipmentTags,
    and both dot-joint {{equip}}.X token suites (that resolver was deleted in
    Batch 3; the slash-joint form is covered by DeploymentArtifactEquipRefParityTests).
Falsified, not assumed: removing the seeded UnsTagReference turns two revived
primary-gate tests red; re-homing the SITE-A node to MAIN turns the scoping test
red. Widening a parity corpus also needed direct value assertions -- parity alone
cannot distinguish "both seams right" from "both seams blind".
Runtime.Tests skips 13->3, OpcUaServer.Tests 4->1; all remaining are deliberate
EquipmentDeviceBindingRetired tombstones.

G-7 (deploy-time TagConfig gate). MQTT shipped an Inspect() nobody ever called;
now wired. Replaced the hand-maintained list with a reflection guard, because the
existing test enumerates driver types by hand and so guards renames but can never
notice a gap. NOTE: the first version of that guard was hollow -- it discovered
candidates via GetReferencedAssemblies(), which is circular, since the compiler
elides a reference nothing in the IL uses. Removing MQTT from the map also removed
its Contracts assembly from the manifest, and the guard passed green with the bug
reintroduced. Rewritten to scan the output directory; re-falsified and it now
fails naming MqttTagDefinitionFactory. Residual recorded at the code: Sql,
MTConnect, Calculation and OpcUaClient have no Contracts-side Inspect at all, so
the deploy gate stays weaker than the AdminUI's authoring gate for them.

OPCUACLIENT INTEGRATION TESTS (3 of 4 failing, on master too). Not fixture rot:
the simulator was healthy and Client.CLI read the very same node Good. v3 made the
driver resolve every reference through its authored RawTags table, so a bare
"ns=3;s=StepUp" fails LOCALLY at the resolver with BadNodeIdInvalid and never
reaches the wire -- the tests were exercising the unresolved-reference guard.
Fixed by seeding OpcPlcProfile.RawTags in the deploy artifact's TagConfig shape
and addressing by RawPath. 4/4 pass.

DOCS. Applied the Phase7* -> AddressSpace* rename across the four live docs
(historical bannered files deliberately untouched), resolving the three that are
not renames to their real members. Found en route that the earlier banner pass
missed three files: VirtualTags.md, OpcUaServer.md and ScriptedAlarms.md all
still presented the test-scaffolding GenericDriverNodeManager as the production
dispatch path. All three now carry the correction.

Claude-Session: https://claude.ai/code/session_015p7wGqy3YpZNCpDzTpGMKo
2026-07-28 14:32:13 -04:00

124 lines
5.5 KiB
C#

using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests;
/// <summary>
/// End-to-end smoke against a live <c>opc-plc</c> (task #215). Drives the real
/// OPC UA Secure Channel + Session + MonitoredItem exchange — no mocks. Every
/// test here proves a capability surface that loopback against our own server
/// couldn't exercise cleanly: real cert negotiation, real endpoint descriptions,
/// real simulated nodes that change without a write.
/// </summary>
[Collection(OpcPlcCollection.Name)]
[Trait("Category", "Integration")]
[Trait("Simulator", "opc-plc")]
public sealed class OpcUaClientSmokeTests(OpcPlcFixture sim)
{
/// <summary>Verifies that the client can connect and read a node through the real OPC UA stack.</summary>
[Fact]
public async Task Client_connects_and_reads_StepUp_node_through_real_OPC_UA_stack()
{
if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);
var options = OpcPlcProfile.BuildOptions(sim.EndpointUrl);
await using var drv = new OpcUaClientDriver(options, driverInstanceId: "opcua-smoke-read");
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
var snapshots = await drv.ReadAsync(
[OpcPlcProfile.StepUpRef], TestContext.Current.CancellationToken);
snapshots.Count.ShouldBe(1);
snapshots[0].StatusCode.ShouldBe(0u, "opc-plc StepUp read must succeed end-to-end");
snapshots[0].Value.ShouldNotBeNull("StepUp always has a current value");
}
/// <summary>Verifies that the client can read a batch of varied types from the simulator.</summary>
[Fact]
public async Task Client_reads_batch_of_varied_types_from_live_simulator()
{
if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);
var options = OpcPlcProfile.BuildOptions(sim.EndpointUrl);
await using var drv = new OpcUaClientDriver(options, driverInstanceId: "opcua-smoke-batch");
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
var snapshots = await drv.ReadAsync(
[OpcPlcProfile.StepUpRef, OpcPlcProfile.RandomSignedInt32Ref, OpcPlcProfile.AlternatingBooleanRef],
TestContext.Current.CancellationToken);
snapshots.Count.ShouldBe(3);
foreach (var s in snapshots)
{
s.StatusCode.ShouldBe(0u);
s.Value.ShouldNotBeNull();
}
// AlternatingBoolean should decode as a bool specifically — catches a common
// attribute-mapping regression where the driver stringifies variant values.
snapshots[2].Value.ShouldBeOfType<bool>();
}
/// <summary>Verifies that the client can subscribe to data changes from the live server.</summary>
[Fact]
public async Task Client_subscribe_receives_StepUp_data_changes_from_live_server()
{
if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);
var options = OpcPlcProfile.BuildOptions(sim.EndpointUrl);
await using var drv = new OpcUaClientDriver(options, driverInstanceId: "opcua-smoke-sub");
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
var observed = new List<DataChangeEventArgs>();
var gate = new SemaphoreSlim(0);
drv.OnDataChange += (_, e) =>
{
lock (observed) observed.Add(e);
gate.Release();
};
var handle = await drv.SubscribeAsync(
[OpcPlcProfile.FastUInt1Ref], TimeSpan.FromMilliseconds(250),
TestContext.Current.CancellationToken);
// FastUInt1 ticks every 100 ms — one publishing interval (250 ms) should deliver.
// Wait up to 3 s to tolerate container warm-up + first-publish delay.
var got = await gate.WaitAsync(TimeSpan.FromSeconds(3), TestContext.Current.CancellationToken);
got.ShouldBeTrue("opc-plc FastUInt1 must publish at least one data change within 3s");
int observedCount;
lock (observed) observedCount = observed.Count;
observedCount.ShouldBeGreaterThan(0);
await drv.UnsubscribeAsync(handle, TestContext.Current.CancellationToken);
}
/// <summary>
/// Verifies HistoryReadEvents passthrough issues a well-formed request and returns a
/// result without throwing. opc-plc exposes live alarm conditions (--alm) but is NOT a
/// historian, so the upstream may return zero historical events or reject the service —
/// either way the driver must produce a HistoricalEventsResult, never throw. This proves
/// the wire request + unwrap path; a non-empty event list is infra-gated on an upstream
/// that historizes events.
/// </summary>
[Fact]
public async Task Client_reads_events_returns_result_without_throwing()
{
if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);
var options = OpcPlcProfile.BuildOptions(sim.EndpointUrl);
await using var drv = new OpcUaClientDriver(options, driverInstanceId: "opcua-smoke-events");
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
var result = await drv.ReadEventsAsync(
sourceName: null, // null → upstream Server object (i=2253)
startUtc: DateTime.UtcNow.AddHours(-1),
endUtc: DateTime.UtcNow,
maxEvents: 100,
cancellationToken: TestContext.Current.CancellationToken);
result.ShouldNotBeNull();
result.Events.ShouldNotBeNull();
}
}