ncdu/scan: add unit tests for the scan package
Tests cover Dir size/count accounting, AttrI behaviour for files and directories, error propagation, and Remove correctly updating ancestor totals up the tree.
This commit is contained in:
@@ -0,0 +1,247 @@
|
||||
package scan
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
_ "github.com/rclone/rclone/backend/local"
|
||||
"github.com/rclone/rclone/fs"
|
||||
"github.com/rclone/rclone/fstest"
|
||||
"github.com/rclone/rclone/fstest/mockdir"
|
||||
"github.com/rclone/rclone/fstest/mockobject"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestScan(t *testing.T) {
|
||||
fstest.Initialise()
|
||||
|
||||
// testfiles (from cmd/tree) contains: file1, file2, file3 (empty), subdir/{file4,file5} (empty)
|
||||
f, err := fs.NewFs(context.Background(), "../../tree/testfiles")
|
||||
require.NoError(t, err)
|
||||
|
||||
rootChan, errChan, updatedChan := Scan(context.Background(), f)
|
||||
|
||||
root := <-rootChan
|
||||
require.NotNil(t, root)
|
||||
require.NoError(t, <-errChan)
|
||||
|
||||
assert.Equal(t, "", root.Path())
|
||||
assert.Nil(t, root.Parent())
|
||||
|
||||
size, count := root.Attr()
|
||||
assert.Equal(t, int64(0), size) // all files are empty
|
||||
assert.Equal(t, int64(5), count) // 3 root files + 2 subdir files
|
||||
|
||||
// Entries are sorted: file1, file2, file3, subdir
|
||||
require.Len(t, root.Entries(), 4)
|
||||
i := indexByName(root.Entries(), "subdir")
|
||||
require.NotEqual(t, -1, i, "subdir not found")
|
||||
subDir, _ := root.GetDir(i)
|
||||
require.NotNil(t, subDir)
|
||||
|
||||
assert.Equal(t, "subdir", subDir.Path())
|
||||
assert.Equal(t, root, subDir.Parent())
|
||||
|
||||
subSize, subCount := subDir.Attr()
|
||||
assert.Equal(t, int64(0), subSize)
|
||||
assert.Equal(t, int64(2), subCount)
|
||||
|
||||
select {
|
||||
case <-updatedChan:
|
||||
// at least one update was signalled during the scan
|
||||
default:
|
||||
t.Error("expected at least one update signal from Scan")
|
||||
}
|
||||
}
|
||||
|
||||
// indexByName returns the index of the entry with the given name, or -1 if not found.
|
||||
func indexByName(entries fs.DirEntries, name string) int {
|
||||
for i, e := range entries {
|
||||
if e.Remote() == name {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func TestAttrsAverageSize(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
a Attrs
|
||||
want float64
|
||||
}{
|
||||
{"no files", Attrs{Count: 0}, 0},
|
||||
{"all sizes known", Attrs{Count: 4, Size: 100}, 25},
|
||||
{"some sizes unknown", Attrs{Count: 4, CountUnknownSize: 2, Size: 100}, 50},
|
||||
{"all sizes unknown", Attrs{Count: 3, CountUnknownSize: 3}, 0},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert.Equal(t, tc.want, tc.a.AverageSize())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// fileEntry returns a mock fs.Object with the given name and content.
|
||||
// The size of the object equals len(content).
|
||||
func fileEntry(name, content string) fs.Object {
|
||||
return mockobject.New(name).WithContent([]byte(content), mockobject.SeekModeNone)
|
||||
}
|
||||
|
||||
func TestNewDirSizeAndCount(t *testing.T) {
|
||||
entries := fs.DirEntries{
|
||||
fileEntry("file1", "hello"), // 5 bytes
|
||||
fileEntry("file2", "hi"), // 2 bytes
|
||||
mockdir.New("subdir"),
|
||||
}
|
||||
d := newDir(nil, "", entries, nil)
|
||||
|
||||
size, count := d.Attr()
|
||||
assert.Equal(t, int64(7), size)
|
||||
assert.Equal(t, int64(2), count) // dirs are not counted, only files
|
||||
}
|
||||
|
||||
func TestNewDirUnknownSize(t *testing.T) {
|
||||
obj := mockobject.New("unknown").WithContent([]byte("some content"), mockobject.SeekModeNone)
|
||||
obj.SetUnknownSize(true) // simulate a backend that doesn't know the object size
|
||||
|
||||
d := newDir(nil, "", fs.DirEntries{obj}, nil)
|
||||
|
||||
size, count := d.Attr()
|
||||
assert.Equal(t, int64(0), size) // unknown size treated as 0
|
||||
assert.Equal(t, int64(1), count) // still counted as a file
|
||||
}
|
||||
|
||||
func TestNewDirReadErrorSetsEntriesHaveErrors(t *testing.T) {
|
||||
// AttrI returns subDir.entriesHaveErrors (not subDir.readError directly).
|
||||
// entriesHaveErrors is set on a dir when one of *its* children has a readError.
|
||||
// So we need three levels: root → child → grandchild(readError),
|
||||
// then root.AttrI(child) reflects child.entriesHaveErrors == true.
|
||||
root := newDir(nil, "", fs.DirEntries{mockdir.New("child")}, nil)
|
||||
child := newDir(root, "child", fs.DirEntries{mockdir.New("child/grand")}, nil)
|
||||
newDir(child, "child/grand", fs.DirEntries{}, fmt.Errorf("permission denied"))
|
||||
|
||||
i := indexByName(root.Entries(), "child")
|
||||
require.NotEqual(t, -1, i)
|
||||
attrs, err := root.AttrI(i)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, attrs.EntriesHaveErrors)
|
||||
}
|
||||
|
||||
func TestAttrIFile(t *testing.T) {
|
||||
d := newDir(nil, "", fs.DirEntries{fileEntry("f", "hello")}, nil)
|
||||
attrs, err := d.AttrI(0)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, attrs.IsDir)
|
||||
assert.True(t, attrs.Readable)
|
||||
assert.Equal(t, int64(5), attrs.Size)
|
||||
}
|
||||
|
||||
func TestAttrIDirLoaded(t *testing.T) {
|
||||
root := newDir(nil, "", fs.DirEntries{mockdir.New("child")}, nil)
|
||||
newDir(root, "child", fs.DirEntries{fileEntry("child/f", "hello")}, nil)
|
||||
|
||||
i := indexByName(root.Entries(), "child")
|
||||
require.NotEqual(t, -1, i, "child not found")
|
||||
attrs, err := root.AttrI(i)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, attrs.IsDir)
|
||||
assert.True(t, attrs.Readable)
|
||||
assert.Equal(t, int64(5), attrs.Size)
|
||||
assert.Equal(t, int64(1), attrs.Count)
|
||||
}
|
||||
|
||||
func TestAttrIDirUnloaded(t *testing.T) {
|
||||
// Directory entry present in entries but no child Dir created yet.
|
||||
d := newDir(nil, "", fs.DirEntries{mockdir.New("missing")}, nil)
|
||||
attrs, err := d.AttrI(0)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, attrs.IsDir)
|
||||
assert.False(t, attrs.Readable)
|
||||
assert.Equal(t, int64(0), attrs.Size)
|
||||
}
|
||||
|
||||
func TestAttrWithModTimeI(t *testing.T) {
|
||||
obj := mockobject.New("file").WithContent([]byte("abc"), mockobject.SeekModeNone)
|
||||
d := newDir(nil, "", fs.DirEntries{obj}, nil)
|
||||
attrs, err := d.AttrWithModTimeI(context.Background(), 0)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, attrs.IsDir)
|
||||
assert.Equal(t, int64(3), attrs.Size)
|
||||
assert.Equal(t, obj.ModTime(context.Background()), attrs.ModTime)
|
||||
}
|
||||
|
||||
func TestRemoveFileUpdatesDir(t *testing.T) {
|
||||
entries := fs.DirEntries{
|
||||
fileEntry("file1", "hello"), // 5 bytes
|
||||
fileEntry("file2", "hi"), // 2 bytes
|
||||
}
|
||||
d := newDir(nil, "", entries, nil)
|
||||
|
||||
i := indexByName(d.Entries(), "file1")
|
||||
require.NotEqual(t, -1, i)
|
||||
d.Remove(i)
|
||||
|
||||
size, count := d.Attr()
|
||||
assert.Equal(t, int64(2), size)
|
||||
assert.Equal(t, int64(1), count)
|
||||
assert.Len(t, d.Entries(), 1)
|
||||
}
|
||||
|
||||
func TestRemoveUnknownSizeFile(t *testing.T) {
|
||||
obj := mockobject.New("unknown").WithContent([]byte("data"), mockobject.SeekModeNone)
|
||||
obj.SetUnknownSize(true)
|
||||
|
||||
d := newDir(nil, "", fs.DirEntries{obj}, nil)
|
||||
d.Remove(0)
|
||||
|
||||
size, count := d.Attr()
|
||||
assert.Equal(t, int64(0), size)
|
||||
assert.Equal(t, int64(0), count)
|
||||
}
|
||||
|
||||
func TestRemoveDirUpdatesParent(t *testing.T) {
|
||||
root := newDir(nil, "", fs.DirEntries{mockdir.New("child")}, nil)
|
||||
newDir(root, "child", fs.DirEntries{
|
||||
fileEntry("child/file1", "hello"), // 5 bytes
|
||||
fileEntry("child/file2", "world"), // 5 bytes
|
||||
}, nil)
|
||||
|
||||
rootSize, rootCount := root.Attr()
|
||||
assert.Equal(t, int64(10), rootSize)
|
||||
assert.Equal(t, int64(2), rootCount)
|
||||
|
||||
i := indexByName(root.Entries(), "child")
|
||||
require.NotEqual(t, -1, i)
|
||||
root.Remove(i)
|
||||
|
||||
rootSize, rootCount = root.Attr()
|
||||
assert.Equal(t, int64(0), rootSize)
|
||||
assert.Equal(t, int64(0), rootCount)
|
||||
assert.Empty(t, root.Entries())
|
||||
}
|
||||
|
||||
func TestRemoveFilePropagatesCountsToParent(t *testing.T) {
|
||||
root := newDir(nil, "", fs.DirEntries{mockdir.New("child")}, nil)
|
||||
|
||||
childEntries := fs.DirEntries{
|
||||
fileEntry("child/file1", "hello"), // 5 bytes
|
||||
fileEntry("child/file2", "world"), // 5 bytes
|
||||
}
|
||||
child := newDir(root, "child", childEntries, nil)
|
||||
|
||||
// After building the tree: root accumulates child's files (10 bytes, 2 files)
|
||||
rootSize, rootCount := root.Attr()
|
||||
assert.Equal(t, int64(10), rootSize)
|
||||
assert.Equal(t, int64(2), rootCount)
|
||||
|
||||
i := indexByName(child.Entries(), "child/file1")
|
||||
require.NotEqual(t, -1, i)
|
||||
child.Remove(i)
|
||||
|
||||
// After removing child/file1: root should reflect the remaining 5 bytes and 1 file
|
||||
rootSize, rootCount = root.Attr()
|
||||
assert.Equal(t, int64(5), rootSize)
|
||||
assert.Equal(t, int64(1), rootCount)
|
||||
}
|
||||
Reference in New Issue
Block a user