Skip to content

Commit 6d8706e

Browse files
[tmpnet] Add network reuse to e2e fixture (#2935)
Signed-off-by: marun <maru.newby@avalabs.org> Co-authored-by: Stephen Buttolph <stephen@avalabs.org>
1 parent bd3eb68 commit 6d8706e

File tree

8 files changed

+179
-117
lines changed

8 files changed

+179
-117
lines changed

scripts/tests.e2e.existing.sh

Lines changed: 24 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,33 @@ 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+
echo "building e2e.test"
34+
go install -v github.com/onsi/ginkgo/v2/ginkgo@v2.13.1
35+
ACK_GINKGO_RC=true ginkgo build ./tests/e2e
36+
4537
print_separator
46-
./build/tmpnetctl start-network
38+
echo "starting initial test run that should create the reusable network"
39+
ginkgo -v ./tests/e2e/e2e.test -- --reuse-network --ginkgo.focus-file=permissionless_subnets.go
4740

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
41+
print_separator
42+
echo "determining the network path of the reusable network created by the first test run"
43+
SYMLINK_PATH="${HOME}/.tmpnet/networks/latest_avalanchego-e2e"
44+
INITIAL_NETWORK_DIR="$(realpath "${SYMLINK_PATH}")"
5745

5846
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
47+
echo "starting second test run that should reuse the network created by the first run"
48+
ginkgo -v ./tests/e2e/e2e.test -- --reuse-network --ginkgo.focus-file=permissionless_subnets.go
49+
50+
51+
SUBSEQUENT_NETWORK_DIR="$(realpath "${SYMLINK_PATH}")"
52+
echo "checking that the symlink path remains the same, indicating that the network was reused"
53+
if [[ "${INITIAL_NETWORK_DIR}" != "${SUBSEQUENT_NETWORK_DIR}" ]]; then
54+
print_separator
55+
echo "network was not reused across test runs"
56+
exit 1
57+
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: 69 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,80 @@ 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+
symlinkPath, err := tmpnet.GetReusableNetworkPathForOwner(desiredNetwork.Owner)
71+
require.NoError(err)
72+
_, err = os.Stat(symlinkPath)
73+
if !errors.Is(err, os.ErrNotExist) {
74+
// Try to load the existing network
75+
require.NoError(err)
76+
networkDir = symlinkPath
77+
// Enable removal of the referenced symlink if --stop-network is specified
78+
networkSymlink = symlinkPath
7679
}
77-
network.Subnets = append(network.Subnets, subnet)
7880
}
79-
} else {
80-
network = desiredNetwork
81-
StartNetwork(network, flagVars.AvalancheGoExecPath(), flagVars.PluginDir(), flagVars.NetworkShutdownDelay())
82-
}
8381

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))
82+
if len(networkDir) > 0 {
83+
var err error
84+
network, err = tmpnet.ReadNetwork(networkDir)
85+
require.NoError(err)
86+
tests.Outf("{{yellow}}Loaded a network configured at %s{{/}}\n", network.Dir)
87+
}
8888

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

108138
uris := network.GetNodeURIs()
109139
require.NotEmpty(uris, "network contains no nodes")
@@ -173,5 +203,6 @@ func (te *TestEnvironment) StartPrivateNetwork(network *tmpnet.Network) {
173203
sharedNetwork.DefaultRuntimeConfig.AvalancheGoPath,
174204
pluginDir,
175205
te.PrivateNetworkShutdownDelay,
206+
false, /* reuseNetwork */
176207
)
177208
}

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 & 3 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)
@@ -242,6 +265,4 @@ func StartNetwork(network *tmpnet.Network, avalancheGoExecPath string, pluginDir
242265
defer cancel()
243266
require.NoError(network.Stop(ctx))
244267
})
245-
246-
tests.Outf("{{green}}Successfully started network{{/}}\n")
247268
}

0 commit comments

Comments
 (0)