diff --git a/docs/index.md b/docs/index.md index e44e91a29..5b3f3c81f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1215,6 +1215,38 @@ 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. +#### HaveExactElements(element ...interface{}) + +```go +Expect(ACTUAL).To(HaveExactElements(ELEMENT1, ELEMENT2, ELEMENT3, ...)) +``` + +or + +```go +Expect(ACTUAL).To(HaveExactElements([]SOME_TYPE{ELEMENT1, ELEMENT2, ELEMENT3, ...})) +``` + +succeeds if `ACTUAL` contains precisely the elements and ordering passed into the matchers. + +By default `HaveExactElements()` uses `Equal()` to match the elements, however custom matchers can be passed in instead. Here are some examples: + +```go +Expect([]string{"Foo", "FooBar"}).To(HaveExactElements("Foo", "FooBar")) +Expect([]string{"Foo", "FooBar"}).To(HaveExactElements("Foo", ContainSubstring("Bar"))) +Expect([]string{"Foo", "FooBar"}).To(HaveExactElements(ContainSubstring("Foo"), ContainSubstring("Foo"))) +``` + +Actual must be an `array` or `slice`. + +You typically pass variadic arguments to `HaveExactElements` (as in the examples above). However, if you need to pass in a slice you can provided that it +is the only element passed in to `HaveExactElements`: + +```go +Expect([]string{"Foo", "FooBar"}).To(HaveExactElements([]string{"FooBar", "Foo"})) +``` + +Note that Go's type system does not allow you to write this as `HaveExactElements([]string{"FooBar", "Foo"}...)` as `[]string` and `[]interface{}` are different types - hence the need for this special rule. #### HaveEach(element ...interface{}) diff --git a/matchers.go b/matchers.go index 857586a91..44056ad64 100644 --- a/matchers.go +++ b/matchers.go @@ -349,6 +349,20 @@ func ConsistOf(elements ...interface{}) types.GomegaMatcher { } } +// HaveExactElemets succeeds if actual contains elements that precisely match the elemets passed into the matcher. The ordering of the elements does matter. +// By default HaveExactElements() uses Equal() to match the elements, however custom matchers can be passed in instead. Here are some examples: +// +// Expect([]string{"Foo", "FooBar"}).Should(HaveExactElements("Foo", "FooBar")) +// Expect([]string{"Foo", "FooBar"}).Should(HaveExactElements("Foo", ContainSubstring("Bar"))) +// Expect([]string{"Foo", "FooBar"}).Should(HaveExactElements(ContainSubstring("Foo"), ContainSubstring("Foo"))) +// +// Actual must be an array or slice. +func HaveExactElements(elements ...interface{}) types.GomegaMatcher { + return &matchers.HaveExactElementsMatcher{ + Elements: elements, + } +} + // 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: // diff --git a/matchers/have_exact_elements.go b/matchers/have_exact_elements.go new file mode 100644 index 000000000..19d8f3d1d --- /dev/null +++ b/matchers/have_exact_elements.go @@ -0,0 +1,75 @@ +package matchers + +import ( + "fmt" + + "github.com/onsi/gomega/format" +) + +type mismatchFailure struct { + failure string + index int +} + +type HaveExactElementsMatcher struct { + Elements []interface{} + mismatchFailures []mismatchFailure + missingIndex int + extraIndex int +} + +func (matcher *HaveExactElementsMatcher) Match(actual interface{}) (success bool, err error) { + if isMap(actual) { + return false, fmt.Errorf("error") + } + + matchers := matchers(matcher.Elements) + values := valuesOf(actual) + + lenMatchers := len(matchers) + lenValues := len(values) + + for i := 0; i < lenMatchers || i < lenValues; i++ { + if i >= lenMatchers { + matcher.extraIndex = i + continue + } + + if i >= lenValues { + matcher.missingIndex = i + return + } + + elemMatcher := matchers[i].(omegaMatcher) + match, err := elemMatcher.Match(values[i]) + if err != nil || !match { + matcher.mismatchFailures = append(matcher.mismatchFailures, mismatchFailure{ + index: i, + failure: elemMatcher.FailureMessage(values[i]), + }) + } + } + + return matcher.missingIndex+matcher.extraIndex+len(matcher.mismatchFailures) == 0, nil +} + +func (matcher *HaveExactElementsMatcher) FailureMessage(actual interface{}) (message string) { + message = format.Message(actual, "to have exact elements with", presentable(matcher.Elements)) + if matcher.missingIndex > 0 { + message = fmt.Sprintf("%s\nthe missing elements start from index %d", message, matcher.missingIndex) + } + if matcher.extraIndex > 0 { + message = fmt.Sprintf("%s\nthe extra elements start from index %d", message, matcher.extraIndex) + } + if len(matcher.mismatchFailures) != 0 { + message = fmt.Sprintf("%s\nthe mismatch indexes were:", message) + } + for _, mismatch := range matcher.mismatchFailures { + message = fmt.Sprintf("%s\n%d: %s", message, mismatch.index, mismatch.failure) + } + return +} + +func (matcher *HaveExactElementsMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "not to contain elements", presentable(matcher.Elements)) +} diff --git a/matchers/have_exact_elements_test.go b/matchers/have_exact_elements_test.go new file mode 100644 index 000000000..154bb3c74 --- /dev/null +++ b/matchers/have_exact_elements_test.go @@ -0,0 +1,113 @@ +package matchers_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("HaveExactElements", func() { + Context("with a slice", func() { + It("should do the right thing", func() { + Expect([]string{"foo", "bar"}).Should(HaveExactElements("foo", "bar")) + Expect([]string{"foo", "bar"}).ShouldNot(HaveExactElements("foo")) + Expect([]string{"foo", "bar"}).ShouldNot(HaveExactElements("foo", "bar", "baz")) + Expect([]string{"foo", "bar"}).ShouldNot(HaveExactElements("bar", "foo")) + }) + }) + Context("with an array", func() { + It("should do the right thing", func() { + Expect([2]string{"foo", "bar"}).Should(HaveExactElements("foo", "bar")) + Expect([2]string{"foo", "bar"}).ShouldNot(HaveExactElements("foo")) + Expect([2]string{"foo", "bar"}).ShouldNot(HaveExactElements("foo", "bar", "baz")) + Expect([2]string{"foo", "bar"}).ShouldNot(HaveExactElements("bar", "foo")) + }) + }) + Context("with map", func() { + It("should error", func() { + failures := InterceptGomegaFailures(func() { + Expect(map[int]string{1: "foo"}).Should(HaveExactElements("foo")) + }) + + Expect(failures).Should(HaveLen(1)) + }) + }) + Context("with anything else", func() { + It("should error", func() { + failures := InterceptGomegaFailures(func() { + Expect("foo").Should(HaveExactElements("f", "o", "o")) + }) + + Expect(failures).Should(HaveLen(1)) + }) + }) + + When("passed matchers", func() { + It("should pass if matcher pass", func() { + Expect([]string{"foo", "bar", "baz"}).Should(HaveExactElements("foo", MatchRegexp("^ba"), MatchRegexp("az$"))) + Expect([]string{"foo", "bar", "baz"}).ShouldNot(HaveExactElements("foo", MatchRegexp("az$"), MatchRegexp("^ba"))) + Expect([]string{"foo", "bar", "baz"}).ShouldNot(HaveExactElements("foo", MatchRegexp("az$"))) + Expect([]string{"foo", "bar", "baz"}).ShouldNot(HaveExactElements("foo", MatchRegexp("az$"), "baz", "bac")) + }) + + When("a matcher errors", func() { + It("should soldier on", func() { + Expect([]string{"foo", "bar", "baz"}).ShouldNot(HaveExactElements(BeFalse(), "bar", "baz")) + Expect([]interface{}{"foo", "bar", false}).Should(HaveExactElements(ContainSubstring("foo"), "bar", BeFalse())) + }) + }) + }) + + When("passed exactly one argument, and that argument is a slice", func() { + It("should match against the elements of that arguments", func() { + Expect([]string{"foo", "bar", "baz"}).Should(HaveExactElements([]string{"foo", "bar", "baz"})) + Expect([]string{"foo", "bar", "baz"}).ShouldNot(HaveExactElements([]string{"foo", "bar"})) + }) + }) + + Describe("Failure Message", func() { + When("actual contains extra elements", func() { + It("should print the starting index of the extra elements", func() { + failures := InterceptGomegaFailures(func() { + Expect([]int{1, 2}).Should(HaveExactElements(1)) + }) + + expected := "Expected\n.*\\[1, 2\\]\nto have exact elements with\n.*\\[1\\]\nthe extra elements start from index 1" + Expect(failures).To(ConsistOf(MatchRegexp(expected))) + }) + }) + + When("actual misses an element", func() { + It("should print the starting index of missing element", func() { + failures := InterceptGomegaFailures(func() { + Expect([]int{1}).Should(HaveExactElements(1, 2)) + }) + + expected := "Expected\n.*\\[1\\]\nto have exact elements with\n.*\\[1, 2\\]\nthe missing elements start from index 1" + Expect(failures).To(ConsistOf(MatchRegexp(expected))) + }) + }) + + When("actual have mismatched elements", func() { + It("should print the index, expected element, and actual element", func() { + failures := InterceptGomegaFailures(func() { + Expect([]int{1, 2}).Should(HaveExactElements(2, 1)) + }) + + expected := `Expected +.*\[1, 2\] +to have exact elements with +.*\[2, 1\] +the mismatch indexes were: +0: Expected + : 1 +to equal + : 2 +1: Expected + : 2 +to equal + : 1` + Expect(failures[0]).To(MatchRegexp(expected)) + }) + }) + }) +})