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/commands_test.go

276 lines
8.4 KiB

package expr
import (
"context"
"encoding/json"
"fmt"
"math"
"math/rand"
"testing"
"time"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/expr/mathexp"
"github.com/grafana/grafana/pkg/expr/mathexp/parse"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/util"
)
func Test_UnmarshalReduceCommand_Settings(t *testing.T) {
var tests = []struct {
name string
querySettings string
isError bool
expectedMapper mathexp.ReduceMapper
}{
{
name: "no mapper function when settings is not specified",
querySettings: ``,
expectedMapper: nil,
},
{
name: "no mapper function when mode is not specified",
querySettings: `, "settings" : { }`,
expectedMapper: nil,
},
{
name: "error when settings is not object",
querySettings: `, "settings" : "drop-nan"`,
isError: true,
},
{
name: "no mapper function when mode is empty",
querySettings: `, "settings" : { "mode": "" }`,
expectedMapper: nil,
},
{
name: "error when mode is not known",
querySettings: `, "settings" : { "mode": "test" }`,
isError: true,
},
{
name: "filterNonNumber function when mode is 'dropNN'",
querySettings: `, "settings" : { "mode": "dropNN" }`,
expectedMapper: mathexp.DropNonNumber{},
},
{
name: "replaceNanWithValue function when mode is 'dropNN'",
querySettings: `, "settings" : { "mode": "replaceNN" , "replaceWithValue": -12 }`,
expectedMapper: mathexp.ReplaceNonNumberWithValue{Value: -12},
},
{
name: "error if mode is 'replaceNN' but field replaceWithValue is not specified",
querySettings: `, "settings" : { "mode": "replaceNN" }`,
isError: true,
},
{
name: "error if mode is 'replaceNN' but field replaceWithValue is not a number",
querySettings: `, "settings" : { "mode": "replaceNN", "replaceWithValue" : "-12" }`,
isError: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
q := fmt.Sprintf(`{ "expression" : "$A", "reducer": "sum"%s }`, test.querySettings)
var qmap = make(map[string]any)
require.NoError(t, json.Unmarshal([]byte(q), &qmap))
cmd, err := UnmarshalReduceCommand(&rawNode{
RefID: "A",
Query: qmap,
QueryType: "",
TimeRange: RelativeTimeRange{},
DataSource: nil,
})
if test.isError {
require.Error(t, err)
return
}
require.NotNil(t, cmd)
require.Equal(t, test.expectedMapper, cmd.seriesMapper)
})
}
}
func TestReduceExecute(t *testing.T) {
varToReduce := util.GenerateShortUID()
t.Run("when mapper is nil", func(t *testing.T) {
cmd, err := NewReduceCommand(util.GenerateShortUID(), randomReduceFunc(), varToReduce, nil)
require.NoError(t, err)
t.Run("should noop if Number", func(t *testing.T) {
var numbers mathexp.Values = []mathexp.Value{
mathexp.GenerateNumber(util.Pointer(rand.Float64())),
mathexp.GenerateNumber(util.Pointer(rand.Float64())),
mathexp.GenerateNumber(util.Pointer(rand.Float64())),
}
vars := map[string]mathexp.Results{
varToReduce: {
Values: numbers,
},
}
execute, err := cmd.Execute(context.Background(), time.Now(), vars, tracing.InitializeTracerForTest())
require.NoError(t, err)
require.Len(t, execute.Values, len(numbers))
for i, value := range execute.Values {
expected := numbers[i]
require.Equal(t, expected.Type(), value.Type())
require.Equal(t, expected.GetLabels(), value.GetLabels())
expectedValue := expected.Value().(*mathexp.Number).GetFloat64Value()
actualValue := value.Value().(*mathexp.Number).GetFloat64Value()
require.Equal(t, expectedValue, actualValue)
}
t.Run("should add warn notices to the first frame", func(t *testing.T) {
frames := execute.Values.AsDataFrames("test")
notice := frames[0].Meta.Notices[0]
require.Equal(t, data.NoticeSeverityWarning, notice.Severity)
for _, frame := range frames[1:] {
require.Empty(t, frame.Meta.Notices)
}
})
})
})
t.Run("when mapper is not nil", func(t *testing.T) {
var numbers mathexp.Values = []mathexp.Value{
mathexp.GenerateNumber(util.Pointer(rand.Float64())),
mathexp.GenerateNumber(nil),
mathexp.GenerateNumber(util.Pointer(math.NaN())),
mathexp.GenerateNumber(util.Pointer(math.Inf(-1))),
mathexp.GenerateNumber(util.Pointer(math.Inf(1))),
mathexp.GenerateNumber(util.Pointer(rand.Float64())),
}
varToReduce := util.GenerateShortUID()
vars := map[string]mathexp.Results{
varToReduce: {
Values: numbers,
},
}
t.Run("drop all non numbers if mapper is DropNonNumber", func(t *testing.T) {
cmd, err := NewReduceCommand(util.GenerateShortUID(), randomReduceFunc(), varToReduce, &mathexp.DropNonNumber{})
require.NoError(t, err)
execute, err := cmd.Execute(context.Background(), time.Now(), vars, tracing.InitializeTracerForTest())
require.NoError(t, err)
require.Len(t, execute.Values, 2)
})
t.Run("replace all non numbers if mapper is ReplaceNonNumberWithValue", func(t *testing.T) {
cmd, err := NewReduceCommand(util.GenerateShortUID(), randomReduceFunc(), varToReduce, &mathexp.ReplaceNonNumberWithValue{Value: 1})
require.NoError(t, err)
execute, err := cmd.Execute(context.Background(), time.Now(), vars, tracing.InitializeTracerForTest())
require.NoError(t, err)
require.Len(t, execute.Values, len(numbers))
for _, value := range execute.Values[1 : len(numbers)-1] {
require.IsType(t, &mathexp.Number{}, value.Value())
f := value.Value().(*mathexp.Number)
require.Equal(t, float64(1), *f.GetFloat64Value())
}
})
})
t.Run("should return new NoData", func(t *testing.T) {
var noData mathexp.Values = []mathexp.Value{
mathexp.NoData{Frame: data.NewFrame("no data")},
}
vars := map[string]mathexp.Results{
varToReduce: {
Values: noData,
},
}
cmd, err := NewReduceCommand(util.GenerateShortUID(), randomReduceFunc(), varToReduce, nil)
require.NoError(t, err)
results, err := cmd.Execute(context.Background(), time.Now(), vars, tracing.InitializeTracerForTest())
require.NoError(t, err)
require.Len(t, results.Values, 1)
v := results.Values[0]
assert.Equal(t, v, mathexp.NoData{}.New())
// should not be able to change the original frame
v.AsDataFrame().Name = "there is still no data"
assert.NotEqual(t, v, mathexp.NoData{}.New())
assert.NotEqual(t, v, noData[0])
assert.Equal(t, "no data", noData[0].AsDataFrame().Name)
})
}
func randomReduceFunc() mathexp.ReducerID {
res := mathexp.GetSupportedReduceFuncs()
return res[rand.Intn(len(res))]
}
func TestResampleCommand_Execute(t *testing.T) {
varToReduce := util.GenerateShortUID()
tr := RelativeTimeRange{
From: -10 * time.Second,
To: 0,
}
cmd, err := NewResampleCommand(util.GenerateShortUID(), "1s", varToReduce, "sum", "pad", tr)
require.NoError(t, err)
var tests = []struct {
name string
vals mathexp.Value
isError bool
expectedType parse.ReturnType
}{
{
name: "should resample when input Series",
vals: mathexp.NewSeries(varToReduce, nil, 100),
expectedType: parse.TypeSeriesSet,
},
{
name: "should return NoData when input NoData",
vals: mathexp.NoData{},
expectedType: parse.TypeNoData,
}, {
name: "should return error when input Number",
vals: mathexp.NewNumber("test", nil),
isError: true,
}, {
name: "should return error when input Scalar",
vals: mathexp.NewScalar("test", util.Pointer(rand.Float64())),
isError: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := cmd.Execute(context.Background(), time.Now(), mathexp.Vars{
varToReduce: mathexp.Results{Values: mathexp.Values{test.vals}},
}, tracing.InitializeTracerForTest())
if test.isError {
require.Error(t, err)
} else {
require.NoError(t, err)
require.Len(t, result.Values, 1)
res := result.Values[0]
require.Equal(t, test.expectedType, res.Type())
}
})
}
t.Run("should return empty result if input is nil Value", func(t *testing.T) {
result, err := cmd.Execute(context.Background(), time.Now(), mathexp.Vars{
varToReduce: mathexp.Results{Values: mathexp.Values{nil}},
}, tracing.InitializeTracerForTest())
require.Empty(t, result.Values)
require.NoError(t, err)
})
}