Skip to content

Commit

Permalink
Add ContainElements matcher
Browse files Browse the repository at this point in the history
Resolves #361
  • Loading branch information
ansd committed Dec 8, 2019
1 parent d93d97d commit 49ad95b
Show file tree
Hide file tree
Showing 4 changed files with 189 additions and 34 deletions.
14 changes: 14 additions & 0 deletions matchers.go
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,20 @@ func ConsistOf(elements ...interface{}) types.GomegaMatcher {
}
}

//ContainElements succeeds if actual contains the passed in elements. The ordering of the elements does not matter.
//By default ContainElements() uses Equal() to match the elements, however custom matchers can be passed in instead. Here are some examples:
//
// Expect([]string{"Foo", "FooBar"}).Should(ContainElements("FooBar"))
// Expect([]string{"Foo", "FooBar"}).Should(ContainElements(ContainSubstring("Bar"), "Foo"))
//
//Actual must be an array, slice or map.
//For maps, ContainElements searches through the map's values.
func ContainElements(elements ...interface{}) types.GomegaMatcher {
return &matchers.ContainElementsMatcher{
Elements: elements,
}
}

//HaveKey succeeds if actual is a map with the passed in key.
//By default HaveKey uses Equal() to perform the match, however a
//matcher can be passed in instead:
Expand Down
80 changes: 46 additions & 34 deletions matchers/consist_of.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,35 +16,18 @@ type ConsistOfMatcher struct {
extraElements []interface{}
}

var neighbours = func(v, m interface{}) (bool, error) {
match, err := m.(omegaMatcher).Match(v)
return match && err == nil, nil
}

