github.com/MetalBlockchain/metalgo@v1.11.9/tests/fixture/e2e/helpers.go (about) 1 // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved. 2 // See the file LICENSE for licensing terms. 3 4 package e2e 5 6 import ( 7 "context" 8 "errors" 9 "fmt" 10 "math/big" 11 "os" 12 "strings" 13 "time" 14 15 "github.com/MetalBlockchain/coreth/core/types" 16 "github.com/MetalBlockchain/coreth/ethclient" 17 "github.com/MetalBlockchain/coreth/interfaces" 18 "github.com/stretchr/testify/require" 19 20 "github.com/MetalBlockchain/metalgo/ids" 21 "github.com/MetalBlockchain/metalgo/tests" 22 "github.com/MetalBlockchain/metalgo/tests/fixture/tmpnet" 23 "github.com/MetalBlockchain/metalgo/vms/secp256k1fx" 24 "github.com/MetalBlockchain/metalgo/wallet/subnet/primary" 25 "github.com/MetalBlockchain/metalgo/wallet/subnet/primary/common" 26 27 ginkgo "github.com/onsi/ginkgo/v2" 28 ) 29 30 const ( 31 // A long default timeout used to timeout failed operations but 32 // unlikely to induce flaking due to unexpected resource 33 // contention. 34 DefaultTimeout = 2 * time.Minute 35 36 DefaultPollingInterval = tmpnet.DefaultPollingInterval 37 38 // Setting this env will disable post-test bootstrap 39 // checks. Useful for speeding up iteration during test 40 // development. 41 SkipBootstrapChecksEnvName = "E2E_SKIP_BOOTSTRAP_CHECKS" 42 43 DefaultValidatorStartTimeDiff = tmpnet.DefaultValidatorStartTimeDiff 44 45 DefaultGasLimit = uint64(21000) // Standard gas limit 46 47 // An empty string prompts the use of the default path which ensures a 48 // predictable target for github's upload-artifact action. 49 DefaultNetworkDir = "" 50 51 // Directory used to store private networks (specific to a single test) 52 // under the shared network dir. 53 PrivateNetworksDirName = "private_networks" 54 ) 55 56 // Create a new wallet for the provided keychain against the specified node URI. 57 func NewWallet(keychain *secp256k1fx.Keychain, nodeURI tmpnet.NodeURI) primary.Wallet { 58 tests.Outf("{{blue}} initializing a new wallet for node %s with URI: %s {{/}}\n", nodeURI.NodeID, nodeURI.URI) 59 baseWallet, err := primary.MakeWallet(DefaultContext(), &primary.WalletConfig{ 60 URI: nodeURI.URI, 61 AVAXKeychain: keychain, 62 EthKeychain: keychain, 63 }) 64 require.NoError(ginkgo.GinkgoT(), err) 65 return primary.NewWalletWithOptions( 66 baseWallet, 67 common.WithPostIssuanceFunc( 68 func(id ids.ID) { 69 tests.Outf(" issued transaction with ID: %s\n", id) 70 }, 71 ), 72 ) 73 } 74 75 // Create a new eth client targeting the specified node URI. 76 func NewEthClient(nodeURI tmpnet.NodeURI) ethclient.Client { 77 tests.Outf("{{blue}} initializing a new eth client for node %s with URI: %s {{/}}\n", nodeURI.NodeID, nodeURI.URI) 78 nodeAddress := strings.Split(nodeURI.URI, "//")[1] 79 uri := fmt.Sprintf("ws://%s/ext/bc/C/ws", nodeAddress) 80 client, err := ethclient.Dial(uri) 81 require.NoError(ginkgo.GinkgoT(), err) 82 return client 83 } 84 85 // Helper simplifying use of a timed context by canceling the context on ginkgo teardown. 86 func ContextWithTimeout(duration time.Duration) context.Context { 87 ctx, cancel := context.WithTimeout(context.Background(), duration) 88 ginkgo.DeferCleanup(cancel) 89 return ctx 90 } 91 92 // Helper simplifying use of a timed context configured with the default timeout. 93 func DefaultContext() context.Context { 94 return ContextWithTimeout(DefaultTimeout) 95 } 96 97 // Helper simplifying use via an option of a timed context configured with the default timeout. 98 func WithDefaultContext() common.Option { 99 return common.WithContext(DefaultContext()) 100 } 101 102 // Re-implementation of testify/require.Eventually that is compatible with ginkgo. testify's 103 // version calls the condition function with a goroutine and ginkgo assertions don't work 104 // properly in goroutines. 105 func Eventually(condition func() bool, waitFor time.Duration, tick time.Duration, msg string) { 106 ticker := time.NewTicker(tick) 107 defer ticker.Stop() 108 109 ctx, cancel := context.WithTimeout(context.Background(), waitFor) 110 defer cancel() 111 for !condition() { 112 select { 113 case <-ctx.Done(): 114 require.Fail(ginkgo.GinkgoT(), msg) 115 case <-ticker.C: 116 } 117 } 118 } 119 120 // Adds an ephemeral node intended to be used by a single test. 121 func AddEphemeralNode(network *tmpnet.Network, flags tmpnet.FlagsMap) *tmpnet.Node { 122 require := require.New(ginkgo.GinkgoT()) 123 124 node := tmpnet.NewEphemeralNode(flags) 125 require.NoError(network.StartNode(DefaultContext(), ginkgo.GinkgoWriter, node)) 126 127 ginkgo.DeferCleanup(func() { 128 tests.Outf("shutting down ephemeral node %q\n", node.NodeID) 129 ctx, cancel := context.WithTimeout(context.Background(), DefaultTimeout) 130 defer cancel() 131 require.NoError(node.Stop(ctx)) 132 }) 133 return node 134 } 135 136 // Wait for the given node to report healthy. 137 func WaitForHealthy(node *tmpnet.Node) { 138 // Need to use explicit context (vs DefaultContext()) to support use with DeferCleanup 139 ctx, cancel := context.WithTimeout(context.Background(), DefaultTimeout) 140 defer cancel() 141 require.NoError(ginkgo.GinkgoT(), tmpnet.WaitForHealthy(ctx, node)) 142 } 143 144 // Sends an eth transaction, waits for the transaction receipt to be issued 145 // and checks that the receipt indicates success. 146 func SendEthTransaction(ethClient ethclient.Client, signedTx *types.Transaction) *types.Receipt { 147 require := require.New(ginkgo.GinkgoT()) 148 149 txID := signedTx.Hash() 150 tests.Outf(" sending eth transaction with ID: %s\n", txID) 151 152 require.NoError(ethClient.SendTransaction(DefaultContext(), signedTx)) 153 154 // Wait for the receipt 155 var receipt *types.Receipt 156 Eventually(func() bool { 157 var err error 158 receipt, err = ethClient.TransactionReceipt(DefaultContext(), txID) 159 if errors.Is(err, interfaces.NotFound) { 160 return false // Transaction is still pending 161 } 162 require.NoError(err) 163 return true 164 }, DefaultTimeout, DefaultPollingInterval, "failed to see transaction acceptance before timeout") 165 166 require.Equal(types.ReceiptStatusSuccessful, receipt.Status) 167 return receipt 168 } 169 170 // Determines the suggested gas price for the configured client that will 171 // maximize the chances of transaction acceptance. 172 func SuggestGasPrice(ethClient ethclient.Client) *big.Int { 173 gasPrice, err := ethClient.SuggestGasPrice(DefaultContext()) 174 require.NoError(ginkgo.GinkgoT(), err) 175 // Double the suggested gas price to maximize the chances of 176 // acceptance. Maybe this can be revisited pending resolution of 177 // https://github.com/MetalBlockchain/coreth/issues/314. 178 gasPrice.Add(gasPrice, gasPrice) 179 return gasPrice 180 } 181 182 // Helper simplifying use via an option of a gas price appropriate for testing. 183 func WithSuggestedGasPrice(ethClient ethclient.Client) common.Option { 184 baseFee := SuggestGasPrice(ethClient) 185 return common.WithBaseFee(baseFee) 186 } 187 188 // Verify that a new node can bootstrap into the network. This function is safe to call 189 // from `Teardown` by virtue of not depending on ginkgo.DeferCleanup. 190 func CheckBootstrapIsPossible(network *tmpnet.Network) { 191 require := require.New(ginkgo.GinkgoT()) 192 193 if len(os.Getenv(SkipBootstrapChecksEnvName)) > 0 { 194 tests.Outf("{{yellow}}Skipping bootstrap check due to the %s env var being set", SkipBootstrapChecksEnvName) 195 return 196 } 197 ginkgo.By("checking if bootstrap is possible with the current network state") 198 199 ctx, cancel := context.WithTimeout(context.Background(), DefaultTimeout) 200 defer cancel() 201 202 node := tmpnet.NewEphemeralNode(tmpnet.FlagsMap{}) 203 require.NoError(network.StartNode(ctx, ginkgo.GinkgoWriter, node)) 204 // StartNode will initiate node stop if an error is encountered during start, 205 // so no further cleanup effort is required if an error is seen here. 206 207 // Ensure the node is always stopped at the end of the check 208 defer func() { 209 ctx, cancel = context.WithTimeout(context.Background(), DefaultTimeout) 210 defer cancel() 211 require.NoError(node.Stop(ctx)) 212 }() 213 214 // Check that the node becomes healthy within timeout 215 require.NoError(tmpnet.WaitForHealthy(ctx, node)) 216 } 217 218 // Start a temporary network with the provided avalanchego binary. 219 func StartNetwork( 220 network *tmpnet.Network, 221 avalancheGoExecPath string, 222 pluginDir string, 223 shutdownDelay time.Duration, 224 reuseNetwork bool, 225 ) { 226 require := require.New(ginkgo.GinkgoT()) 227 228 require.NoError( 229 tmpnet.BootstrapNewNetwork( 230 DefaultContext(), 231 ginkgo.GinkgoWriter, 232 network, 233 DefaultNetworkDir, 234 avalancheGoExecPath, 235 pluginDir, 236 ), 237 ) 238 239 tests.Outf("{{green}}Successfully started network{{/}}\n") 240 241 symlinkPath, err := tmpnet.GetReusableNetworkPathForOwner(network.Owner) 242 require.NoError(err) 243 244 if reuseNetwork { 245 // Symlink the path of the created network to the default owner path (e.g. latest_avalanchego-e2e) 246 // to enable easy discovery for reuse. 247 require.NoError(os.Symlink(network.Dir, symlinkPath)) 248 tests.Outf("{{green}}Symlinked %s to %s to enable reuse{{/}}\n", network.Dir, symlinkPath) 249 } 250 251 ginkgo.DeferCleanup(func() { 252 if reuseNetwork { 253 tests.Outf("{{yellow}}Skipping shutdown for network %s (symlinked to %s) to enable reuse{{/}}\n", network.Dir, symlinkPath) 254 return 255 } 256 257 if shutdownDelay > 0 { 258 tests.Outf("Waiting %s before network shutdown to ensure final metrics scrape\n", shutdownDelay) 259 time.Sleep(shutdownDelay) 260 } 261 262 tests.Outf("Shutting down network\n") 263 ctx, cancel := context.WithTimeout(context.Background(), DefaultTimeout) 264 defer cancel() 265 require.NoError(network.Stop(ctx)) 266 }) 267 }