From e9d6135e337e65c4517b4807e4d70ada6fd63a57 Mon Sep 17 00:00:00 2001 From: Yuri Tseretyan Date: Tue, 16 Apr 2024 13:35:41 -0400 Subject: [PATCH] SSE: Threshold expression to use simple functions (#86062) * replace math expression with predicates --- pkg/expr/hysteresis_test.go | 4 +- pkg/expr/testing.go | 43 ++++++ pkg/expr/threshold.go | 133 +++++++++++----- pkg/expr/threshold_bench_test.go | 88 +++++++++++ pkg/expr/threshold_test.go | 258 ++++++++++++++++++++++--------- 5 files changed, 414 insertions(+), 112 deletions(-) create mode 100644 pkg/expr/threshold_bench_test.go diff --git a/pkg/expr/hysteresis_test.go b/pkg/expr/hysteresis_test.go index 9a58a1a3492..31fd875213a 100644 --- a/pkg/expr/hysteresis_test.go +++ b/pkg/expr/hysteresis_test.go @@ -95,13 +95,13 @@ func TestHysteresisExecute(t *testing.T) { ReferenceVar: "A", RefID: "B", ThresholdFunc: ThresholdIsAbove, - Conditions: []float64{loadThreshold}, + predicate: greaterThanPredicate{loadThreshold}, }, UnloadingThresholdFunc: ThresholdCommand{ ReferenceVar: "A", RefID: "B", ThresholdFunc: ThresholdIsAbove, - Conditions: []float64{unloadThreshold}, + predicate: greaterThanPredicate{unloadThreshold}, }, LoadedDimensions: tc.loadedDimensions, } diff --git a/pkg/expr/testing.go b/pkg/expr/testing.go index 1c47d6f6e03..412c38a2f26 100644 --- a/pkg/expr/testing.go +++ b/pkg/expr/testing.go @@ -2,9 +2,12 @@ package expr import ( "context" + "time" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/expr/mathexp" "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/datasources" ) @@ -82,3 +85,43 @@ func (f *recordingCallResourceHandler) CallResource(_ context.Context, req *back f.recordings = append(f.recordings, req) return sender.Send(f.response) } + +func newScalar(value *float64) mathexp.Scalar { + n := mathexp.NewScalar("", value) + return n +} + +func newNumber(labels data.Labels, value *float64) mathexp.Number { + n := mathexp.NewNumber("", labels) + n.SetValue(value) + return n +} + +func newSeries(points ...float64) mathexp.Series { + series := mathexp.NewSeries("", nil, len(points)) + for idx, point := range points { + p := point + series.SetPoint(idx, time.Unix(int64(idx), 0), &p) + } + return series +} + +func newSeriesPointer(points ...*float64) mathexp.Series { + series := mathexp.NewSeries("", nil, len(points)) + for idx, point := range points { + series.SetPoint(idx, time.Unix(int64(idx), 0), point) + } + return series +} + +func newSeriesWithLabels(labels data.Labels, values ...*float64) mathexp.Series { + series := mathexp.NewSeries("", labels, len(values)) + for idx, value := range values { + series.SetPoint(idx, time.Unix(int64(idx), 0), value) + } + return series +} + +func newResults(values ...mathexp.Value) mathexp.Results { + return mathexp.Results{Values: values} +} diff --git a/pkg/expr/threshold.go b/pkg/expr/threshold.go index 6a1903f10cc..9daab174bc0 100644 --- a/pkg/expr/threshold.go +++ b/pkg/expr/threshold.go @@ -13,14 +13,19 @@ import ( "github.com/grafana/grafana/pkg/expr/mathexp" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/util" ) +type predicate interface { + Eval(f float64) bool +} + type ThresholdCommand struct { ReferenceVar string RefID string ThresholdFunc ThresholdType - Conditions []float64 Invert bool + predicate predicate } // +enum @@ -43,15 +48,28 @@ var ( ) func NewThresholdCommand(refID, referenceVar string, thresholdFunc ThresholdType, conditions []float64) (*ThresholdCommand, error) { + var predicate predicate switch thresholdFunc { - case ThresholdIsOutsideRange, ThresholdIsWithinRange: + case ThresholdIsOutsideRange: + if len(conditions) < 2 { + return nil, fmt.Errorf("incorrect number of arguments for threshold function '%s': got %d but need 2", thresholdFunc, len(conditions)) + } + predicate = outsideRangePredicate{left: conditions[0], right: conditions[1]} + case ThresholdIsWithinRange: if len(conditions) < 2 { - return nil, fmt.Errorf("incorrect number of arguments: got %d but need 2", len(conditions)) + return nil, fmt.Errorf("incorrect number of arguments for threshold function '%s': got %d but need 2", thresholdFunc, len(conditions)) } - case ThresholdIsAbove, ThresholdIsBelow: + predicate = withinRangePredicate{left: conditions[0], right: conditions[1]} + case ThresholdIsAbove: + if len(conditions) < 1 { + return nil, fmt.Errorf("incorrect number of arguments for threshold function '%s': got %d but need 1", thresholdFunc, len(conditions)) + } + predicate = greaterThanPredicate{value: conditions[0]} + case ThresholdIsBelow: if len(conditions) < 1 { - return nil, fmt.Errorf("incorrect number of arguments: got %d but need 1", len(conditions)) + return nil, fmt.Errorf("incorrect number of arguments for threshold function '%s': got %d but need 1", thresholdFunc, len(conditions)) } + predicate = lessThanPredicate{value: conditions[0]} default: return nil, fmt.Errorf("expected threshold function to be one of [%s], got %s", strings.Join(supportedThresholdFuncs, ", "), thresholdFunc) } @@ -60,7 +78,7 @@ func NewThresholdCommand(refID, referenceVar string, thresholdFunc ThresholdType RefID: refID, ReferenceVar: referenceVar, ThresholdFunc: thresholdFunc, - Conditions: conditions, + predicate: predicate, }, nil } @@ -114,46 +132,51 @@ func (tc *ThresholdCommand) NeedsVars() []string { return []string{tc.ReferenceVar} } -func (tc *ThresholdCommand) Execute(ctx context.Context, now time.Time, vars mathexp.Vars, tracer tracing.Tracer) (mathexp.Results, error) { - mathExpression, err := createMathExpression(tc.ReferenceVar, tc.ThresholdFunc, tc.Conditions, tc.Invert) - if err != nil { - return mathexp.Results{}, err +func (tc *ThresholdCommand) Execute(_ context.Context, _ time.Time, vars mathexp.Vars, _ tracing.Tracer) (mathexp.Results, error) { + eval := func(maybeValue *float64) *float64 { + if maybeValue == nil { + return nil + } + result := tc.predicate.Eval(*maybeValue) + if tc.Invert { + result = !result + } + if result { + return util.Pointer(float64(1)) + } + return util.Pointer(float64(0)) } - mathCommand, err := NewMathCommand(tc.RefID, mathExpression) - if err != nil { - return mathexp.Results{}, err + refVarResult := vars[tc.ReferenceVar] + newRes := mathexp.Results{Values: make(mathexp.Values, 0, len(refVarResult.Values))} + for _, val := range refVarResult.Values { + switch v := val.(type) { + case mathexp.Series: + s := mathexp.NewSeries(tc.RefID, v.GetLabels(), v.Len()) + for i := 0; i < v.Len(); i++ { + t, value := v.GetPoint(i) + s.SetPoint(i, t, eval(value)) + } + newRes.Values = append(newRes.Values, s) + case mathexp.Number: + copyV := mathexp.NewNumber(tc.RefID, v.GetLabels()) + copyV.SetValue(eval(v.GetFloat64Value())) + newRes.Values = append(newRes.Values, copyV) + case mathexp.Scalar: + copyV := mathexp.NewScalar(tc.RefID, eval(v.GetFloat64Value())) + newRes.Values = append(newRes.Values, copyV) + case mathexp.NoData: + newRes.Values = append(newRes.Values, mathexp.NewNoData()) + default: + return newRes, fmt.Errorf("unsupported format of the input data, got type %v", val.Type()) + } } - - return mathCommand.Execute(ctx, now, vars, tracer) + return newRes, nil } func (tc *ThresholdCommand) Type() string { return TypeThreshold.String() } - -// createMathExpression converts all the info we have about a "threshold" expression in to a Math expression -func createMathExpression(referenceVar string, thresholdFunc ThresholdType, args []float64, invert bool) (string, error) { - var exp string - switch thresholdFunc { - case ThresholdIsAbove: - exp = fmt.Sprintf("${%s} > %f", referenceVar, args[0]) - case ThresholdIsBelow: - exp = fmt.Sprintf("${%s} < %f", referenceVar, args[0]) - case ThresholdIsWithinRange: - exp = fmt.Sprintf("${%s} > %f && ${%s} < %f", referenceVar, args[0], referenceVar, args[1]) - case ThresholdIsOutsideRange: - exp = fmt.Sprintf("${%s} < %f || ${%s} > %f", referenceVar, args[0], referenceVar, args[1]) - default: - return "", fmt.Errorf("failed to evaluate threshold expression: no such threshold function %s", thresholdFunc) - } - - if invert { - return fmt.Sprintf("!(%s)", exp), nil - } - return exp, nil -} - func IsSupportedThresholdFunc(name string) bool { isSupported := false @@ -237,3 +260,37 @@ func getConditionForHysteresisCommand(query map[string]any) (map[string]any, err } return condition, nil } + +type withinRangePredicate struct { + left float64 + right float64 +} + +func (r withinRangePredicate) Eval(f float64) bool { + return f > r.left && f < r.right +} + +type outsideRangePredicate struct { + left float64 + right float64 +} + +func (r outsideRangePredicate) Eval(f float64) bool { + return f < r.left || f > r.right +} + +type lessThanPredicate struct { + value float64 +} + +func (r lessThanPredicate) Eval(f float64) bool { + return f < r.value +} + +type greaterThanPredicate struct { + value float64 +} + +func (r greaterThanPredicate) Eval(f float64) bool { + return f > r.value +} diff --git a/pkg/expr/threshold_bench_test.go b/pkg/expr/threshold_bench_test.go new file mode 100644 index 00000000000..ddda45eba97 --- /dev/null +++ b/pkg/expr/threshold_bench_test.go @@ -0,0 +1,88 @@ +package expr + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/grafana/grafana-plugin-sdk-go/data" + + "github.com/grafana/grafana/pkg/expr/mathexp" + "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/util" +) + +func BenchmarkThreshold(b *testing.B) { + results := make(mathexp.Values, 0, 1000) + for i := 0; i < cap(results); i++ { + n := newNumber(data.Labels{"test": fmt.Sprintf("series-%d", i)}, util.Pointer(float64(i))) + results = append(results, n) + } + ctx := context.Background() + timeNow := time.Now() + vars := mathexp.Vars{ + "A": newResults(results...), + } + trace := tracing.InitializeTracerForTest() + b.ResetTimer() + b.Run("greater than", func(b *testing.B) { + greater, err := NewThresholdCommand("B", "A", ThresholdIsAbove, []float64{500}) + if err != nil { + b.Fatalf("error: %s", err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = greater.Execute(ctx, timeNow, vars, trace) + } + }) + b.Run("less than", func(b *testing.B) { + greater, err := NewThresholdCommand("B", "A", ThresholdIsAbove, []float64{500}) + if err != nil { + b.Fatalf("error: %s", err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = greater.Execute(ctx, timeNow, vars, trace) + } + }) + b.Run("within range", func(b *testing.B) { + greater, err := NewThresholdCommand("B", "A", ThresholdIsWithinRange, []float64{400.0, 600.0}) + if err != nil { + b.Fatalf("error: %s", err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = greater.Execute(ctx, timeNow, vars, trace) + } + }) + b.Run("within range, no labels", func(b *testing.B) { + greater, err := NewThresholdCommand("B", "A", ThresholdIsWithinRange, []float64{400.0, 600.0}) + if err != nil { + b.Fatalf("error: %s", err) + } + + results := make(mathexp.Values, 0, 1000) + for i := 0; i < cap(results); i++ { + n := newNumber(nil, util.Pointer(float64(i))) + results = append(results, n) + } + vars := mathexp.Vars{ + "A": newResults(results...), + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = greater.Execute(ctx, timeNow, vars, trace) + } + }) + b.Run("outside range", func(b *testing.B) { + greater, err := NewThresholdCommand("B", "A", ThresholdIsOutsideRange, []float64{400.0, 600.0}) + if err != nil { + b.Fatalf("error: %s", err) + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = greater.Execute(ctx, timeNow, vars, trace) + } + }) +} diff --git a/pkg/expr/threshold_test.go b/pkg/expr/threshold_test.go index c7e1c1f2eb6..d458304ae08 100644 --- a/pkg/expr/threshold_test.go +++ b/pkg/expr/threshold_test.go @@ -1,15 +1,22 @@ package expr import ( + "context" "encoding/json" - "fmt" "math" + "slices" "sort" "testing" + "time" + "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/stretchr/testify/require" + "golang.org/x/exp/maps" + "github.com/grafana/grafana/pkg/expr/mathexp" + "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/util" ) func TestNewThresholdCommand(t *testing.T) { @@ -108,7 +115,7 @@ func TestUnmarshalThresholdCommand(t *testing.T) { cmd := command.(*ThresholdCommand) require.Equal(t, []string{"A"}, cmd.NeedsVars()) require.Equal(t, ThresholdIsAbove, cmd.ThresholdFunc) - require.Equal(t, []float64{20.0, 80.0}, cmd.Conditions) + require.Equal(t, greaterThanPredicate{20.0}, cmd.predicate) }, }, { @@ -173,10 +180,10 @@ func TestUnmarshalThresholdCommand(t *testing.T) { require.Equal(t, []string{"B"}, cmd.NeedsVars()) require.Equal(t, []string{"B"}, cmd.LoadingThresholdFunc.NeedsVars()) require.Equal(t, ThresholdIsAbove, cmd.LoadingThresholdFunc.ThresholdFunc) - require.Equal(t, []float64{100.0}, cmd.LoadingThresholdFunc.Conditions) + require.Equal(t, greaterThanPredicate{100.0}, cmd.LoadingThresholdFunc.predicate) require.Equal(t, []string{"B"}, cmd.UnloadingThresholdFunc.NeedsVars()) require.Equal(t, ThresholdIsBelow, cmd.UnloadingThresholdFunc.ThresholdFunc) - require.Equal(t, []float64{31.0}, cmd.UnloadingThresholdFunc.Conditions) + require.Equal(t, lessThanPredicate{31.0}, cmd.UnloadingThresholdFunc.predicate) require.True(t, cmd.UnloadingThresholdFunc.Invert) require.NotNil(t, cmd.LoadedDimensions) actual := make([]uint64, 0, len(cmd.LoadedDimensions)) @@ -227,74 +234,6 @@ func TestThresholdCommandVars(t *testing.T) { require.Equal(t, cmd.NeedsVars(), []string{"A"}) } -func TestCreateMathExpression(t *testing.T) { - type testCase struct { - description string - expected string - - ref string - function ThresholdType - params []float64 - } - - cases := []testCase{ - { - description: "is above", - ref: "My Ref", - function: "gt", - params: []float64{0}, - expected: "${My Ref} > 0.000000", - }, - { - description: "is below", - ref: "A", - function: "lt", - params: []float64{0}, - expected: "${A} < 0.000000", - }, - { - description: "is within", - ref: "B", - function: "within_range", - params: []float64{20, 80}, - expected: "${B} > 20.000000 && ${B} < 80.000000", - }, - { - description: "is outside", - ref: "B", - function: "outside_range", - params: []float64{20, 80}, - expected: "${B} < 20.000000 || ${B} > 80.000000", - }, - } - - for _, tc := range cases { - t.Run(tc.description, func(t *testing.T) { - expr, err := createMathExpression(tc.ref, tc.function, tc.params, false) - - require.Nil(t, err) - require.NotNil(t, expr) - - require.Equal(t, tc.expected, expr) - - t.Run("inverted", func(t *testing.T) { - expr, err := createMathExpression(tc.ref, tc.function, tc.params, true) - require.Nil(t, err) - require.NotNil(t, expr) - - require.Equal(t, fmt.Sprintf("!(%s)", tc.expected), expr) - }) - }) - } - - t.Run("should error if function is unsupported", func(t *testing.T) { - expr, err := createMathExpression("A", "foo", []float64{0}, false) - require.Equal(t, expr, "") - require.NotNil(t, err) - require.Contains(t, err.Error(), "no such threshold function") - }) -} - func TestIsSupportedThresholdFunc(t *testing.T) { type testCase struct { function ThresholdType @@ -443,3 +382,178 @@ func TestSetLoadedDimensionsToHysteresisCommand(t *testing.T) { require.Equal(t, fingerprints, cmd.(*HysteresisCommand).LoadedDimensions) }) } + +func TestThresholdExecute(t *testing.T) { + input := map[string]mathexp.Value{ + // + "no-data": mathexp.NewNoData(), + // + "series - numbers": newSeries(8, 9, 10, 11, 12), + "series - empty": newSeriesPointer(), + "series - all nils": newSeriesPointer(nil, nil, nil), + "series - with labels": newSeriesWithLabels(data.Labels{"test": "test"}, nil, util.Pointer(float64(9)), nil, util.Pointer(float64(11)), nil), + "series - with NaNs": newSeries(math.NaN(), math.NaN(), math.NaN()), + // + "scalar - nil": newScalar(nil), + "scalar - NaN": newScalar(util.Pointer(math.NaN())), + "scalar - 8": newScalar(util.Pointer(float64(8))), + "scalar - 9": newScalar(util.Pointer(float64(9))), + "scalar - 10": newScalar(util.Pointer(float64(10))), + "scalar - 11": newScalar(util.Pointer(float64(11))), + "scalar - 12": newScalar(util.Pointer(float64(12))), + // + "number - nil": newNumber(data.Labels{"number": "test"}, nil), + "number - NaN": newNumber(data.Labels{"number": "test"}, util.Pointer(math.NaN())), + "number - 8": newNumber(data.Labels{"number": "test"}, util.Pointer(float64(8))), + "number - 9": newNumber(data.Labels{"number": "test"}, util.Pointer(float64(9))), + "number - 10": newNumber(data.Labels{"number": "test"}, util.Pointer(float64(10))), + "number - 11": newNumber(data.Labels{"number": "test"}, util.Pointer(float64(11))), + "number - 12": newNumber(data.Labels{"number": "test"}, util.Pointer(float64(12))), + } + keys := maps.Keys(input) + slices.Sort(keys) + testCases := []struct { + name string + pred predicate + expected map[string]mathexp.Value + errorMsg string + }{ + { + name: "greater than 10", + pred: greaterThanPredicate{10.0}, + expected: map[string]mathexp.Value{ + // + "no-data": mathexp.NewNoData(), + // + "series - numbers": newSeries(0, 0, 0, 1, 1), + "series - empty": newSeriesPointer(), + "series - all nils": newSeriesPointer(nil, nil, nil), + "series - with labels": newSeriesWithLabels(data.Labels{"test": "test"}, nil, util.Pointer(float64(0)), nil, util.Pointer(float64(1)), nil), + "series - with NaNs": newSeries(0, 0, 0), + // + "scalar - nil": newScalar(nil), + "scalar - NaN": newScalar(util.Pointer(float64(0))), + "scalar - 8": newScalar(util.Pointer(float64(0))), + "scalar - 9": newScalar(util.Pointer(float64(0))), + "scalar - 10": newScalar(util.Pointer(float64(0))), + "scalar - 11": newScalar(util.Pointer(float64(1))), + "scalar - 12": newScalar(util.Pointer(float64(1))), + // + "number - nil": newNumber(data.Labels{"number": "test"}, nil), + "number - NaN": newNumber(data.Labels{"number": "test"}, util.Pointer(float64(0))), + "number - 8": newNumber(data.Labels{"number": "test"}, util.Pointer(float64(0))), + "number - 9": newNumber(data.Labels{"number": "test"}, util.Pointer(float64(0))), + "number - 10": newNumber(data.Labels{"number": "test"}, util.Pointer(float64(0))), + "number - 11": newNumber(data.Labels{"number": "test"}, util.Pointer(float64(1))), + "number - 12": newNumber(data.Labels{"number": "test"}, util.Pointer(float64(1))), + }, + }, + { + name: "less than 10", + pred: lessThanPredicate{10.0}, + expected: map[string]mathexp.Value{ + // + "no-data": mathexp.NewNoData(), + // + "series - numbers": newSeries(1, 1, 0, 0, 0), + "series - empty": newSeriesPointer(), + "series - all nils": newSeriesPointer(nil, nil, nil), + "series - with labels": newSeriesWithLabels(data.Labels{"test": "test"}, nil, util.Pointer(float64(1)), nil, util.Pointer(float64(0)), nil), + "series - with NaNs": newSeries(0, 0, 0), + // + "scalar - nil": newScalar(nil), + "scalar - NaN": newScalar(util.Pointer(float64(0))), + "scalar - 8": newScalar(util.Pointer(float64(1))), + "scalar - 9": newScalar(util.Pointer(float64(1))), + "scalar - 10": newScalar(util.Pointer(float64(0))), + "scalar - 11": newScalar(util.Pointer(float64(0))), + "scalar - 12": newScalar(util.Pointer(float64(0))), + // + "number - nil": newNumber(data.Labels{"number": "test"}, nil), + "number - NaN": newNumber(data.Labels{"number": "test"}, util.Pointer(float64(0))), + "number - 8": newNumber(data.Labels{"number": "test"}, util.Pointer(float64(1))), + "number - 9": newNumber(data.Labels{"number": "test"}, util.Pointer(float64(1))), + "number - 10": newNumber(data.Labels{"number": "test"}, util.Pointer(float64(0))), + "number - 11": newNumber(data.Labels{"number": "test"}, util.Pointer(float64(0))), + "number - 12": newNumber(data.Labels{"number": "test"}, util.Pointer(float64(0))), + }, + }, + { + name: "within range (8,11)", + pred: withinRangePredicate{8, 11}, + expected: map[string]mathexp.Value{ + // + "no-data": mathexp.NewNoData(), + // + "series - numbers": newSeries(0, 1, 1, 0, 0), + "series - empty": newSeriesPointer(), + "series - all nils": newSeriesPointer(nil, nil, nil), + "series - with labels": newSeriesWithLabels(data.Labels{"test": "test"}, nil, util.Pointer(float64(1)), nil, util.Pointer(float64(0)), nil), + "series - with NaNs": newSeries(0, 0, 0), + // + "scalar - nil": newScalar(nil), + "scalar - NaN": newScalar(util.Pointer(float64(0))), + "scalar - 8": newScalar(util.Pointer(float64(0))), + "scalar - 9": newScalar(util.Pointer(float64(1))), + "scalar - 10": newScalar(util.Pointer(float64(1))), + "scalar - 11": newScalar(util.Pointer(float64(0))), + "scalar - 12": newScalar(util.Pointer(float64(0))), + // + "number - nil": newNumber(data.Labels{"number": "test"}, nil), + "number - NaN": newNumber(data.Labels{"number": "test"}, util.Pointer(float64(0))), + "number - 8": newNumber(data.Labels{"number": "test"}, util.Pointer(float64(0))), + "number - 9": newNumber(data.Labels{"number": "test"}, util.Pointer(float64(1))), + "number - 10": newNumber(data.Labels{"number": "test"}, util.Pointer(float64(1))), + "number - 11": newNumber(data.Labels{"number": "test"}, util.Pointer(float64(0))), + "number - 12": newNumber(data.Labels{"number": "test"}, util.Pointer(float64(0))), + }, + }, + { + name: "outside range (8, 11)", + pred: outsideRangePredicate{8, 11}, + expected: map[string]mathexp.Value{ + // + "no-data": mathexp.NewNoData(), + // + "series - numbers": newSeries(0, 0, 0, 0, 1), + "series - empty": newSeriesPointer(), + "series - all nils": newSeriesPointer(nil, nil, nil), + "series - with labels": newSeriesWithLabels(data.Labels{"test": "test"}, nil, util.Pointer(float64(0)), nil, util.Pointer(float64(0)), nil), + "series - with NaNs": newSeries(0, 0, 0), + // + "scalar - nil": newScalar(nil), + "scalar - NaN": newScalar(util.Pointer(float64(0))), + "scalar - 8": newScalar(util.Pointer(float64(0))), + "scalar - 9": newScalar(util.Pointer(float64(0))), + "scalar - 10": newScalar(util.Pointer(float64(0))), + "scalar - 11": newScalar(util.Pointer(float64(0))), + "scalar - 12": newScalar(util.Pointer(float64(1))), + // + "number - nil": newNumber(data.Labels{"number": "test"}, nil), + "number - NaN": newNumber(data.Labels{"number": "test"}, util.Pointer(float64(0))), + "number - 8": newNumber(data.Labels{"number": "test"}, util.Pointer(float64(0))), + "number - 9": newNumber(data.Labels{"number": "test"}, util.Pointer(float64(0))), + "number - 10": newNumber(data.Labels{"number": "test"}, util.Pointer(float64(0))), + "number - 11": newNumber(data.Labels{"number": "test"}, util.Pointer(float64(0))), + "number - 12": newNumber(data.Labels{"number": "test"}, util.Pointer(float64(1))), + }, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + cmd := ThresholdCommand{ + predicate: tc.pred, + ReferenceVar: "A", + } + for _, name := range keys { + t.Run(name, func(t *testing.T) { + result, err := cmd.Execute(context.Background(), time.Now(), mathexp.Vars{ + "A": newResults(input[name]), + }, tracing.InitializeTracerForTest()) + require.NoError(t, err) + require.Equal(t, newResults(tc.expected[name]), result) + }) + } + }) + } +}