491 lines
11 KiB
Go
491 lines
11 KiB
Go
package interpreter_test
|
|
|
|
import (
|
|
"errors"
|
|
"regexp"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/influxdata/platform/query/ast"
|
|
"github.com/influxdata/platform/query/interpreter"
|
|
"github.com/influxdata/platform/query/parser"
|
|
"github.com/influxdata/platform/query/semantic"
|
|
"github.com/influxdata/platform/query/semantic/semantictest"
|
|
"github.com/influxdata/platform/query/values"
|
|
)
|
|
|
|
var testScope = make(map[string]values.Value)
|
|
var optionScope = make(map[string]values.Value)
|
|
var testDeclarations = make(semantic.DeclarationScope)
|
|
var optionsObject = values.NewObject()
|
|
|
|
func addFunc(f *function) {
|
|
testScope[f.name] = f
|
|
testDeclarations[f.name] = semantic.NewExternalVariableDeclaration(f.name, f.t)
|
|
}
|
|
|
|
func addOption(name string, opt values.Value) {
|
|
optionScope[name] = opt
|
|
}
|
|
|
|
func init() {
|
|
addFunc(&function{
|
|
name: "fortyTwo",
|
|
t: semantic.NewFunctionType(semantic.FunctionSignature{
|
|
ReturnType: semantic.Float,
|
|
}),
|
|
call: func(args values.Object) (values.Value, error) {
|
|
return values.NewFloatValue(42.0), nil
|
|
},
|
|
hasSideEffect: false,
|
|
})
|
|
addFunc(&function{
|
|
name: "six",
|
|
t: semantic.NewFunctionType(semantic.FunctionSignature{
|
|
ReturnType: semantic.Float,
|
|
}),
|
|
call: func(args values.Object) (values.Value, error) {
|
|
return values.NewFloatValue(6.0), nil
|
|
},
|
|
hasSideEffect: false,
|
|
})
|
|
addFunc(&function{
|
|
name: "nine",
|
|
t: semantic.NewFunctionType(semantic.FunctionSignature{
|
|
ReturnType: semantic.Float,
|
|
}),
|
|
call: func(args values.Object) (values.Value, error) {
|
|
return values.NewFloatValue(9.0), nil
|
|
},
|
|
hasSideEffect: false,
|
|
})
|
|
addFunc(&function{
|
|
name: "fail",
|
|
t: semantic.NewFunctionType(semantic.FunctionSignature{
|
|
ReturnType: semantic.Bool,
|
|
}),
|
|
call: func(args values.Object) (values.Value, error) {
|
|
return nil, errors.New("fail")
|
|
},
|
|
hasSideEffect: false,
|
|
})
|
|
addFunc(&function{
|
|
name: "plusOne",
|
|
t: semantic.NewFunctionType(semantic.FunctionSignature{
|
|
Params: map[string]semantic.Type{"x": semantic.Float},
|
|
ReturnType: semantic.Float,
|
|
PipeArgument: "x",
|
|
}),
|
|
call: func(args values.Object) (values.Value, error) {
|
|
v, ok := args.Get("x")
|
|
if !ok {
|
|
return nil, errors.New("missing argument x")
|
|
}
|
|
return values.NewFloatValue(v.Float() + 1), nil
|
|
},
|
|
hasSideEffect: false,
|
|
})
|
|
addFunc(&function{
|
|
name: "sideEffect",
|
|
t: semantic.NewFunctionType(semantic.FunctionSignature{
|
|
ReturnType: semantic.Int,
|
|
}),
|
|
call: func(args values.Object) (values.Value, error) {
|
|
return values.NewIntValue(0), nil
|
|
},
|
|
hasSideEffect: true,
|
|
})
|
|
|
|
optionsObject.Set("name", values.NewStringValue("foo"))
|
|
optionsObject.Set("repeat", values.NewIntValue(100))
|
|
|
|
addOption("task", optionsObject)
|
|
}
|
|
|
|
// TestEval tests whether a program can run to completion or not
|
|
func TestEval(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
query string
|
|
wantErr bool
|
|
want []values.Value
|
|
}{
|
|
{
|
|
name: "call function",
|
|
query: "six()",
|
|
want: []values.Value{
|
|
values.NewFloatValue(6.0),
|
|
},
|
|
},
|
|
{
|
|
name: "call function with fail",
|
|
query: "fail()",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "call function with duplicate args",
|
|
query: "plusOne(x:1.0, x:2.0)",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "call function with missing args",
|
|
query: "plusOne()",
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "reassign nested scope",
|
|
query: `
|
|
six = six()
|
|
six()
|
|
`,
|
|
wantErr: true,
|
|
want: []values.Value{
|
|
values.NewFloatValue(6.0),
|
|
},
|
|
},
|
|
{
|
|
name: "binary expressions",
|
|
query: `
|
|
six = six()
|
|
nine = nine()
|
|
|
|
answer = fortyTwo() == six * nine
|
|
`,
|
|
want: []values.Value{
|
|
values.NewFloatValue(6),
|
|
values.NewFloatValue(9),
|
|
values.NewBoolValue(false),
|
|
},
|
|
},
|
|
{
|
|
name: "logical expressions short circuit",
|
|
query: `
|
|
six = six()
|
|
nine = nine()
|
|
|
|
answer = (not (fortyTwo() == six * nine)) or fail()
|
|
`,
|
|
want: []values.Value{
|
|
values.NewFloatValue(6.0),
|
|
values.NewFloatValue(9.0),
|
|
values.NewBoolValue(true),
|
|
},
|
|
},
|
|
{
|
|
name: "arrow function",
|
|
query: `
|
|
plusSix = (r) => r + six()
|
|
plusSix(r:1.0) == 7.0 or fail()
|
|
`,
|
|
},
|
|
{
|
|
name: "arrow function block",
|
|
query: `
|
|
f = (r) => {
|
|
r2 = r * r
|
|
return (r - r2) / r2
|
|
}
|
|
f(r:2.0) == -0.5 or fail()
|
|
`,
|
|
},
|
|
{
|
|
name: "arrow function with default param",
|
|
query: `
|
|
addN = (r,n=4) => r + n
|
|
addN(r:2) == 6 or fail()
|
|
addN(r:3,n:1) == 4 or fail()
|
|
`,
|
|
},
|
|
{
|
|
name: "scope closing",
|
|
query: `
|
|
x = 5
|
|
plusX = (r) => r + x
|
|
plusX(r:2) == 7 or fail()
|
|
`,
|
|
},
|
|
{
|
|
name: "scope closing mutable",
|
|
query: `
|
|
x = 5
|
|
plusX = (r) => r + x
|
|
plusX(r:2) == 7 or fail()
|
|
x = 1
|
|
plusX(r:2) == 3 or fail()
|
|
`,
|
|
},
|
|
{
|
|
name: "nested scope mutations not visible outside",
|
|
query: `
|
|
x = 5
|
|
xinc = () => {
|
|
x = x + 1
|
|
return x
|
|
}
|
|
xinc() == 6 or fail()
|
|
x == 5 or fail()
|
|
x = 1
|
|
xinc() == 2 or fail()
|
|
`,
|
|
},
|
|
{
|
|
name: "return map from func",
|
|
query: `
|
|
toMap = (a,b) => ({
|
|
a: a,
|
|
b: b,
|
|
})
|
|
m = toMap(a:1, b:false)
|
|
m.a == 1 or fail()
|
|
not m.b or fail()
|
|
`,
|
|
},
|
|
{
|
|
name: "pipe expression",
|
|
query: `
|
|
add = (a=<-,b) => a + b
|
|
one = 1
|
|
one |> add(b:2) == 3 or fail()
|
|
`,
|
|
},
|
|
{
|
|
name: "ignore pipe default",
|
|
query: `
|
|
add = (a=<-,b) => a + b
|
|
add(a:1, b:2) == 3 or fail()
|
|
`,
|
|
},
|
|
{
|
|
name: "pipe expression function",
|
|
query: `
|
|
add = (a=<-,b) => a + b
|
|
six() |> add(b:2.0) == 8.0 or fail()
|
|
`,
|
|
},
|
|
{
|
|
name: "pipe builtin function",
|
|
query: `
|
|
six() |> plusOne() == 7.0 or fail()
|
|
`,
|
|
want: []values.Value{
|
|
values.NewBoolValue(true),
|
|
},
|
|
},
|
|
{
|
|
name: "regex match",
|
|
query: `
|
|
"abba" =~ /^a.*a$/ or fail()
|
|
`,
|
|
want: []values.Value{
|
|
values.NewBoolValue(true),
|
|
},
|
|
},
|
|
{
|
|
name: "regex not match",
|
|
query: `
|
|
"abc" =~ /^a.*a$/ and fail()
|
|
`,
|
|
want: []values.Value{
|
|
values.NewBoolValue(false),
|
|
},
|
|
},
|
|
{
|
|
name: "not regex match",
|
|
query: `
|
|
"abc" !~ /^a.*a$/ or fail()
|
|
`,
|
|
want: []values.Value{
|
|
values.NewBoolValue(true),
|
|
},
|
|
},
|
|
{
|
|
name: "not regex not match",
|
|
query: `
|
|
"abba" !~ /^a.*a$/ and fail()
|
|
`,
|
|
want: []values.Value{
|
|
values.NewBoolValue(false),
|
|
},
|
|
},
|
|
{
|
|
name: "options metadata before query",
|
|
query: `
|
|
option task = {
|
|
name: "foo",
|
|
repeat: 100,
|
|
}
|
|
task.name == "foo" or fail()
|
|
task.repeat == 100 or fail()
|
|
`,
|
|
want: []values.Value{
|
|
optionsObject,
|
|
values.NewBoolValue(true),
|
|
values.NewBoolValue(true),
|
|
},
|
|
},
|
|
{
|
|
name: "query with side effects",
|
|
query: `sideEffect() == 0 or fail()`,
|
|
want: []values.Value{
|
|
values.NewIntValue(0),
|
|
values.NewBoolValue(true),
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
program, err := parser.NewAST(tc.query)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
graph, err := semantic.New(program, testDeclarations.Copy())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create new interpreter scope for each test case
|
|
itrp := interpreter.NewInterpreter(optionScope, testScope)
|
|
|
|
err = itrp.Eval(graph)
|
|
if !tc.wantErr && err != nil {
|
|
t.Fatal(err)
|
|
} else if tc.wantErr && err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
if tc.want != nil && !cmp.Equal(tc.want, itrp.SideEffects(), semantictest.CmpOptions...) {
|
|
t.Fatalf("unexpected side effect values -want/+got: \n%s", cmp.Diff(tc.want, itrp.SideEffects(), semantictest.CmpOptions...))
|
|
}
|
|
})
|
|
}
|
|
|
|
}
|
|
func TestResolver(t *testing.T) {
|
|
var got semantic.Expression
|
|
declarations := make(semantic.DeclarationScope)
|
|
f := &function{
|
|
name: "resolver",
|
|
t: semantic.NewFunctionType(semantic.FunctionSignature{
|
|
Params: map[string]semantic.Type{
|
|
"f": semantic.NewFunctionType(semantic.FunctionSignature{
|
|
Params: map[string]semantic.Type{"r": semantic.Int},
|
|
}),
|
|
},
|
|
ReturnType: semantic.Int,
|
|
}),
|
|
call: func(args values.Object) (values.Value, error) {
|
|
f, ok := args.Get("f")
|
|
if !ok {
|
|
return nil, errors.New("missing argument f")
|
|
}
|
|
resolver, ok := f.Function().(interpreter.Resolver)
|
|
if !ok {
|
|
return nil, errors.New("function cannot be resolved")
|
|
}
|
|
g, err := resolver.Resolve()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
got = g.(semantic.Expression)
|
|
return nil, nil
|
|
},
|
|
hasSideEffect: false,
|
|
}
|
|
testScope[f.name] = f
|
|
declarations[f.name] = semantic.NewExternalVariableDeclaration(f.name, f.t)
|
|
|
|
program, err := parser.NewAST(`
|
|
x = 42
|
|
resolver(f: (r) => r + x)
|
|
`)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
graph, err := semantic.New(program, testDeclarations)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
itrp := interpreter.NewInterpreter(optionScope, testScope)
|
|
|
|
if err := itrp.Eval(graph); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
want := &semantic.FunctionExpression{
|
|
Params: []*semantic.FunctionParam{{Key: &semantic.Identifier{Name: "r"}}},
|
|
Body: &semantic.BinaryExpression{
|
|
Operator: ast.AdditionOperator,
|
|
Left: &semantic.IdentifierExpression{Name: "r"},
|
|
Right: &semantic.IntegerLiteral{Value: 42},
|
|
},
|
|
}
|
|
if !cmp.Equal(want, got, semantictest.CmpOptions...) {
|
|
t.Errorf("unexpected resoved function: -want/+got\n%s", cmp.Diff(want, got, semantictest.CmpOptions...))
|
|
}
|
|
if wt, gt := want.Type(), got.Type(); wt != gt {
|
|
t.Errorf("unexpected resoved function types: want: %v got: %v", wt, gt)
|
|
}
|
|
}
|
|
|
|
type function struct {
|
|
name string
|
|
t semantic.Type
|
|
call func(args values.Object) (values.Value, error)
|
|
hasSideEffect bool
|
|
}
|
|
|
|
func (f *function) Type() semantic.Type {
|
|
return f.t
|
|
}
|
|
|
|
func (f *function) Str() string {
|
|
panic(values.UnexpectedKind(semantic.Object, semantic.String))
|
|
}
|
|
func (f *function) Int() int64 {
|
|
panic(values.UnexpectedKind(semantic.Object, semantic.Int))
|
|
}
|
|
func (f *function) UInt() uint64 {
|
|
panic(values.UnexpectedKind(semantic.Object, semantic.UInt))
|
|
}
|
|
func (f *function) Float() float64 {
|
|
panic(values.UnexpectedKind(semantic.Object, semantic.Float))
|
|
}
|
|
func (f *function) Bool() bool {
|
|
panic(values.UnexpectedKind(semantic.Object, semantic.Bool))
|
|
}
|
|
func (f *function) Time() values.Time {
|
|
panic(values.UnexpectedKind(semantic.Object, semantic.Time))
|
|
}
|
|
func (f *function) Duration() values.Duration {
|
|
panic(values.UnexpectedKind(semantic.Object, semantic.Duration))
|
|
}
|
|
func (f *function) Regexp() *regexp.Regexp {
|
|
panic(values.UnexpectedKind(semantic.Object, semantic.Regexp))
|
|
}
|
|
func (f *function) Array() values.Array {
|
|
panic(values.UnexpectedKind(semantic.Object, semantic.Function))
|
|
}
|
|
func (f *function) Object() values.Object {
|
|
panic(values.UnexpectedKind(semantic.Object, semantic.Object))
|
|
}
|
|
func (f *function) Function() values.Function {
|
|
return f
|
|
}
|
|
func (f *function) Equal(rhs values.Value) bool {
|
|
if f.Type() != rhs.Type() {
|
|
return false
|
|
}
|
|
v, ok := rhs.(*function)
|
|
return ok && (f == v)
|
|
}
|
|
func (f *function) HasSideEffect() bool {
|
|
return f.hasSideEffect
|
|
}
|
|
|
|
func (f *function) Call(args values.Object) (values.Value, error) {
|
|
return f.call(args)
|
|
}
|