157 lines
8.5 KiB
C#
157 lines
8.5 KiB
C#
using Opc.Ua;
|
|
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests;
|
|
|
|
/// <summary>
|
|
/// Sweep coverage for <see cref="OpcUaClientDriver.MapAggregateToNodeId"/> over the full
|
|
/// <see cref="HistoryAggregateType"/> catalog. PR-13 (issue #285) extended the enum from 5
|
|
/// to ~30 values matching OPC UA Part 13 §5; these tests guard the mapping table so a
|
|
/// future addition either gets a switch arm or trips the
|
|
/// <see cref="ArgumentOutOfRangeException"/> default — never silently returns
|
|
/// <c>NodeId.Null</c>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para>
|
|
/// <b>Why this is unit-only.</b> The OPC UA Part 13 aggregate NodeIds are well-known —
|
|
/// the SDK exposes them as static readonly fields on <c>Opc.Ua.ObjectIds</c>. Round-trip
|
|
/// testing against a live upstream is the integration suite's job (see
|
|
/// <c>OpcUaClientAggregateSweepTests</c>); the wire path doesn't add anything to the
|
|
/// enum-to-NodeId mapping itself.
|
|
/// </para>
|
|
/// <para>
|
|
/// <b>Cascading-quality rule.</b> Aggregates the upstream server doesn't honour come
|
|
/// back with <c>BadAggregateNotSupported</c> on the per-row HistoryRead result, not as
|
|
/// a thrown exception — the driver's mapping is best-effort by design.
|
|
/// </para>
|
|
/// </remarks>
|
|
[Trait("Category", "Unit")]
|
|
public sealed class OpcUaClientAggregateMappingTests
|
|
{
|
|
/// <summary>
|
|
/// Every declared <see cref="HistoryAggregateType"/> value resolves to a non-null
|
|
/// namespace-0 <see cref="NodeId"/>. Sweeps the full enum so a new value can't land
|
|
/// without a switch arm — the default branch throws
|
|
/// <see cref="ArgumentOutOfRangeException"/> which the test would surface immediately.
|
|
/// </summary>
|
|
[Theory]
|
|
[MemberData(nameof(AllHistoryAggregateTypes))]
|
|
public void MapAggregateToNodeId_resolves_every_enum_value_to_a_namespace0_NodeId(HistoryAggregateType aggregate)
|
|
{
|
|
var nodeId = OpcUaClientDriver.MapAggregateToNodeId(aggregate);
|
|
|
|
NodeId.IsNull(nodeId).ShouldBeFalse(
|
|
$"HistoryAggregateType.{aggregate} must map to a Part 13 AggregateFunction_* NodeId");
|
|
nodeId.NamespaceIndex.ShouldBe((ushort)0,
|
|
$"HistoryAggregateType.{aggregate} maps to a standard NodeId — namespace must be 0");
|
|
nodeId.IdType.ShouldBe(IdType.Numeric,
|
|
$"HistoryAggregateType.{aggregate} maps to a numeric Part 13 NodeId");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Regression cover for the original 5 ordinals — Average/Minimum/Maximum/Total/Count
|
|
/// stay pinned to their existing SDK NodeIds. Guards against an accidental swap when
|
|
/// the switch table grew to ~30 arms.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData(HistoryAggregateType.Average, "AggregateFunction_Average")]
|
|
[InlineData(HistoryAggregateType.Minimum, "AggregateFunction_Minimum")]
|
|
[InlineData(HistoryAggregateType.Maximum, "AggregateFunction_Maximum")]
|
|
[InlineData(HistoryAggregateType.Total, "AggregateFunction_Total")]
|
|
[InlineData(HistoryAggregateType.Count, "AggregateFunction_Count")]
|
|
public void MapAggregateToNodeId_original_five_aggregates_stay_pinned_to_their_SDK_NodeIds(
|
|
HistoryAggregateType aggregate, string expectedSdkFieldName)
|
|
{
|
|
var nodeId = OpcUaClientDriver.MapAggregateToNodeId(aggregate);
|
|
var expected = GetSdkAggregateNodeId(expectedSdkFieldName);
|
|
|
|
nodeId.ShouldBe(expected);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The new Part 13 aggregates added in PR-13 each resolve to the SDK constant whose
|
|
/// name matches the enum value. Guards against a transposition bug on any of the 25
|
|
/// new arms.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData(HistoryAggregateType.TimeAverage, "AggregateFunction_TimeAverage")]
|
|
[InlineData(HistoryAggregateType.TimeAverage2, "AggregateFunction_TimeAverage2")]
|
|
[InlineData(HistoryAggregateType.Interpolative, "AggregateFunction_Interpolative")]
|
|
[InlineData(HistoryAggregateType.MinimumActualTime, "AggregateFunction_MinimumActualTime")]
|
|
[InlineData(HistoryAggregateType.MaximumActualTime, "AggregateFunction_MaximumActualTime")]
|
|
[InlineData(HistoryAggregateType.Range, "AggregateFunction_Range")]
|
|
[InlineData(HistoryAggregateType.Range2, "AggregateFunction_Range2")]
|
|
[InlineData(HistoryAggregateType.AnnotationCount, "AggregateFunction_AnnotationCount")]
|
|
[InlineData(HistoryAggregateType.DurationGood, "AggregateFunction_DurationGood")]
|
|
[InlineData(HistoryAggregateType.DurationBad, "AggregateFunction_DurationBad")]
|
|
[InlineData(HistoryAggregateType.PercentGood, "AggregateFunction_PercentGood")]
|
|
[InlineData(HistoryAggregateType.PercentBad, "AggregateFunction_PercentBad")]
|
|
[InlineData(HistoryAggregateType.WorstQuality, "AggregateFunction_WorstQuality")]
|
|
[InlineData(HistoryAggregateType.WorstQuality2, "AggregateFunction_WorstQuality2")]
|
|
[InlineData(HistoryAggregateType.StandardDeviationSample, "AggregateFunction_StandardDeviationSample")]
|
|
[InlineData(HistoryAggregateType.StandardDeviationPopulation, "AggregateFunction_StandardDeviationPopulation")]
|
|
[InlineData(HistoryAggregateType.VarianceSample, "AggregateFunction_VarianceSample")]
|
|
[InlineData(HistoryAggregateType.VariancePopulation, "AggregateFunction_VariancePopulation")]
|
|
[InlineData(HistoryAggregateType.NumberOfTransitions, "AggregateFunction_NumberOfTransitions")]
|
|
[InlineData(HistoryAggregateType.DurationInStateZero, "AggregateFunction_DurationInStateZero")]
|
|
[InlineData(HistoryAggregateType.DurationInStateNonZero, "AggregateFunction_DurationInStateNonZero")]
|
|
[InlineData(HistoryAggregateType.Start, "AggregateFunction_Start")]
|
|
[InlineData(HistoryAggregateType.End, "AggregateFunction_End")]
|
|
[InlineData(HistoryAggregateType.Delta, "AggregateFunction_Delta")]
|
|
[InlineData(HistoryAggregateType.StartBound, "AggregateFunction_StartBound")]
|
|
[InlineData(HistoryAggregateType.EndBound, "AggregateFunction_EndBound")]
|
|
public void MapAggregateToNodeId_new_Part13_aggregates_resolve_to_matching_SDK_NodeIds(
|
|
HistoryAggregateType aggregate, string expectedSdkFieldName)
|
|
{
|
|
var nodeId = OpcUaClientDriver.MapAggregateToNodeId(aggregate);
|
|
var expected = GetSdkAggregateNodeId(expectedSdkFieldName);
|
|
|
|
nodeId.ShouldBe(expected);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Out-of-range enum values trip the default arm with
|
|
/// <see cref="ArgumentOutOfRangeException"/>. <c>int.MaxValue</c> is a future-proof
|
|
/// sentinel that is guaranteed never to collide with a real enum ordinal.
|
|
/// </summary>
|
|
[Fact]
|
|
public void MapAggregateToNodeId_rejects_out_of_range_enum_value()
|
|
{
|
|
Should.Throw<ArgumentOutOfRangeException>(() =>
|
|
OpcUaClientDriver.MapAggregateToNodeId((HistoryAggregateType)int.MaxValue));
|
|
}
|
|
|
|
/// <summary>
|
|
/// The enum sweep is the source of truth — every value declared in
|
|
/// <see cref="HistoryAggregateType"/> participates in
|
|
/// <see cref="MapAggregateToNodeId_resolves_every_enum_value_to_a_namespace0_NodeId"/>.
|
|
/// Adding a new enum value automatically adds a new test row.
|
|
/// </summary>
|
|
public static TheoryData<HistoryAggregateType> AllHistoryAggregateTypes()
|
|
{
|
|
var data = new TheoryData<HistoryAggregateType>();
|
|
foreach (var v in Enum.GetValues<HistoryAggregateType>())
|
|
{
|
|
data.Add(v);
|
|
}
|
|
return data;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reflect the requested static field off <see cref="ObjectIds"/>. Used so the test
|
|
/// table stays declarative — adding a new aggregate is one [InlineData] row plus an
|
|
/// enum value, with no SDK-version-specific casting.
|
|
/// </summary>
|
|
private static NodeId GetSdkAggregateNodeId(string fieldName)
|
|
{
|
|
var field = typeof(ObjectIds).GetField(fieldName)
|
|
?? throw new InvalidOperationException(
|
|
$"OPC UA SDK does not expose ObjectIds.{fieldName}. " +
|
|
"If the SDK was upgraded and the field was renamed, update the mapping table.");
|
|
var value = field.GetValue(null);
|
|
return (NodeId)value!;
|
|
}
|
|
}
|