cmd/go: search breadth-first instead of depth-first for test dependency cycles

When we are looking for a dependency cycle involving a specific
package, we need to keep track of visited packages in order to avoid
repeatedly traversing a cycle that does not involve that package.

If we're keeping track of all visited packages anyway, we're already
spending O(N) memory on the traversal, so we may as well use
breadth-first search. That not only keeps the bookkeeping simple, but
also guarantees that we will find a shortest path (rather than a
completely arbitrary one).

Fixes #45863

Change-Id: I810c7337857e42dcb83630abbdea75021554be45
Reviewed-on: https://go-review.googlesource.com/c/go/+/330430
Trust: Bryan C. Mills <bcmills@google.com>
Reviewed-by: Jay Conrod <jayconrod@google.com>
This commit is contained in:
Bryan C. Mills 2021-06-23 15:28:37 -04:00
parent 73496e0df0
commit 44a12e5f33
2 changed files with 59 additions and 14 deletions

View file

@ -116,7 +116,7 @@ func TestPackagesAndErrors(ctx context.Context, opts PackageOpts, p *Package, co
// Can't change that code, because that code is only for loading the // Can't change that code, because that code is only for loading the
// non-test copy of a package. // non-test copy of a package.
ptestErr = &PackageError{ ptestErr = &PackageError{
ImportStack: testImportStack(stk[0], p1, p.ImportPath), ImportStack: importCycleStack(p1, p.ImportPath),
Err: errors.New("import cycle not allowed in test"), Err: errors.New("import cycle not allowed in test"),
IsImportCycle: true, IsImportCycle: true,
} }
@ -375,23 +375,45 @@ func TestPackagesAndErrors(ctx context.Context, opts PackageOpts, p *Package, co
return pmain, ptest, pxtest return pmain, ptest, pxtest
} }
func testImportStack(top string, p *Package, target string) []string { // importCycleStack returns an import stack from p to the package whose import
stk := []string{top, p.ImportPath} // path is target.
Search: func importCycleStack(p *Package, target string) []string {
for p.ImportPath != target { // importerOf maps each import path to its importer nearest to p.
for _, p1 := range p.Internal.Imports { importerOf := map[string]string{p.ImportPath: ""}
if p1.ImportPath == target || str.Contains(p1.Deps, target) {
stk = append(stk, p1.ImportPath) // q is a breadth-first queue of packages to search for target.
p = p1 // Every package added to q has a corresponding entry in pathTo.
continue Search //
} // We search breadth-first for two reasons:
} //
// Can't happen, but in case it does... // 1. We want to report the shortest cycle.
stk = append(stk, "<lost path to cycle>") //
break // 2. If p contains multiple cycles, the first cycle we encounter might not
// contain target. To ensure termination, we have to break all cycles
// other than the first.
q := []*Package{p}
for len(q) > 0 {
p := q[0]
q = q[1:]
if path := p.ImportPath; path == target {
var stk []string
for path != "" {
stk = append(stk, path)
path = importerOf[path]
} }
return stk return stk
} }
for _, dep := range p.Internal.Imports {
if _, ok := importerOf[dep.ImportPath]; !ok {
importerOf[dep.ImportPath] = p.ImportPath
q = append(q, dep)
}
}
}
panic("lost path to cycle")
}
// recompileForTest copies and replaces certain packages in pmain's dependency // recompileForTest copies and replaces certain packages in pmain's dependency
// graph. This is necessary for two reasons. First, if ptest is different than // graph. This is necessary for two reasons. First, if ptest is different than

View file

@ -0,0 +1,23 @@
# https://golang.org/issue/45863: a typo in a test package leading to an
# import cycle should be diagnosed, instead of causing an infinite loop.
# The failure mode of this test prior to the fix was a timeout or OOM crash.
go list -e -test -deps ./datastore/sql
-- go.mod --
module golang.org/issue45863
go 1.17
-- datastore/datastore_health.go --
package datastore
import (
"golang.org/issue45863/datastore"
"golang.org/issue45863/datastore/sql"
)
-- datastore/sql/sql.go --
package sql
-- datastore/sql/sql_test.go --
package sql
import _ "golang.org/issue45863/datastore"