Skip to content

Commit 2f57380

Browse files
ansdblgm
authored andcommitted
Add ContainElements matcher (#370)
* Add ContainElements matcher Resolves #361 * Convert variable to constant function * Add negative tests
1 parent acbca72 commit 2f57380

File tree

5 files changed

+191
-34
lines changed

5 files changed

+191
-34
lines changed

matchers.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,20 @@ func ConsistOf(elements ...interface{}) types.GomegaMatcher {
306306
}
307307
}
308308

309+
//ContainElements succeeds if actual contains the passed in elements. The ordering of the elements does not matter.
310+
//By default ContainElements() uses Equal() to match the elements, however custom matchers can be passed in instead. Here are some examples:
311+
//
312+
// Expect([]string{"Foo", "FooBar"}).Should(ContainElements("FooBar"))
313+
// Expect([]string{"Foo", "FooBar"}).Should(ContainElements(ContainSubstring("Bar"), "Foo"))
314+
//
315+
//Actual must be an array, slice or map.
316+
//For maps, ContainElements searches through the map's values.
317+
func ContainElements(elements ...interface{}) types.GomegaMatcher {
318+
return &matchers.ContainElementsMatcher{
319+
Elements: elements,
320+
}
321+
}
322+
309323
//HaveKey succeeds if actual is a map with the passed in key.
310324
//By default HaveKey uses Equal() to perform the match, however a
311325
//matcher can be passed in instead:

matchers/consist_of.go

Lines changed: 46 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -21,30 +21,8 @@ func (matcher *ConsistOfMatcher) Match(actual interface{}) (success bool, err er
2121
return false, fmt.Errorf("ConsistOf matcher expects an array/slice/map. Got:\n%s", format.Object(actual, 1))
2222
}
2323

24-
elements := matcher.Elements
25-
if len(matcher.Elements) == 1 && isArrayOrSlice(matcher.Elements[0]) {
26-
elements = []interface{}{}
27-
value := reflect.ValueOf(matcher.Elements[0])
28-
for i := 0; i < value.Len(); i++ {
29-
elements = append(elements, value.Index(i).Interface())
30-
}
31-
}
32-
33-
matchers := []interface{}{}
34-
for _, element := range elements {
35-
matcher, isMatcher := element.(omegaMatcher)
36-
if !isMatcher {
37-
matcher = &EqualMatcher{Expected: element}
38-
}
39-
matchers = append(matchers, matcher)
40-
}
41-
42-
values := matcher.valuesOf(actual)
43-
44-
neighbours := func(v, m interface{}) (bool, error) {
45-
match, err := m.(omegaMatcher).Match(v)
46-
return match && err == nil, nil
47-
}
24+
matchers := matchers(matcher.Elements)
25+
values := valuesOf(actual)
4826

