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  }