Skip to content

Commit

Permalink
Add HaveExactElements matcher (#634)
Browse files Browse the repository at this point in the history
  • Loading branch information
rickyson96 committed Feb 16, 2023
1 parent 296a68b commit 9d50783
Show file tree
Hide file tree
Showing 4 changed files with 234 additions and 0 deletions.
32 changes: 32 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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{})

Expand Down
14 changes: 14 additions & 0 deletions matchers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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:
//
Expand Down
75 changes: 75 additions & 0 deletions matchers/have_exact_elements.go
Original file line number Diff line number Diff line change
@@ -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))
}
113 changes: 113 additions & 0 deletions matchers/have_exact_elements_test.go
Original file line number Diff line number Diff line change
@@ -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
<int>: 1
to equal
<int>: 2
1: Expected
<int>: 2
to equal
<int>: 1`
Expect(failures[0]).To(MatchRegexp(expected))
})
})
})
})

0 comments on commit 9d50783

Please sign in to comment.