Skip to content

Commit 6b9a223

Browse files
committed
e2e: Improve test isolation with per-test funded key allocation
1 parent d6674b3 commit 6b9a223

File tree

18 files changed

+378
-427
lines changed

18 files changed

+378
-427
lines changed

scripts/tests.e2e.sh

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ if [[ -z "${AVALANCHEGO_PATH}" ]]; then
1717
echo "Usage: ${0} [AVALANCHEGO_PATH]" >> /dev/stderr
1818
exit 255
1919
fi
20+
# Avoid requiring test execution to use the same working directory as
21+
# the script by resolving a relative path.
22+
AVALANCHEGO_PATH="$(realpath ${AVALANCHEGO_PATH})"
2023

2124
# Set the CGO flags to use the portable version of BLST
2225
#
@@ -39,10 +42,11 @@ ACK_GINKGO_RC=true ginkgo build ./tests/e2e
3942

4043
#################################
4144
echo "running e2e tests against the local cluster with ${AVALANCHEGO_PATH}"
42-
./tests/e2e/e2e.test \
43-
--ginkgo.v \
44-
--avalanchego-path=${AVALANCHEGO_PATH} \
45-
--test-keys-file=tests/test.insecure.secp256k1.keys \
45+
# - Execute in parallel (-p) with the ginkgo cli to minimize execution time.
46+
# The test binary by itself isn't capable of running specs in parallel.
47+
# - Execute in random order to identify unwanted dependency
48+
ginkgo -p -v --randomize-all ./tests/e2e/e2e.test -- \
49+
--avalanchego-path=${AVALANCHEGO_PATH} \
4650
&& EXIT_CODE=$? || EXIT_CODE=$?
4751

4852
if [[ ${EXIT_CODE} -gt 0 ]]; then

tests/e2e/banff/suites.go

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111

1212
"github.com/onsi/gomega"
1313

14-
"github.com/ava-labs/avalanchego/genesis"
1514
"github.com/ava-labs/avalanchego/ids"
1615
"github.com/ava-labs/avalanchego/tests"
1716
"github.com/ava-labs/avalanchego/tests/e2e"
@@ -24,10 +23,6 @@ import (
2423
)
2524

