From 7a3d2712c0aae00c6368589540be3adefb919c89 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 18 Jun 2026 06:12:51 -0400 Subject: [PATCH] test(opcuaclient): event-history smoke + docs(historian): driver event passthrough --- docs/Historian.md | 13 +++++++++ .../OpcUaClientSmokeTests.cs | 28 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/docs/Historian.md b/docs/Historian.md index be658b8a..d0a19663 100644 --- a/docs/Historian.md +++ b/docs/Historian.md @@ -113,6 +113,19 @@ Wonderware historian using that source. Event-field projection supports the stan `BaseEventType` select clauses — `EventId`, `SourceName`, `Time`, `ReceiveTime`, `Message`, and `Severity`; an unsupported select operand returns a null field (spec-conformant). +### OpcUaClient driver — upstream passthrough for all four variants + +The OpcUaClient driver's `IHistoryProvider` implementation forwards **all four** history-read +variants (Raw, Processed, AtTime, and Events) to its upstream OPC UA server. For the Events +variant it sends a fixed canonical `BaseEventType` `EventFilter` selecting the standard six +fields (`EventId`, `SourceName`, `Time`, `ReceiveTime`, `Message`, `Severity`) and maps the +upstream `HistoryEvent` onto `HistoricalEvent` — the same six-field projection the OtOpcUa +node-manager itself projects when serving event history. This is a **driver-level capability**: +the OpcUaClient driver acts as a passthrough to whatever historian the upstream server exposes, +and is independent of the single server-side `IHistorianDataSource` backend +(`WonderwareHistorianClient` / `NullHistorianDataSource`) that the OtOpcUa node-manager +dispatches HistoryRead to for tags on other drivers (Galaxy, Modbus, S7, etc.). + ### Graceful degradation | Situation | HistoryRead status | diff --git a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/OpcUaClientSmokeTests.cs b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/OpcUaClientSmokeTests.cs index 6635564d..601cd2a1 100644 --- a/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/OpcUaClientSmokeTests.cs +++ b/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests/OpcUaClientSmokeTests.cs @@ -92,4 +92,32 @@ public sealed class OpcUaClientSmokeTests(OpcPlcFixture sim) await drv.UnsubscribeAsync(handle, TestContext.Current.CancellationToken); } + + /// + /// 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. + /// + [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(); + } }