Skip to content

Commit

Permalink
implements issue #520 HaveEach matcher (#523)
Browse files Browse the repository at this point in the history
  • Loading branch information
thediveo committed Feb 28, 2022
1 parent d01bc22 commit 9fc2ae2
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 0 deletions.
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())
})
})
})

0 comments on commit 9fc2ae2

Please sign in to comment.