Skip to content

Commit 8a8adc2

Browse files
author
Bryan C. Mills
committed
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 <[email protected]> TryBot-Result: Gobot Gobot <[email protected]> Reviewed-by: Jay Conrod <[email protected]>
1 parent 2cdb2ec commit 8a8adc2

File tree

1 file changed

+223
-0
lines changed

1 file changed

+223
-0
lines changed
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
// Copyright 2020 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package moddeps_test
6+
7+
import (
8+
"encoding/json"
9+
"fmt"
10+
"internal/testenv"
11+
"io/ioutil"
12+
"os"
13+
"os/exec"
14+
"path/filepath"
15+
"runtime"
16+
"strings"
17+
"sync"
18+
"testing"
19+
20+
"golang.org/x/mod/module"
21+
)
22+
23+
type gorootModule struct {
24+
Path string
25+
Dir string
26+
hasVendor bool
27+
}
28+
29+
// findGorootModules returns the list of modules found in the GOROOT source tree.
30+
func findGorootModules(t *testing.T) []gorootModule {
31+
t.Helper()
32+
goBin := testenv.GoToolPath(t)
33+
34+
goroot.once.Do(func() {
35+
goroot.err = filepath.Walk(runtime.GOROOT(), func(path string, info os.FileInfo, err error) error {
36+
if err != nil {
37+
return err
38+
}
39+
if info.Name() == "vendor" || info.Name() == "testdata" {
40+
return filepath.SkipDir
41+
}
42+
if info.IsDir() || info.Name() != "go.mod" {
43+
return nil
44+
}
45+
dir := filepath.Dir(path)
46+
47+
// Use 'go list' to describe the module contained in this directory (but
48+
// not its dependencies).
49+
cmd := exec.Command(goBin, "list", "-json", "-m")
50+
cmd.Dir = dir
51+
cmd.Stderr = new(strings.Builder)
52+
out, err := cmd.Output()
53+
if err != nil {
54+
return fmt.Errorf("'go list -json -m' in %s: %w\n%s", dir, err, cmd.Stderr)
55+
}
56+
57+
var m gorootModule
58+
if err := json.Unmarshal(out, &m); err != nil {
59+
return fmt.Errorf("decoding 'go list -json -m' in %s: %w", dir, err)
60+
}
61+
if m.Path == "" || m.Dir == "" {
62+
return fmt.Errorf("'go list -json -m' in %s failed to populate Path and/or Dir", dir)
63+
}
64+
if _, err := os.Stat(filepath.Join(dir, "vendor")); err == nil {
65+
m.hasVendor = true
66+
}
67+
goroot.modules = append(goroot.modules, m)
68+
return nil
69+
})
70+
})
71+
72+
if goroot.err != nil {
73+
t.Fatal(goroot.err)
74+
}
75+
return goroot.modules
76+
}
77+
78+
// goroot caches the list of modules found in the GOROOT source tree.
79+
var goroot struct {
80+
once sync.Once
81+
modules []gorootModule
82+
err error
83+
}
84+
85+
// TestAllDependenciesVendored ensures that all packages imported within GOROOT
86+
// are vendored in the corresponding GOROOT module.
87+
//
88+
// This property allows offline development within the Go project, and ensures
89+
// that all dependency changes are presented in the usual code review process.
90+
//
91+
// This test does NOT ensure that the vendored contents match the unmodified
92+
// contents of the corresponding dependency versions. Such as test would require
93+
// network access, and would currently either need to copy the entire GOROOT module
94+
// or explicitly invoke version control to check for changes.
95+
// (See golang.org/issue/36852 and golang.org/issue/27348.)
96+
func TestAllDependenciesVendored(t *testing.T) {
97+
goBin := testenv.GoToolPath(t)
98+
99+
for _, m := range findGorootModules(t) {
100+
t.Run(m.Path, func(t *testing.T) {
101+
if m.hasVendor {
102+
// Load all of the packages in the module to ensure that their
103+
// dependencies are vendored. If any imported package is missing,
104+
// 'go list -deps' will fail when attempting to load it.
105+
cmd := exec.Command(goBin, "list", "-mod=vendor", "-deps", "./...")
106+
cmd.Dir = m.Dir
107+
cmd.Stderr = new(strings.Builder)
108+
_, err := cmd.Output()
109+
if err != nil {
110+
t.Errorf("%s: %v\n%s", strings.Join(cmd.Args, " "), err, cmd.Stderr)
111+
t.Logf("(Run 'go mod vendor' in %s to ensure that dependecies have been vendored.)", m.Dir)
112+
}
113+
return
114+
}
115+
116+
// There is no vendor directory, so the module must have no dependencies.
117+
// Check that the list of active modules contains only the main module.
118+
cmd := exec.Command(goBin, "list", "-m", "all")
119+
cmd.Dir = m.Dir
120+
cmd.Stderr = new(strings.Builder)
121+
out, err := cmd.Output()
122+
if err != nil {
123+
t.Fatalf("%s: %v\n%s", strings.Join(cmd.Args, " "), err, cmd.Stderr)
124+
}
125+
if strings.TrimSpace(string(out)) != m.Path {
126+
t.Errorf("'%s' reported active modules other than %s:\n%s", strings.Join(cmd.Args, " "), m.Path, out)
127+
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)
128+
}
129+
})
130+
}
131+
}
132+
133+
// TestDependencyVersionsConsistent verifies that each module in GOROOT that
134+
// requires a given external dependency requires the same version of that
135+
// dependency.
136+
//
137+
// This property allows us to maintain a single release branch of each such
138+
// dependency, minimizing the number of backports needed to pull in critical
139+
// fixes. It also ensures that any bug detected and fixed in one GOROOT module
140+
// (such as "std") is fixed in all other modules (such as "cmd") as well.
141+
func TestDependencyVersionsConsistent(t *testing.T) {
142+
// Collect the dependencies of all modules in GOROOT, indexed by module path.
143+
type requirement struct {
144+
Required module.Version
145+
Replacement module.Version
146+
}
147+
seen := map[string]map[requirement][]gorootModule{} // module path → requirement → set of modules with that requirement
148+
for _, m := range findGorootModules(t) {
149+
if !m.hasVendor {
150+
// TestAllDependenciesVendored will ensure that the module has no
151+
// dependencies.
152+
continue
153+
}
154+
155+
// We want this test to be able to run offline and with an empty module
156+
// cache, so we verify consistency only for the module versions listed in
157+
// vendor/modules.txt. That includes all direct dependencies and all modules
158+
// that provide any imported packages.
159+
//
160+
// It's ok if there are undetected differences in modules that do not
161+
// provide imported packages: we will not have to pull in any backports of
162+
// fixes to those modules anyway.
163+
vendor, err := ioutil.ReadFile(filepath.Join(m.Dir, "vendor", "modules.txt"))
164+
if err != nil {
165+
t.Error(err)
166+
continue
167+
}
168+
169+
for _, line := range strings.Split(strings.TrimSpace(string(vendor)), "\n") {
170+
parts := strings.Fields(line)
171+
if len(parts) < 3 || parts[0] != "#" {
172+
continue
173+
}
174+
175+
// This line is of the form "# module version [=> replacement [version]]".
176+
var r requirement
177+
r.Required.Path = parts[1]
178+
r.Required.Version = parts[2]
179+
if len(parts) >= 5 && parts[3] == "=>" {
180+
r.Replacement.Path = parts[4]
181+
if module.CheckPath(r.Replacement.Path) != nil {
182+
// If the replacement is a filesystem path (rather than a module path),
183+
// we don't know whether the filesystem contents have changed since
184+
// the module was last vendored.
185+
//
186+
// Fortunately, we do not currently use filesystem-local replacements
187+
// in GOROOT modules.
188+
t.Errorf("cannot check consistency for filesystem-local replacement in module %s (%s):\n%s", m.Path, m.Dir, line)
189+
}
190+
191+
if len(parts) >= 6 {
192+
r.Replacement.Version = parts[5]
193+
}
194+
}
195+
196+
if seen[r.Required.Path] == nil {
197+
seen[r.Required.Path] = make(map[requirement][]gorootModule)
198+
}
199+
seen[r.Required.Path][r] = append(seen[r.Required.Path][r], m)
200+
}
201+
}
202+
203+
// Now verify that we saw only one distinct version for each module.
204+
for path, versions := range seen {
205+
if len(versions) > 1 {
206+
t.Errorf("Modules within GOROOT require different versions of %s.", path)
207+
for r, mods := range versions {
208+
desc := new(strings.Builder)
209+
desc.WriteString(r.Required.Version)
210+
if r.Replacement.Path != "" {
211+
fmt.Fprintf(desc, " => %s", r.Replacement.Path)
212+
if r.Replacement.Version != "" {
213+
fmt.Fprintf(desc, " %s", r.Replacement.Version)
214+
}
215+
}
216+
217+
for _, m := range mods {
218+
t.Logf("%s\trequires %v", m.Path, desc)
219+
}
220+
}
221+
}
222+
}
223+
}

0 commit comments

Comments
 (0)