ed05d50795
The frozen 1.5.378.182 StatusAggregateCalculator counts Uncertain regions as Good under TreatUncertainAsBad=false, so live AggregateFilter PercentGood / DurationGood disagreed with HistoryRead (Part 13 §5.4.3.31/§5.4.3.33, Mantis 0006166) whenever Uncertain data was present. Owner-approved 2026-11-11. - New app-owned GoodOnlyStatusAggregateCalculator subclasses the baseline and replaces only the PercentGood/DurationGood computation (Good regions only); CompatibilityAggregateFactory routes exactly those two IDs to it. The copied SDK files are untouched; the other 35 aggregates are byte-identical. - Golden test: the 64 changed vectors (38 PercentGood + 26 DurationGood) are exempted exactly — those whose baseline output differs from the same case's baseline TreatUncertainAsBad=true twin — and pinned to that twin (baseline evidence, not regenerated from the implementation); plus a full Good-only pin over all 352 PercentGood/DurationGood vectors and a hand-computed case. - Live-vs-history parity over Good/Uncertain/Bad mixes (no masks), with a discrimination check against the pre-#809 baseline; the booted-runtime facts test now expects 8000 ms / 40 %. - SdkAggregateOracle: Run/RunLive = the registered factory; RunSdkAsIs = the untouched baseline; its Part13GoodCalculator duplicate is removed. - README (recorded decision), ServerAggregateConfiguration remarks, docs/Historian.md and .claude/rules/historian.md updated. Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
58 lines
3.3 KiB
C#
58 lines
3.3 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. The #821 Total / TimeAverage
|
|
/// facts pin current behaviour for the owner decision; they do not choose. #809 is decided (2026-11-11): live
|
|
/// PercentGood / DurationGood count Good time only, equal to HistoryRead. 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) is not Good — only [12,20) is, live as in HistoryRead (8 000 ms / 40 %; before #809
|
|
// the live calculator counted the Uncertain time too: 15 000 ms / 75 %).
|
|
One(ObjectIds.AggregateFunction_DurationGood).ShouldBe(8_000d, 1e-9);
|
|
One(ObjectIds.AggregateFunction_PercentGood).ShouldBe(40d, 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);
|
|
}
|
|
}
|