Skip to content

Commit

Permalink
Add Satisfy() matcher (#437)
Browse files Browse the repository at this point in the history
  • Loading branch information
kaoet committed Apr 25, 2021
1 parent 26a6ffc commit c548f31
Show file tree
Hide file tree
Showing 3 changed files with 193 additions and 0 deletions.
8 changes: 8 additions & 0 deletions matchers.go
Expand Up @@ -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)
}
66 changes: 66 additions & 0 deletions 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)
}
119 changes: 119 additions & 0 deletions 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 '<nil>'"))
})

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 '<nil>'"))
})
})

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 <int>: 1\nto satisfy predicate\n <func(int) bool>: "))
})
})

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 <int>: 2\nto not satisfy predicate\n <func(int) bool>: "))
})
})

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'"))
})
})
})
})

0 comments on commit c548f31

Please sign in to comment.