diff --git a/.clog.yaml b/.clog.yaml index bab256f8d9..fca2575143 100644 --- a/.clog.yaml +++ b/.clog.yaml @@ -12,3 +12,4 @@ tags: - cli - modules - simulation + - types diff --git a/.pending/improvements/types/4821-types-errors-pa b/.pending/improvements/types/4821-types-errors-pa new file mode 100644 index 0000000000..e8afa72f55 --- /dev/null +++ b/.pending/improvements/types/4821-types-errors-pa @@ -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 diff --git a/types/errors/abci.go b/types/errors/abci.go new file mode 100644 index 0000000000..1f70758cdd --- /dev/null +++ b/types/errors/abci.go @@ -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 +} diff --git a/types/errors/abci_test.go b/types/errors/abci_test.go new file mode 100644 index 0000000000..b0981c2f74 --- /dev/null +++ b/types/errors/abci_test.go @@ -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" } diff --git a/types/errors/doc.go b/types/errors/doc.go new file mode 100644 index 0000000000..6cca61580d --- /dev/null +++ b/types/errors/doc.go @@ -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 diff --git a/types/errors/errors.go b/types/errors/errors.go new file mode 100644 index 0000000000..77cd698f18 --- /dev/null +++ b/types/errors/errors.go @@ -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 +} diff --git a/types/errors/errors_test.go b/types/errors/errors_test.go new file mode 100644 index 0000000000..d2c46a3e65 --- /dev/null +++ b/types/errors/errors_test.go @@ -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 "); err != nil { + t.Fatal(err) + } +} diff --git a/types/errors/stacktrace.go b/types/errors/stacktrace.go new file mode 100644 index 0000000000..f7079c56d8 --- /dev/null +++ b/types/errors/stacktrace.go @@ -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 + } + } +} diff --git a/types/errors/stacktrace_test.go b/types/errors/stacktrace_test.go new file mode 100644 index 0000000000..392ab47402 --- /dev/null +++ b/types/errors/stacktrace_test.go @@ -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) + } + }) + } +}