-
Notifications
You must be signed in to change notification settings - Fork 50
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #19 from dnephin/add-fs-assert
fs: Add fs.Equal() and fs.Manifest
- Loading branch information
Showing
13 changed files
with
947 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
package fs_test | ||
|
||
import ( | ||
"os" | ||
"testing" | ||
|
||
"github.com/gotestyourself/gotestyourself/assert" | ||
"github.com/gotestyourself/gotestyourself/fs" | ||
) | ||
|
||
func TestNewDirWithOpsAndManifestEqual(t *testing.T) { | ||
var userOps []fs.PathOp | ||
if os.Geteuid() == 0 { | ||
userOps = append(userOps, fs.AsUser(1001, 1002)) | ||
} | ||
|
||
ops := []fs.PathOp{ | ||
fs.WithFile("file1", "contenta", fs.WithMode(0400)), | ||
fs.WithFile("file2", "", fs.WithBytes([]byte{0, 1, 2})), | ||
fs.WithFile("file5", "", userOps...), | ||
fs.WithSymlink("link1", "file1"), | ||
fs.WithDir("sub", | ||
fs.WithFiles(map[string]string{ | ||
"file3": "contentb", | ||
"file4": "contentc", | ||
}), | ||
fs.WithMode(0705), | ||
), | ||
} | ||
|
||
dir := fs.NewDir(t, "test-all", ops...) | ||
defer dir.Remove() | ||
|
||
manifestOps := append( | ||
ops[:3], | ||
fs.WithSymlink("link1", dir.Join("file1")), | ||
ops[4], | ||
) | ||
assert.Assert(t, fs.Equal(dir.Path(), fs.Expected(t, manifestOps...))) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
package fs | ||
|
||
import ( | ||
"io" | ||
"io/ioutil" | ||
"os" | ||
"path/filepath" | ||
|
||
"github.com/gotestyourself/gotestyourself/assert" | ||
"github.com/pkg/errors" | ||
) | ||
|
||
// Manifest stores the expected structure and properties of files and directories | ||
// in a filesystem. | ||
type Manifest struct { | ||
root *directory | ||
} | ||
|
||
type resource struct { | ||
mode os.FileMode | ||
uid uint32 | ||
gid uint32 | ||
} | ||
|
||
type file struct { | ||
resource | ||
content io.ReadCloser | ||
} | ||
|
||
func (f *file) Type() string { | ||
return "file" | ||
} | ||
|
||
type symlink struct { | ||
resource | ||
target string | ||
} | ||
|
||
func (f *symlink) Type() string { | ||
return "symlink" | ||
} | ||
|
||
type directory struct { | ||
resource | ||
items map[string]dirEntry | ||
} | ||
|
||
func (f *directory) Type() string { | ||
return "directory" | ||
} | ||
|
||
type dirEntry interface { | ||
Type() string | ||
} | ||
|
||
// ManifestFromDir creates a Manifest by reading the directory at path. The | ||
// manifest stores the structure and properties of files in the directory. | ||
// ManifestFromDir can be used with Equal to compare two directories. | ||
func ManifestFromDir(t assert.TestingT, path string) Manifest { | ||
if ht, ok := t.(helperT); ok { | ||
ht.Helper() | ||
} | ||
|
||
manifest, err := manifestFromDir(path) | ||
assert.NilError(t, err) | ||
return manifest | ||
} | ||
|
||
func manifestFromDir(path string) (Manifest, error) { | ||
info, err := os.Stat(path) | ||
switch { | ||
case err != nil: | ||
return Manifest{}, err | ||
case !info.IsDir(): | ||
return Manifest{}, errors.Errorf("path %s must be a directory", path) | ||
} | ||
|
||
directory, err := newDirectory(path, info) | ||
return Manifest{root: directory}, err | ||
} | ||
|
||
func newDirectory(path string, info os.FileInfo) (*directory, error) { | ||
items := make(map[string]dirEntry) | ||
children, err := ioutil.ReadDir(path) | ||
if err != nil { | ||
return nil, err | ||
} | ||
for _, child := range children { | ||
fullPath := filepath.Join(path, child.Name()) | ||
items[child.Name()], err = getTypedResource(fullPath, child) | ||
if err != nil { | ||
return nil, err | ||
} | ||
} | ||
|
||
return &directory{ | ||
resource: newResourceFromInfo(info), | ||
items: items, | ||
}, nil | ||
} | ||
|
||
func getTypedResource(path string, info os.FileInfo) (dirEntry, error) { | ||
switch { | ||
case info.IsDir(): | ||
return newDirectory(path, info) | ||
case info.Mode()&os.ModeSymlink != 0: | ||
return newSymlink(path, info) | ||
// TODO: devices, pipes? | ||
default: | ||
return newFile(path, info) | ||
} | ||
} | ||
|
||
func newSymlink(path string, info os.FileInfo) (*symlink, error) { | ||
target, err := os.Readlink(path) | ||
return &symlink{ | ||
resource: newResourceFromInfo(info), | ||
target: target, | ||
}, err | ||
} | ||
|
||
func newFile(path string, info os.FileInfo) (*file, error) { | ||
// TODO: defer file opening to reduce number of open FDs? | ||
readCloser, err := os.Open(path) | ||
return &file{ | ||
resource: newResourceFromInfo(info), | ||
content: readCloser, | ||
}, err | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
package fs | ||
|
||
import ( | ||
"bytes" | ||
"io" | ||
"io/ioutil" | ||
"os" | ||
"runtime" | ||
"strings" | ||
"testing" | ||
|
||
"github.com/google/go-cmp/cmp" | ||
"github.com/gotestyourself/gotestyourself/assert" | ||
) | ||
|
||
func TestManifestFromDir(t *testing.T) { | ||
var defaultFileMode os.FileMode = 0644 | ||
var subDirMode = 0755 | os.ModeDir | ||
var jFileMode os.FileMode = 0600 | ||
if runtime.GOOS == "windows" { | ||
defaultFileMode = 0666 | ||
subDirMode = 0777 | os.ModeDir | ||
jFileMode = 0666 | ||
} | ||
|
||
var userOps []PathOp | ||
var expectedUserResource = newResource(defaultFileMode) | ||
if os.Geteuid() == 0 { | ||
userOps = append(userOps, AsUser(1001, 1002)) | ||
expectedUserResource = resource{mode: defaultFileMode, uid: 1001, gid: 1002} | ||
} | ||
|
||
srcDir := NewDir(t, t.Name(), | ||
WithFile("j", "content j", WithMode(0600)), | ||
WithDir("s", | ||
WithFile("k", "content k")), | ||
WithSymlink("f", "j"), | ||
WithFile("x", "content x", userOps...)) | ||
defer srcDir.Remove() | ||
|
||
expected := Manifest{ | ||
root: &directory{ | ||
resource: newResource(defaultRootDirMode), | ||
items: map[string]dirEntry{ | ||
"j": &file{ | ||
resource: newResource(jFileMode), | ||
content: readCloser("content j"), | ||
}, | ||
"s": &directory{ | ||
resource: newResource(subDirMode), | ||
items: map[string]dirEntry{ | ||
"k": &file{ | ||
resource: newResource(defaultFileMode), | ||
content: readCloser("content k"), | ||
}, | ||
}, | ||
}, | ||
"f": &symlink{ | ||
resource: newResource(defaultSymlinkMode), | ||
target: srcDir.Join("j"), | ||
}, | ||
"x": &file{ | ||
resource: expectedUserResource, | ||
content: readCloser("content x"), | ||
}, | ||
}, | ||
}, | ||
} | ||
actual := ManifestFromDir(t, srcDir.Path()) | ||
assert.DeepEqual(t, actual, expected, cmpManifest) | ||
actual.root.items["j"].(*file).content.Close() | ||
actual.root.items["x"].(*file).content.Close() | ||
actual.root.items["s"].(*directory).items["k"].(*file).content.Close() | ||
} | ||
|
||
var cmpManifest = cmp.Options{ | ||
cmp.AllowUnexported(Manifest{}, resource{}, file{}, symlink{}, directory{}), | ||
cmp.Comparer(func(x, y io.ReadCloser) bool { | ||
if x == nil || y == nil { | ||
return x == y | ||
} | ||
xContent, err := ioutil.ReadAll(x) | ||
if err != nil { | ||
return false | ||
} | ||
|
||
yContent, err := ioutil.ReadAll(y) | ||
if err != nil { | ||
return false | ||
} | ||
return bytes.Equal(xContent, yContent) | ||
}), | ||
} | ||
|
||
func readCloser(s string) io.ReadCloser { | ||
return ioutil.NopCloser(strings.NewReader(s)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
// +build !windows | ||
|
||
package fs | ||
|
||
import ( | ||
"os" | ||
"syscall" | ||
) | ||
|
||
const ( | ||
defaultRootDirMode = os.ModeDir | 0700 | ||
defaultSymlinkMode = os.ModeSymlink | 0777 | ||
) | ||
|
||
func newResourceFromInfo(info os.FileInfo) resource { | ||
statT := info.Sys().(*syscall.Stat_t) | ||
return resource{ | ||
mode: info.Mode(), | ||
uid: statT.Uid, | ||
gid: statT.Gid, | ||
} | ||
} | ||
|
||
func (p *filePath) SetMode(mode os.FileMode) { | ||
p.file.mode = mode | ||
} | ||
|
||
func (p *directoryPath) SetMode(mode os.FileMode) { | ||
p.directory.mode = mode | os.ModeDir | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package fs | ||
|
||
import "os" | ||
|
||
const ( | ||
defaultRootDirMode = os.ModeDir | 0777 | ||
defaultSymlinkMode = os.ModeSymlink | 0666 | ||
) | ||
|
||
func newResourceFromInfo(info os.FileInfo) resource { | ||
return resource{mode: info.Mode()} | ||
} | ||
|
||
func (p *filePath) SetMode(mode os.FileMode) { | ||
bits := mode & 0600 | ||
p.file.mode = bits + bits/010 + bits/0100 | ||
} | ||
|
||
// TODO: is mode ignored on windows? | ||
func (p *directoryPath) SetMode(mode os.FileMode) { | ||
p.directory.mode = defaultRootDirMode | ||
} |
Oops, something went wrong.