diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2c819617e7..f8cb95c691 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -60,7 +60,7 @@ jobs: if: "env.GIT_DIFF != ''" - name: test & coverage report creation run: | - cat xaa.txt | xargs go test -mod=readonly -timeout 8m -race -coverprofile=coverage.txt -covermode=atomic -tags='ledger test_ledger_mock' + cat xaa.txt | xargs go test -mod=readonly -timeout 8m -coverprofile=coverage.txt -covermode=atomic -tags='ledger test_ledger_mock' if: "env.GIT_DIFF != ''" - name: filter out DONTCOVER run: | @@ -98,7 +98,7 @@ jobs: if: "env.GIT_DIFF != ''" - name: test & coverage report creation run: | - cat xab.txt | xargs go test -mod=readonly -timeout 6m -race -coverprofile=coverage.txt -covermode=atomic -tags='ledger test_ledger_mock' + cat xab.txt | xargs go test -mod=readonly -timeout 6m -coverprofile=coverage.txt -covermode=atomic -tags='ledger test_ledger_mock' if: "env.GIT_DIFF != ''" - name: filter out DONTCOVER run: | @@ -136,7 +136,7 @@ jobs: if: "env.GIT_DIFF != ''" - name: test & coverage report creation run: | - cat xac.txt | xargs go test -mod=readonly -timeout 6m -race -coverprofile=coverage.txt -covermode=atomic -tags='ledger test_ledger_mock' + cat xac.txt | xargs go test -mod=readonly -timeout 6m -coverprofile=coverage.txt -covermode=atomic -tags='ledger test_ledger_mock' if: "env.GIT_DIFF != ''" - name: filter out DONTCOVER run: | @@ -174,7 +174,7 @@ jobs: if: "env.GIT_DIFF != ''" - name: test & coverage report creation run: | - cat xad.txt | xargs go test -mod=readonly -timeout 6m -race -coverprofile=coverage.txt -covermode=atomic -tags='ledger test_ledger_mock' + cat xad.txt | xargs go test -mod=readonly -timeout 6m -coverprofile=coverage.txt -covermode=atomic -tags='ledger test_ledger_mock' if: "env.GIT_DIFF != ''" - name: filter out DONTCOVER run: | @@ -213,27 +213,3 @@ jobs: run: | make test-integration if: "env.GIT_DIFF != ''" - - liveness-test: - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - uses: actions/checkout@v2 - - uses: technote-space/get-diff-action@v1 - id: git_diff - with: - SUFFIX_FILTER: | - .go - .mod - .sum - - name: build image - run: | - make build-docker-local-simapp - - name: start localnet - run: | - make clean build-sim-linux localnet-start - if: "env.GIT_DIFF != ''" - - name: test liveness - run: | - ./contrib/localnet_liveness.sh 100 5 50 localhost - if: "env.GIT_DIFF != ''" diff --git a/CHANGELOG.md b/CHANGELOG.md index f243a59954..240be85848 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -146,6 +146,7 @@ be used to retrieve the actual proposal `Content`. Also the `NewMsgSubmitProposa ### Features +* (tests) [\#6489](https://github.com/cosmos/cosmos-sdk/pull/6489) Introduce package `testutil`, new in-process testing network framework for use in integration and unit tests. * (crypto/multisig) [\#6241](https://github.com/cosmos/cosmos-sdk/pull/6241) Add Multisig type directly to the repo. Previously this was in tendermint. * (rest) [\#6167](https://github.com/cosmos/cosmos-sdk/pull/6167) Support `max-body-bytes` CLI flag for the REST service. * (x/ibc) [\#5588](https://github.com/cosmos/cosmos-sdk/pull/5588) Add [ICS 024 - Host State Machine Requirements](https://github.com/cosmos/ics/tree/master/spec/ics-024-host-requirements) subpackage to `x/ibc` module. diff --git a/contrib/localnet_liveness.sh b/contrib/localnet_liveness.sh deleted file mode 100755 index 1dac38fa72..0000000000 --- a/contrib/localnet_liveness.sh +++ /dev/null @@ -1,55 +0,0 @@ -#!/bin/bash - -CNT=0 -ITER=$1 -SLEEP=$2 -NUMBLOCKS=$3 -NODEADDR=$4 - -if [ -z "$1" ]; then - echo "Need to input number of iterations to run..." - exit 1 -fi - -if [ -z "$2" ]; then - echo "Need to input number of seconds to sleep between iterations" - exit 1 -fi - -if [ -z "$3" ]; then - echo "Need to input block height to declare completion..." - exit 1 -fi - -if [ -z "$4" ]; then - echo "Need to input node address to poll..." - exit 1 -fi - -docker_containers=( $(docker ps -q -f name=simdnode --format='{{.Names}}') ) - -while [ ${CNT} -lt $ITER ]; do - curr_block=$(curl -s $NODEADDR:26657/status | jq -r '.result.sync_info.latest_block_height') - - if [ ! -z ${curr_block} ] ; then - echo "Number of Blocks: ${curr_block}" - fi - - if [ ! -z ${curr_block} ] && [ ${curr_block} -gt ${NUMBLOCKS} ]; then - echo "Number of blocks reached. Success!" - exit 0 - fi - - # Emulate network chaos: - # - # Every 10 blocks, pick a random container and restart it. - if ! ((${CNT} % 10)); then - rand_container=${docker_containers["$[RANDOM % ${#docker_containers[@]}]"]}; - echo "Restarting random docker container ${rand_container}" - docker restart ${rand_container} &>/dev/null & - fi - let CNT=CNT+1 - sleep $SLEEP -done -echo "Timeout reached. Failure!" -exit 1 diff --git a/go.mod b/go.mod index 11e264f4eb..ee9584a3d1 100644 --- a/go.mod +++ b/go.mod @@ -39,6 +39,7 @@ require ( github.com/tendermint/iavl v0.14.0-rc1 github.com/tendermint/tendermint v0.33.5 github.com/tendermint/tm-db v0.5.1 + golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e google.golang.org/grpc v1.30.0 gopkg.in/yaml.v2 v2.3.0 ) diff --git a/go.sum b/go.sum index b84f80f3ff..b5f51a2367 100644 --- a/go.sum +++ b/go.sum @@ -605,6 +605,7 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/server/api/server.go b/server/api/server.go index 588614f2f6..b54f5ad68c 100644 --- a/server/api/server.go +++ b/server/api/server.go @@ -4,7 +4,6 @@ import ( "fmt" "net" "net/http" - "os" "strings" "time" @@ -33,11 +32,11 @@ type Server struct { listener net.Listener } -func New(clientCtx client.Context) *Server { +func New(clientCtx client.Context, logger log.Logger) *Server { return &Server{ Router: mux.NewRouter(), ClientCtx: clientCtx, - logger: log.NewTMLogger(log.NewSyncWriter(os.Stdout)).With("module", "api-server"), + logger: logger, } } diff --git a/server/start.go b/server/start.go index b2b7949388..a0f6d263ab 100644 --- a/server/start.go +++ b/server/start.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "runtime/pprof" + "time" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -199,8 +200,9 @@ func startInProcess(ctx *Context, cdc codec.JSONMarshaler, appCreator AppCreator return err } - config := config.GetConfig() var apiSrv *api.Server + + config := config.GetConfig() if config.API.Enable { genDoc, err := genDocProvider() if err != nil { @@ -210,18 +212,28 @@ func startInProcess(ctx *Context, cdc codec.JSONMarshaler, appCreator AppCreator // TODO: Since this is running in process, do we need to provide a verifier // and set TrustNode=false? If so, we need to add additional logic that // waits for a block to be committed first before starting the API server. - ctx := client.Context{}. + clientCtx := client.Context{}. WithHomeDir(home). WithChainID(genDoc.ChainID). WithJSONMarshaler(cdc). WithClient(local.New(tmNode)). WithTrustNode(true) - apiSrv = api.New(ctx) + apiSrv = api.New(clientCtx, ctx.Logger.With("module", "api-server")) app.RegisterAPIRoutes(apiSrv) - if err := apiSrv.Start(config); err != nil { + errCh := make(chan error) + + go func() { + if err := apiSrv.Start(config); err != nil { + errCh <- err + } + }() + + select { + case err := <-errCh: return err + case <-time.After(5 * time.Second): // assume server started successfully } } diff --git a/testutil/doc.go b/testutil/doc.go new file mode 100644 index 0000000000..3035d4e1a2 --- /dev/null +++ b/testutil/doc.go @@ -0,0 +1,65 @@ +/* +Package testutil implements and exposes a fully operational in-process Tendermint +test network that consists of at least one or potentially many validators. This +test network can be used primarily for integration tests or unit test suites. + +The testnetwork utilizes SimApp as the ABCI application and uses all the modules +defined in the Cosmos SDK. An in-process test network can be configured with any +number of validators as well as account funds and even custom genesis state. + +When creating a test network, a series of Validator objects are returned. Each +Validator object has useful information such as their address and pubkey. A +Validator will also provide its RPC, P2P, and API addresses that can be useful +for integration testing. In addition, a Tendermint local RPC client is also provided +which can be handy for making direct RPC calls to Tendermint. + +Note, due to limitations in concurrency and the design of the RPC layer in +Tendermint, only the first Validator object will have an RPC and API client +exposed. Due to this exact same limitation, only a single test network can exist +at a time. A caller must be certain it calls Cleanup after it no longer needs +the network. + +A typical testing flow might look like the following: + + type IntegrationTestSuite struct { + suite.Suite + + cfg testutil.Config + network *testutil.Network + } + + func (s *IntegrationTestSuite) SetupSuite() { + s.T().Log("setting up integration test suite") + + cfg := testutil.DefaultConfig() + cfg.NumValidators = 1 + + s.cfg = cfg + s.network = testutil.NewTestNetwork(s.T(), cfg) + + _, err := s.network.WaitForHeight(1) + s.Require().NoError(err) + } + + func (s *IntegrationTestSuite) TearDownSuite() { + s.T().Log("tearing down integration test suite") + + // This is important and must be called to ensure other tests can create + // a network! + s.network.Cleanup() + } + + func (s *IntegrationTestSuite) TestQueryBalancesRequestHandlerFn() { + val := s.network.Validators[0] + baseURL := val.APIAddress + + // Use baseURL to make API HTTP requests or use val.RPCClient to make direct + // Tendermint RPC calls. + // ... + } + + func TestIntegrationTestSuite(t *testing.T) { + suite.Run(t, new(IntegrationTestSuite)) + } +*/ +package testutil diff --git a/testutil/network.go b/testutil/network.go new file mode 100644 index 0000000000..b4f3c46a2a --- /dev/null +++ b/testutil/network.go @@ -0,0 +1,384 @@ +package testutil + +import ( + "bufio" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/url" + "os" + "path/filepath" + "sync" + "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" + dbm "github.com/tendermint/tm-db" + + "github.com/cosmos/cosmos-sdk/baseapp" + "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() + + // package-wide network lock to only allow one test network at a time + lock = new(sync.Mutex) +) + +// AppConstructor defines a function which accepts a network configuration and +// creates an ABCI Application to provide to Tendermint. +type AppConstructor = func(val Validator) server.Application + +func NewSimApp(val Validator) server.Application { + return simapp.NewSimApp( + val.Ctx.Logger, dbm.NewMemDB(), nil, true, make(map[int64]bool), val.Ctx.Config.RootDir, 0, + baseapp.SetPruning(storetypes.NewPruningOptionsFromString(val.AppConfig.Pruning)), + baseapp.SetMinGasPrices(val.AppConfig.MinGasPrices), + ) +} + +// Config defines the necessary configuration used to bootstrap and start an +// in-process local testing network. +type Config struct { + AppConstructor AppConstructor // the ABCI application constructor + GenesisState map[string]json.RawMessage // custom gensis state to provide + TimeoutCommit time.Duration // the consensus commitment timeout + ChainID string // the network chain-id + NumValidators int // the total number of validators to create and bond + BondDenom string // the staking bond denomination + MinGasPrices string // the minimum gas prices each validator will accept + Passphrase string // the passphrase provided to the test keyring + AccountTokens sdk.Int // the amount of unique validator tokens (e.g. 1000node0) + StakingTokens sdk.Int // the amount of tokens each validator has available to stake + BondedTokens sdk.Int // the amount of tokens each validator stakes + PruningStrategy string // the pruning strategy each validator will have + EnableLogging bool // enable Tendermint logging to STDOUT + CleanupDir bool // remove base temporary directory during cleanup +} + +// DefaultConfig returns a sane default configuration suitable for nearly all +// testing requirements. +func DefaultConfig() Config { + return Config{ + AppConstructor: NewSimApp, + 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), + PruningStrategy: storetypes.PruningOptionNothing, + CleanupDir: true, + } +} + +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 + // clients. Typically, this test network would be used in client and integration + // testing where user input is expected. + // + // Note, due to Tendermint constraints in regards to RPC functionality, there + // may only be one test network running at a time. Thus, any caller must be + // sure to Cleanup after testing is finished in order to allow other tests + // to create networks. In addition, only the first validator will have a valid + // RPC and API server/client. + Network struct { + T *testing.T + BaseDir string + Validators []*Validator + + config Config + } + + // 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 + APIAddress 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 { + // only one caller/test can create and use a network at a time + t.Log("acquiring test network lock") + lock.Lock() + + baseDir, err := ioutil.TempDir(os.TempDir(), cfg.ChainID) + require.NoError(t, err) + t.Logf("created temporary directory: %s", baseDir) + + network := &Network{ + T: t, + BaseDir: baseDir, + Validators: make([]*Validator, cfg.NumValidators), + config: cfg, + } + + 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 = cfg.PruningStrategy + appCfg.MinGasPrices = cfg.MinGasPrices + appCfg.API.Enable = true + appCfg.API.Swagger = false + appCfg.Telemetry.Enabled = false + + ctx := server.NewDefaultContext() + tmCfg := ctx.Config + tmCfg.Consensus.TimeoutCommit = cfg.TimeoutCommit + + // Only allow the first validator to expose an RPC and API server/client + // due to Tendermint in-process constraints. + apiAddr := "" + tmCfg.RPC.ListenAddress = "" + if i == 0 { + apiListenAddr, _, err := server.FreeTCPAddr() + require.NoError(t, err) + appCfg.API.Address = apiListenAddr + + apiURL, err := url.Parse(apiListenAddr) + require.NoError(t, err) + apiAddr = fmt.Sprintf("http://%s:%s", apiURL.Hostname(), apiURL.Port()) + + rpcAddr, _, err := server.FreeTCPAddr() + require.NoError(t, err) + tmCfg.RPC.ListenAddress = rpcAddr + } + + 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 + + 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.NewCoins( + 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), + 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: tmCfg.RPC.ListenAddress, + P2PAddress: tmCfg.P2P.ListenAddress, + APIAddress: apiAddr, + 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...") + for _, v := range network.Validators { + require.NoError(t, startInProcess(cfg, v)) + } + + t.Log("started test network") + + // Ensure we cleanup incase any test was abruptly halted (e.g. SIGINT) as any + // defer in a test would not be called. + server.TrapSignal(network.Cleanup) + + return network +} + +// WaitForHeight performs a blocking check where it waits for a block to be +// committed after a given block. If that height is not reached within a timeout, +// an error is returned. Regardless, the latest height queried is returned. +func (n *Network) WaitForHeight(h int64) (int64, error) { + return n.WaitForHeightWithTimeout(h, 10*time.Second) +} + +// WaitForHeightWithTimeout is the same as WaitForHeight except the caller can +// provide a custom timeout. +func (n *Network) WaitForHeightWithTimeout(h int64, t time.Duration) (int64, error) { + ticker := time.NewTicker(time.Second) + timeout := time.After(t) + + if len(n.Validators) == 0 { + return 0, errors.New("no validators available") + } + + var latestHeight int64 + val := n.Validators[0] + + for { + select { + case <-timeout: + ticker.Stop() + return latestHeight, errors.New("timeout exceeded waiting for block") + case <-ticker.C: + status, err := val.RPCClient.Status() + if err == nil && status != nil { + latestHeight = status.SyncInfo.LatestBlockHeight + if latestHeight >= h { + return latestHeight, nil + } + } + } + } +} + +// Cleanup removes the root testing (temporary) directory and stops both the +// Tendermint and API services. It allows other callers to create and start +// test networks. This method must be called when a test is finished, typically +// in a defer. +func (n *Network) Cleanup() { + defer func() { + lock.Unlock() + n.T.Log("released test network lock") + }() + + 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() + } + } + + if n.config.CleanupDir { + _ = os.RemoveAll(n.BaseDir) + } + + n.T.Log("finished cleaning up test network") +} diff --git a/testutil/network_test.go b/testutil/network_test.go new file mode 100644 index 0000000000..12f2338cd6 --- /dev/null +++ b/testutil/network_test.go @@ -0,0 +1,38 @@ +package testutil + +import ( + "testing" + "time" + + "github.com/stretchr/testify/suite" +) + +type IntegrationTestSuite struct { + suite.Suite + + network *Network +} + +func (s *IntegrationTestSuite) SetupSuite() { + s.T().Log("setting up integration test suite") + + s.network = NewTestNetwork(s.T(), DefaultConfig()) + s.Require().NotNil(s.network) + + _, err := s.network.WaitForHeight(1) + s.Require().NoError(err) +} + +func (s *IntegrationTestSuite) TearDownSuite() { + s.T().Log("tearing down integration test suite") + s.network.Cleanup() +} + +func (s *IntegrationTestSuite) TestNetwork_Liveness() { + h, err := s.network.WaitForHeightWithTimeout(10, time.Minute) + s.Require().NoError(err, "expected to reach 10 blocks; got %d", h) +} + +func TestIntegrationTestSuite(t *testing.T) { + suite.Run(t, new(IntegrationTestSuite)) +} diff --git a/testutil/util.go b/testutil/util.go new file mode 100644 index 0000000000..eb89e2272b --- /dev/null +++ b/testutil/util.go @@ -0,0 +1,179 @@ +package testutil + +import ( + "path/filepath" + "time" + + tmos "github.com/tendermint/tendermint/libs/os" + "github.com/tendermint/tendermint/node" + "github.com/tendermint/tendermint/p2p" + pvm "github.com/tendermint/tendermint/privval" + "github.com/tendermint/tendermint/proxy" + "github.com/tendermint/tendermint/rpc/client/local" + "github.com/tendermint/tendermint/types" + tmtime "github.com/tendermint/tendermint/types/time" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/server/api" + 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" + genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" +) + +func startInProcess(cfg Config, val *Validator) error { + logger := val.Ctx.Logger + tmCfg := val.Ctx.Config + tmCfg.Instrumentation.Prometheus = false + + nodeKey, err := p2p.LoadOrGenNodeKey(tmCfg.NodeKeyFile()) + if err != nil { + return err + } + + app := cfg.AppConstructor(*val) + + genDocProvider := node.DefaultGenesisDocProviderFunc(tmCfg) + tmNode, err := node.NewNode( + tmCfg, + pvm.LoadOrGenFilePV(tmCfg.PrivValidatorKeyFile(), tmCfg.PrivValidatorStateFile()), + nodeKey, + proxy.NewLocalClientCreator(app), + genDocProvider, + node.DefaultDBProvider, + node.DefaultMetricsProvider(tmCfg.Instrumentation), + logger.With("module", val.Moniker), + ) + if err != nil { + return err + } + + if err := tmNode.Start(); err != nil { + return err + } + + val.tmNode = tmNode + + if val.RPCAddress != "" { + val.RPCClient = local.New(tmNode) + } + + if val.APIAddress != "" { + val.ClientCtx = client.Context{}. + WithHomeDir(tmCfg.RootDir). + WithChainID(cfg.ChainID). + WithJSONMarshaler(cdc). + WithClient(val.RPCClient). + WithTrustNode(true) + + apiSrv := api.New(val.ClientCtx, logger.With("module", "api-server")) + app.RegisterAPIRoutes(apiSrv) + + errCh := make(chan error) + + go func() { + if err := apiSrv.Start(*val.AppConfig); err != nil { + errCh <- err + } + }() + + select { + case err := <-errCh: + return err + case <-time.After(5 * time.Second): // assume server started successfully + } + + val.api = apiSrv + } + + return nil +} + +func collectGenFiles(cfg Config, vals []*Validator, outputDir string) error { + genTime := tmtime.Now() + + for i := 0; i < cfg.NumValidators; i++ { + tmCfg := vals[i].Ctx.Config + + nodeDir := filepath.Join(outputDir, vals[i].Moniker, "simd") + gentxsDir := filepath.Join(outputDir, "gentxs") + + tmCfg.Moniker = vals[i].Moniker + tmCfg.SetRoot(nodeDir) + + initCfg := genutiltypes.NewInitConfig(cfg.ChainID, gentxsDir, vals[i].Moniker, vals[i].NodeID, vals[i].PubKey) + + genFile := tmCfg.GenesisFile() + genDoc, err := types.GenesisDocFromFile(genFile) + if err != nil { + return err + } + + appState, err := genutil.GenAppStateFromConfig(cdc, tmCfg, initCfg, *genDoc, banktypes.GenesisBalancesIterator{}) + if err != nil { + return err + } + + // overwrite each validator's genesis file to have a canonical genesis time + if err := genutil.ExportGenesisFileWithTime(genFile, cfg.ChainID, nil, appState, genTime); err != nil { + return err + } + } + + return nil +} + +func initGenFiles(cfg Config, genAccounts []authtypes.GenesisAccount, genBalances []banktypes.Balance, genFiles []string) error { + + // set the accounts in the genesis state + var authGenState authtypes.GenesisState + cdc.MustUnmarshalJSON(cfg.GenesisState[authtypes.ModuleName], &authGenState) + + authGenState.Accounts = genAccounts + cfg.GenesisState[authtypes.ModuleName] = cdc.MustMarshalJSON(authGenState) + + // set the balances in the genesis state + var bankGenState banktypes.GenesisState + cdc.MustUnmarshalJSON(cfg.GenesisState[banktypes.ModuleName], &bankGenState) + + bankGenState.Balances = genBalances + cfg.GenesisState[banktypes.ModuleName] = cdc.MustMarshalJSON(bankGenState) + + appGenStateJSON, err := codec.MarshalJSONIndent(cdc, cfg.GenesisState) + if err != nil { + return err + } + + genDoc := types.GenesisDoc{ + ChainID: cfg.ChainID, + AppState: appGenStateJSON, + Validators: nil, + } + + // generate empty genesis files for each validator and save + for i := 0; i < cfg.NumValidators; i++ { + if err := genDoc.SaveAs(genFiles[i]); err != nil { + return err + } + } + + return nil +} + +func writeFile(name string, dir string, contents []byte) error { + writePath := filepath.Join(dir) + file := filepath.Join(writePath, name) + + err := tmos.EnsureDir(writePath, 0755) + if err != nil { + return err + } + + err = tmos.WriteFile(file, contents, 0644) + if err != nil { + return err + } + + return nil +} diff --git a/types/rest/rest.go b/types/rest/rest.go index bee3bbbeaa..3825c95049 100644 --- a/types/rest/rest.go +++ b/types/rest/rest.go @@ -42,6 +42,17 @@ func NewResponseWithHeight(height int64, result json.RawMessage) ResponseWithHei } } +// ParseResponseWithHeight returns the raw result from a JSON-encoded +// ResponseWithHeight object. +func ParseResponseWithHeight(cdc codec.JSONMarshaler, bz []byte) ([]byte, error) { + r := ResponseWithHeight{} + if err := cdc.UnmarshalJSON(bz, &r); err != nil { + return nil, err + } + + return r.Result, nil +} + // GasEstimateResponse defines a response definition for tx gas estimation. type GasEstimateResponse struct { GasEstimate uint64 `json:"gas_estimate"` @@ -425,3 +436,23 @@ func ParseQueryParamBool(r *http.Request, param string) bool { return false } + +// GetRequest defines a wrapper around an HTTP GET request with a provided URL. +// An error is returned if the request or reading the body fails. +func GetRequest(url string) ([]byte, error) { + res, err := http.Get(url) // nolint:gosec + if err != nil { + return nil, err + } + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, err + } + + if err = res.Body.Close(); err != nil { + return nil, err + } + + return body, nil +} diff --git a/x/bank/client/rest/query_test.go b/x/bank/client/rest/query_test.go new file mode 100644 index 0000000000..d682c86850 --- /dev/null +++ b/x/bank/client/rest/query_test.go @@ -0,0 +1,134 @@ +package rest_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/suite" + + "github.com/cosmos/cosmos-sdk/testutil" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/rest" +) + +type IntegrationTestSuite struct { + suite.Suite + + cfg testutil.Config + network *testutil.Network +} + +func (s *IntegrationTestSuite) SetupSuite() { + s.T().Log("setting up integration test suite") + + cfg := testutil.DefaultConfig() + cfg.NumValidators = 1 + + s.cfg = cfg + s.network = testutil.NewTestNetwork(s.T(), cfg) + + _, err := s.network.WaitForHeight(1) + s.Require().NoError(err) +} + +func (s *IntegrationTestSuite) TearDownSuite() { + s.T().Log("tearing down integration test suite") + s.network.Cleanup() +} + +func (s *IntegrationTestSuite) TestQueryBalancesRequestHandlerFn() { + val := s.network.Validators[0] + baseURL := val.APIAddress + + testCases := []struct { + name string + url string + respType fmt.Stringer + expected fmt.Stringer + }{ + { + "total account balance", + fmt.Sprintf("%s/bank/balances/%s?height=1", baseURL, val.Address), + &sdk.Coins{}, + sdk.NewCoins( + sdk.NewCoin(fmt.Sprintf("%stoken", val.Moniker), s.cfg.AccountTokens), + sdk.NewCoin(s.cfg.BondDenom, s.cfg.StakingTokens.Sub(s.cfg.BondedTokens)), + ), + }, + { + "total account balance of a specific denom", + fmt.Sprintf("%s/bank/balances/%s?height=1&denom=%s", baseURL, val.Address, s.cfg.BondDenom), + &sdk.Coin{}, + sdk.NewCoin(s.cfg.BondDenom, s.cfg.StakingTokens.Sub(s.cfg.BondedTokens)), + }, + { + "total account balance of a bogus denom", + fmt.Sprintf("%s/bank/balances/%s?height=1&denom=foobar", baseURL, val.Address), + &sdk.Coin{}, + sdk.NewCoin("foobar", sdk.ZeroInt()), + }, + } + + for _, tc := range testCases { + tc := tc + s.Run(tc.name, func() { + resp, err := rest.GetRequest(tc.url) + s.Require().NoError(err) + + bz, err := rest.ParseResponseWithHeight(val.ClientCtx.JSONMarshaler, resp) + s.Require().NoError(err) + s.Require().NoError(val.ClientCtx.JSONMarshaler.UnmarshalJSON(bz, tc.respType)) + s.Require().Equal(tc.expected.String(), tc.respType.String()) + }) + } +} + +func (s *IntegrationTestSuite) TestTotalSupplyHandlerFn() { + val := s.network.Validators[0] + baseURL := val.APIAddress + + testCases := []struct { + name string + url string + respType fmt.Stringer + expected fmt.Stringer + }{ + { + "total supply", + fmt.Sprintf("%s/bank/total?height=1", baseURL), + &sdk.Coins{}, sdk.NewCoins( + sdk.NewCoin(fmt.Sprintf("%stoken", val.Moniker), s.cfg.AccountTokens), + sdk.NewCoin(s.cfg.BondDenom, s.cfg.StakingTokens.Add(sdk.NewInt(10))), + ), + }, + { + "total supply of a specific denom", + fmt.Sprintf("%s/bank/total/%s?height=1", baseURL, s.cfg.BondDenom), + &sdk.Coin{}, + sdk.NewCoin(s.cfg.BondDenom, s.cfg.StakingTokens.Add(sdk.NewInt(10))), + }, + { + "total supply of a bogus denom", + fmt.Sprintf("%s/bank/total/foobar?height=1", baseURL), + &sdk.Coin{}, + sdk.NewCoin("foobar", sdk.ZeroInt()), + }, + } + + for _, tc := range testCases { + tc := tc + s.Run(tc.name, func() { + resp, err := rest.GetRequest(tc.url) + s.Require().NoError(err) + + bz, err := rest.ParseResponseWithHeight(val.ClientCtx.JSONMarshaler, resp) + s.Require().NoError(err) + s.Require().NoError(val.ClientCtx.JSONMarshaler.UnmarshalJSON(bz, tc.respType)) + s.Require().Equal(tc.expected.String(), tc.respType.String()) + }) + } +} + +func TestIntegrationTestSuite(t *testing.T) { + suite.Run(t, new(IntegrationTestSuite)) +} diff --git a/x/bank/keeper/querier.go b/x/bank/keeper/querier.go index c73db39356..4d5f838df0 100644 --- a/x/bank/keeper/querier.go +++ b/x/bank/keeper/querier.go @@ -99,12 +99,13 @@ func querySupplyOf(ctx sdk.Context, req abci.RequestQuery, k Keeper) ([]byte, er return nil, sdkerrors.Wrap(sdkerrors.ErrJSONUnmarshal, err.Error()) } - supply := k.GetSupply(ctx).GetTotal().AmountOf(params.Denom) + amount := k.GetSupply(ctx).GetTotal().AmountOf(params.Denom) + supply := sdk.NewCoin(params.Denom, amount) - res, err := supply.MarshalJSON() + bz, err := codec.MarshalJSONIndent(types.ModuleCdc, supply) if err != nil { return nil, sdkerrors.Wrap(sdkerrors.ErrJSONMarshal, err.Error()) } - return res, nil + return bz, nil } diff --git a/x/bank/keeper/querier_test.go b/x/bank/keeper/querier_test.go index 5fb7122c62..1c52079cad 100644 --- a/x/bank/keeper/querier_test.go +++ b/x/bank/keeper/querier_test.go @@ -133,9 +133,9 @@ func (suite *IntegrationTestSuite) TestQuerier_QueryTotalSupplyOf() { suite.Require().NoError(err) suite.Require().NotNil(res) - var resp sdk.Int + var resp sdk.Coin suite.Require().NoError(app.Codec().UnmarshalJSON(res, &resp)) - suite.Require().Equal(test1Supply.Amount, resp) + suite.Require().Equal(test1Supply, resp) } func (suite *IntegrationTestSuite) TestQuerierRouteNotFound() { diff --git a/x/ibc/02-client/alias.go b/x/ibc/02-client/alias.go index 2536d66011..885b0ab701 100644 --- a/x/ibc/02-client/alias.go +++ b/x/ibc/02-client/alias.go @@ -24,7 +24,6 @@ const ( var ( // functions aliases RegisterCodec = types.RegisterCodec - SetSubModuleCodec = types.SetSubModuleCodec NewClientConsensusStates = types.NewClientConsensusStates NewGenesisState = types.NewGenesisState DefaultGenesisState = types.DefaultGenesisState diff --git a/x/ibc/02-client/types/codec.go b/x/ibc/02-client/types/codec.go index b8c0eed6ee..792a05a8de 100644 --- a/x/ibc/02-client/types/codec.go +++ b/x/ibc/02-client/types/codec.go @@ -2,12 +2,19 @@ package types import ( "github.com/cosmos/cosmos-sdk/codec" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" "github.com/cosmos/cosmos-sdk/x/ibc/02-client/exported" ) // SubModuleCdc defines the IBC client codec. var SubModuleCdc *codec.Codec +func init() { + SubModuleCdc = codec.New() + cryptocodec.RegisterCrypto(SubModuleCdc) + RegisterCodec(SubModuleCdc) +} + // RegisterCodec registers the IBC client interfaces and types func RegisterCodec(cdc *codec.Codec) { cdc.RegisterInterface((*exported.ClientState)(nil), nil) @@ -16,10 +23,4 @@ func RegisterCodec(cdc *codec.Codec) { cdc.RegisterInterface((*exported.ConsensusState)(nil), nil) cdc.RegisterInterface((*exported.Header)(nil), nil) cdc.RegisterInterface((*exported.Misbehaviour)(nil), nil) - - SetSubModuleCodec(cdc) -} - -func SetSubModuleCodec(cdc *codec.Codec) { - SubModuleCdc = cdc } diff --git a/x/ibc/07-tendermint/types/codec.go b/x/ibc/07-tendermint/types/codec.go index 2ac179d62c..b4e0c4325a 100644 --- a/x/ibc/07-tendermint/types/codec.go +++ b/x/ibc/07-tendermint/types/codec.go @@ -2,11 +2,18 @@ package types import ( "github.com/cosmos/cosmos-sdk/codec" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" ) // SubModuleCdc defines the IBC tendermint client codec. var SubModuleCdc *codec.Codec +func init() { + SubModuleCdc = codec.New() + cryptocodec.RegisterCrypto(SubModuleCdc) + RegisterCodec(SubModuleCdc) +} + // RegisterCodec registers the Tendermint types func RegisterCodec(cdc *codec.Codec) { cdc.RegisterConcrete(ClientState{}, "ibc/client/tendermint/ClientState", nil) @@ -16,11 +23,4 @@ func RegisterCodec(cdc *codec.Codec) { cdc.RegisterConcrete(&MsgCreateClient{}, "ibc/client/tendermint/MsgCreateClient", nil) cdc.RegisterConcrete(&MsgUpdateClient{}, "ibc/client/tendermint/MsgUpdateClient", nil) cdc.RegisterConcrete(&MsgSubmitClientMisbehaviour{}, "ibc/client/tendermint/MsgSubmitClientMisbehaviour", nil) - - SetSubModuleCodec(cdc) -} - -// SetSubModuleCodec sets the ibc tendermint client codec -func SetSubModuleCodec(cdc *codec.Codec) { - SubModuleCdc = cdc } diff --git a/x/ibc/09-localhost/types/codec.go b/x/ibc/09-localhost/types/codec.go index a0dff8718c..b6ccf0ebb4 100644 --- a/x/ibc/09-localhost/types/codec.go +++ b/x/ibc/09-localhost/types/codec.go @@ -2,6 +2,7 @@ package types import ( "github.com/cosmos/cosmos-sdk/codec" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" ) const ( @@ -12,14 +13,14 @@ const ( // SubModuleCdc defines the IBC localhost client codec. var SubModuleCdc *codec.Codec +func init() { + SubModuleCdc = codec.New() + cryptocodec.RegisterCrypto(SubModuleCdc) + RegisterCodec(SubModuleCdc) +} + // RegisterCodec registers the localhost types func RegisterCodec(cdc *codec.Codec) { cdc.RegisterConcrete(ClientState{}, "ibc/client/localhost/ClientState", nil) cdc.RegisterConcrete(&MsgCreateClient{}, "ibc/client/localhost/MsgCreateClient", nil) - SetSubModuleCodec(cdc) -} - -// SetSubModuleCodec sets the ibc localhost client codec -func SetSubModuleCodec(cdc *codec.Codec) { - SubModuleCdc = cdc }