cosmos-sdk/testutil/simsx/runner.go
2025-04-14 17:30:59 +00:00

418 lines
14 KiB
Go

package simsx
import (
"encoding/json"
"fmt"
"io"
"math"
"os"
"path/filepath"
"strings"
"testing"
dbm "github.com/cosmos/cosmos-db"
"github.com/stretchr/testify/require"
"cosmossdk.io/log"
"github.com/cosmos/cosmos-sdk/baseapp"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/codec"
"github.com/cosmos/cosmos-sdk/runtime"
servertypes "github.com/cosmos/cosmos-sdk/server/types"
simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/module"
simtypes "github.com/cosmos/cosmos-sdk/types/simulation"
"github.com/cosmos/cosmos-sdk/x/simulation"
"github.com/cosmos/cosmos-sdk/x/simulation/client/cli"
)
const SimAppChainID = "simulation-app"
// this list of seeds was imported from the original simulation runner: https://github.com/cosmos/tools/blob/v1.0.0/cmd/runsim/main.go#L32
var defaultSeeds = []int64{
1, 2, 4, 7,
32, 123, 124, 582, 1893, 2989,
3012, 4728, 37827, 981928, 87821, 891823782,
989182, 89182391, 11, 22, 44, 77, 99, 2020,
3232, 123123, 124124, 582582, 18931893,
29892989, 30123012, 47284728, 7601778, 8090485,
977367484, 491163361, 424254581, 673398983,
}
// SimStateFactory is a factory type that provides a convenient way to create a simulation state for testing.
// It contains the following fields:
// - Codec: a codec used for serializing other objects
// - AppStateFn: a function that returns the app state JSON bytes and the genesis accounts
// - BlockedAddr: a map of blocked addresses
// - AccountSource: an interface for retrieving accounts
// - BalanceSource: an interface for retrieving balance-related information
type SimStateFactory struct {
Codec codec.Codec
AppStateFn simtypes.AppStateFn
BlockedAddr map[string]bool
AccountSource AccountSourceX
BalanceSource BalanceSource
}
// SimulationApp abstract app that is used by sims
type SimulationApp interface {
runtime.AppI
SetNotSigverifyTx()
GetBaseApp() *baseapp.BaseApp
TxConfig() client.TxConfig
Close() error
}
// Run is a helper function that runs a simulation test with the given parameters.
// It calls the RunWithSeeds function with the default seeds and parameters.
//
// This is the entrypoint to run simulation tests that used to run with the runsim binary.
func Run[T SimulationApp](
t *testing.T,
appFactory func(
logger log.Logger,
db dbm.DB,
traceStore io.Writer,
loadLatest bool,
appOpts servertypes.AppOptions,
baseAppOptions ...func(*baseapp.BaseApp),
) T,
setupStateFactory func(app T) SimStateFactory,
postRunActions ...func(t testing.TB, app TestInstance[T], accs []simtypes.Account),
) {
t.Helper()
RunWithSeeds(t, appFactory, setupStateFactory, defaultSeeds, nil, postRunActions...)
}
// RunWithSeeds is a helper function that runs a simulation test with the given parameters.
// It iterates over the provided seeds and runs the simulation test for each seed in parallel.
//
// It sets up the environment, creates an instance of the simulation app,
// calls the simulation.SimulateFromSeed function to run the simulation, and performs post-run actions for each seed.
// The execution is deterministic and can be used for fuzz tests as well.
//
// The system under test is isolated for each run but unlike the old runsim command, there is no Process separation.
// This means, global caches may be reused for example. This implementation build upon the vanilla Go stdlib test framework.
func RunWithSeeds[T SimulationApp](
t *testing.T,
appFactory func(
logger log.Logger,
db dbm.DB,
traceStore io.Writer,
loadLatest bool,
appOpts servertypes.AppOptions,
baseAppOptions ...func(*baseapp.BaseApp),
) T,
setupStateFactory func(app T) SimStateFactory,
seeds []int64,
fuzzSeed []byte,
postRunActions ...func(t testing.TB, app TestInstance[T], accs []simtypes.Account),
) {
t.Helper()
RunWithSeedsAndRandAcc(t, appFactory, setupStateFactory, seeds, fuzzSeed, simtypes.RandomAccounts, postRunActions...)
}
// RunWithSeedsAndRandAcc calls RunWithSeeds with randAccFn
func RunWithSeedsAndRandAcc[T SimulationApp](
t *testing.T,
appFactory func(
logger log.Logger,
db dbm.DB,
traceStore io.Writer,
loadLatest bool,
appOpts servertypes.AppOptions,
baseAppOptions ...func(*baseapp.BaseApp),
) T,
setupStateFactory func(app T) SimStateFactory,
seeds []int64,
fuzzSeed []byte,
randAccFn simtypes.RandomAccountFn,
postRunActions ...func(t testing.TB, app TestInstance[T], accs []simtypes.Account),
) {
t.Helper()
if deprecatedParams := cli.GetDeprecatedFlagUsed(); len(deprecatedParams) != 0 {
fmt.Printf("Warning: Deprecated flag are used: %s", strings.Join(deprecatedParams, ","))
}
cfg := cli.NewConfigFromFlags()
cfg.ChainID = SimAppChainID
for i := range seeds {
seed := seeds[i]
t.Run(fmt.Sprintf("seed: %d", seed), func(t *testing.T) {
t.Parallel()
RunWithSeed(t, cfg, appFactory, setupStateFactory, seed, fuzzSeed, postRunActions...)
})
}
}
// RunWithSeed is a helper function that runs a simulation test with the given parameters.
// It iterates over the provided seeds and runs the simulation test for each seed in parallel.
//
// It sets up the environment, creates an instance of the simulation app,
// calls the simulation.SimulateFromSeed function to run the simulation, and performs post-run actions for the seed.
// The execution is deterministic and can be used for fuzz tests as well.
func RunWithSeed[T SimulationApp](
tb testing.TB,
cfg simtypes.Config,
appFactory func(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, appOpts servertypes.AppOptions, baseAppOptions ...func(*baseapp.BaseApp)) T,
setupStateFactory func(app T) SimStateFactory,
seed int64,
fuzzSeed []byte,
postRunActions ...func(t testing.TB, app TestInstance[T], accs []simtypes.Account),
) {
tb.Helper()
RunWithSeedAndRandAcc(tb, cfg, appFactory, setupStateFactory, seed, fuzzSeed, simtypes.RandomAccounts, postRunActions...)
}
// RunWithSeedAndRandAcc calls RunWithSeed with randAccFn
func RunWithSeedAndRandAcc[T SimulationApp](
tb testing.TB,
cfg simtypes.Config,
appFactory func(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, appOpts servertypes.AppOptions, baseAppOptions ...func(*baseapp.BaseApp)) T,
setupStateFactory func(app T) SimStateFactory,
seed int64,
fuzzSeed []byte,
randAccFn simtypes.RandomAccountFn,
postRunActions ...func(t testing.TB, app TestInstance[T], accs []simtypes.Account),
) {
tb.Helper()
// setup environment
tCfg := cfg.With(tb, seed, fuzzSeed)
testInstance := NewSimulationAppInstance(tb, tCfg, appFactory)
var runLogger log.Logger
if cli.FlagVerboseValue {
runLogger = log.NewTestLogger(tb)
} else {
runLogger = log.NewTestLoggerInfo(tb)
}
runLogger = runLogger.With("seed", tCfg.Seed)
app := testInstance.App
stateFactory := setupStateFactory(app)
ops, reporter := prepareWeightedOps(app.SimulationManager(), stateFactory, tCfg, testInstance.App.TxConfig(), runLogger)
simParams, accs, err := simulation.SimulateFromSeedX(
tb,
runLogger,
WriteToDebugLog(runLogger),
app.GetBaseApp(),
stateFactory.AppStateFn,
randAccFn,
ops,
stateFactory.BlockedAddr,
tCfg,
stateFactory.Codec,
testInstance.ExecLogWriter,
)
require.NoError(tb, err)
err = simtestutil.CheckExportSimulation(app, tCfg, simParams)
require.NoError(tb, err)
if tCfg.Commit {
simtestutil.PrintStats(testInstance.DB)
}
// not using tb.Log to always print the summary
fmt.Printf("+++ DONE (seed: %d): \n%s\n", seed, reporter.Summary().String())
for _, step := range postRunActions {
step(tb, testInstance, accs)
}
require.NoError(tb, app.Close())
}
type (
HasWeightedOperationsX interface {
WeightedOperationsX(weight WeightSource, reg Registry)
}
HasWeightedOperationsXWithProposals interface {
WeightedOperationsX(weights WeightSource, reg Registry, proposals WeightedProposalMsgIter,
legacyProposals []simtypes.WeightedProposalContent) //nolint: staticcheck // used for legacy proposal types
}
HasProposalMsgsX interface {
ProposalMsgsX(weights WeightSource, reg Registry)
}
)
type (
HasLegacyWeightedOperations interface {
// WeightedOperations simulation operations (i.e msgs) with their respective weight
WeightedOperations(simState module.SimulationState) []simtypes.WeightedOperation
}
// HasLegacyProposalMsgs defines the messages that can be used to simulate governance (v1) proposals
// Deprecated replaced by HasProposalMsgsX
HasLegacyProposalMsgs interface {
// ProposalMsgs msg fu nctions used to simulate governance proposals
ProposalMsgs(simState module.SimulationState) []simtypes.WeightedProposalMsg
}
// HasLegacyProposalContents defines the contents that can be used to simulate legacy governance (v1beta1) proposals
// Deprecated replaced by HasProposalMsgsX
HasLegacyProposalContents interface {
// ProposalContents content functions used to simulate governance proposals
ProposalContents(simState module.SimulationState) []simtypes.WeightedProposalContent //nolint:staticcheck // legacy v1beta1 governance
}
)
// TestInstance is a generic type that represents an instance of a SimulationApp used for testing simulations.
// It contains the following fields:
// - App: The instance of the SimulationApp under test.
// - DB: The LevelDB database for the simulation app.
// - WorkDir: The temporary working directory for the simulation app.
// - Cfg: The configuration flags for the simulator.
// - AppLogger: The logger used for logging in the app during the simulation, with seed value attached.
// - ExecLogWriter: Captures block and operation data coming from the simulation
type TestInstance[T SimulationApp] struct {
App T
DB dbm.DB
WorkDir string
Cfg simtypes.Config
AppLogger log.Logger
ExecLogWriter simulation.LogWriter
}
// included to avoid cyclic dependency in testutils/sims
func prepareWeightedOps(
sm *module.SimulationManager,
stateFact SimStateFactory,
config simtypes.Config,
txConfig client.TxConfig,
logger log.Logger,
) (simulation.WeightedOperations, *BasicSimulationReporter) {
cdc := stateFact.Codec
simState := module.SimulationState{
AppParams: make(simtypes.AppParams),
Cdc: cdc,
TxConfig: txConfig,
BondDenom: sdk.DefaultBondDenom,
}
if config.ParamsFile != "" {
bz, err := os.ReadFile(config.ParamsFile)
if err != nil {
panic(err)
}
err = json.Unmarshal(bz, &simState.AppParams)
if err != nil {
panic(err)
}
}
weights := ParamWeightSource(simState.AppParams)
reporter := NewBasicSimulationReporter()
pReg := make(UniqueTypeRegistry)
wContent := make([]simtypes.WeightedProposalContent, 0) //nolint:staticcheck // required for legacy type
legacyPReg := NewWeightedFactoryMethods()
// add gov proposals types
for _, m := range sm.Modules {
switch xm := m.(type) {
case HasProposalMsgsX:
xm.ProposalMsgsX(weights, pReg)
case HasLegacyProposalMsgs:
for _, p := range xm.ProposalMsgs(simState) {
weight := weights.Get(p.AppParamsKey(), safeUint(p.DefaultWeight()))
legacyPReg.Add(weight, legacyToMsgFactoryAdapter(p.MsgSimulatorFn()))
}
case HasLegacyProposalContents:
wContent = append(wContent, xm.ProposalContents(simState)...)
}
}
oReg := NewSimsMsgRegistryAdapter(
reporter,
stateFact.AccountSource,
stateFact.BalanceSource,
txConfig,
logger,
)
wOps := make([]simtypes.WeightedOperation, 0, len(sm.Modules))
for _, m := range sm.Modules {
// add operations
switch xm := m.(type) {
case HasWeightedOperationsX:
xm.WeightedOperationsX(weights, oReg)
case HasWeightedOperationsXWithProposals:
xm.WeightedOperationsX(weights, oReg, AppendIterators(legacyPReg.Iterator(), pReg.Iterator()), wContent)
case HasLegacyWeightedOperations:
wOps = append(wOps, xm.WeightedOperations(simState)...)
}
}
return append(wOps, Collect(oReg.items, func(a weightedOperation) simtypes.WeightedOperation { return a })...), reporter
}
func safeUint(p int) uint32 {
if p < 0 || p > math.MaxUint32 {
panic(fmt.Sprintf("can not cast to uint32: %d", p))
}
return uint32(p)
}
// NewSimulationAppInstance initializes and returns a TestInstance of a SimulationApp.
// The function takes a testing.T instance, a simtypes.Config instance, and an appFactory function as parameters.
// It creates a temporary working directory and a LevelDB database for the simulation app.
// The function then initializes a logger based on the verbosity flag and sets the logger's seed to the test configuration's seed.
// The database is closed and cleaned up on test completion.
func NewSimulationAppInstance[T SimulationApp](
tb testing.TB,
tCfg simtypes.Config,
appFactory func(logger log.Logger, db dbm.DB, traceStore io.Writer, loadLatest bool, appOpts servertypes.AppOptions, baseAppOptions ...func(*baseapp.BaseApp)) T,
) TestInstance[T] {
tb.Helper()
workDir := tb.TempDir()
require.NoError(tb, os.Mkdir(filepath.Join(workDir, "data"), 0o750))
dbDir := filepath.Join(workDir, "leveldb-app-sim")
var logger log.Logger
if cli.FlagVerboseValue {
logger = log.NewTestLogger(tb)
} else {
logger = log.NewTestLoggerError(tb)
}
logger = logger.With("seed", tCfg.Seed)
db, err := dbm.NewDB("Simulation", dbm.BackendType(tCfg.DBBackend), dbDir)
require.NoError(tb, err)
tb.Cleanup(func() {
_ = db.Close() // ensure db is closed
})
appOptions := make(simtestutil.AppOptionsMap)
appOptions[flags.FlagHome] = workDir
opts := []func(*baseapp.BaseApp){baseapp.SetChainID(tCfg.ChainID)}
if tCfg.FauxMerkle {
opts = append(opts, FauxMerkleModeOpt)
}
app := appFactory(logger, db, nil, true, appOptions, opts...)
if !cli.FlagSigverifyTxValue {
app.SetNotSigverifyTx()
}
return TestInstance[T]{
App: app,
DB: db,
WorkDir: workDir,
Cfg: tCfg,
AppLogger: logger,
ExecLogWriter: &simulation.StandardLogWriter{Seed: tCfg.Seed},
}
}
var _ io.Writer = writerFn(nil)
type writerFn func(p []byte) (n int, err error)
func (w writerFn) Write(p []byte) (n int, err error) {
return w(p)
}
// WriteToDebugLog is an adapter to io.Writer interface
func WriteToDebugLog(logger log.Logger) io.Writer {
return writerFn(func(p []byte) (n int, err error) {
logger.Debug(string(p))
return len(p), nil
})
}
// FauxMerkleModeOpt returns a BaseApp option to use a dbStoreAdapter instead of
// an IAVLStore for faster simulation speed.
func FauxMerkleModeOpt(bapp *baseapp.BaseApp) {
bapp.SetFauxMerkleMode()
}