Skip to content

Commit 4c964c6

Browse files
thediveoonsi
authored andcommitted
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 <[email protected]>
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
})

0 commit comments

Comments
 (0)