Skip to content
Merged
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.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
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.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=
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
54 changes: 53 additions & 1 deletion internal/test/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand All @@ -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,
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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)
}
Expand Down
151 changes: 148 additions & 3 deletions internal/test/test_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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")
}
41 changes: 41 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,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)
}
Expand Down
Loading