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

Add HaveExactElements matcher #634

Merged
merged 2 commits into from
Feb 16, 2023
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
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))
})
})
})
})