Skip to content

Commit 11d563c

Browse files
authored
Merge pull request #65 from LandonTClipp/preOrderDFS
Add ErrWalkSkipSubtree
2 parents 136345e + 93caedc commit 11d563c

File tree

3 files changed

+236
-40
lines changed

3 files changed

+236
-40
lines changed

errors.go

+12-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@ var (
1414
ErrLstatNotPossible = fmt.Errorf("lstat is not possible")
1515
// ErrRelativeTo indicates that we could not make one path relative to another
1616
ErrRelativeTo = fmt.Errorf("failed to make path relative to other")
17-
// ErrStopWalk indicates to the Walk function that the walk should be aborted
18-
ErrStopWalk = fmt.Errorf("stop filesystem walk")
17+
ErrWalk = fmt.Errorf("walk control")
18+
// ErrSkipSubtree indicates to the walk function that the current subtree of
19+
// directories should be skipped. It's recommended to only use this error
20+
// with the AlgorithmPreOrderDepthFirst algorithm, as many other walk algorithms
21+
// will not respect this error due to the nature of the ordering in which the
22+
// algorithms visit each node of the filesystem tree.
23+
ErrWalkSkipSubtree = fmt.Errorf("skip subtree: %w", ErrWalk)
24+
// ErrStopWalk indicates to the Walk function that the walk should be aborted.
25+
// DEPRECATED: Use ErrWalkStop
26+
ErrStopWalk = ErrWalkStop
27+
// ErrWalkStop indicates to the Walk function that the walk should be aborted.
28+
ErrWalkStop = fmt.Errorf("stop filesystem walk: %w", ErrWalk)
1929
)

walk.go

+18-9
Original file line numberDiff line numberDiff line change
@@ -90,9 +90,13 @@ const (
9090
// AlgorithmDepthFirst is a walk algorithm. More specifically, it is a post-order
9191
// depth first search whereby subdirectories are recursed into before
9292
// visiting the children of the current directory.
93+
// DEPRECATED: Use AlgorithmPostOrderDepthFirst
9394
AlgorithmDepthFirst
94-
// AlgorithmPreOrderDepthFirst is a walk algorithm. It visits all of a node's children
95-
// before recursing into the subdirectories.
95+
// AlgorithmPostOrderDepthFirst is a walk algorithm that recurses into all of its children
96+
// before visiting any of a node's elements.
97+
AlgorithmPostOrderDepthFirst
98+
// AlgorithmPreOrderDepthFirst is a walk algorithm. It visits all of a node's elements
99+
// before recursing into its children.
96100
AlgorithmPreOrderDepthFirst
97101
)
98102

