feat: standalone errors go.mod (#10779)
## Description This PR: * moves all of the `types/errors` code to a new `errors` go module, except: * the `RootCodespace` errors in `types/errors` stay there * ABCI stuff that depends on tendermint stays in `types/errors * adds aliases to everything in `types/errors` referencing `errors` so **this is not a breaking change** This will allow standalone go modules to use the same error types as the SDK. In particular, I want the `orm` to reference `errors` and then the SDK will be able to import `orm` and it can stay standalone. The same could apply to the `db` module. After this PR the plan is to: * tag `github.com/cosmos/cosmos-sdk/errors` as `v1.0` 🎉 * remove the `replace` directive for `errors` in the main SDK `go.mod` --- ### Author Checklist *All items are required. Please add a note to the item if the item is not applicable and please add links to any relevant follow up issues.* I have... - [x] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title - [ ] added `!` to the type prefix if API or client breaking change - [x] targeted the correct branch (see [PR Targeting](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#pr-targeting)) - [ ] provided a link to the relevant issue or specification - [ ] followed the guidelines for [building modules](https://github.com/cosmos/cosmos-sdk/blob/master/docs/building-modules) - [ ] included the necessary unit and integration [tests](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#testing) - [ ] added a changelog entry to `CHANGELOG.md` - [ ] included comments for [documenting Go code](https://blog.golang.org/godoc) - [ ] updated the relevant documentation or specification - [x] reviewed "Files changed" and left comments if necessary - [ ] confirmed all CI checks have passed ### Reviewers Checklist *All items are required. Please add a note if the item is not applicable and please add your handle next to the items reviewed if you only reviewed selected items.* I have... - [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title - [ ] confirmed `!` in the type prefix if API or client breaking change - [ ] confirmed all author checklist items have been addressed - [ ] reviewed state machine logic - [ ] reviewed API design and naming - [ ] reviewed documentation is accurate - [ ] reviewed tests and test coverage - [ ] manually tested (if applicable)
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
<!--
|
||||
Guiding Principles:
|
||||
|
||||
Changelogs are for humans, not machines.
|
||||
There should be an entry for every single version.
|
||||
The same types of changes should be grouped.
|
||||
Versions and sections should be linkable.
|
||||
The latest version comes first.
|
||||
The release date of each version is displayed.
|
||||
Mention whether you follow Semantic Versioning.
|
||||
|
||||
Usage:
|
||||
|
||||
Change log entries are to be added to the Unreleased section under the
|
||||
appropriate stanza (see below). Each entry should ideally include a tag and
|
||||
the Github issue reference in the following format:
|
||||
|
||||
* (<tag>) \#<issue-number> message
|
||||
|
||||
The issue numbers will later be link-ified during the release process so you do
|
||||
not have to worry about including a link manually, but you can if you wish.
|
||||
|
||||
Types of changes (Stanzas):
|
||||
|
||||
"Features" for new features.
|
||||
"Improvements" for changes in existing functionality.
|
||||
"Deprecated" for soon-to-be removed features.
|
||||
"Bug Fixes" for any bug fixes.
|
||||
"Client Breaking" for breaking Protobuf, gRPC and REST routes used by end-users.
|
||||
"CLI Breaking" for breaking CLI commands.
|
||||
"API Breaking" for breaking exported APIs used by developers building on SDK.
|
||||
Ref: https://keepachangelog.com/en/1.0.0/
|
||||
-->
|
||||
|
||||
# Changelog
|
||||
|
||||
## v1.0.0
|
||||
|
||||
### Features
|
||||
* [\#10779](https://github.com/cosmos/cosmos-sdk/pull/10779) Import code from the `github.com/cosmos/cosmos-sdk/types/errors` package.
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
const (
|
||||
// SuccessABCICode declares an ABCI response use 0 to signal that the
|
||||
// processing was successful and no error is returned.
|
||||
SuccessABCICode uint32 = 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
|
||||
)
|
||||
|
||||
// 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 tests 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
|
||||
}
|
||||
|
||||
var errPanicWithMsg = Wrapf(ErrPanic, "panic message redacted to hide potentially sensitive system info")
|
||||
|
||||
// Redact replaces an error that is not initialized as an ABCI Error with a
|
||||
// generic internal error instance. If the error is an ABCI Error, that error is
|
||||
// simply returned.
|
||||
func Redact(err error) error {
|
||||
if ErrPanic.Is(err) {
|
||||
return errPanicWithMsg
|
||||
}
|
||||
if abciCode(err) == internalABCICode {
|
||||
return errInternal
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type abciTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func TestABCITestSuite(t *testing.T) {
|
||||
suite.Run(t, new(abciTestSuite))
|
||||
}
|
||||
|
||||
func (s *abciTestSuite) SetupSuite() {
|
||||
s.T().Parallel()
|
||||
}
|
||||
|
||||
func (s *abciTestSuite) TestABCInfo() {
|
||||
cases := map[string]struct {
|
||||
err error
|
||||
debug bool
|
||||
wantCode uint32
|
||||
wantSpace string
|
||||
wantLog string
|
||||
}{
|
||||
"plain SDK error": {
|
||||
err: ErrUnauthorized,
|
||||
debug: false,
|
||||
wantLog: "unauthorized",
|
||||
wantCode: ErrUnauthorized.code,
|
||||
wantSpace: testCodespace,
|
||||
},
|
||||
"wrapped SDK error": {
|
||||
err: Wrap(Wrap(ErrUnauthorized, "foo"), "bar"),
|
||||
debug: false,
|
||||
wantLog: "bar: foo: unauthorized",
|
||||
wantCode: ErrUnauthorized.code,
|
||||
wantSpace: testCodespace,
|
||||
},
|
||||
"nil is empty message": {
|
||||
err: nil,
|
||||
debug: false,
|
||||
wantLog: "",
|
||||
wantCode: 0,
|
||||
wantSpace: "",
|
||||
},
|
||||
"nil SDK 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",
|
||||
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",
|
||||
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 {
|
||||
space, code, log := ABCIInfo(tc.err, tc.debug)
|
||||
s.Require().Equal(tc.wantSpace, space, testName)
|
||||
s.Require().Equal(tc.wantCode, code, testName)
|
||||
s.Require().Equal(tc.wantLog, log, testName)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *abciTestSuite) TestABCIInfoStacktrace() {
|
||||
cases := map[string]struct {
|
||||
err error
|
||||
debug bool
|
||||
wantStacktrace bool
|
||||
wantErrMsg string
|
||||
}{
|
||||
"wrapped SDK error in debug mode provides stacktrace": {
|
||||
err: Wrap(ErrUnauthorized, "wrapped"),
|
||||
debug: true,
|
||||
wantStacktrace: true,
|
||||
wantErrMsg: "wrapped: unauthorized",
|
||||
},
|
||||
"wrapped SDK 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",
|
||||
},
|
||||
}
|
||||
|
||||
const thisTestSrc = "github.com/cosmos/cosmos-sdk/types/errors.(*abciTestSuite).TestABCIInfoStacktrace"
|
||||
|
||||
for testName, tc := range cases {
|
||||
_, _, log := ABCIInfo(tc.err, tc.debug)
|
||||
if !tc.wantStacktrace {
|
||||
s.Require().Equal(tc.wantErrMsg, log, testName)
|
||||
continue
|
||||
}
|
||||
|
||||
s.Require().True(strings.Contains(log, thisTestSrc), testName)
|
||||
s.Require().True(strings.Contains(log, tc.wantErrMsg), testName)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *abciTestSuite) TestABCIInfoHidesStacktrace() {
|
||||
err := Wrap(ErrUnauthorized, "wrapped")
|
||||
_, _, log := ABCIInfo(err, false)
|
||||
s.Require().Equal("wrapped: unauthorized", log)
|
||||
}
|
||||
|
||||
func (s *abciTestSuite) TestRedact() {
|
||||
cases := map[string]struct {
|
||||
err error
|
||||
untouched bool // if true we expect the same error after redact
|
||||
changed error // if untouched == false, expect this error
|
||||
}{
|
||||
"panic looses message": {
|
||||
err: Wrap(ErrPanic, "some secret stack trace"),
|
||||
changed: errPanicWithMsg,
|
||||
},
|
||||
"sdk errors untouched": {
|
||||
err: Wrap(ErrUnauthorized, "cannot drop db"),
|
||||
untouched: true,
|
||||
},
|
||||
"pass though custom errors with ABCI code": {
|
||||
err: customErr{},
|
||||
untouched: true,
|
||||
},
|
||||
"redact stdlib error": {
|
||||
err: fmt.Errorf("stdlib error"),
|
||||
changed: errInternal,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range cases {
|
||||
spec := tc
|
||||
redacted := Redact(spec.err)
|
||||
if spec.untouched {
|
||||
s.Require().Equal(spec.err, redacted, name)
|
||||
continue
|
||||
}
|
||||
|
||||
// see if we got the expected redact
|
||||
s.Require().Equal(spec.changed, redacted, name)
|
||||
// make sure the ABCI code did not change
|
||||
s.Require().Equal(abciCode(spec.err), abciCode(redacted), name)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (s *abciTestSuite) TestABCIInfoSerializeErr() {
|
||||
var (
|
||||
// Create errors with stacktrace for equal comparison.
|
||||
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),
|
||||
},
|
||||
"redact in default encoder": {
|
||||
src: myPanic,
|
||||
exp: "panic message redacted to hide potentially sensitive system info: panic",
|
||||
},
|
||||
"do not redact in debug encoder": {
|
||||
src: myPanic,
|
||||
debug: true,
|
||||
exp: fmt.Sprintf("%+v", myPanic),
|
||||
},
|
||||
}
|
||||
for msg, spec := range specs {
|
||||
spec := spec
|
||||
_, _, log := ABCIInfo(spec.src, spec.debug)
|
||||
s.Require().Equal(spec.exp, log, msg)
|
||||
}
|
||||
}
|
||||
|
||||
// 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,266 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// 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
|
||||
errInternal = Register(UndefinedCodespace, 1, "internal")
|
||||
|
||||
// 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 := New(codespace, code, 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(New(codespace, code, "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 New(codespace string, code uint32, desc string) *Error {
|
||||
return &Error{codespace: codespace, code: code, desc: desc}
|
||||
}
|
||||
|
||||
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 (e *Error) Is(err error) bool {
|
||||
// Reflect usage is necessary to correctly compare with
|
||||
// a nil implementation of an error.
|
||||
if e == nil {
|
||||
return isNilErr(err)
|
||||
}
|
||||
|
||||
for {
|
||||
if err == e {
|
||||
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 _, er := range u.Unpack() {
|
||||
if e.Is(er) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c, ok := err.(causer); ok {
|
||||
err = c.Cause()
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap extends this error with an additional information.
|
||||
// It's a handy function to call Wrap with sdk errors.
|
||||
func (e *Error) Wrap(desc string) error { return Wrap(e, desc) }
|
||||
|
||||
// Wrapf extends this error with an additional information.
|
||||
// It's a handy function to call Wrapf with sdk errors.
|
||||
func (e *Error) Wrapf(desc string, args ...interface{}) error { return Wrapf(e, desc, args...) }
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Is reports whether any error in e's chain matches a target.
|
||||
func (e *wrappedError) Is(target error) bool {
|
||||
if e == target {
|
||||
return true
|
||||
}
|
||||
|
||||
w := e.Cause()
|
||||
for {
|
||||
if w == target {
|
||||
return true
|
||||
}
|
||||
|
||||
x, ok := w.(causer)
|
||||
if ok {
|
||||
w = x.Cause()
|
||||
}
|
||||
if x == nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unwrap implements the built-in errors.Unwrap
|
||||
func (e *wrappedError) Unwrap() 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))
|
||||
}
|
||||
|
||||
// IsOf checks if a received error is caused by one of the target errors.
|
||||
// It extends the errors.Is functionality to a list of errors.
|
||||
func IsOf(received error, targets ...error) bool {
|
||||
for _, t := range targets {
|
||||
if errors.Is(received, t) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 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,261 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
stdlib "errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type errorsTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func TestErrorsTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(errorsTestSuite))
|
||||
}
|
||||
|
||||
func (s *errorsTestSuite) SetupSuite() {
|
||||
s.T().Parallel()
|
||||
}
|
||||
|
||||
func (s *errorsTestSuite) TestCause() {
|
||||
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 {
|
||||
s.Require().Equal(tc.root, errors.Cause(tc.err), testName)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *errorsTestSuite) TestErrorIs() {
|
||||
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,
|
||||
},
|
||||
}
|
||||
for testName, tc := range cases {
|
||||
s.Require().Equal(tc.wantIs, tc.a.Is(tc.b), testName)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *errorsTestSuite) TestIsOf() {
|
||||
require := s.Require()
|
||||
|
||||
var errNil *Error
|
||||
var err = ErrInvalidAddress
|
||||
var errW = Wrap(ErrLogic, "more info")
|
||||
|
||||
require.False(IsOf(errNil), "nil error should always have no causer")
|
||||
require.False(IsOf(errNil, err), "nil error should always have no causer")
|
||||
|
||||
require.False(IsOf(err))
|
||||
require.False(IsOf(err, nil))
|
||||
require.False(IsOf(err, ErrLogic))
|
||||
|
||||
require.True(IsOf(errW, ErrLogic))
|
||||
require.True(IsOf(errW, err, ErrLogic))
|
||||
require.True(IsOf(errW, nil, errW), "error should much itself")
|
||||
var err2 = errors.New("other error")
|
||||
require.True(IsOf(err2, nil, err2), "error should much itself")
|
||||
}
|
||||
|
||||
type customError struct {
|
||||
}
|
||||
|
||||
func (customError) Error() string {
|
||||
return "custom error"
|
||||
}
|
||||
|
||||
func (s *errorsTestSuite) TestWrapEmpty() {
|
||||
s.Require().Nil(Wrap(nil, "wrapping <nil>"))
|
||||
}
|
||||
|
||||
func (s *errorsTestSuite) TestWrappedIs() {
|
||||
require := s.Require()
|
||||
err := Wrap(ErrTxTooLarge, "context")
|
||||
require.True(stdlib.Is(err, ErrTxTooLarge))
|
||||
|
||||
err = Wrap(err, "more context")
|
||||
require.True(stdlib.Is(err, ErrTxTooLarge))
|
||||
|
||||
err = Wrap(err, "even more context")
|
||||
require.True(stdlib.Is(err, ErrTxTooLarge))
|
||||
|
||||
err = Wrap(ErrInsufficientFee, "...")
|
||||
require.False(stdlib.Is(err, ErrTxTooLarge))
|
||||
|
||||
errs := stdlib.New("other")
|
||||
require.True(stdlib.Is(errs, errs))
|
||||
|
||||
errw := &wrappedError{"msg", errs}
|
||||
require.True(errw.Is(errw), "should match itself")
|
||||
|
||||
require.True(stdlib.Is(ErrInsufficientFee.Wrap("wrapped"), ErrInsufficientFee))
|
||||
require.True(IsOf(ErrInsufficientFee.Wrap("wrapped"), ErrInsufficientFee))
|
||||
require.True(stdlib.Is(ErrInsufficientFee.Wrapf("wrapped"), ErrInsufficientFee))
|
||||
require.True(IsOf(ErrInsufficientFee.Wrapf("wrapped"), ErrInsufficientFee))
|
||||
}
|
||||
|
||||
func (s *errorsTestSuite) TestWrappedIsMultiple() {
|
||||
var errTest = errors.New("test error")
|
||||
var errTest2 = errors.New("test error 2")
|
||||
err := Wrap(errTest2, Wrap(errTest, "some random description").Error())
|
||||
s.Require().True(stdlib.Is(err, errTest2))
|
||||
}
|
||||
|
||||
func (s *errorsTestSuite) TestWrappedIsFail() {
|
||||
var errTest = errors.New("test error")
|
||||
var errTest2 = errors.New("test error 2")
|
||||
err := Wrap(errTest2, Wrap(errTest, "some random description").Error())
|
||||
s.Require().False(stdlib.Is(err, errTest))
|
||||
}
|
||||
|
||||
func (s *errorsTestSuite) TestWrappedUnwrap() {
|
||||
var errTest = errors.New("test error")
|
||||
err := Wrap(errTest, "some random description")
|
||||
s.Require().Equal(errTest, stdlib.Unwrap(err))
|
||||
}
|
||||
|
||||
func (s *errorsTestSuite) TestWrappedUnwrapMultiple() {
|
||||
var errTest = errors.New("test error")
|
||||
var errTest2 = errors.New("test error 2")
|
||||
err := Wrap(errTest2, Wrap(errTest, "some random description").Error())
|
||||
s.Require().Equal(errTest2, stdlib.Unwrap(err))
|
||||
}
|
||||
|
||||
func (s *errorsTestSuite) TestWrappedUnwrapFail() {
|
||||
var errTest = errors.New("test error")
|
||||
var errTest2 = errors.New("test error 2")
|
||||
err := Wrap(errTest2, Wrap(errTest, "some random description").Error())
|
||||
s.Require().NotEqual(errTest, stdlib.Unwrap(err))
|
||||
}
|
||||
|
||||
func (s *errorsTestSuite) TestABCIError() {
|
||||
s.Require().Equal("custom: tx parse error", ABCIError(testCodespace, 2, "custom").Error())
|
||||
s.Require().Equal("custom: unknown", ABCIError("unknown", 1, "custom").Error())
|
||||
}
|
||||
|
||||
func ExampleWrap() {
|
||||
err1 := Wrap(ErrInsufficientFunds, "90 is smaller than 100")
|
||||
err2 := errors.Wrap(ErrInsufficientFunds, "90 is smaller than 100")
|
||||
fmt.Println(err1.Error())
|
||||
fmt.Println(err2.Error())
|
||||
// Output:
|
||||
// 90 is smaller than 100: insufficient funds
|
||||
// 90 is smaller than 100: insufficient funds
|
||||
}
|
||||
|
||||
func ExampleWrapf() {
|
||||
err1 := Wrap(ErrInsufficientFunds, "90 is smaller than 100")
|
||||
err2 := errors.Wrap(ErrInsufficientFunds, "90 is smaller than 100")
|
||||
fmt.Println(err1.Error())
|
||||
fmt.Println(err2.Error())
|
||||
// Output:
|
||||
// 90 is smaller than 100: insufficient funds
|
||||
// 90 is smaller than 100: insufficient funds
|
||||
}
|
||||
|
||||
const testCodespace = "testtesttest"
|
||||
|
||||
var (
|
||||
ErrTxDecode = Register(testCodespace, 2, "tx parse error")
|
||||
ErrInvalidSequence = Register(testCodespace, 3, "invalid sequence")
|
||||
ErrUnauthorized = Register(testCodespace, 4, "unauthorized")
|
||||
ErrInsufficientFunds = Register(testCodespace, 5, "insufficient funds")
|
||||
ErrUnknownRequest = Register(testCodespace, 6, "unknown request")
|
||||
ErrInvalidAddress = Register(testCodespace, 7, "invalid address")
|
||||
ErrInvalidPubKey = Register(testCodespace, 8, "invalid pubkey")
|
||||
ErrUnknownAddress = Register(testCodespace, 9, "unknown address")
|
||||
ErrInvalidCoins = Register(testCodespace, 10, "invalid coins")
|
||||
ErrOutOfGas = Register(testCodespace, 11, "out of gas")
|
||||
ErrInsufficientFee = Register(testCodespace, 13, "insufficient fee")
|
||||
ErrTooManySignatures = Register(testCodespace, 14, "maximum number of signatures exceeded")
|
||||
ErrNoSignatures = Register(testCodespace, 15, "no signatures supplied")
|
||||
ErrJSONMarshal = Register(testCodespace, 16, "failed to marshal JSON bytes")
|
||||
ErrJSONUnmarshal = Register(testCodespace, 17, "failed to unmarshal JSON bytes")
|
||||
ErrInvalidRequest = Register(testCodespace, 18, "invalid request")
|
||||
ErrMempoolIsFull = Register(testCodespace, 20, "mempool is full")
|
||||
ErrTxTooLarge = Register(testCodespace, 21, "tx too large")
|
||||
ErrKeyNotFound = Register(testCodespace, 22, "key not found")
|
||||
ErrorInvalidSigner = Register(testCodespace, 24, "tx intended signer does not match the given signer")
|
||||
ErrInvalidChainID = Register(testCodespace, 28, "invalid chain-id")
|
||||
ErrInvalidType = Register(testCodespace, 29, "invalid type")
|
||||
ErrUnknownExtensionOptions = Register(testCodespace, 31, "unknown extension options")
|
||||
ErrPackAny = Register(testCodespace, 33, "failed packing protobuf message to Any")
|
||||
ErrLogic = Register(testCodespace, 35, "internal logic error")
|
||||
ErrConflict = Register(testCodespace, 36, "conflict")
|
||||
ErrNotSupported = Register(testCodespace, 37, "feature not supported")
|
||||
ErrNotFound = Register(testCodespace, 38, "not found")
|
||||
ErrIO = Register(testCodespace, 39, "Internal IO error")
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
module errors
|
||||
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/stretchr/testify v1.7.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -0,0 +1,12 @@
|
||||
package errors
|
||||
|
||||
import "fmt"
|
||||
|
||||
// AssertNil panics on error
|
||||
// Should be only used with interface methods, which require return error, but the
|
||||
// error is always nil
|
||||
func AssertNil(err error) {
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("logic error - this should never happen. %w", 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,62 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (s *errorsTestSuite) TestStackTrace() {
|
||||
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 _, tc := range cases {
|
||||
s.Require().True(reflect.DeepEqual(tc.err.Error(), tc.wantError))
|
||||
s.Require().NotNil(stackTrace(tc.err))
|
||||
fullStack := fmt.Sprintf("%+v", tc.err)
|
||||
s.Require().True(strings.Contains(fullStack, thisTestSrc))
|
||||
s.Require().True(strings.Contains(fullStack, tc.wantError))
|
||||
|
||||
for _, src := range unwantedSrc {
|
||||
if strings.Contains(fullStack, src) {
|
||||
s.T().Logf("Stack trace below\n----%s\n----", fullStack)
|
||||
s.T().Logf("full stack contains unwanted source file path: %q", src)
|
||||
}
|
||||
}
|
||||
|
||||
tinyStack := fmt.Sprintf("%v", tc.err)
|
||||
s.Require().True(strings.HasPrefix(tinyStack, tc.wantError))
|
||||
s.Require().False(strings.Contains(tinyStack, "\n"))
|
||||
// contains a link to where it was created, which must
|
||||
// be here, not the Wrap() function
|
||||
s.Require().True(strings.Contains(tinyStack, thisTestSrc))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user