From 954025e399a8b0baaddfdfd7d13072d99aaf3770 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sat, 9 May 2015 14:45:39 +0200 Subject: [PATCH 01/31] Add integration test with the go testing framework --- cmd/restic/integration_test.go | 88 ++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 cmd/restic/integration_test.go diff --git a/cmd/restic/integration_test.go b/cmd/restic/integration_test.go new file mode 100644 index 000000000..f18de86d1 --- /dev/null +++ b/cmd/restic/integration_test.go @@ -0,0 +1,88 @@ +// +build integration + +package main + +import ( + "flag" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "testing" + + . "github.com/restic/restic/test" +) + +var TestDataFile = flag.String("test.datafile", "", `specify tar.gz file with test data to backup and restore (required for integration 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) { + // use cache dir within tempdir + OK(t, os.Setenv("RESTIC_CACHE", filepath.Join(tempdir, "cache"))) + + // configure environment + opts.Repo = filepath.Join(tempdir, "repo") + OK(t, os.Setenv("RESTIC_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) + OK(t, err) +} + +func system(command string, args ...string) error { + cmd := exec.Command(command, args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + return cmd.Run() +} + +func cmdInit(t testing.TB) { + cmd := &CmdInit{} + OK(t, cmd.Execute(nil)) + + t.Logf("repository initialized at %v", opts.Repo) +} + +func cmdBackup(t testing.TB, target []string) { + cmd := &CmdBackup{} + t.Logf("backing up %v", target) + + OK(t, cmd.Execute(target)) +} + +func TestBackup(t *testing.T) { + if *TestDataFile == "" { + t.Fatal("no data tar file specified, use flag '-test.datafile'") + } + + tempdir := setupTempdir(t) + defer cleanupTempdir(t, tempdir) + + configureRestic(t, tempdir) + + cmdInit(t) + + datadir := filepath.Join(tempdir, "testdata") + + setupTarTestFixture(t, datadir, *TestDataFile) + + cmdBackup(t, []string{datadir}) +} From 9b7db4df24cda157990bba75126d9ebf99df3f96 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sat, 9 May 2015 22:40:10 +0200 Subject: [PATCH 02/31] travis: Add new integration tests --- Makefile | 26 ++++++++++++++++++-------- backend/sftp_test.go | 2 ++ coverage_all.sh | 2 +- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index c0c198057..04099f41b 100644 --- a/Makefile +++ b/Makefile @@ -48,17 +48,27 @@ gox: .gopath $(SOURCE) cd $(BASEPATH) && \ gox -verbose -os "$(GOX_OS)" ./cmd/restic -test-integration: .gopath restic restic.debug dirdiff - # run testsuite - PATH=.:$(PATH) ./testsuite.sh +test-integration: .gopath + cd $(BASEPATH) && go test $(GOTESTFLAGS) \ + -tags integration \ + ./backend \ + -cover -covermode=count -coverprofile=integration-sftp.cov \ + -test.sftppath=$(SFTP_PATH) - # run sftp integration tests - cd $(BASEPATH)/backend && \ - go test $(GOTESTFLAGS) -test.sftppath $(SFTP_PATH) ./... + cd $(BASEPATH) && go test $(GOTESTFLAGS) \ + -tags integration \ + ./cmd/restic \ + -cover -covermode=count -coverprofile=integration.cov \ + -test.datafile=$(PWD)/testsuite/fake-data.tar.gz -all.cov: .gopath $(SOURCE) +all.cov: .gopath $(SOURCE) test-integration cd $(BASEPATH) && \ - ./coverage_all.sh all.cov + go list ./... | while read pkg; do \ + go test -covermode=count -coverprofile=$$(echo $$pkg | base64).cov $$pkg; \ + done + + echo "mode: count" > all.cov + tail -q -n +2 *.cov >> all.cov env: @echo export GOPATH=\"$(GOPATH)\" diff --git a/backend/sftp_test.go b/backend/sftp_test.go index dded89ca8..189e26d53 100644 --- a/backend/sftp_test.go +++ b/backend/sftp_test.go @@ -1,3 +1,5 @@ +// +build integration + package backend_test import ( diff --git a/coverage_all.sh b/coverage_all.sh index 552387e41..ae448b743 100755 --- a/coverage_all.sh +++ b/coverage_all.sh @@ -7,4 +7,4 @@ go list ./... | while read pkg; do done echo "mode: count" > $TARGETFILE -tail -q -n +2 *.cov >> $TARGETFILE +tail -q -n +2 *.cov */*.cov */*/*.cov >> $TARGETFILE From 7c107acf0bb93b146ddc1e6bf7d3eb2774f646d3 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 10 May 2015 02:41:16 +0200 Subject: [PATCH 03/31] More integration tests --- cmd/restic/cmd_list.go | 14 ++- cmd/restic/cmd_restore.go | 2 +- cmd/restic/integration_helpers_test.go | 140 +++++++++++++++++++++++++ cmd/restic/integration_test.go | 81 +++++++++++++- 4 files changed, 231 insertions(+), 6 deletions(-) create mode 100644 cmd/restic/integration_helpers_test.go diff --git a/cmd/restic/cmd_list.go b/cmd/restic/cmd_list.go index 01201cef0..641d72c2e 100644 --- a/cmd/restic/cmd_list.go +++ b/cmd/restic/cmd_list.go @@ -3,11 +3,15 @@ package main import ( "errors" "fmt" + "io" + "os" "github.com/restic/restic/backend" ) -type CmdList struct{} +type CmdList struct { + w io.Writer +} func init() { _, err := parser.AddCommand("list", @@ -24,6 +28,10 @@ func (cmd CmdList) Usage() string { } func (cmd CmdList) Execute(args []string) error { + if cmd.w == nil { + cmd.w = os.Stdout + } + if len(args) != 1 { return fmt.Errorf("type not specified, Usage: %s", cmd.Usage()) } @@ -42,7 +50,7 @@ func (cmd CmdList) Execute(args []string) error { } for blob := range s.Index().Each(nil) { - fmt.Println(blob.ID) + fmt.Fprintln(cmd.w, blob.ID) } return nil @@ -61,7 +69,7 @@ func (cmd CmdList) Execute(args []string) error { } for id := range s.List(t, nil) { - fmt.Printf("%s\n", id) + fmt.Fprintf(cmd.w, "%s\n", id) } return nil diff --git a/cmd/restic/cmd_restore.go b/cmd/restic/cmd_restore.go index 3d14b17fb..cd91228e6 100644 --- a/cmd/restic/cmd_restore.go +++ b/cmd/restic/cmd_restore.go @@ -81,7 +81,7 @@ func (cmd CmdRestore) Execute(args []string) error { } } - fmt.Printf("restoring %s to %s\n", res.Snapshot(), target) + verbosePrintf("restoring %s to %s\n", res.Snapshot(), target) err = res.RestoreTo(target) if err != nil { diff --git a/cmd/restic/integration_helpers_test.go b/cmd/restic/integration_helpers_test.go new file mode 100644 index 000000000..2f3f0ec72 --- /dev/null +++ b/cmd/restic/integration_helpers_test.go @@ -0,0 +1,140 @@ +// +build integration + +package main + +import ( + "fmt" + "os" + "path/filepath" + "syscall" +) + +type dirEntry struct { + path string + fi os.FileInfo +} + +func walkDir(dir string) <-chan *dirEntry { + ch := make(chan *dirEntry, 100) + + go func() { + err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + return nil + } + + name, err := filepath.Rel(dir, path) + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + return nil + } + + ch <- &dirEntry{ + path: name, + fi: info, + } + + return nil + }) + + if err != nil { + fmt.Fprintf(os.Stderr, "Walk() error: %v\n", err) + } + + close(ch) + }() + + // first element is root + _ = <-ch + + return ch +} + +func (e *dirEntry) equals(other *dirEntry) bool { + if e.path != other.path { + fmt.Printf("path does not match\n") + return false + } + + if e.fi.Mode() != other.fi.Mode() { + fmt.Printf("mode does not match\n") + 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 + // } + + stat, _ := e.fi.Sys().(*syscall.Stat_t) + stat2, _ := other.fi.Sys().(*syscall.Stat_t) + + if stat.Uid != stat2.Uid || stat2.Gid != stat2.Gid { + return false + } + + return true +} + +func directoriesEqualContents(dir1, dir2 string) bool { + ch1 := walkDir(dir1) + ch2 := walkDir(dir2) + + changes := false + + var a, b *dirEntry + for { + var ok bool + + if ch1 != nil && a == nil { + a, ok = <-ch1 + if !ok { + ch1 = nil + } + } + + if ch2 != nil && b == nil { + b, ok = <-ch2 + if !ok { + ch2 = nil + } + } + + if ch1 == nil && ch2 == nil { + break + } + + if ch1 == nil { + fmt.Printf("+%v\n", b.path) + changes = true + } else if ch2 == nil { + fmt.Printf("-%v\n", a.path) + changes = true + } else if !a.equals(b) { + if a.path < b.path { + fmt.Printf("-%v\n", a.path) + changes = true + a = nil + continue + } else if a.path > b.path { + fmt.Printf("+%v\n", b.path) + changes = true + b = nil + continue + } else { + fmt.Printf("%%%v\n", a.path) + changes = true + } + } + + a, b = nil, nil + } + + if changes { + return false + } + + return true +} diff --git a/cmd/restic/integration_test.go b/cmd/restic/integration_test.go index f18de86d1..4d8106974 100644 --- a/cmd/restic/integration_test.go +++ b/cmd/restic/integration_test.go @@ -3,13 +3,16 @@ package main import ( + "bufio" "flag" + "io" "io/ioutil" "os" "os/exec" "path/filepath" "testing" + "github.com/restic/restic/backend" . "github.com/restic/restic/test" ) @@ -54,6 +57,23 @@ func system(command string, args ...string) error { return cmd.Run() } +func parseIDsFromReader(t testing.TB, rd io.Reader) backend.IDs { + IDs := backend.IDs{} + sc := bufio.NewScanner(rd) + + for sc.Scan() { + id, err := backend.ParseID(sc.Text()) + if err != nil { + t.Logf("parse id %v: %v", sc.Text(), err) + continue + } + + IDs = append(IDs, id) + } + + return IDs +} + func cmdInit(t testing.TB) { cmd := &CmdInit{} OK(t, cmd.Execute(nil)) @@ -61,13 +81,43 @@ func cmdInit(t testing.TB) { t.Logf("repository initialized at %v", opts.Repo) } -func cmdBackup(t testing.TB, target []string) { +func cmdBackup(t testing.TB, target []string, parentID backend.ID) { cmd := &CmdBackup{} + cmd.Parent = parentID.String() + t.Logf("backing up %v", target) OK(t, cmd.Execute(target)) } +func cmdList(t testing.TB, tpe string) []backend.ID { + + rd, wr := io.Pipe() + + cmd := &CmdList{w: wr} + + go func() { + OK(t, cmd.Execute([]string{tpe})) + OK(t, wr.Close()) + }() + + IDs := parseIDsFromReader(t, rd) + + t.Logf("Listing %v: %v", tpe, IDs) + + return IDs +} + +func cmdRestore(t testing.TB, dir string, snapshotID backend.ID) { + cmd := &CmdRestore{} + cmd.Execute([]string{snapshotID.String(), dir}) +} + +func cmdFsck(t testing.TB) { + cmd := &CmdFsck{CheckData: true, Orphaned: true} + OK(t, cmd.Execute(nil)) +} + func TestBackup(t *testing.T) { if *TestDataFile == "" { t.Fatal("no data tar file specified, use flag '-test.datafile'") @@ -84,5 +134,32 @@ func TestBackup(t *testing.T) { setupTarTestFixture(t, datadir, *TestDataFile) - cmdBackup(t, []string{datadir}) + // first backup + cmdBackup(t, []string{datadir}, nil) + snapshotIDs := cmdList(t, "snapshots") + Assert(t, len(snapshotIDs) == 1, + "more than one snapshot ID 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") + + // 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 _, 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) } From 12677b4f8a3be83c591849fc310aa2e2d7988727 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Fri, 5 Jun 2015 22:33:39 +0200 Subject: [PATCH 04/31] Use flag instead of build tag to run integration tests --- Makefile | 4 ++-- backend/sftp_test.go | 6 ++++-- cmd/restic/integration_helpers_test.go | 2 -- cmd/restic/integration_test.go | 8 +++++--- test/backend.go | 9 ++++++--- 5 files changed, 17 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index 04099f41b..fbebac11d 100644 --- a/Makefile +++ b/Makefile @@ -50,15 +50,15 @@ gox: .gopath $(SOURCE) test-integration: .gopath cd $(BASEPATH) && go test $(GOTESTFLAGS) \ - -tags integration \ ./backend \ -cover -covermode=count -coverprofile=integration-sftp.cov \ + -test.integration \ -test.sftppath=$(SFTP_PATH) cd $(BASEPATH) && go test $(GOTESTFLAGS) \ - -tags integration \ ./cmd/restic \ -cover -covermode=count -coverprofile=integration.cov \ + -test.integration \ -test.datafile=$(PWD)/testsuite/fake-data.tar.gz all.cov: .gopath $(SOURCE) test-integration diff --git a/backend/sftp_test.go b/backend/sftp_test.go index 189e26d53..15ab04ead 100644 --- a/backend/sftp_test.go +++ b/backend/sftp_test.go @@ -1,5 +1,3 @@ -// +build integration - package backend_test import ( @@ -37,6 +35,10 @@ func teardownSFTPBackend(t *testing.T, b *sftp.SFTP) { } func TestSFTPBackend(t *testing.T) { + if !*RunIntegrationTest { + t.Skip("integration tests disabled, use `-test.integration` to enable") + } + if *sftpPath == "" { t.Skipf("sftppath not set, skipping TestSFTPBackend") } diff --git a/cmd/restic/integration_helpers_test.go b/cmd/restic/integration_helpers_test.go index 2f3f0ec72..65da5878b 100644 --- a/cmd/restic/integration_helpers_test.go +++ b/cmd/restic/integration_helpers_test.go @@ -1,5 +1,3 @@ -// +build integration - package main import ( diff --git a/cmd/restic/integration_test.go b/cmd/restic/integration_test.go index 4d8106974..f07c35d10 100644 --- a/cmd/restic/integration_test.go +++ b/cmd/restic/integration_test.go @@ -1,5 +1,3 @@ -// +build integration - package main import ( @@ -119,8 +117,12 @@ func cmdFsck(t testing.TB) { } func TestBackup(t *testing.T) { + if !*RunIntegrationTest { + t.Skip("integration tests disabled, use `-test.integration` to enable") + } + if *TestDataFile == "" { - t.Fatal("no data tar file specified, use flag '-test.datafile'") + t.Fatal("no data tar file specified, use flag `-test.datafile`") } tempdir := setupTempdir(t) diff --git a/test/backend.go b/test/backend.go index f1374d14d..8abd28582 100644 --- a/test/backend.go +++ b/test/backend.go @@ -12,9 +12,12 @@ import ( "github.com/restic/restic/repository" ) -var TestPassword = flag.String("test.password", "geheim", `use this password for repositories created during tests (default: "geheim")`) -var TestCleanup = flag.Bool("test.cleanup", true, "clean up after running tests (remove local backend directory with all content)") -var TestTempDir = flag.String("test.tempdir", "", "use this directory for temporary storage (default: system temp dir)") +var ( + TestPassword = flag.String("test.password", "geheim", `use this password for repositories created during tests (default: "geheim")`) + TestCleanup = flag.Bool("test.cleanup", true, "clean up after running tests (remove local backend directory with all content)") + TestTempDir = flag.String("test.tempdir", "", "use this directory for temporary storage (default: system temp dir)") + RunIntegrationTest = flag.Bool("test.integration", false, "run integration tests (default: false)") +) func SetupRepo(t testing.TB) *repository.Repository { tempdir, err := ioutil.TempDir(*TestTempDir, "restic-test-") From 9c2478a291f16ad4d238632fb56c34272f3289bb Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Mon, 8 Jun 2015 00:35:21 +0200 Subject: [PATCH 05/31] Add run_tests.go --- run_tests.go | 162 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 run_tests.go diff --git a/run_tests.go b/run_tests.go new file mode 100644 index 000000000..28b1ef389 --- /dev/null +++ b/run_tests.go @@ -0,0 +1,162 @@ +// +build ignore + +package main + +import ( + "bufio" + "fmt" + "io" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" +) + +func specialDir(name string) bool { + if name == "." { + return false + } + + base := filepath.Base(name) + return base[0] == '_' || base[0] == '.' +} + +func emptyDir(name string) bool { + dir, err := os.Open(name) + defer dir.Close() + if err != nil { + fmt.Fprintf(os.Stderr, "unable to open directory %v: %v\n", name, err) + return true + } + + fis, err := dir.Readdir(-1) + if err != nil { + fmt.Fprintf(os.Stderr, "Readdirnames(%v): %v\n", name, err) + return true + } + + for _, fi := range fis { + if fi.IsDir() { + continue + } + + if filepath.Ext(fi.Name()) == ".go" { + return false + } + } + + return true +} + +func forceRelativeDirname(dirname string) string { + if dirname == "." { + return dirname + } + + if strings.HasPrefix(dirname, "./") { + return dirname + } + + return "./" + dirname +} + +func mergeCoverprofile(file *os.File, out io.Writer) error { + _, err := file.Seek(0, 0) + if err != nil { + return err + } + + rd := bufio.NewReader(file) + _, err = rd.ReadString('\n') + if err == io.EOF { + return nil + } + + if err != nil { + return err + } + + _, err = io.Copy(out, rd) + if err != nil { + return err + } + + err = file.Close() + if err != nil { + return err + } + + return err +} + +func testPackage(pkg string, out io.Writer) error { + file, err := ioutil.TempFile("", "test-coverage-") + defer os.Remove(file.Name()) + defer file.Close() + if err != nil { + return err + } + + cmd := exec.Command("go", "test", + "-cover", "-covermode", "set", "-coverprofile", file.Name(), + pkg, "-test.integration") + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + + err = cmd.Run() + if err != nil { + return err + } + + return mergeCoverprofile(file, out) +} + +func main() { + if len(os.Args) < 2 { + fmt.Fprintln(os.Stderr, "USAGE: run_tests COVERPROFILE [PATHS]") + os.Exit(1) + } + + target := os.Args[1] + dirs := os.Args[2:] + + if len(dirs) == 0 { + dirs = append(dirs, ".") + } + + file, err := os.Create(target) + if err != nil { + fmt.Fprintf(os.Stderr, "create coverprofile failed: %v\n", err) + os.Exit(1) + } + + fmt.Fprintln(file, "mode: set") + + for _, dir := range dirs { + err := filepath.Walk(dir, + func(p string, fi os.FileInfo, e error) error { + if e != nil { + return e + } + + if !fi.IsDir() { + return nil + } + + if specialDir(p) || emptyDir(p) { + return filepath.SkipDir + } + + return testPackage(forceRelativeDirname(p), file) + }) + + if err != nil { + fmt.Fprintf(os.Stderr, "walk(%q): %v\n", dir, err) + } + } + + err = file.Close() + + fmt.Printf("coverprofile: %v\n", file.Name()) +} From d8d09b6d691d9b7b91183810a546b462c532b55d Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Thu, 11 Jun 2015 21:07:51 +0200 Subject: [PATCH 06/31] Fix restic configuration for integration tests --- cmd/restic/integration_test.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/cmd/restic/integration_test.go b/cmd/restic/integration_test.go index f07c35d10..63c74691e 100644 --- a/cmd/restic/integration_test.go +++ b/cmd/restic/integration_test.go @@ -24,12 +24,11 @@ func setupTempdir(t testing.TB) (tempdir string) { } func configureRestic(t testing.TB, tempdir string) { - // use cache dir within tempdir - OK(t, os.Setenv("RESTIC_CACHE", filepath.Join(tempdir, "cache"))) - - // configure environment + opts.CacheDir = filepath.Join(tempdir, "cache") opts.Repo = filepath.Join(tempdir, "repo") - OK(t, os.Setenv("RESTIC_PASSWORD", *TestPassword)) + opts.Quiet = true + + opts.password = *TestPassword } func cleanupTempdir(t testing.TB, tempdir string) { From e071ca57d506d4b07089cd372dc9f0e176b5da5f Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Thu, 11 Jun 2015 21:06:18 +0200 Subject: [PATCH 07/31] Use `run_tests.go` for testing coverage --- Makefile | 10 ++-------- coverage_all.sh | 10 ---------- 2 files changed, 2 insertions(+), 18 deletions(-) delete mode 100755 coverage_all.sh diff --git a/Makefile b/Makefile index fbebac11d..2f6f1cdd1 100644 --- a/Makefile +++ b/Makefile @@ -61,14 +61,8 @@ test-integration: .gopath -test.integration \ -test.datafile=$(PWD)/testsuite/fake-data.tar.gz -all.cov: .gopath $(SOURCE) test-integration - cd $(BASEPATH) && \ - go list ./... | while read pkg; do \ - go test -covermode=count -coverprofile=$$(echo $$pkg | base64).cov $$pkg; \ - done - - echo "mode: count" > all.cov - tail -q -n +2 *.cov >> all.cov +all.cov: .gopath $(SOURCE) + cd $(BASEPATH) && go run run_tests.go all.cov env: @echo export GOPATH=\"$(GOPATH)\" diff --git a/coverage_all.sh b/coverage_all.sh deleted file mode 100755 index ae448b743..000000000 --- a/coverage_all.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -TARGETFILE="$1" - -go list ./... | while read pkg; do - go test -covermode=count -coverprofile=$(base64 <<< $pkg).cov $pkg -done - -echo "mode: count" > $TARGETFILE -tail -q -n +2 *.cov */*.cov */*/*.cov >> $TARGETFILE From cd5b788e480f9adeef8d0f68310704f7c8cd12dd Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Thu, 11 Jun 2015 22:45:22 +0200 Subject: [PATCH 08/31] Ignore goverall errors --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 225576060..43acbb905 100644 --- a/.travis.yml +++ b/.travis.yml @@ -36,6 +36,6 @@ script: - make test-integration - GOARCH=386 make test-integration - make all.cov - - goveralls -coverprofile=all.cov -service=travis-ci -repotoken "$COVERALLS_TOKEN" + - goveralls -coverprofile=all.cov -service=travis-ci -repotoken "$COVERALLS_TOKEN" || true - gofmt -l *.go */*.go */*/*.go - test -z "$(gofmt -l *.go */*.go */*/*.go)" From da2e9d447f89bd159ebdbe718c1057773e009a75 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Thu, 11 Jun 2015 23:17:15 +0200 Subject: [PATCH 09/31] Make tests run by travis less verbose --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 43acbb905..3fe3823b5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,7 +31,7 @@ install: script: - make restic - make gox - - make test + - GOTESTFLAGS="" make test - GOARCH=386 make test - make test-integration - GOARCH=386 make test-integration From 002c7883c33e1306bc3d54eb406081858050100b Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sat, 13 Jun 2015 12:26:38 +0200 Subject: [PATCH 10/31] run_tests: Do not ignore subdirs of empty dirs --- run_tests.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/run_tests.go b/run_tests.go index 28b1ef389..5cfdaa51e 100644 --- a/run_tests.go +++ b/run_tests.go @@ -144,10 +144,14 @@ func main() { return nil } - if specialDir(p) || emptyDir(p) { + if specialDir(p) { return filepath.SkipDir } + if emptyDir(p) { + return nil + } + return testPackage(forceRelativeDirname(p), file) }) From 030f08a410992b863a303ccbc4be10b1473477c2 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sat, 13 Jun 2015 12:35:19 +0200 Subject: [PATCH 11/31] Remove flags from tests --- backend/backend_test.go | 2 +- backend/local_test.go | 2 +- backend/sftp_test.go | 4 ++-- cmd/restic/integration_test.go | 8 +++---- node_test.go | 4 ++-- test/backend.go | 40 +++++++++++++++++++++++++++------- tree_test.go | 4 ++-- 7 files changed, 44 insertions(+), 20 deletions(-) diff --git a/backend/backend_test.go b/backend/backend_test.go index da5606551..862d85657 100644 --- a/backend/backend_test.go +++ b/backend/backend_test.go @@ -102,7 +102,7 @@ func testBackend(b backend.Backend, t *testing.T) { } // remove content if requested - if *TestCleanup { + if TestCleanup { for _, test := range TestStrings { id, err := backend.ParseID(test.id) OK(t, err) diff --git a/backend/local_test.go b/backend/local_test.go index 9bc903f0a..24c7091bd 100644 --- a/backend/local_test.go +++ b/backend/local_test.go @@ -22,7 +22,7 @@ func setupLocalBackend(t *testing.T) *local.Local { } func teardownLocalBackend(t *testing.T, b *local.Local) { - if !*TestCleanup { + if !TestCleanup { t.Logf("leaving local backend at %s\n", b.Location()) return } diff --git a/backend/sftp_test.go b/backend/sftp_test.go index 15ab04ead..aa262176d 100644 --- a/backend/sftp_test.go +++ b/backend/sftp_test.go @@ -25,7 +25,7 @@ func setupSFTPBackend(t *testing.T) *sftp.SFTP { } func teardownSFTPBackend(t *testing.T, b *sftp.SFTP) { - if !*TestCleanup { + if !TestCleanup { t.Logf("leaving backend at %s\n", b.Location()) return } @@ -35,7 +35,7 @@ func teardownSFTPBackend(t *testing.T, b *sftp.SFTP) { } func TestSFTPBackend(t *testing.T) { - if !*RunIntegrationTest { + if !RunIntegrationTest { t.Skip("integration tests disabled, use `-test.integration` to enable") } diff --git a/cmd/restic/integration_test.go b/cmd/restic/integration_test.go index 63c74691e..e602b9113 100644 --- a/cmd/restic/integration_test.go +++ b/cmd/restic/integration_test.go @@ -17,7 +17,7 @@ import ( var TestDataFile = flag.String("test.datafile", "", `specify tar.gz file with test data to backup and restore (required for integration test)`) func setupTempdir(t testing.TB) (tempdir string) { - tempdir, err := ioutil.TempDir(*TestTempDir, "restic-test-") + tempdir, err := ioutil.TempDir(TestTempDir, "restic-test-") OK(t, err) return tempdir @@ -28,11 +28,11 @@ func configureRestic(t testing.TB, tempdir string) { opts.Repo = filepath.Join(tempdir, "repo") opts.Quiet = true - opts.password = *TestPassword + opts.password = TestPassword } func cleanupTempdir(t testing.TB, tempdir string) { - if !*TestCleanup { + if !TestCleanup { t.Logf("leaving temporary directory %v used for test", tempdir) return } @@ -116,7 +116,7 @@ func cmdFsck(t testing.TB) { } func TestBackup(t *testing.T) { - if !*RunIntegrationTest { + if !RunIntegrationTest { t.Skip("integration tests disabled, use `-test.integration` to enable") } diff --git a/node_test.go b/node_test.go index 33d222d78..ff4d4d721 100644 --- a/node_test.go +++ b/node_test.go @@ -103,11 +103,11 @@ var nodeTests = []restic.Node{ } func TestNodeRestoreAt(t *testing.T) { - tempdir, err := ioutil.TempDir(*TestTempDir, "restic-test-") + tempdir, err := ioutil.TempDir(TestTempDir, "restic-test-") OK(t, err) defer func() { - if *TestCleanup { + if TestCleanup { OK(t, os.RemoveAll(tempdir)) } else { t.Logf("leaving tempdir at %v", tempdir) diff --git a/test/backend.go b/test/backend.go index 8abd28582..9baa3f220 100644 --- a/test/backend.go +++ b/test/backend.go @@ -1,8 +1,9 @@ package test_helper import ( - "flag" + "fmt" "io/ioutil" + "os" "path/filepath" "testing" @@ -13,14 +14,37 @@ import ( ) var ( - TestPassword = flag.String("test.password", "geheim", `use this password for repositories created during tests (default: "geheim")`) - TestCleanup = flag.Bool("test.cleanup", true, "clean up after running tests (remove local backend directory with all content)") - TestTempDir = flag.String("test.tempdir", "", "use this directory for temporary storage (default: system temp dir)") - RunIntegrationTest = flag.Bool("test.integration", false, "run integration tests (default: false)") + TestPassword = getStringVar("RESTIC_TEST_PASSWORD", "geheim") + TestCleanup = getBoolVar("RESTIC_TEST_CLEANUP", true) + TestTempDir = getStringVar("RESTIC_TEST_TMPDIR", "") + RunIntegrationTest = getBoolVar("RESTIC_TEST_INTEGRATION", true) ) +func getStringVar(name, defaultValue string) string { + if e := os.Getenv(name); e != "" { + return e + } + + return defaultValue +} + +func getBoolVar(name string, defaultValue bool) bool { + if e := os.Getenv(name); e != "" { + switch e { + case "1": + return true + case "0": + return false + default: + fmt.Fprintf(os.Stderr, "invalid value for variable %q, using default\n", name) + } + } + + return defaultValue +} + func SetupRepo(t testing.TB) *repository.Repository { - tempdir, err := ioutil.TempDir(*TestTempDir, "restic-test-") + tempdir, err := ioutil.TempDir(TestTempDir, "restic-test-") OK(t, err) // create repository below temp dir @@ -28,12 +52,12 @@ func SetupRepo(t testing.TB) *repository.Repository { OK(t, err) repo := repository.New(b) - OK(t, repo.Init(*TestPassword)) + OK(t, repo.Init(TestPassword)) return repo } func TeardownRepo(t testing.TB, repo *repository.Repository) { - if !*TestCleanup { + if !TestCleanup { l := repo.Backend().(*local.Local) t.Logf("leaving local backend at %s\n", l.Location()) return diff --git a/tree_test.go b/tree_test.go index 6a1984d48..b46f01b0a 100644 --- a/tree_test.go +++ b/tree_test.go @@ -22,7 +22,7 @@ var testFiles = []struct { } func createTempDir(t *testing.T) string { - tempdir, err := ioutil.TempDir(*TestTempDir, "restic-test-") + tempdir, err := ioutil.TempDir(TestTempDir, "restic-test-") OK(t, err) for _, test := range testFiles { @@ -49,7 +49,7 @@ func createTempDir(t *testing.T) string { func TestTree(t *testing.T) { dir := createTempDir(t) defer func() { - if *TestCleanup { + if TestCleanup { OK(t, os.RemoveAll(dir)) } }() From 3a65f27c3f5c381c428221a449cbd0febadded46 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sat, 13 Jun 2015 12:40:16 +0200 Subject: [PATCH 12/31] Automatically find fixtures for integration test --- cmd/restic/integration_test.go | 14 ++++++++------ cmd/restic/testdata/backup-data.tar.gz | Bin 0 -> 177734 bytes 2 files changed, 8 insertions(+), 6 deletions(-) create mode 100644 cmd/restic/testdata/backup-data.tar.gz diff --git a/cmd/restic/integration_test.go b/cmd/restic/integration_test.go index e602b9113..0a77517e4 100644 --- a/cmd/restic/integration_test.go +++ b/cmd/restic/integration_test.go @@ -2,7 +2,6 @@ package main import ( "bufio" - "flag" "io" "io/ioutil" "os" @@ -14,8 +13,6 @@ import ( . "github.com/restic/restic/test" ) -var TestDataFile = flag.String("test.datafile", "", `specify tar.gz file with test data to backup and restore (required for integration test)`) - func setupTempdir(t testing.TB) (tempdir string) { tempdir, err := ioutil.TempDir(TestTempDir, "restic-test-") OK(t, err) @@ -120,9 +117,14 @@ func TestBackup(t *testing.T) { t.Skip("integration tests disabled, use `-test.integration` to enable") } - if *TestDataFile == "" { - t.Fatal("no data tar file specified, use flag `-test.datafile`") + 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()) tempdir := setupTempdir(t) defer cleanupTempdir(t, tempdir) @@ -133,7 +135,7 @@ func TestBackup(t *testing.T) { datadir := filepath.Join(tempdir, "testdata") - setupTarTestFixture(t, datadir, *TestDataFile) + setupTarTestFixture(t, datadir, datafile) // first backup cmdBackup(t, []string{datadir}, nil) diff --git a/cmd/restic/testdata/backup-data.tar.gz b/cmd/restic/testdata/backup-data.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..337c18fd9d54d427fcda76a39fae9c73a881d7fb GIT binary patch literal 177734 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

