Skip to content

Commit 4c964c6

Browse files
thediveoonsi
authored andcommittedNov 24, 2024·
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 <thediveo@gmx.eu>
1 parent ece6872 commit 4c964c6

25 files changed

+1527
-92
lines changed
 

‎docs/index.md

+47-12
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,8 @@ A number of community-supported matchers have appeared as well. A list is maint
690690

691691
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.
692692

693+
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.
694+
693695
### Asserting Equivalence
694696

695697
#### Equal(expected interface{})
@@ -1114,15 +1116,15 @@ It is an error for either `ACTUAL` or `EXPECTED` to be invalid YAML.
11141116
Ω(ACTUAL).Should(BeEmpty())
11151117
```
11161118

1117-
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.
1119+
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.
11181120

11191121
#### HaveLen(count int)
11201122

11211123
```go
11221124
Ω(ACTUAL).Should(HaveLen(INT))
11231125
```
11241126

1125-
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.
1127+
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.
11261128

11271129
#### HaveCap(count int)
11281130

@@ -1145,7 +1147,7 @@ or
11451147
```
11461148

11471149

1148-
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!).
1150+
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.
11491151

11501152
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:
11511153

@@ -1176,6 +1178,34 @@ var findings map[int]string
11761178
}).Should(ContainElement(ContainSubstring("foo"), &findings))
11771179
```
11781180

1181+
In case of `iter.Seq` and `iter.Seq2`-like iterators, the matching contained elements can be returned in the slice referenced by the pointer.
1182+
1183+
```go
1184+
it := func(yield func(string) bool) {
1185+
for _, element := range []string{"foo", "bar", "baz"} {
1186+
if !yield(element) {
1187+
return
1188+
}
1189+
}
1190+
}
1191+
var findings []string
1192+
Ω(it).Should(ContainElement(HasPrefix("ba"), &findings))
1193+
```
1194+
1195+
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.
1196+
1197+
```go
1198+
it := func(yield func(int, string) bool) {
1199+
for key, element := range []string{"foo", "bar", "baz"} {
1200+
if !yield(key, element) {
1201+
return
1202+
}
1203+
}
1204+
}
1205+
var findings map[int]string
1206+
Ω(it).Should(ContainElement(HasPrefix("ba"), &findings))
1207+
```
1208+
11791209
#### ContainElements(element ...interface{})
11801210

11811211
```go
@@ -1197,7 +1227,7 @@ By default `ContainElements()` uses `Equal()` to match the elements, however cus
11971227
Ω([]string{"Foo", "FooBar"}).Should(ContainElements(ContainSubstring("Bar"), "Foo"))
11981228
```
11991229

1200-
Actual must be an `array`, `slice` or `map`. For maps, `ContainElements` matches against the `map`'s values.
1230+
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.
12011231

12021232
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
12031233
is the only element passed in to `ContainElements`:
@@ -1208,6 +1238,8 @@ is the only element passed in to `ContainElements`:
12081238

12091239
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.
12101240

1241+
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`.
1242+
12111243
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.
12121244

12131245
#### BeElementOf(elements ...interface{})
@@ -1263,17 +1295,18 @@ By default `ConsistOf()` uses `Equal()` to match the elements, however custom ma
12631295
Ω([]string{"Foo", "FooBar"}).Should(ConsistOf(ContainSubstring("Foo"), ContainSubstring("Foo")))
12641296
```
12651297

1266-
Actual must be an `array`, `slice` or `map`. For maps, `ConsistOf` matches against the `map`'s values.
1298+
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.
12671299

1268-
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
1269-
is the only element passed in to `ConsistOf`:
1300+
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`:
12701301

12711302
```go
12721303
Ω([]string{"Foo", "FooBar"}).Should(ConsistOf([]string{"FooBar", "Foo"}))
12731304
```
12741305

12751306
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.
12761307

1308+
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`.
1309+
12771310
#### HaveExactElements(element ...interface{})
12781311

12791312
```go
@@ -1296,7 +1329,7 @@ Expect([]string{"Foo", "FooBar"}).To(HaveExactElements("Foo", ContainSubstring("
12961329
Expect([]string{"Foo", "FooBar"}).To(HaveExactElements(ContainSubstring("Foo"), ContainSubstring("Foo")))
12971330
```
12981331

1299-
Actual must be an `array` or `slice`.
1332+
`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`).
13001333

13011334
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
13021335
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
13131346
Ω(ACTUAL).Should(HaveEach(ELEMENT))
13141347
```
13151348

1316-
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!).
1349+
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.
13171350

1318-
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.
1351+
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.
13191352

13201353
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:
13211354

@@ -1329,7 +1362,7 @@ By default `HaveEach()` uses the `Equal()` matcher under the hood to assert equa
13291362
Ω(ACTUAL).Should(HaveKey(KEY))
13301363
```
13311364

1332-
succeeds if `ACTUAL` is a map with a key that equals `KEY`. It is an error for `ACTUAL` to not be a `map`.
1365+
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`.
13331366

13341367
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:
13351368

@@ -1343,14 +1376,16 @@ By default `HaveKey()` uses the `Equal()` matcher under the hood to assert equal
13431376
Ω(ACTUAL).Should(HaveKeyWithValue(KEY, VALUE))
13441377
```
13451378

1346-
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`.
1379+
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`.
13471380

13481381
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:
13491382

13501383
```go
13511384
Ω(map[string]int{"Foo": 3, "BazFoo": 4}).Should(HaveKeyWithValue(MatchRegexp(`.+Foo$`), BeNumerically(">", 3)))
13521385
```
13531386

1387+
### Working with Structs
1388+
13541389
#### HaveField(field interface{}, value interface{})
13551390

13561391
```go

‎matchers/be_empty_matcher.go

+15-1
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,31 @@ package matchers
44

55
import (
66
"fmt"
7+
"reflect"
78

89
"github.com/onsi/gomega/format"
10+
"github.com/onsi/gomega/matchers/internal/miter"
911
)
1012

1113
type BeEmptyMatcher struct {
1214
}
1315

1416
func (matcher *BeEmptyMatcher) Match(actual interface{}) (success bool, err error) {
17+
// short-circuit the iterator case, as we only need to see the first
18+
// element, if any.
19+
if miter.IsIter(actual) {
20+
var length int
21+
if miter.IsSeq2(actual) {
22+
miter.IterateKV(actual, func(k, v reflect.Value) bool { length++; return false })
23+
} else {
24+
miter.IterateV(actual, func(v reflect.Value) bool { length++; return false })
25+
}
26+
return length == 0, nil
27+
}
28+
1529
length, ok := lengthOf(actual)
1630
if !ok {
17-
return false, fmt.Errorf("BeEmpty matcher expects a string/array/map/channel/slice. Got:\n%s", format.Object(actual, 1))
31+
return false, fmt.Errorf("BeEmpty matcher expects a string/array/map/channel/slice/iterator. Got:\n%s", format.Object(actual, 1))
1832
}
1933

2034
return length == 0, nil

‎matchers/be_empty_matcher_test.go

+29
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
. "github.com/onsi/ginkgo/v2"
55
. "github.com/onsi/gomega"
66
. "github.com/onsi/gomega/matchers"
7+
"github.com/onsi/gomega/matchers/internal/miter"
78
)
89

910
var _ = Describe("BeEmpty", func() {
@@ -49,4 +50,32 @@ var _ = Describe("BeEmpty", func() {
4950
Expect(err).Should(HaveOccurred())
5051
})
5152
})
53+
54+
Context("iterators", func() {
55+
BeforeEach(func() {
56+
if !miter.HasIterators() {
57+
Skip("iterators not available")
58+
}
59+
})
60+
61+
When("passed an iterator type", func() {
62+
It("should do the right thing", func() {
63+
Expect(emptyIter).To(BeEmpty())
64+
Expect(emptyIter2).To(BeEmpty())
65+
66+
Expect(universalIter).NotTo(BeEmpty())
67+
Expect(universalIter2).NotTo(BeEmpty())
68+
})
69+
})
70+
71+
When("passed a correctly typed nil", func() {
72+
It("should be true", func() {
73+
var nilIter func(func(string) bool)
74+
Expect(nilIter).Should(BeEmpty())
75+
76+
var nilIter2 func(func(int, string) bool)
77+
Expect(nilIter2).Should(BeEmpty())
78+
})
79+
})
80+
})
5281
})

‎matchers/consist_of.go

+28-4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"reflect"
88

99
"github.com/onsi/gomega/format"
10+
"github.com/onsi/gomega/matchers/internal/miter"
1011
"github.com/onsi/gomega/matchers/support/goraph/bipartitegraph"
1112
)
1213

@@ -17,8 +18,8 @@ type ConsistOfMatcher struct {
1718
}
1819

