Skip to content

Commit

Permalink
test: add basic end-to-end test cases (#5450)
Browse files Browse the repository at this point in the history
Partial fix for #5291.

This adds a basic set of test cases for core network invariants. Although small, it is sufficient to replace and extend the current set of P2P tests. Further test cases can be added later.
  • Loading branch information
erikgrinaker committed Oct 22, 2020
1 parent a58454e commit 64b0f5b
Show file tree
Hide file tree
Showing 4 changed files with 386 additions and 16 deletions.
58 changes: 54 additions & 4 deletions test/e2e/tests/app_test.go
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
package e2e_test

import (
"fmt"
"math/rand"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
e2e "github.com/tendermint/tendermint/test/e2e/pkg"
"github.com/tendermint/tendermint/types"
)

// Tests that any initial state given in genesis has made it into the app.
func TestApp_InitialState(t *testing.T) {
testNode(t, func(t *testing.T, node e2e.Node) {
switch {
case node.Mode == e2e.ModeSeed:
return
case len(node.Testnet.InitialState) == 0:
if len(node.Testnet.InitialState) == 0 {
return
}

Expand All @@ -28,3 +29,52 @@ func TestApp_InitialState(t *testing.T) {
}
})
}

// Tests that the app hash (as reported by the app) matches the last
// block and the node sync status.
func TestApp_Hash(t *testing.T) {
testNode(t, func(t *testing.T, node e2e.Node) {
client, err := node.Client()
require.NoError(t, err)
info, err := client.ABCIInfo(ctx)
require.NoError(t, err)
require.NotEmpty(t, info.Response.LastBlockAppHash, "expected app to return app hash")

block, err := client.Block(ctx, nil)
require.NoError(t, err)
require.EqualValues(t, info.Response.LastBlockAppHash, block.Block.AppHash,
"app hash does not match last block's app hash")

status, err := client.Status(ctx)
require.NoError(t, err)
require.EqualValues(t, info.Response.LastBlockAppHash, status.SyncInfo.LatestAppHash,
"app hash does not match node status")
})
}

// Tests that we can set a value and retrieve it.
func TestApp_Tx(t *testing.T) {
testNode(t, func(t *testing.T, node e2e.Node) {
client, err := node.Client()
require.NoError(t, err)

// Generate a random value, to prevent duplicate tx errors when
// manually running the test multiple times for a testnet.
r := rand.New(rand.NewSource(time.Now().UnixNano()))
bz := make([]byte, 32)
_, err = r.Read(bz)
require.NoError(t, err)

key := fmt.Sprintf("testapp-tx-%v", node.Name)
value := fmt.Sprintf("%x", bz)
tx := types.Tx(fmt.Sprintf("%v=%v", key, value))

_, err = client.BroadcastTxCommit(ctx, tx)
require.NoError(t, err)

resp, err := client.ABCIQuery(ctx, "", []byte(key))
require.NoError(t, err)
assert.Equal(t, key, string(resp.Response.Key))
assert.Equal(t, value, string(resp.Response.Value))
})
}
82 changes: 82 additions & 0 deletions test/e2e/tests/block_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package e2e_test

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
e2e "github.com/tendermint/tendermint/test/e2e/pkg"
)

// Tests that block headers are identical across nodes where present.
func TestBlock_Header(t *testing.T) {
blocks := fetchBlockChain(t)
testNode(t, func(t *testing.T, node e2e.Node) {
client, err := node.Client()
require.NoError(t, err)
status, err := client.Status(ctx)
require.NoError(t, err)

first := status.SyncInfo.EarliestBlockHeight
last := status.SyncInfo.LatestBlockHeight
if node.RetainBlocks > 0 {
first++ // avoid race conditions with block pruning
}

for _, block := range blocks {
if block.Header.Height < first {
continue
}
if block.Header.Height > last {
break
}
resp, err := client.Block(ctx, &block.Header.Height)
require.NoError(t, err)
require.Equal(t, block, resp.Block,
"block mismatch for height %v", block.Header.Height)
}
})
}

// Tests that the node contains the expected block range.
func TestBlock_Range(t *testing.T) {
testNode(t, func(t *testing.T, node e2e.Node) {
client, err := node.Client()
require.NoError(t, err)
status, err := client.Status(ctx)
require.NoError(t, err)

first := status.SyncInfo.EarliestBlockHeight
last := status.SyncInfo.LatestBlockHeight

switch {
case node.StateSync:
assert.Greater(t, first, node.Testnet.InitialHeight,
"state synced nodes should not contain network's initial height")

case node.RetainBlocks > 0 && int64(node.RetainBlocks) < (last-node.Testnet.InitialHeight+1):
// Delta handles race conditions in reading first/last heights.
assert.InDelta(t, node.RetainBlocks, last-first+1, 1,
"node not pruning expected blocks")

default:
assert.Equal(t, node.Testnet.InitialHeight, first,
"node's first block should be network's initial height")
}

for h := first; h <= last; h++ {
resp, err := client.Block(ctx, &(h))
if err != nil && node.RetainBlocks > 0 && h == first {
// Ignore errors in first block if node is pruning blocks due to race conditions.
continue
}
require.NoError(t, err)
assert.Equal(t, h, resp.Block.Height)
}

for h := node.Testnet.InitialHeight; h < first; h++ {
_, err := client.Block(ctx, &(h))
require.Error(t, err)
}
})
}
101 changes: 89 additions & 12 deletions test/e2e/tests/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,29 @@ import (
"context"
"os"
"path/filepath"
"sync"
"testing"

"github.com/stretchr/testify/require"
rpchttp "github.com/tendermint/tendermint/rpc/client/http"
rpctypes "github.com/tendermint/tendermint/rpc/core/types"
e2e "github.com/tendermint/tendermint/test/e2e/pkg"
"github.com/tendermint/tendermint/types"
)

func init() {
// This can be used to manually specify a testnet manifest and/or node to
// run tests against. The testnet must have been started by the runner first.
//os.Setenv("E2E_MANIFEST", "networks/simple.toml")
//os.Setenv("E2E_NODE", "validator01")
// os.Setenv("E2E_MANIFEST", "networks/ci.toml")
// os.Setenv("E2E_NODE", "validator01")
}

var (
ctx = context.Background()
ctx = context.Background()
testnetCache = map[string]e2e.Testnet{}
testnetCacheMtx = sync.Mutex{}
blocksCache = map[string][]*types.Block{}
blocksCacheMtx = sync.Mutex{}
)

// testNode runs tests for testnet nodes. The callback function is given a
Expand All @@ -29,16 +37,9 @@ var (
// test runs. If E2E_NODE is also set, only the specified node is tested,
// otherwise all nodes are tested.
func testNode(t *testing.T, testFunc func(*testing.T, e2e.Node)) {
manifest := os.Getenv("E2E_MANIFEST")
if manifest == "" {
t.Skip("E2E_MANIFEST not set, not an end-to-end test run")
}
if !filepath.IsAbs(manifest) {
manifest = filepath.Join("..", manifest)
}
t.Helper()

testnet, err := e2e.LoadTestnet(manifest)
require.NoError(t, err)
testnet := loadTestnet(t)
nodes := testnet.Nodes

if name := os.Getenv("E2E_NODE"); name != "" {
Expand All @@ -55,3 +56,79 @@ func testNode(t *testing.T, testFunc func(*testing.T, e2e.Node)) {
})
}
}

// loadTestnet loads the testnet based on the E2E_MANIFEST envvar.
func loadTestnet(t *testing.T) e2e.Testnet {
t.Helper()

manifest := os.Getenv("E2E_MANIFEST")
if manifest == "" {
t.Skip("E2E_MANIFEST not set, not an end-to-end test run")
}
if !filepath.IsAbs(manifest) {
manifest = filepath.Join("..", manifest)
}

testnetCacheMtx.Lock()
defer testnetCacheMtx.Unlock()
if testnet, ok := testnetCache[manifest]; ok {
return testnet
}

testnet, err := e2e.LoadTestnet(manifest)
require.NoError(t, err)
testnetCache[manifest] = *testnet
return *testnet
}

// fetchBlockChain fetches a complete, up-to-date block history from
// the freshest testnet archive node.
func fetchBlockChain(t *testing.T) []*types.Block {
t.Helper()

testnet := loadTestnet(t)

// Find the freshest archive node
var (
client *rpchttp.HTTP
status *rpctypes.ResultStatus
)
for _, node := range testnet.ArchiveNodes() {
c, err := node.Client()
require.NoError(t, err)
s, err := c.Status(ctx)
require.NoError(t, err)
if status == nil || s.SyncInfo.LatestBlockHeight > status.SyncInfo.LatestBlockHeight {
client = c
status = s
}
}
require.NotNil(t, client, "couldn't find an archive node")

// Fetch blocks. Look for existing block history in the block cache, and
// extend it with any new blocks that have been produced.
blocksCacheMtx.Lock()
defer blocksCacheMtx.Unlock()

from := status.SyncInfo.EarliestBlockHeight
to := status.SyncInfo.LatestBlockHeight
blocks, ok := blocksCache[testnet.Name]
if !ok {
blocks = make([]*types.Block, 0, to-from+1)
}
if len(blocks) > 0 {
from = blocks[len(blocks)-1].Height + 1
}

for h := from; h <= to; h++ {
resp, err := client.Block(ctx, &(h))
require.NoError(t, err)
require.NotNil(t, resp.Block)
require.Equal(t, h, resp.Block.Height, "unexpected block height %v", resp.Block.Height)
blocks = append(blocks, resp.Block)
}
require.NotEmpty(t, blocks, "blockchain does not contain any blocks")
blocksCache[testnet.Name] = blocks

return blocks
}
Loading

0 comments on commit 64b0f5b

Please sign in to comment.