The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/expr/mathexp/exp_nan_null_val_test.go

446 lines
11 KiB

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())
})
}
})
}
}