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  					// 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
   225  To fix it, run:
   226  
   227  %s
   228  (If module %[1]s is definitely tidy, this could mean
   229  there'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.
   237  func 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.
   253  func 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  
   320  type runner struct {
   321  	Dir string
   322  	Env []string
   323  }
   324  
   325  // run runs the command and requires that it succeeds.
   326  func (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.
   349  func 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  
   432  type 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.
   439  func 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.
   535  var goroot struct {
   536  	once    sync.Once
   537  	modules []gorootModule
   538  	err     error
   539  }
   540  

View as plain text