Skip to content

Commit 0cf3d29

Browse files
authored
e2e: Diagnose and fix e2e test flakes (#1941)
1 parent a73bcb6 commit 0cf3d29

File tree

16 files changed

+155
-52
lines changed

16 files changed

+155
-52
lines changed

tests/colors.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
package tests
55

66
import (
7-
"fmt"
7+
ginkgo "github.com/onsi/ginkgo/v2"
88

99
"github.com/onsi/ginkgo/v2/formatter"
1010
)
@@ -20,5 +20,11 @@ import (
2020
// for an exhaustive list of color options.
2121
func Outf(format string, args ...interface{}) {
2222
s := formatter.F(format, args...)
23-
fmt.Fprint(formatter.ColorableStdOut, s)
23+
// Use GinkgoWriter to ensure that output from this function is
24+
// printed sequentially within other test output produced with
25+
// GinkgoWriter (e.g. `STEP:...`) when tests are run in
26+
// parallel. ginkgo collects and writes stdout separately from
27+
// GinkgoWriter during parallel execution and the resulting output
28+
// can be confusing.
29+
ginkgo.GinkgoWriter.Print(s)
2430
}

tests/e2e/banff/suites.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ var _ = ginkgo.Describe("[Banff]", func() {
2929
),
3030
func() {
3131
keychain := e2e.Env.NewKeychain(1)
32-
wallet := e2e.Env.NewWallet(keychain)
32+
wallet := e2e.Env.NewWallet(keychain, e2e.Env.GetRandomNodeURI())
3333

3434
// Get the P-chain and the X-chain wallets
3535
pWallet := wallet.P()

tests/e2e/c/interchain_workflow.go

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ var _ = e2e.DescribeCChain("[Interchain Workflow]", func() {
3232
)
3333

3434
ginkgo.It("should ensure that funds can be transferred from the C-Chain to the X-Chain and the P-Chain", func() {
35+
ginkgo.By("initializing a new eth client")
36+
// Select a random node URI to use for both the eth client and
37+
// the wallet to avoid having to verify that all nodes are at
38+
// the same height before initializing the wallet.
39+
nodeURI := e2e.Env.GetRandomNodeURI()
40+
ethClient := e2e.Env.NewEthClient(nodeURI)
41+
3542
ginkgo.By("allocating a pre-funded key to send from and a recipient key to deliver to")
3643
senderKey := e2e.Env.AllocateFundedKey()
3744
senderEthAddress := evm.GetEthAddress(senderKey)
@@ -40,18 +47,11 @@ var _ = e2e.DescribeCChain("[Interchain Workflow]", func() {
4047
require.NoError(err)
4148
recipientEthAddress := evm.GetEthAddress(recipientKey)
4249

43-
// Select a random node URI to use for both the eth client and
44-
// the wallet to avoid having to verify that all nodes are at
45-
// the same height before initializing the wallet.
46-
nodeURI := e2e.Env.GetRandomNodeURI()
47-
ethClient := e2e.Env.NewEthClientForURI(nodeURI)
48-
4950
ginkgo.By("sending funds from one address to another on the C-Chain", func() {
5051
// Create transaction
5152
acceptedNonce, err := ethClient.AcceptedNonceAt(e2e.DefaultContext(), senderEthAddress)
5253
require.NoError(err)
53-
gasPrice, err := ethClient.SuggestGasPrice(e2e.DefaultContext())
54-
require.NoError(err)
54+
gasPrice := e2e.SuggestGasPrice(ethClient)
5555
tx := types.NewTransaction(
5656
acceptedNonce,
5757
recipientEthAddress,
@@ -68,7 +68,7 @@ var _ = e2e.DescribeCChain("[Interchain Workflow]", func() {
6868
signedTx, err := types.SignTx(tx, signer, senderKey.ToECDSA())
6969
require.NoError(err)
7070

71-
require.NoError(ethClient.SendTransaction(e2e.DefaultContext(), signedTx))
71+
_ = e2e.SendEthTransaction(ethClient, signedTx)
7272

7373
ginkgo.By("waiting for the C-Chain recipient address to have received the sent funds")
7474
e2e.Eventually(func() bool {
@@ -83,7 +83,7 @@ var _ = e2e.DescribeCChain("[Interchain Workflow]", func() {
8383
// matches on-chain state.
8484
ginkgo.By("initializing a keychain and associated wallet")
8585
keychain := secp256k1fx.NewKeychain(senderKey, recipientKey)
86-
baseWallet := e2e.Env.NewWalletForURI(keychain, nodeURI)
86+
baseWallet := e2e.Env.NewWallet(keychain, nodeURI)
8787
xWallet := baseWallet.X()
8888
cWallet := baseWallet.C()
8989
pWallet := baseWallet.P()
@@ -115,6 +115,7 @@ var _ = e2e.DescribeCChain("[Interchain Workflow]", func() {
115115
xWallet.BlockchainID(),
116116
exportOutputs,
117117
e2e.WithDefaultContext(),
118+
e2e.WithSuggestedGasPrice(ethClient),
118119
)
119120
require.NoError(err)
120121
})
@@ -141,6 +142,7 @@ var _ = e2e.DescribeCChain("[Interchain Workflow]", func() {
141142
constants.PlatformChainID,
142143
exportOutputs,
143144
e2e.WithDefaultContext(),
145+
e2e.WithSuggestedGasPrice(ethClient),
144146
)
145147
require.NoError(err)
146148
})

tests/e2e/e2e.go

Lines changed: 70 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ package e2e
77
import (
88
"context"
99
"encoding/json"
10+
"errors"
1011
"fmt"
12+
"math/big"
1113
"math/rand"
1214
"strings"
1315
"time"
@@ -16,8 +18,11 @@ import (
1618

1719
"github.com/stretchr/testify/require"
1820

21+
"github.com/ava-labs/coreth/core/types"
1922
"github.com/ava-labs/coreth/ethclient"
23+
"github.com/ava-labs/coreth/interfaces"
2024

25+
"github.com/ava-labs/avalanchego/ids"
2126
"github.com/ava-labs/avalanchego/tests"
2227
"github.com/ava-labs/avalanchego/tests/fixture"
2328
"github.com/ava-labs/avalanchego/tests/fixture/testnet"
@@ -58,7 +63,7 @@ type TestEnvironment struct {
5863
// The directory where the test network configuration is stored
5964
NetworkDir string
6065
// URIs used to access the API endpoints of nodes of the network
61-
URIs []string
66+
URIs []testnet.NodeURI
6267
// The URI used to access the http server that allocates test data
6368
TestDataServerURI string
6469

@@ -76,9 +81,11 @@ func InitTestEnvironment(envBytes []byte) {
7681

7782
// Retrieve a random URI to naively attempt to spread API load across
7883
// nodes.
79-
func (te *TestEnvironment) GetRandomNodeURI() string {
84+
func (te *TestEnvironment) GetRandomNodeURI() testnet.NodeURI {
8085
r := rand.New(rand.NewSource(time.Now().Unix())) //#nosec G404
81-
return te.URIs[r.Intn(len(te.URIs))]
86+
nodeURI := te.URIs[r.Intn(len(te.URIs))]
87+
tests.Outf("{{blue}} targeting node %s with URI: %s{{/}}\n", nodeURI.NodeID, nodeURI.URI)
88+
return nodeURI
8289
}
8390

8491
// Retrieve the network to target for testing.
@@ -92,6 +99,7 @@ func (te *TestEnvironment) GetNetwork() testnet.Network {
9299
func (te *TestEnvironment) AllocateFundedKeys(count int) []*secp256k1.PrivateKey {
93100
keys, err := fixture.AllocateFundedKeys(te.TestDataServerURI, count)
94101
te.require.NoError(err)
102+
tests.Outf("{{blue}} allocated funded key(s): %+v{{/}}\n", keys)
95103
return keys
96104
}
97105

@@ -102,36 +110,33 @@ func (te *TestEnvironment) AllocateFundedKey() *secp256k1.PrivateKey {
102110

103111
// Create a new keychain with the specified number of test keys.
104112
func (te *TestEnvironment) NewKeychain(count int) *secp256k1fx.Keychain {
105-
tests.Outf("{{blue}} initializing keychain with %d keys {{/}}\n", count)
106113
keys := te.AllocateFundedKeys(count)
107114
return secp256k1fx.NewKeychain(keys...)
108115
}
109116

110-
// Create a new wallet for the provided keychain against a random node URI.
111-
func (te *TestEnvironment) NewWallet(keychain *secp256k1fx.Keychain) primary.Wallet {
112-
return te.NewWalletForURI(keychain, te.GetRandomNodeURI())
113-
}
114-
115117
// Create a new wallet for the provided keychain against the specified node URI.
116-
func (te *TestEnvironment) NewWalletForURI(keychain *secp256k1fx.Keychain, uri string) primary.Wallet {
117-
tests.Outf("{{blue}} initializing a new wallet {{/}}\n")
118-
wallet, err := primary.MakeWallet(DefaultContext(), &primary.WalletConfig{
119-
URI: uri,
118+
func (te *TestEnvironment) NewWallet(keychain *secp256k1fx.Keychain, nodeURI testnet.NodeURI) primary.Wallet {
119+
tests.Outf("{{blue}} initializing a new wallet for node %s with URI: %s {{/}}\n", nodeURI.NodeID, nodeURI.URI)
120+
baseWallet, err := primary.MakeWallet(DefaultContext(), &primary.WalletConfig{
121+
URI: nodeURI.URI,
120122
AVAXKeychain: keychain,
121123
EthKeychain: keychain,
122124
})
123125
te.require.NoError(err)
124-
return wallet
125-
}
126-
127-
// Create a new eth client targeting a random node.
128-
func (te *TestEnvironment) NewEthClient() ethclient.Client {
129-
return te.NewEthClientForURI(te.GetRandomNodeURI())
126+
return primary.NewWalletWithOptions(
127+
baseWallet,
128+
common.WithPostIssuanceFunc(
129+
func(id ids.ID) {
130+
tests.Outf(" issued transaction with ID: %s\n", id)
131+
},
132+
),
133+
)
130134
}
131135

132136
// Create a new eth client targeting the specified node URI.
133-
func (te *TestEnvironment) NewEthClientForURI(nodeURI string) ethclient.Client {
134-
nodeAddress := strings.Split(nodeURI, "//")[1]
137+
func (te *TestEnvironment) NewEthClient(nodeURI testnet.NodeURI) ethclient.Client {
138+
tests.Outf("{{blue}} initializing a new eth client for node %s with URI: %s {{/}}\n", nodeURI.NodeID, nodeURI.URI)
139+
nodeAddress := strings.Split(nodeURI.URI, "//")[1]
135140
uri := fmt.Sprintf("ws://%s/ext/bc/C/ws", nodeAddress)
136141
client, err := ethclient.Dial(uri)
137142
te.require.NoError(err)
@@ -198,3 +203,47 @@ func WaitForHealthy(node testnet.Node) {
198203
defer cancel()
199204
require.NoError(ginkgo.GinkgoT(), testnet.WaitForHealthy(ctx, node))
200205
}
206+
207+
// Sends an eth transaction, waits for the transaction receipt to be issued
208+
// and checks that the receipt indicates success.
209+
func SendEthTransaction(ethClient ethclient.Client, signedTx *types.Transaction) *types.Receipt {
210+
require := require.New(ginkgo.GinkgoT())
211+
212+
txID := signedTx.Hash()
213+
tests.Outf(" sending eth transaction with ID: %s\n", txID)
214+
215+
require.NoError(ethClient.SendTransaction(DefaultContext(), signedTx))
216+
217+
// Wait for the receipt
218+
var receipt *types.Receipt
219+
Eventually(func() bool {
220+
var err error
221+
receipt, err = ethClient.TransactionReceipt(DefaultContext(), txID)
222+
if errors.Is(err, interfaces.NotFound) {
223+
return false // Transaction is still pending
224+
}
225+
require.NoError(err)
226+
return true
227+
}, DefaultTimeout, DefaultPollingInterval, "failed to see transaction acceptance before timeout")
228+
229+
require.Equal(receipt.Status, types.ReceiptStatusSuccessful)
230+
return receipt
231+
}
232+
233+
// Determines the suggested gas price for the configured client that will
234+
// maximize the chances of transaction acceptance.
235+
func SuggestGasPrice(ethClient ethclient.Client) *big.Int {
236+
gasPrice, err := ethClient.SuggestGasPrice(DefaultContext())
237+
require.NoError(ginkgo.GinkgoT(), err)
238+
// Double the suggested gas price to maximize the chances of
239+
// acceptance. Maybe this can be revisited pending resolution of
240+
// https://github.com/ava-labs/coreth/issues/314.
241+
gasPrice.Add(gasPrice, gasPrice)
242+
return gasPrice
243+
}
244+
245+
// Helper simplifying use via an option of a gas price appropriate for testing.
246+
func WithSuggestedGasPrice(ethClient ethclient.Client) common.Option {
247+
baseFee := SuggestGasPrice(ethClient)
248+
return common.WithBaseFee(baseFee)
249+
}

tests/e2e/p/permissionless_subnets.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,18 @@ var _ = e2e.DescribePChain("[Permissionless Subnets]", func() {
3535
"permissionless-subnets",
3636
),
3737
func() {
38+
nodeURI := e2e.Env.GetRandomNodeURI()
39+
3840
keychain := e2e.Env.NewKeychain(1)
39-
baseWallet := e2e.Env.NewWallet(keychain)
41+
baseWallet := e2e.Env.NewWallet(keychain, nodeURI)
4042

41-
nodeURI := e2e.Env.GetRandomNodeURI()
4243
pWallet := baseWallet.P()
4344
xWallet := baseWallet.X()
4445
xChainID := xWallet.BlockchainID()
4546

4647
var validatorID ids.NodeID
4748
ginkgo.By("retrieving the node ID of a primary network validator", func() {
48-
pChainClient := platformvm.NewClient(nodeURI)
49+
pChainClient := platformvm.NewClient(nodeURI.URI)
4950
ctx, cancel := context.WithTimeout(context.Background(), e2e.DefaultTimeout)
5051
validatorIDs, err := pChainClient.SampleValidators(ctx, constants.PrimaryNetworkID, 1)
5152
cancel()

tests/e2e/p/workflow.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,12 @@ var _ = e2e.DescribePChain("[Workflow]", func() {
4444
func() {
4545
nodeURI := e2e.Env.GetRandomNodeURI()
4646
keychain := e2e.Env.NewKeychain(2)
47-
baseWallet := e2e.Env.NewWallet(keychain)
47+
baseWallet := e2e.Env.NewWallet(keychain, nodeURI)
4848

4949
pWallet := baseWallet.P()
5050
avaxAssetID := baseWallet.P().AVAXAssetID()
5151
xWallet := baseWallet.X()
52-
pChainClient := platformvm.NewClient(nodeURI)
52+
pChainClient := platformvm.NewClient(nodeURI.URI)
5353

5454
tests.Outf("{{blue}} fetching minimal stake amounts {{/}}\n")
5555
ctx, cancel := context.WithTimeout(context.Background(), e2e.DefaultWalletCreationTimeout)
@@ -60,7 +60,7 @@ var _ = e2e.DescribePChain("[Workflow]", func() {
6060
tests.Outf("{{green}} minimal delegator stake: %d {{/}}\n", minDelStake)
6161

6262
tests.Outf("{{blue}} fetching tx fee {{/}}\n")
63-
infoClient := info.NewClient(nodeURI)
63+
infoClient := info.NewClient(nodeURI.URI)
6464
ctx, cancel = context.WithTimeout(context.Background(), e2e.DefaultWalletCreationTimeout)
6565
fees, err := infoClient.GetTxFee(ctx)
6666
cancel()

tests/e2e/static-handlers/suites.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ var _ = ginkgo.Describe("[StaticHandlers]", func() {
110110
},
111111
},
112112
}
113-
staticClient := avm.NewStaticClient(e2e.Env.GetRandomNodeURI())
113+
staticClient := avm.NewStaticClient(e2e.Env.GetRandomNodeURI().URI)
114114
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
115115
resp, err := staticClient.BuildGenesis(ctx, &avmArgs)
116116
cancel()
@@ -181,7 +181,7 @@ var _ = ginkgo.Describe("[StaticHandlers]", func() {
181181
Encoding: formatting.Hex,
182182
}
183183

184-
staticClient := api.NewStaticClient(e2e.Env.GetRandomNodeURI())
184+
staticClient := api.NewStaticClient(e2e.Env.GetRandomNodeURI().URI)
185185
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
186186
resp, err := staticClient.BuildGenesis(ctx, &buildGenesisArgs)
187187
cancel()

tests/e2e/x/interchain_workflow.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,15 @@ var _ = e2e.DescribeXChain("[Interchain Workflow]", func() {
2929
const transferAmount = 10 * units.Avax
3030

3131
ginkgo.It("should ensure that funds can be transferred from the X-Chain to the C-Chain and the P-Chain", func() {
32+
nodeURI := e2e.Env.GetRandomNodeURI()
33+
3234
ginkgo.By("creating wallet with a funded key to send from and recipient key to deliver to")
3335
factory := secp256k1.Factory{}
3436
recipientKey, err := factory.NewPrivateKey()
3537
require.NoError(err)
3638
keychain := e2e.Env.NewKeychain(1)
3739
keychain.Add(recipientKey)
38-
baseWallet := e2e.Env.NewWallet(keychain)
40+
baseWallet := e2e.Env.NewWallet(keychain, nodeURI)
3941
xWallet := baseWallet.X()
4042
cWallet := baseWallet.C()
4143
pWallet := baseWallet.P()
@@ -101,17 +103,20 @@ var _ = e2e.DescribeXChain("[Interchain Workflow]", func() {
101103
require.NoError(err)
102104
})
103105

106+
ginkgo.By("initializing a new eth client")
107+
ethClient := e2e.Env.NewEthClient(nodeURI)
108+
104109
ginkgo.By("importing AVAX from the X-Chain to the C-Chain", func() {
105110
_, err := cWallet.IssueImportTx(
106111
xWallet.BlockchainID(),
107112
recipientEthAddress,
108113
e2e.WithDefaultContext(),
114+
e2e.WithSuggestedGasPrice(ethClient),
109115
)
110116
require.NoError(err)
111117
})
112118

113119
ginkgo.By("checking that the recipient address has received imported funds on the C-Chain")
114-
ethClient := e2e.Env.NewEthClient()
115120
e2e.Eventually(func() bool {
116121
balance, err := ethClient.BalanceAt(e2e.DefaultContext(), recipientEthAddress, nil)
117122
require.NoError(err)

tests/e2e/x/transfer/virtuous.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,10 @@ var _ = e2e.DescribeXChainSerial("[Virtuous Transfer Tx AVAX]", func() {
4444
"virtuous-transfer-tx-avax",
4545
),
4646
func() {
47-
rpcEps := e2e.Env.URIs
47+
rpcEps := make([]string, len(e2e.Env.URIs))
48+
for i, nodeURI := range e2e.Env.URIs {
49+
rpcEps[i] = nodeURI.URI
50+
}
4851

4952
// Waiting for ongoing blocks to have completed before starting this
5053
// test avoids the case of a previous test having initiated block
@@ -84,7 +87,7 @@ var _ = e2e.DescribeXChainSerial("[Virtuous Transfer Tx AVAX]", func() {
8487
}
8588

8689
keychain := secp256k1fx.NewKeychain(testKeys...)
87-
baseWallet := e2e.Env.NewWallet(keychain)
90+
baseWallet := e2e.Env.NewWallet(keychain, e2e.Env.GetRandomNodeURI())
8891
avaxAssetID := baseWallet.X().AVAXAssetID()
8992

9093
wallets := make([]primary.Wallet, len(testKeys))

tests/fixture/testnet/config.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,12 @@ func (c *NetworkConfig) EnsureGenesis(networkID uint32, validatorIDs []ids.NodeI
157157
return nil
158158
}
159159

160+
// NodeURI associates a node ID with its API URI.
161+
type NodeURI struct {
162+
NodeID ids.NodeID
163+
URI string
164+
}
165+
160166
// NodeConfig defines configuration for an AvalancheGo node.
161167
type NodeConfig struct {
162168
NodeID ids.NodeID

0 commit comments

Comments
 (0)