1920
func (matcher *ConsistOfMatcher) Match(actual interface{}) (success bool, err error) {
20-
if !isArrayOrSlice(actual) && !isMap(actual) {
21-
return false, fmt.Errorf("ConsistOf matcher expects an array/slice/map. Got:\n%s", format.Object(actual, 1))
21+
if !isArrayOrSlice(actual) && !isMap(actual) && !miter.IsIter(actual) {
22+
return false, fmt.Errorf("ConsistOf matcher expects an array/slice/map/iter.Seq/iter.Seq2. Got:\n%s", format.Object(actual, 1))
2223
}
2324

2425
matchers := matchers(matcher.Elements)
@@ -60,10 +61,21 @@ func equalMatchersToElements(matchers []interface{}) (elements []interface{}) {
6061
}
6162

6263
func flatten(elems []interface{}) []interface{} {
63-
if len(elems) != 1 || !isArrayOrSlice(elems[0]) {
64+
if len(elems) != 1 ||
65+
!(isArrayOrSlice(elems[0]) ||
66+
(miter.IsIter(elems[0]) && !miter.IsSeq2(elems[0]))) {
6467
return elems
6568
}
6669

70+
if miter.IsIter(elems[0]) {
71+
flattened := []any{}
72+
miter.IterateV(elems[0], func(v reflect.Value) bool {
73+
flattened = append(flattened, v.Interface())
74+
return true
75+
})
76+
return flattened
77+
}
78+
6779
value := reflect.ValueOf(elems[0])
6880
flattened := make([]interface{}, value.Len())
6981
for i := 0; i < value.Len(); i++ {
@@ -116,7 +128,19 @@ func presentable(elems []interface{}) interface{} {
116128
func valuesOf(actual interface{}) []interface{} {
117129
value := reflect.ValueOf(actual)
118130
values := []interface{}{}
119-
if isMap(actual) {
131+
if miter.IsIter(actual) {
132+
if miter.IsSeq2(actual) {
133+
miter.IterateKV(actual, func(k, v reflect.Value) bool {
134+
values = append(values, v.Interface())
135+
return true
136+
})
137+
} else {
138+
miter.IterateV(actual, func(v reflect.Value) bool {
139+
values = append(values, v.Interface())
140+
return true
141+
})
142+
}
143+
} else if isMap(actual) {
120144
keys := value.MapKeys()
121145
for i := 0; i < value.Len(); i++ {
122146
values = append(values, value.MapIndex(keys[i]).Interface())

‎matchers/consist_of_test.go

+39
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package matchers_test
33
import (
44
. "github.com/onsi/ginkgo/v2"
55
. "github.com/onsi/gomega"
6+
"github.com/onsi/gomega/matchers/internal/miter"
67
)
78

89
var _ = Describe("ConsistOf", func() {
@@ -196,4 +197,42 @@ the extra elements were
196197
})
197198
})
198199
})
200+
201+
Context("iterators", func() {
202+
BeforeEach(func() {
203+
if !miter.HasIterators() {
204+
Skip("iterators not available")
205+
}
206+
})
207+
208+
Context("with an iter.Seq", func() {
209+
It("should do the right thing", func() {
210+
Expect(universalIter).Should(ConsistOf("foo", "bar", "baz"))
211+
Expect(universalIter).Should(ConsistOf("foo", "bar", "baz"))
212+
Expect(universalIter).Should(ConsistOf("baz", "bar", "foo"))
213+
Expect(universalIter).ShouldNot(ConsistOf("baz", "bar", "foo", "foo"))
214+
Expect(universalIter).ShouldNot(ConsistOf("baz", "foo"))
215+
})
216+
})
217+
218+
Context("with an iter.Seq2", func() {
219+
It("should do the right thing", func() {
220+
Expect(universalIter2).Should(ConsistOf("foo", "bar", "baz"))
221+
Expect(universalIter2).Should(ConsistOf("foo", "bar", "baz"))
222+
Expect(universalIter2).Should(ConsistOf("baz", "bar", "foo"))
223+
Expect(universalIter2).ShouldNot(ConsistOf("baz", "bar", "foo", "foo"))
224+
Expect(universalIter2).ShouldNot(ConsistOf("baz", "foo"))
225+
})
226+
})
227+
228+
When("passed exactly one argument, and that argument is an iter.Seq", func() {
229+
It("should match against the elements of that argument", func() {
230+
Expect(universalIter).Should(ConsistOf(universalIter))
231+
Expect(universalIter).ShouldNot(ConsistOf(fooElements))
232+
233+
Expect(universalIter2).Should(ConsistOf(universalIter))
234+
Expect(universalIter2).ShouldNot(ConsistOf(fooElements))
235+
})
236+
})
237+
})
199238
})

‎matchers/contain_element_matcher.go

+179-60
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"reflect"
99

1010
"github.com/onsi/gomega/format"
11+
"github.com/onsi/gomega/matchers/internal/miter"
1112
)
1213

1314
type ContainElementMatcher struct {
@@ -16,16 +17,18 @@ type ContainElementMatcher struct {
1617
}
1718

1819
func (matcher *ContainElementMatcher) Match(actual interface{}) (success bool, err error) {
19-
if !isArrayOrSlice(actual) && !isMap(actual) {
20-
return false, fmt.Errorf("ContainElement matcher expects an array/slice/map. Got:\n%s", format.Object(actual, 1))
20+
if !isArrayOrSlice(actual) && !isMap(actual) && !miter.IsIter(actual) {
21+
return false, fmt.Errorf("ContainElement matcher expects an array/slice/map/iterator. Got:\n%s", format.Object(actual, 1))
2122
}
2223

2324
var actualT reflect.Type
2425
var result reflect.Value
25-
switch l := len(matcher.Result); {
26-
case l > 1:
26+
switch numResultArgs := len(matcher.Result); {
27+
case numResultArgs > 1:
2728
return false, errors.New("ContainElement matcher expects at most a single optional pointer to store its findings at")
28-
case l == 1:
29+
case numResultArgs == 1:
30+
// Check the optional result arg to point to a single value/array/slice/map
31+
// of a type compatible with the actual value.
2932
if reflect.ValueOf(matcher.Result[0]).Kind() != reflect.Ptr {
3033
return false, fmt.Errorf("ContainElement matcher expects a non-nil pointer to store its findings at. Got\n%s",
3134
format.Object(matcher.Result[0], 1))
@@ -34,93 +37,209 @@ func (matcher *ContainElementMatcher) Match(actual interface{}) (success bool, e
3437
resultReference := matcher.Result[0]
3538
result = reflect.ValueOf(resultReference).Elem() // what ResultReference points to, to stash away our findings
3639
switch result.Kind() {
37-
case reflect.Array:
40+
case reflect.Array: // result arrays are not supported, as they cannot be dynamically sized.
41+
if miter.IsIter(actual) {
42+
_, actualvT := miter.IterKVTypes(actual)
43+
return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s",
44+
reflect.SliceOf(actualvT), result.Type().String())
45+
}
3846
return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s",
3947
reflect.SliceOf(actualT.Elem()).String(), result.Type().String())
40-
case reflect.Slice:
41-
if !isArrayOrSlice(actual) {
48+
49+
case reflect.Slice: // result slice
50+
// can we assign elements in actual to elements in what the result
51+
// arg points to?
52+
// - ✔ actual is an array or slice
53+
// - ✔ actual is an iter.Seq producing "v" elements
54+
// - ✔ actual is an iter.Seq2 producing "v" elements, ignoring
55+
// the "k" elements.
56+
switch {
57+
case isArrayOrSlice(actual):
58+
if !actualT.Elem().AssignableTo(result.Type().Elem()) {
59+
return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s",
60+
actualT.String(), result.Type().String())
61+
}
62+
63+
case miter.IsIter(actual):
64+
_, actualvT := miter.IterKVTypes(actual)
65+
if !actualvT.AssignableTo(result.Type().Elem()) {
66+
return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s",
67+
actualvT.String(), result.Type().String())
68+
}
69+
70+
default: // incompatible result reference
4271
return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s",
4372
reflect.MapOf(actualT.Key(), actualT.Elem()).String(), result.Type().String())
4473
}
45-
if !actualT.Elem().AssignableTo(result.Type().Elem()) {
46-
return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s",
47-
actualT.String(), result.Type().String())
48-
}
49-
case reflect.Map:
50-
if !isMap(actual) {
51-
return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s",
52-
actualT.String(), result.Type().String())
53-
}
54-
if !actualT.AssignableTo(result.Type()) {
74+
75+
case reflect.Map: // result map
76+
// can we assign elements in actual to elements in what the result
77+
// arg points to?
78+
// - ✔ actual is a map
79+
// - ✔ actual is an iter.Seq2 (iter.Seq doesn't fit though)
80+
switch {
81+
case isMap(actual):
82+
if !actualT.AssignableTo(result.Type()) {
83+
return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s",
84+
actualT.String(), result.Type().String())
85+
}
86+
87+
case miter.IsIter(actual):
88+
actualkT, actualvT := miter.IterKVTypes(actual)
89+
if actualkT == nil {
90+
return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s",
91+
reflect.SliceOf(actualvT).String(), result.Type().String())
92+
}
93+
if !reflect.MapOf(actualkT, actualvT).AssignableTo(result.Type()) {
94+
return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s",
95+
reflect.MapOf(actualkT, actualvT), result.Type().String())
96+
}
97+
98+
default: // incompatible result reference
5599
return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s",
56100
actualT.String(), result.Type().String())
57101
}
102+
58103
default:
59-
if !actualT.Elem().AssignableTo(result.Type()) {
60-
return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s",
61-
actualT.Elem().String(), result.Type().String())
104+
// can we assign a (single) element in actual to what the result arg
105+
// points to?
106+
switch {
107+
case miter.IsIter(actual):
108+
_, actualvT := miter.IterKVTypes(actual)
109+
if !actualvT.AssignableTo(result.Type()) {
110+
return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s",
111+
actualvT.String(), result.Type().String())
112+
}
113+
default:
114+
if !actualT.Elem().AssignableTo(result.Type()) {
115+
return false, fmt.Errorf("ContainElement cannot return findings. Need *%s, got *%s",
116+
actualT.Elem().String(), result.Type().String())
117+
}
62118
}
63119
}
64120
}
65121

122+
// If the supplied matcher isn't an Omega matcher, default to the Equal
123+
// matcher.
66124
elemMatcher, elementIsMatcher := matcher.Element.(omegaMatcher)
67125
if !elementIsMatcher {
68126
elemMatcher = &EqualMatcher{Expected: matcher.Element}
69127
}
70128

71129
value := reflect.ValueOf(actual)
72-
var valueAt func(int) interface{}
73130

74-
var getFindings func() reflect.Value
75-
var foundAt func(int)
131+
var getFindings func() reflect.Value // abstracts how the findings are collected and stored
132+
var lastError error
76133

