baseapp: refactor tests

* simplify mock tx type, msgs, and handlers
* remove dependencies on auth and bank
* dedup with setupBaseApp
* lots of comments and cleanup
* fixes where we weren't checking results
* use some table driven tests
* remove TestValidatorChange - its not testing anything since baseapp
doesnt track validator changes
* prepare for CheckTx only running the AnteHandler
* fix runTx gas handling and add more tests
* new tests for multi-msgs
This commit is contained in:
Ethan Buchman 2018-07-06 10:47:07 -04:00
parent 51a50210e9
commit fe7ae1151d
4 changed files with 568 additions and 744 deletions

View File

@ -122,6 +122,11 @@ func (app *BaseApp) SetTxDecoder(txDecoder sdk.TxDecoder) {
}
// default custom logic for transaction decoding
// TODO: remove auth and wire dependencies from baseapp
// - move this to auth.DefaultTxDecoder
// - set the default here to JSON decode like docs/examples/app1 (it will fail
// for multiple messages ;))
// - pass a TxDecoder into NewBaseApp, instead of a codec.
func defaultTxDecoder(cdc *wire.Codec) sdk.TxDecoder {
return func(txBytes []byte) (sdk.Tx, sdk.Error) {
var tx = auth.StdTx{}
@ -370,6 +375,8 @@ func (app *BaseApp) Query(req abci.RequestQuery) (res abci.ResponseQuery) {
return app.FilterPeerByAddrPort(path[3])
}
if path[2] == "pubkey" {
// TODO: this should be changed to `id`
// NOTE: this changed in tendermint and we didn't notice...
return app.FilterPeerByPubKey(path[3])
}
}
@ -468,28 +475,38 @@ func (app *BaseApp) Deliver(tx sdk.Tx) (result sdk.Result) {
// txBytes may be nil in some cases, eg. in tests.
// Also, in the future we may support "internal" transactions.
func (app *BaseApp) runTx(mode runTxMode, txBytes []byte, tx sdk.Tx) (result sdk.Result) {
//NOTE: GasWanted should be returned by the AnteHandler.
// GasUsed is determined by the GasMeter.
// We need access to the context to get the gas meter so
// we initialize upfront
var gasWanted int64
ctx := app.getContextForAnte(mode, txBytes)
// Handle any panics.
defer func() {
if r := recover(); r != nil {
switch r.(type) {
switch rType := r.(type) {
case sdk.ErrorOutOfGas:
log := fmt.Sprintf("out of gas in location: %v", r.(sdk.ErrorOutOfGas).Descriptor)
log := fmt.Sprintf("out of gas in location: %v", rType.Descriptor)
result = sdk.ErrOutOfGas(log).Result()
default:
log := fmt.Sprintf("recovered: %v\nstack:\n%v", r, string(debug.Stack()))
result = sdk.ErrInternal(log).Result()
}
}
result.GasWanted = gasWanted
result.GasUsed = ctx.GasMeter().GasConsumed()
}()
// Get the Msg.
var msgs = tx.GetMsgs()
if msgs == nil || len(msgs) == 0 {
// TODO: probably shouldn't be ErrInternal. Maybe new ErrInvalidMessage, or ?
return sdk.ErrInternal("Tx.GetMsgs() must return at least one message in list").Result()
}
for _, msg := range msgs {
// Validate the Msg
// Validate the Msg.
err := msg.ValidateBasic()
if err != nil {
err = err.WithDefaultCodespace(sdk.CodespaceRoot)
@ -497,29 +514,16 @@ func (app *BaseApp) runTx(mode runTxMode, txBytes []byte, tx sdk.Tx) (result sdk
}
}
// Get the context
var ctx sdk.Context
if mode == runTxModeCheck || mode == runTxModeSimulate {
ctx = app.checkState.ctx.WithTxBytes(txBytes)
} else {
ctx = app.deliverState.ctx.WithTxBytes(txBytes)
ctx = ctx.WithSigningValidators(app.signedValidators)
}
// Simulate a DeliverTx for gas calculation
if mode == runTxModeSimulate {
ctx = ctx.WithIsCheckTx(false)
}
// Run the ante handler.
if app.anteHandler != nil {
newCtx, result, abort := app.anteHandler(ctx, tx)
newCtx, anteResult, abort := app.anteHandler(ctx, tx)
if abort {
return result
return anteResult
}
if !newCtx.IsZero() {
ctx = newCtx
}
gasWanted = anteResult.GasWanted
}
// Get the correct cache
@ -534,9 +538,12 @@ func (app *BaseApp) runTx(mode runTxMode, txBytes []byte, tx sdk.Tx) (result sdk
ctx = ctx.WithMultiStore(msCache)
}
finalResult := sdk.Result{}
// accumulate results
var logs []string
for i, msg := range msgs {
var data []byte
var tags sdk.Tags
var code sdk.ABCICodeType
for msgIdx, msg := range msgs {
// Match route.
msgType := msg.Type()
handler := app.router.Route(msgType)
@ -544,43 +551,61 @@ func (app *BaseApp) runTx(mode runTxMode, txBytes []byte, tx sdk.Tx) (result sdk
return sdk.ErrUnknownRequest("Unrecognized Msg type: " + msgType).Result()
}
result = handler(ctx, msg)
msgResult := handler(ctx, msg)
// Set gas utilized
finalResult.GasUsed += ctx.GasMeter().GasConsumed()
finalResult.GasWanted += result.GasWanted
// NOTE: GasWanted is determined by ante handler and
// GasUsed by the GasMeter
// Append Data and Tags
finalResult.Data = append(finalResult.Data, result.Data...)
finalResult.Tags = append(finalResult.Tags, result.Tags...)
// Construct usable logs in multi-message transactions. Messages are 1-indexed in logs.
logs = append(logs, fmt.Sprintf("Msg %d: %s", i+1, finalResult.Log))
data = append(data, msgResult.Data...)
tags = append(tags, msgResult.Tags...)
// Stop execution and return on first failed message.
if !result.IsOK() {
if len(msgs) == 1 {
return result
}
result.GasUsed = finalResult.GasUsed
if i == 0 {
result.Log = fmt.Sprintf("Msg 1 failed: %s", result.Log)
} else {
result.Log = fmt.Sprintf("Msg 1-%d Passed. Msg %d failed: %s", i, i+1, result.Log)
}
return result
if !msgResult.IsOK() {
logs = append(logs, fmt.Sprintf("Msg %d failed: %s", msgIdx, msgResult.Log))
code = msgResult.Code
break
}
// Construct usable logs in multi-message transactions.
logs = append(logs, fmt.Sprintf("Msg %d: %s", msgIdx, msgResult.Log))
}
// If not a simulated run and result was successful, write to app.checkState.ms or app.deliverState.ms
// Only update state if all messages pass.
if mode != runTxModeSimulate && result.IsOK() {
// Set the final gas values.
result = sdk.Result{
Code: code,
Data: data,
Log: strings.Join(logs, "\n"),
GasWanted: gasWanted,
GasUsed: ctx.GasMeter().GasConsumed(),
// TODO: FeeAmount/FeeDenom
Tags: tags,
}
// Only update state if all messages pass and we're not in a simulation.
if result.IsOK() && mode != runTxModeSimulate {
msCache.Write()
}
finalResult.Log = strings.Join(logs, "\n")
return result
}
return finalResult
func (app *BaseApp) getContextForAnte(mode runTxMode, txBytes []byte) sdk.Context {
var ctx sdk.Context
// Get the context.
if mode == runTxModeCheck || mode == runTxModeSimulate {
ctx = app.checkState.ctx.WithTxBytes(txBytes)
} else {
ctx = app.deliverState.ctx.WithTxBytes(txBytes)
ctx = ctx.WithSigningValidators(app.signedValidators)
}
// Simulate a DeliverTx for gas calculation.
if mode == runTxModeSimulate {
ctx = ctx.WithIsCheckTx(false)
}
return ctx
}
// Implements ABCI

File diff suppressed because it is too large Load Diff

View File

@ -1,352 +0,0 @@
package baseapp
import (
"encoding/json"
"fmt"
"testing"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/wire"
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/stretchr/testify/require"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/crypto"
)
// tests multiple msgs of same type from same address in single tx
func TestMultipleBurn(t *testing.T) {
// Create app.
app := newTestApp(t.Name())
capKey := sdk.NewKVStoreKey("key")
app.MountStoresIAVL(capKey)
app.SetTxDecoder(func(txBytes []byte) (sdk.Tx, sdk.Error) {
var tx auth.StdTx
fromJSON(txBytes, &tx)
return tx, nil
})
err := app.LoadLatestVersion(capKey)
if err != nil {
panic(err)
}
app.accountMapper = auth.NewAccountMapper(app.cdc, capKey, &auth.BaseAccount{})
app.SetAnteHandler(auth.NewAnteHandler(app.accountMapper, auth.FeeCollectionKeeper{}))
app.Router().
AddRoute("burn", newHandleBurn(app.accountMapper)).
AddRoute("send", newHandleSpend(app.accountMapper))
app.InitChain(abci.RequestInitChain{})
app.BeginBlock(abci.RequestBeginBlock{})
// Set chain-id
app.deliverState.ctx = app.deliverState.ctx.WithChainID(t.Name())
priv := makePrivKey("my secret")
addr := priv.PubKey().Address()
addCoins(app.accountMapper, app.deliverState.ctx, addr, sdk.Coins{{"foocoin", sdk.NewInt(100)}})
require.Equal(t, sdk.Coins{{"foocoin", sdk.NewInt(100)}}, app.accountMapper.GetAccount(app.deliverState.ctx, addr).GetCoins(), "Balance did not update")
msg := testBurnMsg{addr, sdk.Coins{{"foocoin", sdk.NewInt(50)}}}
tx := GenTx(t.Name(), []sdk.Msg{msg, msg}, []int64{0}, []int64{0}, priv)
res := app.Deliver(tx)
require.Equal(t, true, res.IsOK(), res.Log)
require.Equal(t, sdk.Coins(nil), getCoins(app.accountMapper, app.deliverState.ctx, addr), "Double burn did not work")
}
// tests multiples msgs of same type from different addresses in single tx
func TestBurnMultipleOwners(t *testing.T) {
// Create app.
app := newTestApp(t.Name())
capKey := sdk.NewKVStoreKey("key")
app.MountStoresIAVL(capKey)
app.SetTxDecoder(func(txBytes []byte) (sdk.Tx, sdk.Error) {
var tx auth.StdTx
fromJSON(txBytes, &tx)
return tx, nil
})
err := app.LoadLatestVersion(capKey)
if err != nil {
panic(err)
}
app.accountMapper = auth.NewAccountMapper(app.cdc, capKey, &auth.BaseAccount{})
app.SetAnteHandler(auth.NewAnteHandler(app.accountMapper, auth.FeeCollectionKeeper{}))
app.Router().
AddRoute("burn", newHandleBurn(app.accountMapper)).
AddRoute("send", newHandleSpend(app.accountMapper))
app.InitChain(abci.RequestInitChain{})
app.BeginBlock(abci.RequestBeginBlock{})
// Set chain-id
app.deliverState.ctx = app.deliverState.ctx.WithChainID(t.Name())
priv1 := makePrivKey("my secret 1")
addr1 := priv1.PubKey().Address()
priv2 := makePrivKey("my secret 2")
addr2 := priv2.PubKey().Address()
// fund accounts
addCoins(app.accountMapper, app.deliverState.ctx, addr1, sdk.Coins{{"foocoin", sdk.NewInt(100)}})
addCoins(app.accountMapper, app.deliverState.ctx, addr2, sdk.Coins{{"foocoin", sdk.NewInt(100)}})
require.Equal(t, sdk.Coins{{"foocoin", sdk.NewInt(100)}}, getCoins(app.accountMapper, app.deliverState.ctx, addr1), "Balance1 did not update")
require.Equal(t, sdk.Coins{{"foocoin", sdk.NewInt(100)}}, getCoins(app.accountMapper, app.deliverState.ctx, addr2), "Balance2 did not update")
msg1 := testBurnMsg{addr1, sdk.Coins{{"foocoin", sdk.NewInt(100)}}}
msg2 := testBurnMsg{addr2, sdk.Coins{{"foocoin", sdk.NewInt(100)}}}
// test wrong signers: Address 1 signs both messages
tx := GenTx(t.Name(), []sdk.Msg{msg1, msg2}, []int64{0, 0}, []int64{0, 0}, priv1, priv1)
res := app.Deliver(tx)
require.Equal(t, sdk.ABCICodeType(0x10003), res.Code, "Wrong signatures passed")
require.Equal(t, sdk.Coins{{"foocoin", sdk.NewInt(100)}}, getCoins(app.accountMapper, app.deliverState.ctx, addr1), "Balance1 changed after invalid sig")
require.Equal(t, sdk.Coins{{"foocoin", sdk.NewInt(100)}}, getCoins(app.accountMapper, app.deliverState.ctx, addr2), "Balance2 changed after invalid sig")
// test valid tx
tx = GenTx(t.Name(), []sdk.Msg{msg1, msg2}, []int64{0, 1}, []int64{1, 0}, priv1, priv2)
res = app.Deliver(tx)
require.Equal(t, true, res.IsOK(), res.Log)
require.Equal(t, sdk.Coins(nil), getCoins(app.accountMapper, app.deliverState.ctx, addr1), "Balance1 did not change after valid tx")
require.Equal(t, sdk.Coins(nil), getCoins(app.accountMapper, app.deliverState.ctx, addr2), "Balance2 did not change after valid tx")
}
func getCoins(am auth.AccountMapper, ctx sdk.Context, addr sdk.Address) sdk.Coins {
return am.GetAccount(ctx, addr).GetCoins()
}
func addCoins(am auth.AccountMapper, ctx sdk.Context, addr sdk.Address, coins sdk.Coins) sdk.Error {
acc := am.GetAccount(ctx, addr)
if acc == nil {
acc = am.NewAccountWithAddress(ctx, addr)
}
err := acc.SetCoins(acc.GetCoins().Plus(coins))
if err != nil {
fmt.Println(err)
return sdk.ErrInternal(err.Error())
}
am.SetAccount(ctx, acc)
return nil
}
// tests different msg types in single tx with different addresses
func TestSendBurn(t *testing.T) {
// Create app.
app := newTestApp(t.Name())
capKey := sdk.NewKVStoreKey("key")
app.MountStoresIAVL(capKey)
app.SetTxDecoder(func(txBytes []byte) (sdk.Tx, sdk.Error) {
var tx auth.StdTx
fromJSON(txBytes, &tx)
return tx, nil
})
err := app.LoadLatestVersion(capKey)
if err != nil {
panic(err)
}
app.accountMapper = auth.NewAccountMapper(app.cdc, capKey, &auth.BaseAccount{})
app.SetAnteHandler(auth.NewAnteHandler(app.accountMapper, auth.FeeCollectionKeeper{}))
app.Router().
AddRoute("burn", newHandleBurn(app.accountMapper)).
AddRoute("send", newHandleSpend(app.accountMapper))
app.InitChain(abci.RequestInitChain{})
app.BeginBlock(abci.RequestBeginBlock{})
// Set chain-id
app.deliverState.ctx = app.deliverState.ctx.WithChainID(t.Name())
priv1 := makePrivKey("my secret 1")
addr1 := priv1.PubKey().Address()
priv2 := makePrivKey("my secret 2")
addr2 := priv2.PubKey().Address()
// fund accounts
addCoins(app.accountMapper, app.deliverState.ctx, addr1, sdk.Coins{{"foocoin", sdk.NewInt(100)}})
acc := app.accountMapper.NewAccountWithAddress(app.deliverState.ctx, addr2)
app.accountMapper.SetAccount(app.deliverState.ctx, acc)
require.Equal(t, sdk.Coins{{"foocoin", sdk.NewInt(100)}}, getCoins(app.accountMapper, app.deliverState.ctx, addr1), "Balance1 did not update")
sendMsg := testSendMsg{addr1, addr2, sdk.Coins{{"foocoin", sdk.NewInt(50)}}}
msg1 := testBurnMsg{addr1, sdk.Coins{{"foocoin", sdk.NewInt(50)}}}
msg2 := testBurnMsg{addr2, sdk.Coins{{"foocoin", sdk.NewInt(50)}}}
// send then burn
tx := GenTx(t.Name(), []sdk.Msg{sendMsg, msg2, msg1}, []int64{0, 1}, []int64{0, 0}, priv1, priv2)
res := app.Deliver(tx)
require.Equal(t, true, res.IsOK(), res.Log)
require.Equal(t, sdk.Coins(nil), getCoins(app.accountMapper, app.deliverState.ctx, addr1), "Balance1 did not change after valid tx")
require.Equal(t, sdk.Coins(nil), getCoins(app.accountMapper, app.deliverState.ctx, addr2), "Balance2 did not change after valid tx")
// Check that state is only updated if all msgs in tx pass.
addCoins(app.accountMapper, app.deliverState.ctx, addr1, sdk.Coins{{"foocoin", sdk.NewInt(50)}})
// burn then send, with fee thats greater than individual tx, but less than combination
tx = GenTxWithFeeAmt(50000, t.Name(), []sdk.Msg{msg1, sendMsg}, []int64{0}, []int64{1}, priv1)
res = app.Deliver(tx)
require.Equal(t, sdk.ABCICodeType(0x1000c), res.Code, "Allowed tx to pass with insufficient funds")
// Double check that state is correct after Commit.
app.EndBlock(abci.RequestEndBlock{})
app.Commit()
app.BeginBlock(abci.RequestBeginBlock{})
app.deliverState.ctx = app.deliverState.ctx.WithChainID(t.Name())
require.Equal(t, sdk.Coins{{"foocoin", sdk.NewInt(50)}}, getCoins(app.accountMapper, app.deliverState.ctx, addr1), "Allowed valid msg to pass in invalid tx")
require.Equal(t, sdk.Coins(nil), getCoins(app.accountMapper, app.deliverState.ctx, addr2), "Balance2 changed after invalid tx")
}
// Use burn and send msg types to test multiple msgs in one tx
type testBurnMsg struct {
Addr sdk.Address
Amount sdk.Coins
}
const msgType3 = "burn"
func (msg testBurnMsg) Type() string { return msgType3 }
func (msg testBurnMsg) GetSignBytes() []byte {
bz, _ := json.Marshal(msg)
return sdk.MustSortJSON(bz)
}
func (msg testBurnMsg) ValidateBasic() sdk.Error {
if msg.Addr == nil {
return sdk.ErrInvalidAddress("Cannot use nil as Address")
}
return nil
}
func (msg testBurnMsg) GetSigners() []sdk.Address {
return []sdk.Address{msg.Addr}
}
type testSendMsg struct {
Sender sdk.Address
Receiver sdk.Address
Amount sdk.Coins
}
const msgType4 = "send"
func (msg testSendMsg) Type() string { return msgType4 }
func (msg testSendMsg) GetSignBytes() []byte {
bz, _ := json.Marshal(msg)
return sdk.MustSortJSON(bz)
}
func (msg testSendMsg) ValidateBasic() sdk.Error {
if msg.Sender == nil || msg.Receiver == nil {
return sdk.ErrInvalidAddress("Cannot use nil as Address")
}
return nil
}
func (msg testSendMsg) GetSigners() []sdk.Address {
return []sdk.Address{msg.Sender}
}
// Simple Handlers for burn and send
func newHandleBurn(am auth.AccountMapper) sdk.Handler {
return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
ctx.GasMeter().ConsumeGas(20000, "burning coins")
burnMsg := msg.(testBurnMsg)
err := addCoins(am, ctx, burnMsg.Addr, burnMsg.Amount.Negative())
if err != nil {
return err.Result()
}
return sdk.Result{}
}
}
func newHandleSpend(am auth.AccountMapper) sdk.Handler {
return func(ctx sdk.Context, msg sdk.Msg) sdk.Result {
ctx.GasMeter().ConsumeGas(40000, "spending coins")
spendMsg := msg.(testSendMsg)
err := addCoins(am, ctx, spendMsg.Sender, spendMsg.Amount.Negative())
if err != nil {
return err.Result()
}
err = addCoins(am, ctx, spendMsg.Receiver, spendMsg.Amount)
if err != nil {
return err.Result()
}
return sdk.Result{}
}
}
// generate a signed transaction
func GenTx(chainID string, msgs []sdk.Msg, accnums []int64, seq []int64, priv ...crypto.PrivKey) auth.StdTx {
return GenTxWithFeeAmt(100000, chainID, msgs, accnums, seq, priv...)
}
// generate a signed transaction with the given fee amount
func GenTxWithFeeAmt(feeAmt int64, chainID string, msgs []sdk.Msg, accnums []int64, seq []int64, priv ...crypto.PrivKey) auth.StdTx {
// make the transaction free
fee := auth.StdFee{
sdk.Coins{{"foocoin", sdk.NewInt(0)}},
feeAmt,
}
sigs := make([]auth.StdSignature, len(priv))
for i, p := range priv {
sig, err := p.Sign(auth.StdSignBytes(chainID, accnums[i], seq[i], fee, msgs, ""))
// TODO: replace with proper error handling:
if err != nil {
panic(err)
}
sigs[i] = auth.StdSignature{
PubKey: p.PubKey(),
Signature: sig,
AccountNumber: accnums[i],
Sequence: seq[i],
}
}
return auth.NewStdTx(msgs, fee, sigs, "")
}
// spin up simple app for testing
type testApp struct {
*BaseApp
accountMapper auth.AccountMapper
}
func newTestApp(name string) testApp {
return testApp{
BaseApp: newBaseApp(name),
}
}
func MakeCodec() *wire.Codec {
cdc := wire.NewCodec()
cdc.RegisterInterface((*sdk.Msg)(nil), nil)
crypto.RegisterAmino(cdc)
cdc.RegisterInterface((*auth.Account)(nil), nil)
cdc.RegisterConcrete(&auth.BaseAccount{}, "cosmos-sdk/BaseAccount", nil)
cdc.Seal()
return cdc
}

View File

@ -36,7 +36,7 @@ var isAlpha = regexp.MustCompile(`^[a-zA-Z]+$`).MatchString
// AddRoute - TODO add description
func (rtr *router) AddRoute(r string, h sdk.Handler) Router {
if !isAlpha(r) {
panic("route expressions can only contain alphanumeric characters")
panic("route expressions can only contain alphabet characters")
}
rtr.routes = append(rtr.routes, route{r, h})