mirror of
https://github.com/golang/go
synced 2024-11-17 21:04:43 -07:00
cmd/internal/moddeps: check for consistent versioning among all modules in GOROOT
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>
This commit is contained in:
parent
2cdb2ecacc
commit
8a8adc23d4
223
src/cmd/internal/moddeps/moddeps_test.go
Normal file
223
src/cmd/internal/moddeps/moddeps_test.go
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user