Skip to content

Commit

Permalink
fix(sims): TestAppSimulationAfterImport and legacy proposal handling (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
alpe committed Sep 18, 2024
1 parent 356df96 commit cc8366c
Show file tree
Hide file tree
Showing 11 changed files with 201 additions and 60 deletions.
3 changes: 0 additions & 3 deletions scripts/build/simulations.mk
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,6 @@ test-sim-multi-seed-short:
@cd ${CURRENT_DIR}/simapp && go test -failfast -mod=readonly -timeout 30m -tags='sims' -run TestFullAppSimulation \
-NumBlocks=50 -Period=10 -FauxMerkle=true

test-v2-sim-wip:
@echo "Running v2 simapp. This may take awhile!"
@cd ${CURRENT_DIR}/simapp/v2 && go test -failfast -mod=readonly -timeout 30m -tags='sims' -run TestSimsAppV2

test-sim-benchmark-invariants:
@echo "Running simulation invariant benchmarks..."
Expand Down
20 changes: 12 additions & 8 deletions simapp/sim_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ var (
)

func TestAppImportExport(t *testing.T) {
simsx.Run(t, NewSimApp, setupStateFactory, func(t testing.TB, ti simsx.TestInstance[*SimApp]) {
simsx.Run(t, NewSimApp, setupStateFactory, func(t testing.TB, ti simsx.TestInstance[*SimApp], _ []simtypes.Account) {
app := ti.App
t.Log("exporting genesis...\n")
exported, err := app.ExportAppStateAndValidators(false, exportWithValidatorSet, exportAllModules)
Expand Down Expand Up @@ -116,35 +116,39 @@ func TestAppImportExport(t *testing.T) {
// set up a new node instance, Init chain from exported genesis
// run new instance for n blocks
func TestAppSimulationAfterImport(t *testing.T) {
simsx.Run(t, NewSimApp, setupStateFactory, func(t testing.TB, ti simsx.TestInstance[*SimApp]) {
simsx.Run(t, NewSimApp, setupStateFactory, func(t testing.TB, ti simsx.TestInstance[*SimApp], accs []simtypes.Account) {
app := ti.App
t.Log("exporting genesis...\n")
exported, err := app.ExportAppStateAndValidators(false, exportWithValidatorSet, exportAllModules)
require.NoError(t, err)

t.Log("importing genesis...\n")
importGenesisStateFactory := func(app *SimApp) simsx.SimStateFactory {
return simsx.SimStateFactory{
Codec: app.AppCodec(),
AppStateFn: func(r *rand.Rand, accs []simtypes.Account, config simtypes.Config) (json.RawMessage, []simtypes.Account, string, time.Time) {
AppStateFn: func(r *rand.Rand, _ []simtypes.Account, config simtypes.Config) (json.RawMessage, []simtypes.Account, string, time.Time) {
t.Log("importing genesis...\n")
genesisTimestamp := time.Unix(config.GenesisTime, 0)

_, err = app.InitChain(&abci.InitChainRequest{
AppStateBytes: exported.AppState,
ChainId: simsx.SimAppChainID,
InitialHeight: exported.Height,
Time: genesisTimestamp,
})
if IsEmptyValidatorSetErr(err) {
t.Skip("Skipping simulation as all validators have been unbonded")
return nil, nil, "", time.Time{}
}
acc, err := simtestutil.AccountsFromAppState(app.AppCodec(), exported.AppState)
require.NoError(t, err)
genesisTimestamp := time.Unix(config.GenesisTime, 0)
return exported.AppState, acc, config.ChainID, genesisTimestamp
// use accounts from initial run
return exported.AppState, accs, config.ChainID, genesisTimestamp
},
BlockedAddr: must(BlockedAddresses(app.AuthKeeper.AddressCodec())),
AccountSource: app.AuthKeeper,
BalanceSource: app.BankKeeper,
}
}
ti.Cfg.InitialBlockHeight = int(exported.Height)
simsx.RunWithSeed(t, ti.Cfg, NewSimApp, importGenesisStateFactory, ti.Cfg.Seed, ti.Cfg.FuzzSeed)
})
}
Expand Down Expand Up @@ -191,7 +195,7 @@ func TestAppStateDeterminism(t *testing.T) {
var mx sync.Mutex
appHashResults := make(map[int64][][]byte)
appSimLogger := make(map[int64][]simulation.LogWriter)
captureAndCheckHash := func(t testing.TB, ti simsx.TestInstance[*SimApp]) {
captureAndCheckHash := func(t testing.TB, ti simsx.TestInstance[*SimApp], _ []simtypes.Account) {
seed, appHash := ti.Cfg.Seed, ti.App.LastCommitID().Hash
mx.Lock()
otherHashes, execWriters := appHashResults[seed], appSimLogger[seed]
Expand Down
3 changes: 3 additions & 0 deletions simsx/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,9 @@ func NewChainDataSource(
for i, a := range oldSimAcc {
acc[i] = SimAccount{Account: a, r: r, bank: bank}
index[a.AddressBech32] = i
if a.AddressBech32 == "" {
panic("test account has empty bech32 address")
}
}
return &ChainDataSource{
r: r,
Expand Down
30 changes: 29 additions & 1 deletion simsx/msg_factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,41 @@ import (
sdk "github.com/cosmos/cosmos-sdk/types"
)

// SimMsgFactoryX is an interface for creating and handling fuzz test-like simulation messages in the system.
type SimMsgFactoryX interface {
// MsgType returns an empty instance of the concrete message type that the factory provides.
// This instance is primarily used for deduplication and reporting purposes.
// The result must not be nil
MsgType() sdk.Msg

// Create returns a FactoryMethod implementation which is responsible for constructing new instances of the message
// on each invocation.
Create() FactoryMethod

// DeliveryResultHandler returns a SimDeliveryResultHandler instance which processes the delivery
// response error object. While most simulation factories anticipate successful message delivery,
// certain factories employ this handler to validate execution errors, thereby covering negative
// test scenarios.
DeliveryResultHandler() SimDeliveryResultHandler
}

type (
// FactoryMethod method signature that is implemented by the concrete message factories
// FactoryMethod is a method signature implemented by concrete message factories for SimMsgFactoryX
//
// This factory method is responsible for creating a new `sdk.Msg` instance and determining the
// proposed signers who are expected to successfully sign the message for delivery.
//
// Parameters:
// - ctx: The context for the operation
// - testData: A pointer to a `ChainDataSource` which provides helper methods and simple access to accounts
// and balances within the chain.
// - reporter: An instance of `SimulationReporter` used to report the results of the simulation.
// If no valid message can be provided, the factory method should call `reporter.Skip("some comment")`
// with both `signer` and `msg` set to nil.
//
// Returns:
// - signer: A slice of `SimAccount` representing the proposed signers.
// - msg: An instance of `sdk.Msg` representing the message to be delivered.
FactoryMethod func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg sdk.Msg)

// FactoryMethodWithFutureOps extended message factory method for the gov module or others that have to schedule operations for a future block.
Expand Down
70 changes: 66 additions & 4 deletions simsx/registry.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package simsx

import (
"cmp"
"context"
"iter"
"maps"
Expand Down Expand Up @@ -36,7 +37,7 @@ type (
)

// WeightedProposalMsgIter iterator for weighted gov proposal payload messages
type WeightedProposalMsgIter = iter.Seq2[uint32, SimMsgFactoryX]
type WeightedProposalMsgIter = iter.Seq2[uint32, FactoryMethod]

var _ Registry = &WeightedOperationRegistryAdapter{}

Expand Down Expand Up @@ -108,6 +109,7 @@ type HasFutureOpsRegistry interface {
SetFutureOpsRegistry(FutureOpsRegistry)
}

// msg factory to legacy Operation type
func legacyOperationAdapter(l regCommon, fx SimMsgFactoryX) simtypes.Operation {
return func(
r *rand.Rand, app AppEntrypoint, ctx sdk.Context,
Expand Down Expand Up @@ -167,15 +169,15 @@ func (s UniqueTypeRegistry) Add(weight uint32, f SimMsgFactoryX) {
}

// Iterator returns an iterator function for a Go for loop sorted by weight desc.
func (s UniqueTypeRegistry) Iterator() iter.Seq2[uint32, SimMsgFactoryX] {
func (s UniqueTypeRegistry) Iterator() WeightedProposalMsgIter {
x := maps.Values(s)
sortedWeightedFactory := slices.SortedFunc(x, func(a, b WeightedFactory) int {
return a.Compare(b)
})

return func(yield func(uint32, SimMsgFactoryX) bool) {
return func(yield func(uint32, FactoryMethod) bool) {
for _, v := range sortedWeightedFactory {
if !yield(v.Weight, v.Factory) {
if !yield(v.Weight, v.Factory.Create()) {
return
}
}
Expand Down Expand Up @@ -204,3 +206,63 @@ func (f WeightedFactory) Compare(b WeightedFactory) int {
return strings.Compare(sdk.MsgTypeURL(f.Factory.MsgType()), sdk.MsgTypeURL(b.Factory.MsgType()))
}
}

// WeightedFactoryMethod is a data tuple used for registering legacy proposal operations
type WeightedFactoryMethod struct {
Weight uint32
Factory FactoryMethod
}

type WeightedFactoryMethods []WeightedFactoryMethod

// NewWeightedFactoryMethods constructor
func NewWeightedFactoryMethods() WeightedFactoryMethods {
return make(WeightedFactoryMethods, 0)
}

// Add adds a new WeightedFactoryMethod to the WeightedFactoryMethods slice.
// If weight is zero or f is nil, it returns without making any changes.
func (s *WeightedFactoryMethods) Add(weight uint32, f FactoryMethod) {
if weight == 0 {
return
}
if f == nil {
panic("message factory must not be nil")
}
*s = append(*s, WeightedFactoryMethod{Weight: weight, Factory: f})
}

// Iterator returns an iterator function for a Go for loop sorted by weight desc.
func (s WeightedFactoryMethods) Iterator() WeightedProposalMsgIter {
slices.SortFunc(s, func(e, e2 WeightedFactoryMethod) int {
return cmp.Compare(e.Weight, e2.Weight)
})
return func(yield func(uint32, FactoryMethod) bool) {
for _, v := range s {
if !yield(v.Weight, v.Factory) {
return
}
}
}
}

// legacy operation to Msg factory type
func legacyToMsgFactoryAdapter(fn simtypes.MsgSimulatorFnX) FactoryMethod {
return func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg sdk.Msg) {
msg, err := fn(ctx, testData.r, testData.AllAccounts(), testData.AddressCodec())
if err != nil {
reporter.Skip(err.Error())
return nil, nil
}
return []SimAccount{}, msg
}
}

// AppendIterators takes multiple WeightedProposalMsgIter and returns a single iterator that sequentially yields items after each one.
func AppendIterators(iterators ...WeightedProposalMsgIter) WeightedProposalMsgIter {
return func(yield func(uint32, FactoryMethod) bool) {
for _, it := range iterators {
it(yield)
}
}
}
64 changes: 55 additions & 9 deletions simsx/registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,20 +116,21 @@ func TestSimsMsgRegistryAdapter(t *testing.T) {
}

func TestUniqueTypeRegistry(t *testing.T) {
f1 := SimMsgFactoryFn[*testdata.TestMsg](func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg *testdata.TestMsg) {
exampleFactory := SimMsgFactoryFn[*testdata.TestMsg](func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg *testdata.TestMsg) {
return []SimAccount{}, nil
})

specs := map[string]struct {
src []WeightedFactory
exp []WeightedFactory
exp []WeightedFactoryMethod
expErr bool
}{
"unique": {
src: []WeightedFactory{{Weight: 1, Factory: f1}},
exp: []WeightedFactory{{Weight: 1, Factory: f1}},
src: []WeightedFactory{{Weight: 1, Factory: exampleFactory}},
exp: []WeightedFactoryMethod{{Weight: 1, Factory: exampleFactory.Create()}},
},
"duplicate": {
src: []WeightedFactory{{Weight: 1, Factory: f1}, {Weight: 2, Factory: f1}},
src: []WeightedFactory{{Weight: 1, Factory: exampleFactory}, {Weight: 2, Factory: exampleFactory}},
expErr: true,
},
}
Expand All @@ -148,11 +149,56 @@ func TestUniqueTypeRegistry(t *testing.T) {
reg.Add(v.Weight, v.Factory)
}
// then
var got []WeightedFactory
for w, f := range reg.Iterator() {
got = append(got, WeightedFactory{Weight: w, Factory: f})
}
got := readAll(reg.Iterator())
require.Len(t, got, len(spec.exp))
})
}
}

func TestWeightedFactories(t *testing.T) {
r := NewWeightedFactoryMethods()
f1 := func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg sdk.Msg) {
panic("not implemented")
}
f2 := func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg sdk.Msg) {
panic("not implemented")
}
r.Add(1, f1)
r.Add(2, f2)
got := readAll(r.Iterator())
require.Len(t, got, 2)

assert.Equal(t, uint32(1), r[0].Weight)
assert.Equal(t, uint32(2), r[1].Weight)
}

func TestAppendIterators(t *testing.T) {
r1 := NewWeightedFactoryMethods()
r1.Add(2, func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg sdk.Msg) {
panic("not implemented")
})
r1.Add(2, func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg sdk.Msg) {
panic("not implemented")
})
r1.Add(3, func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg sdk.Msg) {
panic("not implemented")
})
r2 := NewUniqueTypeRegistry()
r2.Add(1, SimMsgFactoryFn[*testdata.TestMsg](func(ctx context.Context, testData *ChainDataSource, reporter SimulationReporter) (signer []SimAccount, msg *testdata.TestMsg) {
panic("not implemented")
}))
// when
all := readAll(AppendIterators(r1.Iterator(), r2.Iterator()))
// then
require.Len(t, all, 4)
gotWeights := Collect(all, func(a WeightedFactoryMethod) uint32 { return a.Weight })
assert.Equal(t, []uint32{2, 2, 3, 1}, gotWeights)
}

func readAll(iterator WeightedProposalMsgIter) []WeightedFactoryMethod {
var ret []WeightedFactoryMethod
for w, f := range iterator {
ret = append(ret, WeightedFactoryMethod{Weight: w, Factory: f})
}
return ret
}
3 changes: 3 additions & 0 deletions simsx/reporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,9 @@ func (s *ExecutionSummary) String() string {
for _, key := range keys {
sb.WriteString(fmt.Sprintf("%s: %d\n", key, s.counts[key]))
}
if len(s.skipReasons) != 0 {
sb.WriteString("\nSkip reasons:\n")
}
for m, c := range s.skipReasons {
values := maps.Values(c)
keys := maps.Keys(c)
Expand Down
Loading

0 comments on commit cc8366c

Please sign in to comment.