aboutsummaryrefslogtreecommitdiff
path: root/cmp/cmpopts
diff options
context:
space:
mode:
authorJoe Tsai <joetsai@digital-static.net>2019-12-16 09:05:41 -0800
committerGitHub <noreply@github.com>2019-12-16 09:05:41 -0800
commit340f1ebe299ef6712c79da23ad5bc6e3efad8250 (patch)
tree0b8618df2e9f563d358f9b4a0869cf3272310e7a /cmp/cmpopts
parent3838af334ff48ab62a68998c9a4ee9d847017617 (diff)
downloadgo-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.go34
-rw-r--r--cmp/cmpopts/util_test.go164
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}}}},