feat(integration): allow to run begin and endblocker easily (#15732)
This commit is contained in:
parent
0a70647dea
commit
9a5413dbaa
@ -40,6 +40,7 @@ Ref: https://keepachangelog.com/en/1.0.0/
|
||||
|
||||
### Features
|
||||
|
||||
* (testutil/integration) [#15556](https://github.com/cosmos/cosmos-sdk/pull/15556) Introduce `testutil/integration` package for module integration testing.
|
||||
* (types) [#15735](https://github.com/cosmos/cosmos-sdk/pull/15735) Make `ValidateBasic() error` method of `Msg` interface optional. Modules should validate messages directly in their message handlers ([RFC 001](https://docs.cosmos.network/main/rfc/rfc-001-tx-validation)).
|
||||
* (x/genutil) [#15679](https://github.com/cosmos/cosmos-sdk/pull/15679) Allow applications to specify a custom genesis migration function for the `genesis migrate` command.
|
||||
* (client) [#15458](https://github.com/cosmos/cosmos-sdk/pull/15458) Add a `CmdContext` field to client.Context initialized to cobra command's context.
|
||||
|
||||
@ -8,6 +8,7 @@ import (
|
||||
storetypes "cosmossdk.io/store/types"
|
||||
"github.com/cosmos/cosmos-sdk/runtime"
|
||||
"github.com/cosmos/cosmos-sdk/testutil/integration"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
moduletestutil "github.com/cosmos/cosmos-sdk/types/module/testutil"
|
||||
"github.com/cosmos/cosmos-sdk/x/auth"
|
||||
authkeeper "github.com/cosmos/cosmos-sdk/x/auth/keeper"
|
||||
@ -46,8 +47,13 @@ func Example() {
|
||||
mintModule := mint.NewAppModule(encodingCfg.Codec, mintKeeper, accountKeeper, nil, nil)
|
||||
|
||||
// create the application and register all the modules from the previous step
|
||||
// replace the name and the logger by testing values in a real test case (e.g. t.Name() and log.NewTestLogger(t))
|
||||
integrationApp := integration.NewIntegrationApp("example", log.NewLogger(io.Discard), keys, authModule, mintModule)
|
||||
// replace the logger by testing values in a real test case (e.g. log.NewTestLogger(t))
|
||||
integrationApp := integration.NewIntegrationApp(
|
||||
log.NewLogger(io.Discard, log.OutputJSONOption()),
|
||||
keys,
|
||||
encodingCfg.Codec,
|
||||
authModule, mintModule,
|
||||
)
|
||||
|
||||
// register the message and query servers
|
||||
authtypes.RegisterMsgServer(integrationApp.MsgServiceRouter(), authkeeper.NewMsgServerImpl(accountKeeper))
|
||||
@ -79,8 +85,10 @@ func Example() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
sdkCtx := sdk.UnwrapSDKContext(integrationApp.Context())
|
||||
|
||||
// we should also check the state of the application
|
||||
got := mintKeeper.GetParams(integrationApp.SDKContext())
|
||||
got := mintKeeper.GetParams(sdkCtx)
|
||||
if diff := cmp.Diff(got, params); diff != "" {
|
||||
panic(diff)
|
||||
}
|
||||
@ -109,8 +117,13 @@ func Example_oneModule() {
|
||||
authModule := auth.NewAppModule(encodingCfg.Codec, accountKeeper, authsims.RandomGenesisAccounts, nil)
|
||||
|
||||
// create the application and register all the modules from the previous step
|
||||
// replace the name and the logger by testing values in a real test case (e.g. t.Name() and log.NewTestLogger(t))
|
||||
integrationApp := integration.NewIntegrationApp("example-one-module", log.NewLogger(io.Discard), keys, authModule)
|
||||
// replace the logger by testing values in a real test case (e.g. log.NewTestLogger(t))
|
||||
integrationApp := integration.NewIntegrationApp(
|
||||
log.NewLogger(io.Discard),
|
||||
keys,
|
||||
encodingCfg.Codec,
|
||||
authModule,
|
||||
)
|
||||
|
||||
// register the message and query servers
|
||||
authtypes.RegisterMsgServer(integrationApp.MsgServiceRouter(), authkeeper.NewMsgServerImpl(accountKeeper))
|
||||
@ -122,11 +135,23 @@ func Example_oneModule() {
|
||||
result, err := integrationApp.RunMsg(&authtypes.MsgUpdateParams{
|
||||
Authority: authority,
|
||||
Params: params,
|
||||
})
|
||||
},
|
||||
// this allows to the begin and end blocker of the module before and after the message
|
||||
integration.WithAutomaticBeginEndBlock(),
|
||||
// this allows to commit the state after the message
|
||||
integration.WithAutomaticCommit(),
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// verify that the begin and end blocker were called
|
||||
// NOTE: in this example, we are testing auth, which doesn't have any begin or end blocker
|
||||
// so verifying the block height is enough
|
||||
if integrationApp.LastBlockHeight() != 2 {
|
||||
panic(fmt.Errorf("expected block height to be 2, got %d", integrationApp.LastBlockHeight()))
|
||||
}
|
||||
|
||||
// in this example the result is an empty response, a nil check is enough
|
||||
// in other cases, it is recommended to check the result value.
|
||||
if result == nil {
|
||||
@ -140,8 +165,10 @@ func Example_oneModule() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
sdkCtx := sdk.UnwrapSDKContext(integrationApp.Context())
|
||||
|
||||
// we should also check the state of the application
|
||||
got := accountKeeper.GetParams(integrationApp.SDKContext())
|
||||
got := accountKeeper.GetParams(sdkCtx)
|
||||
if diff := cmp.Diff(got, params); diff != "" {
|
||||
panic(diff)
|
||||
}
|
||||
|
||||
25
testutil/integration/options.go
Normal file
25
testutil/integration/options.go
Normal file
@ -0,0 +1,25 @@
|
||||
package integration
|
||||
|
||||
// Config is the configuration for the integration app.
|
||||
type Config struct {
|
||||
AutomaticBeginEndBlock bool
|
||||
AutomaticCommit bool
|
||||
}
|
||||
|
||||
// Option is a function that can be used to configure the integration app.
|
||||
type Option func(*Config)
|
||||
|
||||
// WithAutomaticBlockCreation enables begin/end block calls.
|
||||
func WithAutomaticBeginEndBlock() Option {
|
||||
return func(cfg *Config) {
|
||||
cfg.AutomaticBeginEndBlock = true
|
||||
}
|
||||
}
|
||||
|
||||
// WithAutomaticCommit enables automatic commit.
|
||||
// This means that the integration app will automatically commit the state after each msgs.
|
||||
func WithAutomaticCommit() Option {
|
||||
return func(cfg *Config) {
|
||||
cfg.AutomaticCommit = true
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,10 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/cometbft/cometbft/abci/types"
|
||||
cmtabcitypes "github.com/cometbft/cometbft/abci/types"
|
||||
cmtproto "github.com/cometbft/cometbft/proto/tendermint/types"
|
||||
|
||||
"cosmossdk.io/log"
|
||||
@ -18,18 +19,19 @@ import (
|
||||
authtx "github.com/cosmos/cosmos-sdk/x/auth/tx"
|
||||
)
|
||||
|
||||
const appName = "integration-app"
|
||||
|
||||
// App is a test application that can be used to test the integration of modules.
|
||||
type App struct {
|
||||
*baseapp.BaseApp
|
||||
|
||||
ctx sdk.Context
|
||||
logger log.Logger
|
||||
|
||||
ctx sdk.Context
|
||||
logger log.Logger
|
||||
queryHelper *baseapp.QueryServiceTestHelper
|
||||
}
|
||||
|
||||
// NewIntegrationApp creates an application for testing purposes. This application is able to route messages to their respective handlers.
|
||||
func NewIntegrationApp(nameSuffix string, logger log.Logger, keys map[string]*storetypes.KVStoreKey, modules ...module.AppModuleBasic) *App {
|
||||
func NewIntegrationApp(logger log.Logger, keys map[string]*storetypes.KVStoreKey, appCodec codec.Codec, modules ...module.AppModule) *App {
|
||||
db := dbm.NewMemDB()
|
||||
|
||||
interfaceRegistry := codectypes.NewInterfaceRegistry()
|
||||
@ -38,11 +40,25 @@ func NewIntegrationApp(nameSuffix string, logger log.Logger, keys map[string]*st
|
||||
}
|
||||
|
||||
txConfig := authtx.NewTxConfig(codec.NewProtoCodec(interfaceRegistry), authtx.DefaultSignModes)
|
||||
|
||||
bApp := baseapp.NewBaseApp(fmt.Sprintf("integration-app-%s", nameSuffix), logger, db, txConfig.TxDecoder())
|
||||
bApp := baseapp.NewBaseApp(appName, logger, db, txConfig.TxDecoder(), baseapp.SetChainID(appName))
|
||||
bApp.MountKVStores(keys)
|
||||
bApp.SetInitChainer(func(ctx sdk.Context, req types.RequestInitChain) (types.ResponseInitChain, error) {
|
||||
return types.ResponseInitChain{}, nil
|
||||
|
||||
bApp.SetInitChainer(func(ctx sdk.Context, req cmtabcitypes.RequestInitChain) (cmtabcitypes.ResponseInitChain, error) {
|
||||
for _, mod := range modules {
|
||||
if m, ok := mod.(module.HasGenesis); ok {
|
||||
m.InitGenesis(ctx, appCodec, m.DefaultGenesis(appCodec))
|
||||
}
|
||||
}
|
||||
|
||||
return cmtabcitypes.ResponseInitChain{}, nil
|
||||
})
|
||||
|
||||
moduleManager := module.NewManager(modules...)
|
||||
bApp.SetBeginBlocker(func(ctx sdk.Context, req cmtabcitypes.RequestBeginBlock) (cmtabcitypes.ResponseBeginBlock, error) {
|
||||
return moduleManager.BeginBlock(ctx, req)
|
||||
})
|
||||
bApp.SetEndBlocker(func(ctx sdk.Context, req cmtabcitypes.RequestEndBlock) (cmtabcitypes.ResponseEndBlock, error) {
|
||||
return moduleManager.EndBlock(ctx, req)
|
||||
})
|
||||
|
||||
router := baseapp.NewMsgServiceRouter()
|
||||
@ -53,7 +69,10 @@ func NewIntegrationApp(nameSuffix string, logger log.Logger, keys map[string]*st
|
||||
panic(fmt.Errorf("failed to load application version from store: %w", err))
|
||||
}
|
||||
|
||||
ctx := bApp.NewContext(true, cmtproto.Header{})
|
||||
bApp.InitChain(cmtabcitypes.RequestInitChain{ChainId: appName})
|
||||
bApp.Commit()
|
||||
|
||||
ctx := bApp.NewContext(true, cmtproto.Header{ChainID: appName})
|
||||
|
||||
return &App{
|
||||
BaseApp: bApp,
|
||||
@ -70,7 +89,27 @@ func NewIntegrationApp(nameSuffix string, logger log.Logger, keys map[string]*st
|
||||
// The result of the message execution is returned as a Any type.
|
||||
// That any type can be unmarshaled to the expected response type.
|
||||
// If the message execution fails, an error is returned.
|
||||
func (app *App) RunMsg(msg sdk.Msg) (*codectypes.Any, error) {
|
||||
func (app *App) RunMsg(msg sdk.Msg, option ...Option) (*codectypes.Any, error) {
|
||||
// set options
|
||||
cfg := Config{}
|
||||
for _, opt := range option {
|
||||
opt(&cfg)
|
||||
}
|
||||
|
||||
if cfg.AutomaticCommit {
|
||||
defer app.Commit()
|
||||
}
|
||||
|
||||
if cfg.AutomaticBeginEndBlock {
|
||||
height := app.LastBlockHeight() + 1
|
||||
app.logger.Info("Running beging block", "height", height)
|
||||
app.BeginBlock(cmtabcitypes.RequestBeginBlock{Header: cmtproto.Header{Height: height, ChainID: appName}})
|
||||
defer func() {
|
||||
app.logger.Info("Running end block", "height", height)
|
||||
app.EndBlock(cmtabcitypes.RequestEndBlock{})
|
||||
}()
|
||||
}
|
||||
|
||||
app.logger.Info("Running msg", "msg", msg.String())
|
||||
|
||||
handler := app.MsgServiceRouter().Handler(msg)
|
||||
@ -96,10 +135,14 @@ func (app *App) RunMsg(msg sdk.Msg) (*codectypes.Any, error) {
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (app *App) SDKContext() sdk.Context {
|
||||
// Context returns the application context.
|
||||
// It can be unwraped to a sdk.Context, with the sdk.UnwrapSDKContext function.
|
||||
func (app *App) Context() context.Context {
|
||||
return app.ctx
|
||||
}
|
||||
|
||||
// QueryHelper returns the application query helper.
|
||||
// It can be used when registering query services.
|
||||
func (app *App) QueryHelper() *baseapp.QueryServiceTestHelper {
|
||||
return app.queryHelper
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user