package expr import ( "encoding/json" "testing" "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/featuremgmt" ) func TestServicebuildPipeLine(t *testing.T) { var tests = []struct { name string req *Request expectedOrder []string expectErrContains string }{ { name: "simple: a requires b", req: &Request{ Queries: []Query{ { RefID: "A", DataSource: dataSourceModel(), JSON: json.RawMessage(`{ "expression": "B", "reducer": "mean", "type": "reduce" }`), }, { RefID: "B", DataSource: &datasources.DataSource{ UID: "Fake", }, TimeRange: AbsoluteTimeRange{}, }, }, }, expectedOrder: []string{"B", "A"}, }, { name: "cycle will error", req: &Request{ Queries: []Query{ { RefID: "A", DataSource: dataSourceModel(), JSON: json.RawMessage(`{ "expression": "$B", "type": "math" }`), }, { RefID: "B", DataSource: dataSourceModel(), JSON: json.RawMessage(`{ "expression": "$A", "type": "math" }`), }, }, }, expectErrContains: "cyclic components", }, { name: "self reference will error", req: &Request{ Queries: []Query{ { RefID: "A", DataSource: dataSourceModel(), JSON: json.RawMessage(`{ "expression": "$A", "type": "math" }`), }, }, }, expectErrContains: "expression 'A' cannot reference itself. Must be query or another expression", }, { name: "missing dependency will error", req: &Request{ Queries: []Query{ { RefID: "A", DataSource: dataSourceModel(), JSON: json.RawMessage(`{ "expression": "$B", "type": "math" }`), }, }, }, expectErrContains: "find dependent", }, { name: "classic can not take input from another expression", req: &Request{ Queries: []Query{ { RefID: "A", DataSource: dataSourceModel(), JSON: json.RawMessage(`{ "type": "classic_conditions", "conditions": [ { "evaluator": { "params": [ 2, 3 ], "type": "within_range" }, "operator": { "type": "or" }, "query": { "params": [ "B" ] }, "reducer": { "params": [], "type": "diff" }, "type": "query" } ] }`), }, { RefID: "B", DataSource: dataSourceModel(), JSON: json.RawMessage(`{ "expression": "C", "reducer": "mean", "type": "reduce" }`), }, { RefID: "C", DataSource: &datasources.DataSource{ UID: "Fake", }, TimeRange: AbsoluteTimeRange{}, }, }, }, expectErrContains: "only data source queries may be inputs to a classic condition", }, { name: "classic can not output to another expression", req: &Request{ Queries: []Query{ { RefID: "A", DataSource: dataSourceModel(), JSON: json.RawMessage(`{ "type": "classic_conditions", "conditions": [ { "evaluator": { "params": [ 2, 3 ], "type": "within_range" }, "operator": { "type": "or" }, "query": { "params": [ "C" ] }, "reducer": { "params": [], "type": "diff" }, "type": "query" } ] }`), }, { RefID: "B", DataSource: dataSourceModel(), JSON: json.RawMessage(`{ "expression": "A", "reducer": "mean", "type": "reduce" }`), }, { RefID: "C", DataSource: &datasources.DataSource{ UID: "Fake", }, TimeRange: AbsoluteTimeRange{}, }, }, }, expectErrContains: "classic conditions may not be the input for other expressions", }, { name: "Queries with new datasource ref object", req: &Request{ Queries: []Query{ { RefID: "A", DataSource: dataSourceModel(), JSON: json.RawMessage(`{ "expression": "B", "reducer": "mean", "type": "reduce" }`), }, { RefID: "B", DataSource: &datasources.DataSource{ UID: "Fake", }, TimeRange: AbsoluteTimeRange{}, }, }, }, expectedOrder: []string{"B", "A"}, }, } s := Service{ features: featuremgmt.WithFeatures(featuremgmt.FlagExpressionParser), } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { nodes, err := s.buildPipeline(tt.req) if tt.expectErrContains != "" { require.Error(t, err) require.Contains(t, err.Error(), tt.expectErrContains) } else { require.NoError(t, err) require.Equal(t, tt.expectedOrder, getRefIDOrder(nodes)) } }) } } func TestGetCommandsFromPipeline(t *testing.T) { pipeline := DataPipeline{ &MLNode{}, &DSNode{}, &CMDNode{ baseNode: baseNode{}, CMDType: 0, Command: &ReduceCommand{}, }, &CMDNode{ baseNode: baseNode{}, CMDType: 0, Command: &ReduceCommand{}, }, &CMDNode{ baseNode: baseNode{}, CMDType: 0, Command: &HysteresisCommand{}, }, } t.Run("should find command that exists", func(t *testing.T) { cmds := GetCommandsFromPipeline[*HysteresisCommand](pipeline) require.Len(t, cmds, 1) require.Equal(t, pipeline[4].(*CMDNode).Command, cmds[0]) }) t.Run("should find all commands that exist", func(t *testing.T) { cmds := GetCommandsFromPipeline[*ReduceCommand](pipeline) require.Len(t, cmds, 2) }) t.Run("should not find all command that does not exist", func(t *testing.T) { cmds := GetCommandsFromPipeline[*MathCommand](pipeline) require.Len(t, cmds, 0) }) } func getRefIDOrder(nodes []Node) []string { ids := make([]string, 0, len(nodes)) for _, n := range nodes { ids = append(ids, n.RefID()) } return ids }