Skip to content

Commit

Permalink
[tmpnet] Add network reuse to e2e fixture
Browse files Browse the repository at this point in the history
Previously, using an existing network with the e2e suite required
starting the network with the tmpnetctl cli. This has been replaced
with direct support for network reuse within the e2e fixture to better
support testing of networks with subnets and remove the need for
other repos (subnet-evm and hypersdk) to use tmpnetctl:

 - Replace the --use-existing-network flag with --reuse-network flag
 which will ensure that a suite-compatible network is started if
 not already running and reused if already running

 - Added the --stop-network flag to support stopping a network
 previously started by --reuse-network and exiting immediately without
 running tests.

tmpnetctl is left in place for now but its future is uncertain.
  • Loading branch information
marun committed Apr 10, 2024
1 parent 5c070f8 commit 8233457
Show file tree
Hide file tree
Showing 7 changed files with 180 additions and 115 deletions.
55 changes: 23 additions & 32 deletions scripts/tests.e2e.existing.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,10 @@

set -euo pipefail

################################################################
# This script deploys a temporary network and configures
# tests.e2e.sh to execute the e2e suite against it. This
# validates that tmpnetctl is capable of starting a network and
# that the e2e suite is capable of executing against a network
# that it did not create.
################################################################
# This script verifies that a network can be reused across test runs.

