mirror of
https://github.com/grafana/grafana.git
synced 2024-12-01 13:09:22 -06:00
294 lines
9.5 KiB
Go
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)
|
|
})
|
|
}
|