diff --git a/go.mod b/go.mod index ff3492474..11c5760f1 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.7.1-0.20251024154941-cbcc3082a8d9 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..c391b83d5 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.7.1-0.20251024154941-cbcc3082a8d9 h1:0mtib01RICP/RvJbQImYDv3Td4O/3jRp+ctc5juHvwE= +github.com/onflow/cadence-tools/test v1.7.1-0.20251024154941-cbcc3082a8d9/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..e8136d369 100644 --- a/internal/test/test.go +++ b/internal/test/test.go @@ -31,6 +31,7 @@ import ( cdcTests "github.com/onflow/cadence-tools/test" "github.com/onflow/cadence/common" "github.com/onflow/cadence/runtime" + flowgo "github.com/onflow/flow-go/model/flow" "github.com/rs/zerolog" "github.com/spf13/cobra" @@ -74,6 +75,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 `default:"" info:"Fork tests from a remote network defined in flow.json (typically mainnet or testnet). If provided without a value, defaults to mainnet."` + 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 +100,13 @@ flow test test1.cdc test2.cdc`, RunS: run, } +func init() { + // add default value to --fork flag + if f := TestCommand.Cmd.Flags().Lookup("fork"); f != nil { + f.NoOptDefVal = "mainnet" + } +} + func run( args []string, _ command.GlobalFlags, @@ -171,6 +184,36 @@ func testCode( logger := zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}).With().Timestamp().Logger() runner := cdcTests.NewTestRunner().WithLogger(logger) + // Configure fork mode if requested + effectiveForkHost := strings.TrimSpace(flags.ForkHost) + var forkChainID flowgo.ChainID + + if effectiveForkHost == "" && strings.TrimSpace(flags.Fork) != "" { + // Resolve network endpoint from flow.json + network, err := state.Networks().ByName(strings.ToLower(flags.Fork)) + 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) + } + + // Detect chain ID from the network + forkChainID, err = util.GetNetworkChainID(state, strings.ToLower(flags.Fork)) + if err != nil { + return nil, err + } + } + + if effectiveForkHost != "" { + 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 +242,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 flags.Fork != "" { + aliasNetwork = 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..8d49c5c7c 100644 --- a/internal/test/test_test.go +++ b/internal/test/test_test.go @@ -76,8 +76,7 @@ func TestExecutingTests(t *testing.T) { err = result.Results[script.Filename][0].Error require.Error(t, err) - var assertionErr *stdlib.AssertionError - assert.ErrorAs(t, err, &assertionErr) + assert.ErrorAs(t, err, &stdlib.AssertionError{}) }) t.Run("with import", func(t *testing.T) { @@ -712,8 +711,7 @@ 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) + assert.ErrorAs(t, result.Results[scriptFailing.Filename][0].Error, &stdlib.AssertionError{}) assert.Contains( t, @@ -755,3 +753,85 @@ Seed: 1521 ) }) } + +func TestForkMode_UsesMainnetAliases(t *testing.T) { + t.Parallel() + + _, state, _ := util.TestMocks(t) + + // Provide only mainnet alias; no testing alias on purpose + mainnetAliases := config.Aliases{{ + Network: "mainnet", + Address: flowsdk.HexToAddress("0x0000000000000007"), + }} + c := config.Contract{ + Name: tests.ContractHelloString.Name, + Location: tests.ContractHelloString.Filename, + Aliases: mainnetAliases, + } + state.Contracts().AddOrUpdate(c) + + script := tests.TestScriptWithImport + testFiles := map[string][]byte{ + script.Filename: script.Source, + } + + flags := flagsTests{ + ForkURL: "access.mainnet.nodes.onflow.org:9000", + ForkNetwork: "mainnet", + } + + result, err := testCode(testFiles, state, flags) + + require.NoError(t, err) + require.Len(t, result.Results, 1) + assert.NoError(t, result.Results[script.Filename][0].Error) +} + +func TestForkMode_UsesTestnetAliasesExplicit(t *testing.T) { + t.Parallel() + + _, state, _ := util.TestMocks(t) + + testnetAliases := config.Aliases{{ + Network: "testnet", + Address: flowsdk.HexToAddress("0x0000000000000007"), + }} + c := config.Contract{ + Name: tests.ContractHelloString.Name, + Location: tests.ContractHelloString.Filename, + Aliases: testnetAliases, + } + state.Contracts().AddOrUpdate(c) + + script := tests.TestScriptWithImport + testFiles := map[string][]byte{ + script.Filename: script.Source, + } + + flags := flagsTests{ + ForkURL: "access.testnet.nodes.onflow.org:9000", + ForkNetwork: "testnet", + } + + result, err := testCode(testFiles, state, flags) + + require.NoError(t, err) + require.Len(t, result.Results, 1) + assert.NoError(t, result.Results[script.Filename][0].Error) +} + +func TestForkMode_AutodetectFailureRequiresExplicitNetwork(t *testing.T) { + t.Parallel() + + _, state, _ := util.TestMocks(t) + + // No network hints in URL; expect early error + flags := flagsTests{ + ForkURL: "rpc.foobar.org:9000", + } + + _, err := testCode(map[string][]byte{}, state, flags) + require.Error(t, err) + assert.ErrorContains(t, err, "could not auto-detect fork network") +} diff --git a/internal/util/util.go b/internal/util/util.go index 9391218bd..8a44bd025 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,37 @@ func NetworkToChainID(network string) (flow.ChainID, error) { } } +// 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) + } + + 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 +} + func CreateTabWriter(b *bytes.Buffer) *tabwriter.Writer { return tabwriter.NewWriter(b, 0, 8, 1, '\t', tabwriter.AlignRight) }