@@ -205,7 +209,7 @@ func (w *Walk) walkDFS(walkFn WalkFunc, root *Path, currentDepth int) error {
205209
// Since we are doing depth-first, we have to first recurse through all the directories,
206210
// and save all non-directory objects so we can defer handling at a later time.
207211
if IsDir(info.Mode()) {
208-
if err := w.walkDFS(walkFn, child, currentDepth+1); err != nil {
212+
if err := w.walkDFS(walkFn, child, currentDepth+1); err != nil && !errors.Is(err, ErrWalkSkipSubtree) {
209213
return err
210214
}
211215
}
@@ -316,7 +320,9 @@ func (w *Walk) walkBasic(walkFn WalkFunc, root *Path, currentDepth int) error {
316320

317321
err := w.iterateImmediateChildren(root, func(child *Path, info os.FileInfo, encounteredErr error) error {
318322
if IsDir(info.Mode()) {
319-
if err := w.walkBasic(walkFn, child, currentDepth+1); err != nil {
323+
// In the case the error is ErrWalkSkipSubtree, we ignore it as we've already
324+
// exited from the recursive call. Any other error should be bubbled up.
325+
if err := w.walkBasic(walkFn, child, currentDepth+1); err != nil && !errors.Is(err, ErrWalkSkipSubtree) {
320326
return err
321327
}
322328
}
@@ -364,7 +370,7 @@ func (w *Walk) walkPreOrderDFS(walkFn WalkFunc, root *Path, currentDepth int) er
364370
return err
365371
}
366372
for _, dir := range dirs {
367-
if err := w.walkPreOrderDFS(walkFn, dir, currentDepth+1); err != nil {
373+
if err := w.walkPreOrderDFS(walkFn, dir, currentDepth+1); err != nil && !errors.Is(err, ErrWalkSkipSubtree) {
368374
return err
369375
}
370376
}
@@ -374,12 +380,15 @@ func (w *Walk) walkPreOrderDFS(walkFn WalkFunc, root *Path, currentDepth int) er
374380
// WalkFunc is the function provided to the Walk function for each directory.
375381
type WalkFunc func(path *Path, info os.FileInfo, err error) error
376382

377-
// Walk walks the directory using the algorithm specified in the configuration.
383+
// Walk walks the directory using the algorithm specified in the configuration. Your WalkFunc
384+
// may return any of the ErrWalk* errors to control various behavior of the walker. See the documentation
385+
// of each error for more details.
378386
func (w *Walk) Walk(walkFn WalkFunc) error {
379387
funcs := map[Algorithm]func(walkFn WalkFunc, root *Path, currentDepth int) error{
380-
AlgorithmBasic: w.walkBasic,
381-
AlgorithmDepthFirst: w.walkDFS,
382-
AlgorithmPreOrderDepthFirst: w.walkPreOrderDFS,
388+
AlgorithmBasic: w.walkBasic,
389+
AlgorithmDepthFirst: w.walkDFS,
390+
AlgorithmPostOrderDepthFirst: w.walkDFS,
391+
AlgorithmPreOrderDepthFirst: w.walkPreOrderDFS,
383392
}
384393
algoFunc, ok := funcs[w.Opts.Algorithm]
385394
if !ok {

walk_test.go

+206-29
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
os "os"
66
"reflect"
7+
"slices"
78
"testing"
89

910
"github.com/spf13/afero"
@@ -296,36 +297,212 @@ func TestNewWalk(t *testing.T) {
296297
}
297298
}
298299

299-
func TestWalkPreOrderDFS(t *testing.T) {
300-
root := NewPath(t.TempDir())
301-
children := []string{
302-
"1.txt",
303-
"2.txt",
304-
"3.txt",
305-
"subdir/4.txt",
306-
"subdir/5.txt",
300+
type FSObject struct {
301+
path *Path
302+
contents string
303+
dir bool
304+
}
305+
306+
func TestWalkerOrder(t *testing.T) {
307+
type test struct {
308+
name string
309+
algorithm Algorithm
310+
walkOpts []WalkOptsFunc
311+
objects []FSObject
312+
expectedOrder []*Path
313+
}
314+
for _, tt := range []test{
315+
{
316+
name: "Pre-Order DFS simple",
317+
algorithm: AlgorithmPreOrderDepthFirst,
318+
objects: []FSObject{
319+
{path: NewPath("1.txt")},
320+
{path: NewPath("2.txt")},
321+
{path: NewPath("3.txt")},
322+
{path: NewPath("subdir"), dir: true},
323+
{path: NewPath("subdir").Join("4.txt")},
324+
},
325+
walkOpts: []WalkOptsFunc{WalkVisitDirs(true)},
326+
expectedOrder: []*Path{
327+
NewPath("1.txt"),
328+
NewPath("2.txt"),
329+
NewPath("3.txt"),
330+
NewPath("subdir"),
331+
NewPath("subdir").Join("4.txt"),
332+
},
333+
},
334+
{
335+
name: "Post-Order DFS simple",
336+
algorithm: AlgorithmDepthFirst,
337+
objects: []FSObject{
338+
{path: NewPath("1.txt")},
339+
{path: NewPath("2.txt")},
340+
{path: NewPath("3.txt")},
341+
{path: NewPath("subdir"), dir: true},
342+
{path: NewPath("subdir").Join("4.txt")},
343+
},
344+
walkOpts: []WalkOptsFunc{WalkVisitDirs(true)},
345+
expectedOrder: []*Path{
346+
NewPath("subdir").Join("4.txt"),
347+
NewPath("1.txt"),
348+
NewPath("2.txt"),
349+
NewPath("3.txt"),
350+
NewPath("subdir"),
351+
},
352+
},
353+
{
354+
name: "Basic simple",
355+
algorithm: AlgorithmBasic,
356+
objects: []FSObject{
357+
{path: NewPath("1")},
358+
{path: NewPath("2"), dir: true},
359+
{path: NewPath("2").Join("3")},
360+
{path: NewPath("4")},
361+
},
362+
walkOpts: []WalkOptsFunc{WalkVisitDirs(true)},
363+
expectedOrder: []*Path{
364+
NewPath("1"),
365+
NewPath("2").Join("3"),
366+
NewPath("2"),
367+
NewPath("4"),
368+
},
369+
},
370+
} {
371+
t.Run(tt.name, func(t *testing.T) {
372+
root := NewPath(t.TempDir())
373+
for _, child := range tt.objects {
374+
c := root.JoinPath(child.path)
375+
if child.dir {
376+
require.NoError(t, c.Mkdir())
377+
continue
378+
}
379+
require.NoError(t, c.WriteFile([]byte(child.contents)))
380+
}
381+
opts := []WalkOptsFunc{WalkAlgorithm(tt.algorithm), WalkSortChildren(true)}
382+
opts = append(opts, tt.walkOpts...)
383+
walker, err := NewWalk(root, opts...)
384+
require.NoError(t, err)
385+
386+
actualOrder := []*Path{}
387+
require.NoError(
388+
t,
389+
walker.Walk(func(path *Path, info os.FileInfo, err error) error {
390+
require.NoError(t, err)
391+
relative, err := path.RelativeTo(root)
392+
require.NoError(t, err)
393+
actualOrder = append(actualOrder, relative)
394+
return nil
395+
}),
396+
)
397+
require.Equal(t, len(tt.expectedOrder), len(actualOrder))
398+
for i, path := range tt.expectedOrder {
399+
assert.True(t, path.Equals(actualOrder[i]), "incorrect ordering at %d: %s != %s", i, path, actualOrder[i])
400+
}
401+
})
402+
}
403+
}
404+
405+
// TestErrWalkSkipSubtree tests the behavior of each algorithm when we tell it to skip a subtree.
406+
func TestErrWalkSkipSubtree(t *testing.T) {
407+
type test struct {
408+
name string
409+
algorithm Algorithm
410+
tree []*Path
411+
skipAt *Path
412+
expected []*Path
307413
}
308-
for _, child := range children {
309-
c := root.Join(child)
310-
require.NoError(t, c.Parent().MkdirAll())
311-
require.NoError(t, c.WriteFile([]byte("hello")))
312414

415+
for _, tt := range []test{
416+
{
417+
// In AlgorithmBasic, the ordering in which children/nodes are visited
418+
// is filesystem and OS dependent. Some filesystems return paths in a lexically-ordered
419+
// manner, some return them in the order in which they were created. For this test,
420+
// we tell the walker to order the children before iterating over them. That way,
421+
// the test will visit "subdir1/subdir2/foo.txt" before "subdir1/subdir2/subdir3/foo.txt",
422+
// in which case we would tell the walker to skip the subdir3 subtree before it recursed.
423+
"Basic",
424+
AlgorithmBasic,
425+
nil,
426+
NewPath("subdir1").Join("subdir2", "foo.txt"),
427+
[]*Path{
428+
NewPath("foo1.txt"),
429+
NewPath("subdir1").Join("foo.txt"),
430+
NewPath("subdir1").Join("subdir2", "foo.txt"),
431+
},
432+
},
433+
{
434+
"PreOrderDFS",
435+
AlgorithmPreOrderDepthFirst,
436+
nil,
437+
NewPath("subdir1").Join("subdir2", "foo.txt"),
438+
[]*Path{
439+
NewPath("foo1.txt"),
440+
NewPath("subdir1").Join("foo.txt"),
441+
NewPath("subdir1").Join("subdir2", "foo.txt"),
442+
},
443+
},
444+
// Note about the PostOrderDFS case. ErrWalkSkipSubtree effectively
445+
// has no meaning to this algorithm because in this case, the algorithm
446+
// visits all children before visiting each node. Thus, our WalkFunc has
447+
// no opportunity to tell it to skip a particular subtree. This test
448+
// serves to ensure this behavior doesn't change.
449+
{
450+
"PostOrderDFS",
451+
AlgorithmPostOrderDepthFirst,
452+
nil,
453+
NewPath("subdir1").Join("subdir2", "foo.txt"),
454+
[]*Path{
455+
NewPath("foo1.txt"),
456+
NewPath("subdir1").Join("foo.txt"),
457+
NewPath("subdir1").Join("subdir2", "foo.txt"),
458+
NewPath("subdir1").Join("subdir2", "subdir3", "foo.txt"),
459+
},
460+
},
461+
} {
462+
t.Run(tt.name, func(t *testing.T) {
463+
root := NewPath(t.TempDir())
464+
walker, err := NewWalk(root, WalkAlgorithm(tt.algorithm), WalkVisitDirs(false), WalkSortChildren(true))
465+
require.NoError(t, err)
466+
467+
var tree []*Path
468+
if tt.tree == nil {
469+
tree = []*Path{
470+
NewPath("foo1.txt"),
471+
NewPath("subdir1").Join("foo.txt"),
472+
NewPath("subdir1").Join("subdir2", "foo.txt"),
473+
NewPath("subdir1").Join("subdir2", "subdir3", "foo.txt"),
474+
}
475+
}
476+
for _, path := range tree {
477+
p := root.JoinPath(path)
478+
require.NoError(t, p.Parent().MkdirAll())
479+
require.NoError(t, p.WriteFile([]byte("")))
480+
}
481+
482+
visited := map[string]struct{}{}
483+
require.NoError(t, walker.Walk(func(path *Path, info os.FileInfo, err error) error {
484+
t.Logf("visited: %v", path.String())
485+
require.NoError(t, err)
486+
rel, err := path.RelativeTo(root)
487+
require.NoError(t, err)
488+
visited[rel.String()] = struct{}{}
489+
if rel.Equals(tt.skipAt) {
490+
return ErrWalkSkipSubtree
491+
}
492+
return nil
493+
}))
494+
visitedSorted := []string{}
495+
for key := range visited {
496+
visitedSorted = append(visitedSorted, key)
497+
}
498+
slices.Sort(visitedSorted)
499+
500+
expected := []string{}
501+
for _, path := range tt.expected {
502+
expected = append(expected, path.String())
503+
}
504+
assert.Equal(t, expected, visitedSorted)
505+
506+
})
313507
}
314-
walker, err := NewWalk(
315-
root,
316-
WalkAlgorithm(AlgorithmPreOrderDepthFirst),
317-
WalkSortChildren(true),
318-
WalkVisitDirs(false),
319-
)
320-
require.NoError(t, err)
321-
seenChildren := []string{}
322-
err = walker.Walk(func(path *Path, info os.FileInfo, err error) error {
323-
require.NoError(t, err)
324-
relative, err := path.RelativeTo(root)
325-
require.NoError(t, err)
326-
seenChildren = append(seenChildren, relative.String())
327-
return nil
328-
})
329-
require.NoError(t, err)
330-
assert.Equal(t, children, seenChildren)
331508
}

0 commit comments

Comments
 (0)