0d249f3ee9
Review follow-up: on the Gateway every value is a Double (AVEVA stores a Boolean as a 0/1 Int2), so the raw-evaluated AtTime interpolated a Boolean node to 0.5. - IHistorianDataSource gains a default-interface overload ReadAtTimeAsync(fullReference, timestamps, TypeCode? valueType, ct) that forwards to the existing one (SqlServer/QuestDB/Null/fakes unchanged). - OtOpcUaNodeManager.HistoryReadAtTime passes the node's scalar DataType (AtTimeValueType: built-in type -> TypeCode; arrays/other types => null); Raw and UNS NodeIds alike. - GatewayAtTimeReader converts each raw row to the hinted kind BEFORE the shared AtTimeEvaluator runs (Boolean = value != 0 => stepped; integers => Int64 => interpolated, rounded half away from zero, returned as the node's integer type; floats Double), so the evaluator's own type branch decides. - AtTimeEvaluator boxed every interpolated Int64 as a Double (`cond ? (long)x : v` widens to double); its "keeping its type" test passed because Shouldly ShouldBe compares numbers across types. Fixed (affects SqlServer/QuestDB too) and pinned with ShouldBeOfType.
124 lines
4.9 KiB
C#
124 lines
4.9 KiB
C#
using Shouldly;
|
|
using Xunit;
|
|
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|
using static ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common.Tests.Fixture;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common.Tests;
|
|
|
|
/// <summary>Direct tests of the AtTime rules, with the literals of the SqlServer <c>AtTimeReadTests</c>.</summary>
|
|
public sealed class AtTimeEvaluatorTests
|
|
{
|
|
private static readonly DateTime Now = At(3600);
|
|
|
|
[Fact]
|
|
public void AtTime_inside_an_outage_returns_the_bad_neighbours_code_with_no_value()
|
|
{
|
|
var t0 = new DateTime(2026, 10, 9, 8, 0, 0, DateTimeKind.Utc);
|
|
var prev = new StoredSample(t0, HistorianValue.Empty, 0x80310000u);
|
|
var next = new StoredSample(t0.AddSeconds(10), HistorianValue.FromDouble(5), 0);
|
|
var r = AtTimeEvaluator.Evaluate(t0.AddSeconds(5), prev, next, t0, t0.AddHours(1));
|
|
r.StatusCode.ShouldBe(0x80310000u);
|
|
r.Value.ShouldBeNull();
|
|
}
|
|
|
|
[Fact]
|
|
public void A_bad_valued_previous_neighbour_keeps_its_code_and_drops_its_value()
|
|
{
|
|
var r = AtTimeEvaluator.Evaluate(At(25), A[2], A[3], Now, Now);
|
|
r.Value.ShouldBeNull(); // NOT 35 and NOT 30
|
|
r.StatusCode.ShouldBe(0x80000000u);
|
|
r.SourceTimestampUtc.ShouldBe(At(25));
|
|
}
|
|
|
|
[Fact]
|
|
public void Exact_hit_returns_the_stored_value_and_status_unchanged()
|
|
{
|
|
var r = AtTimeEvaluator.Evaluate(At(20), A[2], A[3], At(21), Now);
|
|
r.Value.ShouldBe(30d);
|
|
r.StatusCode.ShouldBe(0x80000000u);
|
|
r.ServerTimestampUtc.ShouldBe(At(21));
|
|
}
|
|
|
|
[Fact]
|
|
public void No_previous_sample_is_BadNoData_with_no_value()
|
|
{
|
|
var r = AtTimeEvaluator.Evaluate(At(0).AddTicks(-1), null, A[0], Now, Now);
|
|
r.Value.ShouldBeNull();
|
|
r.StatusCode.ShouldBe(0x809B0000u);
|
|
r.SourceTimestampUtc.ShouldBe(At(0).AddTicks(-1));
|
|
}
|
|
|
|
[Fact]
|
|
public void A_bad_next_neighbour_holds_the_previous_value_uncertain()
|
|
{
|
|
var r = AtTimeEvaluator.Evaluate(At(15), A[1], A[2], Now, Now);
|
|
r.Value.ShouldBe(20d); // NOT 25
|
|
r.StatusCode.ShouldBe(UncertainInterpolated);
|
|
}
|
|
|
|
[Fact]
|
|
public void Between_two_good_doubles_is_linearly_interpolated_good()
|
|
{
|
|
AtTimeEvaluator.Evaluate(At(5), A[0], A[1], Now, Now).Value.ShouldBe(15d);
|
|
AtTimeEvaluator.Evaluate(At(2.5), A[0], A[1], Now, Now).Value.ShouldBe(12.5d);
|
|
var r = AtTimeEvaluator.Evaluate(At(32.5), A[3], A[4], Now, Now);
|
|
r.Value.ShouldBe(42.5d);
|
|
r.StatusCode.ShouldBe(GoodInterpolated);
|
|
r.SourceTimestampUtc.ShouldBe(At(32.5));
|
|
}
|
|
|
|
[Fact]
|
|
public void An_uncertain_neighbour_makes_the_interpolation_uncertain()
|
|
{
|
|
var r = AtTimeEvaluator.Evaluate(At(5), S(0, HistorianValue.FromDouble(10), Uncertain),
|
|
S(10, HistorianValue.FromDouble(20), Good), Now, Now);
|
|
r.Value.ShouldBe(15d);
|
|
r.StatusCode.ShouldBe(UncertainInterpolated);
|
|
}
|
|
|
|
[Fact]
|
|
public void Int64_is_interpolated_and_rounded_away_from_zero_keeping_its_type()
|
|
{
|
|
var p = S(0, HistorianValue.FromInt64(10), Good);
|
|
var n = S(10, HistorianValue.FromInt64(15), Good);
|
|
AtTimeEvaluator.Evaluate(At(5), p, n, Now, Now).Value.ShouldBe(13L); // 12.5 ⇒ 13
|
|
AtTimeEvaluator.Evaluate(At(2), p, n, Now, Now).Value.ShouldBe(11L);
|
|
// #693: ShouldBe compares numbers across types, so the type needs its own assertion — a C# conditional
|
|
// `long : double` boxed a Double here, and the line above still passed.
|
|
AtTimeEvaluator.Evaluate(At(5), p, n, Now, Now).Value.ShouldBeOfType<long>();
|
|
AtTimeEvaluator.Evaluate(At(5), S(0, HistorianValue.FromDouble(10), Good), S(10, HistorianValue.FromDouble(15), Good), Now, Now)
|
|
.Value.ShouldBeOfType<double>();
|
|
}
|
|
|
|
[Fact]
|
|
public void After_the_last_sample_the_value_is_stepped_good_until_now_then_uncertain()
|
|
{
|
|
var now = At(42);
|
|
var held = AtTimeEvaluator.Evaluate(At(41), A[4], null, now, now);
|
|
held.Value.ShouldBe(50d);
|
|
held.StatusCode.ShouldBe(GoodInterpolated);
|
|
var future = AtTimeEvaluator.Evaluate(At(45), A[4], null, now, now);
|
|
future.Value.ShouldBe(50d);
|
|
future.StatusCode.ShouldBe(UncertainInterpolated);
|
|
}
|
|
|
|
[Fact]
|
|
public void A_non_numeric_kind_is_stepped_with_the_previous_status_plus_interpolated()
|
|
{
|
|
var p = S(0, HistorianValue.FromString("a"), Uncertain);
|
|
var n = S(10, HistorianValue.FromString("b"), Good);
|
|
var r = AtTimeEvaluator.Evaluate(At(5), p, n, Now, Now);
|
|
r.Value.ShouldBe("a");
|
|
r.StatusCode.ShouldBe(Uncertain | 0x402u);
|
|
}
|
|
|
|
[Fact]
|
|
public void An_uncertain_next_neighbour_makes_the_interpolation_uncertain()
|
|
{
|
|
var r = AtTimeEvaluator.Evaluate(At(5), S(0, HistorianValue.FromDouble(10), Good),
|
|
S(10, HistorianValue.FromDouble(20), Uncertain), Now, Now);
|
|
r.Value.ShouldBe(15d);
|
|
r.StatusCode.ShouldBe(UncertainInterpolated);
|
|
}
|
|
}
|