Skip to content

Commit

Permalink
Adding Directory type
Browse files Browse the repository at this point in the history
Treats a path to a directory on the filesystem as a collection of other
files and directories.
  • Loading branch information
marstr committed Oct 3, 2017
1 parent 64ef442 commit 99bf06d
Show file tree
Hide file tree
Showing 5 changed files with 210 additions and 0 deletions.
77 changes: 77 additions & 0 deletions filesystem.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package collection

import (
"errors"
"os"
"path/filepath"
)

// EnumerateDirectoryOptions is a means of configuring a `Directory` instance to including various children in its enumeration without
// supplying a `Where` clause later.
type DirectoryOptions uint

// These constants define all of the supported options for configuring a `Directory`
const (
DirectoryOptionsExcludeFiles = 1 << iota
DirectoryOptionsExcludeDirectories
DirectoryOptionsRecursive
)

// Directory treats a filesystem path as a collection of filesystem entries, specifically a collection of directories and files.
type Directory struct {
Location string
Options DirectoryOptions
}

func defaultEnumeratePredicate(loc string, info os.FileInfo) bool {
return true
}

func (d Directory) applyOptions(loc string, info os.FileInfo) bool {
if info.IsDir() && 0 != (d.Options&DirectoryOptionsExcludeDirectories) {
return false
}

if !info.IsDir() && 0 != d.Options&DirectoryOptionsExcludeFiles {
return false
}

return true
}

// Enumerate lists the items in a `Directory`
func (d Directory) Enumerate(cancel <-chan struct{}) Enumerator {
results := make(chan interface{})

go func() {
defer close(results)

filepath.Walk(d.Location, func(currentLocation string, info os.FileInfo, openErr error) (err error) {
if openErr != nil {
err = openErr
return
}

if d.Location == currentLocation {
return
}

if info.IsDir() && 0 == d.Options&DirectoryOptionsRecursive {
err = filepath.SkipDir
}

if d.applyOptions(currentLocation, info) {
select {
case results <- currentLocation:
// Intentionally Left Blank
case <-cancel:
err = errors.New("directory enumeration cancelled")
}
}

return
})
}()

return results
}
133 changes: 133 additions & 0 deletions filesystem_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package collection

import (
"fmt"
"math"
"path/filepath"
"testing"
)

func TestEnumerateDirectoryOptions_UniqueBits(t *testing.T) {
isPowerOfTwo := func(subject float64) bool {
a := math.Abs(math.Log2(subject))
b := math.Floor(a)

return a-b < .0000001
}

if !isPowerOfTwo(64) {
t.Log("isPowerOfTwo decided 64 is not a power of two.")
t.FailNow()
}

if isPowerOfTwo(91) {
t.Log("isPowerOfTwo decided 91 is a power of two.")
t.FailNow()
}

seen := make(map[DirectoryOptions]struct{})

declared := []DirectoryOptions{
DirectoryOptionsExcludeFiles,
DirectoryOptionsExcludeDirectories,
DirectoryOptionsRecursive,
}

for _, option := range declared {
if _, ok := seen[option]; ok {
t.Logf("Option: %d has already been declared.", option)
t.Fail()
}
seen[option] = struct{}{}

if !isPowerOfTwo(float64(option)) {
t.Logf("Option should have been a power of two, got %g instead.", float64(option))
t.Fail()
}
}
}

func TestDirectory_Enumerate(t *testing.T) {
subject := Directory{
Location: filepath.Join(".", "testdata", "foo"),
}

testCases := []struct {
options DirectoryOptions
expected map[string]struct{}
}{
{
options: 0,
expected: map[string]struct{}{
filepath.Join("testdata", "foo", "a.txt"): struct{}{},
filepath.Join("testdata", "foo", "c.txt"): struct{}{},
filepath.Join("testdata", "foo", "bar"): struct{}{},
},
},
{
options: DirectoryOptionsExcludeFiles,
expected: map[string]struct{}{
filepath.Join("testdata", "foo", "bar"): struct{}{},
},
},
{
options: DirectoryOptionsExcludeDirectories,
expected: map[string]struct{}{
filepath.Join("testdata", "foo", "a.txt"): struct{}{},
filepath.Join("testdata", "foo", "c.txt"): struct{}{},
},
},
{
options: DirectoryOptionsRecursive,
expected: map[string]struct{}{
filepath.Join("testdata", "foo", "bar"): struct{}{},
filepath.Join("testdata", "foo", "bar", "b.txt"): struct{}{},
filepath.Join("testdata", "foo", "a.txt"): struct{}{},
filepath.Join("testdata", "foo", "c.txt"): struct{}{},
},
},
{
options: DirectoryOptionsExcludeFiles | DirectoryOptionsRecursive,
expected: map[string]struct{}{
filepath.Join("testdata", "foo", "bar"): struct{}{},
},
},
{
options: DirectoryOptionsRecursive | DirectoryOptionsExcludeDirectories,
expected: map[string]struct{}{
filepath.Join("testdata", "foo", "a.txt"): struct{}{},
filepath.Join("testdata", "foo", "bar", "b.txt"): struct{}{},
filepath.Join("testdata", "foo", "c.txt"): struct{}{},
},
},
{
options: DirectoryOptionsExcludeDirectories | DirectoryOptionsExcludeFiles,
expected: map[string]struct{}{},
},
{
options: DirectoryOptionsExcludeFiles | DirectoryOptionsRecursive | DirectoryOptionsExcludeDirectories,
expected: map[string]struct{}{},
},
}

for _, tc := range testCases {
subject.Options = tc.options
t.Run(fmt.Sprintf("%d", uint(tc.options)), func(t *testing.T) {
for entry := range subject.Enumerate(nil) {
cast := entry.(string)
if _, ok := tc.expected[cast]; !ok {
t.Logf("unexpected result: %q", cast)
t.Fail()
}
delete(tc.expected, cast)
}

if len(tc.expected) != 0 {
for unseenFile := range tc.expected {
t.Logf("missing file: %q", unseenFile)
}
t.Fail()
}
})
}
}
Empty file added testdata/foo/a.txt
Empty file.
Empty file added testdata/foo/bar/b.txt
Empty file.
Empty file added testdata/foo/c.txt
Empty file.

0 comments on commit 99bf06d

Please sign in to comment.