diff options
Diffstat (limited to 'src/cmd/internal/moddeps')
-rw-r--r-- | src/cmd/internal/moddeps/moddeps_test.go | 522 |
1 files changed, 522 insertions, 0 deletions
diff --git a/src/cmd/internal/moddeps/moddeps_test.go b/src/cmd/internal/moddeps/moddeps_test.go new file mode 100644 index 0000000..4122064 --- /dev/null +++ b/src/cmd/internal/moddeps/moddeps_test.go @@ -0,0 +1,522 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package moddeps_test + +import ( + "bytes" + "encoding/json" + "fmt" + "internal/testenv" + "io" + "io/fs" + "os" + "path/filepath" + "strings" + "sync" + "testing" + + "golang.org/x/mod/module" +) + +// TestAllDependencies ensures dependencies of all +// modules in GOROOT are in a consistent state. +// +// In short mode, it does a limited quick check and stops there. +// In long mode, it also makes a copy of the entire GOROOT tree +// and requires network access to perform more thorough checks. +// Keep this distinction in mind when adding new checks. +// +// See issues 36852, 41409, and 43687. +// (Also see golang.org/issue/27348.) +func TestAllDependencies(t *testing.T) { + goBin := testenv.GoToolPath(t) + + // Ensure that all packages imported within GOROOT + // are vendored in the corresponding GOROOT module. + // + // This property allows offline development within the Go project, and ensures + // that all dependency changes are presented in the usual code review process. + // + // As a quick first-order check, avoid network access and the need to copy the + // entire GOROOT tree or explicitly invoke version control to check for changes. + // Just check that packages are vendored. (In non-short mode, we go on to also + // copy the GOROOT tree and perform more rigorous consistency checks. Jump below + // for more details.) + for _, m := range findGorootModules(t) { + // This short test does NOT ensure that the vendored contents match + // the unmodified contents of the corresponding dependency versions. + t.Run(m.Path+"(quick)", func(t *testing.T) { + if m.hasVendor { + // Load all of the packages in the module to ensure that their + // dependencies are vendored. If any imported package is missing, + // 'go list -deps' will fail when attempting to load it. + cmd := testenv.Command(t, goBin, "list", "-mod=vendor", "-deps", "./...") + cmd.Env = append(os.Environ(), "GO111MODULE=on", "GOWORK=off") + cmd.Dir = m.Dir + cmd.Stderr = new(strings.Builder) + _, err := cmd.Output() + if err != nil { + t.Errorf("%s: %v\n%s", strings.Join(cmd.Args, " "), err, cmd.Stderr) + t.Logf("(Run 'go mod vendor' in %s to ensure that dependencies have been vendored.)", m.Dir) + } + return + } + + // There is no vendor directory, so the module must have no dependencies. + // Check that the list of active modules contains only the main module. + cmd := testenv.Command(t, goBin, "list", "-mod=readonly", "-m", "all") + cmd.Env = append(os.Environ(), "GO111MODULE=on", "GOWORK=off") + cmd.Dir = m.Dir + cmd.Stderr = new(strings.Builder) + out, err := cmd.Output() + if err != nil { + t.Fatalf("%s: %v\n%s", strings.Join(cmd.Args, " "), err, cmd.Stderr) + } + if strings.TrimSpace(string(out)) != m.Path { + t.Errorf("'%s' reported active modules other than %s:\n%s", strings.Join(cmd.Args, " "), m.Path, out) + t.Logf("(Run 'go mod tidy' in %s to ensure that no extraneous dependencies were added, or 'go mod vendor' to copy in imported packages.)", m.Dir) + } + }) + } + + // We now get to the slow, but more thorough part of the test. + // Only run it in long test mode. + if testing.Short() { + return + } + + // Ensure that all modules within GOROOT are tidy, vendored, and bundled. + // Ensure that the vendored contents match the unmodified contents of the + // corresponding dependency versions. + // + // The non-short section of this test requires network access and the diff + // command. + // + // It makes a temporary copy of the entire GOROOT tree (where it can safely + // perform operations that may mutate the tree), executes the same module + // maintenance commands that we expect Go developers to run, and then + // diffs the potentially modified module copy with the real one in GOROOT. + // (We could try to rely on Git to do things differently, but that's not the + // path we've chosen at this time. This allows the test to run when the tree + // is not checked into Git.) + + testenv.MustHaveExternalNetwork(t) + if haveDiff := func() bool { + diff, err := testenv.Command(t, "diff", "--recursive", "--unified", ".", ".").CombinedOutput() + if err != nil || len(diff) != 0 { + return false + } + diff, err = testenv.Command(t, "diff", "--recursive", "--unified", ".", "..").CombinedOutput() + if err == nil || len(diff) == 0 { + return false + } + return true + }(); !haveDiff { + // For now, the diff command is a mandatory dependency of this test. + // This test will primarily run on longtest builders, since few people + // would test the cmd/internal/moddeps package directly, and all.bash + // runs tests in short mode. It's fine to skip if diff is unavailable. + t.Skip("skipping because a diff command with support for --recursive and --unified flags is unavailable") + } + + // We're going to check the standard modules for tidiness, so we need a usable + // GOMODCACHE. If the default directory doesn't exist, use a temporary + // directory instead. (That can occur, for example, when running under + // run.bash with GO_TEST_SHORT=0: run.bash sets GOPATH=/nonexist-gopath, and + // GO_TEST_SHORT=0 causes it to run this portion of the test.) + var modcacheEnv []string + { + out, err := testenv.Command(t, goBin, "env", "GOMODCACHE").Output() + if err != nil { + t.Fatalf("%s env GOMODCACHE: %v", goBin, err) + } + modcacheOk := false + if gomodcache := string(bytes.TrimSpace(out)); gomodcache != "" { + if _, err := os.Stat(gomodcache); err == nil { + modcacheOk = true + } + } + if !modcacheOk { + modcacheEnv = []string{ + "GOMODCACHE=" + t.TempDir(), + "GOFLAGS=" + os.Getenv("GOFLAGS") + " -modcacherw", // Allow t.TempDir() to clean up subdirectories. + } + } + } + + // Build the bundle binary at the golang.org/x/tools + // module version specified in GOROOT/src/cmd/go.mod. + bundleDir := t.TempDir() + r := runner{ + Dir: filepath.Join(testenv.GOROOT(t), "src/cmd"), + Env: append(os.Environ(), modcacheEnv...), + } + r.run(t, goBin, "build", "-mod=readonly", "-o", bundleDir, "golang.org/x/tools/cmd/bundle") + + var gorootCopyDir string + for _, m := range findGorootModules(t) { + // Create a test-wide GOROOT copy. It can be created once + // and reused between subtests whenever they don't fail. + // + // This is a relatively expensive operation, but it's a pre-requisite to + // be able to safely run commands like "go mod tidy", "go mod vendor", and + // "go generate" on the GOROOT tree content. Those commands may modify the + // tree, and we don't want to happen to the real tree as part of executing + // a test. + if gorootCopyDir == "" { + gorootCopyDir = makeGOROOTCopy(t) + } + + t.Run(m.Path+"(thorough)", func(t *testing.T) { + defer func() { + if t.Failed() { + // The test failed, which means it's possible the GOROOT copy + // may have been modified. No choice but to reset it for next + // module test case. (This is slow, but it happens only during + // test failures.) + gorootCopyDir = "" + } + }() + + rel, err := filepath.Rel(testenv.GOROOT(t), m.Dir) + if err != nil { + t.Fatalf("filepath.Rel(%q, %q): %v", testenv.GOROOT(t), m.Dir, err) + } + r := runner{ + Dir: filepath.Join(gorootCopyDir, rel), + Env: append(append(os.Environ(), modcacheEnv...), + // Set GOROOT. + "GOROOT="+gorootCopyDir, + // Explicitly override PWD and clear GOROOT_FINAL so that GOROOT=gorootCopyDir is definitely used. + "PWD="+filepath.Join(gorootCopyDir, rel), + "GOROOT_FINAL=", + // Add GOROOTcopy/bin and bundleDir to front of PATH. + "PATH="+filepath.Join(gorootCopyDir, "bin")+string(filepath.ListSeparator)+ + bundleDir+string(filepath.ListSeparator)+os.Getenv("PATH"), + "GOWORK=off", + ), + } + goBinCopy := filepath.Join(gorootCopyDir, "bin", "go") + r.run(t, goBinCopy, "mod", "tidy") // See issue 43687. + r.run(t, goBinCopy, "mod", "verify") // Verify should be a no-op, but test it just in case. + r.run(t, goBinCopy, "mod", "vendor") // See issue 36852. + pkgs := packagePattern(m.Path) + r.run(t, goBinCopy, "generate", `-run=^//go:generate bundle `, pkgs) // See issue 41409. + advice := "$ cd " + m.Dir + "\n" + + "$ go mod tidy # to remove extraneous dependencies\n" + + "$ go mod vendor # to vendor dependencies\n" + + "$ go generate -run=bundle " + pkgs + " # to regenerate bundled packages\n" + if m.Path == "std" { + r.run(t, goBinCopy, "generate", "syscall", "internal/syscall/...") // See issue 43440. + advice += "$ go generate syscall internal/syscall/... # to regenerate syscall packages\n" + } + // TODO(golang.org/issue/43440): Check anything else influenced by dependency versions. + + diff, err := testenv.Command(t, "diff", "--recursive", "--unified", r.Dir, m.Dir).CombinedOutput() + if err != nil || len(diff) != 0 { + t.Errorf(`Module %s in %s is not tidy (-want +got): + +%s +To fix it, run: + +%s +(If module %[1]s is definitely tidy, this could mean +there's a problem in the go or bundle command.)`, m.Path, m.Dir, diff, advice) + } + }) + } +} + +// packagePattern returns a package pattern that matches all packages +// in the module modulePath, and ideally as few others as possible. +func packagePattern(modulePath string) string { + if modulePath == "std" { + return "std" + } + return modulePath + "/..." +} + +// makeGOROOTCopy makes a temporary copy of the current GOROOT tree. +// The goal is to allow the calling test t to safely mutate a GOROOT +// copy without also modifying the original GOROOT. +// +// It copies the entire tree as is, with the exception of the GOROOT/.git +// directory, which is skipped, and the GOROOT/{bin,pkg} directories, +// which are symlinked. This is done for speed, since a GOROOT tree is +// functional without being in a Git repository, and bin and pkg are +// deemed safe to share for the purpose of the TestAllDependencies test. +func makeGOROOTCopy(t *testing.T) string { + t.Helper() + gorootCopyDir := t.TempDir() + err := filepath.Walk(testenv.GOROOT(t), func(src string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() && src == filepath.Join(testenv.GOROOT(t), ".git") { + return filepath.SkipDir + } + + rel, err := filepath.Rel(testenv.GOROOT(t), src) + if err != nil { + return fmt.Errorf("filepath.Rel(%q, %q): %v", testenv.GOROOT(t), src, err) + } + dst := filepath.Join(gorootCopyDir, rel) + + if info.IsDir() && (src == filepath.Join(testenv.GOROOT(t), "bin") || + src == filepath.Join(testenv.GOROOT(t), "pkg")) { + // If the OS supports symlinks, use them instead + // of copying the bin and pkg directories. + if err := os.Symlink(src, dst); err == nil { + return filepath.SkipDir + } + } + + perm := info.Mode() & os.ModePerm + if info.Mode()&os.ModeSymlink != 0 { + info, err = os.Stat(src) + if err != nil { + return err + } + perm = info.Mode() & os.ModePerm + } + + // If it's a directory, make a corresponding directory. + if info.IsDir() { + return os.MkdirAll(dst, perm|0200) + } + + // Copy the file bytes. + // We can't create a symlink because the file may get modified; + // we need to ensure that only the temporary copy is affected. + s, err := os.Open(src) + if err != nil { + return err + } + defer s.Close() + d, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_EXCL, perm) + if err != nil { + return err + } + _, err = io.Copy(d, s) + if err != nil { + d.Close() + return err + } + return d.Close() + }) + if err != nil { + t.Fatal(err) + } + return gorootCopyDir +} + +type runner struct { + Dir string + Env []string +} + +// run runs the command and requires that it succeeds. +func (r runner) run(t *testing.T, args ...string) { + t.Helper() + cmd := testenv.Command(t, args[0], args[1:]...) + cmd.Dir = r.Dir + cmd.Env = r.Env + out, err := cmd.CombinedOutput() + if err != nil { + t.Logf("> %s\n", strings.Join(args, " ")) + t.Fatalf("command failed: %s\n%s", err, out) + } +} + +// TestDependencyVersionsConsistent verifies that each module in GOROOT that +// requires a given external dependency requires the same version of that +// dependency. +// +// This property allows us to maintain a single release branch of each such +// dependency, minimizing the number of backports needed to pull in critical +// fixes. It also ensures that any bug detected and fixed in one GOROOT module +// (such as "std") is fixed in all other modules (such as "cmd") as well. +func TestDependencyVersionsConsistent(t *testing.T) { + // Collect the dependencies of all modules in GOROOT, indexed by module path. + type requirement struct { + Required module.Version + Replacement module.Version + } + seen := map[string]map[requirement][]gorootModule{} // module path → requirement → set of modules with that requirement + for _, m := range findGorootModules(t) { + if !m.hasVendor { + // TestAllDependencies will ensure that the module has no dependencies. + continue + } + + // We want this test to be able to run offline and with an empty module + // cache, so we verify consistency only for the module versions listed in + // vendor/modules.txt. That includes all direct dependencies and all modules + // that provide any imported packages. + // + // It's ok if there are undetected differences in modules that do not + // provide imported packages: we will not have to pull in any backports of + // fixes to those modules anyway. + vendor, err := os.ReadFile(filepath.Join(m.Dir, "vendor", "modules.txt")) + if err != nil { + t.Error(err) + continue + } + + for _, line := range strings.Split(strings.TrimSpace(string(vendor)), "\n") { + parts := strings.Fields(line) + if len(parts) < 3 || parts[0] != "#" { + continue + } + + // This line is of the form "# module version [=> replacement [version]]". + var r requirement + r.Required.Path = parts[1] + r.Required.Version = parts[2] + if len(parts) >= 5 && parts[3] == "=>" { + r.Replacement.Path = parts[4] + if module.CheckPath(r.Replacement.Path) != nil { + // If the replacement is a filesystem path (rather than a module path), + // we don't know whether the filesystem contents have changed since + // the module was last vendored. + // + // Fortunately, we do not currently use filesystem-local replacements + // in GOROOT modules. + t.Errorf("cannot check consistency for filesystem-local replacement in module %s (%s):\n%s", m.Path, m.Dir, line) + } + + if len(parts) >= 6 { + r.Replacement.Version = parts[5] + } + } + + if seen[r.Required.Path] == nil { + seen[r.Required.Path] = make(map[requirement][]gorootModule) + } + seen[r.Required.Path][r] = append(seen[r.Required.Path][r], m) + } + } + + // Now verify that we saw only one distinct version for each module. + for path, versions := range seen { + if len(versions) > 1 { + t.Errorf("Modules within GOROOT require different versions of %s.", path) + for r, mods := range versions { + desc := new(strings.Builder) + desc.WriteString(r.Required.Version) + if r.Replacement.Path != "" { + fmt.Fprintf(desc, " => %s", r.Replacement.Path) + if r.Replacement.Version != "" { + fmt.Fprintf(desc, " %s", r.Replacement.Version) + } + } + + for _, m := range mods { + t.Logf("%s\trequires %v", m.Path, desc) + } + } + } + } +} + +type gorootModule struct { + Path string + Dir string + hasVendor bool +} + +// findGorootModules returns the list of modules found in the GOROOT source tree. +func findGorootModules(t *testing.T) []gorootModule { + t.Helper() + goBin := testenv.GoToolPath(t) + + goroot.once.Do(func() { + goroot.err = filepath.WalkDir(testenv.GOROOT(t), func(path string, info fs.DirEntry, err error) error { + if err != nil { + return err + } + if info.IsDir() && (info.Name() == "vendor" || info.Name() == "testdata") { + return filepath.SkipDir + } + if info.IsDir() && path == filepath.Join(testenv.GOROOT(t), "pkg") { + // GOROOT/pkg contains generated artifacts, not source code. + // + // In https://golang.org/issue/37929 it was observed to somehow contain + // a module cache, so it is important to skip. (That helps with the + // running time of this test anyway.) + return filepath.SkipDir + } + if info.IsDir() && (strings.HasPrefix(info.Name(), "_") || strings.HasPrefix(info.Name(), ".")) { + // _ and . prefixed directories can be used for internal modules + // without a vendor directory that don't contribute to the build + // but might be used for example as code generators. + return filepath.SkipDir + } + if info.IsDir() || info.Name() != "go.mod" { + return nil + } + dir := filepath.Dir(path) + + // Use 'go list' to describe the module contained in this directory (but + // not its dependencies). + cmd := testenv.Command(t, goBin, "list", "-json", "-m") + cmd.Env = append(os.Environ(), "GO111MODULE=on", "GOWORK=off") + cmd.Dir = dir + cmd.Stderr = new(strings.Builder) + out, err := cmd.Output() + if err != nil { + return fmt.Errorf("'go list -json -m' in %s: %w\n%s", dir, err, cmd.Stderr) + } + + var m gorootModule + if err := json.Unmarshal(out, &m); err != nil { + return fmt.Errorf("decoding 'go list -json -m' in %s: %w", dir, err) + } + if m.Path == "" || m.Dir == "" { + return fmt.Errorf("'go list -json -m' in %s failed to populate Path and/or Dir", dir) + } + if _, err := os.Stat(filepath.Join(dir, "vendor")); err == nil { + m.hasVendor = true + } + goroot.modules = append(goroot.modules, m) + return nil + }) + if goroot.err != nil { + return + } + + // knownGOROOTModules is a hard-coded list of modules that are known to exist in GOROOT. + // If findGorootModules doesn't find a module, it won't be covered by tests at all, + // so make sure at least these modules are found. See issue 46254. If this list + // becomes a nuisance to update, can be replaced with len(goroot.modules) check. + knownGOROOTModules := [...]string{ + "std", + "cmd", + "misc", + "test/bench/go1", + } + var seen = make(map[string]bool) // Key is module path. + for _, m := range goroot.modules { + seen[m.Path] = true + } + for _, m := range knownGOROOTModules { + if !seen[m] { + goroot.err = fmt.Errorf("findGorootModules didn't find the well-known module %q", m) + break + } + } + }) + if goroot.err != nil { + t.Fatal(goroot.err) + } + return goroot.modules +} + +// goroot caches the list of modules found in the GOROOT source tree. +var goroot struct { + once sync.Once + modules []gorootModule + err error +} |