| // Copyright 2018 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| package gopathwalk | |
| import ( | |
| "os" | |
| "path/filepath" | |
| "reflect" | |
| "runtime" | |
| "sort" | |
| "strings" | |
| "sync" | |
| "testing" | |
| ) | |
| func TestSymlinkTraversal(t *testing.T) { | |
| t.Parallel() | |
| gopath := t.TempDir() | |
| if err := mapToDir(gopath, map[string]string{ | |
| "a/b/c": "LINK:../../a/d", | |
| "a/b/pkg/pkg.go": "package pkg", | |
| "a/d/e": "LINK:../../a/b", | |
| "a/d/pkg/pkg.go": "package pkg", | |
| "a/f/loop": "LINK:../f", | |
| "a/f/pkg/pkg.go": "package pkg", | |
| "a/g/pkg/pkg.go": "LINK:../../f/pkg/pkg.go", | |
| "a/self": "LINK:.", | |
| }); err != nil { | |
| switch runtime.GOOS { | |
| case "windows", "plan9": | |
| t.Skipf("skipping symlink-requiring test on %s", runtime.GOOS) | |
| } | |
| t.Fatal(err) | |
| } | |
| pkgc := make(chan []string, 1) | |
| pkgc <- nil | |
| add := func(root Root, dir string) { | |
| rel, err := filepath.Rel(filepath.Join(root.Path, "src"), dir) | |
| if err != nil { | |
| t.Error(err) | |
| } | |
| pkgc <- append(<-pkgc, filepath.ToSlash(rel)) | |
| } | |
| Walk([]Root{{Path: gopath, Type: RootGOPATH}}, add, Options{Logf: t.Logf}) | |
| pkgs := <-pkgc | |
| sort.Strings(pkgs) | |
| t.Logf("Found packages:\n\t%s", strings.Join(pkgs, "\n\t")) | |
| got := make(map[string]bool, len(pkgs)) | |
| for _, pkg := range pkgs { | |
| got[pkg] = true | |
| } | |
| tests := []struct { | |
| path string | |
| want bool | |
| why string | |
| }{ | |
| { | |
| path: "a/b/pkg", | |
| want: true, | |
| why: "found via regular directories", | |
| }, | |
| { | |
| path: "a/b/c/pkg", | |
| want: true, | |
| why: "found via non-cyclic dir link", | |
| }, | |
| { | |
| path: "a/b/c/e/pkg", | |
| want: true, | |
| why: "found via two non-cyclic dir links", | |
| }, | |
| { | |
| path: "a/d/e/c/pkg", | |
| want: true, | |
| why: "found via two non-cyclic dir links", | |
| }, | |
| { | |
| path: "a/f/loop/pkg", | |
| want: true, | |
| why: "found via a single parent-dir link", | |
| }, | |
| { | |
| path: "a/f/loop/loop/pkg", | |
| want: false, | |
| why: "would follow loop symlink twice", | |
| }, | |
| { | |
| path: "a/self/b/pkg", | |
| want: true, | |
| why: "follows self-link once", | |
| }, | |
| { | |
| path: "a/self/self/b/pkg", | |
| want: false, | |
| why: "would follow self-link twice", | |
| }, | |
| } | |
| for _, tc := range tests { | |
| if got[tc.path] != tc.want { | |
| if tc.want { | |
| t.Errorf("MISSING: %s (%s)", tc.path, tc.why) | |
| } else { | |
| t.Errorf("UNEXPECTED: %s (%s)", tc.path, tc.why) | |
| } | |
| } | |
| } | |
| } | |
| // TestSkip tests that various goimports rules are followed in non-modules mode. | |
| func TestSkip(t *testing.T) { | |
| t.Parallel() | |
| dir := t.TempDir() | |
| if err := mapToDir(dir, map[string]string{ | |
| "ignoreme/f.go": "package ignoreme", // ignored by .goimportsignore | |
| "node_modules/f.go": "package nodemodules;", // ignored by hardcoded node_modules filter | |
| "v/f.go": "package v;", // ignored by hardcoded vgo cache rule | |
| "mod/f.go": "package mod;", // ignored by hardcoded vgo cache rule | |
| "shouldfind/f.go": "package shouldfind;", // not ignored | |
| ".goimportsignore": "ignoreme\n", | |
| }); err != nil { | |
| t.Fatal(err) | |
| } | |
| var found []string | |
| var mu sync.Mutex | |
| walkDir(Root{filepath.Join(dir, "src"), RootGOPATH}, | |
| func(root Root, dir string) { | |
| mu.Lock() | |
| defer mu.Unlock() | |
| found = append(found, dir[len(root.Path)+1:]) | |
| }, func(root Root, dir string) bool { | |
| return false | |
| }, Options{ | |
| ModulesEnabled: false, | |
| Logf: t.Logf, | |
| }) | |
| if want := []string{"shouldfind"}; !reflect.DeepEqual(found, want) { | |
| t.Errorf("expected to find only %v, got %v", want, found) | |
| } | |
| } | |
| // TestSkipFunction tests that scan successfully skips directories from user callback. | |
| func TestSkipFunction(t *testing.T) { | |
| t.Parallel() | |
| dir := t.TempDir() | |
| if err := mapToDir(dir, map[string]string{ | |
| "ignoreme/f.go": "package ignoreme", // ignored by skip | |
| "ignoreme/subignore/f.go": "package subignore", // also ignored by skip | |
| "shouldfind/f.go": "package shouldfind;", // not ignored | |
| }); err != nil { | |
| t.Fatal(err) | |
| } | |
| var found []string | |
| var mu sync.Mutex | |
| walkDir(Root{filepath.Join(dir, "src"), RootGOPATH}, | |
| func(root Root, dir string) { | |
| mu.Lock() | |
| defer mu.Unlock() | |
| found = append(found, dir[len(root.Path)+1:]) | |
| }, func(root Root, dir string) bool { | |
| return strings.HasSuffix(dir, "ignoreme") | |
| }, | |
| Options{ | |
| ModulesEnabled: false, | |
| Logf: t.Logf, | |
| }) | |
| if want := []string{"shouldfind"}; !reflect.DeepEqual(found, want) { | |
| t.Errorf("expected to find only %v, got %v", want, found) | |
| } | |
| } | |
| // TestWalkSymlinkConcurrentDeletion is a regression test for the panic reported | |
| // in https://go.dev/issue/58054#issuecomment-1791513726. | |
| func TestWalkSymlinkConcurrentDeletion(t *testing.T) { | |
| t.Parallel() | |
| src := t.TempDir() | |
| m := map[string]string{ | |
| "dir/readme.txt": "dir is not a go package", | |
| "dirlink": "LINK:dir", | |
| } | |
| if err := mapToDir(src, m); err != nil { | |
| switch runtime.GOOS { | |
| case "windows", "plan9": | |
| t.Skipf("skipping symlink-requiring test on %s", runtime.GOOS) | |
| } | |
| t.Fatal(err) | |
| } | |
| done := make(chan struct{}) | |
| go func() { | |
| if err := os.RemoveAll(src); err != nil { | |
| t.Log(err) | |
| } | |
| close(done) | |
| }() | |
| defer func() { | |
| <-done | |
| }() | |
| add := func(root Root, dir string) { | |
| t.Errorf("unexpected call to add(%q, %q)", root.Path, dir) | |
| } | |
| Walk([]Root{{Path: src, Type: RootGOPATH}}, add, Options{Logf: t.Logf}) | |
| } | |
| func mapToDir(destDir string, files map[string]string) error { | |
| var symlinkPaths []string | |
| for path, contents := range files { | |
| file := filepath.Join(destDir, "src", path) | |
| if err := os.MkdirAll(filepath.Dir(file), 0755); err != nil { | |
| return err | |
| } | |
| var err error | |
| if strings.HasPrefix(contents, "LINK:") { | |
| // To work around https://go.dev/issue/39183, wait to create symlinks | |
| // until we have created all non-symlink paths. | |
| symlinkPaths = append(symlinkPaths, path) | |
| } else { | |
| err = os.WriteFile(file, []byte(contents), 0644) | |
| } | |
| if err != nil { | |
| return err | |
| } | |
| } | |
| for _, path := range symlinkPaths { | |
| file := filepath.Join(destDir, "src", path) | |
| target := filepath.FromSlash(strings.TrimPrefix(files[path], "LINK:")) | |
| err := os.Symlink(target, file) | |
| if err != nil { | |
| return err | |
| } | |
| } | |
| return nil | |
| } |