thKh&Fwhb!2fm6ttc-Bd{>9I~ir(TMd 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Ⓢ@|+yXauB-2e0TL1lzByjK1 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 Date: Sat, 13 Jun 2015 12:50:03 +0200 Subject: [PATCH 13/31] run_tests: Pass additional parameters to `go test` --- run_tests.go | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/run_tests.go b/run_tests.go index 5cfdaa51e..26a885ccf 100644 --- a/run_tests.go +++ b/run_tests.go @@ -90,7 +90,7 @@ func mergeCoverprofile(file *os.File, out io.Writer) error { return err } -func testPackage(pkg string, out io.Writer) error { +func testPackage(pkg string, params []string, out io.Writer) error { file, err := ioutil.TempFile("", "test-coverage-") defer os.Remove(file.Name()) defer file.Close() @@ -98,9 +98,10 @@ func testPackage(pkg string, out io.Writer) error { return err } - cmd := exec.Command("go", "test", - "-cover", "-covermode", "set", "-coverprofile", file.Name(), - pkg, "-test.integration") + args := []string{"test", "-cover", "-covermode", "set", "-coverprofile", + file.Name(), pkg} + args = append(args, params...) + cmd := exec.Command("go", args...) cmd.Stderr = os.Stderr cmd.Stdout = os.Stdout @@ -114,12 +115,23 @@ func testPackage(pkg string, out io.Writer) error { func main() { if len(os.Args) < 2 { - fmt.Fprintln(os.Stderr, "USAGE: run_tests COVERPROFILE [PATHS]") + fmt.Fprintln(os.Stderr, "USAGE: run_tests COVERPROFILE [TESTFLAGS] [-- [PATHS]]") os.Exit(1) } target := os.Args[1] - dirs := os.Args[2:] + args := os.Args[2:] + + paramsForTest := []string{} + dirs := []string{} + for i, arg := range args { + if arg == "--" { + dirs = args[i+1:] + break + } + + paramsForTest = append(paramsForTest, arg) + } if len(dirs) == 0 { dirs = append(dirs, ".") @@ -152,7 +164,7 @@ func main() { return nil } - return testPackage(forceRelativeDirname(p), file) + return testPackage(forceRelativeDirname(p), paramsForTest, file) }) if err != nil { From 9853fbcf4859314b2c751deb8976c2f69c2a5ecc Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sat, 13 Jun 2015 13:16:43 +0200 Subject: [PATCH 14/31] Remove more flags from tests --- backend/sftp_test.go | 33 ++++++++++++++++++++++++--------- cmd/restic/integration_test.go | 2 +- test/backend.go | 2 ++ 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/backend/sftp_test.go b/backend/sftp_test.go index aa262176d..b678e8ea9 100644 --- a/backend/sftp_test.go +++ b/backend/sftp_test.go @@ -1,22 +1,37 @@ package backend_test import ( - "flag" "io/ioutil" "os" + "path/filepath" + "strings" "testing" "github.com/restic/restic/backend/sftp" . "github.com/restic/restic/test" ) -var sftpPath = flag.String("test.sftppath", "", "sftp binary path (default: empty)") - func setupSFTPBackend(t *testing.T) *sftp.SFTP { + sftpserver := "" + + for _, dir := range strings.Split(TestSFTPPath, ":") { + testpath := filepath.Join(dir, "sftp-server") + fd, err := os.Open(testpath) + fd.Close() + if !os.IsNotExist(err) { + sftpserver = testpath + break + } + } + + if sftpserver == "" { + return nil + } + tempdir, err := ioutil.TempDir("", "restic-test-") OK(t, err) - b, err := sftp.Create(tempdir, *sftpPath) + b, err := sftp.Create(tempdir, sftpserver) OK(t, err) t.Logf("created sftp backend locally at %s", tempdir) @@ -36,14 +51,14 @@ func teardownSFTPBackend(t *testing.T, b *sftp.SFTP) { func TestSFTPBackend(t *testing.T) { if !RunIntegrationTest { - t.Skip("integration tests disabled, use `-test.integration` to enable") - } - - if *sftpPath == "" { - t.Skipf("sftppath not set, skipping TestSFTPBackend") + t.Skip("integration tests disabled") } s := setupSFTPBackend(t) + if s == nil { + t.Skip("unable to find sftp-server binary") + return + } defer teardownSFTPBackend(t, s) testBackend(s, t) diff --git a/cmd/restic/integration_test.go b/cmd/restic/integration_test.go index 0a77517e4..ac5f921bf 100644 --- a/cmd/restic/integration_test.go +++ b/cmd/restic/integration_test.go @@ -114,7 +114,7 @@ func cmdFsck(t testing.TB) { func TestBackup(t *testing.T) { if !RunIntegrationTest { - t.Skip("integration tests disabled, use `-test.integration` to enable") + t.Skip("integration tests disabled") } datafile := filepath.Join("testdata", "backup-data.tar.gz") diff --git a/test/backend.go b/test/backend.go index 9baa3f220..bcf874c0f 100644 --- a/test/backend.go +++ b/test/backend.go @@ -18,6 +18,8 @@ var ( TestCleanup = getBoolVar("RESTIC_TEST_CLEANUP", true) TestTempDir = getStringVar("RESTIC_TEST_TMPDIR", "") RunIntegrationTest = getBoolVar("RESTIC_TEST_INTEGRATION", true) + TestSFTPPath = getStringVar("RESTIC_TEST_SFTPPATH", + "/usr/lib/ssh:/usr/lib/openssh") ) func getStringVar(name, defaultValue string) string { From cf27a0fdc76520c9690f11a8de8f5639eae0a57f Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sat, 13 Jun 2015 13:21:00 +0200 Subject: [PATCH 15/31] Test travis --- .travis.yml | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3fe3823b5..dd424db47 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,7 @@ os: - linux - osx -env: SFTP_PATH="/usr/lib/openssh/sftp-server" GOX_OS="linux darwin openbsd freebsd" +env: GOX_OS="linux darwin openbsd freebsd" notifications: irc: @@ -19,23 +19,19 @@ notifications: skip_join: true install: + - go version + - export GOBIN="$GOPATH/bin" + - export PATH="$PATH:$GOBIN" + - export GOPATH="$GOPATH:${TRAVIS_BUILD_DIR}/Godeps/_workspace" + - go env - go get github.com/mattn/goveralls - go get github.com/mitchellh/gox - gox -build-toolchain -os "$GOX_OS" - - go version - - go env - - make env - - make goenv - - make list script: - - make restic - - make gox - - GOTESTFLAGS="" make test - - GOARCH=386 make test - - make test-integration - - GOARCH=386 make test-integration - - make all.cov + - gox -verbose -os "${GOX_OS}" ./cmd/restic + - go run run_tests.go all.cov + - GOARCH=386 go test ./... - goveralls -coverprofile=all.cov -service=travis-ci -repotoken "$COVERALLS_TOKEN" || true - gofmt -l *.go */*.go */*/*.go - test -z "$(gofmt -l *.go */*.go */*/*.go)" From 246fdb13f9a43855e9f1bdba981972d96e233662 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sat, 13 Jun 2015 14:30:15 +0200 Subject: [PATCH 16/31] Makefile: Remove target `test-integration` --- Makefile | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/Makefile b/Makefile index 2f6f1cdd1..2821e4a5d 100644 --- a/Makefile +++ b/Makefile @@ -48,19 +48,6 @@ gox: .gopath $(SOURCE) cd $(BASEPATH) && \ gox -verbose -os "$(GOX_OS)" ./cmd/restic -test-integration: .gopath - cd $(BASEPATH) && go test $(GOTESTFLAGS) \ - ./backend \ - -cover -covermode=count -coverprofile=integration-sftp.cov \ - -test.integration \ - -test.sftppath=$(SFTP_PATH) - - cd $(BASEPATH) && go test $(GOTESTFLAGS) \ - ./cmd/restic \ - -cover -covermode=count -coverprofile=integration.cov \ - -test.integration \ - -test.datafile=$(PWD)/testsuite/fake-data.tar.gz - all.cov: .gopath $(SOURCE) cd $(BASEPATH) && go run run_tests.go all.cov From a176b1b5a6e36338785303ed83fbf17443389fac Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 14 Jun 2015 15:19:11 +0200 Subject: [PATCH 17/31] Add more integration tests --- cmd/restic/integration_helpers_test.go | 100 ++++++++++++- cmd/restic/integration_test.go | 194 ++++++++++++++++--------- 2 files changed, 217 insertions(+), 77 deletions(-) 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) + }) } From e2563b3ecac5eb3be87faa571a4146d02677705d Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Thu, 18 Jun 2015 21:28:38 +0200 Subject: [PATCH 18/31] Fix comments --- cmd/restic/integration_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/restic/integration_test.go b/cmd/restic/integration_test.go index 1da97c700..d097ada5d 100644 --- a/cmd/restic/integration_test.go +++ b/cmd/restic/integration_test.go @@ -91,7 +91,7 @@ func TestBackup(t *testing.T) { 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) + t.Skipf("unable to find data file %q, skipping", datafile) return } OK(t, err) @@ -182,7 +182,7 @@ func TestIncrementalBackup(t *testing.T) { 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) + t.Skipf("unable to find data file %q, skipping", datafile) return } OK(t, err) From 1216ded14bb08793f44be6b48fb15febe8f0a02f Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Thu, 18 Jun 2015 21:28:50 +0200 Subject: [PATCH 19/31] Add integration test for key command --- cmd/restic/integration_test.go | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/cmd/restic/integration_test.go b/cmd/restic/integration_test.go index d097ada5d..a241bb203 100644 --- a/cmd/restic/integration_test.go +++ b/cmd/restic/integration_test.go @@ -86,6 +86,11 @@ func cmdFsck(t testing.TB) { OK(t, cmd.Execute(nil)) } +func cmdKey(t testing.TB, args ...string) { + cmd := &CmdKey{} + OK(t, cmd.Execute(args)) +} + func TestBackup(t *testing.T) { withTestEnvironment(t, func(env *testEnvironment) { datafile := filepath.Join("testdata", "backup-data.tar.gz") @@ -220,3 +225,19 @@ func TestIncrementalBackup(t *testing.T) { t.Logf("repository grown by %d bytes", stat3.size-stat2.size) }) } + +func TestKeyAddRemove(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", datafile) + return + } + OK(t, err) + OK(t, fd.Close()) + + cmdInit(t) + cmdKey(t, "list") + }) +} From a3e0907fc7035438cd2d29331a671fe38b54ef57 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Thu, 18 Jun 2015 21:29:15 +0200 Subject: [PATCH 20/31] remove old tests --- testsuite/test-backup-dedup.sh | 23 ----------------------- testsuite/test-backup-incremental.sh | 28 ---------------------------- testsuite/test-backup.sh | 18 ------------------ 3 files changed, 69 deletions(-) delete mode 100755 testsuite/test-backup-dedup.sh delete mode 100755 testsuite/test-backup-incremental.sh delete mode 100755 testsuite/test-backup.sh diff --git a/testsuite/test-backup-dedup.sh b/testsuite/test-backup-dedup.sh deleted file mode 100755 index a3f2e7a4d..000000000 --- a/testsuite/test-backup-dedup.sh +++ /dev/null @@ -1,23 +0,0 @@ -set -e - -prepare -run restic init - -# first backup without dedup -run restic backup "${BASE}/fake-data" -size=$(du -sm "$RESTIC_REPOSITORY" | cut -f1) -debug "size before: $size" - -# second backup with dedup -run restic backup "${BASE}/fake-data" -size2=$(du -sm "$RESTIC_REPOSITORY" | cut -f1) -debug "size after: $size2" - -# check if the repository hasn't grown more than 5% -threshhold=$(($size+$size/20)) -debug "threshhold is $threshhold" -if [[ "$size2" -gt "$threshhold" ]]; then - fail "dedup failed, repo grown more than 5%, before ${size}MiB after ${size2}MiB threshhold ${threshhold}MiB" -fi - -cleanup diff --git a/testsuite/test-backup-incremental.sh b/testsuite/test-backup-incremental.sh deleted file mode 100755 index 0a9ca3ec5..000000000 --- a/testsuite/test-backup-incremental.sh +++ /dev/null @@ -1,28 +0,0 @@ -set -e - -prepare -run restic init - -# create testfile -echo "testfile" > ${BASE}/fake-data/file - -# run first backup -run restic backup "${BASE}/fake-data" - -# remember snapshot id -SNAPSHOT=$(run restic list snapshots) - -# add data to testfile -date >> ${BASE}/fake-data/file - -# run backup again -run restic backup "${BASE}/fake-data" - -# add data to testfile -date >> ${BASE}/fake-data/file - -# run incremental backup -run restic backup -p "$SNAPSHOT" "${BASE}/fake-data" - -run restic fsck -o --check-data -cleanup diff --git a/testsuite/test-backup.sh b/testsuite/test-backup.sh deleted file mode 100755 index 4c3803d2c..000000000 --- a/testsuite/test-backup.sh +++ /dev/null @@ -1,18 +0,0 @@ -set -e - -prepare -run restic init -run restic backup "${BASE}/fake-data" -run restic restore "$(basename "$RESTIC_REPOSITORY"/snapshots/*)" "${BASE}/fake-data-restore" -dirdiff "${BASE}/fake-data" "${BASE}/fake-data-restore/fake-data" - -SNAPSHOT=$(restic list snapshots) -run restic backup -p "$SNAPSHOT" "${BASE}/fake-data" -run restic restore "$(basename "$RESTIC_REPOSITORY"/snapshots/*)" "${BASE}/fake-data-restore-incremental" -dirdiff "${BASE}/fake-data" "${BASE}/fake-data-restore-incremental/fake-data" - -echo "snapshot id is $SNAPSHOT" -restic ls "$SNAPSHOT" fake-data/0/0/1 | head -n 10 - -run restic fsck -o --check-data -cleanup From 2fa259816b78d264b059a6042c33ab148c630fa5 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 21 Jun 2015 11:38:07 +0200 Subject: [PATCH 21/31] rename `opts` to `mainOpts` --- cmd/restic/cmd_cache.go | 2 +- cmd/restic/integration_helpers_test.go | 8 +++--- cmd/restic/integration_test.go | 2 +- cmd/restic/main.go | 38 +++++++++++++------------- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/cmd/restic/cmd_cache.go b/cmd/restic/cmd_cache.go index 2dc2b73b7..dfbe52227 100644 --- a/cmd/restic/cmd_cache.go +++ b/cmd/restic/cmd_cache.go @@ -32,7 +32,7 @@ func (cmd CmdCache) Execute(args []string) error { return err } - cache, err := restic.NewCache(s, opts.CacheDir) + cache, err := restic.NewCache(s, mainOpts.CacheDir) if err != nil { return err } diff --git a/cmd/restic/integration_helpers_test.go b/cmd/restic/integration_helpers_test.go index a3b1e07ef..c93f2ed77 100644 --- a/cmd/restic/integration_helpers_test.go +++ b/cmd/restic/integration_helpers_test.go @@ -177,11 +177,11 @@ type testEnvironment struct { } func configureRestic(t testing.TB, cache, repo string) { - opts.CacheDir = cache - opts.Repo = repo - opts.Quiet = true + mainOpts.CacheDir = cache + mainOpts.Repo = repo + mainOpts.Quiet = true - opts.password = TestPassword + mainOpts.password = TestPassword } func cleanupTempdir(t testing.TB, tempdir string) { diff --git a/cmd/restic/integration_test.go b/cmd/restic/integration_test.go index a241bb203..143824bc5 100644 --- a/cmd/restic/integration_test.go +++ b/cmd/restic/integration_test.go @@ -49,7 +49,7 @@ func cmdInit(t testing.TB) { cmd := &CmdInit{} OK(t, cmd.Execute(nil)) - t.Logf("repository initialized at %v", opts.Repo) + t.Logf("repository initialized at %v", mainOpts.Repo) } func cmdBackup(t testing.TB, target []string, parentID backend.ID) { diff --git a/cmd/restic/main.go b/cmd/restic/main.go index b34755c84..812b57d67 100644 --- a/cmd/restic/main.go +++ b/cmd/restic/main.go @@ -19,7 +19,7 @@ import ( var version = "compiled manually" -var opts struct { +var mainOpts struct { Repo string `short:"r" long:"repo" description:"Repository directory to backup to/restore from"` CacheDir string ` long:"cache-dir" description:"Directory to use as a local cache"` Quiet bool `short:"q" long:"quiet" default:"false" description:"Do not output comprehensive progress report"` @@ -27,7 +27,7 @@ var opts struct { password string } -var parser = flags.NewParser(&opts, flags.Default) +var parser = flags.NewParser(&mainOpts, flags.Default) func errx(code int, format string, data ...interface{}) { if len(format) > 0 && format[len(format)-1] != '\n' { @@ -49,7 +49,7 @@ func readPassword(prompt string) string { } func disableProgress() bool { - if opts.Quiet { + if mainOpts.Quiet { return true } @@ -61,7 +61,7 @@ func disableProgress() bool { } func silenceRequested() bool { - if opts.Quiet { + if mainOpts.Quiet { return true } @@ -79,11 +79,11 @@ func verbosePrintf(format string, args ...interface{}) { type CmdInit struct{} func (cmd CmdInit) Execute(args []string) error { - if opts.Repo == "" { + if mainOpts.Repo == "" { return errors.New("Please specify repository location (-r)") } - if opts.password == "" { + if mainOpts.password == "" { pw := readPassword("enter password for new backend: ") pw2 := readPassword("enter password again: ") @@ -91,23 +91,23 @@ func (cmd CmdInit) Execute(args []string) error { errx(1, "passwords do not match") } - opts.password = pw + mainOpts.password = pw } - be, err := create(opts.Repo) + be, err := create(mainOpts.Repo) if err != nil { - fmt.Fprintf(os.Stderr, "creating backend at %s failed: %v\n", opts.Repo, err) + fmt.Fprintf(os.Stderr, "creating backend at %s failed: %v\n", mainOpts.Repo, err) os.Exit(1) } s := repository.New(be) - err = s.Init(opts.password) + err = s.Init(mainOpts.password) if err != nil { - fmt.Fprintf(os.Stderr, "creating key in backend at %s failed: %v\n", opts.Repo, err) + fmt.Fprintf(os.Stderr, "creating key in backend at %s failed: %v\n", mainOpts.Repo, err) os.Exit(1) } - verbosePrintf("created restic backend %v at %s\n", s.Config.ID[:10], opts.Repo) + verbosePrintf("created restic backend %v at %s\n", s.Config.ID[:10], mainOpts.Repo) verbosePrintf("\n") verbosePrintf("Please note that knowledge of your password is required to access\n") verbosePrintf("the repository. Losing your password means that your data is\n") @@ -163,22 +163,22 @@ func create(u string) (backend.Backend, error) { } func OpenRepo() (*repository.Repository, error) { - if opts.Repo == "" { + if mainOpts.Repo == "" { return nil, errors.New("Please specify repository location (-r)") } - be, err := open(opts.Repo) + be, err := open(mainOpts.Repo) if err != nil { return nil, err } s := repository.New(be) - if opts.password == "" { - opts.password = readPassword("enter password for repository: ") + if mainOpts.password == "" { + mainOpts.password = readPassword("enter password for repository: ") } - err = s.SearchKey(opts.password) + err = s.SearchKey(mainOpts.password) if err != nil { return nil, fmt.Errorf("unable to open repo: %v", err) } @@ -202,8 +202,8 @@ func init() { func main() { // defer profile.Start(profile.MemProfileRate(100000), profile.ProfilePath(".")).Stop() // defer profile.Start(profile.CPUProfile, profile.ProfilePath(".")).Stop() - opts.Repo = os.Getenv("RESTIC_REPOSITORY") - opts.password = os.Getenv("RESTIC_PASSWORD") + mainOpts.Repo = os.Getenv("RESTIC_REPOSITORY") + mainOpts.password = os.Getenv("RESTIC_PASSWORD") debug.Log("restic", "main %#v", os.Args) From 4388474cdc4ec06fb47dd130c0a9b0988d9d651d Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 21 Jun 2015 13:02:56 +0200 Subject: [PATCH 22/31] Restructure `cmd/restic`, no functional changes --- cmd/restic/cmd_backup.go | 26 ++-- cmd/restic/cmd_cache.go | 10 +- cmd/restic/cmd_cat.go | 8 +- cmd/restic/cmd_dump.go | 8 +- cmd/restic/cmd_find.go | 5 +- cmd/restic/cmd_fsck.go | 28 ++-- cmd/restic/cmd_init.go | 52 +++++++ cmd/restic/cmd_key.go | 39 +++-- cmd/restic/cmd_list.go | 7 +- cmd/restic/cmd_ls.go | 8 +- cmd/restic/cmd_restore.go | 12 +- cmd/restic/cmd_snapshots.go | 8 +- cmd/restic/global.go | 162 +++++++++++++++++++++ cmd/restic/integration_helpers_test.go | 8 +- cmd/restic/integration_test.go | 2 +- cmd/restic/main.go | 190 +------------------------ 16 files changed, 309 insertions(+), 264 deletions(-) create mode 100644 cmd/restic/cmd_init.go create mode 100644 cmd/restic/global.go diff --git a/cmd/restic/cmd_backup.go b/cmd/restic/cmd_backup.go index d8f2a3509..86bde9dbb 100644 --- a/cmd/restic/cmd_backup.go +++ b/cmd/restic/cmd_backup.go @@ -16,13 +16,15 @@ import ( type CmdBackup struct { Parent string `short:"p" long:"parent" description:"use this parent snapshot (default: last snapshot in repo that has the same target)"` Force bool `short:"f" long:"force" description:"Force re-reading the target. Overrides the \"parent\" flag"` + + global *GlobalOptions } func init() { _, err := parser.AddCommand("backup", "save file/directory", "The backup command creates a snapshot of a file or directory", - &CmdBackup{}) + &CmdBackup{global: &globalOpts}) if err != nil { panic(err) } @@ -97,8 +99,8 @@ func (cmd CmdBackup) Usage() string { return "DIR/FILE [DIR/FILE] [...]" } -func newScanProgress() *restic.Progress { - if disableProgress() { +func (cmd CmdBackup) newScanProgress() *restic.Progress { + if !cmd.global.ShowProgress() { return nil } @@ -113,8 +115,8 @@ func newScanProgress() *restic.Progress { return p } -func newArchiveProgress(todo restic.Stat) *restic.Progress { - if disableProgress() { +func (cmd CmdBackup) newArchiveProgress(todo restic.Stat) *restic.Progress { + if !cmd.global.ShowProgress() { return nil } @@ -213,7 +215,7 @@ func (cmd CmdBackup) Execute(args []string) error { target = append(target, d) } - s, err := OpenRepo() + s, err := cmd.global.OpenRepository() if err != nil { return err } @@ -232,7 +234,7 @@ func (cmd CmdBackup) Execute(args []string) error { return fmt.Errorf("invalid id %q: %v", cmd.Parent, err) } - verbosePrintf("found parent snapshot %v\n", parentSnapshotID.Str()) + cmd.global.Printf("found parent snapshot %v\n", parentSnapshotID.Str()) } // Find last snapshot to set it as parent, if not already set @@ -243,13 +245,13 @@ func (cmd CmdBackup) Execute(args []string) error { } if parentSnapshotID != nil { - verbosePrintf("using parent snapshot %v\n", parentSnapshotID) + cmd.global.Printf("using parent snapshot %v\n", parentSnapshotID) } } - verbosePrintf("scan %v\n", target) + cmd.global.Printf("scan %v\n", target) - stat, err := restic.Scan(target, newScanProgress()) + stat, err := restic.Scan(target, cmd.newScanProgress()) // TODO: add filter // arch.Filter = func(dir string, fi os.FileInfo) bool { @@ -264,12 +266,12 @@ func (cmd CmdBackup) Execute(args []string) error { return nil } - _, id, err := arch.Snapshot(newArchiveProgress(stat), target, parentSnapshotID) + _, id, err := arch.Snapshot(cmd.newArchiveProgress(stat), target, parentSnapshotID) if err != nil { return err } - verbosePrintf("snapshot %s saved\n", id.Str()) + cmd.global.Printf("snapshot %s saved\n", id.Str()) return nil } diff --git a/cmd/restic/cmd_cache.go b/cmd/restic/cmd_cache.go index dfbe52227..26a6b7970 100644 --- a/cmd/restic/cmd_cache.go +++ b/cmd/restic/cmd_cache.go @@ -6,13 +6,15 @@ import ( "github.com/restic/restic" ) -type CmdCache struct{} +type CmdCache struct { + global *GlobalOptions +} func init() { _, err := parser.AddCommand("cache", "manage cache", "The cache command creates and manages the local cache", - &CmdCache{}) + &CmdCache{global: &globalOpts}) if err != nil { panic(err) } @@ -27,12 +29,12 @@ func (cmd CmdCache) Execute(args []string) error { // return fmt.Errorf("wrong number of parameters, Usage: %s", cmd.Usage()) // } - s, err := OpenRepo() + s, err := cmd.global.OpenRepository() if err != nil { return err } - cache, err := restic.NewCache(s, mainOpts.CacheDir) + cache, err := restic.NewCache(s, cmd.global.CacheDir) if err != nil { return err } diff --git a/cmd/restic/cmd_cat.go b/cmd/restic/cmd_cat.go index 65e60c51f..65345cfe7 100644 --- a/cmd/restic/cmd_cat.go +++ b/cmd/restic/cmd_cat.go @@ -14,13 +14,15 @@ import ( "github.com/restic/restic/repository" ) -type CmdCat struct{} +type CmdCat struct { + global *GlobalOptions +} func init() { _, err := parser.AddCommand("cat", "dump something", "The cat command dumps data structures or data from a repository", - &CmdCat{}) + &CmdCat{global: &globalOpts}) if err != nil { panic(err) } @@ -35,7 +37,7 @@ func (cmd CmdCat) Execute(args []string) error { return fmt.Errorf("type or ID not specified, Usage: %s", cmd.Usage()) } - s, err := OpenRepo() + s, err := cmd.global.OpenRepository() if err != nil { return err } diff --git a/cmd/restic/cmd_dump.go b/cmd/restic/cmd_dump.go index 7e7a54293..bf5f8db86 100644 --- a/cmd/restic/cmd_dump.go +++ b/cmd/restic/cmd_dump.go @@ -15,13 +15,15 @@ import ( "github.com/restic/restic/repository" ) -type CmdDump struct{} +type CmdDump struct { + global *MainOptions +} func init() { _, err := parser.AddCommand("dump", "dump data structures", "The dump command dumps data structures from a repository as JSON documents", - &CmdDump{}) + &CmdDump{global: &mainOpts}) if err != nil { panic(err) } @@ -102,7 +104,7 @@ func (cmd CmdDump) Execute(args []string) error { return fmt.Errorf("type not specified, Usage: %s", cmd.Usage()) } - repo, err := OpenRepo() + repo, err := cmd.global.OpenRepository() if err != nil { return err } diff --git a/cmd/restic/cmd_find.go b/cmd/restic/cmd_find.go index 932d596cf..c1a56ebc6 100644 --- a/cmd/restic/cmd_find.go +++ b/cmd/restic/cmd_find.go @@ -23,6 +23,7 @@ type CmdFind struct { oldest, newest time.Time pattern string + global *GlobalOptions } var timeFormats = []string{ @@ -43,7 +44,7 @@ func init() { _, err := parser.AddCommand("find", "find a file/directory", "The find command searches for files or directories in snapshots", - &CmdFind{}) + &CmdFind{global: &globalOpts}) if err != nil { panic(err) } @@ -156,7 +157,7 @@ func (c CmdFind) Execute(args []string) error { } } - s, err := OpenRepo() + s, err := c.global.OpenRepository() if err != nil { return err } diff --git a/cmd/restic/cmd_fsck.go b/cmd/restic/cmd_fsck.go index 1f84a3b6e..17a0608ce 100644 --- a/cmd/restic/cmd_fsck.go +++ b/cmd/restic/cmd_fsck.go @@ -19,6 +19,8 @@ type CmdFsck struct { Orphaned bool `short:"o" long:"orphaned" description:"Check for orphaned blobs"` RemoveOrphaned bool `short:"r" long:"remove-orphaned" description:"Remove orphaned blobs (implies -o)"` + global *GlobalOptions + // lists checking for orphaned blobs o_data *backend.IDSet o_trees *backend.IDSet @@ -28,13 +30,13 @@ func init() { _, err := parser.AddCommand("fsck", "check the repository", "The fsck command check the integrity and consistency of the repository", - &CmdFsck{}) + &CmdFsck{global: &globalOpts}) if err != nil { panic(err) } } -func fsckFile(opts CmdFsck, repo *repository.Repository, IDs []backend.ID) (uint64, error) { +func fsckFile(global CmdFsck, repo *repository.Repository, IDs []backend.ID) (uint64, error) { debug.Log("restic.fsckFile", "checking file %v", IDs) var bytes uint64 @@ -50,7 +52,7 @@ func fsckFile(opts CmdFsck, repo *repository.Repository, IDs []backend.ID) (uint bytes += uint64(length - crypto.Extension) debug.Log("restic.fsck", " blob found in pack %v\n", packID) - if opts.CheckData { + if global.CheckData { // load content _, err := repo.LoadBlob(pack.Data, id) if err != nil { @@ -69,16 +71,16 @@ func fsckFile(opts CmdFsck, repo *repository.Repository, IDs []backend.ID) (uint } // if orphan check is active, record storage id - if opts.o_data != nil { + if global.o_data != nil { debug.Log("restic.fsck", " recording blob %v as used\n", id) - opts.o_data.Insert(id) + global.o_data.Insert(id) } } return bytes, nil } -func fsckTree(opts CmdFsck, repo *repository.Repository, id backend.ID) error { +func fsckTree(global CmdFsck, repo *repository.Repository, id backend.ID) error { debug.Log("restic.fsckTree", "checking tree %v", id.Str()) tree, err := restic.LoadTree(repo, id) @@ -87,9 +89,9 @@ func fsckTree(opts CmdFsck, repo *repository.Repository, id backend.ID) error { } // if orphan check is active, record storage id - if opts.o_trees != nil { + if global.o_trees != nil { // add ID to list - opts.o_trees.Insert(id) + global.o_trees.Insert(id) } var firstErr error @@ -123,7 +125,7 @@ func fsckTree(opts CmdFsck, repo *repository.Repository, id backend.ID) error { } debug.Log("restic.fsckTree", "check file %v (%v)", node.Name, id.Str()) - bytes, err := fsckFile(opts, repo, node.Content) + bytes, err := fsckFile(global, repo, node.Content) if err != nil { return err } @@ -140,7 +142,7 @@ func fsckTree(opts CmdFsck, repo *repository.Repository, id backend.ID) error { // record id seenIDs.Insert(node.Subtree) - err = fsckTree(opts, repo, node.Subtree) + err = fsckTree(global, repo, node.Subtree) if err != nil { firstErr = err fmt.Fprintf(os.Stderr, "%v\n", err) @@ -158,7 +160,7 @@ func fsckTree(opts CmdFsck, repo *repository.Repository, id backend.ID) error { return firstErr } -func fsckSnapshot(opts CmdFsck, repo *repository.Repository, id backend.ID) error { +func fsckSnapshot(global CmdFsck, repo *repository.Repository, id backend.ID) error { debug.Log("restic.fsck", "checking snapshot %v\n", id) sn, err := restic.LoadSnapshot(repo, id) @@ -166,7 +168,7 @@ func fsckSnapshot(opts CmdFsck, repo *repository.Repository, id backend.ID) erro return fmt.Errorf("loading snapshot %v failed: %v", id, err) } - err = fsckTree(opts, repo, sn.Tree) + err = fsckTree(global, repo, sn.Tree) if err != nil { debug.Log("restic.fsck", " checking tree %v for snapshot %v\n", sn.Tree, id) fmt.Fprintf(os.Stderr, "snapshot %v:\n error for tree %v:\n %v\n", id, sn.Tree, err) @@ -188,7 +190,7 @@ func (cmd CmdFsck) Execute(args []string) error { cmd.Orphaned = true } - s, err := OpenRepo() + s, err := cmd.global.OpenRepository() if err != nil { return err } diff --git a/cmd/restic/cmd_init.go b/cmd/restic/cmd_init.go new file mode 100644 index 000000000..1e75ee5f9 --- /dev/null +++ b/cmd/restic/cmd_init.go @@ -0,0 +1,52 @@ +package main + +import ( + "errors" + + "github.com/restic/restic/repository" +) + +type CmdInit struct { + global *GlobalOptions +} + +func (cmd CmdInit) Execute(args []string) error { + if cmd.global.Repo == "" { + return errors.New("Please specify repository location (-r)") + } + + if cmd.global.password == "" { + cmd.global.password = cmd.global.ReadPasswordTwice( + "enter password for new backend: ", + "enter password again: ") + } + + be, err := create(cmd.global.Repo) + if err != nil { + cmd.global.Exitf(1, "creating backend at %s failed: %v\n", cmd.global.Repo, err) + } + + s := repository.New(be) + err = s.Init(cmd.global.password) + if err != nil { + cmd.global.Exitf(1, "creating key in backend at %s failed: %v\n", cmd.global.Repo, err) + } + + cmd.global.Printf("created restic backend %v at %s\n", s.Config.ID[:10], cmd.global.Repo) + cmd.global.Printf("\n") + cmd.global.Printf("Please note that knowledge of your password is required to access\n") + cmd.global.Printf("the repository. Losing your password means that your data is\n") + cmd.global.Printf("irrecoverably lost.\n") + + return nil +} + +func init() { + _, err := parser.AddCommand("init", + "create repository", + "The init command creates a new repository", + &CmdInit{global: &globalOpts}) + if err != nil { + panic(err) + } +} diff --git a/cmd/restic/cmd_key.go b/cmd/restic/cmd_key.go index 86e390eef..a110357aa 100644 --- a/cmd/restic/cmd_key.go +++ b/cmd/restic/cmd_key.go @@ -9,19 +9,21 @@ import ( "github.com/restic/restic/repository" ) -type CmdKey struct{} +type CmdKey struct { + global *GlobalOptions +} func init() { _, err := parser.AddCommand("key", "manage keys", "The key command manages keys (passwords) of a repository", - &CmdKey{}) + &CmdKey{global: &globalOpts}) if err != nil { panic(err) } } -func listKeys(s *repository.Repository) error { +func (cmd CmdKey) listKeys(s *repository.Repository) error { tab := NewTable() tab.Header = fmt.Sprintf(" %-10s %-10s %-10s %s", "ID", "User", "Host", "Created") tab.RowFormat = "%s%-10s %-10s %-10s %s" @@ -56,23 +58,20 @@ func listKeys(s *repository.Repository) error { return nil } -func getNewPassword() (string, error) { +func (cmd CmdKey) getNewPassword() (string, error) { newPassword := os.Getenv("RESTIC_NEWPASSWORD") if newPassword == "" { - newPassword = readPassword("enter password for new key: ") - newPassword2 := readPassword("enter password again: ") - - if newPassword != newPassword2 { - return "", errors.New("passwords do not match") - } + newPassword = cmd.global.ReadPasswordTwice( + "enter password for new key: ", + "enter password again: ") } return newPassword, nil } -func addKey(repo *repository.Repository) error { - newPassword, err := getNewPassword() +func (cmd CmdKey) addKey(repo *repository.Repository) error { + newPassword, err := cmd.getNewPassword() if err != nil { return err } @@ -87,7 +86,7 @@ func addKey(repo *repository.Repository) error { return nil } -func deleteKey(repo *repository.Repository, name string) error { +func (cmd CmdKey) deleteKey(repo *repository.Repository, name string) error { if name == repo.KeyName() { return errors.New("refusing to remove key currently used to access repository") } @@ -101,8 +100,8 @@ func deleteKey(repo *repository.Repository, name string) error { return nil } -func changePassword(repo *repository.Repository) error { - newPassword, err := getNewPassword() +func (cmd CmdKey) changePassword(repo *repository.Repository) error { + newPassword, err := cmd.getNewPassword() if err != nil { return err } @@ -131,25 +130,25 @@ func (cmd CmdKey) Execute(args []string) error { return fmt.Errorf("wrong number of arguments, Usage: %s", cmd.Usage()) } - s, err := OpenRepo() + s, err := cmd.global.OpenRepository() if err != nil { return err } switch args[0] { case "list": - return listKeys(s) + return cmd.listKeys(s) case "add": - return addKey(s) + return cmd.addKey(s) case "rm": id, err := backend.Find(s.Backend(), backend.Key, args[1]) if err != nil { return err } - return deleteKey(s, id) + return cmd.deleteKey(s, id) case "passwd": - return changePassword(s) + return cmd.changePassword(s) } return nil diff --git a/cmd/restic/cmd_list.go b/cmd/restic/cmd_list.go index 641d72c2e..4d5cf0ab1 100644 --- a/cmd/restic/cmd_list.go +++ b/cmd/restic/cmd_list.go @@ -10,14 +10,15 @@ import ( ) type CmdList struct { - w io.Writer + w io.Writer + global *GlobalOptions } func init() { _, err := parser.AddCommand("list", "lists data", "The list command lists structures or data of a repository", - &CmdList{}) + &CmdList{global: &globalOpts}) if err != nil { panic(err) } @@ -36,7 +37,7 @@ func (cmd CmdList) Execute(args []string) error { return fmt.Errorf("type not specified, Usage: %s", cmd.Usage()) } - s, err := OpenRepo() + s, err := cmd.global.OpenRepository() if err != nil { return err } diff --git a/cmd/restic/cmd_ls.go b/cmd/restic/cmd_ls.go index 1f75b515c..59144befc 100644 --- a/cmd/restic/cmd_ls.go +++ b/cmd/restic/cmd_ls.go @@ -10,13 +10,15 @@ import ( "github.com/restic/restic/repository" ) -type CmdLs struct{} +type CmdLs struct { + global *GlobalOptions +} func init() { _, err := parser.AddCommand("ls", "list files", "The ls command lists all files and directories in a snapshot", - &CmdLs{}) + &CmdLs{global: &globalOpts}) if err != nil { panic(err) } @@ -67,7 +69,7 @@ func (cmd CmdLs) Execute(args []string) error { return fmt.Errorf("wrong number of arguments, Usage: %s", cmd.Usage()) } - s, err := OpenRepo() + s, err := cmd.global.OpenRepository() if err != nil { return err } diff --git a/cmd/restic/cmd_restore.go b/cmd/restic/cmd_restore.go index cd91228e6..183798932 100644 --- a/cmd/restic/cmd_restore.go +++ b/cmd/restic/cmd_restore.go @@ -8,13 +8,15 @@ import ( "github.com/restic/restic" ) -type CmdRestore struct{} +type CmdRestore struct { + global *GlobalOptions +} func init() { _, err := parser.AddCommand("restore", "restore a snapshot", "The restore command restores a snapshot to a directory", - &CmdRestore{}) + &CmdRestore{global: &globalOpts}) if err != nil { panic(err) } @@ -29,7 +31,7 @@ func (cmd CmdRestore) Execute(args []string) error { return fmt.Errorf("wrong number of arguments, Usage: %s", cmd.Usage()) } - s, err := OpenRepo() + s, err := cmd.global.OpenRepository() if err != nil { return err } @@ -41,7 +43,7 @@ func (cmd CmdRestore) Execute(args []string) error { id, err := restic.FindSnapshot(s, args[0]) if err != nil { - errx(1, "invalid id %q: %v", args[0], err) + cmd.global.Exitf(1, "invalid id %q: %v", args[0], err) } target := args[1] @@ -81,7 +83,7 @@ func (cmd CmdRestore) Execute(args []string) error { } } - verbosePrintf("restoring %s to %s\n", res.Snapshot(), target) + cmd.global.Printf("restoring %s to %s\n", res.Snapshot(), target) err = res.RestoreTo(target) if err != nil { diff --git a/cmd/restic/cmd_snapshots.go b/cmd/restic/cmd_snapshots.go index 3703884b2..f1f8ba31e 100644 --- a/cmd/restic/cmd_snapshots.go +++ b/cmd/restic/cmd_snapshots.go @@ -71,13 +71,15 @@ func reltime(t time.Time) string { } } -type CmdSnapshots struct{} +type CmdSnapshots struct { + global *GlobalOptions +} func init() { _, err := parser.AddCommand("snapshots", "show snapshots", "The snapshots command lists all snapshots stored in a repository", - &CmdSnapshots{}) + &CmdSnapshots{global: &globalOpts}) if err != nil { panic(err) } @@ -92,7 +94,7 @@ func (cmd CmdSnapshots) Execute(args []string) error { return fmt.Errorf("wrong number of arguments, usage: %s", cmd.Usage()) } - s, err := OpenRepo() + s, err := cmd.global.OpenRepository() if err != nil { return err } diff --git a/cmd/restic/global.go b/cmd/restic/global.go new file mode 100644 index 000000000..2f7ffe27e --- /dev/null +++ b/cmd/restic/global.go @@ -0,0 +1,162 @@ +package main + +import ( + "errors" + "fmt" + "io" + "net/url" + "os" + + "github.com/jessevdk/go-flags" + "github.com/restic/restic/backend" + "github.com/restic/restic/backend/local" + "github.com/restic/restic/backend/sftp" + "github.com/restic/restic/repository" + "golang.org/x/crypto/ssh/terminal" +) + +var version = "compiled manually" + +type GlobalOptions struct { + Repo string `short:"r" long:"repo" description:"Repository directory to backup to/restore from"` + CacheDir string ` long:"cache-dir" description:"Directory to use as a local cache"` + Quiet bool `short:"q" long:"quiet" default:"false" description:"Do not output comprehensive progress report"` + + password string + stdout io.Writer +} + +var globalOpts = GlobalOptions{stdout: os.Stdout} +var parser = flags.NewParser(&globalOpts, flags.Default) + +func (o GlobalOptions) Printf(format string, args ...interface{}) { + if o.Quiet { + return + } + + _, err := fmt.Fprintf(o.stdout, format, args...) + if err != nil { + fmt.Fprintf(os.Stderr, "unable to write to stdout: %v\n", err) + os.Exit(100) + } +} + +func (o GlobalOptions) ShowProgress() bool { + if o.Quiet { + return false + } + + if !terminal.IsTerminal(int(os.Stdout.Fd())) { + return false + } + + return true +} + +func (o GlobalOptions) Warnf(format string, args ...interface{}) { + _, err := fmt.Fprintf(os.Stderr, format, args...) + if err != nil { + fmt.Fprintf(os.Stderr, "unable to write to stderr: %v\n", err) + os.Exit(100) + } +} + +func (o GlobalOptions) Exitf(exitcode int, format string, args ...interface{}) { + if format[len(format)-1] != '\n' { + format += "\n" + } + + o.Warnf(format, args...) + os.Exit(exitcode) +} + +func (o GlobalOptions) ReadPassword(prompt string) string { + fmt.Fprint(os.Stderr, prompt) + pw, err := terminal.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + o.Exitf(2, "unable to read password: %v", err) + } + fmt.Fprintln(os.Stderr) + + return string(pw) +} + +func (o GlobalOptions) ReadPasswordTwice(prompt1, prompt2 string) string { + pw1 := o.ReadPassword(prompt1) + pw2 := o.ReadPassword(prompt2) + if pw1 != pw2 { + o.Exitf(1, "passwords do not match") + } + + return pw1 +} + +func (o GlobalOptions) OpenRepository() (*repository.Repository, error) { + if o.Repo == "" { + return nil, errors.New("Please specify repository location (-r)") + } + + be, err := open(o.Repo) + if err != nil { + return nil, err + } + + s := repository.New(be) + + if o.password == "" { + o.password = o.ReadPassword("enter password for repository: ") + } + + err = s.SearchKey(o.password) + if err != nil { + return nil, fmt.Errorf("unable to open repo: %v", err) + } + + return s, nil +} + +// Open the backend specified by URI. +// Valid formats are: +// * /foo/bar -> local repository at /foo/bar +// * sftp://user@host/foo/bar -> remote sftp repository on host for user at path foo/bar +// * sftp://host//tmp/backup -> remote sftp repository on host at path /tmp/backup +func open(u string) (backend.Backend, error) { + url, err := url.Parse(u) + if err != nil { + return nil, err + } + + if url.Scheme == "" { + return local.Open(url.Path) + } + + args := []string{url.Host} + if url.User != nil && url.User.Username() != "" { + args = append(args, "-l") + args = append(args, url.User.Username()) + } + args = append(args, "-s") + args = append(args, "sftp") + return sftp.Open(url.Path[1:], "ssh", args...) +} + +// Create the backend specified by URI. +func create(u string) (backend.Backend, error) { + url, err := url.Parse(u) + if err != nil { + return nil, err + } + + if url.Scheme == "" { + return local.Create(url.Path) + } + + args := []string{url.Host} + if url.User != nil && url.User.Username() != "" { + args = append(args, "-l") + args = append(args, url.User.Username()) + } + args = append(args, "-s") + args = append(args, "sftp") + return sftp.Create(url.Path[1:], "ssh", args...) +} diff --git a/cmd/restic/integration_helpers_test.go b/cmd/restic/integration_helpers_test.go index c93f2ed77..ee025f955 100644 --- a/cmd/restic/integration_helpers_test.go +++ b/cmd/restic/integration_helpers_test.go @@ -177,11 +177,11 @@ type testEnvironment struct { } func configureRestic(t testing.TB, cache, repo string) { - mainOpts.CacheDir = cache - mainOpts.Repo = repo - mainOpts.Quiet = true + globalOpts.CacheDir = cache + globalOpts.Repo = repo + globalOpts.Quiet = true - mainOpts.password = TestPassword + globalOpts.password = TestPassword } func cleanupTempdir(t testing.TB, tempdir string) { diff --git a/cmd/restic/integration_test.go b/cmd/restic/integration_test.go index 143824bc5..d61432b6a 100644 --- a/cmd/restic/integration_test.go +++ b/cmd/restic/integration_test.go @@ -49,7 +49,7 @@ func cmdInit(t testing.TB) { cmd := &CmdInit{} OK(t, cmd.Execute(nil)) - t.Logf("repository initialized at %v", mainOpts.Repo) + t.Logf("repository initialized at %v", globalOpts.Repo) } func cmdBackup(t testing.TB, target []string, parentID backend.ID) { diff --git a/cmd/restic/main.go b/cmd/restic/main.go index 812b57d67..8669ae8d6 100644 --- a/cmd/restic/main.go +++ b/cmd/restic/main.go @@ -1,209 +1,23 @@ package main import ( - "errors" - "fmt" - "net/url" "os" "runtime" - "golang.org/x/crypto/ssh/terminal" - "github.com/jessevdk/go-flags" - "github.com/restic/restic/backend" - "github.com/restic/restic/backend/local" - "github.com/restic/restic/backend/sftp" "github.com/restic/restic/debug" - "github.com/restic/restic/repository" ) -var version = "compiled manually" - -var mainOpts struct { - Repo string `short:"r" long:"repo" description:"Repository directory to backup to/restore from"` - CacheDir string ` long:"cache-dir" description:"Directory to use as a local cache"` - Quiet bool `short:"q" long:"quiet" default:"false" description:"Do not output comprehensive progress report"` - - password string -} - -var parser = flags.NewParser(&mainOpts, flags.Default) - -func errx(code int, format string, data ...interface{}) { - if len(format) > 0 && format[len(format)-1] != '\n' { - format += "\n" - } - fmt.Fprintf(os.Stderr, format, data...) - os.Exit(code) -} - -func readPassword(prompt string) string { - fmt.Fprint(os.Stderr, prompt) - pw, err := terminal.ReadPassword(int(os.Stdin.Fd())) - if err != nil { - errx(2, "unable to read password: %v", err) - } - fmt.Fprintln(os.Stderr) - - return string(pw) -} - -func disableProgress() bool { - if mainOpts.Quiet { - return true - } - - if !terminal.IsTerminal(int(os.Stdout.Fd())) { - return true - } - - return false -} - -func silenceRequested() bool { - if mainOpts.Quiet { - return true - } - - return false -} - -func verbosePrintf(format string, args ...interface{}) { - if silenceRequested() { - return - } - - fmt.Printf(format, args...) -} - -type CmdInit struct{} - -func (cmd CmdInit) Execute(args []string) error { - if mainOpts.Repo == "" { - return errors.New("Please specify repository location (-r)") - } - - if mainOpts.password == "" { - pw := readPassword("enter password for new backend: ") - pw2 := readPassword("enter password again: ") - - if pw != pw2 { - errx(1, "passwords do not match") - } - - mainOpts.password = pw - } - - be, err := create(mainOpts.Repo) - if err != nil { - fmt.Fprintf(os.Stderr, "creating backend at %s failed: %v\n", mainOpts.Repo, err) - os.Exit(1) - } - - s := repository.New(be) - err = s.Init(mainOpts.password) - if err != nil { - fmt.Fprintf(os.Stderr, "creating key in backend at %s failed: %v\n", mainOpts.Repo, err) - os.Exit(1) - } - - verbosePrintf("created restic backend %v at %s\n", s.Config.ID[:10], mainOpts.Repo) - verbosePrintf("\n") - verbosePrintf("Please note that knowledge of your password is required to access\n") - verbosePrintf("the repository. Losing your password means that your data is\n") - verbosePrintf("irrecoverably lost.\n") - - return nil -} - -// Open the backend specified by URI. -// Valid formats are: -// * /foo/bar -> local repository at /foo/bar -// * sftp://user@host/foo/bar -> remote sftp repository on host for user at path foo/bar -// * sftp://host//tmp/backup -> remote sftp repository on host at path /tmp/backup -func open(u string) (backend.Backend, error) { - url, err := url.Parse(u) - if err != nil { - return nil, err - } - - if url.Scheme == "" { - return local.Open(url.Path) - } - - args := []string{url.Host} - if url.User != nil && url.User.Username() != "" { - args = append(args, "-l") - args = append(args, url.User.Username()) - } - args = append(args, "-s") - args = append(args, "sftp") - return sftp.Open(url.Path[1:], "ssh", args...) -} - -// Create the backend specified by URI. -func create(u string) (backend.Backend, error) { - url, err := url.Parse(u) - if err != nil { - return nil, err - } - - if url.Scheme == "" { - return local.Create(url.Path) - } - - args := []string{url.Host} - if url.User != nil && url.User.Username() != "" { - args = append(args, "-l") - args = append(args, url.User.Username()) - } - args = append(args, "-s") - args = append(args, "sftp") - return sftp.Create(url.Path[1:], "ssh", args...) -} - -func OpenRepo() (*repository.Repository, error) { - if mainOpts.Repo == "" { - return nil, errors.New("Please specify repository location (-r)") - } - - be, err := open(mainOpts.Repo) - if err != nil { - return nil, err - } - - s := repository.New(be) - - if mainOpts.password == "" { - mainOpts.password = readPassword("enter password for repository: ") - } - - err = s.SearchKey(mainOpts.password) - if err != nil { - return nil, fmt.Errorf("unable to open repo: %v", err) - } - - return s, nil -} - func init() { // set GOMAXPROCS to number of CPUs runtime.GOMAXPROCS(runtime.NumCPU()) - - _, err := parser.AddCommand("init", - "create repository", - "The init command creates a new repository", - &CmdInit{}) - if err != nil { - panic(err) - } } func main() { // defer profile.Start(profile.MemProfileRate(100000), profile.ProfilePath(".")).Stop() // defer profile.Start(profile.CPUProfile, profile.ProfilePath(".")).Stop() - mainOpts.Repo = os.Getenv("RESTIC_REPOSITORY") - mainOpts.password = os.Getenv("RESTIC_PASSWORD") + globalOpts.Repo = os.Getenv("RESTIC_REPOSITORY") + globalOpts.password = os.Getenv("RESTIC_PASSWORD") debug.Log("restic", "main %#v", os.Args) From a43733d552f386e9ede5cb2a9643814afab9d6f3 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 21 Jun 2015 13:25:26 +0200 Subject: [PATCH 23/31] Introduce `Verbosef` --- cmd/restic/cmd_backup.go | 8 ++++---- cmd/restic/cmd_init.go | 10 +++++----- cmd/restic/cmd_list.go | 11 ++--------- cmd/restic/cmd_restore.go | 2 +- cmd/restic/global.go | 12 ++++++++---- 5 files changed, 20 insertions(+), 23 deletions(-) diff --git a/cmd/restic/cmd_backup.go b/cmd/restic/cmd_backup.go index 86bde9dbb..088b3afab 100644 --- a/cmd/restic/cmd_backup.go +++ b/cmd/restic/cmd_backup.go @@ -234,7 +234,7 @@ func (cmd CmdBackup) Execute(args []string) error { return fmt.Errorf("invalid id %q: %v", cmd.Parent, err) } - cmd.global.Printf("found parent snapshot %v\n", parentSnapshotID.Str()) + cmd.global.Verbosef("found parent snapshot %v\n", parentSnapshotID.Str()) } // Find last snapshot to set it as parent, if not already set @@ -245,11 +245,11 @@ func (cmd CmdBackup) Execute(args []string) error { } if parentSnapshotID != nil { - cmd.global.Printf("using parent snapshot %v\n", parentSnapshotID) + cmd.global.Verbosef("using parent snapshot %v\n", parentSnapshotID) } } - cmd.global.Printf("scan %v\n", target) + cmd.global.Verbosef("scan %v\n", target) stat, err := restic.Scan(target, cmd.newScanProgress()) @@ -271,7 +271,7 @@ func (cmd CmdBackup) Execute(args []string) error { return err } - cmd.global.Printf("snapshot %s saved\n", id.Str()) + cmd.global.Verbosef("snapshot %s saved\n", id.Str()) return nil } diff --git a/cmd/restic/cmd_init.go b/cmd/restic/cmd_init.go index 1e75ee5f9..e4333b06e 100644 --- a/cmd/restic/cmd_init.go +++ b/cmd/restic/cmd_init.go @@ -32,11 +32,11 @@ func (cmd CmdInit) Execute(args []string) error { cmd.global.Exitf(1, "creating key in backend at %s failed: %v\n", cmd.global.Repo, err) } - cmd.global.Printf("created restic backend %v at %s\n", s.Config.ID[:10], cmd.global.Repo) - cmd.global.Printf("\n") - cmd.global.Printf("Please note that knowledge of your password is required to access\n") - cmd.global.Printf("the repository. Losing your password means that your data is\n") - cmd.global.Printf("irrecoverably lost.\n") + cmd.global.Verbosef("created restic backend %v at %s\n", s.Config.ID[:10], cmd.global.Repo) + cmd.global.Verbosef("\n") + cmd.global.Verbosef("Please note that knowledge of your password is required to access\n") + cmd.global.Verbosef("the repository. Losing your password means that your data is\n") + cmd.global.Verbosef("irrecoverably lost.\n") return nil } diff --git a/cmd/restic/cmd_list.go b/cmd/restic/cmd_list.go index 4d5cf0ab1..bd01e6eda 100644 --- a/cmd/restic/cmd_list.go +++ b/cmd/restic/cmd_list.go @@ -3,14 +3,11 @@ package main import ( "errors" "fmt" - "io" - "os" "github.com/restic/restic/backend" ) type CmdList struct { - w io.Writer global *GlobalOptions } @@ -29,10 +26,6 @@ func (cmd CmdList) Usage() string { } func (cmd CmdList) Execute(args []string) error { - if cmd.w == nil { - cmd.w = os.Stdout - } - if len(args) != 1 { return fmt.Errorf("type not specified, Usage: %s", cmd.Usage()) } @@ -51,7 +44,7 @@ func (cmd CmdList) Execute(args []string) error { } for blob := range s.Index().Each(nil) { - fmt.Fprintln(cmd.w, blob.ID) + cmd.global.Printf("%s\n", blob.ID) } return nil @@ -70,7 +63,7 @@ func (cmd CmdList) Execute(args []string) error { } for id := range s.List(t, nil) { - fmt.Fprintf(cmd.w, "%s\n", id) + cmd.global.Printf("%s\n", id) } return nil diff --git a/cmd/restic/cmd_restore.go b/cmd/restic/cmd_restore.go index 183798932..ef3fb577a 100644 --- a/cmd/restic/cmd_restore.go +++ b/cmd/restic/cmd_restore.go @@ -83,7 +83,7 @@ func (cmd CmdRestore) Execute(args []string) error { } } - cmd.global.Printf("restoring %s to %s\n", res.Snapshot(), target) + cmd.global.Verbosef("restoring %s to %s\n", res.Snapshot(), target) err = res.RestoreTo(target) if err != nil { diff --git a/cmd/restic/global.go b/cmd/restic/global.go index 2f7ffe27e..7c411a85c 100644 --- a/cmd/restic/global.go +++ b/cmd/restic/global.go @@ -30,10 +30,6 @@ var globalOpts = GlobalOptions{stdout: os.Stdout} var parser = flags.NewParser(&globalOpts, flags.Default) func (o GlobalOptions) Printf(format string, args ...interface{}) { - if o.Quiet { - return - } - _, err := fmt.Fprintf(o.stdout, format, args...) if err != nil { fmt.Fprintf(os.Stderr, "unable to write to stdout: %v\n", err) @@ -41,6 +37,14 @@ func (o GlobalOptions) Printf(format string, args ...interface{}) { } } +func (o GlobalOptions) Verbosef(format string, args ...interface{}) { + if o.Quiet { + return + } + + o.Printf(format, args...) +} + func (o GlobalOptions) ShowProgress() bool { if o.Quiet { return false From a99a460b32813e7bba4f60add27ba59e9b5de1ef Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 21 Jun 2015 13:27:56 +0200 Subject: [PATCH 24/31] Fix integration tests --- cmd/restic/cmd_key.go | 12 ++-- cmd/restic/integration_helpers_test.go | 18 ++--- cmd/restic/integration_test.go | 91 +++++++++++++------------- 3 files changed, 62 insertions(+), 59 deletions(-) diff --git a/cmd/restic/cmd_key.go b/cmd/restic/cmd_key.go index a110357aa..201e23c0f 100644 --- a/cmd/restic/cmd_key.go +++ b/cmd/restic/cmd_key.go @@ -39,7 +39,7 @@ func (cmd CmdKey) listKeys(s *repository.Repository) error { for id := range s.List(backend.Key, done) { k, err := repository.LoadKey(s, id.String()) if err != nil { - fmt.Fprintf(os.Stderr, "LoadKey() failed: %v\n", err) + cmd.global.Warnf("LoadKey() failed: %v\n", err) continue } @@ -53,9 +53,7 @@ func (cmd CmdKey) listKeys(s *repository.Repository) error { k.Username, k.Hostname, k.Created.Format(TimeFormat)}) } - tab.Write(os.Stdout) - - return nil + return tab.Write(cmd.global.stdout) } func (cmd CmdKey) getNewPassword() (string, error) { @@ -81,7 +79,7 @@ func (cmd CmdKey) addKey(repo *repository.Repository) error { return fmt.Errorf("creating new key failed: %v\n", err) } - fmt.Printf("saved new key as %s\n", id) + cmd.global.Verbosef("saved new key as %s\n", id) return nil } @@ -96,7 +94,7 @@ func (cmd CmdKey) deleteKey(repo *repository.Repository, name string) error { return err } - fmt.Printf("removed key %v\n", name) + cmd.global.Verbosef("removed key %v\n", name) return nil } @@ -116,7 +114,7 @@ func (cmd CmdKey) changePassword(repo *repository.Repository) error { return err } - fmt.Printf("saved new key as %s\n", id) + cmd.global.Verbosef("saved new key as %s\n", id) return nil } diff --git a/cmd/restic/integration_helpers_test.go b/cmd/restic/integration_helpers_test.go index ee025f955..b934bb0a7 100644 --- a/cmd/restic/integration_helpers_test.go +++ b/cmd/restic/integration_helpers_test.go @@ -176,12 +176,15 @@ type testEnvironment struct { base, cache, repo, testdata string } -func configureRestic(t testing.TB, cache, repo string) { - globalOpts.CacheDir = cache - globalOpts.Repo = repo - globalOpts.Quiet = true +func configureRestic(t testing.TB, cache, repo string) GlobalOptions { + return GlobalOptions{ + CacheDir: cache, + Repo: repo, + Quiet: true, - globalOpts.password = TestPassword + password: TestPassword, + stdout: os.Stdout, + } } func cleanupTempdir(t testing.TB, tempdir string) { @@ -195,7 +198,7 @@ func cleanupTempdir(t testing.TB, tempdir string) { // 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)) { +func withTestEnvironment(t testing.TB, f func(*testEnvironment, GlobalOptions)) { if !RunIntegrationTest { t.Skip("integration tests disabled") } @@ -210,10 +213,9 @@ func withTestEnvironment(t testing.TB, f func(*testEnvironment)) { testdata: filepath.Join(tempdir, "testdata"), } - configureRestic(t, env.cache, env.repo) OK(t, os.MkdirAll(env.testdata, 0700)) - f(&env) + f(&env, configureRestic(t, env.cache, env.repo)) if !TestCleanup { t.Logf("leaving temporary directory %v used for test", tempdir) diff --git a/cmd/restic/integration_test.go b/cmd/restic/integration_test.go index d61432b6a..9d905790c 100644 --- a/cmd/restic/integration_test.go +++ b/cmd/restic/integration_test.go @@ -15,7 +15,7 @@ import ( ) func setupTarTestFixture(t testing.TB, outputDir, tarFile string) { - err := system("sh", "-c", `mkdir "$1" && (cd "$1" && tar xz) < "$2"`, + err := system("sh", "-c", `(cd "$1" && tar xz) < "$2"`, "sh", outputDir, tarFile) OK(t, err) } @@ -45,15 +45,15 @@ func parseIDsFromReader(t testing.TB, rd io.Reader) backend.IDs { return IDs } -func cmdInit(t testing.TB) { - cmd := &CmdInit{} +func cmdInit(t testing.TB, global GlobalOptions) { + cmd := &CmdInit{global: &global} OK(t, cmd.Execute(nil)) - t.Logf("repository initialized at %v", globalOpts.Repo) + t.Logf("repository initialized at %v", global.Repo) } -func cmdBackup(t testing.TB, target []string, parentID backend.ID) { - cmd := &CmdBackup{} +func cmdBackup(t testing.TB, global GlobalOptions, target []string, parentID backend.ID) { + cmd := &CmdBackup{global: &global} cmd.Parent = parentID.String() t.Logf("backing up %v", target) @@ -61,10 +61,10 @@ func cmdBackup(t testing.TB, target []string, parentID backend.ID) { OK(t, cmd.Execute(target)) } -func cmdList(t testing.TB, tpe string) []backend.ID { +func cmdList(t testing.TB, global GlobalOptions, tpe string) []backend.ID { rd, wr := io.Pipe() - - cmd := &CmdList{w: wr} + global.stdout = wr + cmd := &CmdList{global: &global} go func() { OK(t, cmd.Execute([]string{tpe})) @@ -76,23 +76,23 @@ func cmdList(t testing.TB, tpe string) []backend.ID { return IDs } -func cmdRestore(t testing.TB, dir string, snapshotID backend.ID) { - cmd := &CmdRestore{} +func cmdRestore(t testing.TB, global GlobalOptions, dir string, snapshotID backend.ID) { + cmd := &CmdRestore{global: &global} cmd.Execute([]string{snapshotID.String(), dir}) } -func cmdFsck(t testing.TB) { - cmd := &CmdFsck{CheckData: true, Orphaned: true} +func cmdFsck(t testing.TB, global GlobalOptions) { + cmd := &CmdFsck{global: &global, CheckData: true, Orphaned: true} OK(t, cmd.Execute(nil)) } -func cmdKey(t testing.TB, args ...string) { - cmd := &CmdKey{} +func cmdKey(t testing.TB, global GlobalOptions, args ...string) { + cmd := &CmdKey{global: &global} OK(t, cmd.Execute(args)) } func TestBackup(t *testing.T) { - withTestEnvironment(t, func(env *testEnvironment) { + withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) { datafile := filepath.Join("testdata", "backup-data.tar.gz") fd, err := os.Open(datafile) if os.IsNotExist(err) { @@ -102,25 +102,24 @@ func TestBackup(t *testing.T) { OK(t, err) OK(t, fd.Close()) - cmdInit(t) + cmdInit(t, global) - datadir := filepath.Join(env.base, "testdata") - setupTarTestFixture(t, datadir, datafile) + setupTarTestFixture(t, env.testdata, datafile) // first backup - cmdBackup(t, []string{datadir}, nil) - snapshotIDs := cmdList(t, "snapshots") + cmdBackup(t, global, []string{env.testdata}, nil) + snapshotIDs := cmdList(t, global, "snapshots") Assert(t, len(snapshotIDs) == 1, - "more than one snapshot ID in repo") + "expected one snapshot, got %v", snapshotIDs) - cmdFsck(t) + cmdFsck(t, global) stat1 := dirStats(env.repo) // second backup, implicit incremental - cmdBackup(t, []string{datadir}, nil) - snapshotIDs = cmdList(t, "snapshots") + cmdBackup(t, global, []string{env.testdata}, nil) + snapshotIDs = cmdList(t, global, "snapshots") Assert(t, len(snapshotIDs) == 2, - "more than one snapshot ID in repo") + "expected two snapshots, got %v", snapshotIDs) stat2 := dirStats(env.repo) if stat2.size > stat1.size+stat1.size/10 { @@ -128,12 +127,12 @@ func TestBackup(t *testing.T) { } t.Logf("repository grown by %d bytes", stat2.size-stat1.size) - cmdFsck(t) + cmdFsck(t, global) // third backup, explicit incremental - cmdBackup(t, []string{datadir}, snapshotIDs[0]) - snapshotIDs = cmdList(t, "snapshots") + cmdBackup(t, global, []string{env.testdata}, snapshotIDs[0]) + snapshotIDs = cmdList(t, global, "snapshots") Assert(t, len(snapshotIDs) == 3, - "more than two snapshot IDs in repo") + "expected three snapshots, got %v", snapshotIDs) stat3 := dirStats(env.repo) if stat3.size > stat1.size+stat1.size/10 { @@ -145,12 +144,12 @@ func TestBackup(t *testing.T) { 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")), + cmdRestore(t, global, restoredir, snapshotIDs[0]) + Assert(t, directoriesEqualContents(env.testdata, filepath.Join(restoredir, "testdata")), "directories are not equal") } - cmdFsck(t) + cmdFsck(t, global) }) } @@ -182,8 +181,12 @@ func appendRandomData(filename string, bytes uint) error { return f.Close() } +func TestInit(t *testing.T) { + +} + func TestIncrementalBackup(t *testing.T) { - withTestEnvironment(t, func(env *testEnvironment) { + withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) { datafile := filepath.Join("testdata", "backup-data.tar.gz") fd, err := os.Open(datafile) if os.IsNotExist(err) { @@ -193,21 +196,21 @@ func TestIncrementalBackup(t *testing.T) { OK(t, err) OK(t, fd.Close()) - cmdInit(t) + cmdInit(t, global) datadir := filepath.Join(env.base, "testdata") testfile := filepath.Join(datadir, "testfile") OK(t, appendRandomData(testfile, incrementalFirstWrite)) - cmdBackup(t, []string{datadir}, nil) - cmdFsck(t) + cmdBackup(t, global, []string{datadir}, nil) + cmdFsck(t, global) stat1 := dirStats(env.repo) OK(t, appendRandomData(testfile, incrementalSecondWrite)) - cmdBackup(t, []string{datadir}, nil) - cmdFsck(t) + cmdBackup(t, global, []string{datadir}, nil) + cmdFsck(t, global) stat2 := dirStats(env.repo) if stat2.size-stat1.size > incrementalFirstWrite { t.Errorf("repository size has grown by more than %d bytes", incrementalFirstWrite) @@ -216,8 +219,8 @@ func TestIncrementalBackup(t *testing.T) { OK(t, appendRandomData(testfile, incrementalThirdWrite)) - cmdBackup(t, []string{datadir}, nil) - cmdFsck(t) + cmdBackup(t, global, []string{datadir}, nil) + cmdFsck(t, global) stat3 := dirStats(env.repo) if stat3.size-stat2.size > incrementalFirstWrite { t.Errorf("repository size has grown by more than %d bytes", incrementalFirstWrite) @@ -227,7 +230,7 @@ func TestIncrementalBackup(t *testing.T) { } func TestKeyAddRemove(t *testing.T) { - withTestEnvironment(t, func(env *testEnvironment) { + withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) { datafile := filepath.Join("testdata", "backup-data.tar.gz") fd, err := os.Open(datafile) if os.IsNotExist(err) { @@ -237,7 +240,7 @@ func TestKeyAddRemove(t *testing.T) { OK(t, err) OK(t, fd.Close()) - cmdInit(t) - cmdKey(t, "list") + cmdInit(t, global) + cmdKey(t, global, "list") }) } From cfaf8ab8a66c765555626081db8880778b6f66d5 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 21 Jun 2015 15:01:52 +0200 Subject: [PATCH 25/31] Add integration test for key handling --- cmd/restic/cmd_key.go | 32 +++------- cmd/restic/global.go | 4 ++ cmd/restic/integration_test.go | 103 +++++++++++++++++++++++-------- testsuite/test-key-add-remove.sh | 42 ------------- 4 files changed, 90 insertions(+), 91 deletions(-) delete mode 100755 testsuite/test-key-add-remove.sh diff --git a/cmd/restic/cmd_key.go b/cmd/restic/cmd_key.go index 201e23c0f..ad1a358fd 100644 --- a/cmd/restic/cmd_key.go +++ b/cmd/restic/cmd_key.go @@ -3,14 +3,14 @@ package main import ( "errors" "fmt" - "os" "github.com/restic/restic/backend" "github.com/restic/restic/repository" ) type CmdKey struct { - global *GlobalOptions + global *GlobalOptions + newPassword string } func init() { @@ -56,25 +56,18 @@ func (cmd CmdKey) listKeys(s *repository.Repository) error { return tab.Write(cmd.global.stdout) } -func (cmd CmdKey) getNewPassword() (string, error) { - newPassword := os.Getenv("RESTIC_NEWPASSWORD") - - if newPassword == "" { - newPassword = cmd.global.ReadPasswordTwice( - "enter password for new key: ", - "enter password again: ") +func (cmd CmdKey) getNewPassword() string { + if cmd.newPassword != "" { + return cmd.newPassword } - return newPassword, nil + return cmd.global.ReadPasswordTwice( + "enter password for new key: ", + "enter password again: ") } func (cmd CmdKey) addKey(repo *repository.Repository) error { - newPassword, err := cmd.getNewPassword() - if err != nil { - return err - } - - id, err := repository.AddKey(repo, newPassword, repo.Key()) + id, err := repository.AddKey(repo, cmd.getNewPassword(), repo.Key()) if err != nil { return fmt.Errorf("creating new key failed: %v\n", err) } @@ -99,12 +92,7 @@ func (cmd CmdKey) deleteKey(repo *repository.Repository, name string) error { } func (cmd CmdKey) changePassword(repo *repository.Repository) error { - newPassword, err := cmd.getNewPassword() - if err != nil { - return err - } - - id, err := repository.AddKey(repo, newPassword, repo.Key()) + id, err := repository.AddKey(repo, cmd.getNewPassword(), repo.Key()) if err != nil { return fmt.Errorf("creating new key failed: %v\n", err) } diff --git a/cmd/restic/global.go b/cmd/restic/global.go index 7c411a85c..79c9eaf98 100644 --- a/cmd/restic/global.go +++ b/cmd/restic/global.go @@ -82,6 +82,10 @@ func (o GlobalOptions) ReadPassword(prompt string) string { } fmt.Fprintln(os.Stderr) + if len(pw) == 0 { + o.Exitf(1, "an empty password is not a password") + } + return string(pw) } diff --git a/cmd/restic/integration_test.go b/cmd/restic/integration_test.go index 9d905790c..22e00ded5 100644 --- a/cmd/restic/integration_test.go +++ b/cmd/restic/integration_test.go @@ -2,12 +2,14 @@ package main import ( "bufio" + "bytes" "crypto/rand" "fmt" "io" "os" "os/exec" "path/filepath" + "regexp" "testing" "github.com/restic/restic/backend" @@ -62,16 +64,12 @@ func cmdBackup(t testing.TB, global GlobalOptions, target []string, parentID bac } func cmdList(t testing.TB, global GlobalOptions, tpe string) []backend.ID { - rd, wr := io.Pipe() - global.stdout = wr + var buf bytes.Buffer + global.stdout = &buf cmd := &CmdList{global: &global} - go func() { - OK(t, cmd.Execute([]string{tpe})) - OK(t, wr.Close()) - }() - - IDs := parseIDsFromReader(t, rd) + OK(t, cmd.Execute([]string{tpe})) + IDs := parseIDsFromReader(t, &buf) return IDs } @@ -86,11 +84,6 @@ func cmdFsck(t testing.TB, global GlobalOptions) { OK(t, cmd.Execute(nil)) } -func cmdKey(t testing.TB, global GlobalOptions, args ...string) { - cmd := &CmdKey{global: &global} - OK(t, cmd.Execute(args)) -} - func TestBackup(t *testing.T) { withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) { datafile := filepath.Join("testdata", "backup-data.tar.gz") @@ -181,10 +174,6 @@ func appendRandomData(filename string, bytes uint) error { return f.Close() } -func TestInit(t *testing.T) { - -} - func TestIncrementalBackup(t *testing.T) { withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) { datafile := filepath.Join("testdata", "backup-data.tar.gz") @@ -229,18 +218,78 @@ func TestIncrementalBackup(t *testing.T) { }) } -func TestKeyAddRemove(t *testing.T) { - withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) { - 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", datafile) - return - } - OK(t, err) - OK(t, fd.Close()) +func cmdKey(t testing.TB, global GlobalOptions, args ...string) string { + var buf bytes.Buffer + global.stdout = &buf + cmd := &CmdKey{global: &global} + OK(t, cmd.Execute(args)) + + return buf.String() +} + +func cmdKeyListOtherIDs(t testing.TB, global GlobalOptions) []string { + var buf bytes.Buffer + + global.stdout = &buf + cmd := &CmdKey{global: &global} + OK(t, cmd.Execute([]string{"list"})) + + scanner := bufio.NewScanner(&buf) + exp := regexp.MustCompile(`^ ([a-f0-9]+) `) + + IDs := []string{} + for scanner.Scan() { + if id := exp.FindStringSubmatch(scanner.Text()); id != nil { + IDs = append(IDs, id[1]) + } + } + + return IDs +} + +func cmdKeyAddNewKey(t testing.TB, global GlobalOptions, newPassword string) { + cmd := &CmdKey{global: &global, newPassword: newPassword} + OK(t, cmd.Execute([]string{"add"})) +} + +func cmdKeyPasswd(t testing.TB, global GlobalOptions, newPassword string) { + cmd := &CmdKey{global: &global, newPassword: newPassword} + OK(t, cmd.Execute([]string{"passwd"})) +} + +func cmdKeyRemove(t testing.TB, global GlobalOptions, IDs []string) { + cmd := &CmdKey{global: &global} + t.Logf("remove %d keys: %q\n", len(IDs), IDs) + for _, id := range IDs { + OK(t, cmd.Execute([]string{"rm", id})) + } +} + +func TestKeyAddRemove(t *testing.T) { + passwordList := []string{ + "OnnyiasyatvodsEvVodyawit", + "raicneirvOjEfEigonOmLasOd", + } + + withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) { cmdInit(t, global) + + cmdKeyPasswd(t, global, "geheim2") + global.password = "geheim2" + t.Logf("changed password to %q", global.password) + + for _, newPassword := range passwordList { + cmdKeyAddNewKey(t, global, newPassword) + t.Logf("added new password %q", newPassword) + global.password = newPassword + cmdKeyRemove(t, global, cmdKeyListOtherIDs(t, global)) + } + + global.password = passwordList[len(passwordList)-1] + t.Logf("testing access with last password %q\n", global.password) cmdKey(t, global, "list") + + cmdFsck(t, global) }) } diff --git a/testsuite/test-key-add-remove.sh b/testsuite/test-key-add-remove.sh deleted file mode 100755 index b012ea552..000000000 --- a/testsuite/test-key-add-remove.sh +++ /dev/null @@ -1,42 +0,0 @@ -set -e - -dump_repo() { - if [ "$FAILED" == "1" ]; then - tar cvz "$RESTIC_REPOSITORY" | base64 >&2 - fi -} - -FAILED=1 - -trap dump_repo 0 - -prepare -unset RESTIC_PASSWORD -RESTIC_PASSWORD=foo run restic init -RESTIC_PASSWORD=foo run restic key list - -RESTIC_PASSWORD=foo RESTIC_NEWPASSWORD=foobar run restic key passwd -RESTIC_PASSWORD=foobar run restic key list -RESTIC_PASSWORD=foobar RESTIC_NEWPASSWORD=foo run restic key passwd - -OLD_PWD=foo -for i in {1..3}; do - NEW_PWD=bar$i - RESTIC_PASSWORD=$OLD_PWD RESTIC_NEWPASSWORD=$NEW_PWD run restic key add - RESTIC_PASSWORD=$OLD_PWD run restic key list - RESTIC_PASSWORD=$NEW_PWD run restic key list - - export RESTIC_PASSWORD=$OLD_PWD - ID=$(restic key list | grep '^\*'|cut -d ' ' -f 1| sed 's/^.//') - unset RESTIC_PASSWORD - RESTIC_PASSWORD=$NEW_PWD run restic key rm $ID - RESTIC_PASSWORD=$NEW_PWD run restic key list - - OLD_PWD=bar$i -done - -RESTIC_PASSWORD=$OLD_PWD run restic fsck -o --check-data - -cleanup - -FAILED=0 From 675f341b6d81b1a9689f65c9f0a4aff77741c80c Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 21 Jun 2015 15:20:54 +0200 Subject: [PATCH 26/31] Output warnings/errors to configurable writer --- cmd/restic/cmd_backup.go | 2 +- cmd/restic/cmd_restore.go | 6 ++---- cmd/restic/global.go | 5 +++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/cmd/restic/cmd_backup.go b/cmd/restic/cmd_backup.go index 088b3afab..2ab9dc5e0 100644 --- a/cmd/restic/cmd_backup.go +++ b/cmd/restic/cmd_backup.go @@ -262,7 +262,7 @@ func (cmd CmdBackup) Execute(args []string) error { arch.Error = func(dir string, fi os.FileInfo, err error) error { // TODO: make ignoring errors configurable - fmt.Fprintf(os.Stderr, "\x1b[2K\rerror for %s: %v\n", dir, err) + cmd.global.Warnf("\x1b[2K\rerror for %s: %v\n", dir, err) return nil } diff --git a/cmd/restic/cmd_restore.go b/cmd/restic/cmd_restore.go index ef3fb577a..1f3697d4d 100644 --- a/cmd/restic/cmd_restore.go +++ b/cmd/restic/cmd_restore.go @@ -2,7 +2,6 @@ package main import ( "fmt" - "os" "path/filepath" "github.com/restic/restic" @@ -51,12 +50,11 @@ func (cmd CmdRestore) Execute(args []string) error { // create restorer res, err := restic.NewRestorer(s, id) if err != nil { - fmt.Fprintf(os.Stderr, "creating restorer failed: %v\n", err) - os.Exit(2) + cmd.global.Exitf(2, "creating restorer failed: %v\n", err) } res.Error = func(dir string, node *restic.Node, err error) error { - fmt.Fprintf(os.Stderr, "error for %s: %+v\n", dir, err) + cmd.global.Warnf("error for %s: %+v\n", dir, err) // if node.Type == "dir" { // if e, ok := err.(*os.PathError); ok { diff --git a/cmd/restic/global.go b/cmd/restic/global.go index 79c9eaf98..3f20c9248 100644 --- a/cmd/restic/global.go +++ b/cmd/restic/global.go @@ -24,9 +24,10 @@ type GlobalOptions struct { password string stdout io.Writer + stderr io.Writer } -var globalOpts = GlobalOptions{stdout: os.Stdout} +var globalOpts = GlobalOptions{stdout: os.Stdout, stderr: os.Stderr} var parser = flags.NewParser(&globalOpts, flags.Default) func (o GlobalOptions) Printf(format string, args ...interface{}) { @@ -58,7 +59,7 @@ func (o GlobalOptions) ShowProgress() bool { } func (o GlobalOptions) Warnf(format string, args ...interface{}) { - _, err := fmt.Fprintf(os.Stderr, format, args...) + _, err := fmt.Fprintf(o.stderr, format, args...) if err != nil { fmt.Fprintf(os.Stderr, "unable to write to stderr: %v\n", err) os.Exit(100) From 43d4558a90c5f14b88b25bdc16dc01f985a82fe6 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 21 Jun 2015 15:32:26 +0200 Subject: [PATCH 27/31] Add test for backing up non-existing directories --- cmd/restic/integration_test.go | 29 ++++++++++++++++++++++ testsuite/test-backup-non-existing-file.sh | 10 -------- 2 files changed, 29 insertions(+), 10 deletions(-) delete mode 100755 testsuite/test-backup-non-existing-file.sh diff --git a/cmd/restic/integration_test.go b/cmd/restic/integration_test.go index 22e00ded5..62d972b97 100644 --- a/cmd/restic/integration_test.go +++ b/cmd/restic/integration_test.go @@ -6,6 +6,7 @@ import ( "crypto/rand" "fmt" "io" + "io/ioutil" "os" "os/exec" "path/filepath" @@ -146,6 +147,34 @@ func TestBackup(t *testing.T) { }) } +func TestBackupNonExistingFile(t *testing.T) { + withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) { + 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", datafile) + return + } + OK(t, err) + OK(t, fd.Close()) + + setupTarTestFixture(t, env.testdata, datafile) + + cmdInit(t, global) + + global.stderr = ioutil.Discard + + p := filepath.Join(env.testdata, "0", "0") + dirs := []string{ + filepath.Join(p, "0"), + filepath.Join(p, "1"), + filepath.Join(p, "nonexisting"), + filepath.Join(p, "5"), + } + cmdBackup(t, global, dirs, nil) + }) +} + const ( incrementalFirstWrite = 20 * 1042 * 1024 incrementalSecondWrite = 12 * 1042 * 1024 diff --git a/testsuite/test-backup-non-existing-file.sh b/testsuite/test-backup-non-existing-file.sh deleted file mode 100755 index d06fe5c8e..000000000 --- a/testsuite/test-backup-non-existing-file.sh +++ /dev/null @@ -1,10 +0,0 @@ -set -em - -# setup restic -prepare -run restic init - -# start backup with non existing dir -run timeout 10s restic.debug backup "${BASE}/fake-data/0/0/"{0,1,foobar,5} && debug "done" || false - -cleanup From 5ae04b6834cb3a1e1370b5302870037b0107eaf5 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 21 Jun 2015 17:12:38 +0200 Subject: [PATCH 28/31] Add last integration tests, remove testsuite --- archiver.go | 2 +- cmd/restic/integration_test.go | 84 ++++++++++++++++-- debug/debug.go | 56 ------------ debug/debug_release.go | 4 - debug/hooks.go | 28 ++++++ debug/hooks_release.go | 9 ++ pipe/pipe.go | 27 +----- testsuite.sh | 12 --- testsuite/fake-data.tar.gz | Bin 177734 -> 0 bytes testsuite/run.sh | 117 ------------------------- testsuite/test-backup-missing-file1.sh | 16 ---- testsuite/test-backup-missing-file2.sh | 16 ---- 12 files changed, 115 insertions(+), 256 deletions(-) create mode 100644 debug/hooks.go create mode 100644 debug/hooks_release.go delete mode 100755 testsuite.sh delete mode 100644 testsuite/fake-data.tar.gz delete mode 100755 testsuite/run.sh delete mode 100755 testsuite/test-backup-missing-file1.sh delete mode 100755 testsuite/test-backup-missing-file2.sh diff --git a/archiver.go b/archiver.go index 00d299f4d..bd48c1147 100644 --- a/archiver.go +++ b/archiver.go @@ -534,7 +534,7 @@ func (j archiveJob) Copy() pipe.Job { func (arch *Archiver) Snapshot(p *Progress, paths []string, parentID backend.ID) (*Snapshot, backend.ID, error) { debug.Log("Archiver.Snapshot", "start for %v", paths) - debug.Break("Archiver.Snapshot") + debug.RunHook("Archiver.Snapshot", nil) sort.Strings(paths) // signal the whole pipeline to stop diff --git a/cmd/restic/integration_test.go b/cmd/restic/integration_test.go index 62d972b97..746ac4e56 100644 --- a/cmd/restic/integration_test.go +++ b/cmd/restic/integration_test.go @@ -14,6 +14,7 @@ import ( "testing" "github.com/restic/restic/backend" + "github.com/restic/restic/debug" . "github.com/restic/restic/test" ) @@ -175,6 +176,80 @@ func TestBackupNonExistingFile(t *testing.T) { }) } +func TestBackupMissingFile1(t *testing.T) { + withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) { + 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", datafile) + return + } + OK(t, err) + OK(t, fd.Close()) + + setupTarTestFixture(t, env.testdata, datafile) + + cmdInit(t, global) + + ranHook := false + debug.Hook("pipe.walk1", func(context interface{}) { + pathname := context.(string) + + if pathname != filepath.Join("testdata", "0", "0", "9") { + return + } + + t.Logf("in hook, removing test file testdata/0/0/9/37") + ranHook = true + + OK(t, os.Remove(filepath.Join(env.testdata, "0", "0", "9", "37"))) + }) + + cmdBackup(t, global, []string{env.testdata}, nil) + cmdFsck(t, global) + + Assert(t, ranHook, "hook did not run") + debug.RemoveHook("pipe.walk1") + }) +} + +func TestBackupMissingFile2(t *testing.T) { + withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) { + 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", datafile) + return + } + OK(t, err) + OK(t, fd.Close()) + + setupTarTestFixture(t, env.testdata, datafile) + + cmdInit(t, global) + + ranHook := false + debug.Hook("pipe.walk2", func(context interface{}) { + pathname := context.(string) + + if pathname != filepath.Join("testdata", "0", "0", "9", "37") { + return + } + + t.Logf("in hook, removing test file testdata/0/0/9/37") + ranHook = true + + OK(t, os.Remove(filepath.Join(env.testdata, "0", "0", "9", "37"))) + }) + + cmdBackup(t, global, []string{env.testdata}, nil) + cmdFsck(t, global) + + Assert(t, ranHook, "hook did not run") + debug.RemoveHook("pipe.walk2") + }) +} + const ( incrementalFirstWrite = 20 * 1042 * 1024 incrementalSecondWrite = 12 * 1042 * 1024 @@ -205,15 +280,6 @@ func appendRandomData(filename string, bytes uint) error { func TestIncrementalBackup(t *testing.T) { withTestEnvironment(t, func(env *testEnvironment, global GlobalOptions) { - 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", datafile) - return - } - OK(t, err) - OK(t, fd.Close()) - cmdInit(t, global) datadir := filepath.Join(env.base, "testdata") diff --git a/debug/debug.go b/debug/debug.go index a77cb3836..4e9094226 100644 --- a/debug/debug.go +++ b/debug/debug.go @@ -18,7 +18,6 @@ import ( var opts struct { logger *log.Logger tags map[string]bool - breaks map[string]bool m sync.Mutex } @@ -29,7 +28,6 @@ var _ = initDebug() func initDebug() bool { initDebugLogger() initDebugTags() - initDebugBreaks() fmt.Fprintf(os.Stderr, "debug enabled\n") @@ -105,25 +103,6 @@ func initDebugTags() { fmt.Fprintf(os.Stderr, "debug log enabled for: %v\n", tags) } -func initDebugBreaks() { - opts.breaks = make(map[string]bool) - - env := os.Getenv("DEBUG_BREAK") - if len(env) == 0 { - return - } - - breaks := []string{} - - for _, tag := range strings.Split(env, ",") { - t := strings.TrimSpace(tag) - opts.breaks[t] = true - breaks = append(breaks, t) - } - - fmt.Fprintf(os.Stderr, "debug breaks enabled for: %v\n", breaks) -} - // taken from https://github.com/VividCortex/trace func goroutineNum() int { b := make([]byte, 20) @@ -194,38 +173,3 @@ func Log(tag string, f string, args ...interface{}) { dbgprint() } } - -// Break stops the program if the debug tag is active and the string in tag is -// contained in the DEBUG_BREAK environment variable. -func Break(tag string) { - // check if breaking is enabled - if v, ok := opts.breaks[tag]; !ok || !v { - return - } - - _, file, line, _ := runtime.Caller(1) - Log("break", "stopping process %d at %s (%v:%v)\n", os.Getpid(), tag, file, line) - p, err := os.FindProcess(os.Getpid()) - if err != nil { - panic(err) - } - - err = p.Signal(syscall.SIGSTOP) - if err != nil { - panic(err) - } -} - -// BreakIf stops the program if the debug tag is active and the string in tag -// is contained in the DEBUG_BREAK environment variable and the return value of -// fn is true. -func BreakIf(tag string, fn func() bool) { - // check if breaking is enabled - if v, ok := opts.breaks[tag]; !ok || !v { - return - } - - if fn() { - Break(tag) - } -} diff --git a/debug/debug_release.go b/debug/debug_release.go index 3aa6aef49..9062d8ce8 100644 --- a/debug/debug_release.go +++ b/debug/debug_release.go @@ -3,7 +3,3 @@ package debug func Log(tag string, fmt string, args ...interface{}) {} - -func Break(string) {} - -func BreakIf(string, func() bool) {} diff --git a/debug/hooks.go b/debug/hooks.go new file mode 100644 index 000000000..19eee7e3c --- /dev/null +++ b/debug/hooks.go @@ -0,0 +1,28 @@ +// +build !release + +package debug + +var ( + hooks map[string]func(interface{}) +) + +func init() { + hooks = make(map[string]func(interface{})) +} + +func Hook(name string, f func(interface{})) { + hooks[name] = f +} + +func RunHook(name string, context interface{}) { + f, ok := hooks[name] + if !ok { + return + } + + f(context) +} + +func RemoveHook(name string) { + delete(hooks, name) +} diff --git a/debug/hooks_release.go b/debug/hooks_release.go new file mode 100644 index 000000000..376df26ac --- /dev/null +++ b/debug/hooks_release.go @@ -0,0 +1,9 @@ +// +build release + +package debug + +func Hook(name string, f func(interface{})) {} + +func RunHook(name string, context interface{}) {} + +func RemoveHook(name string) {} diff --git a/pipe/pipe.go b/pipe/pipe.go index 522a6a00b..a419f082d 100644 --- a/pipe/pipe.go +++ b/pipe/pipe.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "os" - "path" "path/filepath" "sort" @@ -108,17 +107,7 @@ func walk(basedir, dir string, done chan struct{}, jobs chan<- Job, res chan<- R // Insert breakpoint to allow testing behaviour with vanishing files // between Readdir() and lstat() - debug.BreakIf("pipe.walk1", func() bool { - match, err := path.Match(os.Getenv("DEBUG_BREAK_PIPE"), relpath) - if err != nil { - panic(err) - } - if match { - debug.Log("break", "break pattern matches for %v\n", relpath) - } - - return match - }) + debug.RunHook("pipe.walk1", relpath) entries := make([]<-chan Result, 0, len(names)) @@ -140,19 +129,7 @@ func walk(basedir, dir string, done chan struct{}, jobs chan<- Job, res chan<- R // Insert breakpoint to allow testing behaviour with vanishing files // between walk and open - debug.BreakIf("pipe.walk2", func() bool { - p := filepath.Join(relpath, name) - - match, err := path.Match(os.Getenv("DEBUG_BREAK_PIPE"), p) - if err != nil { - panic(err) - } - if match { - debug.Log("break", "break pattern matches for %v\n", p) - } - - return match - }) + debug.RunHook("pipe.walk2", filepath.Join(relpath, name)) if isDir(fi) { err = walk(basedir, subpath, done, jobs, ch) diff --git a/testsuite.sh b/testsuite.sh deleted file mode 100755 index 0e3b9511c..000000000 --- a/testsuite.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/bash - -# tempdir for binaries -export BASEDIR="$(mktemp --tmpdir --directory restic-testsuite-XXXXXX)" -export DEBUG_LOG="${BASEDIR}/restic.log" - -export TZ=UTC - -echo "restic testsuite basedir ${BASEDIR}" - -# run tests -testsuite/run.sh "$@" diff --git a/testsuite/fake-data.tar.gz b/testsuite/fake-data.tar.gz deleted file mode 100644 index 337c18fd9d54d427fcda76a39fae9c73a881d7fb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 177734 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

thKh&Fwhb!2fm6ttc-Bd{>9I~ir(TMd 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Ⓢ@|+yXauB-2e0TL1lzByjK1 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 /dev/null - fi -} - -export -f prepare cleanup msg debug pass err fail run - -if [ -z "$BASEDIR" ]; then - echo "BASEDIR not set" >&2 - exit 2 -fi - -which restic > /dev/null || fail "restic binary not found!" -which restic.debug > /dev/null || fail "restic.debug binary not found!" -which dirdiff > /dev/null || fail "dirdiff binary not found!" - -debug "restic path: $(which restic)" -debug "restic.debug path: $(which restic.debug)" -debug "dirdiff path: $(which dirdiff)" -debug "path: $PATH" - -debug "restic versions:" -run restic version -run restic.debug version - -if [ "$#" -gt 0 ]; then - testfiles="$1" -else - testfiles=(${dir}/test-*.sh) -fi - -echo "testfiles: ${testfiles[@]}" - -failed="" -for testfile in "${testfiles[@]}"; do - msg "================================================================================" - msg "run test $testfile" - msg "" - - current=$(basename "${testfile}" .sh) - - if [ "$DEBUG" = "1" ]; then - OPTS="-v" - fi - - if bash $OPTS "${testfile}"; then - pass "${current} pass" - else - err "${current} failed!" - failed+=" ${current}" - fi -done - -if [ -n "$failed" ]; then - err "failed tests: ${failed}" - msg "restic versions:" - run restic version - run restic.debug version - exit 1 -fi diff --git a/testsuite/test-backup-missing-file1.sh b/testsuite/test-backup-missing-file1.sh deleted file mode 100755 index 87a2dc26c..000000000 --- a/testsuite/test-backup-missing-file1.sh +++ /dev/null @@ -1,16 +0,0 @@ -set -em - -# setup restic -prepare -run restic init - -# start backup, break between readdir and lstat -DEBUG_BREAK=pipe.walk1 DEBUG_BREAK_PIPE="fake-data/0/0/9" run restic.debug backup "${BASE}/fake-data" && debug "done" - -# remove file -rm -f "${BASE}/fake-data/0/0/9/37" - -# resume backup -fg - -cleanup diff --git a/testsuite/test-backup-missing-file2.sh b/testsuite/test-backup-missing-file2.sh deleted file mode 100755 index c93d84b06..000000000 --- a/testsuite/test-backup-missing-file2.sh +++ /dev/null @@ -1,16 +0,0 @@ -set -em - -# setup restic -prepare -run restic init - -# start backup, break between walk and save -DEBUG_BREAK=pipe.walk2 DEBUG_BREAK_PIPE="fake-data/0/0/9/37" run restic.debug backup "${BASE}/fake-data" && debug "done" - -# remove file -rm -f "${BASE}/fake-data/0/0/9/37" - -# resume backup -fg - -cleanup From ae01af045d4ce14127bf7615c0fc5cf986de49db Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 21 Jun 2015 17:15:15 +0200 Subject: [PATCH 29/31] Add release tag to travis and Makefile --- .travis.yml | 2 +- Makefile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index dd424db47..d28825e4b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,7 +29,7 @@ install: - gox -build-toolchain -os "$GOX_OS" script: - - gox -verbose -os "${GOX_OS}" ./cmd/restic + - gox -verbose -os "${GOX_OS}" -tags "release" ./cmd/restic - go run run_tests.go all.cov - GOARCH=386 go test ./... - goveralls -coverprofile=all.cov -service=travis-ci -repotoken "$COVERALLS_TOKEN" || true diff --git a/Makefile b/Makefile index 2821e4a5d..68419d5be 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,7 @@ all: restic %: cmd/% .gopath $(SOURCE) cd $(BASEPATH) && \ - go build -a -ldflags "-s" -o $@ ./$< + go build -a -tags release -ldflags "-s" -o $@ ./$< %.debug: cmd/% .gopath $(SOURCE) cd $(BASEPATH) && \ From 79f2bb200f9aae6e84e065d7915107d196299402 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 21 Jun 2015 17:32:19 +0200 Subject: [PATCH 30/31] Do not run integration tests on i386 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d28825e4b..67d9a9ef5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,7 +31,7 @@ install: script: - gox -verbose -os "${GOX_OS}" -tags "release" ./cmd/restic - go run run_tests.go all.cov - - GOARCH=386 go test ./... + - GOARCH=386 RESTIC_TEST_INTEGRATION=0 go test ./... - goveralls -coverprofile=all.cov -service=travis-ci -repotoken "$COVERALLS_TOKEN" || true - gofmt -l *.go */*.go */*/*.go - test -z "$(gofmt -l *.go */*.go */*/*.go)" From 0005191d7100697550bc57706f1bb911b8d650ff Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 21 Jun 2015 17:32:40 +0200 Subject: [PATCH 31/31] Remove `dirdiff` and `gentestdata` --- cmd/dirdiff/main.go | 142 ---------------------------------------- cmd/gentestdata/main.go | 69 ------------------- 2 files changed, 211 deletions(-) delete mode 100644 cmd/dirdiff/main.go delete mode 100644 cmd/gentestdata/main.go diff --git a/cmd/dirdiff/main.go b/cmd/dirdiff/main.go deleted file mode 100644 index 23182f5bf..000000000 --- a/cmd/dirdiff/main.go +++ /dev/null @@ -1,142 +0,0 @@ -package main - -import ( - "fmt" - "os" - "path/filepath" - "syscall" -) - -type entry struct { - path string - fi os.FileInfo -} - -func walk(dir string) <-chan *entry { - ch := make(chan *entry, 100) - - go func() { - err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { - if err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - return nil - } - - name, err := filepath.Rel(dir, path) - if err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - return nil - } - - ch <- &entry{ - path: name, - fi: info, - } - - return nil - }) - - if err != nil { - fmt.Fprintf(os.Stderr, "Walk() error: %v\n", err) - } - - close(ch) - }() - - // first element is root - _ = <-ch - - return ch -} - -func (e *entry) equals(other *entry) bool { - if e.path != other.path { - fmt.Printf("path does not match\n") - return false - } - - if e.fi.Mode() != other.fi.Mode() { - fmt.Printf("mode does not match\n") - return false - } - - if e.fi.ModTime() != other.fi.ModTime() { - fmt.Printf("%s: ModTime does not match\n", e.path) - // TODO: Fix ModTime for symlinks, return false - // see http://grokbase.com/t/gg/golang-nuts/154wnph4y8/go-nuts-no-way-to-utimes-a-symlink - return true - } - - stat, _ := e.fi.Sys().(*syscall.Stat_t) - stat2, _ := other.fi.Sys().(*syscall.Stat_t) - - if stat.Uid != stat2.Uid || stat2.Gid != stat2.Gid { - return false - } - - return true -} - -func main() { - if len(os.Args) != 3 { - fmt.Fprintf(os.Stderr, "USAGE: %s DIR1 DIR2\n", os.Args[0]) - os.Exit(1) - } - - ch1 := walk(os.Args[1]) - ch2 := walk(os.Args[2]) - - changes := false - - var a, b *entry - for { - var ok bool - - if ch1 != nil && a == nil { - a, ok = <-ch1 - if !ok { - ch1 = nil - } - } - - if ch2 != nil && b == nil { - b, ok = <-ch2 - if !ok { - ch2 = nil - } - } - - if ch1 == nil && ch2 == nil { - break - } - - if ch1 == nil { - fmt.Printf("+%v\n", b.path) - changes = true - } else if ch2 == nil { - fmt.Printf("-%v\n", a.path) - changes = true - } else if !a.equals(b) { - if a.path < b.path { - fmt.Printf("-%v\n", a.path) - changes = true - a = nil - continue - } else if a.path > b.path { - fmt.Printf("+%v\n", b.path) - changes = true - b = nil - continue - } else { - fmt.Printf("%%%v\n", a.path) - changes = true - } - } - - a, b = nil, nil - } - - if changes { - os.Exit(1) - } -} diff --git a/cmd/gentestdata/main.go b/cmd/gentestdata/main.go deleted file mode 100644 index c4b3604b3..000000000 --- a/cmd/gentestdata/main.go +++ /dev/null @@ -1,69 +0,0 @@ -package main - -import ( - "fmt" - "io" - "math/rand" - "os" - "path/filepath" -) - -const ( - MaxFiles = 23 - MaxDepth = 3 -) - -var urnd *os.File - -func init() { - f, err := os.Open("/dev/urandom") - if err != nil { - panic(err) - } - - urnd = f -} - -func rndRd(bytes int) io.Reader { - return io.LimitReader(urnd, int64(bytes)) -} - -func createDir(target string, depth int) { - fmt.Printf("createDir %s, depth %d\n", target, depth) - err := os.Mkdir(target, 0755) - if err != nil && !os.IsExist(err) { - panic(err) - } - - for i := 0; i < MaxFiles; i++ { - if depth == 0 { - filename := filepath.Join(target, fmt.Sprintf("file%d", i)) - fmt.Printf("create file %v\n", filename) - f, err := os.Create(filename) - if err != nil { - panic(err) - } - - _, err = io.Copy(f, rndRd(rand.Intn(1024))) - if err != nil { - panic(err) - } - - err = f.Close() - if err != nil { - panic(err) - } - } else { - createDir(filepath.Join(target, fmt.Sprintf("dir%d", i)), depth-1) - } - } -} - -func main() { - if len(os.Args) != 2 { - fmt.Fprintf(os.Stderr, "USAGE: %s TARGETDIR\n", os.Args[0]) - os.Exit(1) - } - - createDir(os.Args[1], MaxDepth) -}