| // Copyright 2020 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 ( |
| "fmt" |
| "testing" |
| |
| "github.com/google/cel-go/common/containers" |
| "github.com/google/cel-go/common/types" |
| ) |
| |
| // attr describes a simplified format for specifying common Attribute and Qualifier values for |
| // use in pattern matching tests. |
| type attr struct { |
| // unchecked indicates whether the attribute has not been type-checked and thus not gone |
| // the variable and function resolution step. |
| unchecked bool |
| // container simulates the expression container and is only relevant on 'unchecked' test inputs |
| // as the container is used to resolve the potential fully qualified variable names represented |
| // by an identifier or select expression. |
| container string |
| // variable name, fully qualified unless the attr is marked as unchecked=true |
| name string |
| // quals contains a list of static qualifiers. |
| quals []any |
| } |
| |
| // patternTest describes a pattern, and a set of matches and misses for the pattern to highlight |
| // what the pattern will and will not match. |
| type patternTest struct { |
| pattern *AttributePattern |
| matches []attr |
| misses []attr |
| } |
| |
| var patternTests = map[string]patternTest{ |
| "var": { |
| pattern: NewAttributePattern("var"), |
| matches: []attr{ |
| {name: "var"}, |
| {name: "var", quals: []any{"field"}}, |
| }, |
| misses: []attr{ |
| {name: "ns.var"}, |
| }, |
| }, |
| "var_namespace": { |
| pattern: NewAttributePattern("ns.app.var"), |
| matches: []attr{ |
| {name: "ns.app.var"}, |
| {name: "ns.app.var", quals: []any{int64(0)}}, |
| { |
| name: "ns", |
| quals: []any{"app", "var", "foo"}, |
| container: "ns.app", |
| unchecked: true, |
| }, |
| }, |
| misses: []attr{ |
| {name: "ns.var"}, |
| { |
| name: "ns", |
| quals: []any{"var"}, |
| container: "ns.app", |
| unchecked: true, |
| }, |
| }, |
| }, |
| "var_field": { |
| pattern: NewAttributePattern("var").QualString("field"), |
| matches: []attr{ |
| {name: "var"}, |
| {name: "var", quals: []any{"field"}}, |
| {name: "var", quals: []any{"field"}, unchecked: true}, |
| {name: "var", quals: []any{"field", uint64(1)}}, |
| }, |
| misses: []attr{ |
| {name: "var", quals: []any{"other"}}, |
| }, |
| }, |
| "var_index": { |
| pattern: NewAttributePattern("var").QualInt(0), |
| matches: []attr{ |
| {name: "var"}, |
| {name: "var", quals: []any{int64(0)}}, |
| {name: "var", quals: []any{float64(0)}}, |
| {name: "var", quals: []any{int64(0), false}}, |
| {name: "var", quals: []any{uint64(0)}}, |
| }, |
| misses: []attr{ |
| {name: "var", quals: []any{int64(1), false}}, |
| }, |
| }, |
| "var_index_uint": { |
| pattern: NewAttributePattern("var").QualUint(1), |
| matches: []attr{ |
| {name: "var"}, |
| {name: "var", quals: []any{uint64(1)}}, |
| {name: "var", quals: []any{uint64(1), true}}, |
| {name: "var", quals: []any{int64(1), false}}, |
| }, |
| misses: []attr{ |
| {name: "var", quals: []any{uint64(0)}}, |
| }, |
| }, |
| "var_index_bool": { |
| pattern: NewAttributePattern("var").QualBool(true), |
| matches: []attr{ |
| {name: "var"}, |
| {name: "var", quals: []any{true}}, |
| {name: "var", quals: []any{true, "name"}}, |
| }, |
| misses: []attr{ |
| {name: "var", quals: []any{false}}, |
| {name: "none"}, |
| }, |
| }, |
| "var_wildcard": { |
| pattern: NewAttributePattern("ns.var").Wildcard(), |
| matches: []attr{ |
| {name: "ns.var"}, |
| // The unchecked attributes consider potential namespacing and field selection |
| // when testing variable names. |
| { |
| name: "var", |
| quals: []any{true}, |
| container: "ns", |
| unchecked: true, |
| }, |
| { |
| name: "var", |
| quals: []any{"name"}, |
| container: "ns", |
| unchecked: true, |
| }, |
| { |
| name: "var", |
| quals: []any{"name"}, |
| container: "ns", |
| unchecked: true, |
| }, |
| }, |
| misses: []attr{ |
| {name: "var", quals: []any{false}}, |
| {name: "none"}, |
| }, |
| }, |
| "var_wildcard_field": { |
| pattern: NewAttributePattern("var").Wildcard().QualString("field"), |
| matches: []attr{ |
| {name: "var"}, |
| {name: "var", quals: []any{true}}, |
| {name: "var", quals: []any{int64(10), "field"}}, |
| }, |
| misses: []attr{ |
| {name: "var", quals: []any{int64(10), "other"}}, |
| }, |
| }, |
| "var_wildcard_wildcard": { |
| pattern: NewAttributePattern("var").Wildcard().Wildcard(), |
| matches: []attr{ |
| {name: "var"}, |
| {name: "var", quals: []any{true}}, |
| {name: "var", quals: []any{int64(10), "field"}}, |
| }, |
| misses: []attr{ |
| {name: "none"}, |
| }, |
| }, |
| } |
| |
| func TestAttributePattern_UnknownResolution(t *testing.T) { |
| reg := newTestRegistry(t) |
| for nm, tc := range patternTests { |
| tst := tc |
| t.Run(nm, func(t *testing.T) { |
| for i, match := range tst.matches { |
| m := match |
| t.Run(fmt.Sprintf("match[%d]", i), func(t *testing.T) { |
| var err error |
| cont := containers.DefaultContainer |
| if m.unchecked { |
| cont, err = containers.NewContainer(containers.Name(m.container)) |
| if err != nil { |
| t.Fatal(err) |
| } |
| } |
| fac := NewPartialAttributeFactory(cont, reg, reg) |
| attr := genAttr(fac, m) |
| partVars, _ := NewPartialActivation(EmptyActivation(), tst.pattern) |
| val, err := attr.Resolve(partVars) |
| if err != nil { |
| t.Fatalf("Got error: %s, wanted unknown", err) |
| } |
| _, isUnk := val.(*types.Unknown) |
| if !isUnk { |
| t.Fatalf("Got value %v, wanted unknown", val) |
| } |
| }) |
| } |
| for i, miss := range tst.misses { |
| m := miss |
| t.Run(fmt.Sprintf("miss[%d]", i), func(t *testing.T) { |
| cont := containers.DefaultContainer |
| if m.unchecked { |
| var err error |
| cont, err = containers.NewContainer(containers.Name(m.container)) |
| if err != nil { |
| t.Fatal(err) |
| } |
| } |
| fac := NewPartialAttributeFactory(cont, reg, reg) |
| attr := genAttr(fac, m) |
| partVars, _ := NewPartialActivation(EmptyActivation(), tst.pattern) |
| val, err := attr.Resolve(partVars) |
| if err == nil { |
| t.Fatalf("Got value: %s, wanted error", val) |
| } |
| }) |
| } |
| }) |
| } |
| } |
| |
| func TestAttributePattern_CrossReference(t *testing.T) { |
| reg := newTestRegistry(t) |
| fac := NewPartialAttributeFactory(containers.DefaultContainer, reg, reg) |
| a := fac.AbsoluteAttribute(1, "a") |
| b := fac.AbsoluteAttribute(2, "b") |
| a.AddQualifier(b) |
| |
| // Ensure that var a[b], the dynamic index into var 'a' is the unknown value |
| // returned from attribute resolution. |
| partVars, _ := NewPartialActivation( |
| map[string]any{"a": []int64{1, 2}}, |
| NewAttributePattern("b")) |
| val, err := a.Resolve(partVars) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if !types.NewUnknown(2, types.NewAttributeTrail("b")).Contains(val.(*types.Unknown)) { |
| t.Errorf("Got %v, wanted unknown attribute id for 'b' (2)", val) |
| } |
| |
| // Ensure that a[b], the dynamic index into var 'a' is the unknown value |
| // returned from attribute resolution. Note, both 'a' and 'b' have unknown attribute |
| // patterns specified. This changes the evaluation behavior slightly, but the end |
| // result is the same. |
| partVars, _ = NewPartialActivation( |
| map[string]any{"a": []int64{1, 2}}, |
| NewAttributePattern("a").QualInt(0), |
| NewAttributePattern("b")) |
| val, err = a.Resolve(partVars) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if !types.NewUnknown(2, types.NewAttributeTrail("b")).Contains(val.(*types.Unknown)) { |
| t.Errorf("Got %v, wanted unknown attribute id for 'b' (2)", val) |
| } |
| |
| // Note, that only 'a[0].c' will result in an unknown result since both 'a' and 'b' |
| // have values. However, since the attribute being pattern matched is just 'a.b', |
| // the outcome will indicate that 'a[b]' is unknown. |
| partVars, _ = NewPartialActivation( |
| map[string]any{"a": []int64{1, 2}, "b": 0}, |
| NewAttributePattern("a").QualInt(0).QualString("c")) |
| val, err = a.Resolve(partVars) |
| if err != nil { |
| t.Fatal(err) |
| } |
| unkAttr := types.NewAttributeTrail("a") |
| types.QualifyAttribute[int64](unkAttr, 0) |
| wantUnk := types.NewUnknown(2, unkAttr) |
| if !wantUnk.Contains(val.(*types.Unknown)) { |
| t.Errorf("Got %v, wanted unknown attribute id for %v", val, wantUnk) |
| } |
| |
| // Test a positive case that returns a valid value even though the attribugte factory |
| // is the partial attribute factory. |
| partVars, _ = NewPartialActivation( |
| map[string]any{"a": []int64{1, 2}, "b": 0}) |
| val, err = a.Resolve(partVars) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if val != int64(1) { |
| t.Errorf("Got %v, wanted 1 for a[b]", val) |
| } |
| |
| // Ensure the unknown attribute id moves when the attribute becomes more specific. |
| partVars, _ = NewPartialActivation( |
| map[string]any{"a": []int64{1, 2}, "b": 0}, |
| NewAttributePattern("a").QualInt(0).QualString("c")) |
| // Qualify a[b] with 'c', a[b].c |
| c, _ := fac.NewQualifier(nil, 3, "c", false) |
| a.AddQualifier(c) |
| // The resolve step should return unknown |
| val, err = a.Resolve(partVars) |
| if err != nil { |
| t.Fatal(err) |
| } |
| unkAttr = types.NewAttributeTrail("a") |
| types.QualifyAttribute[int64](unkAttr, 0) |
| types.QualifyAttribute[string](unkAttr, "c") |
| wantUnk = types.NewUnknown(3, unkAttr) |
| if !wantUnk.Contains(val.(*types.Unknown)) { |
| t.Errorf("Got %v, wanted unknown attribute id for %v", val, wantUnk) |
| } |
| } |
| |
| func genAttr(fac AttributeFactory, a attr) Attribute { |
| id := int64(1) |
| var attr Attribute |
| if a.unchecked { |
| attr = fac.MaybeAttribute(1, a.name) |
| } else { |
| attr = fac.AbsoluteAttribute(1, a.name) |
| } |
| for _, q := range a.quals { |
| qual, _ := fac.NewQualifier(nil, id, q, false) |
| attr.AddQualifier(qual) |
| id++ |
| } |
| return attr |
| } |
| |
| func TestAttributePattern_LocallyBound(t *testing.T) { |
| reg := newTestRegistry(t) |
| fac := NewPartialAttributeFactory(containers.DefaultContainer, reg, reg) |
| |
| // Create a partial activation that designates "x" and "y" as unknown. |
| partAct, err := NewPartialActivation(EmptyActivation(), NewAttributePattern("x"), NewAttributePattern("y")) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| frame := mustNewExecutionFrame(t, partAct) |
| defer frame.Close() |
| |
| // Create folder (representing a local scope for variable "x" only) |
| fold := &evalFold{ |
| iterVar: "x", |
| adapter: types.DefaultTypeAdapter, |
| } |
| fld := newFolder(fold, frame) |
| defer releaseFolder(fld) |
| |
| // Push onto the frame stack to simulate local variable scope |
| frame1 := frame.Push(fld) |
| defer frame1.Pop() |
| |
| // 1. Resolve attribute matching "x". |
| // Because "x" is locally bound in fld, it should not resolve to types.Unknown. |
| attrX := fac.AbsoluteAttribute(1, "x") |
| valX, err := attrX.Resolve(frame1) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if _, isUnk := valX.(*types.Unknown); isUnk { |
| t.Errorf("Resolve(x) got unknown, wanted value (since x is locally bound)") |
| } |
| |
| // 2. Resolve attribute matching "y". |
| // Because "y" is NOT locally bound in any local scope layer, it should resolve to types.Unknown. |
| attrY := fac.AbsoluteAttribute(2, "y") |
| valY, err := attrY.Resolve(frame1) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if _, isUnk := valY.(*types.Unknown); !isUnk { |
| t.Errorf("Resolve(y) got %v, wanted types.Unknown (since y is not locally bound)", valY) |
| } |
| } |
| |
| func TestQualifierValueEquals(t *testing.T) { |
| tests := []struct { |
| name string |
| qualifier interface { |
| QualifierValueEquals(any) bool |
| } |
| cases []struct { |
| input any |
| want bool |
| } |
| }{ |
| { |
| name: "fieldQualifier", |
| qualifier: &fieldQualifier{Name: "foo"}, |
| cases: []struct { |
| input any |
| want bool |
| }{ |
| {input: "foo", want: true}, |
| {input: "bar", want: false}, |
| {input: 123, want: false}, |
| {input: true, want: false}, |
| {input: nil, want: false}, |
| }, |
| }, |
| { |
| name: "stringQualifier", |
| qualifier: &stringQualifier{value: "hello"}, |
| cases: []struct { |
| input any |
| want bool |
| }{ |
| {input: "hello", want: true}, |
| {input: "world", want: false}, |
| {input: 123, want: false}, |
| {input: nil, want: false}, |
| }, |
| }, |
| { |
| name: "boolQualifier", |
| qualifier: &boolQualifier{value: true}, |
| cases: []struct { |
| input any |
| want bool |
| }{ |
| {input: true, want: true}, |
| {input: false, want: false}, |
| {input: "true", want: false}, |
| {input: 1, want: false}, |
| {input: nil, want: false}, |
| }, |
| }, |
| { |
| name: "intQualifier", |
| qualifier: &intQualifier{celValue: types.Int(42)}, |
| cases: []struct { |
| input any |
| want bool |
| }{ |
| {input: int64(42), want: true}, |
| {input: int32(42), want: true}, |
| {input: uint64(42), want: true}, |
| {input: float64(42.0), want: true}, |
| {input: types.Int(42), want: true}, |
| {input: types.Double(42.0), want: true}, |
| {input: int64(100), want: false}, |
| {input: "42", want: false}, |
| {input: nil, want: false}, |
| }, |
| }, |
| { |
| name: "uintQualifier", |
| qualifier: &uintQualifier{celValue: types.Uint(100)}, |
| cases: []struct { |
| input any |
| want bool |
| }{ |
| {input: uint64(100), want: true}, |
| {input: int64(100), want: true}, |
| {input: float64(100.0), want: true}, |
| {input: types.Uint(100), want: true}, |
| {input: uint64(50), want: false}, |
| {input: "100", want: false}, |
| {input: nil, want: false}, |
| }, |
| }, |
| { |
| name: "doubleQualifier", |
| qualifier: &doubleQualifier{celValue: types.Double(3.14)}, |
| cases: []struct { |
| input any |
| want bool |
| }{ |
| {input: float64(3.14), want: true}, |
| {input: types.Double(3.14), want: true}, |
| {input: float64(2.71), want: false}, |
| {input: int64(3), want: false}, |
| {input: "3.14", want: false}, |
| {input: nil, want: false}, |
| }, |
| }, |
| } |
| |
| for _, tc := range tests { |
| t.Run(tc.name, func(t *testing.T) { |
| for _, c := range tc.cases { |
| if got := tc.qualifier.QualifierValueEquals(c.input); got != c.want { |
| t.Errorf("%s.QualifierValueEquals(%v (%T)) = %t, wanted %t", tc.name, c.input, c.input, got, c.want) |
| } |
| } |
| }) |
| } |
| } |
| |
| func TestPartialAttributeFactory_MaybeAttributeGloballyNamespaced(t *testing.T) { |
| reg := newTestRegistry(t) |
| fac := NewPartialAttributeFactory(containers.DefaultContainer, reg, reg) |
| dotAttr := fac.MaybeAttribute(10, ".global_var") |
| if dotAttr == nil { |
| t.Error("MaybeAttribute(.global_var) got nil") |
| } |
| } |
| |
| func TestPartialAttributeFactory_ResolveUnknownQualifier(t *testing.T) { |
| reg := newTestRegistry(t) |
| fac := NewPartialAttributeFactory(containers.DefaultContainer, reg, reg) |
| partVars, _ := NewPartialActivation( |
| map[string]any{"a": map[string]any{"b": 1}}, |
| NewAttributePattern("a").QualString("b")) |
| a := fac.AbsoluteAttribute(1, "a") |
| b, _ := fac.NewQualifier(nil, 2, "b", false) |
| a.AddQualifier(b) |
| val, err := a.Resolve(partVars) |
| if err != nil { |
| t.Fatal(err) |
| } |
| unkVal, ok := val.(*types.Unknown) |
| if !ok { |
| t.Fatalf("Resolve(a.b) got %v (%T), wanted *types.Unknown", val, val) |
| } |
| unkAttr := types.NewAttributeTrail("a") |
| types.QualifyAttribute[string](unkAttr, "b") |
| wantUnk := types.NewUnknown(2, unkAttr) |
| if !wantUnk.Contains(unkVal) { |
| t.Errorf("Resolve(a.b) got unknown %v, wanted %v", unkVal, wantUnk) |
| } |
| } |