Source file src/cmd/internal/moddeps/moddeps_test.go

     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  	"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.)
    35  func 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  					// Explicitly clear GOROOT_FINAL so that GOROOT=gorootCopyDir is definitely used.
   199  					"GOROOT_FINAL=",
   200  					// Add GOROOTcopy/bin and bundleDir to front of PATH.
   201  					"PATH="+filepath.Join(gorootCopyDir, "bin")+string(filepath.ListSeparator)+
   202  						bundleDir+string(filepath.ListSeparator)+os.Getenv("PATH"),
   203  					"GOWORK=off",
   204  				),
   205  			}
   206  			goBinCopy := filepath.Join(gorootCopyDir, "bin", "go")
   207  			r.run(t, goBinCopy, "mod", "tidy")   // See issue 43687.
   208  			r.run(t, goBinCopy, "mod", "verify") // Verify should be a no-op, but test it just in case.
   209  			r.run(t, goBinCopy, "mod", "vendor") // See issue 36852.
   210  			pkgs := packagePattern(m.Path)
   211  			r.run(t, goBinCopy, "generate", `-run=^//go:generate bundle `, pkgs) // See issue 41409.
   212  			advice := "$ cd " + m.Dir + "\n" +
   213  				"$ go mod tidy                               # to remove extraneous dependencies\n" +
   214  				"$ go mod vendor                             # to vendor dependencies\n" +
   215  				"$ go generate -run=bundle " + pkgs + "               # to regenerate bundled packages\n"
   216  			if m.Path == "std" {
   217  				r.run(t, goBinCopy, "generate", "syscall", "internal/syscall/...") // See issue 43440.
   218  				advice += "$ go generate syscall internal/syscall/...  # to regenerate syscall packages\n"
   219  			}
   220  			// TODO(golang.org/issue/43440): Check anything else influenced by dependency versions.
   221  
   222  			diff, err := testenv.Command(t, "diff", "--recursive", "--unified", r.Dir, m.Dir).CombinedOutput()
   223  			if err != nil || len(diff) != 0 {
   224  				t.Errorf(`Module %s in %s is not tidy (-want +got):
   225  
   226  %s
   227  To fix it, run:
   228  
   229  %s
   230  (If module %[1]s is definitely tidy, this could mean
   231  there's a problem in the go or bundle command.)`, m.Path, m.Dir, diff, advice)
   232  			}
   233  		})
   234  	}
   235  }
   236  
   237  // packagePattern returns a package pattern that matches all packages
   238  // in the module modulePath, and ideally as few others as possible.
   239  func packagePattern(modulePath string) string {
   240  	if modulePath == "std" {
   241  		return "std"
   242  	}
   243  	return modulePath + "/..."
   244  }
   245  
   246  // makeGOROOTCopy makes a temporary copy of the current GOROOT tree.
   247  // The goal is to allow the calling test t to safely mutate a GOROOT
   248  // copy without also modifying the original GOROOT.
   249  //
   250  // It copies the entire tree as is, with the exception of the GOROOT/.git
   251  // directory, which is skipped, and the GOROOT/{bin,pkg} directories,
   252  // which are symlinked. This is done for speed, since a GOROOT tree is
   253  // functional without being in a Git repository, and bin and pkg are
   254  // deemed safe to share for the purpose of the TestAllDependencies test.
   255  func makeGOROOTCopy(t *testing.T) string {
   256  	t.Helper()
   257  
   258  	gorootCopyDir := t.TempDir()
   259  	err := filepath.Walk(testenv.GOROOT(t), func(src string, info os.FileInfo, err error) error {
   260  		if err != nil {
   261  			return err
   262  		}
   263  		if info.IsDir() && src == filepath.Join(testenv.GOROOT(t), ".git") {
   264  			return filepath.SkipDir
   265  		}
   266  
   267  		rel, err := filepath.Rel(testenv.GOROOT(t), src)
   268  		if err != nil {
   269  			return fmt.Errorf("filepath.Rel(%q, %q): %v", testenv.GOROOT(t), src, err)
   270  		}
   271  		dst := filepath.Join(gorootCopyDir, rel)
   272  
   273  		if info.IsDir() && (src == filepath.Join(testenv.GOROOT(t), "bin") ||
   274  			src == filepath.Join(testenv.GOROOT(t), "pkg")) {
   275  			// If the OS supports symlinks, use them instead
   276  			// of copying the bin and pkg directories.
   277  			if err := os.Symlink(src, dst); err == nil {
   278  				return filepath.SkipDir
   279  			}
   280  		}
   281  
   282  		perm := info.Mode() & os.ModePerm
   283  		if info.Mode()&os.ModeSymlink != 0 {
   284  			info, err = os.Stat(src)
   285  			if err != nil {
   286  				return err
   287  			}
   288  			perm = info.Mode() & os.ModePerm
   289  		}
   290  
   291  		// If it's a directory, make a corresponding directory.
   292  		if info.IsDir() {
   293  			return os.MkdirAll(dst, perm|0200)
   294  		}
   295  
   296  		// Copy the file bytes.
   297  		// We can't create a symlink because the file may get modified;
   298  		// we need to ensure that only the temporary copy is affected.
   299  		s, err := os.Open(src)
   300  		if err != nil {
   301  			return err
   302  		}
   303  		defer s.Close()
   304  		d, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_EXCL, perm)
   305  		if err != nil {
   306  			return err
   307  		}
   308  		_, err = io.Copy(d, s)
   309  		if err != nil {
   310  			d.Close()
   311  			return err
   312  		}
   313  		return d.Close()
   314  	})
   315  	if err != nil {
   316  		t.Fatal(err)
   317  	}
   318  	t.Logf("copied GOROOT from %s to %s", testenv.GOROOT(t), gorootCopyDir)
   319  	return gorootCopyDir
   320  }
   321  
   322  type runner struct {
   323  	Dir string
   324  	Env []string
   325  }
   326  
   327  // run runs the command and requires that it succeeds.
   328  func (r runner) run(t *testing.T, args ...string) {
   329  	t.Helper()
   330  	cmd := testenv.Command(t, args[0], args[1:]...)
   331  	cmd.Dir = r.Dir
   332  	cmd.Env = slices.Clip(r.Env)
   333  	if r.Dir != "" {
   334  		cmd.Env = append(cmd.Env, "PWD="+r.Dir)
   335  	}
   336  	out, err := cmd.CombinedOutput()
   337  	if err != nil {
   338  		t.Logf("> %s\n", strings.Join(args, " "))
   339  		t.Fatalf("command failed: %s\n%s", err, out)
   340  	}
   341  }
   342  
   343  // TestDependencyVersionsConsistent verifies that each module in GOROOT that
   344  // requires a given external dependency requires the same version of that
   345  // dependency.
   346  //
   347  // This property allows us to maintain a single release branch of each such
   348  // dependency, minimizing the number of backports needed to pull in critical
   349  // fixes. It also ensures that any bug detected and fixed in one GOROOT module
   350  // (such as "std") is fixed in all other modules (such as "cmd") as well.
   351  func TestDependencyVersionsConsistent(t *testing.T) {
   352  	// Collect the dependencies of all modules in GOROOT, indexed by module path.
   353  	type requirement struct {
   354  		Required    module.Version
   355  		Replacement module.Version
   356  	}
   357  	seen := map[string]map[requirement][]gorootModule{} // module path → requirement → set of modules with that requirement
   358  	for _, m := range findGorootModules(t) {
   359  		if !m.hasVendor {
   360  			// TestAllDependencies will ensure that the module has no dependencies.
   361  			continue
   362  		}
   363  
   364  		// We want this test to be able to run offline and with an empty module
   365  		// cache, so we verify consistency only for the module versions listed in
   366  		// vendor/modules.txt. That includes all direct dependencies and all modules
   367  		// that provide any imported packages.
   368  		//
   369  		// It's ok if there are undetected differences in modules that do not
   370  		// provide imported packages: we will not have to pull in any backports of
   371  		// fixes to those modules anyway.
   372  		vendor, err := os.ReadFile(filepath.Join(m.Dir, "vendor", "modules.txt"))
   373  		if err != nil {
   374  			t.Error(err)
   375  			continue
   376  		}
   377  
   378  		for _, line := range strings.Split(strings.TrimSpace(string(vendor)), "\n") {
   379  			parts := strings.Fields(line)
   380  			if len(parts) < 3 || parts[0] != "#" {
   381  				continue
   382  			}
   383  
   384  			// This line is of the form "# module version [=> replacement [version]]".
   385  			var r requirement
   386  			r.Required.Path = parts[1]
   387  			r.Required.Version = parts[2]
   388  			if len(parts) >= 5 && parts[3] == "=>" {
   389  				r.Replacement.Path = parts[4]
   390  				if module.CheckPath(r.Replacement.Path) != nil {
   391  					// If the replacement is a filesystem path (rather than a module path),
   392  					// we don't know whether the filesystem contents have changed since
   393  					// the module was last vendored.
   394  					//
   395  					// Fortunately, we do not currently use filesystem-local replacements
   396  					// in GOROOT modules.
   397  					t.Errorf("cannot check consistency for filesystem-local replacement in module %s (%s):\n%s", m.Path, m.Dir, line)
   398  				}
   399  
   400  				if len(parts) >= 6 {
   401  					r.Replacement.Version = parts[5]
   402  				}
   403  			}
   404  
   405  			if seen[r.Required.Path] == nil {
   406  				seen[r.Required.Path] = make(map[requirement][]gorootModule)
   407  			}
   408  			seen[r.Required.Path][r] = append(seen[r.Required.Path][r], m)
   409  		}
   410  	}
   411  
   412  	// Now verify that we saw only one distinct version for each module.
   413  	for path, versions := range seen {
   414  		if len(versions) > 1 {
   415  			t.Errorf("Modules within GOROOT require different versions of %s.", path)
   416  			for r, mods := range versions {
   417  				desc := new(strings.Builder)
   418  				desc.WriteString(r.Required.Version)
   419  				if r.Replacement.Path != "" {
   420  					fmt.Fprintf(desc, " => %s", r.Replacement.Path)
   421  					if r.Replacement.Version != "" {
   422  						fmt.Fprintf(desc, " %s", r.Replacement.Version)
   423  					}
   424  				}
   425  
   426  				for _, m := range mods {
   427  					t.Logf("%s\trequires %v", m.Path, desc)
   428  				}
   429  			}
   430  		}
   431  	}
   432  }
   433  
   434  type gorootModule struct {
   435  	Path      string
   436  	Dir       string
   437  	hasVendor bool
   438  }
   439  
   440  // findGorootModules returns the list of modules found in the GOROOT source tree.
   441  func findGorootModules(t *testing.T) []gorootModule {
   442  	t.Helper()
   443  	goBin := testenv.GoToolPath(t)
   444  
   445  	goroot.once.Do(func() {
   446  		// If the root itself is a symlink to a directory,
   447  		// we want to follow it (see https://go.dev/issue/64375).
   448  		// Add a trailing separator to force that to happen.
   449  		root := testenv.GOROOT(t)
   450  		if !os.IsPathSeparator(root[len(root)-1]) {
   451  			root += string(filepath.Separator)
   452  		}
   453  		goroot.err = filepath.WalkDir(root, func(path string, info fs.DirEntry, err error) error {
   454  			if err != nil {
   455  				return err
   456  			}
   457  			if info.IsDir() && (info.Name() == "vendor" || info.Name() == "testdata") {
   458  				return filepath.SkipDir
   459  			}
   460  			if info.IsDir() && path == filepath.Join(testenv.GOROOT(t), "pkg") {
   461  				// GOROOT/pkg contains generated artifacts, not source code.
   462  				//
   463  				// In https://golang.org/issue/37929 it was observed to somehow contain
   464  				// a module cache, so it is important to skip. (That helps with the
   465  				// running time of this test anyway.)
   466  				return filepath.SkipDir
   467  			}
   468  			if info.IsDir() && (strings.HasPrefix(info.Name(), "_") || strings.HasPrefix(info.Name(), ".")) {
   469  				// _ and . prefixed directories can be used for internal modules
   470  				// without a vendor directory that don't contribute to the build
   471  				// but might be used for example as code generators.
   472  				return filepath.SkipDir
   473  			}
   474  			if info.IsDir() || info.Name() != "go.mod" {
   475  				return nil
   476  			}
   477  			dir := filepath.Dir(path)
   478  
   479  			// Use 'go list' to describe the module contained in this directory (but
   480  			// not its dependencies).
   481  			cmd := testenv.Command(t, goBin, "list", "-json", "-m")
   482  			cmd.Dir = dir
   483  			cmd.Env = append(cmd.Environ(), "GO111MODULE=on", "GOWORK=off")
   484  			cmd.Stderr = new(strings.Builder)
   485  			out, err := cmd.Output()
   486  			if err != nil {
   487  				return fmt.Errorf("'go list -json -m' in %s: %w\n%s", dir, err, cmd.Stderr)
   488  			}
   489  
   490  			var m gorootModule
   491  			if err := json.Unmarshal(out, &m); err != nil {
   492  				return fmt.Errorf("decoding 'go list -json -m' in %s: %w", dir, err)
   493  			}
   494  			if m.Path == "" || m.Dir == "" {
   495  				return fmt.Errorf("'go list -json -m' in %s failed to populate Path and/or Dir", dir)
   496  			}
   497  			if _, err := os.Stat(filepath.Join(dir, "vendor")); err == nil {
   498  				m.hasVendor = true
   499  			}
   500  			goroot.modules = append(goroot.modules, m)
   501  			return nil
   502  		})
   503  		if goroot.err != nil {
   504  			return
   505  		}
   506  
   507  		// knownGOROOTModules is a hard-coded list of modules that are known to exist in GOROOT.
   508  		// If findGorootModules doesn't find a module, it won't be covered by tests at all,
   509  		// so make sure at least these modules are found. See issue 46254. If this list
   510  		// becomes a nuisance to update, can be replaced with len(goroot.modules) check.
   511  		knownGOROOTModules := [...]string{
   512  			"std",
   513  			"cmd",
   514  			// The "misc" module sometimes exists, but cmd/distpack intentionally removes it.
   515  		}
   516  		var seen = make(map[string]bool) // Key is module path.
   517  		for _, m := range goroot.modules {
   518  			seen[m.Path] = true
   519  		}
   520  		for _, m := range knownGOROOTModules {
   521  			if !seen[m] {
   522  				goroot.err = fmt.Errorf("findGorootModules didn't find the well-known module %q", m)
   523  				break
   524  			}
   525  		}
   526  		sort.Slice(goroot.modules, func(i, j int) bool {
   527  			return goroot.modules[i].Dir < goroot.modules[j].Dir
   528  		})
   529  	})
   530  	if goroot.err != nil {
   531  		t.Fatal(goroot.err)
   532  	}
   533  	return goroot.modules
   534  }
   535  
   536  // goroot caches the list of modules found in the GOROOT source tree.
   537  var goroot struct {
   538  	once    sync.Once
   539  	modules []gorootModule
   540  	err     error
   541  }
   542  

View as plain text