grafana/pkg/expr/ml_test.go
Yuri Tseretyan 541bfe636d
SSE: Support for ML query node (#69963)
* introduce a new node-type ML and implement a command outlier that uses ML plugin as a source of data.
* add feature flag mlExpressions that guards the feature
2023-07-13 20:37:50 +03:00

220 lines
5.4 KiB
Go

package expr
import (
"context"
"encoding/json"
"errors"
"net/http"
"testing"
"time"
"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/expr/ml"
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
"github.com/grafana/grafana/pkg/services/user"
)
func TestMLNodeExecute(t *testing.T) {
timeNow := time.Now()
expectedOrgID := int64(123)
timeRange := RelativeTimeRange{
From: -10 * time.Hour,
To: 0,
}
request := &Request{
Headers: map[string]string{
"test": "test",
},
Debug: false,
OrgId: expectedOrgID,
Queries: nil,
User: &user.SignedInUser{
UserID: 1,
},
}
expectedResponse := &backend.CallResourceResponse{
Status: 200,
Headers: nil,
Body: []byte("test-response"),
}
pluginsClient := &recordingCallResourceHandler{
response: expectedResponse,
}
pluginCtx := &fakePluginContextProvider{
result: map[string]*backend.AppInstanceSettings{
mlPluginID: {
JSONData: json.RawMessage(`{ "initialized": true }`),
},
},
}
s := &Service{
cfg: nil,
dataService: nil,
pCtxProvider: pluginCtx,
features: nil,
pluginsClient: pluginsClient,
tracer: nil,
metrics: newMetrics(nil),
}
cmdResponse := data.NewFrame("test",
data.NewField("Time", nil, []time.Time{time.Unix(1, 0)}),
data.NewField("Value", nil, []*float64{fp(1)}),
)
cmd := &ml.FakeCommand{
Method: http.MethodPost,
Path: "/test/ml",
Payload: []byte(`{}`),
Response: &backend.QueryDataResponse{
Responses: map[string]backend.DataResponse{
"A": {
Frames: data.Frames{
cmdResponse,
},
Status: backend.StatusOK,
},
},
},
Error: nil,
}
node := &MLNode{
baseNode: baseNode{},
command: cmd,
TimeRange: timeRange,
request: request,
}
result, err := node.Execute(context.Background(), timeNow, nil, s)
require.NoError(t, err)
require.NotNil(t, result)
require.NotEmpty(t, result.Values)
t.Run("should get plugin context", func(t *testing.T) {
require.NotEmpty(t, pluginCtx.recordings)
require.Equal(t, "Get", pluginCtx.recordings[0].method)
require.Equal(t, mlPluginID, pluginCtx.recordings[0].params[0])
require.Equal(t, request.User, pluginCtx.recordings[0].params[1])
})
t.Run("should call command execute with correct parameters", func(t *testing.T) {
require.NotEmpty(t, cmd.Recordings)
rec := cmd.Recordings[0]
require.Equal(t, timeRange.AbsoluteTime(timeNow).From, rec.From)
require.Equal(t, timeRange.AbsoluteTime(timeNow).To, rec.To)
require.NotNil(t, rec.Response)
require.Equal(t, expectedResponse.Status, rec.Response.Status())
require.Equal(t, expectedResponse.Body, rec.Response.Body())
})
t.Run("should call plugin API", func(t *testing.T) {
require.NotEmpty(t, pluginsClient.recordings)
req := pluginsClient.recordings[0]
require.Equal(t, cmd.Payload, req.Body)
require.Equal(t, cmd.Path, req.Path)
require.Equal(t, cmd.Method, req.Method)
require.NotNil(t, req.PluginContext)
require.Equal(t, mlPluginID, req.PluginContext.PluginID)
t.Run("should append request headers to API call", func(t *testing.T) {
for key, value := range request.Headers {
require.Contains(t, req.Headers, key)
require.Equal(t, value, req.Headers[key][0])
}
})
})
t.Run("should fail if plugin is not installed", func(t *testing.T) {
s := &Service{
cfg: nil,
dataService: nil,
pCtxProvider: &fakePluginContextProvider{
errorResult: plugincontext.ErrPluginNotFound,
},
features: nil,
pluginsClient: nil,
tracer: nil,
metrics: nil,
}
_, err := node.Execute(context.Background(), timeNow, nil, s)
require.ErrorIs(t, err, errMLPluginDoesNotExist)
})
t.Run("should fail if plugin settings cannot be retrieved", func(t *testing.T) {
expectedErr := errors.New("test-error")
s := &Service{
cfg: nil,
dataService: nil,
pCtxProvider: &fakePluginContextProvider{
errorResult: expectedErr,
},
features: nil,
pluginsClient: nil,
tracer: nil,
metrics: nil,
}
_, err := node.Execute(context.Background(), timeNow, nil, s)
require.ErrorIs(t, err, expectedErr)
})
t.Run("should fail if plugin is not initialized", func(t *testing.T) {
s := &Service{
cfg: nil,
dataService: nil,
pCtxProvider: &fakePluginContextProvider{
result: map[string]*backend.AppInstanceSettings{
mlPluginID: {
JSONData: json.RawMessage(`{}`),
},
},
},
features: nil,
pluginsClient: nil,
tracer: nil,
metrics: nil,
}
_, err := node.Execute(context.Background(), timeNow, nil, s)
require.ErrorIs(t, err, errMLPluginDoesNotExist)
})
t.Run("should return QueryError if command failed", func(t *testing.T) {
s := &Service{
cfg: nil,
dataService: nil,
pCtxProvider: pluginCtx,
features: nil,
pluginsClient: pluginsClient,
tracer: nil,
metrics: newMetrics(nil),
}
cmd := &ml.FakeCommand{
Error: errors.New("failed to execute command"),
}
node := &MLNode{
baseNode: baseNode{},
command: cmd,
TimeRange: timeRange,
request: request,
}
_, err := node.Execute(context.Background(), timeNow, nil, s)
require.IsType(t, err, QueryError{})
require.ErrorIs(t, err, cmd.Error)
})
}