Merge PR #4821: Port weave errors package

This commit is contained in:
Ethan Frey 2019-07-31 18:37:11 +02:00 committed by Alexander Bezobchuk
parent 7c70912263
commit 68dd969b4d
9 changed files with 1069 additions and 0 deletions

View File

@ -12,3 +12,4 @@ tags:
- cli
- modules
- simulation
- types

View File

@ -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

130
types/errors/abci.go Normal file
View File

@ -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
}

272
types/errors/abci_test.go Normal file
View File

@ -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" }

34
types/errors/doc.go Normal file
View File

@ -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

269
types/errors/errors.go Normal file
View File

@ -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
}

158
types/errors/errors_test.go Normal file
View File

@ -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)
}
}

121
types/errors/stacktrace.go Normal file
View File

@ -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
}
}
}

View File

@ -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)
}
})
}
}