2625
var _ = ginkgo.Describe("[Banff]", func() {
27-
ginkgo.BeforeEach(func() {
28-
e2e.Env.EnsurePristineNetwork()
29-
})
30-
3126
ginkgo.It("can send custom assets X->P and P->X",
3227
// use this for filtering tests by labels
3328
// ref. https://onsi.github.io/ginkgo/#spec-labels
@@ -36,13 +31,11 @@ var _ = ginkgo.Describe("[Banff]", func() {
3631
"banff",
3732
),
3833
func() {
39-
uris := e2e.Env.GetURIsRW()
40-
gomega.Expect(uris).ShouldNot(gomega.BeEmpty())
41-
42-
kc := secp256k1fx.NewKeychain(genesis.EWOQKey)
34+
key := e2e.Env.AllocateTestKeys(1)[0]
35+
kc := secp256k1fx.NewKeychain(key)
4336
var wallet primary.Wallet
4437
ginkgo.By("initialize wallet", func() {
45-
walletURI := uris[0]
38+
walletURI := e2e.Env.URIs[0]
4639

4740
// 5-second is enough to fetch initial UTXOs for test cluster in "primary.NewWallet"
4841
ctx, cancel := context.WithTimeout(context.Background(), e2e.DefaultWalletCreationTimeout)
@@ -63,7 +56,7 @@ var _ = ginkgo.Describe("[Banff]", func() {
6356
owner := &secp256k1fx.OutputOwners{
6457
Threshold: 1,
6558
Addrs: []ids.ShortID{
66-
genesis.EWOQKey.PublicKey().Address(),
59+
key.Address(),
6760
},
6861
}
6962

tests/e2e/describe.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ func DescribeXChain(text string, body func()) bool {
1313
return ginkgo.Describe("[X-Chain] "+text, body)
1414
}
1515

16+
// DescribeXChainSerial annotates serial tests for X-Chain.
17+
// Can run with any type of cluster (e.g., local, fuji, mainnet).
18+
func DescribeXChainSerial(text string, body func()) bool {
19+
return ginkgo.Describe("[X-Chain] "+text, ginkgo.Serial, body)
20+
}
21+
1622
// DescribePChain annotates the tests for P-Chain.
1723
// Can run with any type of cluster (e.g., local, fuji, mainnet).
1824
func DescribePChain(text string, body func()) bool {

tests/e2e/e2e.go

Lines changed: 16 additions & 252 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,15 @@
55
package e2e
66

77
import (
8-
"errors"
9-
"fmt"
10-
"os"
11-
"sync"
128
"time"
139

1410
ginkgo "github.com/onsi/ginkgo/v2"
15-
"github.com/onsi/gomega"
11+
"github.com/stretchr/testify/require"
1612

17-
"github.com/ava-labs/avalanchego/ids"
18-
"github.com/ava-labs/avalanchego/tests"
13+
"github.com/ava-labs/avalanchego/tests/fixture"
1914
"github.com/ava-labs/avalanchego/tests/fixture/testnet"
2015
"github.com/ava-labs/avalanchego/tests/fixture/testnet/local"
2116
"github.com/ava-labs/avalanchego/utils/crypto/secp256k1"
22-
"github.com/ava-labs/avalanchego/vms/secp256k1fx"
2317
)
2418

2519
const (
@@ -29,256 +23,26 @@ const (
2923
// Defines default tx confirmation timeout.
3024
// Enough for test/custom networks.
3125
DefaultConfirmTxTimeout = 20 * time.Second
32-
33-
DefaultShutdownTimeout = 2 * time.Minute
3426
)
3527

36-
// Env is the global struct containing all we need to test
37-
var (
38-
Env = &TestEnvironment{}
39-
40-
errNoKeyFile = errors.New("test keys file not provided")
41-
)
28+
// Env is used to access shared test fixture. Intended to be
29+
// initialized by SynchronizedBeforeSuite.
30+
var Env TestEnvironment
4231

4332
type TestEnvironment struct {
44-
avalancheGoExecPath string
45-
isPersistentNetwork bool
46-
47-
setupCalled bool
48-
49-
urisMu sync.RWMutex
50-
uris []string
51-
52-
networkDir string
53-
54-
// isNetworkPristine is set to true after network creation and
55-
// false after a call to GetURIsRW to know when a test needs the
56-
// network to be recreated.
57-
isNetworkPristine bool
58-
59-
testKeysMu sync.RWMutex
60-
testKeys []*secp256k1.PrivateKey
61-
}
62-
63-
// Setup ensures the environment is configured with a network that can
64-
// be targeted for testing.
65-
func (te *TestEnvironment) Setup(
66-
avalancheGoExecPath string,
67-
testKeysFile string,
68-
persistentNetworkDir string,
69-
) error {
70-
// TODO(marun) Use testify instead of returning errors
71-
if te.setupCalled {
72-
return errors.New("setup has already been called and should only be called once")
73-
} else {
74-
te.setupCalled = true
75-
}
76-
77-
// Need to always configure avalanchego. Even if using a
78-
// persistent network, avalanchego is required for operations like
79-
// node addition.
80-
if avalancheGoExecPath != "" {
81-
if _, err := os.Stat(avalancheGoExecPath); err != nil {
82-
return fmt.Errorf("could not find avalanchego binary: %w", err)
83-
}
84-
}
85-
te.avalancheGoExecPath = avalancheGoExecPath
86-
87-
// TODO(marun) Does this really have to be configurable? Maybe just embed.
88-
err := te.LoadKeys(testKeysFile)
89-
if err != nil {
90-
return err
91-
}
92-
93-
te.networkDir = persistentNetworkDir
94-
if len(te.networkDir) > 0 {
95-
tests.Outf("{{yellow}}Using a pre-existing network{{/}}\n")
96-
te.isPersistentNetwork = true
97-
98-
network, err := local.LoadNetwork(te.networkDir)
99-
if err != nil {
100-
return fmt.Errorf("failed to load network: %w", err)
101-
}
102-
uris := network.GetURIs()
103-
if len(uris) == 0 {
104-
return fmt.Errorf("network path contains no nodes: %s", te.networkDir)
105-
}
106-
te.setURIs(uris)
107-
}
108-
109-
// Network setup will be performed just-in-time by the first
110-
// test that needs it. This avoids the cost of starting the
111-
// default network when iterating on tests that won't use it.
112-
113-
return nil
114-
}
115-
116-
func (te *TestEnvironment) LoadKeys(testKeysFile string) error {
117-
// load test keys
118-
if len(testKeysFile) == 0 {
119-
return errNoKeyFile
120-
}
121-
testKeys, err := tests.LoadHexTestKeys(testKeysFile)
122-
if err != nil {
123-
return fmt.Errorf("failed loading test keys: %w", err)
124-
}
125-
te.setTestKeys(testKeys)
126-
return nil
127-
}
128-
129-
// EnsureNetwork starts a network if one is not already running.
130-
func (te *TestEnvironment) EnsureNetwork() {
131-
if te.isPersistentNetwork {
132-
tests.Outf("{{yellow}}using pre-existing network.{{/}}\n")
133-
return
134-
}
135-
136-
// TODO(marun) Add locking to support just-in-time network
137-
// provisioning among tests executing in parallel.
138-
139-
// The absence of URIs is an indication that the network has not yet been started.
140-
if len(te.GetURIsRO()) == 0 {
141-
err := te.startCluster()
142-
gomega.Expect(err).Should(gomega.BeNil())
143-
}
144-
}
145-
146-
// EnsurePristineNetwork attempts to ensure that the currently active
147-
// network is compatible with tests that require a pristine
148-
// (unmodified) state.
149-
func (te *TestEnvironment) EnsurePristineNetwork() {
150-
if te.isPersistentNetwork {
151-
tests.Outf("{{yellow}}unable to ensure pristine initial state with a pre-existing network.{{/}}\n")
152-
return
153-
}
154-
155-
networkRunning := len(te.networkDir) != 0
156-
if networkRunning {
157-
if te.isNetworkPristine {
158-
tests.Outf("{{green}}network is already pristine.{{/}}\n")
159-
return
160-
}
161-
162-
tests.Outf("{{yellow}}network state is not pristine, the current network needs to be replaced.{{/}}\n")
163-
164-
tests.Outf("{{magenta}}shutting down the current network.{{/}}\n")
165-
err := local.StopNetwork(te.networkDir)
166-
gomega.Expect(err).Should(gomega.BeNil())
167-
tests.Outf("{{green}}network shutdown successful.{{/}}\n")
168-
}
169-
170-
err := te.startCluster()
171-
gomega.Expect(err).Should(gomega.BeNil())
172-
}
173-
174-
// startCluster launches a new network
175-
func (te *TestEnvironment) startCluster() error {
176-
tests.Outf("{{magenta}}starting network with %q{{/}}\n", te.avalancheGoExecPath)
177-
178-
// TODO(marun) Ensure removal of tmp dir
179-
tmpDir, err := os.MkdirTemp("", "")
180-
if err != nil {
181-
return fmt.Errorf("failed to create tmp dir for network: %w", err)
182-
}
183-
networkID := uint32(0) // Network ID will be generated
184-
nodeCount := 5
185-
network, err := local.StartNetwork(
186-
ginkgo.GinkgoWriter,
187-
tmpDir,
188-
&local.LocalNetwork{
189-
LocalConfig: local.LocalConfig{
190-
ExecPath: te.avalancheGoExecPath,
191-
UseStaticPorts: false, // Avoid conflicting with persistent local networks
192-
},
193-
},
194-
networkID,
195-
nodeCount,
196-
)
197-
if err != nil {
198-
return err
199-
}
200-
201-
// TODO(marun) Remove need for locking
202-
te.networkDir = network.Dir
203-
204-
uris := network.GetURIs()
205-
if len(uris) == 0 {
206-
return fmt.Errorf("network %s has no running nodes", te.networkDir)
207-
}
208-
te.setURIs(uris)
209-
tests.Outf("{{green}}successfully started network: {{/}} %+v\n", uris)
210-
211-
te.isNetworkPristine = true
212-
213-
return nil
214-
}
215-
216-
func (te *TestEnvironment) setURIs(us []string) {
217-
te.urisMu.Lock()
218-
te.uris = us
219-
te.urisMu.Unlock()
220-
}
221-
222-
// GetURIsRW returns the current network URIs for read and write
223-
// operations. The network is assumed to have been modified from its
224-
// initial state after this function has been called.
225-
//
226-
// TODO(marun) Find a better way of determining whether a network has
227-
// been modified from its initial state. Or maybe ensure most (if not
228-
// all) tests are capable of targeting any network in a
229-
// non-pathological state?
230-
func (te *TestEnvironment) GetURIsRW() []string {
231-
te.urisMu.Lock()
232-
us := te.uris
233-
te.isNetworkPristine = false
234-
te.urisMu.Unlock()
235-
return us
236-
}
237-
238-
// GetURIsRO returns the current network URIs for read-only operations.
239-
func (te *TestEnvironment) GetURIsRO() []string {
240-
te.urisMu.RLock()
241-
us := te.uris
242-
te.urisMu.RUnlock()
243-
return us
244-
}
245-
246-
func (te *TestEnvironment) setTestKeys(ks []*secp256k1.PrivateKey) {
247-
te.testKeysMu.Lock()
248-
te.testKeys = ks
249-
te.testKeysMu.Unlock()
250-
}
251-
252-
func (te *TestEnvironment) GetTestKeys() ([]*secp256k1.PrivateKey, []ids.ShortID, *secp256k1fx.Keychain) {
253-
te.testKeysMu.RLock()
254-
testKeys := te.testKeys
255-
te.testKeysMu.RUnlock()
256-
testKeyAddrs := make([]ids.ShortID, len(testKeys))
257-
for i := range testKeyAddrs {
258-
testKeyAddrs[i] = testKeys[i].PublicKey().Address()
259-
}
260-
keyChain := secp256k1fx.NewKeychain(testKeys...)
261-
return testKeys, testKeyAddrs, keyChain
33+
NetworkDir string
34+
URIs []string
35+
KeyServerURI string
26236
}
26337

264-
func (te *TestEnvironment) Teardown() error {
265-
gomega.Expect(te.setupCalled).Should(gomega.BeTrue())
266-
267-
if te.isPersistentNetwork {
268-
tests.Outf("{{yellow}}teardown not required for persistent network{{/}}\n")
269-
return nil
270-
}
271-
272-
networkRunning := len(te.networkDir) != 0
273-
if networkRunning {
274-
// Teardown the active network.
275-
tests.Outf("{{red}}shutting down network{{/}}\n")
276-
return local.StopNetwork(te.networkDir)
277-
}
278-
279-
return nil
38+
func (te *TestEnvironment) GetNetwork() testnet.Network {
39+
network, err := local.LoadNetwork(te.NetworkDir)
40+
require.NoError(ginkgo.GinkgoT(), err)
41+
return network
28042
}
28143

282-
func (te *TestEnvironment) GetNetwork() (testnet.Network, error) {
283-
return local.LoadNetwork(te.networkDir)
44+
func (te *TestEnvironment) AllocateTestKeys(count int) []*secp256k1.PrivateKey {
45+
keys, err := fixture.AllocateTestKeys(te.KeyServerURI, count)
46+
require.NoError(ginkgo.GinkgoT(), err)
47+
return keys
28448
}

0 commit comments

Comments
 (0)