| // Copyright 2024 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package filter |
| |
| import ( |
| "encoding/json" |
| "fmt" |
| "os" |
| "path/filepath" |
| "slices" |
| "strconv" |
| "strings" |
| "testing" |
| "time" |
| |
| "gopkg.in/yaml.v3" |
| ) |
| |
| // yamlTests holds the contents of a testdata/*_test.yaml file. |
| type yamlTests struct { |
| Tests []yamlTest `yaml:"tests"` |
| } |
| |
| // yamlTest holds the contents of a single test. |
| type yamlTest struct { |
| Description string `yaml:"description"` |
| Expr string `yaml:"expr"` |
| Error string `yaml:"error"` |
| Matches []int `yaml:"matches"` |
| Skip bool `yaml:"skip"` |
| } |
| |
| type filterBasicTestData struct { |
| Resources []resource `json:"resources"` |
| } |
| |
| type resource struct { |
| BoolField bool `json:"bool_field"` |
| CaseField string `json:"case_field"` |
| IntField int64 `json:"int_field"` |
| FloatField float64 `json:"float_field"` |
| EnumField string `json:"enum_field"` |
| StringField string `json:"string_field"` |
| TimestampField time.Time `json:"timestamp_field"` |
| Compound compound `json:"compound"` |
| False bool `json:"false"` |
| True bool `json:"true"` |
| Undefined *string `json:"undefined"` |
| Text string `json:"text"` |
| URL string `json:"url"` |
| Members []member `json:"members"` |
| Logical string `json:"logical"` |
| None *string `json:"none"` |
| QuoteDouble string `json:"quote_double"` |
| QuoteSingle string `json:"quote_single"` |
| Subject string `json:"subject"` |
| Words string `json:"words"` |
| UnicodeField string `json:"unicode_field"` |
| ExistsScalarInt int64 `json:"exists_scalar_int"` |
| ExistsScalarString string `json:"exists_scalar_string"` |
| ExistsScalarMessage member `json:"exists_scalar_message"` |
| ExistsArrayInt []int64 `json:"exists_array_int"` |
| ExistsArrayString []string `json:"exists_array_string"` |
| ExistsArrayMessage []member `json:"exists_array_message"` |
| } |
| |
| type member struct { |
| First string `json:"first"` |
| Last string `json:"last"` |
| ID int64 `json:"id"` |
| } |
| |
| type compound struct { |
| IntField int64 `json:"int_field"` |
| StringField string `json:"string_field"` |
| Num numberObjects `json:"num"` |
| Str stringObjects `json:"str"` |
| } |
| |
| type numberObjects struct { |
| Array []float64 `json:"array"` |
| Dictionary map[string]float64 `json:"dictionary"` |
| } |
| |
| type stringObjects struct { |
| Array []string `json:"array"` |
| Dictionary map[string]string `json:"dictionary"` |
| Value string `json:"value"` |
| } |
| |
| func TestEvalBasic(t *testing.T) { |
| var data filterBasicTestData |
| unmarshalJSON(t, "basic_test.json", &data) |
| |
| var tests yamlTests |
| unmarshalYAML(t, "basic_test.yaml", &tests) |
| |
| 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 |
| var idx int |
| for _, test := range tests { |
| if test.Description != "" { |
| desc = test.Description |
| idx = 1 |
| if test.Expr == "" { |
| continue |
| } |
| } |
| |
| 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 |
| } |
| |
| t.Run(fmt.Sprintf("%s %d", desc, idx), func(t *testing.T) { |
| runOneTest(t, &test, data) |
| }) |
| |
| idx++ |
| } |
| } |
| |
| // runOneTest runs on YAML test. |
| func runOneTest[T any](t *testing.T, test *yamlTest, data []T) { |
| e, err := ParseFilter(test.Expr) |
| if err != nil { |
| if test.Error != "" { |
| t.Logf("parse of %q failed (%v) when error is expected (%s)", test.Expr, err, test.Error) |
| return |
| } |
| t.Fatalf("can't parse %q: %v", test.Expr, err) |
| } |
| |
| eval, msgs := Evaluator[T](e, nil) |
| |
| if len(msgs) > 0 { |
| if test.Error != "" { |
| t.Logf("evaluation of %q failed when error is expected (%s)", test.Expr, test.Error) |
| } else { |
| t.Errorf("%d messages reported when building evaluator for %q", len(msgs), test.Expr) |
| } |
| |
| for _, msg := range msgs { |
| t.Log(msg) |
| } |
| |
| if test.Error != "" { |
| return |
| } |
| } |
| |
| var matches []int |
| for i, d := range data { |
| if eval(d) { |
| matches = append(matches, i+1) |
| } |
| } |
| |
| if !slices.Equal(matches, test.Matches) { |
| t.Errorf("got matches %v, want %v", matches, test.Matches) |
| } |
| } |
| |
| // unmarshalJSON reads JSON encoded data from a testdata file into v. |
| func unmarshalJSON(t *testing.T, filename string, v any) { |
| f, err := os.Open(filepath.Join("testdata", filename)) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer f.Close() |
| |
| dec := json.NewDecoder(f) |
| dec.DisallowUnknownFields() |
| if err := dec.Decode(v); err != nil { |
| t.Fatal(err) |
| } |
| } |
| |
| // unmarshalYAML reads YAML encoded data from a testdata file into v. |
| func unmarshalYAML(t *testing.T, filename string, v any) { |
| f, err := os.Open(filepath.Join("testdata", filename)) |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer f.Close() |
| |
| dec := yaml.NewDecoder(f) |
| dec.KnownFields(true) |
| if err := dec.Decode(v); err != nil { |
| 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) |
| } |
| } |
| } |
| } |