From 05a2a0d0bccad87e250cdca0984d10ad345cfbae Mon Sep 17 00:00:00 2001 From: Patrick Fuchs Date: Fri, 20 Mar 2026 15:47:11 +0200 Subject: [PATCH 1/5] fix: remove data races in parallel tests --- internal/cadence/lint_test.go | 48 +++++---------- internal/test/test_test.go | 87 +++++++++------------------ internal/transactions/profile_test.go | 8 --- 3 files changed, 45 insertions(+), 98 deletions(-) diff --git a/internal/cadence/lint_test.go b/internal/cadence/lint_test.go index 29f3ba9f4..8523513df 100644 --- a/internal/cadence/lint_test.go +++ b/internal/cadence/lint_test.go @@ -45,9 +45,8 @@ func Test_Lint(t *testing.T) { }) t.Run("lints file with no issues", func(t *testing.T) { - t.Parallel() - state := setupMockState(t) + t.Parallel() results, err := lintFiles(state, "NoError.cdc") require.NoError(t, err) @@ -67,9 +66,8 @@ func Test_Lint(t *testing.T) { }) t.Run("lints file with import", func(t *testing.T) { - t.Parallel() - state := setupMockState(t) + t.Parallel() results, err := lintFiles(state, "foo/WithImports.cdc") require.NoError(t, err) @@ -90,9 +88,8 @@ func Test_Lint(t *testing.T) { }) t.Run("lints multiple files", func(t *testing.T) { - t.Parallel() - state := setupMockState(t) + t.Parallel() results, err := lintFiles(state, "NoError.cdc", "foo/WithImports.cdc") require.NoError(t, err) @@ -116,9 +113,8 @@ func Test_Lint(t *testing.T) { }) t.Run("lints file with warning", func(t *testing.T) { - t.Parallel() - state := setupMockState(t) + t.Parallel() results, err := lintFiles(state, "LintWarning.cdc") require.NoError(t, err) @@ -148,9 +144,8 @@ func Test_Lint(t *testing.T) { }) t.Run("lints file with error", func(t *testing.T) { - t.Parallel() - state := setupMockState(t) + t.Parallel() results, err := lintFiles(state, "LintError.cdc") require.NoError(t, err) @@ -190,9 +185,8 @@ func Test_Lint(t *testing.T) { }) t.Run("generates synthetic replacement for replacement category diagnostics", func(t *testing.T) { - t.Parallel() - state := setupMockState(t) + t.Parallel() results, err := lintFiles(state, "ReplacementHint.cdc") require.NoError(t, err) @@ -217,9 +211,8 @@ func Test_Lint(t *testing.T) { }) t.Run("linter resolves imports from flowkit state", func(t *testing.T) { - t.Parallel() - state := setupMockState(t) + t.Parallel() results, err := lintFiles(state, "WithFlowkitImport.cdc") require.NoError(t, err) @@ -239,9 +232,8 @@ func Test_Lint(t *testing.T) { }) t.Run("resolves stdlib imports contracts", func(t *testing.T) { - t.Parallel() - state := setupMockState(t) + t.Parallel() results, err := lintFiles(state, "StdlibImportsContract.cdc") require.NoError(t, err) @@ -273,9 +265,8 @@ func Test_Lint(t *testing.T) { }) t.Run("resolves stdlib imports transactions", func(t *testing.T) { - t.Parallel() - state := setupMockState(t) + t.Parallel() results, err := lintFiles(state, "StdlibImportsTransaction.cdc") require.NoError(t, err) @@ -307,9 +298,8 @@ func Test_Lint(t *testing.T) { }) t.Run("resolves stdlib imports scripts", func(t *testing.T) { - t.Parallel() - state := setupMockState(t) + t.Parallel() results, err := lintFiles(state, "StdlibImportsScript.cdc") require.NoError(t, err) @@ -329,9 +319,8 @@ func Test_Lint(t *testing.T) { }) t.Run("resolves stdlib imports Crypto", func(t *testing.T) { - t.Parallel() - state := setupMockState(t) + t.Parallel() results, err := lintFiles(state, "StdlibImportsCrypto.cdc") require.NoError(t, err) @@ -351,9 +340,8 @@ func Test_Lint(t *testing.T) { }) t.Run("resolves nested imports when contract imported by name", func(t *testing.T) { - t.Parallel() - state := setupMockState(t) + t.Parallel() results, err := lintFiles(state, "TransactionImportingContractWithNestedImports.cdc") require.NoError(t, err) @@ -373,9 +361,8 @@ func Test_Lint(t *testing.T) { }) t.Run("allows access(account) when contracts on same account", func(t *testing.T) { - t.Parallel() - state := setupMockStateWithAccountAccess(t) + t.Parallel() results, err := lintFiles(state, "ContractA.cdc") require.NoError(t, err) @@ -396,9 +383,8 @@ func Test_Lint(t *testing.T) { }) t.Run("denies access(account) when contracts on different accounts", func(t *testing.T) { - t.Parallel() - state := setupMockStateWithAccountAccess(t) + t.Parallel() results, err := lintFiles(state, "ContractC.cdc") require.NoError(t, err) @@ -412,9 +398,8 @@ func Test_Lint(t *testing.T) { }) t.Run("allows access(account) when dependencies on same account (peak-money repro)", func(t *testing.T) { - t.Parallel() - state := setupMockStateWithDependencies(t) + t.Parallel() results, err := lintFiles(state, "imports/testaddr/DepA.cdc") require.NoError(t, err) @@ -435,9 +420,8 @@ func Test_Lint(t *testing.T) { }) t.Run("allows access(account) when dependencies have Source but no Aliases", func(t *testing.T) { - t.Parallel() - state := setupMockStateWithSourceOnly(t) + t.Parallel() // Verify that AddDependencyAsContract automatically adds Source to Aliases sourceAContract, _ := state.Contracts().ByName("SourceA") diff --git a/internal/test/test_test.go b/internal/test/test_test.go index 71440ff24..a5f985d1a 100644 --- a/internal/test/test_test.go +++ b/internal/test/test_test.go @@ -45,9 +45,8 @@ func TestExecutingTests(t *testing.T) { }} t.Run("simple", func(t *testing.T) { - t.Parallel() - _, state, _ := util.TestMocks(t) + t.Parallel() script := tests.TestScriptSimple testFiles := map[string][]byte{ @@ -61,9 +60,8 @@ func TestExecutingTests(t *testing.T) { }) t.Run("simple failing", func(t *testing.T) { - t.Parallel() - _, state, _ := util.TestMocks(t) + t.Parallel() script := tests.TestScriptSimpleFailing testFiles := map[string][]byte{ @@ -81,16 +79,14 @@ func TestExecutingTests(t *testing.T) { }) t.Run("with import", func(t *testing.T) { - t.Parallel() - _, state, _ := util.TestMocks(t) - c := config.Contract{ Name: tests.ContractHelloString.Name, Location: tests.ContractHelloString.Filename, Aliases: aliases, } state.Contracts().AddOrUpdate(c) + t.Parallel() // Execute script script := tests.TestScriptWithImport @@ -105,8 +101,6 @@ func TestExecutingTests(t *testing.T) { }) t.Run("with relative imports", func(t *testing.T) { - t.Parallel() - // Setup _, state, _ := util.TestMocks(t) readerWriter := state.ReaderWriter() @@ -133,6 +127,7 @@ func TestExecutingTests(t *testing.T) { Aliases: aliases, } state.Contracts().AddOrUpdate(contractFoo) + t.Parallel() // Execute script script := tests.TestScriptWithRelativeImports @@ -147,9 +142,8 @@ func TestExecutingTests(t *testing.T) { }) t.Run("with helper script import", func(t *testing.T) { - t.Parallel() - _, state, _ := util.TestMocks(t) + t.Parallel() // Execute script script := tests.TestScriptWithHelperImport @@ -164,10 +158,9 @@ func TestExecutingTests(t *testing.T) { }) t.Run("with missing contract in config", func(t *testing.T) { - t.Parallel() - // Setup _, state, _ := util.TestMocks(t) + t.Parallel() // Execute script script := tests.TestScriptWithMissingContract @@ -185,11 +178,8 @@ func TestExecutingTests(t *testing.T) { }) t.Run("with missing testing alias in config", func(t *testing.T) { - t.Parallel() - // Setup _, state, _ := util.TestMocks(t) - c := config.Contract{ Name: tests.ContractHelloString.Name, Location: tests.ContractHelloString.Filename, @@ -199,6 +189,7 @@ func TestExecutingTests(t *testing.T) { }}, } state.Contracts().AddOrUpdate(c) + t.Parallel() // Execute script script := tests.TestScriptWithImport @@ -216,11 +207,8 @@ func TestExecutingTests(t *testing.T) { }) t.Run("without testing alias for common contracts", func(t *testing.T) { - t.Parallel() - // Setup _, state, _ := util.TestMocks(t) - c := config.Contract{ Name: tests.ContractHelloString.Name, Location: tests.ContractHelloString.Filename, @@ -234,6 +222,7 @@ func TestExecutingTests(t *testing.T) { Location: "cadence/contracts/FungibleToken.cdc", } state.Contracts().AddOrUpdate(fungibleToken) + t.Parallel() // Execute script script := tests.TestScriptWithImport @@ -246,15 +235,13 @@ func TestExecutingTests(t *testing.T) { }) t.Run("with file read", func(t *testing.T) { - t.Parallel() - _, state, rw := util.TestMocks(t) - _ = rw.WriteFile( tests.SomeFile.Filename, tests.SomeFile.Source, os.ModeTemporary, ) + t.Parallel() // Execute script script := tests.TestScriptWithFileRead @@ -269,16 +256,14 @@ func TestExecutingTests(t *testing.T) { }) t.Run("with code coverage", func(t *testing.T) { - t.Parallel() - // Setup _, state, _ := util.TestMocks(t) - state.Contracts().AddOrUpdate(config.Contract{ Name: tests.ContractFooCoverage.Name, Location: tests.ContractFooCoverage.Filename, Aliases: aliases, }) + t.Parallel() // Execute script script := tests.TestScriptWithCoverage @@ -397,16 +382,14 @@ func TestExecutingTests(t *testing.T) { }) t.Run("with code coverage for contracts only", func(t *testing.T) { - t.Parallel() - // Setup _, state, _ := util.TestMocks(t) - state.Contracts().AddOrUpdate(config.Contract{ Name: tests.ContractFooCoverage.Name, Location: tests.ContractFooCoverage.Filename, Aliases: aliases, }) + t.Parallel() // Execute script script := tests.TestScriptWithCoverage @@ -523,16 +506,14 @@ func TestExecutingTests(t *testing.T) { }) t.Run("with random test case execution", func(t *testing.T) { - t.Parallel() - // Setup _, state, _ := util.TestMocks(t) - state.Contracts().AddOrUpdate(config.Contract{ Name: tests.ContractFooCoverage.Name, Location: tests.ContractFooCoverage.Filename, Aliases: aliases, }) + t.Parallel() // Execute script script := tests.TestScriptWithCoverage @@ -558,16 +539,14 @@ func TestExecutingTests(t *testing.T) { }) t.Run("with input seed for test case execution", func(t *testing.T) { - t.Parallel() - // Setup _, state, _ := util.TestMocks(t) - state.Contracts().AddOrUpdate(config.Contract{ Name: tests.ContractFooCoverage.Name, Location: tests.ContractFooCoverage.Filename, Aliases: aliases, }) + t.Parallel() // Execute script script := tests.TestScriptWithCoverage @@ -607,16 +586,14 @@ Seed: 1521 }) t.Run("with JSON output", func(t *testing.T) { - t.Parallel() - // Setup _, state, _ := util.TestMocks(t) - state.Contracts().AddOrUpdate(config.Contract{ Name: tests.ContractFooCoverage.Name, Location: tests.ContractFooCoverage.Filename, Aliases: aliases, }) + t.Parallel() // Execute script script := tests.TestScriptWithCoverage @@ -660,10 +637,9 @@ Seed: 1521 }) t.Run("run specific test case by name", func(t *testing.T) { - t.Parallel() - // Setup _, state, _ := util.TestMocks(t) + t.Parallel() // Execute script script := tests.TestScriptSimple @@ -689,10 +665,9 @@ Seed: 1521 }) t.Run("run specific test case by name multiple files", func(t *testing.T) { - t.Parallel() - // Setup _, state, _ := util.TestMocks(t) + t.Parallel() scriptPassing := tests.TestScriptSimple scriptFailing := tests.TestScriptSimpleFailing @@ -730,10 +705,9 @@ Seed: 1521 }) t.Run("run specific test case by name will do nothing if not found", func(t *testing.T) { - t.Parallel() - // Setup _, state, _ := util.TestMocks(t) + t.Parallel() // Execute script script := tests.TestScriptSimple @@ -760,7 +734,6 @@ func TestForkMode_UsesMainnetAliases(t *testing.T) { if os.Getenv("SKIP_NETWORK_TESTS") != "" { t.Skip("skipping network-dependent test") } - t.Parallel() _, state, _ := util.TestMocks(t) @@ -787,6 +760,7 @@ func TestForkMode_UsesMainnetAliases(t *testing.T) { Aliases: mainnetAliases, } state.Contracts().AddOrUpdate(c) + t.Parallel() // Test script that deploys and uses the contract testScript := []byte(` @@ -799,7 +773,7 @@ func TestForkMode_UsesMainnetAliases(t *testing.T) { 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, []) @@ -828,7 +802,6 @@ func TestForkMode_UsesTestnetAliasesExplicit(t *testing.T) { if os.Getenv("SKIP_NETWORK_TESTS") != "" { t.Skip("skipping network-dependent test") } - t.Parallel() _, state, _ := util.TestMocks(t) @@ -855,6 +828,7 @@ func TestForkMode_UsesTestnetAliasesExplicit(t *testing.T) { Aliases: testnetAliases, } state.Contracts().AddOrUpdate(c) + t.Parallel() // Test script that deploys and uses the contract testScript := []byte(` @@ -867,7 +841,7 @@ func TestForkMode_UsesTestnetAliasesExplicit(t *testing.T) { 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, []) @@ -893,9 +867,8 @@ func TestForkMode_UsesTestnetAliasesExplicit(t *testing.T) { } func TestForkMode_AutodetectFailureRequiresExplicitNetwork(t *testing.T) { - t.Parallel() - _, state, _ := util.TestMocks(t) + t.Parallel() // No network hints in URL; expect early error flags := flagsTests{ @@ -911,7 +884,6 @@ func TestNetworkForkResolution_Success(t *testing.T) { if os.Getenv("SKIP_NETWORK_TESTS") != "" { t.Skip("skipping network-dependent test") } - t.Parallel() _, state, _ := util.TestMocks(t) @@ -926,6 +898,7 @@ func TestNetworkForkResolution_Success(t *testing.T) { Name: "mainnet-fork", Fork: "mainnet", }) + t.Parallel() // Create a simple test that uses the test_fork pragma testScript := []byte(` @@ -950,8 +923,6 @@ access(all) fun testSimple() { } func TestNetworkForkResolution_ForkNetworkNotFound(t *testing.T) { - t.Parallel() - _, state, _ := util.TestMocks(t) // Add mainnet-fork that references non-existent network @@ -959,6 +930,7 @@ func TestNetworkForkResolution_ForkNetworkNotFound(t *testing.T) { Name: "mainnet-fork", Fork: "nonexistent", }) + t.Parallel() // Create a simple test that uses the fork network testScript := []byte(` @@ -982,8 +954,6 @@ access(all) fun testSimple() { } func TestNetworkForkResolution_ForkNetworkHasNoHost(t *testing.T) { - t.Parallel() - _, state, _ := util.TestMocks(t) // Add mainnet network with no host @@ -996,6 +966,7 @@ func TestNetworkForkResolution_ForkNetworkHasNoHost(t *testing.T) { Name: "mainnet-fork", Fork: "mainnet", }) + t.Parallel() // Create a simple test that uses the fork network testScript := []byte(` @@ -1023,7 +994,6 @@ func TestNetworkForkResolution_WithOwnHost(t *testing.T) { if os.Getenv("SKIP_NETWORK_TESTS") != "" { t.Skip("skipping network-dependent test") } - t.Parallel() _, state, _ := util.TestMocks(t) @@ -1040,6 +1010,7 @@ func TestNetworkForkResolution_WithOwnHost(t *testing.T) { Host: "127.0.0.1:3569", Fork: "mainnet", }) + t.Parallel() // Create a simple test that uses the fork network testScript := []byte(` @@ -1067,7 +1038,6 @@ func TestContractAddressForkResolution_UsesMainnetForkFirst(t *testing.T) { if os.Getenv("SKIP_NETWORK_TESTS") != "" { t.Skip("skipping network-dependent test") } - t.Parallel() _, state, _ := util.TestMocks(t) @@ -1105,6 +1075,7 @@ access(all) contract TestContract { }, } state.Contracts().AddOrUpdate(c) + t.Parallel() testScript := []byte(` #test_fork(network: "mainnet-fork", height: nil) @@ -1134,7 +1105,6 @@ func TestContractAddressForkResolution_FallbackToMainnet(t *testing.T) { if os.Getenv("SKIP_NETWORK_TESTS") != "" { t.Skip("skipping network-dependent test") } - t.Parallel() _, state, _ := util.TestMocks(t) @@ -1172,6 +1142,7 @@ access(all) contract TestContract { }, } state.Contracts().AddOrUpdate(c) + t.Parallel() testScript := []byte(` #test_fork(network: "mainnet-fork", height: nil) @@ -1201,7 +1172,6 @@ func TestContractAddressForkResolution_PrioritizesForkOverParent(t *testing.T) { if os.Getenv("SKIP_NETWORK_TESTS") != "" { t.Skip("skipping network-dependent test") } - t.Parallel() _, state, _ := util.TestMocks(t) @@ -1245,6 +1215,7 @@ access(all) contract TestContract { }, } state.Contracts().AddOrUpdate(c) + t.Parallel() // Should use the mainnet-fork address (0xf233dcee88fe0abe), not mainnet (0x1654653399040a61) testScript := []byte(` diff --git a/internal/transactions/profile_test.go b/internal/transactions/profile_test.go index 7b513ca67..8763f5b55 100644 --- a/internal/transactions/profile_test.go +++ b/internal/transactions/profile_test.go @@ -148,8 +148,6 @@ func Test_ProfilingResult(t *testing.T) { func Test_Profile_Integration_LocalEmulator(t *testing.T) { t.Run("Profile user transaction", func(t *testing.T) { - t.Parallel() - port := getFreePort(t) emulatorHost := fmt.Sprintf("127.0.0.1:%d", port) emulatorServer, testTxID, testBlockHeight := startEmulatorWithTestTransaction(t, emulatorHost, port) @@ -161,8 +159,6 @@ func Test_Profile_Integration_LocalEmulator(t *testing.T) { }) t.Run("Profile failed transaction", func(t *testing.T) { - t.Parallel() - port := getFreePort(t) emulatorHost := fmt.Sprintf("127.0.0.1:%d", port) emulatorServer, failedTxID, testBlockHeight := startEmulatorWithFailedTransaction(t, emulatorHost, port) @@ -174,8 +170,6 @@ func Test_Profile_Integration_LocalEmulator(t *testing.T) { }) t.Run("Profile transaction with multiple prior transactions", func(t *testing.T) { - t.Parallel() - port := getFreePort(t) emulatorHost := fmt.Sprintf("127.0.0.1:%d", port) emulatorServer, targetTxID, testBlockHeight := startEmulatorWithMultipleTransactions(t, emulatorHost, port, 5) @@ -187,8 +181,6 @@ func Test_Profile_Integration_LocalEmulator(t *testing.T) { }) t.Run("Profile system transaction", func(t *testing.T) { - t.Parallel() - port := getFreePort(t) emulatorHost := fmt.Sprintf("127.0.0.1:%d", port) From 8a9609bac4b237629375bdca2959b078658fcaf0 Mon Sep 17 00:00:00 2001 From: Patrick Fuchs Date: Fri, 20 Mar 2026 16:47:21 +0200 Subject: [PATCH 2/5] fix Makefile Binary rule --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 42df8a1a4..dfe6b3771 100644 --- a/Makefile +++ b/Makefile @@ -37,6 +37,7 @@ endif .PHONY: ci ci: generate test coverage +.PHONY: $(BINARY) $(BINARY): CGO_ENABLED=1 \ CGO_CFLAGS="-O2 -D__BLST_PORTABLE__ -std=gnu11" \ From a55945fc6fa2f7a4efac5459fa423da13f1baac8 Mon Sep 17 00:00:00 2001 From: Patrick Fuchs Date: Fri, 20 Mar 2026 16:50:04 +0200 Subject: [PATCH 3/5] feat: run test files concurrently Add --jobs flag to limit concurrent test file execution. --- internal/test/bench_test.go | 56 +++++++++ internal/test/test.go | 242 +++++++++++++++++++++--------------- internal/test/test_test.go | 74 +++++++++++ 3 files changed, 275 insertions(+), 97 deletions(-) create mode 100644 internal/test/bench_test.go diff --git a/internal/test/bench_test.go b/internal/test/bench_test.go new file mode 100644 index 000000000..9fdf63079 --- /dev/null +++ b/internal/test/bench_test.go @@ -0,0 +1,56 @@ +/* + * Flow CLI + * + * Copyright Flow Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package test + +import ( + "fmt" + "testing" + + "github.com/onflow/flow-go-sdk/crypto" + "github.com/onflow/flowkit/v2" + "github.com/onflow/flowkit/v2/accounts" + "github.com/onflow/flowkit/v2/tests" +) + +func buildTestFiles(n int) map[string][]byte { + script := tests.TestScriptSimple + files := make(map[string][]byte, n) + for i := range n { + files[fmt.Sprintf("test_%02d_%s", i, script.Filename)] = script.Source + } + return files +} + +func BenchmarkTestCode_NFiles(b *testing.B) { + rw, _ := tests.ReaderWriter() + state, err := flowkit.Init(rw) + if err != nil { + b.Fatal(err) + } + emulatorAccount, _ := accounts.NewEmulatorAccount(rw, crypto.ECDSA_P256, crypto.SHA3_256, "") + state.Accounts().AddOrUpdate(emulatorAccount) + testFiles := buildTestFiles(10) + + for b.Loop() { + _, err := testCode(testFiles, state, flagsTests{}) + if err != nil { + b.Fatal(err) + } + } +} diff --git a/internal/test/test.go b/internal/test/test.go index 382879556..f00e8afd8 100644 --- a/internal/test/test.go +++ b/internal/test/test.go @@ -28,6 +28,7 @@ import ( "regexp" goRuntime "runtime" "strings" + "sync" cdcTests "github.com/onflow/cadence-tools/test" "github.com/onflow/cadence/common" @@ -77,6 +78,7 @@ 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"` + Jobs int `default:"0" flag:"jobs" info:"Maximum number of test files to run concurrently (default: number of CPU cores)"` // Fork mode flags Fork string // Use definition in init() @@ -190,41 +192,6 @@ func testCode( // Track network resolutions per file for pragma-based fork detection // Map: filename -> resolved network name fileNetworkResolutions := make(map[string]string) - var currentTestFile string - - // Resolve network labels using flow.json state - resolveNetworkFromState := func(label string) (string, bool) { - normalizedLabel := strings.ToLower(strings.TrimSpace(label)) - network, err := state.Networks().ByName(normalizedLabel) - if err != nil || network == nil { - return "", false - } - - // If network has a fork, resolve the fork network's host - host := strings.TrimSpace(network.Host) - if network.Fork != "" { - forkName := strings.ToLower(strings.TrimSpace(network.Fork)) - forkNetwork, err := state.Networks().ByName(forkName) - if err != nil { - return "", false - } - host = strings.TrimSpace(forkNetwork.Host) - } - - if host == "" { - return "", false - } - - // Track network resolution for current test file (indicates pragma-based fork usage) - // Only track if it's not the default "testing" network - if currentTestFile != "" && normalizedLabel != "testing" { - if _, exists := fileNetworkResolutions[currentTestFile]; !exists { - fileNetworkResolutions[currentTestFile] = normalizedLabel - } - } - - return host, true - } // Configure fork mode if requested var effectiveForkHost string @@ -300,92 +267,173 @@ func testCode( seed = int64(rand.Intn(150000)) } - testResults := make(map[string]cdcTests.Results, 0) - exitCode := 0 + // Limit concurrency to flags.Jobs, defaulting to number of CPU cores. + jobs := flags.Jobs + if jobs <= 0 { + jobs = goRuntime.NumCPU() + } + sem := make(chan struct{}, jobs) + + type fileResult struct { + scriptPath string + results cdcTests.Results + networkResolution string + err error + } + + resultCh := make(chan fileResult, len(testFiles)) + var wg sync.WaitGroup + for scriptPath, code := range testFiles { - // Set current test file for network resolution tracking - currentTestFile = scriptPath - - // Create a new test runner per file to ensure complete isolation. - // Each file gets its own runner with its own backend state. - fileRunner := cdcTests.NewTestRunner(). - WithLogger(logger). - WithNetworkResolver(resolveNetworkFromState). - WithNetworkLabel(networkLabel). - WithImportResolver(importResolver(scriptPath, state)). - WithFileResolver(fileResolver(scriptPath, state)). - WithContractAddressResolver(func(network string, contractName string) (common.Address, error) { - contractsByName := make(map[string]config.Contract) - for _, c := range *state.Contracts() { - contractsByName[c.Name] = c + wg.Add(1) + go func(scriptPath string, code []byte) { + defer wg.Done() + sem <- struct{}{} + defer func() { <-sem }() + + // Each file gets its own resolver so network resolution tracking is per-file. + var resolvedNetwork string + resolveNetworkFromState := func(label string) (string, bool) { + normalizedLabel := strings.ToLower(strings.TrimSpace(label)) + network, err := state.Networks().ByName(normalizedLabel) + if err != nil || network == nil { + return "", false } - contract, exists := contractsByName[contractName] - if !exists { - return common.Address{}, fmt.Errorf("contract not found: %s", contractName) + // If network has a fork, resolve the fork network's host + host := strings.TrimSpace(network.Host) + if network.Fork != "" { + forkName := strings.ToLower(strings.TrimSpace(network.Fork)) + forkNetwork, err := state.Networks().ByName(forkName) + if err != nil { + return "", false + } + host = strings.TrimSpace(forkNetwork.Host) } - alias := contract.Aliases.ByNetwork(network) - if alias != nil { - return common.Address(alias.Address), nil + if host == "" { + return "", false } - // Fallback to fork network if configured - networkConfig, err := state.Networks().ByName(network) - if err == nil && networkConfig != nil && networkConfig.Fork != "" { - forkAlias := contract.Aliases.ByNetwork(networkConfig.Fork) - if forkAlias != nil { - return common.Address(forkAlias.Address), nil - } + // Track network resolution for current test file (indicates pragma-based fork usage) + // Only track if it's not the default "testing" network + if resolvedNetwork == "" && normalizedLabel != "testing" { + resolvedNetwork = normalizedLabel } - return common.Address{}, fmt.Errorf("no address for contract %s on network %s", contractName, network) - }) + return host, true + } - if forkCfg != nil { - fileRunner = fileRunner.WithFork(*forkCfg) - } - if coverageReport != nil { - fileRunner = fileRunner.WithCoverageReport(coverageReport) - } - if seed > 0 { - fileRunner = fileRunner.WithRandomSeed(seed) - } + // Create a new test runner per file to ensure complete isolation. + // Each file gets its own runner with its own backend state. + fileRunner := cdcTests.NewTestRunner(). + WithLogger(logger). + WithNetworkResolver(resolveNetworkFromState). + WithNetworkLabel(networkLabel). + WithImportResolver(importResolver(scriptPath, state)). + WithFileResolver(fileResolver(scriptPath, state)). + WithContractAddressResolver(func(network string, contractName string) (common.Address, error) { + contractsByName := make(map[string]config.Contract) + for _, c := range *state.Contracts() { + contractsByName[c.Name] = c + } + + contract, exists := contractsByName[contractName] + if !exists { + return common.Address{}, fmt.Errorf("contract not found: %s", contractName) + } + + alias := contract.Aliases.ByNetwork(network) + if alias != nil { + return common.Address(alias.Address), nil + } - if flags.Name != "" { - testFunctions, err := fileRunner.GetTests(string(code)) - if err != nil { - return nil, err + // Fallback to fork network if configured + networkConfig, err := state.Networks().ByName(network) + if err == nil && networkConfig != nil && networkConfig.Fork != "" { + forkAlias := contract.Aliases.ByNetwork(networkConfig.Fork) + if forkAlias != nil { + return common.Address(forkAlias.Address), nil + } + } + + return common.Address{}, fmt.Errorf("no address for contract %s on network %s", contractName, network) + }) + + if forkCfg != nil { + fileRunner = fileRunner.WithFork(*forkCfg) + } + if coverageReport != nil { + fileRunner = fileRunner.WithCoverageReport(coverageReport) + } + if seed > 0 { + fileRunner = fileRunner.WithRandomSeed(seed) } - for _, testFunction := range testFunctions { - if testFunction != flags.Name { - continue - } + var fileResults cdcTests.Results + var runErr error - result, err := fileRunner.RunTest(string(code), flags.Name) + if flags.Name != "" { + testFunctions, err := fileRunner.GetTests(string(code)) if err != nil { - return nil, err + resultCh <- fileResult{scriptPath: scriptPath, err: err} + return + } + + for _, testFunction := range testFunctions { + if testFunction != flags.Name { + continue + } + + r, err := fileRunner.RunTest(string(code), flags.Name) + if err != nil { + runErr = err + break + } + fileResults = []cdcTests.Result{*r} } - testResults[scriptPath] = []cdcTests.Result{*result} + } else { + fileResults, runErr = fileRunner.RunTests(string(code)) } - } else { - results, err := fileRunner.RunTests(string(code)) - if err != nil { - return nil, err + + resultCh <- fileResult{ + scriptPath: scriptPath, + results: fileResults, + networkResolution: resolvedNetwork, + err: runErr, } - testResults[scriptPath] = results - } + }(scriptPath, code) + } + + go func() { + wg.Wait() + close(resultCh) + }() - for _, result := range testResults[scriptPath] { - if result.Error != nil { + testResults := make(map[string]cdcTests.Results, 0) + exitCode := 0 + var firstErr error + + for r := range resultCh { + if r.err != nil && firstErr == nil { + firstErr = r.err + } + if r.results != nil { + testResults[r.scriptPath] = r.results + } + if r.networkResolution != "" { + fileNetworkResolutions[r.scriptPath] = r.networkResolution + } + for _, res := range testResults[r.scriptPath] { + if res.Error != nil { exitCode = 1 break } } + } - // Clear current test file after processing - currentTestFile = "" + if firstErr != nil { + return nil, firstErr } // Track fork test usage metrics - aggregate into single event diff --git a/internal/test/test_test.go b/internal/test/test_test.go index a5f985d1a..82c9efdec 100644 --- a/internal/test/test_test.go +++ b/internal/test/test_test.go @@ -1241,3 +1241,77 @@ access(all) fun testPrioritizesFork() { require.Len(t, result.Results, 1) assert.NoError(t, result.Results["test_priority.cdc"][0].Error) } + +func TestMultipleFiles_ForkNetwork(t *testing.T) { + if os.Getenv("SKIP_NETWORK_TESTS") != "" { + t.Skip("skipping network-dependent test") + } + + _, state, _ := util.TestMocks(t) + + state.Networks().AddOrUpdate(config.Network{ + Name: "mainnet", + Host: "access.mainnet.nodes.onflow.org:9000", + }) + state.Networks().AddOrUpdate(config.Network{ + Name: "mainnet-fork", + Host: "127.0.0.1:3569", + Fork: "mainnet", + }) + + addrA := flowsdk.HexToAddress("0x1654653399040a61") + addrB := flowsdk.HexToAddress("0xf233dcee88fe0abe") + + _ = state.ReaderWriter().WriteFile("ContractA.cdc", []byte(` +access(all) contract ContractA { + access(all) var value: Int + init() { self.value = 1 } +} +`), 0644) + _ = state.ReaderWriter().WriteFile("ContractB.cdc", []byte(` +access(all) contract ContractB { + access(all) var value: Int + init() { self.value = 2 } +} +`), 0644) + + state.Contracts().AddOrUpdate(config.Contract{ + Name: "ContractA", + Location: "ContractA.cdc", + Aliases: config.Aliases{{Network: "mainnet-fork", Address: addrA}}, + }) + state.Contracts().AddOrUpdate(config.Contract{ + Name: "ContractB", + Location: "ContractB.cdc", + Aliases: config.Aliases{{Network: "mainnet-fork", Address: addrB}}, + }) + t.Parallel() + + testFiles := map[string][]byte{ + "test_file_a.cdc": []byte(` +#test_fork(network: "mainnet-fork", height: nil) +import Test +import "ContractA" +access(all) fun testContractA() { + let addr = Type().address! + Test.assertEqual(0x1654653399040a61 as Address, addr) +} +`), + "test_file_b.cdc": []byte(` +#test_fork(network: "mainnet-fork", height: nil) +import Test +import "ContractB" +access(all) fun testContractB() { + let addr = Type().address! + Test.assertEqual(0xf233dcee88fe0abe as Address, addr) +} +`), + } + + result, err := testCode(testFiles, state, flagsTests{}) + + require.NoError(t, err) + require.Len(t, result.Results, 2) + assert.NoError(t, result.Results["test_file_a.cdc"][0].Error) + assert.NoError(t, result.Results["test_file_b.cdc"][0].Error) +} From c3fd7abe9eae63aea67e1afb131f575578ca4a8d Mon Sep 17 00:00:00 2001 From: Patrick Fuchs Date: Sat, 21 Mar 2026 15:58:20 +0200 Subject: [PATCH 4/5] fix viper race condition --- internal/settings/settings.go | 56 ++++++++++++++++------------------- 1 file changed, 25 insertions(+), 31 deletions(-) diff --git a/internal/settings/settings.go b/internal/settings/settings.go index 71c1c701c..412f25f4f 100644 --- a/internal/settings/settings.go +++ b/internal/settings/settings.go @@ -23,6 +23,7 @@ import ( "fmt" "os" "path/filepath" + "sync" "github.com/spf13/viper" ) @@ -33,8 +34,28 @@ const settingsDir = "flow-cli" const settingsType = "yaml" -// viperLoaded only load settings file once -var viperLoaded = false +var initViper = sync.OnceValue(func() error { + if err := createSettingsDir(); err != nil { + return err + } + + if err := viper.MergeConfigMap(defaults); err != nil { + return err + } + + // Load settings file + if err := viper.MergeInConfig(); err != nil { + switch err.(type) { + case viper.ConfigFileNotFoundError: + // Create settings file for the first time + return viper.SafeWriteConfig() + default: + return err + } + } + + return nil +}) func init() { viper.SetConfigName(settingsFile) @@ -68,36 +89,9 @@ func Set(key string, val any) error { return nil } -// loadViper loads the global settings file +// loadViper loads the global settings file once and returns the same error on every call. func loadViper() error { - if viperLoaded { - return nil - } - viperLoaded = true - - if err := createSettingsDir(); err != nil { - return err - } - - err := viper.MergeConfigMap(defaults) - if err != nil { - return err - } - - // Load settings file - if err := viper.MergeInConfig(); err != nil { - switch err.(type) { - case viper.ConfigFileNotFoundError: - // Create settings file for the first time - if err = viper.SafeWriteConfig(); err != nil { - return err - } - default: - return err - } - } - - return nil + return initViper() } // createSettingsDir creates settings dir if it doesn't exist From b9fe87301fe636ec083de4b483f9631972aa6768 Mon Sep 17 00:00:00 2001 From: Patrick Fuchs Date: Sat, 21 Mar 2026 16:03:22 +0200 Subject: [PATCH 5/5] refactor: decompose testCode into focused helper functions --- internal/test/test.go | 537 +++++++++++++++++++++++------------------- 1 file changed, 298 insertions(+), 239 deletions(-) diff --git a/internal/test/test.go b/internal/test/test.go index f00e8afd8..f767e150c 100644 --- a/internal/test/test.go +++ b/internal/test/test.go @@ -20,6 +20,7 @@ package test import ( "bytes" + "context" "encoding/json" "fmt" "math/rand" @@ -28,7 +29,6 @@ import ( "regexp" goRuntime "runtime" "strings" - "sync" cdcTests "github.com/onflow/cadence-tools/test" "github.com/onflow/cadence/common" @@ -36,6 +36,7 @@ import ( flowGo "github.com/onflow/flow-go/model/flow" "github.com/rs/zerolog" "github.com/spf13/cobra" + "golang.org/x/sync/errgroup" "github.com/onflow/flowkit/v2" "github.com/onflow/flowkit/v2/config" @@ -182,6 +183,26 @@ func run( return result, nil } +// testRunConfig holds the resolved runtime configuration for a test run. +type testRunConfig struct { + forkCfg *cdcTests.ForkConfig + coverageReport *runtime.CoverageReport + networkLabel string + seed int64 + jobs int + name string + // raw flag values retained for telemetry + forkFlag string + forkHostFlag string +} + +// concurrencyResult holds the aggregated output of runTestsConcurrently. +type concurrencyResult struct { + testResults map[string]cdcTests.Results + fileNetworkResolutions map[string]string + exitCode int +} + func testCode( testFiles map[string][]byte, state *flowkit.State, @@ -189,90 +210,240 @@ func testCode( ) (*result, error) { logger := zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr}).With().Timestamp().Logger() - // Track network resolutions per file for pragma-based fork detection - // Map: filename -> resolved network name - fileNetworkResolutions := make(map[string]string) + forkCfg, networkLabel, err := resolveForkConfig(flags, state) + if err != nil { + return nil, err + } + + cfg := testRunConfig{ + forkCfg: forkCfg, + coverageReport: buildCoverageReport(flags, state), + networkLabel: networkLabel, + seed: resolveSeed(flags), + jobs: flags.Jobs, + name: flags.Name, + forkFlag: flags.Fork, + forkHostFlag: flags.ForkHost, + } + + cr, err := runTestsConcurrently(testFiles, state, cfg, logger) + if err != nil { + return nil, err + } + + trackForkMetrics(cr, cfg, len(testFiles)) - // Configure fork mode if requested + return &result{ + Results: cr.testResults, + CoverageReport: cfg.coverageReport, + RandomSeed: cfg.seed, + exitCode: cr.exitCode, + }, nil +} + +// resolveForkConfig determines the fork configuration and network label from flags. +func resolveForkConfig(flags flagsTests, state *flowkit.State) (*cdcTests.ForkConfig, string, error) { + networkLabel := "testing" 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) + 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) + 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) + return nil, "", fmt.Errorf("network %q has no host configured", flags.Fork) } } - // Determine network label (used by resolver/addresses); default to testing - networkLabel := "testing" if strings.TrimSpace(flags.Fork) != "" { networkLabel = strings.ToLower(flags.Fork) } - // If fork mode is enabled, query the host to get chain ID - var forkCfg *cdcTests.ForkConfig - 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) + if effectiveForkHost == "" { + return nil, networkLabel, nil + } + + forkChainID, err := util.GetChainIDFromHost(effectiveForkHost) + if err != nil { + return nil, "", fmt.Errorf("failed to get chain ID from fork host %q: %w", effectiveForkHost, err) + } + + // Map chain ID to a sensible network label if not provided explicitly + if strings.TrimSpace(flags.Fork) == "" { + switch forkChainID { + case flowGo.Mainnet: + networkLabel = "mainnet" + case flowGo.Testnet: + networkLabel = "testnet" } + } - cfg := cdcTests.ForkConfig{ - ForkHost: effectiveForkHost, - ChainID: forkChainID, - ForkHeight: flags.ForkHeight, + cfg := cdcTests.ForkConfig{ + ForkHost: effectiveForkHost, + ChainID: forkChainID, + ForkHeight: flags.ForkHeight, + } + return &cfg, networkLabel, nil +} + +// buildCoverageReport creates a coverage report if coverage is enabled. +func buildCoverageReport(flags flagsTests, state *flowkit.State) *runtime.CoverageReport { + if !flags.Cover { + return nil + } + coverageReport := state.CreateCoverageReport("testing") + if flags.CoverCode == contractsCoverCode { + coverageReport.WithLocationFilter(func(location common.Location) bool { + // We only allow inspection of AddressLocation, + // since scripts and transactions cannot be + // attributed to their source files anyway. + _, addressLoc := location.(common.AddressLocation) + return addressLoc + }) + } + return coverageReport +} + +// resolveSeed returns the random seed to use for test execution. +func resolveSeed(flags flagsTests) int64 { + if flags.Seed > 0 { + return flags.Seed + } + if flags.Random { + return int64(rand.Intn(150000)) + } + return 0 +} + +// networkResolver returns a function that resolves a network label to its host, +// tracking which non-testing network was resolved via resolvedNetwork. +func networkResolver(state *flowkit.State, resolvedNetwork *string) func(string) (string, bool) { + return func(label string) (string, bool) { + normalizedLabel := strings.ToLower(strings.TrimSpace(label)) + network, err := state.Networks().ByName(normalizedLabel) + if err != nil || network == nil { + return "", false } - forkCfg = &cfg - // Map chain ID to a sensible network label if not provided explicitly - if strings.TrimSpace(flags.Fork) == "" { - switch forkChainID { - case flowGo.Mainnet: - networkLabel = "mainnet" - case flowGo.Testnet: - networkLabel = "testnet" + host := strings.TrimSpace(network.Host) + if network.Fork != "" { + forkName := strings.ToLower(strings.TrimSpace(network.Fork)) + forkNetwork, err := state.Networks().ByName(forkName) + if err != nil { + return "", false } + host = strings.TrimSpace(forkNetwork.Host) + } + + if host == "" { + return "", false } + + // Track network resolution for the current test file (indicates pragma-based fork usage). + // Only track if it's not the default "testing" network. + if *resolvedNetwork == "" && normalizedLabel != "testing" { + *resolvedNetwork = normalizedLabel + } + + return host, true } +} - var coverageReport *runtime.CoverageReport - if flags.Cover { - coverageReport = state.CreateCoverageReport("testing") - if flags.CoverCode == contractsCoverCode { - coverageReport.WithLocationFilter( - func(location common.Location) bool { - _, addressLoc := location.(common.AddressLocation) - // We only allow inspection of AddressLocation, - // since scripts and transactions cannot be - // attributed to their source files anyway. - return addressLoc - }, - ) +// contractAddressResolver returns a function that resolves a contract name to its address on a network. +func contractAddressResolver(state *flowkit.State) func(string, string) (common.Address, error) { + contractsByName := make(map[string]config.Contract) + for _, c := range *state.Contracts() { + contractsByName[c.Name] = c + } + + return func(network string, contractName string) (common.Address, error) { + contract, exists := contractsByName[contractName] + if !exists { + return common.Address{}, fmt.Errorf("contract not found: %s", contractName) } + + if alias := contract.Aliases.ByNetwork(network); alias != nil { + return common.Address(alias.Address), nil + } + + // Fallback to fork network if configured. + networkConfig, err := state.Networks().ByName(network) + if err == nil && networkConfig != nil && networkConfig.Fork != "" { + if forkAlias := contract.Aliases.ByNetwork(networkConfig.Fork); forkAlias != nil { + return common.Address(forkAlias.Address), nil + } + } + + return common.Address{}, fmt.Errorf("no address for contract %s on network %s", contractName, network) } +} - var seed int64 - if flags.Seed > 0 { - seed = flags.Seed - } else if flags.Random { - seed = int64(rand.Intn(150000)) +// buildTestRunner creates an isolated test runner for a single file. +// It also returns a pointer to a string that will be populated with the resolved +// non-testing network name (if any) after the runner executes. +func buildTestRunner(scriptPath string, state *flowkit.State, cfg testRunConfig, logger zerolog.Logger) (*cdcTests.TestRunner, *string) { + var resolvedNetwork string + + runner := cdcTests.NewTestRunner(). + WithLogger(logger). + WithNetworkResolver(networkResolver(state, &resolvedNetwork)). + WithNetworkLabel(cfg.networkLabel). + WithImportResolver(importResolver(scriptPath, state)). + WithFileResolver(fileResolver(scriptPath, state)). + WithContractAddressResolver(contractAddressResolver(state)) + + if cfg.forkCfg != nil { + runner = runner.WithFork(*cfg.forkCfg) + } + if cfg.coverageReport != nil { + runner = runner.WithCoverageReport(cfg.coverageReport) + } + if cfg.seed > 0 { + runner = runner.WithRandomSeed(cfg.seed) } - // Limit concurrency to flags.Jobs, defaulting to number of CPU cores. - jobs := flags.Jobs + return runner, &resolvedNetwork +} + +// runFileTests runs the tests in code using runner, optionally filtering by name. +func runFileTests(runner *cdcTests.TestRunner, code []byte, name string) (cdcTests.Results, error) { + if name == "" { + return runner.RunTests(string(code)) + } + + testFunctions, err := runner.GetTests(string(code)) + if err != nil { + return nil, err + } + + for _, fn := range testFunctions { + if fn != name { + continue + } + r, err := runner.RunTest(string(code), name) + if err != nil { + return nil, err + } + return cdcTests.Results{*r}, nil + } + + return nil, nil +} + +func runTestsConcurrently( + testFiles map[string][]byte, + state *flowkit.State, + cfg testRunConfig, + logger zerolog.Logger, +) (*concurrencyResult, error) { + jobs := cfg.jobs if jobs <= 0 { jobs = goRuntime.NumCPU() } - sem := make(chan struct{}, jobs) type fileResult struct { scriptPath string @@ -282,218 +453,106 @@ func testCode( } resultCh := make(chan fileResult, len(testFiles)) - var wg sync.WaitGroup - - for scriptPath, code := range testFiles { - wg.Add(1) - go func(scriptPath string, code []byte) { - defer wg.Done() - sem <- struct{}{} - defer func() { <-sem }() - - // Each file gets its own resolver so network resolution tracking is per-file. - var resolvedNetwork string - resolveNetworkFromState := func(label string) (string, bool) { - normalizedLabel := strings.ToLower(strings.TrimSpace(label)) - network, err := state.Networks().ByName(normalizedLabel) - if err != nil || network == nil { - return "", false - } - // If network has a fork, resolve the fork network's host - host := strings.TrimSpace(network.Host) - if network.Fork != "" { - forkName := strings.ToLower(strings.TrimSpace(network.Fork)) - forkNetwork, err := state.Networks().ByName(forkName) - if err != nil { - return "", false - } - host = strings.TrimSpace(forkNetwork.Host) - } - - if host == "" { - return "", false - } + g, ctx := errgroup.WithContext(context.Background()) + g.SetLimit(jobs) - // Track network resolution for current test file (indicates pragma-based fork usage) - // Only track if it's not the default "testing" network - if resolvedNetwork == "" && normalizedLabel != "testing" { - resolvedNetwork = normalizedLabel - } - - return host, true - } - - // Create a new test runner per file to ensure complete isolation. - // Each file gets its own runner with its own backend state. - fileRunner := cdcTests.NewTestRunner(). - WithLogger(logger). - WithNetworkResolver(resolveNetworkFromState). - WithNetworkLabel(networkLabel). - WithImportResolver(importResolver(scriptPath, state)). - WithFileResolver(fileResolver(scriptPath, state)). - WithContractAddressResolver(func(network string, contractName string) (common.Address, error) { - contractsByName := make(map[string]config.Contract) - for _, c := range *state.Contracts() { - contractsByName[c.Name] = c - } - - contract, exists := contractsByName[contractName] - if !exists { - return common.Address{}, fmt.Errorf("contract not found: %s", contractName) - } - - alias := contract.Aliases.ByNetwork(network) - if alias != nil { - return common.Address(alias.Address), nil - } - - // Fallback to fork network if configured - networkConfig, err := state.Networks().ByName(network) - if err == nil && networkConfig != nil && networkConfig.Fork != "" { - forkAlias := contract.Aliases.ByNetwork(networkConfig.Fork) - if forkAlias != nil { - return common.Address(forkAlias.Address), nil - } - } - - return common.Address{}, fmt.Errorf("no address for contract %s on network %s", contractName, network) - }) - - if forkCfg != nil { - fileRunner = fileRunner.WithFork(*forkCfg) - } - if coverageReport != nil { - fileRunner = fileRunner.WithCoverageReport(coverageReport) - } - if seed > 0 { - fileRunner = fileRunner.WithRandomSeed(seed) + for scriptPath, code := range testFiles { + g.Go(func() error { + if ctx.Err() != nil { + return ctx.Err() } - var fileResults cdcTests.Results - var runErr error - - if flags.Name != "" { - testFunctions, err := fileRunner.GetTests(string(code)) - if err != nil { - resultCh <- fileResult{scriptPath: scriptPath, err: err} - return - } - - for _, testFunction := range testFunctions { - if testFunction != flags.Name { - continue - } - - r, err := fileRunner.RunTest(string(code), flags.Name) - if err != nil { - runErr = err - break - } - fileResults = []cdcTests.Result{*r} - } - } else { - fileResults, runErr = fileRunner.RunTests(string(code)) - } + runner, resolvedNetwork := buildTestRunner(scriptPath, state, cfg, logger) + results, err := runFileTests(runner, code, cfg.name) resultCh <- fileResult{ scriptPath: scriptPath, - results: fileResults, - networkResolution: resolvedNetwork, - err: runErr, + results: results, + networkResolution: *resolvedNetwork, + err: err, } - }(scriptPath, code) + return nil + }) } - go func() { - wg.Wait() - close(resultCh) - }() + waitErr := g.Wait() + close(resultCh) - testResults := make(map[string]cdcTests.Results, 0) - exitCode := 0 - var firstErr error + cr := &concurrencyResult{ + testResults: make(map[string]cdcTests.Results), + fileNetworkResolutions: make(map[string]string), + } for r := range resultCh { - if r.err != nil && firstErr == nil { - firstErr = r.err + if r.err != nil && waitErr == nil { + waitErr = r.err } if r.results != nil { - testResults[r.scriptPath] = r.results + cr.testResults[r.scriptPath] = r.results + // Check for individual test failures to set exit code + for _, res := range r.results { + if res.Error != nil { + cr.exitCode = 1 + } + } } if r.networkResolution != "" { - fileNetworkResolutions[r.scriptPath] = r.networkResolution - } - for _, res := range testResults[r.scriptPath] { - if res.Error != nil { - exitCode = 1 - break - } + cr.fileNetworkResolutions[r.scriptPath] = r.networkResolution } } - if firstErr != nil { - return nil, firstErr - } + return cr, waitErr +} - // Track fork test usage metrics - aggregate into single event - hasPragmaFiles := len(fileNetworkResolutions) > 0 - hasStaticFork := forkCfg != nil +// trackForkMetrics emits a telemetry event when fork mode is used. +func trackForkMetrics(cr *concurrencyResult, cfg testRunConfig, totalFiles int) { + hasPragmaFiles := len(cr.fileNetworkResolutions) > 0 + hasStaticFork := cfg.forkCfg != nil - if hasPragmaFiles || hasStaticFork { - // Determine primary fork source - forkSource := "none" - var primaryNetwork string - var chainID string - hasHeight := false + if !hasPragmaFiles && !hasStaticFork { + return + } - if hasPragmaFiles { - // Pragma takes priority - collect unique networks - forkSource = "pragma" - networkSet := make(map[string]bool) - for _, network := range fileNetworkResolutions { - networkSet[network] = true - } - // Use first resolved network as primary (for single-value tracking) - for _, network := range fileNetworkResolutions { - primaryNetwork = network - break - } - // If multiple networks, note that in source - if len(networkSet) > 1 { - forkSource = "pragma-mixed" - } - } else if hasStaticFork { - // Static flags - if flags.ForkHost != "" { - forkSource = "fork-host-flag" - } else if flags.Fork != "" { - forkSource = "fork-flag" - } - primaryNetwork = networkLabel - chainID = forkCfg.ChainID.String() - hasHeight = forkCfg.ForkHeight > 0 - } - - command.TrackEvent("test-fork", map[string]any{ - "fork_source": forkSource, - "network": primaryNetwork, - "chain_id": chainID, - "has_height": hasHeight, - "pragma_files": len(fileNetworkResolutions), - "total_files": len(testFiles), - "version": build.Semver(), - "os": goRuntime.GOOS, - "ci": os.Getenv("CI") != "", - }) + forkSource := "none" + var primaryNetwork, chainID string + hasHeight := false + + if hasPragmaFiles { + forkSource = "pragma" + networkSet := make(map[string]bool) + for _, network := range cr.fileNetworkResolutions { + networkSet[network] = true + } + for _, network := range cr.fileNetworkResolutions { + primaryNetwork = network + break + } + if len(networkSet) > 1 { + forkSource = "pragma-mixed" + } + } else { + if cfg.forkHostFlag != "" { + forkSource = "fork-host-flag" + } else if cfg.forkFlag != "" { + forkSource = "fork-flag" + } + primaryNetwork = cfg.networkLabel + chainID = cfg.forkCfg.ChainID.String() + hasHeight = cfg.forkCfg.ForkHeight > 0 } - return &result{ - Results: testResults, - CoverageReport: coverageReport, - RandomSeed: seed, - exitCode: exitCode, - }, nil + command.TrackEvent("test-fork", map[string]any{ + "fork_source": forkSource, + "network": primaryNetwork, + "chain_id": chainID, + "has_height": hasHeight, + "pragma_files": len(cr.fileNetworkResolutions), + "total_files": totalFiles, + "version": build.Semver(), + "os": goRuntime.GOOS, + "ci": os.Getenv("CI") != "", + }) } func importResolver(scriptPath string, state *flowkit.State) cdcTests.ImportResolver {