From 26c8b3c25b80cff068b1adad94b7ae0a94fb5a13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20Pazos?= Date: Wed, 17 Jun 2026 15:41:36 -0300 Subject: [PATCH] fix(holtWintersAberration): guard against out-of-range band index --- .../holtWintersAberration/function.go | 11 ++++-- .../holtWintersAberration/function_test.go | 39 +++++++++++++++++++ expr/holtwinters/hw_test.go | 19 +++++++++ 3 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 expr/holtwinters/hw_test.go diff --git a/expr/functions/holtWintersAberration/function.go b/expr/functions/holtWintersAberration/function.go index 2a6023187..b978e027c 100644 --- a/expr/functions/holtWintersAberration/function.go +++ b/expr/functions/holtWintersAberration/function.go @@ -77,13 +77,16 @@ func (f *holtWintersAberration) Do(ctx context.Context, eval interfaces.Evaluato lowerBand, upperBand := holtwinters.HoltWintersConfidenceBands(adjustedStartSeries[arg.Name].Values, stepTime, delta, bootstrapInterval, seasonality) for i, v := range arg.Values { - if math.IsNaN(v) { + // We may not have upper/lower band values if the bootstrap period is shorter than the requested series + hasBand := i < len(upperBand) && i < len(lowerBand) + switch { + case math.IsNaN(v): aberration = append(aberration, 0) - } else if !math.IsNaN(upperBand[i]) && v > upperBand[i] { + case hasBand && !math.IsNaN(upperBand[i]) && v > upperBand[i]: aberration = append(aberration, v-upperBand[i]) - } else if !math.IsNaN(lowerBand[i]) && v < lowerBand[i] { + case hasBand && !math.IsNaN(lowerBand[i]) && v < lowerBand[i]: aberration = append(aberration, v-lowerBand[i]) - } else { + default: aberration = append(aberration, 0) } } diff --git a/expr/functions/holtWintersAberration/function_test.go b/expr/functions/holtWintersAberration/function_test.go index 9fdfb8158..404e008ea 100644 --- a/expr/functions/holtWintersAberration/function_test.go +++ b/expr/functions/holtWintersAberration/function_test.go @@ -1,6 +1,7 @@ package holtWintersAberration import ( + "context" "testing" "github.com/go-graphite/carbonapi/expr/holtwinters" @@ -10,6 +11,7 @@ import ( "github.com/go-graphite/carbonapi/expr/types" "github.com/go-graphite/carbonapi/pkg/parser" th "github.com/go-graphite/carbonapi/tests" + "github.com/stretchr/testify/assert" ) var ( @@ -117,6 +119,43 @@ func TestHoltWintersAberration(t *testing.T) { } } +// TestHoltWintersAberrationShortBootstrap is a regression test to ensure +// no panics occur when the bootstrap period is shorter than the requested series. +func TestHoltWintersAberrationShortBootstrap(t *testing.T) { + var startTime int64 = 2678400 + var step int64 = 600 + var requestedPoints int64 = 10 + var bootstrapInterval int64 = 86400 // '1d' => windowPoints = 86400/600 = 144 + + // The bootstrap fetch returns only windowPoints+5 points, so after trimming + // the warm-up the bands have length 5, shorter than the 10 requested points. + bootstrapPoints := (bootstrapInterval / step) + 5 + + tt := th.EvalTestItemWithRange{ + Target: "holtWintersAberration(metric*,3,'1d','2d')", + M: map[parser.MetricRequest][]*types.MetricData{ + {Metric: "metric*", From: startTime, Until: startTime + step*requestedPoints}: { + types.MakeMetricData("metric1", generateHwRange(0, requestedPoints*step, step, 0), step, startTime), + }, + {Metric: "metric*", From: startTime - bootstrapInterval, Until: startTime + step*requestedPoints}: { + types.MakeMetricData("metric1", generateHwRange(0, bootstrapPoints*step, step, 0), step, startTime-bootstrapInterval), + }, + }, + From: startTime, + Until: startTime + step*requestedPoints, + } + + exp, _, err := parser.ParseExpr(tt.Target) + assert.NoError(t, err) + + eval := th.EvaluatorFromFunc(md[0].F) + got, err := eval.Eval(context.Background(), exp, tt.From, tt.Until, tt.M) + assert.NoError(t, err) + if assert.Len(t, got, 1) { + assert.Len(t, got[0].Values, int(requestedPoints)) + } +} + func generateHwRange(x, y, jump, t int64) []float64 { var valuesList []float64 for x < y { diff --git a/expr/holtwinters/hw_test.go b/expr/holtwinters/hw_test.go new file mode 100644 index 000000000..4189d1373 --- /dev/null +++ b/expr/holtwinters/hw_test.go @@ -0,0 +1,19 @@ +package holtwinters + +import ( + "math" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestHoltWintersAnalysisNaN(t *testing.T) { + predictions, deviations := HoltWintersAnalysis([]float64{math.NaN()}, 1, DefaultSeasonality) + + if assert.Len(t, predictions, 1) { + assert.True(t, math.IsNaN(predictions[0]), "expected NaN prediction, got %v", predictions[0]) + } + if assert.Len(t, deviations, 1) { + assert.Zero(t, deviations[0]) + } +}