From dbfa7031d2cb58f2bfbe9fba5280b454b3ec5a42 Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Sat, 7 May 2016 17:12:34 +0100 Subject: [PATCH] Factor Lister into own file, write tests and fix --- fs/fs.go | 280 ++----------------------------------- fs/lister.go | 256 +++++++++++++++++++++++++++++++++ fs/lister_test.go | 350 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 614 insertions(+), 272 deletions(-) create mode 100644 fs/lister.go create mode 100644 fs/lister_test.go diff --git a/fs/fs.go b/fs/fs.go index 7e55f1e23..e7d2b2103 100644 --- a/fs/fs.go +++ b/fs/fs.go @@ -9,8 +9,6 @@ import ( "path/filepath" "regexp" "sort" - "strings" - "sync" "time" ) @@ -95,10 +93,8 @@ func Register(info *RegInfo) { fsRegistry = append(fsRegistry, info) } -// Fs is the interface a cloud storage system must provide -type Fs interface { - Info - +// ListFser is the interface for listing a remote Fs +type ListFser interface { // List the objects and directories of the Fs starting from dir // // dir should be "" to start from the root, and should not @@ -107,6 +103,12 @@ type Fs interface { // This should return ErrDirNotFound (using out.SetError()) // if the directory isn't found. List(out ListOpts, dir string) +} + +// Fs is the interface a cloud storage system must provide +type Fs interface { + Info + ListFser // NewFsObject finds the Object at remote. Returns nil if can't be found NewFsObject(remote string) Object @@ -288,272 +290,6 @@ type ListOpts interface { IsFinished() bool } -// listerResult is returned by the lister methods -type listerResult struct { - Obj Object - Dir *Dir - Err error -} - -// Lister objects are used for controlling listing of Fs objects -type Lister struct { - mu sync.RWMutex - buffer int - abort bool - results chan listerResult - finished sync.Once - level int - filter *Filter -} - -// NewLister creates a Lister object. -// -// The default channel buffer size will be Config.Checkers unless -// overridden with SetBuffer. The default level will be infinite. -func NewLister() *Lister { - o := &Lister{} - return o.SetLevel(-1).SetBuffer(Config.Checkers) -} - -// Start starts a go routine listing the Fs passed in. It returns the -// same Lister that was passed in for convenience. -func (o *Lister) Start(f Fs, dir string) *Lister { - o.results = make(chan listerResult, o.buffer) - go func() { - f.List(o, dir) - }() - return o -} - -// SetLevel sets the level to recurse to. It returns same Lister that -// was passed in for convenience. If Level is < 0 then it sets it to -// infinite. Must be called before Start(). -func (o *Lister) SetLevel(level int) *Lister { - if level < 0 { - o.level = MaxLevel - } else { - o.level = level - } - return o -} - -// SetFilter sets the Filter that is in use. It defaults to no -// filtering. Must be called before Start(). -func (o *Lister) SetFilter(filter *Filter) *Lister { - o.filter = filter - return o -} - -// Level gets the recursion level for this listing. -// -// Fses may ignore this, but should implement it for improved efficiency if possible. -// -// Level 1 means list just the contents of the directory -// -// Each returned item must have less than level `/`s in. -func (o *Lister) Level() int { - return o.level -} - -// SetBuffer sets the channel buffer size in use. Must be called -// before Start(). -func (o *Lister) SetBuffer(buffer int) *Lister { - if buffer < 1 { - buffer = 1 - } - o.buffer = buffer - return o -} - -// Buffer gets the channel buffer size in use -func (o *Lister) Buffer() int { - return o.buffer -} - -// Add an object to the output. -// If the function returns true, the operation has been aborted. -// Multiple goroutines can safely add objects concurrently. -func (o *Lister) Add(obj Object) (abort bool) { - o.mu.RLock() - defer o.mu.RUnlock() - if o.abort { - return true - } - o.results <- listerResult{Obj: obj} - return false -} - -// AddDir will a directory to the output. -// If the function returns true, the operation has been aborted. -// Multiple goroutines can safely add objects concurrently. -func (o *Lister) AddDir(dir *Dir) (abort bool) { - o.mu.RLock() - defer o.mu.RUnlock() - if o.abort { - return true - } - remote := dir.Name - remote = strings.Trim(remote, "/") - dir.Name = remote - // Check the level and ignore if too high - slashes := strings.Count(remote, "/") - if slashes >= o.level { - return false - } - // Check if directory is included - if !o.IncludeDirectory(remote) { - return false - } - o.results <- listerResult{Dir: dir} - return false -} - -// IncludeDirectory returns whether this directory should be -// included in the listing (and recursed into or not). -func (o *Lister) IncludeDirectory(remote string) bool { - if o.filter == nil { - return true - } - return o.filter.IncludeDirectory(remote) -} - -// SetError will set an error state, and will cause the listing to -// be aborted. -// Multiple goroutines can set the error state concurrently, -// but only the first will be returned to the caller. -func (o *Lister) SetError(err error) { - o.mu.RLock() - if err != nil && !o.abort { - o.results <- listerResult{Err: err} - } - o.mu.RUnlock() - o.Finished() -} - -// Finished should be called when listing is finished -func (o *Lister) Finished() { - o.finished.Do(func() { - o.mu.Lock() - o.abort = true - close(o.results) - o.mu.Unlock() - }) -} - -// IsFinished returns whether the directory listing is finished or not -func (o *Lister) IsFinished() bool { - o.mu.RLock() - defer o.mu.RUnlock() - return o.abort -} - -// Get an object from the listing. -// Will return either an object or a directory, never both. -// Will return (nil, nil, nil) when all objects have been returned. -func (o *Lister) Get() (Object, *Dir, error) { - select { - case r := <-o.results: - return r.Obj, r.Dir, r.Err - } -} - -// Get all the objects and dirs from the listing. -func (o *Lister) GetAll() (objs []Object, dirs []*Dir, err error) { - for { - obj, dir, err := o.Get() - switch { - case err != nil: - return nil, nil, err - case obj != nil: - objs = append(objs, obj) - case dir != nil: - dirs = append(dirs, dir) - default: - return objs, dirs, nil - } - } -} - -// GetObject will return an object from the listing. -// It will skip over any directories. -// Will return (nil, nil) when all objects have been returned. -func (o *Lister) GetObject() (Object, error) { - for { - obj, dir, err := o.Get() - if err != nil { - return nil, err - } - // Check if we are finished - if dir == nil && obj == nil { - return nil, nil - } - // Ignore directories - if dir != nil { - continue - } - return obj, nil - } -} - -// GetObjects will return a slice of object from the listing. -// It will skip over any directories. -func (o *Lister) GetObjects() (objs []Object, err error) { - for { - obj, dir, err := o.Get() - if err != nil { - return nil, err - } - // Check if we are finished - if dir == nil && obj == nil { - break - } - if obj != nil { - objs = append(objs, obj) - } - } - return objs, nil -} - -// GetDir will return a directory from the listing. -// It will skip over any objects. -// Will return (nil, nil) when all objects have been returned. -func (o *Lister) GetDir() (*Dir, error) { - for { - obj, dir, err := o.Get() - if err != nil { - return nil, err - } - // Check if we are finished - if dir == nil && obj == nil { - return nil, nil - } - // Ignore objects - if obj != nil { - continue - } - return dir, nil - } -} - -// GetDirs will return a slice of directories from the listing. -// It will skip over any objects. -func (o *Lister) GetDirs() (dirs []*Dir, err error) { - for { - obj, dir, err := o.Get() - if err != nil { - return nil, err - } - // Check if we are finished - if dir == nil && obj == nil { - break - } - if dir != nil { - dirs = append(dirs, dir) - } - } - return dirs, nil -} - // Objects is a slice of Object~s type Objects []Object diff --git a/fs/lister.go b/fs/lister.go new file mode 100644 index 000000000..3361a75fd --- /dev/null +++ b/fs/lister.go @@ -0,0 +1,256 @@ +// This file implements the Lister object + +package fs + +import "sync" + +// listerResult is returned by the lister methods +type listerResult struct { + Obj Object + Dir *Dir + Err error +} + +// Lister objects are used for conniltrolling listing of Fs objects +type Lister struct { + mu sync.RWMutex + buffer int + abort bool + results chan listerResult + finished sync.Once + level int + filter *Filter +} + +// NewLister creates a Lister object. +// +// The default channel buffer size will be Config.Checkers unless +// overridden with SetBuffer. The default level will be infinite. +func NewLister() *Lister { + o := &Lister{} + return o.SetLevel(-1).SetBuffer(Config.Checkers) +} + +// Start starts a go routine listing the Fs passed in. It returns the +// same Lister that was passed in for convenience. +func (o *Lister) Start(f ListFser, dir string) *Lister { + o.results = make(chan listerResult, o.buffer) + go func() { + f.List(o, dir) + }() + return o +} + +// SetLevel sets the level to recurse to. It returns same Lister that +// was passed in for convenience. If Level is < 0 then it sets it to +// infinite. Must be called before Start(). +func (o *Lister) SetLevel(level int) *Lister { + if level < 0 { + o.level = MaxLevel + } else { + o.level = level + } + return o +} + +// SetFilter sets the Filter that is in use. It defaults to no +// filtering. Must be called before Start(). +func (o *Lister) SetFilter(filter *Filter) *Lister { + o.filter = filter + return o +} + +// Level gets the recursion level for this listing. +// +// Fses may ignore this, but should implement it for improved efficiency if possible. +// +// Level 1 means list just the contents of the directory +// +// Each returned item must have less than level `/`s in. +func (o *Lister) Level() int { + return o.level +} + +// SetBuffer sets the channel buffer size in use. Must be called +// before Start(). +func (o *Lister) SetBuffer(buffer int) *Lister { + if buffer < 1 { + buffer = 1 + } + o.buffer = buffer + return o +} + +// Buffer gets the channel buffer size in use +func (o *Lister) Buffer() int { + return o.buffer +} + +// Add an object to the output. +// If the function returns true, the operation has been aborted. +// Multiple goroutines can safely add objects concurrently. +func (o *Lister) Add(obj Object) (abort bool) { + o.mu.RLock() + defer o.mu.RUnlock() + if o.abort { + return true + } + o.results <- listerResult{Obj: obj} + return false +} + +// AddDir will a directory to the output. +// If the function returns true, the operation has been aborted. +// Multiple goroutines can safely add objects concurrently. +func (o *Lister) AddDir(dir *Dir) (abort bool) { + o.mu.RLock() + defer o.mu.RUnlock() + if o.abort { + return true + } + o.results <- listerResult{Dir: dir} + return false +} + +// IncludeDirectory returns whether this directory should be +// included in the listing (and recursed into or not). +func (o *Lister) IncludeDirectory(remote string) bool { + if o.filter == nil { + return true + } + return o.filter.IncludeDirectory(remote) +} + +// SetError will set an error state, and will cause the listing to +// be aborted. +// Multiple goroutines can set the error state concurrently, +// but only the first will be returned to the caller. +func (o *Lister) SetError(err error) { + o.mu.RLock() + if err != nil && !o.abort { + o.results <- listerResult{Err: err} + } + o.mu.RUnlock() + o.Finished() +} + +// Finished should be called when listing is finished +func (o *Lister) Finished() { + o.finished.Do(func() { + o.mu.Lock() + o.abort = true + close(o.results) + o.mu.Unlock() + }) +} + +// IsFinished returns whether the directory listing is finished or not +func (o *Lister) IsFinished() bool { + o.mu.RLock() + defer o.mu.RUnlock() + return o.abort +} + +// Get an object from the listing. +// Will return either an object or a directory, never both. +// Will return (nil, nil, nil) when all objects have been returned. +func (o *Lister) Get() (Object, *Dir, error) { + select { + case r := <-o.results: + return r.Obj, r.Dir, r.Err + } +} + +// GetAll gets all the objects and dirs from the listing. +func (o *Lister) GetAll() (objs []Object, dirs []*Dir, err error) { + for { + obj, dir, err := o.Get() + switch { + case err != nil: + return nil, nil, err + case obj != nil: + objs = append(objs, obj) + case dir != nil: + dirs = append(dirs, dir) + default: + return objs, dirs, nil + } + } +} + +// GetObject will return an object from the listing. +// It will skip over any directories. +// Will return (nil, nil) when all objects have been returned. +func (o *Lister) GetObject() (Object, error) { + for { + obj, dir, err := o.Get() + switch { + case err != nil: + return nil, err + case obj != nil: + return obj, nil + case dir != nil: + // ignore + default: + return nil, nil + } + } +} + +// GetObjects will return a slice of object from the listing. +// It will skip over any directories. +func (o *Lister) GetObjects() (objs []Object, err error) { + for { + obj, dir, err := o.Get() + switch { + case err != nil: + return nil, err + case obj != nil: + objs = append(objs, obj) + case dir != nil: + // ignore + default: + return objs, nil + } + } +} + +// GetDir will return a directory from the listing. +// It will skip over any objects. +// Will return (nil, nil) when all objects have been returned. +func (o *Lister) GetDir() (*Dir, error) { + for { + obj, dir, err := o.Get() + switch { + case err != nil: + return nil, err + case obj != nil: + // ignore + case dir != nil: + return dir, nil + default: + return nil, nil + } + } +} + +// GetDirs will return a slice of directories from the listing. +// It will skip over any objects. +func (o *Lister) GetDirs() (dirs []*Dir, err error) { + for { + obj, dir, err := o.Get() + switch { + case err != nil: + return nil, err + case obj != nil: + // ignore + case dir != nil: + dirs = append(dirs, dir) + default: + return dirs, nil + } + } +} + +// Check interface +var _ ListOpts = (*Lister)(nil) diff --git a/fs/lister_test.go b/fs/lister_test.go new file mode 100644 index 000000000..18a72c3c5 --- /dev/null +++ b/fs/lister_test.go @@ -0,0 +1,350 @@ +package fs + +import ( + "errors" + "io" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestListerNew(t *testing.T) { + o := NewLister() + assert.Equal(t, Config.Checkers, o.buffer) + assert.Equal(t, false, o.abort) + assert.Equal(t, MaxLevel, o.level) +} + +var errNotImpl = errors.New("Not implemented") + +type mockObject string + +func (o mockObject) String() string { return string(o) } +func (o mockObject) Fs() Info { return nil } +func (o mockObject) Remote() string { return string(o) } +func (o mockObject) Hash(HashType) (string, error) { return "", errNotImpl } +func (o mockObject) ModTime() (t time.Time) { return t } +func (o mockObject) Size() int64 { return 0 } +func (o mockObject) Storable() bool { return true } +func (o mockObject) SetModTime(time.Time) error { return errNotImpl } +func (o mockObject) Open() (io.ReadCloser, error) { return nil, errNotImpl } +func (o mockObject) Update(in io.Reader, src ObjectInfo) error { return errNotImpl } +func (o mockObject) Remove() error { return errNotImpl } + +type mockFs struct { + listFn func(o ListOpts, dir string) +} + +func (f *mockFs) List(o ListOpts, dir string) { + defer o.Finished() + f.listFn(o, dir) +} + +func TestListerStart(t *testing.T) { + f := &mockFs{} + ranList := false + f.listFn = func(o ListOpts, dir string) { + ranList = true + } + o := NewLister().Start(f, "") + objs, dirs, err := o.GetAll() + require.Nil(t, err) + assert.Len(t, objs, 0) + assert.Len(t, dirs, 0) + assert.Equal(t, true, ranList) +} + +func TestListerSetLevel(t *testing.T) { + o := NewLister() + o.SetLevel(1) + assert.Equal(t, 1, o.level) + o.SetLevel(0) + assert.Equal(t, 0, o.level) + o.SetLevel(-1) + assert.Equal(t, MaxLevel, o.level) +} + +func TestListerSetFilter(t *testing.T) { + filter := &Filter{} + o := NewLister().SetFilter(filter) + assert.Equal(t, filter, o.filter) +} + +func TestListerLevel(t *testing.T) { + o := NewLister() + assert.Equal(t, MaxLevel, o.Level()) + o.SetLevel(123) + assert.Equal(t, 123, o.Level()) +} + +func TestListerSetBuffer(t *testing.T) { + o := NewLister() + o.SetBuffer(2) + assert.Equal(t, 2, o.buffer) + o.SetBuffer(1) + assert.Equal(t, 1, o.buffer) + o.SetBuffer(0) + assert.Equal(t, 1, o.buffer) + o.SetBuffer(-1) + assert.Equal(t, 1, o.buffer) +} + +func TestListerBuffer(t *testing.T) { + o := NewLister() + assert.Equal(t, Config.Checkers, o.Buffer()) + o.SetBuffer(123) + assert.Equal(t, 123, o.Buffer()) +} + +func TestListerAdd(t *testing.T) { + f := &mockFs{} + objs := []Object{ + mockObject("1"), + mockObject("2"), + } + f.listFn = func(o ListOpts, dir string) { + for _, obj := range objs { + assert.Equal(t, false, o.Add(obj)) + } + } + o := NewLister().Start(f, "") + gotObjs, gotDirs, err := o.GetAll() + require.Nil(t, err) + assert.Equal(t, objs, gotObjs) + assert.Len(t, gotDirs, 0) +} + +func TestListerAddDir(t *testing.T) { + f := &mockFs{} + dirs := []*Dir{ + &Dir{Name: "1"}, + &Dir{Name: "2"}, + } + f.listFn = func(o ListOpts, dir string) { + for _, dir := range dirs { + assert.Equal(t, false, o.AddDir(dir)) + } + } + o := NewLister().Start(f, "") + gotObjs, gotDirs, err := o.GetAll() + require.Nil(t, err) + assert.Len(t, gotObjs, 0) + assert.Equal(t, dirs, gotDirs) +} + +func TestListerIncludeDirectory(t *testing.T) { + o := NewLister() + assert.Equal(t, true, o.IncludeDirectory("whatever")) + filter, err := NewFilter() + require.Nil(t, err) + require.NotNil(t, filter) + require.Nil(t, filter.AddRule("!")) + require.Nil(t, filter.AddRule("+ potato/*")) + require.Nil(t, filter.AddRule("- *")) + o.SetFilter(filter) + assert.Equal(t, false, o.IncludeDirectory("floop")) + assert.Equal(t, true, o.IncludeDirectory("potato")) + assert.Equal(t, false, o.IncludeDirectory("potato/sausage")) +} + +func TestListerSetError(t *testing.T) { + f := &mockFs{} + f.listFn = func(o ListOpts, dir string) { + assert.Equal(t, false, o.Add(mockObject("1"))) + o.SetError(errNotImpl) + assert.Equal(t, true, o.Add(mockObject("2"))) + o.SetError(errors.New("not signalled")) + assert.Equal(t, true, o.AddDir(&Dir{Name: "2"})) + } + o := NewLister().Start(f, "") + gotObjs, gotDirs, err := o.GetAll() + assert.Equal(t, err, errNotImpl) + assert.Nil(t, gotObjs) + assert.Nil(t, gotDirs) +} + +func TestListerIsFinished(t *testing.T) { + f := &mockFs{} + f.listFn = func(o ListOpts, dir string) { + assert.Equal(t, false, o.IsFinished()) + o.Finished() + assert.Equal(t, true, o.IsFinished()) + } + o := NewLister().Start(f, "") + gotObjs, gotDirs, err := o.GetAll() + assert.Nil(t, err) + assert.Len(t, gotObjs, 0) + assert.Len(t, gotDirs, 0) +} + +func testListerGet(t *testing.T) *Lister { + f := &mockFs{} + f.listFn = func(o ListOpts, dir string) { + assert.Equal(t, false, o.Add(mockObject("1"))) + assert.Equal(t, false, o.AddDir(&Dir{Name: "2"})) + } + return NewLister().Start(f, "") +} + +func TestListerGet(t *testing.T) { + o := testListerGet(t) + obj, dir, err := o.Get() + assert.Nil(t, err) + assert.Equal(t, obj.Remote(), "1") + assert.Nil(t, dir) + obj, dir, err = o.Get() + assert.Nil(t, err) + assert.Nil(t, obj) + assert.Equal(t, dir.Name, "2") + obj, dir, err = o.Get() + assert.Nil(t, err) + assert.Nil(t, obj) + assert.Nil(t, dir) + assert.Equal(t, true, o.IsFinished()) +} + +func TestListerGetObject(t *testing.T) { + o := testListerGet(t) + obj, err := o.GetObject() + assert.Nil(t, err) + assert.Equal(t, obj.Remote(), "1") + obj, err = o.GetObject() + assert.Nil(t, err) + assert.Nil(t, obj) + assert.Equal(t, true, o.IsFinished()) +} + +func TestListerGetDir(t *testing.T) { + o := testListerGet(t) + dir, err := o.GetDir() + assert.Nil(t, err) + assert.Equal(t, dir.Name, "2") + dir, err = o.GetDir() + assert.Nil(t, err) + assert.Nil(t, dir) + assert.Equal(t, true, o.IsFinished()) +} + +func testListerGetError(t *testing.T) *Lister { + f := &mockFs{} + f.listFn = func(o ListOpts, dir string) { + o.SetError(errNotImpl) + } + return NewLister().Start(f, "") +} + +func TestListerGetError(t *testing.T) { + o := testListerGetError(t) + obj, dir, err := o.Get() + assert.Equal(t, err, errNotImpl) + assert.Nil(t, obj) + assert.Nil(t, dir) + obj, dir, err = o.Get() + assert.Nil(t, err) + assert.Nil(t, obj) + assert.Nil(t, dir) + assert.Equal(t, true, o.IsFinished()) +} + +func TestListerGetObjectError(t *testing.T) { + o := testListerGetError(t) + obj, err := o.GetObject() + assert.Equal(t, err, errNotImpl) + assert.Nil(t, obj) + obj, err = o.GetObject() + assert.Nil(t, err) + assert.Nil(t, obj) + assert.Equal(t, true, o.IsFinished()) +} + +func TestListerGetDirError(t *testing.T) { + o := testListerGetError(t) + dir, err := o.GetDir() + assert.Equal(t, err, errNotImpl) + assert.Nil(t, dir) + dir, err = o.GetDir() + assert.Nil(t, err) + assert.Nil(t, dir) + assert.Equal(t, true, o.IsFinished()) +} + +func testListerGetAll(t *testing.T) (*Lister, []Object, []*Dir) { + objs := []Object{ + mockObject("1f"), + mockObject("2f"), + mockObject("3f"), + } + dirs := []*Dir{ + &Dir{Name: "1d"}, + &Dir{Name: "2d"}, + } + f := &mockFs{} + f.listFn = func(o ListOpts, dir string) { + assert.Equal(t, false, o.Add(objs[0])) + assert.Equal(t, false, o.Add(objs[1])) + assert.Equal(t, false, o.AddDir(dirs[0])) + assert.Equal(t, false, o.Add(objs[2])) + assert.Equal(t, false, o.AddDir(dirs[1])) + } + return NewLister().Start(f, ""), objs, dirs +} + +func TestListerGetAll(t *testing.T) { + o, objs, dirs := testListerGetAll(t) + gotObjs, gotDirs, err := o.GetAll() + assert.Nil(t, err) + assert.Equal(t, objs, gotObjs) + assert.Equal(t, dirs, gotDirs) + assert.Equal(t, true, o.IsFinished()) +} + +func TestListerGetObjects(t *testing.T) { + o, objs, _ := testListerGetAll(t) + gotObjs, err := o.GetObjects() + assert.Nil(t, err) + assert.Equal(t, objs, gotObjs) + assert.Equal(t, true, o.IsFinished()) +} + +func TestListerGetDirs(t *testing.T) { + o, _, dirs := testListerGetAll(t) + gotDirs, err := o.GetDirs() + assert.Nil(t, err) + assert.Equal(t, dirs, gotDirs) + assert.Equal(t, true, o.IsFinished()) +} + +func testListerGetAllError(t *testing.T) *Lister { + f := &mockFs{} + f.listFn = func(o ListOpts, dir string) { + o.SetError(errNotImpl) + } + return NewLister().Start(f, "") +} + +func TestListerGetAllError(t *testing.T) { + o := testListerGetAllError(t) + gotObjs, gotDirs, err := o.GetAll() + assert.Equal(t, errNotImpl, err) + assert.Len(t, gotObjs, 0) + assert.Len(t, gotDirs, 0) + assert.Equal(t, true, o.IsFinished()) +} + +func TestListerGetObjectsError(t *testing.T) { + o := testListerGetAllError(t) + gotObjs, err := o.GetObjects() + assert.Equal(t, errNotImpl, err) + assert.Len(t, gotObjs, 0) + assert.Equal(t, true, o.IsFinished()) +} + +func TestListerGetDirsError(t *testing.T) { + o := testListerGetAllError(t) + gotDirs, err := o.GetDirs() + assert.Equal(t, errNotImpl, err) + assert.Len(t, gotDirs, 0) + assert.Equal(t, true, o.IsFinished()) +}