From 87e7d4d2cfee160256e7504b6297d87b133953d0 Mon Sep 17 00:00:00 2001 From: thediveo Date: Wed, 20 Nov 2024 21:42:24 +0100 Subject: [PATCH] new: make collection-related matchers Go 1.23 iterator aware - new internal helper package for dealing with Go 1.23 iterators via reflection; for Go versions before 1.23 this package provides the same helper functions as stubs instead, shielding both the matchers code base as well as their tests from any code that otherwise would not build on pre-iterator versions. This allows to keep new iterator-related matcher code and associated tests inline, hopefully ensuring good maintainability. - with the exception of ContainElements and ConsistOf, the other iterator-aware matchers do not need to go through producing all collection elements first in order to work on a slice of these elements. Instead, they directly work on the collection elements individually as their iterator produces them. - BeEmpty: iter.Seq, iter.Seq2 w/ tests - HaveLen: iter.Seq, iter.Seq2 w/ tests - HaveEach: iter.Seq, iter.Seq2 w/ tests - ContainElement: iter.Seq, iter.Seq2 w/ tests - HaveExactElements: iter.Seq, iter.Seq2 w/ tests - ContainElements: iter.Seq, iter.Seq2 w/ tests - ConsistOf: iter.Seq, iter.Seq2 w/ test - HaveKey: iter.Seq2 only w/ test - HaveKeyWithValue: iter.Seq2 only w/ test - updated documentation. Signed-off-by: thediveo --- docs/index.md | 59 ++++- matchers/be_empty_matcher.go | 16 +- matchers/be_empty_matcher_test.go | 29 +++ matchers/consist_of.go | 32 ++- matchers/consist_of_test.go | 39 +++ matchers/contain_element_matcher.go | 239 +++++++++++++----- matchers/contain_element_matcher_test.go | 214 ++++++++++++++++ matchers/contain_elements_matcher.go | 5 +- matchers/contain_elements_matcher_test.go | 37 +++ matchers/have_each_matcher.go | 40 ++- matchers/have_each_matcher_test.go | 64 +++++ matchers/have_exact_elements.go | 53 +++- matchers/have_exact_elements_test.go | 149 +++++++++++ matchers/have_key_matcher.go | 19 +- matchers/have_key_matcher_test.go | 46 ++++ matchers/have_key_with_value_matcher.go | 26 +- matchers/have_key_with_value_matcher_test.go | 56 ++++ matchers/have_len_matcher.go | 2 +- matchers/have_len_matcher_test.go | 30 +++ matchers/internal/miter/miter_suite_test.go | 13 + matchers/internal/miter/type_support_iter.go | 128 ++++++++++ .../internal/miter/type_support_iter_test.go | 211 ++++++++++++++++ .../internal/miter/type_support_noiter.go | 44 ++++ matchers/iter_support_test.go | 55 ++++ matchers/type_support.go | 13 + 25 files changed, 1527 insertions(+), 92 deletions(-) create mode 100644 matchers/internal/miter/miter_suite_test.go create mode 100644 matchers/internal/miter/type_support_iter.go create mode 100644 matchers/internal/miter/type_support_iter_test.go create mode 100644 matchers/internal/miter/type_support_noiter.go create mode 100644 matchers/iter_support_test.go diff --git a/docs/index.md b/docs/index.md index c6449b254..ca870ab28 100644 --- a/docs/index.md +++ b/docs/index.md @@ -690,6 +690,8 @@ A number of community-supported matchers have appeared as well. A list is maint These docs only go over the positive assertion case (`Should`), the negative case (`ShouldNot`) is simply the negation of the positive case. They also use the `Ω` notation, but - as mentioned above - the `Expect` notation is equivalent. +When using Go toolchain of version 1.23 or later, certain matchers as documented below become iterator-aware, handling iterator functions with `iter.Seq` and `iter.Seq2`-like signatures as collections in the same way as array/slice/map. + ### Asserting Equivalence #### Equal(expected interface{}) @@ -1114,7 +1116,7 @@ It is an error for either `ACTUAL` or `EXPECTED` to be invalid YAML. Ω(ACTUAL).Should(BeEmpty()) ``` -succeeds if `ACTUAL` is, in fact, empty. `ACTUAL` must be of type `string`, `array`, `map`, `chan`, or `slice`. It is an error for it to have any other type. +succeeds if `ACTUAL` is, in fact, empty. `ACTUAL` must be of type `string`, `array`, `map`, `chan`, or `slice`. Starting with Go 1.23, `ACTUAL` can be also an iterator assignable to `iter.Seq` or `iter.Seq2`. It is an error for `ACTUAL` to have any other type. #### HaveLen(count int) @@ -1122,7 +1124,7 @@ succeeds if `ACTUAL` is, in fact, empty. `ACTUAL` must be of type `string`, `arr Ω(ACTUAL).Should(HaveLen(INT)) ``` -succeeds if the length of `ACTUAL` is `INT`. `ACTUAL` must be of type `string`, `array`, `map`, `chan`, or `slice`. It is an error for it to have any other type. +succeeds if the length of `ACTUAL` is `INT`. `ACTUAL` must be of type `string`, `array`, `map`, `chan`, or `slice`. Starting with Go 1.23, `ACTUAL` can be also an iterator assignable to `iter.Seq` or `iter.Seq2`. It is an error for `ACTUAL` to have any other type. #### HaveCap(count int) @@ -1145,7 +1147,7 @@ or ``` -succeeds if `ACTUAL` contains an element that equals `ELEMENT`. `ACTUAL` must be an `array`, `slice`, or `map` -- anything else is an error. For `map`s `ContainElement` searches through the map's values (not keys!). +succeeds if `ACTUAL` contains an element that equals `ELEMENT`. `ACTUAL` must be an `array`, `slice`, or `map`. Starting with Go 1.23, `ACTUAL` can be also an iterator assignable to `iter.Seq` or `iter.Seq2`. It is an error for it to have any other type. For `map`s `ContainElement` searches through the map's values and not the keys. Similarly, for an iterator assignable to `iter.Seq2` `ContainElement` searches through the `v` elements of the produced (_, `v`) pairs. By default `ContainElement()` uses the `Equal()` matcher under the hood to assert equality between `ACTUAL`'s elements and `ELEMENT`. You can change this, however, by passing `ContainElement` a `GomegaMatcher`. For example, to check that a slice of strings has an element that matches a substring: @@ -1176,6 +1178,34 @@ var findings map[int]string }).Should(ContainElement(ContainSubstring("foo"), &findings)) ``` +In case of `iter.Seq` and `iter.Seq2`-like iterators, the matching contained elements can be returned in the slice referenced by the pointer. + +```go +it := func(yield func(string) bool) { + for _, element := range []string{"foo", "bar", "baz"} { + if !yield(element) { + return + } + } +} +var findings []string +Ω(it).Should(ContainElement(HasPrefix("ba"), &findings)) +``` + +Only in case of `iter.Seq2`-like iterators, the matching contained pairs can also be returned in the map referenced by the pointer. A (k, v) pair matches when it's "v" value matches. + +```go +it := func(yield func(int, string) bool) { + for key, element := range []string{"foo", "bar", "baz"} { + if !yield(key, element) { + return + } + } +} +var findings map[int]string +Ω(it).Should(ContainElement(HasPrefix("ba"), &findings)) +``` + #### ContainElements(element ...interface{}) ```go @@ -1197,7 +1227,7 @@ By default `ContainElements()` uses `Equal()` to match the elements, however cus Ω([]string{"Foo", "FooBar"}).Should(ContainElements(ContainSubstring("Bar"), "Foo")) ``` -Actual must be an `array`, `slice` or `map`. For maps, `ContainElements` matches against the `map`'s values. +Actual must be an `array`, `slice` or `map`. Starting with Go 1.23, `ACTUAL` can be also an iterator assignable to `iter.Seq` or `iter.Seq2`. For maps, `ContainElements` matches against the `map`'s values. Similarly, for an iterator assignable to `iter.Seq2` `ContainElements` searches through the `v` elements of the produced (_, `v`) pairs. You typically pass variadic arguments to `ContainElements` (as in the examples above). However, if you need to pass in a slice you can provided that it is the only element passed in to `ContainElements`: @@ -1208,6 +1238,8 @@ is the only element passed in to `ContainElements`: Note that Go's type system does not allow you to write this as `ContainElements([]string{"FooBar", "Foo"}...)` as `[]string` and `[]interface{}` are different types - hence the need for this special rule. +Starting with Go 1.23, you can also pass in an iterator assignable to `iter.Seq` (but not `iter.Seq2`) as the only element to `ConsistOf`. + The difference between the `ContainElements` and `ConsistOf` matchers is that the latter is more restrictive because the `ConsistOf` matcher checks additionally that the `ACTUAL` elements and the elements passed into the matcher have the same length. #### BeElementOf(elements ...interface{}) @@ -1263,10 +1295,9 @@ By default `ConsistOf()` uses `Equal()` to match the elements, however custom ma Ω([]string{"Foo", "FooBar"}).Should(ConsistOf(ContainSubstring("Foo"), ContainSubstring("Foo"))) ``` -Actual must be an `array`, `slice` or `map`. For maps, `ConsistOf` matches against the `map`'s values. +Actual must be an `array`, `slice` or `map`. Starting with Go 1.23, `ACTUAL` can be also an iterator assignable to `iter.Seq` or `iter.Seq2`. For maps, `ConsistOf` matches against the `map`'s values. Similarly, for an iterator assignable to `iter.Seq2` `ContainElement` searches through the `v` elements of the produced (_, `v`) pairs. -You typically pass variadic arguments to `ConsistOf` (as in the examples above). However, if you need to pass in a slice you can provided that it -is the only element passed in to `ConsistOf`: +You typically pass variadic arguments to `ConsistOf` (as in the examples above). However, if you need to pass in a slice you can provided that it is the only element passed in to `ConsistOf`: ```go Ω([]string{"Foo", "FooBar"}).Should(ConsistOf([]string{"FooBar", "Foo"})) @@ -1274,6 +1305,8 @@ is the only element passed in to `ConsistOf`: Note that Go's type system does not allow you to write this as `ConsistOf([]string{"FooBar", "Foo"}...)` as `[]string` and `[]interface{}` are different types - hence the need for this special rule. +Starting with Go 1.23, you can also pass in an iterator assignable to `iter.Seq` (but not `iter.Seq2`) as the only element to `ConsistOf`. + #### HaveExactElements(element ...interface{}) ```go @@ -1296,7 +1329,7 @@ Expect([]string{"Foo", "FooBar"}).To(HaveExactElements("Foo", ContainSubstring(" Expect([]string{"Foo", "FooBar"}).To(HaveExactElements(ContainSubstring("Foo"), ContainSubstring("Foo"))) ``` -Actual must be an `array` or `slice`. +`ACTUAL` must be an `array` or `slice`. Starting with Go 1.23, `ACTUAL` can be also an iterator assignable to `iter.Seq` (but not `iter.Seq2`). You typically pass variadic arguments to `HaveExactElements` (as in the examples above). However, if you need to pass in a slice you can provided that it is the only element passed in to `HaveExactElements`: @@ -1313,9 +1346,9 @@ Note that Go's type system does not allow you to write this as `HaveExactElement Ω(ACTUAL).Should(HaveEach(ELEMENT)) ``` -succeeds if `ACTUAL` solely consists of elements that equal `ELEMENT`. `ACTUAL` must be an `array`, `slice`, or `map` -- anything else is an error. For `map`s `HaveEach` searches through the map's values (not keys!). +succeeds if `ACTUAL` solely consists of elements that equal `ELEMENT`. `ACTUAL` must be an `array`, `slice`, or `map`. For `map`s `HaveEach` searches through the map's values, not its keys. Starting with Go 1.23, `ACTUAL` can be also an iterator assignable to `iter.Seq` or `iter.Seq2`. For `iter.Seq2` `HaveEach` searches through the `v` part of the yielded (_, `v`) pairs. -In order to avoid ambiguity it is an error for `ACTUAL` to be an empty `array`, `slice`, or `map` (or a correctly typed `nil`) -- in these cases it cannot be decided if `HaveEach` should match, or should not match. If in your test it is acceptable for `ACTUAL` to be empty, you can use `Or(BeEmpty(), HaveEach(ELEMENT))` instead. +In order to avoid ambiguity it is an error for `ACTUAL` to be an empty `array`, `slice`, or `map` (or a correctly typed `nil`) -- in these cases it cannot be decided if `HaveEach` should match, or should not match. If in your test it is acceptable for `ACTUAL` to be empty, you can use `Or(BeEmpty(), HaveEach(ELEMENT))` instead. Similar, an iterator not yielding any elements is also considered to be an error. By default `HaveEach()` uses the `Equal()` matcher under the hood to assert equality between `ACTUAL`'s elements and `ELEMENT`. You can change this, however, by passing `HaveEach` a `GomegaMatcher`. For example, to check that a slice of strings has an element that matches a substring: @@ -1329,7 +1362,7 @@ By default `HaveEach()` uses the `Equal()` matcher under the hood to assert equa Ω(ACTUAL).Should(HaveKey(KEY)) ``` -succeeds if `ACTUAL` is a map with a key that equals `KEY`. It is an error for `ACTUAL` to not be a `map`. +succeeds if `ACTUAL` is a map with a key that equals `KEY`. Starting with Go 1.23, `ACTUAL` can be also an iterator assignable to `iter.Seq2` and `HaveKey(KEY)` then succeeds if the iterator produces a (`KEY`, `_`) pair. It is an error for `ACTUAL` to have any other type than `map` or `iter.Seq2`. By default `HaveKey()` uses the `Equal()` matcher under the hood to assert equality between `ACTUAL`'s keys and `KEY`. You can change this, however, by passing `HaveKey` a `GomegaMatcher`. For example, to check that a map has a key that matches a regular expression: @@ -1343,7 +1376,7 @@ By default `HaveKey()` uses the `Equal()` matcher under the hood to assert equal Ω(ACTUAL).Should(HaveKeyWithValue(KEY, VALUE)) ``` -succeeds if `ACTUAL` is a map with a key that equals `KEY` mapping to a value that equals `VALUE`. It is an error for `ACTUAL` to not be a `map`. +succeeds if `ACTUAL` is a map with a key that equals `KEY` mapping to a value that equals `VALUE`. Starting with Go 1.23, `ACTUAL` can be also an iterator assignable to `iter.Seq2` and `HaveKeyWithValue(KEY)` then succeeds if the iterator produces a (`KEY`, `VALUE`) pair. It is an error for `ACTUAL` to have any other type than `map` or `iter.Seq2`. By default `HaveKeyWithValue()` uses the `Equal()` matcher under the hood to assert equality between `ACTUAL`'s keys and `KEY` and between the associated value and `VALUE`. You can change this, however, by passing `HaveKeyWithValue` a `GomegaMatcher` for either parameter. For example, to check that a map has a key that matches a regular expression and which is also associated with a value that passes some numerical threshold: @@ -1351,6 +1384,8 @@ By default `HaveKeyWithValue()` uses the `Equal()` matcher under the hood to ass Ω(map[string]int{"Foo": 3, "BazFoo": 4}).Should(HaveKeyWithValue(MatchRegexp(`.+Foo$`), BeNumerically(">", 3))) ``` +### Working with Structs + #### HaveField(field interface{}, value interface{}) ```go diff --git a/matchers/be_empty_matcher.go b/matchers/be_empty_matcher.go index 527c1a1c1..bd7f0b96e 100644 --- a/matchers/be_empty_matcher.go +++ b/matchers/be_empty_matcher.go @@ -4,17 +4,31 @@ package matchers import ( "fmt" + "reflect" "github.com/onsi/gomega/format" + "github.com/onsi/gomega/matchers/internal/miter" ) type BeEmptyMatcher struct { } func (matcher *BeEmptyMatcher) Match(actual interface{}) (success bool, err error) { + // short-circuit the iterator case, as we only need to see the first + // element, if any. + if miter.IsIter(actual) { + var length int + if miter.IsSeq2(actual) { + miter.IterateKV(actual, func(k, v reflect.Value) bool { length++; return false }) + } else { + miter.IterateV(actual, func(v reflect.Value) bool { length++; return false }) + } + return length == 0, nil + } + length, ok := lengthOf(actual) if !ok { - return false, fmt.Errorf("BeEmpty matcher expects a string/array/map/channel/slice. Got:\n%s", format.Object(actual, 1)) + return false, fmt.Errorf("BeEmpty matcher expects a string/array/map/channel/slice/iterator. Got:\n%s", format.Object(actual, 1)) } return length == 0, nil diff --git a/matchers/be_empty_matcher_test.go b/matchers/be_empty_matcher_test.go index 86afe3d1e..25453def2 100644 --- a/matchers/be_empty_matcher_test.go +++ b/matchers/be_empty_matcher_test.go @@ -4,6 +4,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" . "github.com/onsi/gomega/matchers" + "github.com/onsi/gomega/matchers/internal/miter" ) var _ = Describe("BeEmpty", func() { @@ -49,4 +50,32 @@ var _ = Describe("BeEmpty", func() { Expect(err).Should(HaveOccurred()) }) }) + + Context("iterators", func() { + BeforeEach(func() { + if !miter.HasIterators() { + Skip("iterators not available") + } + }) + + When("passed an iterator type", func() { + It("should do the right thing", func() { + Expect(emptyIter).To(BeEmpty()) + Expect(emptyIter2).To(BeEmpty()) + + Expect(universalIter).NotTo(BeEmpty()) + Expect(universalIter2).NotTo(BeEmpty()) + }) + }) + + When("passed a correctly typed nil", func() { + It("should be true", func() { + var nilIter func(func(string) bool) + Expect(nilIter).Should(BeEmpty()) + + var nilIter2 func(func(int, string) bool) + Expect(nilIter2).Should(BeEmpty()) + }) + }) + }) }) diff --git a/matchers/consist_of.go b/matchers/consist_of.go index f69037a4f..a11188182 100644 --- a/matchers/consist_of.go +++ b/matchers/consist_of.go @@ -7,6 +7,7 @@ import ( "reflect" "github.com/onsi/gomega/format" + "github.com/onsi/gomega/matchers/internal/miter" "github.com/onsi/gomega/matchers/support/goraph/bipartitegraph" ) @@ -17,8 +18,8 @@ type ConsistOfMatcher struct { } func (matcher *ConsistOfMatcher) Match(actual interface{}) (success bool, err error) { - if !isArrayOrSlice(actual) && !isMap(actual) { - return false, fmt.Errorf("ConsistOf matcher expects an array/slice/map. Got:\n%s", format.Object(actual, 1)) + if !isArrayOrSlice(actual) && !isMap(actual) && !miter.IsIter(actual) { + return false, fmt.Errorf("ConsistOf matcher expects an array/slice/map/iter.Seq/iter.Seq2. Got:\n%s", format.Object(actual, 1)) } matchers := matchers(matcher.Elements) @@ -60,10 +61,21 @@ func equalMatchersToElements(matchers []interface{}) (elements []interface{}) { } func flatten(elems []interface{}) []interface{} { - if len(elems) != 1 || !isArrayOrSlice(elems[0]) { + if len(elems) != 1 || + !(isArrayOrSlice(elems[0]) || + (miter.IsIter(elems[0]) && !miter.IsSeq2(elems[0]))) { return elems } + if miter.IsIter(elems[0]) { + flattened := []any{} + miter.IterateV(elems[0], func(v reflect.Value) bool { + flattened = append(flattened, v.Interface()) + return true + }) + return flattened + } + value := reflect.ValueOf(elems[0]) flattened := make([]interface{}, value.Len()) for i := 0; i < value.Len(); i++ { @@ -116,7 +128,19 @@ func presentable(elems []interface{}) interface{} { func valuesOf(actual interface{}) []interface{} { value := reflect.ValueOf(actual) values := []interface{}{} - if isMap(actual) { + if miter.IsIter(actual) { + if miter.IsSeq2(actual) { + miter.IterateKV(actual, func(k, v reflect.Value) bool { + values = append(values, v.Interface()) + return true + }) + } else { + miter.IterateV(actual, func(v reflect.Value) bool { + values = append(values, v.Interface()) + return true + }) + } + } else if isMap(actual) { keys := value.MapKeys() for i := 0; i < value.Len(); i++ { values = append(values, value.MapIndex(keys[i]).Interface()) diff --git a/matchers/consist_of_test.go b/matchers/consist_of_test.go index c7149b714..8ad696e45 100644 --- a/matchers/consist_of_test.go +++ b/matchers/consist_of_test.go @@ -3,6 +3,7 @@ package matchers_test import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/onsi/gomega/matchers/internal/miter" ) var _ = Describe("ConsistOf", func() { @@ -196,4 +197,42 @@ the extra elements were }) }) }) + + Context("iterators", func() { + BeforeEach(func() { + if !miter.HasIterators() { + Skip("iterators not available") + } + }) + + Context("with an iter.Seq", func() { + It("should do the right thing", func() { + Expect(universalIter).Should(ConsistOf("foo", "bar", "baz")) + Expect(universalIter).Should(ConsistOf("foo", "bar", "baz")) + Expect(universalIter).Should(ConsistOf("baz", "bar", "foo")) + Expect(universalIter).ShouldNot(ConsistOf("baz", "bar", "foo", "foo")) + Expect(universalIter).ShouldNot(ConsistOf("baz", "foo")) + }) + }) + + Context("with an iter.Seq2", func() { + It("should do the right thing", func() { + Expect(universalIter2).Should(ConsistOf("foo", "bar", "baz")) + Expect(universalIter2).Should(ConsistOf("foo", "bar", "baz")) + Expect(universalIter2).Should(ConsistOf("baz", "bar", "foo")) + Expect(universalIter2).ShouldNot(ConsistOf("baz", "bar", "foo", "foo")) + Expect(universalIter2).ShouldNot(ConsistOf("baz", "foo")) + }) + }) + + When("passed exactly one argument, and that argument is an iter.Seq", func() { + It("should match against the elements of that argument", func() { + Expect(universalIter).Should(ConsistOf(universalIter)) + Expect(universalIter).ShouldNot(ConsistOf(fooElements)) + + Expect(universalIter2).Should(ConsistOf(universalIter)) + Expect(universalIter2).ShouldNot(ConsistOf(fooElements)) + }) + }) + }) }) diff --git a/matchers/contain_element_matcher.go b/matchers/contain_element_matcher.go index 3d45c9ebc..830239c7b 100644 --- a/matchers/contain_element_matcher.go +++ b/matchers/contain_element_matcher.go @@ -8,6 +8,7 @@ import ( "reflect" "github.com/onsi/gomega/format" + "github.com/onsi/gomega/matchers/internal/miter" ) type ContainElementMatcher struct { @@ -16,16 +17,18 @@ type ContainElementMatcher struct { } func (matcher *ContainElementMatcher) Match(actual interface{}) (success bool, err error) { - if !isArrayOrSlice(actual) && !isMap(actual) { - return false, fmt.Errorf("ContainElement matcher expects an array/slice/map. Got:\n%s", format.Object(actual, 1)) + if !isArrayOrSlice(actual) && !isMap(actual) && !miter.IsIter(actual) { + return false, fmt.Errorf("ContainElement matcher expects an array/slice/map/iterator. Got:\n%s", format.Object(actual, 1)) } var actualT reflect.Type var result reflect.Value - switch l := len(matcher.Result); { - case l > 1: + switch numResultArgs := len(matcher.Result); { + case numResultArgs > 1: return false, errors.New("ContainElement matcher expects at most a single optional pointer to store its findings at") - case l == 1: + case numResultArgs == 1: + // Check the optional result arg to point to a single value/array/slice/map + // of a type compatible with the actual value. if reflect.ValueOf(matcher.Result[0]).Kind() != reflect.Ptr { return false, fmt.Errorf("ContainElement matcher expects a non-nil pointer to store its findings at. Got\n%s", format.Object(matcher.Result[0], 1)) @@ -34,93 +37,209 @@ func (matcher *ContainElementMatcher) Match(actual interface{}) (success bool, e resultReference := matcher.Result[0] result = reflect.ValueOf(resultReference).Elem() // what ResultReference points to, to stash away our findings switch result.Kind() { - case reflect.Array: + case reflect.Array: // result arrays are not supported, as they cannot be dynamically sized. + if miter.IsIter(actual) { + _, actualvT := miter.IterKVTypes(actual) + return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s", + reflect.SliceOf(actualvT), result.Type().String()) + } return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s", reflect.SliceOf(actualT.Elem()).String(), result.Type().String()) - case reflect.Slice: - if !isArrayOrSlice(actual) { + + case reflect.Slice: // result slice + // can we assign elements in actual to elements in what the result + // arg points to? + // - ✔ actual is an array or slice + // - ✔ actual is an iter.Seq producing "v" elements + // - ✔ actual is an iter.Seq2 producing "v" elements, ignoring + // the "k" elements. + switch { + case isArrayOrSlice(actual): + if !actualT.Elem().AssignableTo(result.Type().Elem()) { + return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s", + actualT.String(), result.Type().String()) + } + + case miter.IsIter(actual): + _, actualvT := miter.IterKVTypes(actual) + if !actualvT.AssignableTo(result.Type().Elem()) { + return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s", + actualvT.String(), result.Type().String()) + } + + default: // incompatible result reference return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s", reflect.MapOf(actualT.Key(), actualT.Elem()).String(), result.Type().String()) } - if !actualT.Elem().AssignableTo(result.Type().Elem()) { - return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s", - actualT.String(), result.Type().String()) - } - case reflect.Map: - if !isMap(actual) { - return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s", - actualT.String(), result.Type().String()) - } - if !actualT.AssignableTo(result.Type()) { + + case reflect.Map: // result map + // can we assign elements in actual to elements in what the result + // arg points to? + // - ✔ actual is a map + // - ✔ actual is an iter.Seq2 (iter.Seq doesn't fit though) + switch { + case isMap(actual): + if !actualT.AssignableTo(result.Type()) { + return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s", + actualT.String(), result.Type().String()) + } + + case miter.IsIter(actual): + actualkT, actualvT := miter.IterKVTypes(actual) + if actualkT == nil { + return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s", + reflect.SliceOf(actualvT).String(), result.Type().String()) + } + if !reflect.MapOf(actualkT, actualvT).AssignableTo(result.Type()) { + return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s", + reflect.MapOf(actualkT, actualvT), result.Type().String()) + } + + default: // incompatible result reference return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s", actualT.String(), result.Type().String()) } + default: - if !actualT.Elem().AssignableTo(result.Type()) { - return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s", - actualT.Elem().String(), result.Type().String()) + // can we assign a (single) element in actual to what the result arg + // points to? + switch { + case miter.IsIter(actual): + _, actualvT := miter.IterKVTypes(actual) + if !actualvT.AssignableTo(result.Type()) { + return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s", + actualvT.String(), result.Type().String()) + } + default: + if !actualT.Elem().AssignableTo(result.Type()) { + return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s", + actualT.Elem().String(), result.Type().String()) + } } } } + // If the supplied matcher isn't an Omega matcher, default to the Equal + // matcher. elemMatcher, elementIsMatcher := matcher.Element.(omegaMatcher) if !elementIsMatcher { elemMatcher = &EqualMatcher{Expected: matcher.Element} } value := reflect.ValueOf(actual) - var valueAt func(int) interface{} - var getFindings func() reflect.Value - var foundAt func(int) + var getFindings func() reflect.Value // abstracts how the findings are collected and stored + var lastError error - if isMap(actual) { - keys := value.MapKeys() - valueAt = func(i int) interface{} { - return value.MapIndex(keys[i]).Interface() + if !miter.IsIter(actual) { + var valueAt func(int) interface{} + var foundAt func(int) + // We're dealing with an array/slice/map, so in all cases we can iterate + // over the elements in actual using indices (that can be considered + // keys in case of maps). + if isMap(actual) { + keys := value.MapKeys() + valueAt = func(i int) interface{} { + return value.MapIndex(keys[i]).Interface() + } + if result.Kind() != reflect.Invalid { + fm := reflect.MakeMap(actualT) + getFindings = func() reflect.Value { return fm } + foundAt = func(i int) { + fm.SetMapIndex(keys[i], value.MapIndex(keys[i])) + } + } + } else { + valueAt = func(i int) interface{} { + return value.Index(i).Interface() + } + if result.Kind() != reflect.Invalid { + var fsl reflect.Value + if result.Kind() == reflect.Slice { + fsl = reflect.MakeSlice(result.Type(), 0, 0) + } else { + fsl = reflect.MakeSlice(reflect.SliceOf(result.Type()), 0, 0) + } + getFindings = func() reflect.Value { return fsl } + foundAt = func(i int) { + fsl = reflect.Append(fsl, value.Index(i)) + } + } } - if result.Kind() != reflect.Invalid { - fm := reflect.MakeMap(actualT) - getFindings = func() reflect.Value { - return fm + + for i := 0; i < value.Len(); i++ { + elem := valueAt(i) + success, err := elemMatcher.Match(elem) + if err != nil { + lastError = err + continue } - foundAt = func(i int) { - fm.SetMapIndex(keys[i], value.MapIndex(keys[i])) + if success { + if result.Kind() == reflect.Invalid { + return true, nil + } + foundAt(i) } } } else { - valueAt = func(i int) interface{} { - return value.Index(i).Interface() - } + // We're dealing with an iterator as a first-class construct, so things + // are slightly different: there is no index defined as in case of + // arrays/slices/maps, just "ooooorder" + var found func(k, v reflect.Value) if result.Kind() != reflect.Invalid { - var f reflect.Value - if result.Kind() == reflect.Slice { - f = reflect.MakeSlice(result.Type(), 0, 0) + if result.Kind() == reflect.Map { + fm := reflect.MakeMap(result.Type()) + getFindings = func() reflect.Value { return fm } + found = func(k, v reflect.Value) { fm.SetMapIndex(k, v) } } else { - f = reflect.MakeSlice(reflect.SliceOf(result.Type()), 0, 0) - } - getFindings = func() reflect.Value { - return f - } - foundAt = func(i int) { - f = reflect.Append(f, value.Index(i)) + var fsl reflect.Value + if result.Kind() == reflect.Slice { + fsl = reflect.MakeSlice(result.Type(), 0, 0) + } else { + fsl = reflect.MakeSlice(reflect.SliceOf(result.Type()), 0, 0) + } + getFindings = func() reflect.Value { return fsl } + found = func(_, v reflect.Value) { fsl = reflect.Append(fsl, v) } } } - } - var lastError error - for i := 0; i < value.Len(); i++ { - elem := valueAt(i) - success, err := elemMatcher.Match(elem) - if err != nil { - lastError = err - continue + success := false + actualkT, _ := miter.IterKVTypes(actual) + if actualkT == nil { + miter.IterateV(actual, func(v reflect.Value) bool { + var err error + success, err = elemMatcher.Match(v.Interface()) + if err != nil { + lastError = err + return true // iterate on... + } + if success { + if result.Kind() == reflect.Invalid { + return false // a match and no result needed, so we're done + } + found(reflect.Value{}, v) + } + return true // iterate on... + }) + } else { + miter.IterateKV(actual, func(k, v reflect.Value) bool { + var err error + success, err = elemMatcher.Match(v.Interface()) + if err != nil { + lastError = err + return true // iterate on... + } + if success { + if result.Kind() == reflect.Invalid { + return false // a match and no result needed, so we're done + } + found(k, v) + } + return true // iterate on... + }) } - if success { - if result.Kind() == reflect.Invalid { - return true, nil - } - foundAt(i) + if success && result.Kind() == reflect.Invalid { + return true, nil } } diff --git a/matchers/contain_element_matcher_test.go b/matchers/contain_element_matcher_test.go index 700871953..8578ef482 100644 --- a/matchers/contain_element_matcher_test.go +++ b/matchers/contain_element_matcher_test.go @@ -1,6 +1,8 @@ package matchers_test import ( + "github.com/onsi/gomega/matchers/internal/miter" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" . "github.com/onsi/gomega/matchers" @@ -82,6 +84,12 @@ var _ = Describe("ContainElement", func() { MatchError(MatchRegexp(`expects a non-nil pointer.+ Got\n +: nil`))) }) + It("rejects multiple result args", func() { + Expect(ContainElement("foo", 42, 43).Match([]string{"foo"})).Error().To( + MatchError(MatchRegexp(`expects at most a single optional pointer`))) + + }) + Context("with match(es)", func() { When("passed an assignable result reference", func() { It("should assign a single finding to a scalar result reference", func() { @@ -142,23 +150,27 @@ var _ = Describe("ContainElement", func() { var stash int Expect(ContainElement("foo", &stash).Match(actual)).Error().To(HaveOccurred()) }) + It("should error for actual []T, return reference [...]T", func() { actual := []string{"bar", "foo"} var arrstash [2]string Expect(ContainElement("foo", &arrstash).Match(actual)).Error().To(HaveOccurred()) }) + It("should error for actual []interface{}, return reference T", func() { actual := []interface{}{"foo", 42} var stash int Expect(ContainElement(Not(BeZero()), &stash).Match(actual)).Error().To( MatchError(MatchRegexp(`cannot return findings\. Need \*interface.+, got \*int`))) }) + It("should error for actual []interface{}, return reference []T", func() { actual := []interface{}{"foo", 42} var stash []string Expect(ContainElement(Not(BeZero()), &stash).Match(actual)).Error().To( MatchError(MatchRegexp(`cannot return findings\. Need \*\[\]interface.+, got \*\[\]string`))) }) + It("should error for actual map[T]T, return reference map[T]interface{}", func() { actual := map[string]string{ "foo": "foo", @@ -169,6 +181,7 @@ var _ = Describe("ContainElement", func() { Expect(ContainElement(Not(BeZero()), &stash).Match(actual)).Error().To( MatchError(MatchRegexp(`cannot return findings\. Need \*map\[string\]string, got \*map\[string\]interface`))) }) + It("should error for actual map[T]T, return reference []T", func() { actual := map[string]string{ "foo": "foo", @@ -219,4 +232,205 @@ var _ = Describe("ContainElement", func() { }) }) + Context("iterators", func() { + BeforeEach(func() { + if !miter.HasIterators() { + Skip("iterators not available") + } + }) + + Describe("matching only", func() { + When("passed a supported type", func() { + Context("and expecting a non-matcher", func() { + It("should do the right thing", func() { + Expect(universalIter).To(ContainElement("baz")) + Expect(universalIter).NotTo(ContainElement("barrrrz")) + + Expect(universalIter2).To(ContainElement("baz")) + Expect(universalIter2).NotTo(ContainElement("barrrrz")) + }) + }) + + Context("and expecting a matcher", func() { + It("should pass each element through the matcher", func() { + Expect(universalIter).To(ContainElement(HaveLen(3))) + Expect(universalIter).NotTo(ContainElement(HaveLen(4))) + + Expect(universalIter2).To(ContainElement(HaveLen(3))) + Expect(universalIter2).NotTo(ContainElement(HaveLen(5))) + }) + + It("should power through even if the matcher ever fails", func() { + elements := []any{1, 2, "3", 4} + it := func(yield func(any) bool) { + for _, element := range elements { + if !yield(element) { + return + } + } + } + Expect(it).Should(ContainElement(BeNumerically(">=", 3))) + + it2 := func(yield func(int, any) bool) { + for idx, element := range elements { + if !yield(idx, element) { + return + } + } + } + Expect(it2).Should(ContainElement(BeNumerically(">=", 3))) + }) + + It("should fail if the matcher fails", func() { + elements := []interface{}{1, 2, "3", "4"} + it := func(yield func(any) bool) { + for _, element := range elements { + if !yield(element) { + return + } + } + } + success, err := (&ContainElementMatcher{Element: BeNumerically(">=", 3)}).Match(it) + Expect(success).Should(BeFalse()) + Expect(err).Should(HaveOccurred()) + + it2 := func(yield func(int, any) bool) { + for idx, element := range elements { + if !yield(idx, element) { + return + } + } + } + success, err = (&ContainElementMatcher{Element: BeNumerically(">=", 3)}).Match(it2) + Expect(success).Should(BeFalse()) + Expect(err).Should(HaveOccurred()) + }) + }) + }) + + When("passed a correctly typed nil", func() { + It("should operate succesfully on the passed in value", func() { + var nilIter func(func(string) bool) + Expect(nilIter).ShouldNot(ContainElement(1)) + + var nilIter2 func(func(int, string) bool) + Expect(nilIter2).ShouldNot(ContainElement("foo")) + }) + }) + }) + + Describe("returning findings", func() { + Context("with match(es)", func() { + When("passed an assignable result reference", func() { + It("should assign a single finding to a scalar result reference", func() { + var stash string + Expect(universalIter).To(ContainElement("bar", &stash)) + Expect(stash).To(Equal("bar")) + + Expect(universalIter2).To(ContainElement("baz", &stash)) + Expect(stash).To(Equal("baz")) + }) + + It("should assign a single finding to a slice return reference", func() { + var stash []string + Expect(universalIter).To(ContainElement("baz", &stash)) + Expect(stash).To(HaveLen(1)) + Expect(stash).To(ContainElement("baz")) + + stash = []string{} + Expect(universalIter2).To(ContainElement("baz", &stash)) + Expect(stash).To(HaveLen(1)) + Expect(stash).To(ContainElement("baz")) + }) + + It("should assign multiple findings to a slice return reference", func() { + var stash []string + Expect(universalIter).To(ContainElement(HavePrefix("ba"), &stash)) + Expect(stash).To(HaveLen(2)) + Expect(stash).To(HaveExactElements("bar", "baz")) + + stash = []string{} + Expect(universalIter2).To(ContainElement(HavePrefix("ba"), &stash)) + Expect(stash).To(HaveLen(2)) + Expect(stash).To(HaveExactElements("bar", "baz")) + }) + + It("should assign iter.Seq2 findings to a map return reference", func() { + m := map[int]string{ + 0: "foo", + 42: "bar", + 666: "baz", + } + iter2 := func(yield func(int, string) bool) { + for k, v := range m { + if !yield(k, v) { + return + } + } + } + + var stash map[int]string + Expect(iter2).To(ContainElement(HavePrefix("ba"), &stash)) + Expect(stash).To(HaveLen(2)) + Expect(stash).To(ConsistOf("bar", "baz")) + }) + }) + + When("passed a scalar return reference for multiple matches", func() { + It("should error", func() { + var stash string + Expect(ContainElement(HavePrefix("ba"), &stash).Match(universalIter)).Error().To( + MatchError(MatchRegexp(`cannot return multiple findings\. Need \*\[\]string, got \*string`))) + }) + }) + + When("passed an unassignable return reference for matches", func() { + It("should error for actual iter.Seq[T1]/iter.Seq2[..., T1], return reference T2", func() { + var stash int + Expect(ContainElement("foo", &stash).Match(universalIter)).Error().To(HaveOccurred()) + Expect(ContainElement("foo", &stash).Match(emptyIter2)).Error().To(HaveOccurred()) + }) + + It("should error for actual iter.Seq[T]/iter.Seq2[..., T], return reference [...]T", func() { + var arrstash [2]string + Expect(ContainElement("foo", &arrstash).Match(universalIter)).Error().To(HaveOccurred()) + Expect(ContainElement("foo", &arrstash).Match(universalIter2)).Error().To(HaveOccurred()) + }) + + It("should error for actual map[T1]T2, return reference map[T1]interface{}", func() { + var stash map[int]interface{} + Expect(ContainElement(Not(BeZero()), &stash).Match(universalIter2)).Error().To( + MatchError(MatchRegexp(`cannot return findings\. Need \*map\[int\]string, got \*map\[int\]interface`))) + }) + }) + }) + + Context("without any matches", func() { + When("the matcher did not error", func() { + It("should report non-match", func() { + var stash string + rem := ContainElement("barrz", &stash) + m, err := rem.Match(universalIter) + Expect(m).To(BeFalse()) + Expect(err).NotTo(HaveOccurred()) + Expect(rem.FailureMessage(universalIter)).To(MatchRegexp(`Expected\n.+\nto contain element matching\n.+: barrz`)) + + var stashslice []string + rem = ContainElement("barrz", &stashslice) + m, err = rem.Match(universalIter) + Expect(m).To(BeFalse()) + Expect(err).NotTo(HaveOccurred()) + Expect(rem.FailureMessage(universalIter)).To(MatchRegexp(`Expected\n.+\nto contain element matching\n.+: barrz`)) + }) + }) + + When("the matcher errors", func() { + It("should report last matcher error", func() { + var stash []interface{} + Expect(ContainElement(HaveField("yeehaw", 42), &stash).Match(universalIter)).Error().To(MatchError(MatchRegexp(`HaveField encountered:\n.*: baz\nWhich is not a struct`))) + }) + }) + }) + }) + }) }) diff --git a/matchers/contain_elements_matcher.go b/matchers/contain_elements_matcher.go index 946cd8bea..d9fcb8b80 100644 --- a/matchers/contain_elements_matcher.go +++ b/matchers/contain_elements_matcher.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/onsi/gomega/format" + "github.com/onsi/gomega/matchers/internal/miter" "github.com/onsi/gomega/matchers/support/goraph/bipartitegraph" ) @@ -13,8 +14,8 @@ type ContainElementsMatcher struct { } func (matcher *ContainElementsMatcher) Match(actual interface{}) (success bool, err error) { - if !isArrayOrSlice(actual) && !isMap(actual) { - return false, fmt.Errorf("ContainElements matcher expects an array/slice/map. Got:\n%s", format.Object(actual, 1)) + if !isArrayOrSlice(actual) && !isMap(actual) && !miter.IsIter(actual) { + return false, fmt.Errorf("ContainElements matcher expects an array/slice/map/iter.Seq/iter.Seq2. Got:\n%s", format.Object(actual, 1)) } matchers := matchers(matcher.Elements) diff --git a/matchers/contain_elements_matcher_test.go b/matchers/contain_elements_matcher_test.go index 4a424a4bc..48bd9ef8e 100644 --- a/matchers/contain_elements_matcher_test.go +++ b/matchers/contain_elements_matcher_test.go @@ -3,6 +3,7 @@ package matchers_test import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/onsi/gomega/matchers/internal/miter" ) var _ = Describe("ContainElements", func() { @@ -149,4 +150,40 @@ the missing elements were }) }) }) + + Context("iterators", func() { + BeforeEach(func() { + if !miter.HasIterators() { + Skip("iterators not available") + } + }) + + Context("with an iter.Seq", func() { + It("should do the right thing", func() { + Expect(universalIter).Should(ContainElements("foo", "bar", "baz")) + Expect(universalIter).Should(ContainElements("bar")) + Expect(universalIter).Should(ContainElements()) + Expect(universalIter).ShouldNot(ContainElements("baz", "bar", "foo", "foo")) + }) + }) + + Context("with an iter.Seq2", func() { + It("should do the right thing", func() { + Expect(universalIter2).Should(ContainElements("foo", "bar", "baz")) + Expect(universalIter2).Should(ContainElements("bar")) + Expect(universalIter2).Should(ContainElements()) + Expect(universalIter2).ShouldNot(ContainElements("baz", "bar", "foo", "foo")) + }) + }) + + When("passed exactly one argument, and that argument is an iter.Seq", func() { + It("should match against the elements of that argument", func() { + Expect(universalIter).Should(ContainElements(universalIter)) + Expect(universalIter).ShouldNot(ContainElements(fooElements)) + + Expect(universalIter2).Should(ContainElements(universalIter)) + Expect(universalIter2).ShouldNot(ContainElements(fooElements)) + }) + }) + }) }) diff --git a/matchers/have_each_matcher.go b/matchers/have_each_matcher.go index 025b6e1ac..4111f2b86 100644 --- a/matchers/have_each_matcher.go +++ b/matchers/have_each_matcher.go @@ -5,6 +5,7 @@ import ( "reflect" "github.com/onsi/gomega/format" + "github.com/onsi/gomega/matchers/internal/miter" ) type HaveEachMatcher struct { @@ -12,8 +13,8 @@ type HaveEachMatcher struct { } func (matcher *HaveEachMatcher) Match(actual interface{}) (success bool, err error) { - if !isArrayOrSlice(actual) && !isMap(actual) { - return false, fmt.Errorf("HaveEach matcher expects an array/slice/map. Got:\n%s", + if !isArrayOrSlice(actual) && !isMap(actual) && !miter.IsIter(actual) { + return false, fmt.Errorf("HaveEach matcher expects an array/slice/map/iter.Seq/iter.Seq2. Got:\n%s", format.Object(actual, 1)) } @@ -22,6 +23,38 @@ func (matcher *HaveEachMatcher) Match(actual interface{}) (success bool, err err elemMatcher = &EqualMatcher{Expected: matcher.Element} } + if miter.IsIter(actual) { + // rejecting the non-elements case works different for iterators as we + // don't want to fetch all elements into a slice first. + count := 0 + var success bool + var err error + if miter.IsSeq2(actual) { + miter.IterateKV(actual, func(k, v reflect.Value) bool { + count++ + success, err = elemMatcher.Match(v.Interface()) + if err != nil { + return false + } + return success + }) + } else { + miter.IterateV(actual, func(v reflect.Value) bool { + count++ + success, err = elemMatcher.Match(v.Interface()) + if err != nil { + return false + } + return success + }) + } + if count == 0 { + return false, fmt.Errorf("HaveEach matcher expects a non-empty iter.Seq/iter.Seq2. Got:\n%s", + format.Object(actual, 1)) + } + return success, err + } + value := reflect.ValueOf(actual) if value.Len() == 0 { return false, fmt.Errorf("HaveEach matcher expects a non-empty array/slice/map. Got:\n%s", @@ -40,7 +73,8 @@ func (matcher *HaveEachMatcher) Match(actual interface{}) (success bool, err err } } - // if there are no elements, then HaveEach will match. + // if we never failed then we succeed; the empty/nil cases have already been + // rejected above. for i := 0; i < value.Len(); i++ { success, err := elemMatcher.Match(valueAt(i)) if err != nil { diff --git a/matchers/have_each_matcher_test.go b/matchers/have_each_matcher_test.go index 284d03e44..1f2ef4017 100644 --- a/matchers/have_each_matcher_test.go +++ b/matchers/have_each_matcher_test.go @@ -1,6 +1,8 @@ package matchers_test import ( + "github.com/onsi/gomega/matchers/internal/miter" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" . "github.com/onsi/gomega/matchers" @@ -92,4 +94,66 @@ var _ = Describe("HaveEach", func() { Expect(err).Should(HaveOccurred()) }) }) + + Context("iterators", func() { + BeforeEach(func() { + if !miter.HasIterators() { + Skip("iterators not available") + } + }) + + When("passed an iterator type", func() { + Context("and expecting a non-matcher", func() { + It("should do the right thing", func() { + Expect(fooIter).Should(HaveEach("foo")) + Expect(fooIter).ShouldNot(HaveEach("bar")) + + Expect(fooIter2).Should(HaveEach("foo")) + Expect(fooIter2).ShouldNot(HaveEach("bar")) + }) + }) + + Context("and expecting a matcher", func() { + It("should pass each element through the matcher", func() { + Expect(universalIter).Should(HaveEach(HaveLen(3))) + Expect(universalIter).ShouldNot(HaveEach(HaveLen(4))) + + Expect(universalIter2).Should(HaveEach(HaveLen(3))) + Expect(universalIter2).ShouldNot(HaveEach(HaveLen(4))) + }) + + It("should not power through if the matcher ever fails", func() { + success, err := (&HaveEachMatcher{Element: BeNumerically(">=", 1)}).Match(universalIter) + Expect(success).Should(BeFalse()) + Expect(err).Should(HaveOccurred()) + + success, err = (&HaveEachMatcher{Element: BeNumerically(">=", 1)}).Match(universalIter2) + Expect(success).Should(BeFalse()) + Expect(err).Should(HaveOccurred()) + }) + }) + }) + + When("passed an iterator yielding nothing or correctly typed nil", func() { + It("should error", func() { + success, err := (&HaveEachMatcher{Element: "foo"}).Match(emptyIter) + Expect(success).Should(BeFalse()) + Expect(err).Should(HaveOccurred()) + + success, err = (&HaveEachMatcher{Element: "foo"}).Match(emptyIter2) + Expect(success).Should(BeFalse()) + Expect(err).Should(HaveOccurred()) + + var nilIter func(func(string) bool) + success, err = (&HaveEachMatcher{Element: "foo"}).Match(nilIter) + Expect(success).Should(BeFalse()) + Expect(err).Should(HaveOccurred()) + + var nilIter2 func(func(int, string) bool) + success, err = (&HaveEachMatcher{Element: "foo"}).Match(nilIter2) + Expect(success).Should(BeFalse()) + Expect(err).Should(HaveOccurred()) + }) + }) + }) }) diff --git a/matchers/have_exact_elements.go b/matchers/have_exact_elements.go index 5a236d7d6..23799f1c6 100644 --- a/matchers/have_exact_elements.go +++ b/matchers/have_exact_elements.go @@ -2,8 +2,10 @@ package matchers import ( "fmt" + "reflect" "github.com/onsi/gomega/format" + "github.com/onsi/gomega/matchers/internal/miter" ) type mismatchFailure struct { @@ -21,17 +23,58 @@ type HaveExactElementsMatcher struct { func (matcher *HaveExactElementsMatcher) Match(actual interface{}) (success bool, err error) { matcher.resetState() - if isMap(actual) { - return false, fmt.Errorf("error") + if isMap(actual) || miter.IsSeq2(actual) { + return false, fmt.Errorf("HaveExactElements matcher doesn't work on map or iter.Seq2. Got:\n%s", format.Object(actual, 1)) } matchers := matchers(matcher.Elements) - values := valuesOf(actual) - lenMatchers := len(matchers) - lenValues := len(values) + success = true + if miter.IsIter(actual) { + // In the worst case, we need to see everything before we can give our + // verdict. The only exception is fast fail. + i := 0 + miter.IterateV(actual, func(v reflect.Value) bool { + if i >= lenMatchers { + // the iterator produces more values than we got matchers: this + // is not good. + matcher.extraIndex = i + success = false + return false + } + + elemMatcher := matchers[i].(omegaMatcher) + match, err := elemMatcher.Match(v.Interface()) + if err != nil { + matcher.mismatchFailures = append(matcher.mismatchFailures, mismatchFailure{ + index: i, + failure: err.Error(), + }) + success = false + } else if !match { + matcher.mismatchFailures = append(matcher.mismatchFailures, mismatchFailure{ + index: i, + failure: elemMatcher.FailureMessage(v.Interface()), + }) + success = false + } + i++ + return true + }) + if i < len(matchers) { + // the iterator produced less values than we got matchers: this is + // no good, no no no. + matcher.missingIndex = i + success = false + } + return success, nil + } + + values := valuesOf(actual) + lenValues := len(values) + for i := 0; i < lenMatchers || i < lenValues; i++ { if i >= lenMatchers { matcher.extraIndex = i diff --git a/matchers/have_exact_elements_test.go b/matchers/have_exact_elements_test.go index 66e146815..bb938a033 100644 --- a/matchers/have_exact_elements_test.go +++ b/matchers/have_exact_elements_test.go @@ -3,6 +3,7 @@ package matchers_test import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/onsi/gomega/matchers/internal/miter" ) var _ = Describe("HaveExactElements", func() { @@ -142,4 +143,152 @@ to equal Expect([]bool{false}).Should(matchSingleFalse) }) }) + + Context("iterators", func() { + BeforeEach(func() { + if !miter.HasIterators() { + Skip("iterators not available") + } + }) + + Context("with an iter.Seq", func() { + It("should do the right thing", func() { + Expect(universalIter).Should(HaveExactElements("foo", "bar", "baz")) + Expect(universalIter).ShouldNot(HaveExactElements("foo")) + Expect(universalIter).ShouldNot(HaveExactElements("foo", "bar", "baz", "argh")) + Expect(universalIter).ShouldNot(HaveExactElements("foo", "bar")) + + var nilIter func(func(string) bool) + Expect(nilIter).Should(HaveExactElements()) + }) + }) + + Context("with an iter.Seq2", func() { + It("should error", func() { + failures := InterceptGomegaFailures(func() { + Expect(universalIter2).Should(HaveExactElements("foo")) + }) + + Expect(failures).Should(HaveLen(1)) + }) + }) + + When("passed matchers", func() { + It("should pass if matcher pass", func() { + Expect(universalIter).Should(HaveExactElements("foo", MatchRegexp("^ba"), MatchRegexp("az$"))) + Expect(universalIter).ShouldNot(HaveExactElements("foo", MatchRegexp("az$"), MatchRegexp("^ba"))) + Expect(universalIter).ShouldNot(HaveExactElements("foo", MatchRegexp("az$"))) + Expect(universalIter).ShouldNot(HaveExactElements("foo", MatchRegexp("az$"), "baz", "bac")) + }) + + When("a matcher errors", func() { + It("should soldier on", func() { + Expect(universalIter).ShouldNot(HaveExactElements(BeFalse(), "bar", "baz")) + poly := []any{"foo", "bar", false} + polyIter := func(yield func(any) bool) { + for _, v := range poly { + if !yield(v) { + return + } + } + } + Expect(polyIter).Should(HaveExactElements(ContainSubstring("foo"), "bar", BeFalse())) + }) + + It("should include the error message, not the failure message", func() { + failures := InterceptGomegaFailures(func() { + Expect(universalIter).Should(HaveExactElements("foo", BeFalse(), "bar")) + }) + Ω(failures[0]).ShouldNot(ContainSubstring("to be false")) + Ω(failures[0]).Should(ContainSubstring("1: Expected a boolean. Got:\n : bar")) + }) + }) + }) + When("passed exactly one argument, and that argument is a slice", func() { + It("should match against the elements of that arguments", func() { + Expect(universalIter).Should(HaveExactElements([]string{"foo", "bar", "baz"})) + Expect(universalIter).ShouldNot(HaveExactElements([]string{"foo", "bar"})) + }) + }) + + When("passed nil", func() { + It("should fail correctly", func() { + failures := InterceptGomegaFailures(func() { + var expected []any + Expect(universalIter).Should(HaveExactElements(expected...)) + }) + Expect(failures).Should(HaveLen(1)) + }) + }) + + Describe("Failure Message", func() { + When("actual contains extra elements", func() { + It("should print the starting index of the extra elements", func() { + failures := InterceptGomegaFailures(func() { + Expect(universalIter).Should(HaveExactElements("foo")) + }) + + expected := "Expected\n.*:.*\nto have exact elements with\n.*\\[\"foo\"\\]\nthe extra elements start from index 1" + Expect(failures).To(ConsistOf(MatchRegexp(expected))) + }) + }) + + When("actual misses an element", func() { + It("should print the starting index of missing element", func() { + failures := InterceptGomegaFailures(func() { + Expect(universalIter).Should(HaveExactElements("foo", "bar", "baz", "argh")) + }) + + expected := "Expected\n.*:.*\nto have exact elements with\n.*\\[\"foo\", \"bar\", \"baz\", \"argh\"\\]\nthe missing elements start from index 3" + Expect(failures).To(ConsistOf(MatchRegexp(expected))) + }) + }) + }) + + When("actual have mismatched elements", func() { + It("should print the index, expected element, and actual element", func() { + failures := InterceptGomegaFailures(func() { + Expect(universalIter).Should(HaveExactElements("bar", "baz", "foo")) + }) + + expected := `Expected +.*:.* +to have exact elements with +.*\["bar", "baz", "foo"\] +the mismatch indexes were: +0: Expected + : foo +to equal + : bar +1: Expected + : bar +to equal + : baz +2: Expected + : baz +to equal + : foo` + Expect(failures[0]).To(MatchRegexp(expected)) + }) + }) + + When("matcher instance is reused", func() { + // This is a regression test for https://github.com/onsi/gomega/issues/647. + // Matcher instance may be reused, if placed inside ContainElement() or other collection matchers. + It("should work properly", func() { + matchSingleFalse := HaveExactElements(Equal(false)) + allOf := func(a []bool) func(func(bool) bool) { + return func(yield func(bool) bool) { + for _, b := range a { + if !yield(b) { + return + } + } + } + } + Expect(allOf([]bool{true})).ShouldNot(matchSingleFalse) + Expect(allOf([]bool{false})).Should(matchSingleFalse) + }) + }) + }) }) diff --git a/matchers/have_key_matcher.go b/matchers/have_key_matcher.go index 00cffec70..b62ee93cb 100644 --- a/matchers/have_key_matcher.go +++ b/matchers/have_key_matcher.go @@ -7,6 +7,7 @@ import ( "reflect" "github.com/onsi/gomega/format" + "github.com/onsi/gomega/matchers/internal/miter" ) type HaveKeyMatcher struct { @@ -14,8 +15,8 @@ type HaveKeyMatcher struct { } func (matcher *HaveKeyMatcher) Match(actual interface{}) (success bool, err error) { - if !isMap(actual) { - return false, fmt.Errorf("HaveKey matcher expects a map. Got:%s", format.Object(actual, 1)) + if !isMap(actual) && !miter.IsSeq2(actual) { + return false, fmt.Errorf("HaveKey matcher expects a map/iter.Seq2. Got:%s", format.Object(actual, 1)) } keyMatcher, keyIsMatcher := matcher.Key.(omegaMatcher) @@ -23,6 +24,20 @@ func (matcher *HaveKeyMatcher) Match(actual interface{}) (success bool, err erro keyMatcher = &EqualMatcher{Expected: matcher.Key} } + if miter.IsSeq2(actual) { + var success bool + var err error + miter.IterateKV(actual, func(k, v reflect.Value) bool { + success, err = keyMatcher.Match(k.Interface()) + if err != nil { + err = fmt.Errorf("HaveKey's key matcher failed with:\n%s%s", format.Indent, err.Error()) + return false + } + return !success + }) + return success, err + } + keys := reflect.ValueOf(actual).MapKeys() for i := 0; i < len(keys); i++ { success, err := keyMatcher.Match(keys[i].Interface()) diff --git a/matchers/have_key_matcher_test.go b/matchers/have_key_matcher_test.go index 25da6c590..6a97ffbc7 100644 --- a/matchers/have_key_matcher_test.go +++ b/matchers/have_key_matcher_test.go @@ -4,6 +4,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" . "github.com/onsi/gomega/matchers" + "github.com/onsi/gomega/matchers/internal/miter" ) var _ = Describe("HaveKey", func() { @@ -70,4 +71,49 @@ var _ = Describe("HaveKey", func() { Expect(err).Should(HaveOccurred()) }) }) + + Context("iterators", func() { + BeforeEach(func() { + if !miter.HasIterators() { + Skip("iterators not available") + } + }) + + When("passed an iter.Seq2", func() { + It("should do the right thing", func() { + Expect(universalMapIter2).To(HaveKey("bar")) + Expect(universalMapIter2).To(HaveKey(HavePrefix("ba"))) + Expect(universalMapIter2).NotTo(HaveKey("barrrrz")) + Expect(universalMapIter2).NotTo(HaveKey(42)) + }) + }) + + When("passed a correctly typed nil", func() { + It("should operate succesfully on the passed in value", func() { + var nilIter2 func(func(string, int) bool) + Expect(nilIter2).ShouldNot(HaveKey("foo")) + }) + }) + + When("the passed in key is actually a matcher", func() { + It("should pass each element through the matcher", func() { + Expect(universalMapIter2).Should(HaveKey(ContainSubstring("oo"))) + Expect(universalMapIter2).ShouldNot(HaveKey(ContainSubstring("foobar"))) + }) + + It("should fail if the matcher ever fails", func() { + success, err := (&HaveKeyMatcher{Key: ContainSubstring("ar")}).Match(universalIter2) + Expect(success).Should(BeFalse()) + Expect(err).Should(HaveOccurred()) + }) + }) + + When("passed something that is not an iter.Seq2", func() { + It("should error", func() { + success, err := (&HaveKeyMatcher{Key: "foo"}).Match(universalIter) + Expect(success).Should(BeFalse()) + Expect(err).Should(HaveOccurred()) + }) + }) + }) }) diff --git a/matchers/have_key_with_value_matcher.go b/matchers/have_key_with_value_matcher.go index 4c5916804..3d608f63e 100644 --- a/matchers/have_key_with_value_matcher.go +++ b/matchers/have_key_with_value_matcher.go @@ -7,6 +7,7 @@ import ( "reflect" "github.com/onsi/gomega/format" + "github.com/onsi/gomega/matchers/internal/miter" ) type HaveKeyWithValueMatcher struct { @@ -15,8 +16,8 @@ type HaveKeyWithValueMatcher struct { } func (matcher *HaveKeyWithValueMatcher) Match(actual interface{}) (success bool, err error) { - if !isMap(actual) { - return false, fmt.Errorf("HaveKeyWithValue matcher expects a map. Got:%s", format.Object(actual, 1)) + if !isMap(actual) && !miter.IsSeq2(actual) { + return false, fmt.Errorf("HaveKeyWithValue matcher expects a map/iter.Seq2. Got:%s", format.Object(actual, 1)) } keyMatcher, keyIsMatcher := matcher.Key.(omegaMatcher) @@ -29,6 +30,27 @@ func (matcher *HaveKeyWithValueMatcher) Match(actual interface{}) (success bool, valueMatcher = &EqualMatcher{Expected: matcher.Value} } + if miter.IsSeq2(actual) { + var success bool + var err error + miter.IterateKV(actual, func(k, v reflect.Value) bool { + success, err = keyMatcher.Match(k.Interface()) + if err != nil { + err = fmt.Errorf("HaveKey's key matcher failed with:\n%s%s", format.Indent, err.Error()) + return false + } + if success { + success, err = valueMatcher.Match(v.Interface()) + if err != nil { + err = fmt.Errorf("HaveKeyWithValue's value matcher failed with:\n%s%s", format.Indent, err.Error()) + return false + } + } + return !success + }) + return success, err + } + keys := reflect.ValueOf(actual).MapKeys() for i := 0; i < len(keys); i++ { success, err := keyMatcher.Match(keys[i].Interface()) diff --git a/matchers/have_key_with_value_matcher_test.go b/matchers/have_key_with_value_matcher_test.go index 42dd3c685..a0e81cf94 100644 --- a/matchers/have_key_with_value_matcher_test.go +++ b/matchers/have_key_with_value_matcher_test.go @@ -4,6 +4,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" . "github.com/onsi/gomega/matchers" + "github.com/onsi/gomega/matchers/internal/miter" ) var _ = Describe("HaveKeyWithValue", func() { @@ -79,4 +80,59 @@ var _ = Describe("HaveKeyWithValue", func() { Expect(err).Should(HaveOccurred()) }) }) + + Context("iterators", func() { + BeforeEach(func() { + if !miter.HasIterators() { + Skip("iterators not available") + } + }) + + When("passed an iter.Seq2", func() { + It("should do the right thing", func() { + Expect(universalMapIter2).Should(HaveKeyWithValue("foo", 0)) + Expect(universalMapIter2).ShouldNot(HaveKeyWithValue("foo", 1)) + Expect(universalMapIter2).ShouldNot(HaveKeyWithValue("baz", 2)) + Expect(universalMapIter2).ShouldNot(HaveKeyWithValue("baz", 1)) + + Expect(universalMapIter2).Should(HaveKeyWithValue("bar", 42)) + Expect(universalMapIter2).Should(HaveKeyWithValue("baz", 666)) + + Expect(universalMapIter2).ShouldNot(HaveKeyWithValue("bar", "abc")) + Expect(universalMapIter2).ShouldNot(HaveKeyWithValue(555, "abc")) + }) + }) + + When("passed a correctly typed nil", func() { + It("should operate succesfully on the passed in value", func() { + var nilIter2 func(func(string, int) bool) + Expect(nilIter2).ShouldNot(HaveKeyWithValue("foo", 0)) + }) + }) + + When("the passed in key or value is actually a matcher", func() { + It("should pass each element through the matcher", func() { + Expect(universalMapIter2).Should(HaveKeyWithValue(ContainSubstring("oo"), BeNumerically("<", 1))) + Expect(universalMapIter2).Should(HaveKeyWithValue(ContainSubstring("foo"), 0)) + }) + + It("should fail if the matcher ever fails", func() { + success, err := (&HaveKeyWithValueMatcher{Key: "bar", Value: ContainSubstring("argh")}).Match(universalMapIter2) + Expect(success).Should(BeFalse()) + Expect(err).Should(HaveOccurred()) + + success, err = (&HaveKeyWithValueMatcher{Key: "foo", Value: ContainSubstring("1")}).Match(universalMapIter2) + Expect(success).Should(BeFalse()) + Expect(err).Should(HaveOccurred()) + }) + }) + + When("passed something that is not an iter.Seq2", func() { + It("should error", func() { + success, err := (&HaveKeyWithValueMatcher{Key: "foo", Value: "bar"}).Match(universalIter) + Expect(success).Should(BeFalse()) + Expect(err).Should(HaveOccurred()) + }) + }) + }) }) diff --git a/matchers/have_len_matcher.go b/matchers/have_len_matcher.go index ee4276189..ca25713fe 100644 --- a/matchers/have_len_matcher.go +++ b/matchers/have_len_matcher.go @@ -13,7 +13,7 @@ type HaveLenMatcher struct { func (matcher *HaveLenMatcher) Match(actual interface{}) (success bool, err error) { length, ok := lengthOf(actual) if !ok { - return false, fmt.Errorf("HaveLen matcher expects a string/array/map/channel/slice. Got:\n%s", format.Object(actual, 1)) + return false, fmt.Errorf("HaveLen matcher expects a string/array/map/channel/slice/iterator. Got:\n%s", format.Object(actual, 1)) } return length == matcher.Count, nil diff --git a/matchers/have_len_matcher_test.go b/matchers/have_len_matcher_test.go index 069c017fd..0d3ed964e 100644 --- a/matchers/have_len_matcher_test.go +++ b/matchers/have_len_matcher_test.go @@ -1,6 +1,8 @@ package matchers_test import ( + "github.com/onsi/gomega/matchers/internal/miter" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" . "github.com/onsi/gomega/matchers" @@ -50,4 +52,32 @@ var _ = Describe("HaveLen", func() { Expect(err).Should(HaveOccurred()) }) }) + + Context("iterators", func() { + BeforeEach(func() { + if !miter.HasIterators() { + Skip("iterators not available") + } + }) + + When("passed an iterator type", func() { + It("should do the right thing", func() { + Expect(emptyIter).To(HaveLen(0)) + Expect(emptyIter2).To(HaveLen(0)) + + Expect(universalIter).To(HaveLen(len(universalElements))) + Expect(universalIter2).To(HaveLen(len(universalElements))) + }) + }) + + When("passed a correctly typed nil", func() { + It("should operate succesfully on the passed in value", func() { + var nilIter func(func(string) bool) + Expect(nilIter).Should(HaveLen(0)) + + var nilIter2 func(func(int, string) bool) + Expect(nilIter2).Should(HaveLen(0)) + }) + }) + }) }) diff --git a/matchers/internal/miter/miter_suite_test.go b/matchers/internal/miter/miter_suite_test.go new file mode 100644 index 000000000..43a84919d --- /dev/null +++ b/matchers/internal/miter/miter_suite_test.go @@ -0,0 +1,13 @@ +package miter_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestMatcherIter(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Matcher Iter Support Suite") +} diff --git a/matchers/internal/miter/type_support_iter.go b/matchers/internal/miter/type_support_iter.go new file mode 100644 index 000000000..d8837a4d0 --- /dev/null +++ b/matchers/internal/miter/type_support_iter.go @@ -0,0 +1,128 @@ +//go:build go1.23 + +package miter + +import ( + "reflect" +) + +// HasIterators always returns false for Go versions before 1.23. +func HasIterators() bool { return true } + +// IsIter returns true if the specified value is a function type that can be +// range-d over, otherwise false. +// +// We don't use reflect's CanSeq and CanSeq2 directly, as these would return +// true also for other value types that are range-able, such as integers, +// slices, et cetera. Here, we aim only at range-able (iterator) functions. +func IsIter(it any) bool { + if it == nil { // on purpose we only test for untyped nil. + return false + } + // reject all non-iterator-func values, even if they're range-able. + t := reflect.TypeOf(it) + if t.Kind() != reflect.Func { + return false + } + return t.CanSeq() || t.CanSeq2() +} + +// IterKVTypes returns the reflection types of an iterator's yield function's K +// and optional V arguments, otherwise nil K and V reflection types. +func IterKVTypes(it any) (k, v reflect.Type) { + if it == nil { + return + } + // reject all non-iterator-func values, even if they're range-able. + t := reflect.TypeOf(it) + if t.Kind() != reflect.Func { + return + } + // get the reflection types for V, and where applicable, K. + switch { + case t.CanSeq(): + v = t. /*iterator fn*/ In(0). /*yield fn*/ In(0) + case t.CanSeq2(): + yieldfn := t. /*iterator fn*/ In(0) + k = yieldfn.In(0) + v = yieldfn.In(1) + } + return +} + +// IsSeq2 returns true if the passed iterator function is compatible with +// iter.Seq2, otherwise false. +// +// IsSeq2 hides the Go 1.23+ specific reflect.Type.CanSeq2 behind a facade which +// is empty for Go versions before 1.23. +func IsSeq2(it any) bool { + if it == nil { + return false + } + t := reflect.TypeOf(it) + return t.Kind() == reflect.Func && t.CanSeq2() +} + +// isNilly returns true if v is either an untyped nil, or is a nil function (not +// necessarily an iterator function). +func isNilly(v any) bool { + if v == nil { + return true + } + rv := reflect.ValueOf(v) + return rv.Kind() == reflect.Func && rv.IsNil() +} + +// IterateV loops over the elements produced by an iterator function, passing +// the elements to the specified yield function individually and stopping only +// when either the iterator function runs out of elements or the yield function +// tell us to stop it. +// +// IterateV works very much like reflect.Value.Seq but hides the Go 1.23+ +// specific parts behind a facade which is empty for Go versions before 1.23, in +// order to simplify code maintenance for matchers when using older Go versions. +func IterateV(it any, yield func(v reflect.Value) bool) { + if isNilly(it) { + return + } + // reject all non-iterator-func values, even if they're range-able. + t := reflect.TypeOf(it) + if t.Kind() != reflect.Func || !t.CanSeq() { + return + } + // Call the specified iterator function, handing it our adaptor to call the + // specified generic reflection yield function. + reflectedYield := reflect.MakeFunc( + t. /*iterator fn*/ In(0), + func(args []reflect.Value) []reflect.Value { + return []reflect.Value{reflect.ValueOf(yield(args[0]))} + }) + reflect.ValueOf(it).Call([]reflect.Value{reflectedYield}) +} + +// IterateKV loops over the key-value elements produced by an iterator function, +// passing the elements to the specified yield function individually and +// stopping only when either the iterator function runs out of elements or the +// yield function tell us to stop it. +// +// IterateKV works very much like reflect.Value.Seq2 but hides the Go 1.23+ +// specific parts behind a facade which is empty for Go versions before 1.23, in +// order to simplify code maintenance for matchers when using older Go versions. +func IterateKV(it any, yield func(k, v reflect.Value) bool) { + if isNilly(it) { + return + } + // reject all non-iterator-func values, even if they're range-able. + t := reflect.TypeOf(it) + if t.Kind() != reflect.Func || !t.CanSeq2() { + return + } + // Call the specified iterator function, handing it our adaptor to call the + // specified generic reflection yield function. + reflectedYield := reflect.MakeFunc( + t. /*iterator fn*/ In(0), + func(args []reflect.Value) []reflect.Value { + return []reflect.Value{reflect.ValueOf(yield(args[0], args[1]))} + }) + reflect.ValueOf(it).Call([]reflect.Value{reflectedYield}) +} diff --git a/matchers/internal/miter/type_support_iter_test.go b/matchers/internal/miter/type_support_iter_test.go new file mode 100644 index 000000000..e561a4056 --- /dev/null +++ b/matchers/internal/miter/type_support_iter_test.go @@ -0,0 +1,211 @@ +//go:build go1.23 + +package miter_test + +import ( + "reflect" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + . "github.com/onsi/gomega/matchers/internal/miter" +) + +var _ = Describe("iterator function types", func() { + + When("detecting iterator functions", func() { + + It("doesn't match a nil value", func() { + Expect(IsIter(nil)).To(BeFalse()) + }) + + It("doesn't match a range-able numeric value", func() { + Expect(IsIter(42)).To(BeFalse()) + }) + + It("doesn't match a non-iter function", func() { + Expect(IsIter(func(yabadabadu string) {})).To(BeFalse()) + }) + + It("matches an iter.Seq-like iter function", func() { + Expect(IsIter(func(yield func(v int) bool) {})).To(BeTrue()) + var nilIter func(func(string) bool) + Expect(IsIter(nilIter)).To(BeTrue()) + }) + + It("matches an iter.Seq2-like iter function", func() { + Expect(IsIter(func(yield func(k uint, v string) bool) {})).To(BeTrue()) + var nilIter2 func(func(string, bool) bool) + Expect(IsIter(nilIter2)).To(BeTrue()) + }) + + }) + + It("detects iter.Seq2", func() { + Expect(IsSeq2(42)).To(BeFalse()) + Expect(IsSeq2(func(func(int) bool) {})).To(BeFalse()) + Expect(IsSeq2(func(func(int, int) bool) {})).To(BeTrue()) + + var nilIter2 func(func(string, bool) bool) + Expect(IsSeq2(nilIter2)).To(BeTrue()) + }) + + When("getting iterator function K, V types", func() { + + It("has no types when nil", func() { + k, v := IterKVTypes(nil) + Expect(k).To(BeNil()) + Expect(v).To(BeNil()) + }) + + It("has no types for range-able numbers", func() { + k, v := IterKVTypes(42) + Expect(k).To(BeNil()) + Expect(v).To(BeNil()) + }) + + It("returns correct reflection type for the iterator's V", func() { + type foo uint + k, v := IterKVTypes(func(yield func(v foo) bool) {}) + Expect(k).To(BeNil()) + Expect(v).To(Equal(reflect.TypeOf(foo(42)))) + }) + + It("returns correct reflection types for the iterator's K and V", func() { + type foo uint + type bar string + k, v := IterKVTypes(func(yield func(k foo, v bar) bool) {}) + Expect(k).To(Equal(reflect.TypeOf(foo(42)))) + Expect(v).To(Equal(reflect.TypeOf(bar("")))) + }) + + }) + + When("iterating single value reflections", func() { + + iterelements := []string{"foo", "bar", "baz"} + + it := func(yield func(v string) bool) { + for _, el := range iterelements { + if !yield(el) { + break + } + } + } + + It("doesn't loop over a nil iterator", func() { + Expect(func() { + IterateV(nil, func(v reflect.Value) bool { panic("reflection yield must not be called") }) + }).NotTo(Panic()) + }) + + It("doesn't loop over a typed-nil iterator", func() { + var nilIter func(func(string) bool) + Expect(func() { + IterateV(nilIter, func(v reflect.Value) bool { panic("reflection yield must not be called") }) + }).NotTo(Panic()) + }) + + It("doesn't loop over a non-iterator value", func() { + Expect(func() { + IterateV(42, func(v reflect.Value) bool { panic("reflection yield must not be called") }) + }).NotTo(Panic()) + }) + + It("doesn't loop over an iter.Seq2", func() { + Expect(func() { + IterateV( + func(k uint, v string) bool { panic("it.Seq2 must not be called") }, + func(v reflect.Value) bool { panic("reflection yield must not be called") }) + }).NotTo(Panic()) + }) + + It("yields all reflection values", func() { + els := []string{} + IterateV(it, func(v reflect.Value) bool { + els = append(els, v.String()) + return true + }) + Expect(els).To(ConsistOf(iterelements)) + }) + + It("stops yielding reflection values before reaching THE END", func() { + els := []string{} + IterateV(it, func(v reflect.Value) bool { + els = append(els, v.String()) + return len(els) < 2 + }) + Expect(els).To(ConsistOf(iterelements[:2])) + }) + + }) + + When("iterating key-value reflections", func() { + + type kv struct { + k uint + v string + } + + iterelements := []kv{ + {k: 42, v: "foo"}, + {k: 66, v: "bar"}, + {k: 666, v: "baz"}, + } + + it := func(yield func(k uint, v string) bool) { + for _, el := range iterelements { + if !yield(el.k, el.v) { + break + } + } + } + + It("doesn't loop over a nil iterator", func() { + Expect(func() { + IterateKV(nil, func(k, v reflect.Value) bool { panic("reflection yield must not be called") }) + }).NotTo(Panic()) + }) + + It("doesn't loop over a typed-nil iterator", func() { + var nilIter2 func(func(int, string) bool) + Expect(func() { + IterateKV(nilIter2, func(k, v reflect.Value) bool { panic("reflection yield must not be called") }) + }).NotTo(Panic()) + }) + + It("doesn't loop over a non-iterator value", func() { + Expect(func() { + IterateKV(42, func(k, v reflect.Value) bool { panic("reflection yield must not be called") }) + }).NotTo(Panic()) + }) + + It("doesn't loop over an iter.Seq", func() { + Expect(func() { + IterateKV( + func(v string) bool { panic("it.Seq must not be called") }, + func(k, v reflect.Value) bool { panic("reflection yield must not be called") }) + }).NotTo(Panic()) + }) + + It("yields all reflection key-values", func() { + els := []kv{} + IterateKV(it, func(k, v reflect.Value) bool { + els = append(els, kv{k: uint(k.Uint()), v: v.String()}) + return true + }) + Expect(els).To(ConsistOf(iterelements)) + }) + + It("stops yielding reflection key-values before reaching THE END", func() { + els := []kv{} + IterateKV(it, func(k, v reflect.Value) bool { + els = append(els, kv{k: uint(k.Uint()), v: v.String()}) + return len(els) < 2 + }) + Expect(els).To(ConsistOf(iterelements[:2])) + }) + + }) + +}) diff --git a/matchers/internal/miter/type_support_noiter.go b/matchers/internal/miter/type_support_noiter.go new file mode 100644 index 000000000..4b8fcc55b --- /dev/null +++ b/matchers/internal/miter/type_support_noiter.go @@ -0,0 +1,44 @@ +//go:build !go1.23 + +/* +Gomega matchers + +This package implements the Gomega matchers and does not typically need to be imported. +See the docs for Gomega for documentation on the matchers + +http://onsi.github.io/gomega/ +*/ + +package miter + +import "reflect" + +// HasIterators always returns false for Go versions before 1.23. +func HasIterators() bool { return false } + +// IsIter always returns false for Go versions before 1.23 as there is no +// iterator (function) pattern defined yet; see also: +// https://tip.golang.org/blog/range-functions. +func IsIter(i any) bool { return false } + +// IsSeq2 always returns false for Go versions before 1.23 as there is no +// iterator (function) pattern defined yet; see also: +// https://tip.golang.org/blog/range-functions. +func IsSeq2(it any) bool { return false } + +// IterKVTypes always returns nil reflection types for Go versions before 1.23 +// as there is no iterator (function) pattern defined yet; see also: +// https://tip.golang.org/blog/range-functions. +func IterKVTypes(i any) (k, v reflect.Type) { + return +} + +// IterateV never loops over what has been passed to it as an iterator for Go +// versions before 1.23 as there is no iterator (function) pattern defined yet; +// see also: https://tip.golang.org/blog/range-functions. +func IterateV(it any, yield func(v reflect.Value) bool) {} + +// IterateKV never loops over what has been passed to it as an iterator for Go +// versions before 1.23 as there is no iterator (function) pattern defined yet; +// see also: https://tip.golang.org/blog/range-functions. +func IterateKV(it any, yield func(k, v reflect.Value) bool) {} diff --git a/matchers/iter_support_test.go b/matchers/iter_support_test.go new file mode 100644 index 000000000..d728db37e --- /dev/null +++ b/matchers/iter_support_test.go @@ -0,0 +1,55 @@ +package matchers_test + +var ( + universalElements = []string{"foo", "bar", "baz"} + universalMap = map[string]int{ + "foo": 0, + "bar": 42, + "baz": 666, + } + fooElements = []string{"foo", "foo", "foo"} +) + +func universalIter(yield func(string) bool) { + for _, element := range universalElements { + if !yield(element) { + return + } + } +} + +func universalIter2(yield func(int, string) bool) { + for idx, element := range universalElements { + if !yield(idx, element) { + return + } + } +} + +func emptyIter(yield func(string) bool) {} + +func emptyIter2(yield func(int, string) bool) {} + +func universalMapIter2(yield func(string, int) bool) { + for k, v := range universalMap { + if !yield(k, v) { + return + } + } +} + +func fooIter(yield func(string) bool) { + for _, foo := range fooElements { + if !yield(foo) { + return + } + } +} + +func fooIter2(yield func(int, string) bool) { + for idx, foo := range fooElements { + if !yield(idx, foo) { + return + } + } +} diff --git a/matchers/type_support.go b/matchers/type_support.go index dced2419e..b9440ac7a 100644 --- a/matchers/type_support.go +++ b/matchers/type_support.go @@ -15,6 +15,8 @@ import ( "encoding/json" "fmt" "reflect" + + "github.com/onsi/gomega/matchers/internal/miter" ) type omegaMatcher interface { @@ -152,6 +154,17 @@ func lengthOf(a interface{}) (int, bool) { switch reflect.TypeOf(a).Kind() { case reflect.Map, reflect.Array, reflect.String, reflect.Chan, reflect.Slice: return reflect.ValueOf(a).Len(), true + case reflect.Func: + if !miter.IsIter(a) { + return 0, false + } + var l int + if miter.IsSeq2(a) { + miter.IterateKV(a, func(k, v reflect.Value) bool { l++; return true }) + } else { + miter.IterateV(a, func(v reflect.Value) bool { l++; return true }) + } + return l, true default: return 0, false }