fix(historian): #681 on #682/#684 — Gateway served set is the 12-member ServedAggregates; raw buckets use Good-only values
- #682: ServedAggregates (ten raw-evaluated per-sample + TimeAverage/Total) replaces the placeholder AggregateModeMapper.Served; NativeAggregates is derived from the mapper's switch. The Gateway expectations in AggregateModeMapperTests, HistorianBackendRegistryTests and AdvertisedAggregateFunctionsWireTests move to the 12-member set (the wire tests' 'not served by Gateway' example is now TimeAverage2). - #684: RawBucketAccumulator applies the SQL backends' Good-only value columns (Avg/Min/Max/Sum and the ActualTime extreme over Good numeric samples), UncertainMin/UncertainMax over Uncertain numeric samples, and GoodNonNumeric. The parity oracle carries the #684 InMemoryRows and the DenseUncertain / UncertainBeyond / UncertainText datasets.
This commit is contained in:
+35
-9
@@ -78,10 +78,29 @@ public sealed class GatewayRawAggregateTests
|
||||
/// <summary>Equal extremes in one bucket (ActualTime ties ⇒ earliest).</summary>
|
||||
private static readonly StoredSample[] Ties = [D(1, 5), D(3, 9), D(4, 1), D(6, 9), D(8, 1), D(25, 2)];
|
||||
|
||||
/// <summary>#684 (Sql.Common's DenseUncertain): Dense with every 5th sample an in-range Uncertain 16.</summary>
|
||||
private static readonly StoredSample[] DenseUncertain = Enumerable.Range(0, 45)
|
||||
.Select(i => (t: -10 + i * 2.5, i))
|
||||
.Select(x => x.i % 5 == 0 && x.t != 22.5
|
||||
? D(x.t, 16, Uncertain)
|
||||
: D(x.t, 10 + (x.i * 7 % 13), x.t == 22.5 ? Bad : Good))
|
||||
.ToArray();
|
||||
|
||||
/// <summary>#684 (Sql.Common's UncertainBeyond): Uncertain values below and above the Good extremes in one interval.</summary>
|
||||
private static readonly StoredSample[] UncertainBeyond = [D(-5, 20), D(2, 10), D(4, 5, Uncertain), D(6, 99, Uncertain), D(8, 30), D(25, 20)];
|
||||
|
||||
/// <summary>#684: an Uncertain string among Good numbers (does not refuse the Good-value aggregates) and a Good one.</summary>
|
||||
private static readonly StoredSample[] UncertainText =
|
||||
[
|
||||
D(-5, 1), D(2, 4), S(4, HistorianValue.FromString("u"), Uncertain), D(6, 8), S(14, HistorianValue.FromString("g"), Good),
|
||||
D(16, 3),
|
||||
];
|
||||
|
||||
private static readonly (string Name, StoredSample[] Data)[] Datasets =
|
||||
[
|
||||
("A", A), ("Dense", Dense), ("Gappy", Gappy), ("Bracketed", Bracketed), ("EarlyEnd", EarlyEnd),
|
||||
("EndOnEdge", EndOnEdge), ("LateStart", LateStart), ("OutageEnds", OutageEnds), ("Mixed", Mixed), ("Ties", Ties),
|
||||
("DenseUncertain", DenseUncertain), ("UncertainBeyond", UncertainBeyond), ("UncertainText", UncertainText),
|
||||
];
|
||||
|
||||
private static readonly (double From, double To, double Interval)[] Windows =
|
||||
@@ -174,7 +193,7 @@ public sealed class GatewayRawAggregateTests
|
||||
earliest, latest, Now);
|
||||
}
|
||||
|
||||
/// <summary>Independent LINQ statement of the per-bucket statistics (copied from Sql.Common.Tests).</summary>
|
||||
/// <summary>Independent LINQ statement of the per-bucket statistics (copied from Sql.Common.Tests, #684 semantics).</summary>
|
||||
private static IEnumerable<BucketRow> InMemoryRows(BucketPlan plan, IEnumerable<StoredSample> samples, bool maximum)
|
||||
{
|
||||
foreach (var g in samples.Select(s => (s, b: plan.IndexOf(s.TimeUtc))).Where(x => x.b is not null)
|
||||
@@ -184,7 +203,9 @@ public sealed class GatewayRawAggregateTests
|
||||
var usable = all.Where(s => s.IsUsable).ToList();
|
||||
var numeric = usable.Where(s => s.Value.Kind is HistorianValueKind.Double or HistorianValueKind.Int64
|
||||
or HistorianValueKind.Boolean).ToList();
|
||||
var extreme = numeric.OrderBy(s => maximum ? -s.Value.Number : s.Value.Number).ThenBy(s => s.TimeUtc)
|
||||
var good = numeric.Where(s => s.IsGood).ToList();
|
||||
var uncertain = numeric.Where(s => !s.IsGood).ToList();
|
||||
var extreme = good.OrderBy(s => maximum ? -s.Value.Number : s.Value.Number).ThenBy(s => s.TimeUtc)
|
||||
.Cast<StoredSample?>().FirstOrDefault();
|
||||
yield return new BucketRow
|
||||
{
|
||||
@@ -194,10 +215,13 @@ public sealed class GatewayRawAggregateTests
|
||||
Good = all.Count(s => s.IsGood),
|
||||
Bad = all.Count - usable.Count,
|
||||
NonNumeric = usable.Count - numeric.Count,
|
||||
Avg = numeric.Count == 0 ? null : numeric.Average(s => s.Value.Number),
|
||||
Min = numeric.Count == 0 ? null : numeric.Min(s => s.Value.Number),
|
||||
Max = numeric.Count == 0 ? null : numeric.Max(s => s.Value.Number),
|
||||
Sum = numeric.Count == 0 ? null : numeric.Sum(s => s.Value.Number),
|
||||
GoodNonNumeric = usable.Count(s => s.IsGood) - numeric.Count(s => s.IsGood),
|
||||
Avg = good.Count == 0 ? null : good.Average(s => s.Value.Number),
|
||||
Min = good.Count == 0 ? null : good.Min(s => s.Value.Number),
|
||||
Max = good.Count == 0 ? null : good.Max(s => s.Value.Number),
|
||||
Sum = good.Count == 0 ? null : good.Sum(s => s.Value.Number),
|
||||
UncertainMin = uncertain.Count == 0 ? null : uncertain.Min(s => s.Value.Number),
|
||||
UncertainMax = uncertain.Count == 0 ? null : uncertain.Max(s => s.Value.Number),
|
||||
First = usable.Count == 0 ? HistorianValue.Empty : usable[0].Value,
|
||||
Last = usable.Count == 0 ? HistorianValue.Empty : usable[^1].Value,
|
||||
Extreme = extreme?.Value.Number,
|
||||
@@ -525,16 +549,18 @@ public sealed class GatewayRawAggregateTests
|
||||
foreach (var maximum in new[] { false, true })
|
||||
{
|
||||
var acc = new RawBucketAccumulator(maximum);
|
||||
foreach (var s in Dense.Reverse())
|
||||
foreach (var s in DenseUncertain.Reverse())
|
||||
if (plan.IndexOf(s.TimeUtc) is { } b) acc.Add(b, s);
|
||||
var expected = InMemoryRows(plan, Dense, maximum).ToList();
|
||||
var expected = InMemoryRows(plan, DenseUncertain, maximum).ToList();
|
||||
var actual = acc.Rows().ToList();
|
||||
actual.Count.ShouldBe(expected.Count);
|
||||
foreach (var (a, e) in actual.Zip(expected))
|
||||
{
|
||||
(a.Bucket, a.Total, a.Usable, a.Good, a.Bad, a.NonNumeric).ShouldBe((e.Bucket, e.Total, e.Usable, e.Good, e.Bad, e.NonNumeric));
|
||||
(a.Min, a.Max, a.Extreme, a.ExtremeTime).ShouldBe((e.Min, e.Max, e.Extreme, e.ExtremeTime));
|
||||
a.Avg!.Value.ShouldBe(e.Avg!.Value, 1e-9);
|
||||
(a.UncertainMin, a.UncertainMax, a.GoodNonNumeric).ShouldBe((e.UncertainMin, e.UncertainMax, e.GoodNonNumeric));
|
||||
(a.Avg is null).ShouldBe(e.Avg is null);
|
||||
if (a.Avg is { } avg) avg.ShouldBe(e.Avg!.Value, 1e-9);
|
||||
(a.First, a.Last, a.FirstAny, a.LastAny).ShouldBe((e.First, e.Last, e.FirstAny, e.LastAny));
|
||||
}
|
||||
}
|
||||
|
||||
+5
-5
@@ -109,16 +109,16 @@ public sealed class AdvertisedAggregateFunctionsWireTests
|
||||
|
||||
rig.Nm.HistorianDataSource = Backend("SqlServer");
|
||||
(await AdvertisedAsync(session, ObjectIds.HistoryServerCapabilities_AggregateFunctions, ct)).ShouldBe(SqlNames);
|
||||
(await ParentsAsync(session, ObjectIds.AggregateFunction_Average, ct)).ShouldBe(
|
||||
(await ParentsAsync(session, ObjectIds.AggregateFunction_TimeAverage2, ct)).ShouldBe(
|
||||
[ObjectIds.Server_ServerCapabilities_AggregateFunctions, ObjectIds.HistoryServerCapabilities_AggregateFunctions],
|
||||
ignoreOrder: true);
|
||||
|
||||
// Gateway has no arithmetic Average: dropped from the history folder (inverse reference too), kept in the
|
||||
// server folder.
|
||||
// Gateway has no TimeAverage2 (#681: no prior-sample read for Part 13 bounds): dropped from the history folder
|
||||
// (inverse reference too), kept in the server folder.
|
||||
rig.Nm.HistorianDataSource = Backend("Gateway");
|
||||
(await AdvertisedAsync(session, ObjectIds.HistoryServerCapabilities_AggregateFunctions, ct)).ShouldBe(GatewayNames);
|
||||
(await AdvertisedAsync(session, ObjectIds.Server_ServerCapabilities_AggregateFunctions, ct)).ShouldBe(AllSdkNames);
|
||||
(await ParentsAsync(session, ObjectIds.AggregateFunction_Average, ct)).ShouldBe(
|
||||
(await ParentsAsync(session, ObjectIds.AggregateFunction_TimeAverage2, ct)).ShouldBe(
|
||||
[ObjectIds.Server_ServerCapabilities_AggregateFunctions]);
|
||||
|
||||
// And back: removed references are restored.
|
||||
@@ -130,7 +130,7 @@ public sealed class AdvertisedAggregateFunctionsWireTests
|
||||
|
||||
[Theory]
|
||||
[InlineData("StandardDeviationSample")] // served by no historian backend, never in the history folder
|
||||
[InlineData("Average")] // not served by the Gateway backend wired below
|
||||
[InlineData("TimeAverage2")] // not served by the Gateway backend wired below (#681)
|
||||
public async Task A_live_AggregateFilter_subscription_for_an_unadvertised_aggregate_still_works(string aggregateName)
|
||||
{
|
||||
// The trim touches only the advertised folders, not the AggregateManager's calculator factories, which is what
|
||||
|
||||
Reference in New Issue
Block a user