Merge pull request #108 from jim-slattery-rs/composition
Added Add, Or, Not, WithTransform matchers for composability.
diff --git a/internal/asyncassertion/async_assertion.go b/internal/asyncassertion/async_assertion.go
index 7bbec43..b007383 100644
--- a/internal/asyncassertion/async_assertion.go
+++ b/internal/asyncassertion/async_assertion.go
@@ -95,6 +95,12 @@
return true
}
+ return MatchMayChangeInTheFuture(matcher, value)
+}
+
+//MatchMayChangeInTheFuture is a helper to call MatchMayChangeInTheFuture on an unknown matcher.
+//If matcher implements oracleMatcher, it will call the method. Otherwise just returns true.
+func MatchMayChangeInTheFuture(matcher types.GomegaMatcher, value interface{}) bool {
oracleMatcher, ok := matcher.(oracleMatcher)
if !ok {
return true
diff --git a/matchers.go b/matchers.go
index 3b39677..b6110c4 100644
--- a/matchers.go
+++ b/matchers.go
@@ -343,3 +343,51 @@
func BeADirectory() types.GomegaMatcher {
return &matchers.BeADirectoryMatcher{}
}
+
+//And succeeds only if all of the given matchers succeed.
+//The matchers are tried in order, and will fail-fast if one doesn't succeed.
+// Expect("hi").To(And(HaveLen(2), Equal("hi"))
+//
+//And(), Or(), Not() and WithTransform() allow matchers to be composed into complex expressions.
+func And(ms ...types.GomegaMatcher) types.GomegaMatcher {
+ return &matchers.AndMatcher{Matchers: ms}
+}
+
+//SatisfyAll is an alias for And().
+// Ω("hi").Should(SatisfyAll(HaveLen(2), Equal("hi")))
+func SatisfyAll(matchers ...types.GomegaMatcher) types.GomegaMatcher {
+ return And(matchers...)
+}
+
+//Or succeeds if any of the given matchers succeed.
+//The matchers are tried in order and will return immediately upon the first successful match.
+// Expect("hi").To(Or(HaveLen(3), HaveLen(2))
+//
+//And(), Or(), Not() and WithTransform() allow matchers to be composed into complex expressions.
+func Or(ms ...types.GomegaMatcher) types.GomegaMatcher {
+ return &matchers.OrMatcher{Matchers: ms}
+}
+
+//SatisfyAny is an alias for Or().
+// Expect("hi").SatisfyAny(Or(HaveLen(3), HaveLen(2))
+func SatisfyAny(matchers ...types.GomegaMatcher) types.GomegaMatcher {
+ return Or(matchers...)
+}
+
+//Not negates the given matcher; it succeeds if the given matcher fails.
+// Expect(1).To(Not(Equal(2))
+//
+//And(), Or(), Not() and WithTransform() allow matchers to be composed into complex expressions.
+func Not(matcher types.GomegaMatcher) types.GomegaMatcher {
+ return &matchers.NotMatcher{Matcher: matcher}
+}
+
+//WithTransform applies the `transform` to the actual value and matches it against `matcher`.
+//The given transform must be a function of one parameter that returns one value.
+// var plus1 = func(i int) int { return i + 1 }
+// Expect(1).To(WithTransform(plus1, Equal(2))
+//
+//And(), Or(), Not() and WithTransform() allow matchers to be composed into complex expressions.
+func WithTransform(transform interface{}, matcher types.GomegaMatcher) types.GomegaMatcher {
+ return matchers.NewWithTransformMatcher(transform, matcher)
+}
diff --git a/matchers/and.go b/matchers/and.go
new file mode 100644
index 0000000..5257b76
--- /dev/null
+++ b/matchers/and.go
@@ -0,0 +1,63 @@
+package matchers
+
+import (
+ "fmt"
+ "github.com/onsi/gomega/format"
+ "github.com/onsi/gomega/internal/asyncassertion"
+ "github.com/onsi/gomega/types"
+)
+
+type AndMatcher struct {
+ Matchers []types.GomegaMatcher
+
+ // state
+ firstFailedMatcher types.GomegaMatcher
+}
+
+func (m *AndMatcher) Match(actual interface{}) (success bool, err error) {
+ m.firstFailedMatcher = nil
+ for _, matcher := range m.Matchers {
+ success, err := matcher.Match(actual)
+ if !success || err != nil {
+ m.firstFailedMatcher = matcher
+ return false, err
+ }
+ }
+ return true, nil
+}
+
+func (m *AndMatcher) FailureMessage(actual interface{}) (message string) {
+ return m.firstFailedMatcher.FailureMessage(actual)
+}
+
+func (m *AndMatcher) NegatedFailureMessage(actual interface{}) (message string) {
+ // not the most beautiful list of matchers, but not bad either...
+ return format.Message(actual, fmt.Sprintf("To not satisfy all of these matchers: %s", m.Matchers))
+}
+
+func (m *AndMatcher) MatchMayChangeInTheFuture(actual interface{}) bool {
+ /*
+ Example with 3 matchers: A, B, C
+
+ Match evaluates them: T, F, <?> => F
+ So match is currently F, what should MatchMayChangeInTheFuture() return?
+ Seems like it only depends on B, since currently B MUST change to allow the result to become T
+
+ Match eval: T, T, T => T
+ So match is currently T, what should MatchMayChangeInTheFuture() return?
+ Seems to depend on ANY of them being able to change to F.
+ */
+
+ if m.firstFailedMatcher == nil {
+ // so all matchers succeeded.. Any one of them changing would change the result.
+ for _, matcher := range m.Matchers {
+ if asyncassertion.MatchMayChangeInTheFuture(matcher, actual) {
+ return true
+ }
+ }
+ return false // none of were going to change
+ } else {
+ // one of the matchers failed.. it must be able to change in order to affect the result
+ return asyncassertion.MatchMayChangeInTheFuture(m.firstFailedMatcher, actual)
+ }
+}
diff --git a/matchers/and_test.go b/matchers/and_test.go
new file mode 100644
index 0000000..acf778c
--- /dev/null
+++ b/matchers/and_test.go
@@ -0,0 +1,103 @@
+package matchers_test
+
+import (
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+ . "github.com/onsi/gomega/matchers"
+ "github.com/onsi/gomega/types"
+)
+
+// sample data
+var (
+ // example input
+ input = "hi"
+ // some matchers that succeed against the input
+ true1 = HaveLen(2)
+ true2 = Equal("hi")
+ true3 = MatchRegexp("hi")
+ // some matchers that fail against the input.
+ false1 = HaveLen(1)
+ false2 = Equal("hip")
+ false3 = MatchRegexp("hope")
+)
+
+// verifyFailureMessage expects the matcher to fail with the given input, and verifies the failure message.
+func verifyFailureMessage(m types.GomegaMatcher, input string, expectedFailureMsgFragment string) {
+ Expect(m.Match(input)).To(BeFalse())
+ Expect(m.FailureMessage(input)).To(Equal(
+ "Expected\n <string>: " + input + "\n" + expectedFailureMsgFragment))
+}
+
+var _ = Describe("AndMatcher", func() {
+ It("works with positive cases", func() {
+ Expect(input).To(And())
+ Expect(input).To(And(true1))
+ Expect(input).To(And(true1, true2))
+ Expect(input).To(And(true1, true2, true3))
+
+ // use alias
+ Expect(input).To(SatisfyAll(true1, true2, true3))
+ })
+
+ It("works with negative cases", func() {
+ Expect(input).ToNot(And(false1, false2))
+ Expect(input).ToNot(And(true1, true2, false3))
+ Expect(input).ToNot(And(true1, false2, false3))
+ Expect(input).ToNot(And(false1, true1, true2))
+ })
+
+ Context("failure messages", func() {
+ Context("when match fails", func() {
+ It("gives a descriptive message", func() {
+ verifyFailureMessage(And(false1, true1), input, "to have length 1")
+ verifyFailureMessage(And(true1, false2), input, "to equal\n <string>: hip")
+ verifyFailureMessage(And(true1, true2, false3), input, "to match regular expression\n <string>: hope")
+ })
+ })
+
+ Context("when match succeeds, but expected it to fail", func() {
+ It("gives a descriptive message", func() {
+ verifyFailureMessage(Not(And(true1, true2)), input,
+ `To not satisfy all of these matchers: [%!s(*matchers.HaveLenMatcher=&{2}) %!s(*matchers.EqualMatcher=&{hi})]`)
+ })
+ })
+ })
+
+ Context("MatchMayChangeInTheFuture", func() {
+ Context("Match returned false", func() {
+ Context("returns value of the failed matcher", func() {
+ It("false if failed matcher not going to change", func() {
+ // 3 matchers: 1st returns true, 2nd returns false and is not going to change, 3rd is never called
+ m := And(Not(BeNil()), Or(), Equal(1))
+ Expect(m.Match("hi")).To(BeFalse())
+ Expect(m.(*AndMatcher).MatchMayChangeInTheFuture("hi")).To(BeFalse()) // empty Or() indicates not going to change
+ })
+ It("true if failed matcher indicates it might change", func() {
+ // 3 matchers: 1st returns true, 2nd returns false and "might" change, 3rd is never called
+ m := And(Not(BeNil()), Equal(5), Equal(1))
+ Expect(m.Match("hi")).To(BeFalse())
+ Expect(m.(*AndMatcher).MatchMayChangeInTheFuture("hi")).To(BeTrue()) // Equal(5) indicates it might change
+ })
+ })
+ })
+ Context("Match returned true", func() {
+ It("returns true if any of the matchers could change", func() {
+ // 3 matchers, all return true, and all could change
+ m := And(Not(BeNil()), Equal("hi"), HaveLen(2))
+ Expect(m.Match("hi")).To(BeTrue())
+ Expect(m.(*AndMatcher).MatchMayChangeInTheFuture("hi")).To(BeTrue()) // all 3 of these matchers default to 'true'
+ })
+ It("returns false if none of the matchers could change", func() {
+ // empty And() has the property of always matching, and never can change since there are no sub-matchers that could change
+ m := And()
+ Expect(m.Match("anything")).To(BeTrue())
+ Expect(m.(*AndMatcher).MatchMayChangeInTheFuture("anything")).To(BeFalse())
+
+ // And() with 3 sub-matchers that return true, and can't change
+ m = And(And(), And(), And())
+ Expect(m.Match("hi")).To(BeTrue())
+ Expect(m.(*AndMatcher).MatchMayChangeInTheFuture("hi")).To(BeFalse()) // the 3 empty And()'s won't change
+ })
+ })
+ })
+})
diff --git a/matchers/not.go b/matchers/not.go
new file mode 100644
index 0000000..6aed858
--- /dev/null
+++ b/matchers/not.go
@@ -0,0 +1,30 @@
+package matchers
+
+import (
+ "github.com/onsi/gomega/internal/asyncassertion"
+ "github.com/onsi/gomega/types"
+)
+
+type NotMatcher struct {
+ Matcher types.GomegaMatcher
+}
+
+func (m *NotMatcher) Match(actual interface{}) (bool, error) {
+ success, err := m.Matcher.Match(actual)
+ if err != nil {
+ return false, err
+ }
+ return !success, nil
+}
+
+func (m *NotMatcher) FailureMessage(actual interface{}) (message string) {
+ return m.Matcher.NegatedFailureMessage(actual) // works beautifully
+}
+
+func (m *NotMatcher) NegatedFailureMessage(actual interface{}) (message string) {
+ return m.Matcher.FailureMessage(actual) // works beautifully
+}
+
+func (m *NotMatcher) MatchMayChangeInTheFuture(actual interface{}) bool {
+ return asyncassertion.MatchMayChangeInTheFuture(m.Matcher, actual) // just return m.Matcher's value
+}
diff --git a/matchers/not_test.go b/matchers/not_test.go
new file mode 100644
index 0000000..b3c1fdb
--- /dev/null
+++ b/matchers/not_test.go
@@ -0,0 +1,57 @@
+package matchers_test
+
+import (
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+ . "github.com/onsi/gomega/matchers"
+)
+
+var _ = Describe("NotMatcher", func() {
+ Context("basic examples", func() {
+ It("works", func() {
+ Expect(input).To(Not(false1))
+ Expect(input).To(Not(Not(true2)))
+ Expect(input).ToNot(Not(true3))
+ Expect(input).ToNot(Not(Not(false1)))
+ Expect(input).To(Not(Not(Not(false2))))
+ })
+ })
+
+ Context("De Morgan's laws", func() {
+ It("~(A && B) == ~A || ~B", func() {
+ Expect(input).To(Not(And(false1, false2)))
+ Expect(input).To(Or(Not(false1), Not(false2)))
+ })
+ It("~(A || B) == ~A && ~B", func() {
+ Expect(input).To(Not(Or(false1, false2)))
+ Expect(input).To(And(Not(false1), Not(false2)))
+ })
+ })
+
+ Context("failure messages are opposite of original matchers' failure messages", func() {
+ Context("when match fails", func() {
+ It("gives a descriptive message", func() {
+ verifyFailureMessage(Not(HaveLen(2)), input, "not to have length 2")
+ })
+ })
+
+ Context("when match succeeds, but expected it to fail", func() {
+ It("gives a descriptive message", func() {
+ verifyFailureMessage(Not(Not(HaveLen(3))), input, "to have length 3")
+ })
+ })
+ })
+
+ Context("MatchMayChangeInTheFuture()", func() {
+ It("Propagates value from wrapped matcher", func() {
+ m := Not(Or()) // an empty Or() always returns false, and indicates it cannot change
+ Expect(m.Match("anything")).To(BeTrue())
+ Expect(m.(*NotMatcher).MatchMayChangeInTheFuture("anything")).To(BeFalse())
+ })
+ It("Defaults to true", func() {
+ m := Not(Equal(1)) // Equal does not have this method
+ Expect(m.Match(2)).To(BeTrue())
+ Expect(m.(*NotMatcher).MatchMayChangeInTheFuture(2)).To(BeTrue()) // defaults to true
+ })
+ })
+})
diff --git a/matchers/or.go b/matchers/or.go
new file mode 100644
index 0000000..29ad5c6
--- /dev/null
+++ b/matchers/or.go
@@ -0,0 +1,66 @@
+package matchers
+
+import (
+ "fmt"
+ "github.com/onsi/gomega/format"
+ "github.com/onsi/gomega/internal/asyncassertion"
+ "github.com/onsi/gomega/types"
+)
+
+type OrMatcher struct {
+ Matchers []types.GomegaMatcher
+
+ // state
+ firstSuccessfulMatcher types.GomegaMatcher
+}
+
+func (m *OrMatcher) Match(actual interface{}) (success bool, err error) {
+ m.firstSuccessfulMatcher = nil
+ for _, matcher := range m.Matchers {
+ success, err := matcher.Match(actual)
+ if err != nil {
+ return false, err
+ }
+ if success {
+ m.firstSuccessfulMatcher = matcher
+ return true, nil
+ }
+ }
+ return false, nil
+}
+
+func (m *OrMatcher) FailureMessage(actual interface{}) (message string) {
+ // not the most beautiful list of matchers, but not bad either...
+ return format.Message(actual, fmt.Sprintf("To satisfy at least one of these matchers: %s", m.Matchers))
+}
+
+func (m *OrMatcher) NegatedFailureMessage(actual interface{}) (message string) {
+ return m.firstSuccessfulMatcher.NegatedFailureMessage(actual)
+}
+
+func (m *OrMatcher) MatchMayChangeInTheFuture(actual interface{}) bool {
+ /*
+ Example with 3 matchers: A, B, C
+
+ Match evaluates them: F, T, <?> => T
+ So match is currently T, what should MatchMayChangeInTheFuture() return?
+ Seems like it only depends on B, since currently B MUST change to allow the result to become F
+
+ Match eval: F, F, F => F
+ So match is currently F, what should MatchMayChangeInTheFuture() return?
+ Seems to depend on ANY of them being able to change to T.
+ */
+
+ if m.firstSuccessfulMatcher != nil {
+ // one of the matchers succeeded.. it must be able to change in order to affect the result
+ return asyncassertion.MatchMayChangeInTheFuture(m.firstSuccessfulMatcher, actual)
+ } else {
+ // so all matchers failed.. Any one of them changing would change the result.
+ for _, matcher := range m.Matchers {
+ if asyncassertion.MatchMayChangeInTheFuture(matcher, actual) {
+ return true
+ }
+ }
+ return false // none of were going to change
+ }
+}
diff --git a/matchers/or_test.go b/matchers/or_test.go
new file mode 100644
index 0000000..9589a17
--- /dev/null
+++ b/matchers/or_test.go
@@ -0,0 +1,85 @@
+package matchers_test
+
+import (
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+ . "github.com/onsi/gomega/matchers"
+)
+
+var _ = Describe("OrMatcher", func() {
+ It("works with positive cases", func() {
+ Expect(input).To(Or(true1))
+ Expect(input).To(Or(true1, true2))
+ Expect(input).To(Or(true1, false1))
+ Expect(input).To(Or(false1, true2))
+ Expect(input).To(Or(true1, true2, true3))
+ Expect(input).To(Or(true1, true2, false3))
+ Expect(input).To(Or(true1, false2, true3))
+ Expect(input).To(Or(false1, true2, true3))
+ Expect(input).To(Or(true1, false2, false3))
+ Expect(input).To(Or(false1, false2, true3))
+
+ // use alias
+ Expect(input).To(SatisfyAny(false1, false2, true3))
+ })
+
+ It("works with negative cases", func() {
+ Expect(input).ToNot(Or())
+ Expect(input).ToNot(Or(false1))
+ Expect(input).ToNot(Or(false1, false2))
+ Expect(input).ToNot(Or(false1, false2, false3))
+ })
+
+ Context("failure messages", func() {
+ Context("when match fails", func() {
+ It("gives a descriptive message", func() {
+ verifyFailureMessage(Or(false1, false2), input,
+ "To satisfy at least one of these matchers: [%!s(*matchers.HaveLenMatcher=&{1}) %!s(*matchers.EqualMatcher=&{hip})]")
+ })
+ })
+
+ Context("when match succeeds, but expected it to fail", func() {
+ It("gives a descriptive message", func() {
+ verifyFailureMessage(Not(Or(true1, true2)), input, `not to have length 2`)
+ })
+ })
+ })
+
+ Context("MatchMayChangeInTheFuture", func() {
+ Context("Match returned false", func() {
+ It("returns true if any of the matchers could change", func() {
+ // 3 matchers, all return false, and all could change
+ m := Or(BeNil(), Equal("hip"), HaveLen(1))
+ Expect(m.Match("hi")).To(BeFalse())
+ Expect(m.(*OrMatcher).MatchMayChangeInTheFuture("hi")).To(BeTrue()) // all 3 of these matchers default to 'true'
+ })
+ It("returns false if none of the matchers could change", func() {
+ // empty Or() has the property of never matching, and never can change since there are no sub-matchers that could change
+ m := Or()
+ Expect(m.Match("anything")).To(BeFalse())
+ Expect(m.(*OrMatcher).MatchMayChangeInTheFuture("anything")).To(BeFalse())
+
+ // Or() with 3 sub-matchers that return false, and can't change
+ m = Or(Or(), Or(), Or())
+ Expect(m.Match("hi")).To(BeFalse())
+ Expect(m.(*OrMatcher).MatchMayChangeInTheFuture("hi")).To(BeFalse()) // the 3 empty Or()'s won't change
+ })
+ })
+ Context("Match returned true", func() {
+ Context("returns value of the successful matcher", func() {
+ It("false if successful matcher not going to change", func() {
+ // 3 matchers: 1st returns false, 2nd returns true and is not going to change, 3rd is never called
+ m := Or(BeNil(), And(), Equal(1))
+ Expect(m.Match("hi")).To(BeTrue())
+ Expect(m.(*OrMatcher).MatchMayChangeInTheFuture("hi")).To(BeFalse())
+ })
+ It("true if successful matcher indicates it might change", func() {
+ // 3 matchers: 1st returns false, 2nd returns true and "might" change, 3rd is never called
+ m := Or(Not(BeNil()), Equal("hi"), Equal(1))
+ Expect(m.Match("hi")).To(BeTrue())
+ Expect(m.(*OrMatcher).MatchMayChangeInTheFuture("hi")).To(BeTrue()) // Equal("hi") indicates it might change
+ })
+ })
+ })
+ })
+})
diff --git a/matchers/with_transform.go b/matchers/with_transform.go
new file mode 100644
index 0000000..21d9fe0
--- /dev/null
+++ b/matchers/with_transform.go
@@ -0,0 +1,71 @@
+package matchers
+
+import (
+ "fmt"
+ "github.com/onsi/gomega/internal/asyncassertion"
+ "github.com/onsi/gomega/types"
+ "reflect"
+)
+
+type WithTransformMatcher struct {
+ // input
+ Transform interface{} // must be a function of one parameter that returns one value
+ Matcher types.GomegaMatcher
+
+ // cached value
+ transformArgType reflect.Type
+
+ // state
+ transformedValue interface{}
+}
+
+func NewWithTransformMatcher(transform interface{}, matcher types.GomegaMatcher) *WithTransformMatcher {
+ if transform == nil {
+ panic("transform function cannot be nil")
+ }
+ txType := reflect.TypeOf(transform)
+ if txType.NumIn() != 1 {
+ panic("transform function must have 1 argument")
+ }
+ if txType.NumOut() != 1 {
+ panic("transform function must have 1 return value")
+ }
+
+ return &WithTransformMatcher{
+ Transform: transform,
+ Matcher: matcher,
+ transformArgType: reflect.TypeOf(transform).In(0),
+ }
+}
+
+func (m *WithTransformMatcher) Match(actual interface{}) (bool, error) {
+ // return error if actual's type is incompatible with Transform function's argument type
+ actualType := reflect.TypeOf(actual)
+ if !actualType.AssignableTo(m.transformArgType) {
+ return false, fmt.Errorf("Transform function expects '%s' but we have '%s'", m.transformArgType, actualType)
+ }
+
+ // call the Transform function with `actual`
+ fn := reflect.ValueOf(m.Transform)
+ result := fn.Call([]reflect.Value{reflect.ValueOf(actual)})
+ m.transformedValue = result[0].Interface() // expect exactly one value
+
+ return m.Matcher.Match(m.transformedValue)
+}
+
+func (m *WithTransformMatcher) FailureMessage(_ interface{}) (message string) {
+ return m.Matcher.FailureMessage(m.transformedValue)
+}
+
+func (m *WithTransformMatcher) NegatedFailureMessage(_ interface{}) (message string) {
+ return m.Matcher.NegatedFailureMessage(m.transformedValue)
+}
+
+func (m *WithTransformMatcher) MatchMayChangeInTheFuture(_ interface{}) bool {
+ // TODO: Maybe this should always just return true? (Only an issue for non-deterministic transformers.)
+ //
+ // Querying the next matcher is fine if the transformer always will return the same value.
+ // But if the transformer is non-deterministic and returns a different value each time, then there
+ // is no point in querying the next matcher, since it can only comment on the last transformed value.
+ return asyncassertion.MatchMayChangeInTheFuture(m.Matcher, m.transformedValue)
+}
diff --git a/matchers/with_transform_test.go b/matchers/with_transform_test.go
new file mode 100644
index 0000000..2824df9
--- /dev/null
+++ b/matchers/with_transform_test.go
@@ -0,0 +1,101 @@
+package matchers_test
+
+import (
+ "errors"
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+ . "github.com/onsi/gomega/matchers"
+)
+
+var _ = Describe("WithTransformMatcher", func() {
+
+ var plus1 = func(i int) int { return i + 1 }
+
+ Context("Panic if transform function invalid", func() {
+ panicsWithTransformer := func(transform interface{}) {
+ ExpectWithOffset(1, func() { WithTransform(transform, nil) }).To(Panic())
+ }
+ It("nil", func() {
+ panicsWithTransformer(nil)
+ })
+ Context("Invalid number of args, but correct return value count", func() {
+ It("zero", func() {
+ panicsWithTransformer(func() int { return 5 })
+ })
+ It("two", func() {
+ panicsWithTransformer(func(i, j int) int { return 5 })
+ })
+ })
+ Context("Invalid number of return values, but correct number of arguments", func() {
+ It("zero", func() {
+ panicsWithTransformer(func(i int) {})
+ })
+ It("two", func() {
+ panicsWithTransformer(func(i int) (int, int) { return 5, 6 })
+ })
+ })
+ })
+
+ It("works with positive cases", func() {
+ Expect(1).To(WithTransform(plus1, Equal(2)))
+ Expect(1).To(WithTransform(plus1, WithTransform(plus1, Equal(3))))
+ Expect(1).To(WithTransform(plus1, And(Equal(2), BeNumerically(">", 1))))
+
+ // transform expects custom type
+ type S struct {
+ A int
+ B string
+ }
+ transformer := func(s S) string { return s.B }
+ Expect(S{1, "hi"}).To(WithTransform(transformer, Equal("hi")))
+
+ // transform expects interface
+ errString := func(e error) string { return e.Error() }
+ Expect(errors.New("abc")).To(WithTransform(errString, Equal("abc")))
+ })
+
+ It("works with negative cases", func() {
+ Expect(1).ToNot(WithTransform(plus1, Equal(3)))
+ Expect(1).ToNot(WithTransform(plus1, WithTransform(plus1, Equal(2))))
+ })
+
+ Context("failure messages", func() {
+ Context("when match fails", func() {
+ It("gives a descriptive message", func() {
+ m := WithTransform(plus1, Equal(3))
+ Expect(m.Match(1)).To(BeFalse())
+ Expect(m.FailureMessage(1)).To(Equal("Expected\n <int>: 2\nto equal\n <int>: 3"))
+ })
+ })
+
+ Context("when match succeeds, but expected it to fail", func() {
+ It("gives a descriptive message", func() {
+ m := Not(WithTransform(plus1, Equal(3)))
+ Expect(m.Match(2)).To(BeFalse())
+ Expect(m.FailureMessage(2)).To(Equal("Expected\n <int>: 3\nnot to equal\n <int>: 3"))
+ })
+ })
+
+ Context("actual value is incompatible with transform function's argument type", func() {
+ It("gracefully fails if transform cannot be performed", func() {
+ m := WithTransform(plus1, Equal(3))
+ result, err := m.Match("hi") // give it a string but transform expects int; doesn't panic
+ Expect(result).To(BeFalse())
+ Expect(err).To(MatchError("Transform function expects 'int' but we have 'string'"))
+ })
+ })
+ })
+
+ Context("MatchMayChangeInTheFuture()", func() {
+ It("Propagates value from wrapped matcher on the transformed value", func() {
+ m := WithTransform(plus1, Or()) // empty Or() always returns false, and indicates it cannot change
+ Expect(m.Match(1)).To(BeFalse())
+ Expect(m.(*WithTransformMatcher).MatchMayChangeInTheFuture(1)).To(BeFalse()) // empty Or() indicates cannot change
+ })
+ It("Defaults to true", func() {
+ m := WithTransform(plus1, Equal(2)) // Equal does not have this method
+ Expect(m.Match(1)).To(BeTrue())
+ Expect(m.(*WithTransformMatcher).MatchMayChangeInTheFuture(1)).To(BeTrue()) // defaults to true
+ })
+ })
+})