Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand All @@ -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=
Expand Down
50 changes: 49 additions & 1 deletion internal/test/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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{}
Expand All @@ -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,
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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)
}
Expand Down
88 changes: 84 additions & 4 deletions internal/test/test_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,7 @@

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) {
Expand Down Expand Up @@ -712,8 +711,7 @@
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,
Expand Down Expand Up @@ -755,3 +753,85 @@
)
})
}

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",

Check failure on line 780 in internal/test/test_test.go

View workflow job for this annotation

GitHub Actions / lint

unknown field ForkURL in struct literal of type flagsTests

Check failure on line 780 in internal/test/test_test.go

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest)

unknown field ForkURL in struct literal of type flagsTests
ForkNetwork: "mainnet",

Check failure on line 781 in internal/test/test_test.go

View workflow job for this annotation

GitHub Actions / lint

unknown field ForkNetwork in struct literal of type flagsTests

Check failure on line 781 in internal/test/test_test.go

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest)

unknown field ForkNetwork in struct literal of type flagsTests
}

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",

Check failure on line 813 in internal/test/test_test.go

View workflow job for this annotation

GitHub Actions / lint

unknown field ForkURL in struct literal of type flagsTests

Check failure on line 813 in internal/test/test_test.go

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest)

unknown field ForkURL in struct literal of type flagsTests
ForkNetwork: "testnet",

Check failure on line 814 in internal/test/test_test.go

View workflow job for this annotation

GitHub Actions / lint

unknown field ForkNetwork in struct literal of type flagsTests

Check failure on line 814 in internal/test/test_test.go

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest)

unknown field ForkNetwork in struct literal of type flagsTests
}

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",

Check failure on line 831 in internal/test/test_test.go

View workflow job for this annotation

GitHub Actions / lint

unknown field ForkURL in struct literal of type flagsTests (typecheck)

Check failure on line 831 in internal/test/test_test.go

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest)

unknown field ForkURL in struct literal of type flagsTests
}

_, err := testCode(map[string][]byte{}, state, flags)
require.Error(t, err)
assert.ErrorContains(t, err, "could not auto-detect fork network")
}
35 changes: 35 additions & 0 deletions internal/util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package util

import (
"bytes"
"context"
"encoding/hex"
"fmt"
"net"
Expand All @@ -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"
Expand Down Expand Up @@ -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)
}
Expand Down
Loading