From 1a64750fcdcb33f41f98040ea9b386cc705441d0 Mon Sep 17 00:00:00 2001 From: Nic Cope Date: Thu, 9 Sep 2021 05:39:02 +0000 Subject: [PATCH] Add an errors package with a similar API to github.com/pkg/errors Go introduced a 'native' way to wrap errors back in v1.13. At that point we were already using github.com/pkg/errors to 'wrap' errors with context, and we never got around to migrating. In addition to pure inertia, I've personally avoided making the switch because I prefer the github.com/pkg/errors API. Specifically I like that errors.Wrap handles the "outer context: inner context" error format that Go uses by convention, and that errors.Wrap will return nil when passed a nil error. Given that github.com/pkg/errors has long been in maintenance mode, and is (per https://github.com/pkg/errors/issues/245) no longer used by its original author now seems as good a time as any to migrate. This commit attempts to ease that migration for the Crossplane project - and to retain the nice API - by adding a package that acts as a small github.com/pkg/errors style shim layer around the stdlib pkg/errors (and friends, like fmt.Errorf). Signed-off-by: Nic Cope --- pkg/errors/errors.go | 128 ++++++++++++++++++++++++++++++++++++++ pkg/errors/errors_test.go | 126 +++++++++++++++++++++++++++++++++++++ 2 files changed, 254 insertions(+) create mode 100644 pkg/errors/errors.go create mode 100644 pkg/errors/errors_test.go diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go new file mode 100644 index 000000000..24cc306f5 --- /dev/null +++ b/pkg/errors/errors.go @@ -0,0 +1,128 @@ +/* +Copyright 2021 The Crossplane Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package errors is a github.com/pkg/errors compatible API for native errors. +// It includes only the subset of the github.com/pkg/errors API that is used by +// the Crossplane project. +package errors + +import ( + "errors" + "fmt" +) + +// New returns an error that formats as the given text. Each call to New returns +// a distinct error value even if the text is identical. +func New(text string) error { return errors.New(text) } + +// Is reports whether any error in err's chain matches target. +// +// The chain consists of err itself followed by the sequence of errors obtained +// by repeatedly calling Unwrap. +// +// An error is considered to match a target if it is equal to that target or if +// it implements a method Is(error) bool such that Is(target) returns true. +// +// An error type might provide an Is method so it can be treated as equivalent +// to an existing error. For example, if MyError defines +// +// func (m MyError) Is(target error) bool { return target == fs.ErrExist } +// +// then Is(MyError{}, fs.ErrExist) returns true. See syscall.Errno.Is for +// an example in the standard library. +func Is(err, target error) bool { return errors.Is(err, target) } + +// As finds the first error in err's chain that matches target, and if so, sets +// target to that error value and returns true. Otherwise, it returns false. +// +// The chain consists of err itself followed by the sequence of errors obtained +// by repeatedly calling Unwrap. +// +// An error matches target if the error's concrete value is assignable to the +// value pointed to by target, or if the error has a method As(interface{}) bool +// such that As(target) returns true. In the latter case, the As method is +// responsible for setting target. +// +// An error type might provide an As method so it can be treated as if it were a +// different error type. +// +// As panics if target is not a non-nil pointer to either a type that implements +// error, or to any interface type. +func As(err error, target interface{}) bool { return errors.As(err, target) } + +// Unwrap returns the result of calling the Unwrap method on err, if err's type +// contains an Unwrap method returning error. Otherwise, Unwrap returns nil. +func Unwrap(err error) error { return errors.Unwrap(err) } + +// Errorf formats according to a format specifier and returns the string as a +// value that satisfies error. +// +// If the format specifier includes a %w verb with an error operand, the +// returned error will implement an Unwrap method returning the operand. It is +// invalid to include more than one %w verb or to supply it with an operand that +// does not implement the error interface. The %w verb is otherwise a synonym +// for %v. +func Errorf(format string, a ...interface{}) error { return fmt.Errorf(format, a...) } + +// WithMessage annotates err with a new message. If err is nil, WithMessage +// returns nil. +func WithMessage(err error, message string) error { + if err == nil { + return nil + } + return fmt.Errorf("%s: %w", message, err) +} + +// WithMessagef annotates err with the format specifier. If err is nil, +// WithMessagef returns nil. +func WithMessagef(err error, format string, args ...interface{}) error { + if err == nil { + return nil + } + return fmt.Errorf("%s: %w", fmt.Sprintf(format, args...), err) +} + +// Wrap is an alias for WithMessage. +func Wrap(err error, message string) error { + return WithMessage(err, message) +} + +// Wrapf is an alias for WithMessagef +func Wrapf(err error, format string, args ...interface{}) error { + return WithMessagef(err, format, args...) +} + +// Cause calls Unwrap on each error it finds. It returns the first error it +// finds that does not have an Unwrap method - i.e. the first error that was not +// the result of a Wrap call, a Wrapf call, or an Errorf call with %w wrapping. +func Cause(err error) error { + type wrapped interface { + Unwrap() error + } + + for err != nil { + // We're ignoring errorlint telling us to use errors.As because + // we actually do want to check the outermost error. + //nolint:errorlint + w, ok := err.(wrapped) + if !ok { + return err + } + err = w.Unwrap() + } + + return err +} diff --git a/pkg/errors/errors_test.go b/pkg/errors/errors_test.go new file mode 100644 index 000000000..be518950f --- /dev/null +++ b/pkg/errors/errors_test.go @@ -0,0 +1,126 @@ +/* +Copyright 2021 The Crossplane Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package errors + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/crossplane/crossplane-runtime/pkg/test" +) + +func TestWrap(t *testing.T) { + type args struct { + err error + message string + } + cases := map[string]struct { + args args + want error + }{ + "NilError": { + args: args{ + err: nil, + message: "very useful context", + }, + want: nil, + }, + "NonNilError": { + args: args{ + err: New("boom"), + message: "very useful context", + }, + want: Errorf("very useful context: %w", New("boom")), + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got := Wrap(tc.args.err, tc.args.message) + if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" { + t.Errorf("Wrap(...): -want, +got:\n%s", diff) + } + }) + } +} + +func TestWrapf(t *testing.T) { + type args struct { + err error + message string + args []interface{} + } + cases := map[string]struct { + args args + want error + }{ + "NilError": { + args: args{ + err: nil, + message: "very useful context", + }, + want: nil, + }, + "NonNilError": { + args: args{ + err: New("boom"), + message: "very useful context about %s", + args: []interface{}{"ducks"}, + }, + want: Errorf("very useful context about %s: %w", "ducks", New("boom")), + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got := Wrapf(tc.args.err, tc.args.message, tc.args.args...) + if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" { + t.Errorf("Wrapf(...): -want, +got:\n%s", diff) + } + }) + } +} + +func TestCause(t *testing.T) { + cases := map[string]struct { + err error + want error + }{ + "NilError": { + err: nil, + want: nil, + }, + "BareError": { + err: New("boom"), + want: New("boom"), + }, + "WrappedError": { + err: Wrap(Wrap(New("boom"), "interstitial context"), "very important context"), + want: New("boom"), + }, + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + got := Cause(tc.err) + if diff := cmp.Diff(tc.want, got, test.EquateErrors()); diff != "" { + t.Errorf("Cause(...): -want, +got:\n%s", diff) + } + }) + } +}