Refactor changes and test functions

Remove change type in favor of explicit change function.
Using change function makes it more difficult to unnecessarily
add to the change interface.

Update test apply functions to use an interface rather
than a function type.

Signed-off-by: Derek McGowan <derek@mcgstyle.net> (github: dmcgowan)
This commit is contained in:
Derek McGowan 2017-02-02 17:30:11 -08:00
parent 65e8c07847
commit d96e6e3952
5 changed files with 172 additions and 193 deletions

View file

@ -16,14 +16,14 @@ import (
// setxattr fstest.SetXAttr("/home", "trusted.overlay.opaque", "y"), // setxattr fstest.SetXAttr("/home", "trusted.overlay.opaque", "y"),
func TestCopyDirectory(t *testing.T) { func TestCopyDirectory(t *testing.T) {
apply := fstest.MultiApply( apply := fstest.Apply(
fstest.CreateDirectory("/etc/", 0755), fstest.CreateDir("/etc/", 0755),
fstest.NewTestFile("/etc/hosts", []byte("localhost 127.0.0.1"), 0644), fstest.CreateFile("/etc/hosts", []byte("localhost 127.0.0.1"), 0644),
fstest.Link("/etc/hosts", "/etc/hosts.allow"), fstest.Link("/etc/hosts", "/etc/hosts.allow"),
fstest.CreateDirectory("/usr/local/lib", 0755), fstest.CreateDir("/usr/local/lib", 0755),
fstest.NewTestFile("/usr/local/lib/libnothing.so", []byte{0x00, 0x00}, 0755), fstest.CreateFile("/usr/local/lib/libnothing.so", []byte{0x00, 0x00}, 0755),
fstest.Symlink("libnothing.so", "/usr/local/lib/libnothing.so.2"), fstest.Symlink("libnothing.so", "/usr/local/lib/libnothing.so.2"),
fstest.CreateDirectory("/home", 0755), fstest.CreateDir("/home", 0755),
) )
if err := testCopy(apply); err != nil { if err := testCopy(apply); err != nil {
@ -41,7 +41,7 @@ func testCopy(apply fstest.Applier) error {
return errors.Wrap(err, "failed to create temporary directory") return errors.Wrap(err, "failed to create temporary directory")
} }
if err := apply(t1); err != nil { if err := apply.Apply(t1); err != nil {
return errors.Wrap(err, "failed to apply changes") return errors.Wrap(err, "failed to apply changes")
} }

View file

@ -48,12 +48,17 @@ type Change struct {
Path string Path string
} }
// Changes computes changes between lower and upper calling the // ChangeFunc is the type of function called for each change
// given change function for each computed change. Callbacks // computed during a directory changes calculation.
// will be done serialially and order by path name. type ChangeFunc func(ChangeKind, string, os.FileInfo, error) error
// Changes computes changes between two directories calling the
// given change function for each computed change. The first
// directory is intended to the base directory and second
// directory the changed directory.
// //
// Changes are ordered by name and should be appliable in the // The change callback is called by the order of path names and
// order in which they received. // should be appliable in that order.
// Due to this apply ordering, the following is true // Due to this apply ordering, the following is true
// - Removed directory trees only create a single change for the root // - Removed directory trees only create a single change for the root
// directory removed. Remaining changes are implied. // directory removed. Remaining changes are implied.
@ -62,7 +67,7 @@ type Change struct {
// by the removal of the parent directory. // by the removal of the parent directory.
// //
// Opaque directories will not be treated specially and each file // Opaque directories will not be treated specially and each file
// removed from the lower will show up as a removal // removed from the base directory will show up as a removal.
// //
// File content comparisons will be done on files which have timestamps // File content comparisons will be done on files which have timestamps
// which may have been truncated. If either of the files being compared // which may have been truncated. If either of the files being compared
@ -71,22 +76,20 @@ type Change struct {
// nanosecond values where one of those values is zero, the files will // nanosecond values where one of those values is zero, the files will
// be considered unchanged if the content is the same. This behavior // be considered unchanged if the content is the same. This behavior
// is to account for timestamp truncation during archiving. // is to account for timestamp truncation during archiving.
func Changes(ctx context.Context, upper, lower string, ch func(Change, os.FileInfo) error) error { func Changes(ctx context.Context, a, b string, changeFn ChangeFunc) error {
if lower == "" { if a == "" {
logrus.Debugf("Using single walk diff for %s", upper) logrus.Debugf("Using single walk diff for %s", b)
return addDirChanges(ctx, ch, upper) return addDirChanges(ctx, changeFn, b)
} else if diffOptions := detectDirDiff(upper, lower); diffOptions != nil { } else if diffOptions := detectDirDiff(b, a); diffOptions != nil {
logrus.Debugf("Using single walk diff for %s from %s", diffOptions.diffDir, lower) logrus.Debugf("Using single walk diff for %s from %s", diffOptions.diffDir, a)
return diffDirChanges(ctx, ch, lower, diffOptions) return diffDirChanges(ctx, changeFn, a, diffOptions)
} }
logrus.Debugf("Using double walk diff for %s from %s", upper, lower) logrus.Debugf("Using double walk diff for %s from %s", b, a)
return doubleWalkDiff(ctx, ch, upper, lower) return doubleWalkDiff(ctx, changeFn, a, b)
} }
type changeFn func(Change, os.FileInfo) error func addDirChanges(ctx context.Context, changeFn ChangeFunc, root string) error {
func addDirChanges(ctx context.Context, changes changeFn, root string) error {
return filepath.Walk(root, func(path string, f os.FileInfo, err error) error { return filepath.Walk(root, func(path string, f os.FileInfo, err error) error {
if err != nil { if err != nil {
return err return err
@ -105,25 +108,20 @@ func addDirChanges(ctx context.Context, changes changeFn, root string) error {
return nil return nil
} }
change := Change{ return changeFn(ChangeKindAdd, path, f, nil)
Path: path,
Kind: ChangeKindAdd,
}
return changes(change, f)
}) })
} }
// diffDirOptions is used when the diff can be directly calculated from // diffDirOptions is used when the diff can be directly calculated from
// a diff directory to its lower, without walking both trees. // a diff directory to its base, without walking both trees.
type diffDirOptions struct { type diffDirOptions struct {
diffDir string diffDir string
skipChange func(string) (bool, error) skipChange func(string) (bool, error)
deleteChange func(string, string, os.FileInfo) (string, error) deleteChange func(string, string, os.FileInfo) (string, error)
} }
// diffDirChanges walks the diff directory and compares changes against the lower. // diffDirChanges walks the diff directory and compares changes against the base.
func diffDirChanges(ctx context.Context, changes changeFn, lower string, o *diffDirOptions) error { func diffDirChanges(ctx context.Context, changeFn ChangeFunc, base string, o *diffDirOptions) error {
changedDirs := make(map[string]struct{}) changedDirs := make(map[string]struct{})
return filepath.Walk(o.diffDir, func(path string, f os.FileInfo, err error) error { return filepath.Walk(o.diffDir, func(path string, f os.FileInfo, err error) error {
if err != nil { if err != nil {
@ -152,9 +150,7 @@ func diffDirChanges(ctx context.Context, changes changeFn, lower string, o *diff
} }
} }
change := Change{ var kind ChangeKind
Path: path,
}
deletedFile, err := o.deleteChange(o.diffDir, path, f) deletedFile, err := o.deleteChange(o.diffDir, path, f)
if err != nil { if err != nil {
@ -163,20 +159,20 @@ func diffDirChanges(ctx context.Context, changes changeFn, lower string, o *diff
// Find out what kind of modification happened // Find out what kind of modification happened
if deletedFile != "" { if deletedFile != "" {
change.Path = deletedFile path = deletedFile
change.Kind = ChangeKindDelete kind = ChangeKindDelete
f = nil f = nil
} else { } else {
// Otherwise, the file was added // Otherwise, the file was added
change.Kind = ChangeKindAdd kind = ChangeKindAdd
// ...Unless it already existed in a lower, in which case, it's a modification // ...Unless it already existed in a base, in which case, it's a modification
stat, err := os.Stat(filepath.Join(lower, path)) stat, err := os.Stat(filepath.Join(base, path))
if err != nil && !os.IsNotExist(err) { if err != nil && !os.IsNotExist(err) {
return err return err
} }
if err == nil { if err == nil {
// The file existed in the lower, so that's a modification // The file existed in the base, so that's a modification
// However, if it's a directory, maybe it wasn't actually modified. // However, if it's a directory, maybe it wasn't actually modified.
// If you modify /foo/bar/baz, then /foo will be part of the changed files only because it's the parent of bar // If you modify /foo/bar/baz, then /foo will be part of the changed files only because it's the parent of bar
@ -186,7 +182,7 @@ func diffDirChanges(ctx context.Context, changes changeFn, lower string, o *diff
return nil return nil
} }
} }
change.Kind = ChangeKindModify kind = ChangeKindModify
} }
} }
@ -197,30 +193,23 @@ func diffDirChanges(ctx context.Context, changes changeFn, lower string, o *diff
if f.IsDir() { if f.IsDir() {
changedDirs[path] = struct{}{} changedDirs[path] = struct{}{}
} }
if change.Kind == ChangeKindAdd || change.Kind == ChangeKindDelete { if kind == ChangeKindAdd || kind == ChangeKindDelete {
parent := filepath.Dir(path) parent := filepath.Dir(path)
if _, ok := changedDirs[parent]; !ok && parent != "/" { if _, ok := changedDirs[parent]; !ok && parent != "/" {
pi, err := os.Stat(filepath.Join(o.diffDir, parent)) pi, err := os.Stat(filepath.Join(o.diffDir, parent))
if err != nil { if err := changeFn(ChangeKindModify, parent, pi, err); err != nil {
return err
}
dirChange := Change{
Path: parent,
Kind: ChangeKindModify,
}
if err := changes(dirChange, pi); err != nil {
return err return err
} }
changedDirs[parent] = struct{}{} changedDirs[parent] = struct{}{}
} }
} }
return changes(change, f) return changeFn(kind, path, f, nil)
}) })
} }
// doubleWalkDiff walks both directories to create a diff // doubleWalkDiff walks both directories to create a diff
func doubleWalkDiff(ctx context.Context, changes changeFn, upper, lower string) (err error) { func doubleWalkDiff(ctx context.Context, changeFn ChangeFunc, a, b string) (err error) {
g, ctx := errgroup.WithContext(ctx) g, ctx := errgroup.WithContext(ctx)
var ( var (
@ -232,11 +221,11 @@ func doubleWalkDiff(ctx context.Context, changes changeFn, upper, lower string)
) )
g.Go(func() error { g.Go(func() error {
defer close(c1) defer close(c1)
return pathWalk(ctx, lower, c1) return pathWalk(ctx, a, c1)
}) })
g.Go(func() error { g.Go(func() error {
defer close(c2) defer close(c2)
return pathWalk(ctx, upper, c2) return pathWalk(ctx, b, c2)
}) })
g.Go(func() error { g.Go(func() error {
for c1 != nil || c2 != nil { for c1 != nil || c2 != nil {
@ -264,8 +253,8 @@ func doubleWalkDiff(ctx context.Context, changes changeFn, upper, lower string)
} }
var f os.FileInfo var f os.FileInfo
c := pathChange(f1, f2) k, p := pathChange(f1, f2)
switch c.Kind { switch k {
case ChangeKindAdd: case ChangeKindAdd:
if rmdir != "" { if rmdir != "" {
rmdir = "" rmdir = ""
@ -301,7 +290,7 @@ func doubleWalkDiff(ctx context.Context, changes changeFn, upper, lower string)
continue continue
} }
} }
if err := changes(c, f); err != nil { if err := changeFn(k, p, f, nil); err != nil {
return err return err
} }
} }

View file

@ -21,21 +21,21 @@ import (
// - hardlink test // - hardlink test
func TestSimpleDiff(t *testing.T) { func TestSimpleDiff(t *testing.T) {
l1 := fstest.MultiApply( l1 := fstest.Apply(
fstest.CreateDirectory("/etc", 0755), fstest.CreateDir("/etc", 0755),
fstest.NewTestFile("/etc/hosts", []byte("mydomain 10.0.0.1"), 0644), fstest.CreateFile("/etc/hosts", []byte("mydomain 10.0.0.1"), 0644),
fstest.NewTestFile("/etc/profile", []byte("PATH=/usr/bin"), 0644), fstest.CreateFile("/etc/profile", []byte("PATH=/usr/bin"), 0644),
fstest.NewTestFile("/etc/unchanged", []byte("PATH=/usr/bin"), 0644), fstest.CreateFile("/etc/unchanged", []byte("PATH=/usr/bin"), 0644),
fstest.NewTestFile("/etc/unexpected", []byte("#!/bin/sh"), 0644), fstest.CreateFile("/etc/unexpected", []byte("#!/bin/sh"), 0644),
) )
l2 := fstest.MultiApply( l2 := fstest.Apply(
fstest.NewTestFile("/etc/hosts", []byte("mydomain 10.0.0.120"), 0644), fstest.CreateFile("/etc/hosts", []byte("mydomain 10.0.0.120"), 0644),
fstest.NewTestFile("/etc/profile", []byte("PATH=/usr/bin"), 0666), fstest.CreateFile("/etc/profile", []byte("PATH=/usr/bin"), 0666),
fstest.CreateDirectory("/root", 0700), fstest.CreateDir("/root", 0700),
fstest.NewTestFile("/root/.bashrc", []byte("PATH=/usr/sbin:/usr/bin"), 0644), fstest.CreateFile("/root/.bashrc", []byte("PATH=/usr/sbin:/usr/bin"), 0644),
fstest.RemoveFile("/etc/unexpected"), fstest.RemoveFile("/etc/unexpected"),
) )
diff := []Change{ diff := []testChange{
Modify("/etc/hosts"), Modify("/etc/hosts"),
Modify("/etc/profile"), Modify("/etc/profile"),
Delete("/etc/unexpected"), Delete("/etc/unexpected"),
@ -49,18 +49,18 @@ func TestSimpleDiff(t *testing.T) {
} }
func TestDirectoryReplace(t *testing.T) { func TestDirectoryReplace(t *testing.T) {
l1 := fstest.MultiApply( l1 := fstest.Apply(
fstest.CreateDirectory("/dir1", 0755), fstest.CreateDir("/dir1", 0755),
fstest.NewTestFile("/dir1/f1", []byte("#####"), 0644), fstest.CreateFile("/dir1/f1", []byte("#####"), 0644),
fstest.CreateDirectory("/dir1/f2", 0755), fstest.CreateDir("/dir1/f2", 0755),
fstest.NewTestFile("/dir1/f2/f3", []byte("#!/bin/sh"), 0644), fstest.CreateFile("/dir1/f2/f3", []byte("#!/bin/sh"), 0644),
) )
l2 := fstest.MultiApply( l2 := fstest.Apply(
fstest.NewTestFile("/dir1/f11", []byte("#New file here"), 0644), fstest.CreateFile("/dir1/f11", []byte("#New file here"), 0644),
fstest.RemoveFile("/dir1/f2"), fstest.RemoveFile("/dir1/f2"),
fstest.NewTestFile("/dir1/f2", []byte("Now file"), 0666), fstest.CreateFile("/dir1/f2", []byte("Now file"), 0666),
) )
diff := []Change{ diff := []testChange{
Add("/dir1/f11"), Add("/dir1/f11"),
Modify("/dir1/f2"), Modify("/dir1/f2"),
} }
@ -71,15 +71,15 @@ func TestDirectoryReplace(t *testing.T) {
} }
func TestRemoveDirectoryTree(t *testing.T) { func TestRemoveDirectoryTree(t *testing.T) {
l1 := fstest.MultiApply( l1 := fstest.Apply(
fstest.CreateDirectory("/dir1/dir2/dir3", 0755), fstest.CreateDir("/dir1/dir2/dir3", 0755),
fstest.NewTestFile("/dir1/f1", []byte("f1"), 0644), fstest.CreateFile("/dir1/f1", []byte("f1"), 0644),
fstest.NewTestFile("/dir1/dir2/f2", []byte("f2"), 0644), fstest.CreateFile("/dir1/dir2/f2", []byte("f2"), 0644),
) )
l2 := fstest.MultiApply( l2 := fstest.Apply(
fstest.RemoveFile("/dir1"), fstest.RemoveFile("/dir1"),
) )
diff := []Change{ diff := []testChange{
Delete("/dir1"), Delete("/dir1"),
} }
@ -89,15 +89,15 @@ func TestRemoveDirectoryTree(t *testing.T) {
} }
func TestFileReplace(t *testing.T) { func TestFileReplace(t *testing.T) {
l1 := fstest.MultiApply( l1 := fstest.Apply(
fstest.NewTestFile("/dir1", []byte("a file, not a directory"), 0644), fstest.CreateFile("/dir1", []byte("a file, not a directory"), 0644),
) )
l2 := fstest.MultiApply( l2 := fstest.Apply(
fstest.RemoveFile("/dir1"), fstest.RemoveFile("/dir1"),
fstest.CreateDirectory("/dir1/dir2", 0755), fstest.CreateDir("/dir1/dir2", 0755),
fstest.NewTestFile("/dir1/dir2/f1", []byte("also a file"), 0644), fstest.CreateFile("/dir1/dir2/f1", []byte("also a file"), 0644),
) )
diff := []Change{ diff := []testChange{
Modify("/dir1"), Modify("/dir1"),
Add("/dir1/dir2"), Add("/dir1/dir2"),
Add("/dir1/dir2/f1"), Add("/dir1/dir2/f1"),
@ -112,31 +112,31 @@ func TestUpdateWithSameTime(t *testing.T) {
tt := time.Now().Truncate(time.Second) tt := time.Now().Truncate(time.Second)
t1 := tt.Add(5 * time.Nanosecond) t1 := tt.Add(5 * time.Nanosecond)
t2 := tt.Add(6 * time.Nanosecond) t2 := tt.Add(6 * time.Nanosecond)
l1 := fstest.MultiApply( l1 := fstest.Apply(
fstest.NewTestFile("/file-modified-time", []byte("1"), 0644), fstest.CreateFile("/file-modified-time", []byte("1"), 0644),
fstest.Chtime("/file-modified-time", t1), fstest.Chtime("/file-modified-time", t1),
fstest.NewTestFile("/file-no-change", []byte("1"), 0644), fstest.CreateFile("/file-no-change", []byte("1"), 0644),
fstest.Chtime("/file-no-change", t1), fstest.Chtime("/file-no-change", t1),
fstest.NewTestFile("/file-same-time", []byte("1"), 0644), fstest.CreateFile("/file-same-time", []byte("1"), 0644),
fstest.Chtime("/file-same-time", t1), fstest.Chtime("/file-same-time", t1),
fstest.NewTestFile("/file-truncated-time-1", []byte("1"), 0644), fstest.CreateFile("/file-truncated-time-1", []byte("1"), 0644),
fstest.Chtime("/file-truncated-time-1", t1), fstest.Chtime("/file-truncated-time-1", t1),
fstest.NewTestFile("/file-truncated-time-2", []byte("1"), 0644), fstest.CreateFile("/file-truncated-time-2", []byte("1"), 0644),
fstest.Chtime("/file-truncated-time-2", tt), fstest.Chtime("/file-truncated-time-2", tt),
) )
l2 := fstest.MultiApply( l2 := fstest.Apply(
fstest.NewTestFile("/file-modified-time", []byte("2"), 0644), fstest.CreateFile("/file-modified-time", []byte("2"), 0644),
fstest.Chtime("/file-modified-time", t2), fstest.Chtime("/file-modified-time", t2),
fstest.NewTestFile("/file-no-change", []byte("1"), 0644), fstest.CreateFile("/file-no-change", []byte("1"), 0644),
fstest.Chtime("/file-no-change", tt), // use truncated time, should be regarded as no change fstest.Chtime("/file-no-change", tt), // use truncated time, should be regarded as no change
fstest.NewTestFile("/file-same-time", []byte("2"), 0644), fstest.CreateFile("/file-same-time", []byte("2"), 0644),
fstest.Chtime("/file-same-time", t1), fstest.Chtime("/file-same-time", t1),
fstest.NewTestFile("/file-truncated-time-1", []byte("2"), 0644), fstest.CreateFile("/file-truncated-time-1", []byte("2"), 0644),
fstest.Chtime("/file-truncated-time-1", tt), fstest.Chtime("/file-truncated-time-1", tt),
fstest.NewTestFile("/file-truncated-time-2", []byte("2"), 0644), fstest.CreateFile("/file-truncated-time-2", []byte("2"), 0644),
fstest.Chtime("/file-truncated-time-2", tt), fstest.Chtime("/file-truncated-time-2", tt),
) )
diff := []Change{ diff := []testChange{
// "/file-same-time" excluded because matching non-zero nanosecond values // "/file-same-time" excluded because matching non-zero nanosecond values
Modify("/file-modified-time"), Modify("/file-modified-time"),
Modify("/file-truncated-time-1"), Modify("/file-truncated-time-1"),
@ -148,7 +148,7 @@ func TestUpdateWithSameTime(t *testing.T) {
} }
} }
func testDiffWithBase(base, diff fstest.Applier, expected []Change) error { func testDiffWithBase(base, diff fstest.Applier, expected []testChange) error {
t1, err := ioutil.TempDir("", "diff-with-base-lower-") t1, err := ioutil.TempDir("", "diff-with-base-lower-")
if err != nil { if err != nil {
return errors.Wrap(err, "failed to create temp dir") return errors.Wrap(err, "failed to create temp dir")
@ -160,7 +160,7 @@ func testDiffWithBase(base, diff fstest.Applier, expected []Change) error {
} }
defer os.RemoveAll(t2) defer os.RemoveAll(t2)
if err := base(t1); err != nil { if err := base.Apply(t1); err != nil {
return errors.Wrap(err, "failed to apply base filesytem") return errors.Wrap(err, "failed to apply base filesytem")
} }
@ -168,11 +168,11 @@ func testDiffWithBase(base, diff fstest.Applier, expected []Change) error {
return errors.Wrap(err, "failed to copy base directory") return errors.Wrap(err, "failed to copy base directory")
} }
if err := diff(t2); err != nil { if err := diff.Apply(t2); err != nil {
return errors.Wrap(err, "failed to apply diff filesystem") return errors.Wrap(err, "failed to apply diff filesystem")
} }
changes, err := collectChanges(t2, t1) changes, err := collectChanges(t1, t2)
if err != nil { if err != nil {
return errors.Wrap(err, "failed to collect changes") return errors.Wrap(err, "failed to collect changes")
} }
@ -181,14 +181,14 @@ func testDiffWithBase(base, diff fstest.Applier, expected []Change) error {
} }
func TestBaseDirectoryChanges(t *testing.T) { func TestBaseDirectoryChanges(t *testing.T) {
apply := fstest.MultiApply( apply := fstest.Apply(
fstest.CreateDirectory("/etc", 0755), fstest.CreateDir("/etc", 0755),
fstest.NewTestFile("/etc/hosts", []byte("mydomain 10.0.0.1"), 0644), fstest.CreateFile("/etc/hosts", []byte("mydomain 10.0.0.1"), 0644),
fstest.NewTestFile("/etc/profile", []byte("PATH=/usr/bin"), 0644), fstest.CreateFile("/etc/profile", []byte("PATH=/usr/bin"), 0644),
fstest.CreateDirectory("/root", 0700), fstest.CreateDir("/root", 0700),
fstest.NewTestFile("/root/.bashrc", []byte("PATH=/usr/sbin:/usr/bin"), 0644), fstest.CreateFile("/root/.bashrc", []byte("PATH=/usr/sbin:/usr/bin"), 0644),
) )
changes := []Change{ changes := []testChange{
Add("/etc"), Add("/etc"),
Add("/etc/hosts"), Add("/etc/hosts"),
Add("/etc/profile"), Add("/etc/profile"),
@ -201,18 +201,18 @@ func TestBaseDirectoryChanges(t *testing.T) {
} }
} }
func testDiffWithoutBase(apply fstest.Applier, expected []Change) error { func testDiffWithoutBase(apply fstest.Applier, expected []testChange) error {
tmp, err := ioutil.TempDir("", "diff-without-base-") tmp, err := ioutil.TempDir("", "diff-without-base-")
if err != nil { if err != nil {
return errors.Wrap(err, "failed to create temp dir") return errors.Wrap(err, "failed to create temp dir")
} }
defer os.RemoveAll(tmp) defer os.RemoveAll(tmp)
if err := apply(tmp); err != nil { if err := apply.Apply(tmp); err != nil {
return errors.Wrap(err, "failed to apply filesytem changes") return errors.Wrap(err, "failed to apply filesytem changes")
} }
changes, err := collectChanges(tmp, "") changes, err := collectChanges("", tmp)
if err != nil { if err != nil {
return errors.Wrap(err, "failed to collect changes") return errors.Wrap(err, "failed to collect changes")
} }
@ -220,13 +220,13 @@ func testDiffWithoutBase(apply fstest.Applier, expected []Change) error {
return checkChanges(tmp, changes, expected) return checkChanges(tmp, changes, expected)
} }
func checkChanges(root string, changes []testChange, expected []Change) error { func checkChanges(root string, changes, expected []testChange) error {
if len(changes) != len(expected) { if len(changes) != len(expected) {
return errors.Errorf("Unexpected number of changes:\n%s", diffString(convertTestChanges(changes), expected)) return errors.Errorf("Unexpected number of changes:\n%s", diffString(changes, expected))
} }
for i := range changes { for i := range changes {
if changes[i].Path != expected[i].Path || changes[i].Kind != expected[i].Kind { if changes[i].Path != expected[i].Path || changes[i].Kind != expected[i].Kind {
return errors.Errorf("Unexpected change at %d:\n%s", i, diffString(convertTestChanges(changes), expected)) return errors.Errorf("Unexpected change at %d:\n%s", i, diffString(changes, expected))
} }
if changes[i].Kind != ChangeKindDelete { if changes[i].Kind != ChangeKindDelete {
filename := filepath.Join(root, changes[i].Path) filename := filepath.Join(root, changes[i].Path)
@ -254,18 +254,23 @@ func checkChanges(root string, changes []testChange, expected []Change) error {
} }
type testChange struct { type testChange struct {
Change Kind ChangeKind
Path string
FileInfo os.FileInfo FileInfo os.FileInfo
Source string Source string
} }
func collectChanges(upper, lower string) ([]testChange, error) { func collectChanges(a, b string) ([]testChange, error) {
changes := []testChange{} changes := []testChange{}
err := Changes(context.Background(), upper, lower, func(c Change, f os.FileInfo) error { err := Changes(context.Background(), a, b, func(k ChangeKind, p string, f os.FileInfo, err error) error {
if err != nil {
return err
}
changes = append(changes, testChange{ changes = append(changes, testChange{
Change: c, Kind: k,
Path: p,
FileInfo: f, FileInfo: f,
Source: filepath.Join(upper, c.Path), Source: filepath.Join(b, p),
}) })
return nil return nil
}) })
@ -276,20 +281,12 @@ func collectChanges(upper, lower string) ([]testChange, error) {
return changes, nil return changes, nil
} }
func convertTestChanges(c []testChange) []Change { func diffString(c1, c2 []testChange) string {
nc := make([]Change, len(c))
for i := range c {
nc[i] = c[i].Change
}
return nc
}
func diffString(c1, c2 []Change) string {
return fmt.Sprintf("got(%d):\n%s\nexpected(%d):\n%s", len(c1), changesString(c1), len(c2), changesString(c2)) return fmt.Sprintf("got(%d):\n%s\nexpected(%d):\n%s", len(c1), changesString(c1), len(c2), changesString(c2))
} }
func changesString(c []Change) string { func changesString(c []testChange) string {
strs := make([]string, len(c)) strs := make([]string, len(c))
for i := range c { for i := range c {
strs[i] = fmt.Sprintf("\t%s\t%s", c[i].Kind, c[i].Path) strs[i] = fmt.Sprintf("\t%s\t%s", c[i].Kind, c[i].Path)
@ -297,22 +294,22 @@ func changesString(c []Change) string {
return strings.Join(strs, "\n") return strings.Join(strs, "\n")
} }
func Add(p string) Change { func Add(p string) testChange {
return Change{ return testChange{
Kind: ChangeKindAdd, Kind: ChangeKindAdd,
Path: p, Path: p,
} }
} }
func Delete(p string) Change { func Delete(p string) testChange {
return Change{ return testChange{
Kind: ChangeKindDelete, Kind: ChangeKindDelete,
Path: p, Path: p,
} }
} }
func Modify(p string) Change { func Modify(p string) testChange {
return Change{ return testChange{
Kind: ChangeKindModify, Kind: ChangeKindModify,
Path: p, Path: p,
} }

View file

@ -10,12 +10,20 @@ import (
) )
// Applier applies single file changes // Applier applies single file changes
type Applier func(root string) error type Applier interface {
Apply(root string) error
}
// NewTestFile returns a file applier which creates a file as the type applyFn func(root string) error
func (a applyFn) Apply(root string) error {
return a(root)
}
// CreateFile returns a file applier which creates a file as the
// provided name with the given content and permission. // provided name with the given content and permission.
func NewTestFile(name string, content []byte, perm os.FileMode) Applier { func CreateFile(name string, content []byte, perm os.FileMode) Applier {
return func(root string) error { return applyFn(func(root string) error {
fullPath := filepath.Join(root, name) fullPath := filepath.Join(root, name)
if err := ioutil.WriteFile(fullPath, content, perm); err != nil { if err := ioutil.WriteFile(fullPath, content, perm); err != nil {
return err return err
@ -26,67 +34,67 @@ func NewTestFile(name string, content []byte, perm os.FileMode) Applier {
} }
return nil return nil
} })
} }
// RemoveFile returns a file applier which removes the provided file name // RemoveFile returns a file applier which removes the provided file name
func RemoveFile(name string) Applier { func RemoveFile(name string) Applier {
return func(root string) error { return applyFn(func(root string) error {
return os.RemoveAll(filepath.Join(root, name)) return os.RemoveAll(filepath.Join(root, name))
} })
} }
// CreateDirectory returns a file applier to create the directory with // CreateDir returns a file applier to create the directory with
// the provided name and permission // the provided name and permission
func CreateDirectory(name string, perm os.FileMode) Applier { func CreateDir(name string, perm os.FileMode) Applier {
return func(root string) error { return applyFn(func(root string) error {
fullPath := filepath.Join(root, name) fullPath := filepath.Join(root, name)
if err := os.MkdirAll(fullPath, perm); err != nil { if err := os.MkdirAll(fullPath, perm); err != nil {
return err return err
} }
return nil return nil
} })
} }
// Rename returns a file applier which renames a file // Rename returns a file applier which renames a file
func Rename(old, new string) Applier { func Rename(old, new string) Applier {
return func(root string) error { return applyFn(func(root string) error {
return os.Rename(filepath.Join(root, old), filepath.Join(root, new)) return os.Rename(filepath.Join(root, old), filepath.Join(root, new))
} })
} }
// Chown returns a file applier which changes the ownership of a file // Chown returns a file applier which changes the ownership of a file
func Chown(name string, uid, gid int) Applier { func Chown(name string, uid, gid int) Applier {
return func(root string) error { return applyFn(func(root string) error {
return os.Chown(filepath.Join(root, name), uid, gid) return os.Chown(filepath.Join(root, name), uid, gid)
} })
} }
// Chtime changes access and mod time of file // Chtime changes access and mod time of file
func Chtime(name string, t time.Time) Applier { func Chtime(name string, t time.Time) Applier {
return func(root string) error { return applyFn(func(root string) error {
return os.Chtimes(filepath.Join(root, name), t, t) return os.Chtimes(filepath.Join(root, name), t, t)
} })
} }
// Symlink returns a file applier which creates a symbolic link // Symlink returns a file applier which creates a symbolic link
func Symlink(oldname, newname string) Applier { func Symlink(oldname, newname string) Applier {
return func(root string) error { return applyFn(func(root string) error {
return os.Symlink(oldname, filepath.Join(root, newname)) return os.Symlink(oldname, filepath.Join(root, newname))
} })
} }
// Link returns a file applier which creates a hard link // Link returns a file applier which creates a hard link
func Link(oldname, newname string) Applier { func Link(oldname, newname string) Applier {
return func(root string) error { return applyFn(func(root string) error {
return os.Link(filepath.Join(root, oldname), filepath.Join(root, newname)) return os.Link(filepath.Join(root, oldname), filepath.Join(root, newname))
} })
} }
func SetXAttr(name, key, value string) Applier { func SetXAttr(name, key, value string) Applier {
return func(root string) error { return applyFn(func(root string) error {
return sysx.LSetxattr(name, key, []byte(value), 0) return sysx.LSetxattr(name, key, []byte(value), 0)
} })
} }
// TODO: Make platform specific, windows applier is always no-op // TODO: Make platform specific, windows applier is always no-op
@ -96,14 +104,14 @@ func SetXAttr(name, key, value string) Applier {
// } // }
//} //}
// MultiApply returns a new applier from the given appliers // Apply returns a new applier from the given appliers
func MultiApply(appliers ...Applier) Applier { func Apply(appliers ...Applier) Applier {
return func(root string) error { return applyFn(func(root string) error {
for _, a := range appliers { for _, a := range appliers {
if err := a(root); err != nil { if err := a.Apply(root); err != nil {
return err return err
} }
} }
return nil return nil
} })
} }

View file

@ -15,42 +15,27 @@ type currentPath struct {
fullPath string fullPath string
} }
func pathChange(lower, upper *currentPath) Change { func pathChange(lower, upper *currentPath) (ChangeKind, string) {
if lower == nil { if lower == nil {
if upper == nil { if upper == nil {
panic("cannot compare nil paths") panic("cannot compare nil paths")
} }
return Change{ return ChangeKindAdd, upper.path
Kind: ChangeKindAdd,
Path: upper.path,
}
} }
if upper == nil { if upper == nil {
return Change{ return ChangeKindDelete, lower.path
Kind: ChangeKindDelete,
Path: lower.path,
}
} }
// TODO: compare by directory // TODO: compare by directory
switch i := strings.Compare(lower.path, upper.path); { switch i := strings.Compare(lower.path, upper.path); {
case i < 0: case i < 0:
// File in lower that is not in upper // File in lower that is not in upper
return Change{ return ChangeKindDelete, lower.path
Kind: ChangeKindDelete,
Path: lower.path,
}
case i > 0: case i > 0:
// File in upper that is not in lower // File in upper that is not in lower
return Change{ return ChangeKindAdd, upper.path
Kind: ChangeKindAdd,
Path: upper.path,
}
default: default:
return Change{ return ChangeKindModify, upper.path
Kind: ChangeKindModify,
Path: upper.path,
}
} }
} }