Skip to content

Commit bdc087c

Browse files
authored
Add HaveValue matcher (#485)
* 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
1 parent 2b4b2c0 commit bdc087c

File tree

2 files changed

+149
-0
lines changed

2 files changed

+149
-0
lines changed

matchers/have_value.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package matchers
2+
3+
import (
4+
"errors"
5+
"reflect"
6+
7+
"github.com/onsi/gomega/format"
8+
"github.com/onsi/gomega/types"
9+
)
10+
11+
const maxIndirections = 31
12+
13+
// HaveValue applies the given matcher to the value of actual, optionally and
14+
// repeatedly dereferencing pointers or taking the concrete value of interfaces.
15+
// Thus, the matcher will always be applied to non-pointer and non-interface
16+
// values only. HaveValue will fail with an error if a pointer or interface is
17+
// nil. It will also fail for more than 31 pointer or interface dereferences to
18+
// guard against mistakenly applying it to arbitrarily deep linked pointers.
19+
//
20+
// HaveValue differs from gstruct.PointTo in that it does not expect actual to
21+
// be a pointer (as gstruct.PointTo does) but instead also accepts non-pointer
22+
// and even interface values.
23+
//
24+
// actual := 42
25+
// Expect(actual).To(HaveValue(42))
26+
// Expect(&actual).To(HaveValue(42))
27+
func HaveValue(matcher types.GomegaMatcher) types.GomegaMatcher {
28+
return &HaveValueMatcher{
29+
Matcher: matcher,
30+
}
31+
}
32+
33+
type HaveValueMatcher struct {
34+
Matcher types.GomegaMatcher // the matcher to apply to the "resolved" actual value.
35+
resolvedActual interface{} // the ("resolved") value.
36+
}
37+
38+
func (m *HaveValueMatcher) Match(actual interface{}) (bool, error) {
39+
val := reflect.ValueOf(actual)
40+
for allowedIndirs := maxIndirections; allowedIndirs > 0; allowedIndirs-- {
41+
// return an error if value isn't valid. Please note that we cannot
42+
// check for nil here, as we might not deal with a pointer or interface
43+
// at this point.
44+
if !val.IsValid() {
45+
return false, errors.New(format.Message(
46+
actual, "not to be <nil>"))
47+
}
48+
switch val.Kind() {
49+
case reflect.Ptr, reflect.Interface:
50+
// resolve pointers and interfaces to their values, then rinse and
51+
// repeat.
52+
if val.IsNil() {
53+
return false, errors.New(format.Message(
54+
actual, "not to be <nil>"))
55+
}
56+
val = val.Elem()
57+
continue
58+
default:
59+
// forward the final value to the specified matcher.
60+
m.resolvedActual = val.Interface()
61+
return m.Matcher.Match(m.resolvedActual)
62+
}
63+
}
64+
// too many indirections: extreme star gazing, indeed...?
65+
return false, errors.New(format.Message(actual, "too many indirections"))
66+
}
67+
68+
func (m *HaveValueMatcher) FailureMessage(_ interface{}) (message string) {
69+
return m.Matcher.FailureMessage(m.resolvedActual)
70+
}
71+
72+
func (m *HaveValueMatcher) NegatedFailureMessage(_ interface{}) (message string) {
73+
return m.Matcher.NegatedFailureMessage(m.resolvedActual)
74+
}

matchers/have_value_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package matchers_test
2+
3+
import (
4+
"reflect"
5+
6+
. "github.com/onsi/ginkgo/v2"
7+
. "github.com/onsi/gomega"
8+
. "github.com/onsi/gomega/matchers"
9+
)
10+
11+
type I interface {
12+
M()
13+
}
14+
15+
type S struct {
16+
V int
17+
}
18+
19+
func (s S) M() {}
20+
21+
var _ = Describe("HaveValue", func() {
22+
23+
It("should fail when passed nil", func() {
24+
var p *struct{}
25+
m := HaveValue(BeNil())
26+
Expect(m.Match(p)).Error().To(MatchError(MatchRegexp("not to be <nil>$")))
27+
})
28+
29+
It("should fail when passed nil indirectly", func() {
30+
var p *struct{}
31+
m := HaveValue(BeNil())
32+
Expect(m.Match(&p)).Error().To(MatchError(MatchRegexp("not to be <nil>$")))
33+
})
34+
35+
It("should use the matcher's failure message", func() {
36+
m := HaveValue(Equal(42))
37+
Expect(m.Match(666)).To(BeFalse())
38+
Expect(m.FailureMessage(nil)).To(Equal("Expected\n <int>: 666\nto equal\n <int>: 42"))
39+
Expect(m.NegatedFailureMessage(nil)).To(Equal("Expected\n <int>: 666\nnot to equal\n <int>: 42"))
40+
})
41+
42+
It("should unwrap the value pointed to, even repeatedly", func() {
43+
i := 1
44+
Expect(&i).To(HaveValue(Equal(1)))
45+
Expect(&i).NotTo(HaveValue(Equal(2)))
46+
47+
pi := &i
48+
Expect(pi).To(HaveValue(Equal(1)))
49+
Expect(pi).NotTo(HaveValue(Equal(2)))
50+
51+
Expect(&pi).To(HaveValue(Equal(1)))
52+
Expect(&pi).NotTo(HaveValue(Equal(2)))
53+
})
54+
55+
It("shouldn't endlessly star-gaze", func() {
56+
dave := "It's full of stars!"
57+
stargazer := reflect.ValueOf(dave)
58+
for stars := 1; stars <= 31; stars++ {
59+
p := reflect.New(stargazer.Type())
60+
p.Elem().Set(stargazer)
61+
stargazer = p
62+
}
63+
m := HaveValue(Equal(dave))
64+
Expect(m.Match(stargazer.Interface())).Error().To(
65+
MatchError(MatchRegexp(`too many indirections`)))
66+
Expect(m.Match(stargazer.Elem().Interface())).To(BeTrue())
67+
})
68+
69+
It("should unwrap the value of an interface", func() {
70+
var i I = &S{V: 42}
71+
Expect(i).To(HaveValue(Equal(S{V: 42})))
72+
Expect(i).NotTo(HaveValue(Equal(S{})))
73+
})
74+
75+
})

0 commit comments

Comments
 (0)