| // Copyright 2019 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 cel |
| |
| import ( |
| "fmt" |
| "strings" |
| "testing" |
| "time" |
| |
| "google.golang.org/protobuf/proto" |
| |
| "github.com/google/cel-go/checker/decls" |
| celast "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" |
| |
| proto3pb "github.com/google/cel-go/test/proto3pb" |
| exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" |
| ) |
| |
| func TestRefValueToValueRoundTrip(t *testing.T) { |
| tests := []struct { |
| value any |
| }{ |
| {value: types.NullValue}, |
| {value: types.Bool(true)}, |
| {value: types.String("abc")}, |
| {value: types.Double(0.0)}, |
| {value: types.Bytes(make([]byte, 0, 5))}, |
| {value: types.Int(0)}, |
| {value: types.Uint(0)}, |
| {value: types.Duration{Duration: time.Hour}}, |
| {value: types.Timestamp{Time: time.Unix(0, 0)}}, |
| {value: types.IntType}, |
| {value: types.NewOpaqueType("CustomType")}, |
| {value: map[int64]int64{1: 1}}, |
| {value: []any{true, "abc"}}, |
| {value: &proto3pb.TestAllTypes{SingleString: "abc"}}, |
| } |
| |
| env, err := NewEnv(Types(&proto3pb.TestAllTypes{})) |
| if err != nil { |
| t.Fatalf("NewEnv() failed: %v", err) |
| } |
| |
| for i, tst := range tests { |
| tc := tst |
| t.Run(fmt.Sprintf("[%d]%v", i, tc.value), func(t *testing.T) { |
| refVal := env.TypeAdapter().NativeToValue(tc.value) |
| val, err := RefValueToValue(refVal) |
| if err != nil { |
| t.Fatalf("RefValueToValue(%v) failed with error: %v", refVal, err) |
| } |
| actual, err := ValueToRefValue(env.TypeAdapter(), val) |
| if err != nil { |
| t.Fatalf("ValueToRefValue() failed: %v", err) |
| } |
| if refVal.Equal(actual) != types.True { |
| t.Errorf("got val %v, wanted %v", actual, refVal) |
| } |
| }) |
| } |
| } |
| |
| func TestAstToProto(t *testing.T) { |
| stdEnv, _ := NewEnv(Declarations( |
| decls.NewVar("a", decls.Dyn), |
| decls.NewVar("b", decls.Dyn), |
| )) |
| ast, iss := stdEnv.Parse("a + b") |
| if iss.Err() != nil { |
| t.Fatalf("Parse('a + b') failed: %v", iss.Err()) |
| } |
| parsed, err := AstToParsedExpr(ast) |
| if err != nil { |
| t.Fatalf("AstToParsedExpr() failed: %v", err) |
| } |
| ast2 := ParsedExprToAst(parsed) |
| if !proto.Equal(ast2.Expr(), ast.Expr()) { |
| t.Errorf("got expr %v, wanted %v", ast2, ast) |
| } |
| ast3 := ParsedExprToAstWithSource(parsed, ast.Source()) |
| if !proto.Equal(ast3.Expr(), ast.Expr()) { |
| t.Errorf("got expr %v, wanted %v", ast2, ast) |
| } |
| if ast3.Source() != ast.Source() { |
| t.Errorf("got source %v, wanted %v", ast3.Source(), ast.Source()) |
| } |
| |
| _, err = AstToCheckedExpr(ast) |
| if err == nil { |
| t.Error("expected error converting unchecked ast") |
| } |
| ast, iss = stdEnv.Check(ast) |
| if iss != nil && iss.Err() != nil { |
| t.Fatalf("stdEnv.Check(ast) failed: %v", iss.Err()) |
| } |
| checked, err := AstToCheckedExpr(ast) |
| if err != nil { |
| t.Fatalf("AstToCheckedExpr(ast) failed: %v", err) |
| } |
| ast4 := CheckedExprToAst(checked) |
| if !proto.Equal(ast4.Expr(), ast.Expr()) { |
| t.Fatalf("got ast %v, wanted %v", ast4, ast) |
| } |
| ast5, err := CheckedExprToAstWithSource(checked, ast.Source()) |
| if err != nil { |
| t.Fatalf("CheckedExprToAstWithSource() failed: %v", err) |
| } |
| if !proto.Equal(ast5.Expr(), ast.Expr()) { |
| t.Errorf("got expr %v, wanted %v", ast5, ast) |
| } |
| if ast5.Source() != ast.Source() { |
| t.Errorf("got source %v, wanted %v", ast5.Source(), ast.Source()) |
| } |
| } |
| |
| func TestAstToString(t *testing.T) { |
| stdEnv, err := NewEnv() |
| if err != nil { |
| t.Fatalf("NewEnv() failed: %v", err) |
| } |
| in := "a + b - (c ? (-d + 4) : e)" |
| ast, iss := stdEnv.Parse(in) |
| if iss.Err() != nil { |
| t.Fatalf("stdEnv.Parse(%q) failed: %v", in, iss.Err()) |
| } |
| expr, err := AstToString(ast) |
| if err != nil { |
| t.Fatalf("AstToString(ast) failed: %v", err) |
| } |
| if expr != in { |
| t.Errorf("got %v, wanted %v", expr, in) |
| } |
| } |
| |
| func TestExprToString(t *testing.T) { |
| stdEnv, err := NewEnv(EnableMacroCallTracking()) |
| if err != nil { |
| t.Fatalf("NewEnv() failed: %v", err) |
| } |
| in := "[a, b].filter(i, (i > 0) ? (-i + 4) : i)" |
| ast, iss := stdEnv.Parse(in) |
| if iss.Err() != nil { |
| t.Fatalf("stdEnv.Parse(%q) failed: %v", in, iss.Err()) |
| } |
| expr, err := ExprToString(ast.NativeRep().Expr(), ast.NativeRep().SourceInfo()) |
| if err != nil { |
| t.Fatalf("ExprToString(ast) failed: %v", err) |
| } |
| if expr != in { |
| t.Errorf("got %v, wanted %v", expr, in) |
| } |
| |
| // Test sub-expression unparsing. |
| navExpr := celast.NavigateAST(ast.NativeRep()) |
| condExpr := celast.MatchDescendants(navExpr, celast.FunctionMatcher(operators.Conditional))[0] |
| want := `(i > 0) ? (-i + 4) : i` |
| expr, err = ExprToString(condExpr, ast.NativeRep().SourceInfo()) |
| if err != nil { |
| t.Fatalf("ExprToString(ast) failed: %v", err) |
| } |
| if expr != want { |
| t.Errorf("got %v, wanted %v", expr, want) |
| } |
| |
| // Also passes with a nil source info, but only because the sub-expr doesn't contain macro calls. |
| expr, err = ExprToString(condExpr, nil) |
| if err != nil { |
| t.Fatalf("ExprToString(ast) failed: %v", err) |
| } |
| if expr != want { |
| t.Errorf("got %v, wanted %v", expr, want) |
| } |
| |
| // Fails do to missing macro information. |
| _, err = ExprToString(ast.NativeRep().Expr(), nil) |
| if err == nil { |
| t.Error("ExprToString() succeeded, wanted error") |
| } |
| } |
| |
| func TestRefValToExprValue(t *testing.T) { |
| tests := []struct { |
| name string |
| refVal ref.Val |
| expectError bool |
| }{ |
| { |
| name: "unknown value", |
| refVal: types.NewUnknown(1, nil), |
| expectError: false, |
| }, |
| { |
| name: "error value", |
| refVal: types.NewErr("test error"), |
| expectError: false, |
| }, |
| { |
| name: "bool value", |
| refVal: types.Bool(true), |
| expectError: false, |
| }, |
| { |
| name: "string value", |
| refVal: types.String("test"), |
| expectError: false, |
| }, |
| { |
| name: "int value", |
| refVal: types.Int(1), |
| expectError: false, |
| }, |
| } |
| for _, tst := range tests { |
| tc := tst |
| t.Run(tc.name, func(t *testing.T) { |
| exprVal, err := ExprValueAsProto(tc.refVal) |
| if tc.expectError { |
| if err == nil { |
| t.Errorf("RefValToExprValue(%v) expected error, got %v", tc.refVal, exprVal) |
| } |
| } else { |
| if err != nil { |
| t.Errorf("RefValToExprValue(%v) failed with error: %v", tc.refVal, err) |
| } |
| if exprVal == nil { |
| t.Errorf("RefValToExprValue(%v) expected value, got nil", tc.refVal) |
| } |
| } |
| }) |
| } |
| } |
| |
| func TestAstToStringNil(t *testing.T) { |
| expr, err := AstToString(nil) |
| if err == nil || !strings.Contains(err.Error(), "unsupported expr") { |
| t.Errorf("env.AstToString() got (%v, %v) wanted unsupported expr error", expr, err) |
| } |
| } |
| |
| func TestAstToCheckedExprNil(t *testing.T) { |
| expr, err := AstToCheckedExpr(nil) |
| if err == nil || !strings.Contains(err.Error(), "cannot convert unchecked ast") { |
| t.Errorf("env.AstToCheckedExpr() got (%v, %v) wanted conversion error", expr, err) |
| } |
| } |
| |
| func TestAstToParsedExprNil(t *testing.T) { |
| expr, err := AstToParsedExpr(nil) |
| if err != nil { |
| t.Errorf("env.AstToParsedExpr() got (%v, %v) wanted conversion error", expr, err) |
| } |
| } |
| |
| func TestCheckedExprToAstConstantExpr(t *testing.T) { |
| stdEnv, err := NewEnv() |
| if err != nil { |
| t.Fatalf("NewEnv() failed: %v", err) |
| } |
| in := "10" |
| ast, iss := stdEnv.Compile(in) |
| if iss.Err() != nil { |
| t.Fatalf("stdEnv.Compile(%q) failed: %v", in, iss.Err()) |
| } |
| expr, err := AstToCheckedExpr(ast) |
| if err != nil { |
| t.Fatalf("AstToCheckedExpr(ast) failed: %v", err) |
| } |
| ast2 := CheckedExprToAst(expr) |
| if !proto.Equal(ast2.Expr(), ast.Expr()) { |
| t.Fatalf("got ast %v, wanted %v", ast2, ast) |
| } |
| } |
| |
| func TestCheckedExprToAstMissingInfo(t *testing.T) { |
| stdEnv, err := NewEnv() |
| if err != nil { |
| t.Fatalf("NewEnv() failed: %v", err) |
| } |
| in := "10" |
| ast, iss := stdEnv.Parse(in) |
| if iss.Err() != nil { |
| t.Fatalf("stdEnv.Compile(%q) failed: %v", in, iss.Err()) |
| } |
| if ast.ResultType() != decls.Dyn { |
| t.Fatalf("ast.ResultType() got %v, wanted 'dyn'", ast.ResultType()) |
| } |
| expr, err := AstToParsedExpr(ast) |
| if err != nil { |
| t.Fatalf("AstToParsedExpr(ast) failed: %v", err) |
| } |
| checkedExpr := &exprpb.CheckedExpr{ |
| TypeMap: map[int64]*exprpb.Type{expr.GetExpr().GetId(): decls.Int}, |
| Expr: expr.GetExpr(), |
| } |
| ast2 := CheckedExprToAst(checkedExpr) |
| if !ast2.IsChecked() { |
| t.Fatal("CheckedExprToAst() did not produce a 'checked' ast") |
| } |
| if ast2.ResultType() != decls.Int { |
| t.Fatalf("ast2.ResultType() got %v, wanted 'int'", ast.ResultType()) |
| } |
| } |