Skip to content

Commit

Permalink
Merge pull request #19 from dnephin/add-fs-assert
Browse files Browse the repository at this point in the history
fs: Add fs.Equal() and fs.Manifest
  • Loading branch information
dnephin committed Mar 21, 2018
2 parents bf63f3f + fa34e47 commit f2728c7
Show file tree
Hide file tree
Showing 13 changed files with 947 additions and 22 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ patterns.
* [env](http://godoc.org/github.com/gotestyourself/gotestyourself/env) -
test code that uses environment variables
* [fs](http://godoc.org/github.com/gotestyourself/gotestyourself/fs) -
create test files and directories
create test files and compare directories structures
* [golden](http://godoc.org/github.com/gotestyourself/gotestyourself/golden) -
compare large multi-line strings
* [icmd](http://godoc.org/github.com/gotestyourself/gotestyourself/icmd) -
Expand Down
19 changes: 19 additions & 0 deletions fs/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/gotestyourself/gotestyourself/assert"
"github.com/gotestyourself/gotestyourself/assert/cmp"
"github.com/gotestyourself/gotestyourself/fs"
"github.com/gotestyourself/gotestyourself/golden"
)

var t = &testing.T{}
Expand Down Expand Up @@ -41,3 +42,21 @@ func ExampleWithDir() {
)
defer dir.Remove()
}

// Test that a directory contains the expected files, and all the files have the
// expected properties.
func ExampleEqual() {
path := operationWhichCreatesFiles()
expected := fs.Expected(t,
fs.WithFile("one", "",
fs.WithBytes(golden.Get(t, "one.golden")),
fs.WithMode(0600)),
fs.WithDir("data",
fs.WithFile("config", "", fs.MatchAnyFileContent)))

assert.Assert(t, fs.Equal(path, expected))
}

func operationWhichCreatesFiles() string {
return "example-path"
}
8 changes: 5 additions & 3 deletions fs/file.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*Package fs provides tools for creating and working with temporary files and
directories.
/*Package fs provides tools for creating temporary files, and testing the
contents and structure of a directory.
*/
package fs

Expand All @@ -12,7 +12,9 @@ import (
"github.com/gotestyourself/gotestyourself/x/subtest"
)

// Path objects return their filesystem path. Both File and Dir implement Path.
// Path objects return their filesystem path. Path may be implemented by a
// real filesystem object (such as File and Dir) or by a type which updates
// entries in a Manifest.
type Path interface {
Path() string
Remove()
Expand Down
40 changes: 40 additions & 0 deletions fs/file_test.go
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...)))
}
129 changes: 129 additions & 0 deletions fs/manifest.go
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
}
97 changes: 97 additions & 0 deletions fs/manifest_test.go
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))
}
30 changes: 30 additions & 0 deletions fs/manifest_unix.go
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
}
22 changes: 22 additions & 0 deletions fs/manifest_windows.go
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
}

0 comments on commit f2728c7

Please sign in to comment.