4927
bipartiteGraph, err := bipartitegraph.NewBipartiteGraph(values, matchers, neighbours)
5028
if err != nil {
@@ -58,19 +36,48 @@ func (matcher *ConsistOfMatcher) Match(actual interface{}) (success bool, err er
5836

5937
var missingMatchers []interface{}
6038
matcher.extraElements, missingMatchers = bipartiteGraph.FreeLeftRight(edges)
39+
matcher.missingElements = equalMatchersToElements(missingMatchers)
40+
41+
return false, nil
42+
}
43+
44+
func neighbours(value, matcher interface{}) (bool, error) {
45+
match, err := matcher.(omegaMatcher).Match(value)
46+
return match && err == nil, nil
47+
}
6148

62-
for _, missing := range missingMatchers {
63-
equalMatcher, ok := missing.(*EqualMatcher)
49+
func equalMatchersToElements(matchers []interface{}) (elements []interface{}) {
50+
for _, matcher := range matchers {
51+
equalMatcher, ok := matcher.(*EqualMatcher)
6452
if ok {
65-
missing = equalMatcher.Expected
53+
matcher = equalMatcher.Expected
6654
}
67-
matcher.missingElements = append(matcher.missingElements, missing)
55+
elements = append(elements, matcher)
6856
}
57+
return
58+
}
6959

70-
return false, nil
60+
func matchers(expectedElems []interface{}) (matchers []interface{}) {
61+
elems := expectedElems
62+
if len(expectedElems) == 1 && isArrayOrSlice(expectedElems[0]) {
63+
elems = []interface{}{}
64+
value := reflect.ValueOf(expectedElems[0])
65+
for i := 0; i < value.Len(); i++ {
66+
elems = append(elems, value.Index(i).Interface())
67+
}
68+
}
69+
70+
for _, e := range elems {
71+
matcher, isMatcher := e.(omegaMatcher)
72+
if !isMatcher {
73+
matcher = &EqualMatcher{Expected: e}
74+
}
75+
matchers = append(matchers, matcher)
76+
}
77+
return
7178
}
7279

73-
func (matcher *ConsistOfMatcher) valuesOf(actual interface{}) []interface{} {
80+
func valuesOf(actual interface{}) []interface{} {
7481
value := reflect.ValueOf(actual)
7582
values := []interface{}{}
7683
if isMap(actual) {
@@ -89,17 +96,22 @@ func (matcher *ConsistOfMatcher) valuesOf(actual interface{}) []interface{} {
8996

9097
func (matcher *ConsistOfMatcher) FailureMessage(actual interface{}) (message string) {
9198
message = format.Message(actual, "to consist of", matcher.Elements)
92-
if len(matcher.missingElements) > 0 {
93-
message = fmt.Sprintf("%s\nthe missing elements were\n%s", message,
94-
format.Object(matcher.missingElements, 1))
95-
}
99+
message = appendMissingElements(message, matcher.missingElements)
96100
if len(matcher.extraElements) > 0 {
97101
message = fmt.Sprintf("%s\nthe extra elements were\n%s", message,
98102
format.Object(matcher.extraElements, 1))
99103
}
100104
return
101105
}
102106

107+
func appendMissingElements(message string, missingElements []interface{}) string {
108+
if len(missingElements) == 0 {
109+
return message
110+
}
111+
return fmt.Sprintf("%s\nthe missing elements were\n%s", message,
112+
format.Object(missingElements, 1))
113+
}
114+
103115
func (matcher *ConsistOfMatcher) NegatedFailureMessage(actual interface{}) (message string) {
104116
return format.Message(actual, "not to consist of", matcher.Elements)
105117
}

matchers/consist_of_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ var _ = Describe("ConsistOf", func() {
7070
When("passed exactly one argument, and that argument is a slice", func() {
7171
It("should match against the elements of that argument", func() {
7272
Expect([]string{"foo", "bar", "baz"}).Should(ConsistOf([]string{"foo", "bar", "baz"}))
73+
Expect([]string{"foo", "bar", "baz"}).ShouldNot(ConsistOf([]string{"foo", "bar"}))
7374
})
7475
})
7576

matchers/contain_elements_matcher.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package matchers
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/onsi/gomega/format"
7+
"github.com/onsi/gomega/matchers/support/goraph/bipartitegraph"
8+
)
9+
10+
type ContainElementsMatcher struct {
11+
Elements []interface{}
12+
missingElements []interface{}
13+
}
14+
15+
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))
18+
}
19+
20+
matchers := matchers(matcher.Elements)
21+
bipartiteGraph, err := bipartitegraph.NewBipartiteGraph(valuesOf(actual), matchers, neighbours)
22+
if err != nil {
23+
return false, err
24+
}
25+
26+
edges := bipartiteGraph.LargestMatching()
27+
if len(edges) == len(matchers) {
28+
return true, nil
29+
}
30+
31+
_, missingMatchers := bipartiteGraph.FreeLeftRight(edges)
32+
matcher.missingElements = equalMatchersToElements(missingMatchers)
33+
34+
return false, nil
35+
}
36+
37+
func (matcher *ContainElementsMatcher) FailureMessage(actual interface{}) (message string) {
38+
message = format.Message(actual, "to contain elements", matcher.Elements)
39+
return appendMissingElements(message, matcher.missingElements)
40+
}
41+
42+
func (matcher *ContainElementsMatcher) NegatedFailureMessage(actual interface{}) (message string) {
43+
return format.Message(actual, "not to contain elements", matcher.Elements)
44+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package matchers_test
2+
3+
import (
4+
. "github.com/onsi/ginkgo"
5+
. "github.com/onsi/gomega"
6+
)
7+
8+
var _ = Describe("ContainElements", func() {
9+
Context("with a slice", func() {
10+
It("should do the right thing", func() {
11+
Expect([]string{"foo", "bar", "baz"}).Should(ContainElements("foo", "bar", "baz"))
12+
Expect([]string{"foo", "bar", "baz"}).Should(ContainElements("bar"))
13+
Expect([]string{"foo", "bar", "baz"}).Should(ContainElements())
14+
Expect([]string{"foo", "bar", "baz"}).ShouldNot(ContainElements("baz", "bar", "foo", "foo"))
15+
})
16+
})
17+
18+
Context("with an array", func() {
19+
It("should do the right thing", func() {
20+
Expect([3]string{"foo", "bar", "baz"}).Should(ContainElements("foo", "bar", "baz"))
21+
Expect([3]string{"foo", "bar", "baz"}).Should(ContainElements("bar"))
22+
Expect([3]string{"foo", "bar", "baz"}).Should(ContainElements())
23+
Expect([3]string{"foo", "bar", "baz"}).ShouldNot(ContainElements("baz", "bar", "foo", "foo"))
24+
})
25+
})
26+
27+
Context("with a map", func() {
28+
It("should apply to the values", func() {
29+
Expect(map[int]string{1: "foo", 2: "bar", 3: "baz"}).Should(ContainElements("foo", "bar", "baz"))
30+
Expect(map[int]string{1: "foo", 2: "bar", 3: "baz"}).Should(ContainElements("bar"))
31+
Expect(map[int]string{1: "foo", 2: "bar", 3: "baz"}).Should(ContainElements())
32+
Expect(map[int]string{1: "foo", 2: "bar", 3: "baz"}).ShouldNot(ContainElements("baz", "bar", "foo", "foo"))
33+
})
34+
35+
})
36+
37+
Context("with anything else", func() {
38+
It("should error", func() {
39+
failures := InterceptGomegaFailures(func() {
40+
Expect("foo").Should(ContainElements("f", "o", "o"))
41+
})
42+
43+
Expect(failures).Should(HaveLen(1))
44+
})
45+
})
46+
47+
Context("when passed matchers", func() {
48+
It("should pass if the matchers pass", func() {
49+
Expect([]string{"foo", "bar", "baz"}).Should(ContainElements("foo", MatchRegexp("^ba"), "baz"))
50+
Expect([]string{"foo", "bar", "baz"}).Should(ContainElements("foo", MatchRegexp("^ba")))
51+
Expect([]string{"foo", "bar", "baz"}).ShouldNot(ContainElements("foo", MatchRegexp("^ba"), MatchRegexp("foo")))
52+
Expect([]string{"foo", "bar", "baz"}).Should(ContainElements("foo", MatchRegexp("^ba"), MatchRegexp("^ba")))
53+
Expect([]string{"foo", "bar", "baz"}).ShouldNot(ContainElements("foo", MatchRegexp("^ba"), MatchRegexp("turducken")))
54+
})
55+
56+
It("should not depend on the order of the matchers", func() {
57+
Expect([][]int{{1, 2}, {2}}).Should(ContainElements(ContainElement(1), ContainElement(2)))
58+
Expect([][]int{{1, 2}, {2}}).Should(ContainElements(ContainElement(2), ContainElement(1)))
59+
})
60+
61+
Context("when a matcher errors", func() {
62+
It("should soldier on", func() {
63+
Expect([]string{"foo", "bar", "baz"}).ShouldNot(ContainElements(BeFalse(), "foo", "bar"))
64+
Expect([]interface{}{"foo", "bar", false}).Should(ContainElements(BeFalse(), ContainSubstring("foo"), "bar"))
65+
})
66+
})
67+
})
68+
69+
Context("when passed exactly one argument, and that argument is a slice", func() {
70+
It("should match against the elements of that argument", func() {
71+
Expect([]string{"foo", "bar", "baz"}).Should(ContainElements([]string{"foo", "baz"}))
72+
Expect([]string{"foo", "bar", "baz"}).ShouldNot(ContainElements([]string{"foo", "nope"}))
73+
})
74+
})
75+
76+
Describe("FailureMessage", func() {
77+
It("prints missing elements", func() {
78+
failures := InterceptGomegaFailures(func() {
79+
Expect([]int{2}).Should(ContainElements(1, 2, 3))
80+
})
81+
82+
expected := "Expected\n.*\\[2\\]\nto contain elements\n.*\\[1, 2, 3\\]\nthe missing elements were\n.*\\[1, 3\\]"
83+
Expect(failures).To(ContainElements(MatchRegexp(expected)))
84+
})
85+
})
86+
})

0 commit comments

Comments
 (0)