using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests; /// /// Sweep coverage for the full catalog over a real /// opc-plc upstream. Loops every enum value, calls ReadProcessedAsync with a /// 1-second processing interval, and asserts the wire path doesn't crash even when the /// simulator declines to honour a particular aggregate (it returns /// BadAggregateNotSupported on the per-row HistoryRead result rather than a /// thrown exception). /// /// /// /// Build-only scaffold for now. opc-plc's default profile doesn't enable /// history simulation on the well-known nodes — ns=3;s=StepUp isn't /// historized out of the box. This test therefore /// until the fixture image is upgraded to one of the opc-plc history-sim profiles /// (e.g. --useslowtypes + --ut=10) AND a known-good historized /// NodeId is wired into . /// /// /// Why it sweeps every enum. The unit-test sweep /// (OpcUaClientAggregateMappingTests) covers the enum-to-NodeId mapping. /// This integration test catches any wire-side regression where the SDK rejects /// a NodeId we thought was well-known — e.g. a future SDK version retires a /// constant. Aggregates the simulator doesn't honour come back as /// BadAggregateNotSupported; we count and log them rather than failing, /// since server-side support is a runtime capability advertisement, not a /// driver-side bug. /// /// [Collection(OpcPlcCollection.Name)] [Trait("Category", "Integration")] [Trait("Simulator", "opc-plc")] public sealed class OpcUaClientAggregateSweepTests(OpcPlcFixture sim) { /// /// Iterates the entire enum against opc-plc. /// Each call must return a result object (possibly with empty samples and/or a /// bad-status row inside) without throwing; aggregates the simulator declines are /// surfaced as BadAggregateNotSupported on the data rows rather than failing /// the test. /// [Fact] public async Task ReadProcessedAsync_sweeps_every_HistoryAggregateType_without_crashing() { if (sim.SkipReason is not null) Assert.Skip(sim.SkipReason); Assert.Skip( "opc-plc default profile does not enable HistoryRead on well-known nodes. " + "Re-enable when OpcPlcFixture is upgraded to a history-sim profile and a known " + "historized NodeId is added to OpcPlcProfile (e.g. --useslowtypes --ut=10)."); #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-aggregate-sweep"); await drv.InitializeAsync("{}", TestContext.Current.CancellationToken); var end = DateTime.UtcNow; var start = end.AddMinutes(-1); var interval = TimeSpan.FromSeconds(1); // Placeholder NodeId — swap to OpcPlcProfile.HistorizedNode once the fixture is upgraded. const string historizedNode = OpcPlcProfile.StepUp; var unsupported = new List(); var supported = new List(); foreach (var aggregate in Enum.GetValues()) { HistoryReadResult? result = null; try { result = await drv.ReadProcessedAsync( historizedNode, start, end, interval, aggregate, TestContext.Current.CancellationToken); } catch (Exception ex) { Assert.Fail( $"ReadProcessedAsync({aggregate}) threw {ex.GetType().Name}: {ex.Message}. " + "Wire path should never throw — unsupported aggregates surface as BadAggregateNotSupported."); } result.ShouldNotBeNull(); // BadAggregateNotSupported = 0x80330000 per OPC UA Part 4 status codes. Detect by // inspecting the per-row StatusCode — opc-plc returns the bad code on the (single) // sample row when it can't honour the aggregate. const uint BadAggregateNotSupported = 0x80330000; const uint BadHistoryOperationUnsupported = 0x80710000; var anyBadAggregate = result.Samples.Any(s => s.StatusCode == BadAggregateNotSupported || s.StatusCode == BadHistoryOperationUnsupported); if (anyBadAggregate || result.Samples.Count == 0) { unsupported.Add(aggregate); } else { supported.Add(aggregate); } } // Sanity: at least one aggregate should round-trip cleanly. If none do, the upstream // is wholly history-disabled and the fixture-upgrade gate above didn't kick in. supported.Count.ShouldBeGreaterThan(0, "at least one Part 13 aggregate should round-trip against opc-plc; " + $"all {Enum.GetValues().Length} returned BadAggregateNotSupported. " + $"Unsupported set: {string.Join(", ", unsupported)}"); #pragma warning restore CS0162 } }