From 9fc2ae2ec676dc2118f46343c4ce0ad2a9e1471d Mon Sep 17 00:00:00 2001 From: TheDiveO Date: Mon, 28 Feb 2022 13:25:45 +0100 Subject: [PATCH] implements issue #520 HaveEach matcher (#523) --- docs/index.md | 14 +++++ matchers.go | 14 +++++ matchers/have_each_matcher.go | 60 +++++++++++++++++++++ matchers/have_each_matcher_test.go | 85 ++++++++++++++++++++++++++++++ 4 files changed, 173 insertions(+) create mode 100644 matchers/have_each_matcher.go create mode 100644 matchers/have_each_matcher_test.go diff --git a/docs/index.md b/docs/index.md index cedb6094a..903fa2cf8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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 diff --git a/matchers.go b/matchers.go index b46e461a5..b09066899 100644 --- a/matchers.go +++ b/matchers.go @@ -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: diff --git a/matchers/have_each_matcher.go b/matchers/have_each_matcher.go new file mode 100644 index 000000000..14eee470f --- /dev/null +++ b/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) +} diff --git a/matchers/have_each_matcher_test.go b/matchers/have_each_matcher_test.go new file mode 100644 index 000000000..8beb3e80b --- /dev/null +++ b/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()) + }) + }) +})