diff --git a/matchers.go b/matchers.go index 16218d4c5..667160ade 100644 --- a/matchers.go +++ b/matchers.go @@ -474,3 +474,11 @@ func Not(matcher types.GomegaMatcher) types.GomegaMatcher { func WithTransform(transform interface{}, matcher types.GomegaMatcher) types.GomegaMatcher { return matchers.NewWithTransformMatcher(transform, matcher) } + +//Satisfy matches the actual value against the `predicate` function. +//The given predicate must be a function of one paramter that returns bool. +// var isEven = func(i int) bool { return i%2 == 0 } +// Expect(2).To(Satisfy(isEven)) +func Satisfy(predicate interface{}) types.GomegaMatcher { + return matchers.NewSatisfyMatcher(predicate) +} diff --git a/matchers/satisfy_matcher.go b/matchers/satisfy_matcher.go new file mode 100644 index 000000000..ec68fe8b6 --- /dev/null +++ b/matchers/satisfy_matcher.go @@ -0,0 +1,66 @@ +package matchers + +import ( + "fmt" + "reflect" + + "github.com/onsi/gomega/format" +) + +type SatisfyMatcher struct { + Predicate interface{} + + // cached type + predicateArgType reflect.Type +} + +func NewSatisfyMatcher(predicate interface{}) *SatisfyMatcher { + if predicate == nil { + panic("predicate cannot be nil") + } + predicateType := reflect.TypeOf(predicate) + if predicateType.Kind() != reflect.Func { + panic("predicate must be a function") + } + if predicateType.NumIn() != 1 { + panic("predicate must have 1 argument") + } + if predicateType.NumOut() != 1 || predicateType.Out(0).Kind() != reflect.Bool { + panic("predicate must return bool") + } + + return &SatisfyMatcher{ + Predicate: predicate, + predicateArgType: predicateType.In(0), + } +} + +func (m *SatisfyMatcher) Match(actual interface{}) (success bool, err error) { + // prepare a parameter to pass to the predicate + var param reflect.Value + if actual != nil && reflect.TypeOf(actual).AssignableTo(m.predicateArgType) { + // The dynamic type of actual is compatible with the predicate argument. + param = reflect.ValueOf(actual) + + } else if actual == nil && m.predicateArgType.Kind() == reflect.Interface { + // The dynamic type of actual is unknown, so there's no way to make its + // reflect.Value. Create a nil of the predicate argument, which is known. + param = reflect.Zero(m.predicateArgType) + + } else { + return false, fmt.Errorf("predicate expects '%s' but we have '%T'", m.predicateArgType, actual) + } + + // call the predicate with `actual` + fn := reflect.ValueOf(m.Predicate) + result := fn.Call([]reflect.Value{param}) + return result[0].Bool(), nil +} + +func (m *SatisfyMatcher) FailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to satisfy predicate", m.Predicate) +} + +func (m *SatisfyMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return format.Message(actual, "to not satisfy predicate", m.Predicate) +} diff --git a/matchers/satisfy_matcher_test.go b/matchers/satisfy_matcher_test.go new file mode 100644 index 000000000..b91e8060f --- /dev/null +++ b/matchers/satisfy_matcher_test.go @@ -0,0 +1,119 @@ +package matchers_test + +import ( + "errors" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("SatisfyMatcher", func() { + + var isEven = func(x int) bool { return x%2 == 0 } + + Context("Panic if predicate is invalid", func() { + panicsWithPredicate := func(predicate interface{}) { + ExpectWithOffset(1, func() { Satisfy(predicate) }).To(Panic()) + } + It("nil", func() { + panicsWithPredicate(nil) + }) + Context("Invalid number of args, but correct return value count", func() { + It("zero", func() { + panicsWithPredicate(func() int { return 5 }) + }) + It("two", func() { + panicsWithPredicate(func(i, j int) int { return 5 }) + }) + }) + Context("Invalid return types, but correct number of arguments", func() { + It("zero", func() { + panicsWithPredicate(func(i int) {}) + }) + It("two", func() { + panicsWithPredicate(func(i int) (int, int) { return 5, 6 }) + }) + It("invalid type", func() { + panicsWithPredicate(func(i int) string { return "" }) + }) + }) + }) + + When("the actual value is incompatible", func() { + It("fails to pass int to func(string)", func() { + actual, predicate := int(0), func(string) bool { return false } + success, err := Satisfy(predicate).Match(actual) + Expect(success).To(BeFalse()) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("expects 'string'")) + Expect(err.Error()).To(ContainSubstring("have 'int'")) + }) + + It("fails to pass string to func(interface)", func() { + actual, predicate := "bang", func(error) bool { return false } + success, err := Satisfy(predicate).Match(actual) + Expect(success).To(BeFalse()) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("expects 'error'")) + Expect(err.Error()).To(ContainSubstring("have 'string'")) + }) + + It("fails to pass nil interface to func(int)", func() { + actual, predicate := error(nil), func(int) bool { return false } + success, err := Satisfy(predicate).Match(actual) + Expect(success).To(BeFalse()) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("expects 'int'")) + Expect(err.Error()).To(ContainSubstring("have ''")) + }) + + It("fails to pass nil interface to func(pointer)", func() { + actual, predicate := error(nil), func(*string) bool { return false } + success, err := Satisfy(predicate).Match(actual) + Expect(success).To(BeFalse()) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("expects '*string'")) + Expect(err.Error()).To(ContainSubstring("have ''")) + }) + }) + + It("works with positive cases", func() { + Expect(2).To(Satisfy(isEven)) + + // transform expects interface + takesError := func(error) bool { return true } + Expect(nil).To(Satisfy(takesError), "handles nil actual values") + Expect(errors.New("abc")).To(Satisfy(takesError)) + }) + + It("works with negative cases", func() { + Expect(1).ToNot(Satisfy(isEven)) + }) + + Context("failure messages", func() { + When("match fails", func() { + It("gives a descriptive message", func() { + m := Satisfy(isEven) + Expect(m.Match(1)).To(BeFalse()) + Expect(m.FailureMessage(1)).To(ContainSubstring("Expected\n : 1\nto satisfy predicate\n : ")) + }) + }) + + When("match succeeds, but expected it to fail", func() { + It("gives a descriptive message", func() { + m := Not(Satisfy(isEven)) + Expect(m.Match(2)).To(BeFalse()) + Expect(m.FailureMessage(2)).To(ContainSubstring("Expected\n : 2\nto not satisfy predicate\n : ")) + }) + }) + + Context("actual value is incompatible with predicate's argument type", func() { + It("gracefully fails", func() { + m := Satisfy(isEven) + result, err := m.Match("hi") // give it a string but predicate expects int; doesn't panic + Expect(result).To(BeFalse()) + Expect(err).To(MatchError("predicate expects 'int' but we have 'string'")) + }) + }) + }) +})