using Monitor.Backend.Analysis; namespace Monitor.Backend.Analysis.Tests; public sealed class AccumulatorTests { [Theory] [InlineData(0.5)] [InlineData(0.9)] [InlineData(0.95)] [InlineData(0.99)] public void Histogram_Quantile_WithinRelativeError(double q) { var rng = new Random(42); var values = new List(); var hist = new HistogramAcc(); for (int i = 0; i < 20_000; i++) { // 对数正态分布:中位数约 50 ms,长尾 double v = 50 * Math.Exp(rng.NextDouble() * 2.5 - 1.2); values.Add(v); hist.Add(v); } values.Sort(); double exact = values[(int)(q * (values.Count - 1))]; double estimate = hist.Quantile(q)!.Value; Assert.InRange(Math.Abs(estimate - exact) / exact, 0, 0.045); } [Fact] public void Histogram_Merge_EqualsSingleAccumulator() { var rng = new Random(7); var whole = new HistogramAcc(); var left = new HistogramAcc(); var right = new HistogramAcc(); for (int i = 0; i < 5000; i++) { double v = rng.NextDouble() * 200; whole.Add(v); (i % 2 == 0 ? left : right).Add(v); } left.Merge(right); Assert.Equal(whole.Count, left.Count); Assert.Equal(whole.Quantile(0.95), left.Quantile(0.95)); Assert.Equal(whole.Quantile(0.5), left.Quantile(0.5)); Assert.Equal(whole.Max, left.Max); Assert.Equal(whole.NonEmptyBuckets, left.NonEmptyBuckets); } [Fact] public void Histogram_Empty_HasNoQuantiles() { var hist = new HistogramAcc(); Assert.Null(hist.Quantile(0.5)); Assert.Null(hist.Mean); } [Fact] public void Histogram_SingleSample_QuantilesClampToSample() { var hist = new HistogramAcc(); hist.Add(42); Assert.Equal(42, hist.Quantile(0.01)); Assert.Equal(42, hist.Quantile(0.99)); } [Fact] public void Histogram_IgnoresNegativeAndNaN() { var hist = new HistogramAcc(); hist.Add(-1); hist.Add(double.NaN); Assert.Equal(0UL, hist.Count); } [Fact] public void Histogram_ValuesBeyondRange_LandInEdgeBuckets() { var hist = new HistogramAcc(); hist.Add(0.01); hist.Add(1_000_000); Assert.Equal(2UL, hist.Count); Assert.Equal(2, hist.NonEmptyBuckets); } [Fact] public void Ratio_Merge_SumsNumeratorAndDenominator() { var a = new RatioAcc(); a.Add(1, 10); var b = new RatioAcc(); b.Add(3, 10); a.Merge(b); Assert.Equal(20.0, a.Percent); Assert.Null(new RatioAcc().Percent); } [Fact] public void Merge_DifferentKinds_Throws() { Assert.Throws(() => new CounterAcc().Merge(new RatioAcc())); } [Fact] public void Codec_RoundTrip_PreservesEveryKind() { var set = new MetricSet(); set.GetOrAdd("c", AccKind.Counter).Add(5); set.GetOrAdd("r", AccKind.Ratio).Add(2, 9); set.GetOrAdd("s", AccKind.Sum).Add(2.5); set.GetOrAdd("n", AccKind.Min).Add(-3); set.GetOrAdd("x", AccKind.Max).Add(11); set.GetOrAdd("l", AccKind.RunLength).Add(4); var hist = (HistogramAcc)set.GetOrAdd("h", AccKind.Histogram); foreach (double v in new[] { 1.0, 20, 20, 300 }) { hist.Add(v); } set.GetOrAdd("t", AccKind.Series).Add(2, 7.5); MetricSet decoded = AccumulatorCodec.Decode(AccumulatorCodec.Encode(set)).Value; Assert.Equal(5UL, ((CounterAcc)decoded.Items["c"]).Value); Assert.Equal(9UL, ((RatioAcc)decoded.Items["r"]).Denominator); Assert.Equal(2.5, ((SumAcc)decoded.Items["s"]).Sum); Assert.Equal(-3, ((MinAcc)decoded.Items["n"]).Value); Assert.Equal(11, ((MaxAcc)decoded.Items["x"]).Value); Assert.Equal(4, ((RunLengthAcc)decoded.Items["l"]).Max); var h2 = (HistogramAcc)decoded.Items["h"]; Assert.Equal(hist.Count, h2.Count); Assert.Equal(hist.Quantile(0.5), h2.Quantile(0.5)); Assert.Equal(7.5, ((SeriesAcc)decoded.Items["t"]).Points[2]); } [Fact] public void Codec_Decode_RejectsTruncatedData() { var set = new MetricSet(); set.GetOrAdd("h", AccKind.Histogram).Add(10); byte[] bytes = AccumulatorCodec.Encode(set); Assert.False(AccumulatorCodec.Decode(bytes.AsSpan(0, bytes.Length - 3)).IsSuccess); Assert.False(AccumulatorCodec.Decode([200, 200]).IsSuccess); } [Fact] public void MetricSet_Merge_CombinesDisjointAndSharedMetrics() { var a = new MetricSet(); a.GetOrAdd("x", AccKind.Counter).Add(1); var b = new MetricSet(); b.GetOrAdd("x", AccKind.Counter).Add(2); b.GetOrAdd("y", AccKind.Counter).Add(4); a.Merge(b); Assert.Equal(3UL, ((CounterAcc)a.Items["x"]).Value); Assert.Equal(4UL, ((CounterAcc)a.Items["y"]).Value); Assert.Equal(2UL, ((CounterAcc)b.Items["x"]).Value); } }