Fixup on a misleading error being returned due to a missing return statement in the code. Was returning the error "conversion succeeded but no frames" even though there was an error.
297 lines
8.1 KiB
Go
297 lines
8.1 KiB
Go
package expr
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
|
"github.com/grafana/grafana/pkg/expr/mathexp"
|
|
"github.com/grafana/grafana/pkg/expr/metrics"
|
|
"github.com/prometheus/client_golang/prometheus/testutil"
|
|
"github.com/stretchr/testify/require"
|
|
"go.opentelemetry.io/otel/attribute"
|
|
"go.opentelemetry.io/otel/codes"
|
|
"go.opentelemetry.io/otel/trace"
|
|
)
|
|
|
|
func TestNewCommand(t *testing.T) {
|
|
cmd, err := NewSQLCommand(t.Context(), log.NewNullLogger(), "a", "", "select a from foo, bar", 0, 0, 0)
|
|
if err != nil && strings.Contains(err.Error(), "feature is not enabled") {
|
|
return
|
|
}
|
|
|
|
if err != nil {
|
|
t.Fail()
|
|
return
|
|
}
|
|
|
|
for _, v := range cmd.varsToQuery {
|
|
if strings.Contains("foo bar", v) {
|
|
continue
|
|
}
|
|
t.Fail()
|
|
return
|
|
}
|
|
}
|
|
|
|
// Helper function for creating test data
|
|
func createFrameWithRowsAndCols(rows int, cols int) *data.Frame {
|
|
frame := data.NewFrame("dummy")
|
|
|
|
for c := 0; c < cols; c++ {
|
|
values := make([]string, rows)
|
|
frame.Fields = append(frame.Fields, data.NewField(fmt.Sprintf("col%d", c), nil, values))
|
|
}
|
|
|
|
return frame
|
|
}
|
|
|
|
func TestSQLCommandCellLimits(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
limit int64
|
|
frames []*data.Frame
|
|
vars []string
|
|
expectError bool
|
|
errorContains string
|
|
}{
|
|
{
|
|
name: "single (long) frame within cell limit",
|
|
limit: 10,
|
|
frames: []*data.Frame{
|
|
createFrameWithRowsAndCols(10, 1), // 10 cells
|
|
},
|
|
vars: []string{"foo"},
|
|
},
|
|
{
|
|
name: "single (wide) frame within cell limit",
|
|
limit: 10,
|
|
frames: []*data.Frame{
|
|
createFrameWithRowsAndCols(1, 10), // 10 cells
|
|
},
|
|
vars: []string{"foo"},
|
|
},
|
|
{
|
|
name: "multiple frames within cell limit",
|
|
limit: 12,
|
|
frames: []*data.Frame{
|
|
createFrameWithRowsAndCols(2, 3), // 6 cells
|
|
createFrameWithRowsAndCols(2, 3), // 6 cells
|
|
},
|
|
vars: []string{"foo", "bar"},
|
|
},
|
|
{
|
|
name: "single (long) frame exceeds cell limit",
|
|
limit: 9,
|
|
frames: []*data.Frame{
|
|
createFrameWithRowsAndCols(10, 1), // 10 cells > 9 limit
|
|
},
|
|
vars: []string{"foo"},
|
|
expectError: true,
|
|
errorContains: "exceeded the configured limit",
|
|
},
|
|
{
|
|
name: "single (wide) frame exceeds cell limit",
|
|
limit: 9,
|
|
frames: []*data.Frame{
|
|
createFrameWithRowsAndCols(1, 10), // 10 cells > 9 limit
|
|
},
|
|
vars: []string{"foo"},
|
|
expectError: true,
|
|
errorContains: "exceeded the configured limit",
|
|
},
|
|
{
|
|
name: "multiple frames exceed cell limit",
|
|
limit: 11,
|
|
frames: []*data.Frame{
|
|
createFrameWithRowsAndCols(2, 3), // 6 cells
|
|
createFrameWithRowsAndCols(2, 3), // 6 cells
|
|
},
|
|
vars: []string{"foo", "bar"},
|
|
expectError: true,
|
|
errorContains: "exceeded the configured limit",
|
|
},
|
|
{
|
|
name: "limit of 0 means no limit: allow large frame",
|
|
limit: 0,
|
|
frames: []*data.Frame{
|
|
createFrameWithRowsAndCols(200000, 1), // 200,000 cells
|
|
},
|
|
vars: []string{"foo", "bar"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
cmd, err := NewSQLCommand(t.Context(), log.New(), "a", "", "select a from foo, bar", tt.limit, 0, 0)
|
|
require.NoError(t, err, "Failed to create SQL command")
|
|
|
|
vars := mathexp.Vars{}
|
|
|
|
for i, frame := range tt.frames {
|
|
vars[tt.vars[i]] = mathexp.Results{
|
|
Values: mathexp.Values{mathexp.TableData{Frame: frame}},
|
|
}
|
|
}
|
|
|
|
res, _ := cmd.Execute(context.Background(), time.Now(), vars, &testTracer{}, metrics.NewTestMetrics())
|
|
|
|
if tt.expectError {
|
|
require.Error(t, res.Error)
|
|
require.ErrorContains(t, res.Error, tt.errorContains)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSQLCommandMetrics(t *testing.T) {
|
|
// Create test metrics
|
|
m := metrics.NewTestMetrics()
|
|
|
|
// Create a command
|
|
cmd, err := NewSQLCommand(t.Context(), log.NewNullLogger(), "A", "someformat", "select * from foo", 0, 0, 0)
|
|
require.NoError(t, err)
|
|
|
|
// Execute successful command
|
|
_, err = cmd.Execute(context.Background(), time.Now(), mathexp.Vars{}, &testTracer{}, m)
|
|
require.NoError(t, err)
|
|
|
|
// Verify count metric was recorded
|
|
require.Equal(t, 1, testutil.CollectAndCount(m.SqlCommandCount), "Expected count metric to be recorded")
|
|
|
|
// Verify duration was recorded
|
|
require.Equal(t, 1, testutil.CollectAndCount(m.SqlCommandDuration), "Expected duration metric to be recorded")
|
|
|
|
// Verify cell count was recorded
|
|
require.Equal(t, 1, testutil.CollectAndCount(m.SqlCommandCellCount), "Expected cell count metric to be recorded")
|
|
}
|
|
|
|
func TestHandleSqlInput(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
frames data.Frames
|
|
expectErr string
|
|
expectFrame bool
|
|
converted bool
|
|
}{
|
|
{
|
|
name: "single frame with no fields and no type is passed through",
|
|
frames: data.Frames{data.NewFrame("")},
|
|
expectFrame: true,
|
|
},
|
|
{
|
|
name: "single frame with no fields but type timeseries-multi is passed through",
|
|
frames: data.Frames{data.NewFrame("").SetMeta(&data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti})},
|
|
expectFrame: true,
|
|
},
|
|
{
|
|
name: "single frame, no labels, no type → passes through",
|
|
frames: data.Frames{
|
|
data.NewFrame("",
|
|
data.NewField("time", nil, []time.Time{time.Unix(1, 0)}),
|
|
data.NewField("value", nil, []*float64{fp(2)}),
|
|
),
|
|
},
|
|
expectFrame: true,
|
|
},
|
|
{
|
|
name: "single frame with labels, but missing FrameMeta.Type → error",
|
|
frames: data.Frames{
|
|
data.NewFrame("",
|
|
data.NewField("time", nil, []time.Time{time.Unix(1, 0)}),
|
|
data.NewField("value", data.Labels{"foo": "bar"}, []*float64{fp(2)}),
|
|
),
|
|
},
|
|
expectErr: "labels in the response that can not be mapped to a table",
|
|
},
|
|
{
|
|
name: "multiple frames, no type → error",
|
|
frames: data.Frames{
|
|
data.NewFrame("",
|
|
data.NewField("time", nil, []time.Time{time.Unix(1, 0)}),
|
|
data.NewField("value", nil, []*float64{fp(2)}),
|
|
),
|
|
data.NewFrame("",
|
|
data.NewField("time", nil, []time.Time{time.Unix(1, 0)}),
|
|
data.NewField("value", nil, []*float64{fp(2)}),
|
|
),
|
|
},
|
|
expectErr: "more than one dataframe that can not be automatically mapped to a single table",
|
|
},
|
|
{
|
|
name: "supported type (timeseries-multi) triggers ConvertToFullLong",
|
|
frames: data.Frames{
|
|
data.NewFrame("",
|
|
data.NewField("time", nil, []time.Time{time.Unix(1, 0)}),
|
|
data.NewField("value", data.Labels{"host": "a"}, []*float64{fp(2)}),
|
|
).SetMeta(&data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti}),
|
|
},
|
|
expectFrame: true,
|
|
converted: true,
|
|
},
|
|
{
|
|
name: "supported type (timeseries-multi) but malformed returns error",
|
|
frames: data.Frames{
|
|
data.NewFrame("",
|
|
data.NewField("time", nil, []string{"1"}), // string is not valid for time field
|
|
data.NewField("value", data.Labels{"host": "a"}, []*float64{fp(2)}),
|
|
).SetMeta(&data.FrameMeta{Type: data.FrameTypeTimeSeriesMulti}),
|
|
},
|
|
expectErr: "missing time field",
|
|
converted: true,
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
res, c := handleSqlInput(t.Context(), &testTracer{}, "a", map[string]struct{}{"b": {}}, "fakeDS", tc.frames)
|
|
require.Equal(t, tc.converted, c, "conversion bool mismatch")
|
|
if tc.expectErr != "" {
|
|
require.Error(t, res.Error)
|
|
require.ErrorContains(t, res.Error, tc.expectErr)
|
|
} else {
|
|
require.NoError(t, res.Error)
|
|
if tc.expectFrame {
|
|
require.Len(t, res.Values, 1)
|
|
require.IsType(t, mathexp.TableData{}, res.Values[0])
|
|
require.NotNil(t, res.Values[0].(mathexp.TableData).Frame)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type testTracer struct {
|
|
trace.Tracer
|
|
}
|
|
|
|
func (t *testTracer) Start(ctx context.Context, name string, s ...trace.SpanStartOption) (context.Context, trace.Span) {
|
|
return ctx, &testSpan{}
|
|
}
|
|
func (t *testTracer) Inject(context.Context, http.Header, trace.Span) {
|
|
|
|
}
|
|
|
|
type testSpan struct {
|
|
trace.Span
|
|
}
|
|
|
|
func (ts *testSpan) End(opt ...trace.SpanEndOption) {
|
|
}
|
|
|
|
func (ts *testSpan) RecordError(err error, opt ...trace.EventOption) {
|
|
}
|
|
|
|
func (ts *testSpan) SetStatus(code codes.Code, msg string) {}
|
|
|
|
func (ts *testSpan) AddEvent(name string, opts ...trace.EventOption) {}
|
|
|
|
func (ts *testSpan) SetAttributes(kv ...attribute.KeyValue) {}
|