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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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" \
Expand Down
14 changes: 12 additions & 2 deletions internal/cadence/lint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,8 +332,18 @@ func Test_Lint(t *testing.T) {
&lintResult{
Results: []fileResult{
{
FilePath: "StdlibImportsScript.cdc",
Diagnostics: []analysis.Diagnostic{},
FilePath: "StdlibImportsScript.cdc",
Diagnostics: []analysis.Diagnostic{
{
Location: common.StringLocation("StdlibImportsScript.cdc"),
Category: "security",
Message: "hardcoded address detected — consider using named address imports for portability",
Range: ast.Range{
StartPos: ast.Position{Offset: 109, Line: 5, Column: 37},
EndPos: ast.Position{Offset: 112, Line: 5, Column: 40},
},
},
},
},
},
exitCode: 0,
Expand Down
56 changes: 56 additions & 0 deletions internal/test/bench_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
242 changes: 145 additions & 97 deletions internal/test/test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"regexp"
goRuntime "runtime"
"strings"
"sync"

cdcTests "github.com/onflow/cadence-tools/test"
"github.com/onflow/cadence/common"
Expand Down Expand Up @@ -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)"`
BaseDir string `default:"" flag:"base-dir" info:"Directory to search for test files (defaults to current directory)"`

// Fork mode flags
Expand Down Expand Up @@ -195,41 +197,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
Expand Down Expand Up @@ -305,92 +272,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
Expand Down
Loading
Loading