fix(kpi): per-metric bucket reduction — Rate series sum per bucket instead of keeping one delta (plan R2-04 T5)
This commit is contained in:
@@ -369,4 +369,97 @@ public class KpiSeriesBucketerTests
|
||||
Assert.Equal(4.0, result[0].Value);
|
||||
Assert.Equal(9.0, result[1].Value);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Per-metric reduction: Rate series sum per bucket (arch-review 04 round 2, R3)
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
[Fact]
|
||||
public void Bucket_RateSeries_SumsPerBucket_InsteadOfLastValue()
|
||||
{
|
||||
// 6 points of value 10 across a 60-min / 2-bucket window → each bucket = 30 (sum),
|
||||
// not 10 (last value). Three deltas per bucket; last-value would keep 1 of 3.
|
||||
var raw = new[]
|
||||
{
|
||||
new KpiSeriesPoint(T(5), 10.0), // bucket 0: [0, 30)
|
||||
new KpiSeriesPoint(T(15), 10.0), // bucket 0
|
||||
new KpiSeriesPoint(T(25), 10.0), // bucket 0
|
||||
new KpiSeriesPoint(T(35), 10.0), // bucket 1: [30, 60]
|
||||
new KpiSeriesPoint(T(45), 10.0), // bucket 1
|
||||
new KpiSeriesPoint(T(55), 10.0), // bucket 1
|
||||
};
|
||||
|
||||
var result = KpiSeriesBucketer.Bucket(
|
||||
raw, T(0), T(60), maxPoints: 2, aggregation: KpiRollupAggregation.Rate);
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Equal(30.0, result[0].Value);
|
||||
Assert.Equal(30.0, result[1].Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bucket_RateSeries_ShortSeries_StillSumsClusteredPoints()
|
||||
{
|
||||
// 3 points (< maxPoints 5) with two sharing a bucket → the shared bucket is their SUM.
|
||||
// Rate series skip the raw.Count <= maxPoints early return so totals stay truthful.
|
||||
// 60-min / 5-bucket window → 12 min each. T(2),T(5) → bucket 0 [0,12); T(30) → bucket 2 [24,36).
|
||||
var raw = new[]
|
||||
{
|
||||
new KpiSeriesPoint(T(2), 10.0),
|
||||
new KpiSeriesPoint(T(5), 10.0),
|
||||
new KpiSeriesPoint(T(30), 10.0),
|
||||
};
|
||||
|
||||
var result = KpiSeriesBucketer.Bucket(
|
||||
raw, T(0), T(60), maxPoints: 5, aggregation: KpiRollupAggregation.Rate);
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Equal(20.0, result[0].Value); // T(2)+T(5) summed
|
||||
Assert.Equal(10.0, result[1].Value); // T(30) alone
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bucket_RateSeries_PreservesSeriesTotal()
|
||||
{
|
||||
// Strong invariant: sum(output values) == sum(in-window input values) for Rate.
|
||||
var raw = Enumerable
|
||||
.Range(0, 20)
|
||||
.Select(i => new KpiSeriesPoint(T(i * 3), (double)(i + 1)))
|
||||
.ToArray();
|
||||
|
||||
var result = KpiSeriesBucketer.Bucket(
|
||||
raw, T(0), T(60), maxPoints: 4, aggregation: KpiRollupAggregation.Rate);
|
||||
|
||||
var inWindowTotal = raw
|
||||
.Where(p => p.BucketStartUtc >= T(0) && p.BucketStartUtc <= T(60))
|
||||
.Sum(p => p.Value);
|
||||
Assert.Equal(inWindowTotal, result.Sum(p => p.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bucket_DefaultAggregation_IsGauge_AndBehaviorUnchanged()
|
||||
{
|
||||
// Calling the existing 4-arg shape produces identical output to the explicit Gauge call.
|
||||
var raw = new[]
|
||||
{
|
||||
new KpiSeriesPoint(T(5), 1.0),
|
||||
new KpiSeriesPoint(T(15), 2.0),
|
||||
new KpiSeriesPoint(T(25), 99.0),
|
||||
new KpiSeriesPoint(T(35), 5.0),
|
||||
};
|
||||
|
||||
var fourArg = KpiSeriesBucketer.Bucket(raw, T(0), T(60), maxPoints: 2);
|
||||
var explicitGauge = KpiSeriesBucketer.Bucket(
|
||||
raw, T(0), T(60), maxPoints: 2, aggregation: KpiRollupAggregation.Gauge);
|
||||
|
||||
Assert.Equal(fourArg.Count, explicitGauge.Count);
|
||||
for (int i = 0; i < fourArg.Count; i++)
|
||||
{
|
||||
Assert.Equal(fourArg[i].BucketStartUtc, explicitGauge[i].BucketStartUtc);
|
||||
Assert.Equal(fourArg[i].Value, explicitGauge[i].Value);
|
||||
}
|
||||
// And the last-value semantics are unchanged.
|
||||
Assert.Equal(99.0, fourArg[0].Value);
|
||||
Assert.Equal(5.0, fourArg[1].Value);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user