blob: 7c133e96f98dbd7e3a302eaf67d055fd99c440f8 [file] [log] [blame]
// Copyright 2018 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 server
import (
"context"
"log"
"os"
"testing"
"github.com/google/cel-go/checker/decls"
"github.com/google/cel-go/common/operators"
"github.com/google/cel-go/test"
"github.com/google/cel-spec/tools/celrpc"
confpb "google.golang.org/genproto/googleapis/api/expr/conformance/v1alpha1"
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
)
type serverTest struct {
client celrpc.ConfClient
}
var (
globals = serverTest{}
)
// TestMain performs setup for testing.
func TestMain(m *testing.M) {
// Use a helper function to ensure we run shutdown()
// before calling os.Exit()
os.Exit(mainHelper(m))
}
func mainHelper(m *testing.M) int {
client, err := celrpc.NewGrpcClient(os.Args[1])
globals.client = client
defer client.Shutdown()
if err != nil {
// testing.M doesn't have a logging method. hmm...
log.Fatal(err)
return 1
}
return m.Run()
}
var (
parsed = &exprpb.ParsedExpr{
Expr: test.ExprCall(1, operators.Add,
test.ExprLiteral(2, int64(1)),
test.ExprLiteral(3, int64(1))),
SourceInfo: &exprpb.SourceInfo{
Location: "the location",
Positions: map[int64]int32{
1: 0,
2: 0,
3: 4,
},
},
}
)
// TestParse tests the Parse method.
func TestParse(t *testing.T) {
req := confpb.ParseRequest{
CelSource: "1 + 1",
}
res, err := globals.client.Parse(context.Background(), &req)
if err != nil {
t.Fatal(err)
}
if res == nil {
t.Fatal("Empty result")
}
if res.ParsedExpr == nil {
t.Fatal("Empty parsed expression in result")
}
// Could check against 'parsed' above,
// but the expression ids are arbitrary,
// and explicit comparison logic is about as
// much work as normalization would be.
if res.ParsedExpr.Expr == nil {
t.Fatal("Empty expression in result")
}
switch res.ParsedExpr.Expr.ExprKind.(type) {
case *exprpb.Expr_CallExpr:
c := res.ParsedExpr.Expr.GetCallExpr()
if c.Target != nil {
t.Error("Call has target", c)
}
if c.Function != "_+_" {
t.Error("Wrong function", c)
}
if len(c.Args) != 2 {
t.Error("Too many or few args", c)
}
for i, a := range c.Args {
switch a.ExprKind.(type) {
case *exprpb.Expr_ConstExpr:
l := a.GetConstExpr()
switch l.ConstantKind.(type) {
case *exprpb.Constant_Int64Value:
if l.GetInt64Value() != int64(1) {
t.Errorf("Arg %d wrong value: %v", i, a)
}
default:
t.Errorf("Arg %d not int: %v", i, a)
}
default:
t.Errorf("Arg %d not literal: %v", i, a)
}
}
default:
t.Error("Wrong expression type", res.ParsedExpr.Expr)
}
}
// TestCheck tests the Check method.
func TestCheck(t *testing.T) {
// If TestParse() passes, it validates a good chunk
// of the server mechanisms for data conversion, so we
// won't be as fussy here..
req := confpb.CheckRequest{
ParsedExpr: parsed,
}
res, err := globals.client.Check(context.Background(), &req)
if err != nil {
t.Fatal(err)
}
if res == nil {
t.Fatal("Empty result")
}
if res.CheckedExpr == nil {
t.Fatal("No checked expression")
}
tp, present := res.CheckedExpr.TypeMap[int64(1)]
if !present {
t.Fatal("No type for top level expression", res)
}
switch tp.TypeKind.(type) {
case *exprpb.Type_Primitive:
if tp.GetPrimitive() != exprpb.Type_INT64 {
t.Error("Bad top-level type", tp)
}
default:
t.Error("Bad top-level type", tp)
}
}
// TestEval tests the Eval method.
func TestEval(t *testing.T) {
req := confpb.EvalRequest{
ExprKind: &confpb.EvalRequest_ParsedExpr{ParsedExpr: parsed},
}
res, err := globals.client.Eval(context.Background(), &req)
if err != nil {
t.Fatal(err)
}
if res == nil || res.Result == nil {
t.Fatal("Nil result")
}
switch res.Result.Kind.(type) {
case *exprpb.ExprValue_Value:
v := res.Result.GetValue()
switch v.Kind.(type) {
case *exprpb.Value_Int64Value:
if v.GetInt64Value() != int64(2) {
t.Error("Wrong result for 1 + 1", v)
}
default:
t.Error("Wrong result value type", v)
}
default:
t.Fatal("Result not a value", res.Result)
}
}
// TestFullUp tests Parse, Check, and Eval back-to-back.
func TestFullUp(t *testing.T) {
preq := confpb.ParseRequest{
CelSource: "x + y",
}
pres, err := globals.client.Parse(context.Background(), &preq)
if err != nil {
t.Fatal(err)
}
parsedExpr := pres.ParsedExpr
if parsedExpr == nil {
t.Fatal("Empty parsed expression")
}
creq := confpb.CheckRequest{
ParsedExpr: parsedExpr,
TypeEnv: []*exprpb.Decl{
decls.NewVar("x", decls.Int),
decls.NewVar("y", decls.Int),
},
}
cres, err := globals.client.Check(context.Background(), &creq)
if err != nil {
t.Fatal(err)
}
if cres == nil {
t.Fatal("Empty check result")
}
checkedExpr := cres.CheckedExpr
if checkedExpr == nil {
t.Fatal("No checked expression")
}
tp, present := checkedExpr.TypeMap[int64(1)]
if !present {
t.Fatal("No type for top level expression", cres)
}
switch tp.TypeKind.(type) {
case *exprpb.Type_Primitive:
if tp.GetPrimitive() != exprpb.Type_INT64 {
t.Error("Bad top-level type", tp)
}
default:
t.Error("Bad top-level type", tp)
}
ereq := confpb.EvalRequest{
ExprKind: &confpb.EvalRequest_CheckedExpr{CheckedExpr: checkedExpr},
Bindings: map[string]*exprpb.ExprValue{
"x": exprValueInt64(1),
"y": exprValueInt64(2),
},
}
eres, err := globals.client.Eval(context.Background(), &ereq)
if err != nil {
t.Fatal(err)
}
if eres == nil || eres.Result == nil {
t.Fatal("Nil result")
}
switch eres.Result.Kind.(type) {
case *exprpb.ExprValue_Value:
v := eres.Result.GetValue()
switch v.Kind.(type) {
case *exprpb.Value_Int64Value:
if v.GetInt64Value() != int64(3) {
t.Error("Wrong result for 1 + 2", v)
}
default:
t.Error("Wrong result value type", v)
}
default:
t.Fatal("Result not a value", eres.Result)
}
}
func exprValueInt64(x int64) *exprpb.ExprValue {
return &exprpb.ExprValue{
Kind: &exprpb.ExprValue_Value{
Value: &exprpb.Value{
Kind: &exprpb.Value_Int64Value{Int64Value: x},
},
},
}
}
// fullPipeline parses, checks, and evaluates the CEL expression in source
// and returns the result from the Eval call.
func fullPipeline(t *testing.T, source string) (*confpb.ParseResponse, *confpb.CheckResponse, *confpb.EvalResponse) {
t.Helper()
// Parse
preq := confpb.ParseRequest{
CelSource: source,
}
pres, err := globals.client.Parse(context.Background(), &preq)
if err != nil {
t.Fatal(err)
}
if pres == nil {
t.Fatal("Empty parse result")
}
parsedExpr := pres.ParsedExpr
if parsedExpr == nil {
t.Fatal("Empty parsed expression")
}
if parsedExpr.Expr == nil {
t.Fatal("Empty root expression")
}
// Check
creq := confpb.CheckRequest{
ParsedExpr: parsedExpr,
}
cres, err := globals.client.Check(context.Background(), &creq)
if err != nil {
t.Fatal(err)
}
if cres == nil {
t.Fatal("Empty check result")
}
checkedExpr := cres.CheckedExpr
if checkedExpr == nil {
t.Fatal("No checked expression")
}
// Eval
ereq := confpb.EvalRequest{
ExprKind: &confpb.EvalRequest_CheckedExpr{CheckedExpr: checkedExpr},
}
eres, err := globals.client.Eval(context.Background(), &ereq)
if err != nil {
t.Fatal(err)
}
if eres == nil || eres.Result == nil {
t.Fatal("Nil result")
}
return pres, cres, eres
}
// expectEvalTrue parses, checks, and evaluates the CEL expression in source
// and checks that the result is the boolean value 'true'.
func expectEvalTrue(t *testing.T, source string) {
t.Helper()
pres, cres, eres := fullPipeline(t, source)
rootID := pres.ParsedExpr.Expr.Id
topType, present := cres.CheckedExpr.TypeMap[rootID]
if !present {
t.Fatal("No type for top level expression", cres)
}
switch topType.TypeKind.(type) {
case *exprpb.Type_Primitive:
if topType.GetPrimitive() != exprpb.Type_BOOL {
t.Error("Bad top-level type", topType)
}
default:
t.Error("Bad top-level type", topType)
}
switch eres.Result.Kind.(type) {
case *exprpb.ExprValue_Value:
v := eres.Result.GetValue()
switch v.Kind.(type) {
case *exprpb.Value_BoolValue:
if !v.GetBoolValue() {
t.Error("Wrong result", v)
}
default:
t.Error("Wrong result value type", v)
}
default:
t.Fatal("Result not a value", eres.Result)
}
}
// TestCondTrue tests true conditional behavior.
func TestCondTrue(t *testing.T) {
expectEvalTrue(t, "(true ? 'a' : 'b') == 'a'")
}
// TestCondFalse tests false conditional behavior.
func TestCondFalse(t *testing.T) {
expectEvalTrue(t, "(false ? 'a' : 'b') == 'b'")
}
// TestMapOrderInsignificant tests that maps with different order are equal.
func TestMapOrderInsignificant(t *testing.T) {
expectEvalTrue(t, "{1: 'a', 2: 'b'} == {2: 'b', 1: 'a'}")
}
// FailsTestOneMetaType tests that types of different types are equal.
func FailsTestOneMetaType(t *testing.T) {
expectEvalTrue(t, "type(type(1)) == type(type('foo'))")
}
// FailsTestTypeType tests that the meta-type is its own type.
func FailsTestTypeType(t *testing.T) {
expectEvalTrue(t, "type(type) == type")
}
// FailsTestNullTypeName checks that the type of null is "null_type".
func FailsTestNullTypeName(t *testing.T) {
expectEvalTrue(t, "type(null) == null_type")
}
// TestError ensures that errors are properly transmitted.
func TestError(t *testing.T) {
_, _, eres := fullPipeline(t, "1 / 0")
switch eres.Result.Kind.(type) {
case *exprpb.ExprValue_Error:
return
}
t.Fatalf("got %v, want division by zero error", eres.Result)
}