grafana/pkg/services/ngalert/backtesting/eval_data_test.go
2023-08-30 18:46:47 +03:00

294 lines
9.5 KiB
Go

package backtesting
import (
"context"
"errors"
"fmt"
"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/services/ngalert/eval"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/util"
)
func GenerateWideSeriesFrame(size int, resolution time.Duration) *data.Frame {
fields := make(data.Fields, 0, rand.Intn(4)+2)
fields = append(fields, data.NewField("time", nil, make([]time.Time, size)))
for i := 1; i < cap(fields); i++ {
name := fmt.Sprintf("values-%d", i)
fields = append(fields, data.NewField(name, models.GenerateAlertLabels(rand.Intn(4)+1, name), make([]int64, size)))
}
frame := data.NewFrame("test", fields...)
tmili := time.Now().UnixMilli()
tmili = tmili - tmili%resolution.Milliseconds()
current := time.UnixMilli(tmili).Add(-resolution * time.Duration(size))
for i := 0; i < size; i++ {
vals := make([]any, 0, len(frame.Fields))
vals = append(vals, current)
for i := 1; i < cap(vals); i++ {
vals = append(vals, rand.Int63n(2)-1) // random value [-1,1]
}
frame.SetRow(i, vals...)
current = current.Add(resolution)
}
return frame
}
func TestDataEvaluator_New(t *testing.T) {
t.Run("should fail if frame is not TimeSeriesTypeWide", func(t *testing.T) {
t.Run("but TimeSeriesTypeNot", func(t *testing.T) {
frameTimeSeriesTypeNot := data.NewFrame("test")
require.Equal(t, data.TimeSeriesTypeNot, frameTimeSeriesTypeNot.TimeSeriesSchema().Type)
_, err := newDataEvaluator(util.GenerateShortUID(), frameTimeSeriesTypeNot)
require.Error(t, err)
})
t.Run("but TimeSeriesTypeLong", func(t *testing.T) {
frameTimeSeriesTypeLong := data.NewFrame("test", data.NewField("time", nil, make([]time.Time, 0)), data.NewField("data", nil, make([]string, 0)), data.NewField("value", nil, make([]int64, 0)))
require.Equal(t, data.TimeSeriesTypeLong, frameTimeSeriesTypeLong.TimeSeriesSchema().Type)
_, err := newDataEvaluator(util.GenerateShortUID(), frameTimeSeriesTypeLong)
require.Error(t, err)
})
})
t.Run("should convert fame to series and sort it", func(t *testing.T) {
refID := util.GenerateShortUID()
frameSize := rand.Intn(100) + 100
frame := GenerateWideSeriesFrame(frameSize, time.Second)
rand.Shuffle(frameSize, func(i, j int) {
rowi := frame.RowCopy(i)
rowj := frame.RowCopy(j)
frame.SetRow(i, rowj...)
frame.SetRow(j, rowi...)
})
e, err := newDataEvaluator(refID, frame)
require.NoError(t, err)
require.Equal(t, refID, e.refID)
require.Len(t, e.data, len(frame.Fields)-1) // timestamp is not counting
for idx, series := range e.data {
assert.Equalf(t, series.Len(), frameSize, "Length of the series %d is %d but expected to be %d", idx, series.Len(), frameSize)
assert.Equalf(t, frame.Fields[idx+1].Labels, series.GetLabels(), "Labels of series %d does not match with original field labels", idx)
assert.Lessf(t, series.GetTime(0), series.GetTime(1), "Series %d is expected to be sorted in ascending order", idx)
}
})
}
func TestDataEvaluator_Eval(t *testing.T) {
type results struct {
time time.Time
results eval.Results
}
refID := util.GenerateShortUID()
frameSize := rand.Intn(100) + 100
frame := GenerateWideSeriesFrame(frameSize, time.Second)
from := frame.At(0, 0).(time.Time)
to := frame.At(0, frame.Rows()-1).(time.Time)
evaluator, err := newDataEvaluator(refID, frame)
require.NoErrorf(t, err, "Frame %v", frame)
t.Run("should use data points when frame resolution matches evaluation interval", func(t *testing.T) {
r := make([]results, 0, frame.Rows())
interval := time.Second
resultsCount := int(to.Sub(from).Seconds() / interval.Seconds())
err = evaluator.Eval(context.Background(), from, time.Second, resultsCount, func(idx int, now time.Time, res eval.Results) error {
r = append(r, results{
now, res,
})
return nil
})
require.NoError(t, err)
require.Len(t, r, resultsCount)
t.Run("results should be in the same refID", func(t *testing.T) {
for _, res := range r {
for _, result := range res.results {
require.Contains(t, result.Values, refID)
}
}
})
t.Run("should be Alerting if value is not 0", func(t *testing.T) {
for _, res := range r {
for _, result := range res.results {
v := result.Values[refID].Value
require.NotNil(t, v)
if *v == 0 {
require.Equalf(t, eval.Normal, result.State, "Result value is %d", *v)
} else {
require.Equalf(t, eval.Alerting, result.State, "Result value is %d", *v)
}
}
}
})
t.Run("results should be in ascending order", func(t *testing.T) {
var prev = results{}
for i := 0; i < len(r); i++ {
current := r[i]
if i > 0 {
require.Less(t, prev.time, current.time)
} else {
require.Equal(t, from, current.time)
}
prev = current
}
})
t.Run("results should be in the same order as fields in frame", func(t *testing.T) {
for i := 0; i < len(r); i++ {
current := r[i]
for idx, result := range current.results {
field := frame.Fields[idx+1]
require.Equal(t, field.Labels, result.Instance)
expected, err := field.FloatAt(i)
require.NoError(t, err)
require.EqualValues(t, expected, *result.Values[refID].Value)
}
}
})
})
t.Run("when frame resolution does not match evaluation interval", func(t *testing.T) {
t.Run("should closest timestamp if interval is smaller than frame resolution", func(t *testing.T) {
interval := 300 * time.Millisecond
size := to.Sub(from).Milliseconds() / interval.Milliseconds()
r := make([]results, 0, size)
err = evaluator.Eval(context.Background(), from, interval, int(size), func(idx int, now time.Time, res eval.Results) error {
r = append(r, results{
now, res,
})
return nil
})
currentRowIdx := 0
nextTime := frame.At(0, currentRowIdx+1).(time.Time)
for id, current := range r {
if !current.time.Before(nextTime) {
currentRowIdx++
if frame.Rows() > currentRowIdx+1 {
nextTime = frame.At(0, currentRowIdx+1).(time.Time)
}
}
for idx, result := range current.results {
field := frame.Fields[idx+1]
require.Equal(t, field.Labels, result.Instance)
expected, err := field.FloatAt(currentRowIdx)
require.NoError(t, err)
require.EqualValuesf(t, expected, *result.Values[refID].Value, "Time %d", id)
}
}
})
t.Run("should downscale series if interval is smaller using previous value", func(t *testing.T) {
interval := 5 * time.Second
size := int(to.Sub(from).Seconds() / interval.Seconds())
r := make([]results, 0, size)
err = evaluator.Eval(context.Background(), from, interval, size, func(idx int, now time.Time, res eval.Results) error {
r = append(r, results{
now, res,
})
return nil
})
currentRowIdx := 0
var frameDate time.Time
for resultNum, current := range r {
for i := currentRowIdx; i < frame.Rows(); i++ {
d := frame.At(0, i).(time.Time)
if d.Equal(current.time) {
currentRowIdx = i
frameDate = d
break
}
if d.After(current.time) {
require.Fail(t, "Interval is not aligned")
}
}
for idx, result := range current.results {
field := frame.Fields[idx+1]
require.Equal(t, field.Labels, result.Instance)
expected, err := field.FloatAt(currentRowIdx)
require.NoError(t, err)
require.EqualValuesf(t, expected, *result.Values[refID].Value, "Current time [%v] frame time [%v]. Result #%d", current.time, frameDate, resultNum)
}
}
})
})
t.Run("when eval interval is larger than data", func(t *testing.T) {
t.Run("should be noData until the frame interval", func(t *testing.T) {
newFrom := from.Add(-10 * time.Second)
r := make([]results, 0, int(to.Sub(newFrom).Seconds()))
err = evaluator.Eval(context.Background(), newFrom, time.Second, cap(r), func(idx int, now time.Time, res eval.Results) error {
r = append(r, results{
now, res,
})
return nil
})
rowIdx := 0
for _, current := range r {
if current.time.Before(from) {
require.Len(t, current.results, 1)
require.Equal(t, eval.NoData, current.results[0].State)
} else {
for idx, result := range current.results {
field := frame.Fields[idx+1]
require.Equal(t, field.Labels, result.Instance)
expected, err := field.FloatAt(rowIdx)
require.NoError(t, err)
require.EqualValues(t, expected, *result.Values[refID].Value)
}
rowIdx++
}
}
})
t.Run("should be the last value after the frame interval", func(t *testing.T) {
newTo := to.Add(10 * time.Second)
r := make([]results, 0, int(newTo.Sub(from).Seconds()))
err = evaluator.Eval(context.Background(), from, time.Second, cap(r), func(idx int, now time.Time, res eval.Results) error {
r = append(r, results{
now, res,
})
return nil
})
rowIdx := 0
for _, current := range r {
for idx, result := range current.results {
field := frame.Fields[idx+1]
require.Equal(t, field.Labels, result.Instance)
expected, err := field.FloatAt(rowIdx)
require.NoError(t, err)
require.EqualValues(t, expected, *result.Values[refID].Value)
}
if current.time.Before(to) {
rowIdx++
}
}
})
})
t.Run("should stop if callback error", func(t *testing.T) {
expectedError := errors.New("error")
err = evaluator.Eval(context.Background(), from, time.Second, 6, func(idx int, now time.Time, res eval.Results) error {
if idx == 5 {
return expectedError
}
return nil
})
require.ErrorIs(t, err, expectedError)
})
}