github.com/charlievieth/fastwalk@v1.0.3/fastwalk_test.go (about)

     1  package fastwalk_test
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/md5"
     6  	"errors"
     7  	"flag"
     8  	"fmt"
     9  	"io"
    10  	"io/fs"
    11  	"os"
    12  	"path/filepath"
    13  	"reflect"
    14  	"runtime"
    15  	"sort"
    16  	"strings"
    17  	"sync"
    18  	"sync/atomic"
    19  	"testing"
    20  
    21  	"github.com/charlievieth/fastwalk"
    22  )
    23  
    24  func formatFileModes(m map[string]os.FileMode) string {
    25  	var keys []string
    26  	for k := range m {
    27  		keys = append(keys, k)
    28  	}
    29  	sort.Strings(keys)
    30  	var buf bytes.Buffer
    31  	for _, k := range keys {
    32  		fmt.Fprintf(&buf, "%-20s: %v\n", k, m[k])
    33  	}
    34  	return buf.String()
    35  }
    36  
    37  func writeFile(filename string, data interface{}, perm os.FileMode) error {
    38  	if err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil {
    39  		return err
    40  	}
    41  	f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
    42  	if err != nil {
    43  		return err
    44  	}
    45  	switch v := data.(type) {
    46  	case []byte:
    47  		_, err = f.Write(v)
    48  	case string:
    49  		_, err = f.WriteString(v)
    50  	case io.Reader:
    51  		_, err = io.Copy(f, v)
    52  	default:
    53  		f.Close()
    54  		return &os.PathError{Op: "WriteFile", Path: filename,
    55  			Err: fmt.Errorf("invalid data type: %T", data)}
    56  	}
    57  	if err1 := f.Close(); err1 != nil && err == nil {
    58  		err = err1
    59  	}
    60  	return err
    61  }
    62  
    63  func symlink(t testing.TB, oldname, newname string) error {
    64  	err := os.Symlink(oldname, newname)
    65  	if err != nil {
    66  		if writeErr := os.WriteFile(newname, []byte(newname), 0644); writeErr == nil {
    67  			// Couldn't create symlink, but could write the file.
    68  			// Probably this filesystem doesn't support symlinks.
    69  			// (Perhaps we are on an older Windows and not running as administrator.)
    70  			t.Skipf("skipping because symlinks appear to be unsupported: %v", err)
    71  		}
    72  	}
    73  	return err
    74  }
    75  
    76  func cleanupOrLogTempDir(t *testing.T, tempdir string) {
    77  	if e := recover(); e != nil {
    78  		t.Log("TMPDIR:", tempdir)
    79  		t.Fatal(e)
    80  	}
    81  	if t.Failed() {
    82  		t.Log("TMPDIR:", tempdir)
    83  	} else {
    84  		os.RemoveAll(tempdir)
    85  	}
    86  }
    87  
    88  func testCreateFiles(t *testing.T, tempdir string, files map[string]string) {
    89  	symlinks := map[string]string{}
    90  	for path, contents := range files {
    91  		file := filepath.Join(tempdir, "/src", path)
    92  		if err := os.MkdirAll(filepath.Dir(file), 0755); err != nil {
    93  			t.Fatal(err)
    94  		}
    95  		var err error
    96  		if strings.HasPrefix(contents, "LINK:") {
    97  			symlinks[file] = filepath.FromSlash(strings.TrimPrefix(contents, "LINK:"))
    98  		} else {
    99  			err = os.WriteFile(file, []byte(contents), 0644)
   100  		}
   101  		if err != nil {
   102  			t.Fatal(err)
   103  		}
   104  	}
   105  
   106  	// Create symlinks after all other files. Otherwise, directory symlinks on
   107  	// Windows are unusable (see https://golang.org/issue/39183).
   108  	for file, dst := range symlinks {
   109  		if err := symlink(t, dst, file); err != nil {
   110  			t.Fatal(err)
   111  		}
   112  	}
   113  }
   114  
   115  func testFastWalkConf(t *testing.T, conf *fastwalk.Config, files map[string]string, callback fs.WalkDirFunc, want map[string]os.FileMode) {
   116  	tempdir, err := os.MkdirTemp("", "test-fast-walk")
   117  	if err != nil {
   118  		t.Fatal(err)
   119  	}
   120  	defer cleanupOrLogTempDir(t, tempdir)
   121  
   122  	testCreateFiles(t, tempdir, files)
   123  
   124  	got := map[string]os.FileMode{}
   125  	var mu sync.Mutex
   126  	err = fastwalk.Walk(conf, tempdir, func(path string, de fs.DirEntry, err error) error {
   127  		if de == nil {
   128  			t.Errorf("nil fs.DirEntry on %q", path)
   129  			return nil
   130  		}
   131  		mu.Lock()
   132  		defer mu.Unlock()
   133  		if !strings.HasPrefix(path, tempdir) {
   134  			t.Errorf("bogus prefix on %q, expect %q", path, tempdir)
   135  		}
   136  		key := filepath.ToSlash(strings.TrimPrefix(path, tempdir))
   137  		if old, dup := got[key]; dup {
   138  			t.Errorf("callback called twice for key %q: %v -> %v", key, old, de.Type())
   139  		}
   140  		got[key] = de.Type()
   141  		return callback(path, de, err)
   142  	})
   143  
   144  	if err != nil {
   145  		t.Fatalf("callback returned: %v", err)
   146  	}
   147  	if !reflect.DeepEqual(got, want) {
   148  		t.Errorf("walk mismatch.\n got:\n%v\nwant:\n%v", formatFileModes(got), formatFileModes(want))
   149  		diffFileModes(t, got, want)
   150  	}
   151  }
   152  
   153  func testFastWalk(t *testing.T, files map[string]string, callback fs.WalkDirFunc, want map[string]os.FileMode) {
   154  	testFastWalkConf(t, nil, files, callback, want)
   155  }
   156  
   157  func requireNoError(t testing.TB, err error) {
   158  	t.Helper()
   159  	if err != nil {
   160  		t.Error("WalkDirFunc called with error:", err)
   161  		panic(err)
   162  	}
   163  }
   164  
   165  func TestFastWalk_Basic(t *testing.T) {
   166  	testFastWalk(t, map[string]string{
   167  		"foo/foo.go":   "one",
   168  		"bar/bar.go":   "two",
   169  		"skip/skip.go": "skip",
   170  	},
   171  		func(path string, typ fs.DirEntry, err error) error {
   172  			requireNoError(t, err)
   173  			return nil
   174  		},
   175  		map[string]os.FileMode{
   176  			"":                  os.ModeDir,
   177  			"/src":              os.ModeDir,
   178  			"/src/bar":          os.ModeDir,
   179  			"/src/bar/bar.go":   0,
   180  			"/src/foo":          os.ModeDir,
   181  			"/src/foo/foo.go":   0,
   182  			"/src/skip":         os.ModeDir,
   183  			"/src/skip/skip.go": 0,
   184  		})
   185  }
   186  
   187  func maxFileNameLength(t testing.TB) int {
   188  	tmp := t.TempDir()
   189  	long := strings.Repeat("a", 8192)
   190  
   191  	// Returns if n is an invalid file name length
   192  	invalidLength := func(n int) bool {
   193  		path := filepath.Join(tmp, long[:n])
   194  		err := os.WriteFile(path, []byte("1"), 0644)
   195  		if err == nil {
   196  			os.Remove(path)
   197  		}
   198  		return err != nil
   199  	}
   200  
   201  	// Use a binary search to find the max filename length (+1)
   202  	n := sort.Search(8192, invalidLength)
   203  	if n <= 1 {
   204  		t.Fatal("Failed to find the max filename length:", n)
   205  	}
   206  	max := n - 1
   207  	if invalidLength(max) {
   208  		t.Fatal("Failed to find the max filename length:", n)
   209  	}
   210  	return max
   211  }
   212  
   213  // This test identified a "checkptr: converted pointer straddles multiple allocations"
   214  // error on darwin when getdirentries64 was used with the race-detector enabled.
   215  func TestFastWalk_LongFileName(t *testing.T) {
   216  	maxNameLen := maxFileNameLength(t)
   217  	if maxNameLen > 255 {
   218  		maxNameLen = 255
   219  	}
   220  	want := map[string]os.FileMode{
   221  		"":     os.ModeDir,
   222  		"/src": os.ModeDir,
   223  	}
   224  	files := make(map[string]string)
   225  	// This triggers with only one sub-directory but use 2 just to be sure.
   226  	for r := 'a'; r <= 'b'; r++ {
   227  		s := string(r)
   228  		name := s + "/" + strings.Repeat(s, maxNameLen)
   229  		for i := len("_/") + 1; i <= len(name); i++ {
   230  			files[name[:i]] = "1"
   231  			want["/src/"+name[:i]] = 0
   232  		}
   233  		want["/src/"+s] = os.ModeDir
   234  	}
   235  	testFastWalk(t, files,
   236  		func(path string, typ fs.DirEntry, err error) error {
   237  			requireNoError(t, err)
   238  			return nil
   239  		},
   240  		want,
   241  	)
   242  }
   243  
   244  func TestFastWalk_Symlink(t *testing.T) {
   245  	testFastWalk(t, map[string]string{
   246  		"foo/foo.go":       "one",
   247  		"bar/bar.go":       "LINK:../foo/foo.go",
   248  		"symdir":           "LINK:foo",
   249  		"broken/broken.go": "LINK:../nonexistent",
   250  	},
   251  		func(path string, typ fs.DirEntry, err error) error {
   252  			requireNoError(t, err)
   253  			return nil
   254  		},
   255  		map[string]os.FileMode{
   256  			"":                      os.ModeDir,
   257  			"/src":                  os.ModeDir,
   258  			"/src/bar":              os.ModeDir,
   259  			"/src/bar/bar.go":       os.ModeSymlink,
   260  			"/src/foo":              os.ModeDir,
   261  			"/src/foo/foo.go":       0,
   262  			"/src/symdir":           os.ModeSymlink,
   263  			"/src/broken":           os.ModeDir,
   264  			"/src/broken/broken.go": os.ModeSymlink,
   265  		})
   266  }
   267  
   268  // Test that the fs.DirEntry passed to WalkFunc is always a fastwalk.DirEntry.
   269  func TestFastWalk_DirEntryType(t *testing.T) {
   270  	testFastWalk(t, map[string]string{
   271  		"foo/foo.go":       "one",
   272  		"bar/bar.go":       "LINK:../foo/foo.go",
   273  		"symdir":           "LINK:foo",
   274  		"broken/broken.go": "LINK:../nonexistent",
   275  	},
   276  		func(path string, de fs.DirEntry, err error) error {
   277  			requireNoError(t, err)
   278  			if _, ok := de.(fastwalk.DirEntry); !ok {
   279  				t.Errorf("%q: not a fastwalk.DirEntry: %T", path, de)
   280  			}
   281  			if de.Type() != de.Type().Type() {
   282  				t.Errorf("%s: type mismatch got: %q want: %q",
   283  					path, de.Type(), de.Type().Type())
   284  			}
   285  			return nil
   286  		},
   287  		map[string]os.FileMode{
   288  			"":                      os.ModeDir,
   289  			"/src":                  os.ModeDir,
   290  			"/src/bar":              os.ModeDir,
   291  			"/src/bar/bar.go":       os.ModeSymlink,
   292  			"/src/foo":              os.ModeDir,
   293  			"/src/foo/foo.go":       0,
   294  			"/src/symdir":           os.ModeSymlink,
   295  			"/src/broken":           os.ModeDir,
   296  			"/src/broken/broken.go": os.ModeSymlink,
   297  		})
   298  }
   299  
   300  func TestFastWalk_SkipDir(t *testing.T) {
   301  	testFastWalk(t, map[string]string{
   302  		"foo/foo.go":   "one",
   303  		"bar/bar.go":   "two",
   304  		"skip/skip.go": "skip",
   305  	},
   306  		func(path string, de fs.DirEntry, err error) error {
   307  			requireNoError(t, err)
   308  			typ := de.Type().Type()
   309  			if typ == os.ModeDir && strings.HasSuffix(path, "skip") {
   310  				return filepath.SkipDir
   311  			}
   312  			return nil
   313  		},
   314  		map[string]os.FileMode{
   315  			"":                os.ModeDir,
   316  			"/src":            os.ModeDir,
   317  			"/src/bar":        os.ModeDir,
   318  			"/src/bar/bar.go": 0,
   319  			"/src/foo":        os.ModeDir,
   320  			"/src/foo/foo.go": 0,
   321  			"/src/skip":       os.ModeDir,
   322  		})
   323  }
   324  
   325  func TestFastWalk_SkipFiles(t *testing.T) {
   326  	// Directory iteration order is undefined, so there's no way to know
   327  	// which file to expect until the walk happens. Rather than mess
   328  	// with the test infrastructure, just mutate want.
   329  	var mu sync.Mutex
   330  	want := map[string]os.FileMode{
   331  		"":              os.ModeDir,
   332  		"/src":          os.ModeDir,
   333  		"/src/zzz":      os.ModeDir,
   334  		"/src/zzz/c.go": 0,
   335  	}
   336  
   337  	testFastWalk(t, map[string]string{
   338  		"a_skipfiles.go": "a",
   339  		"b_skipfiles.go": "b",
   340  		"zzz/c.go":       "c",
   341  	},
   342  		func(path string, _ fs.DirEntry, err error) error {
   343  			requireNoError(t, err)
   344  			if strings.HasSuffix(path, "_skipfiles.go") {
   345  				mu.Lock()
   346  				defer mu.Unlock()
   347  				want["/src/"+filepath.Base(path)] = 0
   348  				return fastwalk.ErrSkipFiles
   349  			}
   350  			return nil
   351  		},
   352  		want)
   353  	if len(want) != 5 {
   354  		t.Errorf("saw too many files: wanted 5, got %v (%v)", len(want), want)
   355  	}
   356  }
   357  
   358  func TestFastWalk_TraverseSymlink(t *testing.T) {
   359  	testFastWalk(t, map[string]string{
   360  		"foo/foo.go": "one",
   361  		"bar/bar.go": "two",
   362  		"symdir":     "LINK:foo",
   363  	},
   364  		func(path string, de fs.DirEntry, err error) error {
   365  			requireNoError(t, err)
   366  			typ := de.Type().Type()
   367  			if typ == os.ModeSymlink {
   368  				return fastwalk.ErrTraverseLink
   369  			}
   370  			return nil
   371  		},
   372  		map[string]os.FileMode{
   373  			"":                   os.ModeDir,
   374  			"/src":               os.ModeDir,
   375  			"/src/bar":           os.ModeDir,
   376  			"/src/bar/bar.go":    0,
   377  			"/src/foo":           os.ModeDir,
   378  			"/src/foo/foo.go":    0,
   379  			"/src/symdir":        os.ModeSymlink,
   380  			"/src/symdir/foo.go": 0,
   381  		})
   382  }
   383  
   384  func TestFastWalk_Follow(t *testing.T) {
   385  	subTests := []struct {
   386  		Name   string
   387  		OnLink func(path string, d fs.DirEntry) error
   388  	}{
   389  		// Test that the walk func does *not* need to return
   390  		// ErrTraverseLink for links to be followed.
   391  		{
   392  			Name:   "Default",
   393  			OnLink: func(path string, d fs.DirEntry) error { return nil },
   394  		},
   395  
   396  		// Test that returning ErrTraverseLink does not interfere
   397  		// with the Follow logic.
   398  		{
   399  			Name: "ErrTraverseLink",
   400  			OnLink: func(path string, d fs.DirEntry) error {
   401  				if d.Type()&os.ModeSymlink != 0 {
   402  					if fi, err := fastwalk.StatDirEntry(path, d); err == nil && fi.IsDir() {
   403  						return fastwalk.ErrTraverseLink
   404  					}
   405  				}
   406  				return nil
   407  			},
   408  		},
   409  	}
   410  	for _, x := range subTests {
   411  		t.Run(x.Name, func(t *testing.T) {
   412  			conf := fastwalk.Config{
   413  				Follow: true,
   414  			}
   415  			testFastWalkConf(t, &conf, map[string]string{
   416  				"foo/foo.go":  "one",
   417  				"bar/bar.go":  "two",
   418  				"foo/symlink": "LINK:foo.go",
   419  				"bar/symdir":  "LINK:../foo/",
   420  				"bar/link1":   "LINK:../foo/",
   421  			},
   422  				func(path string, de fs.DirEntry, err error) error {
   423  					requireNoError(t, err)
   424  					if err != nil {
   425  						return err
   426  					}
   427  					if de.Type()&os.ModeSymlink != 0 {
   428  						return x.OnLink(path, de)
   429  					}
   430  					return nil
   431  				},
   432  				map[string]os.FileMode{
   433  					"":                        os.ModeDir,
   434  					"/src":                    os.ModeDir,
   435  					"/src/bar":                os.ModeDir,
   436  					"/src/bar/bar.go":         0,
   437  					"/src/bar/link1":          os.ModeSymlink,
   438  					"/src/bar/link1/foo.go":   0,
   439  					"/src/bar/link1/symlink":  os.ModeSymlink,
   440  					"/src/bar/symdir":         os.ModeSymlink,
   441  					"/src/bar/symdir/foo.go":  0,
   442  					"/src/bar/symdir/symlink": os.ModeSymlink,
   443  					"/src/foo":                os.ModeDir,
   444  					"/src/foo/foo.go":         0,
   445  					"/src/foo/symlink":        os.ModeSymlink,
   446  				})
   447  		})
   448  	}
   449  }
   450  
   451  func TestFastWalk_Follow_SkipDir(t *testing.T) {
   452  	conf := fastwalk.Config{
   453  		Follow: true,
   454  	}
   455  	testFastWalkConf(t, &conf, map[string]string{
   456  		".dot/baz.go": "one",
   457  		"bar/bar.go":  "three",
   458  		"bar/dot":     "LINK:../.dot/",
   459  		"bar/symdir":  "LINK:../foo/",
   460  		"foo/foo.go":  "two",
   461  		"foo/symlink": "LINK:foo.go",
   462  	},
   463  		func(path string, de fs.DirEntry, err error) error {
   464  			requireNoError(t, err)
   465  			if err != nil {
   466  				return err
   467  			}
   468  			if strings.HasPrefix(de.Name(), ".") {
   469  				return filepath.SkipDir
   470  			}
   471  			return nil
   472  		},
   473  		map[string]os.FileMode{
   474  			"":                        os.ModeDir,
   475  			"/src":                    os.ModeDir,
   476  			"/src/.dot":               os.ModeDir,
   477  			"/src/bar":                os.ModeDir,
   478  			"/src/bar/bar.go":         0,
   479  			"/src/bar/dot":            os.ModeSymlink,
   480  			"/src/bar/dot/baz.go":     0,
   481  			"/src/bar/symdir":         os.ModeSymlink,
   482  			"/src/bar/symdir/foo.go":  0,
   483  			"/src/bar/symdir/symlink": os.ModeSymlink,
   484  			"/src/foo":                os.ModeDir,
   485  			"/src/foo/foo.go":         0,
   486  			"/src/foo/symlink":        os.ModeSymlink,
   487  		})
   488  }
   489  
   490  func TestFastWalk_Follow_SymlinkLoop(t *testing.T) {
   491  	tempdir, err := os.MkdirTemp("", "test-fast-walk")
   492  	if err != nil {
   493  		t.Fatal(err)
   494  	}
   495  	defer cleanupOrLogTempDir(t, tempdir)
   496  
   497  	if err := writeFile(tempdir+"/src/foo.go", "hello", 0644); err != nil {
   498  		t.Fatal(err)
   499  	}
   500  	if err := symlink(t, "../src", tempdir+"/src/loop"); err != nil {
   501  		t.Fatal(err)
   502  	}
   503  
   504  	conf := fastwalk.Config{
   505  		Follow: true,
   506  	}
   507  	var walked int32
   508  	err = fastwalk.Walk(&conf, tempdir, func(path string, de fs.DirEntry, err error) error {
   509  		if err != nil {
   510  			return err
   511  		}
   512  		if n := atomic.AddInt32(&walked, 1); n > 20 {
   513  			return fmt.Errorf("symlink loop: %d", n)
   514  		}
   515  		return nil
   516  	})
   517  	if err != nil {
   518  		t.Fatal(err)
   519  	}
   520  }
   521  
   522  // Test that ErrTraverseLink is ignored when following symlinks
   523  // if it would cause a symlink loop.
   524  func TestFastWalk_Follow_ErrTraverseLink(t *testing.T) {
   525  	conf := fastwalk.Config{
   526  		Follow: true,
   527  	}
   528  	testFastWalkConf(t, &conf, map[string]string{
   529  		"foo/foo.go": "one",
   530  		"bar/bar.go": "two",
   531  		"bar/symdir": "LINK:../foo/",
   532  		"bar/loop":   "LINK:../bar/", // symlink loop
   533  	},
   534  		func(path string, de fs.DirEntry, err error) error {
   535  			requireNoError(t, err)
   536  			if err != nil {
   537  				return err
   538  			}
   539  			if de.Type()&os.ModeSymlink != 0 {
   540  				if fi, err := fastwalk.StatDirEntry(path, de); err == nil && fi.IsDir() {
   541  					return fastwalk.ErrTraverseLink
   542  				}
   543  			}
   544  			return nil
   545  		},
   546  		map[string]os.FileMode{
   547  			"":                       os.ModeDir,
   548  			"/src":                   os.ModeDir,
   549  			"/src/bar":               os.ModeDir,
   550  			"/src/bar/bar.go":        0,
   551  			"/src/bar/loop":          os.ModeSymlink,
   552  			"/src/bar/symdir":        os.ModeSymlink,
   553  			"/src/bar/symdir/foo.go": 0,
   554  			"/src/foo":               os.ModeDir,
   555  			"/src/foo/foo.go":        0,
   556  		})
   557  }
   558  
   559  func TestFastWalk_Error(t *testing.T) {
   560  	tmp := t.TempDir()
   561  	for _, child := range []string{
   562  		"foo/foo.go",
   563  		"bar/bar.go",
   564  		"skip/skip.go",
   565  	} {
   566  		if err := writeFile(filepath.Join(tmp, child), child, 0644); err != nil {
   567  			t.Fatal(err)
   568  		}
   569  	}
   570  
   571  	exp := errors.New("expected")
   572  	err := fastwalk.Walk(nil, tmp, func(_ string, _ fs.DirEntry, err error) error {
   573  		requireNoError(t, err)
   574  		return exp
   575  	})
   576  	if !errors.Is(err, exp) {
   577  		t.Errorf("want error: %#v got: %#v", exp, err)
   578  	}
   579  }
   580  
   581  func TestFastWalk_ErrNotExist(t *testing.T) {
   582  	tmp := t.TempDir()
   583  	if err := os.Remove(tmp); err != nil {
   584  		t.Fatal(err)
   585  	}
   586  	err := fastwalk.Walk(nil, tmp, func(_ string, _ fs.DirEntry, err error) error {
   587  		return err
   588  	})
   589  	if !os.IsNotExist(err) {
   590  		t.Fatalf("os.IsNotExist(%+v) = false want: true", err)
   591  	}
   592  }
   593  
   594  func TestFastWalk_ErrPermission(t *testing.T) {
   595  	if runtime.GOOS == "windows" {
   596  		t.Skip("test not-supported for Windows")
   597  	}
   598  	tempdir := t.TempDir()
   599  	want := map[string]os.FileMode{
   600  		"":     os.ModeDir,
   601  		"/bad": os.ModeDir,
   602  	}
   603  	for i := 0; i < runtime.NumCPU()*4; i++ {
   604  		dir := fmt.Sprintf("/d%03d", i)
   605  		name := fmt.Sprintf("%s/f%03d.txt", dir, i)
   606  		if err := writeFile(filepath.Join(tempdir, name), "data", 0644); err != nil {
   607  			t.Fatal(err)
   608  		}
   609  		want[name] = 0
   610  		want[filepath.Dir(name)] = os.ModeDir
   611  	}
   612  
   613  	filename := filepath.Join(tempdir, "/bad/bad.txt")
   614  	if err := writeFile(filename, "data", 0644); err != nil {
   615  		t.Fatal(err)
   616  	}
   617  	// Make the directory unreadable
   618  	dirname := filepath.Dir(filename)
   619  	if err := os.Chmod(dirname, 0355); err != nil {
   620  		t.Fatal(err)
   621  	}
   622  	t.Cleanup(func() {
   623  		if err := os.Remove(filename); err != nil {
   624  			t.Error(err)
   625  		}
   626  		if err := os.Remove(dirname); err != nil {
   627  			t.Error(err)
   628  		}
   629  	})
   630  
   631  	got := map[string]os.FileMode{}
   632  	var mu sync.Mutex
   633  	err := fastwalk.Walk(nil, tempdir, func(path string, de fs.DirEntry, err error) error {
   634  		if err != nil && os.IsPermission(err) {
   635  			return nil
   636  		}
   637  
   638  		mu.Lock()
   639  		defer mu.Unlock()
   640  		if !strings.HasPrefix(path, tempdir) {
   641  			t.Errorf("bogus prefix on %q, expect %q", path, tempdir)
   642  		}
   643  		key := filepath.ToSlash(strings.TrimPrefix(path, tempdir))
   644  		if old, dup := got[key]; dup {
   645  			t.Errorf("callback called twice for key %q: %v -> %v", key, old, de.Type())
   646  		}
   647  		got[key] = de.Type()
   648  		return nil
   649  	})
   650  	if err != nil {
   651  		t.Error("Walk:", err)
   652  	}
   653  	if !reflect.DeepEqual(got, want) {
   654  		t.Errorf("walk mismatch.\n got:\n%v\nwant:\n%v", formatFileModes(got), formatFileModes(want))
   655  		diffFileModes(t, got, want)
   656  	}
   657  }
   658  
   659  func diffFileModes(t *testing.T, got, want map[string]os.FileMode) {
   660  	type Mode struct {
   661  		Name string
   662  		Mode os.FileMode
   663  	}
   664  	var extra []Mode
   665  	for k, v := range got {
   666  		if _, ok := want[k]; !ok {
   667  			extra = append(extra, Mode{k, v})
   668  		}
   669  	}
   670  	var missing []Mode
   671  	for k, v := range want {
   672  		if _, ok := got[k]; !ok {
   673  			missing = append(missing, Mode{k, v})
   674  		}
   675  	}
   676  	var delta []Mode
   677  	for k, v := range got {
   678  		if vv, ok := want[k]; ok && vv != v {
   679  			delta = append(delta, Mode{k, v}, Mode{k, vv})
   680  		}
   681  	}
   682  	w := new(strings.Builder)
   683  	printMode := func(name string, modes []Mode) {
   684  		if len(modes) == 0 {
   685  			return
   686  		}
   687  		sort.Slice(modes, func(i, j int) bool {
   688  			return modes[i].Name < modes[j].Name
   689  		})
   690  		if w.Len() == 0 {
   691  			w.WriteString("\n")
   692  		}
   693  		fmt.Fprintf(w, "%s:\n", name)
   694  		for _, m := range modes {
   695  			fmt.Fprintf(w, "  %-20s: %s\n", m.Name, m.Mode.String())
   696  		}
   697  	}
   698  	printMode("Extra", extra)
   699  	printMode("Missing", missing)
   700  	printMode("Delta", delta)
   701  	if w.Len() != 0 {
   702  		t.Error(w.String())
   703  	}
   704  }
   705  
   706  // Directory to use for benchmarks, GOROOT is used by default
   707  var benchDir *string
   708  
   709  // Make sure we don't register the "benchdir" twice.
   710  func init() {
   711  	ff := flag.Lookup("benchdir")
   712  	if ff != nil {
   713  		value := ff.DefValue
   714  		if ff.Value != nil {
   715  			value = ff.Value.String()
   716  		}
   717  		benchDir = &value
   718  	} else {
   719  		benchDir = flag.String("benchdir", runtime.GOROOT(), "The directory to scan for BenchmarkFastWalk")
   720  	}
   721  }
   722  
   723  func noopWalkFunc(_ string, _ fs.DirEntry, _ error) error { return nil }
   724  
   725  func benchmarkFastWalk(b *testing.B, conf *fastwalk.Config,
   726  	adapter func(fs.WalkDirFunc) fs.WalkDirFunc) {
   727  
   728  	b.ReportAllocs()
   729  	if adapter != nil {
   730  		walkFn := noopWalkFunc
   731  		for i := 0; i < b.N; i++ {
   732  			err := fastwalk.Walk(conf, *benchDir, adapter(walkFn))
   733  			if err != nil {
   734  				b.Fatal(err)
   735  			}
   736  		}
   737  	} else {
   738  		for i := 0; i < b.N; i++ {
   739  			err := fastwalk.Walk(conf, *benchDir, noopWalkFunc)
   740  			if err != nil {
   741  				b.Fatal(err)
   742  			}
   743  		}
   744  	}
   745  }
   746  
   747  func BenchmarkFastWalk(b *testing.B) {
   748  	benchmarkFastWalk(b, nil, nil)
   749  }
   750  
   751  func BenchmarkFastWalkFollow(b *testing.B) {
   752  	benchmarkFastWalk(b, &fastwalk.Config{Follow: true}, nil)
   753  }
   754  
   755  func BenchmarkFastWalkAdapters(b *testing.B) {
   756  	if testing.Short() {
   757  		b.Skip("Skipping: short test")
   758  	}
   759  	b.Run("IgnoreDuplicateDirs", func(b *testing.B) {
   760  		benchmarkFastWalk(b, nil, fastwalk.IgnoreDuplicateDirs)
   761  	})
   762  
   763  	b.Run("IgnoreDuplicateFiles", func(b *testing.B) {
   764  		benchmarkFastWalk(b, nil, fastwalk.IgnoreDuplicateFiles)
   765  	})
   766  }
   767  
   768  // Benchmark various tasks with different worker counts.
   769  //
   770  // Observations:
   771  //   - Linux (Intel i9-9900K / Samsung Pro NVMe): consistently benefits from
   772  //     more workers
   773  //   - Darwin (m1): IO heavy tasks (Readfile and Stat) and Traversal perform
   774  //     best with 4 workers, and only CPU bound tasks benefit from more workers
   775  func BenchmarkFastWalkNumWorkers(b *testing.B) {
   776  	if testing.Short() {
   777  		b.Skip("Skipping: short test")
   778  	}
   779  
   780  	runBench := func(b *testing.B, walkFn fs.WalkDirFunc) {
   781  		maxWorkers := runtime.NumCPU()
   782  		for i := 2; i <= maxWorkers; i += 2 {
   783  			b.Run(fmt.Sprint(i), func(b *testing.B) {
   784  				conf := fastwalk.Config{
   785  					NumWorkers: i,
   786  				}
   787  				for i := 0; i < b.N; i++ {
   788  					if err := fastwalk.Walk(&conf, *benchDir, walkFn); err != nil {
   789  						b.Fatal(err)
   790  					}
   791  				}
   792  			})
   793  		}
   794  	}
   795  
   796  	// Bench pure traversal speed
   797  	b.Run("NoOp", func(b *testing.B) {
   798  		runBench(b, func(path string, d fs.DirEntry, err error) error {
   799  			return err
   800  		})
   801  	})
   802  
   803  	// No IO and light CPU
   804  	b.Run("NoIO", func(b *testing.B) {
   805  		runBench(b, func(path string, d fs.DirEntry, err error) error {
   806  			if err == nil {
   807  				fmt.Fprintf(io.Discard, "%s: %q\n", d.Type(), path)
   808  			}
   809  			return err
   810  		})
   811  	})
   812  
   813  	// Stat each regular file
   814  	b.Run("Stat", func(b *testing.B) {
   815  		runBench(b, func(path string, d fs.DirEntry, err error) error {
   816  			if err == nil && d.Type().IsRegular() {
   817  				_, _ = d.Info()
   818  			}
   819  			return err
   820  		})
   821  	})
   822  
   823  	// IO heavy task
   824  	b.Run("ReadFile", func(b *testing.B) {
   825  		runBench(b, func(path string, d fs.DirEntry, err error) error {
   826  			if err != nil || !d.Type().IsRegular() {
   827  				return err
   828  			}
   829  			f, err := os.Open(path)
   830  			if err != nil {
   831  				if os.IsNotExist(err) || os.IsPermission(err) {
   832  					return nil
   833  				}
   834  				return err
   835  			}
   836  			defer f.Close()
   837  
   838  			_, err = io.Copy(io.Discard, f)
   839  			return err
   840  		})
   841  	})
   842  
   843  	// CPU and IO heavy task
   844  	b.Run("Hash", func(b *testing.B) {
   845  		bufPool := &sync.Pool{
   846  			New: func() interface{} {
   847  				b := make([]byte, 96*1024)
   848  				return &b
   849  			},
   850  		}
   851  		runBench(b, func(path string, d fs.DirEntry, err error) error {
   852  			if err != nil || !d.Type().IsRegular() {
   853  				return err
   854  			}
   855  			f, err := os.Open(path)
   856  			if err != nil {
   857  				if os.IsNotExist(err) || os.IsPermission(err) {
   858  					return nil
   859  				}
   860  				return err
   861  			}
   862  			defer f.Close()
   863  
   864  			p := bufPool.Get().(*[]byte)
   865  			h := md5.New()
   866  			_, err = io.CopyBuffer(h, f, *p)
   867  			bufPool.Put(p)
   868  			_ = h.Sum(nil)
   869  			return err
   870  		})
   871  	})
   872  }
   873  
   874  var benchWalkFunc = flag.String("walkfunc", "fastwalk", "The function to use for BenchmarkWalkComparison")
   875  
   876  // BenchmarkWalkComparison generates benchmarks using different walk functions
   877  // so that the results can be easily compared with `benchcmp` and `benchstat`.
   878  func BenchmarkWalkComparison(b *testing.B) {
   879  	if testing.Short() {
   880  		b.Skip("Skipping: short test")
   881  	}
   882  	switch *benchWalkFunc {
   883  	case "fastwalk":
   884  		benchmarkFastWalk(b, nil, nil)
   885  	case "godirwalk":
   886  		b.Fatal("comparisons with godirwalk are no longer supported")
   887  	case "filepath":
   888  		for i := 0; i < b.N; i++ {
   889  			err := filepath.WalkDir(*benchDir, func(_ string, _ fs.DirEntry, _ error) error {
   890  				return nil
   891  			})
   892  			if err != nil {
   893  				b.Fatal(err)
   894  			}
   895  		}
   896  	default:
   897  		b.Fatalf("invalid walkfunc: %q", *benchWalkFunc)
   898  	}
   899  }