package expr import ( "context" "encoding/json" "fmt" "sort" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/datasources" datafakes "github.com/grafana/grafana/pkg/services/datasources/fakes" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginconfig" "github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext" "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" ) func TestService(t *testing.T) { dsDF := data.NewFrame("test", data.NewField("time", nil, []time.Time{time.Unix(1, 0)}), data.NewField("value", data.Labels{"test": "label"}, []*float64{fp(2)})) me := &mockEndpoint{ Responses: map[string]backend.DataResponse{ "A": {Frames: data.Frames{dsDF}}, }, } pCtxProvider := plugincontext.ProvideService(setting.NewCfg(), nil, &pluginstore.FakePluginStore{ PluginList: []pluginstore.Plugin{ {JSONData: plugins.JSONData{ID: "test"}}, }, }, &datafakes.FakeCacheService{}, &datafakes.FakeDataSourceService{}, nil, pluginconfig.NewFakePluginRequestConfigProvider()) features := featuremgmt.WithFeatures() s := Service{ cfg: setting.NewCfg(), dataService: me, pCtxProvider: pCtxProvider, features: features, tracer: tracing.InitializeTracerForTest(), metrics: newMetrics(nil), converter: &ResultConverter{ Features: features, Tracer: tracing.InitializeTracerForTest(), }, } queries := []Query{ { RefID: "A", DataSource: &datasources.DataSource{ OrgID: 1, UID: "test", Type: "test", }, JSON: json.RawMessage(`{ "datasource": { "uid": "1" }, "intervalMs": 1000, "maxDataPoints": 1000 }`), TimeRange: AbsoluteTimeRange{ From: time.Time{}, To: time.Time{}, }, }, { RefID: "B", DataSource: dataSourceModel(), JSON: json.RawMessage(`{ "datasource": { "uid": "__expr__", "type": "__expr__"}, "type": "math", "expression": "$A * 2" }`), }, } req := &Request{Queries: queries, User: &user.SignedInUser{}} pl, err := s.BuildPipeline(req) require.NoError(t, err) res, err := s.ExecutePipeline(context.Background(), time.Now(), pl) require.NoError(t, err) bDF := data.NewFrame("", data.NewField("Time", nil, []time.Time{time.Unix(1, 0)}), data.NewField("B", data.Labels{"test": "label"}, []*float64{fp(4)})) bDF.RefID = "B" bDF.SetMeta(&data.FrameMeta{ Type: data.FrameTypeTimeSeriesMulti, TypeVersion: data.FrameTypeVersion{0, 1}, }) expect := &backend.QueryDataResponse{ Responses: backend.Responses{ "A": { Frames: []*data.Frame{dsDF}, }, "B": { Frames: []*data.Frame{bDF}, }, }, } // Service currently doesn't care about order of datas in the return. trans := cmp.Transformer("Sort", func(in []*data.Frame) []*data.Frame { out := append([]*data.Frame(nil), in...) // Copy input to avoid mutating it sort.SliceStable(out, func(i, j int) bool { return out[i].RefID > out[j].RefID }) return out }) options := append([]cmp.Option{trans}, data.FrameTestCompareOptions()...) if diff := cmp.Diff(expect, res, options...); diff != "" { t.Errorf("Result mismatch (-want +got):\n%s", diff) } } func TestDSQueryError(t *testing.T) { me := &mockEndpoint{ Responses: map[string]backend.DataResponse{ "A": {Error: fmt.Errorf("womp womp")}, "B": {Frames: data.Frames{}}, }, } pCtxProvider := plugincontext.ProvideService(setting.NewCfg(), nil, &pluginstore.FakePluginStore{ PluginList: []pluginstore.Plugin{ {JSONData: plugins.JSONData{ID: "test"}}, }, }, &datafakes.FakeCacheService{}, &datafakes.FakeDataSourceService{}, nil, pluginconfig.NewFakePluginRequestConfigProvider()) s := Service{ cfg: setting.NewCfg(), dataService: me, pCtxProvider: pCtxProvider, features: featuremgmt.WithFeatures(), tracer: tracing.InitializeTracerForTest(), metrics: newMetrics(nil), } queries := []Query{ { RefID: "A", DataSource: &datasources.DataSource{ OrgID: 1, UID: "test", Type: "test", }, JSON: json.RawMessage(`{ "datasource": { "uid": "1" }, "intervalMs": 1000, "maxDataPoints": 1000 }`), TimeRange: AbsoluteTimeRange{ From: time.Time{}, To: time.Time{}, }, }, { RefID: "B", DataSource: dataSourceModel(), JSON: json.RawMessage(`{ "datasource": { "uid": "__expr__", "type": "__expr__"}, "type": "math", "expression": "$A * 2" }`), }, { RefID: "C", DataSource: dataSourceModel(), JSON: json.RawMessage(`{ "datasource": { "uid": "__expr__", "type": "__expr__"}, "type": "math", "expression": "42" }`), }, } req := &Request{Queries: queries, User: &user.SignedInUser{}} pl, err := s.BuildPipeline(req) require.NoError(t, err) resp, err := s.ExecutePipeline(context.Background(), time.Now(), pl) require.NoError(t, err) var utilErr errutil.Error require.ErrorContains(t, resp.Responses["A"].Error, "womp womp") require.ErrorAs(t, resp.Responses["B"].Error, &utilErr) require.ErrorIs(t, utilErr, DependencyError) require.Equal(t, fp(42), resp.Responses["C"].Frames[0].Fields[0].At(0)) } func fp(f float64) *float64 { return &f } type mockEndpoint struct { Responses map[string]backend.DataResponse } func (me *mockEndpoint) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { resp := backend.NewQueryDataResponse() for _, ref := range req.Queries { resp.Responses[ref.RefID] = me.Responses[ref.RefID] } return resp, nil } func dataSourceModel() *datasources.DataSource { d, _ := DataSourceModelFromNodeType(TypeCMDNode) return d }