Black Lives Matter. Support the Equal Justice Initiative.

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

Documentation: cmd/internal/moddeps

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

View as plain text