Merge PR #6489: Test Network Testing Framework

This commit is contained in:
Alexander Bezobchuk 2020-06-26 12:30:49 -04:00 committed by GitHub
parent 3df4dd0cd9
commit 9bf3ff75f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 883 additions and 115 deletions

View File

@ -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 != ''"

View File

@ -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.

View File

@ -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

1
go.mod
View File

@ -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
)

1
go.sum
View File

@ -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=

View File

@ -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,
}
}

View File

@ -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
}
}

65
testutil/doc.go Normal file
View File

@ -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

384
testutil/network.go Normal file
View File

@ -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")
}

38
testutil/network_test.go Normal file
View File

@ -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))
}

179
testutil/util.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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))
}

View File

@ -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
}

View File

@ -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() {

View File

@ -24,7 +24,6 @@ const (
var (
// functions aliases
RegisterCodec = types.RegisterCodec
SetSubModuleCodec = types.SetSubModuleCodec
NewClientConsensusStates = types.NewClientConsensusStates
NewGenesisState = types.NewGenesisState
DefaultGenesisState = types.DefaultGenesisState

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}