Skip to content

Commit

Permalink
refactor(e2e): modular test runner (#1999)
Browse files Browse the repository at this point in the history
* use should_run_go_test to skip e2e

* attempt to disable upgrade

* remporary remove get data from build cache

* go setup

* bring back cache

* log for upgrade

* invert the disable condition

* comment

* add more logic regarding skipping some e2e components

* fix markdown lint

* change branch to main

* refactor to be able to run local version without building Docker image in CI

* remove if on building local image

* TestCreatePool runs only post-upgrade

* lint

* TestCreatePoolPostUpgrade

* Document

* restore git diff

* Update tests/e2e/README.md

Co-authored-by: Dev Ojha <[email protected]>

* README updates

* rename files; move validator configs, factory and constants to separate files

* avoid exporting base configurer

* all reference to "local" are replaced to" current branch"

* fix formatting

* move all docker logic to containers package

* move hermes resource to container manager

* move val resources to container manager

* clear docker resources in manager

* unexport hermes resource

* configurer chain package

* pass chain config to RunHermesResource

* rename configurer chain to chain config consistently

* unexport validator resources

* unexport network

* define commands and queries on chain config struct

* lint

* format

* remove unused noRestart function

* remove more redundant structs

* refactor(e2e): modular test runner

* restore test.yml

* fix e2e_setup_test.go

* space

* readme

* fix sf test

* restore initialization

* ibc skip

* minimize diff and lint

* Update tests/e2e/README.md

Co-authored-by: Adam Tucker <[email protected]>

* factory design pattern

* Adam's suggestions

Co-authored-by: Dev Ojha <[email protected]>
Co-authored-by: Adam Tucker <[email protected]>
  • Loading branch information
3 people authored Jul 8, 2022
1 parent 1b5a1d3 commit 05375f8
Show file tree
Hide file tree
Showing 16 changed files with 1,286 additions and 861 deletions.
90 changes: 86 additions & 4 deletions tests/e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,93 @@ core bootstrapping logic that creates a testing environment via Docker
containers. A testing network is created dynamically with 2 test
validators.

The file e2e\_test.go contains the actual end-to-end integration tests
The file `e2e_test.go` contains the actual end-to-end integration tests
that utilize the testing suite.

Currently, there is a single test in `e2e_test.go` to query the balances
of a validator.
Currently, there is a single IBC test in `e2e_test.go`.

Additionally, there is an ability to disable certain components
of the e2e suite. This can be done by setting the environment
variables. See "Environment variables" section below for more details.

## How It Works

Conceptually, we can split the e2e setup into 2 parts:

1. Chain Initialization

The chain can either be initailized off of the current branch, or off the prior mainnet release and then upgraded to the current branch.

If current, we run chain initialization off of the current Git branch
by calling `chain.Init(...)` method in the `configurer/current.go`.

If with the upgrade, the same `chain.Init(...)` function is run inside a Docker container
of the previous Osmosis version, inside `configurer/upgrade.go`. This is
needed to initialize chain configs and the genesis of the previous version that
we are upgrading from.

The decision of what configuration type to use is decided by the `Configurer`.
This is an interface that has `CurrentBranchConfigurer` and `UpgradeConfigurer` implementations.
There is also a `BaseConfigurer` which is shared by the concrete implementations. However,
the user of the `configurer` package does not need to know about this detail.

When the desired configurer is created, the caller may
configure the chain in the desired way as follows:

```go
conf, _ := configurer.New(..., < isIBCEnabled bool >, < isUpgradeEnabled bool >)

conf.ConfigureChains()
```

The caller (e2e setup logic), does not need to be concerned about what type of
configurations is hapenning in the background. The appropriate logic is selected
depending on what the values of the arguments to `configurer.New(...)` are.

The configurer constructor is using a factory design pattern
to decide on what kind of configurer to return. Factory design
pattern is used to decouple the client from the initialization
details of the configurer. More on this can be found
[here](https://www.tutorialspoint.com/design_pattern/factory_pattern.htm)

The rules for deciding on the configurer type
are as follows:

- If only `isIBCEnabled`, we want to have 2 chains initialized at the
current branch version of Osmosis codebase

- If only `isUpgradeEnabled`, that's invalid (we can decouple upgrade
testing from IBC in a future PR)
- If both `isIBCEnabled` and `isUpgradeEnabled`, we want 2 chain
with IBC initialized at the previous Osmosis version
- If none are true, we only need one chain at the current branch version
of the Osmosis code
2. Setting up e2e components
Currently, there exist the following components:
- Base logic
- This is the most basic type of setup where a single chain is created
- It simply spins up the desired number of validators on a chain.
- IBC testing
- 2 chains are created connected by Hermes relayer
- Upgrade Testing
- 2 chains of the older Osmosis version are created, and
connected by Hermes relayer
- Upgrade testing
- CLI commands are run to create an upgrade proposal and approve it
- Old version containers are stopped and the upgrade binary is added
- Current branch Osmosis version is spun up to continue with testing
- State Sync Testing (WIP)
- An additional full node is created after a chain has started.
- This node is meant to state sync with the rest of the system.
This is done in `configurer/setup_runner.go` via function decorator design pattern
where we chain the desired setup components during configurer creation.
[Example](https://github.com/osmosis-labs/osmosis/blob/c5d5c9f0c6b5c7fdf9688057eb78ec793f6dd580/tests/e2e/configurer/configurer.go#L166)
## `initialization` Package
Expand Down Expand Up @@ -56,7 +138,7 @@ Docker containers. Currently, validator containers are created
with a name of the corresponding validator struct that is initialized
in the `chain` package.
## Running Locally
## Running From Current Branch
### To build chain initialization image
Expand Down
218 changes: 218 additions & 0 deletions tests/e2e/configurer/base.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
package configurer

import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path"
"path/filepath"
"testing"
"time"

"github.com/stretchr/testify/require"
rpchttp "github.com/tendermint/tendermint/rpc/client/http"

"github.com/osmosis-labs/osmosis/v7/tests/e2e/configurer/chain"
"github.com/osmosis-labs/osmosis/v7/tests/e2e/containers"
"github.com/osmosis-labs/osmosis/v7/tests/e2e/initialization"
"github.com/osmosis-labs/osmosis/v7/tests/e2e/util"
)

// baseConfigurer is the base implementation for the
// other 2 types of configurers. It is not meant to be used
// on its own. Instead, it is meant to be embedded
// by composition into more concrete configurers.
type baseConfigurer struct {
chainConfigs []*chain.Config
containerManager *containers.Manager
setupTests setupFn
syncUntilHeight int64 // the height until which to wait for validators to sync when first started.
t *testing.T
}

// defaultSyncUntilHeight arbitrary small height to make sure the chain is making progress.
const defaultSyncUntilHeight = 3

func (bc *baseConfigurer) ClearResources() error {
bc.t.Log("tearing down e2e integration test suite...")

if err := bc.containerManager.ClearResources(); err != nil {
return err
}

for _, chainConfig := range bc.chainConfigs {
os.RemoveAll(chainConfig.DataDir)
}
return nil
}

func (bc *baseConfigurer) GetChainConfig(chainIndex int) *chain.Config {
return bc.chainConfigs[chainIndex]
}

func (bc *baseConfigurer) RunValidators() error {
for _, chainConfig := range bc.chainConfigs {
if err := bc.runValidators(chainConfig); err != nil {
return err
}
}
return nil
}

func (bc *baseConfigurer) runValidators(chainConfig *chain.Config) error {
bc.t.Logf("starting %s validator containers...", chainConfig.Id)

for _, val := range chainConfig.NodeConfigs {
resource, err := bc.containerManager.RunValidatorResource(chainConfig.Id, val.Name, val.ConfigDir)
if err != nil {
return err
}
bc.t.Logf("started %s validator container: %s", resource.Container.Name[1:], resource.Container.ID)
}

validatorHostPort, err := bc.containerManager.GetValidatorHostPort(chainConfig.Id, 0, "26657/tcp")
if err != nil {
return err
}

rpcClient, err := rpchttp.New(fmt.Sprintf("tcp://%s", validatorHostPort), "/websocket")
if err != nil {
return err
}

require.Eventually(
bc.t,
func() bool {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()

status, err := rpcClient.Status(ctx)
if err != nil {
return false
}

// let the node produce a few blocks
if status.SyncInfo.CatchingUp && status.SyncInfo.LatestBlockHeight < bc.syncUntilHeight {
return false
}

return true
},
5*time.Minute,
time.Second,
"Osmosis node failed to produce blocks",
)
chainConfig.ExtractValidatorOperatorAddresses()
return nil
}

func (bc *baseConfigurer) RunIBC() error {
// Run a relayer between every possible pair of chains.
for i := 0; i < len(bc.chainConfigs); i++ {
for j := i + 1; j < len(bc.chainConfigs); j++ {
if err := bc.runIBCRelayer(bc.chainConfigs[i], bc.chainConfigs[j]); err != nil {
return err
}
}
}
return nil
}

func (bc *baseConfigurer) runIBCRelayer(chainConfigA *chain.Config, chainConfigB *chain.Config) error {
bc.t.Log("starting Hermes relayer container...")

tmpDir, err := os.MkdirTemp("", "osmosis-e2e-testnet-hermes-")
if err != nil {
return err
}

hermesCfgPath := path.Join(tmpDir, "hermes")

if err := os.MkdirAll(hermesCfgPath, 0o755); err != nil {
return err
}

_, err = util.CopyFile(
filepath.Join("./scripts/", "hermes_bootstrap.sh"),
filepath.Join(hermesCfgPath, "hermes_bootstrap.sh"),
)
if err != nil {
return err
}

hermesResource, err := bc.containerManager.RunHermesResource(
chainConfigA.Id,
chainConfigA.NodeConfigs[0].Mnemonic,
chainConfigB.Id, chainConfigB.NodeConfigs[0].Mnemonic,
hermesCfgPath)
if err != nil {
return err
}

endpoint := fmt.Sprintf("http://%s/state", hermesResource.GetHostPort("3031/tcp"))

require.Eventually(bc.t, func() bool {
resp, err := http.Get(endpoint)
if err != nil {
return false
}

defer resp.Body.Close()

bz, err := io.ReadAll(resp.Body)
if err != nil {
return false
}

var respBody map[string]interface{}
if err := json.Unmarshal(bz, &respBody); err != nil {
return false
}

status, ok := respBody["status"].(string)
require.True(bc.t, ok)
result, ok := respBody["result"].(map[string]interface{})
require.True(bc.t, ok)

chains, ok := result["chains"].([]interface{})
require.True(bc.t, ok)

return status == "success" && len(chains) == 2
},
5*time.Minute,
time.Second,
"hermes relayer not healthy")

bc.t.Logf("started Hermes relayer container: %s", bc.containerManager.GetHermesContainerID())

// XXX: Give time to both networks to start, otherwise we might see gRPC
// transport errors.
time.Sleep(10 * time.Second)

// create the client, connection and channel between the two Osmosis chains
return bc.connectIBCChains(chainConfigA, chainConfigB)
}

func (bc *baseConfigurer) connectIBCChains(chainA *chain.Config, chainB *chain.Config) error {
bc.t.Logf("connecting %s and %s chains via IBC", chainA.ChainMeta.Id, chainB.ChainMeta.Id)
cmd := []string{"hermes", "create", "channel", chainA.ChainMeta.Id, chainB.ChainMeta.Id, "--port-a=transfer", "--port-b=transfer"}
_, _, err := bc.containerManager.ExecCmd(bc.t, "", 0, cmd, "successfully opened init channel")
if err != nil {
return err
}
bc.t.Logf("connected %s and %s chains via IBC", chainA.ChainMeta.Id, chainB.ChainMeta.Id)
return nil
}

func (bc *baseConfigurer) initializeChainConfigFromInitChain(initializedChain *initialization.Chain, chainConfig *chain.Config) {
chainConfig.ChainMeta = initializedChain.ChainMeta
chainConfig.NodeConfigs = make([]*chain.ValidatorConfig, 0, len(initializedChain.Nodes))
for _, validator := range initializedChain.Nodes {
chainConfig.NodeConfigs = append(chainConfig.NodeConfigs, &chain.ValidatorConfig{
Node: *validator,
})
}
}
43 changes: 43 additions & 0 deletions tests/e2e/configurer/chain/chain.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package chain

import (
"testing"

"github.com/osmosis-labs/osmosis/v7/tests/e2e/containers"
"github.com/osmosis-labs/osmosis/v7/tests/e2e/initialization"
)

type Config struct {
initialization.ChainMeta

ValidatorInitConfigs []*initialization.NodeConfig
// voting period is number of blocks it takes to deposit, 1.2 seconds per validator to vote on the prop, and a buffer.
VotingPeriod float32
// upgrade proposal height for chain.
PropHeight int
LatestProposalNumber int
LatestLockNumber int
NodeConfigs []*ValidatorConfig

t *testing.T
containerManager *containers.Manager
}

type status struct {
LatestHeight string `json:"latest_block_height"`
}

type syncInfo struct {
SyncInfo status `json:"SyncInfo"`
}

func New(t *testing.T, containerManager *containers.Manager, id string, initValidatorConfigs []*initialization.NodeConfig) *Config {
return &Config{
ChainMeta: initialization.ChainMeta{
Id: id,
},
ValidatorInitConfigs: initValidatorConfigs,
t: t,
containerManager: containerManager,
}
}
Loading

0 comments on commit 05375f8

Please sign in to comment.