Skip to content

Commit 9d50783

Browse files
authored
Add HaveExactElements matcher (#634)
1 parent 296a68b commit 9d50783

File tree

4 files changed

+234
-0
lines changed

4 files changed

+234
-0
lines changed

docs/index.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1215,6 +1215,38 @@ is the only element passed in to `ConsistOf`:
12151215

12161216
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.
12171217

1218+
#### HaveExactElements(element ...interface{})
1219+
1220+
```go
1221+
Expect(ACTUAL).To(HaveExactElements(ELEMENT1, ELEMENT2, ELEMENT3, ...))
1222+
```
1223+
1224+
or
1225+
1226+
```go
1227+
Expect(ACTUAL).To(HaveExactElements([]SOME_TYPE{ELEMENT1, ELEMENT2, ELEMENT3, ...}))
1228+
```
1229+
1230+
succeeds if `ACTUAL` contains precisely the elements and ordering passed into the matchers.
1231+
1232+
By default `HaveExactElements()` uses `Equal()` to match the elements, however custom matchers can be passed in instead. Here are some examples:
1233+
1234+
```go
1235+
Expect([]string{"Foo", "FooBar"}).To(HaveExactElements("Foo", "FooBar"))
1236+
Expect([]string{"Foo", "FooBar"}).To(HaveExactElements("Foo", ContainSubstring("Bar")))
1237+
Expect([]string{"Foo", "FooBar"}).To(HaveExactElements(ContainSubstring("Foo"), ContainSubstring("Foo")))
1238+
```
1239+
1240+
Actual must be an `array` or `slice`.
1241+
1242+
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
1243+
is the only element passed in to `HaveExactElements`:
1244+
1245+
```go
1246+
Expect([]string{"Foo", "FooBar"}).To(HaveExactElements([]string{"FooBar", "Foo"}))
1247+
```
1248+
1249+
Note that Go's type system does not allow you to write this as `HaveExactElements([]string{"FooBar", "Foo"}...)` as `[]string` and `[]interface{}` are different types - hence the need for this special rule.
12181250

12191251
#### HaveEach(element ...interface{})
12201252

matchers.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,20 @@ func ConsistOf(elements ...interface{}) types.GomegaMatcher {
349349
}
350350
}
351351

352+
// HaveExactElemets succeeds if actual contains elements that precisely match the elemets passed into the matcher. The ordering of the elements does matter.
353+
// By default HaveExactElements() uses Equal() to match the elements, however custom matchers can be passed in instead. Here are some examples:
354+
//
355+
// Expect([]string{"Foo", "FooBar"}).Should(HaveExactElements("Foo", "FooBar"))
356+
// Expect([]string{"Foo", "FooBar"}).Should(HaveExactElements("Foo", ContainSubstring("Bar")))
357+
// Expect([]string{"Foo", "FooBar"}).Should(HaveExactElements(ContainSubstring("Foo"), ContainSubstring("Foo")))
358+
//
359+
// Actual must be an array or slice.
360+
func HaveExactElements(elements ...interface{}) types.GomegaMatcher {
361+
return &matchers.HaveExactElementsMatcher{
362+
Elements: elements,
363+
}
364+
}
365+
352366
// ContainElements succeeds if actual contains the passed in elements. The ordering of the elements does not matter.
353367
// By default ContainElements() uses Equal() to match the elements, however custom matchers can be passed in instead. Here are some examples:
354368
//

