|
| 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