Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(integration): allow to run begin and endblocker easily #15732

Merged
merged 9 commits into from
Apr 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
41 changes: 34 additions & 7 deletions testutil/integration/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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))
Expand All @@ -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 {
Expand All @@ -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)
}
Expand Down
25 changes: 25 additions & 0 deletions testutil/integration/options.go
Original file line number Diff line number Diff line change
@@ -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
}
}
67 changes: 55 additions & 12 deletions testutil/integration/router.go
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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,
Expand All @@ -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)
Expand All @@ -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
}