matchers/have_exact_elements.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package matchers
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/onsi/gomega/format"
7+
)
8+
9+
type mismatchFailure struct {
10+
failure string
11+
index int
12+
}
13+
14+
type HaveExactElementsMatcher struct {
15+
Elements []interface{}
16+
mismatchFailures []mismatchFailure
17+
missingIndex int
18+
extraIndex int
19+
}
20+
21+
func (matcher *HaveExactElementsMatcher) Match(actual interface{}) (success bool, err error) {
22+
if isMap(actual) {
23+
return false, fmt.Errorf("error")
24+
}
25+
26+
matchers := matchers(matcher.Elements)
27+
values := valuesOf(actual)
28+
29+
lenMatchers := len(matchers)
30+
lenValues := len(values)
31+
32+
for i := 0; i < lenMatchers || i < lenValues; i++ {
33+
if i >= lenMatchers {
34+
matcher.extraIndex = i
35+
continue
36+
}
37+
38+
if i >= lenValues {
39+
matcher.missingIndex = i
40+
return
41+
}
42+
43+
elemMatcher := matchers[i].(omegaMatcher)
44+
match, err := elemMatcher.Match(values[i])
45+
if err != nil || !match {
46+
matcher.mismatchFailures = append(matcher.mismatchFailures, mismatchFailure{
47+
index: i,
48+
failure: elemMatcher.FailureMessage(values[i]),
49+
})
50+
}
51+
}
52+
53+
return matcher.missingIndex+matcher.extraIndex+len(matcher.mismatchFailures) == 0, nil
54+
}
55+
56+
func (matcher *HaveExactElementsMatcher) FailureMessage(actual interface{}) (message string) {
57+
message = format.Message(actual, "to have exact elements with", presentable(matcher.Elements))
58+
if matcher.missingIndex > 0 {
59+
message = fmt.Sprintf("%s\nthe missing elements start from index %d", message, matcher.missingIndex)
60+
}
61+
if matcher.extraIndex > 0 {
62+
message = fmt.Sprintf("%s\nthe extra elements start from index %d", message, matcher.extraIndex)
63+
}
64+
if len(matcher.mismatchFailures) != 0 {
65+
message = fmt.Sprintf("%s\nthe mismatch indexes were:", message)
66+
}
67+
for _, mismatch := range matcher.mismatchFailures {
68+
message = fmt.Sprintf("%s\n%d: %s", message, mismatch.index, mismatch.failure)
69+
}
70+
return
71+
}
72+
73+
func (matcher *HaveExactElementsMatcher) NegatedFailureMessage(actual interface{}) (message string) {
74+
return format.Message(actual, "not to contain elements", presentable(matcher.Elements))
75+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package matchers_test
2+
3+
import (
4+
. "github.com/onsi/ginkgo/v2"
5+
. "github.com/onsi/gomega"
6+
)
7+
8+
var _ = Describe("HaveExactElements", func() {
9+
Context("with a slice", func() {
10+
It("should do the right thing", func() {
11+
Expect([]string{"foo", "bar"}).Should(HaveExactElements("foo", "bar"))
12+
Expect([]string{"foo", "bar"}).ShouldNot(HaveExactElements("foo"))
13+
Expect([]string{"foo", "bar"}).ShouldNot(HaveExactElements("foo", "bar", "baz"))
14+
Expect([]string{"foo", "bar"}).ShouldNot(HaveExactElements("bar", "foo"))
15+
})
16+
})
17+
Context("with an array", func() {
18+
It("should do the right thing", func() {
19+
Expect([2]string{"foo", "bar"}).Should(HaveExactElements("foo", "bar"))
20+
Expect([2]string{"foo", "bar"}).ShouldNot(HaveExactElements("foo"))
21+
Expect([2]string{"foo", "bar"}).ShouldNot(HaveExactElements("foo", "bar", "baz"))
22+
Expect([2]string{"foo", "bar"}).ShouldNot(HaveExactElements("bar", "foo"))
23+
})
24+
})
25+
Context("with map", func() {
26+
It("should error", func() {
27+
failures := InterceptGomegaFailures(func() {
28+
Expect(map[int]string{1: "foo"}).Should(HaveExactElements("foo"))
29+
})
30+
31+
Expect(failures).Should(HaveLen(1))
32+
})
33+
})
34+
Context("with anything else", func() {
35+
It("should error", func() {
36+
failures := InterceptGomegaFailures(func() {
37+
Expect("foo").Should(HaveExactElements("f", "o", "o"))
38+
})
39+
40+
Expect(failures).Should(HaveLen(1))
41+
})
42+
})
43+
44+
When("passed matchers", func() {
45+
It("should pass if matcher pass", func() {
46+
Expect([]string{"foo", "bar", "baz"}).Should(HaveExactElements("foo", MatchRegexp("^ba"), MatchRegexp("az$")))
47+
Expect([]string{"foo", "bar", "baz"}).ShouldNot(HaveExactElements("foo", MatchRegexp("az$"), MatchRegexp("^ba")))
48+
Expect([]string{"foo", "bar", "baz"}).ShouldNot(HaveExactElements("foo", MatchRegexp("az$")))
49+
Expect([]string{"foo", "bar", "baz"}).ShouldNot(HaveExactElements("foo", MatchRegexp("az$"), "baz", "bac"))
50+
})
51+
52+
When("a matcher errors", func() {
53+
It("should soldier on", func() {
54+
Expect([]string{"foo", "bar", "baz"}).ShouldNot(HaveExactElements(BeFalse(), "bar", "baz"))
55+
Expect([]interface{}{"foo", "bar", false}).Should(HaveExactElements(ContainSubstring("foo"), "bar", BeFalse()))
56+
})
57+
})
58+
})
59+
60+
When("passed exactly one argument, and that argument is a slice", func() {
61+
It("should match against the elements of that arguments", func() {
62+
Expect([]string{"foo", "bar", "baz"}).Should(HaveExactElements([]string{"foo", "bar", "baz"}))
63+
Expect([]string{"foo", "bar", "baz"}).ShouldNot(HaveExactElements([]string{"foo", "bar"}))
64+
})
65+
})
66+
67+
Describe("Failure Message", func() {
68+
When("actual contains extra elements", func() {
69+
It("should print the starting index of the extra elements", func() {
70+
failures := InterceptGomegaFailures(func() {
71+
Expect([]int{1, 2}).Should(HaveExactElements(1))
72+
})
73+
74+
expected := "Expected\n.*\\[1, 2\\]\nto have exact elements with\n.*\\[1\\]\nthe extra elements start from index 1"
75+
Expect(failures).To(ConsistOf(MatchRegexp(expected)))
76+
})
77+
})
78+
79+
When("actual misses an element", func() {
80+
It("should print the starting index of missing element", func() {
81+
failures := InterceptGomegaFailures(func() {
82+
Expect([]int{1}).Should(HaveExactElements(1, 2))
83+
})
84+
85+
expected := "Expected\n.*\\[1\\]\nto have exact elements with\n.*\\[1, 2\\]\nthe missing elements start from index 1"
86+
Expect(failures).To(ConsistOf(MatchRegexp(expected)))
87+
})
88+
})
89+
90+
When("actual have mismatched elements", func() {
91+
It("should print the index, expected element, and actual element", func() {
92+
failures := InterceptGomegaFailures(func() {
93+
Expect([]int{1, 2}).Should(HaveExactElements(2, 1))
94+
})
95+
96+
expected := `Expected
97+
.*\[1, 2\]
98+
to have exact elements with
99+
.*\[2, 1\]
100+
the mismatch indexes were:
101+
0: Expected
102+
<int>: 1
103+
to equal
104+
<int>: 2
105+
1: Expected
106+
<int>: 2
107+
to equal
108+
<int>: 1`
109+
Expect(failures[0]).To(MatchRegexp(expected))
110+
})
111+
})
112+
})
113+
})

0 commit comments

Comments
 (0)