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

implements issue #520 HaveEach matcher #523

Merged
merged 1 commit into from Feb 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
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 docs/index.md
Expand Up @@ -970,6 +970,20 @@ is the only element passed in to `ConsistOf`:
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.


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

```go
Ω(ACTUAL).Should(HaveEach(ELEMENT))
```

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!).

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:

```go
Ω([]string{"Foo", "FooBar"}).Should(HaveEach(ContainSubstring("Foo")))
```

#### HaveKey(key interface{})

```go
Expand Down
14 changes: 14 additions & 0 deletions matchers.go
Expand Up @@ -320,6 +320,20 @@ func ContainElements(elements ...interface{}) types.GomegaMatcher {
}
}

//HaveEach succeeds if actual solely contains elements that match the passed in element.
//Please note that if actual is empty, HaveEach always will succeed.
//By default HaveEach() uses Equal() to perform the match, however a
//matcher can be passed in instead:
// Expect([]string{"Foo", "FooBar"}).Should(HaveEach(ContainSubstring("Foo")))
//
//Actual must be an array, slice or map.
//For maps, HaveEach searches through the map's values.
func HaveEach(element interface{}) types.GomegaMatcher {
return &matchers.HaveEachMatcher{
Element: element,
}
}

//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
60 changes: 60 additions & 0 deletions matchers/have_each_matcher.go
@@ -0,0 +1,60 @@
package matchers

import (
"fmt"
"reflect"

"github.com/onsi/gomega/format"
)

type HaveEachMatcher struct {
Element interface{}
}

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

elemMatcher, elementIsMatcher := matcher.Element.(omegaMatcher)
if !elementIsMatcher {
elemMatcher = &EqualMatcher{Expected: matcher.Element}
}

value := reflect.ValueOf(actual)
var valueAt func(int) interface{}
if isMap(actual) {
keys := value.MapKeys()
valueAt = func(i int) interface{} {
return value.MapIndex(keys[i]).Interface()
}
} else {
valueAt = func(i int) interface{} {
return value.Index(i).Interface()
}
}

// if there are no elements, then HaveEach will match.
for i := 0; i < value.Len(); i++ {
success, err := elemMatcher.Match(valueAt(i))
if err != nil {
return false, err
}
if !success {
return false, nil
}
}

return true, nil
}

// FailureMessage returns a suitable failure message.
func (matcher *HaveEachMatcher) FailureMessage(actual interface{}) (message string) {
return format.Message(actual, "to contain element matching", matcher.Element)
}

// NegatedFailureMessage returns a suitable negated failure message.
func (matcher *HaveEachMatcher) NegatedFailureMessage(actual interface{}) (message string) {
return format.Message(actual, "not to contain element matching", matcher.Element)
}
85 changes: 85 additions & 0 deletions matchers/have_each_matcher_test.go
@@ -0,0 +1,85 @@
package matchers_test

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

var _ = Describe("HaveEach", func() {
When("passed a supported type", func() {
Context("and expecting a non-matcher", func() {
It("should do the right thing", func() {
Expect([]int{}).Should(HaveEach(42))

Expect([2]int{2, 2}).Should(HaveEach(2))
Expect([2]int{2, 3}).ShouldNot(HaveEach(3))

Expect([]int{2, 2}).Should(HaveEach(2))
Expect([]int{1, 2}).ShouldNot(HaveEach(3))

Expect(map[string]int{"foo": 2, "bar": 2}).Should(HaveEach(2))
Expect(map[int]int{3: 3, 4: 2}).ShouldNot(HaveEach(3))

arr := make([]myCustomType, 2)
arr[0] = myCustomType{s: "foo", n: 3, f: 2.0, arr: []string{"a", "b"}}
arr[1] = myCustomType{s: "foo", n: 3, f: 2.0, arr: []string{"a", "b"}}
Expect(arr).Should(HaveEach(myCustomType{s: "foo", n: 3, f: 2.0, arr: []string{"a", "b"}}))
Expect(arr).ShouldNot(HaveEach(myCustomType{s: "foo", n: 3, f: 2.0, arr: []string{"b", "c"}}))

// ...and finaaaaaly, let's eat our own documentation ;)
Expect([]string{"Foo", "FooBar"}).Should(HaveEach(ContainSubstring("Foo")))
Expect([]string{"Foo", "FooBar"}).ShouldNot(HaveEach(ContainSubstring("Bar")))
})
})

Context("and expecting a matcher", func() {
It("should pass each element through the matcher", func() {
Expect([]int{1, 2, 3}).Should(HaveEach(BeNumerically(">=", 1)))
Expect([]int{1, 2, 3}).ShouldNot(HaveEach(BeNumerically(">", 1)))
Expect(map[string]int{"foo": 1, "bar": 2}).Should(HaveEach(BeNumerically(">=", 1)))
Expect(map[string]int{"foo": 1, "bar": 2}).ShouldNot(HaveEach(BeNumerically(">=", 2)))
})

It("should not power through if the matcher ever fails", func() {
actual := []interface{}{1, 2, "3", 4}
success, err := (&HaveEachMatcher{Element: BeNumerically(">=", 1)}).Match(actual)
Expect(success).Should(BeFalse())
Expect(err).Should(HaveOccurred())
})

It("should fail if the matcher fails", func() {
actual := []interface{}{1, 2, "3", "4"}
success, err := (&HaveEachMatcher{Element: BeNumerically(">=", 1)}).Match(actual)
Expect(success).Should(BeFalse())
Expect(err).Should(HaveOccurred())
})
})
})

When("passed a correctly typed nil", func() {
It("should operate succesfully on the passed in value", func() {
var nilSlice []int
Expect(nilSlice).Should(HaveEach(1))

var nilMap map[int]string
Expect(nilMap).Should(HaveEach("foo"))
})
})

When("passed an unsupported type", func() {
It("should error", func() {
success, err := (&HaveEachMatcher{Element: 0}).Match(0)
Expect(success).Should(BeFalse())
Expect(err).Should(HaveOccurred())

success, err = (&HaveEachMatcher{Element: 0}).Match("abc")
Expect(success).Should(BeFalse())
Expect(err).Should(HaveOccurred())

success, err = (&HaveEachMatcher{Element: 0}).Match(nil)
Expect(success).Should(BeFalse())
Expect(err).Should(HaveOccurred())
})
})
})