rclone/fs/walk/walk_test.go
Nick Craig-Wood 11da2a6c9b Break the fs package up into smaller parts.
The purpose of this is to make it easier to maintain and eventually to
allow the rclone backends to be re-used in other projects without
having to use the rclone configuration system.

The new code layout is documented in CONTRIBUTING.
2018-01-15 17:51:14 +00:00

636 lines
14 KiB
Go

package walk
import (
"fmt"
"sync"
"testing"
"github.com/ncw/rclone/fs"
"github.com/ncw/rclone/fs/filter"
"github.com/ncw/rclone/fstest/mockdir"
"github.com/ncw/rclone/fstest/mockobject"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type (
listResult struct {
entries fs.DirEntries
err error
}
listResults map[string]listResult
errorMap map[string]error
listDirs struct {
mu sync.Mutex
t *testing.T
fs fs.Fs
includeAll bool
results listResults
walkResults listResults
walkErrors errorMap
finalError error
checkMaps bool
maxLevel int
}
)
func newListDirs(t *testing.T, f fs.Fs, includeAll bool, results listResults, walkErrors errorMap, finalError error) *listDirs {
return &listDirs{
t: t,
fs: f,
includeAll: includeAll,
results: results,
walkErrors: walkErrors,
walkResults: listResults{},
finalError: finalError,
checkMaps: true,
maxLevel: -1,
}
}
// NoCheckMaps marks the maps as to be ignored at the end
func (ls *listDirs) NoCheckMaps() *listDirs {
ls.checkMaps = false
return ls
}
// SetLevel(1) turns off recursion
func (ls *listDirs) SetLevel(maxLevel int) *listDirs {
ls.maxLevel = maxLevel
return ls
}
// ListDir returns the expected listing for the directory
func (ls *listDirs) ListDir(f fs.Fs, includeAll bool, dir string) (entries fs.DirEntries, err error) {
ls.mu.Lock()
defer ls.mu.Unlock()
assert.Equal(ls.t, ls.fs, f)
assert.Equal(ls.t, ls.includeAll, includeAll)
// Fetch results for this path
result, ok := ls.results[dir]
if !ok {
ls.t.Errorf("Unexpected list of %q", dir)
return nil, errors.New("unexpected list")
}
delete(ls.results, dir)
// Put expected results for call of WalkFn
ls.walkResults[dir] = result
return result.entries, result.err
}
// ListR returns the expected listing for the directory using ListR
func (ls *listDirs) ListR(dir string, callback fs.ListRCallback) (err error) {
ls.mu.Lock()
defer ls.mu.Unlock()
var errorReturn error
for dirPath, result := range ls.results {
// Put expected results for call of WalkFn
// Note that we don't call the function at all if we got an error
if result.err != nil {
errorReturn = result.err
}
if errorReturn == nil {
err = callback(result.entries)
require.NoError(ls.t, err)
ls.walkResults[dirPath] = result
}
}
ls.results = listResults{}
return errorReturn
}
// IsFinished checks everything expected was used up
func (ls *listDirs) IsFinished() {
if ls.checkMaps {
assert.Equal(ls.t, errorMap{}, ls.walkErrors)
assert.Equal(ls.t, listResults{}, ls.results)
assert.Equal(ls.t, listResults{}, ls.walkResults)
}
}
// WalkFn is called by the walk to test the expectations
func (ls *listDirs) WalkFn(dir string, entries fs.DirEntries, err error) error {
ls.mu.Lock()
defer ls.mu.Unlock()
// ls.t.Logf("WalkFn(%q, %v, %q)", dir, entries, err)
// Fetch expected entries and err
result, ok := ls.walkResults[dir]
if !ok {
ls.t.Errorf("Unexpected walk of %q (result not found)", dir)
return errors.New("result not found")
}
delete(ls.walkResults, dir)
// Check arguments are as expected
assert.Equal(ls.t, result.entries, entries)
assert.Equal(ls.t, result.err, err)
// Fetch return value
returnErr, ok := ls.walkErrors[dir]
if !ok {
ls.t.Errorf("Unexpected walk of %q (error not found)", dir)
return errors.New("error not found")
}
delete(ls.walkErrors, dir)
return returnErr
}
// Walk does the walk and tests the expectations
func (ls *listDirs) Walk() {
err := walk(nil, "", ls.includeAll, ls.maxLevel, ls.WalkFn, ls.ListDir)
assert.Equal(ls.t, ls.finalError, err)
ls.IsFinished()
}
// WalkR does the walkR and tests the expectations
func (ls *listDirs) WalkR() {
err := walkR(nil, "", ls.includeAll, ls.maxLevel, ls.WalkFn, ls.ListR)
assert.Equal(ls.t, ls.finalError, err)
if ls.finalError == nil {
ls.IsFinished()
}
}
func testWalkEmpty(t *testing.T) *listDirs {
return newListDirs(t, nil, false,
listResults{
"": {entries: fs.DirEntries{}, err: nil},
},
errorMap{
"": nil,
},
nil,
)
}
func TestWalkEmpty(t *testing.T) { testWalkEmpty(t).Walk() }
func TestWalkREmpty(t *testing.T) { testWalkEmpty(t).WalkR() }
func testWalkEmptySkip(t *testing.T) *listDirs {
return newListDirs(t, nil, true,
listResults{
"": {entries: fs.DirEntries{}, err: nil},
},
errorMap{
"": ErrorSkipDir,
},
nil,
)
}
func TestWalkEmptySkip(t *testing.T) { testWalkEmptySkip(t).Walk() }
func TestWalkREmptySkip(t *testing.T) { testWalkEmptySkip(t).WalkR() }
func testWalkNotFound(t *testing.T) *listDirs {
return newListDirs(t, nil, true,
listResults{
"": {err: fs.ErrorDirNotFound},
},
errorMap{
"": fs.ErrorDirNotFound,
},
fs.ErrorDirNotFound,
)
}
func TestWalkNotFound(t *testing.T) { testWalkNotFound(t).Walk() }
func TestWalkRNotFound(t *testing.T) { testWalkNotFound(t).WalkR() }
func TestWalkNotFoundMaskError(t *testing.T) {
// this doesn't work for WalkR
newListDirs(t, nil, true,
listResults{
"": {err: fs.ErrorDirNotFound},
},
errorMap{
"": nil,
},
nil,
).Walk()
}
func TestWalkNotFoundSkipkError(t *testing.T) {
// this doesn't work for WalkR
newListDirs(t, nil, true,
listResults{
"": {err: fs.ErrorDirNotFound},
},
errorMap{
"": ErrorSkipDir,
},
nil,
).Walk()
}
func testWalkLevels(t *testing.T, maxLevel int) *listDirs {
da := mockdir.New("a")
oA := mockobject.Object("A")
db := mockdir.New("a/b")
oB := mockobject.Object("a/B")
dc := mockdir.New("a/b/c")
oC := mockobject.Object("a/b/C")
dd := mockdir.New("a/b/c/d")
oD := mockobject.Object("a/b/c/D")
return newListDirs(t, nil, false,
listResults{
"": {entries: fs.DirEntries{oA, da}, err: nil},
"a": {entries: fs.DirEntries{oB, db}, err: nil},
"a/b": {entries: fs.DirEntries{oC, dc}, err: nil},
"a/b/c": {entries: fs.DirEntries{oD, dd}, err: nil},
"a/b/c/d": {entries: fs.DirEntries{}, err: nil},
},
errorMap{
"": nil,
"a": nil,
"a/b": nil,
"a/b/c": nil,
"a/b/c/d": nil,
},
nil,
).SetLevel(maxLevel)
}
func TestWalkLevels(t *testing.T) { testWalkLevels(t, -1).Walk() }
func TestWalkRLevels(t *testing.T) { testWalkLevels(t, -1).WalkR() }
func TestWalkLevelsNoRecursive10(t *testing.T) { testWalkLevels(t, 10).Walk() }
func TestWalkRLevelsNoRecursive10(t *testing.T) { testWalkLevels(t, 10).WalkR() }
func TestWalkNDirTree(t *testing.T) {
ls := testWalkLevels(t, -1)
entries, err := walkNDirTree(nil, "", ls.includeAll, ls.maxLevel, ls.ListDir)
require.NoError(t, err)
assert.Equal(t, `/
A
a/
a/
B
b/
a/b/
C
c/
a/b/c/
D
d/
a/b/c/d/
`, entries.String())
}
func testWalkLevelsNoRecursive(t *testing.T) *listDirs {
da := mockdir.New("a")
oA := mockobject.Object("A")
return newListDirs(t, nil, false,
listResults{
"": {entries: fs.DirEntries{oA, da}, err: nil},
},
errorMap{
"": nil,
},
nil,
).SetLevel(1)
}
func TestWalkLevelsNoRecursive(t *testing.T) { testWalkLevelsNoRecursive(t).Walk() }
func TestWalkRLevelsNoRecursive(t *testing.T) { testWalkLevelsNoRecursive(t).WalkR() }
func testWalkLevels2(t *testing.T) *listDirs {
da := mockdir.New("a")
oA := mockobject.Object("A")
db := mockdir.New("a/b")
oB := mockobject.Object("a/B")
return newListDirs(t, nil, false,
listResults{
"": {entries: fs.DirEntries{oA, da}, err: nil},
"a": {entries: fs.DirEntries{oB, db}, err: nil},
},
errorMap{
"": nil,
"a": nil,
},
nil,
).SetLevel(2)
}
func TestWalkLevels2(t *testing.T) { testWalkLevels2(t).Walk() }
func TestWalkRLevels2(t *testing.T) { testWalkLevels2(t).WalkR() }
func testWalkSkip(t *testing.T) *listDirs {
da := mockdir.New("a")
db := mockdir.New("a/b")
dc := mockdir.New("a/b/c")
return newListDirs(t, nil, false,
listResults{
"": {entries: fs.DirEntries{da}, err: nil},
"a": {entries: fs.DirEntries{db}, err: nil},
"a/b": {entries: fs.DirEntries{dc}, err: nil},
},
errorMap{
"": nil,
"a": nil,
"a/b": ErrorSkipDir,
},
nil,
)
}
func TestWalkSkip(t *testing.T) { testWalkSkip(t).Walk() }
func TestWalkRSkip(t *testing.T) { testWalkSkip(t).WalkR() }
func testWalkErrors(t *testing.T) *listDirs {
lr := listResults{}
em := errorMap{}
de := make(fs.DirEntries, 10)
for i := range de {
path := string('0' + i)
de[i] = mockdir.New(path)
lr[path] = listResult{entries: nil, err: fs.ErrorDirNotFound}
em[path] = fs.ErrorDirNotFound
}
lr[""] = listResult{entries: de, err: nil}
em[""] = nil
return newListDirs(t, nil, true,
lr,
em,
fs.ErrorDirNotFound,
).NoCheckMaps()
}
func TestWalkErrors(t *testing.T) { testWalkErrors(t).Walk() }
func TestWalkRErrors(t *testing.T) { testWalkErrors(t).WalkR() }
var errorBoom = errors.New("boom")
func makeTree(level int, terminalErrors bool) (listResults, errorMap) {
lr := listResults{}
em := errorMap{}
var fill func(path string, level int)
fill = func(path string, level int) {
de := fs.DirEntries{}
if level > 0 {
for _, a := range "0123456789" {
subPath := string(a)
if path != "" {
subPath = path + "/" + subPath
}
de = append(de, mockdir.New(subPath))
fill(subPath, level-1)
}
}
lr[path] = listResult{entries: de, err: nil}
em[path] = nil
if level == 0 && terminalErrors {
em[path] = errorBoom
}
}
fill("", level)
return lr, em
}
func testWalkMulti(t *testing.T) *listDirs {
lr, em := makeTree(3, false)
return newListDirs(t, nil, true,
lr,
em,
nil,
)
}
func TestWalkMulti(t *testing.T) { testWalkMulti(t).Walk() }
func TestWalkRMulti(t *testing.T) { testWalkMulti(t).WalkR() }
func testWalkMultiErrors(t *testing.T) *listDirs {
lr, em := makeTree(3, true)
return newListDirs(t, nil, true,
lr,
em,
errorBoom,
).NoCheckMaps()
}
func TestWalkMultiErrors(t *testing.T) { testWalkMultiErrors(t).Walk() }
func TestWalkRMultiErrors(t *testing.T) { testWalkMultiErrors(t).Walk() }
// a very simple listRcallback function
func makeListRCallback(entries fs.DirEntries, err error) fs.ListRFn {
return func(dir string, callback fs.ListRCallback) error {
if err == nil {
err = callback(entries)
}
return err
}
}
func TestWalkRDirTree(t *testing.T) {
for _, test := range []struct {
entries fs.DirEntries
want string
err error
root string
level int
}{
{fs.DirEntries{}, "/\n", nil, "", -1},
{fs.DirEntries{mockobject.Object("a")}, `/
a
`, nil, "", -1},
{fs.DirEntries{mockobject.Object("a/b")}, `/
a/
a/
b
`, nil, "", -1},
{fs.DirEntries{mockobject.Object("a/b/c/d")}, `/
a/
a/
b/
a/b/
c/
a/b/c/
d
`, nil, "", -1},
{fs.DirEntries{mockobject.Object("a")}, "", errorBoom, "", -1},
{fs.DirEntries{
mockobject.Object("0/1/2/3"),
mockobject.Object("4/5/6/7"),
mockobject.Object("8/9/a/b"),
mockobject.Object("c/d/e/f"),
mockobject.Object("g/h/i/j"),
mockobject.Object("k/l/m/n"),
mockobject.Object("o/p/q/r"),
mockobject.Object("s/t/u/v"),
mockobject.Object("w/x/y/z"),
}, `/
0/
4/
8/
c/
g/
k/
o/
s/
w/
0/
1/
0/1/
2/
0/1/2/
3
4/
5/
4/5/
6/
4/5/6/
7
8/
9/
8/9/
a/
8/9/a/
b
c/
d/
c/d/
e/
c/d/e/
f
g/
h/
g/h/
i/
g/h/i/
j
k/
l/
k/l/
m/
k/l/m/
n
o/
p/
o/p/
q/
o/p/q/
r
s/
t/
s/t/
u/
s/t/u/
v
w/
x/
w/x/
y/
w/x/y/
z
`, nil, "", -1},
{fs.DirEntries{
mockobject.Object("a/b/c/d/e/f1"),
mockobject.Object("a/b/c/d/e/f2"),
mockobject.Object("a/b/c/d/e/f3"),
}, `a/b/c/
d/
a/b/c/d/
e/
a/b/c/d/e/
f1
f2
f3
`, nil, "a/b/c", -1},
{fs.DirEntries{
mockobject.Object("A"),
mockobject.Object("a/B"),
mockobject.Object("a/b/C"),
mockobject.Object("a/b/c/D"),
mockobject.Object("a/b/c/d/E"),
}, `/
A
a/
a/
B
b/
`, nil, "", 2},
{fs.DirEntries{
mockobject.Object("a/b/c"),
mockobject.Object("a/b/c/d/e"),
}, `/
a/
a/
b/
`, nil, "", 2},
} {
r, err := walkRDirTree(nil, test.root, true, test.level, makeListRCallback(test.entries, test.err))
assert.Equal(t, test.err, err, fmt.Sprintf("%+v", test))
assert.Equal(t, test.want, r.String(), fmt.Sprintf("%+v", test))
}
}
func TestWalkRDirTreeExclude(t *testing.T) {
for _, test := range []struct {
entries fs.DirEntries
want string
err error
root string
level int
excludeFile string
includeAll bool
}{
{fs.DirEntries{mockobject.Object("a"), mockobject.Object("ignore")}, "", nil, "", -1, "ignore", false},
{fs.DirEntries{mockobject.Object("a")}, `/
a
`, nil, "", -1, "ignore", false},
{fs.DirEntries{
mockobject.Object("a"),
mockobject.Object("b/b"),
mockobject.Object("b/.ignore"),
}, `/
a
`, nil, "", -1, ".ignore", false},
{fs.DirEntries{
mockobject.Object("a"),
mockobject.Object("b/.ignore"),
mockobject.Object("b/b"),
}, `/
a
b/
b/
.ignore
b
`, nil, "", -1, ".ignore", true},
{fs.DirEntries{
mockobject.Object("a"),
mockobject.Object("b/b"),
mockobject.Object("b/c/d/e"),
mockobject.Object("b/c/ign"),
mockobject.Object("b/c/x"),
}, `/
a
b/
b/
b
`, nil, "", -1, "ign", false},
{fs.DirEntries{
mockobject.Object("a"),
mockobject.Object("b/b"),
mockobject.Object("b/c/d/e"),
mockobject.Object("b/c/ign"),
mockobject.Object("b/c/x"),
}, `/
a
b/
b/
b
c/
b/c/
d/
ign
x
b/c/d/
e
`, nil, "", -1, "ign", true},
} {
filter.Active.Opt.ExcludeFile = test.excludeFile
r, err := walkRDirTree(nil, test.root, test.includeAll, test.level, makeListRCallback(test.entries, test.err))
assert.Equal(t, test.err, err, fmt.Sprintf("%+v", test))
assert.Equal(t, test.want, r.String(), fmt.Sprintf("%+v", test))
}
// Set to default value, to avoid side effects
filter.Active.Opt.ExcludeFile = ""
}