internal/filter: add more tests, and adjust so they (mostly) pass
In particular:
- permit element-wise comparisons against an array
- correct time.Duration support
- permit fetching "seconds" field from a duration
- turn a.b on the right of a comparison into a string
- better though imperfect support for comparing interface values
Change-Id: I3c0a9792d755b5e2e24f05336f335c0cdf19ec1d
Reviewed-on: https://go-review.googlesource.com/c/oscar/+/630057
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Ian Lance Taylor <iant@google.com>
Reviewed-by: Ian Lance Taylor <iant@google.com>
Reviewed-by: Jonathan Amsterdam <jba@google.com>
diff --git a/internal/filter/eval.go b/internal/filter/eval.go
index 501efa5..32e5e58 100644
--- a/internal/filter/eval.go
+++ b/internal/filter/eval.go
@@ -9,6 +9,7 @@
"errors"
"fmt"
"iter"
+ "math"
"reflect"
"regexp"
"strconv"
@@ -111,6 +112,16 @@
return ev.has(e)
}
+ // Although it's not clearly documented,
+ // some existing implementations permit a > 3
+ // where a is a slice. This matches if any
+ // element of a is > 3. Since supporting this
+ // makes the evaluator less efficient and since
+ // it is not the common case, handle it specially.
+ if multi, _ := ev.isMultiple(e.left); multi {
+ return ev.multipleCompare(e)
+ }
+
left, leftType := ev.fieldValue(e.left)
if left == nil {
return ev.alwaysFalse()
@@ -218,6 +229,66 @@
return ev.alwaysFalse()
}
+// isMultiple takes the left hand side of a comparison operation
+// and returns either true, nil if it contains multiple values,
+// or false, type if it does not. An expression contains
+// multiple values if it includes a reference to a slice or a map.
+func (ev *eval[T]) isMultiple(e Expr) (bool, reflect.Type) {
+ isMultipleType := func(typ reflect.Type) bool {
+ switch typ.Kind() {
+ case reflect.Map, reflect.Slice, reflect.Array:
+ return true
+ default:
+ return false
+ }
+ }
+
+ switch e := e.(type) {
+ case *nameExpr:
+ var typ reflect.Type
+ if sf, ok := fieldName(reflect.TypeFor[T](), e.name); ok {
+ typ = sf.Type
+ } else if cfn, ok := ev.functions[e.name]; ok {
+ typ = reflect.TypeOf(cfn).Out(0)
+ } else if e.isString {
+ typ = reflect.TypeFor[string]()
+ } else {
+ return false, nil
+ }
+
+ if isMultipleType(typ) {
+ return true, nil
+ }
+ return false, typ
+
+ case *memberExpr:
+ hmultiple, htype := ev.isMultiple(e.holder)
+ if hmultiple {
+ return true, nil
+ }
+ if htype == nil {
+ return false, nil
+ }
+ if htype.Kind() == reflect.Struct {
+ sf, ok := fieldName(htype, e.member)
+ if ok {
+ if isMultipleType(sf.Type) {
+ return true, nil
+ }
+ return false, sf.Type
+ }
+ }
+ return false, nil
+
+ case *functionExpr:
+ ev.msgs = append(ev.msgs, "isMultiple of functionExpr not implemented")
+ return false, nil
+
+ default:
+ return false, nil
+ }
+}
+
// fieldValue takes the left hand side of a comparison operation.
// It returns a function that evaluates to the value of the field,
// and also returns the type of the field.
@@ -381,6 +452,20 @@
return fn, holderType.Elem()
default:
+ // Special case: we can get the "seconds" field of a
+ // time.Duration. For flexibility we permit that on
+ // any int64 field.
+ if strings.EqualFold(name, "seconds") && holderType.Kind() == reflect.Int64 {
+ fn := func(v reflect.Value) reflect.Value {
+ if !v.IsValid() {
+ return v
+ }
+ secs := time.Duration(v.Int()).Seconds()
+ return reflect.ValueOf(secs)
+ }
+ return fn, reflect.TypeFor[float64]()
+ }
+
ev.msgs = append(ev.msgs, fmt.Sprintf("%s: type %s has no fields (looking for field %s)", pos, holderType, name))
return nil, nil
}
@@ -446,14 +531,31 @@
case *functionExpr:
return ev.compareFunction(op, leftType, right)
+ case *memberExpr:
+ // An a.b on the right of a comparison is just the
+ // string "a.b".
+ var stringifyHolder func(e *memberExpr) string
+ stringifyHolder = func(e *memberExpr) string {
+ switch h := e.holder.(type) {
+ case *nameExpr:
+ return h.name
+ case *memberExpr:
+ return stringifyHolder(h) + "." + h.member
+ default:
+ panic("can't happen")
+ }
+ }
+ name := stringifyHolder(right) + "." + right.member
+ ne := &nameExpr{
+ name: name,
+ pos: right.pos,
+ }
+ return ev.compareName(op, leftType, ne)
+
case *comparisonExpr:
ev.msgs = append(ev.msgs, fmt.Sprintf("%s: invalid comparison on right of comparison", right.pos))
return nil
- case *memberExpr:
- ev.msgs = append(ev.msgs, fmt.Sprintf("%s: invalid member expression on right of comparison", right.pos))
- return nil
-
default:
panic("can't happen")
}
@@ -597,11 +699,11 @@
return fn
}
- var cmpfn func(v reflect.Value) int
+ var cmpFn func(v reflect.Value) int
switch leftType.Kind() {
case reflect.Bool:
rbval := rval.Bool()
- cmpfn = func(v reflect.Value) int {
+ cmpFn = func(v reflect.Value) int {
if v.Bool() {
if rbval {
return 0
@@ -617,50 +719,73 @@
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
rival := rval.Int()
- cmpfn = func(v reflect.Value) int {
+ cmpFn = func(v reflect.Value) int {
return cmp.Compare(v.Int(), rival)
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
ruval := rval.Uint()
- cmpfn = func(v reflect.Value) int {
+ cmpFn = func(v reflect.Value) int {
return cmp.Compare(v.Uint(), ruval)
}
case reflect.Float32, reflect.Float64:
rfval := rval.Float()
- cmpfn = func(v reflect.Value) int {
+ cmpFn = func(v reflect.Value) int {
return cmp.Compare(v.Float(), rfval)
}
+ cmpOp := ev.compareOp(op, cmpFn)
+ return func(v reflect.Value) bool {
+ if !v.IsValid() {
+ return false
+ }
+ // Comparisons with NaN are always false,
+ // except that we treat NaN == NaN as true.
+ if math.IsNaN(v.Float()) {
+ return math.IsNaN(rfval)
+ }
+ return cmpOp(v)
+ }
case reflect.String:
rsval := rval.String()
- cmpfn = func(v reflect.Value) int {
+ cmpFn = func(v reflect.Value) int {
return strings.Compare(v.String(), rsval)
}
+ case reflect.Interface:
+ // This call to Convert won't panic.
+ // rval comes from parseLiteral,
+ // which verified that the conversion
+ // is OK using CanConvert.
+ rival := rval.Convert(leftType).Interface()
+ rsval := fmt.Sprint(rival)
+ cmpFn = func(v reflect.Value) int {
+ // This comparison can't panic.
+ // rival comes from parseLiteral,
+ // which only returns comparable types.
+ if v.Interface() == rival {
+ return 0
+ }
+ // Compare as strings. Seems to be the best we can do.
+ return strings.Compare(fmt.Sprint(v.Interface()), rsval)
+ }
+
case reflect.Struct:
if leftType == reflect.TypeFor[time.Time]() {
rtval := rval.Interface().(time.Time)
- cmpfn = func(v reflect.Value) int {
+ cmpFn = func(v reflect.Value) int {
return v.Interface().(time.Time).Compare(rtval)
}
break
}
- if leftType == reflect.TypeFor[time.Duration]() {
- rdval := rval.Interface().(time.Duration)
- cmpfn = func(v reflect.Value) int {
- return cmp.Compare(v.Interface().(time.Duration), rdval)
- }
- break
- }
fallthrough
default:
panic("can't happen")
}
- return ev.compareOp(op, cmpfn)
+ return ev.compareOp(op, cmpFn)
}
// compareOp returns a function that takes a comparison function
@@ -702,6 +827,26 @@
return func(reflect.Value) bool { return false }
}
+// multipleCompare returns a function that returns the result of a
+// comparison, where the left hand side of the comparison contains
+// multiple values.
+func (ev *eval[T]) multipleCompare(e *comparisonExpr) func(T) bool {
+ switch el := e.left.(type) {
+ case *nameExpr:
+ left, leftType := ev.fieldValue(el)
+ if left == nil {
+ return ev.alwaysFalse()
+ }
+ return ev.multipleName(e.op, left, leftType, e.right)
+
+ case *memberExpr:
+ return ev.multipleMember(e.pos, e.op, el, e.right)
+
+ default:
+ panic("can't happen")
+ }
+}
+
// match returns a function that takes a value of type typ
// and reports whether it is equal to, or contains, val.
// This returns nil if the types can't match.
@@ -752,6 +897,24 @@
return strings.Contains(strings.ToLower(v.String()), esval)
}
+ case reflect.Interface:
+ // This call to Convert won't panic.
+ // eval comes from parseLiteral,
+ // which verified that the conversion
+ // is OK using CanConvert.
+ eival := eval.Convert(typ).Interface()
+ esval := fmt.Sprint(eival)
+ return func(v reflect.Value) bool {
+ // This comparison can't panic.
+ // rival comes from parseLiteral,
+ // which only returns comparable types.
+ if v.Interface() == eival {
+ return true
+ }
+ // Compare as strings. Seems to be the best we can do.
+ return fmt.Sprint(v.Interface()) == esval
+ }
+
default:
panic("can't happen")
}
@@ -835,7 +998,7 @@
var rval reflect.Value
switch typ.Kind() {
case reflect.Bool:
- bval, err := strconv.ParseBool(val)
+ bval, err := strconv.ParseBool(strings.ToLower(val))
if err != nil {
return badParse(err)
}
@@ -844,6 +1007,17 @@
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
ival, err := strconv.ParseInt(val, 0, 64)
if err != nil {
+ if (strings.HasSuffix(val, "s") || strings.HasSuffix(val, "S")) && typ.Kind() == reflect.Int64 {
+ // Try parsing this as a duration.
+ d, err := time.ParseDuration(strings.ToLower(val))
+ if err != nil {
+ return badParse(err)
+ }
+
+ rval = reflect.ValueOf(d)
+ break
+ }
+
return badParse(err)
}
rval = reflect.ValueOf(ival)
@@ -882,17 +1056,6 @@
rval = reflect.ValueOf(tval)
break
}
- if typ == reflect.TypeFor[time.Duration]() {
- s, ok := strings.CutSuffix(val, "s")
- if !ok {
- return badParse(errors.New(`duration does not end in "s"`))
- }
- ival, err := strconv.ParseInt(s, 0, 64)
- if err != nil {
- return badParse(err)
- }
- rval = reflect.ValueOf(time.Duration(ival) * time.Second)
- }
fallthrough
default:
@@ -920,7 +1083,7 @@
return ev.hasName(el, e.right)
case *memberExpr:
- return ev.hasMember(e.pos, el, e.right)
+ return ev.multipleMember(e.pos, tokenHas, el, e.right)
case *binaryExpr, *unaryExpr, *comparisonExpr, *functionExpr:
ev.msgs = append(ev.msgs, "invalid expression on left of has operator")
@@ -956,52 +1119,25 @@
}
}
- cmpFn := ev.hasCompareVal(leftType.Elem(), er)
- if cmpFn == nil {
- return ev.alwaysFalse()
- }
-
- return func(v T) bool {
- mval := left(v)
- if !mval.IsValid() {
- return false
- }
- seq := func(yield func(reflect.Value) bool) {
- mi := mval.MapRange()
- for mi.Next() {
- if !yield(mi.Value()) {
- return
- }
- }
- }
- return cmpFn(seq)
- }
+ return ev.multipleName(tokenHas, left, leftType, er)
case reflect.Slice, reflect.Array:
// This reports whether the slice contains an element
// that matches er.
- cmpFn := ev.hasCompareVal(leftType.Elem(), er)
- if cmpFn == nil {
- return ev.alwaysFalse()
- }
-
- return func(v T) bool {
- sval := left(v)
- if !sval.IsValid() {
- return false
- }
- seq := func(yield func(reflect.Value) bool) {
- for i := range sval.Len() {
- if !yield(sval.Index(i)) {
- return
- }
- }
- }
- return cmpFn(seq)
- }
+ return ev.multipleName(tokenHas, left, leftType, er)
case reflect.Struct:
+ if leftType == reflect.TypeFor[time.Time]() {
+ cmpFn := ev.compareVal(tokenHas, leftType, er)
+ if cmpFn == nil {
+ return ev.alwaysFalse()
+ }
+ return func(v T) bool {
+ return cmpFn(left(v))
+ }
+ }
+
// This reports whether the struct has a member
// that matches er. We don't need an iter.Seq for this case.
@@ -1035,7 +1171,55 @@
}
}
-// holderEmptyStringType tells hasMemberHolder whether we are
+// multipleVal returns a function that reports whether the value
+// left, of type leftType, matches the value in er.
+// leftType is expected to have multiple values.
+func (ev *eval[T]) multipleName(op tokenKind, left func(T) reflect.Value, leftType reflect.Type, er Expr) func(T) bool {
+ cmpFn := ev.multipleCompareVal(op, leftType.Elem(), er)
+ if cmpFn == nil {
+ return ev.alwaysFalse()
+ }
+
+ switch leftType.Kind() {
+ case reflect.Map:
+ return func(v T) bool {
+ mval := left(v)
+ if !mval.IsValid() {
+ return false
+ }
+ seq := func(yield func(reflect.Value) bool) {
+ mi := mval.MapRange()
+ for mi.Next() {
+ if !yield(mi.Value()) {
+ return
+ }
+ }
+ }
+ return cmpFn(seq)
+ }
+
+ case reflect.Slice, reflect.Array:
+ return func(v T) bool {
+ sval := left(v)
+ if !sval.IsValid() {
+ return false
+ }
+ seq := func(yield func(reflect.Value) bool) {
+ for i := range sval.Len() {
+ if !yield(sval.Index(i)) {
+ return
+ }
+ }
+ }
+ return cmpFn(seq)
+ }
+
+ default:
+ panic("can't happen")
+ }
+}
+
+// holderEmptyStringType tells multipleMemberHolder whether we are
// comparing against an empty string.
type holderEmptyStringType bool
@@ -1044,15 +1228,15 @@
holderEmptyString holderEmptyStringType = true
)
-// hasMember returns a function that returns whether the member el
-// has the value in er.
-func (ev *eval[T]) hasMember(pos position, el *memberExpr, er Expr) func(T) bool {
+// multipleMember returns a function that returns whether the member el
+// matches the value in er.
+func (ev *eval[T]) multipleMember(pos position, op tokenKind, el *memberExpr, er Expr) func(T) bool {
emptyString := holderNonEmptyString
if erName, ok := er.(*nameExpr); ok && erName.isString && erName.name == "" {
emptyString = holderEmptyString
}
- hfn, htype := ev.hasMemberHolder(el, emptyString)
+ hfn, htype := ev.multipleMemberHolder(el, emptyString)
if hfn == nil {
return ev.alwaysFalse()
}
@@ -1062,7 +1246,7 @@
switch htype.Kind() {
case reflect.Map:
- cmpFn = ev.hasCompareVal(htype.Elem(), er)
+ cmpFn = ev.multipleCompareVal(op, htype.Elem(), er)
if cmpFn == nil {
return ev.alwaysFalse()
}
@@ -1081,7 +1265,7 @@
}
case reflect.Slice, reflect.Array:
- cmpFn = ev.hasCompareVal(htype.Elem(), er)
+ cmpFn = ev.multipleCompareVal(op, htype.Elem(), er)
if cmpFn == nil {
return ev.alwaysFalse()
}
@@ -1101,7 +1285,7 @@
case reflect.Struct:
cmpStructFn := ev.compareStructFields(htype,
func(ftype reflect.Type) func(reflect.Value) bool {
- return ev.compareVal(tokenHas, ftype, er)
+ return ev.compareVal(op, ftype, er)
},
)
if cmpStructFn == nil {
@@ -1122,7 +1306,7 @@
}
default:
- cmpFn = ev.hasCompareVal(htype, er)
+ cmpFn = ev.multipleCompareVal(op, htype, er)
if cmpFn == nil {
return ev.alwaysFalse()
}
@@ -1140,8 +1324,8 @@
}
}
-// hasMemberHolder is called when we are using the has operator
-// on an aggregate A, and we are fetching a field F from that aggregate.
+// multipleMemberHolder is called when we are comparing against
+// an aggregate A, and we are fetching a field F from that aggregate.
// If A is a map then F is a key in the map.
// If A is a slice then F is checked against each element of the slice.
// If A is a struct then F is a field of the struct.
@@ -1150,13 +1334,13 @@
// takes the value A and returns a sequence of reflect.Value's.
// We also return the type of those reflect.Value's.
// This returns nil, nil on failure.
-func (ev *eval[T]) hasMemberHolder(e *memberExpr, emptyString holderEmptyStringType) (func(iter.Seq[reflect.Value]) iter.Seq[reflect.Value], reflect.Type) {
+func (ev *eval[T]) multipleMemberHolder(e *memberExpr, emptyString holderEmptyStringType) (func(iter.Seq[reflect.Value]) iter.Seq[reflect.Value], reflect.Type) {
var hfn func(iter.Seq[reflect.Value]) iter.Seq[reflect.Value]
var htype reflect.Type
switch holder := e.holder.(type) {
case *memberExpr:
- hfn, htype = ev.hasMemberHolder(holder, emptyString)
+ hfn, htype = ev.multipleMemberHolder(holder, emptyString)
if hfn == nil {
return nil, nil
}
@@ -1238,7 +1422,7 @@
}
return rhfn, ftype
- case reflect.Struct:
+ default:
ffn, ftype := ev.fieldAccessor(e.pos, htype, e.member)
if ffn == nil {
return nil, nil
@@ -1257,24 +1441,20 @@
}
}
return rhfn, ftype
-
- default:
- ev.msgs = append(ev.msgs, fmt.Sprintf("%s: request for member %s in non-aggregate type %s", e.pos, e.member, htype))
- return nil, nil
}
}
-// hasCompareVal returns a function that takes a sequence of reflect.Value,
+// multipleCompareVal returns a function that takes a sequence of reflect.Value,
// where each element of the sequence has type leftType,
-// and evaluate that sequence with the parameter right.
+// and evaluates that sequence with the parameter right.
// The function may evaluate the sequence multiple times.
// The right value may be a logical expression with AND and OR.
// This returns nil on error.
-func (ev *eval[T]) hasCompareVal(leftType reflect.Type, right Expr) func(iter.Seq[reflect.Value]) bool {
+func (ev *eval[T]) multipleCompareVal(op tokenKind, leftType reflect.Type, right Expr) func(iter.Seq[reflect.Value]) bool {
switch right := right.(type) {
case *binaryExpr:
- lf := ev.hasCompareVal(leftType, right.left)
- rf := ev.hasCompareVal(leftType, right.right)
+ lf := ev.multipleCompareVal(op, leftType, right.left)
+ rf := ev.multipleCompareVal(op, leftType, right.right)
if lf == nil || rf == nil {
return nil
}
@@ -1292,7 +1472,7 @@
}
case *unaryExpr:
- uf := ev.hasCompareVal(leftType, right.expr)
+ uf := ev.multipleCompareVal(op, leftType, right.expr)
if uf == nil {
return nil
}
@@ -1306,7 +1486,7 @@
}
default:
- cmpFn := ev.compareVal(tokenHas, leftType, right)
+ cmpFn := ev.compareVal(op, leftType, right)
if cmpFn == nil {
return nil
}
diff --git a/internal/filter/eval_test.go b/internal/filter/eval_test.go
index 3260b9d..9ab2316 100644
--- a/internal/filter/eval_test.go
+++ b/internal/filter/eval_test.go
@@ -10,6 +10,8 @@
"os"
"path/filepath"
"slices"
+ "strconv"
+ "strings"
"testing"
"time"
@@ -98,6 +100,121 @@
runTests(t, tests.Tests, data.Resources)
}
+type filterMiscTestData struct {
+ Resources []testMessage `json:"resources"`
+}
+
+type testMessage struct {
+ Int32Field int32 `json:"int32_field"`
+ Int64Field int64 `json:"int64_field"`
+ Uint32Field uint32 `json:"uint32_field"`
+ Uint64Field uint64 `json:"uint64_field"`
+ FloatField float32 `json:"float_field"`
+ FloatInfinity float32Special `json:"float_infinity"`
+ FloatNegativeInfinity float32Special `json:"float_negative_infinity"`
+ FloatNaN float32Special `json:"float_nan"`
+ DoubleField float64Special `json:"double_field"`
+ DoubleInfinity float64Special `json:"double_infinity"`
+ DoubleNegativeInfinity float64Special `json:"double_negative_infinity"`
+ DoubleNaN float64Special `json:"double_nan"`
+ BoolField bool `json:"bool_field"`
+ StringField string `json:"string_field"`
+ EnumField string `json:"enum_field"`
+ OutOfOrderEnumField string `json:"out_of_order_enum_field"`
+ BytesField string `json:"bytes_field"`
+ NoValueField string `json:"no_value_field"`
+ Nested nestedMessage `json:"nested"`
+ DeprecatedField int64 `json:"deprecated_field"`
+ InternalField string `json:"internal_field"`
+ RepeatedInt32Field []int32 `json:"repeated_int32_field"`
+ RepeatedStringField []string `json:"repeated_string_field"`
+ NonUTF8StringField string `json:"non_utf8_string_field"`
+ NonUTF8RepeatedStringField []string `json:"non_utf8_repeated_string_field"`
+ RepeatedEnumField []string `json:"repeated_enum_field"`
+ RepeatedMessageField []nestedMessage `json:"repeated_message_field"`
+ RepeatedEmptyMessageField []nestedMessage `json:"repeated_empty_message_field"`
+ MapStringIn32Field map[string]int32 `json:"map_string_int32_field"`
+ MapInt32Int32Field map[int32]int32 `json:"map_int32_int32_field"`
+ MapStringNestedField map[string]nestedMessage `json:"map_string_nested_field"`
+ AnyField any `json:"any_field"`
+ RepeatedAnyField []any `json:"repeated_any_field"`
+ Timestamp time.Time `json:"timestamp"`
+ Duration duration `json:"duration"`
+ StructField map[string]any `json:"struct_field"`
+ JSON map[string]any `json:"json"`
+ StringValue string `json:"string_value"`
+ NestedValue nestedMessage `json:"nested_value"`
+ UnicodePathe string `json:"unicode_pathe"`
+ UnicodeResume string `json:"unicode_resume"`
+ UnicodeUnicode string `json:"unicode_unicode"`
+ URLField string `json:"url_field"`
+}
+
+type nestedMessage struct {
+ Uint32Field uint32 `json:"uint32_field"`
+ DeeperNest deeperNestedMessage `json:"deeper_nest"`
+ RepeatedUint32Field []uint32 `json:"repeated_uint32_field"`
+ RepeatedEmptyUint32 []uint32 `json:"repeated_empty_uint32"`
+ StringField string `json:"string_field"`
+ NovalueField string `json:"no_value_field"`
+}
+
+type deeperNestedMessage struct {
+ NiceField string `json:"nice_field"`
+ NicerField string `json:"nicer_field"`
+}
+
+// float32Special is a version of float32 that supports JSON
+// unmarshaling of Infinity and NaN.
+type float32Special float32
+
+func (pf *float32Special) UnmarshalJSON(text []byte) error {
+ str := strings.Trim(string(text), `"`)
+ f, err := strconv.ParseFloat(str, 32)
+ if err != nil {
+ return err
+ }
+ *pf = float32Special(f)
+ return nil
+}
+
+// float64Special is a version of float64 that supports JSON
+// unmarshaling of Infinity and NaN.
+type float64Special float64
+
+func (pf *float64Special) UnmarshalJSON(text []byte) error {
+ str := strings.Trim(string(text), `"`)
+ f, err := strconv.ParseFloat(str, 64)
+ if err != nil {
+ return err
+ }
+ *pf = float64Special(f)
+ return nil
+}
+
+// duration is a version of time.Duration that supports JSON unmarshaling.
+type duration time.Duration
+
+func (pd *duration) UnmarshalJSON(text []byte) error {
+ str := strings.Trim(string(text), `"`)
+ d, err := time.ParseDuration(str)
+ if err != nil {
+ return err
+ }
+ *pd = duration(d)
+ return nil
+}
+
+func TestEvalMisc(t *testing.T) {
+ var data filterMiscTestData
+ unmarshalJSON(t, "misc_test.json", &data)
+
+ var tests yamlTests
+ unmarshalYAML(t, "misc_test.yaml", &tests)
+
+ runTests(t, tests.Tests, data.Resources)
+}
+
// runTests runs a set of YAML tests on a set of input data.
func runTests[T any](t *testing.T, tests []yamlTest, data []T) {
var desc string
@@ -112,6 +229,23 @@
}
if test.Skip {
+ // Check whether the test passes.
+ e, err := ParseFilter(test.Expr)
+ if err == nil {
+ eval, msgs := Evaluator[T](e, nil)
+ if len(msgs) == 0 && test.Error == "" {
+ var matches []int
+ for i, d := range data {
+ if eval(d) {
+ matches = append(matches, i+1)
+ }
+ }
+ if slices.Equal(matches, test.Matches) {
+ t.Errorf("%s %d passes unexpectedly", desc, idx)
+ }
+ }
+ }
+
idx++
continue
}
@@ -194,3 +328,64 @@
t.Fatal(err)
}
}
+
+// Test that we don't panic on an incomparable literal.
+func TestIncomparable(t *testing.T) {
+ e1, err := ParseFilter("A:0")
+ if err != nil {
+ t.Fatal(err)
+ }
+ e2, err := ParseFilter("0")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ type Incomparable1 struct {
+ A [][]byte
+ }
+
+ // Building the evaluator should fail,
+ // because we can't compare 0 to []byte.
+ _, msgs := Evaluator[Incomparable1](e1, nil)
+ if len(msgs) == 0 {
+ t.Error("expected evaluator errors")
+ } else {
+ for _, msg := range msgs {
+ t.Log(msg)
+ }
+ }
+
+ eval, msgs := Evaluator[Incomparable1](e2, nil)
+ if len(msgs) != 0 {
+ t.Error("evaluator failed")
+ for _, msg := range msgs {
+ t.Log(msg)
+ }
+ } else {
+ // Running this should not panic.
+ if eval(Incomparable1{A: [][]byte{[]byte{1}}}) {
+ t.Error("unexpected match")
+ }
+ }
+
+ type Incomparable2 struct {
+ A []any
+ }
+
+ for i, expr := range []Expr{e1, e2} {
+ // This case should succeed, because we can compare 0 to any.
+ eval, msgs := Evaluator[Incomparable2](expr, nil)
+ if len(msgs) != 0 {
+ t.Errorf("%d: evaluator failed", i)
+ for _, msg := range msgs {
+ t.Logf("%d: %s", i, msg)
+ }
+ } else {
+ // Running this with an incomparable type in a
+ // should not panic.
+ if eval(Incomparable2{A: []any{[]byte{0}}}) {
+ t.Errorf("%d: unexpected match", i)
+ }
+ }
+ }
+}
diff --git a/internal/filter/lex.go b/internal/filter/lex.go
index 9069e82..8326b55 100644
--- a/internal/filter/lex.go
+++ b/internal/filter/lex.go
@@ -452,13 +452,14 @@
case '0', '1', '2', '3', '4', '5', '6', '7':
// A backslash sequence of 1-3 octal digits is an octal escape.
if len(lex.input) >= 3 && r <= '3' && isOctalDigit(rune(lex.input[1])) && isOctalDigit(rune(lex.input[2])) {
+ n1 := rune(lex.input[1])
+ n2 := rune(lex.input[2])
+ r = (r - '0') << 6
+ r += (n1 - '0') << 3
+ r += n2 - '0'
lex.advance(r, 1)
- r -= '0'
- r <<= 6
- r += rune(lex.input[1]-'0') << 3
- r += rune(lex.input[2] - '0')
- lex.advance(rune(lex.input[1]), 1)
- lex.advance(rune(lex.input[2]), 1)
+ lex.advance(n1, 1)
+ lex.advance(n2, 1)
sb.WriteRune(r)
return true
}
diff --git a/internal/filter/testdata/basic_test.yaml b/internal/filter/testdata/basic_test.yaml
index e938633..d61618d 100644
--- a/internal/filter/testdata/basic_test.yaml
+++ b/internal/filter/testdata/basic_test.yaml
@@ -854,7 +854,6 @@
- expr: 'compound.str.array=ab'
matches: []
- skip: true
- description: OR precedence over adjacent conjunction
@@ -1181,7 +1180,6 @@
- expr: 'int_field:0 OR int_field:0 AND int_field:0'
matches: [1]
- skip: true
- expr: 'int_field:0 OR int_field:0 AND int_field:1'
matches: [1]
skip: true
@@ -2085,7 +2083,6 @@
- expr: 'compound.num.array!=(1 23)'
matches: [1, 2, 3]
- skip: true
- expr: 'compound.num.array!=(-456 -789)'
matches: [1, 2, 3]
@@ -2653,15 +2650,12 @@
- expr: 'compound.str.dictionary=ab'
matches: []
- skip: true
- expr: 'compound.num.dictionary=1'
matches: []
- skip: true
- expr: 'compound.num.dictionary=(1)'
matches: []
- skip: true
- expr: 'compound.num.dictionary=4'
matches: []
@@ -2693,7 +2687,6 @@
- expr: 'compound.num.dictionary=(3)'
matches: []
- skip: true
- expr: 'compound.num.dictionary=(456)'
matches: []
@@ -2701,15 +2694,12 @@
- expr: 'compound.num.dictionary=(1 3 5)'
matches: []
- skip: true
- expr: "compound.num.dictionary=(1\n3\n5)"
matches: []
- skip: true
- expr: "compound.num.dictionary=(1\n-5\n5)"
matches: []
- skip: true
- expr: 'compound.num.dictionary=( aaa 3 zzz )'
error: operand type mismatch
@@ -3000,7 +2990,6 @@
- expr: 'timestampField<1980-01-01T00\:00\:00.000Z'
matches: [1]
- skip: true
- expr: 'timestampField<1980-01-01T00\:00\:00\.000Z'
matches: [1]
- expr: 'timestampField<1980-01-01T00":"00":"00.000Z'
@@ -3011,7 +3000,6 @@
- expr: 'timestampField>2010-01-01T00\:00\:00.000Z'
matches: [3]
- skip: true
- expr: 'timestampField>2010-01-01T00\:00\:00\.000Z'
matches: [3]
- expr: 'timestampField>2010-01-01T00":"00":"00.000Z'
diff --git a/internal/filter/testdata/misc_test.json b/internal/filter/testdata/misc_test.json
new file mode 100644
index 0000000..0078c73
--- /dev/null
+++ b/internal/filter/testdata/misc_test.json
@@ -0,0 +1,101 @@
+{
+ "resources": [
+ {
+ "bytes_field": "øùúûüýþÿ",
+ "int32_field": -1001,
+ "int64_field": -1001,
+ "uint32_field": 1001,
+ "uint64_field": 1001,
+ "float_field": 1.7976e-38,
+ "float_infinity": "Infinity",
+ "float_negative_infinity": "-Infinity",
+ "float_nan": "NaN",
+ "double_field": -1.7976e+308,
+ "double_infinity": "Infinity",
+ "double_negative_infinity": "-Infinity",
+ "double_nan": "NaN",
+ "bool_field": true,
+ "string_field": "Hello beautiful world.",
+ "enum_field": "VALUE_2",
+ "out_of_order_enum_field": "VALUE_A",
+ "nested": {
+ "uint32_field": 123435,
+ "deeper_nest": {
+ "nice_field": "Foo Bar",
+ "nicer_field": "FooBar Baz"
+ }
+ },
+ "internal_field": "topsecret",
+ "deprecated_field": 200,
+ "repeated_int32_field": [100, 101],
+ "repeated_string_field": [
+ "Apple",
+ "Ball",
+ "Cat Dog Elephant",
+ "sl\\as\\\\h",
+ "qu\"ot\"es",
+ "t,e:s=[tin]+g~o.nly",
+ "t,e:s=t<i>n+g~o.nly",
+ "t,e:s=tin+g~m.at*ch",
+ "/",
+ "//",
+ "//foo.bar/baz",
+ "//foo.bar/baz*"
+ ],
+ "repeated_enum_field": ["VALUE_3", "VALUE_4"],
+ "repeated_message_field": [
+ {
+ "uint32_field": 5555,
+ "deeper_nest": {
+ "nice_field": "nice55",
+ "nicer_field": "nicer55"
+ },
+ "repeated_uint32_field": [6666, 6667]
+ },
+ {
+ "uint32_field": 7777,
+ "deeper_nest": {
+ "nice_field": "nice66",
+ "nicer_field": "nicer66"
+ },
+ "repeated_uint32_field": [8888, 8889],
+ "string_field": "red"
+ }
+ ],
+ "map_string_int32_field": {
+ "foobar": 9876,
+ "foobar.abcd": 9877,
+ "barFoo": 1234
+ },
+ "map_int32_int32_field": {
+ "1234": 4321,
+ "1235": 5321
+ },
+ "map_string_nested_field": {
+ "foobar": {
+ "uint32_field": 1111,
+ "deeper_nest": {
+ "nice_field": "nice11",
+ "nicer_field": "nicer11"
+ },
+ "repeated_uint32_field": [2222, 2223]
+ }
+ },
+ "timestamp": "1970-01-01T00:00:10.000Z",
+ "duration": "5.0s",
+ "string_value": "OneOfAKind",
+ "struct_field": {
+ "foo": "bar",
+ "fooBar": "xyz",
+ "foo.foo": "bar.bar",
+ "foo_bool": true,
+ "foo_number": 12345,
+ "sports": "soccer"
+ },
+ "unicode_pathe": "Path\u00E9",
+ "unicode_resume": "Résumé",
+ "unicode_unicode": "Ṳᾔḯ¢◎ⅾℯ",
+ "url_field": "http://foo.bar/baz*"
+ }
+ ]
+}
diff --git a/internal/filter/testdata/misc_test.yaml b/internal/filter/testdata/misc_test.yaml
new file mode 100644
index 0000000..e2bc6d3
--- /dev/null
+++ b/internal/filter/testdata/misc_test.yaml
@@ -0,0 +1,1196 @@
+tests:
+
+ - description: empty expressions
+
+ - expr: ""
+ matches: [1]
+ - expr: " "
+ matches: [1]
+ - expr: "\n"
+ matches: [1]
+
+ - description: unknown fields
+
+ - expr: "fooField:3"
+ error: unknown field
+
+ - expr: "nested.foo:3"
+ error: unknown field
+
+ - description: Int32Field
+
+ - expr: "int32Field = -1001"
+ matches: [1]
+ - expr: "int32Field != 1001"
+ matches: [1]
+ - expr: "int32Field > -2000"
+ matches: [1]
+ - expr: "int32Field < 0"
+ matches: [1]
+ - expr: "int32Field >= -1001"
+ matches: [1]
+ - expr: "int32Field <= -1001"
+ matches: [1]
+
+ - expr: "int32Field = -1002"
+ matches: []
+ - expr: "int32Field != -1001"
+ matches: []
+ - expr: "int32Field > -1001"
+ matches: []
+ - expr: "int32Field < -2000"
+ matches: []
+ - expr: "int32Field >= -1000"
+ matches: []
+ - expr: "int32Field <= -1002"
+ matches: []
+
+ - expr: "int32Field > 0.0001"
+ error: operand type mismatch
+
+ - description: Int64Field
+
+ - expr: "int64Field = -1001"
+ matches: [1]
+ - expr: "int64Field != 1001"
+ matches: [1]
+ - expr: "int64Field > -2000"
+ matches: [1]
+ - expr: "int64Field < 0"
+ matches: [1]
+ - expr: "int64Field >= -1001"
+ matches: [1]
+ - expr: "int64Field <= -1001"
+ matches: [1]
+
+ - expr: "int64Field = -1002"
+ matches: []
+ - expr: "int64Field != -1001"
+ matches: []
+ - expr: "int64Field > -1001"
+ matches: []
+ - expr: "int64Field < -2000"
+ matches: []
+ - expr: "int64Field >= -1000"
+ matches: []
+ - expr: "int64Field <= -1002"
+ matches: []
+
+ - description: Uint32Field
+
+ - expr: "uint32Field = 1001"
+ matches: [1]
+ - expr: "uint32Field != 1002"
+ matches: [1]
+ - expr: "uint32Field > 1000"
+ matches: [1]
+ - expr: "uint32Field < 2000"
+ matches: [1]
+ - expr: "uint32Field >= 1001"
+ matches: [1]
+ - expr: "uint32Field <= 1001"
+ matches: [1]
+
+ - expr: "uint32Field = 1002"
+ matches: []
+ - expr: "uint32Field != 1001"
+ matches: []
+ - expr: "uint32Field > 1001"
+ matches: []
+ - expr: "uint32Field < 1001"
+ matches: []
+ - expr: "uint32Field >= 1002"
+ matches: []
+ - expr: "uint32Field <= 1000"
+ matches: []
+
+ - expr: "uint32Field <= -1001"
+ error: operand type mismatch
+
+ - description: Uint64Field
+
+ - expr: "uint64Field = 1001"
+ matches: [1]
+ - expr: "uint64Field != 1002"
+ matches: [1]
+ - expr: "uint64Field > 1000"
+ matches: [1]
+ - expr: "uint64Field < 2000"
+ matches: [1]
+ - expr: "uint64Field >= 1001"
+ matches: [1]
+ - expr: "uint64Field <= 1001"
+ matches: [1]
+
+ - expr: "uint64Field = 1002"
+ matches: []
+ - expr: "uint64Field != 1001"
+ matches: []
+ - expr: "uint64Field > 1001"
+ matches: []
+ - expr: "uint64Field < 1001"
+ matches: []
+ - expr: "uint64Field >= 1002"
+ matches: []
+ - expr: "uint64Field <= 1000"
+ matches: []
+
+ - expr: "uint64Field <= -1001"
+ error: operand type mismatch
+
+ - description: FloatField
+
+ - expr: "floatField = 1.7976e-38"
+ matches: [1]
+
+ - expr: "floatField != 1.7976e-37"
+ matches: [1]
+ - expr: "floatField != 1.79761e-38"
+ matches: [1]
+ - expr: "floatField > 1.7976e-39"
+ matches: [1]
+ - expr: "floatField < 1.7976e-37"
+ matches: [1]
+ - expr: "floatField >= 1.797e-38"
+ matches: [1]
+ - expr: "floatField <= 1.7977e-38"
+ matches: [1]
+
+ - expr: "floatField = 1.7976e-37"
+ matches: []
+ - expr: "floatField != 1.7976e-38"
+ matches: []
+
+ - expr: "floatField > 1.7976e-38"
+ matches: []
+ - expr: "floatField < 1.7976e-39"
+ matches: []
+ - expr: "floatField >= 1.79761e-38"
+ matches: []
+ - expr: "floatField <= 1.7976e-39"
+ matches: []
+ - expr: "floatField <= -20"
+ matches: []
+
+ - expr: "floatField = NaN"
+ matches: []
+ - expr: "floatField = Infinity"
+ matches: []
+
+ - description: float extremes
+
+ - expr: "float_infinity = Infinity"
+ matches: [1]
+ - expr: "float_infinity: infinity"
+ matches: [1]
+
+ - expr: "float_infinity > 100"
+ matches: [1]
+ - expr: "float_infinity != -Infinity"
+ matches: [1]
+ skip: true
+
+ - expr: "float_infinity = -Infinity"
+ matches: []
+ skip: true
+
+ - expr: "float_infinity = NaN"
+ matches: []
+ - expr: 'float_infinity = "infinity"'
+ matches: [1]
+
+ - expr: 'float_infinity != "-Infinity"'
+ matches: [1]
+ - expr: 'float_infinity = "-infinity"'
+ matches: []
+
+ - expr: 'float_infinity = "NaN"'
+ matches: []
+
+ - expr: "float_negative_infinity = -Infinity"
+ matches: [1]
+ skip: true
+
+ - expr: "float_negative_infinity: -infinity"
+ matches: [1]
+ skip: true
+
+ - expr: "float_negative_infinity < 100"
+ matches: [1]
+ - expr: "float_negative_infinity != Infinity"
+ matches: [1]
+ - expr: "float_negative_infinity = Infinity"
+ matches: []
+ - expr: "float_negative_infinity = NaN"
+ matches: []
+ - expr: 'float_negative_infinity = "-Infinity"'
+ matches: [1]
+ - expr: 'float_negative_infinity != "Infinity"'
+ matches: [1]
+ - expr: 'float_negative_infinity = "infinity"'
+ matches: []
+
+ - expr: 'float_negative_infinity = "NaN"'
+ matches: []
+
+ - expr: "float_nan:nan"
+ matches: [1]
+
+ - expr: "float_nan = NaN"
+ matches: [1]
+ - expr: "float_nan < 100"
+ matches: []
+ - expr: "float_nan = 100"
+ matches: []
+ - expr: "float_nan > 100"
+ matches: []
+
+ - description: DoubleField
+
+ - expr: "doubleField = -1.7976e+308"
+ matches: [1]
+ - expr: "doubleField != -1.7976e+307"
+ matches: [1]
+ - expr: "doubleField != -1.7975e+308"
+ matches: [1]
+ - expr: "doubleField > -1.79761e+308"
+ matches: [1]
+ - expr: "doubleField < 0"
+ matches: [1]
+ - expr: "doubleField >= -1.79761e+308"
+ matches: [1]
+ - expr: "doubleField <= -1"
+ matches: [1]
+
+ - expr: "doubleField = -1.79762e+308"
+ matches: []
+ - expr: "doubleField != -1.7976e+308"
+ matches: []
+ - expr: "doubleField > 1"
+ matches: []
+ - expr: "doubleField < -1.79761e+308"
+ matches: []
+ - expr: "doubleField >= -1"
+ matches: []
+ - expr: "doubleField <= -1.79761e+308"
+ matches: []
+
+ - expr: "doubleField = NaN"
+ matches: []
+ - expr: "doubleField = Infinity"
+ matches: []
+
+ - description: double extremes
+
+ - expr: "double_infinity = Infinity"
+ matches: [1]
+ - expr: "double_infinity: infinity"
+ matches: [1]
+
+ - expr: "double_infinity > 100"
+ matches: [1]
+ - expr: "double_infinity != -Infinity"
+ matches: [1]
+ skip: true
+
+ - expr: "double_infinity = -Infinity"
+ matches: []
+ skip: true
+
+ - expr: "double_infinity = NaN"
+ matches: []
+ - expr: 'double_infinity = "infinity"'
+ matches: [1]
+
+ - expr: 'double_infinity != "-Infinity"'
+ matches: [1]
+ - expr: 'double_infinity = "-infinity"'
+ matches: []
+
+ - expr: 'double_infinity = "NaN"'
+ matches: []
+
+ - expr: "double_negative_infinity = -Infinity"
+ matches: [1]
+ skip: true
+
+ - expr: "double_negative_infinity: -infinity"
+ matches: [1]
+ skip: true
+
+ - expr: "double_negative_infinity < 100"
+ matches: [1]
+ - expr: "double_negative_infinity != Infinity"
+ matches: [1]
+ - expr: "double_negative_infinity = Infinity"
+ matches: []
+ - expr: "double_negative_infinity = NaN"
+ matches: []
+ - expr: 'double_negative_infinity = "-Infinity"'
+ matches: [1]
+ - expr: 'double_negative_infinity != "Infinity"'
+ matches: [1]
+ - expr: 'double_negative_infinity = "infinity"'
+ matches: []
+
+ - expr: 'double_negative_infinity = "NaN"'
+ matches: []
+
+ - expr: "double_nan:nan"
+ matches: [1]
+
+ - expr: "double_nan = NaN"
+ matches: [1]
+ - expr: "double_nan < 100"
+ matches: []
+ - expr: "double_nan = 100"
+ matches: []
+ - expr: "double_nan > 100"
+ matches: []
+
+ - description: BoolField
+
+ - expr: "boolField = true"
+ matches: [1]
+ - expr: "boolField > false"
+ matches: [1]
+ - expr: "boolField >= false"
+ matches: [1]
+ - expr: "boolField <= true"
+ matches: [1]
+ - expr: "boolField = tRUe"
+ matches: [1]
+ - expr: "boolField = TRUE"
+ matches: [1]
+ - expr: "boolField:True"
+ matches: [1]
+ - expr: "boolField != False"
+ matches: [1]
+
+ - expr: "boolField = false"
+ matches: []
+ - expr: "boolField = faLSe"
+ matches: []
+ - expr: "boolField = FALSE"
+ matches: []
+ - expr: "boolField < false"
+ matches: []
+ - expr: "boolField:False"
+ matches: []
+ - expr: "boolField != True"
+ matches: []
+
+ - expr: "boolField = 0"
+ matches: []
+ - expr: "boolField = 1"
+ matches: [1]
+
+ - expr: "boolField = x"
+ error: operand type mismatch
+
+ - description: StringField
+
+ - expr: "stringField : Hello"
+ matches: [1]
+
+ - expr: "stringField :Hello"
+ matches: [1]
+
+ - expr: "stringField: Hello"
+ matches: [1]
+
+ - expr: "stringField:Hello"
+ matches: [1]
+
+ - expr: "stringField:hello"
+ matches: [1]
+
+ - expr: "stringField:bye OR stringField:hello"
+ matches: [1]
+
+ - expr: "stringField:world"
+ matches: [1]
+
+ - expr: "stringField:hello AND stringField:world"
+ matches: [1]
+
+ - expr: 'stringField="Hello beautiful world."'
+ matches: [1]
+ - expr: 'stringField = "Hello beautiful world."'
+ matches: [1]
+ - expr: 'stringField= "Hello beautiful world."'
+ matches: [1]
+ - expr: "stringField != Google"
+ matches: [1]
+ - expr: "stringField!= Google"
+ matches: [1]
+ - expr: "stringField !=Google"
+ matches: [1]
+ - expr: 'stringField >= "Hello beautiful world."'
+ matches: [1]
+ - expr: 'stringField <= "Hello beautiful world."'
+ matches: [1]
+ - expr: 'stringField:"\150\145\154\154\157"'
+ matches: [1]
+
+ - expr: 'stringField:"\x48\x65\x6C\x6C\x6F"'
+ matches: [1]
+
+ - expr: 'stringField: "\150\145\u013E\u013E\157"'
+ matches: [1]
+ skip: true
+
+ - expr: 'stringField:"Hello World"'
+ matches: []
+ - expr: "stringField:foo"
+ matches: []
+ - expr: "stringField:hello AND stringField:bar"
+ matches: []
+ - expr: "stringField:bye OR stringField:foo"
+ matches: []
+ - expr: 'stringField!= "Hello beautiful world."'
+ matches: []
+ - expr: 'stringField != "Hello beautiful world."'
+ matches: []
+ - expr: "stringField = Hello"
+ matches: []
+ - expr: 'stringField < "Hello beautiful world."'
+ matches: []
+ - expr: 'stringField > "Hello beautiful world."'
+ matches: []
+
+ - description: unicode
+
+ - expr: 'unicode_pathe: Pathe\u0301'
+ matches: []
+ - expr: 'unicode_pathe: "Pathe\u0301"'
+ matches: [1]
+ skip: true
+ - expr: "unicode_pathe: Pathé"
+ matches: [1]
+ skip: true
+ - expr: "unicode_pathe: Pathe"
+ matches: [1]
+ skip: true
+
+ - expr: "unicode_pathe: pAtHe"
+ matches: [1]
+ skip: true
+
+ - expr: "unicode_resume: Résumé"
+ matches: [1]
+ - expr: "unicode_resume: Resume"
+ matches: [1]
+ skip: true
+
+ - expr: "unicode_resume: rEsUmE"
+ matches: [1]
+ skip: true
+
+ - expr: "unicode_unicode: Ṳᾔḯ¢◎ⅾℯ"
+ matches: [1]
+ - expr: "unicode_unicode: U*"
+ matches: [1]
+ skip: true
+
+ - expr: "unicode_unicode: u*"
+ matches: [1]
+ skip: true
+
+ - description: invalid UTF-8 (TBD)
+
+ - description: EscapedValues
+
+ - expr: 'bytes_field:"\370\371\372\373\374\375\376\377"'
+ matches: [1]
+
+ - expr: 'bytes_field="\370\371\372\373\374\375\376\377"'
+ matches: [1]
+
+ - expr: 'bytes_field:"\370"'
+ matches: [1]
+ skip: true
+
+ - expr: 'bytes_field="\370"'
+ matches: []
+ - expr: 'bytes_field: "\372\373"'
+ matches: [1]
+ skip: true
+
+ - expr: 'bytes_field: "\374"'
+ matches: [1]
+ skip: true
+
+ - expr: 'bytes_field: "\360"'
+ matches: []
+ - expr: 'bytes_field:"\370\371\372\373\374\375\376"'
+ matches: [1]
+ skip: true
+
+ - expr: 'bytes_field="\370\371\372\373\374\375\376"'
+ matches: []
+
+ - description: escaped values
+
+ - expr: 'repeatedStringField:Apple'
+ matches: [1]
+ - expr: 'repeatedStringField:"Apple"'
+ matches: [1]
+ - expr: 'repeatedStringField:apple'
+ matches: [1]
+ - expr: 'repeatedStringField:"apple"'
+ matches: [1]
+ - expr: 'repeatedStringField:cat'
+ matches: [1]
+
+ - expr: 'repeatedStringField:dog'
+ matches: [1]
+
+ - expr: 'repeatedStringField:elephant'
+ matches: [1]
+
+ - expr: 'repeatedStringField:ant'
+ matches: []
+
+ - expr: 'repeatedStringField:"sl\\as\\\\h"'
+ matches: [1]
+ - expr: 'repeatedStringField:"qu\"ot\"es"'
+ matches: [1]
+
+ - expr: 'repeatedStringField:"t,e:s=[tin]+g~o.nly"'
+ matches: [1]
+ - expr: 'repeatedStringField:"t,e:s=+g~o.nly"'
+ matches: []
+ - expr: 'repeatedStringField:"t,e:s=[tin]+g~o.n*"'
+ matches: [1]
+
+ - expr: 'repeatedStringField:"t,e:s=t<i>n+g~o.nly"'
+ matches: [1]
+ - expr: 'repeatedStringField:"t,e:s=t<i>n+g~o.n*"'
+ matches: [1]
+
+ - expr: 'repeatedStringField:"t,e:s=tn+g~o.nly"'
+ matches: [1]
+ skip: true
+
+ - expr: 'repeatedStringField:"t,e:s=tn+g~o.n*"'
+ matches: [1]
+ skip: true
+
+ - expr: 'repeatedStringField:"t,e:s=tin+g~m.at*"'
+ matches: [1]
+ - expr: 'repeatedStringField:"t,e:s=tin+g~m.at*ch"'
+ error: suffix matching not supported
+ skip: true
+
+ - description: Any (TBD)
+
+ - description: repeated Any (TBD)
+
+ - description: OneOf
+
+ - expr: "kind.stringValue: OneOfAKind"
+ matches: [1]
+ skip: true
+
+ - expr: "kind.string_value=OneOfAKind"
+ matches: [1]
+ skip: true
+
+ - expr: "OneOfAKind"
+ matches: [1]
+
+ - expr: "string_value:*"
+ matches: [1]
+
+ - expr: "stringValue:*"
+ matches: [1]
+
+ - expr: "nested_value:*"
+ matches: []
+
+ - expr: "nestedValue:*"
+ matches: []
+
+ - description: Negation
+
+ - expr: "-stringField:Google"
+ matches: [1]
+ - expr: "NOT stringField:Google"
+ matches: [1]
+ - expr: "stringField : (NOT Google)"
+ matches: [1]
+
+ - expr: "-stringField:Hello"
+ matches: []
+
+ - expr: "NOT stringField:Hello"
+ matches: []
+
+ - expr: "stringField:(NOT Hello)"
+ matches: []
+
+ - description: ComplexRHS
+
+ - expr: "stringField:(Hello World)"
+ matches: [1]
+
+ - expr: "stringField:(Hello OR Goodbye)"
+ matches: [1]
+
+ - expr: "stringField:(Hello OR (Goodbye OR Bye))"
+ matches: [1]
+
+ - expr: "enumField = (VALUE_2 OR VALUE_3)"
+ matches: [1]
+
+ - expr: "stringField:(Hello AND Goodbye)"
+ matches: []
+
+ - expr: "stringField:(Hello Goodbye)"
+ matches: []
+
+ - expr: "stringField:(Hello:Goodbye)"
+ error: terms not supported in operand subexpression
+
+ - expr: "stringField=(A OR foo=bar)"
+ error: terms not supported in operand subexpression
+
+ - description: EnumField
+
+ - expr: "enumField = VALUE_2"
+ matches: [1]
+ - expr: "enumField = value_2"
+ matches: [1]
+ - expr: "enumField != VALUE_3"
+ matches: [1]
+ - expr: "enumField < VALUE_MAX"
+ matches: [1]
+ - expr: "enumField > VALUE_1"
+ matches: [1]
+ - expr: "enumField > VALUE_1 AND enumField < VALUE_MAX"
+ matches: [1]
+
+ - expr: "enumField=VALUE_1"
+ matches: []
+ - expr: "enumField=VALUE_X"
+ error: unknown enum identifier
+
+ - description: OutOfOrderEnumField
+
+ - expr: "outOfOrderEnumField = VALUE_A"
+ matches: [1]
+ - expr: "outOfOrderEnumField = value_a"
+ matches: [1]
+ - expr: "outOfOrderEnumField != VALUE_B"
+ matches: [1]
+
+ - expr: "outOfOrderEnumField < VALUE_B"
+ matches: []
+ skip: true
+
+ - expr: "outOfOrderEnumField <= VALUE_B"
+ matches: []
+ skip: true
+
+ - expr: "outOfOrderEnumField > VALUE_B"
+ matches: [1]
+ skip: true
+
+ - expr: "outOfOrderEnumField >= VALUE_B"
+ matches: [1]
+ skip: true
+
+ - expr: "outOfOrderEnumField < VALUE_A"
+ matches: []
+ - expr: "outOfOrderEnumField <= VALUE_A"
+ matches: [1]
+ - expr: "outOfOrderEnumField > VALUE_A"
+ matches: []
+ - expr: "outOfOrderEnumField >= VALUE_A"
+ matches: [1]
+
+ - description: GlobalSearch
+
+ - expr: "hello world"
+ matches: [1]
+
+ - expr: "hello bar"
+ matches: [1]
+
+ - expr: "BAZ"
+ matches: [1]
+
+ - expr: "1001"
+ matches: [1]
+
+ - expr: "123435"
+ matches: [1]
+
+ - expr: "1002"
+ matches: []
+
+ - expr: "123436"
+ matches: []
+
+ - expr: "nested"
+ matches: []
+
+ - expr: "Googley"
+ matches: []
+
+ - description: NestedFields
+
+ - expr: "nested.uint32Field = 123435"
+ matches: [1]
+ - expr: "nested.uint32Field > 123434"
+ matches: [1]
+ - expr: "nested.uint32Field < 123436"
+ matches: [1]
+
+ - expr: "nested:*"
+ matches: [1]
+
+ - expr: "nested=12345"
+ error: only :* supported on structs
+
+ - expr: "nested.deeperNest:*"
+ matches: [1]
+
+ - expr: "nested.deeperNest=12345"
+ error: only :* supported on structs
+
+ - description: DeepNestedFields
+
+ - expr: "nested.deeperNest.niceField:foo"
+ matches: [1]
+
+ - expr: "nested.deeperNest.niceField:bAr"
+ matches: [1]
+
+ - expr: 'nested.deeperNest.niceField:"foo bar"'
+ matches: [1]
+ - expr: 'nested.deeperNest.nicerField:"foobar"'
+ matches: [1]
+
+ - expr: "nested.deeperNest.niceField.nonexistent:foo"
+ error: unknown field
+
+ - description: RepeatedFields
+
+ - expr: "repeatedInt32Field > 100"
+ matches: [1]
+
+ - expr: "repeatedInt32Field: *"
+ matches: [1]
+ - expr: "repeatedStringField:Elephant"
+ matches: [1]
+
+ - expr: "dog"
+ matches: [1]
+
+ - expr: "apple dog"
+ matches: [1]
+
+ - expr: '"cat dog"'
+ matches: [1]
+
+ - expr: "repeatedEnumField=(VALUE_3 AND VALUE_4)"
+ matches: [1]
+
+ - expr: "repeatedEnumField != VALUE_2"
+ matches: [1]
+
+ - expr: "repeatedEnumField=VALUE_3"
+ matches: [1]
+
+ - expr: "repeatedEnumField!=VALUE_3"
+ matches: [1]
+
+ - expr: "repeatedMessageField.uint32_field = 5555"
+ matches: [1]
+
+ - expr: "repeatedMessageField.uint32_field = 7777"
+ matches: [1]
+
+ - expr: "repeatedMessageField.deeperNest.niceField = nice55"
+ matches: [1]
+
+ - expr: "repeatedMessageField.deeperNest.niceField = nice66"
+ matches: [1]
+
+ - expr: "repeatedInt32Field > 101"
+ matches: []
+
+ - expr: "repeatedStringField:Fish"
+ matches: []
+ - expr: "dogs"
+ matches: []
+
+ - expr: "apples dog"
+ matches: []
+
+ - expr: '"ball dog"'
+ matches: []
+
+ - expr: "repeatedEnumField=(VALUE_3 AND NOT VALUE_4)"
+ matches: []
+
+ - expr: "NOT repeatedEnumField=VALUE_3"
+ matches: []
+
+ - expr: "nested.repeatedEmptyUint32: 100"
+ matches: []
+ - expr: "nested.repeatedEmptyUint32: *"
+ matches: []
+ - expr: "repeatedEmptyMessageField.uint32Field: 100"
+ matches: []
+ - expr: "repeatedEmptyMessageField.repeatedEmptyUint32:*"
+ matches: []
+ - expr: "repeatedEmptyMessageField.repeatedEmptyUint32: 100"
+ matches: []
+ - expr: "repeatedMessageField.deeperNest.niceField = nice77"
+ matches: []
+
+ - expr: "repeatedMessageField:*"
+ matches: [1]
+
+ - expr: "repeatedMessageField = 5555"
+ error: only :* supported on structs
+
+ - expr: "repeatedMessageField.deeperNest = 5555"
+ error: only :* supported on structs
+
+ - expr: "repeatedMessageField.uint32_field = foo(5555)"
+ error: operand functions not supported
+
+ - description: PresenceTest
+
+ - expr: "stringField: *"
+ matches: [1]
+ - expr: "repeatedMessageField.stringField: *"
+ matches: [1]
+ - expr: "map_int32_int32_field.1234:*"
+ matches: [1]
+ - expr: "map_string_int32_field.foobar:*"
+ matches: [1]
+
+ - expr: "noValueField: *"
+ matches: []
+
+ - expr: "repeatedMessageField.noValueField: *"
+ matches: []
+
+ - expr: "map_string_int32_field.foobaz:*"
+ matches: []
+ - expr: "map_int32_int32_field.1236:*"
+ matches: []
+
+ - description: EmptyValue
+
+ - expr: 'noValueField=""'
+ matches: [1]
+ - expr: "noValueField!=foo"
+ matches: [1]
+ - expr: "noValueField=foo"
+ matches: []
+
+ - description: MapFields
+
+ - expr: "map_string_int32_field.foobar>9800"
+ matches: [1]
+ - expr: "map_string_int32_field.foobar<9877"
+ matches: [1]
+ - expr: "map_string_int32_field.barFoo>1200"
+ matches: [1]
+ - expr: 'map_string_int32_field."foobar.abcd":9877'
+ matches: [1]
+
+ - expr: "map_int32_int32_field.1234 = 4321"
+ matches: [1]
+ - expr: 'map_int32_int32_field."1235" = 5321'
+ matches: [1]
+
+ - expr: "map_string_nested_field.foobar.uint32_field = 1111"
+ matches: [1]
+
+ - expr: "map_string_nested_field.foobar.deeper_nest.nice_field = nice11"
+ matches: [1]
+
+ - expr: "9876"
+ matches: [1]
+
+ - expr: "nice11"
+ matches: [1]
+
+ - expr: "map_string_int32_field.foobar>9876"
+ matches: []
+ - expr: "map_string_int32_field.foobar!=9876"
+ matches: []
+ - expr: "map_string_int32_field.barfoo>1200"
+ matches: []
+
+ - expr: 'map_string_int32_field."foobar.abcd"<9877'
+ matches: []
+
+ - expr: "map_int32_int32_field.1236 = 4321"
+ matches: []
+ - expr: "map_int32_int32_field.1234 = 4320"
+ matches: []
+ - expr: 'map_int32_int32_field."1235" = 5320'
+ matches: []
+
+ - description: Label keys are case-sensitive.
+
+ - expr: "map_string_int32_field.fooBAR>9800"
+ matches: []
+
+ - expr: "map_string_int32_field = 5555"
+ error: only :* supported on structs
+
+ - description: TimeStampTests
+
+ - expr: 'timestamp = "1970-01-01T00:00:10Z"'
+ matches: [1]
+
+ - expr: 'timestamp = "1970-01-01t00:00:10z"'
+ matches: [1]
+ skip: true
+
+ - expr: 'timestamp = "1970-01-01T05:45:10+05:45"'
+ matches: [1]
+
+ - expr: 'timestamp = "1970-01-01T00:00:10.000000000Z"'
+ matches: [1]
+
+ - expr: 'timestamp < "1970-01-01T00:00:10.000000001Z"'
+ matches: [1]
+
+ - expr: 'timestamp != "1970-01-01T00:00:11Z"'
+ matches: [1]
+
+ - expr: 'timestamp > "1970-01-01T00:00:09Z"'
+ matches: [1]
+
+ - expr: 'timestamp >= "1970-01-01T00:00:10Z"'
+ matches: [1]
+
+ - expr: 'timestamp < "1970-01-01T00:00:11Z"'
+ matches: [1]
+
+ - expr: 'timestamp <= "1970-01-01T00:00:10Z"'
+ matches: [1]
+
+ - expr: 'timestamp:"1970-01-01T00:00:10Z"'
+ matches: [1]
+
+ - expr: 'timestamp = "1970-01-01T00:00:11Z"'
+ matches: []
+
+ - expr: 'timestamp != "1970-01-01T00:00:10Z"'
+ matches: []
+
+ - expr: 'timestamp > "1970-01-01T00:00:10Z"'
+ matches: []
+
+ - expr: 'timestamp >= "1970-01-01T00:00:11Z"'
+ matches: []
+
+ - expr: 'timestamp < "1970-01-01T00:00:10Z"'
+ matches: []
+
+ - expr: 'timestamp <= "1970-01-01T00:00:09Z"'
+ matches: []
+
+ - expr: 'timestamp:"1970-01-01T00:00:01Z"'
+ matches: []
+
+ - expr: 'timestamp < 1970-01-01T00:00:11Z'
+ matches: [1]
+ skip: true
+
+ - expr: 'timestamp <= 1970-01-01T00:00:10Z'
+ matches: [1]
+ skip: true
+
+ - expr: 'timestamp = 1970-01-01T00:00:10Z'
+ matches: [1]
+ skip: true
+
+ - expr: 'timestamp >= 1970-01-01T00:00:10Z'
+ matches: [1]
+ skip: true
+
+ - expr: 'timestamp > 1970-01-01T00:00:09Z'
+ matches: [1]
+ skip: true
+
+ - expr: 'timestamp.seconds = 10'
+ matches: [1]
+ skip: true
+
+ - description: DurationTests
+
+ - expr: "duration = 5s"
+ matches: [1]
+
+ - expr: "duration = 5S"
+ matches: [1]
+
+ - expr: "duration = 5.0s"
+ matches: [1]
+
+ - expr: "duration = 5.s"
+ matches: [1]
+
+ - expr: "duration < 5.000000001s"
+ matches: [1]
+
+ - expr: "duration = 5.0000000001s"
+ matches: [1]
+
+ - expr: "duration != 4s"
+ matches: [1]
+
+ - expr: "duration > 4s"
+ matches: [1]
+
+ - expr: "duration >= 5s"
+ matches: [1]
+
+ - expr: "duration < 6s"
+ matches: [1]
+
+ - expr: "duration > -6s"
+ matches: [1]
+
+ - expr: "duration > .1s"
+ matches: [1]
+
+ - expr: "duration <= 5s"
+ matches: [1]
+
+ - expr: "duration:5s"
+ matches: [1]
+
+ - expr: "duration = 5000.0ms"
+ matches: [1]
+
+ - expr: "duration > 10ms"
+ matches: [1]
+
+ - expr: "duration = -5.0s"
+ matches: []
+
+ - expr: "duration = 6s"
+ matches: []
+
+ - expr: "duration != 5s"
+ matches: []
+
+ - expr: "duration > 5s"
+ matches: []
+
+ - expr: "duration >= 6s"
+ matches: []
+
+ - expr: "duration < 5s"
+ matches: []
+
+ - expr: "duration <= 4s"
+ matches: []
+
+ - expr: "duration:1s"
+ matches: []
+
+ - expr: "duration:5"
+ error: invalid duration literal
+
+ - expr: "duration.seconds:5"
+ matches: [1]
+ - expr: "duration.seconds:1"
+ matches: []
+ - expr: 'duration = "1970-01-01T00:00:10Z"'
+ error: invalid duration literal
+
+ - description: StructFields
+
+ - expr: "struct_field.foo = bar"
+ matches: [1]
+
+ - expr: "struct_field.fooBar = xyz"
+ matches: [1]
+
+ - expr: 'struct_field."foo.foo" = bar.bar'
+ matches: [1]
+
+ - expr: "struct_field.foo_number = 12345"
+ matches: [1]
+
+ - expr: "struct_field.foo_bool = true"
+ matches: [1]
+
+ - expr: "soccer"
+ matches: [1]
+
+ - expr: "struct_field.foo = barz"
+ matches: []
+
+ - expr: "struct_field.foobar = xyz"
+ matches: []
+
+ - expr: 'struct_field."foo.foo" = bar'
+ matches: []
+
+ - expr: "struct_field.foo_number > 12345"
+ matches: []
+
+ - expr: "struct_field.foo_bool = false"
+ matches: []
+
+ - description: Global restriction.
+
+ - expr: "beautiful"
+ matches: [1]
+
+ - description: restricted field data not present client side
+
+ - expr: "internalField:*"
+ error: cannot access restricted field
+ skip: true
+
+ - expr: "topsecret"
+ matches: []
+ skip: true
+
+ - description: deprecated fields are visible
+
+ - expr: "deprecatedField:*"
+ matches: [1]
+
+ - expr: "/"
+ matches: [1]
+
+ - expr: "//"
+ matches: [1]
+
+ - expr: "repeated_string_field:/"
+ matches: [1]
+
+ - expr: "repeated_string_field://"
+ matches: [1]
+
+ - expr: "repeated_string_field:\"//foo.bar/baz\""
+ matches: [1]
+
+ - expr: "repeated_string_field:\"//foo.bar/baz*\""
+ matches: [1]
+
+ - expr: "url_field:\"//foo.bar/baz\""
+ matches: [1]
+
+ - expr: "url_field: \"http://foo.bar/baz*\""
+ matches: [1]
+
+ - expr: "/foo AND \"/foo.bar/\""
+ matches: [1]