Skip to content

Commit

Permalink
WIP: add support for fs.SkipAll
Browse files Browse the repository at this point in the history
NB: This seems to impact performace by 5-8% on Mac/Linux. Need to do
more benchmarking to see if this is really the case.
  • Loading branch information
charlievieth committed Aug 31, 2024
1 parent 940d64f commit fd56359
Show file tree
Hide file tree
Showing 5 changed files with 222 additions and 20 deletions.
28 changes: 17 additions & 11 deletions fastwalk.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,10 @@ var ErrSkipFiles = errors.New("fastwalk: skip remaining files in directory")
// as an error by any function.
var SkipDir = fs.SkipDir

// TODO(charlie): Look into implementing the fs.SkipAll behavior of
// filepath.Walk and filepath.WalkDir. This may not be possible without taking
// a performance hit.
// SkipAll is used as a return value from [WalkDirFunc] to indicate that
// all remaining files and directories are to be skipped. It is not returned
// as an error by any function.
var SkipAll = fs.SkipAll

// DefaultNumWorkers returns the default number of worker goroutines to use in
// [Walk] and is the value of [runtime.GOMAXPROCS](-1) clamped to a range
Expand Down Expand Up @@ -577,13 +578,18 @@ func (w *walker) joinPaths(dir, base string) string {

func (w *walker) onDirEnt(dirName, baseName string, de DirEntry) error {
joined := w.joinPaths(dirName, baseName)
err := w.fn(joined, de, nil)
typ := de.Type()
if typ == os.ModeDir {
w.enqueue(walkItem{dir: joined, info: de})
if err != nil {
if err == SkipDir {
return nil
}
return err // May be SkipAll
}
w.enqueue(walkItem{dir: joined, info: de, callbackDone: true})
return nil
}

err := w.fn(joined, de, nil)
if typ == os.ModeSymlink {
if err == ErrTraverseLink {
if !w.follow {
Expand All @@ -594,8 +600,8 @@ func (w *walker) onDirEnt(dirName, baseName string, de DirEntry) error {
}
err = nil // Ignore ErrTraverseLink when Follow is true.
}
if err == filepath.SkipDir {
// Permit SkipDir on symlinks too.
if err == SkipDir {
// Permit SkipDir and SkipAll on symlinks too.
return nil
}
if err == nil && w.follow && w.shouldTraverse(joined, de) {
Expand All @@ -609,10 +615,10 @@ func (w *walker) onDirEnt(dirName, baseName string, de DirEntry) error {
func (w *walker) walk(root string, info DirEntry, runUserCallback bool) error {
if runUserCallback {
err := w.fn(root, info, nil)
if err == filepath.SkipDir {
return nil
}
if err != nil {
if err == SkipDir || err == SkipAll {
return nil
}
return err
}
}
Expand Down
6 changes: 6 additions & 0 deletions fastwalk_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ func (w *walker) readDir(dirName string) (err error) {
de := newUnixDirent(dirName, nm, typ)
if w.sortMode == SortNone {
if err := w.onDirEnt(dirName, nm, de); err != nil {
if err == SkipAll {
return nil
}
if err != ErrSkipFiles {
return err
}
Expand All @@ -92,6 +95,9 @@ func (w *walker) readDir(dirName string) (err error) {
continue
}
if err := w.onDirEnt(dirName, d.Name(), d); err != nil {
if err == SkipAll {
return nil
}
if err != ErrSkipFiles {
return err
}
Expand Down
6 changes: 6 additions & 0 deletions fastwalk_portable.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ func (w *walker) readDir(dirName string) error {
e := newDirEntry(dirName, d)
if w.sortMode == SortNone {
if err := w.onDirEnt(dirName, d.Name(), e); err != nil {
if err == SkipAll {
return nil
}
if err != ErrSkipFiles {
return err
}
Expand All @@ -57,6 +60,9 @@ func (w *walker) readDir(dirName string) error {
continue
}
if err := w.onDirEnt(dirName, d.Name(), d); err != nil {
if err == SkipAll {
return nil
}
if err != ErrSkipFiles {
return err
}
Expand Down
196 changes: 187 additions & 9 deletions fastwalk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,60 @@ func TestFastWalk_DirEntryType(t *testing.T) {
})
}

func TestFastWalk_DirEntryStat(t *testing.T) {
testFastWalk(t, map[string]string{
"foo/foo.go": "one",
"bar/bar.go": "LINK:../foo/foo.go",
"symdir": "LINK:foo",
},
func(path string, d fs.DirEntry, err error) error {
requireNoError(t, err)
de := d.(fastwalk.DirEntry)
if _, ok := de.(fastwalk.DirEntry); !ok {
t.Errorf("%q: not a fastwalk.DirEntry: %T", path, de)
}
ls1, err := os.Lstat(path)
if err != nil {
t.Error(err)
}
ls2, err := de.Info()
if err != nil {
t.Error(err)
}
if !os.SameFile(ls1, ls2) {
t.Errorf("Info(%q) = %v; want: %v", path, ls2, ls1)
}
st1, err := os.Stat(path)
if err != nil {
t.Error(err)
}
st2, err := de.Stat()
if err != nil {
t.Error(err)
}
if !os.SameFile(st1, st2) {
t.Errorf("Stat(%q) = %v; want: %v", path, st2, st1)
}
if de.Name() != filepath.Base(path) {
t.Errorf("Name() = %q; want: %q", de.Name(), filepath.Base(path))
}
if de.Type() != de.Type().Type() {
t.Errorf("%s: type mismatch got: %q want: %q",
path, de.Type(), de.Type().Type())
}
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,
})
}

func TestFastWalk_SkipDir(t *testing.T) {
test := func(t *testing.T, mode fastwalk.SortMode) {
conf := fastwalk.DefaultConfig.Copy()
Expand Down Expand Up @@ -485,6 +539,28 @@ func TestFastWalk_SkipDir(t *testing.T) {
}
}

// Test that returning SkipDir for the root directory aborts the walk
func TestFastWalk_SkipDir_Root(t *testing.T) {
want := map[string]os.FileMode{
"": os.ModeDir,
}
conf := fastwalk.DefaultConfig.Copy()
conf.Sort = fastwalk.SortLexical // Needed for ordering
testFastWalkConf(t, conf, map[string]string{
"a.go": "a",
"b.go": "b",
},
func(path string, de fs.DirEntry, err error) error {
requireNoError(t, err)
return fastwalk.SkipDir
},
want)
if len(want) != 1 {
t.Errorf("invalid number of files visited: wanted 1, got %v (%q)",
len(want), want)
}
}

func TestFastWalk_SkipFiles(t *testing.T) {
mapKeys := func(m map[string]os.FileMode) []string {
a := make([]string, 0, len(m))
Expand Down Expand Up @@ -542,6 +618,117 @@ func TestFastWalk_SkipFiles(t *testing.T) {
}
}

func TestFastWalk_SkipAll(t *testing.T) {
mapKeys := func(m map[string]os.FileMode) []string {
a := make([]string, 0, len(m))
for k := range m {
a = append(a, k)
}
return a
}

t.Run("Root", func(t *testing.T) {
want := map[string]os.FileMode{
"": os.ModeDir,
}
conf := fastwalk.DefaultConfig.Copy()
conf.Sort = fastwalk.SortLexical // Needed for ordering
testFastWalkConf(t, conf, map[string]string{
"a.go": "a",
"b.go": "b",
},
func(path string, de fs.DirEntry, err error) error {
requireNoError(t, err)
return fastwalk.SkipAll
},
want)
if len(want) != 1 {
t.Errorf("invalid number of files visited: wanted 1, got %v (%q)",
len(want), mapKeys(want))
}
})

t.Run("File", func(t *testing.T) {
want := map[string]os.FileMode{
"": os.ModeDir,
"/src": os.ModeDir,
"/src/a.go": 0,
}
conf := fastwalk.DefaultConfig.Copy()
conf.Sort = fastwalk.SortLexical // Needed for ordering
testFastWalkConf(t, conf, map[string]string{
"a.go": "a",
"b.go": "b",
},
func(path string, de fs.DirEntry, err error) error {
requireNoError(t, err)
if de.Name() == "a.go" {
return fastwalk.SkipAll
}
return nil
},
want)
if len(want) != 3 {
t.Errorf("invalid number of files visited: wanted 3, got %v (%q)",
len(want), mapKeys(want))
}
})

t.Run("Directory", func(t *testing.T) {
want := map[string]os.FileMode{
"": os.ModeDir,
"/src": os.ModeDir,
"/src/dir1": os.ModeDir,
}
conf := fastwalk.DefaultConfig.Copy()
conf.Sort = fastwalk.SortDirsFirst // Needed for ordering
testFastWalkConf(t, conf, map[string]string{
"dir1/a.go": "a",
"dir2/a.go": "a",
},
func(path string, de fs.DirEntry, err error) error {
requireNoError(t, err)
if de.Name() == "dir1" {
return fastwalk.SkipAll
}
return nil
},
want)
if len(want) != 3 {
t.Errorf("invalid number of files visited: wanted 3, got %v (%q)",
len(want), mapKeys(want))
}
})

t.Run("Symlink", func(t *testing.T) {
want := map[string]os.FileMode{
"": os.ModeDir,
"/src": os.ModeDir,
"/src/a.go": 0,
"/src/symdir": os.ModeSymlink,
}
conf := fastwalk.DefaultConfig.Copy()
conf.Sort = fastwalk.SortFilesFirst // Needed for ordering
testFastWalkConf(t, conf, map[string]string{
"a.go": "a",
"foo/foo.go": "one",
"symdir": "LINK:foo",
},
func(path string, de fs.DirEntry, err error) error {
requireNoError(t, err)
if de.Type()&fs.ModeSymlink != 0 {
return fastwalk.SkipAll
}
return nil
},
want)
if len(want) != 4 {
t.Errorf("invalid number of files visited: wanted 4, got %v (%q)",
len(want), mapKeys(want))
}
})
}

func TestFastWalk_TraverseSymlink(t *testing.T) {
testFastWalk(t, map[string]string{
"foo/foo.go": "one",
Expand Down Expand Up @@ -1016,15 +1203,6 @@ func TestFastWalkJoinPaths(t *testing.T) {
}
}

func TestSkipAll(t *testing.T) {
err := fastwalk.Walk(nil, ".", func(path string, info fs.DirEntry, err error) error {
return fs.SkipAll
})
if err != fs.SkipAll {
t.Error("Expected fs.SkipAll to be returned got:", err)
}
}

func BenchmarkSortModeString(b *testing.B) {
var s string
for i := 0; i < b.N; i++ {
Expand Down
6 changes: 6 additions & 0 deletions fastwalk_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ func (w *walker) readDir(dirName string) error {
de := newUnixDirent(dirName, name, typ)
if w.sortMode == SortNone {
if err := w.onDirEnt(dirName, name, de); err != nil {
if err == SkipAll {
return nil
}
if err == ErrSkipFiles {
skipFiles = true
continue
Expand All @@ -97,6 +100,9 @@ func (w *walker) readDir(dirName string) error {
continue
}
if err := w.onDirEnt(dirName, d.Name(), d); err != nil {
if err == SkipAll {
return nil
}
if err != ErrSkipFiles {
return err
}
Expand Down

0 comments on commit fd56359

Please sign in to comment.