Skip to content

Commit c548f31

Browse files
authoredApr 25, 2021
Add Satisfy() matcher (#437)
1 parent 26a6ffc commit c548f31

File tree

3 files changed

+193
-0
lines changed

3 files changed

+193
-0
lines changed
 

‎matchers.go

+8
Original file line numberDiff line numberDiff line change
@@ -474,3 +474,11 @@ func Not(matcher types.GomegaMatcher) types.GomegaMatcher {
474474
func WithTransform(transform interface{}, matcher types.GomegaMatcher) types.GomegaMatcher {
475475
return matchers.NewWithTransformMatcher(transform, matcher)
476476
}
477+
478+
//Satisfy matches the actual value against the `predicate` function.
479+
//The given predicate must be a function of one paramter that returns bool.
480+
// var isEven = func(i int) bool { return i%2 == 0 }
481+
// Expect(2).To(Satisfy(isEven))
482+
func Satisfy(predicate interface{}) types.GomegaMatcher {
483+
return matchers.NewSatisfyMatcher(predicate)
484+
}

‎matchers/satisfy_matcher.go

+66
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package matchers
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
7+
"github.com/onsi/gomega/format"
8+
)
9+
10+
type SatisfyMatcher struct {
11+
Predicate interface{}
12+
13+
// cached type
14+
predicateArgType reflect.Type
15+
}
16+
17+
func NewSatisfyMatcher(predicate interface{}) *SatisfyMatcher {
18+
if predicate == nil {
19+
panic("predicate cannot be nil")
20+
}
21+
predicateType := reflect.TypeOf(predicate)
22+
if predicateType.Kind() != reflect.Func {
23+
panic("predicate must be a function")
24+
}
25+
if predicateType.NumIn() != 1 {
26+
panic("predicate must have 1 argument")
27+
}
28+
if predicateType.NumOut() != 1 || predicateType.Out(0).Kind() != reflect.Bool {
29+
panic("predicate must return bool")
30+
}
31+
32+
return &SatisfyMatcher{
33+
Predicate: predicate,
34+
predicateArgType: predicateType.In(0),
35+
}
36+
}
37+
38+
func (m *SatisfyMatcher) Match(actual interface{}) (success bool, err error) {
39+
// prepare a parameter to pass to the predicate
40+
var param reflect.Value
41+
if actual != nil && reflect.TypeOf(actual).AssignableTo(m.predicateArgType) {
42+
// The dynamic type of actual is compatible with the predicate argument.
43+
param = reflect.ValueOf(actual)
44+
45+
} else if actual == nil && m.predicateArgType.Kind() == reflect.Interface {
46+
// The dynamic type of actual is unknown, so there's no way to make its
47+
// reflect.Value. Create a nil of the predicate argument, which is known.
48+
param = reflect.Zero(m.predicateArgType)
49+
50+
} else {
51+
return false, fmt.Errorf("predicate expects '%s' but we have '%T'", m.predicateArgType, actual)
52+
}
53+
54+
// call the predicate with `actual`
55+
fn := reflect.ValueOf(m.Predicate)
56+
result := fn.Call([]reflect.Value{param})
57+
return result[0].Bool(), nil
58+
}
59+
60+
func (m *SatisfyMatcher) FailureMessage(actual interface{}) (message string) {
61+
return format.Message(actual, "to satisfy predicate", m.Predicate)
62+
}
63+
64+
func (m *SatisfyMatcher) NegatedFailureMessage(actual interface{}) (message string) {
65+
return format.Message(actual, "to not satisfy predicate", m.Predicate)
66+
}

‎matchers/satisfy_matcher_test.go

+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package matchers_test
2+
3+
import (
4+
"errors"
5+
6+
. "github.com/onsi/ginkgo"
7+
. "github.com/onsi/gomega"
8+
)
9+
10+
var _ = Describe("SatisfyMatcher", func() {
11+
12+
var isEven = func(x int) bool { return x%2 == 0 }
13+
14+
Context("Panic if predicate is invalid", func() {
15+
panicsWithPredicate := func(predicate interface{}) {
16+
ExpectWithOffset(1, func() { Satisfy(predicate) }).To(Panic())
17+
}
18+
It("nil", func() {
19+
panicsWithPredicate(nil)
20+
})
21+
Context("Invalid number of args, but correct return value count", func() {
22+
It("zero", func() {
23+
panicsWithPredicate(func() int { return 5 })
24+
})
25+
It("two", func() {
26+
panicsWithPredicate(func(i, j int) int { return 5 })
27+
})
28+
})
29+
Context("Invalid return types, but correct number of arguments", func() {
30+
It("zero", func() {
31+
panicsWithPredicate(func(i int) {})
32+
})
33+
It("two", func() {
34+
panicsWithPredicate(func(i int) (int, int) { return 5, 6 })
35+
})
36+
It("invalid type", func() {
37+
panicsWithPredicate(func(i int) string { return "" })
38+
})
39+
})
40+
})
41+
42+
When("the actual value is incompatible", func() {
43+
It("fails to pass int to func(string)", func() {
44+
actual, predicate := int(0), func(string) bool { return false }
45+
success, err := Satisfy(predicate).Match(actual)
46+
Expect(success).To(BeFalse())
47+
Expect(err).To(HaveOccurred())
48+
Expect(err.Error()).To(ContainSubstring("expects 'string'"))
49+
Expect(err.Error()).To(ContainSubstring("have 'int'"))
50+
})
51+
52+
It("fails to pass string to func(interface)", func() {
53+
actual, predicate := "bang", func(error) bool { return false }
54+
success, err := Satisfy(predicate).Match(actual)
55+
Expect(success).To(BeFalse())
56+
Expect(err).To(HaveOccurred())
57+
Expect(err.Error()).To(ContainSubstring("expects 'error'"))
58+
Expect(err.Error()).To(ContainSubstring("have 'string'"))
59+
})
60+
61+
It("fails to pass nil interface to func(int)", func() {
62+
actual, predicate := error(nil), func(int) bool { return false }
63+
success, err := Satisfy(predicate).Match(actual)
64+
Expect(success).To(BeFalse())
65+
Expect(err).To(HaveOccurred())
66+
Expect(err.Error()).To(ContainSubstring("expects 'int'"))
67+
Expect(err.Error()).To(ContainSubstring("have '<nil>'"))
68+
})
69+
70+
It("fails to pass nil interface to func(pointer)", func() {
71+
actual, predicate := error(nil), func(*string) bool { return false }
72+
success, err := Satisfy(predicate).Match(actual)
73+
Expect(success).To(BeFalse())
74+
Expect(err).To(HaveOccurred())
75+
Expect(err.Error()).To(ContainSubstring("expects '*string'"))
76+
Expect(err.Error()).To(ContainSubstring("have '<nil>'"))
77+
})
78+
})
79+
80+
It("works with positive cases", func() {
81+
Expect(2).To(Satisfy(isEven))
82+
83+
// transform expects interface
84+
takesError := func(error) bool { return true }
85+
Expect(nil).To(Satisfy(takesError), "handles nil actual values")
86+
Expect(errors.New("abc")).To(Satisfy(takesError))
87+
})
88+
89+
It("works with negative cases", func() {
90+
Expect(1).ToNot(Satisfy(isEven))
91+
})
92+
93+
Context("failure messages", func() {
94+
When("match fails", func() {
95+
It("gives a descriptive message", func() {
96+
m := Satisfy(isEven)
97+
Expect(m.Match(1)).To(BeFalse())
98+
Expect(m.FailureMessage(1)).To(ContainSubstring("Expected\n <int>: 1\nto satisfy predicate\n <func(int) bool>: "))
99+
})
100+
})
101+
102+
When("match succeeds, but expected it to fail", func() {
103+
It("gives a descriptive message", func() {
104+
m := Not(Satisfy(isEven))
105+
Expect(m.Match(2)).To(BeFalse())
106+
Expect(m.FailureMessage(2)).To(ContainSubstring("Expected\n <int>: 2\nto not satisfy predicate\n <func(int) bool>: "))
107+
})
108+
})
109+
110+
Context("actual value is incompatible with predicate's argument type", func() {
111+
It("gracefully fails", func() {
112+
m := Satisfy(isEven)
113+
result, err := m.Match("hi") // give it a string but predicate expects int; doesn't panic
114+
Expect(result).To(BeFalse())
115+
Expect(err).To(MatchError("predicate expects 'int' but we have 'string'"))
116+
})
117+
})
118+
})
119+
})

0 commit comments

Comments
 (0)
Please sign in to comment.