blob: 6bdb03e19c0f408e9366beaae011fccdaf6c7091 [file] [edit]
// 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 interpreter
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"reflect"
"strings"
"testing"
"time"
"github.com/google/cel-go/checker"
"github.com/google/cel-go/common"
"github.com/google/cel-go/common/ast"
"github.com/google/cel-go/common/containers"
"github.com/google/cel-go/common/decls"
"github.com/google/cel-go/common/functions"
"github.com/google/cel-go/common/operators"
"github.com/google/cel-go/common/stdlib"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/common/types/traits"
"github.com/google/cel-go/parser"
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
structpb "google.golang.org/protobuf/types/known/structpb"
tpb "google.golang.org/protobuf/types/known/timestamppb"
wrapperspb "google.golang.org/protobuf/types/known/wrapperspb"
proto2pb "github.com/google/cel-go/test/proto2pb"
proto3pb "github.com/google/cel-go/test/proto3pb"
)
type testCase struct {
name string
expr string
container string
abbrevs []string
typeOpts []types.RegistryOption
vars []*decls.VariableDecl
funcs []*decls.FunctionDecl
attrs AttributeFactory
unchecked bool
extraOpts []PlannerOption
in any
out any
err string
progErr string
}
func testData(t testing.TB) []testCase {
return []testCase{
{
name: "double_ne_nan",
expr: `0.0/0.0 == 0.0/0.0`,
out: types.False,
},
{
name: "and_false_1st",
expr: `false && true`,
out: types.False,
},
{
name: "and_false_2nd",
expr: `true && false`,
out: types.False,
},
{
name: "and_error_1st_false",
expr: `1/0 != 0 && false`,
out: types.False,
},
{
name: "and_error_2nd_false",
expr: `false && 1/0 != 0`,
out: types.False,
},
{
name: "and_error_1st_error",
expr: `1/0 != 0 && true`,
err: "division by zero",
},
{
name: "and_error_2nd_error",
expr: `true && 1/0 != 0`,
err: "division by zero",
},
{
name: "call_no_args",
expr: `zero()`,
unchecked: true,
funcs: []*decls.FunctionDecl{
funcDecl(t, "zero",
decls.Overload("zero", []*types.Type{}, types.IntType),
decls.SingletonFunctionBinding(func(args ...ref.Val) ref.Val {
return types.IntZero
}),
)},
out: types.IntZero,
},
{
name: "call_one_arg",
expr: `neg(1)`,
unchecked: true,
funcs: []*decls.FunctionDecl{
funcDecl(t, "neg",
decls.Overload("neg_int", []*types.Type{types.IntType}, types.IntType,
decls.OverloadOperandTrait(traits.NegatorType),
decls.UnaryBinding(func(arg ref.Val) ref.Val {
return arg.(traits.Negater).Negate()
}),
),
),
},
out: types.IntNegOne,
},
{
name: "call_two_arg",
expr: `b'abc'.concat(b'def')`,
unchecked: true,
funcs: []*decls.FunctionDecl{
funcDecl(t, "concat",
decls.MemberOverload("bytes_concat_bytes", []*types.Type{types.BytesType, types.BytesType}, types.BytesType,
decls.OverloadOperandTrait(traits.AdderType),
decls.BinaryBinding(func(lhs, rhs ref.Val) ref.Val {
return lhs.(traits.Adder).Add(rhs)
}))),
},
out: []byte{'a', 'b', 'c', 'd', 'e', 'f'},
},
{
name: "call_four_args",
expr: `addall(a, b, c, d) == 10`,
unchecked: true,
funcs: []*decls.FunctionDecl{
funcDecl(t, "addall",
decls.Overload("addall_four",
[]*types.Type{types.IntType, types.IntType, types.IntType, types.IntType},
types.IntType),
decls.DisableTypeGuards(true),
decls.SingletonFunctionBinding(func(args ...ref.Val) ref.Val {
val := types.Int(0)
for _, arg := range args {
val += arg.(types.Int)
}
return val
}, traits.AdderType)),
},
in: map[string]any{
"a": 1, "b": 2, "c": 3, "d": 4,
},
},
{
name: `call_ns_func`,
expr: `base64.encode('hello')`,
funcs: []*decls.FunctionDecl{
funcDecl(t, "base64.encode",
decls.Overload("base64_encode_string", []*types.Type{types.StringType}, types.StringType),
decls.SingletonUnaryBinding(base64Encode)),
},
out: "aGVsbG8=",
},
{
name: `call_ns_func_unchecked`,
expr: `base64.encode('hello')`,
unchecked: true,
funcs: []*decls.FunctionDecl{
funcDecl(t, "base64.encode",
decls.Overload("base64_encode_string", []*types.Type{types.StringType}, types.StringType),
decls.SingletonUnaryBinding(base64Encode)),
},
out: "aGVsbG8=",
},
{
name: `call_ns_func_in_pkg`,
container: `base64`,
expr: `encode('hello')`,
funcs: []*decls.FunctionDecl{
funcDecl(t, "base64.encode",
decls.Overload("base64_encode_string", []*types.Type{types.StringType}, types.StringType),
decls.SingletonUnaryBinding(base64Encode)),
},
out: "aGVsbG8=",
},
{
name: `call_ns_func_unchecked_in_pkg`,
expr: `encode('hello')`,
container: `base64`,
unchecked: true,
funcs: []*decls.FunctionDecl{
funcDecl(t, "base64.encode",
decls.Overload("base64_encode_string", []*types.Type{types.StringType}, types.StringType),
decls.SingletonUnaryBinding(base64Encode)),
},
out: "aGVsbG8=",
},
{
name: "complex",
expr: `
!(headers.ip in ["10.0.1.4", "10.0.1.5"]) &&
((headers.path.startsWith("v1") && headers.token in ["v1", "v2", "admin"]) ||
(headers.path.startsWith("v2") && headers.token in ["v2", "admin"]) ||
(headers.path.startsWith("/admin") && headers.token == "admin" && headers.ip in ["10.0.1.2", "10.0.1.2", "10.0.1.2"]))
`,
vars: []*decls.VariableDecl{
decls.NewVariable("headers", types.NewMapType(types.StringType, types.StringType)),
},
in: map[string]any{
"headers": map[string]any{
"ip": "10.0.1.2",
"path": "/admin/edit",
"token": "admin",
},
},
},
{
name: "complex_qual_vars",
expr: `
!(headers.ip in ["10.0.1.4", "10.0.1.5"]) &&
((headers.path.startsWith("v1") && headers.token in ["v1", "v2", "admin"]) ||
(headers.path.startsWith("v2") && headers.token in ["v2", "admin"]) ||
(headers.path.startsWith("/admin") && headers.token == "admin" && headers.ip in ["10.0.1.2", "10.0.1.2", "10.0.1.2"]))
`,
vars: []*decls.VariableDecl{
decls.NewVariable("headers.ip", types.StringType),
decls.NewVariable("headers.path", types.StringType),
decls.NewVariable("headers.token", types.StringType),
},
in: map[string]any{
"headers.ip": "10.0.1.2",
"headers.path": "/admin/edit",
"headers.token": "admin",
},
},
{
name: "cond",
expr: `a ? b < 1.2 : c == ['hello']`,
vars: []*decls.VariableDecl{
decls.NewVariable("a", types.BoolType),
decls.NewVariable("b", types.DoubleType),
decls.NewVariable("c", types.NewListType(types.StringType)),
},
in: map[string]any{
"a": true,
"b": 2.0,
"c": []string{"hello"},
},
out: types.False,
},
{
name: "cond_attr_out_of_bounds_error",
expr: `m[(x ? 0 : 1)] >= 0`,
vars: []*decls.VariableDecl{
decls.NewVariable("m", types.NewListType(types.IntType)),
decls.NewVariable("x", types.BoolType),
},
in: map[string]any{
"m": []int{-1},
"x": false,
},
err: "index out of bounds: 1",
},
{
name: "cond_attr_qualify_bad_type_error",
expr: `m[(x ? a : b)] >= 0`,
vars: []*decls.VariableDecl{
decls.NewVariable("m", types.NewListType(types.DynType)),
decls.NewVariable("a", types.DynType),
decls.NewVariable("b", types.DynType),
decls.NewVariable("x", types.BoolType),
},
in: map[string]any{
"m": []int{1},
"x": false,
"a": time.Millisecond,
"b": time.Millisecond,
},
err: "invalid qualifier type",
},
{
name: "cond_attr_qualify_bad_field_error",
expr: `m[(x ? a : b).c] >= 0`,
vars: []*decls.VariableDecl{
decls.NewVariable("m", types.NewListType(types.DynType)),
decls.NewVariable("a", types.DynType),
decls.NewVariable("b", types.DynType),
decls.NewVariable("x", types.BoolType),
},
in: map[string]any{
"m": []int{1},
"x": false,
"a": int32(1),
"b": int32(2),
},
err: "no such key: c",
},
{
name: "in_empty_list",
expr: `6 in []`,
out: types.False,
},
{
name: "in_constant_list",
expr: `6 in [2, 12, 6]`,
},
{
name: "bytes_in_constant_list",
expr: "b'hello' in [b'world', b'universe', b'hello']",
},
{
name: "list_in_constant_list",
expr: `[6] in [2, 12, [6]]`,
},
{
name: "in_constant_list_cross_type_uint_int",
expr: `dyn(12u) in [2, 12, 6]`,
},
{
name: "in_constant_list_cross_type_double_int",
expr: `dyn(6.0) in [2, 12, 6]`,
},
{
name: "in_constant_list_cross_type_int_double",
expr: `dyn(6) in [2.1, 12.0, 6.0]`,
},
{
name: "not_in_constant_list_cross_type_int_double",
expr: `dyn(2) in [2.1, 12.0, 6.0]`,
out: types.False,
},
{
name: "in_constant_list_cross_type_int_uint",
expr: `dyn(6) in [2u, 12u, 6u]`,
},
{
name: "in_constant_list_cross_type_negative_int_uint",
expr: `dyn(-6) in [2u, 12u, 6u]`,
out: types.False,
},
{
name: "in_constant_list_cross_type_negative_double_uint",
expr: `dyn(-6.1) in [2u, 12u, 6u]`,
out: types.False,
},
{
name: "in_var_list_int",
expr: `6 in [2, 12, x]`,
vars: []*decls.VariableDecl{
decls.NewVariable("x", types.DynType),
},
in: map[string]any{"x": 6},
},
{
name: "in_var_list_uint",
expr: `6 in [2, 12, x]`,
vars: []*decls.VariableDecl{
decls.NewVariable("x", types.DynType),
},
in: map[string]any{"x": uint64(6)},
},
{
name: "in_var_list_double",
expr: `6 in [2, 12, x]`,
vars: []*decls.VariableDecl{
decls.NewVariable("x", types.DynType),
},
in: map[string]any{"x": 6.0},
},
{
name: "in_var_list_double_double",
expr: `dyn(6.0) in [2, 12, x]`,
vars: []*decls.VariableDecl{
decls.NewVariable("x", types.IntType),
},
in: map[string]any{"x": 6},
},
{
name: "in_constant_map",
expr: `'other-key' in {'key': null, 'other-key': 42}`,
out: types.True,
},
{
name: "in_constant_map_cross_type_string_number",
expr: `'other-key' in {1: null, 2u: 42}`,
out: types.False,
},
{
name: "in_constant_map_cross_type_double_int",
expr: `2.0 in {1: null, 2u: 42}`,
},
{
name: "not_in_constant_map_cross_type_double_int",
expr: `2.1 in {1: null, 2u: 42}`,
out: types.False,
},
{
name: "in_constant_heterogeneous_map",
expr: `'hello' in {1: 'one', false: true, 'hello': 'world'}`,
out: types.True,
},
{
name: "not_in_constant_heterogeneous_map",
expr: `!('hello' in {1: 'one', false: true})`,
out: types.True,
},
{
name: "not_in_constant_heterogeneous_map_with_same_key_type",
expr: `!('hello' in {1: 'one', 'world': true})`,
out: types.True,
},
{
name: "in_var_key_map",
expr: `'other-key' in {x: null, y: 42}`,
out: types.True,
vars: []*decls.VariableDecl{
decls.NewVariable("x", types.StringType),
decls.NewVariable("y", types.IntType),
},
in: map[string]any{
"x": "other-key",
"y": 2,
},
},
{
name: "in_var_value_map",
expr: `'other-key' in {1: x, 2u: y}`,
out: types.False,
vars: []*decls.VariableDecl{
decls.NewVariable("x", types.StringType),
decls.NewVariable("y", types.IntType),
},
in: map[string]any{
"x": "other-value",
"y": 2,
},
},
{
name: "index",
expr: `m['key'][1] == 42u && m['null'] == null && m[string(0)] == 10`,
vars: []*decls.VariableDecl{
decls.NewVariable("m", types.NewMapType(types.StringType, types.DynType)),
},
in: map[string]any{
"m": map[string]any{
"key": []uint{21, 42},
"null": nil,
"0": 10,
},
},
},
{
name: "index_cross_type_float_uint",
expr: `{1: 'hello'}[x] == 'hello' && {2: 'world'}[y] == 'world'`,
vars: []*decls.VariableDecl{
decls.NewVariable("x", types.DynType),
decls.NewVariable("y", types.DynType),
},
in: map[string]any{
"x": float32(1.0),
"y": uint(2),
},
},
{
name: "no_index_cross_type_float_uint",
expr: `{1: 'hello'}[x] == 'hello' && ['world'][y] == 'world'`,
vars: []*decls.VariableDecl{
decls.NewVariable("x", types.DynType),
decls.NewVariable("y", types.DynType),
},
in: map[string]any{
"x": float32(2.0),
"y": uint(3),
},
err: "no such key: 2",
},
{
name: "index_cross_type_double",
expr: `{1: 'hello', 2: 'world'}[x] == 'hello'`,
vars: []*decls.VariableDecl{
decls.NewVariable("x", types.DynType),
},
in: map[string]any{
"x": 1.0,
},
},
{
name: "index_cross_type_double_const",
expr: `{1: 'hello', 2: 'world'}[dyn(2.0)] == 'world'`,
},
{
name: "index_cross_type_uint",
expr: `{1: 'hello', 2: 'world'}[dyn(2u)] == 'world'`,
},
{
name: "index_cross_type_bad_qualifier",
expr: `{1: 'hello', 2: 'world'}[x] == 'world'`,
vars: []*decls.VariableDecl{
decls.NewVariable("x", types.DynType),
},
in: map[string]any{
"x": time.Millisecond,
},
err: "invalid qualifier type",
},
{
name: "index_list_int_double_type_index",
expr: `[7, 8, 9][dyn(0.0)] == 7`,
},
{
name: "index_list_int_uint_type_index",
expr: `[7, 8, 9][dyn(0u)] == 7`,
},
{
name: "index_list_int_bad_double_type_index",
expr: `[7, 8, 9][dyn(0.1)] == 7`,
err: `unsupported index value`,
},
{
name: "index_relative",
expr: `([[[1]], [[2]], [[3]]][0][0] + [2, 3, {'four': {'five': 'six'}}])[3].four.five == 'six'`,
},
{
name: "list_eq_false_with_error",
expr: `['string', 1] == [2, 3]`,
out: types.False,
},
{
name: "list_eq_error",
expr: `['string', true] == [2, 3]`,
out: types.False,
},
{
name: "literal_bool_false",
expr: `false`,
out: types.False,
},
{
name: "literal_bool_true",
expr: `true`,
},
{
name: "literal_null",
expr: `null`,
out: types.NullValue,
},
{
name: "literal_list",
expr: `[1, 2, 3]`,
out: []int64{1, 2, 3},
},
{
name: "literal_map",
expr: `{'hi': 21, 'world': 42u}`,
out: map[string]any{
"hi": 21,
"world": uint(42),
},
},
{
name: "literal_equiv_string_bytes",
expr: `string(bytes("\303\277")) == '''\303\277'''`,
},
{
name: "literal_not_equiv_string_bytes",
expr: `string(b"\303\277") != '''\303\277'''`,
},
{
name: "literal_equiv_bytes_string",
expr: `string(b"\303\277") == 'ÿ'`,
},
{
name: "literal_bytes_string",
expr: `string(b'aaa"bbb')`,
out: `aaa"bbb`,
},
{
name: "literal_bytes_string2",
expr: `string(b"""Kim\t""")`,
out: `Kim `,
},
{
name: "literal_pb3_msg",
container: "google.api.expr",
typeOpts: []types.RegistryOption{types.ProtoTypeDefs(&exprpb.Expr{})},
expr: `v1alpha1.Expr{
id: 1,
const_expr: v1alpha1.Constant{
string_value: "oneof_test"
}
}`,
out: &exprpb.Expr{Id: 1,
ExprKind: &exprpb.Expr_ConstExpr{
ConstExpr: &exprpb.Constant{
ConstantKind: &exprpb.Constant_StringValue{
StringValue: "oneof_test"}}}},
},
{
name: "literal_pb_enum",
container: "google.expr.proto3.test",
typeOpts: []types.RegistryOption{types.ProtoTypeDefs(&proto3pb.TestAllTypes{})},
expr: `TestAllTypes{
repeated_nested_enum: [
0,
TestAllTypes.NestedEnum.BAZ,
TestAllTypes.NestedEnum.BAR],
repeated_int32: [
TestAllTypes.NestedEnum.FOO,
TestAllTypes.NestedEnum.BAZ]}`,
out: &proto3pb.TestAllTypes{
RepeatedNestedEnum: []proto3pb.TestAllTypes_NestedEnum{
proto3pb.TestAllTypes_FOO,
proto3pb.TestAllTypes_BAZ,
proto3pb.TestAllTypes_BAR,
},
RepeatedInt32: []int32{0, 2},
},
},
{
name: "literal_pb_wrapper_assign",
container: "google.expr.proto3.test",
typeOpts: []types.RegistryOption{types.ProtoTypeDefs(&proto3pb.TestAllTypes{})},
expr: `TestAllTypes{
single_int64_wrapper: 10,
single_int32_wrapper: TestAllTypes{}.single_int32_wrapper,
}`,
out: &proto3pb.TestAllTypes{
SingleInt64Wrapper: wrapperspb.Int64(10),
},
},
{
name: "literal_pb_wrapper_assign_roundtrip",
container: "google.expr.proto3.test",
typeOpts: []types.RegistryOption{types.ProtoTypeDefs(&proto3pb.TestAllTypes{})},
expr: `TestAllTypes{
single_int32_wrapper: TestAllTypes{}.single_int32_wrapper,
}.single_int32_wrapper == null`,
out: true,
},
{
name: "literal_pb_list_assign_null_wrapper",
container: "google.expr.proto3.test",
typeOpts: []types.RegistryOption{types.ProtoTypeDefs(&proto3pb.TestAllTypes{})},
expr: `TestAllTypes{
repeated_int32: [123, 456, TestAllTypes{}.single_int32_wrapper],
}`,
err: "field type conversion error",
},
{
name: "literal_pb_map_assign_null_entry_value",
container: "google.expr.proto3.test",
typeOpts: []types.RegistryOption{types.ProtoTypeDefs(&proto3pb.TestAllTypes{})},
expr: `TestAllTypes{
map_string_string: {
'hello': 'world',
'goodbye': TestAllTypes{}.single_string_wrapper,
},
}`,
err: "field type conversion error",
},
{
name: "unset_wrapper_access",
container: "google.expr.proto3.test",
typeOpts: []types.RegistryOption{types.ProtoTypeDefs(&proto3pb.TestAllTypes{})},
expr: `TestAllTypes{}.single_string_wrapper`,
out: types.NullValue,
},
{
name: "timestamp_eq_timestamp",
expr: `timestamp(0) == timestamp(0)`,
},
{
name: "timestamp_ne_timestamp",
expr: `timestamp(1) != timestamp(2)`,
},
{
name: "timestamp_lt_timestamp",
expr: `timestamp(0) < timestamp(1)`,
},
{
name: "timestamp_le_timestamp",
expr: `timestamp(2) <= timestamp(2)`,
},
{
name: "timestamp_gt_timestamp",
expr: `timestamp(1) > timestamp(0)`,
},
{
name: "timestamp_ge_timestamp",
expr: `timestamp(2) >= timestamp(2)`,
},
{
name: "timestamp_methods",
vars: []*decls.VariableDecl{
decls.NewVariable("x", types.TimestampType),
},
in: map[string]any{
"x": time.Unix(7506, 1000000).Local(),
},
expr: `
x.getFullYear() == 1970
&& x.getMonth() == 0
&& x.getDayOfYear() == 0
&& x.getDayOfMonth() == 0
&& x.getDate() == 1
&& x.getDayOfWeek() == 4
&& x.getHours() == 2
&& x.getMinutes() == 5
&& x.getSeconds() == 6
&& x.getMilliseconds() == 1
&& x.getFullYear('-07:30') == 1969
&& x.getDayOfYear('-07:30') == 364
&& x.getMonth('-07:30') == 11
&& x.getDayOfMonth('-07:30') == 30
&& x.getDate('-07:30') == 31
&& x.getDayOfWeek('-07:30') == 3
&& x.getHours('-07:30') == 18
&& x.getMinutes('-07:30') == 35
&& x.getSeconds('-07:30') == 6
&& x.getMilliseconds('-07:30') == 1
&& x.getFullYear('23:15') == 1970
&& x.getDayOfYear('23:15') == 1
&& x.getMonth('23:15') == 0
&& x.getDayOfMonth('23:15') == 1
&& x.getDate('23:15') == 2
&& x.getDayOfWeek('23:15') == 5
&& x.getHours('23:15') == 1
&& x.getMinutes('23:15') == 20
&& x.getSeconds('23:15') == 6
&& x.getMilliseconds('23:15') == 1`,
},
{
name: "string_to_timestamp",
expr: `timestamp('1986-04-26T01:23:40Z')`,
out: &tpb.Timestamp{Seconds: 514862620},
},
{
name: "macro_all_non_strict",
expr: `![0, 2, 4].all(x, 4/x != 2 && 4/(4-x) != 2)`,
},
{
name: "macro_all_non_strict_var",
expr: `code == "111" && ["a", "b"].all(x, x in tags)
|| code == "222" && ["a", "b"].all(x, x in tags)`,
vars: []*decls.VariableDecl{
decls.NewVariable("code", types.StringType),
decls.NewVariable("tags", types.NewListType(types.StringType)),
},
in: map[string]any{
"code": "222",
"tags": []string{"a", "b"},
},
},
{
name: "macro_exists_lit",
expr: `[1, 2, 3, 4, 5u, 1.0].exists(e, type(e) == uint)`,
},
{
name: "macro_exists_nonstrict",
expr: `[0, 2, 4].exists(x, 4/x == 2 && 4/(4-x) == 2)`,
},
{
name: "macro_exists_var",
expr: `elems.exists(e, type(e) == uint)`,
vars: []*decls.VariableDecl{
decls.NewVariable("elems", types.NewListType(types.DynType)),
},
in: map[string]any{
"elems": []any{0, 1, 2, 3, 4, uint(5), 6},
},
},
{
name: "macro_exists_one",
expr: `[1, 2, 3].exists_one(x, (x % 2) == 0)`,
},
{
name: "macro_filter",
expr: `[-10, -9, -8, -7, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3].filter(x, x > 0)`,
out: []int64{1, 2, 3},
},
{
name: "macro_has_map_key",
expr: `has({'a':1}.a) && !has({}.a)`,
},
{
name: "macro_has_pb2_field_undefined",
container: "google.expr.proto2.test",
typeOpts: []types.RegistryOption{types.ProtoTypeDefs(&proto2pb.TestAllTypes{})},
unchecked: true,
expr: `has(TestAllTypes{}.invalid_field)`,
err: "no such field 'invalid_field'",
},
{
name: "macro_has_pb2_field",
container: "google.expr.proto2.test",
typeOpts: []types.RegistryOption{types.ProtoTypeDefs(&proto2pb.TestAllTypes{})},
vars: []*decls.VariableDecl{
decls.NewVariable("pb2", types.NewObjectType("google.expr.proto2.test.TestAllTypes")),
},
in: map[string]any{
"pb2": &proto2pb.TestAllTypes{
RepeatedBool: []bool{false},
MapInt64NestedType: map[int64]*proto2pb.NestedTestAllTypes{
1: {},
},
MapStringString: map[string]string{},
},
},
expr: `has(TestAllTypes{standalone_enum: TestAllTypes.NestedEnum.BAR}.standalone_enum)
&& has(TestAllTypes{standalone_enum: TestAllTypes.NestedEnum.FOO}.standalone_enum)
&& !has(TestAllTypes{single_nested_enum: TestAllTypes.NestedEnum.FOO}.single_nested_message)
&& has(TestAllTypes{single_nested_enum: TestAllTypes.NestedEnum.FOO}.single_nested_enum)
&& !has(TestAllTypes{}.standalone_enum)
&& !has(pb2.single_int64)
&& has(pb2.repeated_bool)
&& !has(pb2.repeated_int32)
&& has(pb2.map_int64_nested_type)
&& !has(pb2.map_string_string)`,
},
{
name: "macro_has_pb2_field_json",
container: "google.expr.proto2.test",
typeOpts: []types.RegistryOption{types.JSONFieldNames(true), types.ProtoTypeDefs(&proto2pb.TestAllTypes{})},
vars: []*decls.VariableDecl{
decls.NewVariable("pb2", types.NewObjectType("google.expr.proto2.test.TestAllTypes")),
},
in: map[string]any{
"pb2": &proto2pb.TestAllTypes{
RepeatedBool: []bool{false},
MapInt64NestedType: map[int64]*proto2pb.NestedTestAllTypes{
1: {},
},
MapStringString: map[string]string{},
},
},
expr: `has(TestAllTypes{standaloneEnum: TestAllTypes.NestedEnum.BAR}.standaloneEnum)
&& has(TestAllTypes{standaloneEnum: TestAllTypes.NestedEnum.FOO}.standaloneEnum)
&& !has(TestAllTypes{singleNestedEnum: TestAllTypes.NestedEnum.FOO}.singleNestedMessage)
&& has(TestAllTypes{singleNestedEnum: TestAllTypes.NestedEnum.FOO}.singleNestedEnum)
&& !has(TestAllTypes{}.singleNestedMessage)
&& has(TestAllTypes{singleNestedMessage: TestAllTypes.NestedMessage{}}.singleNestedMessage)
&& !has(TestAllTypes{}.standaloneEnum)
&& !has(pb2.singleInt64)
&& has(pb2.repeatedBool)
&& !has(pb2.repeatedInt32)
&& has(pb2.mapInt64NestedType)
&& !has(pb2.mapStringString)`,
},
{
name: "macro_has_pb3_field",
typeOpts: []types.RegistryOption{types.ProtoTypeDefs(&proto3pb.TestAllTypes{})},
vars: []*decls.VariableDecl{
decls.NewVariable("pb3", types.NewObjectType("google.expr.proto3.test.TestAllTypes")),
},
container: "google.expr.proto3.test",
in: map[string]any{
"pb3": &proto3pb.TestAllTypes{
RepeatedBool: []bool{false},
MapInt64NestedType: map[int64]*proto3pb.NestedTestAllTypes{
1: {},
},
MapStringString: map[string]string{},
},
},
expr: `has(TestAllTypes{standalone_enum: TestAllTypes.NestedEnum.BAR}.standalone_enum)
&& !has(TestAllTypes{standalone_enum: TestAllTypes.NestedEnum.FOO}.standalone_enum)
&& !has(TestAllTypes{single_nested_enum: TestAllTypes.NestedEnum.FOO}.single_nested_message)
&& has(TestAllTypes{single_nested_enum: TestAllTypes.NestedEnum.FOO}.single_nested_enum)
&& !has(TestAllTypes{}.single_nested_message)
&& has(TestAllTypes{single_nested_message: TestAllTypes.NestedMessage{}}.single_nested_message)
&& !has(TestAllTypes{}.standalone_enum)
&& !has(pb3.single_int64)
&& has(pb3.repeated_bool)
&& !has(pb3.repeated_int32)
&& has(pb3.map_int64_nested_type)
&& !has(pb3.map_string_string)`,
},
{
name: "macro_has_pb3_field_json",
typeOpts: []types.RegistryOption{types.JSONFieldNames(true), types.ProtoTypeDefs(&proto3pb.TestAllTypes{})},
vars: []*decls.VariableDecl{
decls.NewVariable("pb3", types.NewObjectType("google.expr.proto3.test.TestAllTypes")),
},
container: "google.expr.proto3.test",
in: map[string]any{
"pb3": &proto3pb.TestAllTypes{
RepeatedBool: []bool{false},
MapInt64NestedType: map[int64]*proto3pb.NestedTestAllTypes{
1: {},
},
MapStringString: map[string]string{},
},
},
expr: `has(TestAllTypes{standaloneEnum: TestAllTypes.NestedEnum.BAR}.standaloneEnum)
&& !has(TestAllTypes{standaloneEnum: TestAllTypes.NestedEnum.FOO}.standaloneEnum)
&& !has(TestAllTypes{singleNestedEnum: TestAllTypes.NestedEnum.FOO}.singleNestedMessage)
&& has(TestAllTypes{singleNestedEnum: TestAllTypes.NestedEnum.FOO}.singleNestedEnum)
&& !has(TestAllTypes{}.singleNestedMessage)
&& has(TestAllTypes{singleNestedMessage: TestAllTypes.NestedMessage{}}.singleNestedMessage)
&& !has(TestAllTypes{}.standaloneEnum)
&& !has(pb3.singleInt64)
&& has(pb3.repeatedBool)
&& !has(pb3.repeatedInt32)
&& has(pb3.mapInt64NestedType)
&& !has(pb3.mapStringString)`,
},
{
name: "macro_map",
expr: `[1, 2, 3].map(x, x * 2) == [2, 4, 6]`,
},
{
name: "matches_global",
expr: `matches(input, 'k.*')`,
vars: []*decls.VariableDecl{
decls.NewVariable("input", types.StringType),
},
in: map[string]any{
"input": "kathmandu",
},
},
{
name: "matches_member",
expr: `input.matches('k.*')
&& !'foo'.matches('k.*')
&& !'bar'.matches('k.*')
&& 'kilimanjaro'.matches('.*ro')`,
vars: []*decls.VariableDecl{
decls.NewVariable("input", types.StringType),
},
in: map[string]any{
"input": "kathmandu",
},
},
{
name: "matches_error",
expr: `input.matches(')k.*')`,
vars: []*decls.VariableDecl{
decls.NewVariable("input", types.StringType),
},
in: map[string]any{
"input": "kathmandu",
},
extraOpts: []PlannerOption{CompileRegexConstants(MatchesRegexOptimization)},
// unoptimized program should report a regex compile error at runtime
err: "unexpected ): `)k.*`",
// optimized program should report a regex compile at program creation time
progErr: "unexpected ): `)k.*`",
},
{
name: "nested_proto_field",
expr: `pb3.single_nested_message.bb`,
typeOpts: []types.RegistryOption{types.ProtoTypeDefs(&proto3pb.TestAllTypes{})},
vars: []*decls.VariableDecl{
decls.NewVariable("pb3",
types.NewObjectType("google.expr.proto3.test.TestAllTypes")),
},
in: map[string]any{
"pb3": &proto3pb.TestAllTypes{
NestedType: &proto3pb.TestAllTypes_SingleNestedMessage{
SingleNestedMessage: &proto3pb.TestAllTypes_NestedMessage{
Bb: 1234,
},
},
},
},
out: types.Int(1234),
},
{
name: "nested_proto_field_with_index",
expr: `pb3.map_int64_nested_type[0].child.payload.single_int32 == 1`,
typeOpts: []types.RegistryOption{types.ProtoTypeDefs(&proto3pb.TestAllTypes{})},
vars: []*decls.VariableDecl{
decls.NewVariable("pb3",
types.NewObjectType("google.expr.proto3.test.TestAllTypes")),
},
in: map[string]any{
"pb3": &proto3pb.TestAllTypes{
MapInt64NestedType: map[int64]*proto3pb.NestedTestAllTypes{
0: {
Child: &proto3pb.NestedTestAllTypes{
Payload: &proto3pb.TestAllTypes{
SingleInt32: 1,
},
},
},
},
},
},
},
{
name: "or_true_1st",
expr: `ai == 20 || ar["foo"] == "bar"`,
vars: []*decls.VariableDecl{
decls.NewVariable("ai", types.IntType),
decls.NewVariable("ar", types.NewMapType(types.StringType, types.StringType)),
},
in: map[string]any{
"ai": 20,
"ar": map[string]string{
"foo": "bar",
},
},
},
{
name: "or_true_2nd",
expr: `ai == 20 || ar["foo"] == "bar"`,
vars: []*decls.VariableDecl{
decls.NewVariable("ai", types.IntType),
decls.NewVariable("ar", types.NewMapType(types.StringType, types.StringType)),
},
in: map[string]any{
"ai": 2,
"ar": map[string]string{
"foo": "bar",
},
},
},
{
name: "or_false",
expr: `ai == 20 || ar["foo"] == "bar"`,
vars: []*decls.VariableDecl{
decls.NewVariable("ai", types.IntType),
decls.NewVariable("ar", types.NewMapType(types.StringType, types.StringType)),
},
in: map[string]any{
"ai": 2,
"ar": map[string]string{
"foo": "baz",
},
},
out: types.False,
},
{
name: "or_error_1st_error",
expr: `1/0 != 0 || false`,
err: "division by zero",
},
{
name: "or_error_2nd_error",
expr: `false || 1/0 != 0`,
err: "division by zero",
},
{
name: "or_error_1st_true",
expr: `1/0 != 0 || true`,
out: types.True,
},
{
name: "or_error_2nd_true",
expr: `true || 1/0 != 0`,
out: types.True,
},
{
name: "pkg_qualified_id",
expr: `b.c.d != 10`,
container: "a.b",
vars: []*decls.VariableDecl{
decls.NewVariable("a.b.c.d", types.IntType),
},
in: map[string]any{
"a.b.c.d": 9,
},
},
{
name: "pkg_qualified_id_unchecked",
expr: `c.d != 10`,
unchecked: true,
container: "a.b",
in: map[string]any{
"a.c.d": 9,
},
},
{
name: "pkg_qualified_index_unchecked",
expr: `b.c['d'] == 10`,
unchecked: true,
container: "a.b",
in: map[string]any{
"a.b.c": map[string]int{
"d": 10,
},
},
},
{
name: "type_dyn_equals_string",
expr: `type(dyn('')) == string`,
},
{
name: "type_override",
expr: `type == 'string'`,
vars: []*decls.VariableDecl{
decls.NewVariable("type", types.StringType),
},
in: map[string]any{
"type": "string",
},
},
{
name: "select_key",
expr: `m.strMap['val'] == 'string'
&& m.floatMap['val'] == 1.5
&& m.doubleMap['val'] == -2.0
&& m.intMap['val'] == -3
&& m.int32Map['val'] == 4
&& m.int64Map['val'] == -5
&& m.uintMap['val'] == 6u
&& m.uint32Map['val'] == 7u
&& m.uint64Map['val'] == 8u
&& m.boolMap['val'] == true
&& m.boolMap['val'] != false`,
vars: []*decls.VariableDecl{
decls.NewVariable("m", types.NewMapType(types.StringType, types.DynType)),
},
in: map[string]any{
"m": map[string]any{
"strMap": map[string]string{"val": "string"},
"floatMap": map[string]float32{"val": 1.5},
"doubleMap": map[string]float64{"val": -2.0},
"intMap": map[string]int{"val": -3},
"int32Map": map[string]int32{"val": 4},
"int64Map": map[string]int64{"val": -5},
"uintMap": map[string]uint{"val": 6},
"uint32Map": map[string]uint32{"val": 7},
"uint64Map": map[string]uint64{"val": 8},
"boolMap": map[string]bool{"val": true},
},
},
},
{
name: "select_bool_key",
expr: `m.boolStr[true] == 'string'
&& m.boolFloat32[true] == 1.5
&& m.boolFloat64[false] == -2.1
&& m.boolInt[false] == -3
&& m.boolInt32[false] == 0
&& m.boolInt64[true] == 4
&& m.boolUint[true] == 5u
&& m.boolUint32[true] == 6u
&& m.boolUint64[false] == 7u
&& m.boolBool[true]
&& m.boolIface[false] == true`,
vars: []*decls.VariableDecl{
decls.NewVariable("m", types.NewMapType(types.StringType, types.DynType)),
},
in: map[string]any{
"m": map[string]any{
"boolStr": map[bool]string{true: "string"},
"boolFloat32": map[bool]float32{true: 1.5},
"boolFloat64": map[bool]float64{false: -2.1},
"boolInt": map[bool]int{false: -3},
"boolInt32": map[bool]int32{false: 0},
"boolInt64": map[bool]int64{true: 4},
"boolUint": map[bool]uint{true: 5},
"boolUint32": map[bool]uint32{true: 6},
"boolUint64": map[bool]uint64{false: 7},
"boolBool": map[bool]bool{true: true},
"boolIface": map[bool]any{false: true},
},
},
},
{
name: "select_uint_key",
expr: `m.uintIface[1u] == 'string'
&& m.uint32Iface[2u] == 1.5
&& m.uint64Iface[3u] == -2.1
&& m.uint64String[4u] == 'three'`,
vars: []*decls.VariableDecl{
decls.NewVariable("m", types.NewMapType(types.StringType, types.DynType)),
},
in: map[string]any{
"m": map[string]any{
"uintIface": map[uint]any{1: "string"},
"uint32Iface": map[uint32]any{2: 1.5},
"uint64Iface": map[uint64]any{3: -2.1},
"uint64String": map[uint64]string{4: "three"},
},
},
},
{
name: "select_index",
expr: `m.strList[0] == 'string'
&& m.floatList[0] == 1.5
&& m.doubleList[0] == -2.0
&& m.intList[0] == -3
&& m.int32List[0] == 4
&& m.int64List[0] == -5
&& m.uintList[0] == 6u
&& m.uint32List[0] == 7u
&& m.uint64List[0] == 8u
&& m.boolList[0] == true
&& m.boolList[1] != true
&& m.ifaceList[0] == {}`,
vars: []*decls.VariableDecl{
decls.NewVariable("m", types.NewMapType(types.StringType, types.DynType)),
},
in: map[string]any{
"m": map[string]any{
"strList": []string{"string"},
"floatList": []float32{1.5},
"doubleList": []float64{-2.0},
"intList": []int{-3},
"int32List": []int32{4},
"int64List": []int64{-5},
"uintList": []uint{6},
"uint32List": []uint32{7},
"uint64List": []uint64{8},
"boolList": []bool{true, false},
"ifaceList": []any{map[string]string{}},
},
},
},
{
name: "select_field",
expr: `a.b.c
&& pb3.repeated_nested_enum[0] == test.TestAllTypes.NestedEnum.BAR
&& json.list[0] == 'world'`,
container: "google.expr.proto3",
typeOpts: []types.RegistryOption{types.ProtoTypeDefs(&proto3pb.TestAllTypes{})},
vars: []*decls.VariableDecl{
decls.NewVariable("a.b", types.NewMapType(types.StringType, types.BoolType)),
decls.NewVariable("pb3", types.NewObjectType("google.expr.proto3.test.TestAllTypes")),
decls.NewVariable("json", types.NewMapType(types.StringType, types.DynType)),
},
in: map[string]any{
"a.b": map[string]bool{
"c": true,
},
"pb3": &proto3pb.TestAllTypes{
RepeatedNestedEnum: []proto3pb.TestAllTypes_NestedEnum{proto3pb.TestAllTypes_BAR},
},
"json": &structpb.Value{
Kind: &structpb.Value_StructValue{
StructValue: &structpb.Struct{
Fields: map[string]*structpb.Value{
"list": {Kind: &structpb.Value_ListValue{
ListValue: &structpb.ListValue{
Values: []*structpb.Value{
structpb.NewStringValue("world"),
},
},
}},
},
},
},
},
},
},
// pb2 primitive fields may have default values set.
{
name: "select_pb2_primitive_fields",
expr: `!has(a.single_int32)
&& a.single_int32 == -32
&& a.single_int64 == -64
&& a.single_uint32 == 32u
&& a.single_uint64 == 64u
&& a.single_float == 3.0
&& a.single_double == 6.4
&& a.single_bool
&& "empty" == a.single_string`,
typeOpts: []types.RegistryOption{types.ProtoTypeDefs(&proto2pb.TestAllTypes{})},
in: map[string]any{
"a": &proto2pb.TestAllTypes{},
},
vars: []*decls.VariableDecl{
decls.NewVariable("a", types.NewObjectType("google.expr.proto2.test.TestAllTypes")),
},
},
// Wrapper type nil or value test.
{
name: "select_pb3_wrapper_fields",
expr: `!has(a.single_int32_wrapper) && a.single_int32_wrapper == null
&& has(a.single_int64_wrapper) && a.single_int64_wrapper == 0
&& has(a.single_string_wrapper) && a.single_string_wrapper == "hello"
&& a.single_int64_wrapper == Int32Value{value: 0}`,
typeOpts: []types.RegistryOption{types.ProtoTypeDefs(&proto3pb.TestAllTypes{})},
abbrevs: []string{"google.protobuf.Int32Value"},
vars: []*decls.VariableDecl{
decls.NewVariable("a", types.NewObjectType("google.expr.proto3.test.TestAllTypes")),
},
in: map[string]any{
"a": &proto3pb.TestAllTypes{
SingleInt64Wrapper: &wrapperspb.Int64Value{},
SingleStringWrapper: wrapperspb.String("hello"),
},
},
},
{
name: "select_pb3_compare",
expr: `a.single_uint64 > 3u`,
container: "google.expr.proto3.test",
typeOpts: []types.RegistryOption{types.ProtoTypeDefs(&proto3pb.TestAllTypes{})},
vars: []*decls.VariableDecl{
decls.NewVariable("a", types.NewObjectType("google.expr.proto3.test.TestAllTypes")),
},
in: map[string]any{
"a": &proto3pb.TestAllTypes{
SingleUint64: 10,
},
},
out: types.True,
},
{
name: "select_custom_pb3_compare",
expr: `a.bb > 100`,
container: "google.expr.proto3.test",
typeOpts: []types.RegistryOption{types.ProtoTypeDefs(&proto3pb.TestAllTypes_NestedMessage{})},
vars: []*decls.VariableDecl{
decls.NewVariable("a",
types.NewObjectType("google.expr.proto3.test.TestAllTypes.NestedMessage")),
},
attrs: &custAttrFactory{
AttributeFactory: NewAttributeFactory(
testContainer("google.expr.proto3.test"),
newTestRegistry(t, types.ProtoTypeDefs(&proto3pb.TestAllTypes_NestedMessage{})),
newTestRegistry(t, types.ProtoTypeDefs(&proto3pb.TestAllTypes_NestedMessage{})),
),
},
in: map[string]any{
"a": &proto3pb.TestAllTypes_NestedMessage{
Bb: 101,
},
},
out: types.True,
},
{
name: "select_custom_pb3_optional_field",
expr: `a.?bb`,
container: "google.expr.proto3.test",
typeOpts: []types.RegistryOption{types.ProtoTypeDefs(&proto3pb.TestAllTypes_NestedMessage{})},
vars: []*decls.VariableDecl{
decls.NewVariable("a",
types.NewObjectType("google.expr.proto3.test.TestAllTypes.NestedMessage")),
},
attrs: &custAttrFactory{
AttributeFactory: NewAttributeFactory(
testContainer("google.expr.proto3.test"),
newTestRegistry(t, types.ProtoTypeDefs(&proto3pb.TestAllTypes_NestedMessage{})),
newTestRegistry(t, types.ProtoTypeDefs(&proto3pb.TestAllTypes_NestedMessage{})),
),
},
in: map[string]any{
"a": &proto3pb.TestAllTypes_NestedMessage{
Bb: 101,
},
},
out: types.OptionalOf(types.Int(101)),
},
{
name: "select_relative",
expr: `json('{"hi":"world"}').hi == 'world'`,
funcs: []*decls.FunctionDecl{
funcDecl(t, "json",
decls.Overload("json_string", []*types.Type{types.StringType}, types.DynType,
decls.UnaryBinding(func(val ref.Val) ref.Val {
str, ok := val.(types.String)
if !ok {
return types.MaybeNoSuchOverloadErr(val)
}
m := make(map[string]any)
err := json.Unmarshal([]byte(str), &m)
if err != nil {
return types.NewErr("invalid json: %v", err)
}
return types.DefaultTypeAdapter.NativeToValue(m)
}),
),
),
},
},
{
name: "select_subsumed_field",
expr: `a.b.c`,
vars: []*decls.VariableDecl{
decls.NewVariable("a.b.c", types.IntType),
decls.NewVariable("a.b", types.NewMapType(types.StringType, types.StringType)),
},
in: map[string]any{
"a.b.c": 10,
"a.b": map[string]string{
"c": "ten",
},
},
out: types.Int(10),
},
{
name: "select_empty_repeated_nested",
expr: `TestAllTypes{}.repeated_nested_message.size() == 0`,
typeOpts: []types.RegistryOption{types.ProtoTypeDefs(&proto3pb.TestAllTypes{})},
container: "google.expr.proto3.test",
out: types.True,
},
{
name: "call_with_error_unary",
expr: `try(0/0)`,
unchecked: true,
funcs: []*decls.FunctionDecl{
funcDecl(t, "try",
decls.Overload("try_dyn", []*types.Type{types.DynType}, types.DynType,
decls.OverloadIsNonStrict(),
decls.UnaryBinding(func(arg ref.Val) ref.Val {
if types.IsError(arg) {
return types.String(fmt.Sprintf("error: %s", arg))
}
return arg
}),
),
),
},
out: types.String("error: division by zero"),
},
{
name: "call_with_error_binary",
expr: `try(0/0, 0)`,
unchecked: true,
funcs: []*decls.FunctionDecl{
funcDecl(t, "try",
decls.Overload("try_dyn",
[]*types.Type{types.DynType, types.DynType},
types.NewListType(types.DynType),
decls.OverloadIsNonStrict(),
decls.BinaryBinding(func(arg0, arg1 ref.Val) ref.Val {
if types.IsError(arg0) {
return types.String(fmt.Sprintf("error: %s", arg0))
}
return types.NewDynamicList(types.DefaultTypeAdapter, []ref.Val{arg0, arg1})
}),
),
),
},
out: types.String("error: division by zero"),
},
{
name: "call_with_error_function",
expr: `try(0/0, 0, 0)`,
unchecked: true,
funcs: []*decls.FunctionDecl{
funcDecl(t, "try",
decls.Overload("try_dyn",
[]*types.Type{types.DynType, types.DynType, types.DynType},
types.NewListType(types.DynType),
decls.OverloadIsNonStrict(),
decls.FunctionBinding(func(args ...ref.Val) ref.Val {
if types.IsError(args[0]) {
return types.String(fmt.Sprintf("error: %s", args[0]))
}
return types.NewDynamicList(types.DefaultTypeAdapter, args)
}),
),
),
},
out: types.String("error: division by zero"),
},
{
name: "literal_map_optional_field",
expr: `{?'hi': {}.?missing,
?'world': {'present': 42u}.?present}`,
out: map[string]any{
"world": uint(42),
},
},
{
name: "literal_map_optional_field_bad_init",
expr: `{?'hi': 'world'}`,
unchecked: true,
err: `cannot initialize optional entry 'hi' from non-optional`,
},
{
name: "literal_pb_optional_field",
expr: `TestAllTypes{?single_int32: {'value': 1}.?value, ?single_string: {}.?missing}`,
container: "google.expr.proto3.test",
typeOpts: []types.RegistryOption{types.ProtoTypeDefs(&proto3pb.TestAllTypes{})},
out: &proto3pb.TestAllTypes{
SingleInt32: 1,
},
},
{
name: "literal_pb_optional_field_bad_init",
expr: `TestAllTypes{?single_int32: 1}`,
container: "google.expr.proto3.test",
typeOpts: []types.RegistryOption{types.ProtoTypeDefs(&proto3pb.TestAllTypes{})},
unchecked: true,
err: `cannot initialize optional entry 'single_int32' from non-optional`,
},
{
name: "literal_list_optional_element",
expr: `[?{}.?missing, ?{'present': 42u}.?present]`,
out: []uint64{42},
},
{
name: "literal_list_optional_bad_element",
expr: `[?123]`,
unchecked: true,
err: `cannot initialize optional list element from non-optional value 123`,
},
{
name: "bad_argument_in_optimized_list",
expr: `1/0 in [1, 2, 3]`,
err: `division by zero`,
},
{
name: "list_index_error",
expr: `mylistundef[0]`,
unchecked: true,
err: `no such attribute(s): mylistundef`,
},
{
name: "pkg_list_index_error",
container: "goog",
expr: `pkg.mylistundef[0]`,
unchecked: true,
err: `no such attribute(s): goog.pkg.mylistundef, pkg.mylistundef`,
},
{
name: "unknown_attribute",
expr: `a[0]`,
vars: []*decls.VariableDecl{
decls.NewVariable("a",
types.NewMapType(types.IntType, types.BoolType)),
},
attrs: NewPartialAttributeFactory(testContainer(""), types.DefaultTypeAdapter, types.NewEmptyRegistry()),
in: newTestPartialActivation(t, map[string]any{
"a": map[int64]any{
1: true,
},
}, NewAttributePattern("a").QualInt(0)),
out: types.NewUnknown(2, types.QualifyAttribute[int64](types.NewAttributeTrail("a"), 0)),
},
{
name: "macro_has_map_key_unknown_propagates",
expr: `has(a.b)`,
vars: []*decls.VariableDecl{
decls.NewVariable("a", types.NewMapType(types.StringType, types.BoolType)),
},
attrs: NewPartialAttributeFactory(testContainer(""), types.DefaultTypeAdapter, types.NewEmptyRegistry()),
in: newTestPartialActivation(t, map[string]any{}, NewAttributePattern("a")),
out: types.NewUnknown(4, types.NewAttributeTrail("a")),
},
{
name: "unknown_attribute_mixed_qualifier",
expr: `a[dyn(0u)]`,
vars: []*decls.VariableDecl{
decls.NewVariable("a",
types.NewMapType(types.IntType, types.BoolType)),
},
attrs: NewPartialAttributeFactory(testContainer(""), types.DefaultTypeAdapter, types.NewEmptyRegistry()),
in: newTestPartialActivation(t, map[string]any{
"a": map[int64]any{
1: true,
},
}, NewAttributePattern("a").QualInt(0)),
out: types.NewUnknown(2, types.QualifyAttribute[uint64](types.NewAttributeTrail("a"), 0)),
},
{
name: "invalid_presence_test_on_int_literal",
expr: `has(dyn(1).invalid)`,
err: "no such key: invalid",
attrs: NewAttributeFactory(testContainer(""), types.DefaultTypeAdapter, types.NewEmptyRegistry(),
EnableErrorOnBadPresenceTest(true)),
},
{
name: "invalid_presence_test_on_list_literal",
expr: `has(dyn([]).invalid)`,
err: "unsupported index type 'string' in list",
attrs: NewAttributeFactory(testContainer(""), types.DefaultTypeAdapter, types.NewEmptyRegistry(),
EnableErrorOnBadPresenceTest(true)),
},
{
name: "optional_select_on_undefined",
expr: `{}.?invalid`,
out: types.OptionalNone,
},
{
name: "optional_select_on_null_literal",
expr: `{"invalid": dyn(null)}.?invalid.?nested`,
out: types.OptionalNone,
},
{
name: "local_shadow_identifier_in_select",
expr: `[{'z': 0}].exists(y, y.z == 0)`,
container: "cel.example",
vars: []*decls.VariableDecl{
decls.NewVariable("cel.example.y", types.IntType),
},
in: map[string]any{
"cel.example.y": map[string]int{"z": 1},
},
out: types.True,
},
{
name: "local_shadow_identifier_in_select_global_disambiguation",
expr: `[{'z': 0}].exists(y, y.z == 0 && .y.z == 1)`,
container: "y",
vars: []*decls.VariableDecl{
decls.NewVariable("y.z", types.IntType),
},
in: map[string]any{
"y.z": 1,
},
out: types.True,
},
{
name: "local_shadow_identifier_with_global_disambiguation",
expr: `[0].exists(x, x == 0 && .x == 1)`,
vars: []*decls.VariableDecl{
decls.NewVariable("x", types.IntType),
},
in: map[string]any{
"x": 1,
},
out: types.True,
},
{
name: "local_double_shadow_identifier_with_global_disambiguation",
expr: `[0].exists(x, [x+1].exists(x, x == .x))`,
vars: []*decls.VariableDecl{
decls.NewVariable("x", types.IntType),
},
in: map[string]any{
"x": 1,
},
out: types.True,
},
{
name: "unchecked_local_shadow_identifier_in_select",
expr: `[{'z': 0}].exists(y, y.z == 0)`,
unchecked: true,
container: "cel.example",
vars: []*decls.VariableDecl{
decls.NewVariable("cel.example.y", types.IntType),
},
in: map[string]any{
"cel.example.y": map[string]int{"z": 1},
},
out: types.True,
},
{
name: "unchecked_local_shadow_identifier_in_select_global_disambiguation",
expr: `[{'z': 0}].exists(y, y.z == 0 && .y.z == 1)`,
container: "y",
unchecked: true,
vars: []*decls.VariableDecl{
decls.NewVariable("y.z", types.IntType),
},
in: map[string]any{
"y.z": 1,
},
out: types.True,
},
{
name: "unchecked_local_shadow_identifier_with_global_disambiguation",
expr: `[0].exists(x, x == 0 && .x == 1)`,
unchecked: true,
vars: []*decls.VariableDecl{
decls.NewVariable("x", types.IntType),
},
in: map[string]any{
"x": 1,
},
out: types.True,
},
{
name: "unchecked_local_double_shadow_identifier_with_global_disambiguation",
expr: `[0].exists(x, [x+1].exists(x, x == .x))`,
unchecked: true,
vars: []*decls.VariableDecl{
decls.NewVariable("x", types.IntType),
},
in: map[string]any{
"x": 1,
},
out: types.True,
},
}
}
func BenchmarkInterpreter(b *testing.B) {
for _, tst := range testData(b) {
if tst.err != "" || tst.progErr != "" {
continue
}
prg, vars, err := program(b, &tst, Optimize(), CompileRegexConstants(MatchesRegexOptimization))
if err != nil {
b.Fatal(err)
}
// Benchmark the eval.
b.Run(tst.name, func(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
prg.Eval(vars)
}
})
}
}
func BenchmarkInterpreterParallel(b *testing.B) {
for _, tst := range testData(b) {
prg, vars, err := program(b, &tst, Optimize(), CompileRegexConstants(MatchesRegexOptimization))
if tst.err != "" || tst.progErr != "" {
continue
}
if err != nil {
b.Fatal(err)
}
b.ResetTimer()
b.Run(tst.name,
func(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
prg.Eval(vars)
}
})
})
}
}
func TestInterpreter(t *testing.T) {
for _, tst := range testData(t) {
tc := tst
prg, vars, err := program(t, &tc)
if err != nil {
t.Fatalf("%s: %v", tc.name, err)
}
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var want ref.Val = types.True
if tc.out != nil {
want = tc.out.(ref.Val)
}
got := prg.Eval(vars)
_, expectUnk := want.(*types.Unknown)
if expectUnk {
if !reflect.DeepEqual(got, want) {
t.Fatalf("Got %v, wanted %v", got, want)
}
} else if tc.err != "" {
if !types.IsError(got) || !strings.Contains(got.(*types.Err).String(), tc.err) {
t.Fatalf("Got %v (%T), wanted error: %s", got, got, tc.err)
}
} else if got.Equal(want) != types.True {
t.Fatalf("Got %v, wanted %v", got, want)
}
state := NewEvalState()
opts := map[string][]PlannerOption{
"optimize": {Optimize()},
"exhaustive": {ExhaustiveEval(),
EvalStateObserver(EvalStateFactory(func() EvalState { return state }))},
"track": {EvalStateObserver(EvalStateFactory(func() EvalState { return state }))},
}
for mode, opt := range opts {
opts := opt
if tc.extraOpts != nil {
opts = append(opts, tc.extraOpts...)
}
prg, vars, err = program(t, &tc, opts...)
if tc.progErr != "" {
if !types.IsError(got) || !strings.Contains(got.(*types.Err).String(), tc.progErr) {
t.Errorf("Got %v (%T), wanted error: %s", got, got, tc.progErr)
}
continue
}
if err != nil {
t.Fatal(err)
}
t.Run(mode, func(t *testing.T) {
got := prg.Eval(vars)
_, expectUnk := want.(*types.Unknown)
if expectUnk {
if !reflect.DeepEqual(got, want) {
t.Errorf("Got %v, wanted %v", got, want)
}
} else if tc.err != "" {
if !types.IsError(got) || !strings.Contains(got.(*types.Err).String(), tc.err) {
t.Errorf("Got %v (%T), wanted error: %s", got, got, tc.err)
}
type nodeIDer interface {
NodeID() int64
}
nodeErr, ok := got.(nodeIDer)
if !ok || nodeErr.NodeID() == 0 {
t.Errorf("Did not get AST node ID from error: %#v", got)
}
} else if got.Equal(want) != types.True {
t.Errorf("Got %v, wanted %v", got, want)
}
state.Reset()
})
}
})
}
}
func TestInterpreter_ProtoAttributeOpt(t *testing.T) {
inst, _, err := program(t, &testCase{
name: "nested_proto_field_with_index",
expr: `pb3.map_int64_nested_type[0].child.payload.single_int32`,
typeOpts: []types.RegistryOption{types.ProtoTypeDefs(&proto3pb.TestAllTypes{})},
vars: []*decls.VariableDecl{
decls.NewVariable("pb3",
types.NewObjectType("google.expr.proto3.test.TestAllTypes")),
},
in: map[string]any{
"pb3": &proto3pb.TestAllTypes{
MapInt64NestedType: map[int64]*proto3pb.NestedTestAllTypes{
0: {
Child: &proto3pb.NestedTestAllTypes{
Payload: &proto3pb.TestAllTypes{
SingleInt32: 1,
},
},
},
},
},
},
}, Optimize())
if err != nil {
t.Fatal(err)
}
attr, ok := inst.(InterpretableAttribute)
if !ok {
t.Fatalf("got %v, expected attribute value", inst)
}
absAttr, ok := attr.Attr().(NamespacedAttribute)
if !ok {
t.Fatalf("got %v, expected global attribute", attr.Attr())
}
quals := absAttr.Qualifiers()
if len(quals) != 5 {
t.Errorf("got %d qualifiers, wanted 5", len(quals))
}
if !isFieldQual(quals[0], "map_int64_nested_type") ||
!isConstQual(quals[1], types.IntZero) ||
!isFieldQual(quals[2], "child") ||
!isFieldQual(quals[3], "payload") ||
!isFieldQual(quals[4], "single_int32") {
t.Error("unoptimized qualifier types present in optimized attribute")
}
}
func TestInterpreter_LogicalAndMissingType(t *testing.T) {
parsed := testMustParse(t, `a && TestProto{c: true}.c`)
reg := newTestRegistry(t)
cont := containers.DefaultContainer
attrs := NewAttributeFactory(cont, reg, reg)
intr := newStandardInterpreter(t, cont, reg, reg, attrs)
i, err := intr.NewInterpretable(parsed)
if err == nil {
t.Errorf("Got '%v', wanted error", i)
}
}
func TestInterpreter_ExhaustiveConditionalExpr(t *testing.T) {
parsed := testMustParse(t, `a ? b < 1.0 : c == ['hello']`)
state := NewEvalState()
cont := containers.DefaultContainer
reg := newTestRegistry(t, types.ProtoTypeDefs(&exprpb.ParsedExpr{}))
attrs := NewAttributeFactory(cont, reg, reg)
intr := newStandardInterpreter(t, cont, reg, reg, attrs)
interpretable, _ := intr.NewInterpretable(parsed, ExhaustiveEval(),
EvalStateObserver(EvalStateFactory(func() EvalState { return state })))
vars, _ := NewActivation(map[string]any{
"a": types.True,
"b": types.Double(0.999),
"c": types.NewStringList(reg, []string{"hello"})})
result := interpretable.Eval(vars)
// Operator "_==_" is at Expr 7, should be evaluated in exhaustive mode
// even though "a" is true
ev, _ := state.Value(7)
// "==" should be evaluated in exhaustive mode though unnecessary
if ev != types.True {
t.Errorf("Else expression expected to be true, got: %v", ev)
}
if result != types.True {
t.Errorf("Expected true, got: %v", result)
}
}
func TestInterpreter_WrappedActivationEvalState(t *testing.T) {
vars, _ := NewActivation(map[string]any{
"a": types.True,
"b": types.True,
"c": types.False,
"d": types.False,
})
state := NewEvalState()
esa := &evalStateActivation{vars: vars, state: state}
wrappedVars := &testActivationWrapper{esa, "test_activation_wrapper"}
ac, _ := NewActivation(wrappedVars)
es, found := asEvalState(ac)
if !found {
t.Errorf("asEvalState(%v) failed to find EvalState", ac)
}
if es != state {
t.Errorf("asEvalState(%v) returned %v, wanted %v", ac, es, state)
}
}
func TestInterpreter_InterruptableEval(t *testing.T) {
items := make([]int64, 5000)
for i := int64(0); i < 5000; i++ {
items[i] = i
}
tc := testCase{
expr: `items.map(i, i).map(i, i).size() != 0`,
vars: []*decls.VariableDecl{
decls.NewVariable("items", types.NewListType(types.IntType)),
},
in: map[string]any{
"items": items,
},
out: true,
}
prg, vars, err := program(t, &tc, InterruptableEval())
if err != nil {
t.Fatalf("program(%s) failed: %v", tc.expr, err)
}
ctx := context.TODO()
evalCtx, cancel := context.WithTimeout(ctx, 10*time.Microsecond)
defer cancel()
ctxVars := &contextActivation{
Activation: vars,
interrupt: func() bool {
select {
case <-evalCtx.Done():
return true
default:
return false
}
},
}
out := prg.Eval(ctxVars)
if !types.IsError(out) || out.(*types.Err).String() != "operation interrupted" {
t.Errorf("Got %v, wanted operation interrupted error", out)
}
}
type contextActivation struct {
Activation
interruptCount int
interrupt func() bool
}
func (ca *contextActivation) ResolveName(name string) (any, bool) {
if name == "#interrupted" {
ca.interruptCount++
return ca.interruptCount%100 == 0 && ca.interrupt(), true
}
return ca.Activation.ResolveName(name)
}
func TestInterpreter_ExhaustiveLogicalOrEquals(t *testing.T) {
// a || b == "b"
// Operator "==" is at Expr 4, should be evaluated though "a" is true
parsed := testMustParse(t, `a || b == "b"`)
state := NewEvalState()
reg := newTestRegistry(t, types.ProtoTypeDefs(&exprpb.Expr{}))
cont := testContainer("test")
attrs := NewAttributeFactory(cont, reg, reg)
interp := newStandardInterpreter(t, cont, reg, reg, attrs)
i, _ := interp.NewInterpretable(parsed, ExhaustiveEval(),
EvalStateObserver(EvalStateFactory(func() EvalState { return state })))
vars, _ := NewActivation(map[string]any{
"a": true,
"b": "b",
})
result := i.Eval(vars)
rhv, _ := state.Value(3)
// "==" should be evaluated in exhaustive mode though unnecessary
if rhv != types.True {
t.Errorf("Right hand side expression expected to be true, got: %v", rhv)
}
if result != types.True {
t.Errorf("Expected true, got: %v", result)
}
}
func TestInterpreter_SetProto2PrimitiveFields(t *testing.T) {
// Test the use of proto2 primitives within object construction.
src := common.NewTextSource(
`input == TestAllTypes{
single_int32: 1,
single_int64: 2,
single_uint32: 3u,
single_uint64: 4u,
single_float: -3.3,
single_double: -2.2,
single_string: "hello world",
single_bool: true
}`)
parsed := testMustParse(t, src)
cont := testContainer("google.expr.proto2.test")
reg := newTestRegistry(t, types.ProtoTypeDefs(&proto2pb.TestAllTypes{}))
env := newTestEnv(t, cont, reg)
env.AddIdents(
decls.NewVariable("input",
types.NewObjectType("google.expr.proto2.test.TestAllTypes")))
checked, errors := checker.Check(parsed, src, env)
if len(errors.GetErrors()) != 0 {
t.Error(errors.ToDisplayString())
}
attrs := NewAttributeFactory(cont, reg, reg)
i := newStandardInterpreter(t, cont, reg, reg, attrs)
eval, _ := i.NewInterpretable(checked)
one := int32(1)
two := int64(2)
three := uint32(3)
four := uint64(4)
five := float32(-3.3)
six := float64(-2.2)
str := "hello world"
truth := true
input := &proto2pb.TestAllTypes{
SingleInt32: &one,
SingleInt64: &two,
SingleUint32: &three,
SingleUint64: &four,
SingleFloat: &five,
SingleDouble: &six,
SingleString: &str,
SingleBool: &truth,
}
vars, _ := NewActivation(map[string]any{
"input": reg.NativeToValue(input),
})
result := eval.Eval(vars)
got, ok := result.Value().(bool)
if !ok {
t.Fatalf("Got '%v', wanted 'true'.", result)
}
expected := true
if !reflect.DeepEqual(got, expected) {
t.Errorf("Could not build object properly. Got '%v', wanted '%v'",
result.Value(),
expected)
}
}
func TestInterpreter_MissingIdentInSelect(t *testing.T) {
src := common.NewTextSource(`a.b.c`)
parsed := testMustParse(t, src)
cont := testContainer("test")
reg := newTestRegistry(t)
env := newTestEnv(t, cont, reg)
env.AddIdents(decls.NewVariable("a.b", types.DynType))
checked, errors := checker.Check(parsed, src, env)
if len(errors.GetErrors()) != 0 {
t.Fatal(errors.ToDisplayString())
}
attrs := NewPartialAttributeFactory(cont, reg, reg)
interp := newStandardInterpreter(t, cont, reg, reg, attrs)
i, _ := interp.NewInterpretable(checked)
vars, _ := NewPartialActivation(
map[string]any{
"a.b": map[string]any{
"d": "hello",
},
},
NewAttributePattern("a.b").QualString("c"))
result := i.Eval(vars)
if !types.IsUnknown(result) {
t.Errorf("Got %v, wanted unknown", result)
}
result = i.Eval(EmptyActivation())
if !types.IsError(result) {
t.Errorf("Got %v, wanted error", result)
}
}
func TestInterpreter_TypeConversionOpt(t *testing.T) {
tests := []struct {
in string
out ref.Val
err bool
}{
{in: `bool('tru')`, err: true},
{in: `bool("true")`, out: types.True},
{in: `bytes("hello")`, out: types.Bytes("hello")},
{in: `double("_123")`, err: true},
{in: `double("123.0")`, out: types.Double(123.0)},
{in: `duration('12hh3')`, err: true},
{in: `duration('12s')`, out: types.Duration{Duration: time.Duration(12) * time.Second}},
{in: `dyn(1u)`, out: types.Uint(1)},
{in: `int('11l')`, err: true},
{in: `int('11')`, out: types.Int(11)},
{in: `string('11')`, out: types.String("11")},
{in: `timestamp('123')`, err: true},
{in: `timestamp(123)`, out: types.Timestamp{Time: time.Unix(123, 0).UTC()}},
{in: `type(null)`, out: types.NullType},
{in: `type(timestamp(int('123')))`, out: types.TimestampType},
{in: `uint(-1)`, err: true},
{in: `uint(1)`, out: types.Uint(1)},
}
for _, tc := range tests {
src := common.NewTextSource(tc.in)
parsed := testMustParse(t, src)
cont := containers.DefaultContainer
reg := newTestRegistry(t)
env := newTestEnv(t, cont, reg)
checked, errors := checker.Check(parsed, src, env)
if len(errors.GetErrors()) != 0 {
t.Fatal(errors.ToDisplayString())
}
attrs := NewAttributeFactory(cont, reg, reg)
interp := newStandardInterpreter(t, cont, reg, reg, attrs)
// Show that program planning will now produce an error.
i, err := interp.NewInterpretable(checked, Optimize())
if tc.err && err == nil {
t.Fatalf("got %v, expected error", i)
}
if tc.out != nil {
if err != nil {
t.Fatal(err)
}
ic, isConst := i.(InterpretableConst)
if !isConst {
t.Fatalf("got %v, expected constant", ic)
}
if tc.out.Equal(ic.Value()) != types.True {
t.Errorf("got %v, wanted %v", ic.Value(), tc.out)
}
}
// Show how the error returned during program planning is the same as the runtime
// error which would be produced normally.
if tc.err {
i2, err2 := interp.NewInterpretable(checked)
if err2 != nil {
t.Fatalf("got error, wanted interpretable: %v", i2)
}
errVal := i2.Eval(EmptyActivation())
errValStr := errVal.(*types.Err).Error()
if errValStr != err.Error() {
t.Errorf("got error %s, wanted error %s", errValStr, err.Error())
}
}
}
}
func TestInterpreter_PlanOptionalElements(t *testing.T) {
fac := ast.NewExprFactory()
// [?a] manipulated so the optional index is negative.
badOptionalA := fac.NewList(1, []ast.Expr{fac.NewIdent(2, "a")}, []int32{-1})
// [?b] manipulated so the optional index is out of range.
badOptionalB := fac.NewList(1, []ast.Expr{fac.NewIdent(2, "b")}, []int32{24})
cont := containers.DefaultContainer
reg := newTestRegistry(t)
attrs := NewAttributeFactory(cont, reg, reg)
interp := newStandardInterpreter(t, cont, reg, reg, attrs)
_, err := interp.NewInterpretable(ast.NewAST(badOptionalA, nil), Optimize())
if err == nil {
t.Fatal("interp.NewInterpretable() should have failed with negative optional index: -1")
}
_, err = interp.NewInterpretable(ast.NewAST(badOptionalB, nil), Optimize())
if err == nil {
t.Fatal("interp.NewInterpretable() should have failed with out of range optional index: 24")
}
}
func TestInterpreter_PlanListComprehensionTwoVar(t *testing.T) {
fac := ast.NewExprFactory()
listTwoArgTuples := fac.NewComprehensionTwoVar(1,
fac.NewList(2, []ast.Expr{
fac.NewLiteral(3, types.Int(2)),
fac.NewLiteral(4, types.Int(3)),
}, []int32{}),
"i",
"v",
fac.AccuIdentName(),
fac.NewList(5, []ast.Expr{}, []int32{}),
fac.NewLiteral(6, types.True),
fac.NewCall(7, operators.Add, fac.NewAccuIdent(8),
fac.NewList(9, []ast.Expr{fac.NewIdent(10, "i"), fac.NewIdent(11, "v")}, []int32{})),
fac.NewAccuIdent(12),
)
cont := containers.DefaultContainer
reg := newTestRegistry(t)
attrs := NewAttributeFactory(cont, reg, reg)
interp := newStandardInterpreter(t, cont, reg, reg, attrs)
expr, err := interp.NewInterpretable(ast.NewAST(listTwoArgTuples, nil), Optimize())
if err != nil {
t.Fatalf("interp.NewInterpretable() failed for two-variable comprehension: %v", err)
}
result := expr.Eval(EmptyActivation())
if types.IsError(result) {
t.Fatalf("expr.Eval() yielded error: %v", result)
}
want := []int64{0, 2, 1, 3}
out, err := result.ConvertToNative(reflect.TypeOf(want))
if err != nil {
t.Fatalf("result.ConvertToNative() failed: %v", err)
}
if !reflect.DeepEqual(out, want) {
t.Errorf("got %v, wanted %v", out, want)
}
}
func TestInterpreter_PlanMapComprehensionTwoVar(t *testing.T) {
fac := ast.NewExprFactory()
listTwoArgTuples := fac.NewComprehensionTwoVar(1,
fac.NewMap(2, []ast.EntryExpr{
fac.NewMapEntry(3, fac.NewLiteral(4, types.Int(0)), fac.NewLiteral(5, types.String("first")), false),
fac.NewMapEntry(6, fac.NewLiteral(7, types.Int(1)), fac.NewLiteral(8, types.String("second")), false),
}),
"k",
"v",
fac.AccuIdentName(),
fac.NewMap(9, []ast.EntryExpr{}),
fac.NewLiteral(10, types.True),
fac.NewCall(11, "cel.@mapInsert",
fac.NewAccuIdent(12),
fac.NewCall(13, operators.Add, fac.NewIdent(14, "k"), fac.NewLiteral(15, types.IntOne)),
fac.NewIdent(16, "v"),
),
fac.NewAccuIdent(17),
)
cont := containers.DefaultContainer
reg := newTestRegistry(t)
attrs := NewAttributeFactory(cont, reg, reg)
interp := newStandardInterpreter(t, cont, reg, reg, attrs,
funcDecl(t, "cel.@mapInsert",
decls.Overload("cel.@mapInsert",
[]*types.Type{
types.NewMapType(types.IntType, types.StringType),
types.IntType,
types.StringType,
}, types.NewMapType(types.IntType, types.StringType)),
decls.SingletonFunctionBinding(func(args ...ref.Val) ref.Val {
m := args[0].(traits.Mapper)
k := args[1]
v := args[2]
return types.InsertMapKeyValue(m, k, v)
}),
))
expr, err := interp.NewInterpretable(ast.NewAST(listTwoArgTuples, nil), Optimize())
if err != nil {
t.Fatalf("interp.NewInterpretable() failed for two-variable comprehension: %v", err)
}
result := expr.Eval(EmptyActivation())
if types.IsError(result) {
t.Fatalf("expr.Eval() yielded error: %v", result)
}
want := map[int64]string{1: "first", 2: "second"}
out, err := result.ConvertToNative(reflect.TypeOf(want))
if err != nil {
t.Fatalf("result.ConvertToNative() failed: %v", err)
}
if !reflect.DeepEqual(out, want) {
t.Errorf("got %v, wanted %v", out, want)
}
}
func testContainer(name string) *containers.Container {
cont, _ := containers.NewContainer(containers.Name(name))
return cont
}
func program(t testing.TB, tst *testCase, opts ...PlannerOption) (Interpretable, Activation, error) {
// Configure the package.
cont := containers.DefaultContainer
if tst.container != "" {
cont = testContainer(tst.container)
}
var err error
if tst.abbrevs != nil {
cont, err = containers.NewContainer(
containers.Name(cont.Name()),
containers.Abbrevs(tst.abbrevs...))
if err != nil {
return nil, nil, err
}
}
var reg *types.Registry
var env *checker.Env
reg = newTestRegistry(t)
if tst.typeOpts != nil {
reg = newTestRegistry(t, tst.typeOpts...)
}
env = newTestEnv(t, cont, reg)
attrs := NewAttributeFactory(cont, reg, reg)
if tst.attrs != nil {
attrs = tst.attrs
}
if tst.vars != nil {
err = env.AddIdents(tst.vars...)
if err != nil {
return nil, nil, fmt.Errorf("env.Add(%v) failed: %v", tst.vars, err)
}
}
// Configure the program input.
vars := EmptyActivation()
if tst.in != nil {
vars, err = NewActivation(tst.in)
if err != nil {
t.Fatalf("NewActivation(%v) failed: %v", tst.in, err)
}
}
// Adapt the test output, if needed.
if tst.out != nil {
tst.out = reg.NativeToValue(tst.out)
}
disp := NewDispatcher()
addFunctionBindings(t, disp)
if tst.funcs != nil {
err = env.AddFunctions(tst.funcs...)
if err != nil {
return nil, nil, fmt.Errorf("env.Add(%v) failed: %v", tst.funcs, err)
}
disp.Add(funcBindings(t, tst.funcs...)...)
}
interp := NewInterpreter(disp, cont, reg, reg, attrs)
// Parse the expression.
s := common.NewTextSource(tst.expr)
p, err := parser.NewParser(
parser.Macros(parser.AllMacros...),
parser.EnableOptionalSyntax(true),
parser.EnableVariadicOperatorASTs(true),
)
if err != nil {
return nil, nil, err
}
parsed, errs := p.Parse(s)
if len(errs.GetErrors()) != 0 {
return nil, nil, errors.New(errs.ToDisplayString())
}
if tst.unchecked {
// Build the program plan.
prg, err := interp.NewInterpretable(parsed, opts...)
if err != nil {
return nil, nil, err
}
return prg, vars, nil
}
// Check the expression.
checked, errs := checker.Check(parsed, s, env)
if len(errs.GetErrors()) != 0 {
return nil, nil, errors.New(errs.ToDisplayString())
}
// Build the program plan.
prg, err := interp.NewInterpretable(checked, opts...)
if err != nil {
return nil, nil, err
}
return prg, vars, nil
}
func base64Encode(val ref.Val) ref.Val {
str, ok := val.(types.String)
if !ok {
return types.MaybeNoSuchOverloadErr(val)
}
return types.String(base64.StdEncoding.EncodeToString([]byte(str)))
}
func isConstQual(q Qualifier, val ref.Val) bool {
c, ok := q.(ConstantQualifier)
if !ok {
return false
}
return c.Value().Equal(val) == types.True
}
func isFieldQual(q Qualifier, fieldName string) bool {
f, ok := q.(*fieldQualifier)
if !ok {
return false
}
return f.Name == fieldName
}
func testMustParse(t testing.TB, data any) *ast.AST {
t.Helper()
p, err := parser.NewParser()
if err != nil {
t.Fatalf("parser.NewParser() failed: %v", err)
}
var src common.Source
switch d := data.(type) {
case string:
src = common.NewTextSource(d)
case common.Source:
src = d
default:
t.Fatalf("testMustParse() got invalid parse data: %v", data)
}
parsed, errors := p.Parse(src)
if len(errors.GetErrors()) != 0 {
t.Fatalf("Parse(%q) failed: %v", src.Content(), errors.ToDisplayString())
}
return parsed
}
func newTestEnv(t testing.TB, cont *containers.Container, reg *types.Registry) *checker.Env {
t.Helper()
env, err := checker.NewEnv(cont, reg, checker.CrossTypeNumericComparisons(true))
if err != nil {
t.Fatalf("checker.NewEnv(%v, %v) failed: %v", cont, reg, err)
}
err = env.AddFunctions(stdlib.Functions()...)
if err != nil {
t.Fatalf("env.Add(stdlib.Functions()...) failed: %v", err)
}
return env
}
func newTestRegistry(t testing.TB, opts ...types.RegistryOption) *types.Registry {
t.Helper()
reg, err := types.NewProtoRegistry(opts...)
if err != nil {
t.Fatalf("types.NewProtoRegistry() failed: %v", err)
}
return reg
}
func newTestPartialActivation(t testing.TB, in any, unknowns ...*AttributePattern) any {
t.Helper()
vars, err := NewPartialActivation(in, unknowns...)
if err != nil {
t.Fatalf("NewPartialActivation(%v) failed: %v", in, err)
}
return vars
}
// newStandardInterpreter builds a Dispatcher and TypeProvider with support for all of the CEL
// builtins defined in the language definition.
func newStandardInterpreter(t *testing.T,
container *containers.Container,
provider types.Provider,
adapter types.Adapter,
resolver AttributeFactory,
optFuncs ...*decls.FunctionDecl) Interpreter {
t.Helper()
disp := NewDispatcher()
addFunctionBindings(t, disp)
for _, fn := range optFuncs {
bindings, err := fn.Bindings()
if err != nil {
t.Fatalf("fn.Bindings() failed for function %v. error: %v", fn.Name(), err)
}
err = disp.Add(bindings...)
if err != nil {
t.Fatalf("dispatcher.Add() failed: %v", err)
}
}
return NewInterpreter(disp, container, provider, adapter, resolver)
}
func addFunctionBindings(t testing.TB, dispatcher Dispatcher) {
funcs := stdlib.Functions()
for _, fn := range funcs {
bindings, err := fn.Bindings()
if err != nil {
t.Fatalf("fn.Bindings() failed for function %v. error: %v", fn.Name(), err)
}
err = dispatcher.Add(bindings...)
if err != nil {
t.Fatalf("dispatcher.Add() failed: %v", err)
}
}
}
func funcDecl(t testing.TB, name string, opts ...decls.FunctionOpt) *decls.FunctionDecl {
t.Helper()
fn, err := decls.NewFunction(name, opts...)
if err != nil {
t.Fatalf("NewFunction(%v) failed: %v", name, err)
}
return fn
}
func funcBindings(t testing.TB, funcs ...*decls.FunctionDecl) []*functions.Overload {
t.Helper()
bindings := []*functions.Overload{}
for _, fn := range funcs {
overloads, err := fn.Bindings()
if err != nil {
t.Fatalf("fn.Bindings() failed: %v", err)
}
bindings = append(bindings, overloads...)
}
return bindings
}
type testActivationWrapper struct {
Activation
name string
}
func (tw *testActivationWrapper) Unwrap() Activation {
return tw.Activation
}