diff --git a/plugin/evm/testutils/metrics.go b/plugin/evm/testutils/metrics.go index aa2ee19037..53be77de60 100644 --- a/plugin/evm/testutils/metrics.go +++ b/plugin/evm/testutils/metrics.go @@ -10,6 +10,7 @@ import ( // If the [metrics.Enabled] is already true, nothing is done. // Otherwise, it is set to true and is reverted to false when the test finishes. func WithMetrics(t *testing.T) { + panicIfCallsFromNonTest() if metrics.Enabled { return } diff --git a/plugin/evm/testutils/testutils.go b/plugin/evm/testutils/testutils.go new file mode 100644 index 0000000000..662aa7d7e4 --- /dev/null +++ b/plugin/evm/testutils/testutils.go @@ -0,0 +1,27 @@ +// Package testutils contains test utilities ONLY to be used outside plugin/evm. +// The aim is to reduce changes in geth tests by using the utilities defined here. +// This package MUST NOT be imported by non-test packages. + +package testutils + +import ( + "runtime" + "strings" +) + +// panicIfCallsFromNonTest should be added at the top of every function defined in this package +// to enforce this package to be used only by tests. +func panicIfCallsFromNonTest() { + pc := make([]uintptr, 64) + runtime.Callers(0, pc) + frames := runtime.CallersFrames(pc) + for { + f, more := frames.Next() + if strings.HasPrefix(f.File, "/testing/") || strings.HasSuffix(f.File, "_test.go") { + return + } + if !more { + panic("no test file in call stack") + } + } +} diff --git a/plugin/evm/testutils/testutils_test.go b/plugin/evm/testutils/testutils_test.go new file mode 100644 index 0000000000..1f2fe1f229 --- /dev/null +++ b/plugin/evm/testutils/testutils_test.go @@ -0,0 +1,81 @@ +package testutils + +import ( + "fmt" + "go/parser" + "go/token" + "io/fs" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestMustNotImport fails if a package in plugin/evm imports the testutils package. +func TestMustNotImport(t *testing.T) { + var evmPackagePaths []string + filepath.WalkDir("../", func(path string, d fs.DirEntry, err error) error { + if !d.IsDir() { + return nil + } else if path == "../testutils" { + return nil + } + evmPackagePaths = append(evmPackagePaths, path) + return nil + }) + + for _, packagePath := range evmPackagePaths { + packageName := "github.com/ava-labs/subnet-evm/plugin/evm/" + strings.TrimPrefix(packagePath, "../") + imports, err := getPackageImports(packagePath) + require.NoError(t, err) + _, ok := imports["github.com/ava-labs/subnet-evm/plugin/evm/testutils"] + assert.Falsef(t, ok, "package %s imports testutils: testutils should only be used outside plugin/evm.", packageName) + } +} + +func getPackageImports(packagePath string) (imports map[string]struct{}, err error) { + imports = make(map[string]struct{}) + + err = filepath.Walk(packagePath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } else if path != packagePath && info.IsDir() { + return fs.SkipDir + } else if !strings.HasSuffix(info.Name(), ".go") { + return nil + } + err = parseImportsFromFile(path, imports) + if err != nil { + return fmt.Errorf("failed to parse imports: %s", err) + } + return nil + }) + + if err != nil { + return nil, fmt.Errorf("failed to walk through package files: %v", err) + } + + return imports, nil +} + +func parseImportsFromFile(filePath string, imports map[string]struct{}) error { + file, err := os.Open(filePath) + if err != nil { + return fmt.Errorf("failed to open file %s: %v", filePath, err) + } + defer file.Close() + + node, err := parser.ParseFile(token.NewFileSet(), filePath, file, parser.ImportsOnly) + if err != nil { + return fmt.Errorf("failed to parse file %s: %v", filePath, err) + } + + for _, nodeImport := range node.Imports { + imports[strings.Trim(nodeImport.Path.Value, `"`)] = struct{}{} + } + + return nil +}