-
Notifications
You must be signed in to change notification settings - Fork 837
test: add import compliance tests #4537
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
| @@ -0,0 +1,49 @@ | |||
| # Allowed libevm packages that can be imported in the graft directory. | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
grafts/scripts should only be used for graft-related functionality and is likely to go away at some point once we've migrated everything we care about.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this should also be vms/evm - I'll generalize to EVM code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, vms/evm is a likely candidate. A key consideration of where to locate code is who will own it (i.e. be capable of approving changes). No point in requiring non-evm maintainers to approve evm code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agree!
| @@ -0,0 +1,232 @@ | |||
| // Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please find somewhere else for this too, the root of tests/ is for utility functions that get imported, not unit tests like this one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yup, vm/evm also seems appropriate
| ) | ||
|
|
||
| // TestDoNotImportFromGraft ensures proper import rules for graft packages: | ||
| // - graft/coreth can be imported anywhere EXCEPT vms/evm (but vms/evm/emulate is an exception) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Myabe provide more detail as to why these restrictions are necessary?
| return true | ||
| } | ||
|
|
||
| isInEmulate := strings.Contains(path, "/vms/evm/emulate/") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(No action required) Maybe localize the following 3 definitions to the conditionals that use them?
| isInEmulate := strings.Contains(path, "/vms/evm/emulate/") | ||
| isInVmsEvm := strings.Contains(path, "/vms/evm/") | ||
| isCoreth := strings.Contains(importPath, "/graft/coreth") | ||
| isSubnetEVM := strings.Contains(importPath, "/graft/subnet-evm") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(No action required) Not a biggie, but would prefer to see subnet-evm specific stuff as part of the migration not in advance.
But also, why is testing for these independent grafts conflated? Maybe implement 2 tests that use the same helpers, one for each graft location?
| require.Fail(t, formatImportViolations(foundImports, header)) | ||
| } | ||
|
|
||
| // TestDoNotImportLibevmPseudo ensures that no code in the repository imports |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(No action required) If it's libevm-internal only, why isn't the package called internal so that the restriction is enforced automatically by golang?
| // Copyright (C) 2019-2025, Ava Labs, Inc. All rights reserved. | ||
| // See the file LICENSE for licensing terms. | ||
|
|
||
| package tests |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| package tests | |
| package evm |
| // - graft/coreth can be imported anywhere EXCEPT vms/evm (but vms/evm/emulate is an exception) | ||
| // - graft/subnet-evm cannot be imported anywhere EXCEPT vms/evm/emulate |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are these the rules? I understand emulate being allowed to import, but not the other limitations.
Rule of thumb for all code, not just this instance: comments should explain "why", not "what" nor "how".
| // - graft/coreth can be imported anywhere EXCEPT vms/evm (but vms/evm/emulate is an exception) | ||
| // - graft/subnet-evm cannot be imported anywhere EXCEPT vms/evm/emulate | ||
| func TestEnforceGraftImportBoundaries(t *testing.T) { | ||
| graftRegex := regexp.MustCompile(`^github\.com/ava-labs/avalanchego/graft(/|$)`) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(subjective, optional) I've seen enough bugs arising from regular expressions that could have been string comparisons that I tend to avoid them unless absolutely necessary. The functionality in this file could just be:
func isPackageIn(importPath, root string) bool {
if importPath == root {
return true
}
if len(importPath) < len(root)+1 {
return false
}
return strings.HasPrefix(importPath, root) && importPath[len(root)] == '/'
}|
|
||
| // findImportsMatchingPattern is a generalized function that finds all imports | ||
| // matching a given regex pattern in the specified directory. | ||
| // The filterFunc can be used to skip certain files or imports (return true to skip). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return true to skip
This is the opposite to what I'd expect when passing in a filter function:
func myFilter(file string) bool {
return true // intuition says this is now included
}| if err != nil || !strings.HasSuffix(path, ".go") { | ||
| return err | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| if err != nil || !strings.HasSuffix(path, ".go") { | |
| return err | |
| } | |
| if err != nil { | |
| return err | |
| } | |
| if !strings.HasSuffix(path, ".go") { | |
| return nil | |
| } |
Although functionally equivalent, this is much clearer and doesn't force the reader to reason about how the returned err will be nil in the non-Go instance.
|
|
||
| // TestDoNotImportLibevmPseudo ensures that no code in the repository imports | ||
| // the libevm/pseudo package, which is for internal libevm use only. | ||
| func TestDoNotImportLibevmPseudo(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| func TestDoNotImportLibevmPseudo(t *testing.T) { | |
| func TestDoNotImportLibEVMPseudo(t *testing.T) { |
Although the project name is stylised libevm, initialisms in Go identifiers should have all initials with the same capitalisation.
|
|
||
| // TestDoNotImportLibevmPseudo ensures that no code in the repository imports | ||
| // the libevm/pseudo package, which is for internal libevm use only. | ||
| func TestDoNotImportLibevmPseudo(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why does this warrant a separate test?
| func findImportsMatchingPattern( | ||
| rootDir string, | ||
| importRegex *regexp.Regexp, | ||
| filterFunc func(filePath string, importPath string, importSpec *ast.ImportSpec) bool, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| filterFunc func(filePath string, importPath string, importSpec *ast.ImportSpec) bool, | |
| filterFunc func(filePath string, importPath string) bool, |
It's never used.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added comments based on the current macro-pattern, but that being said I think it's been overcomplicated by an unnecessary abstraction to support two tests. There's also no need to collect and then format the output when a t.Error() will do.
| strings.Contains(path, "graft/*/core/main_test.go") || | ||
| strings.Contains(path, "graft/*/tempextrastest/") { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
strings.Contains() doesn't support wild-card matching, but this is captured by the check below anyway.
| foundImports, err := findImportsMatchingPattern("..", graftRegex, func(path string, importPath string, _ *ast.ImportSpec) bool { | ||
| // Skip generated files and test-specific files | ||
| filename := filepath.Base(path) | ||
| if strings.HasPrefix(filename, "gen_") || |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That said, why should generated files be allowed to cross the import boundary?
|
I think the entirety of this PR can be reduced to something like this: func TestImportViolations(t *testing.T) {
const root ".."
err := filepath.Walk(root, func(file string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
if strings.ToLower(filepath.Ext(file)) != "go" {
return nil
}
node, err := parser.ParseFile(token.NewFileSet(), file, nil, parser.ImportsOnly)
if err != nil {
return fmt.Errorf("parser.ParseFile(..., %q, ...): %v", file, err)
}
for _, spec := range node.Imports {
if spec.Path == nil {
continue
}
imp := strings.Trim(spec.Path.Value, `"`)
inGraft := strings.Contains(file, "/graft/")
inEmulate := strings.Contains(file, "/vms/evm/emulate/")
inVMsEVM := strings.Contains(file, "/vms/evm/")
importsPseudo := isPackageIn(imp, "github.com/ava-labs/libevm/libevm/pseudo")
importsCoreth := isPackageIn(imp, "github.com/ava-labs/avalanchego/graft/coreth")
importsSubnetEVM := isPackageIn(imp, "github.com/ava-labs/avalanchego/graft/subnet-evm")
// TODO: before merging, this needs a comment explaining why each of them are a violation
violations := []bool{
importsPseudo,
!inGraft && importsCoreth && inVMsEVM && !inEmulate,
!inGraft && importsSubnetEVM && !inEmulate,
}
if slices.Contains(violations, true) {
t.Errorf("File %q imports %q", file, imp)
}
}
return nil
})
require.NoErrorf(t, err, "filepath.Walk(%q)", root)
}
func isPackageIn(importPath, root string) bool {
if importPath == root {
return true
}
if len(importPath) < len(root)+1 {
return false
}
return strings.HasPrefix(importPath, root) && importPath[len(root)] == '/'
}I'm not particularly happy with the relative path to walk nor the way of checking if something is in a sub-dir (they can be fixed together), but otherwise I think this covers all bases. |
Why this should be merged
This PR accomplishes three things:
vms/evmdirectory from importing packages within thegraft/directory.libevm/pseudofrom being imported anywhere in AvalancheGoHow this works
The boundaries are as follows:
graft/corethcan be imported everywhere in AvalancheGo exceptvms/evm.vms/evm/emulateis an exception to this rulegraft/subnet-evmcan not be imported anywhere in AvalancheGovms/evm/emulateis an exception to this ruleHow this was tested
Existing CI. I also added a sample "bad" commit (404acd5) to show how CI would behave.

edit: this is no longer a good example and is out of date but I kept it to show the formatting -- pretend the file is in
vms/evm/sample_bad.go.Need to be documented in RELEASES.md?
No