From 0a981cb98cafcc01476fe7a37f2b5e7a3ab5b46b Mon Sep 17 00:00:00 2001 From: David Ansari Date: Tue, 5 Nov 2019 17:44:11 +0000 Subject: [PATCH] Support wrapped errors (#359) Add xerrors.Is() and xerrors.As() to MatchError() Resolves #334 --- go.mod | 1 + go.sum | 2 ++ matchers/match_error_matcher.go | 36 ++++++++++++++++++++++------ matchers/match_error_matcher_test.go | 35 ++++++++++++++++++++------- 4 files changed, 59 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index 177a541c4..1eb0dfa68 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f // indirect golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e // indirect golang.org/x/text v0.3.0 // indirect + golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 gopkg.in/fsnotify.v1 v1.4.7 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v2 v2.2.4 diff --git a/go.sum b/go.sum index bbcc05d3e..b872e8a0d 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,8 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUk golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= diff --git a/matchers/match_error_matcher.go b/matchers/match_error_matcher.go index 07499ac95..a41eff8dd 100644 --- a/matchers/match_error_matcher.go +++ b/matchers/match_error_matcher.go @@ -5,6 +5,7 @@ import ( "reflect" "github.com/onsi/gomega/format" + "golang.org/x/xerrors" ) type MatchErrorMatcher struct { @@ -21,25 +22,32 @@ func (matcher *MatchErrorMatcher) Match(actual interface{}) (success bool, err e } actualErr := actual.(error) + expected := matcher.Expected - if isError(matcher.Expected) { - return reflect.DeepEqual(actualErr, matcher.Expected), nil + if isPtrToErrorType(expected) { + return xerrors.As(actualErr, expected), nil } - if isString(matcher.Expected) { - return actualErr.Error() == matcher.Expected, nil + if isError(expected) { + return reflect.DeepEqual(actualErr, expected) || xerrors.Is(actualErr, expected.(error)), nil + } + + if isString(expected) { + return actualErr.Error() == expected, nil } var subMatcher omegaMatcher var hasSubMatcher bool - if matcher.Expected != nil { - subMatcher, hasSubMatcher = (matcher.Expected).(omegaMatcher) + if expected != nil { + subMatcher, hasSubMatcher = (expected).(omegaMatcher) if hasSubMatcher { return subMatcher.Match(actualErr.Error()) } } - return false, fmt.Errorf("MatchError must be passed an error, string, or Matcher that can match on strings. Got:\n%s", format.Object(matcher.Expected, 1)) + return false, fmt.Errorf( + "MatchError must be passed an error, a pointer to a type that implements error, a string, or a Matcher that can match on strings. Got:\n%s", + format.Object(expected, 1)) } func (matcher *MatchErrorMatcher) FailureMessage(actual interface{}) (message string) { @@ -49,3 +57,17 @@ func (matcher *MatchErrorMatcher) FailureMessage(actual interface{}) (message st func (matcher *MatchErrorMatcher) NegatedFailureMessage(actual interface{}) (message string) { return format.Message(actual, "not to match error", matcher.Expected) } + +func isPtrToErrorType(a interface{}) bool { + if isNil(a) { + return false + } + + typeOfA := reflect.TypeOf(a) + if typeOfA.Kind() != reflect.Ptr { + return false + } + + errorType := reflect.TypeOf((*error)(nil)).Elem() + return typeOfA.Elem().Implements(errorType) +} diff --git a/matchers/match_error_matcher_test.go b/matchers/match_error_matcher_test.go index 4aba5e038..a0fccfd83 100644 --- a/matchers/match_error_matcher_test.go +++ b/matchers/match_error_matcher_test.go @@ -7,6 +7,7 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" . "github.com/onsi/gomega/matchers" + "golang.org/x/xerrors" ) type CustomError struct { @@ -18,16 +19,34 @@ func (c CustomError) Error() string { var _ = Describe("MatchErrorMatcher", func() { Context("When asserting against an error", func() { - It("should succeed when matching with an error", func() { - err := errors.New("an error") - fmtErr := fmt.Errorf("an error") - customErr := CustomError{} + Context("when passed an error", func() { + It("should succeed when errors are deeply equal", func() { + err := errors.New("an error") + fmtErr := fmt.Errorf("an error") + customErr := CustomError{} + + Expect(err).Should(MatchError(errors.New("an error"))) + Expect(err).ShouldNot(MatchError(errors.New("another error"))) + + Expect(fmtErr).Should(MatchError(errors.New("an error"))) + Expect(customErr).Should(MatchError(CustomError{})) + }) - Expect(err).Should(MatchError(errors.New("an error"))) - Expect(err).ShouldNot(MatchError(errors.New("another error"))) + It("should succeed when any error in the chain matches the passed error", func() { + innerErr := errors.New("inner error") + outerErr := xerrors.Errorf("outer error wrapping: %w", innerErr) - Expect(fmtErr).Should(MatchError(errors.New("an error"))) - Expect(customErr).Should(MatchError(CustomError{})) + Expect(outerErr).Should(MatchError(innerErr)) + }) + }) + + Context("when passed non-nil pointer to a type that implements error", func() { + It("should succeed when any error in the chain matches the passed pointer", func() { + err := xerrors.Errorf("outer error wrapping: %w", CustomError{}) + + var expectedErrType CustomError + Expect(err).Should(MatchError(&expectedErrType)) + }) }) It("should succeed when matching with a string", func() {