blob: 67a93f6421379da086d1e205d57d00d4234069aa [file]
// 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
}