| // Copyright 2023 Google LLC |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package ext |
| |
| import ( |
| "fmt" |
| "strings" |
| "testing" |
| |
| "github.com/google/cel-go/cel" |
| "github.com/google/cel-go/checker" |
| "github.com/google/cel-go/common/ast" |
| "github.com/google/cel-go/common/operators" |
| "github.com/google/cel-go/common/types" |
| "github.com/google/cel-go/common/types/ref" |
| ) |
| |
| var bindingTests = []struct { |
| name string |
| expr string |
| vars []cel.EnvOption |
| in map[string]any |
| hints map[string]uint64 |
| estimatedCost checker.CostEstimate |
| actualCost uint64 |
| }{ |
| { |
| name: "single bind", |
| expr: `cel.bind(a, 'hell' + 'o' + '!', "%s, %s, %s".format([a, a, a])) == |
| 'hello!, hello!, hello' + '!'`, |
| estimatedCost: checker.CostEstimate{Min: 30, Max: 32}, |
| actualCost: 32, |
| }, |
| { |
| name: "multiple binds", |
| expr: `cel.bind(a, 'hello!', |
| cel.bind(b, 'goodbye', |
| a + ' and, ' + b)) == 'hello! and, goodbye'`, |
| estimatedCost: checker.CostEstimate{Min: 27, Max: 28}, |
| actualCost: 28, |
| }, |
| { |
| name: "shadow binds", |
| expr: `cel.bind(a, |
| cel.bind(a, 'world', a + '!'), |
| 'hello ' + a) == 'hello ' + 'world' + '!'`, |
| estimatedCost: checker.CostEstimate{Min: 30, Max: 31}, |
| actualCost: 31, |
| }, |
| { |
| name: "nested bind with int list", |
| expr: `cel.bind(a, x, |
| cel.bind(b, a[0], |
| cel.bind(c, a[1], b + c))) == 10`, |
| vars: []cel.EnvOption{cel.Variable("x", cel.ListType(cel.IntType))}, |
| in: map[string]any{ |
| "x": []int64{3, 7}, |
| }, |
| hints: map[string]uint64{ |
| "x": 3, |
| }, |
| estimatedCost: checker.CostEstimate{Min: 39, Max: 39}, |
| actualCost: 39, |
| }, |
| { |
| name: "nested bind with string list", |
| expr: `cel.bind(a, x, |
| cel.bind(b, a[0], |
| cel.bind(c, a[1], b + c))) == "threeseven"`, |
| vars: []cel.EnvOption{cel.Variable("x", cel.ListType(cel.StringType))}, |
| in: map[string]any{ |
| "x": []string{"three", "seven"}, |
| }, |
| hints: map[string]uint64{ |
| "x": 3, |
| "x.@items": 10, |
| }, |
| estimatedCost: checker.CostEstimate{Min: 38, Max: 40}, |
| actualCost: 39, |
| }, |
| { |
| name: "shadowed binding", |
| expr: `cel.bind(x, 0, x == 0)`, |
| vars: []cel.EnvOption{cel.Variable("x", cel.StringType)}, |
| in: map[string]any{ |
| "cel.example.x": "1", |
| }, |
| estimatedCost: checker.FixedCostEstimate(12), |
| actualCost: 12, |
| }, |
| { |
| name: "container shadowed binding", |
| expr: `cel.bind(x, 0, x == 0)`, |
| vars: []cel.EnvOption{ |
| cel.Container("cel.example"), |
| cel.Variable("cel.example.x", cel.StringType), |
| }, |
| in: map[string]any{ |
| "cel.example.x": "1", |
| }, |
| estimatedCost: checker.FixedCostEstimate(12), |
| actualCost: 12, |
| }, |
| { |
| name: "shadowing namespace resolution selector", |
| expr: `cel.bind(x, {'y': 0}, x.y == 0)`, |
| vars: []cel.EnvOption{ |
| cel.Container("cel.example"), |
| cel.Variable("cel.example.x.y", cel.IntType), |
| }, |
| in: map[string]any{ |
| "cel.example.x.y": 1, |
| }, |
| estimatedCost: checker.FixedCostEstimate(43), |
| actualCost: 43, |
| }, |
| { |
| name: "shadowing namespace resolution selector with local", |
| expr: `cel.bind(x, {'y': 0}, .x.y == x.y)`, |
| vars: []cel.EnvOption{ |
| cel.Variable("x.y", cel.IntType), |
| }, |
| in: map[string]any{ |
| "x.y": 0, |
| }, |
| estimatedCost: checker.FixedCostEstimate(44), |
| actualCost: 44, |
| }, |
| { |
| name: "namespace disambiguation", |
| expr: `cel.bind(y, 0, .y != y)`, |
| vars: []cel.EnvOption{ |
| cel.Variable("y", cel.IntType), |
| }, |
| in: map[string]any{ |
| "y": 1, |
| }, |
| estimatedCost: checker.FixedCostEstimate(13), |
| actualCost: 13, |
| }, |
| { |
| name: "nesting shadowing", |
| expr: `cel.bind(y, 0, cel.bind(y, 1, y != 0))`, |
| estimatedCost: checker.FixedCostEstimate(22), |
| actualCost: 22, |
| }, |
| } |
| |
| func TestBindings(t *testing.T) { |
| for _, tst := range bindingTests { |
| tc := tst |
| t.Run(tc.name, func(t *testing.T) { |
| var asts []*cel.Ast |
| opts := append([]cel.EnvOption{Bindings(BindingsVersion(0)), Strings()}, tc.vars...) |
| env, err := cel.NewEnv(opts...) |
| if err != nil { |
| t.Fatalf("cel.NewEnv(Bindings(), Strings()) failed: %v", err) |
| } |
| pAst, iss := env.Parse(tc.expr) |
| if iss.Err() != nil { |
| t.Fatalf("env.Parse(%v) failed: %v", tc.expr, iss.Err()) |
| } |
| asts = append(asts, pAst) |
| cAst, iss := env.Check(pAst) |
| if iss.Err() != nil { |
| t.Fatalf("env.Check(%v) failed: %v", tc.expr, iss.Err()) |
| } |
| testCheckCost(t, env, cAst, tc.hints, tc.estimatedCost) |
| asts = append(asts, cAst) |
| for _, ast := range asts { |
| testEvalWithCost(t, env, ast, tc.in, tc.actualCost) |
| } |
| }) |
| } |
| } |
| |
| func TestBindingsNonMatch(t *testing.T) { |
| env, err := cel.NewEnv(Bindings(), Strings()) |
| if err != nil { |
| t.Fatalf("cel.NewEnv(Bindings(), Strings()) failed: %v", err) |
| } |
| nonMatchExpr := `ceel.bind(a, 1, a)` |
| ast, iss := env.Parse(nonMatchExpr) |
| if iss.Err() != nil { |
| t.Fatalf("env.Parse(%v) failed: %v", nonMatchExpr, iss.Err()) |
| } |
| if len(ast.SourceInfo().GetMacroCalls()) != 0 { |
| t.Fatalf("env.Parse(%v) performed a macro replacement when none was expected: %v", |
| nonMatchExpr, ast.SourceInfo().GetMacroCalls()) |
| } |
| } |
| |
| func TestBindingsInvalidIdent(t *testing.T) { |
| env, err := cel.NewEnv(Bindings(), Strings()) |
| if err != nil { |
| t.Fatalf("cel.NewEnv(Bindings(), Strings()) failed: %v", err) |
| } |
| invalidIdentExpr := `cel.bind(a.b, 1, a.b)` |
| wantErr := "ERROR: <input>:1:11: cel.bind() variable names must be simple identifiers" |
| _, iss := env.Parse(invalidIdentExpr) |
| if !strings.Contains(iss.Err().Error(), wantErr) { |
| t.Fatalf("env.Parse(%v) failed: %v", invalidIdentExpr, iss.Err()) |
| } |
| } |
| |
| func BenchmarkBindings(b *testing.B) { |
| env, err := cel.NewEnv(Bindings(), Strings()) |
| if err != nil { |
| b.Fatalf("cel.NewEnv(Bindings(), Strings()) failed: %v", err) |
| } |
| for i, tst := range bindingTests { |
| tc := tst |
| ast, iss := env.Compile(tc.expr) |
| if iss.Err() != nil { |
| b.Fatalf("env.Compile(%q) failed: %v", tc.expr, iss.Err()) |
| } |
| prg, err := env.Program(ast, cel.EvalOptions(cel.OptOptimize)) |
| if err != nil { |
| b.Fatalf("env.Program(ast, Optimize) failed: %v", err) |
| } |
| // Benchmark the eval. |
| b.Run(fmt.Sprintf("[%d]", i), func(b *testing.B) { |
| b.ResetTimer() |
| b.ReportAllocs() |
| for i := 0; i < b.N; i++ { |
| prg.Eval(cel.NoVars()) |
| } |
| }) |
| } |
| } |
| |
| func TestBlockEval(t *testing.T) { |
| fac := ast.NewExprFactory() |
| tests := []struct { |
| name string |
| expr ast.Expr |
| opts []cel.EnvOption |
| in map[string]any |
| out ref.Val |
| }{ |
| { |
| name: "chained block", |
| expr: fac.NewCall( |
| 1, "cel.@block", |
| fac.NewList(2, []ast.Expr{ |
| fac.NewIdent(3, "x"), |
| fac.NewIdent(4, "@index0"), |
| fac.NewIdent(5, "@index1"), |
| }, []int32{}), |
| fac.NewCall(9, operators.Add, |
| fac.NewCall(6, operators.Add, |
| fac.NewIdent(7, "@index2"), |
| fac.NewIdent(8, "@index1")), |
| fac.NewIdent(10, "@index0"), |
| ), |
| ), |
| opts: []cel.EnvOption{ |
| cel.Variable("x", cel.StringType), |
| }, |
| in: map[string]any{"x": "hello"}, |
| out: types.String("hellohellohello"), |
| }, |
| { |
| name: "empty block", |
| expr: fac.NewCall( |
| 1, "cel.@block", |
| fac.NewList(2, []ast.Expr{}, []int32{}), |
| fac.NewCall(3, operators.LogicalNot, fac.NewLiteral(4, types.False)), |
| ), |
| in: map[string]any{}, |
| out: types.True, |
| }, |
| { |
| name: "mixed block constant values", |
| expr: fac.NewCall( |
| 1, "cel.@block", |
| fac.NewList(2, []ast.Expr{ |
| fac.NewLiteral(3, types.String("hello")), |
| fac.NewLiteral(4, types.Int(5)), |
| }, []int32{}), |
| fac.NewCall(5, operators.Equals, |
| fac.NewCall(6, "size", |
| fac.NewIdent(7, "@index0")), |
| fac.NewIdent(8, "@index1"), |
| ), |
| ), |
| opts: []cel.EnvOption{ |
| cel.ExtendedValidations(), |
| }, |
| in: map[string]any{}, |
| out: types.True, |
| }, |
| { |
| name: "mixed block dynamic values", |
| expr: fac.NewCall( |
| 1, "cel.@block", |
| fac.NewList(2, []ast.Expr{ |
| fac.NewIdent(3, "x"), |
| fac.NewLiteral(4, types.Int(5)), |
| }, []int32{}), |
| fac.NewCall(5, operators.Equals, |
| fac.NewCall(6, "size", |
| fac.NewIdent(7, "@index0")), |
| fac.NewIdent(8, "@index1"), |
| ), |
| ), |
| opts: []cel.EnvOption{ |
| cel.Variable("x", cel.StringType), |
| cel.ExtendedValidations(), |
| }, |
| in: map[string]any{"x": "goodbye"}, |
| out: types.False, |
| }, |
| { |
| name: "mixed block constant values dyn var", |
| expr: fac.NewCall( |
| 1, "cel.@block", |
| fac.NewList(2, []ast.Expr{ |
| fac.NewLiteral(3, types.String("hello")), |
| }, []int32{}), |
| fac.NewCall(4, operators.Equals, |
| fac.NewCall(5, "size", |
| fac.NewIdent(6, "@index0")), |
| fac.NewIdent(7, "y"), |
| ), |
| ), |
| opts: []cel.EnvOption{ |
| cel.Variable("y", cel.IntType), |
| cel.ExtendedValidations(), |
| }, |
| in: map[string]any{ |
| "y": 5, |
| }, |
| out: types.True, |
| }, |
| } |
| for _, tst := range tests { |
| tc := tst |
| t.Run(tc.name, func(t *testing.T) { |
| blockAST := ast.NewAST(tc.expr, nil) |
| opts := append([]cel.EnvOption{Bindings()}, tc.opts...) |
| env, err := cel.NewEnv(opts...) |
| if err != nil { |
| t.Fatalf("cel.NewEnv(Bindings()) failed: %v", err) |
| } |
| prg, err := env.PlanProgram(blockAST, cel.EvalOptions(cel.OptOptimize)) |
| if err != nil { |
| t.Fatalf("PlanProgram() failed: %v", err) |
| } |
| out, _, err := prg.Eval(tc.in) |
| if err != nil { |
| t.Fatalf("prg.Eval() failed: %v", err) |
| } |
| if out.Equal(tc.out) != types.True { |
| t.Errorf("got %v, wanted %v", out, tc.out) |
| } |
| }) |
| } |
| } |
| |
| func TestBlockEval_BadPlan(t *testing.T) { |
| fac := ast.NewExprFactory() |
| blockExpr := fac.NewCall( |
| 1, "cel.@block", |
| fac.NewList(2, []ast.Expr{ |
| fac.NewIdent(3, "x"), |
| fac.NewIdent(4, "@index0"), |
| }, []int32{}), |
| fac.NewCall(6, operators.Add, |
| fac.NewIdent(7, "@index1"), |
| fac.NewIdent(8, "@index0")), |
| fac.NewIdent(9, "x"), |
| ) |
| blockAST := ast.NewAST(blockExpr, nil) |
| env, err := cel.NewEnv( |
| Bindings(BindingsVersion(1)), |
| cel.Variable("x", cel.StringType), |
| ) |
| if err != nil { |
| t.Fatalf("cel.NewEnv(Bindings()) failed: %v", err) |
| } |
| _, err = env.PlanProgram(blockAST) |
| if err == nil { |
| t.Fatal("PlanProgram() succeeded, expected error") |
| } |
| } |
| |
| func TestBlockEval_BadBlock(t *testing.T) { |
| fac := ast.NewExprFactory() |
| blockExpr := fac.NewCall( |
| 1, "cel.@block", |
| fac.NewCall(2, operators.Add, |
| fac.NewIdent(3, "@index1"), |
| fac.NewIdent(4, "@index0")), |
| fac.NewIdent(5, "x"), |
| ) |
| blockAST := ast.NewAST(blockExpr, nil) |
| env, err := cel.NewEnv( |
| Bindings(BindingsVersion(1)), |
| cel.Variable("x", cel.StringType), |
| ) |
| if err != nil { |
| t.Fatalf("cel.NewEnv(Bindings()) failed: %v", err) |
| } |
| _, err = env.PlanProgram(blockAST) |
| if err == nil { |
| t.Fatal("PlanProgram() succeeded, expected error") |
| } |
| } |
| |
| func TestBlockEval_RuntimeErrors(t *testing.T) { |
| fac := ast.NewExprFactory() |
| tests := []struct { |
| name string |
| expr ast.Expr |
| }{ |
| { |
| name: "bad index", |
| expr: fac.NewCall( |
| 1, "cel.@block", |
| fac.NewList(2, []ast.Expr{ |
| fac.NewIdent(3, "x"), |
| fac.NewIdent(4, "@indexNext"), |
| }, []int32{}), |
| fac.NewCall(6, operators.Add, |
| fac.NewIdent(7, "@indexNext"), |
| fac.NewIdent(8, "@index0")), |
| ), |
| }, |
| { |
| name: "infinite recursion", |
| expr: fac.NewCall( |
| 1, "cel.@block", |
| fac.NewList(2, []ast.Expr{ |
| fac.NewIdent(3, "@index0"), |
| fac.NewIdent(4, "@index0"), |
| }, []int32{}), |
| fac.NewIdent(10, "@index0"), |
| ), |
| }, |
| { |
| name: "negative index", |
| expr: fac.NewCall( |
| 1, "cel.@block", |
| fac.NewList(2, []ast.Expr{ |
| fac.NewIdent(3, "@index-1"), |
| fac.NewIdent(4, "@index0"), |
| }, []int32{}), |
| fac.NewIdent(10, "@index0"), |
| ), |
| }, |
| { |
| name: "out of range index", |
| expr: fac.NewCall( |
| 1, "cel.@block", |
| fac.NewList(2, []ast.Expr{ |
| fac.NewIdent(3, "@index100"), |
| fac.NewIdent(4, "@index0"), |
| }, []int32{}), |
| fac.NewIdent(10, "@index0"), |
| ), |
| }, |
| } |
| for _, tst := range tests { |
| tc := tst |
| t.Run(tc.name, func(t *testing.T) { |
| blockAST := ast.NewAST(tc.expr, nil) |
| env, err := cel.NewEnv( |
| Bindings(BindingsVersion(1)), |
| cel.Variable("x", cel.StringType), |
| ) |
| if err != nil { |
| t.Fatalf("cel.NewEnv(Bindings()) failed: %v", err) |
| } |
| prg, err := env.PlanProgram(blockAST) |
| if err != nil { |
| t.Fatalf("PlanProgram() failed: %v", err) |
| } |
| _, _, err = prg.Eval(map[string]any{"x": "hello"}) |
| if !strings.Contains(err.Error(), "no such attribute") { |
| t.Fatalf("prg.Eval() got %v, expected no such attribute error", err) |
| } |
| }) |
| } |
| } |