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" \ 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/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 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..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" @@ -35,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" @@ -77,6 +79,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() @@ -180,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, @@ -187,20 +210,125 @@ 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) - var currentTestFile 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)) + + 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 + + if flags.ForkHost != "" { + effectiveForkHost = strings.TrimSpace(flags.ForkHost) + } else if flags.Fork != "" { + 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) + } + } + + if strings.TrimSpace(flags.Fork) != "" { + networkLabel = strings.ToLower(flags.Fork) + } - // Resolve network labels using flow.json state - resolveNetworkFromState := func(label string) (string, bool) { + 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, + } + 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 } - // 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)) @@ -215,237 +343,216 @@ func testCode( 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 - } + // 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 } +} - // Configure fork mode if requested - var effectiveForkHost string +// 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 + } - // 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) + 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) } - effectiveForkHost = network.Host - if effectiveForkHost == "" { - return nil, fmt.Errorf("network %q has no host configured", flags.Fork) + + 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) } +} - // Determine network label (used by resolver/addresses); default to testing - networkLabel := "testing" - if strings.TrimSpace(flags.Fork) != "" { - networkLabel = strings.ToLower(flags.Fork) +// 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) } - // 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) - } + return runner, &resolvedNetwork +} - cfg := cdcTests.ForkConfig{ - ForkHost: effectiveForkHost, - ChainID: forkChainID, - ForkHeight: flags.ForkHeight, - } - forkCfg = &cfg +// 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)) + } - // 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" - } - } + testFunctions, err := runner.GetTests(string(code)) + if err != nil { + return nil, err } - 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 - }, - ) + 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 } - var seed int64 - if flags.Seed > 0 { - seed = flags.Seed - } else if flags.Random { - seed = int64(rand.Intn(150000)) - } + return nil, nil +} - testResults := make(map[string]cdcTests.Results, 0) - exitCode := 0 - 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 - } +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() + } - contract, exists := contractsByName[contractName] - if !exists { - return common.Address{}, fmt.Errorf("contract not found: %s", contractName) - } + type fileResult struct { + scriptPath string + results cdcTests.Results + networkResolution string + err error + } - alias := contract.Aliases.ByNetwork(network) - if alias != nil { - return common.Address(alias.Address), nil - } + resultCh := make(chan fileResult, len(testFiles)) - // 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 - } - } + g, ctx := errgroup.WithContext(context.Background()) + g.SetLimit(jobs) - return common.Address{}, fmt.Errorf("no address for contract %s on network %s", contractName, network) - }) + for scriptPath, code := range testFiles { + g.Go(func() error { + if ctx.Err() != nil { + return ctx.Err() + } - if forkCfg != nil { - fileRunner = fileRunner.WithFork(*forkCfg) - } - if coverageReport != nil { - fileRunner = fileRunner.WithCoverageReport(coverageReport) - } - if seed > 0 { - fileRunner = fileRunner.WithRandomSeed(seed) - } + runner, resolvedNetwork := buildTestRunner(scriptPath, state, cfg, logger) + results, err := runFileTests(runner, code, cfg.name) - if flags.Name != "" { - testFunctions, err := fileRunner.GetTests(string(code)) - if err != nil { - return nil, err + resultCh <- fileResult{ + scriptPath: scriptPath, + results: results, + networkResolution: *resolvedNetwork, + err: err, } + return nil + }) + } - for _, testFunction := range testFunctions { - if testFunction != flags.Name { - continue - } + waitErr := g.Wait() + close(resultCh) - result, err := fileRunner.RunTest(string(code), flags.Name) - if err != nil { - return nil, err + cr := &concurrencyResult{ + testResults: make(map[string]cdcTests.Results), + fileNetworkResolutions: make(map[string]string), + } + + for r := range resultCh { + if r.err != nil && waitErr == nil { + waitErr = r.err + } + if r.results != nil { + 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 } - testResults[scriptPath] = []cdcTests.Result{*result} } - } else { - results, err := fileRunner.RunTests(string(code)) - if err != nil { - return nil, err - } - testResults[scriptPath] = results } - - for _, result := range testResults[scriptPath] { - if result.Error != nil { - exitCode = 1 - break - } + if r.networkResolution != "" { + cr.fileNetworkResolutions[r.scriptPath] = r.networkResolution } - - // Clear current test file after processing - currentTestFile = "" } - // Track fork test usage metrics - aggregate into single event - hasPragmaFiles := len(fileNetworkResolutions) > 0 - hasStaticFork := forkCfg != nil + return cr, waitErr +} - if hasPragmaFiles || hasStaticFork { - // Determine primary fork source - forkSource := "none" - var primaryNetwork string - var chainID string - hasHeight := false +// 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 { - // 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") != "", - }) + if !hasPragmaFiles && !hasStaticFork { + return } - return &result{ - Results: testResults, - CoverageReport: coverageReport, - RandomSeed: seed, - exitCode: exitCode, - }, nil + 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 + } + + 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 { diff --git a/internal/test/test_test.go b/internal/test/test_test.go index 71440ff24..82c9efdec 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(` @@ -1270,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) +} 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)