containerd/fs/diff_test.go
Derek McGowan 574862fd89 Add fs package
Add diff comparison with support for double walking
two trees for comparison or single walking a diff
tree. Single walking requires further implementation
for specific mount types.

Add directory copy function which is intended to provide
fastest possible local copy of file system directories
without hardlinking.

Add test package to make creating filesystems for
test easy and comparisons deep and informative.

Signed-off-by: Derek McGowan <derek@mcgstyle.net> (github: dmcgowan)
2017-02-03 11:27:40 -08:00

304 lines
8.5 KiB
Go

package fs
import (
"context"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/docker/containerd/fs/fstest"
"github.com/pkg/errors"
)
// TODO: Additional tests
// - capability test (requires privilege)
// - chown test (requires privilege)
// - symlink test
// - hardlink test
func TestSimpleDiff(t *testing.T) {
l1 := fstest.MultiApply(
fstest.CreateDirectory("/etc", 0755),
fstest.NewTestFile("/etc/hosts", []byte("mydomain 10.0.0.1"), 0644),
fstest.NewTestFile("/etc/profile", []byte("PATH=/usr/bin"), 0644),
fstest.NewTestFile("/etc/unchanged", []byte("PATH=/usr/bin"), 0644),
fstest.NewTestFile("/etc/unexpected", []byte("#!/bin/sh"), 0644),
)
l2 := fstest.MultiApply(
fstest.NewTestFile("/etc/hosts", []byte("mydomain 10.0.0.120"), 0644),
fstest.NewTestFile("/etc/profile", []byte("PATH=/usr/bin"), 0666),
fstest.CreateDirectory("/root", 0700),
fstest.NewTestFile("/root/.bashrc", []byte("PATH=/usr/sbin:/usr/bin"), 0644),
fstest.RemoveFile("/etc/unexpected"),
)
diff := []Change{
Modify("/etc/hosts"),
Modify("/etc/profile"),
Delete("/etc/unexpected"),
Add("/root"),
Add("/root/.bashrc"),
}
if err := testDiffWithBase(l1, l2, diff); err != nil {
t.Fatalf("Failed diff with base: %+v", err)
}
}
func TestDirectoryReplace(t *testing.T) {
l1 := fstest.MultiApply(
fstest.CreateDirectory("/dir1", 0755),
fstest.NewTestFile("/dir1/f1", []byte("#####"), 0644),
fstest.CreateDirectory("/dir1/f2", 0755),
fstest.NewTestFile("/dir1/f2/f3", []byte("#!/bin/sh"), 0644),
)
l2 := fstest.MultiApply(
fstest.NewTestFile("/dir1/f11", []byte("#New file here"), 0644),
fstest.RemoveFile("/dir1/f2"),
fstest.NewTestFile("/dir1/f2", []byte("Now file"), 0666),
)
diff := []Change{
Add("/dir1/f11"),
Modify("/dir1/f2"),
}
if err := testDiffWithBase(l1, l2, diff); err != nil {
t.Fatalf("Failed diff with base: %+v", err)
}
}
func TestRemoveDirectoryTree(t *testing.T) {
l1 := fstest.MultiApply(
fstest.CreateDirectory("/dir1/dir2/dir3", 0755),
fstest.NewTestFile("/dir1/f1", []byte("f1"), 0644),
fstest.NewTestFile("/dir1/dir2/f2", []byte("f2"), 0644),
)
l2 := fstest.MultiApply(
fstest.RemoveFile("/dir1"),
)
diff := []Change{
Delete("/dir1"),
}
if err := testDiffWithBase(l1, l2, diff); err != nil {
t.Fatalf("Failed diff with base: %+v", err)
}
}
func TestFileReplace(t *testing.T) {
l1 := fstest.MultiApply(
fstest.NewTestFile("/dir1", []byte("a file, not a directory"), 0644),
)
l2 := fstest.MultiApply(
fstest.RemoveFile("/dir1"),
fstest.CreateDirectory("/dir1/dir2", 0755),
fstest.NewTestFile("/dir1/dir2/f1", []byte("also a file"), 0644),
)
diff := []Change{
Modify("/dir1"),
Add("/dir1/dir2"),
Add("/dir1/dir2/f1"),
}
if err := testDiffWithBase(l1, l2, diff); err != nil {
t.Fatalf("Failed diff with base: %+v", err)
}
}
func TestUpdateWithSameTime(t *testing.T) {
tt := time.Now().Truncate(time.Second)
t1 := tt.Add(5 * time.Nanosecond)
t2 := tt.Add(6 * time.Nanosecond)
l1 := fstest.MultiApply(
fstest.NewTestFile("/file-modified-time", []byte("1"), 0644),
fstest.Chtime("/file-modified-time", t1),
fstest.NewTestFile("/file-no-change", []byte("1"), 0644),
fstest.Chtime("/file-no-change", t1),
fstest.NewTestFile("/file-same-time", []byte("1"), 0644),
fstest.Chtime("/file-same-time", t1),
fstest.NewTestFile("/file-truncated-time-1", []byte("1"), 0644),
fstest.Chtime("/file-truncated-time-1", t1),
fstest.NewTestFile("/file-truncated-time-2", []byte("1"), 0644),
fstest.Chtime("/file-truncated-time-2", tt),
)
l2 := fstest.MultiApply(
fstest.NewTestFile("/file-modified-time", []byte("2"), 0644),
fstest.Chtime("/file-modified-time", t2),
fstest.NewTestFile("/file-no-change", []byte("1"), 0644),
fstest.Chtime("/file-no-change", tt), // use truncated time, should be regarded as no change
fstest.NewTestFile("/file-same-time", []byte("2"), 0644),
fstest.Chtime("/file-same-time", t1),
fstest.NewTestFile("/file-truncated-time-1", []byte("2"), 0644),
fstest.Chtime("/file-truncated-time-1", tt),
fstest.NewTestFile("/file-truncated-time-2", []byte("2"), 0644),
fstest.Chtime("/file-truncated-time-2", tt),
)
diff := []Change{
// "/file-same-time" excluded because matching non-zero nanosecond values
Modify("/file-modified-time"),
Modify("/file-truncated-time-1"),
Modify("/file-truncated-time-2"),
}
if err := testDiffWithBase(l1, l2, diff); err != nil {
t.Fatalf("Failed diff with base: %+v", err)
}
}
func testDiffWithBase(base, diff fstest.Applier, expected []Change) error {
t1, err := ioutil.TempDir("", "diff-with-base-lower-")
if err != nil {
return errors.Wrap(err, "failed to create temp dir")
}
defer os.RemoveAll(t1)
t2, err := ioutil.TempDir("", "diff-with-base-upper-")
if err != nil {
return errors.Wrap(err, "failed to create temp dir")
}
defer os.RemoveAll(t2)
if err := base(t1); err != nil {
return errors.Wrap(err, "failed to apply base filesytem")
}
if err := CopyDirectory(t2, t1); err != nil {
return errors.Wrap(err, "failed to copy base directory")
}
if err := diff(t2); err != nil {
return errors.Wrap(err, "failed to apply diff filesystem")
}
changes, err := collectChanges(t2, t1)
if err != nil {
return errors.Wrap(err, "failed to collect changes")
}
return checkChanges(t2, changes, expected)
}
func TestBaseDirectoryChanges(t *testing.T) {
apply := fstest.MultiApply(
fstest.CreateDirectory("/etc", 0755),
fstest.NewTestFile("/etc/hosts", []byte("mydomain 10.0.0.1"), 0644),
fstest.NewTestFile("/etc/profile", []byte("PATH=/usr/bin"), 0644),
fstest.CreateDirectory("/root", 0700),
fstest.NewTestFile("/root/.bashrc", []byte("PATH=/usr/sbin:/usr/bin"), 0644),
)
changes := []Change{
Add("/etc"),
Add("/etc/hosts"),
Add("/etc/profile"),
Add("/root"),
Add("/root/.bashrc"),
}
if err := testDiffWithoutBase(apply, changes); err != nil {
t.Fatalf("Failed diff without base: %+v", err)
}
}
func testDiffWithoutBase(apply fstest.Applier, expected []Change) error {
tmp, err := ioutil.TempDir("", "diff-without-base-")
if err != nil {
return errors.Wrap(err, "failed to create temp dir")
}
defer os.RemoveAll(tmp)
if err := apply(tmp); err != nil {
return errors.Wrap(err, "failed to apply filesytem changes")
}
changes, err := collectChanges(tmp, "")
if err != nil {
return errors.Wrap(err, "failed to collect changes")
}
return checkChanges(tmp, changes, expected)
}
func checkChanges(root string, changes, expected []Change) error {
if len(changes) != len(expected) {
return errors.Errorf("Unexpected number of changes:\n%s", diffString(changes, expected))
}
for i := range changes {
if changes[i].Path != expected[i].Path || changes[i].Kind != expected[i].Kind {
return errors.Errorf("Unexpected change at %d:\n%s", i, diffString(changes, expected))
}
if changes[i].Kind != ChangeKindDelete {
filename := filepath.Join(root, changes[i].Path)
efi, err := os.Stat(filename)
if err != nil {
return errors.Wrapf(err, "failed to stat %q", filename)
}
afi := changes[i].FileInfo
if afi.Size() != efi.Size() {
return errors.Errorf("Unexpected change size %d, %q has size %d", afi.Size(), filename, efi.Size())
}
if afi.Mode() != efi.Mode() {
return errors.Errorf("Unexpected change mode %s, %q has mode %s", afi.Mode(), filename, efi.Mode())
}
if afi.ModTime() != efi.ModTime() {
return errors.Errorf("Unexpected change modtime %s, %q has modtime %s", afi.ModTime(), filename, efi.ModTime())
}
if expected := filepath.Join(root, changes[i].Path); changes[i].Source != expected {
return errors.Errorf("Unexpected source path %s, expected %s", changes[i].Source, expected)
}
}
}
return nil
}
func collectChanges(upper, lower string) ([]Change, error) {
ctx, changeC := Changes(context.Background(), upper, lower)
changes := []Change{}
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
case c, ok := <-changeC:
if !ok {
return changes, nil
}
changes = append(changes, c)
}
}
}
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))
}
func changesString(c []Change) string {
strs := make([]string, len(c))
for i := range c {
strs[i] = fmt.Sprintf("\t%s\t%s", c[i].Kind, c[i].Path)
}
return strings.Join(strs, "\n")
}
func Add(p string) Change {
return Change{
Kind: ChangeKindAdd,
Path: p,
}
}
func Delete(p string) Change {
return Change{
Kind: ChangeKindDelete,
Path: p,
}
}
func Modify(p string) Change {
return Change{
Kind: ChangeKindModify,
Path: p,
}
}