package mathexp import ( "fmt" "math" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/expr/mathexp/parse" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestNaN(t *testing.T) { var tests = []struct { name string expr string vars Vars newErrIs assert.ErrorAssertionFunc execErrIs assert.ErrorAssertionFunc results Results willPanic bool }{ { name: "unary !: Op Number(NaN) is NaN", expr: "! $A", vars: Vars{"A": resultValuesNoErr(makeNumber("", nil, NaN))}, newErrIs: assert.NoError, execErrIs: assert.NoError, results: resultValuesNoErr(makeNumber("", nil, NaN)), }, { name: "unary -: Op Number(NaN) is NaN", expr: "-$A", vars: Vars{"A": resultValuesNoErr(makeNumber("", nil, NaN))}, newErrIs: assert.NoError, execErrIs: assert.NoError, results: resultValuesNoErr(makeNumber("", nil, NaN)), }, { name: "binary: Scalar Op(Non-AND/OR) Number(NaN) is NaN", expr: "1 * $A", vars: Vars{"A": resultValuesNoErr(makeNumber("", nil, NaN))}, newErrIs: assert.NoError, execErrIs: assert.NoError, results: resultValuesNoErr(makeNumber("", nil, NaN)), }, { name: "binary: Scalar Op(AND/OR) Number(NaN) is 0/1", expr: "1 || $A", vars: Vars{"A": resultValuesNoErr(makeNumber("", nil, NaN))}, newErrIs: assert.NoError, execErrIs: assert.NoError, results: resultValuesNoErr(makeNumber("", nil, float64Pointer(1))), }, { name: "binary: Scalar Op(Non-AND/OR) Series(with NaN value) is NaN)", expr: "1 - $A", vars: Vars{ "A": resultValuesNoErr( makeSeries("temp", nil, tp{ time.Unix(5, 0), float64Pointer(2), }, tp{ time.Unix(10, 0), NaN, }), ), }, newErrIs: assert.NoError, execErrIs: assert.NoError, results: resultValuesNoErr( makeSeries("", nil, tp{ time.Unix(5, 0), float64Pointer(-1), }, tp{ time.Unix(10, 0), NaN, }), ), }, { name: "binary: Number Op(Non-AND/OR) Series(with NaN value) is Series with NaN", expr: "$A == $B", vars: Vars{ "A": resultValuesNoErr( makeSeries("temp", nil, tp{ time.Unix(5, 0), float64Pointer(2), }, tp{ time.Unix(10, 0), NaN, }), ), "B": resultValuesNoErr(makeNumber("", nil, float64Pointer(0))), }, newErrIs: assert.NoError, execErrIs: assert.NoError, results: resultValuesNoErr( makeSeries("", nil, tp{ time.Unix(5, 0), float64Pointer(0), }, tp{ time.Unix(10, 0), NaN, }), ), }, { name: "binary: Number(NaN) Op Series(with NaN value) is Series with NaN", expr: "$A + $B", vars: Vars{ "A": resultValuesNoErr( makeSeries("temp", nil, tp{ time.Unix(5, 0), float64Pointer(2), }, tp{ time.Unix(10, 0), NaN, }), ), "B": resultValuesNoErr(makeNumber("", nil, NaN)), }, newErrIs: assert.NoError, execErrIs: assert.NoError, results: resultValuesNoErr( makeSeries("", nil, tp{ time.Unix(5, 0), NaN, }, tp{ time.Unix(10, 0), NaN, }), ), }, } opt := cmp.Comparer(func(x, y float64) bool { return (math.IsNaN(x) && math.IsNaN(y)) || x == y }) options := append([]cmp.Option{opt}, data.FrameTestCompareOptions()...) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { testBlock := func() { e, err := New(tt.expr) tt.newErrIs(t, err) if e != nil { res, err := e.Execute("", tt.vars, tracing.InitializeTracerForTest()) tt.execErrIs(t, err) if diff := cmp.Diff(res, tt.results, options...); diff != "" { assert.FailNow(t, tt.name, diff) } } } if tt.willPanic { assert.Panics(t, testBlock) } else { assert.NotPanics(t, testBlock) } }) } } func TestNullValues(t *testing.T) { var tests = []struct { name string expr string vars Vars newErrIs assert.ErrorAssertionFunc execErrIs assert.ErrorAssertionFunc results Results willPanic bool }{ { name: "scalar: unary ! null(): is null", expr: "! null()", newErrIs: assert.NoError, execErrIs: assert.NoError, results: NewScalarResults("", nil), }, { name: "scalar: binary null() + null(): is null", expr: "null() + null()", newErrIs: assert.NoError, execErrIs: assert.NoError, results: NewScalarResults("", nil), }, { name: "scalar: binary 1 + null(): is null", expr: "1 + null()", newErrIs: assert.NoError, execErrIs: assert.NoError, results: NewScalarResults("", nil), }, { name: "series: unary with a null value in it has a null value in result", expr: "- $A", vars: Vars{ "A": resultValuesNoErr( makeSeries("", nil, tp{ time.Unix(5, 0), float64Pointer(1), }, tp{ time.Unix(10, 0), nil, }), ), }, newErrIs: assert.NoError, execErrIs: assert.NoError, results: resultValuesNoErr( makeSeries("", nil, tp{ time.Unix(5, 0), float64Pointer(-1), }, tp{ time.Unix(10, 0), nil, }), ), }, { name: "series: binary with a null value in it has a null value in result", expr: "$A - $A", vars: Vars{ "A": resultValuesNoErr( makeSeries("", nil, tp{ time.Unix(5, 0), float64Pointer(1), }, tp{ time.Unix(10, 0), nil, }), ), }, newErrIs: assert.NoError, execErrIs: assert.NoError, results: resultValuesNoErr( makeSeries("", nil, tp{ time.Unix(5, 0), float64Pointer(0), }, tp{ time.Unix(10, 0), nil, }), ), }, { name: "series and scalar: binary with a null value in it has a nil value in result", expr: "$A - 1", vars: Vars{ "A": resultValuesNoErr( makeSeries("", nil, tp{ time.Unix(5, 0), float64Pointer(1), }, tp{ time.Unix(10, 0), nil, }), ), }, newErrIs: assert.NoError, execErrIs: assert.NoError, results: resultValuesNoErr( makeSeries("", nil, tp{ time.Unix(5, 0), float64Pointer(0), }, tp{ time.Unix(10, 0), nil, }), ), }, { name: "number: unary ! null number: is null", expr: "! $A", vars: Vars{ "A": resultValuesNoErr(makeNumber("", nil, nil)), }, newErrIs: assert.NoError, execErrIs: assert.NoError, results: resultValuesNoErr(makeNumber("", nil, nil)), }, { name: "number: binary null number and null number: is null", expr: "$A + $A", vars: Vars{ "A": resultValuesNoErr(makeNumber("", nil, nil)), }, newErrIs: assert.NoError, execErrIs: assert.NoError, results: resultValuesNoErr(makeNumber("", nil, nil)), }, { name: "number: binary non-null number and null number: is null", expr: "$A * $B", vars: Vars{ "A": resultValuesNoErr(makeNumber("", nil, nil)), "B": resultValuesNoErr(makeNumber("", nil, float64Pointer(1))), }, newErrIs: assert.NoError, execErrIs: assert.NoError, results: resultValuesNoErr(makeNumber("", nil, nil)), }, { name: "number and series: binary non-null number and series with a null: is null", expr: "$A * $B", vars: Vars{ "A": resultValuesNoErr(makeNumber("", nil, float64Pointer(1))), "B": resultValuesNoErr( makeSeries("", nil, tp{ time.Unix(5, 0), float64Pointer(1), }, tp{ time.Unix(10, 0), nil, }), ), }, newErrIs: assert.NoError, execErrIs: assert.NoError, results: resultValuesNoErr( makeSeries("", nil, tp{ time.Unix(5, 0), float64Pointer(1), }, tp{ time.Unix(10, 0), nil, }), ), }, { name: "number and series: binary null number and series with non-null and null: is null and null", expr: "$A * $B", vars: Vars{ "A": resultValuesNoErr(makeNumber("", nil, nil)), "B": resultValuesNoErr( makeSeries("", nil, tp{ time.Unix(5, 0), float64Pointer(1), }, tp{ time.Unix(10, 0), nil, }), ), }, newErrIs: assert.NoError, execErrIs: assert.NoError, results: resultValuesNoErr( makeSeries("", nil, tp{ time.Unix(5, 0), nil, }, tp{ time.Unix(10, 0), nil, }), ), }, } // go-cmp instead of testify assert is used to compare results here // because it supports an option for NaN equality. // https://github.com/stretchr/testify/pull/691#issuecomment-528457166 opt := cmp.Comparer(func(x, y float64) bool { return (math.IsNaN(x) && math.IsNaN(y)) || x == y }) options := append([]cmp.Option{opt}, data.FrameTestCompareOptions()...) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { testBlock := func() { e, err := New(tt.expr) tt.newErrIs(t, err) if e != nil { res, err := e.Execute("", tt.vars, tracing.InitializeTracerForTest()) tt.execErrIs(t, err) if diff := cmp.Diff(tt.results, res, options...); diff != "" { t.Errorf("Result mismatch (-want +got):\n%s", diff) } } } if tt.willPanic { assert.Panics(t, testBlock) } else { testBlock() } }) } } func TestNoData(t *testing.T) { t.Run("unary operation return NoData if input NoData", func(t *testing.T) { unaryOps := []string{ "abs($A)", "is_inf($A)", "is_nan($A)", "is_null($A)", "is_number($A)", "log($A)", "round($A)", "ceil($A)", "floor($A)", "!$A", "-$A", } vars := Vars{"A": resultValuesNoErr(NewNoData())} for _, expr := range unaryOps { t.Run(fmt.Sprintf("op: %s", expr), func(t *testing.T) { e, err := New(expr) require.NoError(t, err) if e != nil { res, err := e.Execute("", vars, tracing.InitializeTracerForTest()) require.NoError(t, err) require.Len(t, res.Values, 1) require.Equal(t, NewNoData(), res.Values[0]) } }) } }) makeVars := func(a, b Value) Vars { return Vars{ "A": resultValuesNoErr(a), "B": resultValuesNoErr(b), } } bin_ops := []string{ "$A || $B", "$A && $B", "$A + $B", "$A * $B", "$A - $B", "$A / $B", "$A ** $B", "$A % $B", "$A == $B", "$A > $B", "$A != $B", "$A < $B", "$A >= $B", "$A <= $B", "$A || $B", "$A && $B", } series := makeSeries("test", nil, tp{time.Unix(5, 0), float64Pointer(2)}) for _, expr := range bin_ops { t.Run(fmt.Sprintf("op: %s", expr), func(t *testing.T) { e, err := New(expr) require.NoError(t, err) if e != nil { t.Run("$A,$B=nodata", func(t *testing.T) { res, err := e.Execute("", makeVars(NewNoData(), NewNoData()), tracing.InitializeTracerForTest()) require.NoError(t, err) require.Len(t, res.Values, 1) require.Equal(t, parse.TypeNoData, res.Values[0].Type()) }) t.Run("$A=nodata, $B=series", func(t *testing.T) { res, err := e.Execute("", makeVars(NewNoData(), series), tracing.InitializeTracerForTest()) require.NoError(t, err) require.Len(t, res.Values, 1) require.Equal(t, parse.TypeNoData, res.Values[0].Type()) }) t.Run("$A=series, $B=nodata", func(t *testing.T) { res, err := e.Execute("", makeVars(NewNoData(), series), tracing.InitializeTracerForTest()) require.NoError(t, err) require.Len(t, res.Values, 1) require.Equal(t, parse.TypeNoData, res.Values[0].Type()) }) } }) } }