| // Copyright 2019 Google LLC |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package interpreter |
| |
| import ( |
| "errors" |
| "fmt" |
| "reflect" |
| "testing" |
| |
| "github.com/google/cel-go/checker" |
| "github.com/google/cel-go/common" |
| "github.com/google/cel-go/common/containers" |
| "github.com/google/cel-go/common/decls" |
| "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/parser" |
| |
| exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" |
| anypb "google.golang.org/protobuf/types/known/anypb" |
| |
| proto3pb "github.com/google/cel-go/test/proto3pb" |
| ) |
| |
| func TestAttributesAbsoluteAttr(t *testing.T) { |
| reg := newTestRegistry(t) |
| cont, err := containers.NewContainer(containers.Name("acme.ns")) |
| if err != nil { |
| t.Fatal(err) |
| } |
| attrs := NewAttributeFactory(cont, reg, reg) |
| vars, _ := NewActivation(map[string]any{ |
| "acme.a": map[string]any{ |
| "b": map[uint]any{ |
| 4: map[bool]string{ |
| false: "success", |
| }, |
| }, |
| }, |
| }) |
| |
| // acme.a.b[4][false] |
| attr := attrs.AbsoluteAttribute(1, "acme.a") |
| qualB := makeQualifier(t, attrs, nil, 2, "b") |
| qual4 := makeQualifier(t, attrs, nil, 3, uint64(4)) |
| qualFalse := makeQualifier(t, attrs, nil, 4, false) |
| attr.AddQualifier(qualB) |
| attr.AddQualifier(qual4) |
| attr.AddQualifier(qualFalse) |
| out, err := attr.Resolve(vars) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if out != types.String("success") { |
| t.Errorf("Got %v (%T), wanted success", out, out) |
| } |
| } |
| |
| func TestAttributesAbsoluteAttrType(t *testing.T) { |
| reg := newTestRegistry(t) |
| attrs := NewAttributeFactory(containers.DefaultContainer, reg, reg) |
| |
| // int |
| attr := attrs.AbsoluteAttribute(1, "int") |
| out, err := attr.Resolve(EmptyActivation()) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if out != types.IntType { |
| t.Errorf("Got %v, wanted success", out) |
| } |
| } |
| |
| func TestAttributesAbsoluteAttrError(t *testing.T) { |
| reg := newTestRegistry(t) |
| attrs := NewAttributeFactory(containers.DefaultContainer, reg, reg) |
| vars, err := NewActivation(map[string]any{ |
| "err": types.NewErr("invalid variable computation"), |
| }) |
| if err != nil { |
| t.Fatalf("NewActivation() failed: %v", err) |
| } |
| |
| // acme.a.b[4][false] |
| attr := attrs.AbsoluteAttribute(1, "err") |
| qualMsg := makeQualifier(t, attrs, nil, 2, "message") |
| attr.AddQualifier(qualMsg) |
| out, err := attr.Resolve(vars) |
| if err == nil { |
| t.Errorf("attr.Resolve('err') got %v, wanted error", out) |
| } |
| } |
| |
| func TestAttributesRelativeAttr(t *testing.T) { |
| reg := newTestRegistry(t) |
| attrs := NewAttributeFactory(containers.DefaultContainer, reg, reg) |
| data := map[string]any{ |
| "a": map[int]any{ |
| -1: []int32{2, 42}, |
| }, |
| "b": 1, |
| } |
| vars, _ := NewActivation(data) |
| |
| // The relative attribute under test is applied to a map literal: |
| // { |
| // a: {-1: [2, 42], b: 1} |
| // b: 1 |
| // } |
| // |
| // The expression being evaluated is: <map-literal>.a[-1][b] -> 42 |
| op := NewConstValue(1, reg.NativeToValue(data)) |
| attr := attrs.RelativeAttribute(1, op) |
| qualA := makeQualifier(t, attrs, nil, 2, "a") |
| qualNeg1 := makeQualifier(t, attrs, nil, 3, int64(-1)) |
| attr.AddQualifier(qualA) |
| attr.AddQualifier(qualNeg1) |
| attr.AddQualifier(attrs.AbsoluteAttribute(4, "b")) |
| out, err := attr.Resolve(vars) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if out != types.Int(42) { |
| t.Errorf("Got %v (%T), wanted 42", out, out) |
| } |
| } |
| |
| func TestAttributesRelativeAttrOneOf(t *testing.T) { |
| reg := newTestRegistry(t) |
| cont, err := containers.NewContainer(containers.Name("acme.ns")) |
| if err != nil { |
| t.Fatal(err) |
| } |
| attrs := NewAttributeFactory(cont, reg, reg) |
| data := map[string]any{ |
| "a": map[int]any{ |
| -1: []int32{2, 42}, |
| }, |
| "acme.b": 1, |
| } |
| vars, _ := NewActivation(data) |
| |
| // The relative attribute under test is applied to a map literal: |
| // { |
| // a: {-1: [2, 42], b: 1} |
| // b: 1 |
| // } |
| // |
| // The expression being evaluated is: <map-literal>.a[-1][b] -> 42 |
| // |
| // However, since the test is validating what happens with maybe attributes |
| // the attribute resolution must also consider the following variations: |
| // - <map-literal>.a[-1][acme.ns.b] |
| // - <map-literal>.a[-1][acme.b] |
| // |
| // The correct behavior should yield the value of the last alternative. |
| op := NewConstValue(1, reg.NativeToValue(data)) |
| attr := attrs.RelativeAttribute(1, op) |
| qualA := makeQualifier(t, attrs, nil, 2, "a") |
| qualNeg1 := makeQualifier(t, attrs, nil, 3, int64(-1)) |
| attr.AddQualifier(qualA) |
| attr.AddQualifier(qualNeg1) |
| attr.AddQualifier(attrs.MaybeAttribute(4, "b")) |
| out, err := attr.Resolve(vars) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if out != types.Int(42) { |
| t.Errorf("Got %v (%T), wanted 42", out, out) |
| } |
| } |
| |
| func TestAttributesRelativeAttrConditional(t *testing.T) { |
| reg := newTestRegistry(t) |
| attrs := NewAttributeFactory(containers.DefaultContainer, reg, reg) |
| data := map[string]any{ |
| "a": map[int]any{ |
| -1: []int32{2, 42}, |
| }, |
| "b": []int{0, 1}, |
| "c": []any{1, 0}, |
| } |
| vars, _ := NewActivation(data) |
| |
| // The relative attribute under test is applied to a map literal: |
| // { |
| // a: {-1: [2, 42], b: 1} |
| // b: [0, 1], |
| // c: {1, 0}, |
| // } |
| // |
| // The expression being evaluated is: |
| // <map-literal>.a[-1][(false ? b : c)[0]] -> 42 |
| // |
| // Effectively the same as saying <map-literal>.a[-1][c[0]] |
| cond := NewConstValue(2, types.False) |
| condAttr := attrs.ConditionalAttribute(4, cond, |
| attrs.AbsoluteAttribute(5, "b"), |
| attrs.AbsoluteAttribute(6, "c")) |
| qual0 := makeQualifier(t, attrs, nil, 7, 0) |
| condAttr.AddQualifier(qual0) |
| |
| obj := NewConstValue(1, reg.NativeToValue(data)) |
| attr := attrs.RelativeAttribute(1, obj) |
| qualA := makeQualifier(t, attrs, nil, 2, "a") |
| qualNeg1 := makeQualifier(t, attrs, nil, 3, int64(-1)) |
| attr.AddQualifier(qualA) |
| attr.AddQualifier(qualNeg1) |
| attr.AddQualifier(condAttr) |
| out, err := attr.Resolve(vars) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if out != types.Int(42) { |
| t.Errorf("Got %v (%T), wanted 42", out, out) |
| } |
| } |
| |
| func TestAttributesRelativeAttrRelativeQualifier(t *testing.T) { |
| cont, err := containers.NewContainer(containers.Name("acme.ns")) |
| if err != nil { |
| t.Fatal(err) |
| } |
| reg := newTestRegistry(t) |
| attrs := NewAttributeFactory(cont, reg, reg) |
| data := map[string]any{ |
| "a": map[int]any{ |
| -1: map[string]any{ |
| "first": uint(1), |
| "second": uint(2), |
| "third": uint(3), |
| }, |
| }, |
| "b": uint32(2), |
| } |
| vars, _ := NewActivation(data) |
| |
| // The environment declares the following variables: |
| // { |
| // a: { |
| // -1: { |
| // "first": 1u, |
| // "second": 2u, |
| // "third": 3u, |
| // } |
| // }, |
| // b: 2u, |
| // } |
| // |
| // The map of input variables is also re-used as a map-literal <obj> in the expression. |
| // |
| // The relative object under test is the following map literal. |
| // <mp> { |
| // 1u: "first", |
| // 2u: "second", |
| // 3u: "third", |
| // } |
| // |
| // The expression under test is: |
| // <obj>.a[-1][<mp>[b]] |
| // |
| // This is equivalent to: |
| // <obj>.a[-1]["second"] -> 2u |
| obj := NewConstValue(1, reg.NativeToValue(data)) |
| mp := NewConstValue(1, reg.NativeToValue(map[uint32]any{ |
| 1: "first", |
| 2: "second", |
| 3: "third", |
| })) |
| relAttr := attrs.RelativeAttribute(4, mp) |
| qualB := makeQualifier(t, attrs, nil, 5, attrs.AbsoluteAttribute(5, "b")) |
| relAttr.AddQualifier(qualB) |
| attr := attrs.RelativeAttribute(1, obj) |
| qualA := makeQualifier(t, attrs, nil, 2, "a") |
| qualNeg1 := makeQualifier(t, attrs, nil, 3, int64(-1)) |
| attr.AddQualifier(qualA) |
| attr.AddQualifier(qualNeg1) |
| attr.AddQualifier(relAttr) |
| |
| out, err := attr.Resolve(vars) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if out != types.Uint(2) { |
| t.Errorf("Got %v (%T), wanted 2", out, out) |
| } |
| } |
| |
| func TestAttributesOneofAttr(t *testing.T) { |
| reg := newTestRegistry(t) |
| cont, err := containers.NewContainer(containers.Name("acme.ns")) |
| if err != nil { |
| t.Fatal(err) |
| } |
| attrs := NewAttributeFactory(cont, reg, reg) |
| data := map[string]any{ |
| "a": map[string]any{ |
| "b": []int32{2, 42}, |
| }, |
| "acme.a.b": 1, |
| "acme.ns.a.b": "found", |
| } |
| vars, _ := NewActivation(data) |
| |
| // a.b -> should resolve to acme.ns.a.b per namespace resolution rules. |
| attr := attrs.MaybeAttribute(1, "a") |
| qualB := makeQualifier(t, attrs, nil, 2, "b") |
| attr.AddQualifier(qualB) |
| out, err := attr.Resolve(vars) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if out != "found" { |
| t.Errorf("Got %v, wanted 'found'", out) |
| } |
| } |
| |
| func TestAttributesConditionalAttrTrueBranch(t *testing.T) { |
| reg := newTestRegistry(t) |
| attrs := NewAttributeFactory(containers.DefaultContainer, reg, reg) |
| data := map[string]any{ |
| "a": map[int]any{ |
| -1: []int32{2, 42}, |
| }, |
| "b": map[string]any{ |
| "c": map[int32]any{ |
| -1: []uint{2, 42}, |
| }, |
| }, |
| } |
| vars, _ := NewActivation(data) |
| |
| // (true ? a : b.c)[-1][1] |
| tv := attrs.AbsoluteAttribute(2, "a") |
| fv := attrs.MaybeAttribute(3, "b") |
| qualC := makeQualifier(t, attrs, nil, 4, "c") |
| fv.AddQualifier(qualC) |
| cond := attrs.ConditionalAttribute(1, NewConstValue(0, types.True), tv, fv) |
| qualNeg1 := makeQualifier(t, attrs, nil, 5, int64(-1)) |
| qual1 := makeQualifier(t, attrs, nil, 6, int64(1)) |
| cond.AddQualifier(qualNeg1) |
| cond.AddQualifier(qual1) |
| out, err := cond.Resolve(vars) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if out != int32(42) { |
| t.Errorf("Got %v (%T), wanted 42", out, out) |
| } |
| } |
| |
| func TestAttributesConditionalAttrFalseBranch(t *testing.T) { |
| reg := newTestRegistry(t) |
| attrs := NewAttributeFactory(containers.DefaultContainer, reg, reg) |
| data := map[string]any{ |
| "a": map[int]any{ |
| -1: []int32{2, 42}, |
| }, |
| "b": map[string]any{ |
| "c": map[int32]any{ |
| -1: []uint{2, 42}, |
| }, |
| }, |
| } |
| vars, _ := NewActivation(data) |
| |
| // (false ? a : b.c)[-1][1] |
| tv := attrs.AbsoluteAttribute(2, "a") |
| fv := attrs.MaybeAttribute(3, "b") |
| qualC := makeQualifier(t, attrs, nil, 4, "c") |
| fv.AddQualifier(qualC) |
| cond := attrs.ConditionalAttribute(1, NewConstValue(0, types.False), tv, fv) |
| qualNeg1 := makeQualifier(t, attrs, nil, 5, int64(-1)) |
| qual1 := makeQualifier(t, attrs, nil, 6, int64(1)) |
| cond.AddQualifier(qualNeg1) |
| cond.AddQualifier(qual1) |
| out, err := cond.Resolve(vars) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if out != uint(42) { |
| t.Errorf("Got %v (%T), wanted 42", out, out) |
| } |
| } |
| |
| func TestAttributesOptional(t *testing.T) { |
| reg := newTestRegistry(t, types.ProtoTypeDefs(&proto3pb.TestAllTypes{})) |
| cont, err := containers.NewContainer(containers.Name("ns")) |
| if err != nil { |
| t.Fatalf("") |
| } |
| attrs := NewAttributeFactory(cont, reg, reg) |
| tests := []struct { |
| varName string |
| quals []any |
| optQuals []any |
| vars map[string]any |
| out any |
| err error |
| }{ |
| { |
| // a.?b[0][false] |
| varName: "a", |
| optQuals: []any{"b", int32(0), false}, |
| vars: map[string]any{ |
| "a": map[string]any{ |
| "b": map[int]any{ |
| 0: map[bool]string{ |
| false: "success", |
| }, |
| }, |
| }, |
| }, |
| out: types.OptionalOf(reg.NativeToValue("success")), |
| }, |
| { |
| // a.?b[0][false] |
| varName: "a", |
| optQuals: []any{"b", uint32(0), false}, |
| vars: map[string]any{ |
| "a": map[string]any{ |
| "b": map[int]any{ |
| 0: map[bool]string{ |
| false: "success", |
| }, |
| }, |
| }, |
| }, |
| out: types.OptionalOf(reg.NativeToValue("success")), |
| }, |
| { |
| // a.?b[0][false] |
| varName: "a", |
| optQuals: []any{"b", float32(0), false}, |
| vars: map[string]any{ |
| "a": map[string]any{ |
| "b": map[int]any{ |
| 0: map[bool]string{ |
| false: "success", |
| }, |
| }, |
| }, |
| }, |
| out: types.OptionalOf(reg.NativeToValue("success")), |
| }, |
| { |
| // a.?b[1] with no value |
| varName: "a", |
| optQuals: []any{"b", uint(1)}, |
| vars: map[string]any{ |
| "a": map[string]any{ |
| "b": map[uint]any{}, |
| }, |
| }, |
| out: types.OptionalNone, |
| }, |
| { |
| // a.b[1] with no value where b is a map[uint] |
| varName: "a", |
| quals: []any{"b", uint(1)}, |
| vars: map[string]any{ |
| "a": map[string]any{ |
| "b": map[uint]any{}, |
| }, |
| }, |
| err: errors.New("no such key: 1"), |
| }, |
| { |
| // a.b[?1] with no value where 'b' is a []int |
| varName: "a", |
| quals: []any{"b"}, |
| optQuals: []any{1}, |
| vars: map[string]any{ |
| "a": map[string]any{ |
| "b": []int{}, |
| }, |
| }, |
| out: types.OptionalNone, |
| }, |
| { |
| // a.b[1] with no value where 'b' is a map[int]any |
| varName: "a", |
| quals: []any{"b", 1}, |
| vars: map[string]any{ |
| "a": map[string]any{ |
| "b": map[int]any{}, |
| }, |
| }, |
| err: errors.New("no such key: 1"), |
| }, |
| { |
| // a.b[?1] with no value where 'b' is a []int |
| varName: "a", |
| quals: []any{"b", 1, false}, |
| optQuals: []any{}, |
| vars: map[string]any{ |
| "a": map[string]any{ |
| "b": []int{}, |
| }, |
| }, |
| err: errors.New("index out of bounds: 1"), |
| }, |
| { |
| // a.?b[0][true] with no value |
| varName: "a", |
| optQuals: []any{"b", 0, false}, |
| vars: map[string]any{ |
| "a": map[string]any{ |
| "b": map[int]any{ |
| 0: map[bool]any{}, |
| }, |
| }, |
| }, |
| out: types.OptionalNone, |
| }, |
| { |
| // a.b[0][?true] with no value |
| varName: "a", |
| quals: []any{"b", 0}, |
| optQuals: []any{true}, |
| vars: map[string]any{ |
| "a": map[string]any{ |
| "b": map[int]any{ |
| 0: map[bool]any{}, |
| }, |
| }, |
| }, |
| out: types.OptionalNone, |
| }, |
| { |
| // a.b[0][true] with no value |
| varName: "a", |
| quals: []any{"b", 0, true}, |
| vars: map[string]any{ |
| "a": map[string]any{ |
| "b": map[int]any{ |
| 0: map[bool]any{}, |
| }, |
| }, |
| }, |
| err: errors.New("no such key: true"), |
| }, |
| { |
| // a.b[0][false] where 'a' is optional |
| varName: "a", |
| quals: []any{"b", int32(0), false}, |
| vars: map[string]any{ |
| "a": types.OptionalOf(reg.NativeToValue(map[string]any{ |
| "b": map[int]any{ |
| 0: map[bool]string{ |
| false: "success", |
| }, |
| }, |
| })), |
| }, |
| out: types.OptionalOf(reg.NativeToValue("success")), |
| }, |
| { |
| // a.b[0][false] where 'a' is optional none. |
| varName: "a", |
| quals: []any{"b", int32(0), false}, |
| vars: map[string]any{ |
| "a": types.OptionalNone, |
| }, |
| out: types.OptionalNone, |
| }, |
| { |
| // a.?c[1][true] |
| varName: "a", |
| optQuals: []any{"c", int32(1), true}, |
| vars: map[string]any{ |
| "a": map[string]any{}, |
| }, |
| out: types.OptionalNone, |
| }, |
| { |
| // a[?b] where 'b' is dynamically computed. |
| varName: "a", |
| optQuals: []any{attrs.AbsoluteAttribute(0, "b")}, |
| vars: map[string]any{ |
| "a": map[string]any{ |
| "hello": "world", |
| }, |
| "b": "hello", |
| }, |
| out: types.OptionalOf(reg.NativeToValue("world")), |
| }, |
| { |
| // a[?(false ? : b : c.d.e)] |
| varName: "a", |
| optQuals: []any{ |
| attrs.ConditionalAttribute(0, |
| NewConstValue(100, types.False), |
| attrs.AbsoluteAttribute(101, "b"), |
| attrs.MaybeAttribute(102, "c.d.e")), |
| }, |
| vars: map[string]any{ |
| "a": map[string]any{ |
| "hello": "world", |
| "goodbye": "universe", |
| }, |
| "b": "hello", |
| "c.d.e": "goodbye", |
| }, |
| out: types.OptionalOf(reg.NativeToValue("universe")), |
| }, |
| { |
| // a[?c.d.e] |
| varName: "a", |
| optQuals: []any{ |
| attrs.MaybeAttribute(102, "c.d.e"), |
| }, |
| vars: map[string]any{ |
| "a": map[string]any{ |
| "hello": "world", |
| "goodbye": "universe", |
| }, |
| "b": "hello", |
| "c.d.e": "goodbye", |
| }, |
| out: types.OptionalOf(reg.NativeToValue("universe")), |
| }, |
| { |
| // a[c.d.e] where the c.d.e errors |
| varName: "a", |
| quals: []any{ |
| addQualifier(t, attrs.MaybeAttribute(102, "c.d"), makeQualifier(t, attrs, nil, 103, "e")), |
| }, |
| vars: map[string]any{ |
| "a": map[string]any{ |
| "goodbye": "universe", |
| }, |
| "c.d": map[string]any{}, |
| }, |
| err: errors.New("no such key: e"), |
| }, |
| { |
| // a[?c.d.e] where the c.d.e errors |
| varName: "a", |
| optQuals: []any{ |
| addQualifier(t, attrs.MaybeAttribute(102, "c.d"), makeQualifier(t, attrs, nil, 103, "e")), |
| }, |
| vars: map[string]any{ |
| "a": map[string]any{ |
| "goodbye": "universe", |
| }, |
| "c.d": map[string]any{}, |
| }, |
| err: errors.New("no such key: e"), |
| }, |
| { |
| // a.?single_int32 with a value. |
| varName: "a", |
| optQuals: []any{"single_int32"}, |
| vars: map[string]any{ |
| "a": &proto3pb.TestAllTypes{SingleInt32: 1}, |
| }, |
| out: types.OptionalOf(reg.NativeToValue(1)), |
| }, |
| { |
| // a.?single_int32 where the field is not set. |
| varName: "a", |
| optQuals: []any{"single_int32"}, |
| vars: map[string]any{ |
| "a": &proto3pb.TestAllTypes{}, |
| }, |
| out: types.OptionalNone, |
| }, |
| { |
| // a.?single_int32 where the field is set (uses more optimal selection logic) |
| varName: "a", |
| optQuals: []any{ |
| makeOptQualifier(t, |
| attrs, |
| types.NewObjectType("google.expr.proto3.test.TestAllTypes"), |
| 103, |
| "single_int32", |
| ), |
| }, |
| vars: map[string]any{ |
| "a": &proto3pb.TestAllTypes{SingleInt32: 1}, |
| }, |
| out: types.OptionalOf(reg.NativeToValue(1)), |
| }, |
| { |
| // a.c[1][true] |
| varName: "a", |
| quals: []any{"c", int32(1), true}, |
| vars: map[string]any{ |
| "a": map[string]any{}, |
| }, |
| err: errors.New("no such key: c"), |
| }, |
| { |
| // a, no bindings |
| varName: "a", |
| quals: []any{}, |
| vars: map[string]any{}, |
| err: errors.New("no such attribute(s): a"), |
| }, |
| } |
| for i, tst := range tests { |
| tc := tst |
| t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { |
| i := int64(1) |
| attr := attrs.AbsoluteAttribute(i, tc.varName) |
| for _, q := range tc.quals { |
| i++ |
| attr.AddQualifier(makeQualifier(t, attrs, nil, i, q)) |
| } |
| for _, oq := range tc.optQuals { |
| i++ |
| attr.AddQualifier(makeOptQualifier(t, attrs, nil, i, oq)) |
| } |
| vars, err := NewActivation(tc.vars) |
| if err != nil { |
| t.Fatalf("NewActivation() failed: %v", err) |
| } |
| out, err := attr.Resolve(vars) |
| if err != nil { |
| if tc.err != nil { |
| if tc.err.Error() == err.Error() { |
| return |
| } |
| t.Fatalf("attr.Resolve() errored with %v, wanted error %v", err, tc.err) |
| } |
| t.Fatalf("attr.Resolve() failed: %v", err) |
| } |
| if !reflect.DeepEqual(out, tc.out) { |
| t.Errorf("attr.Resolve() got %v, wanted %v", out, tc.out) |
| } |
| }) |
| } |
| } |
| |
| func TestAttributesConditionalAttrErrorUnknown(t *testing.T) { |
| reg := newTestRegistry(t) |
| attrs := NewAttributeFactory(containers.DefaultContainer, reg, reg) |
| |
| // err ? a : b |
| tv := attrs.AbsoluteAttribute(2, "a") |
| fv := attrs.MaybeAttribute(3, "b") |
| cond := attrs.ConditionalAttribute(1, NewConstValue(0, types.NewErr("test error")), tv, fv) |
| out, err := cond.Resolve(EmptyActivation()) |
| if err == nil { |
| t.Errorf("Got %v, wanted error", out) |
| } |
| |
| // unk ? a : b |
| condUnk := attrs.ConditionalAttribute(1, NewConstValue(0, types.NewUnknown(1, nil)), tv, fv) |
| out, err = condUnk.Resolve(EmptyActivation()) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if !types.IsUnknown(out.(ref.Val)) { |
| t.Errorf("Got %v, wanted unknown", out) |
| } |
| } |
| |
| func BenchmarkResolverFieldQualifier(b *testing.B) { |
| msg := &proto3pb.TestAllTypes{ |
| NestedType: &proto3pb.TestAllTypes_SingleNestedMessage{ |
| SingleNestedMessage: &proto3pb.TestAllTypes_NestedMessage{ |
| Bb: 123, |
| }, |
| }, |
| } |
| reg := newTestRegistry(b, types.ProtoTypeDefs(msg)) |
| attrs := NewAttributeFactory(containers.DefaultContainer, reg, reg) |
| vars, _ := NewActivation(map[string]any{ |
| "msg": msg, |
| }) |
| attr := attrs.AbsoluteAttribute(1, "msg") |
| opType, found := reg.FindType("google.expr.proto3.test.TestAllTypes") |
| if !found { |
| b.Fatal("FindType() could not find TestAllTypes") |
| } |
| fieldType, found := reg.FindType("google.expr.proto3.test.TestAllTypes.NestedMessage") |
| if !found { |
| b.Fatal("FindType() could not find NestedMessage") |
| } |
| attr.AddQualifier(makeQualifier(b, attrs, testExprTypeToType(b, opType), 2, "single_nested_message")) |
| attr.AddQualifier(makeQualifier(b, attrs, testExprTypeToType(b, fieldType), 3, "bb")) |
| b.ResetTimer() |
| for i := 0; i < b.N; i++ { |
| _, err := attr.Resolve(vars) |
| if err != nil { |
| b.Fatal(err) |
| } |
| } |
| } |
| |
| func TestResolverCustomQualifier(t *testing.T) { |
| reg := newTestRegistry(t) |
| attrs := &custAttrFactory{ |
| AttributeFactory: NewAttributeFactory(containers.DefaultContainer, reg, reg), |
| } |
| msg := &proto3pb.TestAllTypes_NestedMessage{ |
| Bb: 123, |
| } |
| vars, _ := NewActivation(map[string]any{ |
| "msg": msg, |
| }) |
| attr := attrs.AbsoluteAttribute(1, "msg") |
| fieldType := types.NewObjectType("google.expr.proto3.test.TestAllTypes.NestedMessage") |
| qualBB := makeQualifier(t, attrs, fieldType, 2, "bb") |
| attr.AddQualifier(qualBB) |
| out, err := attr.Resolve(vars) |
| if err != nil { |
| t.Error(err) |
| } |
| if out != int32(123) { |
| t.Errorf("Got %v, wanted 123", out) |
| } |
| } |
| |
| func TestAttributesMissingMsg(t *testing.T) { |
| reg := newTestRegistry(t) |
| attrs := NewAttributeFactory(containers.DefaultContainer, reg, reg) |
| anyPB, _ := anypb.New(&proto3pb.TestAllTypes{}) |
| vars, _ := NewActivation(map[string]any{ |
| "missing_msg": anyPB, |
| }) |
| |
| // missing_msg.field |
| attr := attrs.AbsoluteAttribute(1, "missing_msg") |
| field := makeQualifier(t, attrs, nil, 2, "field") |
| attr.AddQualifier(field) |
| out, err := attr.Resolve(vars) |
| if err == nil { |
| t.Fatalf("got %v, wanted error", out) |
| } |
| if err.Error() != "unknown type: 'google.expr.proto3.test.TestAllTypes'" { |
| t.Fatalf("got %v, wanted unknown type: 'google.expr.proto3.test.TestAllTypes'", err) |
| } |
| } |
| |
| func TestAttributeMissingMsgUnknownField(t *testing.T) { |
| reg := newTestRegistry(t) |
| attrs := NewPartialAttributeFactory(containers.DefaultContainer, reg, reg) |
| anyPB, _ := anypb.New(&proto3pb.TestAllTypes{}) |
| vars, _ := NewPartialActivation(map[string]any{ |
| "missing_msg": anyPB, |
| }, NewAttributePattern("missing_msg").QualString("field")) |
| |
| // missing_msg.field |
| attr := attrs.AbsoluteAttribute(1, "missing_msg") |
| field := makeQualifier(t, attrs, nil, 2, "field") |
| attr.AddQualifier(field) |
| out, err := attr.Resolve(vars) |
| if err != nil { |
| t.Fatal(err) |
| } |
| _, isUnk := out.(*types.Unknown) |
| if !isUnk { |
| t.Errorf("got %v, wanted unknown value", out) |
| } |
| } |
| |
| func TestAttributeStateTracking(t *testing.T) { |
| var tests = []struct { |
| expr string |
| vars []*decls.VariableDecl |
| in any |
| out ref.Val |
| attrs []*AttributePattern |
| state map[int64]any |
| }{ |
| { |
| expr: `[{"field": true}][0].field`, |
| vars: []*decls.VariableDecl{}, |
| in: map[string]any{}, |
| out: types.True, |
| state: map[int64]any{ |
| // [{"field": true}] |
| 1: []ref.Val{types.DefaultTypeAdapter.NativeToValue(map[ref.Val]ref.Val{types.String("field"): types.True})}, |
| // [{"field": true}][0] |
| 6: map[ref.Val]ref.Val{types.String("field"): types.True}, |
| // [{"field": true}][0].field |
| 8: true, |
| }, |
| }, |
| { |
| expr: `a[1]['two']`, |
| vars: []*decls.VariableDecl{ |
| decls.NewVariable("a", types.NewMapType( |
| types.IntType, |
| types.NewMapType(types.StringType, types.BoolType))), |
| }, |
| in: map[string]any{ |
| "a": map[int64]any{ |
| 1: map[string]bool{ |
| "two": true, |
| }, |
| }, |
| }, |
| out: types.True, |
| state: map[int64]any{ |
| // a[1] |
| 2: map[string]bool{"two": true}, |
| // a[1]["two"] |
| 4: true, |
| }, |
| }, |
| { |
| expr: `a[1][2][3]`, |
| vars: []*decls.VariableDecl{ |
| decls.NewVariable("a", types.NewMapType( |
| types.IntType, |
| types.NewMapType(types.DynType, types.DynType))), |
| }, |
| in: map[string]any{ |
| "a": map[int64]any{ |
| 1: map[int64]any{ |
| 1: 0, |
| 2: []string{"index", "middex", "outdex", "dex"}, |
| }, |
| }, |
| }, |
| out: types.String("dex"), |
| state: map[int64]any{ |
| // a[1] |
| 2: map[int64]any{ |
| 1: 0, |
| 2: []string{"index", "middex", "outdex", "dex"}, |
| }, |
| // a[1][2] |
| 4: []string{"index", "middex", "outdex", "dex"}, |
| // a[1][2][3] |
| 6: "dex", |
| }, |
| }, |
| { |
| expr: `a[1][2][a[1][1]]`, |
| vars: []*decls.VariableDecl{ |
| decls.NewVariable("a", types.NewMapType( |
| types.IntType, |
| types.NewMapType(types.DynType, types.DynType))), |
| }, |
| in: map[string]any{ |
| "a": map[int64]any{ |
| 1: map[int64]any{ |
| 1: 0, |
| 2: []string{"index", "middex", "outdex", "dex"}, |
| }, |
| }, |
| }, |
| out: types.String("index"), |
| state: map[int64]any{ |
| // a[1] |
| 2: map[int64]any{ |
| 1: 0, |
| 2: []string{"index", "middex", "outdex", "dex"}, |
| }, |
| // a[1][2] |
| 4: []string{"index", "middex", "outdex", "dex"}, |
| // a[1][2][a[1][1]] |
| 6: "index", |
| // dynamic index into a[1][2] |
| // a[1] |
| 8: map[int64]any{ |
| 1: 0, |
| 2: []string{"index", "middex", "outdex", "dex"}, |
| }, |
| // a[1][1] |
| 10: int64(0), |
| }, |
| }, |
| { |
| expr: `true ? a : b`, |
| vars: []*decls.VariableDecl{ |
| decls.NewVariable("a", types.StringType), |
| decls.NewVariable("b", types.StringType), |
| }, |
| in: map[string]any{ |
| "a": "hello", |
| "b": "world", |
| }, |
| out: types.String("hello"), |
| state: map[int64]any{ |
| // 'hello' |
| 2: types.String("hello"), |
| }, |
| }, |
| { |
| expr: `(a.size() != 0 ? a : b)[0]`, |
| vars: []*decls.VariableDecl{ |
| decls.NewVariable("a", types.NewListType(types.StringType)), |
| decls.NewVariable("b", types.NewListType(types.StringType)), |
| }, |
| in: map[string]any{ |
| "a": []string{"hello", "world"}, |
| "b": []string{"world", "hello"}, |
| }, |
| out: types.String("hello"), |
| state: map[int64]any{ |
| // ["hello", "world"] |
| 1: types.DefaultTypeAdapter.NativeToValue([]string{"hello", "world"}), |
| // ["hello", "world"].size() // 2 |
| 2: types.Int(2), |
| // ["hello", "world"].size() != 0 |
| 3: types.True, |
| // constant 0 |
| 4: types.IntZero, |
| // 'hello' |
| 8: types.String("hello"), |
| }, |
| }, |
| { |
| expr: `a.?b.c`, |
| vars: []*decls.VariableDecl{ |
| decls.NewVariable("a", types.NewMapType(types.StringType, types.NewMapType(types.StringType, types.StringType))), |
| }, |
| in: map[string]any{ |
| "a": map[string]any{"b": map[string]any{"c": "world"}}, |
| }, |
| out: types.OptionalOf(types.String("world")), |
| state: map[int64]any{ |
| // {c: world} |
| 3: types.DefaultTypeAdapter.NativeToValue(map[string]string{"c": "world"}), |
| // 'world' |
| 4: types.OptionalOf(types.String("world")), |
| }, |
| }, |
| { |
| expr: `a.?b.c`, |
| vars: []*decls.VariableDecl{ |
| decls.NewVariable("a", types.NewMapType(types.StringType, types.NewMapType(types.StringType, types.StringType))), |
| }, |
| in: map[string]any{ |
| "a": map[string]any{"b": map[string]string{"random": "value"}}, |
| }, |
| out: types.OptionalNone, |
| state: map[int64]any{ |
| // {random: value} |
| 3: types.DefaultTypeAdapter.NativeToValue(map[string]string{"random": "value"}), |
| // optional.none() |
| 4: types.OptionalNone, |
| }, |
| }, |
| { |
| expr: `a.b.c`, |
| vars: []*decls.VariableDecl{ |
| decls.NewVariable("a", types.NewMapType(types.StringType, types.NewMapType(types.StringType, types.StringType))), |
| }, |
| in: map[string]any{ |
| "a": map[string]any{"b": map[string]any{"c": "world"}}, |
| }, |
| out: types.String("world"), |
| state: map[int64]any{ |
| // {c: world} |
| 2: types.DefaultTypeAdapter.NativeToValue(map[string]string{"c": "world"}), |
| // 'world' |
| 3: types.String("world"), |
| }, |
| }, |
| { |
| expr: `m[has(a.b)]`, |
| vars: []*decls.VariableDecl{ |
| decls.NewVariable("a", types.NewMapType(types.StringType, types.StringType)), |
| decls.NewVariable("m", types.NewMapType(types.BoolType, types.StringType)), |
| }, |
| in: map[string]any{ |
| "a": map[string]string{"b": ""}, |
| "m": map[bool]string{true: "world"}, |
| }, |
| out: types.String("world"), |
| }, |
| { |
| expr: `m[?has(a.b)]`, |
| vars: []*decls.VariableDecl{ |
| decls.NewVariable("a", types.NewMapType(types.StringType, types.StringType)), |
| decls.NewVariable("m", types.NewMapType(types.BoolType, types.StringType)), |
| }, |
| in: map[string]any{ |
| "a": map[string]string{"b": ""}, |
| "m": map[bool]string{true: "world"}, |
| }, |
| out: types.OptionalOf(types.String("world")), |
| }, |
| { |
| expr: `m[?has(a.b.c)]`, |
| vars: []*decls.VariableDecl{ |
| decls.NewVariable("a", types.NewMapType(types.StringType, types.DynType)), |
| decls.NewVariable("m", types.NewMapType(types.BoolType, types.StringType)), |
| }, |
| in: partialActivation( |
| map[string]any{ |
| "a": map[string]any{}, |
| "m": map[bool]string{true: "world"}, |
| }, |
| NewAttributePattern("a").QualString("b"), |
| ), |
| out: types.NewUnknown(5, types.QualifyAttribute[string](types.NewAttributeTrail("a"), "b")), |
| }, |
| { |
| expr: `['a', b.val, 'c'].filter(i, i + 'b' != 'ab')`, |
| vars: []*decls.VariableDecl{ |
| decls.NewVariable("b", types.NewMapType(types.StringType, types.DynType)), |
| }, |
| in: partialActivation( |
| map[string]any{ |
| "b": map[string]any{ |
| "val": 1, |
| }, |
| }, |
| ), |
| // Error node 9 corresponds to the `i + 'b'` expression |
| out: types.LabelErrNode(9, types.NoSuchOverloadErr()), |
| }, |
| } |
| for _, test := range tests { |
| tc := test |
| t.Run(tc.expr, func(t *testing.T) { |
| src := common.NewTextSource(tc.expr) |
| p, err := parser.NewParser( |
| parser.EnableOptionalSyntax(true), |
| parser.Macros(parser.AllMacros...), |
| ) |
| if err != nil { |
| t.Fatalf("parser.NewParser() failed: %v", err) |
| } |
| parsed, errors := p.Parse(src) |
| if len(errors.GetErrors()) != 0 { |
| t.Fatal(errors.ToDisplayString()) |
| } |
| cont := containers.DefaultContainer |
| reg := newTestRegistry(t) |
| env, err := checker.NewEnv(cont, reg) |
| if err != nil { |
| t.Fatalf("checker.NewEnv() failed: %v", err) |
| } |
| env.AddFunctions(stdlib.Functions()...) |
| env.AddFunctions(optionalDecls(t)...) |
| if tc.vars != nil { |
| env.AddIdents(tc.vars...) |
| } |
| checked, errors := checker.Check(parsed, src, env) |
| if len(errors.GetErrors()) != 0 { |
| t.Fatal(errors.ToDisplayString()) |
| } |
| in, err := NewActivation(tc.in) |
| if err != nil { |
| t.Fatalf("NewActivation(%v) failed: %v", tc.in, err) |
| } |
| var attrs AttributeFactory |
| _, isPartial := in.(PartialActivation) |
| if isPartial { |
| attrs = NewPartialAttributeFactory(cont, reg, reg) |
| } else { |
| attrs = NewAttributeFactory(cont, reg, reg) |
| } |
| interp := newStandardInterpreter(t, cont, reg, reg, attrs) |
| // Show that program planning will now produce an error. |
| type stateHolder struct { |
| st EvalState |
| } |
| holder := stateHolder{} |
| i, err := interp.NewInterpretable(checked, Optimize(), |
| EvalStateObserver(EvalStateFactory(func() EvalState { |
| holder.st = NewEvalState() |
| return holder.st |
| }))) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if err != nil { |
| t.Fatal(err) |
| } |
| out := i.Eval(in) |
| if types.IsUnknown(tc.out) && types.IsUnknown(out) { |
| if !reflect.DeepEqual(tc.out, out) { |
| t.Errorf("got %v, wanted %v", out, tc.out) |
| } |
| } else if types.IsError(tc.out) && types.IsError(out) { |
| if tc.out.(*types.Err).Error() != out.(*types.Err).Error() { |
| t.Errorf("got %v, wanted %v", out, tc.out) |
| } |
| } else if tc.out.Equal(out) != types.True { |
| t.Errorf("got %v, wanted %v", out, tc.out) |
| } |
| if err, isErr := out.(*types.Err); isErr && err.NodeID() != 0 { |
| wantErr := tc.out.(*types.Err) |
| if err.NodeID() != wantErr.NodeID() { |
| t.Errorf("err.NodeID() got %d, wanted %d", err.NodeID(), wantErr.NodeID()) |
| } |
| } |
| for id, val := range tc.state { |
| stVal, found := holder.st.Value(id) |
| if !found { |
| for _, id := range holder.st.IDs() { |
| v, _ := holder.st.Value(id) |
| t.Error(id, v) |
| } |
| t.Errorf("state not found for %d=%v", id, val) |
| continue |
| } |
| wantStVal := types.DefaultTypeAdapter.NativeToValue(val) |
| if wantStVal.Equal(stVal) != types.True { |
| t.Errorf("got %v, wanted %v for id: %d", stVal.Value(), val, id) |
| } |
| } |
| }) |
| } |
| } |
| |
| func BenchmarkResolverCustomQualifier(b *testing.B) { |
| reg := newTestRegistry(b) |
| attrs := &custAttrFactory{ |
| AttributeFactory: NewAttributeFactory(containers.DefaultContainer, reg, reg), |
| } |
| msg := &proto3pb.TestAllTypes_NestedMessage{ |
| Bb: 123, |
| } |
| vars, _ := NewActivation(map[string]any{ |
| "msg": msg, |
| }) |
| attr := attrs.AbsoluteAttribute(1, "msg") |
| fieldType := types.NewObjectType("google.expr.proto3.test.TestAllTypes.NestedMessage") |
| qualBB := makeQualifier(b, attrs, fieldType, 2, "bb") |
| attr.AddQualifier(qualBB) |
| for i := 0; i < b.N; i++ { |
| attr.Resolve(vars) |
| } |
| } |
| |
| type custAttrFactory struct { |
| AttributeFactory |
| } |
| |
| func (r *custAttrFactory) NewQualifier(objType *types.Type, qualID int64, val any, opt bool) (Qualifier, error) { |
| if objType.Kind() == types.StructKind && objType.TypeName() == "google.expr.proto3.test.TestAllTypes.NestedMessage" { |
| switch v := val.(type) { |
| case string: |
| return &nestedMsgQualifier{id: qualID, field: v, opt: opt}, nil |
| case types.String: |
| return &nestedMsgQualifier{id: qualID, field: string(v), opt: opt}, nil |
| } |
| } |
| return r.AttributeFactory.NewQualifier(objType, qualID, val, opt) |
| } |
| |
| type nestedMsgQualifier struct { |
| id int64 |
| field string |
| opt bool |
| } |
| |
| func (q *nestedMsgQualifier) ID() int64 { |
| return q.id |
| } |
| |
| func (q *nestedMsgQualifier) IsOptional() bool { |
| return q.opt |
| } |
| |
| func (q *nestedMsgQualifier) Qualify(vars Activation, obj any) (any, error) { |
| pb := obj.(*proto3pb.TestAllTypes_NestedMessage) |
| return pb.GetBb(), nil |
| } |
| |
| func (q *nestedMsgQualifier) QualifyIfPresent(vars Activation, obj any, presenceOnly bool) (any, bool, error) { |
| pb := obj.(*proto3pb.TestAllTypes_NestedMessage) |
| if pb.GetBb() == 0 { |
| return nil, false, nil |
| } |
| return pb.GetBb(), true, nil |
| } |
| |
| func addQualifier(t testing.TB, attr Attribute, qual Qualifier) Attribute { |
| t.Helper() |
| _, err := attr.AddQualifier(qual) |
| if err != nil { |
| t.Fatalf("attr.AddQualifier(%v) failed: %v", qual, err) |
| } |
| return attr |
| } |
| |
| func makeQualifier(t testing.TB, attrs AttributeFactory, fieldType *types.Type, qualID int64, val any) Qualifier { |
| t.Helper() |
| qual, err := attrs.NewQualifier(fieldType, qualID, val, false) |
| if err != nil { |
| t.Fatalf("attrs.NewQualifier() failed: %v", err) |
| } |
| return qual |
| } |
| |
| func makeOptQualifier(t testing.TB, attrs AttributeFactory, fieldType *types.Type, qualID int64, val any) Qualifier { |
| t.Helper() |
| qual, err := attrs.NewQualifier(fieldType, qualID, val, true) |
| if err != nil { |
| t.Fatalf("attrs.NewQualifier() failed: %v", err) |
| } |
| return qual |
| } |
| |
| func findField(t testing.TB, reg ref.TypeRegistry, typeName, field string) *ref.FieldType { |
| t.Helper() |
| ft, found := reg.FindFieldType(typeName, field) |
| if !found { |
| t.Fatalf("reg.FindFieldType(%v, %v) failed", typeName, field) |
| } |
| return ft |
| } |
| |
| func testExprTypeToType(t testing.TB, fieldType *exprpb.Type) *types.Type { |
| t.Helper() |
| ft, err := types.ExprTypeToType(fieldType) |
| if err != nil { |
| t.Fatalf("types.ExprTypeToType() failed: %v", err) |
| } |
| return ft |
| } |