diff --git a/matchers/have_value.go b/matchers/have_value.go new file mode 100644 index 000000000..58f4404db --- /dev/null +++ b/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 ")) + } + 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 ")) + } + 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) +} diff --git a/matchers/have_value_test.go b/matchers/have_value_test.go new file mode 100644 index 000000000..32c8ade29 --- /dev/null +++ b/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 $"))) + }) + + 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 $"))) + }) + + 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 : 666\nto equal\n : 42")) + Expect(m.NegatedFailureMessage(nil)).To(Equal("Expected\n : 666\nnot to equal\n : 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{}))) + }) + +})