blob: cfddc35b198c097afbe2e75021a9af8e03b9529d [file]
// Copyright 2022 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 (
"errors"
"fmt"
"reflect"
"sort"
"strings"
"testing"
"time"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/pb"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/common/types/traits"
"github.com/google/cel-go/test"
structpb "google.golang.org/protobuf/types/known/structpb"
proto3pb "github.com/google/cel-go/test/proto3pb"
)
func TestNativeTypes(t *testing.T) {
var nativeTests = []struct {
expr string
out any
in any
envOpts []any
}{
{
expr: `ext.TestAllTypes{
NestedVal: ext.TestNestedType{NestedMapVal: {1: false}},
BoolVal: true,
BytesVal: b'hello',
DurationVal: duration('5s'),
DoubleVal: 1.5,
FloatVal: 2.5,
Int32Val: 10,
Int64Val: 20,
StringVal: 'hello world',
TimestampVal: timestamp('2011-08-06T01:23:45Z'),
Uint32Val: 100u,
Uint64Val: 200u,
ListVal: [
ext.TestNestedType{
NestedListVal:['goodbye', 'cruel', 'world'],
NestedMapVal: {42: true},
custom_name: 'name',
},
],
ArrayVal: [
ext.TestNestedType{
NestedListVal:['goodbye', 'cruel', 'world'],
NestedMapVal: {42: true},
custom_name: 'name',
},
],
MapVal: {'map-key': ext.TestAllTypes{BoolVal: true}},
CustomSliceVal: [ext.TestNestedSliceType{Value: 'none'}],
CustomMapVal: {'even': ext.TestMapVal{Value: 'more'}},
custom_name: 'name',
}`,
out: &TestAllTypes{
NestedVal: &TestNestedType{NestedMapVal: map[int64]bool{1: false}},
BoolVal: true,
BytesVal: []byte("hello"),
DurationVal: time.Second * 5,
DoubleVal: 1.5,
FloatVal: 2.5,
Int32Val: 10,
Int64Val: 20,
StringVal: "hello world",
TimestampVal: mustParseTime(t, "2011-08-06T01:23:45Z"),
Uint32Val: uint32(100),
Uint64Val: uint64(200),
ListVal: []*TestNestedType{
{
NestedListVal: []string{"goodbye", "cruel", "world"},
NestedMapVal: map[int64]bool{42: true},
NestedCustomName: "name",
},
},
ArrayVal: [1]*TestNestedType{{
NestedListVal: []string{"goodbye", "cruel", "world"},
NestedMapVal: map[int64]bool{42: true},
NestedCustomName: "name",
}},
MapVal: map[string]TestAllTypes{"map-key": {BoolVal: true}},
CustomSliceVal: []TestNestedSliceType{{Value: "none"}},
CustomMapVal: map[string]TestMapVal{"even": {Value: "more"}},
CustomName: "name",
},
envOpts: []any{ParseStructTags(true)},
},
{
expr: `ext.TestAllTypes{
nestedVal: ext.TestNestedType{NestedMapVal: {1: false}},
boolVal: true,
BytesVal: b'hello',
DurationVal: duration('5s'),
DoubleVal: 1.5,
FloatVal: 2.5,
Int32Val: 10,
Int64Val: 20,
StringVal: 'hello world',
TimestampVal: timestamp('2011-08-06T01:23:45Z'),
Uint32Val: 100u,
Uint64Val: 200u,
ListVal: [
ext.TestNestedType{
NestedListVal:['goodbye', 'cruel', 'world'],
NestedMapVal: {42: true},
custom_name: 'name',
},
],
ArrayVal: [
ext.TestNestedType{
NestedListVal:['goodbye', 'cruel', 'world'],
NestedMapVal: {42: true},
custom_name: 'name',
},
],
MapVal: {'map-key': ext.TestAllTypes{boolVal: true}},
CustomSliceVal: [ext.TestNestedSliceType{Value: 'none'}],
CustomMapVal: {'even': ext.TestMapVal{Value: 'more'}},
CustomName: 'name',
}`,
out: &TestAllTypes{
NestedVal: &TestNestedType{NestedMapVal: map[int64]bool{1: false}},
BoolVal: true,
BytesVal: []byte("hello"),
DurationVal: time.Second * 5,
DoubleVal: 1.5,
FloatVal: 2.5,
Int32Val: 10,
Int64Val: 20,
StringVal: "hello world",
TimestampVal: mustParseTime(t, "2011-08-06T01:23:45Z"),
Uint32Val: uint32(100),
Uint64Val: uint64(200),
ListVal: []*TestNestedType{
{
NestedListVal: []string{"goodbye", "cruel", "world"},
NestedMapVal: map[int64]bool{42: true},
NestedCustomName: "name",
},
},
ArrayVal: [1]*TestNestedType{{
NestedListVal: []string{"goodbye", "cruel", "world"},
NestedMapVal: map[int64]bool{42: true},
NestedCustomName: "name",
}},
MapVal: map[string]TestAllTypes{"map-key": {BoolVal: true}},
CustomSliceVal: []TestNestedSliceType{{Value: "none"}},
CustomMapVal: map[string]TestMapVal{"even": {Value: "more"}},
CustomName: "name",
},
envOpts: []any{ParseStructTag("json")},
},
{
expr: `ext.TestAllTypes{
NestedVal: ext.TestNestedType{NestedMapVal: {1: false}},
BoolVal: true,
BytesVal: b'hello',
DurationVal: duration('5s'),
DoubleVal: 1.5,
FloatVal: 2.5,
Int32Val: 10,
Int64Val: 20,
StringVal: 'hello world',
TimestampVal: timestamp('2011-08-06T01:23:45Z'),
Uint32Val: 100u,
Uint64Val: 200u,
ListVal: [
ext.TestNestedType{
NestedListVal:['goodbye', 'cruel', 'world'],
NestedMapVal: {42: true},
NestedCustomName: 'name',
},
],
ArrayVal: [
ext.TestNestedType{
NestedListVal:['goodbye', 'cruel', 'world'],
NestedMapVal: {42: true},
NestedCustomName: 'name',
},
],
MapVal: {'map-key': ext.TestAllTypes{BoolVal: true}},
CustomSliceVal: [ext.TestNestedSliceType{Value: 'none'}],
CustomMapVal: {'even': ext.TestMapVal{Value: 'more'}},
CustomName: 'name',
}`,
out: &TestAllTypes{
NestedVal: &TestNestedType{NestedMapVal: map[int64]bool{1: false}},
BoolVal: true,
BytesVal: []byte("hello"),
DurationVal: time.Second * 5,
DoubleVal: 1.5,
FloatVal: 2.5,
Int32Val: 10,
Int64Val: 20,
StringVal: "hello world",
TimestampVal: mustParseTime(t, "2011-08-06T01:23:45Z"),
Uint32Val: uint32(100),
Uint64Val: uint64(200),
ListVal: []*TestNestedType{
{
NestedListVal: []string{"goodbye", "cruel", "world"},
NestedMapVal: map[int64]bool{42: true},
NestedCustomName: "name",
},
},
ArrayVal: [1]*TestNestedType{{
NestedListVal: []string{"goodbye", "cruel", "world"},
NestedMapVal: map[int64]bool{42: true},
NestedCustomName: "name",
}},
MapVal: map[string]TestAllTypes{"map-key": {BoolVal: true}},
CustomSliceVal: []TestNestedSliceType{{Value: "none"}},
CustomMapVal: map[string]TestMapVal{"even": {Value: "more"}},
CustomName: "name",
},
},
{
expr: `ext.TestAllTypes{
PbVal: test.TestAllTypes{single_int32: 123}
}.PbVal`,
out: &proto3pb.TestAllTypes{SingleInt32: 123},
},
{
expr: `ext.TestAllTypes{PbVal: test.TestAllTypes{}} ==
ext.TestAllTypes{PbVal: test.TestAllTypes{single_bool: false}}`,
},
{expr: `ext.TestNestedType{} == TestNestedType{}`},
{expr: `ext.TestAllTypes{}.BoolVal != true`},
{expr: `!has(ext.TestAllTypes{}.BoolVal) && !has(ext.TestAllTypes{}.NestedVal)`},
{expr: `type(ext.TestAllTypes) == type`},
{expr: `type(ext.TestAllTypes{}) == ext.TestAllTypes`},
{expr: `type(ext.TestAllTypes{}) == ext.TestAllTypes`},
{expr: `ext.TestAllTypes != test.TestAllTypes`},
{expr: `ext.TestAllTypes{BoolVal: true} != dyn(test.TestAllTypes{single_bool: true})`},
{expr: `ext.TestAllTypes{}.NestedVal == ext.TestNestedType{}`},
{expr: `ext.TestNestedType{} == ext.TestAllTypes{}.NestedStructVal`},
{expr: `ext.TestAllTypes{}.NestedStructVal == ext.TestNestedType{}`},
{expr: `ext.TestAllTypes{}.ListVal.size() == 0`},
{expr: `ext.TestAllTypes{}.MapVal.size() == 0`},
{expr: `ext.TestAllTypes{}.TimestampVal == timestamp(0)`},
{expr: `test.TestAllTypes{}.single_timestamp == timestamp(0)`},
{expr: `[TestAllTypes{BoolVal: true}, TestAllTypes{BoolVal: false}].exists(t, t.BoolVal == true)`},
{expr: `[TestAllTypes{CustomName: 'Alice'}, TestAllTypes{CustomName: 'Bob'}].exists(t, t.CustomName == 'Alice')`},
{expr: `[TestAllTypes{custom_name: 'Alice'}, TestAllTypes{custom_name: 'Bob'}].exists(t, t.custom_name == 'Alice')`, envOpts: []any{ParseStructTags(true)}},
{expr: `TestAllTypes{BytesArrayVal: b'1234'}.BytesArrayVal != b'123'`},
{expr: `TestAllTypes{BytesArrayVal: b'1234'}.BytesArrayVal == b'1234'`},
{
expr: `tests.all(t, t.Int32Val > 17)`,
in: map[string]any{
"tests": []*TestAllTypes{{Int32Val: 18}, {Int32Val: 19}, {Int32Val: 20}},
},
},
}
for i, tst := range nativeTests {
tc := tst
t.Run(fmt.Sprintf("[%d]", i), func(t *testing.T) {
env := testNativeEnv(t, tc.envOpts...)
var asts []*cel.Ast
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())
}
asts = append(asts, cAst)
for _, ast := range asts {
prg, err := env.Program(ast)
if err != nil {
t.Fatal(err)
}
in := tc.in
if in == nil {
in = cel.NoVars()
}
out, _, err := prg.Eval(in)
if err != nil {
t.Fatal(err)
}
want := tc.out
if want == nil {
want = true
}
wantPB, isPB := want.(proto.Message)
if isPB && !pb.Equal(wantPB, out.Value().(proto.Message)) {
t.Errorf("got %v, wanted %v for expr: %s", out.Value(), want, tc.expr)
}
if !isPB && !reflect.DeepEqual(out.Value(), want) {
t.Errorf("got %v, wanted %v for expr: %s", out.Value(), want, tc.expr)
}
}
})
}
}
func TestNativeFindStructFieldNames(t *testing.T) {
env := testNativeEnv(t, ParseStructTags(true))
provider := env.CELTypeProvider()
tests := []struct {
typeName string
fields []string
}{
{
typeName: "ext.TestNestedType",
fields: []string{"NestedListVal", "NestedMapVal", "custom_name"},
},
{
typeName: "google.expr.proto3.test.TestAllTypes.NestedMessage",
fields: []string{"bb"},
},
{
typeName: "invalid.TypeName",
fields: []string{},
},
}
for _, tst := range tests {
tc := tst
t.Run(fmt.Sprintf("%s", tc.typeName), func(t *testing.T) {
fields, _ := provider.FindStructFieldNames(tc.typeName)
sort.Strings(fields)
sort.Strings(tc.fields)
if !reflect.DeepEqual(fields, tc.fields) {
t.Errorf("got %v, wanted %v", fields, tc.fields)
}
})
}
}
func TestNativeTypesStaticErrors(t *testing.T) {
var nativeTests = []struct {
expr string
err string
}{
{
expr: `TestAllTypos{}`,
err: `ERROR: <input>:1:13: undeclared reference to 'TestAllTypos' (in container 'ext')
| TestAllTypos{}
| ............^`,
},
{
expr: `ext.TestAllTypes{bool_val: false}`,
err: `ERROR: <input>:1:26: undefined field 'bool_val'
| ext.TestAllTypes{bool_val: false}
| .........................^`,
},
{
expr: `ext.TestAllTypes{UnsupportedVal: null}`,
err: `ERROR: <input>:1:32: undefined field 'UnsupportedVal'
| ext.TestAllTypes{UnsupportedVal: null}
| ...............................^`,
},
{
expr: `ext.TestAllTypes{UnsupportedListVal: null}`,
err: `ERROR: <input>:1:36: undefined field 'UnsupportedListVal'
| ext.TestAllTypes{UnsupportedListVal: null}
| ...................................^`,
},
{
expr: `ext.TestAllTypes{UnsupportedMapVal: null}`,
err: `ERROR: <input>:1:35: undefined field 'UnsupportedMapVal'
| ext.TestAllTypes{UnsupportedMapVal: null}
| ..................................^`,
},
}
env := testNativeEnv(t)
for i, tst := range nativeTests {
tc := tst
t.Run(fmt.Sprintf("[%d]", i), func(t *testing.T) {
_, iss := env.Compile(tc.expr)
if iss.Err() == nil {
t.Fatalf("env.Compile(%v) succeeded, wanted error", tc.expr)
}
if !test.Compare(iss.Err().Error(), tc.err) {
t.Errorf("env.Compile(%v) got %v, wanted error %s", tc.expr, iss.Err(), tc.err)
}
})
}
}
func TestNativeTypesJsonSerialization(t *testing.T) {
tests := []struct {
expr string
out string
additionalEnvOptions []any
}{
{
expr: `[b'string']`,
out: `["c3RyaW5n"]`,
},
{
expr: `TestAllTypes{
BoolVal: true,
DurationVal: duration('5s'),
DoubleVal: 1.5,
FloatVal: 2.0,
Int32Val: 23,
Int64Val: 64,
MapVal: {
'map-key': ext.TestAllTypes{
BoolVal: true
}
},
NestedVal: TestNestedType{
NestedListVal: ["first", "second"],
},
StringVal: "string",
CustomName: "name",
}`,
out: `{
"BoolVal": true,
"CustomName": "name",
"DoubleVal": 1.5,
"DurationVal": "5s",
"FloatVal": 2,
"Int32Val": 23,
"Int64Val": 64,
"MapVal": {
"map-key": {
"BoolVal": true
}
},
"NestedVal": {
"NestedListVal": [
"first",
"second"
]
},
"StringVal": "string"
}`,
},
{
expr: `TestAllTypes{
BoolVal: true,
DurationVal: duration('5s'),
DoubleVal: 1.5,
FloatVal: 2.0,
Int32Val: 23,
Int64Val: 64,
MapVal: {
'map-key': ext.TestAllTypes{
BoolVal: true
}
},
NestedVal: TestNestedType{
NestedListVal: ["first", "second"],
},
StringVal: "string",
custom_name: "name",
}`,
out: `{
"BoolVal": true,
"DoubleVal": 1.5,
"DurationVal": "5s",
"FloatVal": 2,
"Int32Val": 23,
"Int64Val": 64,
"MapVal": {
"map-key": {
"BoolVal": true
}
},
"NestedVal": {
"NestedListVal": [
"first",
"second"
]
},
"StringVal": "string",
"custom_name": "name"
}`,
additionalEnvOptions: []any{ParseStructTags(true)},
},
}
for i, tst := range tests {
tc := tst
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
env := testNativeEnv(t, tst.additionalEnvOptions...)
ast, iss := env.Compile(tc.expr)
if iss.Err() != nil {
t.Fatalf("env.Compile(%v) failed: %v", tc.expr, iss.Err())
}
prg, err := env.Program(ast)
if err != nil {
t.Fatalf("env.Program() failed: %v", err)
}
out, _, err := prg.Eval(cel.NoVars())
if err != nil {
t.Fatalf("prg.Eval() failed: %v", err)
}
conv, err := out.ConvertToNative(reflect.TypeOf(&structpb.Value{}))
if err != nil {
t.Fatalf("out.ConvertToNative(Value) failed: %v", err)
}
json := protojson.Format(conv.(proto.Message))
if !test.Compare(json, tc.out) {
t.Errorf("expr %v converted to %v, wanted %v", tc.expr, json, tc.out)
}
})
}
}
func TestNativeTypesRuntimeErrors(t *testing.T) {
var nativeTests = []struct {
expr string
err string
}{
{
expr: `TestAllTypos{}`,
err: `unknown type: TestAllTypos`,
},
{
expr: `ext.TestAllTypes{bool_val: false}`,
err: `no such field: bool_val`,
},
{
expr: `ext.TestAllTypes{UnsupportedVal: null}`,
err: `no such field: UnsupportedVal`,
},
{
expr: `ext.TestAllTypes{UnsupportedListVal: null}`,
err: `no such field: UnsupportedListVal`,
},
{
expr: `ext.TestAllTypes{UnsupportedMapVal: null}`,
err: `no such field: UnsupportedMapVal`,
},
{
expr: `ext.TestAllTypes{privateVal: null}`,
err: `no such field: privateVal`,
},
{
expr: `ext.TestAllTypes{}.UnsupportedMapVal`,
err: `no such field: UnsupportedMapVal`,
},
{
expr: `ext.TestAllTypes{}.privateVal`,
err: `no such field: privateVal`,
},
{
expr: `ext.TestAllTypes{BoolVal: 'false'}`,
err: `unsupported native conversion from string to 'bool'`,
},
{
expr: `has(ext.TestAllTypes{}.BadFieldName)`,
err: `no such field: BadFieldName`,
},
{
expr: `ext.TestAllTypes{}[42]`,
err: `no such overload`,
},
{
expr: `ext.TestAllTypes{Int32Val: 9223372036854775807}`,
err: `integer overflow`,
},
{
expr: `ext.TestAllTypes{Uint32Val: 9223372036854775807u}`,
err: `unsigned integer overflow`,
},
}
env := testNativeEnv(t)
for i, tst := range nativeTests {
tc := tst
t.Run(fmt.Sprintf("[%d]", i), func(t *testing.T) {
ast, iss := env.Parse(tc.expr)
if iss.Err() != nil {
t.Fatalf("env.Parse(%v) failed: %v", tc.expr, iss.Err())
}
prg, err := env.Program(ast)
if err != nil {
if !strings.Contains(err.Error(), tc.err) {
t.Fatal(err)
}
return
}
out, _, err := prg.Eval(cel.NoVars())
if err == nil || !strings.Contains(err.Error(), tc.err) {
var got any = err
if err == nil {
got = out
}
t.Fatalf("prg.Eval() got %v, wanted error %v", got, tc.err)
}
})
}
}
func TestNativeTypesErrors(t *testing.T) {
envTests := []struct {
nativeType any
err string
}{
{
nativeType: reflect.TypeOf(1),
err: "unsupported reflect.Type",
},
{
nativeType: reflect.ValueOf(1),
err: "unsupported reflect.Type",
},
{
nativeType: 1,
err: "must be reflect.Type or reflect.Value",
},
}
for i, tst := range envTests {
tc := tst
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
_, err := cel.NewEnv(NativeTypes(tc.nativeType))
if err == nil || !strings.Contains(err.Error(), tc.err) {
t.Errorf("cel.NewEnv(NativeTypes(%v)) got error %v, wanted %v", tc.nativeType, err, tc.err)
}
})
}
}
func TestNativeTypesConvertToNative(t *testing.T) {
env := testNativeEnv(t, NativeTypes(reflect.TypeOf(TestNestedType{})))
adapter := env.CELTypeAdapter()
conversions := []struct {
in any
inType *cel.Type
out any
err string
}{
{
in: &TestAllTypes{BoolVal: true},
inType: cel.ObjectType("ext.TestAllTypes"),
out: &TestAllTypes{BoolVal: true},
},
{
in: TestAllTypes{BoolVal: true},
inType: cel.ObjectType("ext.TestAllTypes"),
out: &TestAllTypes{BoolVal: true},
},
{
in: &TestAllTypes{BoolVal: true},
inType: cel.ObjectType("ext.TestAllTypes"),
out: TestAllTypes{BoolVal: true},
},
{
in: nil,
inType: cel.NullType,
out: types.NullValue,
},
{
in: &TestAllTypes{BoolVal: true},
inType: cel.ObjectType("ext.TestAllTypes"),
out: &proto3pb.TestAllTypes{},
err: "type conversion error",
},
{
in: [3]int32{1, 2, 3},
inType: cel.ListType(cel.IntType),
out: []int32{1, 2, 3},
},
{
in: &[3]byte{1, 2, 3},
inType: cel.BytesType,
out: []byte{1, 2, 3},
},
{
in: [3]byte{1, 2, 3},
inType: cel.BytesType,
out: []byte{1, 2, 3},
},
}
for _, c := range conversions {
inVal := adapter.NativeToValue(c.in)
if types.IsError(inVal) {
t.Fatalf("adapter.NativeToValue(%v) failed: %v", c.in, inVal)
}
if inVal.Type().TypeName() != c.inType.TypeName() {
t.Fatalf("adapter.NativeToValue() got type %v, wanted type %v", inVal.Type(), c.inType)
}
out, err := inVal.ConvertToNative(reflect.TypeOf(c.out))
if err != nil {
if c.err != "" {
if !strings.Contains(err.Error(), c.err) {
t.Fatalf("%v.ConvertToNative(%T) got %v, wanted error %v", c.in, c.out, err, c.err)
}
return
}
t.Fatalf("%v.ConvertToNative(%T) failed: %v", c.in, c.out, err)
}
if !reflect.DeepEqual(out, c.out) {
t.Errorf("%v.ConvertToNative(%T) got %v, wanted %v", c.in, c.out, out, c.out)
}
}
}
func TestConvertToTypeErrors(t *testing.T) {
env := testNativeEnv(t, NativeTypes(reflect.TypeOf(TestNestedType{})))
adapter := env.CELTypeAdapter()
conversions := []struct {
in any
out any
err string
}{
{
in: &TestAllTypes{BoolVal: true},
out: &TestAllTypes{BoolVal: true},
},
{
in: TestAllTypes{BoolVal: true},
out: &TestAllTypes{BoolVal: true},
},
{
in: &TestAllTypes{BoolVal: true},
out: TestAllTypes{BoolVal: true},
},
{
in: &TestAllTypes{BoolVal: true},
out: &proto3pb.TestAllTypes{},
err: "type conversion error",
},
}
for _, c := range conversions {
inVal := adapter.NativeToValue(c.in)
outVal := adapter.NativeToValue(c.out)
if types.IsError(inVal) {
t.Fatalf("adapter.NativeToValue(%v) failed: %v", c.in, inVal)
}
if types.IsError(outVal) {
t.Fatalf("adapter.NativeToValue(%v) failed: %v", c.out, outVal)
}
conv := inVal.ConvertToType(outVal.Type())
if c.err != "" {
if !types.IsError(conv) {
t.Fatalf("%v.ConvertToType(%v) got %v, wanted error %v", c.in, outVal.Type(), conv, c.err)
}
convErr := conv.(*types.Err)
if !strings.Contains(convErr.Error(), c.err) {
t.Fatalf("%v.ConvertToType(%v) got %v, wanted error %v", c.in, outVal.Type(), conv, c.err)
}
return
}
if conv != inVal {
t.Errorf("%v.ConvertToType(%v) got %v, wanted %v", c.in, outVal.Type(), conv, c.err)
}
conv = inVal.ConvertToType(types.TypeType)
if conv.Type() != types.TypeType || conv.(ref.Type) != inVal.Type() {
t.Errorf("%v.ConvertToType(Type) got %v, wanted %v", inVal, conv, inVal.Type())
}
}
}
func TestNativeTypesWithOptional(t *testing.T) {
var nativeTests = []struct {
expr string
}{
{expr: `!optional.ofNonZeroValue(ext.TestAllTypes{}).hasValue()`},
{expr: `!ext.TestAllTypes{}.?BoolVal.orValue(false)`},
{expr: `!ext.TestAllTypes{}.?BoolVal.hasValue()`},
{expr: `!ext.TestAllTypes{BoolVal: false}.?BoolVal.hasValue()`},
{expr: `ext.TestAllTypes{BoolVal: true}.?BoolVal.hasValue()`},
{expr: `ext.TestAllTypes{}.NestedVal.?NestedMapVal.orValue({}).size() == 0`},
}
env := testNativeEnv(t, cel.OptionalTypes())
for i, tst := range nativeTests {
tc := tst
t.Run(fmt.Sprintf("[%d]", i), func(t *testing.T) {
var asts []*cel.Ast
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())
}
asts = append(asts, cAst)
for _, ast := range asts {
prg, err := env.Program(ast)
if err != nil {
t.Fatal(err)
}
out, _, err := prg.Eval(cel.NoVars())
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(out.Value(), true) {
t.Errorf("got %v, wanted true for expr: %s", out.Value(), tc.expr)
}
}
})
}
}
func TestNativeTypesWithCELTypedFields(t *testing.T) {
var nativeTests = []struct {
expr string
}{
{
expr: `ext.TestRefValFieldType{optional_name: optional.of('my name')}.optional_name.orValue('') == 'my name'`,
},
{
expr: `ext.TestRefValFieldType{IntVal: 2}.IntVal >= 1`,
},
{
expr: `ext.TestRefValFieldType{time: timestamp('2001-01-01T00:00:00Z')}.time > timestamp('1970-01-01T00:00:00Z')`,
},
}
env := testNativeEnv(t, cel.OptionalTypes(), ParseStructTag("cel"))
for i, tst := range nativeTests {
tc := tst
t.Run(fmt.Sprintf("[%d]", i), func(t *testing.T) {
var asts []*cel.Ast
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())
}
asts = append(asts, cAst)
for _, ast := range asts {
prg, err := env.Program(ast)
if err != nil {
t.Fatal(err)
}
out, _, err := prg.Eval(cel.NoVars())
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(out.Value(), true) {
t.Errorf("got %v, wanted true for expr: %s", out.Value(), tc.expr)
}
}
})
}
}
func TestNativeTypeConvertToType(t *testing.T) {
var nativeTests = []struct {
tag string
}{
{tag: "cel"},
{tag: "json"},
}
for i, tst := range nativeTests {
tc := tst
t.Run(fmt.Sprintf("[%d]", i), func(t *testing.T) {
handler := fieldNameByTag(tc.tag)
nt, err := newNativeType(handler, reflect.TypeOf(&TestAllTypes{}))
if err != nil {
t.Fatalf("newNativeType() failed: %v", err)
}
if nt.ConvertToType(types.TypeType) != types.TypeType {
t.Error("ConvertToType(Type) failed")
}
if !types.IsError(nt.ConvertToType(types.StringType)) {
t.Errorf("ConvertToType(String) got %v, wanted error", nt.ConvertToType(types.StringType))
}
})
}
}
func TestNativeTypeConvertToNative(t *testing.T) {
nt, err := newNativeType(fieldNameByTag("cel"), reflect.TypeOf(&TestAllTypes{}))
if err != nil {
t.Fatalf("newNativeType() failed: %v", err)
}
out, err := nt.ConvertToNative(reflect.TypeOf(1))
if err == nil {
t.Errorf("nt.ConvertToNative(1) produced %v, wanted error", out)
}
}
func TestNativeTypeHasTrait(t *testing.T) {
nt, err := newNativeType(fieldNameByTag("cel"), reflect.TypeOf(&TestAllTypes{}))
if err != nil {
t.Fatalf("newNativeType() failed: %v", err)
}
if !nt.HasTrait(traits.IndexerType) || !nt.HasTrait(traits.FieldTesterType) {
t.Error("nt.HasTrait() failed indicate support for presence test and field access.")
}
}
func TestNativeTypeValue(t *testing.T) {
nt, err := newNativeType(fieldNameByTag("cel"), reflect.TypeOf(&TestAllTypes{}))
if err != nil {
t.Fatalf("newNativeType() failed: %v", err)
}
if nt.Value() != nt.String() {
t.Errorf("nt.Value() got %v, wanted %v", nt.Value(), nt.String())
}
}
func TestNativeStructWithMultipleSameFieldNames(t *testing.T) {
_, err := newNativeType(fieldNameByTag("cel"), reflect.TypeOf(TestStructWithMultipleSameNames{}))
if err == nil {
t.Fatal("newNativeType() did not fail as expected")
}
if !errors.Is(err, errDuplicatedFieldName) {
t.Fatalf("newNativeType() exepected duplicated field name error, but got: %v", err)
}
}
func TestNativeStructEmbedded(t *testing.T) {
var nativeTests = []struct {
expr string
in any
}{
{
expr: `test.embedded.custom_name == "name"`,
in: map[string]any{
"test": &TestEmbeddedTypes{TestNestedType{NestedCustomName: "name"}},
},
},
}
envOpts := []cel.EnvOption{
NativeTypes(
reflect.TypeOf(&TestEmbeddedTypes{}),
reflect.TypeOf(&TestNestedType{}),
ParseStructTag("json"),
),
cel.Variable("test", cel.ObjectType("ext.TestEmbeddedTypes")),
}
env, err := cel.NewEnv(envOpts...)
if err != nil {
t.Fatalf("cel.NewEnv(NativeTypes()) failed: %v", err)
}
for i, tst := range nativeTests {
tc := tst
t.Run(fmt.Sprintf("[%d]", i), func(t *testing.T) {
var asts []*cel.Ast
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())
}
asts = append(asts, cAst)
for _, ast := range asts {
prg, err := env.Program(ast)
if err != nil {
t.Fatal(err)
}
out, _, err := prg.Eval(tc.in)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(out.Value(), true) {
t.Errorf("got %v, wanted true for expr: %s", out.Value(), tc.expr)
}
}
})
}
}
type TestNestedStruct struct {
ListVal []*TestNestedType
}
func TestNativeNestedStruct(t *testing.T) {
var nativeTests = []struct {
expr string
in any
}{
{
expr: `test.ListVal.exists(x, x.custom_name == "name")`,
in: map[string]any{
"test": &TestNestedStruct{ListVal: []*TestNestedType{{NestedCustomName: "name"}}},
},
},
}
envOpts := []cel.EnvOption{
NativeTypes(
reflect.ValueOf(&TestNestedStruct{}),
ParseStructTag("json"),
),
cel.Variable("test", cel.ObjectType("ext.TestNestedStruct")),
}
env, err := cel.NewEnv(envOpts...)
if err != nil {
t.Fatalf("cel.NewEnv(NativeTypes()) failed: %v", err)
}
for i, tst := range nativeTests {
tc := tst
t.Run(fmt.Sprintf("[%d]", i), func(t *testing.T) {
var asts []*cel.Ast
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())
}
asts = append(asts, cAst)
for _, ast := range asts {
prg, err := env.Program(ast)
if err != nil {
t.Fatal(err)
}
out, _, err := prg.Eval(tc.in)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(out.Value(), true) {
t.Errorf("got %v, wanted true for expr: %s", out.Value(), tc.expr)
}
}
})
}
}
func TestNativeTypesVersion(t *testing.T) {
_, err := cel.NewEnv(NativeTypes(NativeTypesVersion(0)))
if err != nil {
t.Fatalf("NewEnv(NativeTypes(NativeTypesVersion(0))) failed: %v", err)
}
}
func TestTypeResolutionRace(t *testing.T) {
customType := reflect.TypeFor[*Custom]()
env, err := cel.NewEnv(
cel.Container("ext"),
NativeTypes(
ParseStructTag("cel"),
customType,
),
)
if err != nil {
t.Fatal("NewEnv:", err)
}
tests := []struct {
name string
expr string
}{
{name: "custom1", expr: `Custom{ name: "name1" }`},
{name: "custom2", expr: `Custom{ name: "name2" }`},
{name: "custom3", expr: `Custom{ name: "name3" }`},
{name: "custom4", expr: `Custom{ name: "name4" }`},
{name: "custom5", expr: `Custom{ name: "name5" }`},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
ast, iss := env.Compile(test.expr)
if err := iss.Err(); err != nil {
t.Fatal("Compile:", err)
}
prg, err := env.Program(ast)
if err != nil {
t.Fatalf("env.Program() failed: %s", err)
}
prg.Eval(cel.NoVars())
})
}
}
// testEnv initializes the test environment common to all tests.
func testNativeEnv(t *testing.T, opts ...any) *cel.Env {
t.Helper()
envOpts := []cel.EnvOption{
cel.Container("ext"),
cel.Abbrevs("google.expr.proto3.test"),
cel.Types(&proto3pb.TestAllTypes{}),
cel.Variable("tests", cel.ListType(cel.ObjectType("ext.TestAllTypes"))),
}
nativeOpts := []any{
reflect.ValueOf(&TestAllTypes{}),
reflect.ValueOf(&TestRefValFieldType{}),
}
for _, o := range opts {
switch opt := o.(type) {
case NativeTypesOption:
nativeOpts = append(nativeOpts, opt)
case cel.EnvOption:
envOpts = append(envOpts, opt)
default:
t.Fatalf("invalid option type: %s", reflect.TypeOf(o).Name())
}
}
envOpts = append(envOpts,
NativeTypes(
nativeOpts...,
),
)
env, err := cel.NewEnv(envOpts...)
if err != nil {
t.Fatalf("cel.NewEnv(NativeTypes()) failed: %v", err)
}
return env
}
func mustParseTime(t *testing.T, timestamp string) time.Time {
t.Helper()
out, err := time.Parse(time.RFC3339, timestamp)
if err != nil {
t.Fatalf("time.Parse(%q) failed: %v", timestamp, err)
}
return out
}
type Custom struct {
Name string `cel:"name"`
}
type TestStructWithMultipleSameNames struct {
Name string
custom_name string `cel:"Name"`
}
type TestNestedType struct {
NestedListVal []string
NestedMapVal map[int64]bool
NestedCustomName string `cel:"custom_name" json:"custom_name"`
}
type TestAllTypes struct {
NestedVal *TestNestedType `json:"nestedVal,omitempty"`
NestedStructVal TestNestedType `json:"nestedStructVal"`
BoolVal bool `json:"boolVal"`
BytesVal []byte
DurationVal time.Duration
DoubleVal float64
FloatVal float32
Int32Val int32
Int64Val int64
StringVal string
TimestampVal time.Time
Uint32Val uint32
Uint64Val uint64
ListVal []*TestNestedType
ArrayVal [1]*TestNestedType
BytesArrayVal [4]byte
MapVal map[string]TestAllTypes
PbVal *proto3pb.TestAllTypes
CustomSliceVal []TestNestedSliceType
CustomMapVal map[string]TestMapVal
CustomName string `cel:"custom_name"`
// channel types are not supported
UnsupportedVal chan string
UnsupportedListVal []chan string
UnsupportedMapVal map[int]chan string
// unexported types can be found but not set or accessed
privateVal map[string]string
}
type TestNestedSliceType struct {
Value string
}
type TestMapVal struct {
Value string
}
type TestEmbeddedTypes struct {
TestNestedType `json:"embedded,omitempty"`
}
type TestRefValFieldType struct {
OptionalName *types.Optional `cel:"optional_name"`
IntVal types.Int
CELTime types.Timestamp `cel:"time"`
}