Skip to content

Commit 30ddbc6

Browse files
committed
[tmpnet] Add network reuse to e2e fixture
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.
1 parent 4fecb49 commit 30ddbc6

File tree

7 files changed

+180
-115
lines changed

7 files changed

+180
-115
lines changed

scripts/tests.e2e.existing.sh

Lines changed: 23 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,10 @@
22

33
set -euo pipefail
44

5-
################################################################
6-
# This script deploys a temporary network and configures
7-
# tests.e2e.sh to execute the e2e suite against it. This
8-
# validates that tmpnetctl is capable of starting a network and
9-
# that the e2e suite is capable of executing against a network
10-
# that it did not create.
11-
################################################################
5+
# This script verifies that a network can be reused across test runs.
126

137
# e.g.,
148
# ./scripts/build.sh
15-
# ./scripts/tests.e2e.existing.sh --ginkgo.label-filter=x # All arguments are supplied to ginkgo
16-
# E2E_SERIAL=1 ./scripts/tests.e2e.sh # Run tests serially
179
# AVALANCHEGO_PATH=./build/avalanchego ./scripts/tests.e2e.existing.sh # Customization of avalanchego path
1810
if ! [[ "$0" =~ scripts/tests.e2e.existing.sh ]]; then
1911
echo "must be run from repository root"
@@ -33,33 +25,32 @@ function print_separator {
3325
# Ensure network cleanup on teardown
3426
function cleanup {
3527
print_separator
36-
echo "cleaning up temporary network"
37-
if [[ -n "${TMPNET_NETWORK_DIR:-}" ]]; then
38-
./build/tmpnetctl stop-network
39-
fi
28+
echo "cleaning up reusable network"
29+
ginkgo -v ./tests/e2e/e2e.test -- --stop-network
4030
}
4131
trap cleanup EXIT
4232

43-
# Start a temporary network
44-
./scripts/build_tmpnetctl.sh
33+
go install -v github.com/onsi/ginkgo/v2/ginkgo@v2.13.1
34+
ACK_GINKGO_RC=true ginkgo build ./tests/e2e
35+
4536
print_separator
46-
./build/tmpnetctl start-network
37+
echo "starting initial test run that should create the reusable network"
38+
ginkgo -v ./tests/e2e/e2e.test -- --reuse-network --ginkgo.focus-file=permissionless_subnets.go
4739

48-
# Determine the network configuration path from the latest symlink
49-
LATEST_SYMLINK_PATH="${HOME}/.tmpnet/networks/latest"
50-
if [[ -h "${LATEST_SYMLINK_PATH}" ]]; then
51-
TMPNET_NETWORK_DIR="$(realpath "${LATEST_SYMLINK_PATH}")"
52-
export TMPNET_NETWORK_DIR
53-
else
54-
echo "failed to find configuration path: ${LATEST_SYMLINK_PATH} symlink not found"
55-
exit 255
56-
fi
40+
print_separator
41+
echo "determining the network path of the reusable network created by the first test run"
42+
SYMLINK_PATH="${HOME}/.tmpnet/networks/latest_avalanchego-e2e"
43+
INITIAL_NETWORK_DIR="$(realpath "${SYMLINK_PATH}")"
5744

5845
print_separator
59-
# - Setting E2E_USE_EXISTING_NETWORK configures tests.e2e.sh to use
60-
# the temporary network identified by TMPNET_NETWORK_DIR.
61-
# - Only a single test (selected with --ginkgo.focus-file) is required
62-
# to validate that an existing network can be used by an e2e test
63-
# suite run. Executing more tests would be duplicative of the testing
64-
# performed against a network created by the test suite.
65-
E2E_USE_EXISTING_NETWORK=1 ./scripts/tests.e2e.sh --ginkgo.focus-file=permissionless_subnets.go
46+
echo "starting second test run that should reuse the network created by the first run"
47+
ginkgo -v ./tests/e2e/e2e.test -- --reuse-network --ginkgo.focus-file=permissionless_subnets.go
48+
49+
50+
SUBSEQUENT_NETWORK_DIR="$(realpath "${SYMLINK_PATH}")"
51+
echo "checking that the symlink path remains the same, indicating that the network was reused"
52+
if [[ "${INITIAL_NETWORK_DIR}" != "${SUBSEQUENT_NETWORK_DIR}" ]]; then
53+
print_separator
54+
echo "network was not reused across test runs"
55+
exit 1
56+
fi

tests/e2e/README.md

Lines changed: 19 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -57,46 +57,33 @@ packages. `x/transfer/virtuous.go` defines X-Chain transfer tests,
5757
labeled with `x`, which can be selected by `./tests/e2e/e2e.test
5858
--ginkgo.label-filter "x"`.
5959

60-
## Testing against an existing network
60+
## Reusing temporary networks
6161

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

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

7172
```bash
72-
# From the root of the avalanchego repo
73-
74-
# Build the tmpnetctl binary
75-
$ ./scripts/build_tmpnetctl.sh
76-
77-
# Start a new network
78-
$ ./build/tmpnetctl start-network --avalanchego-path=/path/to/avalanchego
79-
...
80-
Started network /home/me/.tmpnet/networks/20240306-152305.924531 (UUID: abaab590-b375-44f6-9ca5-f8a6dc061725)
81-
82-
Configure tmpnetctl and the test suite to target this network by default
83-
with one of the following statements:
84-
- source /home/me/.tmpnet/networks/20240306-152305.924531/network.env
85-
- export TMPNET_NETWORK_DIR=/home/me/.tmpnet/networks/20240306-152305.924531
86-
- export TMPNET_NETWORK_DIR=/home/me/.tmpnet/networks/latest
87-
88-
# Start a new test run using the existing network
89-
ginkgo -v ./tests/e2e -- \
90-
--avalanchego-path=/path/to/avalanchego \
91-
--ginkgo.focus-file=[name of file containing test] \
92-
--use-existing-network \
93-
--network-dir=/path/to/network
94-
95-
# It is also possible to set the AVALANCHEGO_PATH env var instead of supplying --avalanchego-path
96-
# and to set TMPNET_NETWORK_DIR instead of supplying --network-dir.
73+
ginkgo -v ./tests/e2e -- --avalanchego-path=/path/to/avalanchego --reuse-network
9774
```
9875

99-
See the tmpnet fixture [README](../fixture/tmpnet/README.md) for more details.
76+
If a network is not already running the first time the suite runs with
77+
`--reuse-network`, one will be started automatically and configured
78+
for reuse by subsequent test runs also supplying `--reuse-network`.
79+
80+
To stop a network configured for reuse, invoke the test suite with the
81+
`--stop-network` argument. This will stop the network and exit
82+
immediately without executing any tests:
83+
84+
```bash
85+
ginkgo -v ./tests/e2e -- --stop-network
86+
```
10087

10188
## Skipping bootstrap checks
10289

tests/fixture/e2e/env.go

Lines changed: 72 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ package e2e
55

66
import (
77
"encoding/json"
8+
"errors"
89
"math/rand"
10+
"os"
911
"time"
1012

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

61-
networkDir := flagVars.NetworkDir()
62-
63-
// Load or create a test network
6463
var network *tmpnet.Network
65-
if len(networkDir) > 0 {
66-
var err error
67-
network, err = tmpnet.ReadNetwork(networkDir)
68-
require.NoError(err)
69-
tests.Outf("{{yellow}}Using an existing network configured at %s{{/}}\n", network.Dir)
70-
71-
// Set the desired subnet configuration to ensure subsequent creation.
72-
for _, subnet := range desiredNetwork.Subnets {
73-
if existing := network.GetSubnet(subnet.Name); existing != nil {
74-
// Already present
75-
continue
64+
// Need to load the network if it is being stopped or reused
65+
if flagVars.StopNetwork() || flagVars.ReuseNetwork() {
66+
networkDir := flagVars.NetworkDir()
67+
var networkSymlink string // If populated, prompts removal of the referenced symlink if --stop-network is specified
68+
if len(networkDir) == 0 {
69+
// Attempt to reuse the network at the default owner path
70+
var err error
71+
symlinkPath, err := tmpnet.GetReusableNetworkPathForOwner(desiredNetwork.Owner)
72+
require.NoError(err)
73+
_, err = os.Stat(symlinkPath)
74+
if errors.Is(err, os.ErrNotExist) {
75+
// New network is required
76+
} else {
77+
// Try to load the existing network
78+
require.NoError(err)
79+
networkDir = symlinkPath
80+
// Enable removal of the referenced symlink if --stop-network is specified
81+
networkSymlink = symlinkPath
7682
}
77-
network.Subnets = append(network.Subnets, subnet)
7883
}
79-
} else {
80-
network = desiredNetwork
81-
StartNetwork(network, flagVars.AvalancheGoExecPath(), flagVars.PluginDir(), flagVars.NetworkShutdownDelay())
82-
}
8384

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

89-
// Wait for chains to have bootstrapped on all nodes
90-
Eventually(func() bool {
91-
for _, subnet := range network.Subnets {
92-
for _, validatorID := range subnet.ValidatorIDs {
93-
uri, err := network.GetURIForNodeID(validatorID)
94-
require.NoError(err)
95-
infoClient := info.NewClient(uri)
96-
for _, chain := range subnet.Chains {
97-
isBootstrapped, err := infoClient.IsBootstrapped(DefaultContext(), chain.ChainID.String())
98-
// Ignore errors since a chain id that is not yet known will result in a recoverable error.
99-
if err != nil || !isBootstrapped {
100-
return false
101-
}
92+
if flagVars.StopNetwork() {
93+
if len(networkSymlink) > 0 {
94+
// Remove the symlink to avoid attempts to reuse the stopped network
95+
tests.Outf("Removing symlink %s\n", networkSymlink)
96+
if err := os.Remove(networkSymlink); !errors.Is(err, os.ErrNotExist) {
97+
require.NoError(err)
10298
}
10399
}
100+
if network != nil {
101+
tests.Outf("Stopping network\n")
102+
require.NoError(network.Stop(DefaultContext()))
103+
} else {
104+
tests.Outf("No network to stop\n")
105+
}
106+
os.Exit(0)
104107
}
105-
return true
106-
}, DefaultTimeout, DefaultPollingInterval, "failed to see all chains bootstrap before timeout")
108+
}
109+
110+
// Start a new network
111+
if network == nil {
112+
network = desiredNetwork
113+
StartNetwork(
114+
network,
115+
flagVars.AvalancheGoExecPath(),
116+
flagVars.PluginDir(),
117+
flagVars.NetworkShutdownDelay(),
118+
flagVars.ReuseNetwork(),
119+
)
120+
121+
// Wait for chains to have bootstrapped on all nodes
122+
Eventually(func() bool {
123+
for _, subnet := range network.Subnets {
124+
for _, validatorID := range subnet.ValidatorIDs {
125+
uri, err := network.GetURIForNodeID(validatorID)
126+
require.NoError(err)
127+
infoClient := info.NewClient(uri)
128+
for _, chain := range subnet.Chains {
129+
isBootstrapped, err := infoClient.IsBootstrapped(DefaultContext(), chain.ChainID.String())
130+
// Ignore errors since a chain id that is not yet known will result in a recoverable error.
131+
if err != nil || !isBootstrapped {
132+
return false
133+
}
134+
}
135+
}
136+
}
137+
return true
138+
}, DefaultTimeout, DefaultPollingInterval, "failed to see all chains bootstrap before timeout")
139+
}
107140

108141
uris := network.GetNodeURIs()
109142
require.NotEmpty(uris, "network contains no nodes")
@@ -173,5 +206,6 @@ func (te *TestEnvironment) StartPrivateNetwork(network *tmpnet.Network) {
173206
sharedNetwork.DefaultRuntimeConfig.AvalancheGoPath,
174207
pluginDir,
175208
te.PrivateNetworkShutdownDelay,
209+
false, /* reuseNetwork */
176210
)
177211
}

tests/fixture/e2e/flags.go

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ type FlagVars struct {
1616
avalancheGoExecPath string
1717
pluginDir string
1818
networkDir string
19-
useExistingNetwork bool
19+
reuseNetwork bool
2020
networkShutdownDelay time.Duration
21+
stopNetwork bool
2122
}
2223

2324
func (v *FlagVars) AvalancheGoExecPath() string {
@@ -29,7 +30,7 @@ func (v *FlagVars) PluginDir() string {
2930
}
3031

3132
func (v *FlagVars) NetworkDir() string {
32-
if !v.useExistingNetwork {
33+
if !v.reuseNetwork {
3334
return ""
3435
}
3536
if len(v.networkDir) > 0 {
@@ -38,14 +39,18 @@ func (v *FlagVars) NetworkDir() string {
3839
return os.Getenv(tmpnet.NetworkDirEnvName)
3940
}
4041

41-
func (v *FlagVars) UseExistingNetwork() bool {
42-
return v.useExistingNetwork
42+
func (v *FlagVars) ReuseNetwork() bool {
43+
return v.reuseNetwork
4344
}
4445

4546
func (v *FlagVars) NetworkShutdownDelay() time.Duration {
4647
return v.networkShutdownDelay
4748
}
4849

50+
func (v *FlagVars) StopNetwork() bool {
51+
return v.stopNetwork
52+
}
53+
4954
func RegisterFlags() *FlagVars {
5055
vars := FlagVars{}
5156
flag.StringVar(
@@ -64,20 +69,26 @@ func RegisterFlags() *FlagVars {
6469
&vars.networkDir,
6570
"network-dir",
6671
"",
67-
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),
72+
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),
6873
)
6974
flag.BoolVar(
70-
&vars.useExistingNetwork,
71-
"use-existing-network",
75+
&vars.reuseNetwork,
76+
"reuse-network",
7277
false,
73-
"[optional] whether to target the existing network identified by --network-dir.",
78+
"[optional] reuse an existing network. If an existing network is not already running, create a new one and leave it running for subsequent usage.",
7479
)
7580
flag.DurationVar(
7681
&vars.networkShutdownDelay,
7782
"network-shutdown-delay",
7883
12*time.Second, // Make sure this value takes into account the scrape_interval defined in scripts/run_prometheus.sh
7984
"[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.",
8085
)
86+
flag.BoolVar(
87+
&vars.stopNetwork,
88+
"stop-network",
89+
false,
90+
"[optional] stop an existing network and exit without executing any tests.",
91+
)
8192

8293
return &vars
8394
}

tests/fixture/e2e/helpers.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,13 @@ func CheckBootstrapIsPossible(network *tmpnet.Network) {
216216
}
217217

218218
// Start a temporary network with the provided avalanchego binary.
219-
func StartNetwork(network *tmpnet.Network, avalancheGoExecPath string, pluginDir string, shutdownDelay time.Duration) {
219+
func StartNetwork(
220+
network *tmpnet.Network,
221+
avalancheGoExecPath string,
222+
pluginDir string,
223+
shutdownDelay time.Duration,
224+
reuseNetwork bool,
225+
) {
220226
require := require.New(ginkgo.GinkgoT())
221227

222228
require.NoError(
@@ -231,7 +237,24 @@ func StartNetwork(network *tmpnet.Network, avalancheGoExecPath string, pluginDir
231237
),
232238
)
233239

240+
tests.Outf("{{green}}Successfully started network{{/}}\n")
241+
242+
symlinkPath, err := tmpnet.GetReusableNetworkPathForOwner(network.Owner)
243+
require.NoError(err)
244+
245+
if reuseNetwork {
246+
// Symlink the path of the created network to the default owner path (e.g. latest_avalanchego-e2e)
247+
// to enable easy discovery for reuse.
248+
require.NoError(os.Symlink(network.Dir, symlinkPath))
249+
tests.Outf("{{green}}Symlinked %s to %s to enable reuse{{/}}\n", network.Dir, symlinkPath)
250+
}
251+
234252
ginkgo.DeferCleanup(func() {
253+
if reuseNetwork {
254+
tests.Outf("{{yellow}}Skipping shutdown for network %s (symlinked to %s) to enable reuse{{/}}\n", network.Dir, symlinkPath)
255+
return
256+
}
257+
235258
if shutdownDelay > 0 {
236259
tests.Outf("Waiting %s before network shutdown to ensure final metrics scrape\n", shutdownDelay)
237260
time.Sleep(shutdownDelay)
@@ -243,5 +266,4 @@ func StartNetwork(network *tmpnet.Network, avalancheGoExecPath string, pluginDir
243266
require.NoError(network.Stop(ctx))
244267
})
245268

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

0 commit comments

Comments
 (0)