diff --git a/tests/precompile/precompile_test.go b/tests/precompile/precompile_test.go index 83c1810d4e..4f296dc466 100644 --- a/tests/precompile/precompile_test.go +++ b/tests/precompile/precompile_test.go @@ -11,18 +11,14 @@ import ( "github.com/onsi/gomega" // Import the solidity package, so that ginkgo maps out the tests declared within the package - _ "github.com/ava-labs/subnet-evm/tests/precompile/solidity" - "github.com/ava-labs/subnet-evm/tests/utils" + "github.com/ava-labs/subnet-evm/tests/precompile/solidity" ) -func init() { - utils.RegisterNodeRun() -} - func TestE2E(t *testing.T) { if basePath := os.Getenv("TEST_SOURCE_ROOT"); basePath != "" { os.Chdir(basePath) } gomega.RegisterFailHandler(ginkgo.Fail) + solidity.RegisterAsyncTests() ginkgo.RunSpecs(t, "subnet-evm precompile ginkgo test suite") } diff --git a/tests/precompile/solidity/suites.go b/tests/precompile/solidity/suites.go index f343f0d470..4aacb83c4b 100644 --- a/tests/precompile/solidity/suites.go +++ b/tests/precompile/solidity/suites.go @@ -6,65 +6,98 @@ package solidity import ( "context" + "fmt" "time" "github.com/ava-labs/subnet-evm/tests/utils" ginkgo "github.com/onsi/ginkgo/v2" ) -var _ = ginkgo.Describe("[Precompiles]", func() { - // Register the ping test first - utils.RegisterPingTest() - - // Each ginkgo It node specifies the name of the genesis file (in ./tests/precompile/genesis/) - // to use to launch the subnet and the name of the TS test file to run on the subnet (in ./contracts/tests/) - ginkgo.It("contract native minter", ginkgo.Label("Precompile"), ginkgo.Label("ContractNativeMinter"), func() { - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() - - utils.RunDefaultHardhatTests(ctx, utils.BlockchainIDs["contract_native_minter"], "contract_native_minter") - }) - - ginkgo.It("tx allow list", ginkgo.Label("Precompile"), ginkgo.Label("TxAllowList"), func() { - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() +// Registers the Asynchronized Precompile Tests +// Before running the tests, this function creates all subnets given in the genesis files +// and then runs the hardhat tests for each one asynchronously if called with `ginkgo run -procs=`. +func RegisterAsyncTests() { + // Tests here assumes that the genesis files are in ./tests/precompile/genesis/ + // with the name {precompile_name}.json + genesisFiles, err := utils.GetFilesAndAliases("./tests/precompile/genesis/*.json") + if err != nil { + ginkgo.AbortSuite("Failed to get genesis files: " + err.Error()) + } + if len(genesisFiles) == 0 { + ginkgo.AbortSuite("No genesis files found") + } + subnetsSuite := utils.CreateSubnetsSuite(genesisFiles) + + var _ = ginkgo.Describe("[Asynchronized Precompile Tests]", func() { + // Register the ping test first + utils.RegisterPingTest() + + // Each ginkgo It node specifies the name of the genesis file (in ./tests/precompile/genesis/) + // to use to launch the subnet and the name of the TS test file to run on the subnet (in ./contracts/tests/) + ginkgo.It("contract native minter", ginkgo.Label("Precompile"), ginkgo.Label("ContractNativeMinter"), func() { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() - utils.RunDefaultHardhatTests(ctx, utils.BlockchainIDs["tx_allow_list"], "tx_allow_list") - }) + blockchainID := subnetsSuite.GetBlockchainID("contract_native_minter") + runDefaultHardhatTests(ctx, blockchainID, "contract_native_minter") + }) - ginkgo.It("contract deployer allow list", ginkgo.Label("Precompile"), ginkgo.Label("ContractDeployerAllowList"), func() { - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() + ginkgo.It("tx allow list", ginkgo.Label("Precompile"), ginkgo.Label("TxAllowList"), func() { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() - utils.RunDefaultHardhatTests(ctx, utils.BlockchainIDs["contract_deployer_allow_list"], "contract_deployer_allow_list") - }) + blockchainID := subnetsSuite.GetBlockchainID("tx_allow_list") + runDefaultHardhatTests(ctx, blockchainID, "tx_allow_list") + }) - ginkgo.It("fee manager", ginkgo.Label("Precompile"), ginkgo.Label("FeeManager"), func() { - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() + ginkgo.It("contract deployer allow list", ginkgo.Label("Precompile"), ginkgo.Label("ContractDeployerAllowList"), func() { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() - utils.RunDefaultHardhatTests(ctx, utils.BlockchainIDs["fee_manager"], "fee_manager") - }) + blockchainID := subnetsSuite.GetBlockchainID("contract_deployer_allow_list") + runDefaultHardhatTests(ctx, blockchainID, "contract_deployer_allow_list") + }) - ginkgo.It("reward manager", ginkgo.Label("Precompile"), ginkgo.Label("RewardManager"), func() { - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() + ginkgo.It("fee manager", ginkgo.Label("Precompile"), ginkgo.Label("FeeManager"), func() { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() - utils.RunDefaultHardhatTests(ctx, utils.BlockchainIDs["reward_manager"], "reward_manager") - }) + blockchainID := subnetsSuite.GetBlockchainID("fee_manager") + runDefaultHardhatTests(ctx, blockchainID, "fee_manager") + }) - // and then runs the hardhat tests for each one without forcing precompile developers to modify this file. - // ADD YOUR PRECOMPILE HERE - /* - ginkgo.It("your precompile", ginkgo.Label("Precompile"), ginkgo.Label("YourPrecompile"), func() { + ginkgo.It("reward manager", ginkgo.Label("Precompile"), ginkgo.Label("RewardManager"), func() { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - // Specify the name shared by the genesis file in ./tests/precompile/genesis/{your_precompile}.json - // and the test file in ./contracts/tests/{your_precompile}.ts - // If you want to use a different test command and genesis path than the defaults, you can - // use the utils.RunTestCMD. See utils.RunDefaultHardhatTests for an example. - utils.RunDefaultHardhatTests(ctx, "your_precompile") + blockchainID := subnetsSuite.GetBlockchainID("reward_manager") + runDefaultHardhatTests(ctx, blockchainID, "reward_manager") }) - */ -}) + + // ADD YOUR PRECOMPILE HERE + /* + ginkgo.It("your precompile", ginkgo.Label("Precompile"), ginkgo.Label("YourPrecompile"), func() { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + // Specify the name shared by the genesis file in ./tests/precompile/genesis/{your_precompile}.json + // and the test file in ./contracts/tests/{your_precompile}.ts + // If you want to use a different test command and genesis path than the defaults, you can + // use the utils.RunTestCMD. See utils.RunDefaultHardhatTests for an example. + subnetsSuite.RunHardhatTests(ctx, "your_precompile") + }) + */ + }) +} + +// Default parameters are: +// +// 1. Hardhat contract environment is located at ./contracts +// 2. Hardhat test file is located at ./contracts/test/.ts +// 3. npx is available in the ./contracts directory +func runDefaultHardhatTests(ctx context.Context, blockchainID, testName string) { + cmdPath := "./contracts" + // test path is relative to the cmd path + testPath := fmt.Sprintf("./test/%s.ts", testName) + utils.RunHardhatTests(ctx, blockchainID, cmdPath, testPath) +} diff --git a/tests/utils/command.go b/tests/utils/command.go index 30f04ab7b2..8a1a9cffb3 100644 --- a/tests/utils/command.go +++ b/tests/utils/command.go @@ -5,10 +5,9 @@ package utils import ( "context" - "encoding/json" "fmt" "os" - "path/filepath" + "os/exec" "strings" "time" @@ -19,22 +18,6 @@ import ( "github.com/onsi/gomega" ) -const ( - // Timeout to boot the AvalancheGo node - bootAvalancheNodeTimeout = 5 * time.Minute - - // Timeout for the health API to check the AvalancheGo is ready - healthCheckTimeout = 5 * time.Second -) - -// At boot time subnets are created, one for each test suite. This global -// variable has all the subnets IDs that can be used. -// -// One process creates the AvalancheGo node and all the subnets, and these -// subnets IDs are passed to all other processes and stored in this global -// variable -var BlockchainIDs map[string]string - // RunCommand starts the command [bin] with the given [args] and returns the command to the caller // TODO cmd package mentions we can do this more efficiently with cmd.NewCmdOptions rather than looping // and calling Status(). @@ -76,22 +59,13 @@ func RegisterPingTest() { }) } +// RegisterNodeRun registers a before suite that starts an AvalancheGo process to use for the e2e tests +// and an after suite that stops the AvalancheGo process func RegisterNodeRun() { - // Keep track of the AvalancheGo external bash script, it is null for most - // processes except the first process that starts AvalancheGo + // BeforeSuite starts an AvalancheGo process to use for the e2e tests var startCmd *cmd.Cmd - - // Our test suite runs in separate processes, ginkgo has - // SynchronizedBeforeSuite() which runs once, and its return value is passed - // over to each worker. - // - // Here an AvalancheGo node instance is started, and subnets are created for - // each test case. Each test case has its own subnet, therefore all tests - // can run in parallel without any issue. - // - // This function also compiles all the solidity contracts - var _ = ginkgo.SynchronizedBeforeSuite(func() []byte { - ctx, cancel := context.WithTimeout(context.Background(), bootAvalancheNodeTimeout) + _ = ginkgo.BeforeSuite(func() { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() wd, err := os.Getwd() @@ -103,34 +77,40 @@ func RegisterNodeRun() { // Assumes that startCmd will launch a node with HTTP Port at [utils.DefaultLocalNodeURI] healthClient := health.NewClient(DefaultLocalNodeURI) - healthy, err := health.AwaitReady(ctx, healthClient, healthCheckTimeout, nil) + healthy, err := health.AwaitReady(ctx, healthClient, HealthCheckTimeout, nil) gomega.Expect(err).Should(gomega.BeNil()) gomega.Expect(healthy).Should(gomega.BeTrue()) log.Info("AvalancheGo node is healthy") - - blockchainIds := make(map[string]string) - files, err := filepath.Glob("./tests/precompile/genesis/*.json") - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - - for _, file := range files { - basename := filepath.Base(file) - index := basename[:len(basename)-5] - blockchainIds[index] = CreateNewSubnet(ctx, file) - } - - blockchainIDsBytes, err := json.Marshal(blockchainIds) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) - return blockchainIDsBytes - }, func(data []byte) { - err := json.Unmarshal(data, &BlockchainIDs) - gomega.Expect(err).NotTo(gomega.HaveOccurred()) }) - // SynchronizedAfterSuite() takes two functions, the first runs after each test suite is done and the second - // function is executed once when all the tests are done. This function is used - // to gracefully shutdown the AvalancheGo node. - var _ = ginkgo.SynchronizedAfterSuite(func() {}, func() { + ginkgo.AfterSuite(func() { gomega.Expect(startCmd).ShouldNot(gomega.BeNil()) gomega.Expect(startCmd.Stop()).Should(gomega.BeNil()) + // TODO add a new node to bootstrap off of the existing node and ensure it can bootstrap all subnets + // created during the test }) } + +// RunDefaultHardhatTests runs the hardhat tests in the given [testPath] on the blockchain with [blockchainID] +// [execPath] is the path where the test command is executed +func RunHardhatTests(ctx context.Context, blockchainID string, execPath string, testPath string) { + chainURI := GetDefaultChainURI(blockchainID) + log.Info( + "Executing HardHat tests on blockchain", + "blockchainID", blockchainID, + "testPath", testPath, + "ChainURI", chainURI, + ) + + cmd := exec.Command("npx", "hardhat", "test", testPath, "--network", "local") + cmd.Dir = execPath + + log.Info("Sleeping to wait for test ping", "rpcURI", chainURI) + err := os.Setenv("RPC_URI", chainURI) + gomega.Expect(err).Should(gomega.BeNil()) + log.Info("Running test command", "cmd", cmd.String()) + + out, err := cmd.CombinedOutput() + fmt.Printf("\nCombined output:\n\n%s\n", string(out)) + gomega.Expect(err).Should(gomega.BeNil()) +} diff --git a/tests/utils/constants.go b/tests/utils/constants.go index 75cc779bd3..4b07626d08 100644 --- a/tests/utils/constants.go +++ b/tests/utils/constants.go @@ -3,7 +3,18 @@ package utils -var ( +import "time" + +const ( + // Timeout to boot the AvalancheGo node + BootAvalancheNodeTimeout = 5 * time.Minute + + // Timeout for the health API to check the AvalancheGo is ready + HealthCheckTimeout = 5 * time.Second + DefaultLocalNodeURI = "http://127.0.0.1:9650" - NodeURIs = []string{DefaultLocalNodeURI, "http://127.0.0.1:9652", "http://127.0.0.1:9654", "http://127.0.0.1:9656", "http://127.0.0.1:9658"} +) + +var ( + NodeURIs = []string{DefaultLocalNodeURI, "http://127.0.0.1:9652", "http://127.0.0.1:9654", "http://127.0.0.1:9656", "http://127.0.0.1:9658"} ) diff --git a/tests/utils/subnet.go b/tests/utils/subnet.go index dc9aef3236..6086ecc571 100644 --- a/tests/utils/subnet.go +++ b/tests/utils/subnet.go @@ -8,9 +8,12 @@ import ( "encoding/json" "fmt" "os" - "os/exec" + "path/filepath" + "strings" + "sync" "time" + "github.com/ava-labs/avalanchego/api/health" "github.com/ava-labs/avalanchego/api/info" "github.com/ava-labs/avalanchego/genesis" "github.com/ava-labs/avalanchego/ids" @@ -18,32 +21,91 @@ import ( wallet "github.com/ava-labs/avalanchego/wallet/subnet/primary" "github.com/ava-labs/subnet-evm/core" "github.com/ava-labs/subnet-evm/plugin/evm" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/log" + "github.com/go-cmd/cmd" + "github.com/onsi/ginkgo/v2" "github.com/onsi/gomega" ) -// RunTestCMD runs a given test command with the given rpcURI -// It also waits for the test ping to succeed before running the test command -func RunTestCMD(testCMD *exec.Cmd, rpcURI string) { - log.Info("Sleeping to wait for test ping", "rpcURI", rpcURI) - client, err := NewEvmClient(rpcURI, 225, 2) - gomega.Expect(err).Should(gomega.BeNil()) +type SubnetSuite struct { + blockchainIDs map[string]string + lock sync.RWMutex +} - bal, err := client.FetchBalance(context.Background(), common.HexToAddress("")) - gomega.Expect(err).Should(gomega.BeNil()) - gomega.Expect(bal.Cmp(common.Big0)).Should(gomega.Equal(0)) +func (s *SubnetSuite) GetBlockchainID(alias string) string { + s.lock.RLock() + defer s.lock.RUnlock() + return s.blockchainIDs[alias] +} - err = os.Setenv("RPC_URI", rpcURI) - gomega.Expect(err).Should(gomega.BeNil()) - log.Info("Running test command", "cmd", testCMD.String()) +func (s *SubnetSuite) SetBlockchainIDs(blockchainIDs map[string]string) { + s.lock.Lock() + defer s.lock.Unlock() + s.blockchainIDs = blockchainIDs +} - out, err := testCMD.CombinedOutput() - fmt.Printf("\nCombined output:\n\n%s\n", string(out)) - if err != nil { - fmt.Printf("\nErr: %s\n", err.Error()) - } - gomega.Expect(err).Should(gomega.BeNil()) +// CreateSubnetsSuite creates subnets for given [genesisFiles], and registers a before suite that starts an AvalancheGo process to use for the e2e tests. +// genesisFiles is a map of test aliases to genesis file paths. +func CreateSubnetsSuite(genesisFiles map[string]string) *SubnetSuite { + // Keep track of the AvalancheGo external bash script, it is null for most + // processes except the first process that starts AvalancheGo + var startCmd *cmd.Cmd + + // This is used to pass the blockchain IDs from the SynchronizedBeforeSuite() to the tests + var globalSuite SubnetSuite + + // Our test suite runs in separate processes, ginkgo has + // SynchronizedBeforeSuite() which runs once, and its return value is passed + // over to each worker. + // + // Here an AvalancheGo node instance is started, and subnets are created for + // each test case. Each test case has its own subnet, therefore all tests + // can run in parallel without any issue. + // + var _ = ginkgo.SynchronizedBeforeSuite(func() []byte { + ctx, cancel := context.WithTimeout(context.Background(), BootAvalancheNodeTimeout) + defer cancel() + + wd, err := os.Getwd() + gomega.Expect(err).Should(gomega.BeNil()) + log.Info("Starting AvalancheGo node", "wd", wd) + cmd, err := RunCommand("./scripts/run.sh") + startCmd = cmd + gomega.Expect(err).Should(gomega.BeNil()) + + // Assumes that startCmd will launch a node with HTTP Port at [utils.DefaultLocalNodeURI] + healthClient := health.NewClient(DefaultLocalNodeURI) + healthy, err := health.AwaitReady(ctx, healthClient, HealthCheckTimeout, nil) + gomega.Expect(err).Should(gomega.BeNil()) + gomega.Expect(healthy).Should(gomega.BeTrue()) + log.Info("AvalancheGo node is healthy") + + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + blockchainIDs := make(map[string]string) + for alias, file := range genesisFiles { + blockchainIDs[alias] = CreateNewSubnet(ctx, file) + } + + blockchainIDsBytes, err := json.Marshal(blockchainIDs) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + return blockchainIDsBytes + }, func(ctx ginkgo.SpecContext, data []byte) { + blockchainIDs := make(map[string]string) + err := json.Unmarshal(data, &blockchainIDs) + gomega.Expect(err).NotTo(gomega.HaveOccurred()) + + globalSuite.SetBlockchainIDs(blockchainIDs) + }) + + // SynchronizedAfterSuite() takes two functions, the first runs after each test suite is done and the second + // function is executed once when all the tests are done. This function is used + // to gracefully shutdown the AvalancheGo node. + var _ = ginkgo.SynchronizedAfterSuite(func() {}, func() { + gomega.Expect(startCmd).ShouldNot(gomega.BeNil()) + gomega.Expect(startCmd.Stop()).Should(gomega.BeNil()) + }) + + return &globalSuite } // CreateNewSubnet creates a new subnet and Subnet-EVM blockchain with the given genesis file. @@ -105,25 +167,16 @@ func GetDefaultChainURI(blockchainID string) string { return fmt.Sprintf("%s/ext/bc/%s/rpc", DefaultLocalNodeURI, blockchainID) } -// RunDefaultHardhatTests runs the hardhat tests on a given blockchain ID -// with default parameters. Default parameters are: -// 1. Hardhat contract environment is located at ./contracts -// 2. Hardhat test file is located at ./contracts/test/.ts -// 3. npx is available in the ./contracts directory -func RunDefaultHardhatTests(ctx context.Context, blockchainID string, test string) { - chainURI := GetDefaultChainURI(blockchainID) - log.Info( - "Executing HardHat tests on a new blockchain", - "blockchainID", blockchainID, - "test", test, - "ChainURI", chainURI, - ) - - cmdPath := "./contracts" - // test path is relative to the cmd path - testPath := fmt.Sprintf("./test/%s.ts", test) - cmd := exec.Command("npx", "hardhat", "test", testPath, "--network", "local") - cmd.Dir = cmdPath - - RunTestCMD(cmd, chainURI) +// GetFilesAndAliases returns a map of aliases to file paths in given [dir]. +func GetFilesAndAliases(dir string) (map[string]string, error) { + files, err := filepath.Glob(dir) + if err != nil { + return nil, err + } + aliasesToFiles := make(map[string]string) + for _, file := range files { + alias := strings.TrimSuffix(filepath.Base(file), filepath.Ext(file)) + aliasesToFiles[alias] = file + } + return aliasesToFiles, nil }