diff --git a/errors/common.go b/errors/common.go index 6d71fe922..addc06d2c 100644 --- a/errors/common.go +++ b/errors/common.go @@ -22,6 +22,10 @@ const ( msgTooManySignatures = "Too many signatures" ) +func InternalError(msg string) TMError { + return New(msg, abci.CodeType_InternalError) +} + func DecodingError() TMError { return New(msgDecoding, abci.CodeType_EncodingError) } diff --git a/stack/helpers.go b/stack/helpers.go index ce99e29e6..dc81e3e8f 100644 --- a/stack/helpers.go +++ b/stack/helpers.go @@ -7,75 +7,77 @@ import ( ) const ( - NameVoid = "void" + NameOK = "ok" NameFail = "fail" NamePanic = "panic" ) -// voidHandler just used to return okay to everything -type voidHandler struct{} +// OKHandler just used to return okay to everything +type OKHandler struct { + Log string +} -var _ basecoin.Handler = voidHandler{} +var _ basecoin.Handler = OKHandler{} -func (_ voidHandler) Name() string { - return NameVoid +func (_ OKHandler) Name() string { + return NameOK } // CheckTx always returns an empty success tx -func (_ voidHandler) CheckTx(ctx basecoin.Context, store types.KVStore, tx basecoin.Tx) (res basecoin.Result, err error) { - return +func (ok OKHandler) CheckTx(ctx basecoin.Context, store types.KVStore, tx basecoin.Tx) (res basecoin.Result, err error) { + return basecoin.Result{Log: ok.Log}, nil } // DeliverTx always returns an empty success tx -func (_ voidHandler) DeliverTx(ctx basecoin.Context, store types.KVStore, tx basecoin.Tx) (res basecoin.Result, err error) { - return +func (ok OKHandler) DeliverTx(ctx basecoin.Context, store types.KVStore, tx basecoin.Tx) (res basecoin.Result, err error) { + return basecoin.Result{Log: ok.Log}, nil } -// failHandler always returns an error -type failHandler struct { - err error +// FailHandler always returns an error +type FailHandler struct { + Err error } -var _ basecoin.Handler = failHandler{} +var _ basecoin.Handler = FailHandler{} -func (_ failHandler) Name() string { +func (_ FailHandler) Name() string { return NameFail } // CheckTx always returns the given error -func (f failHandler) CheckTx(ctx basecoin.Context, store types.KVStore, tx basecoin.Tx) (res basecoin.Result, err error) { - return res, errors.WithStack(f.err) +func (f FailHandler) CheckTx(ctx basecoin.Context, store types.KVStore, tx basecoin.Tx) (res basecoin.Result, err error) { + return res, errors.WithStack(f.Err) } // DeliverTx always returns the given error -func (f failHandler) DeliverTx(ctx basecoin.Context, store types.KVStore, tx basecoin.Tx) (res basecoin.Result, err error) { - return res, errors.WithStack(f.err) +func (f FailHandler) DeliverTx(ctx basecoin.Context, store types.KVStore, tx basecoin.Tx) (res basecoin.Result, err error) { + return res, errors.WithStack(f.Err) } -// panicHandler always panics, using the given error (first choice) or msg (fallback) -type panicHandler struct { - msg string - err error +// PanicHandler always panics, using the given error (first choice) or msg (fallback) +type PanicHandler struct { + Msg string + Err error } -var _ basecoin.Handler = panicHandler{} +var _ basecoin.Handler = PanicHandler{} -func (_ panicHandler) Name() string { +func (_ PanicHandler) Name() string { return NamePanic } // CheckTx always panics -func (p panicHandler) CheckTx(ctx basecoin.Context, store types.KVStore, tx basecoin.Tx) (res basecoin.Result, err error) { - if p.err != nil { - panic(p.err) +func (p PanicHandler) CheckTx(ctx basecoin.Context, store types.KVStore, tx basecoin.Tx) (res basecoin.Result, err error) { + if p.Err != nil { + panic(p.Err) } - panic(p.msg) + panic(p.Msg) } // DeliverTx always panics -func (p panicHandler) DeliverTx(ctx basecoin.Context, store types.KVStore, tx basecoin.Tx) (res basecoin.Result, err error) { - if p.err != nil { - panic(p.err) +func (p PanicHandler) DeliverTx(ctx basecoin.Context, store types.KVStore, tx basecoin.Tx) (res basecoin.Result, err error) { + if p.Err != nil { + panic(p.Err) } - panic(p.msg) + panic(p.Msg) } diff --git a/stack/helpers_test.go b/stack/helpers_test.go new file mode 100644 index 000000000..1beb5c0fa --- /dev/null +++ b/stack/helpers_test.go @@ -0,0 +1,61 @@ +package stack + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/tendermint/basecoin" + "github.com/tendermint/basecoin/types" +) + +func TestOK(t *testing.T) { + assert := assert.New(t) + + ctx := NewContext() + store := types.NewMemKVStore() + data := "this looks okay" + tx := basecoin.Tx{} + + ok := OKHandler{data} + res, err := ok.CheckTx(ctx, store, tx) + assert.Nil(err, "%+v", err) + assert.Equal(data, res.Log) + + res, err = ok.DeliverTx(ctx, store, tx) + assert.Nil(err, "%+v", err) + assert.Equal(data, res.Log) +} + +func TestFail(t *testing.T) { + assert := assert.New(t) + + ctx := NewContext() + store := types.NewMemKVStore() + msg := "big problem" + tx := basecoin.Tx{} + + fail := FailHandler{errors.New(msg)} + _, err := fail.CheckTx(ctx, store, tx) + if assert.NotNil(err) { + assert.Equal(msg, err.Error()) + } + + _, err = fail.DeliverTx(ctx, store, tx) + if assert.NotNil(err) { + assert.Equal(msg, err.Error()) + } +} + +func TestPanic(t *testing.T) { + assert := assert.New(t) + + ctx := NewContext() + store := types.NewMemKVStore() + msg := "system crash!" + tx := basecoin.Tx{} + + fail := PanicHandler{Msg: msg} + assert.Panics(func() { fail.CheckTx(ctx, store, tx) }) + assert.Panics(func() { fail.DeliverTx(ctx, store, tx) }) +} diff --git a/stack/recovery.go b/stack/recovery.go new file mode 100644 index 000000000..a572534fb --- /dev/null +++ b/stack/recovery.go @@ -0,0 +1,49 @@ +package stack + +import ( + "fmt" + + "github.com/tendermint/basecoin" + "github.com/tendermint/basecoin/errors" + "github.com/tendermint/basecoin/types" +) + +const ( + NameRecovery = "rcvr" +) + +// Recovery catches any panics and returns them as errors instead +type Recovery struct{} + +func (_ Recovery) Name() string { + return NameRecovery +} + +var _ Middleware = Recovery{} + +func (_ Recovery) CheckTx(ctx basecoin.Context, store types.KVStore, tx basecoin.Tx, next basecoin.Checker) (res basecoin.Result, err error) { + defer func() { + if r := recover(); r != nil { + err = normalizePanic(r) + } + }() + return next.CheckTx(ctx, store, tx) +} + +func (_ Recovery) DeliverTx(ctx basecoin.Context, store types.KVStore, tx basecoin.Tx, next basecoin.Deliver) (res basecoin.Result, err error) { + defer func() { + if r := recover(); r != nil { + err = normalizePanic(r) + } + }() + return next.DeliverTx(ctx, store, tx) +} + +// normalizePanic makes sure we can get a nice TMError (with stack) out of it +func normalizePanic(p interface{}) error { + if err, isErr := p.(error); isErr { + return errors.Wrap(err) + } + msg := fmt.Sprintf("%v", p) + return errors.InternalError(msg) +} diff --git a/stack/recovery_test.go b/stack/recovery_test.go new file mode 100644 index 000000000..1b55087dc --- /dev/null +++ b/stack/recovery_test.go @@ -0,0 +1,51 @@ +package stack + +import ( + "errors" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/tendermint/basecoin" + "github.com/tendermint/basecoin/types" +) + +func TestRecovery(t *testing.T) { + assert := assert.New(t) + + // generic args here... + ctx := NewContext() + store := types.NewMemKVStore() + tx := basecoin.Tx{} + + cases := []struct { + msg string // what to send to panic + err error // what to send to panic + expected string // expected text in panic + }{ + {"buzz", nil, "buzz"}, + {"", errors.New("owa!"), "owa!"}, + {"text", errors.New("error"), "error"}, + } + + for idx, tc := range cases { + i := strconv.Itoa(idx) + fail := PanicHandler{Msg: tc.msg, Err: tc.err} + rec := Recovery{} + app := NewStack(rec).Use(fail) + + // make sure check returns error, not a panic crash + _, err := app.CheckTx(ctx, store, tx) + if assert.NotNil(err, i) { + assert.Equal(tc.expected, err.Error(), i) + } + + // make sure deliver returns error, not a panic crash + _, err = app.DeliverTx(ctx, store, tx) + if assert.NotNil(err, i) { + assert.Equal(tc.expected, err.Error(), i) + } + + } + +}