77-
if isMap(actual) {
78-
keys := value.MapKeys()
79-
valueAt = func(i int) interface{} {
80-
return value.MapIndex(keys[i]).Interface()
134+
if !miter.IsIter(actual) {
135+
var valueAt func(int) interface{}
136+
var foundAt func(int)
137+
// We're dealing with an array/slice/map, so in all cases we can iterate
138+
// over the elements in actual using indices (that can be considered
139+
// keys in case of maps).
140+
if isMap(actual) {
141+
keys := value.MapKeys()
142+
valueAt = func(i int) interface{} {
143+
return value.MapIndex(keys[i]).Interface()
144+
}
145+
if result.Kind() != reflect.Invalid {
146+
fm := reflect.MakeMap(actualT)
147+
getFindings = func() reflect.Value { return fm }
148+
foundAt = func(i int) {
149+
fm.SetMapIndex(keys[i], value.MapIndex(keys[i]))
150+
}
151+
}
152+
} else {
153+
valueAt = func(i int) interface{} {
154+
return value.Index(i).Interface()
155+
}
156+
if result.Kind() != reflect.Invalid {
157+
var fsl reflect.Value
158+
if result.Kind() == reflect.Slice {
159+
fsl = reflect.MakeSlice(result.Type(), 0, 0)
160+
} else {
161+
fsl = reflect.MakeSlice(reflect.SliceOf(result.Type()), 0, 0)
162+
}
163+
getFindings = func() reflect.Value { return fsl }
164+
foundAt = func(i int) {
165+
fsl = reflect.Append(fsl, value.Index(i))
166+
}
167+
}
81168
}
82-
if result.Kind() != reflect.Invalid {
83-
fm := reflect.MakeMap(actualT)
84-
getFindings = func() reflect.Value {
85-
return fm
169+
170+
for i := 0; i < value.Len(); i++ {
171+
elem := valueAt(i)
172+
success, err := elemMatcher.Match(elem)
173+
if err != nil {
174+
lastError = err
175+
continue
86176
}
87-
foundAt = func(i int) {
88-
fm.SetMapIndex(keys[i], value.MapIndex(keys[i]))
177+
if success {
178+
if result.Kind() == reflect.Invalid {
179+
return true, nil
180+
}
181+
foundAt(i)
89182
}
90183
}
91184
} else {
92-
valueAt = func(i int) interface{} {
93-
return value.Index(i).Interface()
94-
}
185+
// We're dealing with an iterator as a first-class construct, so things
186+
// are slightly different: there is no index defined as in case of
187+
// arrays/slices/maps, just "ooooorder"
188+
var found func(k, v reflect.Value)
95189
if result.Kind() != reflect.Invalid {
96-
var f reflect.Value
97-
if result.Kind() == reflect.Slice {
98-
f = reflect.MakeSlice(result.Type(), 0, 0)
190+
if result.Kind() == reflect.Map {
191+
fm := reflect.MakeMap(result.Type())
192+
getFindings = func() reflect.Value { return fm }
193+
found = func(k, v reflect.Value) { fm.SetMapIndex(k, v) }
99194
} else {
100-
f = reflect.MakeSlice(reflect.SliceOf(result.Type()), 0, 0)
101-
}
102-
getFindings = func() reflect.Value {
103-
return f
104-
}
105-
foundAt = func(i int) {
106-
f = reflect.Append(f, value.Index(i))
195+
var fsl reflect.Value
196+
if result.Kind() == reflect.Slice {
197+
fsl = reflect.MakeSlice(result.Type(), 0, 0)
198+
} else {
199+
fsl = reflect.MakeSlice(reflect.SliceOf(result.Type()), 0, 0)
200+
}
201+
getFindings = func() reflect.Value { return fsl }
202+
found = func(_, v reflect.Value) { fsl = reflect.Append(fsl, v) }
107203
}
108204
}
109-
}
110205

111-
var lastError error
112-
for i := 0; i < value.Len(); i++ {
113-
elem := valueAt(i)
114-
success, err := elemMatcher.Match(elem)
115-
if err != nil {
116-
lastError = err
117-
continue
206+
success := false
207+
actualkT, _ := miter.IterKVTypes(actual)
208+
if actualkT == nil {
209+
miter.IterateV(actual, func(v reflect.Value) bool {
210+
var err error
211+
success, err = elemMatcher.Match(v.Interface())
212+
if err != nil {
213+
lastError = err
214+
return true // iterate on...
215+
}
216+
if success {
217+
if result.Kind() == reflect.Invalid {
218+
return false // a match and no result needed, so we're done
219+
}
220+
found(reflect.Value{}, v)
221+
}
222+
return true // iterate on...
223+
})
224+
} else {
225+
miter.IterateKV(actual, func(k, v reflect.Value) bool {
226+
var err error
227+
success, err = elemMatcher.Match(v.Interface())
228+
if err != nil {
229+
lastError = err
230+
return true // iterate on...
231+
}
232+
if success {
233+
if result.Kind() == reflect.Invalid {
234+
return false // a match and no result needed, so we're done
235+
}
236+
found(k, v)
237+
}
238+
return true // iterate on...
239+
})
118240
}
119-
if success {
120-
if result.Kind() == reflect.Invalid {
121-
return true, nil
122-
}
123-
foundAt(i)
241+
if success && result.Kind() == reflect.Invalid {
242+
return true, nil
124243
}
125244
}
126245

‎matchers/contain_element_matcher_test.go

+214
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package matchers_test
22

33
import (
4+
"github.com/onsi/gomega/matchers/internal/miter"
5+
46
. "github.com/onsi/ginkgo/v2"
57
. "github.com/onsi/gomega"
68
. "github.com/onsi/gomega/matchers"
@@ -82,6 +84,12 @@ var _ = Describe("ContainElement", func() {
8284
MatchError(MatchRegexp(`expects a non-nil pointer.+ Got\n +<nil>: nil`)))
8385
})
8486

87+
It("rejects multiple result args", func() {
88+
Expect(ContainElement("foo", 42, 43).Match([]string{"foo"})).Error().To(
89+
MatchError(MatchRegexp(`expects at most a single optional pointer`)))
90+
91+
})
92+
8593
Context("with match(es)", func() {
8694
When("passed an assignable result reference", func() {
8795
It("should assign a single finding to a scalar result reference", func() {
@@ -142,23 +150,27 @@ var _ = Describe("ContainElement", func() {
142150
var stash int
143151
Expect(ContainElement("foo", &stash).Match(actual)).Error().To(HaveOccurred())
144152
})
153+
145154
It("should error for actual []T, return reference [...]T", func() {
146155
actual := []string{"bar", "foo"}
147156
var arrstash [2]string
148157
Expect(ContainElement("foo", &arrstash).Match(actual)).Error().To(HaveOccurred())
149158
})
159+
150160
It("should error for actual []interface{}, return reference T", func() {
151161
actual := []interface{}{"foo", 42}
152162
var stash int
153163
Expect(ContainElement(Not(BeZero()), &stash).Match(actual)).Error().To(
154164
MatchError(MatchRegexp(`cannot return findings\. Need \*interface.+, got \*int`)))
155165
})
166+
156167
It("should error for actual []interface{}, return reference []T", func() {
157168
actual := []interface{}{"foo", 42}
158169
var stash []string
159170
Expect(ContainElement(Not(BeZero()), &stash).Match(actual)).Error().To(
160171
MatchError(MatchRegexp(`cannot return findings\. Need \*\[\]interface.+, got \*\[\]string`)))
161172
})
173+
162174
It("should error for actual map[T]T, return reference map[T]interface{}", func() {
163175
actual := map[string]string{
164176
"foo": "foo",
@@ -169,6 +181,7 @@ var _ = Describe("ContainElement", func() {
169181
Expect(ContainElement(Not(BeZero()), &stash).Match(actual)).Error().To(
170182
MatchError(MatchRegexp(`cannot return findings\. Need \*map\[string\]string, got \*map\[string\]interface`)))
171183
})
184+
172185
It("should error for actual map[T]T, return reference []T", func() {
173186
actual := map[string]string{
174187
"foo": "foo",
@@ -219,4 +232,205 @@ var _ = Describe("ContainElement", func() {
219232
})
220233
})
221234

235+
Context("iterators", func() {
236+
BeforeEach(func() {
237+
if !miter.HasIterators() {
238+
Skip("iterators not available")
239+
}
240+
})
241+
242+
Describe("matching only", func() {
243+
When("passed a supported type", func() {
244+
Context("and expecting a non-matcher", func() {
245+
It("should do the right thing", func() {
246+
Expect(universalIter).To(ContainElement("baz"))
247+
Expect(universalIter).NotTo(ContainElement("barrrrz"))
248+
249+
Expect(universalIter2).To(ContainElement("baz"))
250+
Expect(universalIter2).NotTo(ContainElement("barrrrz"))
251+
})
252+
})
253+
254+
Context("and expecting a matcher", func() {
255+
It("should pass each element through the matcher", func() {
256+
Expect(universalIter).To(ContainElement(HaveLen(3)))
257+
Expect(universalIter).NotTo(ContainElement(HaveLen(4)))
258+
259+
Expect(universalIter2).To(ContainElement(HaveLen(3)))
260+
Expect(universalIter2).NotTo(ContainElement(HaveLen(5)))
261+
})
262+
263+
It("should power through even if the matcher ever fails", func() {
264+
elements := []any{1, 2, "3", 4}
265+
it := func(yield func(any) bool) {
266+
for _, element := range elements {
267+
if !yield(element) {
268+
return
269+
}
270+
}
271+
}
272+
Expect(it).Should(ContainElement(BeNumerically(">=", 3)))
273+
274+
it2 := func(yield func(int, any) bool) {
275+
for idx, element := range elements {
276+
if !yield(idx, element) {
277+
return
278+
}
279+
}
280+
}
281+
Expect(it2).Should(ContainElement(BeNumerically(">=", 3)))
282+
})
283+
284+
It("should fail if the matcher fails", func() {
285+
elements := []interface{}{1, 2, "3", "4"}
286+
it := func(yield func(any) bool) {
287+
for _, element := range elements {
288+
if !yield(element) {
289+
return
290+
}
291+
}
292+
}
293+
success, err := (&ContainElementMatcher{Element: BeNumerically(">=", 3)}).Match(it)
294+
Expect(success).Should(BeFalse())
295+
Expect(err).Should(HaveOccurred())
296+
297+
it2 := func(yield func(int, any) bool) {
298+
for idx, element := range elements {
299+
if !yield(idx, element) {
300+
return
301+
}
302+
}
303+
}
304+
success, err = (&ContainElementMatcher{Element: BeNumerically(">=", 3)}).Match(it2)
305+
Expect(success).Should(BeFalse())
306+
Expect(err).Should(HaveOccurred())
307+
})
308+
})
309+
})
310+
311+
When("passed a correctly typed nil", func() {
312+
It("should operate succesfully on the passed in value", func() {
313+
var nilIter func(func(string) bool)
314+
Expect(nilIter).ShouldNot(ContainElement(1))
315+
316+
var nilIter2 func(func(int, string) bool)
317+
Expect(nilIter2).ShouldNot(ContainElement("foo"))
318+
})
319+
})
320+
})
321+
322+
Describe("returning findings", func() {
323+
Context("with match(es)", func() {
324+
When("passed an assignable result reference", func() {
325+
It("should assign a single finding to a scalar result reference", func() {
326+
var stash string
327+
Expect(universalIter).To(ContainElement("bar", &stash))
328+
Expect(stash).To(Equal("bar"))
329+
330+
Expect(universalIter2).To(ContainElement("baz", &stash))
331+
Expect(stash).To(Equal("baz"))
332+
})
333+
334+
It("should assign a single finding to a slice return reference", func() {
335+
var stash []string
336+
Expect(universalIter).To(ContainElement("baz", &stash))
337+
Expect(stash).To(HaveLen(1))
338+
Expect(stash).To(ContainElement("baz"))
339+
340+
stash = []string{}
341+
Expect(universalIter2).To(ContainElement("baz", &stash))
342+
Expect(stash).To(HaveLen(1))
343+
Expect(stash).To(ContainElement("baz"))
344+
})
345+
346+
It("should assign multiple findings to a slice return reference", func() {
347+
var stash []string
348+
Expect(universalIter).To(ContainElement(HavePrefix("ba"), &stash))
349+
Expect(stash).To(HaveLen(2))
350+
Expect(stash).To(HaveExactElements("bar", "baz"))
351+
352+
stash = []string{}
353+
Expect(universalIter2).To(ContainElement(HavePrefix("ba"), &stash))
354+
Expect(stash).To(HaveLen(2))
355+
Expect(stash).To(HaveExactElements("bar", "baz"))
356+
})
357+
358+
It("should assign iter.Seq2 findings to a map return reference", func() {
359+
m := map[int]string{
360+
0: "foo",
361+
42: "bar",
362+
666: "baz",
363+
}
364+
iter2 := func(yield func(int, string) bool) {
365+
for k, v := range m {
366+
if !yield(k, v) {
367+
return
368+
}
369+
}
370+
}
371+
372+
var stash map[int]string
373+
Expect(iter2).To(ContainElement(HavePrefix("ba"), &stash))
374+
Expect(stash).To(HaveLen(2))
375+
Expect(stash).To(ConsistOf("bar", "baz"))
376+
})
377+
})
378+
379+
When("passed a scalar return reference for multiple matches", func() {
380+
It("should error", func() {
381+
var stash string
382+
Expect(ContainElement(HavePrefix("ba"), &stash).Match(universalIter)).Error().To(
383+
MatchError(MatchRegexp(`cannot return multiple findings\. Need \*\[\]string, got \*string`)))
384+
})
385+
})
386+
387+
When("passed an unassignable return reference for matches", func() {
388+
It("should error for actual iter.Seq[T1]/iter.Seq2[..., T1], return reference T2", func() {
389+
var stash int
390+
Expect(ContainElement("foo", &stash).Match(universalIter)).Error().To(HaveOccurred())
391+
Expect(ContainElement("foo", &stash).Match(emptyIter2)).Error().To(HaveOccurred())
392+
})
393+
394+
It("should error for actual iter.Seq[T]/iter.Seq2[..., T], return reference [...]T", func() {
395+
var arrstash [2]string
396+
Expect(ContainElement("foo", &arrstash).Match(universalIter)).Error().To(HaveOccurred())
397+
Expect(ContainElement("foo", &arrstash).Match(universalIter2)).Error().To(HaveOccurred())
398+
})
399+
400+
It("should error for actual map[T1]T2, return reference map[T1]interface{}", func() {
401+
var stash map[int]interface{}
402+
Expect(ContainElement(Not(BeZero()), &stash).Match(universalIter2)).Error().To(
403+
MatchError(MatchRegexp(`cannot return findings\. Need \*map\[int\]string, got \*map\[int\]interface`)))
404+
})
405+
})
406+
})
407+
408+
Context("without any matches", func() {
409+
When("the matcher did not error", func() {
410+
It("should report non-match", func() {
411+
var stash string
412+
rem := ContainElement("barrz", &stash)
413+
m, err := rem.Match(universalIter)
414+
Expect(m).To(BeFalse())
415+
Expect(err).NotTo(HaveOccurred())
416+
Expect(rem.FailureMessage(universalIter)).To(MatchRegexp(`Expected\n.+\nto contain element matching\n.+: barrz`))
417+
418+
var stashslice []string
419+
rem = ContainElement("barrz", &stashslice)
420+
m, err = rem.Match(universalIter)
421+
Expect(m).To(BeFalse())
422+
Expect(err).NotTo(HaveOccurred())
423+
Expect(rem.FailureMessage(universalIter)).To(MatchRegexp(`Expected\n.+\nto contain element matching\n.+: barrz`))
424+
})
425+
})
426+
427+
When("the matcher errors", func() {
428+
It("should report last matcher error", func() {
429+
var stash []interface{}
430+
Expect(ContainElement(HaveField("yeehaw", 42), &stash).Match(universalIter)).Error().To(MatchError(MatchRegexp(`HaveField encountered:\n.*<string>: baz\nWhich is not a struct`)))
431+
})
432+
})
433+
})
434+
})
435+
})
222436
})

‎matchers/contain_elements_matcher.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55

66
"github.com/onsi/gomega/format"
7+
"github.com/onsi/gomega/matchers/internal/miter"
78
"github.com/onsi/gomega/matchers/support/goraph/bipartitegraph"
89
)
910

@@ -13,8 +14,8 @@ type ContainElementsMatcher struct {
1314
}
1415

1516
func (matcher *ContainElementsMatcher) Match(actual interface{}) (success bool, err error) {
16-
if !isArrayOrSlice(actual) && !isMap(actual) {
17-
return false, fmt.Errorf("ContainElements matcher expects an array/slice/map. Got:\n%s", format.Object(actual, 1))
17+
if !isArrayOrSlice(actual) && !isMap(actual) && !miter.IsIter(actual) {
18+
return false, fmt.Errorf("ContainElements matcher expects an array/slice/map/iter.Seq/iter.Seq2. Got:\n%s", format.Object(actual, 1))
1819
}
1920

2021
matchers := matchers(matcher.Elements)

‎matchers/contain_elements_matcher_test.go

+37
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package matchers_test
33
import (
44
. "github.com/onsi/ginkgo/v2"
55
. "github.com/onsi/gomega"
6+
"github.com/onsi/gomega/matchers/internal/miter"
67
)
78

89
var _ = Describe("ContainElements", func() {
@@ -149,4 +150,40 @@ the missing elements were
149150
})
150151
})
151152
})
153+
154+
Context("iterators", func() {
155+
BeforeEach(func() {
156+
if !miter.HasIterators() {
157+
Skip("iterators not available")
158+
}
159+
})
160+
161+
Context("with an iter.Seq", func() {
162+
It("should do the right thing", func() {
163+
Expect(universalIter).Should(ContainElements("foo", "bar", "baz"))
164+
Expect(universalIter).Should(ContainElements("bar"))
165+
Expect(universalIter).Should(ContainElements())
166+
Expect(universalIter).ShouldNot(ContainElements("baz", "bar", "foo", "foo"))
167+
})
168+
})
169+
170+
Context("with an iter.Seq2", func() {
171+
It("should do the right thing", func() {
172+
Expect(universalIter2).Should(ContainElements("foo", "bar", "baz"))
173+
Expect(universalIter2).Should(ContainElements("bar"))
174+
Expect(universalIter2).Should(ContainElements())
175+
Expect(universalIter2).ShouldNot(ContainElements("baz", "bar", "foo", "foo"))
176+
})
177+
})
178+
179+
When("passed exactly one argument, and that argument is an iter.Seq", func() {
180+
It("should match against the elements of that argument", func() {
181+
Expect(universalIter).Should(ContainElements(universalIter))
182+
Expect(universalIter).ShouldNot(ContainElements(fooElements))
183+
184+
Expect(universalIter2).Should(ContainElements(universalIter))
185+
Expect(universalIter2).ShouldNot(ContainElements(fooElements))
186+
})
187+
})
188+
})
152189
})

‎matchers/have_each_matcher.go

+37-3
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,16 @@ import (
55
"reflect"
66

77
"github.com/onsi/gomega/format"
8+
"github.com/onsi/gomega/matchers/internal/miter"
89
)
910

1011
type HaveEachMatcher struct {
1112
Element interface{}
1213
}
1314

1415
func (matcher *HaveEachMatcher) Match(actual interface{}) (success bool, err error) {
15-
if !isArrayOrSlice(actual) && !isMap(actual) {
16-
return false, fmt.Errorf("HaveEach matcher expects an array/slice/map. Got:\n%s",
16+
if !isArrayOrSlice(actual) && !isMap(actual) && !miter.IsIter(actual) {
17+
return false, fmt.Errorf("HaveEach matcher expects an array/slice/map/iter.Seq/iter.Seq2. Got:\n%s",
1718
format.Object(actual, 1))
1819
}
1920

@@ -22,6 +23,38 @@ func (matcher *HaveEachMatcher) Match(actual interface{}) (success bool, err err
2223
elemMatcher = &EqualMatcher{Expected: matcher.Element}
2324
}
2425

26+
if miter.IsIter(actual) {
27+
// rejecting the non-elements case works different for iterators as we
28+
// don't want to fetch all elements into a slice first.
29+
count := 0
30+
var success bool
31+
var err error
32+
if miter.IsSeq2(actual) {
33+
miter.IterateKV(actual, func(k, v reflect.Value) bool {
34+
count++
35+
success, err = elemMatcher.Match(v.Interface())
36+
if err != nil {
37+
return false
38+
}
39+
return success
40+
})
41+
} else {
42+
miter.IterateV(actual, func(v reflect.Value) bool {
43+
count++
44+
success, err = elemMatcher.Match(v.Interface())
45+
if err != nil {
46+
return false
47+
}
48+
return success
49+
})
50+
}
51+
if count == 0 {
52+
return false, fmt.Errorf("HaveEach matcher expects a non-empty iter.Seq/iter.Seq2. Got:\n%s",
53+
format.Object(actual, 1))
54+
}
55+
return success, err
56+
}
57+
2558
value := reflect.ValueOf(actual)
2659
if value.Len() == 0 {
2760
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
4073
}
4174
}
4275

43-
// if there are no elements, then HaveEach will match.
76+
// if we never failed then we succeed; the empty/nil cases have already been
77+
// rejected above.
4478
for i := 0; i < value.Len(); i++ {
4579
success, err := elemMatcher.Match(valueAt(i))
4680
if err != nil {

‎matchers/have_each_matcher_test.go

+64
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package matchers_test
22

33
import (
4+
"github.com/onsi/gomega/matchers/internal/miter"
5+
46
. "github.com/onsi/ginkgo/v2"
57
. "github.com/onsi/gomega"
68
. "github.com/onsi/gomega/matchers"
@@ -92,4 +94,66 @@ var _ = Describe("HaveEach", func() {
9294
Expect(err).Should(HaveOccurred())
9395
})
9496
})
97+
98+
Context("iterators", func() {
99+
BeforeEach(func() {
100+
if !miter.HasIterators() {
101+
Skip("iterators not available")
102+
}
103+
})
104+
105+
When("passed an iterator type", func() {
106+
Context("and expecting a non-matcher", func() {
107+
It("should do the right thing", func() {
108+
Expect(fooIter).Should(HaveEach("foo"))
109+
Expect(fooIter).ShouldNot(HaveEach("bar"))
110+
111+
Expect(fooIter2).Should(HaveEach("foo"))
112+
Expect(fooIter2).ShouldNot(HaveEach("bar"))
113+
})
114+
})
115+
116+
Context("and expecting a matcher", func() {
117+
It("should pass each element through the matcher", func() {
118+
Expect(universalIter).Should(HaveEach(HaveLen(3)))
119+
Expect(universalIter).ShouldNot(HaveEach(HaveLen(4)))
120+
121+
Expect(universalIter2).Should(HaveEach(HaveLen(3)))
122+
Expect(universalIter2).ShouldNot(HaveEach(HaveLen(4)))
123+
})
124+
125+
It("should not power through if the matcher ever fails", func() {
126+
success, err := (&HaveEachMatcher{Element: BeNumerically(">=", 1)}).Match(universalIter)
127+
Expect(success).Should(BeFalse())
128+
Expect(err).Should(HaveOccurred())
129+
130+
success, err = (&HaveEachMatcher{Element: BeNumerically(">=", 1)}).Match(universalIter2)
131+
Expect(success).Should(BeFalse())
132+
Expect(err).Should(HaveOccurred())
133+
})
134+
})
135+
})
136+
137+
When("passed an iterator yielding nothing or correctly typed nil", func() {
138+
It("should error", func() {
139+
success, err := (&HaveEachMatcher{Element: "foo"}).Match(emptyIter)
140+
Expect(success).Should(BeFalse())
141+
Expect(err).Should(HaveOccurred())
142+
143+
success, err = (&HaveEachMatcher{Element: "foo"}).Match(emptyIter2)
144+
Expect(success).Should(BeFalse())
145+
Expect(err).Should(HaveOccurred())
146+
147+
var nilIter func(func(string) bool)
148+
success, err = (&HaveEachMatcher{Element: "foo"}).Match(nilIter)
149+
Expect(success).Should(BeFalse())
150+
Expect(err).Should(HaveOccurred())
151+
152+
var nilIter2 func(func(int, string) bool)
153+
success, err = (&HaveEachMatcher{Element: "foo"}).Match(nilIter2)
154+
Expect(success).Should(BeFalse())
155+
Expect(err).Should(HaveOccurred())
156+
})
157+
})
158+
})
95159
})

‎matchers/have_exact_elements.go

+48-5
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package matchers
22

33
import (
44
"fmt"
5+
"reflect"
56

67
"github.com/onsi/gomega/format"
8+
"github.com/onsi/gomega/matchers/internal/miter"
79
)
810

911
type mismatchFailure struct {
@@ -21,17 +23,58 @@ type HaveExactElementsMatcher struct {
2123
func (matcher *HaveExactElementsMatcher) Match(actual interface{}) (success bool, err error) {
2224
matcher.resetState()
2325

24-
if isMap(actual) {
25-
return false, fmt.Errorf("error")
26+
if isMap(actual) || miter.IsSeq2(actual) {
27+
return false, fmt.Errorf("HaveExactElements matcher doesn't work on map or iter.Seq2. Got:\n%s", format.Object(actual, 1))
2628
}
2729

2830
matchers := matchers(matcher.Elements)
29-
values := valuesOf(actual)
30-
3131
lenMatchers := len(matchers)
32-
lenValues := len(values)
32+
3333
success = true
3434

35+
if miter.IsIter(actual) {
36+
// In the worst case, we need to see everything before we can give our
37+
// verdict. The only exception is fast fail.
38+
i := 0
39+
miter.IterateV(actual, func(v reflect.Value) bool {
40+
if i >= lenMatchers {
41+
// the iterator produces more values than we got matchers: this
42+
// is not good.
43+
matcher.extraIndex = i
44+
success = false
45+
return false
46+
}
47+
48+
elemMatcher := matchers[i].(omegaMatcher)
49+
match, err := elemMatcher.Match(v.Interface())
50+
if err != nil {
51+
matcher.mismatchFailures = append(matcher.mismatchFailures, mismatchFailure{
52+
index: i,
53+
failure: err.Error(),
54+
})
55+
success = false
56+
} else if !match {
57+
matcher.mismatchFailures = append(matcher.mismatchFailures, mismatchFailure{
58+
index: i,
59+
failure: elemMatcher.FailureMessage(v.Interface()),
60+
})
61+
success = false
62+
}
63+
i++
64+
return true
65+
})
66+
if i < len(matchers) {
67+
// the iterator produced less values than we got matchers: this is
68+
// no good, no no no.
69+
matcher.missingIndex = i
70+
success = false
71+
}
72+
return success, nil
73+
}
74+
75+
values := valuesOf(actual)
76+
lenValues := len(values)
77+
3578
for i := 0; i < lenMatchers || i < lenValues; i++ {
3679
if i >= lenMatchers {
3780
matcher.extraIndex = i

‎matchers/have_exact_elements_test.go

+149
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package matchers_test
33
import (
44
. "github.com/onsi/ginkgo/v2"
55
. "github.com/onsi/gomega"
6+
"github.com/onsi/gomega/matchers/internal/miter"
67
)
78

89
var _ = Describe("HaveExactElements", func() {
@@ -142,4 +143,152 @@ to equal
142143
Expect([]bool{false}).Should(matchSingleFalse)
143144
})
144145
})
146+
147+
Context("iterators", func() {
148+
BeforeEach(func() {
149+
if !miter.HasIterators() {
150+
Skip("iterators not available")
151+
}
152+
})
153+
154+
Context("with an iter.Seq", func() {
155+
It("should do the right thing", func() {
156+
Expect(universalIter).Should(HaveExactElements("foo", "bar", "baz"))
157+
Expect(universalIter).ShouldNot(HaveExactElements("foo"))
158+
Expect(universalIter).ShouldNot(HaveExactElements("foo", "bar", "baz", "argh"))
159+
Expect(universalIter).ShouldNot(HaveExactElements("foo", "bar"))
160+
161+
var nilIter func(func(string) bool)
162+
Expect(nilIter).Should(HaveExactElements())
163+
})
164+
})
165+
166+
Context("with an iter.Seq2", func() {
167+
It("should error", func() {
168+
failures := InterceptGomegaFailures(func() {
169+
Expect(universalIter2).Should(HaveExactElements("foo"))
170+
})
171+
172+
Expect(failures).Should(HaveLen(1))
173+
})
174+
})
175+
176+
When("passed matchers", func() {
177+
It("should pass if matcher pass", func() {
178+
Expect(universalIter).Should(HaveExactElements("foo", MatchRegexp("^ba"), MatchRegexp("az$")))
179+
Expect(universalIter).ShouldNot(HaveExactElements("foo", MatchRegexp("az$"), MatchRegexp("^ba")))
180+
Expect(universalIter).ShouldNot(HaveExactElements("foo", MatchRegexp("az$")))
181+
Expect(universalIter).ShouldNot(HaveExactElements("foo", MatchRegexp("az$"), "baz", "bac"))
182+
})
183+
184+
When("a matcher errors", func() {
185+
It("should soldier on", func() {
186+
Expect(universalIter).ShouldNot(HaveExactElements(BeFalse(), "bar", "baz"))
187+
poly := []any{"foo", "bar", false}
188+
polyIter := func(yield func(any) bool) {
189+
for _, v := range poly {
190+
if !yield(v) {
191+
return
192+
}
193+
}
194+
}
195+
Expect(polyIter).Should(HaveExactElements(ContainSubstring("foo"), "bar", BeFalse()))
196+
})
197+
198+
It("should include the error message, not the failure message", func() {
199+
failures := InterceptGomegaFailures(func() {
200+
Expect(universalIter).Should(HaveExactElements("foo", BeFalse(), "bar"))
201+
})
202+
Ω(failures[0]).ShouldNot(ContainSubstring("to be false"))
203+
Ω(failures[0]).Should(ContainSubstring("1: Expected a boolean. Got:\n <string>: bar"))
204+
})
205+
})
206+
})
207+
When("passed exactly one argument, and that argument is a slice", func() {
208+
It("should match against the elements of that arguments", func() {
209+
Expect(universalIter).Should(HaveExactElements([]string{"foo", "bar", "baz"}))
210+
Expect(universalIter).ShouldNot(HaveExactElements([]string{"foo", "bar"}))
211+
})
212+
})
213+
214+
When("passed nil", func() {
215+
It("should fail correctly", func() {
216+
failures := InterceptGomegaFailures(func() {
217+
var expected []any
218+
Expect(universalIter).Should(HaveExactElements(expected...))
219+
})
220+
Expect(failures).Should(HaveLen(1))
221+
})
222+
})
223+
224+
Describe("Failure Message", func() {
225+
When("actual contains extra elements", func() {
226+
It("should print the starting index of the extra elements", func() {
227+
failures := InterceptGomegaFailures(func() {
228+
Expect(universalIter).Should(HaveExactElements("foo"))
229+
})
230+
231+
expected := "Expected\n.*<func\\(func\\(string\\) bool\\)>:.*\nto have exact elements with\n.*\\[\"foo\"\\]\nthe extra elements start from index 1"
232+
Expect(failures).To(ConsistOf(MatchRegexp(expected)))
233+
})
234+
})
235+
236+
When("actual misses an element", func() {
237+
It("should print the starting index of missing element", func() {
238+
failures := InterceptGomegaFailures(func() {
239+
Expect(universalIter).Should(HaveExactElements("foo", "bar", "baz", "argh"))
240+
})
241+
242+
expected := "Expected\n.*<func\\(func\\(string\\) bool\\)>:.*\nto have exact elements with\n.*\\[\"foo\", \"bar\", \"baz\", \"argh\"\\]\nthe missing elements start from index 3"
243+
Expect(failures).To(ConsistOf(MatchRegexp(expected)))
244+
})
245+
})
246+
})
247+
248+
When("actual have mismatched elements", func() {
249+
It("should print the index, expected element, and actual element", func() {
250+
failures := InterceptGomegaFailures(func() {
251+
Expect(universalIter).Should(HaveExactElements("bar", "baz", "foo"))
252+
})
253+
254+
expected := `Expected
255+
.*<func\(func\(string\) bool\)>:.*
256+
to have exact elements with
257+
.*\["bar", "baz", "foo"\]
258+
the mismatch indexes were:
259+
0: Expected
260+
<string>: foo
261+
to equal
262+
<string>: bar
263+
1: Expected
264+
<string>: bar
265+
to equal
266+
<string>: baz
267+
2: Expected
268+
<string>: baz
269+
to equal
270+
<string>: foo`
271+
Expect(failures[0]).To(MatchRegexp(expected))
272+
})
273+
})
274+
275+
When("matcher instance is reused", func() {
276+
// This is a regression test for https://github.com/onsi/gomega/issues/647.
277+
// Matcher instance may be reused, if placed inside ContainElement() or other collection matchers.
278+
It("should work properly", func() {
279+
matchSingleFalse := HaveExactElements(Equal(false))
280+
allOf := func(a []bool) func(func(bool) bool) {
281+
return func(yield func(bool) bool) {
282+
for _, b := range a {
283+
if !yield(b) {
284+
return
285+
}
286+
}
287+
}
288+
}
289+
Expect(allOf([]bool{true})).ShouldNot(matchSingleFalse)
290+
Expect(allOf([]bool{false})).Should(matchSingleFalse)
291+
})
292+
})
293+
})
145294
})

‎matchers/have_key_matcher.go

+17-2
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,37 @@ import (
77
"reflect"
88

99
"github.com/onsi/gomega/format"
10+
"github.com/onsi/gomega/matchers/internal/miter"
1011
)
1112

1213
type HaveKeyMatcher struct {
1314
Key interface{}
1415
}
1516

1617
func (matcher *HaveKeyMatcher) Match(actual interface{}) (success bool, err error) {
17-
if !isMap(actual) {
18-
return false, fmt.Errorf("HaveKey matcher expects a map. Got:%s", format.Object(actual, 1))
18+
if !isMap(actual) && !miter.IsSeq2(actual) {
19+
return false, fmt.Errorf("HaveKey matcher expects a map/iter.Seq2. Got:%s", format.Object(actual, 1))
1920
}
2021

2122
keyMatcher, keyIsMatcher := matcher.Key.(omegaMatcher)
2223
if !keyIsMatcher {
2324
keyMatcher = &EqualMatcher{Expected: matcher.Key}
2425
}
2526

27+
if miter.IsSeq2(actual) {
28+
var success bool
29+
var err error
30+
miter.IterateKV(actual, func(k, v reflect.Value) bool {
31+
success, err = keyMatcher.Match(k.Interface())
32+
if err != nil {
33+
err = fmt.Errorf("HaveKey's key matcher failed with:\n%s%s", format.Indent, err.Error())
34+
return false
35+
}
36+
return !success
37+
})
38+
return success, err
39+
}
40+
2641
keys := reflect.ValueOf(actual).MapKeys()
2742
for i := 0; i < len(keys); i++ {
2843
success, err := keyMatcher.Match(keys[i].Interface())

‎matchers/have_key_matcher_test.go

+46
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
. "github.com/onsi/ginkgo/v2"
55
. "github.com/onsi/gomega"
66
. "github.com/onsi/gomega/matchers"
7+
"github.com/onsi/gomega/matchers/internal/miter"
78
)
89

910
var _ = Describe("HaveKey", func() {
@@ -70,4 +71,49 @@ var _ = Describe("HaveKey", func() {
7071
Expect(err).Should(HaveOccurred())
7172
})
7273
})
74+
75+
Context("iterators", func() {
76+
BeforeEach(func() {
77+
if !miter.HasIterators() {
78+
Skip("iterators not available")
79+
}
80+
})
81+
82+
When("passed an iter.Seq2", func() {
83+
It("should do the right thing", func() {
84+
Expect(universalMapIter2).To(HaveKey("bar"))
85+
Expect(universalMapIter2).To(HaveKey(HavePrefix("ba")))
86+
Expect(universalMapIter2).NotTo(HaveKey("barrrrz"))
87+
Expect(universalMapIter2).NotTo(HaveKey(42))
88+
})
89+
})
90+
91+
When("passed a correctly typed nil", func() {
92+
It("should operate succesfully on the passed in value", func() {
93+
var nilIter2 func(func(string, int) bool)
94+
Expect(nilIter2).ShouldNot(HaveKey("foo"))
95+
})
96+
})
97+
98+
When("the passed in key is actually a matcher", func() {
99+
It("should pass each element through the matcher", func() {
100+
Expect(universalMapIter2).Should(HaveKey(ContainSubstring("oo")))
101+
Expect(universalMapIter2).ShouldNot(HaveKey(ContainSubstring("foobar")))
102+
})
103+
104+
It("should fail if the matcher ever fails", func() {
105+
success, err := (&HaveKeyMatcher{Key: ContainSubstring("ar")}).Match(universalIter2)
106+
Expect(success).Should(BeFalse())
107+
Expect(err).Should(HaveOccurred())
108+
})
109+
})
110+
111+
When("passed something that is not an iter.Seq2", func() {
112+
It("should error", func() {
113+
success, err := (&HaveKeyMatcher{Key: "foo"}).Match(universalIter)
114+
Expect(success).Should(BeFalse())
115+
Expect(err).Should(HaveOccurred())
116+
})
117+
})
118+
})
73119
})

‎matchers/have_key_with_value_matcher.go

+24-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"reflect"
88

99
"github.com/onsi/gomega/format"
10+
"github.com/onsi/gomega/matchers/internal/miter"
1011
)
1112

1213
type HaveKeyWithValueMatcher struct {
@@ -15,8 +16,8 @@ type HaveKeyWithValueMatcher struct {
1516
}
1617

1718
func (matcher *HaveKeyWithValueMatcher) Match(actual interface{}) (success bool, err error) {
18-
if !isMap(actual) {
19-
return false, fmt.Errorf("HaveKeyWithValue matcher expects a map. Got:%s", format.Object(actual, 1))
19+
if !isMap(actual) && !miter.IsSeq2(actual) {
20+
return false, fmt.Errorf("HaveKeyWithValue matcher expects a map/iter.Seq2. Got:%s", format.Object(actual, 1))
2021
}
2122

2223
keyMatcher, keyIsMatcher := matcher.Key.(omegaMatcher)
@@ -29,6 +30,27 @@ func (matcher *HaveKeyWithValueMatcher) Match(actual interface{}) (success bool,
2930
valueMatcher = &EqualMatcher{Expected: matcher.Value}
3031
}
3132

33+
if miter.IsSeq2(actual) {
34+
var success bool
35+
var err error
36+
miter.IterateKV(actual, func(k, v reflect.Value) bool {
37+
success, err = keyMatcher.Match(k.Interface())
38+
if err != nil {
39+
err = fmt.Errorf("HaveKey's key matcher failed with:\n%s%s", format.Indent, err.Error())
40+
return false
41+
}
42+
if success {
43+
success, err = valueMatcher.Match(v.Interface())
44+
if err != nil {
45+
err = fmt.Errorf("HaveKeyWithValue's value matcher failed with:\n%s%s", format.Indent, err.Error())
46+
return false
47+
}
48+
}
49+
return !success
50+
})
51+
return success, err
52+
}
53+
3254
keys := reflect.ValueOf(actual).MapKeys()
3355
for i := 0; i < len(keys); i++ {
3456
success, err := keyMatcher.Match(keys[i].Interface())

‎matchers/have_key_with_value_matcher_test.go

+56
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
. "github.com/onsi/ginkgo/v2"
55
. "github.com/onsi/gomega"
66
. "github.com/onsi/gomega/matchers"
7+
"github.com/onsi/gomega/matchers/internal/miter"
78
)
89

910
var _ = Describe("HaveKeyWithValue", func() {
@@ -79,4 +80,59 @@ var _ = Describe("HaveKeyWithValue", func() {
7980
Expect(err).Should(HaveOccurred())
8081
})
8182
})
83+
84+
Context("iterators", func() {
85+
BeforeEach(func() {
86+
if !miter.HasIterators() {
87+
Skip("iterators not available")
88+
}
89+
})
90+
91+
When("passed an iter.Seq2", func() {
92+
It("should do the right thing", func() {
93+
Expect(universalMapIter2).Should(HaveKeyWithValue("foo", 0))
94+
Expect(universalMapIter2).ShouldNot(HaveKeyWithValue("foo", 1))
95+
Expect(universalMapIter2).ShouldNot(HaveKeyWithValue("baz", 2))
96+
Expect(universalMapIter2).ShouldNot(HaveKeyWithValue("baz", 1))
97+
98+
Expect(universalMapIter2).Should(HaveKeyWithValue("bar", 42))
99+
Expect(universalMapIter2).Should(HaveKeyWithValue("baz", 666))
100+
101+
Expect(universalMapIter2).ShouldNot(HaveKeyWithValue("bar", "abc"))
102+
Expect(universalMapIter2).ShouldNot(HaveKeyWithValue(555, "abc"))
103+
})
104+
})
105+
106+
When("passed a correctly typed nil", func() {
107+
It("should operate succesfully on the passed in value", func() {
108+
var nilIter2 func(func(string, int) bool)
109+
Expect(nilIter2).ShouldNot(HaveKeyWithValue("foo", 0))
110+
})
111+
})
112+
113+
When("the passed in key or value is actually a matcher", func() {
114+
It("should pass each element through the matcher", func() {
115+
Expect(universalMapIter2).Should(HaveKeyWithValue(ContainSubstring("oo"), BeNumerically("<", 1)))
116+
Expect(universalMapIter2).Should(HaveKeyWithValue(ContainSubstring("foo"), 0))
117+
})
118+
119+
It("should fail if the matcher ever fails", func() {
120+
success, err := (&HaveKeyWithValueMatcher{Key: "bar", Value: ContainSubstring("argh")}).Match(universalMapIter2)
121+
Expect(success).Should(BeFalse())
122+
Expect(err).Should(HaveOccurred())
123+
124+
success, err = (&HaveKeyWithValueMatcher{Key: "foo", Value: ContainSubstring("1")}).Match(universalMapIter2)
125+
Expect(success).Should(BeFalse())
126+
Expect(err).Should(HaveOccurred())
127+
})
128+
})
129+
130+
When("passed something that is not an iter.Seq2", func() {
131+
It("should error", func() {
132+
success, err := (&HaveKeyWithValueMatcher{Key: "foo", Value: "bar"}).Match(universalIter)
133+
Expect(success).Should(BeFalse())
134+
Expect(err).Should(HaveOccurred())
135+
})
136+
})
137+
})
82138
})

‎matchers/have_len_matcher.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ type HaveLenMatcher struct {
1313
func (matcher *HaveLenMatcher) Match(actual interface{}) (success bool, err error) {
1414
length, ok := lengthOf(actual)
1515
if !ok {
16-
return false, fmt.Errorf("HaveLen matcher expects a string/array/map/channel/slice. Got:\n%s", format.Object(actual, 1))
16+
return false, fmt.Errorf("HaveLen matcher expects a string/array/map/channel/slice/iterator. Got:\n%s", format.Object(actual, 1))
1717
}
1818

1919
return length == matcher.Count, nil

‎matchers/have_len_matcher_test.go

+30
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package matchers_test
22

33
import (
4+
"github.com/onsi/gomega/matchers/internal/miter"
5+
46
. "github.com/onsi/ginkgo/v2"
57
. "github.com/onsi/gomega"
68
. "github.com/onsi/gomega/matchers"
@@ -50,4 +52,32 @@ var _ = Describe("HaveLen", func() {
5052
Expect(err).Should(HaveOccurred())
5153
})
5254
})
55+
56+
Context("iterators", func() {
57+
BeforeEach(func() {
58+
if !miter.HasIterators() {
59+
Skip("iterators not available")
60+
}
61+
})
62+
63+
When("passed an iterator type", func() {
64+
It("should do the right thing", func() {
65+
Expect(emptyIter).To(HaveLen(0))
66+
Expect(emptyIter2).To(HaveLen(0))
67+
68+
Expect(universalIter).To(HaveLen(len(universalElements)))
69+
Expect(universalIter2).To(HaveLen(len(universalElements)))
70+
})
71+
})
72+
73+
When("passed a correctly typed nil", func() {
74+
It("should operate succesfully on the passed in value", func() {
75+
var nilIter func(func(string) bool)
76+
Expect(nilIter).Should(HaveLen(0))
77+
78+
var nilIter2 func(func(int, string) bool)
79+
Expect(nilIter2).Should(HaveLen(0))
80+
})
81+
})
82+
})
5383
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package miter_test
2+
3+
import (
4+
"testing"
5+
6+
. "github.com/onsi/ginkgo/v2"
7+
. "github.com/onsi/gomega"
8+
)
9+
10+
func TestMatcherIter(t *testing.T) {
11+
RegisterFailHandler(Fail)
12+
RunSpecs(t, "Matcher Iter Support Suite")
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
//go:build go1.23
2+
3+
package miter
4+
5+
import (
6+
"reflect"
7+
)
8+
9+
// HasIterators always returns false for Go versions before 1.23.
10+
func HasIterators() bool { return true }
11+
12+
// IsIter returns true if the specified value is a function type that can be
13+
// range-d over, otherwise false.
14+
//
15+
// We don't use reflect's CanSeq and CanSeq2 directly, as these would return
16+
// true also for other value types that are range-able, such as integers,
17+
// slices, et cetera. Here, we aim only at range-able (iterator) functions.
18+
func IsIter(it any) bool {
19+
if it == nil { // on purpose we only test for untyped nil.
20+
return false
21+
}
22+
// reject all non-iterator-func values, even if they're range-able.
23+
t := reflect.TypeOf(it)
24+
if t.Kind() != reflect.Func {
25+
return false
26+
}
27+
return t.CanSeq() || t.CanSeq2()
28+
}
29+
30+
// IterKVTypes returns the reflection types of an iterator's yield function's K
31+
// and optional V arguments, otherwise nil K and V reflection types.
32+
func IterKVTypes(it any) (k, v reflect.Type) {
33+
if it == nil {
34+
return
35+
}
36+
// reject all non-iterator-func values, even if they're range-able.
37+
t := reflect.TypeOf(it)
38+
if t.Kind() != reflect.Func {
39+
return
40+
}
41+
// get the reflection types for V, and where applicable, K.
42+
switch {
43+
case t.CanSeq():
44+
v = t. /*iterator fn*/ In(0). /*yield fn*/ In(0)
45+
case t.CanSeq2():
46+
yieldfn := t. /*iterator fn*/ In(0)
47+
k = yieldfn.In(0)
48+
v = yieldfn.In(1)
49+
}
50+
return
51+
}
52+
53+
// IsSeq2 returns true if the passed iterator function is compatible with
54+
// iter.Seq2, otherwise false.
55+
//
56+
// IsSeq2 hides the Go 1.23+ specific reflect.Type.CanSeq2 behind a facade which
57+
// is empty for Go versions before 1.23.
58+
func IsSeq2(it any) bool {
59+
if it == nil {
60+
return false
61+
}
62+
t := reflect.TypeOf(it)
63+
return t.Kind() == reflect.Func && t.CanSeq2()
64+
}
65+
66+
// isNilly returns true if v is either an untyped nil, or is a nil function (not
67+
// necessarily an iterator function).
68+
func isNilly(v any) bool {
69+
if v == nil {
70+
return true
71+
}
72+
rv := reflect.ValueOf(v)
73+
return rv.Kind() == reflect.Func && rv.IsNil()
74+
}
75+
76+
// IterateV loops over the elements produced by an iterator function, passing
77+
// the elements to the specified yield function individually and stopping only
78+
// when either the iterator function runs out of elements or the yield function
79+
// tell us to stop it.
80+
//
81+
// IterateV works very much like reflect.Value.Seq but hides the Go 1.23+
82+
// specific parts behind a facade which is empty for Go versions before 1.23, in
83+
// order to simplify code maintenance for matchers when using older Go versions.
84+
func IterateV(it any, yield func(v reflect.Value) bool) {
85+
if isNilly(it) {
86+
return
87+
}
88+
// reject all non-iterator-func values, even if they're range-able.
89+
t := reflect.TypeOf(it)
90+
if t.Kind() != reflect.Func || !t.CanSeq() {
91+
return
92+
}
93+
// Call the specified iterator function, handing it our adaptor to call the
94+
// specified generic reflection yield function.
95+
reflectedYield := reflect.MakeFunc(
96+
t. /*iterator fn*/ In(0),
97+
func(args []reflect.Value) []reflect.Value {
98+
return []reflect.Value{reflect.ValueOf(yield(args[0]))}
99+
})
100+
reflect.ValueOf(it).Call([]reflect.Value{reflectedYield})
101+
}
102+
103+
// IterateKV loops over the key-value elements produced by an iterator function,
104+
// passing the elements to the specified yield function individually and
105+
// stopping only when either the iterator function runs out of elements or the
106+
// yield function tell us to stop it.
107+
//
108+
// IterateKV works very much like reflect.Value.Seq2 but hides the Go 1.23+
109+
// specific parts behind a facade which is empty for Go versions before 1.23, in
110+
// order to simplify code maintenance for matchers when using older Go versions.
111+
func IterateKV(it any, yield func(k, v reflect.Value) bool) {
112+
if isNilly(it) {
113+
return
114+
}
115+
// reject all non-iterator-func values, even if they're range-able.
116+
t := reflect.TypeOf(it)
117+
if t.Kind() != reflect.Func || !t.CanSeq2() {
118+
return
119+
}
120+
// Call the specified iterator function, handing it our adaptor to call the
121+
// specified generic reflection yield function.
122+
reflectedYield := reflect.MakeFunc(
123+
t. /*iterator fn*/ In(0),
124+
func(args []reflect.Value) []reflect.Value {
125+
return []reflect.Value{reflect.ValueOf(yield(args[0], args[1]))}
126+
})
127+
reflect.ValueOf(it).Call([]reflect.Value{reflectedYield})
128+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
//go:build go1.23
2+
3+
package miter_test
4+
5+
import (
6+
"reflect"
7+
8+
. "github.com/onsi/ginkgo/v2"
9+
. "github.com/onsi/gomega"
10+
11+
. "github.com/onsi/gomega/matchers/internal/miter"
12+
)
13+
14+
var _ = Describe("iterator function types", func() {
15+
16+
When("detecting iterator functions", func() {
17+
18+
It("doesn't match a nil value", func() {
19+
Expect(IsIter(nil)).To(BeFalse())
20+
})
21+
22+
It("doesn't match a range-able numeric value", func() {
23+
Expect(IsIter(42)).To(BeFalse())
24+
})
25+
26+
It("doesn't match a non-iter function", func() {
27+
Expect(IsIter(func(yabadabadu string) {})).To(BeFalse())
28+
})
29+
30+
It("matches an iter.Seq-like iter function", func() {
31+
Expect(IsIter(func(yield func(v int) bool) {})).To(BeTrue())
32+
var nilIter func(func(string) bool)
33+
Expect(IsIter(nilIter)).To(BeTrue())
34+
})
35+
36+
It("matches an iter.Seq2-like iter function", func() {
37+
Expect(IsIter(func(yield func(k uint, v string) bool) {})).To(BeTrue())
38+
var nilIter2 func(func(string, bool) bool)
39+
Expect(IsIter(nilIter2)).To(BeTrue())
40+
})
41+
42+
})
43+
44+
It("detects iter.Seq2", func() {
45+
Expect(IsSeq2(42)).To(BeFalse())
46+
Expect(IsSeq2(func(func(int) bool) {})).To(BeFalse())
47+
Expect(IsSeq2(func(func(int, int) bool) {})).To(BeTrue())
48+
49+
var nilIter2 func(func(string, bool) bool)
50+
Expect(IsSeq2(nilIter2)).To(BeTrue())
51+
})
52+
53+
When("getting iterator function K, V types", func() {
54+
55+
It("has no types when nil", func() {
56+
k, v := IterKVTypes(nil)
57+
Expect(k).To(BeNil())
58+
Expect(v).To(BeNil())
59+
})
60+
61+
It("has no types for range-able numbers", func() {
62+
k, v := IterKVTypes(42)
63+
Expect(k).To(BeNil())
64+
Expect(v).To(BeNil())
65+
})
66+
67+
It("returns correct reflection type for the iterator's V", func() {
68+
type foo uint
69+
k, v := IterKVTypes(func(yield func(v foo) bool) {})
70+
Expect(k).To(BeNil())
71+
Expect(v).To(Equal(reflect.TypeOf(foo(42))))
72+
})
73+
74+
It("returns correct reflection types for the iterator's K and V", func() {
75+
type foo uint
76+
type bar string
77+
k, v := IterKVTypes(func(yield func(k foo, v bar) bool) {})
78+
Expect(k).To(Equal(reflect.TypeOf(foo(42))))
79+
Expect(v).To(Equal(reflect.TypeOf(bar(""))))
80+
})
81+
82+
})
83+
84+
When("iterating single value reflections", func() {
85+
86+
iterelements := []string{"foo", "bar", "baz"}
87+
88+
it := func(yield func(v string) bool) {
89+
for _, el := range iterelements {
90+
if !yield(el) {
91+
break
92+
}
93+
}
94+
}
95+
96+
It("doesn't loop over a nil iterator", func() {
97+
Expect(func() {
98+
IterateV(nil, func(v reflect.Value) bool { panic("reflection yield must not be called") })
99+
}).NotTo(Panic())
100+
})
101+
102+
It("doesn't loop over a typed-nil iterator", func() {
103+
var nilIter func(func(string) bool)
104+
Expect(func() {
105+
IterateV(nilIter, func(v reflect.Value) bool { panic("reflection yield must not be called") })
106+
}).NotTo(Panic())
107+
})
108+
109+
It("doesn't loop over a non-iterator value", func() {
110+
Expect(func() {
111+
IterateV(42, func(v reflect.Value) bool { panic("reflection yield must not be called") })
112+
}).NotTo(Panic())
113+
})
114+
115+
It("doesn't loop over an iter.Seq2", func() {
116+
Expect(func() {
117+
IterateV(
118+
func(k uint, v string) bool { panic("it.Seq2 must not be called") },
119+
func(v reflect.Value) bool { panic("reflection yield must not be called") })
120+
}).NotTo(Panic())
121+
})
122+
123+
It("yields all reflection values", func() {
124+
els := []string{}
125+
IterateV(it, func(v reflect.Value) bool {
126+
els = append(els, v.String())
127+
return true
128+
})
129+
Expect(els).To(ConsistOf(iterelements))
130+
})
131+
132+
It("stops yielding reflection values before reaching THE END", func() {
133+
els := []string{}
134+
IterateV(it, func(v reflect.Value) bool {
135+
els = append(els, v.String())
136+
return len(els) < 2
137+
})
138+
Expect(els).To(ConsistOf(iterelements[:2]))
139+
})
140+
141+
})
142+
143+
When("iterating key-value reflections", func() {
144+
145+
type kv struct {
146+
k uint
147+
v string
148+
}
149+
150+
iterelements := []kv{
151+
{k: 42, v: "foo"},
152+
{k: 66, v: "bar"},
153+
{k: 666, v: "baz"},
154+
}
155+
156+
it := func(yield func(k uint, v string) bool) {
157+
for _, el := range iterelements {
158+
if !yield(el.k, el.v) {
159+
break
160+
}
161+
}
162+
}
163+
164+
It("doesn't loop over a nil iterator", func() {
165+
Expect(func() {
166+
IterateKV(nil, func(k, v reflect.Value) bool { panic("reflection yield must not be called") })
167+
}).NotTo(Panic())
168+
})
169+
170+
It("doesn't loop over a typed-nil iterator", func() {
171+
var nilIter2 func(func(int, string) bool)
172+
Expect(func() {
173+
IterateKV(nilIter2, func(k, v reflect.Value) bool { panic("reflection yield must not be called") })
174+
}).NotTo(Panic())
175+
})
176+
177+
It("doesn't loop over a non-iterator value", func() {
178+
Expect(func() {
179+
IterateKV(42, func(k, v reflect.Value) bool { panic("reflection yield must not be called") })
180+
}).NotTo(Panic())
181+
})
182+
183+
It("doesn't loop over an iter.Seq", func() {
184+
Expect(func() {
185+
IterateKV(
186+
func(v string) bool { panic("it.Seq must not be called") },
187+
func(k, v reflect.Value) bool { panic("reflection yield must not be called") })
188+
}).NotTo(Panic())
189+
})
190+
191+
It("yields all reflection key-values", func() {
192+
els := []kv{}
193+
IterateKV(it, func(k, v reflect.Value) bool {
194+
els = append(els, kv{k: uint(k.Uint()), v: v.String()})
195+
return true
196+
})
197+
Expect(els).To(ConsistOf(iterelements))
198+
})
199+
200+
It("stops yielding reflection key-values before reaching THE END", func() {
201+
els := []kv{}
202+
IterateKV(it, func(k, v reflect.Value) bool {
203+
els = append(els, kv{k: uint(k.Uint()), v: v.String()})
204+
return len(els) < 2
205+
})
206+
Expect(els).To(ConsistOf(iterelements[:2]))
207+
})
208+
209+
})
210+
211+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//go:build !go1.23
2+
3+
/*
4+
Gomega matchers
5+
6+
This package implements the Gomega matchers and does not typically need to be imported.
7+
See the docs for Gomega for documentation on the matchers
8+
9+
http://onsi.github.io/gomega/
10+
*/
11+
12+
package miter
13+
14+
import "reflect"
15+
16+
// HasIterators always returns false for Go versions before 1.23.
17+
func HasIterators() bool { return false }
18+
19+
// IsIter always returns false for Go versions before 1.23 as there is no
20+
// iterator (function) pattern defined yet; see also:
21+
// https://tip.golang.org/blog/range-functions.
22+
func IsIter(i any) bool { return false }
23+
24+
// IsSeq2 always returns false for Go versions before 1.23 as there is no
25+
// iterator (function) pattern defined yet; see also:
26+
// https://tip.golang.org/blog/range-functions.
27+
func IsSeq2(it any) bool { return false }
28+
29+
// IterKVTypes always returns nil reflection types for Go versions before 1.23
30+
// as there is no iterator (function) pattern defined yet; see also:
31+
// https://tip.golang.org/blog/range-functions.
32+
func IterKVTypes(i any) (k, v reflect.Type) {
33+
return
34+
}
35+
36+
// IterateV never loops over what has been passed to it as an iterator for Go
37+
// versions before 1.23 as there is no iterator (function) pattern defined yet;
38+
// see also: https://tip.golang.org/blog/range-functions.
39+
func IterateV(it any, yield func(v reflect.Value) bool) {}
40+
41+
// IterateKV never loops over what has been passed to it as an iterator for Go
42+
// versions before 1.23 as there is no iterator (function) pattern defined yet;
43+
// see also: https://tip.golang.org/blog/range-functions.
44+
func IterateKV(it any, yield func(k, v reflect.Value) bool) {}

‎matchers/iter_support_test.go

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package matchers_test
2+
3+
var (
4+
universalElements = []string{"foo", "bar", "baz"}
5+
universalMap = map[string]int{
6+
"foo": 0,
7+
"bar": 42,
8+
"baz": 666,
9+
}
10+
fooElements = []string{"foo", "foo", "foo"}
11+
)
12+
13+
func universalIter(yield func(string) bool) {
14+
for _, element := range universalElements {
15+
if !yield(element) {
16+
return
17+
}
18+
}
19+
}
20+
21+
func universalIter2(yield func(int, string) bool) {
22+
for idx, element := range universalElements {
23+
if !yield(idx, element) {
24+
return
25+
}
26+
}
27+
}
28+
29+
func emptyIter(yield func(string) bool) {}
30+
31+
func emptyIter2(yield func(int, string) bool) {}
32+
33+
func universalMapIter2(yield func(string, int) bool) {
34+
for k, v := range universalMap {
35+
if !yield(k, v) {
36+
return
37+
}
38+
}
39+
}
40+
41+
func fooIter(yield func(string) bool) {
42+
for _, foo := range fooElements {
43+
if !yield(foo) {
44+
return
45+
}
46+
}
47+
}
48+
49+
func fooIter2(yield func(int, string) bool) {
50+
for idx, foo := range fooElements {
51+
if !yield(idx, foo) {
52+
return
53+
}
54+
}
55+
}

‎matchers/type_support.go

+13
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import (
1515
"encoding/json"
1616
"fmt"
1717
"reflect"
18+
19+
"github.com/onsi/gomega/matchers/internal/miter"
1820
)
1921

2022
type omegaMatcher interface {
@@ -152,6 +154,17 @@ func lengthOf(a interface{}) (int, bool) {
152154
switch reflect.TypeOf(a).Kind() {
153155
case reflect.Map, reflect.Array, reflect.String, reflect.Chan, reflect.Slice:
154156
return reflect.ValueOf(a).Len(), true
157+
case reflect.Func:
158+
if !miter.IsIter(a) {
159+
return 0, false
160+
}
161+
var l int
162+
if miter.IsSeq2(a) {
163+
miter.IterateKV(a, func(k, v reflect.Value) bool { l++; return true })
164+
} else {
165+
miter.IterateV(a, func(v reflect.Value) bool { l++; return true })
166+
}
167+
return l, true
155168
default:
156169
return 0, false
157170
}

0 commit comments

Comments
 (0)
Please sign in to comment.