Skip to content

Commit 2b39142

Browse files
committedOct 25, 2023
MatchError can now take an optional func(error) bool + description
1 parent ab6045c commit 2b39142

File tree

4 files changed

+112
-15
lines changed

4 files changed

+112
-15
lines changed
 

‎docs/index.md

+8-7
Original file line numberDiff line numberDiff line change
@@ -812,18 +812,19 @@ where `FUNCTION()` is a function call that returns an error-type as its *first o
812812
#### MatchError(expected interface{})
813813

814814
```go
815-
Ω(ACTUAL).Should(MatchError(EXPECTED))
815+
Ω(ACTUAL).Should(MatchError(EXPECTED, <FUNCTION_ERROR_DESCRIPTION>))
816816
```
817817

818818
succeeds if `ACTUAL` is a non-nil `error` that matches `EXPECTED`. `EXPECTED` must be one of the following:
819819

820-
- A string, in which case `ACTUAL.Error()` will be compared against `EXPECTED`.
821-
- A matcher, in which case `ACTUAL.Error()` is tested against the matcher.
822-
- An error, in which case any of the following is satisfied:
823-
- `errors.Is(ACTUAL, EXPECTED)` returns `true`
824-
- `ACTUAL` or any of the errors it wraps (directly or indirectly) equals `EXPECTED` in terms of `reflect.DeepEqual()`.
820+
- A string, in which case the matcher asserts that `ACTUAL.Error() == EXPECTED`
821+
- An error (i.e. anything satisfying Go's `error` interface). In which case the matcher:
822+
- First checks if `errors.Is(ACTUAL, EXPECTED)` returns `true`
823+
- If not, it checks if `ACTUAL` or any of the errors it wraps (directly or indirectly) equals `EXPECTED` via `reflect.DeepEqual()`.
824+
- A matcher, in which case `ACTUAL.Error()` is tested against the matcher, for example `Expect(err).Should(MatchError(ContainSubstring("sprocket not found")))` will pass if `err.Error()` has the substring "sprocke tnot found"
825+
- A function with signature `func(error) bool`. The matcher then passes if `f(ACTUAL)` returns `true`. If using a function in this way you are required to pass a `FUNCTION_ERROR_DESCRIPTION` argument to `MatchError` that describes the function. This description is used in the failure message. For example: `Expect(err).To(MatchError(os.IsNotExist, "IsNotExist))`
825826

826-
Any other type for `EXPECTED` is an error. It is also an error for `ACTUAL` to be nil.
827+
Any other type for `EXPECTED` is an error. It is also an error for `ACTUAL` to be nil. Note that `FUNCTION_ERROR_DESCRIPTION` is a description of the error function, if used. This is required when passing a function but is ignored in all other cases.
827828

828829
### Working with Channels
829830

‎matchers.go

+31-6
Original file line numberDiff line numberDiff line change
@@ -88,19 +88,44 @@ func Succeed() types.GomegaMatcher {
8888
}
8989

9090
// MatchError succeeds if actual is a non-nil error that matches the passed in
91-
// string, error, or matcher.
91+
// string, error, function, or matcher.
9292
//
9393
// These are valid use-cases:
9494
//
95-
// Expect(err).Should(MatchError("an error")) //asserts that err.Error() == "an error"
96-
// Expect(err).Should(MatchError(SomeError)) //asserts that err == SomeError (via reflect.DeepEqual)
97-
// Expect(err).Should(MatchError(ContainSubstring("sprocket not found"))) // asserts that err.Error() contains substring "sprocket not found"
95+
// When passed a string:
96+
//
97+
// Expect(err).To(MatchError("an error"))
98+
//
99+
// asserts that err.Error() == "an error"
100+
//
101+
// When passed an error:
102+
//
103+
// Expect(err).To(MatchError(SomeError))
104+
//
105+
// First checks if errors.Is(err, SomeError).
106+
// If that fails then it checks if reflect.DeepEqual(err, SomeError) repeatedly for err and any errors wrapped by err
107+
//
108+
// When passed a matcher:
109+
//
110+
// Expect(err).To(MatchError(ContainSubstring("sprocket not found")))
111+
//
112+
// the matcher is passed err.Error(). In this case it asserts that err.Error() contains substring "sprocket not found"
113+
//
114+
// When passed a func(err) bool and a description:
115+
//
116+
// Expect(err).To(MatchError(os.IsNotExist, "IsNotExist"))
117+
//
118+
// the function is passed err and matches if the return value is true. The description is required to allow Gomega
119+
// to print a useful error message.
98120
//
99121
// It is an error for err to be nil or an object that does not implement the
100122
// Error interface
101-
func MatchError(expected interface{}) types.GomegaMatcher {
123+
//
124+
// The optional second argument is a description of the error function, if used. This is required when passing a function but is ignored in all other cases.
125+
func MatchError(expected interface{}, functionErrorDescription ...any) types.GomegaMatcher {
102126
return &matchers.MatchErrorMatcher{
103-
Expected: expected,
127+
Expected: expected,
128+
FuncErrDescription: functionErrorDescription,
104129
}
105130
}
106131

‎matchers/match_error_matcher.go

+23-2
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,14 @@ import (
99
)
1010

1111
type MatchErrorMatcher struct {
12-
Expected interface{}
12+
Expected any
13+
FuncErrDescription []any
14+
isFunc bool
1315
}
1416

15-
func (matcher *MatchErrorMatcher) Match(actual interface{}) (success bool, err error) {
17+
func (matcher *MatchErrorMatcher) Match(actual any) (success bool, err error) {
18+
matcher.isFunc = false
19+
1620
if isNil(actual) {
1721
return false, fmt.Errorf("Expected an error, got nil")
1822
}
@@ -42,6 +46,17 @@ func (matcher *MatchErrorMatcher) Match(actual interface{}) (success bool, err e
4246
return actualErr.Error() == expected, nil
4347
}
4448

49+
v := reflect.ValueOf(expected)
50+
t := v.Type()
51+
errorInterface := reflect.TypeOf((*error)(nil)).Elem()
52+
if t.Kind() == reflect.Func && t.NumIn() == 1 && t.In(0).Implements(errorInterface) && t.NumOut() == 1 && t.Out(0).Kind() == reflect.Bool {
53+
if len(matcher.FuncErrDescription) == 0 {
54+
return false, fmt.Errorf("MatchError requires an additional description when passed a function")
55+
}
56+
matcher.isFunc = true
57+
return v.Call([]reflect.Value{reflect.ValueOf(actualErr)})[0].Bool(), nil
58+
}
59+
4560
var subMatcher omegaMatcher
4661
var hasSubMatcher bool
4762
if expected != nil {
@@ -57,9 +72,15 @@ func (matcher *MatchErrorMatcher) Match(actual interface{}) (success bool, err e
5772
}
5873

5974
func (matcher *MatchErrorMatcher) FailureMessage(actual interface{}) (message string) {
75+
if matcher.isFunc {
76+
return format.Message(actual, fmt.Sprintf("to match error function %s", matcher.FuncErrDescription[0]))
77+
}
6078
return format.Message(actual, "to match error", matcher.Expected)
6179
}
6280

6381
func (matcher *MatchErrorMatcher) NegatedFailureMessage(actual interface{}) (message string) {
82+
if matcher.isFunc {
83+
return format.Message(actual, fmt.Sprintf("not to match error function %s", matcher.FuncErrDescription[0]))
84+
}
6485
return format.Message(actual, "not to match error", matcher.Expected)
6586
}

‎matchers/match_error_matcher_test.go

+50
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,36 @@ var _ = Describe("MatchErrorMatcher", func() {
8585
})
8686
})
8787

88+
When("passed a function that takes error and returns bool", func() {
89+
var IsFooError = func(err error) bool {
90+
return err.Error() == "foo"
91+
}
92+
93+
It("requires an additional description", func() {
94+
_, err := (&MatchErrorMatcher{
95+
Expected: IsFooError,
96+
}).Match(errors.New("foo"))
97+
Expect(err).Should(MatchError("MatchError requires an additional description when passed a function"))
98+
})
99+
100+
It("matches iff the function returns true", func() {
101+
Ω(errors.New("foo")).Should(MatchError(IsFooError, "FooError"))
102+
Ω(errors.New("fooo")).ShouldNot(MatchError(IsFooError, "FooError"))
103+
})
104+
105+
It("uses the error description to construct its message", func() {
106+
failuresMessages := InterceptGomegaFailures(func() {
107+
Ω(errors.New("fooo")).Should(MatchError(IsFooError, "FooError"))
108+
})
109+
Ω(failuresMessages[0]).Should(ContainSubstring("fooo\n {s: \"fooo\"}\nto match error function FooError"))
110+
111+
failuresMessages = InterceptGomegaFailures(func() {
112+
Ω(errors.New("foo")).ShouldNot(MatchError(IsFooError, "FooError"))
113+
})
114+
Ω(failuresMessages[0]).Should(ContainSubstring("foo\n {s: \"foo\"}\nnot to match error function FooError"))
115+
})
116+
})
117+
88118
It("should fail when passed anything else", func() {
89119
actualErr := errors.New("an error")
90120
_, err := (&MatchErrorMatcher{
@@ -96,6 +126,26 @@ var _ = Describe("MatchErrorMatcher", func() {
96126
Expected: 3,
97127
}).Match(actualErr)
98128
Expect(err).Should(HaveOccurred())
129+
130+
_, err = (&MatchErrorMatcher{
131+
Expected: func(e error) {},
132+
}).Match(actualErr)
133+
Expect(err).Should(HaveOccurred())
134+
135+
_, err = (&MatchErrorMatcher{
136+
Expected: func() bool { return false },
137+
}).Match(actualErr)
138+
Expect(err).Should(HaveOccurred())
139+
140+
_, err = (&MatchErrorMatcher{
141+
Expected: func() {},
142+
}).Match(actualErr)
143+
Expect(err).Should(HaveOccurred())
144+
145+
_, err = (&MatchErrorMatcher{
146+
Expected: func(e error, a string) (bool, error) { return false, nil },
147+
}).Match(actualErr)
148+
Expect(err).Should(HaveOccurred())
99149
})
100150
})
101151

0 commit comments

Comments
 (0)
Please sign in to comment.