]> Cypherpunks repositories - gostls13.git/commitdiff
cmd/internal/moddeps: check for consistent versioning among all modules in GOROOT
authorBryan C. Mills <bcmills@google.com>
Thu, 30 Jan 2020 22:13:01 +0000 (17:13 -0500)
committerBryan C. Mills <bcmills@google.com>
Wed, 19 Feb 2020 21:53:23 +0000 (21:53 +0000)
Updates #36851
Fixes #36907

Change-Id: I29627729d916e3b8132d46cf458ba856ffb0beeb
Reviewed-on: https://go-review.googlesource.com/c/go/+/217218
Run-TryBot: Bryan C. Mills <bcmills@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Jay Conrod <jayconrod@google.com>
src/cmd/internal/moddeps/moddeps_test.go [new file with mode: 0644]

diff --git a/src/cmd/internal/moddeps/moddeps_test.go b/src/cmd/internal/moddeps/moddeps_test.go
new file mode 100644 (file)
index 0000000..d544a4d
--- /dev/null
@@ -0,0 +1,223 @@
+// 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 (
+       "encoding/json"
+       "fmt"
+       "internal/testenv"
+       "io/ioutil"
+       "os"
+       "os/exec"
+       "path/filepath"
+       "runtime"
+       "strings"
+       "sync"
+       "testing"
+
+       "golang.org/x/mod/module"
+)
+
+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.Walk(runtime.GOROOT(), func(path string, info os.FileInfo, err error) error {
+                       if err != nil {
+                               return err
+                       }
+                       if info.Name() == "vendor" || info.Name() == "testdata" {
+                               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 := exec.Command(goBin, "list", "-json", "-m")
+                       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 {
+               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
+}
+
+// TestAllDependenciesVendored ensures 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.
+//
+// This test does NOT ensure that the vendored contents match the unmodified
+// contents of the corresponding dependency versions. Such as test would require
+// network access, and would currently either need to copy the entire GOROOT module
+// or explicitly invoke version control to check for changes.
+// (See golang.org/issue/36852 and golang.org/issue/27348.)
+func TestAllDependenciesVendored(t *testing.T) {
+       goBin := testenv.GoToolPath(t)
+
+       for _, m := range findGorootModules(t) {
+               t.Run(m.Path, 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 := exec.Command(goBin, "list", "-mod=vendor", "-deps", "./...")
+                               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 dependecies 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 := exec.Command(goBin, "list", "-m", "all")
+                       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)
+                       }
+               })
+       }
+}
+
+// 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 {
+                       // TestAllDependenciesVendored 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 := ioutil.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)
+                               }
+                       }
+               }
+       }
+}