1
2
3
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
26
27
28
29
30
31
32
33
34
35 func TestAllDependencies(t *testing.T) {
36 goBin := testenv.GoToolPath(t)
37
38
39
40
41
42
43
44
45
46
47
48
49 for _, m := range findGorootModules(t) {
50
51
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
57
58
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
72
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
89
90 if testing.Short() {
91 return
92 }
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
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
122
123
124
125 t.Skip("skipping because a diff command with support for --recursive and --unified flags is unavailable")
126 }
127
128
129
130
131
132
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",
149 }
150 }
151 }
152
153
154
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
165
166
167
168
169
170
171
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
182
183
184
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
197 "GOROOT="+gorootCopyDir,
198
199 "GOROOT_FINAL=",
200
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")
208 r.run(t, goBinCopy, "mod", "verify")
209 r.run(t, goBinCopy, "mod", "vendor")
210 pkgs := packagePattern(m.Path)
211 r.run(t, goBinCopy, "generate", `-run=^//go:generate bundle `, pkgs)
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/...")
218 advice += "$ go generate syscall internal/syscall/... # to regenerate syscall packages\n"
219 }
220
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
238
239 func packagePattern(modulePath string) string {
240 if modulePath == "std" {
241 return "std"
242 }
243 return modulePath + "/..."
244 }
245
246
247
248
249
250
251
252
253
254
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
276
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
292 if info.IsDir() {
293 return os.MkdirAll(dst, perm|0200)
294 }
295
296
297
298
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
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
344
345
346
347
348
349
350
351 func TestDependencyVersionsConsistent(t *testing.T) {
352
353 type requirement struct {
354 Required module.Version
355 Replacement module.Version
356 }
357 seen := map[string]map[requirement][]gorootModule{}
358 for _, m := range findGorootModules(t) {
359 if !m.hasVendor {
360
361 continue
362 }
363
364
365
366
367
368
369
370
371
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
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
392
393
394
395
396
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
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
441 func findGorootModules(t *testing.T) []gorootModule {
442 t.Helper()
443 goBin := testenv.GoToolPath(t)
444
445 goroot.once.Do(func() {
446
447
448
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
462
463
464
465
466 return filepath.SkipDir
467 }
468 if info.IsDir() && (strings.HasPrefix(info.Name(), "_") || strings.HasPrefix(info.Name(), ".")) {
469
470
471
472 return filepath.SkipDir
473 }
474 if info.IsDir() || info.Name() != "go.mod" {
475 return nil
476 }
477 dir := filepath.Dir(path)
478
479
480
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
508
509
510
511 knownGOROOTModules := [...]string{
512 "std",
513 "cmd",
514
515 }
516 var seen = make(map[string]bool)
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
537 var goroot struct {
538 once sync.Once
539 modules []gorootModule
540 err error
541 }
542
View as plain text