internal/fastwalk: attempt Symlink tests on Windows

Windows does actually support symlinks, but older versions of
Windows only support symlinks when running as an administrator.
Newer versions of Windows support symlinks for all users.

Instead of skipping based on GOOS, first try the Symlink operation.
If it succeeds, we can proceed with the test; otherwise, we can try to
write a regular file to determine whether the problem was the symlink
operation itself or the destination path.

For golang/go#38772

Change-Id: Idaa9592011473de7f514b889859e420a84db6d01
Reviewed-on: https://go-review.googlesource.com/c/tools/+/234537
Run-TryBot: Bryan C. Mills <bcmills@google.com>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Michael Matloob <matloob@golang.org>
diff --git a/internal/fastwalk/fastwalk_test.go b/internal/fastwalk/fastwalk_test.go
index a6d9bea..d896aeb 100644
--- a/internal/fastwalk/fastwalk_test.go
+++ b/internal/fastwalk/fastwalk_test.go
@@ -40,6 +40,8 @@
 		t.Fatal(err)
 	}
 	defer os.RemoveAll(tempdir)
+
+	symlinks := map[string]string{}
 	for path, contents := range files {
 		file := filepath.Join(tempdir, "/src", path)
 		if err := os.MkdirAll(filepath.Dir(file), 0755); err != nil {
@@ -47,7 +49,7 @@
 		}
 		var err error
 		if strings.HasPrefix(contents, "LINK:") {
-			err = os.Symlink(strings.TrimPrefix(contents, "LINK:"), file)
+			symlinks[file] = filepath.FromSlash(strings.TrimPrefix(contents, "LINK:"))
 		} else {
 			err = ioutil.WriteFile(file, []byte(contents), 0644)
 		}
@@ -55,21 +57,38 @@
 			t.Fatal(err)
 		}
 	}
+
+	// Create symlinks after all other files. Otherwise, directory symlinks on
+	// Windows are unusable (see https://golang.org/issue/39183).
+	for file, dst := range symlinks {
+		err = os.Symlink(dst, file)
+		if err != nil {
+			if writeErr := ioutil.WriteFile(file, []byte(dst), 0644); writeErr == nil {
+				// Couldn't create symlink, but could write the file.
+				// Probably this filesystem doesn't support symlinks.
+				// (Perhaps we are on an older Windows and not running as administrator.)
+				t.Skipf("skipping because symlinks appear to be unsupported: %v", err)
+			}
+		}
+	}
+
 	got := map[string]os.FileMode{}
 	var mu sync.Mutex
-	if err := fastwalk.Walk(tempdir, func(path string, typ os.FileMode) error {
+	err = fastwalk.Walk(tempdir, func(path string, typ os.FileMode) error {
 		mu.Lock()
 		defer mu.Unlock()
 		if !strings.HasPrefix(path, tempdir) {
-			t.Fatalf("bogus prefix on %q, expect %q", path, tempdir)
+			t.Errorf("bogus prefix on %q, expect %q", path, tempdir)
 		}
 		key := filepath.ToSlash(strings.TrimPrefix(path, tempdir))
 		if old, dup := got[key]; dup {
-			t.Fatalf("callback called twice for key %q: %v -> %v", key, old, typ)
+			t.Errorf("callback called twice for key %q: %v -> %v", key, old, typ)
 		}
 		got[key] = typ
 		return callback(path, typ)
-	}); err != nil {
+	})
+
+	if err != nil {
 		t.Fatalf("callback returned: %v", err)
 	}
 	if !reflect.DeepEqual(got, want) {
@@ -116,26 +135,25 @@
 }
 
 func TestFastWalk_Symlink(t *testing.T) {
-	switch runtime.GOOS {
-	case "windows", "plan9":
-		t.Skipf("skipping on %s", runtime.GOOS)
-	}
 	testFastWalk(t, map[string]string{
-		"foo/foo.go": "one",
-		"bar/bar.go": "LINK:../foo.go",
-		"symdir":     "LINK:foo",
+		"foo/foo.go":       "one",
+		"bar/bar.go":       "LINK:../foo/foo.go",
+		"symdir":           "LINK:foo",
+		"broken/broken.go": "LINK:../nonexistent",
 	},
 		func(path string, typ os.FileMode) error {
 			return nil
 		},
 		map[string]os.FileMode{
-			"":                os.ModeDir,
-			"/src":            os.ModeDir,
-			"/src/bar":        os.ModeDir,
-			"/src/bar/bar.go": os.ModeSymlink,
-			"/src/foo":        os.ModeDir,
-			"/src/foo/foo.go": 0,
-			"/src/symdir":     os.ModeSymlink,
+			"":                      os.ModeDir,
+			"/src":                  os.ModeDir,
+			"/src/bar":              os.ModeDir,
+			"/src/bar/bar.go":       os.ModeSymlink,
+			"/src/foo":              os.ModeDir,
+			"/src/foo/foo.go":       0,
+			"/src/symdir":           os.ModeSymlink,
+			"/src/broken":           os.ModeDir,
+			"/src/broken/broken.go": os.ModeSymlink,
 		})
 }
 
@@ -195,11 +213,6 @@
 }
 
 func TestFastWalk_TraverseSymlink(t *testing.T) {
-	switch runtime.GOOS {
-	case "windows", "plan9":
-		t.Skipf("skipping on %s", runtime.GOOS)
-	}
-
 	testFastWalk(t, map[string]string{
 		"foo/foo.go":   "one",
 		"bar/bar.go":   "two",