diff --git a/go.mod b/go.mod index ff3492474..b1452b70f 100644 --- a/go.mod +++ b/go.mod @@ -15,11 +15,11 @@ require ( github.com/onflow/cadence v1.8.1 github.com/onflow/cadence-tools/languageserver v1.7.0 github.com/onflow/cadence-tools/lint v1.6.0 - github.com/onflow/cadence-tools/test v1.7.0 + github.com/onflow/cadence-tools/test v1.8.0 github.com/onflow/fcl-dev-wallet v0.8.0 github.com/onflow/flixkit-go/v2 v2.6.0 github.com/onflow/flow-core-contracts/lib/go/templates v1.9.1 - github.com/onflow/flow-emulator v1.10.0 + github.com/onflow/flow-emulator v1.10.1 github.com/onflow/flow-evm-gateway v1.3.5 github.com/onflow/flow-go v0.43.3-0.20251021182938-b0fef2c5ca47 github.com/onflow/flow-go-sdk v1.9.0 diff --git a/go.sum b/go.sum index b59acd689..fb0c5571e 100644 --- a/go.sum +++ b/go.sum @@ -779,8 +779,8 @@ github.com/onflow/cadence-tools/languageserver v1.7.0 h1:Bf8Ef6oSxlkwr34UAUzUwrO github.com/onflow/cadence-tools/languageserver v1.7.0/go.mod h1:uIKKHJNKR02BmTMKsE8+UW84db+RfpoBD0xXpTzrcSM= github.com/onflow/cadence-tools/lint v1.6.0 h1:xtgVUzQQWIVGe0tvJNov9zc9o1t2kE3eBtPsIEKZwDY= github.com/onflow/cadence-tools/lint v1.6.0/go.mod h1:SpTwSUwZuWl5Gdl6tn97kD/qVAMp8u3xPLjbR3GJ8ZE= -github.com/onflow/cadence-tools/test v1.7.0 h1:TeomK+uVFwmvYdU0RLvRNgwbYgeb5j8QNv0Z9amhxtE= -github.com/onflow/cadence-tools/test v1.7.0/go.mod h1:9gfshvyBMkb1Kut8j5XdVA874L7NWpEaH+REwMp9URY= +github.com/onflow/cadence-tools/test v1.8.0 h1:V/dux1JuUHllxBPPAzEk5au3AZeYcpGQmDi9HUJwLk8= +github.com/onflow/cadence-tools/test v1.8.0/go.mod h1:FRfS8/qX12UOSBzORc9+SgOVOK8Sg5nkxVJbdfiNPbY= github.com/onflow/crypto v0.25.3 h1:XQ3HtLsw8h1+pBN+NQ1JYM9mS2mVXTyg55OldaAIF7U= github.com/onflow/crypto v0.25.3/go.mod h1:+1igaXiK6Tjm9wQOBD1EGwW7bYWMUGKtwKJ/2QL/OWs= github.com/onflow/fcl-dev-wallet v0.8.0 h1:8TWHhJBWrzS6RCZI3eVjRT+SaUBqO6eygUNDaJV/B7s= @@ -793,8 +793,8 @@ github.com/onflow/flow-core-contracts/lib/go/contracts v1.9.1 h1:u6am8NzuWOIKkSk github.com/onflow/flow-core-contracts/lib/go/contracts v1.9.1/go.mod h1:jBDqVep0ICzhXky56YlyO4aiV2Jl/5r7wnqUPpvi7zE= github.com/onflow/flow-core-contracts/lib/go/templates v1.9.1 h1:ebyynXy74ZcfW+JpPwI+aaY0ezlxxA0cUgUrjhJonWg= github.com/onflow/flow-core-contracts/lib/go/templates v1.9.1/go.mod h1:twSVyUt3rNrgzAmxtBX+1Gw64QlPemy17cyvnXYy1Ug= -github.com/onflow/flow-emulator v1.10.0 h1:zrAlCP6yEFmlDg80fja55AqwVtD00OmrVGzeBf+gvcg= -github.com/onflow/flow-emulator v1.10.0/go.mod h1:t4mJAxj+czpJz6y/Jz4POw5ylBDXPrXFYejm2Env9Ak= +github.com/onflow/flow-emulator v1.10.1 h1:c/wtpXDI0o+n/icDUzSgCvT/4mT6WYW+nxaeiggmdGY= +github.com/onflow/flow-emulator v1.10.1/go.mod h1:+PbfGuya48rdW80en3msv2CLH8XM+7YEZYFHNIDNpeo= github.com/onflow/flow-evm-bridge v0.1.0 h1:7X2osvo4NnQgHj8aERUmbYtv9FateX8liotoLnPL9nM= github.com/onflow/flow-evm-bridge v0.1.0/go.mod h1:5UYwsnu6WcBNrwitGFxphCl5yq7fbWYGYuiCSTVF6pk= github.com/onflow/flow-evm-gateway v1.3.5 h1:2Nx5eCYwUsVBVOMNOMPab66PNKj8784t+SPgAckw2zk= diff --git a/internal/test/test.go b/internal/test/test.go index 2a91c1a8d..be5e3c232 100644 --- a/internal/test/test.go +++ b/internal/test/test.go @@ -74,6 +74,11 @@ type flagsTests struct { Random bool `default:"false" flag:"random" info:"Use the random flag to execute test cases randomly"` Seed int64 `default:"0" flag:"seed" info:"Use the seed flag to manipulate random execution of test cases"` Name string `default:"" flag:"name" info:"Use the name flag to run only tests that match the given name"` + + // Fork mode flags + Fork string // Use definition in init() + ForkHost string `default:"" flag:"fork-host" info:"Run tests against a fork of a remote network. Provide the GRPC Access host (host:port)."` + ForkHeight uint64 `default:"0" flag:"fork-height" info:"Optional block height to pin the fork (if supported)."` } var testFlags = flagsTests{} @@ -94,6 +99,15 @@ flow test test1.cdc test2.cdc`, RunS: run, } +func init() { + // Add default value to --fork flag + // workaround because config schema via struct tags doesn't support default values + TestCommand.Cmd.Flags().StringVar(&testFlags.Fork, "fork", "mainnet", "Fork tests from a remote network. If provided without a value, defaults to mainnet") + if f := TestCommand.Cmd.Flags().Lookup("fork"); f != nil { + f.NoOptDefVal = "mainnet" + } +} + func run( args []string, _ command.GlobalFlags, @@ -171,6 +185,39 @@ func testCode( logger := zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}).With().Timestamp().Logger() runner := cdcTests.NewTestRunner().WithLogger(logger) + // Configure fork mode if requested + var effectiveForkHost string + + // Determine the fork host + if flags.ForkHost != "" { + effectiveForkHost = strings.TrimSpace(flags.ForkHost) + } else if flags.Fork != "" { + // Look up network in flow.json + forkNetwork := strings.ToLower(flags.Fork) + network, err := state.Networks().ByName(forkNetwork) + if err != nil { + return nil, fmt.Errorf("network %q not found in flow.json", flags.Fork) + } + effectiveForkHost = network.Host + if effectiveForkHost == "" { + return nil, fmt.Errorf("network %q has no host configured", flags.Fork) + } + } + + // If fork mode is enabled, query the host to get chain ID + if effectiveForkHost != "" { + forkChainID, err := util.GetChainIDFromHost(effectiveForkHost) + if err != nil { + return nil, fmt.Errorf("failed to get chain ID from fork host %q: %w", effectiveForkHost, err) + } + + runner = runner.WithFork(cdcTests.ForkConfig{ + ForkHost: effectiveForkHost, + ChainID: forkChainID, + ForkHeight: flags.ForkHeight, + }) + } + var coverageReport *runtime.CoverageReport if flags.Cover { coverageReport = state.CreateCoverageReport("testing") @@ -199,8 +246,13 @@ func testCode( contractsConfig := *state.Contracts() contracts := make(map[string]common.Address, len(contractsConfig)) + // Choose alias network: default to "testing", but in fork mode use selected chain (mainnet/testnet) + aliasNetwork := "testing" + if strings.TrimSpace(flags.Fork) != "" { + aliasNetwork = strings.ToLower(flags.Fork) + } for _, contract := range contractsConfig { - alias := contract.Aliases.ByNetwork("testing") + alias := contract.Aliases.ByNetwork(aliasNetwork) if alias != nil { contracts[contract.Name] = common.Address(alias.Address) } diff --git a/internal/test/test_test.go b/internal/test/test_test.go index 447356eb8..9084d0478 100644 --- a/internal/test/test_test.go +++ b/internal/test/test_test.go @@ -376,7 +376,7 @@ func TestExecutingTests(t *testing.T) { coverageReport.ExcludedLocationIDs(), ) - expected := "Coverage: 93.9% of statements" + expected := "Coverage: 93.7% of statements" assert.Equal( t, @@ -712,8 +712,8 @@ Seed: 1521 assert.Len(t, result.Results, 2) assert.NoError(t, result.Results[scriptPassing.Filename][0].Error) assert.Error(t, result.Results[scriptFailing.Filename][0].Error) - var assertionErr *stdlib.AssertionError - assert.ErrorAs(t, result.Results[scriptFailing.Filename][0].Error, &assertionErr) + var assertionErr2 *stdlib.AssertionError + assert.ErrorAs(t, result.Results[scriptFailing.Filename][0].Error, &assertionErr2) assert.Contains( t, @@ -755,3 +755,148 @@ Seed: 1521 ) }) } + +func TestForkMode_UsesMainnetAliases(t *testing.T) { + t.Parallel() + + _, state, _ := util.TestMocks(t) + + // Use a real mainnet address (FlowToken system contract) + // This verifies fork mode correctly resolves mainnet aliases + mainnetAliases := config.Aliases{{ + Network: "mainnet", + Address: flowsdk.HexToAddress("0x1654653399040a61"), // FlowToken on mainnet + }} + + // Create a simple test contract to deploy + testContractSource := []byte(` + access(all) contract TestContract { + access(all) var value: Int + init() { self.value = 42 } + access(all) fun getValue(): Int { return self.value } + } + `) + _ = state.ReaderWriter().WriteFile("TestContract.cdc", testContractSource, 0644) + + c := config.Contract{ + Name: "TestContract", + Location: "TestContract.cdc", + Aliases: mainnetAliases, + } + state.Contracts().AddOrUpdate(c) + + // Test script that deploys and uses the contract + testScript := []byte(` + import Test + + access(all) fun testDeployAndUse() { + let err = Test.deployContract( + name: "TestContract", + path: "TestContract.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + // Verify the contract deployed and works + let script = "import TestContract from 0x1654653399040a61\naccess(all) fun main(): Int { return TestContract.getValue() }" + let result = Test.executeScript(script, []) + Test.expect(result, Test.beSucceeded()) + Test.assertEqual(42, result.returnValue! as! Int) + } + `) + + testFiles := map[string][]byte{ + "test_mainnet_fork.cdc": testScript, + } + + flags := flagsTests{ + ForkHost: "access.mainnet.nodes.onflow.org:9000", + Fork: "mainnet", + } + + result, err := testCode(testFiles, state, flags) + + require.NoError(t, err) + require.Len(t, result.Results, 1) + assert.NoError(t, result.Results["test_mainnet_fork.cdc"][0].Error) +} + +func TestForkMode_UsesTestnetAliasesExplicit(t *testing.T) { + t.Parallel() + + _, state, _ := util.TestMocks(t) + + // Use a real testnet address (FlowToken system contract testnet address) + // This verifies fork mode correctly resolves testnet aliases + testnetAliases := config.Aliases{{ + Network: "testnet", + Address: flowsdk.HexToAddress("0x7e60df042a9c0868"), // FlowToken on testnet + }} + + // Create a simple test contract to deploy + testContractSource := []byte(` + access(all) contract TestContract { + access(all) var value: String + init() { self.value = "testnet" } + access(all) fun getValue(): String { return self.value } + } + `) + _ = state.ReaderWriter().WriteFile("TestContract.cdc", testContractSource, 0644) + + c := config.Contract{ + Name: "TestContract", + Location: "TestContract.cdc", + Aliases: testnetAliases, + } + state.Contracts().AddOrUpdate(c) + + // Test script that deploys and uses the contract + testScript := []byte(` + import Test + + access(all) fun testDeployAndUse() { + let err = Test.deployContract( + name: "TestContract", + path: "TestContract.cdc", + arguments: [] + ) + Test.expect(err, Test.beNil()) + + // Verify the contract deployed and works + let script = "import TestContract from 0x7e60df042a9c0868\naccess(all) fun main(): String { return TestContract.getValue() }" + let result = Test.executeScript(script, []) + Test.expect(result, Test.beSucceeded()) + Test.assertEqual("testnet", result.returnValue! as! String) + } + `) + + testFiles := map[string][]byte{ + "test_testnet_fork.cdc": testScript, + } + + flags := flagsTests{ + ForkHost: "access.testnet.nodes.onflow.org:9000", + Fork: "testnet", + } + + result, err := testCode(testFiles, state, flags) + + require.NoError(t, err) + require.Len(t, result.Results, 1) + assert.NoError(t, result.Results["test_testnet_fork.cdc"][0].Error) +} + +func TestForkMode_AutodetectFailureRequiresExplicitNetwork(t *testing.T) { + t.Parallel() + + _, state, _ := util.TestMocks(t) + + // No network hints in URL; expect early error + flags := flagsTests{ + ForkHost: "rpc.foobar.org:9000", + } + + _, err := testCode(map[string][]byte{}, state, flags) + require.Error(t, err) + assert.ErrorContains(t, err, "failed to get chain ID from fork host") +} diff --git a/internal/util/util.go b/internal/util/util.go index 9391218bd..2161add28 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -20,6 +20,7 @@ package util import ( "bytes" + "context" "encoding/hex" "fmt" "net" @@ -34,6 +35,9 @@ import ( "github.com/onflow/flow-go-sdk/crypto" "github.com/onflow/flow-go/fvm/systemcontracts" flowGo "github.com/onflow/flow-go/model/flow" + flowaccess "github.com/onflow/flow/protobuf/go/flow/access" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" "github.com/onflow/flowkit/v2" "github.com/onflow/flowkit/v2/config" @@ -238,6 +242,43 @@ func NetworkToChainID(network string) (flow.ChainID, error) { } } +// GetChainIDFromHost queries the given host directly to get its chain ID. +func GetChainIDFromHost(host string) (flowGo.ChainID, error) { + + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + conn, err := grpc.NewClient(host, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + return "", fmt.Errorf("failed to connect to %s: %w", host, err) + } + defer conn.Close() + + client := flowaccess.NewAccessAPIClient(conn) + resp, err := client.GetNetworkParameters(ctx, &flowaccess.GetNetworkParametersRequest{}) + if err != nil { + return "", fmt.Errorf("failed to get network parameters from %s: %w", host, err) + } + + return flowGo.ChainID(resp.GetChainId()), nil +} + +// GetNetworkChainID resolves a network name from flow.json and returns its chain ID. +// It queries the network's access node via GetNetworkParameters to detect the chain ID. +func GetNetworkChainID(state *flowkit.State, networkName string) (flowGo.ChainID, error) { + network, err := state.Networks().ByName(networkName) + if err != nil { + return "", fmt.Errorf("network %q not found in flow.json", networkName) + } + + host := network.Host + if host == "" { + return "", fmt.Errorf("network %q has no host configured", networkName) + } + + return GetChainIDFromHost(host) +} + func CreateTabWriter(b *bytes.Buffer) *tabwriter.Writer { return tabwriter.NewWriter(b, 0, 8, 1, '\t', tabwriter.AlignRight) }