Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Tests/Mapping/SampleMapperTests.cs
T
Joseph Doherty 78f604e0cd feat(historian): Gateway serves the per-sample aggregates from raw reads with the shared Sql.Common evaluator (#681)
Average, Count, Minimum, Maximum, Range, Start, End, Delta and Minimum/MaximumActualTime are evaluated
on the Gateway backend from forward ReadRaw reads with BucketPlan + AggregateEvaluator, so Gateway
matches SqlServer/QuestDB exactly (values, status bits, interval-start stamps, #677 reverse).

- Window read [start - 1 ms, end] so AVEVA's initial-value row never lands in a bucket; a pre-window
  row proves earlier data only with a non-Bad OPC quality (histsdk puts a value on every NULL row).
- Earlier data otherwise from a 2-row [1970, start) probe; later data from a 2-row [end, now+1d]
  look-ahead; both ignore rows stamped at their query start/end.
- No gateway stream is ever abandoned (a client-side cancel makes the gateway evict its shared
  historian session); past ServerHistorian:MaxTimeWeightedRawRows the bounded stream is drained, then
  BadTooManyOperations. The gateway's max_values-ceiling InvalidArgument maps to the same limit; a
  caller cancellation is not a backend fault.
- TimeAverage/Total stay AVEVA-native (no prior-sample read for exact Part 13 bounds), now stamped with
  the interval start (PENDING the live leg: histsdk's tail+43 start_time field is unverified) and
  carrying the Calculated bits.
- Served set: GatewayHistorianDataSource.ServedAggregates (12). Live AVEVA leg open (VPN).
2026-10-12 09:56:54 -04:00

67 lines
2.8 KiB
C#

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<double>(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);
}
[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<double>(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));
}