Files
lmxopcua/tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/LiveAggregateLateStartFactsTests.cs
T
Joseph Doherty e7b485c0a2 feat(historian): serve DurationGood/DurationBad on the SQL backends; pin the #821 late-start facts (#820, #821)
#820: SqlServer + QuestDB now serve Part 13 DurationGood / DurationBad from the
same Good / Bad region totals TimeWeightedAggregator already computes for
PercentGood / PercentBad — reported in milliseconds (OPC UA Duration, the SDK's
unit), Uncertain time in neither (Mantis 0006166), same BadNoData / status /
Partial rules. AggregateFamilies.Served grows 17 -> 19, so i=11201 and the
up-front refusal follow automatically; Gateway still refuses both (explicit
served set, unchanged). Client.Shared AggregateType / mapper / CLI gain both.
The #796 oracle answers DurationGood through the Good-only subclass as well.
QualityMix conformance asserts both directly (Q and QP, hand values, ms, and
= PercentGood x interval), replaces the "refused up front" test, and adds
Duration* to the forward/reverse/truncated oracle comparisons.

#821 (facts only, no decision): SdkAggregateParityTests pins an Uncertain-first
late-start interval (QP over [0,20), both directions) against ours (Part 13
Total 163.96, TimeAverage 10.93), the frozen 1.5.378.182 calculators (188.96,
9.448), SDK 2.0.1's stock calculators (188.96, 9.448) and a Good-first control
(all 163.96); LiveAggregateLateStartFactsTests pins the same through the booted
server's runtime AggregateManager (live AggregateFilter) incl. DurationGood
15000 / PercentGood 75 there vs 8000 / 40 in HistoryRead (#809). QualityMix
adds the reverse late-start variant (reverse TW pin + reverse oracle windows).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-11-11 01:28:54 -05:00

56 lines
3.0 KiB
C#

using Opc.Ua;
using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests;
/// <summary>
/// #821 / #809 facts, through the BOOTED server's runtime <c>AggregateManager</c> — the calculators a live
/// AggregateFilter monitored item gets, under the server's own default configuration. This pins current behaviour for
/// the owner decision; it does not choose. The HistoryRead (Part 13) answers and SDK 2.0.1's stock answers for the
/// same input are pinned beside it in <c>SdkAggregateParityTests</c>.
/// </summary>
public sealed class LiveAggregateLateStartFactsTests
{
private static readonly DateTime T0 = new(2026, 11, 11, 8, 0, 0, DateTimeKind.Utc);
/// <summary>The #810 QualityMix tag QP: Uncertain 10 at +5, Good 11 at +12, Uncertain 12 at +25, nothing after.</summary>
private static readonly (double Seconds, double Value, uint Status)[] Qp =
[
(5, 10, 0x40000000u), (12, 11, 0x00000000u), (25, 12, 0x40000000u), // Uncertain, Good, Uncertain
];
[Fact]
public async Task Live_AggregateFilter_calculators_on_an_Uncertain_first_late_start_interval()
{
await using var rig = await ServerEventRig.StartAsync("AggregateLateStart", encrypted: false,
TestContext.Current.CancellationToken);
var manager = rig.Server.CurrentInstance.AggregateManager;
var config = manager.GetDefaultConfiguration(NodeId.Null);
config.TreatUncertainAsBad.ShouldBeFalse("the server pins TreatUncertainAsBad = false (#782)");
double One(NodeId aggregate)
{
var calc = manager.CreateCalculator(aggregate, T0, T0.AddSeconds(20), 20_000, false, config);
calc.ShouldNotBeNull();
foreach (var (s, v, st) in Qp)
calc.QueueRawValue(new DataValue(Variant.From(v), st, T0.AddSeconds(s), T0.AddSeconds(s)));
calc.TryGetProcessedValue(true, out var dv).ShouldBeTrue();
dv.WrappedValue.TryGetValue(out double d).ShouldBeTrue();
return d;
}
// Part 13 (HistoryRead on SqlServer/QuestDB): Total = 73.5 + 88 + 32/13 = 163.96…, TimeAverage = Total ÷ 15 s.
// The live calculators integrate the uncovered [0,5) as a ramp from 0 (+25) and divide by the whole 20 s.
const double part13Total = 73.5 + 88 + 32d / 13;
One(ObjectIds.AggregateFunction_Total).ShouldBe(part13Total + 25, 1e-9); // 188.96…
One(ObjectIds.AggregateFunction_TimeAverage).ShouldBe((part13Total + 25) / 20, 1e-9); // 9.448… (Part 13: 10.93…)
// #809: the Uncertain [5,12) counts as Good live (HistoryRead: 8 000 ms / 40 %).
One(ObjectIds.AggregateFunction_DurationGood).ShouldBe(15_000d, 1e-9);
One(ObjectIds.AggregateFunction_PercentGood).ShouldBe(75d, 1e-9);
// DurationBad / PercentBad agree with HistoryRead: the 5 s before the data is Bad.
One(ObjectIds.AggregateFunction_DurationBad).ShouldBe(5_000d, 1e-9);
One(ObjectIds.AggregateFunction_PercentBad).ShouldBe(25d, 1e-9);
}
}