diff options
author | Joe Tsai <joetsai@digital-static.net> | 2019-12-16 09:05:41 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-12-16 09:05:41 -0800 |
commit | 340f1ebe299ef6712c79da23ad5bc6e3efad8250 (patch) | |
tree | 0b8618df2e9f563d358f9b4a0869cf3272310e7a /cmp/cmpopts | |
parent | 3838af334ff48ab62a68998c9a4ee9d847017617 (diff) | |
download | go-cmp-340f1ebe299ef6712c79da23ad5bc6e3efad8250.tar.gz |
Add EquateErrors helper (#178)
The EquateErrors helper equates errors according to errors.Is.
We also declare a sentinel AnyError value that matches any
non-nil error value.
This adds a dependency on golang.org/x/xerrors so that we can
continue to suppport go1.8, which is our current minimally
supported version of Go.
Fixes #89
Diffstat (limited to 'cmp/cmpopts')
-rw-r--r-- | cmp/cmpopts/equate.go | 34 | ||||
-rw-r--r-- | cmp/cmpopts/util_test.go | 164 |
2 files changed, 198 insertions, 0 deletions
diff --git a/cmp/cmpopts/equate.go b/cmp/cmpopts/equate.go index 343dcf9..e102849 100644 --- a/cmp/cmpopts/equate.go +++ b/cmp/cmpopts/equate.go @@ -11,6 +11,7 @@ import ( "time" "github.com/google/go-cmp/cmp" + "golang.org/x/xerrors" ) func equateAlways(_, _ interface{}) bool { return true } @@ -120,3 +121,36 @@ func (a timeApproximator) compare(x, y time.Time) bool { // Note: time.Time doesn't have AfterOrEqual method hence the negation. return !x.Add(a.margin).Before(y) } + +// AnyError is an error that matches any non-nil error. +var AnyError anyError + +type anyError struct{} + +func (anyError) Error() string { return "any error" } +func (anyError) Is(err error) bool { return err != nil } + +// EquateErrors returns a Comparer option that determines errors to be equal +// if errors.Is reports them to match. The AnyError error can be used to +// match any non-nil error. +func EquateErrors() cmp.Option { + return cmp.FilterValues(areConcreteErrors, cmp.Comparer(compareErrors)) +} + +// areConcreteErrors reports whether x and y are types that implement error. +// The input types are deliberately of the interface{} type rather than the +// error type so that we can handle situations where the current type is an +// interface{}, but the underlying concrete types both happen to implement +// the error interface. +func areConcreteErrors(x, y interface{}) bool { + _, ok1 := x.(error) + _, ok2 := y.(error) + return ok1 && ok2 +} + +func compareErrors(x, y interface{}) bool { + xe := x.(error) + ye := y.(error) + // TODO: Use errors.Is when go1.13 is the minimally supported version of Go. + return xerrors.Is(xe, ye) || xerrors.Is(ye, xe) +} diff --git a/cmp/cmpopts/util_test.go b/cmp/cmpopts/util_test.go index e80c963..d0fd888 100644 --- a/cmp/cmpopts/util_test.go +++ b/cmp/cmpopts/util_test.go @@ -6,6 +6,7 @@ package cmpopts import ( "bytes" + "errors" "fmt" "io" "math" @@ -16,6 +17,7 @@ import ( "time" "github.com/google/go-cmp/cmp" + "golang.org/x/xerrors" ) type ( @@ -514,6 +516,168 @@ func TestOptions(t *testing.T) { wantEqual: false, reason: "time difference overflows time.Duration", }, { + label: "EquateErrors", + x: nil, + y: nil, + opts: []cmp.Option{EquateErrors()}, + wantEqual: true, + reason: "nil values are equal", + }, { + label: "EquateErrors", + x: errors.New("EOF"), + y: io.EOF, + opts: []cmp.Option{EquateErrors()}, + wantEqual: false, + reason: "user-defined EOF is not exactly equal", + }, { + label: "EquateErrors", + x: xerrors.Errorf("wrapped: %w", io.EOF), + y: io.EOF, + opts: []cmp.Option{EquateErrors()}, + wantEqual: true, + reason: "wrapped io.EOF is equal according to errors.Is", + }, { + label: "EquateErrors", + x: xerrors.Errorf("wrapped: %w", io.EOF), + y: io.EOF, + wantEqual: false, + reason: "wrapped io.EOF is not equal without EquateErrors option", + }, { + label: "EquateErrors", + x: io.EOF, + y: io.EOF, + opts: []cmp.Option{EquateErrors()}, + wantEqual: true, + reason: "sentinel errors are equal", + }, { + label: "EquateErrors", + x: io.EOF, + y: AnyError, + opts: []cmp.Option{EquateErrors()}, + wantEqual: true, + reason: "AnyError is equal to any non-nil error", + }, { + label: "EquateErrors", + x: io.EOF, + y: AnyError, + wantEqual: false, + reason: "AnyError is not equal to any non-nil error without EquateErrors option", + }, { + label: "EquateErrors", + x: nil, + y: AnyError, + opts: []cmp.Option{EquateErrors()}, + wantEqual: false, + reason: "AnyError is not equal to nil value", + }, { + label: "EquateErrors", + x: nil, + y: nil, + opts: []cmp.Option{EquateErrors()}, + wantEqual: true, + reason: "nil values are equal", + }, { + label: "EquateErrors", + x: errors.New("EOF"), + y: io.EOF, + opts: []cmp.Option{EquateErrors()}, + wantEqual: false, + reason: "user-defined EOF is not exactly equal", + }, { + label: "EquateErrors", + x: xerrors.Errorf("wrapped: %w", io.EOF), + y: io.EOF, + opts: []cmp.Option{EquateErrors()}, + wantEqual: true, + reason: "wrapped io.EOF is equal according to errors.Is", + }, { + label: "EquateErrors", + x: xerrors.Errorf("wrapped: %w", io.EOF), + y: io.EOF, + wantEqual: false, + reason: "wrapped io.EOF is not equal without EquateErrors option", + }, { + label: "EquateErrors", + x: io.EOF, + y: io.EOF, + opts: []cmp.Option{EquateErrors()}, + wantEqual: true, + reason: "sentinel errors are equal", + }, { + label: "EquateErrors", + x: io.EOF, + y: AnyError, + opts: []cmp.Option{EquateErrors()}, + wantEqual: true, + reason: "AnyError is equal to any non-nil error", + }, { + label: "EquateErrors", + x: io.EOF, + y: AnyError, + wantEqual: false, + reason: "AnyError is not equal to any non-nil error without EquateErrors option", + }, { + label: "EquateErrors", + x: nil, + y: AnyError, + opts: []cmp.Option{EquateErrors()}, + wantEqual: false, + reason: "AnyError is not equal to nil value", + }, { + label: "EquateErrors", + x: struct{ E error }{nil}, + y: struct{ E error }{nil}, + opts: []cmp.Option{EquateErrors()}, + wantEqual: true, + reason: "nil values are equal", + }, { + label: "EquateErrors", + x: struct{ E error }{errors.New("EOF")}, + y: struct{ E error }{io.EOF}, + opts: []cmp.Option{EquateErrors()}, + wantEqual: false, + reason: "user-defined EOF is not exactly equal", + }, { + label: "EquateErrors", + x: struct{ E error }{xerrors.Errorf("wrapped: %w", io.EOF)}, + y: struct{ E error }{io.EOF}, + opts: []cmp.Option{EquateErrors()}, + wantEqual: true, + reason: "wrapped io.EOF is equal according to errors.Is", + }, { + label: "EquateErrors", + x: struct{ E error }{xerrors.Errorf("wrapped: %w", io.EOF)}, + y: struct{ E error }{io.EOF}, + wantEqual: false, + reason: "wrapped io.EOF is not equal without EquateErrors option", + }, { + label: "EquateErrors", + x: struct{ E error }{io.EOF}, + y: struct{ E error }{io.EOF}, + opts: []cmp.Option{EquateErrors()}, + wantEqual: true, + reason: "sentinel errors are equal", + }, { + label: "EquateErrors", + x: struct{ E error }{io.EOF}, + y: struct{ E error }{AnyError}, + opts: []cmp.Option{EquateErrors()}, + wantEqual: true, + reason: "AnyError is equal to any non-nil error", + }, { + label: "EquateErrors", + x: struct{ E error }{io.EOF}, + y: struct{ E error }{AnyError}, + wantEqual: false, + reason: "AnyError is not equal to any non-nil error without EquateErrors option", + }, { + label: "EquateErrors", + x: struct{ E error }{nil}, + y: struct{ E error }{AnyError}, + opts: []cmp.Option{EquateErrors()}, + wantEqual: false, + reason: "AnyError is not equal to nil value", + }, { label: "IgnoreFields", x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}}, y: Bar1{Foo3{&Foo2{&Foo1{Alpha: 6}}}}, |