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 }