Auto: opcuaclient-12 — IHistoryProvider.ReadEventsAsync EventFilter spec + impl

Adds a filter-aware overload of IHistoryProvider.ReadEventsAsync that carries
EventFilter SelectClauses + WhereClause, and implements it on the OPC UA
Client driver via Session.HistoryReadAsync + ReadEventDetails.

The change is additive (default-impl returns NotSupportedException) so the
existing Galaxy.Proxy.GalaxyProxyDriver implementation keeps compiling
against the fixed-field overload — no cross-driver refactor required.

* Core.Abstractions: new EventHistoryRequest / SimpleAttributeSpec /
  ContentFilterSpec records mirror the OPC UA wire shape transport-neutrally.
  HistoricalEventBatch / HistoricalEventRow carry an open-ended Fields bag
  keyed by SimpleAttributeSpec.FieldName so server-side dispatch can re-align
  with the client's wire-side SelectClause order.
* OpcUaClient driver: new ReadEventsAsync(fullReference, EventHistoryRequest, ct)
  builds an EventFilter, calls Session.HistoryReadAsync, and unwraps
  HistoryEvent.Events into HistoricalEventBatch rows. Default SelectClause
  set matches BuildHistoryEvent on the server side. ContentFilter bytes are
  decoded through the live session's MessageContext (passthrough — the
  driver does not evaluate filters).
* Unit tests: 7 new tests cover SelectClause translation, default-clause
  fallback, malformed where-clause swallowing, uninitialized-driver guard,
  null-request guard, and IHistoryProvider default fallback.
* Integration scaffold: build-only [Fact] gated on opc-plc --alm; flips to
  green when the fixture image is upgraded.
* Docs: HistoryRead Events section in docs/drivers/OpcUaClient.md plus a
  cross-link from Client.CLI.md historyread page.
* E2E: -HistoryEvents switch on scripts/e2e/test-opcuaclient.ps1 confirms
  the gateway round-trips HistoryReadEvents without
  BadHistoryOperationUnsupported (gated; defaults to skip).

Closes #284
This commit is contained in:
Joseph Doherty
2026-04-26 09:29:40 -04:00
parent 2ee61c0999
commit c36903d6a0
7 changed files with 657 additions and 10 deletions

View File

@@ -0,0 +1,64 @@
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> simulator launched in alarm mode
/// (<c>--alm</c>). Exercises the filter-aware
/// <see cref="OpcUaClientDriver.ReadEventsAsync(string, EventHistoryRequest, System.Threading.CancellationToken)"/>
/// overload by issuing a HistoryReadEvents against the simulator's <c>Server</c> notifier
/// and asserting at least one historical event row comes back with the SelectClause
/// fields populated.
/// </summary>
/// <remarks>
/// Requires the simulator started with <c>--alm</c> (alarm + history simulation), which
/// <see cref="OpcPlcFixture"/> does not guarantee — the test skips with a clear reason
/// when the upstream returns <c>BadHistoryOperationUnsupported</c> instead of a HistoryEvent
/// payload. PR-12 ships the build-only scaffold; the green-test pass lands when the
/// fixture image is upgraded to the alarm SKU.
/// </remarks>
[Collection(OpcPlcCollection.Name)]
[Trait("Category", "Integration")]
[Trait("Simulator", "opc-plc")]
public sealed class OpcUaClientHistoryEventsTests(OpcPlcFixture sim)
{
[Fact]
public async Task ReadEventsAsync_against_opc_plc_alarm_mode_returns_BaseEventType_fields()
{
if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason);
// Build-only scaffold: the test wires up the call but skips before assertions until
// the opc-plc fixture is launched with --alm. When the fixture image carries alarms
// the call should round-trip through Session.HistoryReadAsync + ReadEventDetails and
// produce at least one HistoricalEventRow with the default SelectClause keys
// populated (EventId / SourceName / Time / Message / Severity / ReceiveTime).
Assert.Skip(
"opc-plc --alm mode not guaranteed by the default fixture image. " +
"Re-enable when OpcPlcFixture is upgraded to launch with --alm and a known-good " +
"alarm event source path.");
#pragma warning disable CS0162 // unreachable scaffold below — kept for the post-fixture-upgrade flip
var options = OpcPlcProfile.BuildOptions(sim.EndpointUrl);
await using var drv = new OpcUaClientDriver(options, driverInstanceId: "opcua-events-smoke");
await drv.InitializeAsync("{}", TestContext.Current.CancellationToken);
var request = new EventHistoryRequest(
StartTime: DateTime.UtcNow.AddMinutes(-30),
EndTime: DateTime.UtcNow.AddMinutes(1),
NumValuesPerNode: 100,
SelectClauses: null,
WhereClause: null);
// The Server node (i=2253) is the standard history-events notifier on opc-plc.
var batch = await drv.ReadEventsAsync("i=2253", request, TestContext.Current.CancellationToken);
batch.ShouldNotBeNull();
batch.Events.Count.ShouldBeGreaterThan(0, "opc-plc --alm raises events at least every 5s");
var first = batch.Events[0];
first.Fields.ShouldContainKey("EventId");
first.Fields.ShouldContainKey("Severity");
#pragma warning restore CS0162
}
}