Skip to content

Commit

Permalink
Add HaveValue matcher (#485)
Browse files Browse the repository at this point in the history
* Add HaveValue matcher

* improves HaveValue matcher documentation and tests

* use resolved value in failure msgs, make nils and too many indirections errors instead of failures

* fix inverted failure message; add corresponding tests
  • Loading branch information
thediveo committed Dec 7, 2021
1 parent 2b4b2c0 commit bdc087c
Show file tree
Hide file tree
Showing 2 changed files with 149 additions and 0 deletions.
74 changes: 74 additions & 0 deletions matchers/have_value.go
@@ -0,0 +1,74 @@
package matchers

import (
"errors"
"reflect"

"github.com/onsi/gomega/format"
"github.com/onsi/gomega/types"
)

const maxIndirections = 31

// HaveValue applies the given matcher to the value of actual, optionally and
// repeatedly dereferencing pointers or taking the concrete value of interfaces.
// Thus, the matcher will always be applied to non-pointer and non-interface
// values only. HaveValue will fail with an error if a pointer or interface is
// nil. It will also fail for more than 31 pointer or interface dereferences to
// guard against mistakenly applying it to arbitrarily deep linked pointers.
//
// HaveValue differs from gstruct.PointTo in that it does not expect actual to
// be a pointer (as gstruct.PointTo does) but instead also accepts non-pointer
// and even interface values.
//
// actual := 42
// Expect(actual).To(HaveValue(42))
// Expect(&actual).To(HaveValue(42))
func HaveValue(matcher types.GomegaMatcher) types.GomegaMatcher {
return &HaveValueMatcher{
Matcher: matcher,
}
}

type HaveValueMatcher struct {
Matcher types.GomegaMatcher // the matcher to apply to the "resolved" actual value.
resolvedActual interface{} // the ("resolved") value.
}

func (m *HaveValueMatcher) Match(actual interface{}) (bool, error) {
val := reflect.ValueOf(actual)
for allowedIndirs := maxIndirections; allowedIndirs > 0; allowedIndirs-- {
// return an error if value isn't valid. Please note that we cannot
// check for nil here, as we might not deal with a pointer or interface
// at this point.
if !val.IsValid() {
return false, errors.New(format.Message(
actual, "not to be <nil>"))
}
switch val.Kind() {
case reflect.Ptr, reflect.Interface:
// resolve pointers and interfaces to their values, then rinse and
// repeat.
if val.IsNil() {
return false, errors.New(format.Message(
actual, "not to be <nil>"))
}
val = val.Elem()
continue
default:
// forward the final value to the specified matcher.
m.resolvedActual = val.Interface()
return m.Matcher.Match(m.resolvedActual)
}
}
// too many indirections: extreme star gazing, indeed...?
return false, errors.New(format.Message(actual, "too many indirections"))
}

func (m *HaveValueMatcher) FailureMessage(_ interface{}) (message string) {
return m.Matcher.FailureMessage(m.resolvedActual)
}

func (m *HaveValueMatcher) NegatedFailureMessage(_ interface{}) (message string) {
return m.Matcher.NegatedFailureMessage(m.resolvedActual)
}
75 changes: 75 additions & 0 deletions matchers/have_value_test.go
@@ -0,0 +1,75 @@
package matchers_test

import (
"reflect"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
. "github.com/onsi/gomega/matchers"
)

type I interface {
M()
}

type S struct {
V int
}

func (s S) M() {}

var _ = Describe("HaveValue", func() {

It("should fail when passed nil", func() {
var p *struct{}
m := HaveValue(BeNil())
Expect(m.Match(p)).Error().To(MatchError(MatchRegexp("not to be <nil>$")))
})

It("should fail when passed nil indirectly", func() {
var p *struct{}
m := HaveValue(BeNil())
Expect(m.Match(&p)).Error().To(MatchError(MatchRegexp("not to be <nil>$")))
})

It("should use the matcher's failure message", func() {
m := HaveValue(Equal(42))
Expect(m.Match(666)).To(BeFalse())
Expect(m.FailureMessage(nil)).To(Equal("Expected\n <int>: 666\nto equal\n <int>: 42"))
Expect(m.NegatedFailureMessage(nil)).To(Equal("Expected\n <int>: 666\nnot to equal\n <int>: 42"))
})

It("should unwrap the value pointed to, even repeatedly", func() {
i := 1
Expect(&i).To(HaveValue(Equal(1)))
Expect(&i).NotTo(HaveValue(Equal(2)))

pi := &i
Expect(pi).To(HaveValue(Equal(1)))
Expect(pi).NotTo(HaveValue(Equal(2)))

Expect(&pi).To(HaveValue(Equal(1)))
Expect(&pi).NotTo(HaveValue(Equal(2)))
})

It("shouldn't endlessly star-gaze", func() {
dave := "It's full of stars!"
stargazer := reflect.ValueOf(dave)
for stars := 1; stars <= 31; stars++ {
p := reflect.New(stargazer.Type())
p.Elem().Set(stargazer)
stargazer = p
}
m := HaveValue(Equal(dave))
Expect(m.Match(stargazer.Interface())).Error().To(
MatchError(MatchRegexp(`too many indirections`)))
Expect(m.Match(stargazer.Elem().Interface())).To(BeTrue())
})

It("should unwrap the value of an interface", func() {
var i I = &S{V: 42}
Expect(i).To(HaveValue(Equal(S{V: 42})))
Expect(i).NotTo(HaveValue(Equal(S{})))
})

})

0 comments on commit bdc087c

Please sign in to comment.