# e.g.,
# ./scripts/build.sh
# ./scripts/tests.e2e.existing.sh --ginkgo.label-filter=x # All arguments are supplied to ginkgo
# E2E_SERIAL=1 ./scripts/tests.e2e.sh # Run tests serially
# AVALANCHEGO_PATH=./build/avalanchego ./scripts/tests.e2e.existing.sh # Customization of avalanchego path
if ! [[ "$0" =~ scripts/tests.e2e.existing.sh ]]; then
echo "must be run from repository root"
Expand All @@ -33,33 +25,32 @@ function print_separator {
# Ensure network cleanup on teardown
function cleanup {
print_separator
echo "cleaning up temporary network"
if [[ -n "${TMPNET_NETWORK_DIR:-}" ]]; then
./build/tmpnetctl stop-network
fi
echo "cleaning up reusable network"
ginkgo -v ./tests/e2e/e2e.test -- --stop-network
}
trap cleanup EXIT

# Start a temporary network
./scripts/build_tmpnetctl.sh
go install -v github.com/onsi/ginkgo/v2/ginkgo@v2.13.1
ACK_GINKGO_RC=true ginkgo build ./tests/e2e

print_separator
./build/tmpnetctl start-network
echo "starting initial test run that should create the reusable network"
ginkgo -v ./tests/e2e/e2e.test -- --reuse-network --ginkgo.focus-file=permissionless_subnets.go

# Determine the network configuration path from the latest symlink
LATEST_SYMLINK_PATH="${HOME}/.tmpnet/networks/latest"
if [[ -h "${LATEST_SYMLINK_PATH}" ]]; then
TMPNET_NETWORK_DIR="$(realpath "${LATEST_SYMLINK_PATH}")"
export TMPNET_NETWORK_DIR
else
echo "failed to find configuration path: ${LATEST_SYMLINK_PATH} symlink not found"
exit 255
fi
print_separator
echo "determining the network path of the reusable network created by the first test run"
SYMLINK_PATH="${HOME}/.tmpnet/networks/latest_avalanchego-e2e"
INITIAL_NETWORK_DIR="$(realpath "${SYMLINK_PATH}")"

print_separator
# - Setting E2E_USE_EXISTING_NETWORK configures tests.e2e.sh to use
# the temporary network identified by TMPNET_NETWORK_DIR.
# - Only a single test (selected with --ginkgo.focus-file) is required
# to validate that an existing network can be used by an e2e test
# suite run. Executing more tests would be duplicative of the testing
# performed against a network created by the test suite.
E2E_USE_EXISTING_NETWORK=1 ./scripts/tests.e2e.sh --ginkgo.focus-file=permissionless_subnets.go
echo "starting second test run that should reuse the network created by the first run"
ginkgo -v ./tests/e2e/e2e.test -- --reuse-network --ginkgo.focus-file=permissionless_subnets.go


SUBSEQUENT_NETWORK_DIR="$(realpath "${SYMLINK_PATH}")"
echo "checking that the symlink path remains the same, indicating that the network was reused"
if [[ "${INITIAL_NETWORK_DIR}" != "${SUBSEQUENT_NETWORK_DIR}" ]]; then
print_separator
echo "network was not reused across test runs"
exit 1
fi
51 changes: 19 additions & 32 deletions tests/e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,46 +57,33 @@ packages. `x/transfer/virtuous.go` defines X-Chain transfer tests,
labeled with `x`, which can be selected by `./tests/e2e/e2e.test
--ginkgo.label-filter "x"`.

## Testing against an existing network
## Reusing temporary networks

By default, a new temporary test network will be started before each
test run and stopped at the end of the run. When developing e2e tests,
it may be helpful to create a temporary network that can be used
across multiple test runs. This can increase the speed of iteration by
removing the requirement to start a new network for every invocation
of the test under development.
it may be helpful to reuse temporary networks across multiple test
runs. This can increase the speed of iteration by removing the
requirement to start a new network for every invocation of the test
under development.

To create a temporary network for use across test runs:
To enable network reuse across test runs, pass `--reuse-network` as an
argument to the test suite:

```bash
# From the root of the avalanchego repo

# Build the tmpnetctl binary
$ ./scripts/build_tmpnetctl.sh

# Start a new network
$ ./build/tmpnetctl start-network --avalanchego-path=/path/to/avalanchego
...
Started network /home/me/.tmpnet/networks/20240306-152305.924531 (UUID: abaab590-b375-44f6-9ca5-f8a6dc061725)

Configure tmpnetctl and the test suite to target this network by default
with one of the following statements:
- source /home/me/.tmpnet/networks/20240306-152305.924531/network.env
- export TMPNET_NETWORK_DIR=/home/me/.tmpnet/networks/20240306-152305.924531
- export TMPNET_NETWORK_DIR=/home/me/.tmpnet/networks/latest

# Start a new test run using the existing network
ginkgo -v ./tests/e2e -- \
--avalanchego-path=/path/to/avalanchego \
--ginkgo.focus-file=[name of file containing test] \
--use-existing-network \
--network-dir=/path/to/network

# It is also possible to set the AVALANCHEGO_PATH env var instead of supplying --avalanchego-path
# and to set TMPNET_NETWORK_DIR instead of supplying --network-dir.
ginkgo -v ./tests/e2e -- --avalanchego-path=/path/to/avalanchego --reuse-network
```

See the tmpnet fixture [README](../fixture/tmpnet/README.md) for more details.
If a network is not already running the first time the suite runs with
`--reuse-network`, one will be started automatically and configured
for reuse by subsequent test runs also supplying `--reuse-network`.

To stop a network configured for reuse, invoke the test suite with the
`--stop-network` argument. This will stop the network and exit
immediately without executing any tests:

```bash
ginkgo -v ./tests/e2e -- --stop-network
```

## Skipping bootstrap checks

Expand Down
110 changes: 72 additions & 38 deletions tests/fixture/e2e/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ package e2e

import (
"encoding/json"
"errors"
"math/rand"
"os"
"time"

"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -58,52 +60,83 @@ func (te *TestEnvironment) Marshal() []byte {
func NewTestEnvironment(flagVars *FlagVars, desiredNetwork *tmpnet.Network) *TestEnvironment {
require := require.New(ginkgo.GinkgoT())

networkDir := flagVars.NetworkDir()

// Load or create a test network
var network *tmpnet.Network
if len(networkDir) > 0 {
var err error
network, err = tmpnet.ReadNetwork(networkDir)
require.NoError(err)
tests.Outf("{{yellow}}Using an existing network configured at %s{{/}}\n", network.Dir)

// Set the desired subnet configuration to ensure subsequent creation.
for _, subnet := range desiredNetwork.Subnets {
if existing := network.GetSubnet(subnet.Name); existing != nil {
// Already present
continue
// Need to load the network if it is being stopped or reused
if flagVars.StopNetwork() || flagVars.ReuseNetwork() {
networkDir := flagVars.NetworkDir()
var networkSymlink string // If populated, prompts removal of the referenced symlink if --stop-network is specified
if len(networkDir) == 0 {
// Attempt to reuse the network at the default owner path
var err error
symlinkPath, err := tmpnet.GetReusableNetworkPathForOwner(desiredNetwork.Owner)
require.NoError(err)
_, err = os.Stat(symlinkPath)
if errors.Is(err, os.ErrNotExist) {
// New network is required
} else {
// Try to load the existing network
require.NoError(err)
networkDir = symlinkPath
// Enable removal of the referenced symlink if --stop-network is specified
networkSymlink = symlinkPath
}
network.Subnets = append(network.Subnets, subnet)
}
} else {
network = desiredNetwork
StartNetwork(network, flagVars.AvalancheGoExecPath(), flagVars.PluginDir(), flagVars.NetworkShutdownDelay())
}

// A new network will always need subnet creation and an existing
// network will also need subnets to be created the first time it
// is used.
require.NoError(network.CreateSubnets(DefaultContext(), ginkgo.GinkgoWriter))
if len(networkDir) > 0 {
var err error
network, err = tmpnet.ReadNetwork(networkDir)
require.NoError(err)
tests.Outf("{{yellow}}Loaded a network configured at %s{{/}}\n", network.Dir)
}

// Wait for chains to have bootstrapped on all nodes
Eventually(func() bool {
for _, subnet := range network.Subnets {
for _, validatorID := range subnet.ValidatorIDs {
uri, err := network.GetURIForNodeID(validatorID)
require.NoError(err)
infoClient := info.NewClient(uri)
for _, chain := range subnet.Chains {
isBootstrapped, err := infoClient.IsBootstrapped(DefaultContext(), chain.ChainID.String())
// Ignore errors since a chain id that is not yet known will result in a recoverable error.
if err != nil || !isBootstrapped {
return false
}
if flagVars.StopNetwork() {
if len(networkSymlink) > 0 {
// Remove the symlink to avoid attempts to reuse the stopped network
tests.Outf("Removing symlink %s\n", networkSymlink)
if err := os.Remove(networkSymlink); !errors.Is(err, os.ErrNotExist) {
require.NoError(err)
}
}
if network != nil {
tests.Outf("Stopping network\n")
require.NoError(network.Stop(DefaultContext()))
} else {
tests.Outf("No network to stop\n")
}
os.Exit(0)
}
return true
}, DefaultTimeout, DefaultPollingInterval, "failed to see all chains bootstrap before timeout")
}

// Start a new network
if network == nil {
network = desiredNetwork
StartNetwork(
network,
flagVars.AvalancheGoExecPath(),
flagVars.PluginDir(),
flagVars.NetworkShutdownDelay(),
flagVars.ReuseNetwork(),
)

// Wait for chains to have bootstrapped on all nodes
Eventually(func() bool {
for _, subnet := range network.Subnets {
for _, validatorID := range subnet.ValidatorIDs {
uri, err := network.GetURIForNodeID(validatorID)
require.NoError(err)
infoClient := info.NewClient(uri)
for _, chain := range subnet.Chains {
isBootstrapped, err := infoClient.IsBootstrapped(DefaultContext(), chain.ChainID.String())
// Ignore errors since a chain id that is not yet known will result in a recoverable error.
if err != nil || !isBootstrapped {
return false
}
}
}
}
return true
}, DefaultTimeout, DefaultPollingInterval, "failed to see all chains bootstrap before timeout")
}

uris := network.GetNodeURIs()
require.NotEmpty(uris, "network contains no nodes")
Expand Down Expand Up @@ -173,5 +206,6 @@ func (te *TestEnvironment) StartPrivateNetwork(network *tmpnet.Network) {
sharedNetwork.DefaultRuntimeConfig.AvalancheGoPath,
pluginDir,
te.PrivateNetworkShutdownDelay,
false, /* reuseNetwork */
)
}
27 changes: 19 additions & 8 deletions tests/fixture/e2e/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ type FlagVars struct {
avalancheGoExecPath string
pluginDir string
networkDir string
useExistingNetwork bool
reuseNetwork bool
networkShutdownDelay time.Duration
stopNetwork bool
}

func (v *FlagVars) AvalancheGoExecPath() string {
Expand All @@ -29,7 +30,7 @@ func (v *FlagVars) PluginDir() string {
}

func (v *FlagVars) NetworkDir() string {
if !v.useExistingNetwork {
if !v.reuseNetwork {
return ""
}
if len(v.networkDir) > 0 {
Expand All @@ -38,14 +39,18 @@ func (v *FlagVars) NetworkDir() string {
return os.Getenv(tmpnet.NetworkDirEnvName)
}

func (v *FlagVars) UseExistingNetwork() bool {
return v.useExistingNetwork
func (v *FlagVars) ReuseNetwork() bool {
return v.reuseNetwork
}

func (v *FlagVars) NetworkShutdownDelay() time.Duration {
return v.networkShutdownDelay
}

func (v *FlagVars) StopNetwork() bool {
return v.stopNetwork
}

func RegisterFlags() *FlagVars {
vars := FlagVars{}
flag.StringVar(
Expand All @@ -64,20 +69,26 @@ func RegisterFlags() *FlagVars {
&vars.networkDir,
"network-dir",
"",
fmt.Sprintf("[optional] the dir containing the configuration of an existing network to target for testing. Will only be used if --use-existing-network is specified. Also possible to configure via the %s env variable.", tmpnet.NetworkDirEnvName),
fmt.Sprintf("[optional] the dir containing the configuration of an existing network to target for testing. Will only be used if --reuse-network is specified. Also possible to configure via the %s env variable.", tmpnet.NetworkDirEnvName),
)
flag.BoolVar(
&vars.useExistingNetwork,
"use-existing-network",
&vars.reuseNetwork,
"reuse-network",
false,
"[optional] whether to target the existing network identified by --network-dir.",
"[optional] reuse an existing network. If an existing network is not already running, create a new one and leave it running for subsequent usage.",
)
flag.DurationVar(
&vars.networkShutdownDelay,
"network-shutdown-delay",
12*time.Second, // Make sure this value takes into account the scrape_interval defined in scripts/run_prometheus.sh
"[optional] the duration to wait before shutting down the test network at the end of the test run. A value greater than the scrape interval is suggested. 0 avoids waiting for shutdown.",
)
flag.BoolVar(
&vars.stopNetwork,
"stop-network",
false,
"[optional] stop an existing network and exit without executing any tests.",
)

return &vars
}
26 changes: 24 additions & 2 deletions tests/fixture/e2e/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,13 @@ func CheckBootstrapIsPossible(network *tmpnet.Network) {
}

// Start a temporary network with the provided avalanchego binary.
func StartNetwork(network *tmpnet.Network, avalancheGoExecPath string, pluginDir string, shutdownDelay time.Duration) {
func StartNetwork(
network *tmpnet.Network,
avalancheGoExecPath string,
pluginDir string,
shutdownDelay time.Duration,
reuseNetwork bool,
) {
require := require.New(ginkgo.GinkgoT())

require.NoError(
Expand All @@ -231,7 +237,24 @@ func StartNetwork(network *tmpnet.Network, avalancheGoExecPath string, pluginDir
),
)

tests.Outf("{{green}}Successfully started network{{/}}\n")

symlinkPath, err := tmpnet.GetReusableNetworkPathForOwner(network.Owner)
require.NoError(err)

if reuseNetwork {
// Symlink the path of the created network to the default owner path (e.g. latest_avalanchego-e2e)
// to enable easy discovery for reuse.
require.NoError(os.Symlink(network.Dir, symlinkPath))
tests.Outf("{{green}}Symlinked %s to %s to enable reuse{{/}}\n", network.Dir, symlinkPath)
}

ginkgo.DeferCleanup(func() {
if reuseNetwork {
tests.Outf("{{yellow}}Skipping shutdown for network %s (symlinked to %s) to enable reuse{{/}}\n", network.Dir, symlinkPath)
return
}

if shutdownDelay > 0 {
tests.Outf("Waiting %s before network shutdown to ensure final metrics scrape\n", shutdownDelay)
time.Sleep(shutdownDelay)
Expand All @@ -243,5 +266,4 @@ func StartNetwork(network *tmpnet.Network, avalancheGoExecPath string, pluginDir
require.NoError(network.Stop(ctx))
})

tests.Outf("{{green}}Successfully started network{{/}}\n")
}
Loading

0 comments on commit 8233457

Please sign in to comment.