Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ContainElements matcher #370

Merged
merged 3 commits into from Jan 22, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 14 additions & 0 deletions matchers.go
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
Expand Up @@ -16,35 +16,18 @@ type ConsistOfMatcher struct {
extraElements []interface{}
}

var neighbours = func(v, m interface{}) (bool, error) {
ansd marked this conversation as resolved.
Show resolved Hide resolved
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
@@ -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
}
Comment on lines +26 to +29
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's pretty cool +1


_, 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
@@ -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"}))
ansd marked this conversation as resolved.
Show resolved Hide resolved
})
})

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)))
})
})
})