Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Satisfy() matcher #437

Merged
merged 1 commit into from Apr 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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'"))
})
})
})
})