using Google.Protobuf.WellKnownTypes; using Xunit; using ZB.MOM.WW.HistorianGateway.Contracts.Grpc; using ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Mapping; namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests.Mapping; public sealed class SampleMapperTests { [Fact] public void Numeric_sample_maps_value_quality_and_timestamps() { var s = new HistorianSample { Tag = "T", NumericValue = 12.5, Quality = 192, OpcQuality = 192, Timestamp = Ts(2026, 1, 1, 0, 0, 0) }; var snap = SampleMapper.ToSnapshot(s); Assert.Equal(12.5, Assert.IsType(snap.Value)); Assert.Equal(0x00000000u, snap.StatusCode); Assert.Equal(DateTimeKind.Utc, snap.SourceTimestampUtc!.Value.Kind); } [Fact] public void String_sample_carries_string_value() { var s = new HistorianSample { Tag = "T", StringValue = "abc", OpcQuality = 192, Timestamp = Ts(2026, 1, 1, 0, 0, 0) }; Assert.Equal("abc", SampleMapper.ToSnapshot(s).Value); } [Fact] public void Bad_quality_sample_maps_to_bad_status() { var s = new HistorianSample { Tag = "T", NumericValue = 1.0, OpcQuality = 0, Timestamp = Ts(2026, 1, 1, 0, 0, 0) }; Assert.Equal(0x80000000u, SampleMapper.ToSnapshot(s).StatusCode); } [Theory] // #690: histsdk puts a value on every row, so a NULL row arrives as 0 / "" with a Bad quality [InlineData(0u, 1u)] // the live NULL row: opc_quality 0, AVEVA quality 1 [InlineData(24u, 0u)] // any Bad OPC quality (out of service) public void A_Bad_numeric_row_carries_no_value(uint opc, uint quality) { var s = new HistorianSample { Tag = "T", NumericValue = 0, OpcQuality = opc, Quality = quality, Timestamp = Ts(2026, 1, 1, 0, 0, 0) }; var snap = SampleMapper.ToSnapshot(s); Assert.True(snap.StatusCode >= 0x80000000u); Assert.Null(snap.Value); } [Fact] // #690: a string tag's NULL row arrives as "" + Bad public void A_Bad_string_row_carries_no_value() { var s = new HistorianSample { Tag = "T", StringValue = "", OpcQuality = 0, Quality = 1, Timestamp = Ts(2026, 1, 1, 0, 0, 0) }; Assert.Null(SampleMapper.ToSnapshot(s).Value); } [Fact] // #690: an Uncertain row keeps its value (only Bad means "no value") public void An_Uncertain_row_keeps_its_value() { var s = new HistorianSample { Tag = "T", NumericValue = 3, OpcQuality = 64, Timestamp = Ts(2026, 1, 1, 0, 0, 0) }; Assert.Equal(3d, SampleMapper.ToSnapshot(s).Value); } [Fact] public void Aggregate_null_value_is_BadNoData() { var a = new HistorianAggregateSample { Tag = "T", /* Value unset, no Good quality */ EndTime = Ts(2026, 1, 1, 0, 0, 0) }; var snap = SampleMapper.ToAggregateSnapshot(a); Assert.Equal(0x809B0401u, snap.StatusCode); // BadNoData + Calculated (#681) Assert.Null(snap.Value); } [Fact] public void Aggregate_good_bucket_carries_value() { var a = new HistorianAggregateSample { Tag = "T", Value = 42.0, OpcQuality = 192, EndTime = Ts(2026, 1, 1, 0, 0, 0) }; var snap = SampleMapper.ToAggregateSnapshot(a); Assert.Equal(0x00000401u, snap.StatusCode); // Good + Calculated (#681) Assert.Equal(42.0, Assert.IsType(snap.Value)); } [Fact] // #681: Part 13 stamps a processed value with its interval START (it used to be the end) public void Aggregate_bucket_is_stamped_with_its_interval_start() { var a = new HistorianAggregateSample { Tag = "T", Value = 1.0, OpcQuality = 192, StartTime = Ts(2026, 1, 1, 0, 0, 0), EndTime = Ts(2026, 1, 1, 0, 1, 0), }; Assert.Equal(new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc), SampleMapper.ToAggregateSnapshot(a).SourceTimestampUtc); } // Ts(...) builds a Google.Protobuf.WellKnownTypes.Timestamp from UTC parts. private static Timestamp Ts(int y, int mo, int d, int h, int mi, int s) => Timestamp.FromDateTime(new DateTime(y, mo, d, h, mi, s, DateTimeKind.Utc)); }