test(historian): shared ProcessedConformance asserts DurationGood/DurationBad on every SQL backend (#840)
#820 served DurationGood/DurationBad (ms) on SqlServer/QuestDB but only QualityMixConformance asserted them. The shared Processed suite now covers them like the percentages: hand values over tag A (20000/10000/20000/BadNoData and 0/10000/0) with statuses + timestamps and Duration = Percent x interval; the SDK-calculator parity theory (forward/reverse/truncated); outage markers; the only-Bad interval; string tags; and the time-weighted row cap. Live: SqlServer.IntegrationTests 211/211 (central SQL), QuestDb.IntegrationTests 249 passed / 1 skipped (shared /opt/otopcua-questdb fixture). Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -127,6 +127,27 @@ public abstract class ProcessedConformance : ConformanceBase
|
||||
s.Select(x => x.StatusCode).ShouldBe([GoodCalc, GoodCalc, GoodCalc | 0x4u, NoDataCalc]);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(HistoryAggregateType.DurationGood, 20_000d, 10_000d, 20_000d)]
|
||||
[InlineData(HistoryAggregateType.DurationBad, 0d, 10_000d, 0d)]
|
||||
public async Task Duration_aggregates_are_the_region_times_in_milliseconds(HistoryAggregateType agg, double b0, double b1,
|
||||
double b2)
|
||||
{
|
||||
// #820/#840 (Part 13 §5.4.3.31/32): the same stepped regions as the percentages above, as an OPC UA Duration (ms) —
|
||||
// B1: [20,30) Bad = 10 000 ms, [30,40) Good = 10 000 ms; B2 runs from the last sample (+40) to the interval end,
|
||||
// Partial; B3 is after the end of the data ⇒ BadNoData. Same statuses as PercentGood/PercentBad.
|
||||
await ConformanceData.SeedAAsync(B);
|
||||
var s = (await ReadA(agg)).Samples;
|
||||
s.Select(x => x.Value).ShouldBe([b0, b1, b2, null]);
|
||||
s.Select(x => x.StatusCode).ShouldBe([GoodCalc, GoodCalc, GoodCalc | Partial, NoDataCalc]);
|
||||
s.Select(x => x.SourceTimestampUtc).ShouldBe([B.Round(At(0)), B.Round(At(20)), B.Round(At(40)), B.Round(At(60))]);
|
||||
// Duration = Percent × interval ÷ 100, interval by interval (one oracle for both families).
|
||||
var percent = (await ReadA(agg == HistoryAggregateType.DurationGood
|
||||
? HistoryAggregateType.PercentGood : HistoryAggregateType.PercentBad)).Samples;
|
||||
for (var i = 0; i < 3; i++)
|
||||
((double)s[i].Value!).ShouldBe((double)percent[i].Value! * Twenty.TotalMilliseconds / 100, 1e-9, $"bucket {i}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Actual_time_aggregates_stamp_the_sample_time()
|
||||
{
|
||||
@@ -415,6 +436,7 @@ public abstract class ProcessedConformance : ConformanceBase
|
||||
HistoryAggregateType.Range, HistoryAggregateType.Start, HistoryAggregateType.End,
|
||||
HistoryAggregateType.Delta, HistoryAggregateType.MinimumActualTime, HistoryAggregateType.MaximumActualTime,
|
||||
HistoryAggregateType.PercentGood, HistoryAggregateType.PercentBad,
|
||||
HistoryAggregateType.DurationGood, HistoryAggregateType.DurationBad, // #820/#840
|
||||
})
|
||||
{
|
||||
var ours = (await src.ReadProcessedAsync("P", At(from), At(to), interval, agg, Ct)).Samples.ToList();
|
||||
@@ -481,6 +503,9 @@ public abstract class ProcessedConformance : ConformanceBase
|
||||
(await One(HistoryAggregateType.Start)).Value.ShouldBe(10d);
|
||||
// #680: duration-based — [0,5) Good, [5,10) Bad, [10,20) Good (no end bound: the last region runs to the interval end).
|
||||
((double)(await One(HistoryAggregateType.PercentBad)).Value!).ShouldBe(25d, 1e-9);
|
||||
// #840: the same regions in ms — [5,10) Bad = 5 000, [0,5) + [10,20) Good = 15 000.
|
||||
((double)(await One(HistoryAggregateType.DurationBad)).Value!).ShouldBe(5_000d, 1e-9);
|
||||
((double)(await One(HistoryAggregateType.DurationGood)).Value!).ShouldBe(15_000d, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -507,6 +532,11 @@ public abstract class ProcessedConformance : ConformanceBase
|
||||
pb.Value.ShouldBe(100d);
|
||||
// The tag's data starts (+1) inside the interval ⇒ Partial on the non-Bad percentage (#677).
|
||||
pb.StatusCode.ShouldBe(GoodCalc | 0x4u);
|
||||
// #840: DurationBad is the same statement in ms — the whole 20 s interval — with the same status.
|
||||
var db = await One(HistoryAggregateType.DurationBad);
|
||||
db.Value.ShouldBe(20_000d);
|
||||
db.StatusCode.ShouldBe(GoodCalc | 0x4u);
|
||||
(await One(HistoryAggregateType.DurationGood)).Value.ShouldBe(0d);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -662,6 +692,7 @@ public abstract class ProcessedConformance : ConformanceBase
|
||||
(await One(HistoryAggregateType.Start)).Value.ShouldBe("a");
|
||||
(await One(HistoryAggregateType.End)).Value.ShouldBe("b");
|
||||
(await One(HistoryAggregateType.PercentGood)).Value.ShouldBe(100d);
|
||||
(await One(HistoryAggregateType.DurationGood)).Value.ShouldBe(20_000d); // #840: quality, not value — strings too
|
||||
foreach (var a in new[]
|
||||
{
|
||||
HistoryAggregateType.Average, HistoryAggregateType.Minimum, HistoryAggregateType.Maximum,
|
||||
@@ -845,6 +876,11 @@ public abstract class ProcessedConformance : ConformanceBase
|
||||
// #680: PercentGood/PercentBad are duration-based, served by the same window stream ⇒ capped too.
|
||||
(await Should.ThrowAsync<HistoryReadLimitExceededException>(
|
||||
() => under.ReadProcessedAsync("A", At(0), At(80), Twenty, HistoryAggregateType.PercentGood, Ct))).Limit.ShouldBe(4);
|
||||
// #820/#840: DurationGood/DurationBad ride the same window stream ⇒ capped too.
|
||||
(await Should.ThrowAsync<HistoryReadLimitExceededException>(
|
||||
() => under.ReadProcessedAsync("A", At(0), At(80), Twenty, HistoryAggregateType.DurationGood, Ct))).Limit.ShouldBe(4);
|
||||
(await Should.ThrowAsync<HistoryReadLimitExceededException>(
|
||||
() => under.ReadProcessedAsync("A", At(0), At(80), Twenty, HistoryAggregateType.DurationBad, Ct))).Limit.ShouldBe(4);
|
||||
// Pushed-down (per-sample) aggregates are not capped; the bound rows outside the window do not count.
|
||||
(await under.ReadProcessedAsync("A", At(0), At(80), Twenty, HistoryAggregateType.Average, Ct)).Samples.Count.ShouldBe(4);
|
||||
(await B.CreateDataSource(3).ReadProcessedAsync("W", At(0), At(80), Twenty, HistoryAggregateType.TimeAverage, Ct))
|
||||
|
||||
Reference in New Issue
Block a user