using Opc.Ua; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Tests; /// /// Sweep coverage for over the full /// 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 /// default — never silently returns /// NodeId.Null. /// /// /// /// Why this is unit-only. The OPC UA Part 13 aggregate NodeIds are well-known — /// the SDK exposes them as static readonly fields on Opc.Ua.ObjectIds. Round-trip /// testing against a live upstream is the integration suite's job (see /// OpcUaClientAggregateSweepTests); the wire path doesn't add anything to the /// enum-to-NodeId mapping itself. /// /// /// Cascading-quality rule. Aggregates the upstream server doesn't honour come /// back with BadAggregateNotSupported on the per-row HistoryRead result, not as /// a thrown exception — the driver's mapping is best-effort by design. /// /// [Trait("Category", "Unit")] public sealed class OpcUaClientAggregateMappingTests { /// /// Every declared value resolves to a non-null /// namespace-0 . Sweeps the full enum so a new value can't land /// without a switch arm — the default branch throws /// which the test would surface immediately. /// [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"); } /// /// 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. /// [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); } /// /// 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. /// [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); } /// /// Out-of-range enum values trip the default arm with /// . int.MaxValue is a future-proof /// sentinel that is guaranteed never to collide with a real enum ordinal. /// [Fact] public void MapAggregateToNodeId_rejects_out_of_range_enum_value() { Should.Throw(() => OpcUaClientDriver.MapAggregateToNodeId((HistoryAggregateType)int.MaxValue)); } /// /// The enum sweep is the source of truth — every value declared in /// participates in /// . /// Adding a new enum value automatically adds a new test row. /// public static TheoryData AllHistoryAggregateTypes() { var data = new TheoryData(); foreach (var v in Enum.GetValues()) { data.Add(v); } return data; } /// /// Reflect the requested static field off . 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. /// 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!; } }