mirror of https://github.com/grafana/grafana.git
				
				
				
			
		
			
				
	
	
		
			301 lines
		
	
	
		
			8.2 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			301 lines
		
	
	
		
			8.2 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) {}
 | |
| 
 | |
| func (ts *testSpan) SpanContext() trace.SpanContext {
 | |
| 	return trace.SpanContext{}
 | |
| }
 |