// 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 }