116 lines
5.6 KiB
C#
116 lines
5.6 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.IntegrationTests;
|
|
|
|
/// <summary>
|
|
/// Sweep coverage for the full <see cref="HistoryAggregateType"/> catalog over a real
|
|
/// <c>opc-plc</c> upstream. Loops every enum value, calls <c>ReadProcessedAsync</c> 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
|
|
/// <c>BadAggregateNotSupported</c> on the per-row HistoryRead result rather than a
|
|
/// thrown exception).
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <b>Build-only scaffold for now.</b> opc-plc's default profile doesn't enable
|
|
/// history simulation on the well-known nodes — <c>ns=3;s=StepUp</c> isn't
|
|
/// historized out of the box. This test therefore <see cref="Assert.Skip(string)"/>
|
|
/// until the fixture image is upgraded to one of the opc-plc history-sim profiles
|
|
/// (e.g. <c>--useslowtypes</c> + <c>--ut=10</c>) AND a known-good historized
|
|
/// NodeId is wired into <see cref="OpcPlcProfile"/>.
|
|
/// </para>
|
|
/// <para>
|
|
/// <b>Why it sweeps every enum.</b> The unit-test sweep
|
|
/// (<c>OpcUaClientAggregateMappingTests</c>) 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
|
|
/// <c>BadAggregateNotSupported</c>; we count and log them rather than failing,
|
|
/// since server-side support is a runtime capability advertisement, not a
|
|
/// driver-side bug.
|
|
/// </para>
|
|
/// </remarks>
|
|
[Collection(OpcPlcCollection.Name)]
|
|
[Trait("Category", "Integration")]
|
|
[Trait("Simulator", "opc-plc")]
|
|
public sealed class OpcUaClientAggregateSweepTests(OpcPlcFixture sim)
|
|
{
|
|
/// <summary>
|
|
/// Iterates the entire <see cref="HistoryAggregateType"/> 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 <c>BadAggregateNotSupported</c> on the data rows rather than failing
|
|
/// the test.
|
|
/// </summary>
|
|
[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<HistoryAggregateType>();
|
|
var supported = new List<HistoryAggregateType>();
|
|
|
|
foreach (var aggregate in Enum.GetValues<HistoryAggregateType>())
|
|
{
|
|
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<HistoryAggregateType>().Length} returned BadAggregateNotSupported. " +
|
|
$"Unsupported set: {string.Join(", ", unsupported)}");
|
|
#pragma warning restore CS0162
|
|
}
|
|
}
|