func (matcher *ConsistOfMatcher) Match(actual interface{}) (success bool, err error) {
if !isArrayOrSlice(actual) && !isMap(actual) {
return false, fmt.Errorf("ConsistOf matcher expects an array/slice/map. Got:\n%s", format.Object(actual, 1))
}

elements := matcher.Elements
if len(matcher.Elements) == 1 && isArrayOrSlice(matcher.Elements[0]) {
elements = []interface{}{}
value := reflect.ValueOf(matcher.Elements[0])
for i := 0; i < value.Len(); i++ {
elements = append(elements, value.Index(i).Interface())
}
}

matchers := []interface{}{}
for _, element := range elements {
matcher, isMatcher := element.(omegaMatcher)
if !isMatcher {
matcher = &EqualMatcher{Expected: element}
}
matchers = append(matchers, matcher)
}

values := matcher.valuesOf(actual)

neighbours := func(v, m interface{}) (bool, error) {
match, err := m.(omegaMatcher).Match(v)
return match && err == nil, nil
}
matchers := matchers(matcher.Elements)
values := valuesOf(actual)

bipartiteGraph, err := bipartitegraph.NewBipartiteGraph(values, matchers, neighbours)
if err != nil {
Expand All @@ -58,19 +41,43 @@ func (matcher *ConsistOfMatcher) Match(actual interface{}) (success bool, err er

var missingMatchers []interface{}
matcher.extraElements, missingMatchers = bipartiteGraph.FreeLeftRight(edges)
matcher.missingElements = equalMatchersToElements(missingMatchers)

return false, nil
}

for _, missing := range missingMatchers {
equalMatcher, ok := missing.(*EqualMatcher)
func equalMatchersToElements(matchers []interface{}) (elements []interface{}) {
for _, matcher := range matchers {
equalMatcher, ok := matcher.(*EqualMatcher)
if ok {
missing = equalMatcher.Expected
matcher = equalMatcher.Expected
}
matcher.missingElements = append(matcher.missingElements, missing)
elements = append(elements, matcher)
}
return
}

return false, nil
func matchers(expectedElems []interface{}) (matchers []interface{}) {
elems := expectedElems
if len(expectedElems) == 1 && isArrayOrSlice(expectedElems[0]) {
elems = []interface{}{}
value := reflect.ValueOf(expectedElems[0])
for i := 0; i < value.Len(); i++ {
elems = append(elems, value.Index(i).Interface())
}
}

for _, e := range elems {
matcher, isMatcher := e.(omegaMatcher)
if !isMatcher {
matcher = &EqualMatcher{Expected: e}
}
matchers = append(matchers, matcher)
}
return
}

func (matcher *ConsistOfMatcher) valuesOf(actual interface{}) []interface{} {
func valuesOf(actual interface{}) []interface{} {
value := reflect.ValueOf(actual)
values := []interface{}{}
if isMap(actual) {
Expand All @@ -89,17 +96,22 @@ func (matcher *ConsistOfMatcher) valuesOf(actual interface{}) []interface{} {

func (matcher *ConsistOfMatcher) FailureMessage(actual interface{}) (message string) {
message = format.Message(actual, "to consist of", matcher.Elements)
if len(matcher.missingElements) > 0 {
message = fmt.Sprintf("%s\nthe missing elements were\n%s", message,
format.Object(matcher.missingElements, 1))
}
message = appendMissingElements(message, matcher.missingElements)
if len(matcher.extraElements) > 0 {
message = fmt.Sprintf("%s\nthe extra elements were\n%s", message,
format.Object(matcher.extraElements, 1))
}
return
}

func appendMissingElements(message string, missingElements []interface{}) string {
if len(missingElements) == 0 {
return message
}
return fmt.Sprintf("%s\nthe missing elements were\n%s", message,
format.Object(missingElements, 1))
}

func (matcher *ConsistOfMatcher) NegatedFailureMessage(actual interface{}) (message string) {
return format.Message(actual, "not to consist of", matcher.Elements)
}
44 changes: 44 additions & 0 deletions matchers/contain_elements_matcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package matchers

import (
"fmt"

"github.com/onsi/gomega/format"
"github.com/onsi/gomega/matchers/support/goraph/bipartitegraph"
)

type ContainElementsMatcher struct {
Elements []interface{}
missingElements []interface{}
}

func (matcher *ContainElementsMatcher) Match(actual interface{}) (success bool, err error) {
if !isArrayOrSlice(actual) && !isMap(actual) {
return false, fmt.Errorf("ContainElements matcher expects an array/slice/map. Got:\n%s", format.Object(actual, 1))
}

matchers := matchers(matcher.Elements)
bipartiteGraph, err := bipartitegraph.NewBipartiteGraph(valuesOf(actual), matchers, neighbours)
if err != nil {
return false, err
}

edges := bipartiteGraph.LargestMatching()
if len(edges) == len(matchers) {
return true, nil
}

_, missingMatchers := bipartiteGraph.FreeLeftRight(edges)
matcher.missingElements = equalMatchersToElements(missingMatchers)

return false, nil
}

func (matcher *ContainElementsMatcher) FailureMessage(actual interface{}) (message string) {
message = format.Message(actual, "to contain elements", matcher.Elements)
return appendMissingElements(message, matcher.missingElements)
}

func (matcher *ContainElementsMatcher) NegatedFailureMessage(actual interface{}) (message string) {
return format.Message(actual, "not to contain elements", matcher.Elements)
}
85 changes: 85 additions & 0 deletions matchers/contain_elements_matcher_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package matchers_test

import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

var _ = Describe("ContainElements", func() {
Context("with a slice", func() {
It("should do the right thing", func() {
Expect([]string{"foo", "bar", "baz"}).Should(ContainElements("foo", "bar", "baz"))
Expect([]string{"foo", "bar", "baz"}).Should(ContainElements("bar"))
Expect([]string{"foo", "bar", "baz"}).Should(ContainElements())
Expect([]string{"foo", "bar", "baz"}).ShouldNot(ContainElements("baz", "bar", "foo", "foo"))
})
})

Context("with an array", func() {
It("should do the right thing", func() {
Expect([3]string{"foo", "bar", "baz"}).Should(ContainElements("foo", "bar", "baz"))
Expect([3]string{"foo", "bar", "baz"}).Should(ContainElements("bar"))
Expect([3]string{"foo", "bar", "baz"}).Should(ContainElements())
Expect([3]string{"foo", "bar", "baz"}).ShouldNot(ContainElements("baz", "bar", "foo", "foo"))
})
})

Context("with a map", func() {
It("should apply to the values", func() {
Expect(map[int]string{1: "foo", 2: "bar", 3: "baz"}).Should(ContainElements("foo", "bar", "baz"))
Expect(map[int]string{1: "foo", 2: "bar", 3: "baz"}).Should(ContainElements("bar"))
Expect(map[int]string{1: "foo", 2: "bar", 3: "baz"}).Should(ContainElements())
Expect(map[int]string{1: "foo", 2: "bar", 3: "baz"}).ShouldNot(ContainElements("baz", "bar", "foo", "foo"))
})

})

Context("with anything else", func() {
It("should error", func() {
failures := InterceptGomegaFailures(func() {
Expect("foo").Should(ContainElements("f", "o", "o"))
})

Expect(failures).Should(HaveLen(1))
})
})

Context("when passed matchers", func() {
It("should pass if the matchers pass", func() {
Expect([]string{"foo", "bar", "baz"}).Should(ContainElements("foo", MatchRegexp("^ba"), "baz"))
Expect([]string{"foo", "bar", "baz"}).Should(ContainElements("foo", MatchRegexp("^ba")))
Expect([]string{"foo", "bar", "baz"}).ShouldNot(ContainElements("foo", MatchRegexp("^ba"), MatchRegexp("foo")))
Expect([]string{"foo", "bar", "baz"}).Should(ContainElements("foo", MatchRegexp("^ba"), MatchRegexp("^ba")))
Expect([]string{"foo", "bar", "baz"}).ShouldNot(ContainElements("foo", MatchRegexp("^ba"), MatchRegexp("turducken")))
})

It("should not depend on the order of the matchers", func() {
Expect([][]int{{1, 2}, {2}}).Should(ContainElements(ContainElement(1), ContainElement(2)))
Expect([][]int{{1, 2}, {2}}).Should(ContainElements(ContainElement(2), ContainElement(1)))
})

Context("when a matcher errors", func() {
It("should soldier on", func() {
Expect([]string{"foo", "bar", "baz"}).ShouldNot(ContainElements(BeFalse(), "foo", "bar"))
Expect([]interface{}{"foo", "bar", false}).Should(ContainElements(BeFalse(), ContainSubstring("foo"), "bar"))
})
})
})

Context("when passed exactly one argument, and that argument is a slice", func() {
It("should match against the elements of that argument", func() {
Expect([]string{"foo", "bar", "baz"}).Should(ContainElements([]string{"foo", "baz"}))
})
})

Describe("FailureMessage", func() {
It("prints missing elements", func() {
failures := InterceptGomegaFailures(func() {
Expect([]int{2}).Should(ContainElements(1, 2, 3))
})

expected := "Expected\n.*\\[2\\]\nto contain elements\n.*\\[1, 2, 3\\]\nthe missing elements were\n.*\\[1, 3\\]"
Expect(failures).To(ContainElements(MatchRegexp(expected)))
})
})
})

0 comments on commit 49ad95b

Please sign in to comment.