diff --git a/cmd/restic/integration_helpers_test.go b/cmd/restic/integration_helpers_test.go index 65da5878b..a3b1e07ef 100644 --- a/cmd/restic/integration_helpers_test.go +++ b/cmd/restic/integration_helpers_test.go @@ -2,9 +2,13 @@ package main import ( "fmt" + "io/ioutil" "os" "path/filepath" "syscall" + "testing" + + . "github.com/restic/restic/test" ) type dirEntry struct { @@ -51,31 +55,33 @@ func walkDir(dir string) <-chan *dirEntry { func (e *dirEntry) equals(other *dirEntry) bool { if e.path != other.path { - fmt.Printf("path does not match\n") + fmt.Fprintf(os.Stderr, "%v: path does not match\n", e.path) return false } if e.fi.Mode() != other.fi.Mode() { - fmt.Printf("mode does not match\n") + fmt.Fprintf(os.Stderr, "%v: mode does not match\n", e.path) return false } - // if e.fi.ModTime() != other.fi.ModTime() { - // fmt.Printf("%s: ModTime does not match\n", e.path) - // // TODO: Fix ModTime for directories, return false - // return true - // } + if e.fi.ModTime() != other.fi.ModTime() { + fmt.Fprintf(os.Stderr, "%v: ModTime does not match\n", e.path) + return false + } stat, _ := e.fi.Sys().(*syscall.Stat_t) stat2, _ := other.fi.Sys().(*syscall.Stat_t) if stat.Uid != stat2.Uid || stat2.Gid != stat2.Gid { + fmt.Fprintf(os.Stderr, "%v: UID or GID do not match\n", e.path) return false } return true } +// directoriesEqualContents checks if both directories contain exactly the same +// contents. func directoriesEqualContents(dir1, dir2 string) bool { ch1 := walkDir(dir1) ch2 := walkDir(dir2) @@ -136,3 +142,83 @@ func directoriesEqualContents(dir1, dir2 string) bool { return true } + +type dirStat struct { + files, dirs, other uint + size uint64 +} + +func isFile(fi os.FileInfo) bool { + return fi.Mode()&(os.ModeType|os.ModeCharDevice) == 0 +} + +// dirStats walks dir and collects stats. +func dirStats(dir string) (stat dirStat) { + for entry := range walkDir(dir) { + if isFile(entry.fi) { + stat.files++ + stat.size += uint64(entry.fi.Size()) + continue + } + + if entry.fi.IsDir() { + stat.dirs++ + continue + } + + stat.other++ + } + + return stat +} + +type testEnvironment struct { + base, cache, repo, testdata string +} + +func configureRestic(t testing.TB, cache, repo string) { + opts.CacheDir = cache + opts.Repo = repo + opts.Quiet = true + + opts.password = TestPassword +} + +func cleanupTempdir(t testing.TB, tempdir string) { + if !TestCleanup { + t.Logf("leaving temporary directory %v used for test", tempdir) + return + } + + OK(t, os.RemoveAll(tempdir)) +} + +// withTestEnvironment creates a test environment and calls f with it. After f has +// returned, the temporary directory is removed. +func withTestEnvironment(t testing.TB, f func(*testEnvironment)) { + if !RunIntegrationTest { + t.Skip("integration tests disabled") + } + + tempdir, err := ioutil.TempDir(TestTempDir, "restic-test-") + OK(t, err) + + env := testEnvironment{ + base: tempdir, + cache: filepath.Join(tempdir, "cache"), + repo: filepath.Join(tempdir, "repo"), + testdata: filepath.Join(tempdir, "testdata"), + } + + configureRestic(t, env.cache, env.repo) + OK(t, os.MkdirAll(env.testdata, 0700)) + + f(&env) + + if !TestCleanup { + t.Logf("leaving temporary directory %v used for test", tempdir) + return + } + + OK(t, os.RemoveAll(tempdir)) +} diff --git a/cmd/restic/integration_test.go b/cmd/restic/integration_test.go index ac5f921bf..1da97c700 100644 --- a/cmd/restic/integration_test.go +++ b/cmd/restic/integration_test.go @@ -2,8 +2,9 @@ package main import ( "bufio" + "crypto/rand" + "fmt" "io" - "io/ioutil" "os" "os/exec" "path/filepath" @@ -13,30 +14,6 @@ import ( . "github.com/restic/restic/test" ) -func setupTempdir(t testing.TB) (tempdir string) { - tempdir, err := ioutil.TempDir(TestTempDir, "restic-test-") - OK(t, err) - - return tempdir -} - -func configureRestic(t testing.TB, tempdir string) { - opts.CacheDir = filepath.Join(tempdir, "cache") - opts.Repo = filepath.Join(tempdir, "repo") - opts.Quiet = true - - opts.password = TestPassword -} - -func cleanupTempdir(t testing.TB, tempdir string) { - if !TestCleanup { - t.Logf("leaving temporary directory %v used for test", tempdir) - return - } - - OK(t, os.RemoveAll(tempdir)) -} - func setupTarTestFixture(t testing.TB, outputDir, tarFile string) { err := system("sh", "-c", `mkdir "$1" && (cd "$1" && tar xz) < "$2"`, "sh", outputDir, tarFile) @@ -85,7 +62,6 @@ func cmdBackup(t testing.TB, target []string, parentID backend.ID) { } func cmdList(t testing.TB, tpe string) []backend.ID { - rd, wr := io.Pipe() cmd := &CmdList{w: wr} @@ -97,8 +73,6 @@ func cmdList(t testing.TB, tpe string) []backend.ID { IDs := parseIDsFromReader(t, rd) - t.Logf("Listing %v: %v", tpe, IDs) - return IDs } @@ -113,56 +87,136 @@ func cmdFsck(t testing.TB) { } func TestBackup(t *testing.T) { - if !RunIntegrationTest { - t.Skip("integration tests disabled") - } + withTestEnvironment(t, func(env *testEnvironment) { + datafile := filepath.Join("testdata", "backup-data.tar.gz") + fd, err := os.Open(datafile) + if os.IsNotExist(err) { + t.Skipf("unable to find data file %q, skipping TestBackup", datafile) + return + } + OK(t, err) + OK(t, fd.Close()) - datafile := filepath.Join("testdata", "backup-data.tar.gz") - fd, err := os.Open(datafile) - if os.IsNotExist(err) { - t.Skipf("unable to find data file %q, skipping TestBackup", datafile) - return - } - OK(t, err) - OK(t, fd.Close()) + cmdInit(t) - tempdir := setupTempdir(t) - defer cleanupTempdir(t, tempdir) + datadir := filepath.Join(env.base, "testdata") + setupTarTestFixture(t, datadir, datafile) - configureRestic(t, tempdir) + // first backup + cmdBackup(t, []string{datadir}, nil) + snapshotIDs := cmdList(t, "snapshots") + Assert(t, len(snapshotIDs) == 1, + "more than one snapshot ID in repo") - cmdInit(t) + cmdFsck(t) + stat1 := dirStats(env.repo) - datadir := filepath.Join(tempdir, "testdata") + // second backup, implicit incremental + cmdBackup(t, []string{datadir}, nil) + snapshotIDs = cmdList(t, "snapshots") + Assert(t, len(snapshotIDs) == 2, + "more than one snapshot ID in repo") - setupTarTestFixture(t, datadir, datafile) + stat2 := dirStats(env.repo) + if stat2.size > stat1.size+stat1.size/10 { + t.Error("repository size has grown by more than 10 percent") + } + t.Logf("repository grown by %d bytes", stat2.size-stat1.size) - // first backup - cmdBackup(t, []string{datadir}, nil) - snapshotIDs := cmdList(t, "snapshots") - Assert(t, len(snapshotIDs) == 1, - "more than one snapshot ID in repo") + cmdFsck(t) + // third backup, explicit incremental + cmdBackup(t, []string{datadir}, snapshotIDs[0]) + snapshotIDs = cmdList(t, "snapshots") + Assert(t, len(snapshotIDs) == 3, + "more than two snapshot IDs in repo") - // second backup, implicit incremental - cmdBackup(t, []string{datadir}, nil) - snapshotIDs = cmdList(t, "snapshots") - Assert(t, len(snapshotIDs) == 2, - "more than one snapshot ID in repo") + stat3 := dirStats(env.repo) + if stat3.size > stat1.size+stat1.size/10 { + t.Error("repository size has grown by more than 10 percent") + } + t.Logf("repository grown by %d bytes", stat3.size-stat2.size) - // third backup, explicit incremental - cmdBackup(t, []string{datadir}, snapshotIDs[0]) - snapshotIDs = cmdList(t, "snapshots") - Assert(t, len(snapshotIDs) == 3, - "more than one snapshot ID in repo") + // restore all backups and compare + for i, snapshotID := range snapshotIDs { + restoredir := filepath.Join(env.base, fmt.Sprintf("restore%d", i)) + t.Logf("restoring snapshot %v to %v", snapshotID.Str(), restoredir) + cmdRestore(t, restoredir, snapshotIDs[0]) + Assert(t, directoriesEqualContents(datadir, filepath.Join(restoredir, "testdata")), + "directories are not equal") + } - // restore all backups and compare - for _, snapshotID := range snapshotIDs { - restoredir := filepath.Join(tempdir, "restore", snapshotID.String()) - t.Logf("restoring snapshot %v to %v", snapshotID.Str(), restoredir) - cmdRestore(t, restoredir, snapshotIDs[0]) - Assert(t, directoriesEqualContents(datadir, filepath.Join(restoredir, "testdata")), - "directories are not equal") - } - - cmdFsck(t) + cmdFsck(t) + }) +} + +const ( + incrementalFirstWrite = 20 * 1042 * 1024 + incrementalSecondWrite = 12 * 1042 * 1024 + incrementalThirdWrite = 4 * 1042 * 1024 +) + +func appendRandomData(filename string, bytes uint) error { + f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, 0666) + if err != nil { + fmt.Fprint(os.Stderr, err) + return err + } + + _, err = f.Seek(0, 2) + if err != nil { + fmt.Fprint(os.Stderr, err) + return err + } + + _, err = io.Copy(f, io.LimitReader(rand.Reader, int64(bytes))) + if err != nil { + fmt.Fprint(os.Stderr, err) + return err + } + + return f.Close() +} + +func TestIncrementalBackup(t *testing.T) { + withTestEnvironment(t, func(env *testEnvironment) { + datafile := filepath.Join("testdata", "backup-data.tar.gz") + fd, err := os.Open(datafile) + if os.IsNotExist(err) { + t.Skipf("unable to find data file %q, skipping TestBackup", datafile) + return + } + OK(t, err) + OK(t, fd.Close()) + + cmdInit(t) + + datadir := filepath.Join(env.base, "testdata") + testfile := filepath.Join(datadir, "testfile") + + OK(t, appendRandomData(testfile, incrementalFirstWrite)) + + cmdBackup(t, []string{datadir}, nil) + cmdFsck(t) + stat1 := dirStats(env.repo) + + OK(t, appendRandomData(testfile, incrementalSecondWrite)) + + cmdBackup(t, []string{datadir}, nil) + cmdFsck(t) + stat2 := dirStats(env.repo) + if stat2.size-stat1.size > incrementalFirstWrite { + t.Errorf("repository size has grown by more than %d bytes", incrementalFirstWrite) + } + t.Logf("repository grown by %d bytes", stat2.size-stat1.size) + + OK(t, appendRandomData(testfile, incrementalThirdWrite)) + + cmdBackup(t, []string{datadir}, nil) + cmdFsck(t) + stat3 := dirStats(env.repo) + if stat3.size-stat2.size > incrementalFirstWrite { + t.Errorf("repository size has grown by more than %d bytes", incrementalFirstWrite) + } + t.Logf("repository grown by %d bytes", stat3.size-stat2.size) + }) }