mirror of
https://github.com/hanzoai/expr.git
synced 2026-09-24 16:58:58 +00:00
* Make expr.Eval faster * Add bench_call_test.go * Make parser config optional
2727 lines
50 KiB
Go
2727 lines
50 KiB
Go
package expr_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"reflect"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/expr-lang/expr/internal/testify/assert"
|
|
"github.com/expr-lang/expr/internal/testify/require"
|
|
"github.com/expr-lang/expr/types"
|
|
|
|
"github.com/expr-lang/expr"
|
|
"github.com/expr-lang/expr/ast"
|
|
"github.com/expr-lang/expr/file"
|
|
"github.com/expr-lang/expr/test/mock"
|
|
)
|
|
|
|
func ExampleEval() {
|
|
output, err := expr.Eval("greet + name", map[string]any{
|
|
"greet": "Hello, ",
|
|
"name": "world!",
|
|
})
|
|
if err != nil {
|
|
fmt.Printf("err: %v", err)
|
|
return
|
|
}
|
|
|
|
fmt.Printf("%v", output)
|
|
|
|
// Output: Hello, world!
|
|
}
|
|
|
|
func ExampleEval_runtime_error() {
|
|
_, err := expr.Eval(`map(1..3, {1 % (# - 3)})`, nil)
|
|
fmt.Print(err)
|
|
|
|
// Output: runtime error: integer divide by zero (1:14)
|
|
// | map(1..3, {1 % (# - 3)})
|
|
// | .............^
|
|
}
|
|
|
|
func ExampleCompile() {
|
|
env := map[string]any{
|
|
"foo": 1,
|
|
"bar": 99,
|
|
}
|
|
|
|
program, err := expr.Compile("foo in 1..99 and bar in 1..99", expr.Env(env))
|
|
if err != nil {
|
|
fmt.Printf("%v", err)
|
|
return
|
|
}
|
|
|
|
output, err := expr.Run(program, env)
|
|
if err != nil {
|
|
fmt.Printf("%v", err)
|
|
return
|
|
}
|
|
|
|
fmt.Printf("%v", output)
|
|
|
|
// Output: true
|
|
}
|
|
|
|
func ExampleEnv() {
|
|
type Segment struct {
|
|
Origin string
|
|
}
|
|
type Passengers struct {
|
|
Adults int
|
|
}
|
|
type Meta struct {
|
|
Tags map[string]string
|
|
}
|
|
type Env struct {
|
|
Meta
|
|
Segments []*Segment
|
|
Passengers *Passengers
|
|
Marker string
|
|
}
|
|
|
|
code := `all(Segments, {.Origin == "MOW"}) && Passengers.Adults > 0 && Tags["foo"] startsWith "bar"`
|
|
|
|
program, err := expr.Compile(code, expr.Env(Env{}))
|
|
if err != nil {
|
|
fmt.Printf("%v", err)
|
|
return
|
|
}
|
|
|
|
env := Env{
|
|
Meta: Meta{
|
|
Tags: map[string]string{
|
|
"foo": "bar",
|
|
},
|
|
},
|
|
Segments: []*Segment{
|
|
{Origin: "MOW"},
|
|
},
|
|
Passengers: &Passengers{
|
|
Adults: 2,
|
|
},
|
|
Marker: "test",
|
|
}
|
|
|
|
output, err := expr.Run(program, env)
|
|
if err != nil {
|
|
fmt.Printf("%v", err)
|
|
return
|
|
}
|
|
|
|
fmt.Printf("%v", output)
|
|
|
|
// Output: true
|
|
}
|
|
|
|
func ExampleEnv_tagged_field_names() {
|
|
env := struct {
|
|
FirstWord string
|
|
Separator string `expr:"Space"`
|
|
SecondWord string `expr:"second_word"`
|
|
}{
|
|
FirstWord: "Hello",
|
|
Separator: " ",
|
|
SecondWord: "World",
|
|
}
|
|
|
|
output, err := expr.Eval(`FirstWord + Space + second_word`, env)
|
|
if err != nil {
|
|
fmt.Printf("%v", err)
|
|
return
|
|
}
|
|
|
|
fmt.Printf("%v", output)
|
|
|
|
// Output : Hello World
|
|
}
|
|
|
|
func ExampleAsKind() {
|
|
program, err := expr.Compile("{a: 1, b: 2}", expr.AsKind(reflect.Map))
|
|
if err != nil {
|
|
fmt.Printf("%v", err)
|
|
return
|
|
}
|
|
|
|
output, err := expr.Run(program, nil)
|
|
if err != nil {
|
|
fmt.Printf("%v", err)
|
|
return
|
|
}
|
|
|
|
fmt.Printf("%v", output)
|
|
|
|
// Output: map[a:1 b:2]
|
|
}
|
|
|
|
func ExampleAsBool() {
|
|
env := map[string]int{
|
|
"foo": 0,
|
|
}
|
|
|
|
program, err := expr.Compile("foo >= 0", expr.Env(env), expr.AsBool())
|
|
if err != nil {
|
|
fmt.Printf("%v", err)
|
|
return
|
|
}
|
|
|
|
output, err := expr.Run(program, env)
|
|
if err != nil {
|
|
fmt.Printf("%v", err)
|
|
return
|
|
}
|
|
|
|
fmt.Printf("%v", output.(bool))
|
|
|
|
// Output: true
|
|
}
|
|
|
|
func ExampleAsBool_error() {
|
|
env := map[string]any{
|
|
"foo": 0,
|
|
}
|
|
|
|
_, err := expr.Compile("foo + 42", expr.Env(env), expr.AsBool())
|
|
|
|
fmt.Printf("%v", err)
|
|
|
|
// Output: expected bool, but got int
|
|
}
|
|
|
|
func ExampleAsInt() {
|
|
program, err := expr.Compile("42", expr.AsInt())
|
|
if err != nil {
|
|
fmt.Printf("%v", err)
|
|
return
|
|
}
|
|
|
|
output, err := expr.Run(program, nil)
|
|
if err != nil {
|
|
fmt.Printf("%v", err)
|
|
return
|
|
}
|
|
|
|
fmt.Printf("%T(%v)", output, output)
|
|
|
|
// Output: int(42)
|
|
}
|
|
|
|
func ExampleAsInt64() {
|
|
env := map[string]any{
|
|
"rating": 5.5,
|
|
}
|
|
|
|
program, err := expr.Compile("rating", expr.Env(env), expr.AsInt64())
|
|
if err != nil {
|
|
fmt.Printf("%v", err)
|
|
return
|
|
}
|
|
|
|
output, err := expr.Run(program, env)
|
|
if err != nil {
|
|
fmt.Printf("%v", err)
|
|
return
|
|
}
|
|
|
|
fmt.Printf("%v", output.(int64))
|
|
|
|
// Output: 5
|
|
}
|
|
|
|
func ExampleAsFloat64() {
|
|
program, err := expr.Compile("42", expr.AsFloat64())
|
|
if err != nil {
|
|
fmt.Printf("%v", err)
|
|
return
|
|
}
|
|
|
|
output, err := expr.Run(program, nil)
|
|
if err != nil {
|
|
fmt.Printf("%v", err)
|
|
return
|
|
}
|
|
|
|
fmt.Printf("%v", output.(float64))
|
|
|
|
// Output: 42
|
|
}
|
|
|
|
func ExampleAsFloat64_error() {
|
|
_, err := expr.Compile(`!!true`, expr.AsFloat64())
|
|
|
|
fmt.Printf("%v", err)
|
|
|
|
// Output: expected float64, but got bool
|
|
}
|
|
|
|
func ExampleWarnOnAny() {
|
|
// Arrays always have []any type. The expression return type is any.
|
|
// AsInt() instructs compiler to expect int or any, and cast to int,
|
|
// if possible. WarnOnAny() instructs to return an error on any type.
|
|
_, err := expr.Compile(`[42, true, "yes"][0]`, expr.AsInt(), expr.WarnOnAny())
|
|
|
|
fmt.Printf("%v", err)
|
|
|
|
// Output: expected int, but got interface {}
|
|
}
|
|
|
|
func ExampleOperator() {
|
|
code := `
|
|
Now() > CreatedAt &&
|
|
(Now() - CreatedAt).Hours() > 24
|
|
`
|
|
|
|
type Env struct {
|
|
CreatedAt time.Time
|
|
Now func() time.Time
|
|
Sub func(a, b time.Time) time.Duration
|
|
After func(a, b time.Time) bool
|
|
}
|
|
|
|
options := []expr.Option{
|
|
expr.Env(Env{}),
|
|
expr.Operator(">", "After"),
|
|
expr.Operator("-", "Sub"),
|
|
}
|
|
|
|
program, err := expr.Compile(code, options...)
|
|
if err != nil {
|
|
fmt.Printf("%v", err)
|
|
return
|
|
}
|
|
|
|
env := Env{
|
|
CreatedAt: time.Date(2018, 7, 14, 0, 0, 0, 0, time.UTC),
|
|
Now: func() time.Time { return time.Now() },
|
|
Sub: func(a, b time.Time) time.Duration { return a.Sub(b) },
|
|
After: func(a, b time.Time) bool { return a.After(b) },
|
|
}
|
|
|
|
output, err := expr.Run(program, env)
|
|
if err != nil {
|
|
fmt.Printf("%v", err)
|
|
return
|
|
}
|
|
|
|
fmt.Printf("%v", output)
|
|
|
|
// Output: true
|
|
}
|
|
|
|
func ExampleOperator_with_decimal() {
|
|
type Decimal struct{ N float64 }
|
|
code := `A + B - C`
|
|
|
|
type Env struct {
|
|
A, B, C Decimal
|
|
Sub func(a, b Decimal) Decimal
|
|
Add func(a, b Decimal) Decimal
|
|
}
|
|
|
|
options := []expr.Option{
|
|
expr.Env(Env{}),
|
|
expr.Operator("+", "Add"),
|
|
expr.Operator("-", "Sub"),
|
|
}
|
|
|
|
program, err := expr.Compile(code, options...)
|
|
if err != nil {
|
|
fmt.Printf("Compile error: %v", err)
|
|
return
|
|
}
|
|
|
|
env := Env{
|
|
A: Decimal{3},
|
|
B: Decimal{2},
|
|
C: Decimal{1},
|
|
Sub: func(a, b Decimal) Decimal { return Decimal{a.N - b.N} },
|
|
Add: func(a, b Decimal) Decimal { return Decimal{a.N + b.N} },
|
|
}
|
|
|
|
output, err := expr.Run(program, env)
|
|
if err != nil {
|
|
fmt.Printf("%v", err)
|
|
return
|
|
}
|
|
|
|
fmt.Printf("%v", output)
|
|
|
|
// Output: {4}
|
|
}
|
|
|
|
func fib(n int) int {
|
|
if n <= 1 {
|
|
return n
|
|
}
|
|
return fib(n-1) + fib(n-2)
|
|
}
|
|
|
|
func ExampleConstExpr() {
|
|
code := `[fib(5), fib(3+3), fib(dyn)]`
|
|
|
|
env := map[string]any{
|
|
"fib": fib,
|
|
"dyn": 0,
|
|
}
|
|
|
|
options := []expr.Option{
|
|
expr.Env(env),
|
|
expr.ConstExpr("fib"), // Mark fib func as constant expression.
|
|
}
|
|
|
|
program, err := expr.Compile(code, options...)
|
|
if err != nil {
|
|
fmt.Printf("%v", err)
|
|
return
|
|
}
|
|
|
|
// Only fib(5) and fib(6) calculated on Compile, fib(dyn) can be called at runtime.
|
|
env["dyn"] = 7
|
|
|
|
output, err := expr.Run(program, env)
|
|
if err != nil {
|
|
fmt.Printf("%v", err)
|
|
return
|
|
}
|
|
|
|
fmt.Printf("%v\n", output)
|
|
|
|
// Output: [5 8 13]
|
|
}
|
|
|
|
func ExampleAllowUndefinedVariables() {
|
|
code := `name == nil ? "Hello, world!" : sprintf("Hello, %v!", name)`
|
|
|
|
env := map[string]any{
|
|
"sprintf": fmt.Sprintf,
|
|
}
|
|
|
|
options := []expr.Option{
|
|
expr.Env(env),
|
|
expr.AllowUndefinedVariables(), // Allow to use undefined variables.
|
|
}
|
|
|
|
program, err := expr.Compile(code, options...)
|
|
if err != nil {
|
|
fmt.Printf("%v", err)
|
|
return
|
|
}
|
|
|
|
output, err := expr.Run(program, env)
|
|
if err != nil {
|
|
fmt.Printf("%v", err)
|
|
return
|
|
}
|
|
fmt.Printf("%v\n", output)
|
|
|
|
env["name"] = "you" // Define variables later on.
|
|
|
|
output, err = expr.Run(program, env)
|
|
if err != nil {
|
|
fmt.Printf("%v", err)
|
|
return
|
|
}
|
|
fmt.Printf("%v\n", output)
|
|
|
|
// Output: Hello, world!
|
|
// Hello, you!
|
|
}
|
|
|
|
func ExampleAllowUndefinedVariables_zero_value() {
|
|
code := `name == "" ? foo + bar : foo + name`
|
|
|
|
// If environment has different zero values, then undefined variables
|
|
// will have it as default value.
|
|
env := map[string]string{}
|
|
|
|
options := []expr.Option{
|
|
expr.Env(env),
|
|
expr.AllowUndefinedVariables(), // Allow to use undefined variables.
|
|
}
|
|
|
|
program, err := expr.Compile(code, options...)
|
|
if err != nil {
|
|
fmt.Printf("%v", err)
|
|
return
|
|
}
|
|
|
|
env = map[string]string{
|
|
"foo": "Hello, ",
|
|
"bar": "world!",
|
|
}
|
|
|
|
output, err := expr.Run(program, env)
|
|
if err != nil {
|
|
fmt.Printf("%v", err)
|
|
return
|
|
}
|
|
fmt.Printf("%v", output)
|
|
|
|
// Output: Hello, world!
|
|
}
|
|
|
|
func ExampleAllowUndefinedVariables_zero_value_functions() {
|
|
code := `words == "" ? Split("foo,bar", ",") : Split(words, ",")`
|
|
|
|
// Env is map[string]string type on which methods are defined.
|
|
env := mock.MapStringStringEnv{}
|
|
|
|
options := []expr.Option{
|
|
expr.Env(env),
|
|
expr.AllowUndefinedVariables(), // Allow to use undefined variables.
|
|
}
|
|
|
|
program, err := expr.Compile(code, options...)
|
|
if err != nil {
|
|
fmt.Printf("%v", err)
|
|
return
|
|
}
|
|
|
|
output, err := expr.Run(program, env)
|
|
if err != nil {
|
|
fmt.Printf("%v", err)
|
|
return
|
|
}
|
|
fmt.Printf("%v", output)
|
|
|
|
// Output: [foo bar]
|
|
}
|
|
|
|
type patcher struct{}
|
|
|
|
func (p *patcher) Visit(node *ast.Node) {
|
|
switch n := (*node).(type) {
|
|
case *ast.MemberNode:
|
|
ast.Patch(node, &ast.CallNode{
|
|
Callee: &ast.IdentifierNode{Value: "get"},
|
|
Arguments: []ast.Node{n.Node, n.Property},
|
|
})
|
|
}
|
|
}
|
|
|
|
func ExamplePatch() {
|
|
/*
|
|
type patcher struct{}
|
|
|
|
func (p *patcher) Visit(node *ast.Node) {
|
|
switch n := (*node).(type) {
|
|
case *ast.MemberNode:
|
|
ast.Patch(node, &ast.CallNode{
|
|
Callee: &ast.IdentifierNode{Value: "get"},
|
|
Arguments: []ast.Node{n.Node, n.Property},
|
|
})
|
|
}
|
|
}
|
|
*/
|
|
|
|
program, err := expr.Compile(
|
|
`greet.you.world + "!"`,
|
|
expr.Patch(&patcher{}),
|
|
)
|
|
if err != nil {
|
|
fmt.Printf("%v", err)
|
|
return
|
|
}
|
|
|
|
env := map[string]any{
|
|
"greet": "Hello",
|
|
"get": func(a, b string) string {
|
|
return a + ", " + b
|
|
},
|
|
}
|
|
|
|
output, err := expr.Run(program, env)
|
|
if err != nil {
|
|
fmt.Printf("%v", err)
|
|
return
|
|
}
|
|
fmt.Printf("%v", output)
|
|
|
|
// Output : Hello, you, world!
|
|
}
|
|
|
|
func ExampleWithContext() {
|
|
env := map[string]any{
|
|
"fn": func(ctx context.Context, _, _ int) int {
|
|
// An infinite loop that can be canceled by context.
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return 42
|
|
}
|
|
}
|
|
},
|
|
"ctx": context.TODO(), // Context should be passed as a variable.
|
|
}
|
|
|
|
program, err := expr.Compile(`fn(1, 2)`,
|
|
expr.Env(env),
|
|
expr.WithContext("ctx"), // Pass context variable name.
|
|
)
|
|
if err != nil {
|
|
fmt.Printf("%v", err)
|
|
return
|
|
}
|
|
|
|
// Cancel context after 100 milliseconds.
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100)
|
|
defer cancel()
|
|
|
|
// After program is compiled, context can be passed to Run.
|
|
env["ctx"] = ctx
|
|
|
|
// Run will return 42 after 100 milliseconds.
|
|
output, err := expr.Run(program, env)
|
|
if err != nil {
|
|
fmt.Printf("%v", err)
|
|
return
|
|
}
|
|
|
|
fmt.Printf("%v", output)
|
|
// Output: 42
|
|
}
|
|
|
|
func ExampleTimezone() {
|
|
program, err := expr.Compile(`now().Location().String()`, expr.Timezone("Asia/Kamchatka"))
|
|
if err != nil {
|
|
fmt.Printf("%v", err)
|
|
return
|
|
}
|
|
|
|
output, err := expr.Run(program, nil)
|
|
if err != nil {
|
|
fmt.Printf("%v", err)
|
|
return
|
|
}
|
|
|
|
fmt.Printf("%v", output)
|
|
// Output: Asia/Kamchatka
|
|
}
|
|
|
|
func TestExpr_readme_example(t *testing.T) {
|
|
env := map[string]any{
|
|
"greet": "Hello, %v!",
|
|
"names": []string{"world", "you"},
|
|
"sprintf": fmt.Sprintf,
|
|
}
|
|
|
|
code := `sprintf(greet, names[0])`
|
|
|
|
program, err := expr.Compile(code, expr.Env(env))
|
|
require.NoError(t, err)
|
|
|
|
output, err := expr.Run(program, env)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, "Hello, world!", output)
|
|
}
|
|
|
|
func TestExpr(t *testing.T) {
|
|
date := time.Date(2017, time.October, 23, 18, 30, 0, 0, time.UTC)
|
|
oneDay, _ := time.ParseDuration("24h")
|
|
timeNowPlusOneDay := date.Add(oneDay)
|
|
|
|
env := mock.Env{
|
|
Embed: mock.Embed{},
|
|
Ambiguous: "",
|
|
Any: nil,
|
|
Bool: true,
|
|
Float: 0,
|
|
Int64: 0,
|
|
Int32: 0,
|
|
Int: 0,
|
|
One: 1,
|
|
Two: 2,
|
|
Uint32: 0,
|
|
String: "string",
|
|
BoolPtr: nil,
|
|
FloatPtr: nil,
|
|
IntPtr: nil,
|
|
IntPtrPtr: nil,
|
|
StringPtr: nil,
|
|
Foo: mock.Foo{
|
|
Value: "foo",
|
|
Bar: mock.Bar{
|
|
Baz: "baz",
|
|
},
|
|
},
|
|
Abstract: nil,
|
|
ArrayOfAny: nil,
|
|
ArrayOfInt: []int{1, 2, 3, 4, 5},
|
|
ArrayOfFoo: []*mock.Foo{{Value: "foo"}, {Value: "bar"}, {Value: "baz"}},
|
|
MapOfFoo: nil,
|
|
MapOfAny: nil,
|
|
FuncParam: nil,
|
|
FuncParamAny: nil,
|
|
FuncTooManyReturns: nil,
|
|
FuncNamed: nil,
|
|
NilAny: nil,
|
|
NilFn: nil,
|
|
NilStruct: nil,
|
|
Variadic: func(head int, xs ...int) bool {
|
|
sum := 0
|
|
for _, x := range xs {
|
|
sum += x
|
|
}
|
|
return head == sum
|
|
},
|
|
Fast: nil,
|
|
Time: date,
|
|
TimePlusDay: timeNowPlusOneDay,
|
|
Duration: oneDay,
|
|
}
|
|
|
|
tests := []struct {
|
|
code string
|
|
want any
|
|
}{
|
|
{
|
|
`1`,
|
|
1,
|
|
},
|
|
{
|
|
`-.5`,
|
|
-.5,
|
|
},
|
|
{
|
|
`true && false || false`,
|
|
false,
|
|
},
|
|
{
|
|
`Int == 0 && Int32 == 0 && Int64 == 0 && Float64 == 0 && Bool && String == "string"`,
|
|
true,
|
|
},
|
|
{
|
|
`-Int64 == 0`,
|
|
true,
|
|
},
|
|
{
|
|
`"a" != "b"`,
|
|
true,
|
|
},
|
|
{
|
|
`"a" != "b" || 1 == 2`,
|
|
true,
|
|
},
|
|
{
|
|
`Int + 0`,
|
|
0,
|
|
},
|
|
{
|
|
`Uint64 + 0`,
|
|
0,
|
|
},
|
|
{
|
|
`Uint64 + Int64`,
|
|
0,
|
|
},
|
|
{
|
|
`Int32 + Int64`,
|
|
0,
|
|
},
|
|
{
|
|
`Float64 + 0`,
|
|
float64(0),
|
|
},
|
|
{
|
|
`0 + Float64`,
|
|
float64(0),
|
|
},
|
|
{
|
|
`0 <= Float64`,
|
|
true,
|
|
},
|
|
{
|
|
`Float64 < 1`,
|
|
true,
|
|
},
|
|
{
|
|
`Int < 1`,
|
|
true,
|
|
},
|
|
{
|
|
`2 + 2 == 4`,
|
|
true,
|
|
},
|
|
{
|
|
`8 % 3`,
|
|
2,
|
|
},
|
|
{
|
|
`2 ** 8`,
|
|
float64(256),
|
|
},
|
|
{
|
|
`2 ^ 8`,
|
|
float64(256),
|
|
},
|
|
{
|
|
`-(2-5)**3-2/(+4-3)+-2`,
|
|
float64(23),
|
|
},
|
|
{
|
|
`"hello" + " " + "world"`,
|
|
"hello world",
|
|
},
|
|
{
|
|
`0 in -1..1 and 1 in 1..1`,
|
|
true,
|
|
},
|
|
{
|
|
`Int in 0..1`,
|
|
true,
|
|
},
|
|
{
|
|
`Int32 in 0..1`,
|
|
true,
|
|
},
|
|
{
|
|
`Int64 in 0..1`,
|
|
true,
|
|
},
|
|
{
|
|
`1 in [1, 2, 3] && "foo" in {foo: 0, bar: 1} && "Bar" in Foo`,
|
|
true,
|
|
},
|
|
{
|
|
`1 in [1.5] || 1 not in [1]`,
|
|
false,
|
|
},
|
|
{
|
|
`One in 0..1 && Two not in 0..1`,
|
|
true,
|
|
},
|
|
{
|
|
`Two not in 0..1`,
|
|
true,
|
|
},
|
|
{
|
|
`Two not in 0..1`,
|
|
true,
|
|
},
|
|
{
|
|
`-1 not in [1]`,
|
|
true,
|
|
},
|
|
{
|
|
`Int32 in [10, 20]`,
|
|
false,
|
|
},
|
|
{
|
|
`String matches "s.+"`,
|
|
true,
|
|
},
|
|
{
|
|
`String matches ("^" + String + "$")`,
|
|
true,
|
|
},
|
|
{
|
|
`'foo' + 'bar' not matches 'foobar'`,
|
|
false,
|
|
},
|
|
{
|
|
`"foobar" contains "bar"`,
|
|
true,
|
|
},
|
|
{
|
|
`"foobar" startsWith "foo"`,
|
|
true,
|
|
},
|
|
{
|
|
`"foobar" endsWith "bar"`,
|
|
true,
|
|
},
|
|
{
|
|
`(0..10)[5]`,
|
|
5,
|
|
},
|
|
{
|
|
`Foo.Bar.Baz`,
|
|
"baz",
|
|
},
|
|
{
|
|
`Add(10, 5) + GetInt()`,
|
|
15,
|
|
},
|
|
{
|
|
`Foo.Method().Baz`,
|
|
`baz (from Foo.Method)`,
|
|
},
|
|
{
|
|
`Foo.MethodWithArgs("prefix ")`,
|
|
"prefix foo",
|
|
},
|
|
{
|
|
`len([1, 2, 3])`,
|
|
3,
|
|
},
|
|
{
|
|
`len([1, Two, 3])`,
|
|
3,
|
|
},
|
|
{
|
|
`len(["hello", "world"])`,
|
|
2,
|
|
},
|
|
{
|
|
`len("hello, world")`,
|
|
12,
|
|
},
|
|
{
|
|
`len('北京')`,
|
|
2,
|
|
},
|
|
{
|
|
`len('👍🏻')`, // one grapheme cluster, two code points
|
|
2,
|
|
},
|
|
{
|
|
`len('👍')`, // one grapheme cluster, one code point
|
|
1,
|
|
},
|
|
{
|
|
`len(ArrayOfInt)`,
|
|
5,
|
|
},
|
|
{
|
|
`len({a: 1, b: 2, c: 2})`,
|
|
3,
|
|
},
|
|
{
|
|
`max([1, 2, 3])`,
|
|
3,
|
|
},
|
|
{
|
|
`max(1, 2, 3)`,
|
|
3,
|
|
},
|
|
{
|
|
`min([1, 2, 3])`,
|
|
1,
|
|
},
|
|
{
|
|
`min(1, 2, 3)`,
|
|
1,
|
|
},
|
|
{
|
|
`{foo: 0, bar: 1}`,
|
|
map[string]any{"foo": 0, "bar": 1},
|
|
},
|
|
{
|
|
`{foo: 0, bar: 1}`,
|
|
map[string]any{"foo": 0, "bar": 1},
|
|
},
|
|
{
|
|
`(true ? 0+1 : 2+3) + (false ? -1 : -2)`,
|
|
-1,
|
|
},
|
|
{
|
|
`filter(1..9, {# > 7})`,
|
|
[]any{8, 9},
|
|
},
|
|
{
|
|
`map(1..3, {# * #})`,
|
|
[]any{1, 4, 9},
|
|
},
|
|
{
|
|
`all(1..3, {# > 0})`,
|
|
true,
|
|
},
|
|
{
|
|
`count(1..30, {# % 3 == 0})`,
|
|
10,
|
|
},
|
|
{
|
|
`count([true, true, false])`,
|
|
2,
|
|
},
|
|
{
|
|
`"a" < "b"`,
|
|
true,
|
|
},
|
|
{
|
|
`Time.Sub(Time).String() == "0s"`,
|
|
true,
|
|
},
|
|
{
|
|
`1 + 1`,
|
|
2,
|
|
},
|
|
{
|
|
`(One * Two) * 3 == One * (Two * 3)`,
|
|
true,
|
|
},
|
|
{
|
|
`ArrayOfInt[1]`,
|
|
2,
|
|
},
|
|
{
|
|
`ArrayOfInt[0] < ArrayOfInt[1]`,
|
|
true,
|
|
},
|
|
{
|
|
`ArrayOfInt[-1]`,
|
|
5,
|
|
},
|
|
{
|
|
`ArrayOfInt[1:2]`,
|
|
[]int{2},
|
|
},
|
|
{
|
|
`ArrayOfInt[1:4]`,
|
|
[]int{2, 3, 4},
|
|
},
|
|
{
|
|
`ArrayOfInt[-4:-1]`,
|
|
[]int{2, 3, 4},
|
|
},
|
|
{
|
|
`ArrayOfInt[:3]`,
|
|
[]int{1, 2, 3},
|
|
},
|
|
{
|
|
`ArrayOfInt[3:]`,
|
|
[]int{4, 5},
|
|
},
|
|
{
|
|
`ArrayOfInt[0:5] == ArrayOfInt`,
|
|
true,
|
|
},
|
|
{
|
|
`ArrayOfInt[0:] == ArrayOfInt`,
|
|
true,
|
|
},
|
|
{
|
|
`ArrayOfInt[:5] == ArrayOfInt`,
|
|
true,
|
|
},
|
|
{
|
|
`ArrayOfInt[:] == ArrayOfInt`,
|
|
true,
|
|
},
|
|
{
|
|
`4 in 5..1`,
|
|
false,
|
|
},
|
|
{
|
|
`4..0`,
|
|
[]int{},
|
|
},
|
|
{
|
|
`NilStruct`,
|
|
(*mock.Foo)(nil),
|
|
},
|
|
{
|
|
`NilAny == nil && nil == NilAny && nil == nil && NilAny == NilAny && NilInt == nil && NilSlice == nil && NilStruct == nil`,
|
|
true,
|
|
},
|
|
{
|
|
`0 == nil || "str" == nil || true == nil`,
|
|
false,
|
|
},
|
|
{
|
|
`Variadic(6, 1, 2, 3)`,
|
|
true,
|
|
},
|
|
{
|
|
`Variadic(0)`,
|
|
true,
|
|
},
|
|
{
|
|
`String[:]`,
|
|
"string",
|
|
},
|
|
{
|
|
`String[:3]`,
|
|
"str",
|
|
},
|
|
{
|
|
`String[:9]`,
|
|
"string",
|
|
},
|
|
{
|
|
`String[3:9]`,
|
|
"ing",
|
|
},
|
|
{
|
|
`String[7:9]`,
|
|
"",
|
|
},
|
|
{
|
|
`map(filter(ArrayOfInt, # >= 3), # + 1)`,
|
|
[]any{4, 5, 6},
|
|
},
|
|
{
|
|
`Time < Time + Duration`,
|
|
true,
|
|
},
|
|
{
|
|
`Time + Duration > Time`,
|
|
true,
|
|
},
|
|
{
|
|
`Time == Time`,
|
|
true,
|
|
},
|
|
{
|
|
`Time >= Time`,
|
|
true,
|
|
},
|
|
{
|
|
`Time <= Time`,
|
|
true,
|
|
},
|
|
{
|
|
`Time == Time + Duration`,
|
|
false,
|
|
},
|
|
{
|
|
`Time != Time`,
|
|
false,
|
|
},
|
|
{
|
|
`TimePlusDay - Duration`,
|
|
date,
|
|
},
|
|
{
|
|
`duration("1h") == duration("1h")`,
|
|
true,
|
|
},
|
|
{
|
|
`TimePlusDay - Time >= duration("24h")`,
|
|
true,
|
|
},
|
|
{
|
|
`duration("1h") > duration("1m")`,
|
|
true,
|
|
},
|
|
{
|
|
`duration("1h") < duration("1m")`,
|
|
false,
|
|
},
|
|
{
|
|
`duration("1h") >= duration("1m")`,
|
|
true,
|
|
},
|
|
{
|
|
`duration("1h") <= duration("1m")`,
|
|
false,
|
|
},
|
|
{
|
|
`duration("1h") > duration("1m")`,
|
|
true,
|
|
},
|
|
{
|
|
`duration("1h") + duration("1m")`,
|
|
time.Hour + time.Minute,
|
|
},
|
|
{
|
|
`duration("1h") - duration("1m")`,
|
|
time.Hour - time.Minute,
|
|
},
|
|
{
|
|
`7 * duration("1h")`,
|
|
7 * time.Hour,
|
|
},
|
|
{
|
|
`duration("1h") * 7`,
|
|
7 * time.Hour,
|
|
},
|
|
{
|
|
`duration("1s") * .5`,
|
|
5e8,
|
|
},
|
|
{
|
|
`1 /* one */ + 2 // two`,
|
|
3,
|
|
},
|
|
{
|
|
`let x = 1; x + 2`,
|
|
3,
|
|
},
|
|
{
|
|
`map(1..3, let x = #; let y = x * x; y * y)`,
|
|
[]any{1, 16, 81},
|
|
},
|
|
{
|
|
`map(1..2, let x = #; map(2..3, let y = #; x + y))`,
|
|
[]any{[]any{3, 4}, []any{4, 5}},
|
|
},
|
|
{
|
|
`len(filter(1..99, # % 7 == 0))`,
|
|
14,
|
|
},
|
|
{
|
|
`find(ArrayOfFoo, .Value == "baz")`,
|
|
env.ArrayOfFoo[2],
|
|
},
|
|
{
|
|
`findIndex(ArrayOfFoo, .Value == "baz")`,
|
|
2,
|
|
},
|
|
{
|
|
`filter(ArrayOfFoo, .Value == "baz")[0]`,
|
|
env.ArrayOfFoo[2],
|
|
},
|
|
{
|
|
`first(filter(ArrayOfFoo, .Value == "baz"))`,
|
|
env.ArrayOfFoo[2],
|
|
},
|
|
{
|
|
`first(filter(ArrayOfFoo, false))`,
|
|
nil,
|
|
},
|
|
{
|
|
`findLast(1..9, # % 2 == 0)`,
|
|
8,
|
|
},
|
|
{
|
|
`findLastIndex(1..9, # % 2 == 0)`,
|
|
7,
|
|
},
|
|
{
|
|
`filter(1..9, # % 2 == 0)[-1]`,
|
|
8,
|
|
},
|
|
{
|
|
`last(filter(1..9, # % 2 == 0))`,
|
|
8,
|
|
},
|
|
{
|
|
`map(filter(1..9, # % 2 == 0), # * 2)`,
|
|
[]any{4, 8, 12, 16},
|
|
},
|
|
{
|
|
`map(map(filter(1..9, # % 2 == 0), # * 2), # * 2)`,
|
|
[]any{8, 16, 24, 32},
|
|
},
|
|
{
|
|
`first(map(filter(1..9, # % 2 == 0), # * 2))`,
|
|
4,
|
|
},
|
|
{
|
|
`map(filter(1..9, # % 2 == 0), # * 2)[-1]`,
|
|
16,
|
|
},
|
|
{
|
|
`len(map(filter(1..9, # % 2 == 0), # * 2))`,
|
|
4,
|
|
},
|
|
{
|
|
`len(filter(map(1..9, # * 2), # % 2 == 0))`,
|
|
9,
|
|
},
|
|
{
|
|
`first(filter(map(1..9, # * 2), # % 2 == 0))`,
|
|
2,
|
|
},
|
|
{
|
|
`first(map(filter(1..9, # % 2 == 0), # * 2))`,
|
|
4,
|
|
},
|
|
{
|
|
`2^3 == 8`,
|
|
true,
|
|
},
|
|
{
|
|
`4/2 == 2`,
|
|
true,
|
|
},
|
|
{
|
|
`.5 in 0..1`,
|
|
false,
|
|
},
|
|
{
|
|
`.5 in ArrayOfInt`,
|
|
false,
|
|
},
|
|
{
|
|
`bitnot(10)`,
|
|
-11,
|
|
},
|
|
{
|
|
`bitxor(15, 32)`,
|
|
47,
|
|
},
|
|
{
|
|
`bitand(90, 34)`,
|
|
2,
|
|
},
|
|
{
|
|
`bitnand(35, 9)`,
|
|
34,
|
|
},
|
|
{
|
|
`bitor(10, 5)`,
|
|
15,
|
|
},
|
|
{
|
|
`bitshr(7, 2)`,
|
|
1,
|
|
},
|
|
{
|
|
`bitshl(7, 2)`,
|
|
28,
|
|
},
|
|
{
|
|
`bitushr(-100, 5)`,
|
|
576460752303423484,
|
|
},
|
|
{
|
|
`"hello"[1:3]`,
|
|
"el",
|
|
},
|
|
{
|
|
`[1, 2, 3]?.[0]`,
|
|
1,
|
|
},
|
|
{
|
|
`[[1, 2], 3, 4]?.[0]?.[1]`,
|
|
2,
|
|
},
|
|
{
|
|
`[nil, 3, 4]?.[0]?.[1]`,
|
|
nil,
|
|
},
|
|
{
|
|
`1 > 2 < 3`,
|
|
false,
|
|
},
|
|
{
|
|
`1 < 2 < 3`,
|
|
true,
|
|
},
|
|
{
|
|
`1 < 2 < 3 > 4`,
|
|
false,
|
|
},
|
|
{
|
|
`1 < 2 < 3 > 2`,
|
|
true,
|
|
},
|
|
{
|
|
`1 < 2 < 3 == true`,
|
|
true,
|
|
},
|
|
{
|
|
`if 1 > 2 { 333 * 2 + 1 } else { 444 }`,
|
|
444,
|
|
},
|
|
{
|
|
`let a = 3;
|
|
let b = 2;
|
|
if a>b {let c = Add(a, b); c+1} else {Add(10, b)}
|
|
`,
|
|
6,
|
|
},
|
|
{
|
|
`if "a" < "b" {let x = "a"; x} else {"abc"}`,
|
|
"a",
|
|
},
|
|
{
|
|
`1; 2; 3`,
|
|
3,
|
|
},
|
|
{
|
|
`let a = 1; Add(2, 2); let b = 2; a + b`,
|
|
3,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.code, func(t *testing.T) {
|
|
{
|
|
program, err := expr.Compile(tt.code, expr.Env(mock.Env{}))
|
|
require.NoError(t, err, "compile error")
|
|
|
|
got, err := expr.Run(program, env)
|
|
require.NoError(t, err, "run error")
|
|
assert.Equal(t, tt.want, got)
|
|
}
|
|
{
|
|
program, err := expr.Compile(tt.code, expr.Optimize(false))
|
|
require.NoError(t, err, "unoptimized")
|
|
|
|
got, err := expr.Run(program, env)
|
|
require.NoError(t, err, "unoptimized")
|
|
assert.Equal(t, tt.want, got, "unoptimized")
|
|
}
|
|
{
|
|
got, err := expr.Eval(tt.code, env)
|
|
require.NoError(t, err, "eval")
|
|
assert.Equal(t, tt.want, got, "eval")
|
|
}
|
|
{
|
|
program, err := expr.Compile(tt.code, expr.Env(mock.Env{}), expr.Optimize(false))
|
|
require.NoError(t, err)
|
|
|
|
code := program.Node().String()
|
|
got, err := expr.Eval(code, env)
|
|
require.NoError(t, err, code)
|
|
assert.Equal(t, tt.want, got, code)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExpr_error(t *testing.T) {
|
|
env := mock.Env{
|
|
ArrayOfAny: []any{1, "2", 3, true},
|
|
}
|
|
|
|
tests := []struct {
|
|
code string
|
|
want string
|
|
}{
|
|
{
|
|
`filter(1..9, # > 9)[0]`,
|
|
`reflect: slice index out of range (1:20)
|
|
| filter(1..9, # > 9)[0]
|
|
| ...................^`,
|
|
},
|
|
{
|
|
`ArrayOfAny[-7]`,
|
|
`index out of range: -3 (array length is 4) (1:11)
|
|
| ArrayOfAny[-7]
|
|
| ..........^`,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.code, func(t *testing.T) {
|
|
program, err := expr.Compile(tt.code, expr.Env(mock.Env{}))
|
|
require.NoError(t, err)
|
|
|
|
_, err = expr.Run(program, env)
|
|
require.Error(t, err)
|
|
assert.Equal(t, tt.want, err.Error())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExpr_optional_chaining(t *testing.T) {
|
|
env := map[string]any{}
|
|
program, err := expr.Compile("foo?.bar.baz", expr.Env(env), expr.AllowUndefinedVariables())
|
|
require.NoError(t, err)
|
|
|
|
got, err := expr.Run(program, env)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, nil, got)
|
|
}
|
|
|
|
func TestExpr_optional_chaining_property(t *testing.T) {
|
|
env := map[string]any{
|
|
"foo": map[string]any{},
|
|
}
|
|
program, err := expr.Compile("foo.bar?.baz", expr.Env(env))
|
|
require.NoError(t, err)
|
|
|
|
got, err := expr.Run(program, env)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, nil, got)
|
|
}
|
|
|
|
func TestExpr_optional_chaining_nested_chains(t *testing.T) {
|
|
env := map[string]any{
|
|
"foo": map[string]any{
|
|
"id": 1,
|
|
"bar": []map[string]any{
|
|
1: {
|
|
"baz": "baz",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
program, err := expr.Compile("foo?.bar[foo?.id]?.baz", expr.Env(env))
|
|
require.NoError(t, err)
|
|
|
|
got, err := expr.Run(program, env)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "baz", got)
|
|
}
|
|
|
|
func TestExpr_optional_chaining_array(t *testing.T) {
|
|
env := map[string]any{}
|
|
program, err := expr.Compile("foo?.[1]?.[2]?.[3]", expr.Env(env), expr.AllowUndefinedVariables())
|
|
require.NoError(t, err)
|
|
|
|
got, err := expr.Run(program, env)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, nil, got)
|
|
}
|
|
|
|
func TestExpr_eval_with_env(t *testing.T) {
|
|
_, err := expr.Eval("true", expr.Env(map[string]any{}))
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "misused")
|
|
}
|
|
|
|
func TestExpr_fetch_from_func(t *testing.T) {
|
|
_, err := expr.Eval("foo.Value", map[string]any{
|
|
"foo": func() {},
|
|
})
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "cannot fetch Value from func()")
|
|
}
|
|
|
|
func TestExpr_map_default_values(t *testing.T) {
|
|
env := map[string]any{
|
|
"foo": map[string]string{},
|
|
"bar": map[string]*string{},
|
|
}
|
|
|
|
input := `foo['missing'] == '' && bar['missing'] == nil`
|
|
|
|
program, err := expr.Compile(input, expr.Env(env))
|
|
require.NoError(t, err)
|
|
|
|
output, err := expr.Run(program, env)
|
|
require.NoError(t, err)
|
|
require.Equal(t, true, output)
|
|
}
|
|
|
|
func TestExpr_map_default_values_compile_check(t *testing.T) {
|
|
tests := []struct {
|
|
env any
|
|
input string
|
|
}{
|
|
{
|
|
mock.MapStringStringEnv{"foo": "bar"},
|
|
`Split(foo, sep)`,
|
|
},
|
|
{
|
|
mock.MapStringIntEnv{"foo": 1},
|
|
`foo / bar`,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
_, err := expr.Compile(tt.input, expr.Env(tt.env), expr.AllowUndefinedVariables())
|
|
require.NoError(t, err)
|
|
}
|
|
}
|
|
|
|
func TestExpr_calls_with_nil(t *testing.T) {
|
|
env := map[string]any{
|
|
"equals": func(a, b any) any {
|
|
assert.Nil(t, a, "a is not nil")
|
|
assert.Nil(t, b, "b is not nil")
|
|
return a == b
|
|
},
|
|
"is": mock.Is{},
|
|
}
|
|
|
|
p, err := expr.Compile(
|
|
"a == nil && equals(b, nil) && is.Nil(c)",
|
|
expr.Env(env),
|
|
expr.Operator("==", "equals"),
|
|
expr.AllowUndefinedVariables(),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
out, err := expr.Run(p, env)
|
|
require.NoError(t, err)
|
|
require.Equal(t, true, out)
|
|
}
|
|
|
|
func TestExpr_call_float_arg_func_with_int(t *testing.T) {
|
|
env := map[string]any{
|
|
"cnv": func(f float64) any {
|
|
return f
|
|
},
|
|
}
|
|
tests := []struct {
|
|
input string
|
|
expected float64
|
|
}{
|
|
{"-1", -1.0},
|
|
{"1+1", 2.0},
|
|
{"+1", 1.0},
|
|
{"1-1", 0.0},
|
|
{"1/1", 1.0},
|
|
{"1*1", 1.0},
|
|
{"1^1", 1.0},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.input, func(t *testing.T) {
|
|
p, err := expr.Compile(fmt.Sprintf("cnv(%s)", tt.input), expr.Env(env))
|
|
require.NoError(t, err)
|
|
|
|
out, err := expr.Run(p, env)
|
|
require.NoError(t, err)
|
|
require.Equal(t, tt.expected, out)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConstExpr_error_panic(t *testing.T) {
|
|
env := map[string]any{
|
|
"divide": func(a, b int) int { return a / b },
|
|
}
|
|
|
|
_, err := expr.Compile(
|
|
`1 + divide(1, 0)`,
|
|
expr.Env(env),
|
|
expr.ConstExpr("divide"),
|
|
)
|
|
require.Error(t, err)
|
|
require.Equal(t, "compile error: integer divide by zero (1:5)\n | 1 + divide(1, 0)\n | ....^", err.Error())
|
|
}
|
|
|
|
type divideError struct{ Message string }
|
|
|
|
func (e divideError) Error() string {
|
|
return e.Message
|
|
}
|
|
|
|
func TestConstExpr_error_as_error(t *testing.T) {
|
|
env := map[string]any{
|
|
"divide": func(a, b int) (int, error) {
|
|
if b == 0 {
|
|
return 0, divideError{"integer divide by zero"}
|
|
}
|
|
return a / b, nil
|
|
},
|
|
}
|
|
|
|
_, err := expr.Compile(
|
|
`1 + divide(1, 0)`,
|
|
expr.Env(env),
|
|
expr.ConstExpr("divide"),
|
|
)
|
|
require.Error(t, err)
|
|
require.Equal(t, "integer divide by zero", err.Error())
|
|
require.IsType(t, divideError{}, err)
|
|
}
|
|
|
|
func TestConstExpr_error_wrong_type(t *testing.T) {
|
|
env := map[string]any{
|
|
"divide": 0,
|
|
}
|
|
assert.Panics(t, func() {
|
|
_, _ = expr.Compile(
|
|
`1 + divide(1, 0)`,
|
|
expr.Env(env),
|
|
expr.ConstExpr("divide"),
|
|
)
|
|
})
|
|
}
|
|
|
|
func TestConstExpr_error_no_env(t *testing.T) {
|
|
assert.Panics(t, func() {
|
|
_, _ = expr.Compile(
|
|
`1 + divide(1, 0)`,
|
|
expr.ConstExpr("divide"),
|
|
)
|
|
})
|
|
}
|
|
|
|
var stringer = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
|
|
|
|
type stringerPatcher struct{}
|
|
|
|
func (p *stringerPatcher) Visit(node *ast.Node) {
|
|
t := (*node).Type()
|
|
if t == nil {
|
|
return
|
|
}
|
|
if t.Implements(stringer) {
|
|
ast.Patch(node, &ast.CallNode{
|
|
Callee: &ast.MemberNode{
|
|
Node: *node,
|
|
Property: &ast.StringNode{Value: "String"},
|
|
},
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPatch(t *testing.T) {
|
|
program, err := expr.Compile(
|
|
`Foo == "Foo.String"`,
|
|
expr.Env(mock.Env{}),
|
|
expr.Patch(&mock.StringerPatcher{}),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
output, err := expr.Run(program, mock.Env{})
|
|
require.NoError(t, err)
|
|
require.Equal(t, true, output)
|
|
}
|
|
|
|
func TestCompile_exposed_error(t *testing.T) {
|
|
_, err := expr.Compile(`1 == true`)
|
|
require.Error(t, err)
|
|
|
|
fileError, ok := err.(*file.Error)
|
|
require.True(t, ok, "error should be of type *file.Error")
|
|
require.Equal(t, "invalid operation: == (mismatched types int and bool) (1:3)\n | 1 == true\n | ..^", fileError.Error())
|
|
require.Equal(t, 2, fileError.Column)
|
|
require.Equal(t, 1, fileError.Line)
|
|
|
|
b, err := json.Marshal(err)
|
|
require.NoError(t, err)
|
|
require.Equal(t,
|
|
`{"from":2,"to":4,"line":1,"column":2,"message":"invalid operation: == (mismatched types int and bool)","snippet":"\n | 1 == true\n | ..^","prev":null}`,
|
|
string(b),
|
|
)
|
|
}
|
|
|
|
func TestAsBool_exposed_error(t *testing.T) {
|
|
_, err := expr.Compile(`42`, expr.AsBool())
|
|
require.Error(t, err)
|
|
|
|
_, ok := err.(*file.Error)
|
|
require.False(t, ok, "error must not be of type *file.Error")
|
|
require.Equal(t, "expected bool, but got int", err.Error())
|
|
}
|
|
|
|
func TestEval_exposed_error(t *testing.T) {
|
|
_, err := expr.Eval(`1 % 0`, nil)
|
|
require.Error(t, err)
|
|
|
|
fileError, ok := err.(*file.Error)
|
|
require.True(t, ok, "error should be of type *file.Error")
|
|
require.Equal(t, "runtime error: integer divide by zero (1:3)\n | 1 % 0\n | ..^", fileError.Error())
|
|
require.Equal(t, 2, fileError.Column)
|
|
require.Equal(t, 1, fileError.Line)
|
|
}
|
|
|
|
func TestIssue105(t *testing.T) {
|
|
type A struct {
|
|
Field string
|
|
}
|
|
type B struct {
|
|
Field int
|
|
}
|
|
type C struct {
|
|
A
|
|
B
|
|
}
|
|
type Env struct {
|
|
C
|
|
}
|
|
|
|
code := `
|
|
A.Field == '' &&
|
|
C.A.Field == '' &&
|
|
B.Field == 0 &&
|
|
C.B.Field == 0
|
|
`
|
|
|
|
_, err := expr.Compile(code, expr.Env(Env{}))
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestIssue_nested_closures(t *testing.T) {
|
|
code := `all(1..3, { all(1..3, { # > 0 }) and # > 0 })`
|
|
|
|
program, err := expr.Compile(code)
|
|
require.NoError(t, err)
|
|
|
|
output, err := expr.Run(program, nil)
|
|
require.NoError(t, err)
|
|
require.True(t, output.(bool))
|
|
}
|
|
|
|
func TestIssue138(t *testing.T) {
|
|
env := map[string]any{}
|
|
|
|
_, err := expr.Compile(`1 / (1 - 1)`, expr.Env(env))
|
|
require.NoError(t, err)
|
|
|
|
_, err = expr.Compile(`1 % 0`, expr.Env(env))
|
|
require.Error(t, err)
|
|
require.Equal(t, "integer divide by zero (1:3)\n | 1 % 0\n | ..^", err.Error())
|
|
}
|
|
|
|
func TestIssue154(t *testing.T) {
|
|
type Data struct {
|
|
Array *[2]any
|
|
Slice *[]any
|
|
Map *map[string]any
|
|
String *string
|
|
}
|
|
|
|
type Env struct {
|
|
Data *Data
|
|
}
|
|
|
|
b := true
|
|
i := 10
|
|
s := "value"
|
|
|
|
Array := [2]any{
|
|
&b,
|
|
&i,
|
|
}
|
|
|
|
Slice := []any{
|
|
&b,
|
|
&i,
|
|
}
|
|
|
|
Map := map[string]any{
|
|
"Bool": &b,
|
|
"Int": &i,
|
|
}
|
|
|
|
env := Env{
|
|
Data: &Data{
|
|
Array: &Array,
|
|
Slice: &Slice,
|
|
Map: &Map,
|
|
String: &s,
|
|
},
|
|
}
|
|
|
|
tests := []string{
|
|
`Data.Array[0] == true`,
|
|
`Data.Array[1] == 10`,
|
|
`Data.Slice[0] == true`,
|
|
`Data.Slice[1] == 10`,
|
|
`Data.Map["Bool"] == true`,
|
|
`Data.Map["Int"] == 10`,
|
|
`Data.String == "value"`,
|
|
}
|
|
|
|
for _, input := range tests {
|
|
program, err := expr.Compile(input, expr.Env(env))
|
|
require.NoError(t, err, input)
|
|
|
|
output, err := expr.Run(program, env)
|
|
require.NoError(t, err)
|
|
assert.True(t, output.(bool), input)
|
|
}
|
|
}
|
|
|
|
func TestIssue270(t *testing.T) {
|
|
env := map[string]any{
|
|
"int8": int8(1),
|
|
"int16": int16(3),
|
|
"int32": int32(5),
|
|
"int64": int64(7),
|
|
"uint8": uint8(11),
|
|
"uint16": uint16(13),
|
|
"uint32": uint32(17),
|
|
"uint64": uint64(19),
|
|
"int8a": uint(23),
|
|
"int8b": uint(29),
|
|
"int16a": uint(31),
|
|
"int16b": uint(37),
|
|
"int32a": uint(41),
|
|
"int32b": uint(43),
|
|
"int64a": uint(47),
|
|
"int64b": uint(53),
|
|
"uint8a": uint(59),
|
|
"uint8b": uint(61),
|
|
"uint16a": uint(67),
|
|
"uint16b": uint(71),
|
|
"uint32a": uint(73),
|
|
"uint32b": uint(79),
|
|
"uint64a": uint(83),
|
|
"uint64b": uint(89),
|
|
"float32a": float32(97),
|
|
"float32b": float32(101),
|
|
"float64a": float64(103),
|
|
"float64b": float64(107),
|
|
}
|
|
for _, each := range []struct {
|
|
input string
|
|
}{
|
|
{"int8 / int16"},
|
|
{"int32 / int64"},
|
|
{"uint8 / uint16"},
|
|
{"uint32 / uint64"},
|
|
{"int8 / uint64"},
|
|
{"int64 / uint8"},
|
|
{"int8a / int8b"},
|
|
{"int16a / int16b"},
|
|
{"int32a / int32b"},
|
|
{"int64a / int64b"},
|
|
{"uint8a / uint8b"},
|
|
{"uint16a / uint16b"},
|
|
{"uint32a / uint32b"},
|
|
{"uint64a / uint64b"},
|
|
{"float32a / float32b"},
|
|
{"float64a / float64b"},
|
|
} {
|
|
p, err := expr.Compile(each.input, expr.Env(env))
|
|
require.NoError(t, err)
|
|
|
|
out, err := expr.Run(p, env)
|
|
require.NoError(t, err)
|
|
require.IsType(t, float64(0), out)
|
|
}
|
|
}
|
|
|
|
func TestIssue271(t *testing.T) {
|
|
type BarArray []float64
|
|
|
|
type Foo struct {
|
|
Bar BarArray
|
|
Baz int
|
|
}
|
|
|
|
type Env struct {
|
|
Foo Foo
|
|
}
|
|
|
|
code := `Foo.Bar[0]`
|
|
|
|
program, err := expr.Compile(code, expr.Env(Env{}))
|
|
require.NoError(t, err)
|
|
|
|
output, err := expr.Run(program, Env{
|
|
Foo: Foo{
|
|
Bar: BarArray{1.0, 2.0, 3.0},
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1.0, output)
|
|
}
|
|
|
|
type Issue346Array []Issue346Type
|
|
|
|
type Issue346Type struct {
|
|
Bar string
|
|
}
|
|
|
|
func (i Issue346Array) Len() int {
|
|
return len(i)
|
|
}
|
|
|
|
func TestIssue346(t *testing.T) {
|
|
code := `Foo[0].Bar`
|
|
|
|
env := map[string]any{
|
|
"Foo": Issue346Array{
|
|
{Bar: "bar"},
|
|
},
|
|
}
|
|
program, err := expr.Compile(code, expr.Env(env))
|
|
require.NoError(t, err)
|
|
|
|
output, err := expr.Run(program, env)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "bar", output)
|
|
}
|
|
|
|
func TestCompile_allow_to_use_interface_to_get_an_element_from_map(t *testing.T) {
|
|
code := `{"value": "ok"}[vars.key]`
|
|
env := map[string]any{
|
|
"vars": map[string]any{
|
|
"key": "value",
|
|
},
|
|
}
|
|
|
|
program, err := expr.Compile(code, expr.Env(env))
|
|
assert.NoError(t, err)
|
|
|
|
out, err := expr.Run(program, env)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "ok", out)
|
|
|
|
t.Run("with allow undefined variables", func(t *testing.T) {
|
|
code := `{'key': 'value'}[Key]`
|
|
env := mock.MapStringStringEnv{}
|
|
options := []expr.Option{
|
|
expr.AllowUndefinedVariables(),
|
|
}
|
|
|
|
program, err := expr.Compile(code, options...)
|
|
assert.NoError(t, err)
|
|
|
|
out, err := expr.Run(program, env)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, nil, out)
|
|
})
|
|
}
|
|
|
|
func TestFastCall(t *testing.T) {
|
|
env := map[string]any{
|
|
"func": func(in any) float64 {
|
|
return 8
|
|
},
|
|
}
|
|
code := `func("8")`
|
|
|
|
program, err := expr.Compile(code, expr.Env(env))
|
|
assert.NoError(t, err)
|
|
|
|
out, err := expr.Run(program, env)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, float64(8), out)
|
|
}
|
|
|
|
func TestFastCall_OpCallFastErr(t *testing.T) {
|
|
env := map[string]any{
|
|
"func": func(...any) (any, error) {
|
|
return 8, nil
|
|
},
|
|
}
|
|
code := `func("8")`
|
|
|
|
program, err := expr.Compile(code, expr.Env(env))
|
|
assert.NoError(t, err)
|
|
|
|
out, err := expr.Run(program, env)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 8, out)
|
|
}
|
|
|
|
func TestRun_custom_func_returns_an_error_as_second_arg(t *testing.T) {
|
|
env := map[string]any{
|
|
"semver": func(value string, cmp string) (bool, error) { return true, nil },
|
|
}
|
|
|
|
p, err := expr.Compile(`semver("1.2.3", "= 1.2.3")`, expr.Env(env))
|
|
assert.NoError(t, err)
|
|
|
|
out, err := expr.Run(p, env)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, true, out)
|
|
}
|
|
|
|
func TestFunction(t *testing.T) {
|
|
add := expr.Function(
|
|
"add",
|
|
func(p ...any) (any, error) {
|
|
out := 0
|
|
for _, each := range p {
|
|
out += each.(int)
|
|
}
|
|
return out, nil
|
|
},
|
|
new(func(...int) int),
|
|
)
|
|
|
|
p, err := expr.Compile(`add() + add(1) + add(1, 2) + add(1, 2, 3) + add(1, 2, 3, 4)`, add)
|
|
assert.NoError(t, err)
|
|
|
|
out, err := expr.Run(p, nil)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, 20, out)
|
|
}
|
|
|
|
// Nil coalescing operator
|
|
func TestRun_NilCoalescingOperator(t *testing.T) {
|
|
env := map[string]any{
|
|
"foo": map[string]any{
|
|
"bar": "value",
|
|
},
|
|
}
|
|
|
|
t.Run("value", func(t *testing.T) {
|
|
p, err := expr.Compile(`foo.bar ?? "default"`, expr.Env(env))
|
|
assert.NoError(t, err)
|
|
|
|
out, err := expr.Run(p, env)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "value", out)
|
|
})
|
|
|
|
t.Run("default", func(t *testing.T) {
|
|
p, err := expr.Compile(`foo.baz ?? "default"`, expr.Env(env))
|
|
assert.NoError(t, err)
|
|
|
|
out, err := expr.Run(p, env)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "default", out)
|
|
})
|
|
|
|
t.Run("default with chain", func(t *testing.T) {
|
|
p, err := expr.Compile(`foo?.bar ?? "default"`, expr.Env(env))
|
|
assert.NoError(t, err)
|
|
|
|
out, err := expr.Run(p, map[string]any{})
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "default", out)
|
|
})
|
|
}
|
|
|
|
func TestEval_nil_in_maps(t *testing.T) {
|
|
env := map[string]any{
|
|
"m": map[any]any{nil: "bar"},
|
|
"empty": map[any]any{},
|
|
}
|
|
t.Run("nil key exists", func(t *testing.T) {
|
|
p, err := expr.Compile(`m[nil]`, expr.Env(env))
|
|
assert.NoError(t, err)
|
|
|
|
out, err := expr.Run(p, env)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "bar", out)
|
|
})
|
|
t.Run("no nil key", func(t *testing.T) {
|
|
p, err := expr.Compile(`empty[nil]`, expr.Env(env))
|
|
assert.NoError(t, err)
|
|
|
|
out, err := expr.Run(p, env)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, nil, out)
|
|
})
|
|
t.Run("nil in m", func(t *testing.T) {
|
|
p, err := expr.Compile(`nil in m`, expr.Env(env))
|
|
assert.NoError(t, err)
|
|
|
|
out, err := expr.Run(p, env)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, true, out)
|
|
})
|
|
t.Run("nil in empty", func(t *testing.T) {
|
|
p, err := expr.Compile(`nil in empty`, expr.Env(env))
|
|
assert.NoError(t, err)
|
|
|
|
out, err := expr.Run(p, env)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, false, out)
|
|
})
|
|
}
|
|
|
|
// Test the use of env keyword. Forms env[] and env[”] are valid.
|
|
// The enclosed identifier must be in the expression env.
|
|
func TestEnv_keyword(t *testing.T) {
|
|
env := map[string]any{
|
|
"space test": "ok",
|
|
"space_test": "not ok", // Seems to be some underscore substituting happening, check that.
|
|
"Section 1-2a": "ok",
|
|
`c:\ndrive\2015 Information Table`: "ok",
|
|
"%*worst function name ever!!": func() string {
|
|
return "ok"
|
|
}(),
|
|
"1": "o",
|
|
"2": "k",
|
|
"num": 10,
|
|
"mylist": []int{1, 2, 3, 4, 5},
|
|
"MIN": func(a, b int) int {
|
|
if a < b {
|
|
return a
|
|
} else {
|
|
return b
|
|
}
|
|
},
|
|
"red": "n",
|
|
"irect": "um",
|
|
"String Map": map[string]string{
|
|
"one": "two",
|
|
"three": "four",
|
|
},
|
|
"OtherMap": map[string]string{
|
|
"a": "b",
|
|
"c": "d",
|
|
},
|
|
}
|
|
|
|
// No error cases
|
|
var tests = []struct {
|
|
code string
|
|
want any
|
|
}{
|
|
{"$env['space test']", "ok"},
|
|
{"$env['Section 1-2a']", "ok"},
|
|
{`$env["c:\\ndrive\\2015 Information Table"]`, "ok"},
|
|
{"$env['%*worst function name ever!!']", "ok"},
|
|
{"$env['String Map'].one", "two"},
|
|
{"$env['1'] + $env['2']", "ok"},
|
|
{"1 + $env['num'] + $env['num']", 21},
|
|
{"MIN($env['num'],0)", 0},
|
|
{"$env['nu' + 'm']", 10},
|
|
{"$env[red + irect]", 10},
|
|
{"$env['String Map']?.five", ""},
|
|
{"$env.red", "n"},
|
|
{"$env?.unknown", nil},
|
|
{"$env.mylist[1]", 2},
|
|
{"$env?.OtherMap?.a", "b"},
|
|
{"$env?.OtherMap?.d", ""},
|
|
{"'num' in $env", true},
|
|
{"get($env, 'num')", 10},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.code, func(t *testing.T) {
|
|
|
|
program, err := expr.Compile(tt.code, expr.Env(env))
|
|
require.NoError(t, err, "compile error")
|
|
|
|
got, err := expr.Run(program, env)
|
|
require.NoError(t, err, "execution error")
|
|
|
|
assert.Equal(t, tt.want, got, tt.code)
|
|
})
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.code, func(t *testing.T) {
|
|
got, err := expr.Eval(tt.code, env)
|
|
require.NoError(t, err, "eval error: "+tt.code)
|
|
|
|
assert.Equal(t, tt.want, got, "eval: "+tt.code)
|
|
})
|
|
}
|
|
|
|
// error cases
|
|
tests = []struct {
|
|
code string
|
|
want any
|
|
}{
|
|
{"env()", "bad"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.code, func(t *testing.T) {
|
|
_, err := expr.Eval(tt.code, expr.Env(env))
|
|
require.Error(t, err, "compile error")
|
|
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEnv_keyword_with_custom_functions(t *testing.T) {
|
|
fn := expr.Function("fn", func(params ...any) (any, error) {
|
|
return "ok", nil
|
|
})
|
|
|
|
var tests = []struct {
|
|
code string
|
|
error bool
|
|
}{
|
|
{`fn()`, false},
|
|
{`$env.fn()`, true},
|
|
{`$env["fn"]`, true},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.code, func(t *testing.T) {
|
|
_, err := expr.Compile(tt.code, expr.Env(mock.Env{}), fn)
|
|
if tt.error {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIssue401(t *testing.T) {
|
|
program, err := expr.Compile("(a - b + c) / d", expr.AllowUndefinedVariables())
|
|
require.NoError(t, err, "compile error")
|
|
|
|
output, err := expr.Run(program, map[string]any{
|
|
"a": 1,
|
|
"b": 2,
|
|
"c": 3,
|
|
"d": 4,
|
|
})
|
|
require.NoError(t, err, "run error")
|
|
require.Equal(t, 0.5, output)
|
|
}
|
|
|
|
func TestEval_slices_out_of_bound(t *testing.T) {
|
|
tests := []struct {
|
|
code string
|
|
want any
|
|
}{
|
|
{"[1, 2, 3][:99]", []any{1, 2, 3}},
|
|
{"[1, 2, 3][99:]", []any{}},
|
|
{"[1, 2, 3][:-99]", []any{}},
|
|
{"[1, 2, 3][-99:]", []any{1, 2, 3}},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.code, func(t *testing.T) {
|
|
got, err := expr.Eval(tt.code, nil)
|
|
require.NoError(t, err, "eval error: "+tt.code)
|
|
assert.Equal(t, tt.want, got, "eval: "+tt.code)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMemoryBudget(t *testing.T) {
|
|
tests := []struct {
|
|
code string
|
|
}{
|
|
{`map(1..100, {map(1..100, {map(1..100, {0})})})`},
|
|
{`len(1..10000000)`},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.code, func(t *testing.T) {
|
|
program, err := expr.Compile(tt.code)
|
|
require.NoError(t, err, "compile error")
|
|
|
|
_, err = expr.Run(program, nil)
|
|
assert.Error(t, err, "run error")
|
|
assert.Contains(t, err.Error(), "memory budget exceeded")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExpr_custom_tests(t *testing.T) {
|
|
f, err := os.Open("custom_tests.json")
|
|
if os.IsNotExist(err) {
|
|
t.Skip("no custom tests")
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err, "open file error")
|
|
defer f.Close()
|
|
|
|
var tests []string
|
|
err = json.NewDecoder(f).Decode(&tests)
|
|
require.NoError(t, err, "decode json error")
|
|
|
|
for id, tt := range tests {
|
|
t.Run(fmt.Sprintf("line %v", id+2), func(t *testing.T) {
|
|
program, err := expr.Compile(tt)
|
|
require.NoError(t, err)
|
|
|
|
timeout := make(chan bool, 1)
|
|
go func() {
|
|
time.Sleep(time.Second)
|
|
timeout <- true
|
|
}()
|
|
|
|
done := make(chan bool, 1)
|
|
go func() {
|
|
out, err := expr.Run(program, nil)
|
|
// Make sure out is used.
|
|
_ = fmt.Sprintf("%v", out)
|
|
assert.Error(t, err)
|
|
done <- true
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
// Success.
|
|
case <-timeout:
|
|
t.Fatal("timeout")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIssue432(t *testing.T) {
|
|
env := map[string]any{
|
|
"func": func(
|
|
paramUint32 uint32,
|
|
paramUint16 uint16,
|
|
paramUint8 uint8,
|
|
paramUint uint,
|
|
paramInt32 int32,
|
|
paramInt16 int16,
|
|
paramInt8 int8,
|
|
paramInt int,
|
|
paramFloat64 float64,
|
|
paramFloat32 float32,
|
|
) float64 {
|
|
return float64(paramUint32) + float64(paramUint16) + float64(paramUint8) + float64(paramUint) +
|
|
float64(paramInt32) + float64(paramInt16) + float64(paramInt8) + float64(paramInt) +
|
|
float64(paramFloat64) + float64(paramFloat32)
|
|
},
|
|
}
|
|
code := `func(1,1,1,1,1,1,1,1,1,1)`
|
|
|
|
program, err := expr.Compile(code, expr.Env(env))
|
|
assert.NoError(t, err)
|
|
|
|
out, err := expr.Run(program, env)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, float64(10), out)
|
|
}
|
|
|
|
func TestIssue462(t *testing.T) {
|
|
env := map[string]any{
|
|
"foo": func() (string, error) {
|
|
return "bar", nil
|
|
},
|
|
}
|
|
_, err := expr.Compile(`$env.unknown(int())`, expr.Env(env))
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestIssue_embedded_pointer_struct(t *testing.T) {
|
|
var tests = []struct {
|
|
input string
|
|
env mock.Env
|
|
want any
|
|
}{
|
|
{
|
|
input: "EmbedPointerEmbedInt > 0",
|
|
env: mock.Env{
|
|
Embed: mock.Embed{
|
|
EmbedPointerEmbed: &mock.EmbedPointerEmbed{
|
|
EmbedPointerEmbedInt: 123,
|
|
},
|
|
},
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
input: "(Embed).EmbedPointerEmbedInt > 0",
|
|
env: mock.Env{
|
|
Embed: mock.Embed{
|
|
EmbedPointerEmbed: &mock.EmbedPointerEmbed{
|
|
EmbedPointerEmbedInt: 123,
|
|
},
|
|
},
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
input: "(Embed).EmbedPointerEmbedInt > 0",
|
|
env: mock.Env{
|
|
Embed: mock.Embed{
|
|
EmbedPointerEmbed: &mock.EmbedPointerEmbed{
|
|
EmbedPointerEmbedInt: 0,
|
|
},
|
|
},
|
|
},
|
|
want: false,
|
|
},
|
|
{
|
|
input: "(Embed).EmbedPointerEmbedMethod(0)",
|
|
env: mock.Env{
|
|
Embed: mock.Embed{
|
|
EmbedPointerEmbed: &mock.EmbedPointerEmbed{
|
|
EmbedPointerEmbedInt: 0,
|
|
},
|
|
},
|
|
},
|
|
want: "",
|
|
},
|
|
{
|
|
input: "(Embed).EmbedPointerEmbedPointerReceiverMethod(0)",
|
|
env: mock.Env{
|
|
Embed: mock.Embed{
|
|
EmbedPointerEmbed: nil,
|
|
},
|
|
},
|
|
want: "",
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.input, func(t *testing.T) {
|
|
program, err := expr.Compile(tt.input, expr.Env(tt.env))
|
|
require.NoError(t, err)
|
|
|
|
out, err := expr.Run(program, tt.env)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, tt.want, out)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIssue474(t *testing.T) {
|
|
testCases := []struct {
|
|
code string
|
|
fail bool
|
|
}{
|
|
{
|
|
code: `func("invalid")`,
|
|
fail: true,
|
|
},
|
|
{
|
|
code: `func(true)`,
|
|
fail: true,
|
|
},
|
|
{
|
|
code: `func([])`,
|
|
fail: true,
|
|
},
|
|
{
|
|
code: `func({})`,
|
|
fail: true,
|
|
},
|
|
{
|
|
code: `func(1)`,
|
|
fail: false,
|
|
},
|
|
{
|
|
code: `func(1.5)`,
|
|
fail: false,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
ltc := tc
|
|
t.Run(ltc.code, func(t *testing.T) {
|
|
t.Parallel()
|
|
function := expr.Function("func", func(params ...any) (any, error) {
|
|
return true, nil
|
|
}, new(func(float64) bool))
|
|
_, err := expr.Compile(ltc.code, function)
|
|
if ltc.fail {
|
|
if err == nil {
|
|
t.Error("expected an error, but it was nil")
|
|
t.FailNow()
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Errorf("expected nil, but it was %v", err)
|
|
t.FailNow()
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRaceCondition_variables(t *testing.T) {
|
|
program, err := expr.Compile(`let foo = 1; foo + 1`, expr.Env(mock.Env{}))
|
|
require.NoError(t, err)
|
|
|
|
var wg sync.WaitGroup
|
|
|
|
for i := 0; i < 10; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
out, err := expr.Run(program, mock.Env{})
|
|
require.NoError(t, err)
|
|
require.Equal(t, 2, out)
|
|
}()
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
func TestOperatorDependsOnEnv(t *testing.T) {
|
|
env := map[string]any{
|
|
"plus": func(a, b int) int {
|
|
return 42
|
|
},
|
|
}
|
|
program, err := expr.Compile(`1 + 2`, expr.Operator("+", "plus"), expr.Env(env))
|
|
require.NoError(t, err)
|
|
|
|
out, err := expr.Run(program, env)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 42, out)
|
|
}
|
|
|
|
func TestIssue624(t *testing.T) {
|
|
type tag struct {
|
|
Name string
|
|
}
|
|
|
|
type item struct {
|
|
Tags []tag
|
|
}
|
|
|
|
i := item{
|
|
Tags: []tag{
|
|
{Name: "one"},
|
|
{Name: "two"},
|
|
},
|
|
}
|
|
|
|
rule := `[
|
|
true && true,
|
|
one(Tags, .Name in ["one"]),
|
|
one(Tags, .Name in ["two"]),
|
|
one(Tags, .Name in ["one"]) && one(Tags, .Name in ["two"])
|
|
]`
|
|
resp, err := expr.Eval(rule, i)
|
|
require.NoError(t, err)
|
|
require.Equal(t, []interface{}{true, true, true, true}, resp)
|
|
}
|
|
|
|
func TestPredicateCombination(t *testing.T) {
|
|
tests := []struct {
|
|
code1 string
|
|
code2 string
|
|
}{
|
|
{"all(1..3, {# > 0}) && all(1..3, {# < 4})", "all(1..3, {# > 0 && # < 4})"},
|
|
{"all(1..3, {# > 1}) && all(1..3, {# < 4})", "all(1..3, {# > 1 && # < 4})"},
|
|
{"all(1..3, {# > 0}) && all(1..3, {# < 2})", "all(1..3, {# > 0 && # < 2})"},
|
|
{"all(1..3, {# > 1}) && all(1..3, {# < 2})", "all(1..3, {# > 1 && # < 2})"},
|
|
|
|
{"any(1..3, {# > 0}) || any(1..3, {# < 4})", "any(1..3, {# > 0 || # < 4})"},
|
|
{"any(1..3, {# > 1}) || any(1..3, {# < 4})", "any(1..3, {# > 1 || # < 4})"},
|
|
{"any(1..3, {# > 0}) || any(1..3, {# < 2})", "any(1..3, {# > 0 || # < 2})"},
|
|
{"any(1..3, {# > 1}) || any(1..3, {# < 2})", "any(1..3, {# > 1 || # < 2})"},
|
|
|
|
{"none(1..3, {# > 0}) && none(1..3, {# < 4})", "none(1..3, {# > 0 || # < 4})"},
|
|
{"none(1..3, {# > 1}) && none(1..3, {# < 4})", "none(1..3, {# > 1 || # < 4})"},
|
|
{"none(1..3, {# > 0}) && none(1..3, {# < 2})", "none(1..3, {# > 0 || # < 2})"},
|
|
{"none(1..3, {# > 1}) && none(1..3, {# < 2})", "none(1..3, {# > 1 || # < 2})"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.code1, func(t *testing.T) {
|
|
out1, err := expr.Eval(tt.code1, nil)
|
|
require.NoError(t, err)
|
|
|
|
out2, err := expr.Eval(tt.code2, nil)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, out1, out2)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestArrayComparison(t *testing.T) {
|
|
tests := []struct {
|
|
env any
|
|
code string
|
|
}{
|
|
{[]string{"A", "B"}, "foo == ['A', 'B']"},
|
|
{[]int{1, 2}, "foo == [1, 2]"},
|
|
{[]uint8{1, 2}, "foo == [1, 2]"},
|
|
{[]float64{1.1, 2.2}, "foo == [1.1, 2.2]"},
|
|
{[]any{"A", 1, 1.1, true}, "foo == ['A', 1, 1.1, true]"},
|
|
{[]string{"A", "B"}, "foo != [1, 2]"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.code, func(t *testing.T) {
|
|
env := map[string]any{"foo": tt.env}
|
|
program, err := expr.Compile(tt.code, expr.Env(env))
|
|
require.NoError(t, err)
|
|
|
|
out, err := expr.Run(program, env)
|
|
require.NoError(t, err)
|
|
require.Equal(t, true, out)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIssue_570(t *testing.T) {
|
|
type Student struct {
|
|
Name string
|
|
}
|
|
|
|
env := map[string]any{
|
|
"student": (*Student)(nil),
|
|
}
|
|
|
|
program, err := expr.Compile("student?.Name", expr.Env(env))
|
|
require.NoError(t, err)
|
|
|
|
out, err := expr.Run(program, env)
|
|
require.NoError(t, err)
|
|
require.IsType(t, nil, out)
|
|
}
|
|
|
|
func TestIssue_integer_truncated_by_compiler(t *testing.T) {
|
|
env := map[string]any{
|
|
"fn": func(x byte) byte {
|
|
return x
|
|
},
|
|
}
|
|
|
|
_, err := expr.Compile("fn(255)", expr.Env(env))
|
|
require.NoError(t, err)
|
|
|
|
_, err = expr.Compile("fn(256)", expr.Env(env))
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestExpr_crash(t *testing.T) {
|
|
content, err := os.ReadFile("testdata/crash.txt")
|
|
require.NoError(t, err)
|
|
|
|
_, err = expr.Compile(string(content))
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestExpr_nil_op_str(t *testing.T) {
|
|
// Let's test operators, which do `.(string)` in VM, also check for nil.
|
|
|
|
var str *string = nil
|
|
env := map[string]any{
|
|
"nilString": str,
|
|
}
|
|
|
|
tests := []struct{ code string }{
|
|
{`nilString == "str"`},
|
|
{`nilString contains "str"`},
|
|
{`nilString matches "str"`},
|
|
{`nilString startsWith "str"`},
|
|
{`nilString endsWith "str"`},
|
|
|
|
{`"str" == nilString`},
|
|
{`"str" contains nilString`},
|
|
{`"str" matches nilString`},
|
|
{`"str" startsWith nilString`},
|
|
{`"str" endsWith nilString`},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.code, func(t *testing.T) {
|
|
program, err := expr.Compile(tt.code)
|
|
require.NoError(t, err)
|
|
|
|
output, err := expr.Run(program, env)
|
|
require.NoError(t, err)
|
|
require.Equal(t, false, output)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExpr_env_types_map(t *testing.T) {
|
|
envTypes := types.Map{
|
|
"foo": types.Map{
|
|
"bar": types.String,
|
|
},
|
|
}
|
|
|
|
program, err := expr.Compile(`foo.bar`, expr.Env(envTypes))
|
|
require.NoError(t, err)
|
|
|
|
env := map[string]any{
|
|
"foo": map[string]any{
|
|
"bar": "value",
|
|
},
|
|
}
|
|
|
|
output, err := expr.Run(program, env)
|
|
require.NoError(t, err)
|
|
require.Equal(t, "value", output)
|
|
}
|
|
|
|
func TestExpr_env_types_map_error(t *testing.T) {
|
|
envTypes := types.Map{
|
|
"foo": types.Map{
|
|
"bar": types.String,
|
|
},
|
|
}
|
|
|
|
program, err := expr.Compile(`foo.bar`, expr.Env(envTypes))
|
|
require.NoError(t, err)
|
|
|
|
_, err = expr.Run(program, envTypes)
|
|
require.Error(t, err)
|
|
}
|
|
|
|
func TestIssue758_filter_map_index(t *testing.T) {
|
|
env := map[string]interface{}{}
|
|
|
|
exprStr := `
|
|
let a_map = 0..5 | filter(# % 2 == 0) | map(#index);
|
|
let b_filter = 0..5 | filter(# % 2 == 0);
|
|
let b_map = b_filter | map(#index);
|
|
[a_map, b_map]
|
|
`
|
|
|
|
result, err := expr.Eval(exprStr, env)
|
|
require.NoError(t, err)
|
|
|
|
expected := []interface{}{
|
|
[]interface{}{0, 1, 2},
|
|
[]interface{}{0, 1, 2},
|
|
}
|
|
|
|
require.Equal(t, expected, result)
|
|
}
|
|
|
|
func TestExpr_wierd_cases(t *testing.T) {
|
|
env := map[string]any{}
|
|
|
|
_, err := expr.Compile(`A(A)`, expr.Env(env))
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "unknown name A")
|
|
}
|