-
Notifications
You must be signed in to change notification settings - Fork 3.6k
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
Test Network Testing Framework #6489
Changes from 1 commit
c9d5b34
1e651e8
9d8494f
cd2ec2d
ecfa3c8
c7e9b60
f5deb8d
7f51e12
b7c2cca
ec88212
4f43424
e8f5176
433b67e
f7758f7
4bc56bd
d430884
c389c81
a6f8fe3
9442a8b
0aff4e4
69603bc
6d1719f
c53831d
ff14f0d
93454e5
52aa585
f889a0b
cdc9db6
8aa5c71
426505e
c6a208a
05fd4a7
38aac75
195447e
939b6c3
9c77941
89a447d
df0b1b6
ddb342b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,281 @@ | ||
package testutil | ||
|
||
import ( | ||
"bufio" | ||
"encoding/json" | ||
"fmt" | ||
"net/url" | ||
"os" | ||
"path/filepath" | ||
"testing" | ||
"time" | ||
|
||
"github.com/stretchr/testify/require" | ||
tmcfg "github.com/tendermint/tendermint/config" | ||
"github.com/tendermint/tendermint/crypto" | ||
tmflags "github.com/tendermint/tendermint/libs/cli/flags" | ||
"github.com/tendermint/tendermint/libs/log" | ||
tmrand "github.com/tendermint/tendermint/libs/rand" | ||
"github.com/tendermint/tendermint/node" | ||
tmclient "github.com/tendermint/tendermint/rpc/client" | ||
"golang.org/x/sync/errgroup" | ||
|
||
"github.com/cosmos/cosmos-sdk/client" | ||
clientkeys "github.com/cosmos/cosmos-sdk/client/keys" | ||
"github.com/cosmos/cosmos-sdk/crypto/keyring" | ||
"github.com/cosmos/cosmos-sdk/server" | ||
"github.com/cosmos/cosmos-sdk/server/api" | ||
srvconfig "github.com/cosmos/cosmos-sdk/server/config" | ||
"github.com/cosmos/cosmos-sdk/simapp" | ||
storetypes "github.com/cosmos/cosmos-sdk/store/types" | ||
sdk "github.com/cosmos/cosmos-sdk/types" | ||
authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" | ||
banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" | ||
"github.com/cosmos/cosmos-sdk/x/genutil" | ||
stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" | ||
) | ||
|
||
var ( | ||
_, cdc = simapp.MakeCodecs() | ||
) | ||
|
||
// Config defines the necessary configuration used to bootstrap and start an | ||
// in-process local testing network. | ||
type Config struct { | ||
GenesisState map[string]json.RawMessage | ||
TimeoutCommit time.Duration | ||
ChainID string | ||
NumValidators int | ||
BondDenom string | ||
MinGasPrices string | ||
Passphrase string | ||
AccountTokens sdk.Int | ||
StakingTokens sdk.Int | ||
BondedTokens sdk.Int | ||
EnableLogging bool | ||
} | ||
|
||
// DefaultConfig returns a sane default configuration suitable for nearly all | ||
// testing requirements. | ||
func DefaultConfig() Config { | ||
return Config{ | ||
GenesisState: simapp.ModuleBasics.DefaultGenesis(cdc), | ||
TimeoutCommit: 2 * time.Second, | ||
ChainID: "chain-" + tmrand.NewRand().Str(6), | ||
NumValidators: 4, | ||
BondDenom: sdk.DefaultBondDenom, | ||
MinGasPrices: fmt.Sprintf("0.000006%s", sdk.DefaultBondDenom), | ||
Passphrase: clientkeys.DefaultKeyPass, | ||
AccountTokens: sdk.TokensFromConsensusPower(1000), | ||
StakingTokens: sdk.TokensFromConsensusPower(500), | ||
BondedTokens: sdk.TokensFromConsensusPower(100), | ||
} | ||
} | ||
|
||
type ( | ||
// Network defines a local in-process testing network using SimApp. It can be | ||
// configured to start any number of validators, each with its own RPC and API | ||
AdityaSripal marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// clients. Typically, this test network would be used in client and integration | ||
// testing where user input is expected. | ||
Network struct { | ||
T *testing.T | ||
BaseDir string | ||
Validators []*Validator | ||
} | ||
|
||
// Validator defines an in-process Tendermint validator node. Through this object, | ||
// a client can make RPC and API calls and interact with any client command | ||
// or handler. | ||
Validator struct { | ||
AppConfig *srvconfig.Config | ||
ClientCtx client.Context | ||
Ctx *server.Context | ||
Dir string | ||
NodeID string | ||
PubKey crypto.PubKey | ||
Moniker string | ||
RPCAddress string | ||
P2PAddress string | ||
Address sdk.AccAddress | ||
ValAddress sdk.ValAddress | ||
RPCClient tmclient.Client | ||
|
||
tmNode *node.Node | ||
api *api.Server | ||
} | ||
) | ||
|
||
func NewTestNetwork(t *testing.T, cfg Config) *Network { | ||
alexanderbez marked this conversation as resolved.
Show resolved
Hide resolved
|
||
network := &Network{ | ||
T: t, | ||
BaseDir: os.TempDir(), | ||
Validators: make([]*Validator, cfg.NumValidators), | ||
} | ||
|
||
t.Log("preparing test network...") | ||
|
||
monikers := make([]string, cfg.NumValidators) | ||
nodeIDs := make([]string, cfg.NumValidators) | ||
valPubKeys := make([]crypto.PubKey, cfg.NumValidators) | ||
|
||
var ( | ||
genAccounts []authtypes.GenesisAccount | ||
genBalances []banktypes.Balance | ||
genFiles []string | ||
) | ||
|
||
buf := bufio.NewReader(os.Stdin) | ||
|
||
// generate private keys, node IDs, and initial transactions | ||
for i := 0; i < cfg.NumValidators; i++ { | ||
appCfg := srvconfig.DefaultConfig() | ||
appCfg.Pruning = storetypes.PruningOptionNothing | ||
appCfg.MinGasPrices = cfg.MinGasPrices | ||
appCfg.API.Enable = true | ||
appCfg.API.Swagger = false | ||
appCfg.Telemetry.Enabled = false | ||
|
||
apiAddr, _, err := server.FreeTCPAddr() | ||
require.NoError(t, err) | ||
appCfg.API.Address = apiAddr | ||
|
||
ctx := server.NewDefaultContext() | ||
tmCfg := ctx.Config | ||
tmCfg.Consensus.TimeoutCommit = cfg.TimeoutCommit | ||
|
||
logger := log.NewNopLogger() | ||
if cfg.EnableLogging { | ||
logger = log.NewTMLogger(log.NewSyncWriter(os.Stdout)) | ||
logger, _ = tmflags.ParseLogLevel("info", logger, tmcfg.DefaultLogLevel()) | ||
} | ||
|
||
ctx.Logger = logger | ||
|
||
nodeDirName := fmt.Sprintf("node%d", i) | ||
nodeDir := filepath.Join(network.BaseDir, nodeDirName, "simd") | ||
clientDir := filepath.Join(network.BaseDir, nodeDirName, "simcli") | ||
gentxsDir := filepath.Join(network.BaseDir, "gentxs") | ||
|
||
require.NoError(t, os.MkdirAll(filepath.Join(nodeDir, "config"), 0755)) | ||
require.NoError(t, os.MkdirAll(clientDir, 0755)) | ||
|
||
tmCfg.SetRoot(nodeDir) | ||
tmCfg.Moniker = nodeDirName | ||
monikers[i] = nodeDirName | ||
|
||
proxyAddr, _, err := server.FreeTCPAddr() | ||
require.NoError(t, err) | ||
tmCfg.ProxyApp = proxyAddr | ||
|
||
rpcAddr, _, err := server.FreeTCPAddr() | ||
require.NoError(t, err) | ||
tmCfg.RPC.ListenAddress = rpcAddr | ||
|
||
p2pAddr, _, err := server.FreeTCPAddr() | ||
require.NoError(t, err) | ||
tmCfg.P2P.ListenAddress = p2pAddr | ||
tmCfg.P2P.AddrBookStrict = false | ||
tmCfg.P2P.AllowDuplicateIP = true | ||
|
||
nodeID, pubKey, err := genutil.InitializeNodeValidatorFiles(tmCfg) | ||
require.NoError(t, err) | ||
nodeIDs[i] = nodeID | ||
valPubKeys[i] = pubKey | ||
|
||
kb, err := keyring.New(sdk.KeyringServiceName(), keyring.BackendTest, clientDir, buf) | ||
require.NoError(t, err) | ||
|
||
addr, secret, err := server.GenerateSaveCoinKey(kb, nodeDirName, cfg.Passphrase, true) | ||
require.NoError(t, err) | ||
|
||
info := map[string]string{"secret": secret} | ||
infoBz, err := json.Marshal(info) | ||
require.NoError(t, err) | ||
|
||
// save private key seed words | ||
require.NoError(t, writeFile(fmt.Sprintf("%v.json", "key_seed"), clientDir, infoBz)) | ||
|
||
balances := sdk.Coins{ | ||
sdk.NewCoin(fmt.Sprintf("%stoken", nodeDirName), cfg.AccountTokens), | ||
sdk.NewCoin(cfg.BondDenom, cfg.StakingTokens), | ||
} | ||
|
||
genFiles = append(genFiles, tmCfg.GenesisFile()) | ||
genBalances = append(genBalances, banktypes.Balance{Address: addr, Coins: balances.Sort()}) | ||
genAccounts = append(genAccounts, authtypes.NewBaseAccount(addr, nil, 0, 0)) | ||
|
||
createValMsg := stakingtypes.NewMsgCreateValidator( | ||
sdk.ValAddress(addr), | ||
valPubKeys[i], | ||
sdk.NewCoin(sdk.DefaultBondDenom, cfg.BondedTokens), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps an extension worth implementing in a future PR. In addition to different validator configs as mentioned above, we should allow initializing networks with unequal token distributions There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Absolutely! |
||
stakingtypes.NewDescription(nodeDirName, "", "", "", ""), | ||
stakingtypes.NewCommissionRates(sdk.OneDec(), sdk.OneDec(), sdk.OneDec()), | ||
sdk.OneInt(), | ||
) | ||
|
||
p2pURL, err := url.Parse(p2pAddr) | ||
require.NoError(t, err) | ||
|
||
memo := fmt.Sprintf("%s@%s:%s", nodeIDs[i], p2pURL.Hostname(), p2pURL.Port()) | ||
tx := authtypes.NewStdTx([]sdk.Msg{createValMsg}, authtypes.StdFee{}, []authtypes.StdSignature{}, memo) //nolint:staticcheck // SA1019: authtypes.StdFee is deprecated | ||
txBldr := authtypes.TxBuilder{}.WithChainID(cfg.ChainID).WithMemo(memo).WithKeybase(kb) | ||
|
||
signedTx, err := txBldr.SignStdTx(nodeDirName, tx, false) | ||
require.NoError(t, err) | ||
|
||
txBz, err := cdc.MarshalJSON(signedTx) | ||
require.NoError(t, err) | ||
require.NoError(t, writeFile(fmt.Sprintf("%v.json", nodeDirName), gentxsDir, txBz)) | ||
|
||
srvconfig.WriteConfigFile(filepath.Join(nodeDir, "config/app.toml"), appCfg) | ||
|
||
network.Validators[i] = &Validator{ | ||
AppConfig: appCfg, | ||
Ctx: ctx, | ||
Dir: filepath.Join(network.BaseDir, nodeDirName), | ||
NodeID: nodeID, | ||
PubKey: pubKey, | ||
Moniker: nodeDirName, | ||
RPCAddress: rpcAddr, | ||
P2PAddress: p2pAddr, | ||
Address: addr, | ||
ValAddress: sdk.ValAddress(addr), | ||
} | ||
} | ||
|
||
require.NoError(t, initGenFiles(cfg, genAccounts, genBalances, genFiles)) | ||
require.NoError(t, collectGenFiles(cfg, network.Validators, network.BaseDir)) | ||
|
||
t.Log("starting test network...") | ||
var eg errgroup.Group | ||
for _, v := range network.Validators { | ||
val := v | ||
eg.Go(func() error { | ||
return startInProcess(cfg, val) | ||
}) | ||
} | ||
|
||
require.NoError(t, eg.Wait()) | ||
t.Log("started test network") | ||
|
||
// TODO: Do we need to trap signal | ||
|
||
return network | ||
} | ||
|
||
func (n *Network) Cleanup() { | ||
n.T.Log("cleaning up test network...") | ||
|
||
for _, v := range n.Validators { | ||
if v.tmNode != nil && v.tmNode.IsRunning() { | ||
_ = v.tmNode.Stop() | ||
} | ||
|
||
if v.api != nil { | ||
_ = v.api.Close() | ||
} | ||
} | ||
|
||
_ = os.RemoveAll(n.BaseDir) | ||
alexanderbez marked this conversation as resolved.
Show resolved
Hide resolved
|
||
n.T.Log("finished cleaning up test network") | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
package testutil | ||
|
||
import ( | ||
"testing" | ||
"time" | ||
|
||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestNetwork_Liveness(t *testing.T) { | ||
n := NewTestNetwork(t, DefaultConfig()) | ||
defer n.Cleanup() | ||
|
||
require.NotNil(t, n) | ||
require.NotEmpty(t, n.Validators) | ||
client := n.Validators[0].RPCClient | ||
require.NotNil(t, client) | ||
|
||
ticker := time.NewTicker(5 * time.Second) | ||
timeout := time.After(time.Minute) | ||
|
||
for { | ||
select { | ||
case <-ticker.C: | ||
s, _ := client.Status() | ||
if s != nil && s.SyncInfo.LatestBlockHeight >= 10 { | ||
t.Logf("successfully process %d blocks", s.SyncInfo.LatestBlockHeight) | ||
return | ||
} | ||
case <-timeout: | ||
t.Fatal("timeout exceeded waiting for enough committed blocks") | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I did not realize Tendermint HTTP client actually blocks -- this fixes that so we can properly cleanup below.