Merge PR #4821: Port weave errors package
This commit is contained in:
parent
7c70912263
commit
68dd969b4d
|
@ -12,3 +12,4 @@ tags:
|
|||
- cli
|
||||
- modules
|
||||
- simulation
|
||||
- types
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
#4821 types/errors package added with support for stacktraces
|
||||
Meant as a more feature-rich replacement for sdk.Errors in the mid-term
|
|
@ -0,0 +1,130 @@
|
|||
package errors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
const (
|
||||
// SuccessABCICode declares an ABCI response use 0 to signal that the
|
||||
// processing was successful and no error is returned.
|
||||
SuccessABCICode = 0
|
||||
|
||||
// All unclassified errors that do not provide an ABCI code are clubbed
|
||||
// under an internal error code and a generic message instead of
|
||||
// detailed error string.
|
||||
internalABCICodespace = UndefinedCodespace
|
||||
internalABCICode uint32 = 1
|
||||
internalABCILog string = "internal error"
|
||||
// multiErrorABCICode uint32 = 1000
|
||||
)
|
||||
|
||||
// ABCIInfo returns the ABCI error information as consumed by the tendermint
|
||||
// client. Returned codespace, code, and log message should be used as a ABCI response.
|
||||
// Any error that does not provide ABCICode information is categorized as error
|
||||
// with code 1, codespace UndefinedCodespace
|
||||
// When not running in a debug mode all messages of errors that do not provide
|
||||
// ABCICode information are replaced with generic "internal error". Errors
|
||||
// without an ABCICode information as considered internal.
|
||||
func ABCIInfo(err error, debug bool) (codespace string, code uint32, log string) {
|
||||
if errIsNil(err) {
|
||||
return "", SuccessABCICode, ""
|
||||
}
|
||||
|
||||
encode := defaultErrEncoder
|
||||
if debug {
|
||||
encode = debugErrEncoder
|
||||
}
|
||||
|
||||
return abciCodespace(err), abciCode(err), encode(err)
|
||||
}
|
||||
|
||||
// The debugErrEncoder encodes the error with a stacktrace.
|
||||
func debugErrEncoder(err error) string {
|
||||
return fmt.Sprintf("%+v", err)
|
||||
}
|
||||
|
||||
// The defaultErrEncoder applies Redact on the error before encoding it with its internal error message.
|
||||
func defaultErrEncoder(err error) string {
|
||||
return Redact(err).Error()
|
||||
}
|
||||
|
||||
type coder interface {
|
||||
ABCICode() uint32
|
||||
}
|
||||
|
||||
// abciCode test if given error contains an ABCI code and returns the value of
|
||||
// it if available. This function is testing for the causer interface as well
|
||||
// and unwraps the error.
|
||||
func abciCode(err error) uint32 {
|
||||
if errIsNil(err) {
|
||||
return SuccessABCICode
|
||||
}
|
||||
|
||||
for {
|
||||
if c, ok := err.(coder); ok {
|
||||
return c.ABCICode()
|
||||
}
|
||||
|
||||
if c, ok := err.(causer); ok {
|
||||
err = c.Cause()
|
||||
} else {
|
||||
return internalABCICode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type codespacer interface {
|
||||
Codespace() string
|
||||
}
|
||||
|
||||
// abciCodespace tests if given error contains a codespace and returns the value of
|
||||
// it if available. This function is testing for the causer interface as well
|
||||
// and unwraps the error.
|
||||
func abciCodespace(err error) string {
|
||||
if errIsNil(err) {
|
||||
return ""
|
||||
}
|
||||
|
||||
for {
|
||||
if c, ok := err.(codespacer); ok {
|
||||
return c.Codespace()
|
||||
}
|
||||
|
||||
if c, ok := err.(causer); ok {
|
||||
err = c.Cause()
|
||||
} else {
|
||||
return internalABCICodespace
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// errIsNil returns true if value represented by the given error is nil.
|
||||
//
|
||||
// Most of the time a simple == check is enough. There is a very narrowed
|
||||
// spectrum of cases (mostly in tests) where a more sophisticated check is
|
||||
// required.
|
||||
func errIsNil(err error) bool {
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
if val := reflect.ValueOf(err); val.Kind() == reflect.Ptr {
|
||||
return val.IsNil()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Redact replace all errors that do not initialize with a weave error with a
|
||||
// generic internal error instance. This function is supposed to hide
|
||||
// implementation details errors and leave only those that weave framework
|
||||
// originates.
|
||||
func Redact(err error) error {
|
||||
if ErrPanic.Is(err) {
|
||||
return errors.New(internalABCILog)
|
||||
}
|
||||
if abciCode(err) == internalABCICode {
|
||||
return errors.New(internalABCILog)
|
||||
}
|
||||
return err
|
||||
}
|
|
@ -0,0 +1,272 @@
|
|||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestABCInfo(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
err error
|
||||
debug bool
|
||||
wantCode uint32
|
||||
wantSpace string
|
||||
wantLog string
|
||||
}{
|
||||
"plain weave error": {
|
||||
err: ErrUnauthorized,
|
||||
debug: false,
|
||||
wantLog: "unauthorized",
|
||||
wantCode: ErrUnauthorized.code,
|
||||
wantSpace: RootCodespace,
|
||||
},
|
||||
"wrapped weave error": {
|
||||
err: Wrap(Wrap(ErrUnauthorized, "foo"), "bar"),
|
||||
debug: false,
|
||||
wantLog: "bar: foo: unauthorized",
|
||||
wantCode: ErrUnauthorized.code,
|
||||
wantSpace: RootCodespace,
|
||||
},
|
||||
"nil is empty message": {
|
||||
err: nil,
|
||||
debug: false,
|
||||
wantLog: "",
|
||||
wantCode: 0,
|
||||
wantSpace: "",
|
||||
},
|
||||
"nil weave error is not an error": {
|
||||
err: (*Error)(nil),
|
||||
debug: false,
|
||||
wantLog: "",
|
||||
wantCode: 0,
|
||||
wantSpace: "",
|
||||
},
|
||||
"stdlib is generic message": {
|
||||
err: io.EOF,
|
||||
debug: false,
|
||||
wantLog: "internal error",
|
||||
wantCode: 1,
|
||||
wantSpace: UndefinedCodespace,
|
||||
},
|
||||
"stdlib returns error message in debug mode": {
|
||||
err: io.EOF,
|
||||
debug: true,
|
||||
wantLog: "EOF",
|
||||
wantCode: 1,
|
||||
wantSpace: UndefinedCodespace,
|
||||
},
|
||||
"wrapped stdlib is only a generic message": {
|
||||
err: Wrap(io.EOF, "cannot read file"),
|
||||
debug: false,
|
||||
wantLog: "internal error",
|
||||
wantCode: 1,
|
||||
wantSpace: UndefinedCodespace,
|
||||
},
|
||||
// This is hard to test because of attached stacktrace. This
|
||||
// case is tested in an another test.
|
||||
//"wrapped stdlib is a full message in debug mode": {
|
||||
// err: Wrap(io.EOF, "cannot read file"),
|
||||
// debug: true,
|
||||
// wantLog: "cannot read file: EOF",
|
||||
// wantCode: 1,
|
||||
//},
|
||||
"custom error": {
|
||||
err: customErr{},
|
||||
debug: false,
|
||||
wantLog: "custom",
|
||||
wantCode: 999,
|
||||
wantSpace: "extern",
|
||||
},
|
||||
"custom error in debug mode": {
|
||||
err: customErr{},
|
||||
debug: true,
|
||||
wantLog: "custom",
|
||||
wantCode: 999,
|
||||
wantSpace: "extern",
|
||||
},
|
||||
}
|
||||
|
||||
for testName, tc := range cases {
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
space, code, log := ABCIInfo(tc.err, tc.debug)
|
||||
if space != tc.wantSpace {
|
||||
t.Errorf("want %s space, got %s", tc.wantSpace, space)
|
||||
}
|
||||
if code != tc.wantCode {
|
||||
t.Errorf("want %d code, got %d", tc.wantCode, code)
|
||||
}
|
||||
if log != tc.wantLog {
|
||||
t.Errorf("want %q log, got %q", tc.wantLog, log)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestABCIInfoStacktrace(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
err error
|
||||
debug bool
|
||||
wantStacktrace bool
|
||||
wantErrMsg string
|
||||
}{
|
||||
"wrapped weave error in debug mode provides stacktrace": {
|
||||
err: Wrap(ErrUnauthorized, "wrapped"),
|
||||
debug: true,
|
||||
wantStacktrace: true,
|
||||
wantErrMsg: "wrapped: unauthorized",
|
||||
},
|
||||
"wrapped weave error in non-debug mode does not have stacktrace": {
|
||||
err: Wrap(ErrUnauthorized, "wrapped"),
|
||||
debug: false,
|
||||
wantStacktrace: false,
|
||||
wantErrMsg: "wrapped: unauthorized",
|
||||
},
|
||||
"wrapped stdlib error in debug mode provides stacktrace": {
|
||||
err: Wrap(fmt.Errorf("stdlib"), "wrapped"),
|
||||
debug: true,
|
||||
wantStacktrace: true,
|
||||
wantErrMsg: "wrapped: stdlib",
|
||||
},
|
||||
"wrapped stdlib error in non-debug mode does not have stacktrace": {
|
||||
err: Wrap(fmt.Errorf("stdlib"), "wrapped"),
|
||||
debug: false,
|
||||
wantStacktrace: false,
|
||||
wantErrMsg: "internal error",
|
||||
},
|
||||
}
|
||||
|
||||
const thisTestSrc = "github.com/cosmos/cosmos-sdk/types/errors.TestABCIInfoStacktrace"
|
||||
|
||||
for testName, tc := range cases {
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
_, _, log := ABCIInfo(tc.err, tc.debug)
|
||||
if tc.wantStacktrace {
|
||||
if !strings.Contains(log, thisTestSrc) {
|
||||
t.Errorf("log does not contain this file stack trace: %s", log)
|
||||
}
|
||||
|
||||
if !strings.Contains(log, tc.wantErrMsg) {
|
||||
t.Errorf("log does not contain expected error message: %s", log)
|
||||
}
|
||||
} else {
|
||||
if log != tc.wantErrMsg {
|
||||
t.Fatalf("unexpected log message: %s", log)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestABCIInfoHidesStacktrace(t *testing.T) {
|
||||
err := Wrap(ErrUnauthorized, "wrapped")
|
||||
_, _, log := ABCIInfo(err, false)
|
||||
|
||||
if log != "wrapped: unauthorized" {
|
||||
t.Fatalf("unexpected message in non debug mode: %s", log)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedact(t *testing.T) {
|
||||
if err := Redact(ErrPanic); ErrPanic.Is(err) {
|
||||
t.Error("reduct must not pass through panic error")
|
||||
}
|
||||
if err := Redact(ErrUnauthorized); !ErrUnauthorized.Is(err) {
|
||||
t.Error("reduct should pass through weave error")
|
||||
}
|
||||
|
||||
var cerr customErr
|
||||
if err := Redact(cerr); err != cerr {
|
||||
t.Error("reduct should pass through ABCI code error")
|
||||
}
|
||||
|
||||
serr := fmt.Errorf("stdlib error")
|
||||
if err := Redact(serr); err == serr {
|
||||
t.Error("reduct must not pass through a stdlib error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestABCIInfoSerializeErr(t *testing.T) {
|
||||
var (
|
||||
// Create errors with stacktrace for equal comparision.
|
||||
myErrDecode = Wrap(ErrTxDecode, "test")
|
||||
myErrAddr = Wrap(ErrInvalidAddress, "tester")
|
||||
myPanic = ErrPanic
|
||||
)
|
||||
|
||||
specs := map[string]struct {
|
||||
src error
|
||||
debug bool
|
||||
exp string
|
||||
}{
|
||||
"single error": {
|
||||
src: myErrDecode,
|
||||
debug: false,
|
||||
exp: "test: tx parse error",
|
||||
},
|
||||
"second error": {
|
||||
src: myErrAddr,
|
||||
debug: false,
|
||||
exp: "tester: invalid address",
|
||||
},
|
||||
"single error with debug": {
|
||||
src: myErrDecode,
|
||||
debug: true,
|
||||
exp: fmt.Sprintf("%+v", myErrDecode),
|
||||
},
|
||||
// "multi error default encoder": {
|
||||
// src: Append(myErrMsg, myErrAddr),
|
||||
// exp: Append(myErrMsg, myErrAddr).Error(),
|
||||
// },
|
||||
// "multi error default with internal": {
|
||||
// src: Append(myErrMsg, myPanic),
|
||||
// exp: "internal error",
|
||||
// },
|
||||
"redact in default encoder": {
|
||||
src: myPanic,
|
||||
exp: "internal error",
|
||||
},
|
||||
"do not redact in debug encoder": {
|
||||
src: myPanic,
|
||||
debug: true,
|
||||
exp: fmt.Sprintf("%+v", myPanic),
|
||||
},
|
||||
// "redact in multi error": {
|
||||
// src: Append(myPanic, myErrMsg),
|
||||
// debug: false,
|
||||
// exp: "internal error",
|
||||
// },
|
||||
// "no redact in multi error": {
|
||||
// src: Append(myPanic, myErrMsg),
|
||||
// debug: true,
|
||||
// exp: `2 errors occurred:
|
||||
// * panic
|
||||
// * test: invalid message
|
||||
// `,
|
||||
// },
|
||||
// "wrapped multi error with redact": {
|
||||
// src: Wrap(Append(myPanic, myErrMsg), "wrap"),
|
||||
// debug: false,
|
||||
// exp: "internal error",
|
||||
// },
|
||||
}
|
||||
for msg, spec := range specs {
|
||||
t.Run(msg, func(t *testing.T) {
|
||||
_, _, log := ABCIInfo(spec.src, spec.debug)
|
||||
if exp, got := spec.exp, log; exp != got {
|
||||
t.Errorf("expected %v but got %v", exp, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// customErr is a custom implementation of an error that provides an ABCICode
|
||||
// method.
|
||||
type customErr struct{}
|
||||
|
||||
func (customErr) Codespace() string { return "extern" }
|
||||
|
||||
func (customErr) ABCICode() uint32 { return 999 }
|
||||
|
||||
func (customErr) Error() string { return "custom" }
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
Package errors implements custom error interfaces for cosmos-sdk.
|
||||
|
||||
Error declarations should be generic and cover broad range of cases. Each
|
||||
returned error instance can wrap a generic error declaration to provide more
|
||||
details.
|
||||
|
||||
This package provides a broad range of errors declared that fits all common
|
||||
cases. If an error is very specific for an extension it can be registered outside
|
||||
of the errors package. If it will be needed my many extensions, please consider
|
||||
registering it in the errors package. To create a new error instance use Register
|
||||
function. You must provide a unique, non zero error code and a short description, for example:
|
||||
|
||||
var ErrZeroDivision = errors.Register(9241, "zero division")
|
||||
|
||||
When returning an error, you can attach to it an additional context
|
||||
information by using Wrap function, for example:
|
||||
|
||||
func safeDiv(val, div int) (int, err) {
|
||||
if div == 0 {
|
||||
return 0, errors.Wrapf(ErrZeroDivision, "cannot divide %d", val)
|
||||
}
|
||||
return val / div, nil
|
||||
}
|
||||
|
||||
The first time an error instance is wrapped a stacktrace is attached as well.
|
||||
Stacktrace information can be printed using %+v and %v formats.
|
||||
|
||||
%s is just the error message
|
||||
%+v is the full stack trace
|
||||
%v appends a compressed [filename:line] where the error was created
|
||||
|
||||
*/
|
||||
package errors
|
|
@ -0,0 +1,269 @@
|
|||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// RootCodespace is the codespace for all errors defined in this package
|
||||
const RootCodespace = "sdk"
|
||||
|
||||
// UndefinedCodespace when we explicitly declare no codespace
|
||||
const UndefinedCodespace = "undefined"
|
||||
|
||||
var (
|
||||
// errInternal should never be exposed, but we reserve this code for non-specified errors
|
||||
//nolint
|
||||
errInternal = Register(UndefinedCodespace, 1, "internal")
|
||||
|
||||
// ErrTxDecode is returned if we cannot parse a transaction
|
||||
ErrTxDecode = Register(RootCodespace, 2, "tx parse error")
|
||||
|
||||
// ErrInvalidSequence is used the sequence number (nonce) is incorrect
|
||||
// for the signature
|
||||
ErrInvalidSequence = Register(RootCodespace, 3, "invalid sequence")
|
||||
|
||||
// ErrUnauthorized is used whenever a request without sufficient
|
||||
// authorization is handled.
|
||||
ErrUnauthorized = Register(RootCodespace, 4, "unauthorized")
|
||||
|
||||
// ErrInsufficientFunds is used when the account cannot pay requested amount.
|
||||
ErrInsufficientFunds = Register(RootCodespace, 5, "insufficient funds")
|
||||
|
||||
// ErrUnknownRequest to doc
|
||||
ErrUnknownRequest = Register(RootCodespace, 6, "unknown request")
|
||||
|
||||
// ErrInvalidAddress to doc
|
||||
ErrInvalidAddress = Register(RootCodespace, 7, "invalid address")
|
||||
|
||||
// ErrInvalidPubKey to doc
|
||||
ErrInvalidPubKey = Register(RootCodespace, 8, "invalid pubkey")
|
||||
|
||||
// ErrUnknownAddress to doc
|
||||
ErrUnknownAddress = Register(RootCodespace, 9, "unknown address")
|
||||
|
||||
// ErrInsufficientCoins to doc (what is the difference between ErrInsufficientFunds???)
|
||||
ErrInsufficientCoins = Register(RootCodespace, 10, "insufficient coins")
|
||||
|
||||
// ErrInvalidCoins to doc
|
||||
ErrInvalidCoins = Register(RootCodespace, 11, "invalid coins")
|
||||
|
||||
// ErrOutOfGas to doc
|
||||
ErrOutOfGas = Register(RootCodespace, 12, "out of gas")
|
||||
|
||||
// ErrMemoTooLarge to doc
|
||||
ErrMemoTooLarge = Register(RootCodespace, 13, "memo too large")
|
||||
|
||||
// ErrInsufficientFee to doc
|
||||
ErrInsufficientFee = Register(RootCodespace, 14, "insufficient fee")
|
||||
|
||||
// ErrTooManySignatures to doc
|
||||
ErrTooManySignatures = Register(RootCodespace, 15, "maximum numer of signatures exceeded")
|
||||
|
||||
// ErrNoSignatures to doc
|
||||
ErrNoSignatures = Register(RootCodespace, 16, "no signatures supplied")
|
||||
|
||||
// ErrPanic is only set when we recover from a panic, so we know to
|
||||
// redact potentially sensitive system info
|
||||
ErrPanic = Register(UndefinedCodespace, 111222, "panic")
|
||||
)
|
||||
|
||||
// Register returns an error instance that should be used as the base for
|
||||
// creating error instances during runtime.
|
||||
//
|
||||
// Popular root errors are declared in this package, but extensions may want to
|
||||
// declare custom codes. This function ensures that no error code is used
|
||||
// twice. Attempt to reuse an error code results in panic.
|
||||
//
|
||||
// Use this function only during a program startup phase.
|
||||
func Register(codespace string, code uint32, description string) *Error {
|
||||
// TODO - uniqueness is (codespace, code) combo
|
||||
if e := getUsed(codespace, code); e != nil {
|
||||
panic(fmt.Sprintf("error with code %d is already registered: %q", code, e.desc))
|
||||
}
|
||||
err := &Error{
|
||||
code: code,
|
||||
codespace: codespace,
|
||||
desc: description,
|
||||
}
|
||||
setUsed(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// usedCodes is keeping track of used codes to ensure their uniqueness. No two
|
||||
// error instances should share the same (codespace, code) tuple.
|
||||
var usedCodes = map[string]*Error{}
|
||||
|
||||
func errorID(codespace string, code uint32) string {
|
||||
return fmt.Sprintf("%s:%d", codespace, code)
|
||||
}
|
||||
|
||||
func getUsed(codespace string, code uint32) *Error {
|
||||
return usedCodes[errorID(codespace, code)]
|
||||
}
|
||||
|
||||
func setUsed(err *Error) {
|
||||
usedCodes[errorID(err.codespace, err.code)] = err
|
||||
}
|
||||
|
||||
// ABCIError will resolve an error code/log from an abci result into
|
||||
// an error message. If the code is registered, it will map it back to
|
||||
// the canonical error, so we can do eg. ErrNotFound.Is(err) on something
|
||||
// we get back from an external API.
|
||||
//
|
||||
// This should *only* be used in clients, not in the server side.
|
||||
// The server (abci app / blockchain) should only refer to registered errors
|
||||
func ABCIError(codespace string, code uint32, log string) error {
|
||||
if e := getUsed(codespace, code); e != nil {
|
||||
return Wrap(e, log)
|
||||
}
|
||||
// This is a unique error, will never match on .Is()
|
||||
// Use Wrap here to get a stack trace
|
||||
return Wrap(&Error{codespace: codespace, code: code, desc: "unknown"}, log)
|
||||
}
|
||||
|
||||
// Error represents a root error.
|
||||
//
|
||||
// Weave framework is using root error to categorize issues. Each instance
|
||||
// created during the runtime should wrap one of the declared root errors. This
|
||||
// allows error tests and returning all errors to the client in a safe manner.
|
||||
//
|
||||
// All popular root errors are declared in this package. If an extension has to
|
||||
// declare a custom root error, always use Register function to ensure
|
||||
// error code uniqueness.
|
||||
type Error struct {
|
||||
codespace string
|
||||
code uint32
|
||||
desc string
|
||||
}
|
||||
|
||||
func (e Error) Error() string {
|
||||
return e.desc
|
||||
}
|
||||
|
||||
func (e Error) ABCICode() uint32 {
|
||||
return e.code
|
||||
}
|
||||
|
||||
func (e Error) Codespace() string {
|
||||
return e.codespace
|
||||
}
|
||||
|
||||
// Is check if given error instance is of a given kind/type. This involves
|
||||
// unwrapping given error using the Cause method if available.
|
||||
func (kind *Error) Is(err error) bool {
|
||||
// Reflect usage is necessary to correctly compare with
|
||||
// a nil implementation of an error.
|
||||
if kind == nil {
|
||||
return isNilErr(err)
|
||||
}
|
||||
|
||||
for {
|
||||
if err == kind {
|
||||
return true
|
||||
}
|
||||
|
||||
// If this is a collection of errors, this function must return
|
||||
// true if at least one from the group match.
|
||||
if u, ok := err.(unpacker); ok {
|
||||
for _, e := range u.Unpack() {
|
||||
if kind.Is(e) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c, ok := err.(causer); ok {
|
||||
err = c.Cause()
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isNilErr(err error) bool {
|
||||
// Reflect usage is necessary to correctly compare with
|
||||
// a nil implementation of an error.
|
||||
if err == nil {
|
||||
return true
|
||||
}
|
||||
if reflect.ValueOf(err).Kind() == reflect.Struct {
|
||||
return false
|
||||
}
|
||||
return reflect.ValueOf(err).IsNil()
|
||||
}
|
||||
|
||||
// Wrap extends given error with an additional information.
|
||||
//
|
||||
// If the wrapped error does not provide ABCICode method (ie. stdlib errors),
|
||||
// it will be labeled as internal error.
|
||||
//
|
||||
// If err is nil, this returns nil, avoiding the need for an if statement when
|
||||
// wrapping a error returned at the end of a function
|
||||
func Wrap(err error, description string) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If this error does not carry the stacktrace information yet, attach
|
||||
// one. This should be done only once per error at the lowest frame
|
||||
// possible (most inner wrap).
|
||||
if stackTrace(err) == nil {
|
||||
err = errors.WithStack(err)
|
||||
}
|
||||
|
||||
return &wrappedError{
|
||||
parent: err,
|
||||
msg: description,
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapf extends given error with an additional information.
|
||||
//
|
||||
// This function works like Wrap function with additional functionality of
|
||||
// formatting the input as specified.
|
||||
func Wrapf(err error, format string, args ...interface{}) error {
|
||||
desc := fmt.Sprintf(format, args...)
|
||||
return Wrap(err, desc)
|
||||
}
|
||||
|
||||
type wrappedError struct {
|
||||
// This error layer description.
|
||||
msg string
|
||||
// The underlying error that triggered this one.
|
||||
parent error
|
||||
}
|
||||
|
||||
func (e *wrappedError) Error() string {
|
||||
return fmt.Sprintf("%s: %s", e.msg, e.parent.Error())
|
||||
}
|
||||
|
||||
func (e *wrappedError) Cause() error {
|
||||
return e.parent
|
||||
}
|
||||
|
||||
// Recover captures a panic and stop its propagation. If panic happens it is
|
||||
// transformed into a ErrPanic instance and assigned to given error. Call this
|
||||
// function using defer in order to work as expected.
|
||||
func Recover(err *error) {
|
||||
if r := recover(); r != nil {
|
||||
*err = Wrapf(ErrPanic, "%v", r)
|
||||
}
|
||||
}
|
||||
|
||||
// WithType is a helper to augment an error with a corresponding type message
|
||||
func WithType(err error, obj interface{}) error {
|
||||
return Wrap(err, fmt.Sprintf("%T", obj))
|
||||
}
|
||||
|
||||
// causer is an interface implemented by an error that supports wrapping. Use
|
||||
// it to test if an error wraps another error instance.
|
||||
type causer interface {
|
||||
Cause() error
|
||||
}
|
||||
|
||||
type unpacker interface {
|
||||
Unpack() []error
|
||||
}
|
|
@ -0,0 +1,158 @@
|
|||
package errors
|
||||
|
||||
import (
|
||||
stdlib "errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func TestCause(t *testing.T) {
|
||||
std := stdlib.New("this is a stdlib error")
|
||||
|
||||
cases := map[string]struct {
|
||||
err error
|
||||
root error
|
||||
}{
|
||||
"Errors are self-causing": {
|
||||
err: ErrUnauthorized,
|
||||
root: ErrUnauthorized,
|
||||
},
|
||||
"Wrap reveals root cause": {
|
||||
err: Wrap(ErrUnauthorized, "foo"),
|
||||
root: ErrUnauthorized,
|
||||
},
|
||||
"Cause works for stderr as root": {
|
||||
err: Wrap(std, "Some helpful text"),
|
||||
root: std,
|
||||
},
|
||||
}
|
||||
|
||||
for testName, tc := range cases {
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
if got := errors.Cause(tc.err); got != tc.root {
|
||||
t.Fatal("unexpected result")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorIs(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
a *Error
|
||||
b error
|
||||
wantIs bool
|
||||
}{
|
||||
"instance of the same error": {
|
||||
a: ErrUnauthorized,
|
||||
b: ErrUnauthorized,
|
||||
wantIs: true,
|
||||
},
|
||||
"two different coded errors": {
|
||||
a: ErrUnauthorized,
|
||||
b: ErrOutOfGas,
|
||||
wantIs: false,
|
||||
},
|
||||
"successful comparison to a wrapped error": {
|
||||
a: ErrUnauthorized,
|
||||
b: errors.Wrap(ErrUnauthorized, "gone"),
|
||||
wantIs: true,
|
||||
},
|
||||
"unsuccessful comparison to a wrapped error": {
|
||||
a: ErrUnauthorized,
|
||||
b: errors.Wrap(ErrInsufficientFee, "too big"),
|
||||
wantIs: false,
|
||||
},
|
||||
"not equal to stdlib error": {
|
||||
a: ErrUnauthorized,
|
||||
b: fmt.Errorf("stdlib error"),
|
||||
wantIs: false,
|
||||
},
|
||||
"not equal to a wrapped stdlib error": {
|
||||
a: ErrUnauthorized,
|
||||
b: errors.Wrap(fmt.Errorf("stdlib error"), "wrapped"),
|
||||
wantIs: false,
|
||||
},
|
||||
"nil is nil": {
|
||||
a: nil,
|
||||
b: nil,
|
||||
wantIs: true,
|
||||
},
|
||||
"nil is any error nil": {
|
||||
a: nil,
|
||||
b: (*customError)(nil),
|
||||
wantIs: true,
|
||||
},
|
||||
"nil is not not-nil": {
|
||||
a: nil,
|
||||
b: ErrUnauthorized,
|
||||
wantIs: false,
|
||||
},
|
||||
"not-nil is not nil": {
|
||||
a: ErrUnauthorized,
|
||||
b: nil,
|
||||
wantIs: false,
|
||||
},
|
||||
// "multierr with the same error": {
|
||||
// a: ErrUnauthorized,
|
||||
// b: Append(ErrUnauthorized, ErrState),
|
||||
// wantIs: true,
|
||||
// },
|
||||
// "multierr with random order": {
|
||||
// a: ErrUnauthorized,
|
||||
// b: Append(ErrState, ErrUnauthorized),
|
||||
// wantIs: true,
|
||||
// },
|
||||
// "multierr with wrapped err": {
|
||||
// a: ErrUnauthorized,
|
||||
// b: Append(ErrState, Wrap(ErrUnauthorized, "test")),
|
||||
// wantIs: true,
|
||||
// },
|
||||
// "multierr with nil error": {
|
||||
// a: ErrUnauthorized,
|
||||
// b: Append(nil, nil),
|
||||
// wantIs: false,
|
||||
// },
|
||||
// "multierr with different error": {
|
||||
// a: ErrUnauthorized,
|
||||
// b: Append(ErrState, nil),
|
||||
// wantIs: false,
|
||||
// },
|
||||
// "multierr from nil": {
|
||||
// a: nil,
|
||||
// b: Append(ErrState, ErrUnauthorized),
|
||||
// wantIs: false,
|
||||
// },
|
||||
// "field error wrapper": {
|
||||
// a: ErrEmpty,
|
||||
// b: Field("name", ErrEmpty, "name is required"),
|
||||
// wantIs: true,
|
||||
// },
|
||||
// "nil field error wrapper": {
|
||||
// a: nil,
|
||||
// b: Field("name", nil, "name is required"),
|
||||
// wantIs: true,
|
||||
// },
|
||||
}
|
||||
for testName, tc := range cases {
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
if got := tc.a.Is(tc.b); got != tc.wantIs {
|
||||
t.Fatalf("unexpected result - got:%v want: %v", got, tc.wantIs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type customError struct {
|
||||
}
|
||||
|
||||
func (customError) Error() string {
|
||||
return "custom error"
|
||||
}
|
||||
|
||||
func TestWrapEmpty(t *testing.T) {
|
||||
if err := Wrap(nil, "wrapping <nil>"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func matchesFunc(f errors.Frame, prefixes ...string) bool {
|
||||
fn := funcName(f)
|
||||
for _, prefix := range prefixes {
|
||||
if strings.HasPrefix(fn, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// funcName returns the name of this function, if known.
|
||||
func funcName(f errors.Frame) string {
|
||||
// this looks a bit like magic, but follows example here:
|
||||
// https://github.com/pkg/errors/blob/v0.8.1/stack.go#L43-L50
|
||||
pc := uintptr(f) - 1
|
||||
fn := runtime.FuncForPC(pc)
|
||||
if fn == nil {
|
||||
return "unknown"
|
||||
}
|
||||
return fn.Name()
|
||||
}
|
||||
|
||||
func fileLine(f errors.Frame) (string, int) {
|
||||
// this looks a bit like magic, but follows example here:
|
||||
// https://github.com/pkg/errors/blob/v0.8.1/stack.go#L14-L27
|
||||
// as this is where we get the Frames
|
||||
pc := uintptr(f) - 1
|
||||
fn := runtime.FuncForPC(pc)
|
||||
if fn == nil {
|
||||
return "unknown", 0
|
||||
}
|
||||
return fn.FileLine(pc)
|
||||
}
|
||||
|
||||
func trimInternal(st errors.StackTrace) errors.StackTrace {
|
||||
// trim our internal parts here
|
||||
// manual error creation, or runtime for caught panics
|
||||
for matchesFunc(st[0],
|
||||
// where we create errors
|
||||
"github.com/cosmos/cosmos-sdk/types/errors.Wrap",
|
||||
"github.com/cosmos/cosmos-sdk/types/errors.Wrapf",
|
||||
"github.com/cosmos/cosmos-sdk/types/errors.WithType",
|
||||
// runtime are added on panics
|
||||
"runtime.",
|
||||
// _test is defined in coverage tests, causing failure
|
||||
// "/_test/"
|
||||
) {
|
||||
st = st[1:]
|
||||
}
|
||||
// trim out outer wrappers (runtime.goexit and test library if present)
|
||||
for l := len(st) - 1; l > 0 && matchesFunc(st[l], "runtime.", "testing."); l-- {
|
||||
st = st[:l]
|
||||
}
|
||||
return st
|
||||
}
|
||||
|
||||
func writeSimpleFrame(s io.Writer, f errors.Frame) {
|
||||
file, line := fileLine(f)
|
||||
// cut file at "github.com/"
|
||||
// TODO: generalize better for other hosts?
|
||||
chunks := strings.SplitN(file, "github.com/", 2)
|
||||
if len(chunks) == 2 {
|
||||
file = chunks[1]
|
||||
}
|
||||
fmt.Fprintf(s, " [%s:%d]", file, line)
|
||||
}
|
||||
|
||||
// Format works like pkg/errors, with additions.
|
||||
// %s is just the error message
|
||||
// %+v is the full stack trace
|
||||
// %v appends a compressed [filename:line] where the error
|
||||
// was created
|
||||
//
|
||||
// Inspired by https://github.com/pkg/errors/blob/v0.8.1/errors.go#L162-L176
|
||||
func (e *wrappedError) Format(s fmt.State, verb rune) {
|
||||
// normal output here....
|
||||
if verb != 'v' {
|
||||
fmt.Fprint(s, e.Error())
|
||||
return
|
||||
}
|
||||
// work with the stack trace... whole or part
|
||||
stack := trimInternal(stackTrace(e))
|
||||
if s.Flag('+') {
|
||||
fmt.Fprintf(s, "%+v\n", stack)
|
||||
fmt.Fprint(s, e.Error())
|
||||
} else {
|
||||
fmt.Fprint(s, e.Error())
|
||||
writeSimpleFrame(s, stack[0])
|
||||
}
|
||||
}
|
||||
|
||||
// stackTrace returns the first found stack trace frame carried by given error
|
||||
// or any wrapped error. It returns nil if no stack trace is found.
|
||||
func stackTrace(err error) errors.StackTrace {
|
||||
type stackTracer interface {
|
||||
StackTrace() errors.StackTrace
|
||||
}
|
||||
|
||||
for {
|
||||
if st, ok := err.(stackTracer); ok {
|
||||
return st.StackTrace()
|
||||
}
|
||||
|
||||
if c, ok := err.(causer); ok {
|
||||
err = c.Cause()
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
package errors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStackTrace(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
err error
|
||||
wantError string
|
||||
}{
|
||||
"New gives us a stacktrace": {
|
||||
err: Wrap(ErrNoSignatures, "name"),
|
||||
wantError: "name: no signatures supplied",
|
||||
},
|
||||
"Wrapping stderr gives us a stacktrace": {
|
||||
err: Wrap(fmt.Errorf("foo"), "standard"),
|
||||
wantError: "standard: foo",
|
||||
},
|
||||
"Wrapping pkg/errors gives us clean stacktrace": {
|
||||
err: Wrap(errors.New("bar"), "pkg"),
|
||||
wantError: "pkg: bar",
|
||||
},
|
||||
"Wrapping inside another function is still clean": {
|
||||
err: Wrap(fmt.Errorf("indirect"), "do the do"),
|
||||
wantError: "do the do: indirect",
|
||||
},
|
||||
}
|
||||
|
||||
// Wrapping code is unwanted in the errors stack trace.
|
||||
unwantedSrc := []string{
|
||||
"github.com/cosmos/cosmos-sdk/types/errors.Wrap\n",
|
||||
"github.com/cosmos/cosmos-sdk/types/errors.Wrapf\n",
|
||||
"runtime.goexit\n",
|
||||
}
|
||||
const thisTestSrc = "types/errors/stacktrace_test.go"
|
||||
|
||||
for testName, tc := range cases {
|
||||
t.Run(testName, func(t *testing.T) {
|
||||
if !reflect.DeepEqual(tc.err.Error(), tc.wantError) {
|
||||
t.Fatalf("errors not equal, got '%s', want '%s'", tc.err.Error(), tc.wantError)
|
||||
}
|
||||
|
||||
if stackTrace(tc.err) == nil {
|
||||
t.Fatal("expected a stack trace to be present")
|
||||
}
|
||||
|
||||
fullStack := fmt.Sprintf("%+v", tc.err)
|
||||
if !strings.Contains(fullStack, thisTestSrc) {
|
||||
t.Logf("Stack trace below\n----%s\n----", fullStack)
|
||||
t.Error("full stack trace should contain this test source code information")
|
||||
}
|
||||
if !strings.Contains(fullStack, tc.wantError) {
|
||||
t.Logf("Stack trace below\n----%s\n----", fullStack)
|
||||
t.Error("full stack trace should contain the error description")
|
||||
}
|
||||
for _, src := range unwantedSrc {
|
||||
if strings.Contains(fullStack, src) {
|
||||
t.Logf("Stack trace below\n----%s\n----", fullStack)
|
||||
t.Logf("full stack contains unwanted source file path: %q", src)
|
||||
}
|
||||
}
|
||||
|
||||
tinyStack := fmt.Sprintf("%v", tc.err)
|
||||
if !strings.HasPrefix(tinyStack, tc.wantError) {
|
||||
t.Fatalf("prefix mimssing: %s", tinyStack)
|
||||
}
|
||||
if strings.Contains(tinyStack, "\n") {
|
||||
t.Fatal("only one stack line is expected")
|
||||
}
|
||||
// contains a link to where it was created, which must
|
||||
// be here, not the Wrap() function
|
||||
if !strings.Contains(tinyStack, thisTestSrc) {
|
||||
t.Fatalf("this file missing in stack info:\n %s", tinyStack)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue