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 5package moddeps_test 6 7import ( 8 "bytes" 9 "encoding/json" 10 "fmt" 11 "internal/testenv" 12 "io" 13 "io/fs" 14 "os" 15 "path/filepath" 16 "slices" 17 "sort" 18 "strings" 19 "sync" 20 "testing" 21 22 "golang.org/x/mod/module" 23) 24 25// TestAllDependencies ensures dependencies of all 26// modules in GOROOT are in a consistent state. 27// 28// In short mode, it does a limited quick check and stops there. 29// In long mode, it also makes a copy of the entire GOROOT tree 30// and requires network access to perform more thorough checks. 31// Keep this distinction in mind when adding new checks. 32// 33// See issues 36852, 41409, and 43687. 34// (Also see golang.org/issue/27348.) 35func TestAllDependencies(t *testing.T) { 36 goBin := testenv.GoToolPath(t) 37 38 // Ensure that all packages imported within GOROOT 39 // are vendored in the corresponding GOROOT module. 40 // 41 // This property allows offline development within the Go project, and ensures 42 // that all dependency changes are presented in the usual code review process. 43 // 44 // As a quick first-order check, avoid network access and the need to copy the 45 // entire GOROOT tree or explicitly invoke version control to check for changes. 46 // Just check that packages are vendored. (In non-short mode, we go on to also 47 // copy the GOROOT tree and perform more rigorous consistency checks. Jump below 48 // for more details.) 49 for _, m := range findGorootModules(t) { 50 // This short test does NOT ensure that the vendored contents match 51 // the unmodified contents of the corresponding dependency versions. 52 t.Run(m.Path+"(quick)", func(t *testing.T) { 53 t.Logf("module %s in directory %s", m.Path, m.Dir) 54 55 if m.hasVendor { 56 // Load all of the packages in the module to ensure that their 57 // dependencies are vendored. If any imported package is missing, 58 // 'go list -deps' will fail when attempting to load it. 59 cmd := testenv.Command(t, goBin, "list", "-mod=vendor", "-deps", "./...") 60 cmd.Dir = m.Dir 61 cmd.Env = append(cmd.Environ(), "GO111MODULE=on", "GOWORK=off") 62 cmd.Stderr = new(strings.Builder) 63 _, err := cmd.Output() 64 if err != nil { 65 t.Errorf("%s: %v\n%s", strings.Join(cmd.Args, " "), err, cmd.Stderr) 66 t.Logf("(Run 'go mod vendor' in %s to ensure that dependencies have been vendored.)", m.Dir) 67 } 68 return 69 } 70 71 // There is no vendor directory, so the module must have no dependencies. 72 // Check that the list of active modules contains only the main module. 73 cmd := testenv.Command(t, goBin, "list", "-mod=readonly", "-m", "all") 74 cmd.Dir = m.Dir 75 cmd.Env = append(cmd.Environ(), "GO111MODULE=on", "GOWORK=off") 76 cmd.Stderr = new(strings.Builder) 77 out, err := cmd.Output() 78 if err != nil { 79 t.Fatalf("%s: %v\n%s", strings.Join(cmd.Args, " "), err, cmd.Stderr) 80 } 81 if strings.TrimSpace(string(out)) != m.Path { 82 t.Errorf("'%s' reported active modules other than %s:\n%s", strings.Join(cmd.Args, " "), m.Path, out) 83 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) 84 } 85 }) 86 } 87 88 // We now get to the slow, but more thorough part of the test. 89 // Only run it in long test mode. 90 if testing.Short() { 91 return 92 } 93 94 // Ensure that all modules within GOROOT are tidy, vendored, and bundled. 95 // Ensure that the vendored contents match the unmodified contents of the 96 // corresponding dependency versions. 97 // 98 // The non-short section of this test requires network access and the diff 99 // command. 100 // 101 // It makes a temporary copy of the entire GOROOT tree (where it can safely 102 // perform operations that may mutate the tree), executes the same module 103 // maintenance commands that we expect Go developers to run, and then 104 // diffs the potentially modified module copy with the real one in GOROOT. 105 // (We could try to rely on Git to do things differently, but that's not the 106 // path we've chosen at this time. This allows the test to run when the tree 107 // is not checked into Git.) 108 109 testenv.MustHaveExternalNetwork(t) 110 if haveDiff := func() bool { 111 diff, err := testenv.Command(t, "diff", "--recursive", "--unified", ".", ".").CombinedOutput() 112 if err != nil || len(diff) != 0 { 113 return false 114 } 115 diff, err = testenv.Command(t, "diff", "--recursive", "--unified", ".", "..").CombinedOutput() 116 if err == nil || len(diff) == 0 { 117 return false 118 } 119 return true 120 }(); !haveDiff { 121 // For now, the diff command is a mandatory dependency of this test. 122 // This test will primarily run on longtest builders, since few people 123 // would test the cmd/internal/moddeps package directly, and all.bash 124 // runs tests in short mode. It's fine to skip if diff is unavailable. 125 t.Skip("skipping because a diff command with support for --recursive and --unified flags is unavailable") 126 } 127 128 // We're going to check the standard modules for tidiness, so we need a usable 129 // GOMODCACHE. If the default directory doesn't exist, use a temporary 130 // directory instead. (That can occur, for example, when running under 131 // run.bash with GO_TEST_SHORT=0: run.bash sets GOPATH=/nonexist-gopath, and 132 // GO_TEST_SHORT=0 causes it to run this portion of the test.) 133 var modcacheEnv []string 134 { 135 out, err := testenv.Command(t, goBin, "env", "GOMODCACHE").Output() 136 if err != nil { 137 t.Fatalf("%s env GOMODCACHE: %v", goBin, err) 138 } 139 modcacheOk := false 140 if gomodcache := string(bytes.TrimSpace(out)); gomodcache != "" { 141 if _, err := os.Stat(gomodcache); err == nil { 142 modcacheOk = true 143 } 144 } 145 if !modcacheOk { 146 modcacheEnv = []string{ 147 "GOMODCACHE=" + t.TempDir(), 148 "GOFLAGS=" + os.Getenv("GOFLAGS") + " -modcacherw", // Allow t.TempDir() to clean up subdirectories. 149 } 150 } 151 } 152 153 // Build the bundle binary at the golang.org/x/tools 154 // module version specified in GOROOT/src/cmd/go.mod. 155 bundleDir := t.TempDir() 156 r := runner{ 157 Dir: filepath.Join(testenv.GOROOT(t), "src/cmd"), 158 Env: append(os.Environ(), modcacheEnv...), 159 } 160 r.run(t, goBin, "build", "-mod=readonly", "-o", bundleDir, "golang.org/x/tools/cmd/bundle") 161 162 var gorootCopyDir string 163 for _, m := range findGorootModules(t) { 164 // Create a test-wide GOROOT copy. It can be created once 165 // and reused between subtests whenever they don't fail. 166 // 167 // This is a relatively expensive operation, but it's a pre-requisite to 168 // be able to safely run commands like "go mod tidy", "go mod vendor", and 169 // "go generate" on the GOROOT tree content. Those commands may modify the 170 // tree, and we don't want to happen to the real tree as part of executing 171 // a test. 172 if gorootCopyDir == "" { 173 gorootCopyDir = makeGOROOTCopy(t) 174 } 175 176 t.Run(m.Path+"(thorough)", func(t *testing.T) { 177 t.Logf("module %s in directory %s", m.Path, m.Dir) 178 179 defer func() { 180 if t.Failed() { 181 // The test failed, which means it's possible the GOROOT copy 182 // may have been modified. No choice but to reset it for next 183 // module test case. (This is slow, but it happens only during 184 // test failures.) 185 gorootCopyDir = "" 186 } 187 }() 188 189 rel, err := filepath.Rel(testenv.GOROOT(t), m.Dir) 190 if err != nil { 191 t.Fatalf("filepath.Rel(%q, %q): %v", testenv.GOROOT(t), m.Dir, err) 192 } 193 r := runner{ 194 Dir: filepath.Join(gorootCopyDir, rel), 195 Env: append(append(os.Environ(), modcacheEnv...), 196 // Set GOROOT. 197 "GOROOT="+gorootCopyDir, 198 // Add GOROOTcopy/bin and bundleDir to front of PATH. 199 "PATH="+filepath.Join(gorootCopyDir, "bin")+string(filepath.ListSeparator)+ 200 bundleDir+string(filepath.ListSeparator)+os.Getenv("PATH"), 201 "GOWORK=off", 202 ), 203 } 204 goBinCopy := filepath.Join(gorootCopyDir, "bin", "go") 205 r.run(t, goBinCopy, "mod", "tidy") // See issue 43687. 206 r.run(t, goBinCopy, "mod", "verify") // Verify should be a no-op, but test it just in case. 207 r.run(t, goBinCopy, "mod", "vendor") // See issue 36852. 208 pkgs := packagePattern(m.Path) 209 r.run(t, goBinCopy, "generate", `-run=^//go:generate bundle `, pkgs) // See issue 41409. 210 advice := "$ cd " + m.Dir + "\n" + 211 "$ go mod tidy # to remove extraneous dependencies\n" + 212 "$ go mod vendor # to vendor dependencies\n" + 213 "$ go generate -run=bundle " + pkgs + " # to regenerate bundled packages\n" 214 if m.Path == "std" { 215 r.run(t, goBinCopy, "generate", "syscall", "internal/syscall/...") // See issue 43440. 216 advice += "$ go generate syscall internal/syscall/... # to regenerate syscall packages\n" 217 } 218 // TODO(golang.org/issue/43440): Check anything else influenced by dependency versions. 219 220 diff, err := testenv.Command(t, "diff", "--recursive", "--unified", r.Dir, m.Dir).CombinedOutput() 221 if err != nil || len(diff) != 0 { 222 t.Errorf(`Module %s in %s is not tidy (-want +got): 223 224%s 225To fix it, run: 226 227%s 228(If module %[1]s is definitely tidy, this could mean 229there's a problem in the go or bundle command.)`, m.Path, m.Dir, diff, advice) 230 } 231 }) 232 } 233} 234 235// packagePattern returns a package pattern that matches all packages 236// in the module modulePath, and ideally as few others as possible. 237func packagePattern(modulePath string) string { 238 if modulePath == "std" { 239 return "std" 240 } 241 return modulePath + "/..." 242} 243 244// makeGOROOTCopy makes a temporary copy of the current GOROOT tree. 245// The goal is to allow the calling test t to safely mutate a GOROOT 246// copy without also modifying the original GOROOT. 247// 248// It copies the entire tree as is, with the exception of the GOROOT/.git 249// directory, which is skipped, and the GOROOT/{bin,pkg} directories, 250// which are symlinked. This is done for speed, since a GOROOT tree is 251// functional without being in a Git repository, and bin and pkg are 252// deemed safe to share for the purpose of the TestAllDependencies test. 253func makeGOROOTCopy(t *testing.T) string { 254 t.Helper() 255 256 gorootCopyDir := t.TempDir() 257 err := filepath.Walk(testenv.GOROOT(t), func(src string, info os.FileInfo, err error) error { 258 if err != nil { 259 return err 260 } 261 if info.IsDir() && src == filepath.Join(testenv.GOROOT(t), ".git") { 262 return filepath.SkipDir 263 } 264 265 rel, err := filepath.Rel(testenv.GOROOT(t), src) 266 if err != nil { 267 return fmt.Errorf("filepath.Rel(%q, %q): %v", testenv.GOROOT(t), src, err) 268 } 269 dst := filepath.Join(gorootCopyDir, rel) 270 271 if info.IsDir() && (src == filepath.Join(testenv.GOROOT(t), "bin") || 272 src == filepath.Join(testenv.GOROOT(t), "pkg")) { 273 // If the OS supports symlinks, use them instead 274 // of copying the bin and pkg directories. 275 if err := os.Symlink(src, dst); err == nil { 276 return filepath.SkipDir 277 } 278 } 279 280 perm := info.Mode() & os.ModePerm 281 if info.Mode()&os.ModeSymlink != 0 { 282 info, err = os.Stat(src) 283 if err != nil { 284 return err 285 } 286 perm = info.Mode() & os.ModePerm 287 } 288 289 // If it's a directory, make a corresponding directory. 290 if info.IsDir() { 291 return os.MkdirAll(dst, perm|0200) 292 } 293 294 // Copy the file bytes. 295 // We can't create a symlink because the file may get modified; 296 // we need to ensure that only the temporary copy is affected. 297 s, err := os.Open(src) 298 if err != nil { 299 return err 300 } 301 defer s.Close() 302 d, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_EXCL, perm) 303 if err != nil { 304 return err 305 } 306 _, err = io.Copy(d, s) 307 if err != nil { 308 d.Close() 309 return err 310 } 311 return d.Close() 312 }) 313 if err != nil { 314 t.Fatal(err) 315 } 316 t.Logf("copied GOROOT from %s to %s", testenv.GOROOT(t), gorootCopyDir) 317 return gorootCopyDir 318} 319 320type runner struct { 321 Dir string 322 Env []string 323} 324 325// run runs the command and requires that it succeeds. 326func (r runner) run(t *testing.T, args ...string) { 327 t.Helper() 328 cmd := testenv.Command(t, args[0], args[1:]...) 329 cmd.Dir = r.Dir 330 cmd.Env = slices.Clip(r.Env) 331 if r.Dir != "" { 332 cmd.Env = append(cmd.Env, "PWD="+r.Dir) 333 } 334 out, err := cmd.CombinedOutput() 335 if err != nil { 336 t.Logf("> %s\n", strings.Join(args, " ")) 337 t.Fatalf("command failed: %s\n%s", err, out) 338 } 339} 340 341// TestDependencyVersionsConsistent verifies that each module in GOROOT that 342// requires a given external dependency requires the same version of that 343// dependency. 344// 345// This property allows us to maintain a single release branch of each such 346// dependency, minimizing the number of backports needed to pull in critical 347// fixes. It also ensures that any bug detected and fixed in one GOROOT module 348// (such as "std") is fixed in all other modules (such as "cmd") as well. 349func TestDependencyVersionsConsistent(t *testing.T) { 350 // Collect the dependencies of all modules in GOROOT, indexed by module path. 351 type requirement struct { 352 Required module.Version 353 Replacement module.Version 354 } 355 seen := map[string]map[requirement][]gorootModule{} // module path → requirement → set of modules with that requirement 356 for _, m := range findGorootModules(t) { 357 if !m.hasVendor { 358 // TestAllDependencies will ensure that the module has no dependencies. 359 continue 360 } 361 362 // We want this test to be able to run offline and with an empty module 363 // cache, so we verify consistency only for the module versions listed in 364 // vendor/modules.txt. That includes all direct dependencies and all modules 365 // that provide any imported packages. 366 // 367 // It's ok if there are undetected differences in modules that do not 368 // provide imported packages: we will not have to pull in any backports of 369 // fixes to those modules anyway. 370 vendor, err := os.ReadFile(filepath.Join(m.Dir, "vendor", "modules.txt")) 371 if err != nil { 372 t.Error(err) 373 continue 374 } 375 376 for _, line := range strings.Split(strings.TrimSpace(string(vendor)), "\n") { 377 parts := strings.Fields(line) 378 if len(parts) < 3 || parts[0] != "#" { 379 continue 380 } 381 382 // This line is of the form "# module version [=> replacement [version]]". 383 var r requirement 384 r.Required.Path = parts[1] 385 r.Required.Version = parts[2] 386 if len(parts) >= 5 && parts[3] == "=>" { 387 r.Replacement.Path = parts[4] 388 if module.CheckPath(r.Replacement.Path) != nil { 389 // If the replacement is a filesystem path (rather than a module path), 390 // we don't know whether the filesystem contents have changed since 391 // the module was last vendored. 392 // 393 // Fortunately, we do not currently use filesystem-local replacements 394 // in GOROOT modules. 395 t.Errorf("cannot check consistency for filesystem-local replacement in module %s (%s):\n%s", m.Path, m.Dir, line) 396 } 397 398 if len(parts) >= 6 { 399 r.Replacement.Version = parts[5] 400 } 401 } 402 403 if seen[r.Required.Path] == nil { 404 seen[r.Required.Path] = make(map[requirement][]gorootModule) 405 } 406 seen[r.Required.Path][r] = append(seen[r.Required.Path][r], m) 407 } 408 } 409 410 // Now verify that we saw only one distinct version for each module. 411 for path, versions := range seen { 412 if len(versions) > 1 { 413 t.Errorf("Modules within GOROOT require different versions of %s.", path) 414 for r, mods := range versions { 415 desc := new(strings.Builder) 416 desc.WriteString(r.Required.Version) 417 if r.Replacement.Path != "" { 418 fmt.Fprintf(desc, " => %s", r.Replacement.Path) 419 if r.Replacement.Version != "" { 420 fmt.Fprintf(desc, " %s", r.Replacement.Version) 421 } 422 } 423 424 for _, m := range mods { 425 t.Logf("%s\trequires %v", m.Path, desc) 426 } 427 } 428 } 429 } 430} 431 432type gorootModule struct { 433 Path string 434 Dir string 435 hasVendor bool 436} 437 438// findGorootModules returns the list of modules found in the GOROOT source tree. 439func findGorootModules(t *testing.T) []gorootModule { 440 t.Helper() 441 goBin := testenv.GoToolPath(t) 442 443 goroot.once.Do(func() { 444 // If the root itself is a symlink to a directory, 445 // we want to follow it (see https://go.dev/issue/64375). 446 // Add a trailing separator to force that to happen. 447 root := testenv.GOROOT(t) 448 if !os.IsPathSeparator(root[len(root)-1]) { 449 root += string(filepath.Separator) 450 } 451 goroot.err = filepath.WalkDir(root, func(path string, info fs.DirEntry, err error) error { 452 if err != nil { 453 return err 454 } 455 if info.IsDir() && path != root && (info.Name() == "vendor" || info.Name() == "testdata") { 456 return filepath.SkipDir 457 } 458 if info.IsDir() && path == filepath.Join(testenv.GOROOT(t), "pkg") { 459 // GOROOT/pkg contains generated artifacts, not source code. 460 // 461 // In https://golang.org/issue/37929 it was observed to somehow contain 462 // a module cache, so it is important to skip. (That helps with the 463 // running time of this test anyway.) 464 return filepath.SkipDir 465 } 466 if info.IsDir() && path != root && (strings.HasPrefix(info.Name(), "_") || strings.HasPrefix(info.Name(), ".")) { 467 // _ and . prefixed directories can be used for internal modules 468 // without a vendor directory that don't contribute to the build 469 // but might be used for example as code generators. 470 return filepath.SkipDir 471 } 472 if info.IsDir() || info.Name() != "go.mod" { 473 return nil 474 } 475 dir := filepath.Dir(path) 476 477 // Use 'go list' to describe the module contained in this directory (but 478 // not its dependencies). 479 cmd := testenv.Command(t, goBin, "list", "-json", "-m") 480 cmd.Dir = dir 481 cmd.Env = append(cmd.Environ(), "GO111MODULE=on", "GOWORK=off") 482 cmd.Stderr = new(strings.Builder) 483 out, err := cmd.Output() 484 if err != nil { 485 return fmt.Errorf("'go list -json -m' in %s: %w\n%s", dir, err, cmd.Stderr) 486 } 487 488 var m gorootModule 489 if err := json.Unmarshal(out, &m); err != nil { 490 return fmt.Errorf("decoding 'go list -json -m' in %s: %w", dir, err) 491 } 492 if m.Path == "" || m.Dir == "" { 493 return fmt.Errorf("'go list -json -m' in %s failed to populate Path and/or Dir", dir) 494 } 495 if _, err := os.Stat(filepath.Join(dir, "vendor")); err == nil { 496 m.hasVendor = true 497 } 498 goroot.modules = append(goroot.modules, m) 499 return nil 500 }) 501 if goroot.err != nil { 502 return 503 } 504 505 // knownGOROOTModules is a hard-coded list of modules that are known to exist in GOROOT. 506 // If findGorootModules doesn't find a module, it won't be covered by tests at all, 507 // so make sure at least these modules are found. See issue 46254. If this list 508 // becomes a nuisance to update, can be replaced with len(goroot.modules) check. 509 knownGOROOTModules := [...]string{ 510 "std", 511 "cmd", 512 // The "misc" module sometimes exists, but cmd/distpack intentionally removes it. 513 } 514 var seen = make(map[string]bool) // Key is module path. 515 for _, m := range goroot.modules { 516 seen[m.Path] = true 517 } 518 for _, m := range knownGOROOTModules { 519 if !seen[m] { 520 goroot.err = fmt.Errorf("findGorootModules didn't find the well-known module %q", m) 521 break 522 } 523 } 524 sort.Slice(goroot.modules, func(i, j int) bool { 525 return goroot.modules[i].Dir < goroot.modules[j].Dir 526 }) 527 }) 528 if goroot.err != nil { 529 t.Fatal(goroot.err) 530 } 531 return goroot.modules 532} 533 534// goroot caches the list of modules found in the GOROOT source tree. 535var goroot struct { 536 once sync.Once 537 modules []gorootModule 538 err error 539} 540