blob: 8028f818588930ecd61bb9bd433ce6093bd2129d [file] [log] [blame]
// 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
}