From 09365cc4eae5048d5563d4efff7944ebf89821d9 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 8 Apr 2018 21:55:26 +0200 Subject: [PATCH 01/30] Add --trace-profile --- cmd/restic/global_debug.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/cmd/restic/global_debug.go b/cmd/restic/global_debug.go index 7cad172f6..cb7dac10a 100644 --- a/cmd/restic/global_debug.go +++ b/cmd/restic/global_debug.go @@ -18,6 +18,7 @@ var ( listenMemoryProfile string memProfilePath string cpuProfilePath string + traceProfilePath string insecure bool ) @@ -26,6 +27,7 @@ func init() { f.StringVar(&listenMemoryProfile, "listen-profile", "", "listen on this `address:port` for memory profiling") f.StringVar(&memProfilePath, "mem-profile", "", "write memory profile to `dir`") f.StringVar(&cpuProfilePath, "cpu-profile", "", "write cpu profile to `dir`") + f.StringVar(&traceProfilePath, "trace-profile", "", "write trace to `dir`") f.BoolVar(&insecure, "insecure-kdf", false, "use insecure KDF settings") } @@ -46,7 +48,18 @@ func runDebug() error { }() } - if memProfilePath != "" && cpuProfilePath != "" { + profilesEnabled := 0 + if memProfilePath != "" { + profilesEnabled++ + } + if cpuProfilePath != "" { + profilesEnabled++ + } + if traceProfilePath != "" { + profilesEnabled++ + } + + if profilesEnabled > 1 { return errors.Fatal("only one profile (memory or CPU) may be activated at the same time") } @@ -58,6 +71,8 @@ func runDebug() error { prof = profile.Start(profile.Quiet, profile.NoShutdownHook, profile.MemProfile, profile.ProfilePath(memProfilePath)) } else if cpuProfilePath != "" { prof = profile.Start(profile.Quiet, profile.NoShutdownHook, profile.CPUProfile, profile.ProfilePath(cpuProfilePath)) + } else if traceProfilePath != "" { + prof = profile.Start(profile.Quiet, profile.NoShutdownHook, profile.TraceProfile, profile.ProfilePath(traceProfilePath)) } if prof != nil { From e4fdc5eb76150a0b842fa3329916eb394e057cff Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sat, 23 Dec 2017 12:12:36 +0100 Subject: [PATCH 02/30] fs: Add IsRegularFile() --- internal/fs/helpers.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 internal/fs/helpers.go diff --git a/internal/fs/helpers.go b/internal/fs/helpers.go new file mode 100644 index 000000000..b7f7ad6ba --- /dev/null +++ b/internal/fs/helpers.go @@ -0,0 +1,13 @@ +package fs + +import "os" + +// IsRegularFile returns true if fi belongs to a normal file. If fi is nil, +// false is returned. +func IsRegularFile(fi os.FileInfo) bool { + if fi == nil { + return false + } + + return fi.Mode()&(os.ModeType|os.ModeCharDevice) == 0 +} From a472868e06211c6ef217115d88d0eee8977ebea0 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sat, 16 Dec 2017 00:01:28 +0100 Subject: [PATCH 03/30] fs: Add TestChdir() --- internal/archiver/archiver_test.go | 24 ++---------------------- internal/fs/helpers.go | 27 ++++++++++++++++++++++++++- internal/restic/restorer_test.go | 23 +---------------------- 3 files changed, 29 insertions(+), 45 deletions(-) diff --git a/internal/archiver/archiver_test.go b/internal/archiver/archiver_test.go index 035355a32..1e8b9355f 100644 --- a/internal/archiver/archiver_test.go +++ b/internal/archiver/archiver_test.go @@ -12,6 +12,7 @@ import ( "github.com/restic/restic/internal/archiver" "github.com/restic/restic/internal/crypto" + "github.com/restic/restic/internal/fs" "github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/restic" rtest "github.com/restic/restic/internal/test" @@ -226,27 +227,6 @@ func TestArchiveEmptySnapshot(t *testing.T) { } } -func chdir(t testing.TB, target string) (cleanup func()) { - curdir, err := os.Getwd() - if err != nil { - t.Fatal(err) - } - - t.Logf("chdir to %v", target) - err = os.Chdir(target) - if err != nil { - t.Fatal(err) - } - - return func() { - t.Logf("chdir back to %v", curdir) - err := os.Chdir(curdir) - if err != nil { - t.Fatal(err) - } - } -} - func TestArchiveNameCollision(t *testing.T) { repo, cleanup := repository.TestRepository(t) defer cleanup() @@ -260,7 +240,7 @@ func TestArchiveNameCollision(t *testing.T) { rtest.OK(t, ioutil.WriteFile(filepath.Join(dir, "testfile"), []byte("testfile1"), 0644)) rtest.OK(t, ioutil.WriteFile(filepath.Join(dir, "root", "testfile"), []byte("testfile2"), 0644)) - defer chdir(t, root)() + defer fs.TestChdir(t, root)() arch := archiver.New(repo) diff --git a/internal/fs/helpers.go b/internal/fs/helpers.go index b7f7ad6ba..1789f2bca 100644 --- a/internal/fs/helpers.go +++ b/internal/fs/helpers.go @@ -1,6 +1,9 @@ package fs -import "os" +import ( + "os" + "testing" +) // IsRegularFile returns true if fi belongs to a normal file. If fi is nil, // false is returned. @@ -11,3 +14,25 @@ func IsRegularFile(fi os.FileInfo) bool { return fi.Mode()&(os.ModeType|os.ModeCharDevice) == 0 } + +// TestChdir changes the current directory to dest, the function back returns to the previous directory. +func TestChdir(t testing.TB, dest string) (back func()) { + prev, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + t.Logf("chdir to %v", dest) + err = os.Chdir(dest) + if err != nil { + t.Fatal(err) + } + + return func() { + t.Logf("chdir back to %v", prev) + err = os.Chdir(prev) + if err != nil { + t.Fatal(err) + } + } +} diff --git a/internal/restic/restorer_test.go b/internal/restic/restorer_test.go index ec3282e5d..9b1758dc9 100644 --- a/internal/restic/restorer_test.go +++ b/internal/restic/restorer_test.go @@ -346,27 +346,6 @@ func TestRestorer(t *testing.T) { } } -func chdir(t testing.TB, target string) func() { - prev, err := os.Getwd() - if err != nil { - t.Fatal(err) - } - - t.Logf("chdir to %v", target) - err = os.Chdir(target) - if err != nil { - t.Fatal(err) - } - - return func() { - t.Logf("chdir back to %v", prev) - err = os.Chdir(prev) - if err != nil { - t.Fatal(err) - } - } -} - func TestRestorerRelative(t *testing.T) { var tests = []struct { Snapshot @@ -406,7 +385,7 @@ func TestRestorerRelative(t *testing.T) { tempdir, cleanup := rtest.TempDir(t) defer cleanup() - cleanup = chdir(t, tempdir) + cleanup = fs.TestChdir(t, tempdir) defer cleanup() errors := make(map[string]string) From 0532f0804834c7b765053bacefe8a65284897129 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Fri, 30 Mar 2018 22:49:49 +0200 Subject: [PATCH 04/30] Add test.Helper, also works with Go 1.8 --- internal/fs/helpers.go | 5 +++++ internal/repository/testing.go | 2 ++ internal/test/helper.go | 15 +++++++++++++++ internal/test/helper_go18.go | 19 +++++++++++++++++++ 4 files changed, 41 insertions(+) create mode 100644 internal/test/helper.go create mode 100644 internal/test/helper_go18.go diff --git a/internal/fs/helpers.go b/internal/fs/helpers.go index 1789f2bca..7235834ae 100644 --- a/internal/fs/helpers.go +++ b/internal/fs/helpers.go @@ -3,6 +3,8 @@ package fs import ( "os" "testing" + + "github.com/restic/restic/internal/test" ) // IsRegularFile returns true if fi belongs to a normal file. If fi is nil, @@ -17,6 +19,8 @@ func IsRegularFile(fi os.FileInfo) bool { // TestChdir changes the current directory to dest, the function back returns to the previous directory. func TestChdir(t testing.TB, dest string) (back func()) { + test.Helper(t).Helper() + prev, err := os.Getwd() if err != nil { t.Fatal(err) @@ -29,6 +33,7 @@ func TestChdir(t testing.TB, dest string) (back func()) { } return func() { + test.Helper(t).Helper() t.Logf("chdir back to %v", prev) err = os.Chdir(prev) if err != nil { diff --git a/internal/repository/testing.go b/internal/repository/testing.go index a49072335..739aa4d62 100644 --- a/internal/repository/testing.go +++ b/internal/repository/testing.go @@ -42,6 +42,7 @@ const testChunkerPol = chunker.Pol(0x3DA3358B4DC173) // password. If be is nil, an in-memory backend is used. A constant polynomial // is used for the chunker and low-security test parameters. func TestRepositoryWithBackend(t testing.TB, be restic.Backend) (r restic.Repository, cleanup func()) { + test.Helper(t).Helper() TestUseLowSecurityKDFParameters(t) restic.TestDisableCheckPolynomial(t) @@ -70,6 +71,7 @@ func TestRepositoryWithBackend(t testing.TB, be restic.Backend) (r restic.Reposi // a non-existing directory, a local backend is created there and this is used // instead. The directory is not removed, but left there for inspection. func TestRepository(t testing.TB) (r restic.Repository, cleanup func()) { + test.Helper(t).Helper() dir := os.Getenv("RESTIC_TEST_REPO") if dir != "" { _, err := os.Stat(dir) diff --git a/internal/test/helper.go b/internal/test/helper.go new file mode 100644 index 000000000..f0fc1f61b --- /dev/null +++ b/internal/test/helper.go @@ -0,0 +1,15 @@ +// +build go1.9 + +package test + +import "testing" + +// Helperer marks the current function as a test helper. +type Helperer interface { + Helper() +} + +// Helper returns a function that marks the current function as a helper function. +func Helper(t testing.TB) Helperer { + return t +} diff --git a/internal/test/helper_go18.go b/internal/test/helper_go18.go new file mode 100644 index 000000000..d4f8b8de6 --- /dev/null +++ b/internal/test/helper_go18.go @@ -0,0 +1,19 @@ +// +build !go1.9 + +package test + +import "testing" + +// Helperer marks the current function as a test helper. +type Helperer interface { + Helper() +} + +type fakeHelper struct{} + +func (fakeHelper) Helper() {} + +// Helper returns a function that marks the current function as a helper function. +func Helper(t testing.TB) Helperer { + return fakeHelper{} +} From 4e0b2a8e3acd3cf89b2a3de1cf9549297c980dd2 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sat, 23 Dec 2017 14:32:21 +0100 Subject: [PATCH 05/30] snapshot: correct error handling for filepath.Abs --- internal/archiver/archiver.go | 7 +++++++ internal/restic/snapshot.go | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/internal/archiver/archiver.go b/internal/archiver/archiver.go index 9f2e029fb..55024b53a 100644 --- a/internal/archiver/archiver.go +++ b/internal/archiver/archiver.go @@ -692,6 +692,13 @@ func (arch *Archiver) Snapshot(ctx context.Context, p *restic.Progress, paths, t } sn.Excludes = arch.Excludes + // make paths absolute + for i, path := range paths { + if p, err := filepath.Abs(path); err == nil { + paths[i] = p + } + } + jobs := archivePipe{} // use parent snapshot (if some was given) diff --git a/internal/restic/snapshot.go b/internal/restic/snapshot.go index 4622bb530..2eace5cfd 100644 --- a/internal/restic/snapshot.go +++ b/internal/restic/snapshot.go @@ -31,7 +31,7 @@ type Snapshot struct { // time. func NewSnapshot(paths []string, tags []string, hostname string, time time.Time) (*Snapshot, error) { for i, path := range paths { - if p, err := filepath.Abs(path); err != nil { + if p, err := filepath.Abs(path); err == nil { paths[i] = p } } From fa4f438bc1988ba06785398526d650a5f2aabc77 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Wed, 3 Jan 2018 22:10:20 +0100 Subject: [PATCH 06/30] snapshot: Do not modify slice of paths --- internal/restic/snapshot.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/internal/restic/snapshot.go b/internal/restic/snapshot.go index 2eace5cfd..61467013a 100644 --- a/internal/restic/snapshot.go +++ b/internal/restic/snapshot.go @@ -30,14 +30,18 @@ type Snapshot struct { // NewSnapshot returns an initialized snapshot struct for the current user and // time. func NewSnapshot(paths []string, tags []string, hostname string, time time.Time) (*Snapshot, error) { - for i, path := range paths { - if p, err := filepath.Abs(path); err == nil { - paths[i] = p + absPaths := make([]string, 0, len(paths)) + for _, path := range paths { + p, err := filepath.Abs(path) + if err == nil { + absPaths = append(absPaths, p) + } else { + absPaths = append(absPaths, path) } } sn := &Snapshot{ - Paths: paths, + Paths: absPaths, Time: time, Tags: tags, Hostname: hostname, From baebf45e2ec4f4e00f06b6d017777445e4437684 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sat, 3 Mar 2018 18:02:47 +0100 Subject: [PATCH 07/30] FindLatestSnapshot: Use absolute paths --- internal/restic/snapshot_find.go | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/internal/restic/snapshot_find.go b/internal/restic/snapshot_find.go index b5d0a8276..f16b91e3d 100644 --- a/internal/restic/snapshot_find.go +++ b/internal/restic/snapshot_find.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "path/filepath" "time" "github.com/restic/restic/internal/errors" @@ -14,13 +15,25 @@ var ErrNoSnapshotFound = errors.New("no snapshot found") // FindLatestSnapshot finds latest snapshot with optional target/directory, tags and hostname filters. func FindLatestSnapshot(ctx context.Context, repo Repository, targets []string, tagLists []TagList, hostname string) (ID, error) { + var err error + absTargets := make([]string, 0, len(targets)) + for _, target := range targets { + if !filepath.IsAbs(target) { + target, err = filepath.Abs(target) + if err != nil { + return ID{}, errors.Wrap(err, "Abs") + } + } + absTargets = append(absTargets, target) + } + var ( latest time.Time latestID ID found bool ) - err := repo.List(ctx, SnapshotFile, func(snapshotID ID, size int64) error { + err = repo.List(ctx, SnapshotFile, func(snapshotID ID, size int64) error { snapshot, err := LoadSnapshot(ctx, repo, snapshotID) if err != nil { return errors.Errorf("Error loading snapshot %v: %v", snapshotID.Str(), err) @@ -33,7 +46,7 @@ func FindLatestSnapshot(ctx context.Context, repo Repository, targets []string, return nil } - if !snapshot.HasPaths(targets) { + if !snapshot.HasPaths(absTargets) { return nil } From cc847a3d6d631767d69ba71b9b5d190ef33a4891 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Fri, 30 Mar 2018 22:34:17 +0200 Subject: [PATCH 08/30] tree: Improve error for pre-existing node --- internal/restic/tree.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/restic/tree.go b/internal/restic/tree.go index c2cb3b27b..317119096 100644 --- a/internal/restic/tree.go +++ b/internal/restic/tree.go @@ -48,7 +48,7 @@ func (t Tree) Equals(other *Tree) bool { func (t *Tree) Insert(node *Node) error { pos, _, err := t.binarySearch(node.Name) if err == nil { - return errors.New("node already present") + return errors.Errorf("node %q already present", node.Name) } // https://code.google.com/p/go-wiki/wiki/SliceTricks From 6a7c23d2ae66ac3417f43811dbc2fc89ef7af7d3 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Thu, 4 Jan 2018 20:44:53 +0100 Subject: [PATCH 09/30] tree: Add convenience functions --- internal/restic/tree.go | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/internal/restic/tree.go b/internal/restic/tree.go index 317119096..81650105a 100644 --- a/internal/restic/tree.go +++ b/internal/restic/tree.go @@ -21,12 +21,12 @@ func NewTree() *Tree { } } -func (t Tree) String() string { +func (t *Tree) String() string { return fmt.Sprintf("Tree<%d nodes>", len(t.Nodes)) } // Equals returns true if t and other have exactly the same nodes. -func (t Tree) Equals(other *Tree) bool { +func (t *Tree) Equals(other *Tree) bool { if len(t.Nodes) != len(other.Nodes) { debug.Log("tree.Equals(): trees have different number of nodes") return false @@ -46,8 +46,8 @@ func (t Tree) Equals(other *Tree) bool { // Insert adds a new node at the correct place in the tree. func (t *Tree) Insert(node *Node) error { - pos, _, err := t.binarySearch(node.Name) - if err == nil { + pos, found := t.find(node.Name) + if found != nil { return errors.Errorf("node %q already present", node.Name) } @@ -59,16 +59,26 @@ func (t *Tree) Insert(node *Node) error { return nil } -func (t Tree) binarySearch(name string) (int, *Node, error) { +func (t *Tree) find(name string) (int, *Node) { pos := sort.Search(len(t.Nodes), func(i int) bool { return t.Nodes[i].Name >= name }) if pos < len(t.Nodes) && t.Nodes[pos].Name == name { - return pos, t.Nodes[pos], nil + return pos, t.Nodes[pos] } - return pos, nil, errors.New("named node not found") + return pos, nil +} + +// Find returns a node with the given name, or nil if none could be found. +func (t *Tree) Find(name string) *Node { + if t == nil { + return nil + } + + _, node := t.find(name) + return node } // Sort sorts the nodes by name. @@ -79,7 +89,7 @@ func (t *Tree) Sort() { } // Subtrees returns a slice of all subtree IDs of the tree. -func (t Tree) Subtrees() (trees IDs) { +func (t *Tree) Subtrees() (trees IDs) { for _, node := range t.Nodes { if node.Type == "dir" && node.Subtree != nil { trees = append(trees, *node.Subtree) From a069467e72d791796e2ea2ed665fa2f523132e06 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Fri, 30 Mar 2018 22:35:18 +0200 Subject: [PATCH 10/30] ls: Improve output --- cmd/restic/cmd_ls.go | 5 +++-- cmd/restic/format.go | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/cmd/restic/cmd_ls.go b/cmd/restic/cmd_ls.go index 4a046b7ef..d4a768d70 100644 --- a/cmd/restic/cmd_ls.go +++ b/cmd/restic/cmd_ls.go @@ -56,7 +56,8 @@ func printTree(ctx context.Context, repo *repository.Repository, id *restic.ID, Printf("%s\n", formatNode(prefix, entry, lsOptions.ListLong)) if entry.Type == "dir" && entry.Subtree != nil { - if err = printTree(ctx, repo, entry.Subtree, filepath.Join(prefix, entry.Name)); err != nil { + entryPath := prefix + string(filepath.Separator) + entry.Name + if err = printTree(ctx, repo, entry.Subtree, entryPath); err != nil { return err } } @@ -84,7 +85,7 @@ func runLs(opts LsOptions, gopts GlobalOptions, args []string) error { for sn := range FindFilteredSnapshots(ctx, repo, opts.Host, opts.Tags, opts.Paths, args) { Verbosef("snapshot %s of %v at %s):\n", sn.ID().Str(), sn.Paths, sn.Time) - if err = printTree(gopts.ctx, repo, sn.Tree, string(filepath.Separator)); err != nil { + if err = printTree(gopts.ctx, repo, sn.Tree, ""); err != nil { return err } } diff --git a/cmd/restic/format.go b/cmd/restic/format.go index 9f66d1c1d..1f8ab366e 100644 --- a/cmd/restic/format.go +++ b/cmd/restic/format.go @@ -64,8 +64,9 @@ func formatDuration(d time.Duration) string { } func formatNode(prefix string, n *restic.Node, long bool) string { + nodepath := prefix + string(filepath.Separator) + n.Name if !long { - return filepath.Join(prefix, n.Name) + return nodepath } var mode os.FileMode @@ -91,6 +92,6 @@ func formatNode(prefix string, n *restic.Node, long bool) string { return fmt.Sprintf("%s %5d %5d %6d %s %s%s", mode|n.Mode, n.UID, n.GID, n.Size, - n.ModTime.Format(TimeFormat), filepath.Join(prefix, n.Name), + n.ModTime.Format(TimeFormat), nodepath, target) } From 83ca08245b0117e40298c77ee888c03213966081 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sat, 31 Mar 2018 13:22:25 +0200 Subject: [PATCH 11/30] checker: Check metadata size and blob sizes --- internal/checker/checker.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/internal/checker/checker.go b/internal/checker/checker.go index 0b645caa1..432bfa742 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -569,12 +569,24 @@ func (c *Checker) checkTree(id restic.ID, tree *restic.Tree) (errs []error) { errs = append(errs, Error{TreeID: id, Err: errors.Errorf("file %q has nil blob list", node.Name)}) } + var size uint64 for b, blobID := range node.Content { if blobID.IsNull() { errs = append(errs, Error{TreeID: id, Err: errors.Errorf("file %q blob %d has null ID", node.Name, b)}) continue } blobs = append(blobs, blobID) + blobSize, found := c.repo.LookupBlobSize(blobID, restic.DataBlob) + if !found { + errs = append(errs, Error{TreeID: id, Err: errors.Errorf("file %q blob %d size could not be found", node.Name, b)}) + } + size += uint64(blobSize) + } + if size != node.Size { + errs = append(errs, Error{ + TreeID: id, + Err: errors.Errorf("file %q: metadata size (%v) and sum of blob sizes (%v) do not match", node.Name, node.Size, size), + }) } case "dir": if node.Subtree == nil { From c4b2486b7c9f084a33b9b0605a6fa273b9408248 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Wed, 3 Jan 2018 21:53:45 +0100 Subject: [PATCH 12/30] fs: Add interface and FS implementations This adds two implementations of the new `FS` interface: One for the local file system (`Local`) and one for a single file read from an `io.Reader` (`Reader`). --- internal/fs/const.go | 16 ++ internal/fs/const_unix.go | 8 + internal/fs/const_windows.go | 6 + internal/fs/file.go | 14 -- internal/fs/fs_local.go | 96 ++++++++++ internal/fs/fs_reader.go | 289 ++++++++++++++++++++++++++++++ internal/fs/fs_reader_test.go | 319 ++++++++++++++++++++++++++++++++++ internal/fs/fs_track.go | 54 ++++++ internal/fs/interface.go | 38 ++++ internal/fs/stat.go | 34 ++++ internal/fs/stat_bsd.go | 36 ++++ internal/fs/stat_test.go | 31 ++++ internal/fs/stat_unix.go | 36 ++++ internal/fs/stat_windows.go | 31 ++++ 14 files changed, 994 insertions(+), 14 deletions(-) create mode 100644 internal/fs/const.go create mode 100644 internal/fs/const_unix.go create mode 100644 internal/fs/const_windows.go create mode 100644 internal/fs/fs_local.go create mode 100644 internal/fs/fs_reader.go create mode 100644 internal/fs/fs_reader_test.go create mode 100644 internal/fs/fs_track.go create mode 100644 internal/fs/interface.go create mode 100644 internal/fs/stat.go create mode 100644 internal/fs/stat_bsd.go create mode 100644 internal/fs/stat_test.go create mode 100644 internal/fs/stat_unix.go create mode 100644 internal/fs/stat_windows.go diff --git a/internal/fs/const.go b/internal/fs/const.go new file mode 100644 index 000000000..dfa6ad5f0 --- /dev/null +++ b/internal/fs/const.go @@ -0,0 +1,16 @@ +package fs + +import "syscall" + +// Flags to OpenFile wrapping those of the underlying system. Not all flags may +// be implemented on a given system. +const ( + O_RDONLY int = syscall.O_RDONLY // open the file read-only. + O_WRONLY int = syscall.O_WRONLY // open the file write-only. + O_RDWR int = syscall.O_RDWR // open the file read-write. + O_APPEND int = syscall.O_APPEND // append data to the file when writing. + O_CREATE int = syscall.O_CREAT // create a new file if none exists. + O_EXCL int = syscall.O_EXCL // used with O_CREATE, file must not exist + O_SYNC int = syscall.O_SYNC // open for synchronous I/O. + O_TRUNC int = syscall.O_TRUNC // if possible, truncate file when opened. +) diff --git a/internal/fs/const_unix.go b/internal/fs/const_unix.go new file mode 100644 index 000000000..a90d171b1 --- /dev/null +++ b/internal/fs/const_unix.go @@ -0,0 +1,8 @@ +// +build !windows + +package fs + +import "syscall" + +// O_NOFOLLOW instructs the kernel to not follow symlinks when opening a file. +const O_NOFOLLOW int = syscall.O_NOFOLLOW diff --git a/internal/fs/const_windows.go b/internal/fs/const_windows.go new file mode 100644 index 000000000..18c89c27e --- /dev/null +++ b/internal/fs/const_windows.go @@ -0,0 +1,6 @@ +// +build windows + +package fs + +// O_NOFOLLOW is a noop on Windows. +const O_NOFOLLOW int = 0 diff --git a/internal/fs/file.go b/internal/fs/file.go index d055107b4..86c519aff 100644 --- a/internal/fs/file.go +++ b/internal/fs/file.go @@ -1,25 +1,11 @@ package fs import ( - "io" "os" "path/filepath" "time" ) -// File is an open file on a file system. -type File interface { - io.Reader - io.Writer - io.Closer - - Fd() uintptr - Readdirnames(n int) ([]string, error) - Readdir(int) ([]os.FileInfo, error) - Seek(int64, int) (int64, error) - Stat() (os.FileInfo, error) -} - // Mkdir creates a new directory with the specified name and permission bits. // If there is an error, it will be of type *PathError. func Mkdir(name string, perm os.FileMode) error { diff --git a/internal/fs/fs_local.go b/internal/fs/fs_local.go new file mode 100644 index 000000000..dd1faafa0 --- /dev/null +++ b/internal/fs/fs_local.go @@ -0,0 +1,96 @@ +package fs + +import ( + "os" + "path/filepath" +) + +// Local is the local file system. Most methods are just passed on to the stdlib. +type Local struct{} + +// statically ensure that Local implements FS. +var _ FS = &Local{} + +// VolumeName returns leading volume name. Given "C:\foo\bar" it returns "C:" +// on Windows. Given "\\host\share\foo" it returns "\\host\share". On other +// platforms it returns "". +func (fs Local) VolumeName(path string) string { + return filepath.VolumeName(path) +} + +// Open opens a file for reading. +func (fs Local) Open(name string) (File, error) { + f, err := os.Open(fixpath(name)) + if err != nil { + return nil, err + } + return f, nil +} + +// OpenFile is the generalized open call; most users will use Open +// or Create instead. It opens the named file with specified flag +// (O_RDONLY etc.) and perm, (0666 etc.) if applicable. If successful, +// methods on the returned File can be used for I/O. +// If there is an error, it will be of type *PathError. +func (fs Local) OpenFile(name string, flag int, perm os.FileMode) (File, error) { + f, err := os.OpenFile(fixpath(name), flag, perm) + if err != nil { + return nil, err + } + return f, nil +} + +// Stat returns a FileInfo describing the named file. If there is an error, it +// will be of type *PathError. +func (fs Local) Stat(name string) (os.FileInfo, error) { + return os.Stat(fixpath(name)) +} + +// Lstat returns the FileInfo structure describing the named file. +// If the file is a symbolic link, the returned FileInfo +// describes the symbolic link. Lstat makes no attempt to follow the link. +// If there is an error, it will be of type *PathError. +func (fs Local) Lstat(name string) (os.FileInfo, error) { + return os.Lstat(fixpath(name)) +} + +// Join joins any number of path elements into a single path, adding a +// Separator if necessary. Join calls Clean on the result; in particular, all +// empty strings are ignored. On Windows, the result is a UNC path if and only +// if the first path element is a UNC path. +func (fs Local) Join(elem ...string) string { + return filepath.Join(elem...) +} + +// Separator returns the OS and FS dependent separator for dirs/subdirs/files. +func (fs Local) Separator() string { + return string(filepath.Separator) +} + +// IsAbs reports whether the path is absolute. +func (fs Local) IsAbs(path string) bool { + return filepath.IsAbs(path) +} + +// Abs returns an absolute representation of path. If the path is not absolute +// it will be joined with the current working directory to turn it into an +// absolute path. The absolute path name for a given file is not guaranteed to +// be unique. Abs calls Clean on the result. +func (fs Local) Abs(path string) (string, error) { + return filepath.Abs(path) +} + +// Clean returns the cleaned path. For details, see filepath.Clean. +func (fs Local) Clean(p string) string { + return filepath.Clean(p) +} + +// Base returns the last element of path. +func (fs Local) Base(path string) string { + return filepath.Base(path) +} + +// Dir returns path without the last element. +func (fs Local) Dir(path string) string { + return filepath.Dir(path) +} diff --git a/internal/fs/fs_reader.go b/internal/fs/fs_reader.go new file mode 100644 index 000000000..385c8f92b --- /dev/null +++ b/internal/fs/fs_reader.go @@ -0,0 +1,289 @@ +package fs + +import ( + "io" + "os" + "path" + "sync" + "syscall" + "time" + + "github.com/restic/restic/internal/errors" +) + +// Reader is a file system which provides a directory with a single file. When +// this file is opened for reading, the reader is passed through. The file can +// be opened once, all subsequent open calls return syscall.EIO. For Lstat(), +// the provided FileInfo is returned. +type Reader struct { + Name string + io.ReadCloser + + Mode os.FileMode + ModTime time.Time + Size int64 + + open sync.Once +} + +// statically ensure that Local implements FS. +var _ FS = &Reader{} + +// VolumeName returns leading volume name, for the Reader file system it's +// always the empty string. +func (fs *Reader) VolumeName(path string) string { + return "" +} + +// Open opens a file for reading. +func (fs *Reader) Open(name string) (f File, err error) { + switch name { + case fs.Name: + fs.open.Do(func() { + f = newReaderFile(fs.ReadCloser, fs.fi()) + }) + + if f == nil { + return nil, syscall.EIO + } + + return f, nil + case "/", ".": + f = fakeDir{ + entries: []os.FileInfo{fs.fi()}, + } + return f, nil + } + + return nil, syscall.ENOENT +} + +func (fs *Reader) fi() os.FileInfo { + return fakeFileInfo{ + name: fs.Name, + size: fs.Size, + mode: fs.Mode, + modtime: fs.ModTime, + } +} + +// OpenFile is the generalized open call; most users will use Open +// or Create instead. It opens the named file with specified flag +// (O_RDONLY etc.) and perm, (0666 etc.) if applicable. If successful, +// methods on the returned File can be used for I/O. +// If there is an error, it will be of type *PathError. +func (fs *Reader) OpenFile(name string, flag int, perm os.FileMode) (f File, err error) { + if flag & ^(O_RDONLY|O_NOFOLLOW) != 0 { + return nil, errors.Errorf("invalid combination of flags 0x%x", flag) + } + + fs.open.Do(func() { + f = newReaderFile(fs.ReadCloser, fs.fi()) + }) + + if f == nil { + return nil, syscall.EIO + } + + return f, nil +} + +// Stat returns a FileInfo describing the named file. If there is an error, it +// will be of type *PathError. +func (fs *Reader) Stat(name string) (os.FileInfo, error) { + return fs.Lstat(name) +} + +// Lstat returns the FileInfo structure describing the named file. +// If the file is a symbolic link, the returned FileInfo +// describes the symbolic link. Lstat makes no attempt to follow the link. +// If there is an error, it will be of type *PathError. +func (fs *Reader) Lstat(name string) (os.FileInfo, error) { + switch name { + case fs.Name: + return fs.fi(), nil + case "/", ".": + fi := fakeFileInfo{ + name: name, + size: 0, + mode: 0755, + modtime: time.Now(), + } + return fi, nil + } + + return nil, os.ErrNotExist +} + +// Join joins any number of path elements into a single path, adding a +// Separator if necessary. Join calls Clean on the result; in particular, all +// empty strings are ignored. On Windows, the result is a UNC path if and only +// if the first path element is a UNC path. +func (fs *Reader) Join(elem ...string) string { + return path.Join(elem...) +} + +// Separator returns the OS and FS dependent separator for dirs/subdirs/files. +func (fs *Reader) Separator() string { + return "/" +} + +// IsAbs reports whether the path is absolute. For the Reader, this is always the case. +func (fs *Reader) IsAbs(p string) bool { + return true +} + +// Abs returns an absolute representation of path. If the path is not absolute +// it will be joined with the current working directory to turn it into an +// absolute path. The absolute path name for a given file is not guaranteed to +// be unique. Abs calls Clean on the result. +// +// For the Reader, all paths are absolute. +func (fs *Reader) Abs(p string) (string, error) { + return path.Clean(p), nil +} + +// Clean returns the cleaned path. For details, see filepath.Clean. +func (fs *Reader) Clean(p string) string { + return path.Clean(p) +} + +// Base returns the last element of p. +func (fs *Reader) Base(p string) string { + return path.Base(p) +} + +// Dir returns p without the last element. +func (fs *Reader) Dir(p string) string { + return path.Dir(p) +} + +func newReaderFile(rd io.ReadCloser, fi os.FileInfo) readerFile { + return readerFile{ + ReadCloser: rd, + fakeFile: fakeFile{ + FileInfo: fi, + name: fi.Name(), + }, + } +} + +type readerFile struct { + io.ReadCloser + fakeFile +} + +func (r readerFile) Read(p []byte) (int, error) { + return r.ReadCloser.Read(p) +} + +func (r readerFile) Close() error { + return r.ReadCloser.Close() +} + +// ensure that readerFile implements File +var _ File = readerFile{} + +// fakeFile implements all File methods, but only returns errors for anything +// except Stat() and Name(). +type fakeFile struct { + name string + os.FileInfo +} + +// ensure that fakeFile implements File +var _ File = fakeFile{} + +func (f fakeFile) Fd() uintptr { + return 0 +} + +func (f fakeFile) Readdirnames(n int) ([]string, error) { + return nil, os.ErrInvalid +} + +func (f fakeFile) Readdir(n int) ([]os.FileInfo, error) { + return nil, os.ErrInvalid +} + +func (f fakeFile) Seek(int64, int) (int64, error) { + return 0, os.ErrInvalid +} + +func (f fakeFile) Write(p []byte) (int, error) { + return 0, os.ErrInvalid +} + +func (f fakeFile) Read(p []byte) (int, error) { + return 0, os.ErrInvalid +} + +func (f fakeFile) Close() error { + return nil +} + +func (f fakeFile) Stat() (os.FileInfo, error) { + return f.FileInfo, nil +} + +func (f fakeFile) Name() string { + return f.name +} + +// fakeDir implements Readdirnames and Readdir, everything else is delegated to fakeFile. +type fakeDir struct { + entries []os.FileInfo + fakeFile +} + +func (d fakeDir) Readdirnames(n int) ([]string, error) { + if n >= 0 { + return nil, errors.New("not implemented") + } + names := make([]string, 0, len(d.entries)) + for _, entry := range d.entries { + names = append(names, entry.Name()) + } + + return names, nil +} + +func (d fakeDir) Readdir(n int) ([]os.FileInfo, error) { + if n >= 0 { + return nil, errors.New("not implemented") + } + return d.entries, nil +} + +// fakeFileInfo implements the bare minimum of os.FileInfo. +type fakeFileInfo struct { + name string + size int64 + mode os.FileMode + modtime time.Time + sys interface{} +} + +func (fi fakeFileInfo) Name() string { + return fi.name +} + +func (fi fakeFileInfo) Size() int64 { + return fi.size +} + +func (fi fakeFileInfo) Mode() os.FileMode { + return fi.mode +} + +func (fi fakeFileInfo) ModTime() time.Time { + return fi.modtime +} + +func (fi fakeFileInfo) IsDir() bool { + return fi.mode&os.ModeDir > 0 +} + +func (fi fakeFileInfo) Sys() interface{} { + return fi.sys +} diff --git a/internal/fs/fs_reader_test.go b/internal/fs/fs_reader_test.go new file mode 100644 index 000000000..f4cb2bb34 --- /dev/null +++ b/internal/fs/fs_reader_test.go @@ -0,0 +1,319 @@ +package fs + +import ( + "bytes" + "io/ioutil" + "os" + "sort" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/restic/restic/internal/test" +) + +func verifyFileContentOpen(t testing.TB, fs FS, filename string, want []byte) { + f, err := fs.Open(filename) + if err != nil { + t.Fatal(err) + } + + buf, err := ioutil.ReadAll(f) + if err != nil { + t.Fatal(err) + } + + err = f.Close() + if err != nil { + t.Fatal(err) + } + + if !cmp.Equal(want, buf) { + t.Error(cmp.Diff(want, buf)) + } +} + +func verifyFileContentOpenFile(t testing.TB, fs FS, filename string, want []byte) { + f, err := fs.OpenFile(filename, O_RDONLY, 0) + if err != nil { + t.Fatal(err) + } + + buf, err := ioutil.ReadAll(f) + if err != nil { + t.Fatal(err) + } + + err = f.Close() + if err != nil { + t.Fatal(err) + } + + if !cmp.Equal(want, buf) { + t.Error(cmp.Diff(want, buf)) + } +} + +func verifyDirectoryContents(t testing.TB, fs FS, dir string, want []string) { + f, err := fs.Open(dir) + if err != nil { + t.Fatal(err) + } + + entries, err := f.Readdirnames(-1) + if err != nil { + t.Fatal(err) + } + + err = f.Close() + if err != nil { + t.Fatal(err) + } + + sort.Sort(sort.StringSlice(want)) + sort.Sort(sort.StringSlice(entries)) + + if !cmp.Equal(want, entries) { + t.Error(cmp.Diff(want, entries)) + } +} + +type fiSlice []os.FileInfo + +func (s fiSlice) Len() int { + return len(s) +} + +func (s fiSlice) Less(i, j int) bool { + return s[i].Name() < s[j].Name() +} + +func (s fiSlice) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func verifyDirectoryContentsFI(t testing.TB, fs FS, dir string, want []os.FileInfo) { + f, err := fs.Open(dir) + if err != nil { + t.Fatal(err) + } + + entries, err := f.Readdir(-1) + if err != nil { + t.Fatal(err) + } + + err = f.Close() + if err != nil { + t.Fatal(err) + } + + sort.Sort(fiSlice(want)) + sort.Sort(fiSlice(entries)) + + if len(want) != len(entries) { + t.Errorf("wrong number of entries returned, want %d, got %d", len(want), len(entries)) + } + max := len(want) + if len(entries) < max { + max = len(entries) + } + + for i := 0; i < max; i++ { + fi1 := want[i] + fi2 := entries[i] + + if fi1.Name() != fi2.Name() { + t.Errorf("entry %d: wrong value for Name: want %q, got %q", i, fi1.Name(), fi2.Name()) + } + + if fi1.IsDir() != fi2.IsDir() { + t.Errorf("entry %d: wrong value for IsDir: want %v, got %v", i, fi1.IsDir(), fi2.IsDir()) + } + + if fi1.Mode() != fi2.Mode() { + t.Errorf("entry %d: wrong value for Mode: want %v, got %v", i, fi1.Mode(), fi2.Mode()) + } + + if fi1.ModTime() != fi2.ModTime() { + t.Errorf("entry %d: wrong value for ModTime: want %v, got %v", i, fi1.ModTime(), fi2.ModTime()) + } + + if fi1.Size() != fi2.Size() { + t.Errorf("entry %d: wrong value for Size: want %v, got %v", i, fi1.Size(), fi2.Size()) + } + + if fi1.Sys() != fi2.Sys() { + t.Errorf("entry %d: wrong value for Sys: want %v, got %v", i, fi1.Sys(), fi2.Sys()) + } + } +} + +func checkFileInfo(t testing.TB, fi os.FileInfo, filename string, modtime time.Time, mode os.FileMode, isdir bool) { + if fi.IsDir() { + t.Errorf("IsDir returned true, want false") + } + + if fi.Mode() != mode { + t.Errorf("Mode() returned wrong value, want 0%o, got 0%o", mode, fi.Mode()) + } + + if !modtime.Equal(time.Time{}) && !fi.ModTime().Equal(modtime) { + t.Errorf("ModTime() returned wrong value, want %v, got %v", modtime, fi.ModTime()) + } + + if fi.Name() != filename { + t.Errorf("Name() returned wrong value, want %q, got %q", filename, fi.Name()) + } +} + +func TestFSReader(t *testing.T) { + data := test.Random(55, 1<<18+588) + now := time.Now() + filename := "foobar" + + var tests = []struct { + name string + f func(t *testing.T, fs FS) + }{ + { + name: "Readdirnames-slash", + f: func(t *testing.T, fs FS) { + verifyDirectoryContents(t, fs, "/", []string{filename}) + }, + }, + { + name: "Readdirnames-current", + f: func(t *testing.T, fs FS) { + verifyDirectoryContents(t, fs, ".", []string{filename}) + }, + }, + { + name: "Readdir-slash", + f: func(t *testing.T, fs FS) { + fi := fakeFileInfo{ + mode: 0644, + modtime: now, + name: filename, + size: int64(len(data)), + } + verifyDirectoryContentsFI(t, fs, "/", []os.FileInfo{fi}) + }, + }, + { + name: "Readdir-current", + f: func(t *testing.T, fs FS) { + fi := fakeFileInfo{ + mode: 0644, + modtime: now, + name: filename, + size: int64(len(data)), + } + verifyDirectoryContentsFI(t, fs, ".", []os.FileInfo{fi}) + }, + }, + { + name: "file/Open", + f: func(t *testing.T, fs FS) { + verifyFileContentOpen(t, fs, filename, data) + }, + }, + { + name: "file/OpenFile", + f: func(t *testing.T, fs FS) { + verifyFileContentOpenFile(t, fs, filename, data) + }, + }, + { + name: "file/Lstat", + f: func(t *testing.T, fs FS) { + fi, err := fs.Lstat(filename) + if err != nil { + t.Fatal(err) + } + + checkFileInfo(t, fi, filename, now, 0644, false) + }, + }, + { + name: "file/Stat", + f: func(t *testing.T, fs FS) { + f, err := fs.Open(filename) + if err != nil { + t.Fatal(err) + } + + fi, err := f.Stat() + if err != nil { + t.Fatal(err) + } + + err = f.Close() + if err != nil { + t.Fatal(err) + } + + checkFileInfo(t, fi, filename, now, 0644, false) + }, + }, + { + name: "dir/Lstat-slash", + f: func(t *testing.T, fs FS) { + fi, err := fs.Lstat("/") + if err != nil { + t.Fatal(err) + } + + checkFileInfo(t, fi, "/", time.Time{}, 0755, false) + }, + }, + { + name: "dir/Lstat-current", + f: func(t *testing.T, fs FS) { + fi, err := fs.Lstat(".") + if err != nil { + t.Fatal(err) + } + + checkFileInfo(t, fi, ".", time.Time{}, 0755, false) + }, + }, + { + name: "dir/Open-slash", + f: func(t *testing.T, fs FS) { + fi, err := fs.Lstat("/") + if err != nil { + t.Fatal(err) + } + + checkFileInfo(t, fi, "/", time.Time{}, 0755, false) + }, + }, + { + name: "dir/Open-current", + f: func(t *testing.T, fs FS) { + fi, err := fs.Lstat(".") + if err != nil { + t.Fatal(err) + } + + checkFileInfo(t, fi, ".", time.Time{}, 0755, false) + }, + }, + } + + for _, test := range tests { + fs := &Reader{ + Name: filename, + ReadCloser: ioutil.NopCloser(bytes.NewReader(data)), + + Mode: 0644, + Size: int64(len(data)), + ModTime: now, + } + + t.Run(test.name, func(t *testing.T) { + test.f(t, fs) + }) + } +} diff --git a/internal/fs/fs_track.go b/internal/fs/fs_track.go new file mode 100644 index 000000000..319fbfaff --- /dev/null +++ b/internal/fs/fs_track.go @@ -0,0 +1,54 @@ +package fs + +import ( + "fmt" + "os" + "runtime" + "runtime/debug" +) + +// Track is a wrapper around another file system which installs finalizers +// for open files which call panic() when they are not closed when the garbage +// collector releases them. This can be used to find resource leaks via open +// files. +type Track struct { + FS +} + +// Open wraps the Open method of the underlying file system. +func (fs Track) Open(name string) (File, error) { + f, err := fs.FS.Open(fixpath(name)) + if err != nil { + return nil, err + } + + return newTrackFile(debug.Stack(), name, f), nil +} + +// OpenFile wraps the OpenFile method of the underlying file system. +func (fs Track) OpenFile(name string, flag int, perm os.FileMode) (File, error) { + f, err := fs.FS.OpenFile(fixpath(name), flag, perm) + if err != nil { + return nil, err + } + + return newTrackFile(debug.Stack(), name, f), nil +} + +type trackFile struct { + File +} + +func newTrackFile(stack []byte, filename string, file File) *trackFile { + f := &trackFile{file} + runtime.SetFinalizer(f, func(f *trackFile) { + fmt.Fprintf(os.Stderr, "file %s not closed\n\nStacktrack:\n%s\n", filename, stack) + panic("file " + filename + " not closed") + }) + return f +} + +func (f *trackFile) Close() error { + runtime.SetFinalizer(f, nil) + return f.File.Close() +} diff --git a/internal/fs/interface.go b/internal/fs/interface.go new file mode 100644 index 000000000..1c2260215 --- /dev/null +++ b/internal/fs/interface.go @@ -0,0 +1,38 @@ +package fs + +import ( + "io" + "os" +) + +// FS bundles all methods needed for a file system. +type FS interface { + Open(name string) (File, error) + OpenFile(name string, flag int, perm os.FileMode) (File, error) + Stat(name string) (os.FileInfo, error) + Lstat(name string) (os.FileInfo, error) + + Join(elem ...string) string + Separator() string + Abs(path string) (string, error) + Clean(path string) string + VolumeName(path string) string + IsAbs(path string) bool + + Dir(path string) string + Base(path string) string +} + +// File is an open file on a file system. +type File interface { + io.Reader + io.Writer + io.Closer + + Fd() uintptr + Readdirnames(n int) ([]string, error) + Readdir(int) ([]os.FileInfo, error) + Seek(int64, int) (int64, error) + Stat() (os.FileInfo, error) + Name() string +} diff --git a/internal/fs/stat.go b/internal/fs/stat.go new file mode 100644 index 000000000..d37d12942 --- /dev/null +++ b/internal/fs/stat.go @@ -0,0 +1,34 @@ +package fs + +import ( + "os" + "time" +) + +// ExtendedFileInfo is an extended stat_t, filled with attributes that are +// supported by most operating systems. The original FileInfo is embedded. +type ExtendedFileInfo struct { + os.FileInfo + + DeviceID uint64 // ID of device containing the file + Inode uint64 // Inode number + Links uint64 // Number of hard links + UID uint32 // owner user ID + GID uint32 // owner group ID + Device uint64 // Device ID (if this is a device file) + BlockSize int64 // block size for filesystem IO + Blocks int64 // number of allocated filesystem blocks + Size int64 // file size in byte + + AccessTime time.Time // last access time stamp + ModTime time.Time // last (content) modification time stamp +} + +// ExtendedStat returns an ExtendedFileInfo constructed from the os.FileInfo. +func ExtendedStat(fi os.FileInfo) ExtendedFileInfo { + if fi == nil { + panic("os.FileInfo is nil") + } + + return extendedStat(fi) +} diff --git a/internal/fs/stat_bsd.go b/internal/fs/stat_bsd.go new file mode 100644 index 000000000..97c03bedc --- /dev/null +++ b/internal/fs/stat_bsd.go @@ -0,0 +1,36 @@ +// +build freebsd darwin + +package fs + +import ( + "fmt" + "os" + "syscall" + "time" +) + +// extendedStat extracts info into an ExtendedFileInfo for unix based operating systems. +func extendedStat(fi os.FileInfo) ExtendedFileInfo { + s, ok := fi.Sys().(*syscall.Stat_t) + if !ok { + panic(fmt.Sprintf("conversion to syscall.Stat_t failed, type is %T", fi.Sys())) + } + + extFI := ExtendedFileInfo{ + FileInfo: fi, + DeviceID: uint64(s.Dev), + Inode: uint64(s.Ino), + Links: uint64(s.Nlink), + UID: s.Uid, + GID: s.Gid, + Device: uint64(s.Rdev), + BlockSize: int64(s.Blksize), + Blocks: s.Blocks, + Size: s.Size, + + AccessTime: time.Unix(s.Atimespec.Unix()), + ModTime: time.Unix(s.Mtimespec.Unix()), + } + + return extFI +} diff --git a/internal/fs/stat_test.go b/internal/fs/stat_test.go new file mode 100644 index 000000000..43e514047 --- /dev/null +++ b/internal/fs/stat_test.go @@ -0,0 +1,31 @@ +package fs + +import ( + "io/ioutil" + "path/filepath" + "testing" + + restictest "github.com/restic/restic/internal/test" +) + +func TestExtendedStat(t *testing.T) { + tempdir, cleanup := restictest.TempDir(t) + defer cleanup() + + filename := filepath.Join(tempdir, "file") + err := ioutil.WriteFile(filename, []byte("foobar"), 0640) + if err != nil { + t.Fatal(err) + } + + fi, err := Lstat(filename) + if err != nil { + t.Fatal(err) + } + + extFI := ExtendedStat(fi) + + if !extFI.ModTime.Equal(fi.ModTime()) { + t.Errorf("extFI.ModTime does not match, want %v, got %v", fi.ModTime(), extFI.ModTime) + } +} diff --git a/internal/fs/stat_unix.go b/internal/fs/stat_unix.go new file mode 100644 index 000000000..612898566 --- /dev/null +++ b/internal/fs/stat_unix.go @@ -0,0 +1,36 @@ +// +build !windows,!darwin,!freebsd + +package fs + +import ( + "fmt" + "os" + "syscall" + "time" +) + +// extendedStat extracts info into an ExtendedFileInfo for unix based operating systems. +func extendedStat(fi os.FileInfo) ExtendedFileInfo { + s, ok := fi.Sys().(*syscall.Stat_t) + if !ok { + panic(fmt.Sprintf("conversion to syscall.Stat_t failed, type is %T", fi.Sys())) + } + + extFI := ExtendedFileInfo{ + FileInfo: fi, + DeviceID: uint64(s.Dev), + Inode: s.Ino, + Links: uint64(s.Nlink), + UID: s.Uid, + GID: s.Gid, + Device: uint64(s.Rdev), + BlockSize: int64(s.Blksize), + Blocks: s.Blocks, + Size: s.Size, + + AccessTime: time.Unix(s.Atim.Unix()), + ModTime: time.Unix(s.Mtim.Unix()), + } + + return extFI +} diff --git a/internal/fs/stat_windows.go b/internal/fs/stat_windows.go new file mode 100644 index 000000000..16f9fe0eb --- /dev/null +++ b/internal/fs/stat_windows.go @@ -0,0 +1,31 @@ +// +build windows + +package fs + +import ( + "fmt" + "os" + "syscall" + "time" +) + +// extendedStat extracts info into an ExtendedFileInfo for Windows. +func extendedStat(fi os.FileInfo) ExtendedFileInfo { + s, ok := fi.Sys().(*syscall.Win32FileAttributeData) + if !ok { + panic(fmt.Sprintf("conversion to syscall.Win32FileAttributeData failed, type is %T", fi.Sys())) + } + + extFI := ExtendedFileInfo{ + FileInfo: fi, + Size: int64(s.FileSizeLow) + int64(s.FileSizeHigh)<<32, + } + + atime := syscall.NsecToTimespec(s.LastAccessTime.Nanoseconds()) + extFI.AccessTime = time.Unix(atime.Unix()) + + mtime := syscall.NsecToTimespec(s.LastWriteTime.Nanoseconds()) + extFI.ModTime = time.Unix(mtime.Unix()) + + return extFI +} From b6f98bdb026812c3cf21796e2fad4fd61770d3e5 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sat, 31 Mar 2018 13:22:36 +0200 Subject: [PATCH 13/30] node: Fill minimal info --- internal/restic/node.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/restic/node.go b/internal/restic/node.go index 33e5285a8..8cc94be94 100644 --- a/internal/restic/node.go +++ b/internal/restic/node.go @@ -635,6 +635,10 @@ func lookupGroup(gid string) (string, error) { func (node *Node) fillExtra(path string, fi os.FileInfo) error { stat, ok := toStatT(fi.Sys()) if !ok { + // fill minimal info with current values for uid, gid + node.UID = uint32(os.Getuid()) + node.GID = uint32(os.Getgid()) + node.ChangeTime = node.ModTime return nil } From 4c00efd4bf844d1774932c212359bcc498458802 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sat, 23 Dec 2017 13:02:03 +0100 Subject: [PATCH 14/30] Vendor go-cmp --- Gopkg.lock | 6 + vendor/github.com/google/go-cmp/.travis.yml | 18 + .../github.com/google/go-cmp/CONTRIBUTING.md | 23 + vendor/github.com/google/go-cmp/LICENSE | 27 + vendor/github.com/google/go-cmp/README.md | 44 + .../google/go-cmp/cmp/cmpopts/equate.go | 89 + .../google/go-cmp/cmp/cmpopts/ignore.go | 148 ++ .../google/go-cmp/cmp/cmpopts/sort.go | 146 ++ .../google/go-cmp/cmp/cmpopts/sort_go17.go | 46 + .../google/go-cmp/cmp/cmpopts/sort_go18.go | 31 + .../go-cmp/cmp/cmpopts/struct_filter.go | 182 ++ .../google/go-cmp/cmp/cmpopts/util_test.go | 996 +++++++++ .../github.com/google/go-cmp/cmp/compare.go | 529 +++++ .../google/go-cmp/cmp/compare_test.go | 1795 +++++++++++++++++ .../google/go-cmp/cmp/example_test.go | 374 ++++ .../go-cmp/cmp/internal/diff/debug_disable.go | 17 + .../go-cmp/cmp/internal/diff/debug_enable.go | 122 ++ .../google/go-cmp/cmp/internal/diff/diff.go | 373 ++++ .../go-cmp/cmp/internal/diff/diff_test.go | 467 +++++ .../go-cmp/cmp/internal/function/func.go | 49 + .../go-cmp/cmp/internal/testprotos/protos.go | 116 ++ .../cmp/internal/teststructs/project1.go | 267 +++ .../cmp/internal/teststructs/project2.go | 74 + .../cmp/internal/teststructs/project3.go | 77 + .../cmp/internal/teststructs/project4.go | 142 ++ .../cmp/internal/teststructs/structs.go | 197 ++ .../go-cmp/cmp/internal/value/format.go | 259 +++ .../go-cmp/cmp/internal/value/format_test.go | 91 + .../google/go-cmp/cmp/internal/value/sort.go | 111 + .../go-cmp/cmp/internal/value/sort_test.go | 152 ++ .../github.com/google/go-cmp/cmp/options.go | 446 ++++ .../google/go-cmp/cmp/options_test.go | 231 +++ vendor/github.com/google/go-cmp/cmp/path.go | 293 +++ .../github.com/google/go-cmp/cmp/reporter.go | 53 + .../google/go-cmp/cmp/unsafe_panic.go | 15 + .../google/go-cmp/cmp/unsafe_reflect.go | 23 + 36 files changed, 8029 insertions(+) create mode 100644 vendor/github.com/google/go-cmp/.travis.yml create mode 100644 vendor/github.com/google/go-cmp/CONTRIBUTING.md create mode 100644 vendor/github.com/google/go-cmp/LICENSE create mode 100644 vendor/github.com/google/go-cmp/README.md create mode 100644 vendor/github.com/google/go-cmp/cmp/cmpopts/equate.go create mode 100644 vendor/github.com/google/go-cmp/cmp/cmpopts/ignore.go create mode 100644 vendor/github.com/google/go-cmp/cmp/cmpopts/sort.go create mode 100644 vendor/github.com/google/go-cmp/cmp/cmpopts/sort_go17.go create mode 100644 vendor/github.com/google/go-cmp/cmp/cmpopts/sort_go18.go create mode 100644 vendor/github.com/google/go-cmp/cmp/cmpopts/struct_filter.go create mode 100644 vendor/github.com/google/go-cmp/cmp/cmpopts/util_test.go create mode 100644 vendor/github.com/google/go-cmp/cmp/compare.go create mode 100644 vendor/github.com/google/go-cmp/cmp/compare_test.go create mode 100644 vendor/github.com/google/go-cmp/cmp/example_test.go create mode 100644 vendor/github.com/google/go-cmp/cmp/internal/diff/debug_disable.go create mode 100644 vendor/github.com/google/go-cmp/cmp/internal/diff/debug_enable.go create mode 100644 vendor/github.com/google/go-cmp/cmp/internal/diff/diff.go create mode 100644 vendor/github.com/google/go-cmp/cmp/internal/diff/diff_test.go create mode 100644 vendor/github.com/google/go-cmp/cmp/internal/function/func.go create mode 100644 vendor/github.com/google/go-cmp/cmp/internal/testprotos/protos.go create mode 100644 vendor/github.com/google/go-cmp/cmp/internal/teststructs/project1.go create mode 100644 vendor/github.com/google/go-cmp/cmp/internal/teststructs/project2.go create mode 100644 vendor/github.com/google/go-cmp/cmp/internal/teststructs/project3.go create mode 100644 vendor/github.com/google/go-cmp/cmp/internal/teststructs/project4.go create mode 100644 vendor/github.com/google/go-cmp/cmp/internal/teststructs/structs.go create mode 100644 vendor/github.com/google/go-cmp/cmp/internal/value/format.go create mode 100644 vendor/github.com/google/go-cmp/cmp/internal/value/format_test.go create mode 100644 vendor/github.com/google/go-cmp/cmp/internal/value/sort.go create mode 100644 vendor/github.com/google/go-cmp/cmp/internal/value/sort_test.go create mode 100644 vendor/github.com/google/go-cmp/cmp/options.go create mode 100644 vendor/github.com/google/go-cmp/cmp/options_test.go create mode 100644 vendor/github.com/google/go-cmp/cmp/path.go create mode 100644 vendor/github.com/google/go-cmp/cmp/reporter.go create mode 100644 vendor/github.com/google/go-cmp/cmp/unsafe_panic.go create mode 100644 vendor/github.com/google/go-cmp/cmp/unsafe_reflect.go diff --git a/Gopkg.lock b/Gopkg.lock index 49e3a8518..ccbe4f7c8 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -67,6 +67,12 @@ revision = "925541529c1fa6821df4e44ce2723319eb2be768" version = "v1.0.0" +[[projects]] + name = "github.com/google/go-cmp" + packages = ["cmp","cmp/internal/diff","cmp/internal/function","cmp/internal/value"] + revision = "8099a9787ce5dc5984ed879a3bda47dc730a8e97" + version = "v0.1.0" + [[projects]] name = "github.com/inconshreveable/mousetrap" packages = ["."] diff --git a/vendor/github.com/google/go-cmp/.travis.yml b/vendor/github.com/google/go-cmp/.travis.yml new file mode 100644 index 000000000..9d9b7f916 --- /dev/null +++ b/vendor/github.com/google/go-cmp/.travis.yml @@ -0,0 +1,18 @@ +sudo: false +language: go +go: + - 1.x + - master +matrix: + include: + - go: 1.6.x + script: go test -v -race ./... + allow_failures: + - go: master + fast_finish: true +install: + - # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (it is intended for this package to have no dependencies other than the standard library). +script: + - diff -u <(echo -n) <(gofmt -d -s .) + - go tool vet . + - go test -v -race ./... diff --git a/vendor/github.com/google/go-cmp/CONTRIBUTING.md b/vendor/github.com/google/go-cmp/CONTRIBUTING.md new file mode 100644 index 000000000..ae319c70a --- /dev/null +++ b/vendor/github.com/google/go-cmp/CONTRIBUTING.md @@ -0,0 +1,23 @@ +# How to Contribute + +We'd love to accept your patches and contributions to this project. There are +just a few small guidelines you need to follow. + +## Contributor License Agreement + +Contributions to this project must be accompanied by a Contributor License +Agreement. You (or your employer) retain the copyright to your contribution, +this simply gives us permission to use and redistribute your contributions as +part of the project. Head over to to see +your current agreements on file or to sign a new one. + +You generally only need to submit a CLA once, so if you've already submitted one +(even if it was for a different project), you probably don't need to do it +again. + +## Code reviews + +All submissions, including submissions by project members, require review. We +use GitHub pull requests for this purpose. Consult +[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more +information on using pull requests. diff --git a/vendor/github.com/google/go-cmp/LICENSE b/vendor/github.com/google/go-cmp/LICENSE new file mode 100644 index 000000000..32017f8fa --- /dev/null +++ b/vendor/github.com/google/go-cmp/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2017 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/google/go-cmp/README.md b/vendor/github.com/google/go-cmp/README.md new file mode 100644 index 000000000..d82f10bfc --- /dev/null +++ b/vendor/github.com/google/go-cmp/README.md @@ -0,0 +1,44 @@ +# Package for equality of Go values + +[![GoDoc](https://godoc.org/github.com/google/go-cmp/cmp?status.svg)][godoc] +[![Build Status](https://travis-ci.org/google/go-cmp.svg?branch=master)][travis] + +This package is intended to be a more powerful and safer alternative to +`reflect.DeepEqual` for comparing whether two values are semantically equal. + +The primary features of `cmp` are: + +* When the default behavior of equality does not suit the needs of the test, + custom equality functions can override the equality operation. + For example, an equality function may report floats as equal so long as they + are within some tolerance of each other. + +* Types that have an `Equal` method may use that method to determine equality. + This allows package authors to determine the equality operation for the types + that they define. + +* If no custom equality functions are used and no `Equal` method is defined, + equality is determined by recursively comparing the primitive kinds on both + values, much like `reflect.DeepEqual`. Unlike `reflect.DeepEqual`, unexported + fields are not compared by default; they result in panics unless suppressed + by using an `Ignore` option (see `cmpopts.IgnoreUnexported`) or explictly + compared using the `AllowUnexported` option. + +See the [GoDoc documentation][godoc] for more information. + +This is not an official Google product. + +[godoc]: https://godoc.org/github.com/google/go-cmp/cmp +[travis]: https://travis-ci.org/google/go-cmp + +## Install + +``` +go get -u github.com/google/go-cmp/cmp +``` + +## License + +BSD - See [LICENSE][license] file + +[license]: https://github.com/google/go-cmp/blob/master/LICENSE diff --git a/vendor/github.com/google/go-cmp/cmp/cmpopts/equate.go b/vendor/github.com/google/go-cmp/cmp/cmpopts/equate.go new file mode 100644 index 000000000..cc39492cf --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/cmpopts/equate.go @@ -0,0 +1,89 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +// Package cmpopts provides common options for the cmp package. +package cmpopts + +import ( + "math" + "reflect" + + "github.com/google/go-cmp/cmp" +) + +func equateAlways(_, _ interface{}) bool { return true } + +// EquateEmpty returns a Comparer option that determines all maps and slices +// with a length of zero to be equal, regardless of whether they are nil. +// +// EquateEmpty can be used in conjuction with SortSlices and SortMaps. +func EquateEmpty() cmp.Option { + return cmp.FilterValues(isEmpty, cmp.Comparer(equateAlways)) +} + +func isEmpty(x, y interface{}) bool { + vx, vy := reflect.ValueOf(x), reflect.ValueOf(y) + return (x != nil && y != nil && vx.Type() == vy.Type()) && + (vx.Kind() == reflect.Slice || vx.Kind() == reflect.Map) && + (vx.Len() == 0 && vy.Len() == 0) +} + +// EquateApprox returns a Comparer option that determines float32 or float64 +// values to be equal if they are within a relative fraction or absolute margin. +// This option is not used when either x or y is NaN or infinite. +// +// The fraction determines that the difference of two values must be within the +// smaller fraction of the two values, while the margin determines that the two +// values must be within some absolute margin. +// To express only a fraction or only a margin, use 0 for the other parameter. +// The fraction and margin must be non-negative. +// +// The mathematical expression used is equivalent to: +// |x-y| ≤ max(fraction*min(|x|, |y|), margin) +// +// EquateApprox can be used in conjuction with EquateNaNs. +func EquateApprox(fraction, margin float64) cmp.Option { + if margin < 0 || fraction < 0 || math.IsNaN(margin) || math.IsNaN(fraction) { + panic("margin or fraction must be a non-negative number") + } + a := approximator{fraction, margin} + return cmp.Options{ + cmp.FilterValues(areRealF64s, cmp.Comparer(a.compareF64)), + cmp.FilterValues(areRealF32s, cmp.Comparer(a.compareF32)), + } +} + +type approximator struct{ frac, marg float64 } + +func areRealF64s(x, y float64) bool { + return !math.IsNaN(x) && !math.IsNaN(y) && !math.IsInf(x, 0) && !math.IsInf(y, 0) +} +func areRealF32s(x, y float32) bool { + return areRealF64s(float64(x), float64(y)) +} +func (a approximator) compareF64(x, y float64) bool { + relMarg := a.frac * math.Min(math.Abs(x), math.Abs(y)) + return math.Abs(x-y) <= math.Max(a.marg, relMarg) +} +func (a approximator) compareF32(x, y float32) bool { + return a.compareF64(float64(x), float64(y)) +} + +// EquateNaNs returns a Comparer option that determines float32 and float64 +// NaN values to be equal. +// +// EquateNaNs can be used in conjuction with EquateApprox. +func EquateNaNs() cmp.Option { + return cmp.Options{ + cmp.FilterValues(areNaNsF64s, cmp.Comparer(equateAlways)), + cmp.FilterValues(areNaNsF32s, cmp.Comparer(equateAlways)), + } +} + +func areNaNsF64s(x, y float64) bool { + return math.IsNaN(x) && math.IsNaN(y) +} +func areNaNsF32s(x, y float32) bool { + return areNaNsF64s(float64(x), float64(y)) +} diff --git a/vendor/github.com/google/go-cmp/cmp/cmpopts/ignore.go b/vendor/github.com/google/go-cmp/cmp/cmpopts/ignore.go new file mode 100644 index 000000000..016891da3 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/cmpopts/ignore.go @@ -0,0 +1,148 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +package cmpopts + +import ( + "fmt" + "reflect" + "unicode" + "unicode/utf8" + + "github.com/google/go-cmp/cmp" +) + +// IgnoreFields returns an Option that ignores exported fields of the +// given names on a single struct type. +// The struct type is specified by passing in a value of that type. +// +// The name may be a dot-delimited string (e.g., "Foo.Bar") to ignore a +// specific sub-field that is embedded or nested within the parent struct. +// +// This does not handle unexported fields; use IgnoreUnexported instead. +func IgnoreFields(typ interface{}, names ...string) cmp.Option { + sf := newStructFilter(typ, names...) + return cmp.FilterPath(sf.filter, cmp.Ignore()) +} + +// IgnoreTypes returns an Option that ignores all values assignable to +// certain types, which are specified by passing in a value of each type. +func IgnoreTypes(typs ...interface{}) cmp.Option { + tf := newTypeFilter(typs...) + return cmp.FilterPath(tf.filter, cmp.Ignore()) +} + +type typeFilter []reflect.Type + +func newTypeFilter(typs ...interface{}) (tf typeFilter) { + for _, typ := range typs { + t := reflect.TypeOf(typ) + if t == nil { + // This occurs if someone tries to pass in sync.Locker(nil) + panic("cannot determine type; consider using IgnoreInterfaces") + } + tf = append(tf, t) + } + return tf +} +func (tf typeFilter) filter(p cmp.Path) bool { + if len(p) < 1 { + return false + } + t := p[len(p)-1].Type() + for _, ti := range tf { + if t.AssignableTo(ti) { + return true + } + } + return false +} + +// IgnoreInterfaces returns an Option that ignores all values or references of +// values assignable to certain interface types. These interfaces are specified +// by passing in an anonymous struct with the interface types embedded in it. +// For example, to ignore sync.Locker, pass in struct{sync.Locker}{}. +func IgnoreInterfaces(ifaces interface{}) cmp.Option { + tf := newIfaceFilter(ifaces) + return cmp.FilterPath(tf.filter, cmp.Ignore()) +} + +type ifaceFilter []reflect.Type + +func newIfaceFilter(ifaces interface{}) (tf ifaceFilter) { + t := reflect.TypeOf(ifaces) + if ifaces == nil || t.Name() != "" || t.Kind() != reflect.Struct { + panic("input must be an anonymous struct") + } + for i := 0; i < t.NumField(); i++ { + fi := t.Field(i) + switch { + case !fi.Anonymous: + panic("struct cannot have named fields") + case fi.Type.Kind() != reflect.Interface: + panic("embedded field must be an interface type") + case fi.Type.NumMethod() == 0: + // This matches everything; why would you ever want this? + panic("cannot ignore empty interface") + default: + tf = append(tf, fi.Type) + } + } + return tf +} +func (tf ifaceFilter) filter(p cmp.Path) bool { + if len(p) < 1 { + return false + } + t := p[len(p)-1].Type() + for _, ti := range tf { + if t.AssignableTo(ti) { + return true + } + if t.Kind() != reflect.Ptr && reflect.PtrTo(t).AssignableTo(ti) { + return true + } + } + return false +} + +// IgnoreUnexported returns an Option that only ignores the immediate unexported +// fields of a struct, including anonymous fields of unexported types. +// In particular, unexported fields within the struct's exported fields +// of struct types, including anonymous fields, will not be ignored unless the +// type of the field itself is also passed to IgnoreUnexported. +func IgnoreUnexported(typs ...interface{}) cmp.Option { + ux := newUnexportedFilter(typs...) + return cmp.FilterPath(ux.filter, cmp.Ignore()) +} + +type unexportedFilter struct{ m map[reflect.Type]bool } + +func newUnexportedFilter(typs ...interface{}) unexportedFilter { + ux := unexportedFilter{m: make(map[reflect.Type]bool)} + for _, typ := range typs { + t := reflect.TypeOf(typ) + if t == nil || t.Kind() != reflect.Struct { + panic(fmt.Sprintf("invalid struct type: %T", typ)) + } + ux.m[t] = true + } + return ux +} +func (xf unexportedFilter) filter(p cmp.Path) bool { + if len(p) < 2 { + return false + } + sf, ok := p[len(p)-1].(cmp.StructField) + if !ok { + return false + } + return xf.m[p[len(p)-2].Type()] && !isExported(sf.Name()) +} + +// isExported reports whether the identifier is exported. +func isExported(id string) bool { + r, _ := utf8.DecodeRuneInString(id) + return unicode.IsUpper(r) +} diff --git a/vendor/github.com/google/go-cmp/cmp/cmpopts/sort.go b/vendor/github.com/google/go-cmp/cmp/cmpopts/sort.go new file mode 100644 index 000000000..a566d240b --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/cmpopts/sort.go @@ -0,0 +1,146 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +package cmpopts + +import ( + "fmt" + "reflect" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/internal/function" +) + +// SortSlices returns a Transformer option that sorts all []V. +// The less function must be of the form "func(T, T) bool" which is used to +// sort any slice with element type V that is assignable to T. +// +// The less function must be: +// • Deterministic: less(x, y) == less(x, y) +// • Irreflexive: !less(x, x) +// • Transitive: if !less(x, y) and !less(y, z), then !less(x, z) +// +// The less function does not have to be "total". That is, if !less(x, y) and +// !less(y, x) for two elements x and y, their relative order is maintained. +// +// SortSlices can be used in conjuction with EquateEmpty. +func SortSlices(less interface{}) cmp.Option { + vf := reflect.ValueOf(less) + if !function.IsType(vf.Type(), function.Less) || vf.IsNil() { + panic(fmt.Sprintf("invalid less function: %T", less)) + } + ss := sliceSorter{vf.Type().In(0), vf} + return cmp.FilterValues(ss.filter, cmp.Transformer("Sort", ss.sort)) +} + +type sliceSorter struct { + in reflect.Type // T + fnc reflect.Value // func(T, T) bool +} + +func (ss sliceSorter) filter(x, y interface{}) bool { + vx, vy := reflect.ValueOf(x), reflect.ValueOf(y) + if !(x != nil && y != nil && vx.Type() == vy.Type()) || + !(vx.Kind() == reflect.Slice && vx.Type().Elem().AssignableTo(ss.in)) || + (vx.Len() <= 1 && vy.Len() <= 1) { + return false + } + // Check whether the slices are already sorted to avoid an infinite + // recursion cycle applying the same transform to itself. + ok1 := sliceIsSorted(x, func(i, j int) bool { return ss.less(vx, i, j) }) + ok2 := sliceIsSorted(y, func(i, j int) bool { return ss.less(vy, i, j) }) + return !ok1 || !ok2 +} +func (ss sliceSorter) sort(x interface{}) interface{} { + src := reflect.ValueOf(x) + dst := reflect.MakeSlice(src.Type(), src.Len(), src.Len()) + for i := 0; i < src.Len(); i++ { + dst.Index(i).Set(src.Index(i)) + } + sortSliceStable(dst.Interface(), func(i, j int) bool { return ss.less(dst, i, j) }) + ss.checkSort(dst) + return dst.Interface() +} +func (ss sliceSorter) checkSort(v reflect.Value) { + start := -1 // Start of a sequence of equal elements. + for i := 1; i < v.Len(); i++ { + if ss.less(v, i-1, i) { + // Check that first and last elements in v[start:i] are equal. + if start >= 0 && (ss.less(v, start, i-1) || ss.less(v, i-1, start)) { + panic(fmt.Sprintf("incomparable values detected: want equal elements: %v", v.Slice(start, i))) + } + start = -1 + } else if start == -1 { + start = i + } + } +} +func (ss sliceSorter) less(v reflect.Value, i, j int) bool { + vx, vy := v.Index(i), v.Index(j) + return ss.fnc.Call([]reflect.Value{vx, vy})[0].Bool() +} + +// SortMaps returns a Transformer option that flattens map[K]V types to be a +// sorted []struct{K, V}. The less function must be of the form +// "func(T, T) bool" which is used to sort any map with key K that is +// assignable to T. +// +// Flattening the map into a slice has the property that cmp.Equal is able to +// use Comparers on K or the K.Equal method if it exists. +// +// The less function must be: +// • Deterministic: less(x, y) == less(x, y) +// • Irreflexive: !less(x, x) +// • Transitive: if !less(x, y) and !less(y, z), then !less(x, z) +// • Total: if x != y, then either less(x, y) or less(y, x) +// +// SortMaps can be used in conjuction with EquateEmpty. +func SortMaps(less interface{}) cmp.Option { + vf := reflect.ValueOf(less) + if !function.IsType(vf.Type(), function.Less) || vf.IsNil() { + panic(fmt.Sprintf("invalid less function: %T", less)) + } + ms := mapSorter{vf.Type().In(0), vf} + return cmp.FilterValues(ms.filter, cmp.Transformer("Sort", ms.sort)) +} + +type mapSorter struct { + in reflect.Type // T + fnc reflect.Value // func(T, T) bool +} + +func (ms mapSorter) filter(x, y interface{}) bool { + vx, vy := reflect.ValueOf(x), reflect.ValueOf(y) + return (x != nil && y != nil && vx.Type() == vy.Type()) && + (vx.Kind() == reflect.Map && vx.Type().Key().AssignableTo(ms.in)) && + (vx.Len() != 0 || vy.Len() != 0) +} +func (ms mapSorter) sort(x interface{}) interface{} { + src := reflect.ValueOf(x) + outType := mapEntryType(src.Type()) + dst := reflect.MakeSlice(reflect.SliceOf(outType), src.Len(), src.Len()) + for i, k := range src.MapKeys() { + v := reflect.New(outType).Elem() + v.Field(0).Set(k) + v.Field(1).Set(src.MapIndex(k)) + dst.Index(i).Set(v) + } + sortSlice(dst.Interface(), func(i, j int) bool { return ms.less(dst, i, j) }) + ms.checkSort(dst) + return dst.Interface() +} +func (ms mapSorter) checkSort(v reflect.Value) { + for i := 1; i < v.Len(); i++ { + if !ms.less(v, i-1, i) { + panic(fmt.Sprintf("partial order detected: want %v < %v", v.Index(i-1), v.Index(i))) + } + } +} +func (ms mapSorter) less(v reflect.Value, i, j int) bool { + vx, vy := v.Index(i).Field(0), v.Index(j).Field(0) + if !hasReflectStructOf { + vx, vy = vx.Elem(), vy.Elem() + } + return ms.fnc.Call([]reflect.Value{vx, vy})[0].Bool() +} diff --git a/vendor/github.com/google/go-cmp/cmp/cmpopts/sort_go17.go b/vendor/github.com/google/go-cmp/cmp/cmpopts/sort_go17.go new file mode 100644 index 000000000..839b88ca4 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/cmpopts/sort_go17.go @@ -0,0 +1,46 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +// +build !go1.8 + +package cmpopts + +import ( + "reflect" + "sort" +) + +const hasReflectStructOf = false + +func mapEntryType(reflect.Type) reflect.Type { + return reflect.TypeOf(struct{ K, V interface{} }{}) +} + +func sliceIsSorted(slice interface{}, less func(i, j int) bool) bool { + return sort.IsSorted(reflectSliceSorter{reflect.ValueOf(slice), less}) +} +func sortSlice(slice interface{}, less func(i, j int) bool) { + sort.Sort(reflectSliceSorter{reflect.ValueOf(slice), less}) +} +func sortSliceStable(slice interface{}, less func(i, j int) bool) { + sort.Stable(reflectSliceSorter{reflect.ValueOf(slice), less}) +} + +type reflectSliceSorter struct { + slice reflect.Value + less func(i, j int) bool +} + +func (ss reflectSliceSorter) Len() int { + return ss.slice.Len() +} +func (ss reflectSliceSorter) Less(i, j int) bool { + return ss.less(i, j) +} +func (ss reflectSliceSorter) Swap(i, j int) { + vi := ss.slice.Index(i).Interface() + vj := ss.slice.Index(j).Interface() + ss.slice.Index(i).Set(reflect.ValueOf(vj)) + ss.slice.Index(j).Set(reflect.ValueOf(vi)) +} diff --git a/vendor/github.com/google/go-cmp/cmp/cmpopts/sort_go18.go b/vendor/github.com/google/go-cmp/cmp/cmpopts/sort_go18.go new file mode 100644 index 000000000..8a59c0d38 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/cmpopts/sort_go18.go @@ -0,0 +1,31 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +// +build go1.8 + +package cmpopts + +import ( + "reflect" + "sort" +) + +const hasReflectStructOf = true + +func mapEntryType(t reflect.Type) reflect.Type { + return reflect.StructOf([]reflect.StructField{ + {Name: "K", Type: t.Key()}, + {Name: "V", Type: t.Elem()}, + }) +} + +func sliceIsSorted(slice interface{}, less func(i, j int) bool) bool { + return sort.SliceIsSorted(slice, less) +} +func sortSlice(slice interface{}, less func(i, j int) bool) { + sort.Slice(slice, less) +} +func sortSliceStable(slice interface{}, less func(i, j int) bool) { + sort.SliceStable(slice, less) +} diff --git a/vendor/github.com/google/go-cmp/cmp/cmpopts/struct_filter.go b/vendor/github.com/google/go-cmp/cmp/cmpopts/struct_filter.go new file mode 100644 index 000000000..97f707983 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/cmpopts/struct_filter.go @@ -0,0 +1,182 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +package cmpopts + +import ( + "fmt" + "reflect" + "strings" + + "github.com/google/go-cmp/cmp" +) + +// filterField returns a new Option where opt is only evaluated on paths that +// include a specific exported field on a single struct type. +// The struct type is specified by passing in a value of that type. +// +// The name may be a dot-delimited string (e.g., "Foo.Bar") to select a +// specific sub-field that is embedded or nested within the parent struct. +func filterField(typ interface{}, name string, opt cmp.Option) cmp.Option { + // TODO: This is currently unexported over concerns of how helper filters + // can be composed together easily. + // TODO: Add tests for FilterField. + + sf := newStructFilter(typ, name) + return cmp.FilterPath(sf.filter, opt) +} + +type structFilter struct { + t reflect.Type // The root struct type to match on + ft fieldTree // Tree of fields to match on +} + +func newStructFilter(typ interface{}, names ...string) structFilter { + // TODO: Perhaps allow * as a special identifier to allow ignoring any + // number of path steps until the next field match? + // This could be useful when a concrete struct gets transformed into + // an anonymous struct where it is not possible to specify that by type, + // but the transformer happens to provide guarantees about the names of + // the transformed fields. + + t := reflect.TypeOf(typ) + if t == nil || t.Kind() != reflect.Struct { + panic(fmt.Sprintf("%T must be a struct", typ)) + } + var ft fieldTree + for _, name := range names { + cname, err := canonicalName(t, name) + if err != nil { + panic(fmt.Sprintf("%s: %v", strings.Join(cname, "."), err)) + } + ft.insert(cname) + } + return structFilter{t, ft} +} + +func (sf structFilter) filter(p cmp.Path) bool { + for i, ps := range p { + if ps.Type().AssignableTo(sf.t) && sf.ft.matchPrefix(p[i+1:]) { + return true + } + } + return false +} + +// fieldTree represents a set of dot-separated identifiers. +// +// For example, inserting the following selectors: +// Foo +// Foo.Bar.Baz +// Foo.Buzz +// Nuka.Cola.Quantum +// +// Results in a tree of the form: +// {sub: { +// "Foo": {ok: true, sub: { +// "Bar": {sub: { +// "Baz": {ok: true}, +// }}, +// "Buzz": {ok: true}, +// }}, +// "Nuka": {sub: { +// "Cola": {sub: { +// "Quantum": {ok: true}, +// }}, +// }}, +// }} +type fieldTree struct { + ok bool // Whether this is a specified node + sub map[string]fieldTree // The sub-tree of fields under this node +} + +// insert inserts a sequence of field accesses into the tree. +func (ft *fieldTree) insert(cname []string) { + if ft.sub == nil { + ft.sub = make(map[string]fieldTree) + } + if len(cname) == 0 { + ft.ok = true + return + } + sub := ft.sub[cname[0]] + sub.insert(cname[1:]) + ft.sub[cname[0]] = sub +} + +// matchPrefix reports whether any selector in the fieldTree matches +// the start of path p. +func (ft fieldTree) matchPrefix(p cmp.Path) bool { + for _, ps := range p { + switch ps := ps.(type) { + case cmp.StructField: + ft = ft.sub[ps.Name()] + if ft.ok { + return true + } + if len(ft.sub) == 0 { + return false + } + case cmp.Indirect: + default: + return false + } + } + return false +} + +// canonicalName returns a list of identifiers where any struct field access +// through an embedded field is expanded to include the names of the embedded +// types themselves. +// +// For example, suppose field "Foo" is not directly in the parent struct, +// but actually from an embedded struct of type "Bar". Then, the canonical name +// of "Foo" is actually "Bar.Foo". +// +// Suppose field "Foo" is not directly in the parent struct, but actually +// a field in two different embedded structs of types "Bar" and "Baz". +// Then the selector "Foo" causes a panic since it is ambiguous which one it +// refers to. The user must specify either "Bar.Foo" or "Baz.Foo". +func canonicalName(t reflect.Type, sel string) ([]string, error) { + var name string + sel = strings.TrimPrefix(sel, ".") + if sel == "" { + return nil, fmt.Errorf("name must not be empty") + } + if i := strings.IndexByte(sel, '.'); i < 0 { + name, sel = sel, "" + } else { + name, sel = sel[:i], sel[i:] + } + + // Type must be a struct or pointer to struct. + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + if t.Kind() != reflect.Struct { + return nil, fmt.Errorf("%v must be a struct", t) + } + + // Find the canonical name for this current field name. + // If the field exists in an embedded struct, then it will be expanded. + if !isExported(name) { + // Disallow unexported fields: + // * To discourage people from actually touching unexported fields + // * FieldByName is buggy (https://golang.org/issue/4876) + return []string{name}, fmt.Errorf("name must be exported") + } + sf, ok := t.FieldByName(name) + if !ok { + return []string{name}, fmt.Errorf("does not exist") + } + var ss []string + for i := range sf.Index { + ss = append(ss, t.FieldByIndex(sf.Index[:i+1]).Name) + } + if sel == "" { + return ss, nil + } + ssPost, err := canonicalName(sf.Type, sel) + return append(ss, ssPost...), err +} diff --git a/vendor/github.com/google/go-cmp/cmp/cmpopts/util_test.go b/vendor/github.com/google/go-cmp/cmp/cmpopts/util_test.go new file mode 100644 index 000000000..f53278990 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/cmpopts/util_test.go @@ -0,0 +1,996 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +package cmpopts + +import ( + "bytes" + "fmt" + "io" + "math" + "reflect" + "strings" + "sync" + "testing" + "time" + + "github.com/google/go-cmp/cmp" +) + +type ( + MyInt int + MyFloat float32 + MyTime struct{ time.Time } + MyStruct struct { + A, B []int + C, D map[time.Time]string + } + + Foo1 struct{ Alpha, Bravo, Charlie int } + Foo2 struct{ *Foo1 } + Foo3 struct{ *Foo2 } + Bar1 struct{ Foo3 } + Bar2 struct { + Bar1 + *Foo3 + Bravo float32 + } + Bar3 struct { + Bar1 + Bravo *Bar2 + Delta struct{ Echo Foo1 } + *Foo3 + Alpha string + } + + privateStruct struct{ Public, private int } + PublicStruct struct{ Public, private int } + ParentStruct struct { + *privateStruct + *PublicStruct + Public int + private int + } + + Everything struct { + MyInt + MyFloat + MyTime + MyStruct + Bar3 + ParentStruct + } + + EmptyInterface interface{} +) + +func TestOptions(t *testing.T) { + createBar3X := func() *Bar3 { + return &Bar3{ + Bar1: Bar1{Foo3{&Foo2{&Foo1{Bravo: 2}}}}, + Bravo: &Bar2{ + Bar1: Bar1{Foo3{&Foo2{&Foo1{Charlie: 7}}}}, + Foo3: &Foo3{&Foo2{&Foo1{Bravo: 5}}}, + Bravo: 4, + }, + Delta: struct{ Echo Foo1 }{Foo1{Charlie: 3}}, + Foo3: &Foo3{&Foo2{&Foo1{Alpha: 1}}}, + Alpha: "alpha", + } + } + createBar3Y := func() *Bar3 { + return &Bar3{ + Bar1: Bar1{Foo3{&Foo2{&Foo1{Bravo: 3}}}}, + Bravo: &Bar2{ + Bar1: Bar1{Foo3{&Foo2{&Foo1{Charlie: 8}}}}, + Foo3: &Foo3{&Foo2{&Foo1{Bravo: 6}}}, + Bravo: 5, + }, + Delta: struct{ Echo Foo1 }{Foo1{Charlie: 4}}, + Foo3: &Foo3{&Foo2{&Foo1{Alpha: 2}}}, + Alpha: "ALPHA", + } + } + + tests := []struct { + label string // Test name + x, y interface{} // Input values to compare + opts []cmp.Option // Input options + wantEqual bool // Whether the inputs are equal + wantPanic bool // Whether Equal should panic + reason string // The reason for the expected outcome + }{{ + label: "EquateEmpty", + x: []int{}, + y: []int(nil), + wantEqual: false, + reason: "not equal because empty non-nil and nil slice differ", + }, { + label: "EquateEmpty", + x: []int{}, + y: []int(nil), + opts: []cmp.Option{EquateEmpty()}, + wantEqual: true, + reason: "equal because EquateEmpty equates empty slices", + }, { + label: "SortSlices", + x: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, + y: []int{1, 0, 5, 2, 8, 9, 4, 3, 6, 7}, + wantEqual: false, + reason: "not equal because element order differs", + }, { + label: "SortSlices", + x: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, + y: []int{1, 0, 5, 2, 8, 9, 4, 3, 6, 7}, + opts: []cmp.Option{SortSlices(func(x, y int) bool { return x < y })}, + wantEqual: true, + reason: "equal because SortSlices sorts the slices", + }, { + label: "SortSlices", + x: []MyInt{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, + y: []MyInt{1, 0, 5, 2, 8, 9, 4, 3, 6, 7}, + opts: []cmp.Option{SortSlices(func(x, y int) bool { return x < y })}, + wantEqual: false, + reason: "not equal because MyInt is not the same type as int", + }, { + label: "SortSlices", + x: []float64{0, 1, 1, 2, 2, 2}, + y: []float64{2, 0, 2, 1, 2, 1}, + opts: []cmp.Option{SortSlices(func(x, y float64) bool { return x < y })}, + wantEqual: true, + reason: "equal even when sorted with duplicate elements", + }, { + label: "SortSlices", + x: []float64{0, 1, 1, 2, 2, 2, math.NaN(), 3, 3, 3, 3, 4, 4, 4, 4}, + y: []float64{2, 0, 4, 4, 3, math.NaN(), 4, 1, 3, 2, 3, 3, 4, 1, 2}, + opts: []cmp.Option{SortSlices(func(x, y float64) bool { return x < y })}, + wantPanic: true, + reason: "panics because SortSlices used with non-transitive less function", + }, { + label: "SortSlices", + x: []float64{0, 1, 1, 2, 2, 2, math.NaN(), 3, 3, 3, 3, 4, 4, 4, 4}, + y: []float64{2, 0, 4, 4, 3, math.NaN(), 4, 1, 3, 2, 3, 3, 4, 1, 2}, + opts: []cmp.Option{SortSlices(func(x, y float64) bool { + return (!math.IsNaN(x) && math.IsNaN(y)) || x < y + })}, + wantEqual: false, + reason: "no panics because SortSlices used with valid less function; not equal because NaN != NaN", + }, { + label: "SortSlices+EquateNaNs", + x: []float64{0, 1, 1, 2, 2, 2, math.NaN(), 3, 3, 3, math.NaN(), 3, 4, 4, 4, 4}, + y: []float64{2, 0, 4, 4, 3, math.NaN(), 4, 1, 3, 2, 3, 3, 4, 1, math.NaN(), 2}, + opts: []cmp.Option{ + EquateNaNs(), + SortSlices(func(x, y float64) bool { + return (!math.IsNaN(x) && math.IsNaN(y)) || x < y + }), + }, + wantEqual: true, + reason: "no panics because SortSlices used with valid less function; equal because EquateNaNs is used", + }, { + label: "SortMaps", + x: map[time.Time]string{ + time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC): "0th birthday", + time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC): "1st birthday", + time.Date(2011, time.November, 10, 23, 0, 0, 0, time.UTC): "2nd birthday", + }, + y: map[time.Time]string{ + time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "0th birthday", + time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "1st birthday", + time.Date(2011, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "2nd birthday", + }, + wantEqual: false, + reason: "not equal because timezones differ", + }, { + label: "SortMaps", + x: map[time.Time]string{ + time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC): "0th birthday", + time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC): "1st birthday", + time.Date(2011, time.November, 10, 23, 0, 0, 0, time.UTC): "2nd birthday", + }, + y: map[time.Time]string{ + time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "0th birthday", + time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "1st birthday", + time.Date(2011, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "2nd birthday", + }, + opts: []cmp.Option{SortMaps(func(x, y time.Time) bool { return x.Before(y) })}, + wantEqual: true, + reason: "equal because SortMaps flattens to a slice where Time.Equal can be used", + }, { + label: "SortMaps", + x: map[MyTime]string{ + {time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)}: "0th birthday", + {time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC)}: "1st birthday", + {time.Date(2011, time.November, 10, 23, 0, 0, 0, time.UTC)}: "2nd birthday", + }, + y: map[MyTime]string{ + {time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local)}: "0th birthday", + {time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local)}: "1st birthday", + {time.Date(2011, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local)}: "2nd birthday", + }, + opts: []cmp.Option{SortMaps(func(x, y time.Time) bool { return x.Before(y) })}, + wantEqual: false, + reason: "not equal because MyTime is not assignable to time.Time", + }, { + label: "SortMaps", + x: map[int]string{-3: "", -2: "", -1: "", 0: "", 1: "", 2: "", 3: ""}, + // => {0, 1, 2, 3, -1, -2, -3}, + y: map[int]string{300: "", 200: "", 100: "", 0: "", 1: "", 2: "", 3: ""}, + // => {0, 1, 2, 3, 100, 200, 300}, + opts: []cmp.Option{SortMaps(func(a, b int) bool { + if -10 < a && a <= 0 { + a *= -100 + } + if -10 < b && b <= 0 { + b *= -100 + } + return a < b + })}, + wantEqual: false, + reason: "not equal because values differ even though SortMap provides valid ordering", + }, { + label: "SortMaps", + x: map[int]string{-3: "", -2: "", -1: "", 0: "", 1: "", 2: "", 3: ""}, + // => {0, 1, 2, 3, -1, -2, -3}, + y: map[int]string{300: "", 200: "", 100: "", 0: "", 1: "", 2: "", 3: ""}, + // => {0, 1, 2, 3, 100, 200, 300}, + opts: []cmp.Option{ + SortMaps(func(x, y int) bool { + if -10 < x && x <= 0 { + x *= -100 + } + if -10 < y && y <= 0 { + y *= -100 + } + return x < y + }), + cmp.Comparer(func(x, y int) bool { + if -10 < x && x <= 0 { + x *= -100 + } + if -10 < y && y <= 0 { + y *= -100 + } + return x == y + }), + }, + wantEqual: true, + reason: "equal because Comparer used to equate differences", + }, { + label: "SortMaps", + x: map[int]string{-3: "", -2: "", -1: "", 0: "", 1: "", 2: "", 3: ""}, + y: map[int]string{}, + opts: []cmp.Option{SortMaps(func(x, y int) bool { + return x < y && x >= 0 && y >= 0 + })}, + wantPanic: true, + reason: "panics because SortMaps used with non-transitive less function", + }, { + label: "SortMaps", + x: map[int]string{-3: "", -2: "", -1: "", 0: "", 1: "", 2: "", 3: ""}, + y: map[int]string{}, + opts: []cmp.Option{SortMaps(func(x, y int) bool { + return math.Abs(float64(x)) < math.Abs(float64(y)) + })}, + wantPanic: true, + reason: "panics because SortMaps used with partial less function", + }, { + label: "EquateEmpty+SortSlices+SortMaps", + x: MyStruct{ + A: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, + C: map[time.Time]string{ + time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC): "0th birthday", + time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC): "1st birthday", + }, + D: map[time.Time]string{}, + }, + y: MyStruct{ + A: []int{1, 0, 5, 2, 8, 9, 4, 3, 6, 7}, + B: []int{}, + C: map[time.Time]string{ + time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "0th birthday", + time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "1st birthday", + }, + }, + opts: []cmp.Option{ + EquateEmpty(), + SortSlices(func(x, y int) bool { return x < y }), + SortMaps(func(x, y time.Time) bool { return x.Before(y) }), + }, + wantEqual: true, + reason: "no panics because EquateEmpty should compose with the sort options", + }, { + label: "EquateApprox", + x: 3.09, + y: 3.10, + wantEqual: false, + reason: "not equal because floats do not exactly matches", + }, { + label: "EquateApprox", + x: 3.09, + y: 3.10, + opts: []cmp.Option{EquateApprox(0, 0)}, + wantEqual: false, + reason: "not equal because EquateApprox(0 ,0) is equivalent to using ==", + }, { + label: "EquateApprox", + x: 3.09, + y: 3.10, + opts: []cmp.Option{EquateApprox(0.003, 0.009)}, + wantEqual: false, + reason: "not equal because EquateApprox is too strict", + }, { + label: "EquateApprox", + x: 3.09, + y: 3.10, + opts: []cmp.Option{EquateApprox(0, 0.011)}, + wantEqual: true, + reason: "equal because margin is loose enough to match", + }, { + label: "EquateApprox", + x: 3.09, + y: 3.10, + opts: []cmp.Option{EquateApprox(0.004, 0)}, + wantEqual: true, + reason: "equal because fraction is loose enough to match", + }, { + label: "EquateApprox", + x: 3.09, + y: 3.10, + opts: []cmp.Option{EquateApprox(0.004, 0.011)}, + wantEqual: true, + reason: "equal because both the margin and fraction are loose enough to match", + }, { + label: "EquateApprox", + x: float32(3.09), + y: float64(3.10), + opts: []cmp.Option{EquateApprox(0.004, 0)}, + wantEqual: false, + reason: "not equal because the types differ", + }, { + label: "EquateApprox", + x: float32(3.09), + y: float32(3.10), + opts: []cmp.Option{EquateApprox(0.004, 0)}, + wantEqual: true, + reason: "equal because EquateApprox also applies on float32s", + }, { + label: "EquateApprox", + x: []float64{math.Inf(+1), math.Inf(-1)}, + y: []float64{math.Inf(+1), math.Inf(-1)}, + opts: []cmp.Option{EquateApprox(0, 1)}, + wantEqual: true, + reason: "equal because we fall back on == which matches Inf (EquateApprox does not apply on Inf) ", + }, { + label: "EquateApprox", + x: []float64{math.Inf(+1), -1e100}, + y: []float64{+1e100, math.Inf(-1)}, + opts: []cmp.Option{EquateApprox(0, 1)}, + wantEqual: false, + reason: "not equal because we fall back on == where Inf != 1e100 (EquateApprox does not apply on Inf)", + }, { + label: "EquateApprox", + x: float64(+1e100), + y: float64(-1e100), + opts: []cmp.Option{EquateApprox(math.Inf(+1), 0)}, + wantEqual: true, + reason: "equal because infinite fraction matches everything", + }, { + label: "EquateApprox", + x: float64(+1e100), + y: float64(-1e100), + opts: []cmp.Option{EquateApprox(0, math.Inf(+1))}, + wantEqual: true, + reason: "equal because infinite margin matches everything", + }, { + label: "EquateApprox", + x: math.Pi, + y: math.Pi, + opts: []cmp.Option{EquateApprox(0, 0)}, + wantEqual: true, + reason: "equal because EquateApprox(0, 0) is equivalent to ==", + }, { + label: "EquateApprox", + x: math.Pi, + y: math.Nextafter(math.Pi, math.Inf(+1)), + opts: []cmp.Option{EquateApprox(0, 0)}, + wantEqual: false, + reason: "not equal because EquateApprox(0, 0) is equivalent to ==", + }, { + label: "EquateNaNs", + x: []float64{1.0, math.NaN(), math.E, -0.0, +0.0, math.Inf(+1), math.Inf(-1)}, + y: []float64{1.0, math.NaN(), math.E, -0.0, +0.0, math.Inf(+1), math.Inf(-1)}, + wantEqual: false, + reason: "not equal because NaN != NaN", + }, { + label: "EquateNaNs", + x: []float64{1.0, math.NaN(), math.E, -0.0, +0.0, math.Inf(+1), math.Inf(-1)}, + y: []float64{1.0, math.NaN(), math.E, -0.0, +0.0, math.Inf(+1), math.Inf(-1)}, + opts: []cmp.Option{EquateNaNs()}, + wantEqual: true, + reason: "equal because EquateNaNs allows NaN == NaN", + }, { + label: "EquateNaNs", + x: []float32{1.0, float32(math.NaN()), math.E, -0.0, +0.0}, + y: []float32{1.0, float32(math.NaN()), math.E, -0.0, +0.0}, + opts: []cmp.Option{EquateNaNs()}, + wantEqual: true, + reason: "equal because EquateNaNs operates on float32", + }, { + label: "EquateApprox+EquateNaNs", + x: []float64{1.0, math.NaN(), math.E, -0.0, +0.0, math.Inf(+1), math.Inf(-1), 1.01, 5001}, + y: []float64{1.0, math.NaN(), math.E, -0.0, +0.0, math.Inf(+1), math.Inf(-1), 1.02, 5002}, + opts: []cmp.Option{ + EquateNaNs(), + EquateApprox(0.01, 0), + }, + wantEqual: true, + reason: "equal because EquateNaNs and EquateApprox compose together", + }, { + label: "EquateApprox+EquateNaNs", + x: []MyFloat{1.0, MyFloat(math.NaN()), MyFloat(math.E), -0.0, +0.0, MyFloat(math.Inf(+1)), MyFloat(math.Inf(-1)), 1.01, 5001}, + y: []MyFloat{1.0, MyFloat(math.NaN()), MyFloat(math.E), -0.0, +0.0, MyFloat(math.Inf(+1)), MyFloat(math.Inf(-1)), 1.02, 5002}, + opts: []cmp.Option{ + EquateNaNs(), + EquateApprox(0.01, 0), + }, + wantEqual: false, + reason: "not equal because EquateApprox and EquateNaNs do not apply on a named type", + }, { + label: "EquateApprox+EquateNaNs+Transform", + x: []MyFloat{1.0, MyFloat(math.NaN()), MyFloat(math.E), -0.0, +0.0, MyFloat(math.Inf(+1)), MyFloat(math.Inf(-1)), 1.01, 5001}, + y: []MyFloat{1.0, MyFloat(math.NaN()), MyFloat(math.E), -0.0, +0.0, MyFloat(math.Inf(+1)), MyFloat(math.Inf(-1)), 1.02, 5002}, + opts: []cmp.Option{ + cmp.Transformer("", func(x MyFloat) float64 { return float64(x) }), + EquateNaNs(), + EquateApprox(0.01, 0), + }, + wantEqual: true, + reason: "equal because named type is transformed to float64", + }, { + label: "IgnoreFields", + x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}}, + y: Bar1{Foo3{&Foo2{&Foo1{Alpha: 6}}}}, + wantEqual: false, + reason: "not equal because values do not match in deeply embedded field", + }, { + label: "IgnoreFields", + x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}}, + y: Bar1{Foo3{&Foo2{&Foo1{Alpha: 6}}}}, + opts: []cmp.Option{IgnoreFields(Bar1{}, "Alpha")}, + wantEqual: true, + reason: "equal because IgnoreField ignores deeply embedded field: Alpha", + }, { + label: "IgnoreFields", + x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}}, + y: Bar1{Foo3{&Foo2{&Foo1{Alpha: 6}}}}, + opts: []cmp.Option{IgnoreFields(Bar1{}, "Foo1.Alpha")}, + wantEqual: true, + reason: "equal because IgnoreField ignores deeply embedded field: Foo1.Alpha", + }, { + label: "IgnoreFields", + x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}}, + y: Bar1{Foo3{&Foo2{&Foo1{Alpha: 6}}}}, + opts: []cmp.Option{IgnoreFields(Bar1{}, "Foo2.Alpha")}, + wantEqual: true, + reason: "equal because IgnoreField ignores deeply embedded field: Foo2.Alpha", + }, { + label: "IgnoreFields", + x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}}, + y: Bar1{Foo3{&Foo2{&Foo1{Alpha: 6}}}}, + opts: []cmp.Option{IgnoreFields(Bar1{}, "Foo3.Alpha")}, + wantEqual: true, + reason: "equal because IgnoreField ignores deeply embedded field: Foo3.Alpha", + }, { + label: "IgnoreFields", + x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}}, + y: Bar1{Foo3{&Foo2{&Foo1{Alpha: 6}}}}, + opts: []cmp.Option{IgnoreFields(Bar1{}, "Foo3.Foo2.Alpha")}, + wantEqual: true, + reason: "equal because IgnoreField ignores deeply embedded field: Foo3.Foo2.Alpha", + }, { + label: "IgnoreFields", + x: createBar3X(), + y: createBar3Y(), + wantEqual: false, + reason: "not equal because many deeply nested or embedded fields differ", + }, { + label: "IgnoreFields", + x: createBar3X(), + y: createBar3Y(), + opts: []cmp.Option{IgnoreFields(Bar3{}, "Bar1", "Bravo", "Delta", "Foo3", "Alpha")}, + wantEqual: true, + reason: "equal because IgnoreFields ignores fields at the highest levels", + }, { + label: "IgnoreFields", + x: createBar3X(), + y: createBar3Y(), + opts: []cmp.Option{ + IgnoreFields(Bar3{}, + "Bar1.Foo3.Bravo", + "Bravo.Bar1.Foo3.Foo2.Foo1.Charlie", + "Bravo.Foo3.Foo2.Foo1.Bravo", + "Bravo.Bravo", + "Delta.Echo.Charlie", + "Foo3.Foo2.Foo1.Alpha", + "Alpha", + ), + }, + wantEqual: true, + reason: "equal because IgnoreFields ignores fields using fully-qualified field", + }, { + label: "IgnoreFields", + x: createBar3X(), + y: createBar3Y(), + opts: []cmp.Option{ + IgnoreFields(Bar3{}, + "Bar1.Foo3.Bravo", + "Bravo.Foo3.Foo2.Foo1.Bravo", + "Bravo.Bravo", + "Delta.Echo.Charlie", + "Foo3.Foo2.Foo1.Alpha", + "Alpha", + ), + }, + wantEqual: false, + reason: "not equal because one fully-qualified field is not ignored: Bravo.Bar1.Foo3.Foo2.Foo1.Charlie", + }, { + label: "IgnoreFields", + x: createBar3X(), + y: createBar3Y(), + opts: []cmp.Option{IgnoreFields(Bar3{}, "Bar1", "Bravo", "Delta", "Alpha")}, + wantEqual: false, + reason: "not equal because highest-level field is not ignored: Foo3", + }, { + label: "IgnoreTypes", + x: []interface{}{5, "same"}, + y: []interface{}{6, "same"}, + wantEqual: false, + reason: "not equal because 5 != 6", + }, { + label: "IgnoreTypes", + x: []interface{}{5, "same"}, + y: []interface{}{6, "same"}, + opts: []cmp.Option{IgnoreTypes(0)}, + wantEqual: true, + reason: "equal because ints are ignored", + }, { + label: "IgnoreTypes+IgnoreInterfaces", + x: []interface{}{5, "same", new(bytes.Buffer)}, + y: []interface{}{6, "same", new(bytes.Buffer)}, + opts: []cmp.Option{IgnoreTypes(0)}, + wantPanic: true, + reason: "panics because bytes.Buffer has unexported fields", + }, { + label: "IgnoreTypes+IgnoreInterfaces", + x: []interface{}{5, "same", new(bytes.Buffer)}, + y: []interface{}{6, "diff", new(bytes.Buffer)}, + opts: []cmp.Option{ + IgnoreTypes(0, ""), + IgnoreInterfaces(struct{ io.Reader }{}), + }, + wantEqual: true, + reason: "equal because bytes.Buffer is ignored by match on interface type", + }, { + label: "IgnoreTypes+IgnoreInterfaces", + x: []interface{}{5, "same", new(bytes.Buffer)}, + y: []interface{}{6, "same", new(bytes.Buffer)}, + opts: []cmp.Option{ + IgnoreTypes(0, ""), + IgnoreInterfaces(struct { + io.Reader + io.Writer + fmt.Stringer + }{}), + }, + wantEqual: true, + reason: "equal because bytes.Buffer is ignored by match on multiple interface types", + }, { + label: "IgnoreInterfaces", + x: struct{ mu sync.Mutex }{}, + y: struct{ mu sync.Mutex }{}, + wantPanic: true, + reason: "panics because sync.Mutex has unexported fields", + }, { + label: "IgnoreInterfaces", + x: struct{ mu sync.Mutex }{}, + y: struct{ mu sync.Mutex }{}, + opts: []cmp.Option{IgnoreInterfaces(struct{ sync.Locker }{})}, + wantEqual: true, + reason: "equal because IgnoreInterfaces applies on values (with pointer receiver)", + }, { + label: "IgnoreInterfaces", + x: struct{ mu *sync.Mutex }{}, + y: struct{ mu *sync.Mutex }{}, + opts: []cmp.Option{IgnoreInterfaces(struct{ sync.Locker }{})}, + wantEqual: true, + reason: "equal because IgnoreInterfaces applies on pointers", + }, { + label: "IgnoreUnexported", + x: ParentStruct{Public: 1, private: 2}, + y: ParentStruct{Public: 1, private: -2}, + opts: []cmp.Option{cmp.AllowUnexported(ParentStruct{})}, + wantEqual: false, + reason: "not equal because ParentStruct.private differs with AllowUnexported", + }, { + label: "IgnoreUnexported", + x: ParentStruct{Public: 1, private: 2}, + y: ParentStruct{Public: 1, private: -2}, + opts: []cmp.Option{IgnoreUnexported(ParentStruct{})}, + wantEqual: true, + reason: "equal because IgnoreUnexported ignored ParentStruct.private", + }, { + label: "IgnoreUnexported", + x: ParentStruct{Public: 1, private: 2, PublicStruct: &PublicStruct{Public: 3, private: 4}}, + y: ParentStruct{Public: 1, private: -2, PublicStruct: &PublicStruct{Public: 3, private: 4}}, + opts: []cmp.Option{ + cmp.AllowUnexported(PublicStruct{}), + IgnoreUnexported(ParentStruct{}), + }, + wantEqual: true, + reason: "equal because ParentStruct.private is ignored", + }, { + label: "IgnoreUnexported", + x: ParentStruct{Public: 1, private: 2, PublicStruct: &PublicStruct{Public: 3, private: 4}}, + y: ParentStruct{Public: 1, private: -2, PublicStruct: &PublicStruct{Public: 3, private: -4}}, + opts: []cmp.Option{ + cmp.AllowUnexported(PublicStruct{}), + IgnoreUnexported(ParentStruct{}), + }, + wantEqual: false, + reason: "not equal because ParentStruct.PublicStruct.private differs and not ignored by IgnoreUnexported(ParentStruct{})", + }, { + label: "IgnoreUnexported", + x: ParentStruct{Public: 1, private: 2, PublicStruct: &PublicStruct{Public: 3, private: 4}}, + y: ParentStruct{Public: 1, private: -2, PublicStruct: &PublicStruct{Public: 3, private: -4}}, + opts: []cmp.Option{ + IgnoreUnexported(ParentStruct{}, PublicStruct{}), + }, + wantEqual: true, + reason: "equal because both ParentStruct.PublicStruct and ParentStruct.PublicStruct.private are ignored", + }, { + label: "IgnoreUnexported", + x: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: 3, private: 4}}, + y: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: -3, private: -4}}, + opts: []cmp.Option{ + cmp.AllowUnexported(privateStruct{}, PublicStruct{}, ParentStruct{}), + }, + wantEqual: false, + reason: "not equal since ParentStruct.privateStruct differs", + }, { + label: "IgnoreUnexported", + x: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: 3, private: 4}}, + y: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: -3, private: -4}}, + opts: []cmp.Option{ + cmp.AllowUnexported(privateStruct{}, PublicStruct{}), + IgnoreUnexported(ParentStruct{}), + }, + wantEqual: true, + reason: "equal because ParentStruct.privateStruct ignored by IgnoreUnexported(ParentStruct{})", + }, { + label: "IgnoreUnexported", + x: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: 3, private: 4}}, + y: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: 3, private: -4}}, + opts: []cmp.Option{ + cmp.AllowUnexported(PublicStruct{}, ParentStruct{}), + IgnoreUnexported(privateStruct{}), + }, + wantEqual: true, + reason: "equal because privateStruct.private ignored by IgnoreUnexported(privateStruct{})", + }, { + label: "IgnoreUnexported", + x: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: 3, private: 4}}, + y: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: -3, private: -4}}, + opts: []cmp.Option{ + cmp.AllowUnexported(PublicStruct{}, ParentStruct{}), + IgnoreUnexported(privateStruct{}), + }, + wantEqual: false, + reason: "not equal because privateStruct.Public differs and not ignored by IgnoreUnexported(privateStruct{})", + }, { + label: "IgnoreFields+IgnoreTypes+IgnoreUnexported", + x: &Everything{ + MyInt: 5, + MyFloat: 3.3, + MyTime: MyTime{time.Now()}, + Bar3: *createBar3X(), + ParentStruct: ParentStruct{ + Public: 1, private: 2, PublicStruct: &PublicStruct{Public: 3, private: 4}, + }, + }, + y: &Everything{ + MyInt: -5, + MyFloat: 3.3, + MyTime: MyTime{time.Now()}, + Bar3: *createBar3Y(), + ParentStruct: ParentStruct{ + Public: 1, private: -2, PublicStruct: &PublicStruct{Public: -3, private: -4}, + }, + }, + opts: []cmp.Option{ + IgnoreFields(Everything{}, "MyTime", "Bar3.Foo3"), + IgnoreFields(Bar3{}, "Bar1", "Bravo", "Delta", "Alpha"), + IgnoreTypes(MyInt(0), PublicStruct{}), + IgnoreUnexported(ParentStruct{}), + }, + wantEqual: true, + reason: "equal because all Ignore options can be composed together", + }} + + for _, tt := range tests { + tRun(t, tt.label, func(t *testing.T) { + var gotEqual bool + var gotPanic string + func() { + defer func() { + if ex := recover(); ex != nil { + gotPanic = fmt.Sprint(ex) + } + }() + gotEqual = cmp.Equal(tt.x, tt.y, tt.opts...) + }() + switch { + case gotPanic == "" && tt.wantPanic: + t.Errorf("expected Equal panic\nreason: %s", tt.reason) + case gotPanic != "" && !tt.wantPanic: + t.Errorf("unexpected Equal panic: got %v\nreason: %v", gotPanic, tt.reason) + case gotEqual != tt.wantEqual: + t.Errorf("Equal = %v, want %v\nreason: %v", gotEqual, tt.wantEqual, tt.reason) + } + }) + } +} + +func TestPanic(t *testing.T) { + args := func(x ...interface{}) []interface{} { return x } + tests := []struct { + label string // Test name + fnc interface{} // Option function to call + args []interface{} // Arguments to pass in + wantPanic string // Expected panic message + reason string // The reason for the expected outcome + }{{ + label: "EquateApprox", + fnc: EquateApprox, + args: args(0.0, 0.0), + reason: "zero margin and fraction is equivalent to exact equality", + }, { + label: "EquateApprox", + fnc: EquateApprox, + args: args(-0.1, 0.0), + wantPanic: "margin or fraction must be a non-negative number", + reason: "negative inputs are invalid", + }, { + label: "EquateApprox", + fnc: EquateApprox, + args: args(0.0, -0.1), + wantPanic: "margin or fraction must be a non-negative number", + reason: "negative inputs are invalid", + }, { + label: "EquateApprox", + fnc: EquateApprox, + args: args(math.NaN(), 0.0), + wantPanic: "margin or fraction must be a non-negative number", + reason: "NaN inputs are invalid", + }, { + label: "EquateApprox", + fnc: EquateApprox, + args: args(1.0, 0.0), + reason: "fraction of 1.0 or greater is valid", + }, { + label: "EquateApprox", + fnc: EquateApprox, + args: args(0.0, math.Inf(+1)), + reason: "margin of infinity is valid", + }, { + label: "SortSlices", + fnc: SortSlices, + args: args(strings.Compare), + wantPanic: "invalid less function", + reason: "func(x, y string) int is wrong signature for less", + }, { + label: "SortSlices", + fnc: SortSlices, + args: args((func(_, _ int) bool)(nil)), + wantPanic: "invalid less function", + reason: "nil value is not valid", + }, { + label: "SortMaps", + fnc: SortMaps, + args: args(strings.Compare), + wantPanic: "invalid less function", + reason: "func(x, y string) int is wrong signature for less", + }, { + label: "SortMaps", + fnc: SortMaps, + args: args((func(_, _ int) bool)(nil)), + wantPanic: "invalid less function", + reason: "nil value is not valid", + }, { + label: "IgnoreFields", + fnc: IgnoreFields, + args: args(Foo1{}, ""), + wantPanic: "name must not be empty", + reason: "empty selector is invalid", + }, { + label: "IgnoreFields", + fnc: IgnoreFields, + args: args(Foo1{}, "."), + wantPanic: "name must not be empty", + reason: "single dot selector is invalid", + }, { + label: "IgnoreFields", + fnc: IgnoreFields, + args: args(Foo1{}, ".Alpha"), + reason: "dot-prefix is okay since Foo1.Alpha reads naturally", + }, { + label: "IgnoreFields", + fnc: IgnoreFields, + args: args(Foo1{}, "Alpha."), + wantPanic: "name must not be empty", + reason: "dot-suffix is invalid", + }, { + label: "IgnoreFields", + fnc: IgnoreFields, + args: args(Foo1{}, "Alpha "), + wantPanic: "does not exist", + reason: "identifiers must not have spaces", + }, { + label: "IgnoreFields", + fnc: IgnoreFields, + args: args(Foo1{}, "Zulu"), + wantPanic: "does not exist", + reason: "name of non-existent field is invalid", + }, { + label: "IgnoreFields", + fnc: IgnoreFields, + args: args(Foo1{}, "Alpha.NoExist"), + wantPanic: "must be a struct", + reason: "cannot select into a non-struct", + }, { + label: "IgnoreFields", + fnc: IgnoreFields, + args: args(&Foo1{}, "Alpha"), + wantPanic: "must be a struct", + reason: "the type must be a struct (not pointer to a struct)", + }, { + label: "IgnoreFields", + fnc: IgnoreFields, + args: args(Foo1{}, "unexported"), + wantPanic: "name must be exported", + reason: "unexported fields must not be specified", + }, { + label: "IgnoreTypes", + fnc: IgnoreTypes, + reason: "empty input is valid", + }, { + label: "IgnoreTypes", + fnc: IgnoreTypes, + args: args(nil), + wantPanic: "cannot determine type", + reason: "input must not be nil value", + }, { + label: "IgnoreTypes", + fnc: IgnoreTypes, + args: args(0, 0, 0), + reason: "duplicate inputs of the same type is valid", + }, { + label: "IgnoreInterfaces", + fnc: IgnoreInterfaces, + args: args(nil), + wantPanic: "input must be an anonymous struct", + reason: "input must not be nil value", + }, { + label: "IgnoreInterfaces", + fnc: IgnoreInterfaces, + args: args(Foo1{}), + wantPanic: "input must be an anonymous struct", + reason: "input must not be a named struct type", + }, { + label: "IgnoreInterfaces", + fnc: IgnoreInterfaces, + args: args(struct{ _ io.Reader }{}), + wantPanic: "struct cannot have named fields", + reason: "input must not have named fields", + }, { + label: "IgnoreInterfaces", + fnc: IgnoreInterfaces, + args: args(struct{ Foo1 }{}), + wantPanic: "embedded field must be an interface type", + reason: "field types must be interfaces", + }, { + label: "IgnoreInterfaces", + fnc: IgnoreInterfaces, + args: args(struct{ EmptyInterface }{}), + wantPanic: "cannot ignore empty interface", + reason: "field types must not be the empty interface", + }, { + label: "IgnoreInterfaces", + fnc: IgnoreInterfaces, + args: args(struct { + io.Reader + io.Writer + io.Closer + io.ReadWriteCloser + }{}), + reason: "multiple interfaces may be specified, even if they overlap", + }, { + label: "IgnoreUnexported", + fnc: IgnoreUnexported, + reason: "empty input is valid", + }, { + label: "IgnoreUnexported", + fnc: IgnoreUnexported, + args: args(nil), + wantPanic: "invalid struct type", + reason: "input must not be nil value", + }, { + label: "IgnoreUnexported", + fnc: IgnoreUnexported, + args: args(&Foo1{}), + wantPanic: "invalid struct type", + reason: "input must be a struct type (not a pointer to a struct)", + }, { + label: "IgnoreUnexported", + fnc: IgnoreUnexported, + args: args(Foo1{}, struct{ x, X int }{}), + reason: "input may be named or unnamed structs", + }} + + for _, tt := range tests { + tRun(t, tt.label, func(t *testing.T) { + // Prepare function arguments. + vf := reflect.ValueOf(tt.fnc) + var vargs []reflect.Value + for i, arg := range tt.args { + if arg == nil { + tf := vf.Type() + if i == tf.NumIn()-1 && tf.IsVariadic() { + vargs = append(vargs, reflect.Zero(tf.In(i).Elem())) + } else { + vargs = append(vargs, reflect.Zero(tf.In(i))) + } + } else { + vargs = append(vargs, reflect.ValueOf(arg)) + } + } + + // Call the function and capture any panics. + var gotPanic string + func() { + defer func() { + if ex := recover(); ex != nil { + if s, ok := ex.(string); ok { + gotPanic = s + } else { + panic(ex) + } + } + }() + vf.Call(vargs) + }() + + switch { + case tt.wantPanic == "" && gotPanic != "": + t.Errorf("unexpected panic message: %s\nreason: %s", gotPanic, tt.reason) + case tt.wantPanic != "" && !strings.Contains(gotPanic, tt.wantPanic): + t.Errorf("panic message:\ngot: %s\nwant: %s\nreason: %s", gotPanic, tt.wantPanic, tt.reason) + } + }) + } +} + +// TODO: Delete this hack when we drop Go1.6 support. +func tRun(t *testing.T, name string, f func(t *testing.T)) { + type runner interface { + Run(string, func(t *testing.T)) bool + } + var ti interface{} = t + if r, ok := ti.(runner); ok { + r.Run(name, f) + } else { + t.Logf("Test: %s", name) + f(t) + } +} diff --git a/vendor/github.com/google/go-cmp/cmp/compare.go b/vendor/github.com/google/go-cmp/cmp/compare.go new file mode 100644 index 000000000..5527f0149 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/compare.go @@ -0,0 +1,529 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +// Package cmp determines equality of values. +// +// This package is intended to be a more powerful and safer alternative to +// reflect.DeepEqual for comparing whether two values are semantically equal. +// +// The primary features of cmp are: +// +// • When the default behavior of equality does not suit the needs of the test, +// custom equality functions can override the equality operation. +// For example, an equality function may report floats as equal so long as they +// are within some tolerance of each other. +// +// • Types that have an Equal method may use that method to determine equality. +// This allows package authors to determine the equality operation for the types +// that they define. +// +// • If no custom equality functions are used and no Equal method is defined, +// equality is determined by recursively comparing the primitive kinds on both +// values, much like reflect.DeepEqual. Unlike reflect.DeepEqual, unexported +// fields are not compared by default; they result in panics unless suppressed +// by using an Ignore option (see cmpopts.IgnoreUnexported) or explictly compared +// using the AllowUnexported option. +package cmp + +import ( + "fmt" + "reflect" + + "github.com/google/go-cmp/cmp/internal/diff" + "github.com/google/go-cmp/cmp/internal/function" + "github.com/google/go-cmp/cmp/internal/value" +) + +// BUG: Maps with keys containing NaN values cannot be properly compared due to +// the reflection package's inability to retrieve such entries. Equal will panic +// anytime it comes across a NaN key, but this behavior may change. +// +// See https://golang.org/issue/11104 for more details. + +var nothing = reflect.Value{} + +// Equal reports whether x and y are equal by recursively applying the +// following rules in the given order to x and y and all of their sub-values: +// +// • If two values are not of the same type, then they are never equal +// and the overall result is false. +// +// • Let S be the set of all Ignore, Transformer, and Comparer options that +// remain after applying all path filters, value filters, and type filters. +// If at least one Ignore exists in S, then the comparison is ignored. +// If the number of Transformer and Comparer options in S is greater than one, +// then Equal panics because it is ambiguous which option to use. +// If S contains a single Transformer, then use that to transform the current +// values and recursively call Equal on the output values. +// If S contains a single Comparer, then use that to compare the current values. +// Otherwise, evaluation proceeds to the next rule. +// +// • If the values have an Equal method of the form "(T) Equal(T) bool" or +// "(T) Equal(I) bool" where T is assignable to I, then use the result of +// x.Equal(y). Otherwise, no such method exists and evaluation proceeds to +// the next rule. +// +// • Lastly, try to compare x and y based on their basic kinds. +// Simple kinds like booleans, integers, floats, complex numbers, strings, and +// channels are compared using the equivalent of the == operator in Go. +// Functions are only equal if they are both nil, otherwise they are unequal. +// Pointers are equal if the underlying values they point to are also equal. +// Interfaces are equal if their underlying concrete values are also equal. +// +// Structs are equal if all of their fields are equal. If a struct contains +// unexported fields, Equal panics unless the AllowUnexported option is used or +// an Ignore option (e.g., cmpopts.IgnoreUnexported) ignores that field. +// +// Arrays, slices, and maps are equal if they are both nil or both non-nil +// with the same length and the elements at each index or key are equal. +// Note that a non-nil empty slice and a nil slice are not equal. +// To equate empty slices and maps, consider using cmpopts.EquateEmpty. +// Map keys are equal according to the == operator. +// To use custom comparisons for map keys, consider using cmpopts.SortMaps. +func Equal(x, y interface{}, opts ...Option) bool { + s := newState(opts) + s.compareAny(reflect.ValueOf(x), reflect.ValueOf(y)) + return s.result.Equal() +} + +// Diff returns a human-readable report of the differences between two values. +// It returns an empty string if and only if Equal returns true for the same +// input values and options. The output string will use the "-" symbol to +// indicate elements removed from x, and the "+" symbol to indicate elements +// added to y. +// +// Do not depend on this output being stable. +func Diff(x, y interface{}, opts ...Option) string { + r := new(defaultReporter) + opts = Options{Options(opts), r} + eq := Equal(x, y, opts...) + d := r.String() + if (d == "") != eq { + panic("inconsistent difference and equality results") + } + return d +} + +type state struct { + // These fields represent the "comparison state". + // Calling statelessCompare must not result in observable changes to these. + result diff.Result // The current result of comparison + curPath Path // The current path in the value tree + reporter reporter // Optional reporter used for difference formatting + + // dynChecker triggers pseudo-random checks for option correctness. + // It is safe for statelessCompare to mutate this value. + dynChecker dynChecker + + // These fields, once set by processOption, will not change. + exporters map[reflect.Type]bool // Set of structs with unexported field visibility + opts Options // List of all fundamental and filter options +} + +func newState(opts []Option) *state { + s := new(state) + for _, opt := range opts { + s.processOption(opt) + } + return s +} + +func (s *state) processOption(opt Option) { + switch opt := opt.(type) { + case nil: + case Options: + for _, o := range opt { + s.processOption(o) + } + case coreOption: + type filtered interface { + isFiltered() bool + } + if fopt, ok := opt.(filtered); ok && !fopt.isFiltered() { + panic(fmt.Sprintf("cannot use an unfiltered option: %v", opt)) + } + s.opts = append(s.opts, opt) + case visibleStructs: + if s.exporters == nil { + s.exporters = make(map[reflect.Type]bool) + } + for t := range opt { + s.exporters[t] = true + } + case reporter: + if s.reporter != nil { + panic("difference reporter already registered") + } + s.reporter = opt + default: + panic(fmt.Sprintf("unknown option %T", opt)) + } +} + +// statelessCompare compares two values and returns the result. +// This function is stateless in that it does not alter the current result, +// or output to any registered reporters. +func (s *state) statelessCompare(vx, vy reflect.Value) diff.Result { + // We do not save and restore the curPath because all of the compareX + // methods should properly push and pop from the path. + // It is an implementation bug if the contents of curPath differs from + // when calling this function to when returning from it. + + oldResult, oldReporter := s.result, s.reporter + s.result = diff.Result{} // Reset result + s.reporter = nil // Remove reporter to avoid spurious printouts + s.compareAny(vx, vy) + res := s.result + s.result, s.reporter = oldResult, oldReporter + return res +} + +func (s *state) compareAny(vx, vy reflect.Value) { + // TODO: Support cyclic data structures. + + // Rule 0: Differing types are never equal. + if !vx.IsValid() || !vy.IsValid() { + s.report(vx.IsValid() == vy.IsValid(), vx, vy) + return + } + if vx.Type() != vy.Type() { + s.report(false, vx, vy) // Possible for path to be empty + return + } + t := vx.Type() + if len(s.curPath) == 0 { + s.curPath.push(&pathStep{typ: t}) + defer s.curPath.pop() + } + vx, vy = s.tryExporting(vx, vy) + + // Rule 1: Check whether an option applies on this node in the value tree. + if s.tryOptions(vx, vy, t) { + return + } + + // Rule 2: Check whether the type has a valid Equal method. + if s.tryMethod(vx, vy, t) { + return + } + + // Rule 3: Recursively descend into each value's underlying kind. + switch t.Kind() { + case reflect.Bool: + s.report(vx.Bool() == vy.Bool(), vx, vy) + return + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + s.report(vx.Int() == vy.Int(), vx, vy) + return + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + s.report(vx.Uint() == vy.Uint(), vx, vy) + return + case reflect.Float32, reflect.Float64: + s.report(vx.Float() == vy.Float(), vx, vy) + return + case reflect.Complex64, reflect.Complex128: + s.report(vx.Complex() == vy.Complex(), vx, vy) + return + case reflect.String: + s.report(vx.String() == vy.String(), vx, vy) + return + case reflect.Chan, reflect.UnsafePointer: + s.report(vx.Pointer() == vy.Pointer(), vx, vy) + return + case reflect.Func: + s.report(vx.IsNil() && vy.IsNil(), vx, vy) + return + case reflect.Ptr: + if vx.IsNil() || vy.IsNil() { + s.report(vx.IsNil() && vy.IsNil(), vx, vy) + return + } + s.curPath.push(&indirect{pathStep{t.Elem()}}) + defer s.curPath.pop() + s.compareAny(vx.Elem(), vy.Elem()) + return + case reflect.Interface: + if vx.IsNil() || vy.IsNil() { + s.report(vx.IsNil() && vy.IsNil(), vx, vy) + return + } + if vx.Elem().Type() != vy.Elem().Type() { + s.report(false, vx.Elem(), vy.Elem()) + return + } + s.curPath.push(&typeAssertion{pathStep{vx.Elem().Type()}}) + defer s.curPath.pop() + s.compareAny(vx.Elem(), vy.Elem()) + return + case reflect.Slice: + if vx.IsNil() || vy.IsNil() { + s.report(vx.IsNil() && vy.IsNil(), vx, vy) + return + } + fallthrough + case reflect.Array: + s.compareArray(vx, vy, t) + return + case reflect.Map: + s.compareMap(vx, vy, t) + return + case reflect.Struct: + s.compareStruct(vx, vy, t) + return + default: + panic(fmt.Sprintf("%v kind not handled", t.Kind())) + } +} + +func (s *state) tryExporting(vx, vy reflect.Value) (reflect.Value, reflect.Value) { + if sf, ok := s.curPath[len(s.curPath)-1].(*structField); ok && sf.unexported { + if sf.force { + // Use unsafe pointer arithmetic to get read-write access to an + // unexported field in the struct. + vx = unsafeRetrieveField(sf.pvx, sf.field) + vy = unsafeRetrieveField(sf.pvy, sf.field) + } else { + // We are not allowed to export the value, so invalidate them + // so that tryOptions can panic later if not explicitly ignored. + vx = nothing + vy = nothing + } + } + return vx, vy +} + +func (s *state) tryOptions(vx, vy reflect.Value, t reflect.Type) bool { + // If there were no FilterValues, we will not detect invalid inputs, + // so manually check for them and append invalid if necessary. + // We still evaluate the options since an ignore can override invalid. + opts := s.opts + if !vx.IsValid() || !vy.IsValid() { + opts = Options{opts, invalid{}} + } + + // Evaluate all filters and apply the remaining options. + if opt := opts.filter(s, vx, vy, t); opt != nil { + return opt.apply(s, vx, vy) + } + return false +} + +func (s *state) tryMethod(vx, vy reflect.Value, t reflect.Type) bool { + // Check if this type even has an Equal method. + m, ok := t.MethodByName("Equal") + if !ok || !function.IsType(m.Type, function.EqualAssignable) { + return false + } + + eq := s.callTTBFunc(m.Func, vx, vy) + s.report(eq, vx, vy) + return true +} + +func (s *state) callTRFunc(f, v reflect.Value) reflect.Value { + if !s.dynChecker.Next() { + return f.Call([]reflect.Value{v})[0] + } + + // Run the function twice and ensure that we get the same results back. + // We run in goroutines so that the race detector (if enabled) can detect + // unsafe mutations to the input. + c := make(chan reflect.Value) + go detectRaces(c, f, v) + want := f.Call([]reflect.Value{v})[0] + if got := <-c; !s.statelessCompare(got, want).Equal() { + // To avoid false-positives with non-reflexive equality operations, + // we sanity check whether a value is equal to itself. + if !s.statelessCompare(want, want).Equal() { + return want + } + fn := getFuncName(f.Pointer()) + panic(fmt.Sprintf("non-deterministic function detected: %s", fn)) + } + return want +} + +func (s *state) callTTBFunc(f, x, y reflect.Value) bool { + if !s.dynChecker.Next() { + return f.Call([]reflect.Value{x, y})[0].Bool() + } + + // Swapping the input arguments is sufficient to check that + // f is symmetric and deterministic. + // We run in goroutines so that the race detector (if enabled) can detect + // unsafe mutations to the input. + c := make(chan reflect.Value) + go detectRaces(c, f, y, x) + want := f.Call([]reflect.Value{x, y})[0].Bool() + if got := <-c; !got.IsValid() || got.Bool() != want { + fn := getFuncName(f.Pointer()) + panic(fmt.Sprintf("non-deterministic or non-symmetric function detected: %s", fn)) + } + return want +} + +func detectRaces(c chan<- reflect.Value, f reflect.Value, vs ...reflect.Value) { + var ret reflect.Value + defer func() { + recover() // Ignore panics, let the other call to f panic instead + c <- ret + }() + ret = f.Call(vs)[0] +} + +func (s *state) compareArray(vx, vy reflect.Value, t reflect.Type) { + step := &sliceIndex{pathStep{t.Elem()}, 0, 0} + s.curPath.push(step) + + // Compute an edit-script for slices vx and vy. + eq, es := diff.Difference(vx.Len(), vy.Len(), func(ix, iy int) diff.Result { + step.xkey, step.ykey = ix, iy + return s.statelessCompare(vx.Index(ix), vy.Index(iy)) + }) + + // Equal or no edit-script, so report entire slices as is. + if eq || es == nil { + s.curPath.pop() // Pop first since we are reporting the whole slice + s.report(eq, vx, vy) + return + } + + // Replay the edit-script. + var ix, iy int + for _, e := range es { + switch e { + case diff.UniqueX: + step.xkey, step.ykey = ix, -1 + s.report(false, vx.Index(ix), nothing) + ix++ + case diff.UniqueY: + step.xkey, step.ykey = -1, iy + s.report(false, nothing, vy.Index(iy)) + iy++ + default: + step.xkey, step.ykey = ix, iy + if e == diff.Identity { + s.report(true, vx.Index(ix), vy.Index(iy)) + } else { + s.compareAny(vx.Index(ix), vy.Index(iy)) + } + ix++ + iy++ + } + } + s.curPath.pop() + return +} + +func (s *state) compareMap(vx, vy reflect.Value, t reflect.Type) { + if vx.IsNil() || vy.IsNil() { + s.report(vx.IsNil() && vy.IsNil(), vx, vy) + return + } + + // We combine and sort the two map keys so that we can perform the + // comparisons in a deterministic order. + step := &mapIndex{pathStep: pathStep{t.Elem()}} + s.curPath.push(step) + defer s.curPath.pop() + for _, k := range value.SortKeys(append(vx.MapKeys(), vy.MapKeys()...)) { + step.key = k + vvx := vx.MapIndex(k) + vvy := vy.MapIndex(k) + switch { + case vvx.IsValid() && vvy.IsValid(): + s.compareAny(vvx, vvy) + case vvx.IsValid() && !vvy.IsValid(): + s.report(false, vvx, nothing) + case !vvx.IsValid() && vvy.IsValid(): + s.report(false, nothing, vvy) + default: + // It is possible for both vvx and vvy to be invalid if the + // key contained a NaN value in it. There is no way in + // reflection to be able to retrieve these values. + // See https://golang.org/issue/11104 + panic(fmt.Sprintf("%#v has map key with NaNs", s.curPath)) + } + } +} + +func (s *state) compareStruct(vx, vy reflect.Value, t reflect.Type) { + var vax, vay reflect.Value // Addressable versions of vx and vy + + step := &structField{} + s.curPath.push(step) + defer s.curPath.pop() + for i := 0; i < t.NumField(); i++ { + vvx := vx.Field(i) + vvy := vy.Field(i) + step.typ = t.Field(i).Type + step.name = t.Field(i).Name + step.idx = i + step.unexported = !isExported(step.name) + if step.unexported { + // Defer checking of unexported fields until later to give an + // Ignore a chance to ignore the field. + if !vax.IsValid() || !vay.IsValid() { + // For unsafeRetrieveField to work, the parent struct must + // be addressable. Create a new copy of the values if + // necessary to make them addressable. + vax = makeAddressable(vx) + vay = makeAddressable(vy) + } + step.force = s.exporters[t] + step.pvx = vax + step.pvy = vay + step.field = t.Field(i) + } + s.compareAny(vvx, vvy) + } +} + +// report records the result of a single comparison. +// It also calls Report if any reporter is registered. +func (s *state) report(eq bool, vx, vy reflect.Value) { + if eq { + s.result.NSame++ + } else { + s.result.NDiff++ + } + if s.reporter != nil { + s.reporter.Report(vx, vy, eq, s.curPath) + } +} + +// dynChecker tracks the state needed to periodically perform checks that +// user provided functions are symmetric and deterministic. +// The zero value is safe for immediate use. +type dynChecker struct{ curr, next int } + +// Next increments the state and reports whether a check should be performed. +// +// Checks occur every Nth function call, where N is a triangular number: +// 0 1 3 6 10 15 21 28 36 45 55 66 78 91 105 120 136 153 171 190 ... +// See https://en.wikipedia.org/wiki/Triangular_number +// +// This sequence ensures that the cost of checks drops significantly as +// the number of functions calls grows larger. +func (dc *dynChecker) Next() bool { + ok := dc.curr == dc.next + if ok { + dc.curr = 0 + dc.next++ + } + dc.curr++ + return ok +} + +// makeAddressable returns a value that is always addressable. +// It returns the input verbatim if it is already addressable, +// otherwise it creates a new value and returns an addressable copy. +func makeAddressable(v reflect.Value) reflect.Value { + if v.CanAddr() { + return v + } + vc := reflect.New(v.Type()).Elem() + vc.Set(v) + return vc +} diff --git a/vendor/github.com/google/go-cmp/cmp/compare_test.go b/vendor/github.com/google/go-cmp/cmp/compare_test.go new file mode 100644 index 000000000..36a4ecf7d --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/compare_test.go @@ -0,0 +1,1795 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +package cmp_test + +import ( + "bytes" + "crypto/md5" + "fmt" + "io" + "math" + "math/rand" + "reflect" + "regexp" + "sort" + "strings" + "sync" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + pb "github.com/google/go-cmp/cmp/internal/testprotos" + ts "github.com/google/go-cmp/cmp/internal/teststructs" +) + +var now = time.Now() + +func intPtr(n int) *int { return &n } + +type test struct { + label string // Test description + x, y interface{} // Input values to compare + opts []cmp.Option // Input options + wantDiff string // The exact difference string + wantPanic string // Sub-string of an expected panic message +} + +func TestDiff(t *testing.T) { + var tests []test + tests = append(tests, comparerTests()...) + tests = append(tests, transformerTests()...) + tests = append(tests, embeddedTests()...) + tests = append(tests, methodTests()...) + tests = append(tests, project1Tests()...) + tests = append(tests, project2Tests()...) + tests = append(tests, project3Tests()...) + tests = append(tests, project4Tests()...) + + for _, tt := range tests { + tt := tt + tRunParallel(t, tt.label, func(t *testing.T) { + var gotDiff, gotPanic string + func() { + defer func() { + if ex := recover(); ex != nil { + if s, ok := ex.(string); ok { + gotPanic = s + } else { + panic(ex) + } + } + }() + gotDiff = cmp.Diff(tt.x, tt.y, tt.opts...) + }() + if tt.wantPanic == "" { + if gotPanic != "" { + t.Fatalf("unexpected panic message: %s", gotPanic) + } + if got, want := strings.TrimSpace(gotDiff), strings.TrimSpace(tt.wantDiff); got != want { + t.Fatalf("difference message:\ngot:\n%s\n\nwant:\n%s", got, want) + } + } else { + if !strings.Contains(gotPanic, tt.wantPanic) { + t.Fatalf("panic message:\ngot: %s\nwant: %s", gotPanic, tt.wantPanic) + } + } + }) + } +} + +func comparerTests() []test { + const label = "Comparer" + + return []test{{ + label: label, + x: 1, + y: 1, + }, { + label: label, + x: 1, + y: 1, + opts: []cmp.Option{cmp.Ignore()}, + wantPanic: "cannot use an unfiltered option", + }, { + label: label, + x: 1, + y: 1, + opts: []cmp.Option{cmp.Comparer(func(_, _ interface{}) bool { return true })}, + wantPanic: "cannot use an unfiltered option", + }, { + label: label, + x: 1, + y: 1, + opts: []cmp.Option{cmp.Transformer("", func(x interface{}) interface{} { return x })}, + wantPanic: "cannot use an unfiltered option", + }, { + label: label, + x: 1, + y: 1, + opts: []cmp.Option{ + cmp.Comparer(func(x, y int) bool { return true }), + cmp.Transformer("", func(x int) float64 { return float64(x) }), + }, + wantPanic: "ambiguous set of applicable options", + }, { + label: label, + x: 1, + y: 1, + opts: []cmp.Option{ + cmp.FilterPath(func(p cmp.Path) bool { + return len(p) > 0 && p[len(p)-1].Type().Kind() == reflect.Int + }, cmp.Options{cmp.Ignore(), cmp.Ignore(), cmp.Ignore()}), + cmp.Comparer(func(x, y int) bool { return true }), + cmp.Transformer("", func(x int) float64 { return float64(x) }), + }, + }, { + label: label, + opts: []cmp.Option{struct{ cmp.Option }{}}, + wantPanic: "unknown option", + }, { + label: label, + x: struct{ A, B, C int }{1, 2, 3}, + y: struct{ A, B, C int }{1, 2, 3}, + }, { + label: label, + x: struct{ A, B, C int }{1, 2, 3}, + y: struct{ A, B, C int }{1, 2, 4}, + wantDiff: "root.C:\n\t-: 3\n\t+: 4\n", + }, { + label: label, + x: struct{ a, b, c int }{1, 2, 3}, + y: struct{ a, b, c int }{1, 2, 4}, + wantPanic: "cannot handle unexported field", + }, { + label: label, + x: &struct{ A *int }{intPtr(4)}, + y: &struct{ A *int }{intPtr(4)}, + }, { + label: label, + x: &struct{ A *int }{intPtr(4)}, + y: &struct{ A *int }{intPtr(5)}, + wantDiff: "*root.A:\n\t-: 4\n\t+: 5\n", + }, { + label: label, + x: &struct{ A *int }{intPtr(4)}, + y: &struct{ A *int }{intPtr(5)}, + opts: []cmp.Option{ + cmp.Comparer(func(x, y int) bool { return true }), + }, + }, { + label: label, + x: &struct{ A *int }{intPtr(4)}, + y: &struct{ A *int }{intPtr(5)}, + opts: []cmp.Option{ + cmp.Comparer(func(x, y *int) bool { return x != nil && y != nil }), + }, + }, { + label: label, + x: &struct{ R *bytes.Buffer }{}, + y: &struct{ R *bytes.Buffer }{}, + }, { + label: label, + x: &struct{ R *bytes.Buffer }{new(bytes.Buffer)}, + y: &struct{ R *bytes.Buffer }{}, + wantDiff: "root.R:\n\t-: \"\"\n\t+: \n", + }, { + label: label, + x: &struct{ R *bytes.Buffer }{new(bytes.Buffer)}, + y: &struct{ R *bytes.Buffer }{}, + opts: []cmp.Option{ + cmp.Comparer(func(x, y io.Reader) bool { return true }), + }, + }, { + label: label, + x: &struct{ R bytes.Buffer }{}, + y: &struct{ R bytes.Buffer }{}, + wantPanic: "cannot handle unexported field", + }, { + label: label, + x: &struct{ R bytes.Buffer }{}, + y: &struct{ R bytes.Buffer }{}, + opts: []cmp.Option{ + cmp.Comparer(func(x, y io.Reader) bool { return true }), + }, + wantPanic: "cannot handle unexported field", + }, { + label: label, + x: &struct{ R bytes.Buffer }{}, + y: &struct{ R bytes.Buffer }{}, + opts: []cmp.Option{ + cmp.Transformer("Ref", func(x bytes.Buffer) *bytes.Buffer { return &x }), + cmp.Comparer(func(x, y io.Reader) bool { return true }), + }, + }, { + label: label, + x: []*regexp.Regexp{nil, regexp.MustCompile("a*b*c*")}, + y: []*regexp.Regexp{nil, regexp.MustCompile("a*b*c*")}, + wantPanic: "cannot handle unexported field", + }, { + label: label, + x: []*regexp.Regexp{nil, regexp.MustCompile("a*b*c*")}, + y: []*regexp.Regexp{nil, regexp.MustCompile("a*b*c*")}, + opts: []cmp.Option{cmp.Comparer(func(x, y *regexp.Regexp) bool { + if x == nil || y == nil { + return x == nil && y == nil + } + return x.String() == y.String() + })}, + }, { + label: label, + x: []*regexp.Regexp{nil, regexp.MustCompile("a*b*c*")}, + y: []*regexp.Regexp{nil, regexp.MustCompile("a*b*d*")}, + opts: []cmp.Option{cmp.Comparer(func(x, y *regexp.Regexp) bool { + if x == nil || y == nil { + return x == nil && y == nil + } + return x.String() == y.String() + })}, + wantDiff: ` +{[]*regexp.Regexp}[1]: + -: "a*b*c*" + +: "a*b*d*"`, + }, { + label: label, + x: func() ***int { + a := 0 + b := &a + c := &b + return &c + }(), + y: func() ***int { + a := 0 + b := &a + c := &b + return &c + }(), + }, { + label: label, + x: func() ***int { + a := 0 + b := &a + c := &b + return &c + }(), + y: func() ***int { + a := 1 + b := &a + c := &b + return &c + }(), + wantDiff: ` +***{***int}: + -: 0 + +: 1`, + }, { + label: label, + x: []int{1, 2, 3, 4, 5}[:3], + y: []int{1, 2, 3}, + }, { + label: label, + x: struct{ fmt.Stringer }{bytes.NewBufferString("hello")}, + y: struct{ fmt.Stringer }{regexp.MustCompile("hello")}, + opts: []cmp.Option{cmp.Comparer(func(x, y fmt.Stringer) bool { return x.String() == y.String() })}, + }, { + label: label, + x: struct{ fmt.Stringer }{bytes.NewBufferString("hello")}, + y: struct{ fmt.Stringer }{regexp.MustCompile("hello2")}, + opts: []cmp.Option{cmp.Comparer(func(x, y fmt.Stringer) bool { return x.String() == y.String() })}, + wantDiff: ` +root: + -: "hello" + +: "hello2"`, + }, { + label: label, + x: md5.Sum([]byte{'a'}), + y: md5.Sum([]byte{'b'}), + wantDiff: ` +{[16]uint8}: + -: [16]uint8{0x0c, 0xc1, 0x75, 0xb9, 0xc0, 0xf1, 0xb6, 0xa8, 0x31, 0xc3, 0x99, 0xe2, 0x69, 0x77, 0x26, 0x61} + +: [16]uint8{0x92, 0xeb, 0x5f, 0xfe, 0xe6, 0xae, 0x2f, 0xec, 0x3a, 0xd7, 0x1c, 0x77, 0x75, 0x31, 0x57, 0x8f}`, + }, { + label: label, + x: new(fmt.Stringer), + y: nil, + wantDiff: ` +: + -: & + +: `, + }, { + label: label, + x: make([]int, 1000), + y: make([]int, 1000), + opts: []cmp.Option{ + cmp.Comparer(func(_, _ int) bool { + return rand.Intn(2) == 0 + }), + }, + wantPanic: "non-deterministic or non-symmetric function detected", + }, { + label: label, + x: make([]int, 1000), + y: make([]int, 1000), + opts: []cmp.Option{ + cmp.FilterValues(func(_, _ int) bool { + return rand.Intn(2) == 0 + }, cmp.Ignore()), + }, + wantPanic: "non-deterministic or non-symmetric function detected", + }, { + label: label, + x: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, + y: []int{10, 9, 8, 7, 6, 5, 4, 3, 2, 1}, + opts: []cmp.Option{ + cmp.Comparer(func(x, y int) bool { + return x < y + }), + }, + wantPanic: "non-deterministic or non-symmetric function detected", + }, { + label: label, + x: make([]string, 1000), + y: make([]string, 1000), + opts: []cmp.Option{ + cmp.Transformer("", func(x string) int { + return rand.Int() + }), + }, + wantPanic: "non-deterministic function detected", + }, { + // Make sure the dynamic checks don't raise a false positive for + // non-reflexive comparisons. + label: label, + x: make([]int, 10), + y: make([]int, 10), + opts: []cmp.Option{ + cmp.Transformer("", func(x int) float64 { + return math.NaN() + }), + }, + wantDiff: ` +{[]int}: + -: []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0} + +: []int{0, 0, 0, 0, 0, 0, 0, 0, 0, 0}`, + }} +} + +func transformerTests() []test { + const label = "Transformer/" + + return []test{{ + label: label, + x: uint8(0), + y: uint8(1), + opts: []cmp.Option{ + cmp.Transformer("", func(in uint8) uint16 { return uint16(in) }), + cmp.Transformer("", func(in uint16) uint32 { return uint32(in) }), + cmp.Transformer("", func(in uint32) uint64 { return uint64(in) }), + }, + wantDiff: ` +λ(λ(λ({uint8}))): + -: 0x00 + +: 0x01`, + }, { + label: label, + x: 0, + y: 1, + opts: []cmp.Option{ + cmp.Transformer("", func(in int) int { return in / 2 }), + cmp.Transformer("", func(in int) int { return in }), + }, + wantPanic: "ambiguous set of applicable options", + }, { + label: label, + x: []int{0, -5, 0, -1}, + y: []int{1, 3, 0, -5}, + opts: []cmp.Option{ + cmp.FilterValues( + func(x, y int) bool { return x+y >= 0 }, + cmp.Transformer("", func(in int) int64 { return int64(in / 2) }), + ), + cmp.FilterValues( + func(x, y int) bool { return x+y < 0 }, + cmp.Transformer("", func(in int) int64 { return int64(in) }), + ), + }, + wantDiff: ` +λ({[]int}[1]): + -: -5 + +: 3 +λ({[]int}[3]): + -: -1 + +: -5`, + }, { + label: label, + x: 0, + y: 1, + opts: []cmp.Option{ + cmp.Transformer("", func(in int) interface{} { + if in == 0 { + return "string" + } + return float64(in) + }), + }, + wantDiff: ` +λ({int}): + -: "string" + +: 1`, + }} +} + +func embeddedTests() []test { + const label = "EmbeddedStruct/" + + privateStruct := *new(ts.ParentStructA).PrivateStruct() + + createStructA := func(i int) ts.ParentStructA { + s := ts.ParentStructA{} + s.PrivateStruct().Public = 1 + i + s.PrivateStruct().SetPrivate(2 + i) + return s + } + + createStructB := func(i int) ts.ParentStructB { + s := ts.ParentStructB{} + s.PublicStruct.Public = 1 + i + s.PublicStruct.SetPrivate(2 + i) + return s + } + + createStructC := func(i int) ts.ParentStructC { + s := ts.ParentStructC{} + s.PrivateStruct().Public = 1 + i + s.PrivateStruct().SetPrivate(2 + i) + s.Public = 3 + i + s.SetPrivate(4 + i) + return s + } + + createStructD := func(i int) ts.ParentStructD { + s := ts.ParentStructD{} + s.PublicStruct.Public = 1 + i + s.PublicStruct.SetPrivate(2 + i) + s.Public = 3 + i + s.SetPrivate(4 + i) + return s + } + + createStructE := func(i int) ts.ParentStructE { + s := ts.ParentStructE{} + s.PrivateStruct().Public = 1 + i + s.PrivateStruct().SetPrivate(2 + i) + s.PublicStruct.Public = 3 + i + s.PublicStruct.SetPrivate(4 + i) + return s + } + + createStructF := func(i int) ts.ParentStructF { + s := ts.ParentStructF{} + s.PrivateStruct().Public = 1 + i + s.PrivateStruct().SetPrivate(2 + i) + s.PublicStruct.Public = 3 + i + s.PublicStruct.SetPrivate(4 + i) + s.Public = 5 + i + s.SetPrivate(6 + i) + return s + } + + createStructG := func(i int) *ts.ParentStructG { + s := ts.NewParentStructG() + s.PrivateStruct().Public = 1 + i + s.PrivateStruct().SetPrivate(2 + i) + return s + } + + createStructH := func(i int) *ts.ParentStructH { + s := ts.NewParentStructH() + s.PublicStruct.Public = 1 + i + s.PublicStruct.SetPrivate(2 + i) + return s + } + + createStructI := func(i int) *ts.ParentStructI { + s := ts.NewParentStructI() + s.PrivateStruct().Public = 1 + i + s.PrivateStruct().SetPrivate(2 + i) + s.PublicStruct.Public = 3 + i + s.PublicStruct.SetPrivate(4 + i) + return s + } + + createStructJ := func(i int) *ts.ParentStructJ { + s := ts.NewParentStructJ() + s.PrivateStruct().Public = 1 + i + s.PrivateStruct().SetPrivate(2 + i) + s.PublicStruct.Public = 3 + i + s.PublicStruct.SetPrivate(4 + i) + s.Private().Public = 5 + i + s.Private().SetPrivate(6 + i) + s.Public.Public = 7 + i + s.Public.SetPrivate(8 + i) + return s + } + + return []test{{ + label: label + "ParentStructA", + x: ts.ParentStructA{}, + y: ts.ParentStructA{}, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructA", + x: ts.ParentStructA{}, + y: ts.ParentStructA{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructA{}), + }, + }, { + label: label + "ParentStructA", + x: createStructA(0), + y: createStructA(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructA{}), + }, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructA", + x: createStructA(0), + y: createStructA(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructA{}, privateStruct), + }, + }, { + label: label + "ParentStructA", + x: createStructA(0), + y: createStructA(1), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructA{}, privateStruct), + }, + wantDiff: ` +{teststructs.ParentStructA}.privateStruct.Public: + -: 1 + +: 2 +{teststructs.ParentStructA}.privateStruct.private: + -: 2 + +: 3`, + }, { + label: label + "ParentStructB", + x: ts.ParentStructB{}, + y: ts.ParentStructB{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructB{}), + }, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructB", + x: ts.ParentStructB{}, + y: ts.ParentStructB{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructB{}), + cmpopts.IgnoreUnexported(ts.PublicStruct{}), + }, + }, { + label: label + "ParentStructB", + x: createStructB(0), + y: createStructB(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructB{}), + }, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructB", + x: createStructB(0), + y: createStructB(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructB{}, ts.PublicStruct{}), + }, + }, { + label: label + "ParentStructB", + x: createStructB(0), + y: createStructB(1), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructB{}, ts.PublicStruct{}), + }, + wantDiff: ` +{teststructs.ParentStructB}.PublicStruct.Public: + -: 1 + +: 2 +{teststructs.ParentStructB}.PublicStruct.private: + -: 2 + +: 3`, + }, { + label: label + "ParentStructC", + x: ts.ParentStructC{}, + y: ts.ParentStructC{}, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructC", + x: ts.ParentStructC{}, + y: ts.ParentStructC{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructC{}), + }, + }, { + label: label + "ParentStructC", + x: createStructC(0), + y: createStructC(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructC{}), + }, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructC", + x: createStructC(0), + y: createStructC(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructC{}, privateStruct), + }, + }, { + label: label + "ParentStructC", + x: createStructC(0), + y: createStructC(1), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructC{}, privateStruct), + }, + wantDiff: ` +{teststructs.ParentStructC}.privateStruct.Public: + -: 1 + +: 2 +{teststructs.ParentStructC}.privateStruct.private: + -: 2 + +: 3 +{teststructs.ParentStructC}.Public: + -: 3 + +: 4 +{teststructs.ParentStructC}.private: + -: 4 + +: 5`, + }, { + label: label + "ParentStructD", + x: ts.ParentStructD{}, + y: ts.ParentStructD{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructD{}), + }, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructD", + x: ts.ParentStructD{}, + y: ts.ParentStructD{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructD{}), + cmpopts.IgnoreUnexported(ts.PublicStruct{}), + }, + }, { + label: label + "ParentStructD", + x: createStructD(0), + y: createStructD(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructD{}), + }, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructD", + x: createStructD(0), + y: createStructD(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructD{}, ts.PublicStruct{}), + }, + }, { + label: label + "ParentStructD", + x: createStructD(0), + y: createStructD(1), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructD{}, ts.PublicStruct{}), + }, + wantDiff: ` +{teststructs.ParentStructD}.PublicStruct.Public: + -: 1 + +: 2 +{teststructs.ParentStructD}.PublicStruct.private: + -: 2 + +: 3 +{teststructs.ParentStructD}.Public: + -: 3 + +: 4 +{teststructs.ParentStructD}.private: + -: 4 + +: 5`, + }, { + label: label + "ParentStructE", + x: ts.ParentStructE{}, + y: ts.ParentStructE{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructE{}), + }, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructE", + x: ts.ParentStructE{}, + y: ts.ParentStructE{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructE{}), + cmpopts.IgnoreUnexported(ts.PublicStruct{}), + }, + }, { + label: label + "ParentStructE", + x: createStructE(0), + y: createStructE(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructE{}), + }, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructE", + x: createStructE(0), + y: createStructE(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructE{}, ts.PublicStruct{}), + }, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructE", + x: createStructE(0), + y: createStructE(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructE{}, ts.PublicStruct{}, privateStruct), + }, + }, { + label: label + "ParentStructE", + x: createStructE(0), + y: createStructE(1), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructE{}, ts.PublicStruct{}, privateStruct), + }, + wantDiff: ` +{teststructs.ParentStructE}.privateStruct.Public: + -: 1 + +: 2 +{teststructs.ParentStructE}.privateStruct.private: + -: 2 + +: 3 +{teststructs.ParentStructE}.PublicStruct.Public: + -: 3 + +: 4 +{teststructs.ParentStructE}.PublicStruct.private: + -: 4 + +: 5`, + }, { + label: label + "ParentStructF", + x: ts.ParentStructF{}, + y: ts.ParentStructF{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructF{}), + }, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructF", + x: ts.ParentStructF{}, + y: ts.ParentStructF{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructF{}), + cmpopts.IgnoreUnexported(ts.PublicStruct{}), + }, + }, { + label: label + "ParentStructF", + x: createStructF(0), + y: createStructF(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructF{}), + }, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructF", + x: createStructF(0), + y: createStructF(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructF{}, ts.PublicStruct{}), + }, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructF", + x: createStructF(0), + y: createStructF(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructF{}, ts.PublicStruct{}, privateStruct), + }, + }, { + label: label + "ParentStructF", + x: createStructF(0), + y: createStructF(1), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructF{}, ts.PublicStruct{}, privateStruct), + }, + wantDiff: ` +{teststructs.ParentStructF}.privateStruct.Public: + -: 1 + +: 2 +{teststructs.ParentStructF}.privateStruct.private: + -: 2 + +: 3 +{teststructs.ParentStructF}.PublicStruct.Public: + -: 3 + +: 4 +{teststructs.ParentStructF}.PublicStruct.private: + -: 4 + +: 5 +{teststructs.ParentStructF}.Public: + -: 5 + +: 6 +{teststructs.ParentStructF}.private: + -: 6 + +: 7`, + }, { + label: label + "ParentStructG", + x: ts.ParentStructG{}, + y: ts.ParentStructG{}, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructG", + x: ts.ParentStructG{}, + y: ts.ParentStructG{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructG{}), + }, + }, { + label: label + "ParentStructG", + x: createStructG(0), + y: createStructG(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructG{}), + }, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructG", + x: createStructG(0), + y: createStructG(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructG{}, privateStruct), + }, + }, { + label: label + "ParentStructG", + x: createStructG(0), + y: createStructG(1), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructG{}, privateStruct), + }, + wantDiff: ` +{*teststructs.ParentStructG}.privateStruct.Public: + -: 1 + +: 2 +{*teststructs.ParentStructG}.privateStruct.private: + -: 2 + +: 3`, + }, { + label: label + "ParentStructH", + x: ts.ParentStructH{}, + y: ts.ParentStructH{}, + }, { + label: label + "ParentStructH", + x: createStructH(0), + y: createStructH(0), + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructH", + x: ts.ParentStructH{}, + y: ts.ParentStructH{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructH{}), + }, + }, { + label: label + "ParentStructH", + x: createStructH(0), + y: createStructH(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructH{}), + }, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructH", + x: createStructH(0), + y: createStructH(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructH{}, ts.PublicStruct{}), + }, + }, { + label: label + "ParentStructH", + x: createStructH(0), + y: createStructH(1), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructH{}, ts.PublicStruct{}), + }, + wantDiff: ` +{*teststructs.ParentStructH}.PublicStruct.Public: + -: 1 + +: 2 +{*teststructs.ParentStructH}.PublicStruct.private: + -: 2 + +: 3`, + }, { + label: label + "ParentStructI", + x: ts.ParentStructI{}, + y: ts.ParentStructI{}, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructI", + x: ts.ParentStructI{}, + y: ts.ParentStructI{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructI{}), + }, + }, { + label: label + "ParentStructI", + x: createStructI(0), + y: createStructI(0), + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructI{}), + }, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructI", + x: createStructI(0), + y: createStructI(0), + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructI{}, ts.PublicStruct{}), + }, + }, { + label: label + "ParentStructI", + x: createStructI(0), + y: createStructI(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructI{}), + }, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructI", + x: createStructI(0), + y: createStructI(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructI{}, ts.PublicStruct{}, privateStruct), + }, + }, { + label: label + "ParentStructI", + x: createStructI(0), + y: createStructI(1), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructI{}, ts.PublicStruct{}, privateStruct), + }, + wantDiff: ` +{*teststructs.ParentStructI}.privateStruct.Public: + -: 1 + +: 2 +{*teststructs.ParentStructI}.privateStruct.private: + -: 2 + +: 3 +{*teststructs.ParentStructI}.PublicStruct.Public: + -: 3 + +: 4 +{*teststructs.ParentStructI}.PublicStruct.private: + -: 4 + +: 5`, + }, { + label: label + "ParentStructJ", + x: ts.ParentStructJ{}, + y: ts.ParentStructJ{}, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructJ", + x: ts.ParentStructJ{}, + y: ts.ParentStructJ{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructJ{}), + }, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructJ", + x: ts.ParentStructJ{}, + y: ts.ParentStructJ{}, + opts: []cmp.Option{ + cmpopts.IgnoreUnexported(ts.ParentStructJ{}, ts.PublicStruct{}), + }, + }, { + label: label + "ParentStructJ", + x: createStructJ(0), + y: createStructJ(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructJ{}, ts.PublicStruct{}), + }, + wantPanic: "cannot handle unexported field", + }, { + label: label + "ParentStructJ", + x: createStructJ(0), + y: createStructJ(0), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructJ{}, ts.PublicStruct{}, privateStruct), + }, + }, { + label: label + "ParentStructJ", + x: createStructJ(0), + y: createStructJ(1), + opts: []cmp.Option{ + cmp.AllowUnexported(ts.ParentStructJ{}, ts.PublicStruct{}, privateStruct), + }, + wantDiff: ` +{*teststructs.ParentStructJ}.privateStruct.Public: + -: 1 + +: 2 +{*teststructs.ParentStructJ}.privateStruct.private: + -: 2 + +: 3 +{*teststructs.ParentStructJ}.PublicStruct.Public: + -: 3 + +: 4 +{*teststructs.ParentStructJ}.PublicStruct.private: + -: 4 + +: 5 +{*teststructs.ParentStructJ}.Public.Public: + -: 7 + +: 8 +{*teststructs.ParentStructJ}.Public.private: + -: 8 + +: 9 +{*teststructs.ParentStructJ}.private.Public: + -: 5 + +: 6 +{*teststructs.ParentStructJ}.private.private: + -: 6 + +: 7`, + }} +} + +func methodTests() []test { + const label = "EqualMethod/" + + // A common mistake that the Equal method is on a pointer receiver, + // but only a non-pointer value is present in the struct. + // A transform can be used to forcibly reference the value. + derefTransform := cmp.FilterPath(func(p cmp.Path) bool { + if len(p) == 0 { + return false + } + t := p[len(p)-1].Type() + if _, ok := t.MethodByName("Equal"); ok || t.Kind() == reflect.Ptr { + return false + } + if m, ok := reflect.PtrTo(t).MethodByName("Equal"); ok { + tf := m.Func.Type() + return !tf.IsVariadic() && tf.NumIn() == 2 && tf.NumOut() == 1 && + tf.In(0).AssignableTo(tf.In(1)) && tf.Out(0) == reflect.TypeOf(true) + } + return false + }, cmp.Transformer("Ref", func(x interface{}) interface{} { + v := reflect.ValueOf(x) + vp := reflect.New(v.Type()) + vp.Elem().Set(v) + return vp.Interface() + })) + + // For each of these types, there is an Equal method defined, which always + // returns true, while the underlying data are fundamentally different. + // Since the method should be called, these are expected to be equal. + return []test{{ + label: label + "StructA", + x: ts.StructA{"NotEqual"}, + y: ts.StructA{"not_equal"}, + }, { + label: label + "StructA", + x: &ts.StructA{"NotEqual"}, + y: &ts.StructA{"not_equal"}, + }, { + label: label + "StructB", + x: ts.StructB{"NotEqual"}, + y: ts.StructB{"not_equal"}, + wantDiff: ` +{teststructs.StructB}.X: + -: "NotEqual" + +: "not_equal"`, + }, { + label: label + "StructB", + x: ts.StructB{"NotEqual"}, + y: ts.StructB{"not_equal"}, + opts: []cmp.Option{derefTransform}, + }, { + label: label + "StructB", + x: &ts.StructB{"NotEqual"}, + y: &ts.StructB{"not_equal"}, + }, { + label: label + "StructC", + x: ts.StructC{"NotEqual"}, + y: ts.StructC{"not_equal"}, + }, { + label: label + "StructC", + x: &ts.StructC{"NotEqual"}, + y: &ts.StructC{"not_equal"}, + }, { + label: label + "StructD", + x: ts.StructD{"NotEqual"}, + y: ts.StructD{"not_equal"}, + wantDiff: ` +{teststructs.StructD}.X: + -: "NotEqual" + +: "not_equal"`, + }, { + label: label + "StructD", + x: ts.StructD{"NotEqual"}, + y: ts.StructD{"not_equal"}, + opts: []cmp.Option{derefTransform}, + }, { + label: label + "StructD", + x: &ts.StructD{"NotEqual"}, + y: &ts.StructD{"not_equal"}, + }, { + label: label + "StructE", + x: ts.StructE{"NotEqual"}, + y: ts.StructE{"not_equal"}, + wantDiff: ` +{teststructs.StructE}.X: + -: "NotEqual" + +: "not_equal"`, + }, { + label: label + "StructE", + x: ts.StructE{"NotEqual"}, + y: ts.StructE{"not_equal"}, + opts: []cmp.Option{derefTransform}, + }, { + label: label + "StructE", + x: &ts.StructE{"NotEqual"}, + y: &ts.StructE{"not_equal"}, + }, { + label: label + "StructF", + x: ts.StructF{"NotEqual"}, + y: ts.StructF{"not_equal"}, + wantDiff: ` +{teststructs.StructF}.X: + -: "NotEqual" + +: "not_equal"`, + }, { + label: label + "StructF", + x: &ts.StructF{"NotEqual"}, + y: &ts.StructF{"not_equal"}, + }, { + label: label + "StructA1", + x: ts.StructA1{ts.StructA{"NotEqual"}, "equal"}, + y: ts.StructA1{ts.StructA{"not_equal"}, "equal"}, + }, { + label: label + "StructA1", + x: ts.StructA1{ts.StructA{"NotEqual"}, "NotEqual"}, + y: ts.StructA1{ts.StructA{"not_equal"}, "not_equal"}, + wantDiff: "{teststructs.StructA1}.X:\n\t-: \"NotEqual\"\n\t+: \"not_equal\"\n", + }, { + label: label + "StructA1", + x: &ts.StructA1{ts.StructA{"NotEqual"}, "equal"}, + y: &ts.StructA1{ts.StructA{"not_equal"}, "equal"}, + }, { + label: label + "StructA1", + x: &ts.StructA1{ts.StructA{"NotEqual"}, "NotEqual"}, + y: &ts.StructA1{ts.StructA{"not_equal"}, "not_equal"}, + wantDiff: "{*teststructs.StructA1}.X:\n\t-: \"NotEqual\"\n\t+: \"not_equal\"\n", + }, { + label: label + "StructB1", + x: ts.StructB1{ts.StructB{"NotEqual"}, "equal"}, + y: ts.StructB1{ts.StructB{"not_equal"}, "equal"}, + opts: []cmp.Option{derefTransform}, + }, { + label: label + "StructB1", + x: ts.StructB1{ts.StructB{"NotEqual"}, "NotEqual"}, + y: ts.StructB1{ts.StructB{"not_equal"}, "not_equal"}, + opts: []cmp.Option{derefTransform}, + wantDiff: "{teststructs.StructB1}.X:\n\t-: \"NotEqual\"\n\t+: \"not_equal\"\n", + }, { + label: label + "StructB1", + x: &ts.StructB1{ts.StructB{"NotEqual"}, "equal"}, + y: &ts.StructB1{ts.StructB{"not_equal"}, "equal"}, + opts: []cmp.Option{derefTransform}, + }, { + label: label + "StructB1", + x: &ts.StructB1{ts.StructB{"NotEqual"}, "NotEqual"}, + y: &ts.StructB1{ts.StructB{"not_equal"}, "not_equal"}, + opts: []cmp.Option{derefTransform}, + wantDiff: "{*teststructs.StructB1}.X:\n\t-: \"NotEqual\"\n\t+: \"not_equal\"\n", + }, { + label: label + "StructC1", + x: ts.StructC1{ts.StructC{"NotEqual"}, "NotEqual"}, + y: ts.StructC1{ts.StructC{"not_equal"}, "not_equal"}, + }, { + label: label + "StructC1", + x: &ts.StructC1{ts.StructC{"NotEqual"}, "NotEqual"}, + y: &ts.StructC1{ts.StructC{"not_equal"}, "not_equal"}, + }, { + label: label + "StructD1", + x: ts.StructD1{ts.StructD{"NotEqual"}, "NotEqual"}, + y: ts.StructD1{ts.StructD{"not_equal"}, "not_equal"}, + wantDiff: ` +{teststructs.StructD1}.StructD.X: + -: "NotEqual" + +: "not_equal" +{teststructs.StructD1}.X: + -: "NotEqual" + +: "not_equal"`, + }, { + label: label + "StructD1", + x: ts.StructD1{ts.StructD{"NotEqual"}, "NotEqual"}, + y: ts.StructD1{ts.StructD{"not_equal"}, "not_equal"}, + opts: []cmp.Option{derefTransform}, + }, { + label: label + "StructD1", + x: &ts.StructD1{ts.StructD{"NotEqual"}, "NotEqual"}, + y: &ts.StructD1{ts.StructD{"not_equal"}, "not_equal"}, + }, { + label: label + "StructE1", + x: ts.StructE1{ts.StructE{"NotEqual"}, "NotEqual"}, + y: ts.StructE1{ts.StructE{"not_equal"}, "not_equal"}, + wantDiff: ` +{teststructs.StructE1}.StructE.X: + -: "NotEqual" + +: "not_equal" +{teststructs.StructE1}.X: + -: "NotEqual" + +: "not_equal"`, + }, { + label: label + "StructE1", + x: ts.StructE1{ts.StructE{"NotEqual"}, "NotEqual"}, + y: ts.StructE1{ts.StructE{"not_equal"}, "not_equal"}, + opts: []cmp.Option{derefTransform}, + }, { + label: label + "StructE1", + x: &ts.StructE1{ts.StructE{"NotEqual"}, "NotEqual"}, + y: &ts.StructE1{ts.StructE{"not_equal"}, "not_equal"}, + }, { + label: label + "StructF1", + x: ts.StructF1{ts.StructF{"NotEqual"}, "NotEqual"}, + y: ts.StructF1{ts.StructF{"not_equal"}, "not_equal"}, + wantDiff: ` +{teststructs.StructF1}.StructF.X: + -: "NotEqual" + +: "not_equal" +{teststructs.StructF1}.X: + -: "NotEqual" + +: "not_equal"`, + }, { + label: label + "StructF1", + x: &ts.StructF1{ts.StructF{"NotEqual"}, "NotEqual"}, + y: &ts.StructF1{ts.StructF{"not_equal"}, "not_equal"}, + }, { + label: label + "StructA2", + x: ts.StructA2{&ts.StructA{"NotEqual"}, "equal"}, + y: ts.StructA2{&ts.StructA{"not_equal"}, "equal"}, + }, { + label: label + "StructA2", + x: ts.StructA2{&ts.StructA{"NotEqual"}, "NotEqual"}, + y: ts.StructA2{&ts.StructA{"not_equal"}, "not_equal"}, + wantDiff: "{teststructs.StructA2}.X:\n\t-: \"NotEqual\"\n\t+: \"not_equal\"\n", + }, { + label: label + "StructA2", + x: &ts.StructA2{&ts.StructA{"NotEqual"}, "equal"}, + y: &ts.StructA2{&ts.StructA{"not_equal"}, "equal"}, + }, { + label: label + "StructA2", + x: &ts.StructA2{&ts.StructA{"NotEqual"}, "NotEqual"}, + y: &ts.StructA2{&ts.StructA{"not_equal"}, "not_equal"}, + wantDiff: "{*teststructs.StructA2}.X:\n\t-: \"NotEqual\"\n\t+: \"not_equal\"\n", + }, { + label: label + "StructB2", + x: ts.StructB2{&ts.StructB{"NotEqual"}, "equal"}, + y: ts.StructB2{&ts.StructB{"not_equal"}, "equal"}, + }, { + label: label + "StructB2", + x: ts.StructB2{&ts.StructB{"NotEqual"}, "NotEqual"}, + y: ts.StructB2{&ts.StructB{"not_equal"}, "not_equal"}, + wantDiff: "{teststructs.StructB2}.X:\n\t-: \"NotEqual\"\n\t+: \"not_equal\"\n", + }, { + label: label + "StructB2", + x: &ts.StructB2{&ts.StructB{"NotEqual"}, "equal"}, + y: &ts.StructB2{&ts.StructB{"not_equal"}, "equal"}, + }, { + label: label + "StructB2", + x: &ts.StructB2{&ts.StructB{"NotEqual"}, "NotEqual"}, + y: &ts.StructB2{&ts.StructB{"not_equal"}, "not_equal"}, + wantDiff: "{*teststructs.StructB2}.X:\n\t-: \"NotEqual\"\n\t+: \"not_equal\"\n", + }, { + label: label + "StructC2", + x: ts.StructC2{&ts.StructC{"NotEqual"}, "NotEqual"}, + y: ts.StructC2{&ts.StructC{"not_equal"}, "not_equal"}, + }, { + label: label + "StructC2", + x: &ts.StructC2{&ts.StructC{"NotEqual"}, "NotEqual"}, + y: &ts.StructC2{&ts.StructC{"not_equal"}, "not_equal"}, + }, { + label: label + "StructD2", + x: ts.StructD2{&ts.StructD{"NotEqual"}, "NotEqual"}, + y: ts.StructD2{&ts.StructD{"not_equal"}, "not_equal"}, + }, { + label: label + "StructD2", + x: &ts.StructD2{&ts.StructD{"NotEqual"}, "NotEqual"}, + y: &ts.StructD2{&ts.StructD{"not_equal"}, "not_equal"}, + }, { + label: label + "StructE2", + x: ts.StructE2{&ts.StructE{"NotEqual"}, "NotEqual"}, + y: ts.StructE2{&ts.StructE{"not_equal"}, "not_equal"}, + }, { + label: label + "StructE2", + x: &ts.StructE2{&ts.StructE{"NotEqual"}, "NotEqual"}, + y: &ts.StructE2{&ts.StructE{"not_equal"}, "not_equal"}, + }, { + label: label + "StructF2", + x: ts.StructF2{&ts.StructF{"NotEqual"}, "NotEqual"}, + y: ts.StructF2{&ts.StructF{"not_equal"}, "not_equal"}, + }, { + label: label + "StructF2", + x: &ts.StructF2{&ts.StructF{"NotEqual"}, "NotEqual"}, + y: &ts.StructF2{&ts.StructF{"not_equal"}, "not_equal"}, + }, { + label: label + "StructNo", + x: ts.StructNo{"NotEqual"}, + y: ts.StructNo{"not_equal"}, + wantDiff: "{teststructs.StructNo}.X:\n\t-: \"NotEqual\"\n\t+: \"not_equal\"\n", + }, { + label: label + "AssignA", + x: ts.AssignA(func() int { return 0 }), + y: ts.AssignA(func() int { return 1 }), + }, { + label: label + "AssignB", + x: ts.AssignB(struct{ A int }{0}), + y: ts.AssignB(struct{ A int }{1}), + }, { + label: label + "AssignC", + x: ts.AssignC(make(chan bool)), + y: ts.AssignC(make(chan bool)), + }, { + label: label + "AssignD", + x: ts.AssignD(make(chan bool)), + y: ts.AssignD(make(chan bool)), + }} +} + +func project1Tests() []test { + const label = "Project1" + + ignoreUnexported := cmpopts.IgnoreUnexported( + ts.EagleImmutable{}, + ts.DreamerImmutable{}, + ts.SlapImmutable{}, + ts.GoatImmutable{}, + ts.DonkeyImmutable{}, + ts.LoveRadius{}, + ts.SummerLove{}, + ts.SummerLoveSummary{}, + ) + + createEagle := func() ts.Eagle { + return ts.Eagle{ + Name: "eagle", + Hounds: []string{"buford", "tannen"}, + Desc: "some description", + Dreamers: []ts.Dreamer{{}, { + Name: "dreamer2", + Animal: []interface{}{ + ts.Goat{ + Target: "corporation", + Immutable: &ts.GoatImmutable{ + ID: "southbay", + State: (*pb.Goat_States)(intPtr(5)), + Started: now, + }, + }, + ts.Donkey{}, + }, + Amoeba: 53, + }}, + Slaps: []ts.Slap{{ + Name: "slapID", + Args: &pb.MetaData{Stringer: pb.Stringer{"metadata"}}, + Immutable: &ts.SlapImmutable{ + ID: "immutableSlap", + MildSlap: true, + Started: now, + LoveRadius: &ts.LoveRadius{ + Summer: &ts.SummerLove{ + Summary: &ts.SummerLoveSummary{ + Devices: []string{"foo", "bar", "baz"}, + ChangeType: []pb.SummerType{1, 2, 3}, + }, + }, + }, + }, + }}, + Immutable: &ts.EagleImmutable{ + ID: "eagleID", + Birthday: now, + MissingCall: (*pb.Eagle_MissingCalls)(intPtr(55)), + }, + } + } + + return []test{{ + label: label, + x: ts.Eagle{Slaps: []ts.Slap{{ + Args: &pb.MetaData{Stringer: pb.Stringer{"metadata"}}, + }}}, + y: ts.Eagle{Slaps: []ts.Slap{{ + Args: &pb.MetaData{Stringer: pb.Stringer{"metadata"}}, + }}}, + wantPanic: "cannot handle unexported field", + }, { + label: label, + x: ts.Eagle{Slaps: []ts.Slap{{ + Args: &pb.MetaData{Stringer: pb.Stringer{"metadata"}}, + }}}, + y: ts.Eagle{Slaps: []ts.Slap{{ + Args: &pb.MetaData{Stringer: pb.Stringer{"metadata"}}, + }}}, + opts: []cmp.Option{cmp.Comparer(pb.Equal)}, + }, { + label: label, + x: ts.Eagle{Slaps: []ts.Slap{{}, {}, {}, {}, { + Args: &pb.MetaData{Stringer: pb.Stringer{"metadata"}}, + }}}, + y: ts.Eagle{Slaps: []ts.Slap{{}, {}, {}, {}, { + Args: &pb.MetaData{Stringer: pb.Stringer{"metadata2"}}, + }}}, + opts: []cmp.Option{cmp.Comparer(pb.Equal)}, + wantDiff: "{teststructs.Eagle}.Slaps[4].Args:\n\t-: \"metadata\"\n\t+: \"metadata2\"\n", + }, { + label: label, + x: createEagle(), + y: createEagle(), + opts: []cmp.Option{ignoreUnexported, cmp.Comparer(pb.Equal)}, + }, { + label: label, + x: func() ts.Eagle { + eg := createEagle() + eg.Dreamers[1].Animal[0].(ts.Goat).Immutable.ID = "southbay2" + eg.Dreamers[1].Animal[0].(ts.Goat).Immutable.State = (*pb.Goat_States)(intPtr(6)) + eg.Slaps[0].Immutable.MildSlap = false + return eg + }(), + y: func() ts.Eagle { + eg := createEagle() + devs := eg.Slaps[0].Immutable.LoveRadius.Summer.Summary.Devices + eg.Slaps[0].Immutable.LoveRadius.Summer.Summary.Devices = devs[:1] + return eg + }(), + opts: []cmp.Option{ignoreUnexported, cmp.Comparer(pb.Equal)}, + wantDiff: ` +{teststructs.Eagle}.Dreamers[1].Animal[0].(teststructs.Goat).Immutable.ID: + -: "southbay2" + +: "southbay" +*{teststructs.Eagle}.Dreamers[1].Animal[0].(teststructs.Goat).Immutable.State: + -: testprotos.Goat_States(6) + +: testprotos.Goat_States(5) +{teststructs.Eagle}.Slaps[0].Immutable.MildSlap: + -: false + +: true +{teststructs.Eagle}.Slaps[0].Immutable.LoveRadius.Summer.Summary.Devices[1->?]: + -: "bar" + +: +{teststructs.Eagle}.Slaps[0].Immutable.LoveRadius.Summer.Summary.Devices[2->?]: + -: "baz" + +: `, + }} +} + +type germSorter []*pb.Germ + +func (gs germSorter) Len() int { return len(gs) } +func (gs germSorter) Less(i, j int) bool { return gs[i].String() < gs[j].String() } +func (gs germSorter) Swap(i, j int) { gs[i], gs[j] = gs[j], gs[i] } + +func project2Tests() []test { + const label = "Project2" + + sortGerms := cmp.FilterValues(func(x, y []*pb.Germ) bool { + ok1 := sort.IsSorted(germSorter(x)) + ok2 := sort.IsSorted(germSorter(y)) + return !ok1 || !ok2 + }, cmp.Transformer("Sort", func(in []*pb.Germ) []*pb.Germ { + out := append([]*pb.Germ(nil), in...) // Make copy + sort.Sort(germSorter(out)) + return out + })) + + equalDish := cmp.Comparer(func(x, y *ts.Dish) bool { + if x == nil || y == nil { + return x == nil && y == nil + } + px, err1 := x.Proto() + py, err2 := y.Proto() + if err1 != nil || err2 != nil { + return err1 == err2 + } + return pb.Equal(px, py) + }) + + createBatch := func() ts.GermBatch { + return ts.GermBatch{ + DirtyGerms: map[int32][]*pb.Germ{ + 17: { + {Stringer: pb.Stringer{"germ1"}}, + }, + 18: { + {Stringer: pb.Stringer{"germ2"}}, + {Stringer: pb.Stringer{"germ3"}}, + {Stringer: pb.Stringer{"germ4"}}, + }, + }, + GermMap: map[int32]*pb.Germ{ + 13: {Stringer: pb.Stringer{"germ13"}}, + 21: {Stringer: pb.Stringer{"germ21"}}, + }, + DishMap: map[int32]*ts.Dish{ + 0: ts.CreateDish(nil, io.EOF), + 1: ts.CreateDish(nil, io.ErrUnexpectedEOF), + 2: ts.CreateDish(&pb.Dish{Stringer: pb.Stringer{"dish"}}, nil), + }, + HasPreviousResult: true, + DirtyID: 10, + GermStrain: 421, + InfectedAt: now, + } + } + + return []test{{ + label: label, + x: createBatch(), + y: createBatch(), + wantPanic: "cannot handle unexported field", + }, { + label: label, + x: createBatch(), + y: createBatch(), + opts: []cmp.Option{cmp.Comparer(pb.Equal), sortGerms, equalDish}, + }, { + label: label, + x: createBatch(), + y: func() ts.GermBatch { + gb := createBatch() + s := gb.DirtyGerms[18] + s[0], s[1], s[2] = s[1], s[2], s[0] + return gb + }(), + opts: []cmp.Option{cmp.Comparer(pb.Equal), equalDish}, + wantDiff: ` +{teststructs.GermBatch}.DirtyGerms[18][0->?]: + -: "germ2" + +: +{teststructs.GermBatch}.DirtyGerms[18][?->2]: + -: + +: "germ2"`, + }, { + label: label, + x: createBatch(), + y: func() ts.GermBatch { + gb := createBatch() + s := gb.DirtyGerms[18] + s[0], s[1], s[2] = s[1], s[2], s[0] + return gb + }(), + opts: []cmp.Option{cmp.Comparer(pb.Equal), sortGerms, equalDish}, + }, { + label: label, + x: func() ts.GermBatch { + gb := createBatch() + delete(gb.DirtyGerms, 17) + gb.DishMap[1] = nil + return gb + }(), + y: func() ts.GermBatch { + gb := createBatch() + gb.DirtyGerms[18] = gb.DirtyGerms[18][:2] + gb.GermStrain = 22 + return gb + }(), + opts: []cmp.Option{cmp.Comparer(pb.Equal), sortGerms, equalDish}, + wantDiff: ` +{teststructs.GermBatch}.DirtyGerms[17]: + -: + +: []*testprotos.Germ{"germ1"} +{teststructs.GermBatch}.DirtyGerms[18][2->?]: + -: "germ4" + +: +{teststructs.GermBatch}.DishMap[1]: + -: (*teststructs.Dish)(nil) + +: &teststructs.Dish{err: &errors.errorString{s: "unexpected EOF"}} +{teststructs.GermBatch}.GermStrain: + -: 421 + +: 22`, + }} +} + +func project3Tests() []test { + const label = "Project3" + + allowVisibility := cmp.AllowUnexported(ts.Dirt{}) + + ignoreLocker := cmpopts.IgnoreInterfaces(struct{ sync.Locker }{}) + + transformProtos := cmp.Transformer("", func(x pb.Dirt) *pb.Dirt { + return &x + }) + + equalTable := cmp.Comparer(func(x, y ts.Table) bool { + tx, ok1 := x.(*ts.MockTable) + ty, ok2 := y.(*ts.MockTable) + if !ok1 || !ok2 { + panic("table type must be MockTable") + } + return cmp.Equal(tx.State(), ty.State()) + }) + + createDirt := func() (d ts.Dirt) { + d.SetTable(ts.CreateMockTable([]string{"a", "b", "c"})) + d.SetTimestamp(12345) + d.Discord = 554 + d.Proto = pb.Dirt{Stringer: pb.Stringer{"proto"}} + d.SetWizard(map[string]*pb.Wizard{ + "harry": {Stringer: pb.Stringer{"potter"}}, + "albus": {Stringer: pb.Stringer{"dumbledore"}}, + }) + d.SetLastTime(54321) + return d + } + + return []test{{ + label: label, + x: createDirt(), + y: createDirt(), + wantPanic: "cannot handle unexported field", + }, { + label: label, + x: createDirt(), + y: createDirt(), + opts: []cmp.Option{allowVisibility, ignoreLocker, cmp.Comparer(pb.Equal), equalTable}, + wantPanic: "cannot handle unexported field", + }, { + label: label, + x: createDirt(), + y: createDirt(), + opts: []cmp.Option{allowVisibility, transformProtos, ignoreLocker, cmp.Comparer(pb.Equal), equalTable}, + }, { + label: label, + x: func() ts.Dirt { + d := createDirt() + d.SetTable(ts.CreateMockTable([]string{"a", "c"})) + d.Proto = pb.Dirt{Stringer: pb.Stringer{"blah"}} + return d + }(), + y: func() ts.Dirt { + d := createDirt() + d.Discord = 500 + d.SetWizard(map[string]*pb.Wizard{ + "harry": {Stringer: pb.Stringer{"otter"}}, + }) + return d + }(), + opts: []cmp.Option{allowVisibility, transformProtos, ignoreLocker, cmp.Comparer(pb.Equal), equalTable}, + wantDiff: ` +{teststructs.Dirt}.table: + -: &teststructs.MockTable{state: []string{"a", "c"}} + +: &teststructs.MockTable{state: []string{"a", "b", "c"}} +{teststructs.Dirt}.Discord: + -: teststructs.DiscordState(554) + +: teststructs.DiscordState(500) +λ({teststructs.Dirt}.Proto): + -: "blah" + +: "proto" +{teststructs.Dirt}.wizard["albus"]: + -: "dumbledore" + +: +{teststructs.Dirt}.wizard["harry"]: + -: "potter" + +: "otter"`, + }} +} + +func project4Tests() []test { + const label = "Project4" + + allowVisibility := cmp.AllowUnexported( + ts.Cartel{}, + ts.Headquarter{}, + ts.Poison{}, + ) + + transformProtos := cmp.Transformer("", func(x pb.Restrictions) *pb.Restrictions { + return &x + }) + + createCartel := func() ts.Cartel { + var p ts.Poison + p.SetPoisonType(5) + p.SetExpiration(now) + p.SetManufactuer("acme") + + var hq ts.Headquarter + hq.SetID(5) + hq.SetLocation("moon") + hq.SetSubDivisions([]string{"alpha", "bravo", "charlie"}) + hq.SetMetaData(&pb.MetaData{Stringer: pb.Stringer{"metadata"}}) + hq.SetPublicMessage([]byte{1, 2, 3, 4, 5}) + hq.SetHorseBack("abcdef") + hq.SetStatus(44) + + var c ts.Cartel + c.Headquarter = hq + c.SetSource("mars") + c.SetCreationTime(now) + c.SetBoss("al capone") + c.SetPoisons([]*ts.Poison{&p}) + + return c + } + + return []test{{ + label: label, + x: createCartel(), + y: createCartel(), + wantPanic: "cannot handle unexported field", + }, { + label: label, + x: createCartel(), + y: createCartel(), + opts: []cmp.Option{allowVisibility, cmp.Comparer(pb.Equal)}, + wantPanic: "cannot handle unexported field", + }, { + label: label, + x: createCartel(), + y: createCartel(), + opts: []cmp.Option{allowVisibility, transformProtos, cmp.Comparer(pb.Equal)}, + }, { + label: label, + x: func() ts.Cartel { + d := createCartel() + var p1, p2 ts.Poison + p1.SetPoisonType(1) + p1.SetExpiration(now) + p1.SetManufactuer("acme") + p2.SetPoisonType(2) + p2.SetManufactuer("acme2") + d.SetPoisons([]*ts.Poison{&p1, &p2}) + return d + }(), + y: func() ts.Cartel { + d := createCartel() + d.SetSubDivisions([]string{"bravo", "charlie"}) + d.SetPublicMessage([]byte{1, 2, 4, 3, 5}) + return d + }(), + opts: []cmp.Option{allowVisibility, transformProtos, cmp.Comparer(pb.Equal)}, + wantDiff: ` +{teststructs.Cartel}.Headquarter.subDivisions[0->?]: + -: "alpha" + +: +{teststructs.Cartel}.Headquarter.publicMessage[2]: + -: 0x03 + +: 0x04 +{teststructs.Cartel}.Headquarter.publicMessage[3]: + -: 0x04 + +: 0x03 +{teststructs.Cartel}.poisons[0].poisonType: + -: testprotos.PoisonType(1) + +: testprotos.PoisonType(5) +{teststructs.Cartel}.poisons[1->?]: + -: &teststructs.Poison{poisonType: testprotos.PoisonType(2), manufactuer: "acme2"} + +: `, + }} +} + +// TODO: Delete this hack when we drop Go1.6 support. +func tRunParallel(t *testing.T, name string, f func(t *testing.T)) { + type runner interface { + Run(string, func(t *testing.T)) bool + } + var ti interface{} = t + if r, ok := ti.(runner); ok { + r.Run(name, func(t *testing.T) { + t.Parallel() + f(t) + }) + } else { + // Cannot run sub-tests in parallel in Go1.6. + t.Logf("Test: %s", name) + f(t) + } +} diff --git a/vendor/github.com/google/go-cmp/cmp/example_test.go b/vendor/github.com/google/go-cmp/cmp/example_test.go new file mode 100644 index 000000000..8729db398 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/example_test.go @@ -0,0 +1,374 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +package cmp_test + +import ( + "fmt" + "math" + "reflect" + "sort" + "strings" + + "github.com/google/go-cmp/cmp" +) + +// TODO: Re-write these examples in terms of how you actually use the +// fundamental options and filters and not in terms of what cool things you can +// do with them since that overlaps with cmp/cmpopts. + +// Use Diff for printing out human-readable errors for test cases comparing +// nested or structured data. +func ExampleDiff_testing() { + // Code under test: + type ShipManifest struct { + Name string + Crew map[string]string + Androids int + Stolen bool + } + + // AddCrew tries to add the given crewmember to the manifest. + AddCrew := func(m *ShipManifest, name, title string) { + if m.Crew == nil { + m.Crew = make(map[string]string) + } + m.Crew[title] = name + } + + // Test function: + tests := []struct { + desc string + before *ShipManifest + name, title string + after *ShipManifest + }{ + { + desc: "add to empty", + before: &ShipManifest{}, + name: "Zaphod Beeblebrox", + title: "Galactic President", + after: &ShipManifest{ + Crew: map[string]string{ + "Zaphod Beeblebrox": "Galactic President", + }, + }, + }, + { + desc: "add another", + before: &ShipManifest{ + Crew: map[string]string{ + "Zaphod Beeblebrox": "Galactic President", + }, + }, + name: "Trillian", + title: "Human", + after: &ShipManifest{ + Crew: map[string]string{ + "Zaphod Beeblebrox": "Galactic President", + "Trillian": "Human", + }, + }, + }, + { + desc: "overwrite", + before: &ShipManifest{ + Crew: map[string]string{ + "Zaphod Beeblebrox": "Galactic President", + }, + }, + name: "Zaphod Beeblebrox", + title: "Just this guy, you know?", + after: &ShipManifest{ + Crew: map[string]string{ + "Zaphod Beeblebrox": "Just this guy, you know?", + }, + }, + }, + } + + var t fakeT + for _, test := range tests { + AddCrew(test.before, test.name, test.title) + if diff := cmp.Diff(test.before, test.after); diff != "" { + t.Errorf("%s: after AddCrew, manifest differs: (-got +want)\n%s", test.desc, diff) + } + } + + // Output: + // add to empty: after AddCrew, manifest differs: (-got +want) + // {*cmp_test.ShipManifest}.Crew["Galactic President"]: + // -: "Zaphod Beeblebrox" + // +: + // {*cmp_test.ShipManifest}.Crew["Zaphod Beeblebrox"]: + // -: + // +: "Galactic President" + // + // add another: after AddCrew, manifest differs: (-got +want) + // {*cmp_test.ShipManifest}.Crew["Human"]: + // -: "Trillian" + // +: + // {*cmp_test.ShipManifest}.Crew["Trillian"]: + // -: + // +: "Human" + // + // overwrite: after AddCrew, manifest differs: (-got +want) + // {*cmp_test.ShipManifest}.Crew["Just this guy, you know?"]: + // -: "Zaphod Beeblebrox" + // +: + // {*cmp_test.ShipManifest}.Crew["Zaphod Beeblebrox"]: + // -: "Galactic President" + // +: "Just this guy, you know?" +} + +// Approximate equality for floats can be handled by defining a custom +// comparer on floats that determines two values to be equal if they are within +// some range of each other. +// +// This example is for demonstrative purposes; use cmpopts.EquateApprox instead. +func ExampleOption_approximateFloats() { + // This Comparer only operates on float64. + // To handle float32s, either define a similar function for that type + // or use a Transformer to convert float32s into float64s. + opt := cmp.Comparer(func(x, y float64) bool { + delta := math.Abs(x - y) + mean := math.Abs(x+y) / 2.0 + return delta/mean < 0.00001 + }) + + x := []float64{1.0, 1.1, 1.2, math.Pi} + y := []float64{1.0, 1.1, 1.2, 3.14159265359} // Accurate enough to Pi + z := []float64{1.0, 1.1, 1.2, 3.1415} // Diverges too far from Pi + + fmt.Println(cmp.Equal(x, y, opt)) + fmt.Println(cmp.Equal(y, z, opt)) + fmt.Println(cmp.Equal(z, x, opt)) + + // Output: + // true + // false + // false +} + +// Normal floating-point arithmetic defines == to be false when comparing +// NaN with itself. In certain cases, this is not the desired property. +// +// This example is for demonstrative purposes; use cmpopts.EquateNaNs instead. +func ExampleOption_equalNaNs() { + // This Comparer only operates on float64. + // To handle float32s, either define a similar function for that type + // or use a Transformer to convert float32s into float64s. + opt := cmp.Comparer(func(x, y float64) bool { + return (math.IsNaN(x) && math.IsNaN(y)) || x == y + }) + + x := []float64{1.0, math.NaN(), math.E, -0.0, +0.0} + y := []float64{1.0, math.NaN(), math.E, -0.0, +0.0} + z := []float64{1.0, math.NaN(), math.Pi, -0.0, +0.0} // Pi constant instead of E + + fmt.Println(cmp.Equal(x, y, opt)) + fmt.Println(cmp.Equal(y, z, opt)) + fmt.Println(cmp.Equal(z, x, opt)) + + // Output: + // true + // false + // false +} + +// To have floating-point comparisons combine both properties of NaN being +// equal to itself and also approximate equality of values, filters are needed +// to restrict the scope of the comparison so that they are composable. +// +// This example is for demonstrative purposes; +// use cmpopts.EquateNaNs and cmpopts.EquateApprox instead. +func ExampleOption_equalNaNsAndApproximateFloats() { + alwaysEqual := cmp.Comparer(func(_, _ interface{}) bool { return true }) + + opts := cmp.Options{ + // This option declares that a float64 comparison is equal only if + // both inputs are NaN. + cmp.FilterValues(func(x, y float64) bool { + return math.IsNaN(x) && math.IsNaN(y) + }, alwaysEqual), + + // This option declares approximate equality on float64s only if + // both inputs are not NaN. + cmp.FilterValues(func(x, y float64) bool { + return !math.IsNaN(x) && !math.IsNaN(y) + }, cmp.Comparer(func(x, y float64) bool { + delta := math.Abs(x - y) + mean := math.Abs(x+y) / 2.0 + return delta/mean < 0.00001 + })), + } + + x := []float64{math.NaN(), 1.0, 1.1, 1.2, math.Pi} + y := []float64{math.NaN(), 1.0, 1.1, 1.2, 3.14159265359} // Accurate enough to Pi + z := []float64{math.NaN(), 1.0, 1.1, 1.2, 3.1415} // Diverges too far from Pi + + fmt.Println(cmp.Equal(x, y, opts)) + fmt.Println(cmp.Equal(y, z, opts)) + fmt.Println(cmp.Equal(z, x, opts)) + + // Output: + // true + // false + // false +} + +// Sometimes, an empty map or slice is considered equal to an allocated one +// of zero length. +// +// This example is for demonstrative purposes; use cmpopts.EquateEmpty instead. +func ExampleOption_equalEmpty() { + alwaysEqual := cmp.Comparer(func(_, _ interface{}) bool { return true }) + + // This option handles slices and maps of any type. + opt := cmp.FilterValues(func(x, y interface{}) bool { + vx, vy := reflect.ValueOf(x), reflect.ValueOf(y) + return (vx.IsValid() && vy.IsValid() && vx.Type() == vy.Type()) && + (vx.Kind() == reflect.Slice || vx.Kind() == reflect.Map) && + (vx.Len() == 0 && vy.Len() == 0) + }, alwaysEqual) + + type S struct { + A []int + B map[string]bool + } + x := S{nil, make(map[string]bool, 100)} + y := S{make([]int, 0, 200), nil} + z := S{[]int{0}, nil} // []int has a single element (i.e., not empty) + + fmt.Println(cmp.Equal(x, y, opt)) + fmt.Println(cmp.Equal(y, z, opt)) + fmt.Println(cmp.Equal(z, x, opt)) + + // Output: + // true + // false + // false +} + +// Two slices may be considered equal if they have the same elements, +// regardless of the order that they appear in. Transformations can be used +// to sort the slice. +// +// This example is for demonstrative purposes; use cmpopts.SortSlices instead. +func ExampleOption_sortedSlice() { + // This Transformer sorts a []int. + // Since the transformer transforms []int into []int, there is problem where + // this is recursively applied forever. To prevent this, use a FilterValues + // to first check for the condition upon which the transformer ought to apply. + trans := cmp.FilterValues(func(x, y []int) bool { + return !sort.IntsAreSorted(x) || !sort.IntsAreSorted(y) + }, cmp.Transformer("Sort", func(in []int) []int { + out := append([]int(nil), in...) // Copy input to avoid mutating it + sort.Ints(out) + return out + })) + + x := struct{ Ints []int }{[]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}} + y := struct{ Ints []int }{[]int{2, 8, 0, 9, 6, 1, 4, 7, 3, 5}} + z := struct{ Ints []int }{[]int{0, 0, 1, 2, 3, 4, 5, 6, 7, 8}} + + fmt.Println(cmp.Equal(x, y, trans)) + fmt.Println(cmp.Equal(y, z, trans)) + fmt.Println(cmp.Equal(z, x, trans)) + + // Output: + // true + // false + // false +} + +type otherString string + +func (x otherString) Equal(y otherString) bool { + return strings.ToLower(string(x)) == strings.ToLower(string(y)) +} + +// If the Equal method defined on a type is not suitable, the type can be be +// dynamically transformed to be stripped of the Equal method (or any method +// for that matter). +func ExampleOption_avoidEqualMethod() { + // Suppose otherString.Equal performs a case-insensitive equality, + // which is too loose for our needs. + // We can avoid the methods of otherString by declaring a new type. + type myString otherString + + // This transformer converts otherString to myString, allowing Equal to use + // other Options to determine equality. + trans := cmp.Transformer("", func(in otherString) myString { + return myString(in) + }) + + x := []otherString{"foo", "bar", "baz"} + y := []otherString{"fOO", "bAr", "Baz"} // Same as before, but with different case + + fmt.Println(cmp.Equal(x, y)) // Equal because of case-insensitivity + fmt.Println(cmp.Equal(x, y, trans)) // Not equal because of more exact equality + + // Output: + // true + // false +} + +func roundF64(z float64) float64 { + if z < 0 { + return math.Ceil(z - 0.5) + } + return math.Floor(z + 0.5) +} + +// The complex numbers complex64 and complex128 can really just be decomposed +// into a pair of float32 or float64 values. It would be convenient to be able +// define only a single comparator on float64 and have float32, complex64, and +// complex128 all be able to use that comparator. Transformations can be used +// to handle this. +func ExampleOption_transformComplex() { + opts := []cmp.Option{ + // This transformer decomposes complex128 into a pair of float64s. + cmp.Transformer("T1", func(in complex128) (out struct{ Real, Imag float64 }) { + out.Real, out.Imag = real(in), imag(in) + return out + }), + // This transformer converts complex64 to complex128 to allow the + // above transform to take effect. + cmp.Transformer("T2", func(in complex64) complex128 { + return complex128(in) + }), + // This transformer converts float32 to float64. + cmp.Transformer("T3", func(in float32) float64 { + return float64(in) + }), + // This equality function compares float64s as rounded integers. + cmp.Comparer(func(x, y float64) bool { + return roundF64(x) == roundF64(y) + }), + } + + x := []interface{}{ + complex128(3.0), complex64(5.1 + 2.9i), float32(-1.2), float64(12.3), + } + y := []interface{}{ + complex128(3.1), complex64(4.9 + 3.1i), float32(-1.3), float64(11.7), + } + z := []interface{}{ + complex128(3.8), complex64(4.9 + 3.1i), float32(-1.3), float64(11.7), + } + + fmt.Println(cmp.Equal(x, y, opts...)) + fmt.Println(cmp.Equal(y, z, opts...)) + fmt.Println(cmp.Equal(z, x, opts...)) + + // Output: + // true + // false + // false +} + +type fakeT struct{} + +func (t fakeT) Errorf(format string, args ...interface{}) { fmt.Printf(format+"\n", args...) } diff --git a/vendor/github.com/google/go-cmp/cmp/internal/diff/debug_disable.go b/vendor/github.com/google/go-cmp/cmp/internal/diff/debug_disable.go new file mode 100644 index 000000000..42afa4960 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/internal/diff/debug_disable.go @@ -0,0 +1,17 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +// +build !debug + +package diff + +var debug debugger + +type debugger struct{} + +func (debugger) Begin(_, _ int, f EqualFunc, _, _ *EditScript) EqualFunc { + return f +} +func (debugger) Update() {} +func (debugger) Finish() {} diff --git a/vendor/github.com/google/go-cmp/cmp/internal/diff/debug_enable.go b/vendor/github.com/google/go-cmp/cmp/internal/diff/debug_enable.go new file mode 100644 index 000000000..ba46c62e1 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/internal/diff/debug_enable.go @@ -0,0 +1,122 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +// +build debug + +package diff + +import ( + "fmt" + "strings" + "sync" + "time" +) + +// The algorithm can be seen running in real-time by enabling debugging: +// go test -tags=debug -v +// +// Example output: +// === RUN TestDifference/#34 +// ┌───────────────────────────────┐ +// │ \ · · · · · · · · · · · · · · │ +// │ · # · · · · · · · · · · · · · │ +// │ · \ · · · · · · · · · · · · · │ +// │ · · \ · · · · · · · · · · · · │ +// │ · · · X # · · · · · · · · · · │ +// │ · · · # \ · · · · · · · · · · │ +// │ · · · · · # # · · · · · · · · │ +// │ · · · · · # \ · · · · · · · · │ +// │ · · · · · · · \ · · · · · · · │ +// │ · · · · · · · · \ · · · · · · │ +// │ · · · · · · · · · \ · · · · · │ +// │ · · · · · · · · · · \ · · # · │ +// │ · · · · · · · · · · · \ # # · │ +// │ · · · · · · · · · · · # # # · │ +// │ · · · · · · · · · · # # # # · │ +// │ · · · · · · · · · # # # # # · │ +// │ · · · · · · · · · · · · · · \ │ +// └───────────────────────────────┘ +// [.Y..M.XY......YXYXY.|] +// +// The grid represents the edit-graph where the horizontal axis represents +// list X and the vertical axis represents list Y. The start of the two lists +// is the top-left, while the ends are the bottom-right. The '·' represents +// an unexplored node in the graph. The '\' indicates that the two symbols +// from list X and Y are equal. The 'X' indicates that two symbols are similar +// (but not exactly equal) to each other. The '#' indicates that the two symbols +// are different (and not similar). The algorithm traverses this graph trying to +// make the paths starting in the top-left and the bottom-right connect. +// +// The series of '.', 'X', 'Y', and 'M' characters at the bottom represents +// the currently established path from the forward and reverse searches, +// seperated by a '|' character. + +const ( + updateDelay = 100 * time.Millisecond + finishDelay = 500 * time.Millisecond + ansiTerminal = true // ANSI escape codes used to move terminal cursor +) + +var debug debugger + +type debugger struct { + sync.Mutex + p1, p2 EditScript + fwdPath, revPath *EditScript + grid []byte + lines int +} + +func (dbg *debugger) Begin(nx, ny int, f EqualFunc, p1, p2 *EditScript) EqualFunc { + dbg.Lock() + dbg.fwdPath, dbg.revPath = p1, p2 + top := "┌─" + strings.Repeat("──", nx) + "┐\n" + row := "│ " + strings.Repeat("· ", nx) + "│\n" + btm := "└─" + strings.Repeat("──", nx) + "┘\n" + dbg.grid = []byte(top + strings.Repeat(row, ny) + btm) + dbg.lines = strings.Count(dbg.String(), "\n") + fmt.Print(dbg) + + // Wrap the EqualFunc so that we can intercept each result. + return func(ix, iy int) (r Result) { + cell := dbg.grid[len(top)+iy*len(row):][len("│ ")+len("· ")*ix:][:len("·")] + for i := range cell { + cell[i] = 0 // Zero out the multiple bytes of UTF-8 middle-dot + } + switch r = f(ix, iy); { + case r.Equal(): + cell[0] = '\\' + case r.Similar(): + cell[0] = 'X' + default: + cell[0] = '#' + } + return + } +} + +func (dbg *debugger) Update() { + dbg.print(updateDelay) +} + +func (dbg *debugger) Finish() { + dbg.print(finishDelay) + dbg.Unlock() +} + +func (dbg *debugger) String() string { + dbg.p1, dbg.p2 = *dbg.fwdPath, dbg.p2[:0] + for i := len(*dbg.revPath) - 1; i >= 0; i-- { + dbg.p2 = append(dbg.p2, (*dbg.revPath)[i]) + } + return fmt.Sprintf("%s[%v|%v]\n\n", dbg.grid, dbg.p1, dbg.p2) +} + +func (dbg *debugger) print(d time.Duration) { + if ansiTerminal { + fmt.Printf("\x1b[%dA", dbg.lines) // Reset terminal cursor + } + fmt.Print(dbg) + time.Sleep(d) +} diff --git a/vendor/github.com/google/go-cmp/cmp/internal/diff/diff.go b/vendor/github.com/google/go-cmp/cmp/internal/diff/diff.go new file mode 100644 index 000000000..baa41fd23 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/internal/diff/diff.go @@ -0,0 +1,373 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +// Package diff implements an algorithm for producing edit-scripts. +// The edit-script is a sequence of operations needed to transform one list +// of symbols into another (or vice-versa). The edits allowed are insertions, +// deletions, and modifications. The summation of all edits is called the +// Levenshtein distance as this problem is well-known in computer science. +// +// This package prioritizes performance over accuracy. That is, the run time +// is more important than obtaining a minimal Levenshtein distance. +package diff + +// EditType represents a single operation within an edit-script. +type EditType uint8 + +const ( + // Identity indicates that a symbol pair is identical in both list X and Y. + Identity EditType = iota + // UniqueX indicates that a symbol only exists in X and not Y. + UniqueX + // UniqueY indicates that a symbol only exists in Y and not X. + UniqueY + // Modified indicates that a symbol pair is a modification of each other. + Modified +) + +// EditScript represents the series of differences between two lists. +type EditScript []EditType + +// String returns a human-readable string representing the edit-script where +// Identity, UniqueX, UniqueY, and Modified are represented by the +// '.', 'X', 'Y', and 'M' characters, respectively. +func (es EditScript) String() string { + b := make([]byte, len(es)) + for i, e := range es { + switch e { + case Identity: + b[i] = '.' + case UniqueX: + b[i] = 'X' + case UniqueY: + b[i] = 'Y' + case Modified: + b[i] = 'M' + default: + panic("invalid edit-type") + } + } + return string(b) +} + +// stats returns a histogram of the number of each type of edit operation. +func (es EditScript) stats() (s struct{ NI, NX, NY, NM int }) { + for _, e := range es { + switch e { + case Identity: + s.NI++ + case UniqueX: + s.NX++ + case UniqueY: + s.NY++ + case Modified: + s.NM++ + default: + panic("invalid edit-type") + } + } + return +} + +// Dist is the Levenshtein distance and is guaranteed to be 0 if and only if +// lists X and Y are equal. +func (es EditScript) Dist() int { return len(es) - es.stats().NI } + +// LenX is the length of the X list. +func (es EditScript) LenX() int { return len(es) - es.stats().NY } + +// LenY is the length of the Y list. +func (es EditScript) LenY() int { return len(es) - es.stats().NX } + +// EqualFunc reports whether the symbols at indexes ix and iy are equal. +// When called by Difference, the index is guaranteed to be within nx and ny. +type EqualFunc func(ix int, iy int) Result + +// Result is the result of comparison. +// NSame is the number of sub-elements that are equal. +// NDiff is the number of sub-elements that are not equal. +type Result struct{ NSame, NDiff int } + +// Equal indicates whether the symbols are equal. Two symbols are equal +// if and only if NDiff == 0. If Equal, then they are also Similar. +func (r Result) Equal() bool { return r.NDiff == 0 } + +// Similar indicates whether two symbols are similar and may be represented +// by using the Modified type. As a special case, we consider binary comparisons +// (i.e., those that return Result{1, 0} or Result{0, 1}) to be similar. +// +// The exact ratio of NSame to NDiff to determine similarity may change. +func (r Result) Similar() bool { + // Use NSame+1 to offset NSame so that binary comparisons are similar. + return r.NSame+1 >= r.NDiff +} + +// Difference reports whether two lists of lengths nx and ny are equal +// given the definition of equality provided as f. +// +// This function may return a edit-script, which is a sequence of operations +// needed to convert one list into the other. If non-nil, the following +// invariants for the edit-script are maintained: +// • eq == (es.Dist()==0) +// • nx == es.LenX() +// • ny == es.LenY() +// +// This algorithm is not guaranteed to be an optimal solution (i.e., one that +// produces an edit-script with a minimal Levenshtein distance). This algorithm +// favors performance over optimality. The exact output is not guaranteed to +// be stable and may change over time. +func Difference(nx, ny int, f EqualFunc) (eq bool, es EditScript) { + es = searchGraph(nx, ny, f) + st := es.stats() + eq = len(es) == st.NI + if !eq && st.NI < (nx+ny)/4 { + return eq, nil // Edit-script more distracting than helpful + } + return eq, es +} + +func searchGraph(nx, ny int, f EqualFunc) EditScript { + // This algorithm is based on traversing what is known as an "edit-graph". + // See Figure 1 from "An O(ND) Difference Algorithm and Its Variations" + // by Eugene W. Myers. Since D can be as large as N itself, this is + // effectively O(N^2). Unlike the algorithm from that paper, we are not + // interested in the optimal path, but at least some "decent" path. + // + // For example, let X and Y be lists of symbols: + // X = [A B C A B B A] + // Y = [C B A B A C] + // + // The edit-graph can be drawn as the following: + // A B C A B B A + // ┌─────────────┐ + // C │_|_|\|_|_|_|_│ 0 + // B │_|\|_|_|\|\|_│ 1 + // A │\|_|_|\|_|_|\│ 2 + // B │_|\|_|_|\|\|_│ 3 + // A │\|_|_|\|_|_|\│ 4 + // C │ | |\| | | | │ 5 + // └─────────────┘ 6 + // 0 1 2 3 4 5 6 7 + // + // List X is written along the horizontal axis, while list Y is written + // along the vertical axis. At any point on this grid, if the symbol in + // list X matches the corresponding symbol in list Y, then a '\' is drawn. + // The goal of any minimal edit-script algorithm is to find a path from the + // top-left corner to the bottom-right corner, while traveling through the + // fewest horizontal or vertical edges. + // A horizontal edge is equivalent to inserting a symbol from list X. + // A vertical edge is equivalent to inserting a symbol from list Y. + // A diagonal edge is equivalent to a matching symbol between both X and Y. + + // Invariants: + // • 0 ≤ fwdPath.X ≤ (fwdFrontier.X, revFrontier.X) ≤ revPath.X ≤ nx + // • 0 ≤ fwdPath.Y ≤ (fwdFrontier.Y, revFrontier.Y) ≤ revPath.Y ≤ ny + // + // In general: + // • fwdFrontier.X < revFrontier.X + // • fwdFrontier.Y < revFrontier.Y + // Unless, it is time for the algorithm to terminate. + fwdPath := path{+1, point{0, 0}, make(EditScript, 0, (nx+ny)/2)} + revPath := path{-1, point{nx, ny}, make(EditScript, 0)} + fwdFrontier := fwdPath.point // Forward search frontier + revFrontier := revPath.point // Reverse search frontier + + // Search budget bounds the cost of searching for better paths. + // The longest sequence of non-matching symbols that can be tolerated is + // approximately the square-root of the search budget. + searchBudget := 4 * (nx + ny) // O(n) + + // The algorithm below is a greedy, meet-in-the-middle algorithm for + // computing sub-optimal edit-scripts between two lists. + // + // The algorithm is approximately as follows: + // • Searching for differences switches back-and-forth between + // a search that starts at the beginning (the top-left corner), and + // a search that starts at the end (the bottom-right corner). The goal of + // the search is connect with the search from the opposite corner. + // • As we search, we build a path in a greedy manner, where the first + // match seen is added to the path (this is sub-optimal, but provides a + // decent result in practice). When matches are found, we try the next pair + // of symbols in the lists and follow all matches as far as possible. + // • When searching for matches, we search along a diagonal going through + // through the "frontier" point. If no matches are found, we advance the + // frontier towards the opposite corner. + // • This algorithm terminates when either the X coordinates or the + // Y coordinates of the forward and reverse frontier points ever intersect. + // + // This algorithm is correct even if searching only in the forward direction + // or in the reverse direction. We do both because it is commonly observed + // that two lists commonly differ because elements were added to the front + // or end of the other list. + // + // Running the tests with the "debug" build tag prints a visualization of + // the algorithm running in real-time. This is educational for understanding + // how the algorithm works. See debug_enable.go. + f = debug.Begin(nx, ny, f, &fwdPath.es, &revPath.es) + for { + // Forward search from the beginning. + if fwdFrontier.X >= revFrontier.X || fwdFrontier.Y >= revFrontier.Y || searchBudget == 0 { + break + } + for stop1, stop2, i := false, false, 0; !(stop1 && stop2) && searchBudget > 0; i++ { + // Search in a diagonal pattern for a match. + z := zigzag(i) + p := point{fwdFrontier.X + z, fwdFrontier.Y - z} + switch { + case p.X >= revPath.X || p.Y < fwdPath.Y: + stop1 = true // Hit top-right corner + case p.Y >= revPath.Y || p.X < fwdPath.X: + stop2 = true // Hit bottom-left corner + case f(p.X, p.Y).Equal(): + // Match found, so connect the path to this point. + fwdPath.connect(p, f) + fwdPath.append(Identity) + // Follow sequence of matches as far as possible. + for fwdPath.X < revPath.X && fwdPath.Y < revPath.Y { + if !f(fwdPath.X, fwdPath.Y).Equal() { + break + } + fwdPath.append(Identity) + } + fwdFrontier = fwdPath.point + stop1, stop2 = true, true + default: + searchBudget-- // Match not found + } + debug.Update() + } + // Advance the frontier towards reverse point. + if revPath.X-fwdFrontier.X >= revPath.Y-fwdFrontier.Y { + fwdFrontier.X++ + } else { + fwdFrontier.Y++ + } + + // Reverse search from the end. + if fwdFrontier.X >= revFrontier.X || fwdFrontier.Y >= revFrontier.Y || searchBudget == 0 { + break + } + for stop1, stop2, i := false, false, 0; !(stop1 && stop2) && searchBudget > 0; i++ { + // Search in a diagonal pattern for a match. + z := zigzag(i) + p := point{revFrontier.X - z, revFrontier.Y + z} + switch { + case fwdPath.X >= p.X || revPath.Y < p.Y: + stop1 = true // Hit bottom-left corner + case fwdPath.Y >= p.Y || revPath.X < p.X: + stop2 = true // Hit top-right corner + case f(p.X-1, p.Y-1).Equal(): + // Match found, so connect the path to this point. + revPath.connect(p, f) + revPath.append(Identity) + // Follow sequence of matches as far as possible. + for fwdPath.X < revPath.X && fwdPath.Y < revPath.Y { + if !f(revPath.X-1, revPath.Y-1).Equal() { + break + } + revPath.append(Identity) + } + revFrontier = revPath.point + stop1, stop2 = true, true + default: + searchBudget-- // Match not found + } + debug.Update() + } + // Advance the frontier towards forward point. + if revFrontier.X-fwdPath.X >= revFrontier.Y-fwdPath.Y { + revFrontier.X-- + } else { + revFrontier.Y-- + } + } + + // Join the forward and reverse paths and then append the reverse path. + fwdPath.connect(revPath.point, f) + for i := len(revPath.es) - 1; i >= 0; i-- { + t := revPath.es[i] + revPath.es = revPath.es[:i] + fwdPath.append(t) + } + debug.Finish() + return fwdPath.es +} + +type path struct { + dir int // +1 if forward, -1 if reverse + point // Leading point of the EditScript path + es EditScript +} + +// connect appends any necessary Identity, Modified, UniqueX, or UniqueY types +// to the edit-script to connect p.point to dst. +func (p *path) connect(dst point, f EqualFunc) { + if p.dir > 0 { + // Connect in forward direction. + for dst.X > p.X && dst.Y > p.Y { + switch r := f(p.X, p.Y); { + case r.Equal(): + p.append(Identity) + case r.Similar(): + p.append(Modified) + case dst.X-p.X >= dst.Y-p.Y: + p.append(UniqueX) + default: + p.append(UniqueY) + } + } + for dst.X > p.X { + p.append(UniqueX) + } + for dst.Y > p.Y { + p.append(UniqueY) + } + } else { + // Connect in reverse direction. + for p.X > dst.X && p.Y > dst.Y { + switch r := f(p.X-1, p.Y-1); { + case r.Equal(): + p.append(Identity) + case r.Similar(): + p.append(Modified) + case p.Y-dst.Y >= p.X-dst.X: + p.append(UniqueY) + default: + p.append(UniqueX) + } + } + for p.X > dst.X { + p.append(UniqueX) + } + for p.Y > dst.Y { + p.append(UniqueY) + } + } +} + +func (p *path) append(t EditType) { + p.es = append(p.es, t) + switch t { + case Identity, Modified: + p.add(p.dir, p.dir) + case UniqueX: + p.add(p.dir, 0) + case UniqueY: + p.add(0, p.dir) + } + debug.Update() +} + +type point struct{ X, Y int } + +func (p *point) add(dx, dy int) { p.X += dx; p.Y += dy } + +// zigzag maps a consecutive sequence of integers to a zig-zag sequence. +// [0 1 2 3 4 5 ...] => [0 -1 +1 -2 +2 ...] +func zigzag(x int) int { + if x&1 != 0 { + x = ^x + } + return x >> 1 +} diff --git a/vendor/github.com/google/go-cmp/cmp/internal/diff/diff_test.go b/vendor/github.com/google/go-cmp/cmp/internal/diff/diff_test.go new file mode 100644 index 000000000..5996ea2a9 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/internal/diff/diff_test.go @@ -0,0 +1,467 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +package diff + +import ( + "fmt" + "math/rand" + "strings" + "testing" + "unicode" +) + +func TestDifference(t *testing.T) { + tests := []struct { + // Before passing x and y to Difference, we strip all spaces so that + // they can be used by the test author to indicate a missing symbol + // in one of the lists. + x, y string + want string + }{{ + x: "", + y: "", + want: "", + }, { + x: "#", + y: "#", + want: ".", + }, { + x: "##", + y: "# ", + want: ".X", + }, { + x: "a#", + y: "A ", + want: "MX", + }, { + x: "#a", + y: " A", + want: "XM", + }, { + x: "# ", + y: "##", + want: ".Y", + }, { + x: " #", + y: "@#", + want: "Y.", + }, { + x: "@#", + y: " #", + want: "X.", + }, { + x: "##########0123456789", + y: " 0123456789", + want: "XXXXXXXXXX..........", + }, { + x: " 0123456789", + y: "##########0123456789", + want: "YYYYYYYYYY..........", + }, { + x: "#####0123456789#####", + y: " 0123456789 ", + want: "XXXXX..........XXXXX", + }, { + x: " 0123456789 ", + y: "#####0123456789#####", + want: "YYYYY..........YYYYY", + }, { + x: "01234##########56789", + y: "01234 56789", + want: ".....XXXXXXXXXX.....", + }, { + x: "01234 56789", + y: "01234##########56789", + want: ".....YYYYYYYYYY.....", + }, { + x: "0123456789##########", + y: "0123456789 ", + want: "..........XXXXXXXXXX", + }, { + x: "0123456789 ", + y: "0123456789##########", + want: "..........YYYYYYYYYY", + }, { + x: "abcdefghij0123456789", + y: "ABCDEFGHIJ0123456789", + want: "MMMMMMMMMM..........", + }, { + x: "ABCDEFGHIJ0123456789", + y: "abcdefghij0123456789", + want: "MMMMMMMMMM..........", + }, { + x: "01234abcdefghij56789", + y: "01234ABCDEFGHIJ56789", + want: ".....MMMMMMMMMM.....", + }, { + x: "01234ABCDEFGHIJ56789", + y: "01234abcdefghij56789", + want: ".....MMMMMMMMMM.....", + }, { + x: "0123456789abcdefghij", + y: "0123456789ABCDEFGHIJ", + want: "..........MMMMMMMMMM", + }, { + x: "0123456789ABCDEFGHIJ", + y: "0123456789abcdefghij", + want: "..........MMMMMMMMMM", + }, { + x: "ABCDEFGHIJ0123456789 ", + y: " 0123456789abcdefghij", + want: "XXXXXXXXXX..........YYYYYYYYYY", + }, { + x: " 0123456789abcdefghij", + y: "ABCDEFGHIJ0123456789 ", + want: "YYYYYYYYYY..........XXXXXXXXXX", + }, { + x: "ABCDE0123456789 FGHIJ", + y: " 0123456789abcdefghij", + want: "XXXXX..........YYYYYMMMMM", + }, { + x: " 0123456789abcdefghij", + y: "ABCDE0123456789 FGHIJ", + want: "YYYYY..........XXXXXMMMMM", + }, { + x: "ABCDE01234F G H I J 56789 ", + y: " 01234 a b c d e56789fghij", + want: "XXXXX.....XYXYXYXYXY.....YYYYY", + }, { + x: " 01234a b c d e 56789fghij", + y: "ABCDE01234 F G H I J56789 ", + want: "YYYYY.....XYXYXYXYXY.....XXXXX", + }, { + x: "FGHIJ01234ABCDE56789 ", + y: " 01234abcde56789fghij", + want: "XXXXX.....MMMMM.....YYYYY", + }, { + x: " 01234abcde56789fghij", + y: "FGHIJ01234ABCDE56789 ", + want: "YYYYY.....MMMMM.....XXXXX", + }, { + x: "ABCAB BA ", + y: " C BABAC", + want: "XX.X.Y..Y", + }, { + x: "# #### ###", + y: "#y####yy###", + want: ".Y....YY...", + }, { + x: "# #### # ##x#x", + y: "#y####y y## # ", + want: ".Y....YXY..X.X", + }, { + x: "###z#z###### x #", + y: "#y##Z#Z###### yy#", + want: ".Y..M.M......XYY.", + }, { + x: "0 12z3x 456789 x x 0", + y: "0y12Z3 y456789y y y0", + want: ".Y..M.XY......YXYXY.", + }, { + x: "0 2 4 6 8 ..................abXXcdEXF.ghXi", + y: " 1 3 5 7 9..................AB CDE F.GH I", + want: "XYXYXYXYXY..................MMXXMM.X..MMXM", + }, { + x: "I HG.F EDC BA..................9 7 5 3 1 ", + y: "iXhg.FXEdcXXba.................. 8 6 4 2 0", + want: "MYMM..Y.MMYYMM..................XYXYXYXYXY", + }, { + x: "x1234", + y: " 1234", + want: "X....", + }, { + x: "x123x4", + y: " 123 4", + want: "X...X.", + }, { + x: "x1234x56", + y: " 1234 ", + want: "X....XXX", + }, { + x: "x1234xxx56", + y: " 1234 56", + want: "X....XXX..", + }, { + x: ".1234...ab", + y: " 1234 AB", + want: "X....XXXMM", + }, { + x: "x1234xxab.", + y: " 1234 AB ", + want: "X....XXMMX", + }, { + x: " 0123456789", + y: "9012345678 ", + want: "Y.........X", + }, { + x: " 0123456789", + y: "8901234567 ", + want: "YY........XX", + }, { + x: " 0123456789", + y: "7890123456 ", + want: "YYY.......XXX", + }, { + x: " 0123456789", + y: "6789012345 ", + want: "YYYY......XXXX", + }, { + x: "0123456789 ", + y: " 5678901234", + want: "XXXXX.....YYYYY", + }, { + x: "0123456789 ", + y: " 4567890123", + want: "XXXX......YYYY", + }, { + x: "0123456789 ", + y: " 3456789012", + want: "XXX.......YYY", + }, { + x: "0123456789 ", + y: " 2345678901", + want: "XX........YY", + }, { + x: "0123456789 ", + y: " 1234567890", + want: "X.........Y", + }, { + x: "0123456789", + y: "9876543210", + }, { + x: "0123456789", + y: "6725819034", + }, { + x: "FBQMOIGTLN72X90E4SP651HKRJUDA83CVZW", + y: "5WHXO10R9IVKZLCTAJ8P3NSEQM472G6UBDF", + }} + + for _, tt := range tests { + tRun(t, "", func(t *testing.T) { + x := strings.Replace(tt.x, " ", "", -1) + y := strings.Replace(tt.y, " ", "", -1) + es := testStrings(t, x, y) + if got := es.String(); got != tt.want { + t.Errorf("Difference(%s, %s):\ngot %s\nwant %s", x, y, got, tt.want) + } + }) + } +} + +func TestDifferenceFuzz(t *testing.T) { + tests := []struct{ px, py, pm float32 }{ + {px: 0.0, py: 0.0, pm: 0.1}, + {px: 0.0, py: 0.1, pm: 0.0}, + {px: 0.1, py: 0.0, pm: 0.0}, + {px: 0.0, py: 0.1, pm: 0.1}, + {px: 0.1, py: 0.0, pm: 0.1}, + {px: 0.2, py: 0.2, pm: 0.2}, + {px: 0.3, py: 0.1, pm: 0.2}, + {px: 0.1, py: 0.3, pm: 0.2}, + {px: 0.2, py: 0.2, pm: 0.2}, + {px: 0.3, py: 0.3, pm: 0.3}, + {px: 0.1, py: 0.1, pm: 0.5}, + {px: 0.4, py: 0.1, pm: 0.5}, + {px: 0.3, py: 0.2, pm: 0.5}, + {px: 0.2, py: 0.3, pm: 0.5}, + {px: 0.1, py: 0.4, pm: 0.5}, + } + + for i, tt := range tests { + tRun(t, fmt.Sprintf("P%d", i), func(t *testing.T) { + // Sweep from 1B to 1KiB. + for n := 1; n <= 1024; n <<= 1 { + tRun(t, fmt.Sprintf("N%d", n), func(t *testing.T) { + for j := 0; j < 10; j++ { + x, y := generateStrings(n, tt.px, tt.py, tt.pm, int64(j)) + testStrings(t, x, y) + } + }) + } + }) + } +} + +func benchmarkDifference(b *testing.B, n int) { + // TODO: Use testing.B.Run when we drop Go1.6 support. + x, y := generateStrings(n, 0.05, 0.05, 0.10, 0) + b.ReportAllocs() + b.SetBytes(int64(len(x) + len(y))) + for i := 0; i < b.N; i++ { + Difference(len(x), len(y), func(ix, iy int) Result { + return compareByte(x[ix], y[iy]) + }) + } +} +func BenchmarkDifference1K(b *testing.B) { benchmarkDifference(b, 1<<10) } +func BenchmarkDifference4K(b *testing.B) { benchmarkDifference(b, 1<<12) } +func BenchmarkDifference16K(b *testing.B) { benchmarkDifference(b, 1<<14) } +func BenchmarkDifference64K(b *testing.B) { benchmarkDifference(b, 1<<16) } +func BenchmarkDifference256K(b *testing.B) { benchmarkDifference(b, 1<<18) } +func BenchmarkDifference1M(b *testing.B) { benchmarkDifference(b, 1<<20) } + +func generateStrings(n int, px, py, pm float32, seed int64) (string, string) { + if px+py+pm > 1.0 { + panic("invalid probabilities") + } + py += px + pm += py + + b := make([]byte, n) + r := rand.New(rand.NewSource(seed)) + r.Read(b) + + var x, y []byte + for len(b) > 0 { + switch p := r.Float32(); { + case p < px: // UniqueX + x = append(x, b[0]) + case p < py: // UniqueY + y = append(y, b[0]) + case p < pm: // Modified + x = append(x, 'A'+(b[0]%26)) + y = append(y, 'a'+(b[0]%26)) + default: // Identity + x = append(x, b[0]) + y = append(y, b[0]) + } + b = b[1:] + } + return string(x), string(y) +} + +func testStrings(t *testing.T, x, y string) EditScript { + wantEq := x == y + eq, es := Difference(len(x), len(y), func(ix, iy int) Result { + return compareByte(x[ix], y[iy]) + }) + if eq != wantEq { + t.Errorf("equality mismatch: got %v, want %v", eq, wantEq) + } + if es != nil { + if es.LenX() != len(x) { + t.Errorf("es.LenX = %d, want %d", es.LenX(), len(x)) + } + if es.LenY() != len(y) { + t.Errorf("es.LenY = %d, want %d", es.LenY(), len(y)) + } + if got := (es.Dist() == 0); got != wantEq { + t.Errorf("violation of equality invariant: got %v, want %v", got, wantEq) + } + if !validateScript(x, y, es) { + t.Errorf("invalid edit script: %v", es) + } + } + return es +} + +func validateScript(x, y string, es EditScript) bool { + var bx, by []byte + for _, e := range es { + switch e { + case Identity: + if !compareByte(x[len(bx)], y[len(by)]).Equal() { + return false + } + bx = append(bx, x[len(bx)]) + by = append(by, y[len(by)]) + case UniqueX: + bx = append(bx, x[len(bx)]) + case UniqueY: + by = append(by, y[len(by)]) + case Modified: + if !compareByte(x[len(bx)], y[len(by)]).Similar() { + return false + } + bx = append(bx, x[len(bx)]) + by = append(by, y[len(by)]) + } + } + return string(bx) == x && string(by) == y +} + +// compareByte returns a Result where the result is Equal if x == y, +// similar if x and y differ only in casing, and different otherwise. +func compareByte(x, y byte) (r Result) { + switch { + case x == y: + return equalResult // Identity + case unicode.ToUpper(rune(x)) == unicode.ToUpper(rune(y)): + return similarResult // Modified + default: + return differentResult // UniqueX or UniqueY + } +} + +var ( + equalResult = Result{NDiff: 0} + similarResult = Result{NDiff: 1} + differentResult = Result{NDiff: 2} +) + +func TestResult(t *testing.T) { + tests := []struct { + result Result + wantEqual bool + wantSimilar bool + }{ + // equalResult is equal since NDiff == 0, by definition of Equal method. + {equalResult, true, true}, + // similarResult is similar since it is a binary result where only one + // element was compared (i.e., Either NSame==1 or NDiff==1). + {similarResult, false, true}, + // differentResult is different since there are enough differences that + // it isn't even considered similar. + {differentResult, false, false}, + + // Zero value is always equal. + {Result{NSame: 0, NDiff: 0}, true, true}, + + // Binary comparisons (where NSame+NDiff == 1) are always similar. + {Result{NSame: 1, NDiff: 0}, true, true}, + {Result{NSame: 0, NDiff: 1}, false, true}, + + // More complex ratios. The exact ratio for similarity may change, + // and may require updates to these test cases. + {Result{NSame: 1, NDiff: 1}, false, true}, + {Result{NSame: 1, NDiff: 2}, false, true}, + {Result{NSame: 1, NDiff: 3}, false, false}, + {Result{NSame: 2, NDiff: 1}, false, true}, + {Result{NSame: 2, NDiff: 2}, false, true}, + {Result{NSame: 2, NDiff: 3}, false, true}, + {Result{NSame: 3, NDiff: 1}, false, true}, + {Result{NSame: 3, NDiff: 2}, false, true}, + {Result{NSame: 3, NDiff: 3}, false, true}, + {Result{NSame: 1000, NDiff: 0}, true, true}, + {Result{NSame: 1000, NDiff: 1}, false, true}, + {Result{NSame: 1000, NDiff: 2}, false, true}, + {Result{NSame: 0, NDiff: 1000}, false, false}, + {Result{NSame: 1, NDiff: 1000}, false, false}, + {Result{NSame: 2, NDiff: 1000}, false, false}, + } + + for _, tt := range tests { + if got := tt.result.Equal(); got != tt.wantEqual { + t.Errorf("%#v.Equal() = %v, want %v", tt.result, got, tt.wantEqual) + } + if got := tt.result.Similar(); got != tt.wantSimilar { + t.Errorf("%#v.Similar() = %v, want %v", tt.result, got, tt.wantSimilar) + } + } +} + +// TODO: Delete this hack when we drop Go1.6 support. +func tRun(t *testing.T, name string, f func(t *testing.T)) { + type runner interface { + Run(string, func(t *testing.T)) bool + } + var ti interface{} = t + if r, ok := ti.(runner); ok { + r.Run(name, f) + } else { + t.Logf("Test: %s", name) + f(t) + } +} diff --git a/vendor/github.com/google/go-cmp/cmp/internal/function/func.go b/vendor/github.com/google/go-cmp/cmp/internal/function/func.go new file mode 100644 index 000000000..4c35ff11e --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/internal/function/func.go @@ -0,0 +1,49 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +// Package function identifies function types. +package function + +import "reflect" + +type funcType int + +const ( + _ funcType = iota + + ttbFunc // func(T, T) bool + tibFunc // func(T, I) bool + trFunc // func(T) R + + Equal = ttbFunc // func(T, T) bool + EqualAssignable = tibFunc // func(T, I) bool; encapsulates func(T, T) bool + Transformer = trFunc // func(T) R + ValueFilter = ttbFunc // func(T, T) bool + Less = ttbFunc // func(T, T) bool +) + +var boolType = reflect.TypeOf(true) + +// IsType reports whether the reflect.Type is of the specified function type. +func IsType(t reflect.Type, ft funcType) bool { + if t == nil || t.Kind() != reflect.Func || t.IsVariadic() { + return false + } + ni, no := t.NumIn(), t.NumOut() + switch ft { + case ttbFunc: // func(T, T) bool + if ni == 2 && no == 1 && t.In(0) == t.In(1) && t.Out(0) == boolType { + return true + } + case tibFunc: // func(T, I) bool + if ni == 2 && no == 1 && t.In(0).AssignableTo(t.In(1)) && t.Out(0) == boolType { + return true + } + case trFunc: // func(T) R + if ni == 1 && no == 1 { + return true + } + } + return false +} diff --git a/vendor/github.com/google/go-cmp/cmp/internal/testprotos/protos.go b/vendor/github.com/google/go-cmp/cmp/internal/testprotos/protos.go new file mode 100644 index 000000000..120c8b0e8 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/internal/testprotos/protos.go @@ -0,0 +1,116 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +package testprotos + +func Equal(x, y Message) bool { + if x == nil || y == nil { + return x == nil && y == nil + } + return x.String() == y.String() +} + +type Message interface { + Proto() + String() string +} + +type proto interface { + Proto() +} + +type notComparable struct { + unexportedField func() +} + +type Stringer struct{ X string } + +func (s *Stringer) String() string { return s.X } + +// Project1 protocol buffers +type ( + Eagle_States int + Eagle_MissingCalls int + Dreamer_States int + Dreamer_MissingCalls int + Slap_States int + Goat_States int + Donkey_States int + SummerType int + + Eagle struct { + proto + notComparable + Stringer + } + Dreamer struct { + proto + notComparable + Stringer + } + Slap struct { + proto + notComparable + Stringer + } + Goat struct { + proto + notComparable + Stringer + } + Donkey struct { + proto + notComparable + Stringer + } +) + +// Project2 protocol buffers +type ( + Germ struct { + proto + notComparable + Stringer + } + Dish struct { + proto + notComparable + Stringer + } +) + +// Project3 protocol buffers +type ( + Dirt struct { + proto + notComparable + Stringer + } + Wizard struct { + proto + notComparable + Stringer + } + Sadistic struct { + proto + notComparable + Stringer + } +) + +// Project4 protocol buffers +type ( + HoneyStatus int + PoisonType int + MetaData struct { + proto + notComparable + Stringer + } + Restrictions struct { + proto + notComparable + Stringer + } +) diff --git a/vendor/github.com/google/go-cmp/cmp/internal/teststructs/project1.go b/vendor/github.com/google/go-cmp/cmp/internal/teststructs/project1.go new file mode 100644 index 000000000..1999e38fd --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/internal/teststructs/project1.go @@ -0,0 +1,267 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +package teststructs + +import ( + "time" + + pb "github.com/google/go-cmp/cmp/internal/testprotos" +) + +// This is an sanitized example of equality from a real use-case. +// The original equality function was as follows: +/* +func equalEagle(x, y Eagle) bool { + if x.Name != y.Name && + !reflect.DeepEqual(x.Hounds, y.Hounds) && + x.Desc != y.Desc && + x.DescLong != y.DescLong && + x.Prong != y.Prong && + x.StateGoverner != y.StateGoverner && + x.PrankRating != y.PrankRating && + x.FunnyPrank != y.FunnyPrank && + !pb.Equal(x.Immutable.Proto(), y.Immutable.Proto()) { + return false + } + + if len(x.Dreamers) != len(y.Dreamers) { + return false + } + for i := range x.Dreamers { + if !equalDreamer(x.Dreamers[i], y.Dreamers[i]) { + return false + } + } + if len(x.Slaps) != len(y.Slaps) { + return false + } + for i := range x.Slaps { + if !equalSlap(x.Slaps[i], y.Slaps[i]) { + return false + } + } + return true +} +func equalDreamer(x, y Dreamer) bool { + if x.Name != y.Name || + x.Desc != y.Desc || + x.DescLong != y.DescLong || + x.ContSlapsInterval != y.ContSlapsInterval || + x.Ornamental != y.Ornamental || + x.Amoeba != y.Amoeba || + x.Heroes != y.Heroes || + x.FloppyDisk != y.FloppyDisk || + x.MightiestDuck != y.MightiestDuck || + x.FunnyPrank != y.FunnyPrank || + !pb.Equal(x.Immutable.Proto(), y.Immutable.Proto()) { + + return false + } + if len(x.Animal) != len(y.Animal) { + return false + } + for i := range x.Animal { + vx := x.Animal[i] + vy := y.Animal[i] + if reflect.TypeOf(x.Animal) != reflect.TypeOf(y.Animal) { + return false + } + switch vx.(type) { + case Goat: + if !equalGoat(vx.(Goat), vy.(Goat)) { + return false + } + case Donkey: + if !equalDonkey(vx.(Donkey), vy.(Donkey)) { + return false + } + default: + panic(fmt.Sprintf("unknown type: %T", vx)) + } + } + if len(x.PreSlaps) != len(y.PreSlaps) { + return false + } + for i := range x.PreSlaps { + if !equalSlap(x.PreSlaps[i], y.PreSlaps[i]) { + return false + } + } + if len(x.ContSlaps) != len(y.ContSlaps) { + return false + } + for i := range x.ContSlaps { + if !equalSlap(x.ContSlaps[i], y.ContSlaps[i]) { + return false + } + } + return true +} +func equalSlap(x, y Slap) bool { + return x.Name == y.Name && + x.Desc == y.Desc && + x.DescLong == y.DescLong && + pb.Equal(x.Args, y.Args) && + x.Tense == y.Tense && + x.Interval == y.Interval && + x.Homeland == y.Homeland && + x.FunnyPrank == y.FunnyPrank && + pb.Equal(x.Immutable.Proto(), y.Immutable.Proto()) +} +func equalGoat(x, y Goat) bool { + if x.Target != y.Target || + x.FunnyPrank != y.FunnyPrank || + !pb.Equal(x.Immutable.Proto(), y.Immutable.Proto()) { + return false + } + if len(x.Slaps) != len(y.Slaps) { + return false + } + for i := range x.Slaps { + if !equalSlap(x.Slaps[i], y.Slaps[i]) { + return false + } + } + return true +} +func equalDonkey(x, y Donkey) bool { + return x.Pause == y.Pause && + x.Sleep == y.Sleep && + x.FunnyPrank == y.FunnyPrank && + pb.Equal(x.Immutable.Proto(), y.Immutable.Proto()) +} +*/ + +type Eagle struct { + Name string + Hounds []string + Desc string + DescLong string + Dreamers []Dreamer + Prong int64 + Slaps []Slap + StateGoverner string + PrankRating string + FunnyPrank string + Immutable *EagleImmutable +} + +type EagleImmutable struct { + ID string + State *pb.Eagle_States + MissingCall *pb.Eagle_MissingCalls + Birthday time.Time + Death time.Time + Started time.Time + LastUpdate time.Time + Creator string + empty bool +} + +type Dreamer struct { + Name string + Desc string + DescLong string + PreSlaps []Slap + ContSlaps []Slap + ContSlapsInterval int32 + Animal []interface{} // Could be either Goat or Donkey + Ornamental bool + Amoeba int64 + Heroes int32 + FloppyDisk int32 + MightiestDuck bool + FunnyPrank string + Immutable *DreamerImmutable +} + +type DreamerImmutable struct { + ID string + State *pb.Dreamer_States + MissingCall *pb.Dreamer_MissingCalls + Calls int32 + Started time.Time + Stopped time.Time + LastUpdate time.Time + empty bool +} + +type Slap struct { + Name string + Desc string + DescLong string + Args pb.Message + Tense int32 + Interval int32 + Homeland uint32 + FunnyPrank string + Immutable *SlapImmutable +} + +type SlapImmutable struct { + ID string + Out pb.Message + MildSlap bool + PrettyPrint string + State *pb.Slap_States + Started time.Time + Stopped time.Time + LastUpdate time.Time + LoveRadius *LoveRadius + empty bool +} + +type Goat struct { + Target string + Slaps []Slap + FunnyPrank string + Immutable *GoatImmutable +} + +type GoatImmutable struct { + ID string + State *pb.Goat_States + Started time.Time + Stopped time.Time + LastUpdate time.Time + empty bool +} +type Donkey struct { + Pause bool + Sleep int32 + FunnyPrank string + Immutable *DonkeyImmutable +} + +type DonkeyImmutable struct { + ID string + State *pb.Donkey_States + Started time.Time + Stopped time.Time + LastUpdate time.Time + empty bool +} + +type LoveRadius struct { + Summer *SummerLove + empty bool +} + +type SummerLove struct { + Summary *SummerLoveSummary + empty bool +} + +type SummerLoveSummary struct { + Devices []string + ChangeType []pb.SummerType + empty bool +} + +func (EagleImmutable) Proto() *pb.Eagle { return nil } +func (DreamerImmutable) Proto() *pb.Dreamer { return nil } +func (SlapImmutable) Proto() *pb.Slap { return nil } +func (GoatImmutable) Proto() *pb.Goat { return nil } +func (DonkeyImmutable) Proto() *pb.Donkey { return nil } diff --git a/vendor/github.com/google/go-cmp/cmp/internal/teststructs/project2.go b/vendor/github.com/google/go-cmp/cmp/internal/teststructs/project2.go new file mode 100644 index 000000000..536592bbe --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/internal/teststructs/project2.go @@ -0,0 +1,74 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +package teststructs + +import ( + "time" + + pb "github.com/google/go-cmp/cmp/internal/testprotos" +) + +// This is an sanitized example of equality from a real use-case. +// The original equality function was as follows: +/* +func equalBatch(b1, b2 *GermBatch) bool { + for _, b := range []*GermBatch{b1, b2} { + for _, l := range b.DirtyGerms { + sort.Slice(l, func(i, j int) bool { return l[i].String() < l[j].String() }) + } + for _, l := range b.CleanGerms { + sort.Slice(l, func(i, j int) bool { return l[i].String() < l[j].String() }) + } + } + if !pb.DeepEqual(b1.DirtyGerms, b2.DirtyGerms) || + !pb.DeepEqual(b1.CleanGerms, b2.CleanGerms) || + !pb.DeepEqual(b1.GermMap, b2.GermMap) { + return false + } + if len(b1.DishMap) != len(b2.DishMap) { + return false + } + for id := range b1.DishMap { + kpb1, err1 := b1.DishMap[id].Proto() + kpb2, err2 := b2.DishMap[id].Proto() + if !pb.Equal(kpb1, kpb2) || !reflect.DeepEqual(err1, err2) { + return false + } + } + return b1.HasPreviousResult == b2.HasPreviousResult && + b1.DirtyID == b2.DirtyID && + b1.CleanID == b2.CleanID && + b1.GermStrain == b2.GermStrain && + b1.TotalDirtyGerms == b2.TotalDirtyGerms && + b1.InfectedAt.Equal(b2.InfectedAt) +} +*/ + +type GermBatch struct { + DirtyGerms, CleanGerms map[int32][]*pb.Germ + GermMap map[int32]*pb.Germ + DishMap map[int32]*Dish + HasPreviousResult bool + DirtyID, CleanID int32 + GermStrain int32 + TotalDirtyGerms int + InfectedAt time.Time +} + +type Dish struct { + pb *pb.Dish + err error +} + +func CreateDish(m *pb.Dish, err error) *Dish { + return &Dish{pb: m, err: err} +} + +func (d *Dish) Proto() (*pb.Dish, error) { + if d.err != nil { + return nil, d.err + } + return d.pb, nil +} diff --git a/vendor/github.com/google/go-cmp/cmp/internal/teststructs/project3.go b/vendor/github.com/google/go-cmp/cmp/internal/teststructs/project3.go new file mode 100644 index 000000000..00c252e5e --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/internal/teststructs/project3.go @@ -0,0 +1,77 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +package teststructs + +import ( + "sync" + + pb "github.com/google/go-cmp/cmp/internal/testprotos" +) + +// This is an sanitized example of equality from a real use-case. +// The original equality function was as follows: +/* +func equalDirt(x, y *Dirt) bool { + if !reflect.DeepEqual(x.table, y.table) || + !reflect.DeepEqual(x.ts, y.ts) || + x.Discord != y.Discord || + !pb.Equal(&x.Proto, &y.Proto) || + len(x.wizard) != len(y.wizard) || + len(x.sadistic) != len(y.sadistic) || + x.lastTime != y.lastTime { + return false + } + for k, vx := range x.wizard { + vy, ok := y.wizard[k] + if !ok || !pb.Equal(vx, vy) { + return false + } + } + for k, vx := range x.sadistic { + vy, ok := y.sadistic[k] + if !ok || !pb.Equal(vx, vy) { + return false + } + } + return true +} +*/ + +type Dirt struct { + table Table // Always concrete type of MockTable + ts Timestamp + Discord DiscordState + Proto pb.Dirt + wizard map[string]*pb.Wizard + sadistic map[string]*pb.Sadistic + lastTime int64 + mu sync.Mutex +} + +type DiscordState int + +type Timestamp int64 + +func (d *Dirt) SetTable(t Table) { d.table = t } +func (d *Dirt) SetTimestamp(t Timestamp) { d.ts = t } +func (d *Dirt) SetWizard(m map[string]*pb.Wizard) { d.wizard = m } +func (d *Dirt) SetSadistic(m map[string]*pb.Sadistic) { d.sadistic = m } +func (d *Dirt) SetLastTime(t int64) { d.lastTime = t } + +type Table interface { + Operation1() error + Operation2() error + Operation3() error +} + +type MockTable struct { + state []string +} + +func CreateMockTable(s []string) *MockTable { return &MockTable{s} } +func (mt *MockTable) Operation1() error { return nil } +func (mt *MockTable) Operation2() error { return nil } +func (mt *MockTable) Operation3() error { return nil } +func (mt *MockTable) State() []string { return mt.state } diff --git a/vendor/github.com/google/go-cmp/cmp/internal/teststructs/project4.go b/vendor/github.com/google/go-cmp/cmp/internal/teststructs/project4.go new file mode 100644 index 000000000..9b50d738e --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/internal/teststructs/project4.go @@ -0,0 +1,142 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +package teststructs + +import ( + "time" + + pb "github.com/google/go-cmp/cmp/internal/testprotos" +) + +// This is an sanitized example of equality from a real use-case. +// The original equality function was as follows: +/* +func equalCartel(x, y Cartel) bool { + if !(equalHeadquarter(x.Headquarter, y.Headquarter) && + x.Source() == y.Source() && + x.CreationDate().Equal(y.CreationDate()) && + x.Boss() == y.Boss() && + x.LastCrimeDate().Equal(y.LastCrimeDate())) { + return false + } + if len(x.Poisons()) != len(y.Poisons()) { + return false + } + for i := range x.Poisons() { + if !equalPoison(*x.Poisons()[i], *y.Poisons()[i]) { + return false + } + } + return true +} +func equalHeadquarter(x, y Headquarter) bool { + xr, yr := x.Restrictions(), y.Restrictions() + return x.ID() == y.ID() && + x.Location() == y.Location() && + reflect.DeepEqual(x.SubDivisions(), y.SubDivisions()) && + x.IncorporatedDate().Equal(y.IncorporatedDate()) && + pb.Equal(x.MetaData(), y.MetaData()) && + bytes.Equal(x.PrivateMessage(), y.PrivateMessage()) && + bytes.Equal(x.PublicMessage(), y.PublicMessage()) && + x.HorseBack() == y.HorseBack() && + x.Rattle() == y.Rattle() && + x.Convulsion() == y.Convulsion() && + x.Expansion() == y.Expansion() && + x.Status() == y.Status() && + pb.Equal(&xr, &yr) && + x.CreationTime().Equal(y.CreationTime()) +} +func equalPoison(x, y Poison) bool { + return x.PoisonType() == y.PoisonType() && + x.Expiration().Equal(y.Expiration()) && + x.Manufactuer() == y.Manufactuer() && + x.Potency() == y.Potency() +} +*/ + +type Cartel struct { + Headquarter + source string + creationDate time.Time + boss string + lastCrimeDate time.Time + poisons []*Poison +} + +func (p Cartel) Source() string { return p.source } +func (p Cartel) CreationDate() time.Time { return p.creationDate } +func (p Cartel) Boss() string { return p.boss } +func (p Cartel) LastCrimeDate() time.Time { return p.lastCrimeDate } +func (p Cartel) Poisons() []*Poison { return p.poisons } + +func (p *Cartel) SetSource(x string) { p.source = x } +func (p *Cartel) SetCreationDate(x time.Time) { p.creationDate = x } +func (p *Cartel) SetBoss(x string) { p.boss = x } +func (p *Cartel) SetLastCrimeDate(x time.Time) { p.lastCrimeDate = x } +func (p *Cartel) SetPoisons(x []*Poison) { p.poisons = x } + +type Headquarter struct { + id uint64 + location string + subDivisions []string + incorporatedDate time.Time + metaData *pb.MetaData + privateMessage []byte + publicMessage []byte + horseBack string + rattle string + convulsion bool + expansion uint64 + status pb.HoneyStatus + restrictions pb.Restrictions + creationTime time.Time +} + +func (hq Headquarter) ID() uint64 { return hq.id } +func (hq Headquarter) Location() string { return hq.location } +func (hq Headquarter) SubDivisions() []string { return hq.subDivisions } +func (hq Headquarter) IncorporatedDate() time.Time { return hq.incorporatedDate } +func (hq Headquarter) MetaData() *pb.MetaData { return hq.metaData } +func (hq Headquarter) PrivateMessage() []byte { return hq.privateMessage } +func (hq Headquarter) PublicMessage() []byte { return hq.publicMessage } +func (hq Headquarter) HorseBack() string { return hq.horseBack } +func (hq Headquarter) Rattle() string { return hq.rattle } +func (hq Headquarter) Convulsion() bool { return hq.convulsion } +func (hq Headquarter) Expansion() uint64 { return hq.expansion } +func (hq Headquarter) Status() pb.HoneyStatus { return hq.status } +func (hq Headquarter) Restrictions() pb.Restrictions { return hq.restrictions } +func (hq Headquarter) CreationTime() time.Time { return hq.creationTime } + +func (hq *Headquarter) SetID(x uint64) { hq.id = x } +func (hq *Headquarter) SetLocation(x string) { hq.location = x } +func (hq *Headquarter) SetSubDivisions(x []string) { hq.subDivisions = x } +func (hq *Headquarter) SetIncorporatedDate(x time.Time) { hq.incorporatedDate = x } +func (hq *Headquarter) SetMetaData(x *pb.MetaData) { hq.metaData = x } +func (hq *Headquarter) SetPrivateMessage(x []byte) { hq.privateMessage = x } +func (hq *Headquarter) SetPublicMessage(x []byte) { hq.publicMessage = x } +func (hq *Headquarter) SetHorseBack(x string) { hq.horseBack = x } +func (hq *Headquarter) SetRattle(x string) { hq.rattle = x } +func (hq *Headquarter) SetConvulsion(x bool) { hq.convulsion = x } +func (hq *Headquarter) SetExpansion(x uint64) { hq.expansion = x } +func (hq *Headquarter) SetStatus(x pb.HoneyStatus) { hq.status = x } +func (hq *Headquarter) SetRestrictions(x pb.Restrictions) { hq.restrictions = x } +func (hq *Headquarter) SetCreationTime(x time.Time) { hq.creationTime = x } + +type Poison struct { + poisonType pb.PoisonType + expiration time.Time + manufactuer string + potency int +} + +func (p Poison) PoisonType() pb.PoisonType { return p.poisonType } +func (p Poison) Expiration() time.Time { return p.expiration } +func (p Poison) Manufactuer() string { return p.manufactuer } +func (p Poison) Potency() int { return p.potency } + +func (p *Poison) SetPoisonType(x pb.PoisonType) { p.poisonType = x } +func (p *Poison) SetExpiration(x time.Time) { p.expiration = x } +func (p *Poison) SetManufactuer(x string) { p.manufactuer = x } +func (p *Poison) SetPotency(x int) { p.potency = x } diff --git a/vendor/github.com/google/go-cmp/cmp/internal/teststructs/structs.go b/vendor/github.com/google/go-cmp/cmp/internal/teststructs/structs.go new file mode 100644 index 000000000..6b4d2a725 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/internal/teststructs/structs.go @@ -0,0 +1,197 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +package teststructs + +type InterfaceA interface { + InterfaceA() +} + +type ( + StructA struct{ X string } // Equal method on value receiver + StructB struct{ X string } // Equal method on pointer receiver + StructC struct{ X string } // Equal method (with interface argument) on value receiver + StructD struct{ X string } // Equal method (with interface argument) on pointer receiver + StructE struct{ X string } // Equal method (with interface argument on value receiver) on pointer receiver + StructF struct{ X string } // Equal method (with interface argument on pointer receiver) on value receiver + + // These embed the above types as a value. + StructA1 struct { + StructA + X string + } + StructB1 struct { + StructB + X string + } + StructC1 struct { + StructC + X string + } + StructD1 struct { + StructD + X string + } + StructE1 struct { + StructE + X string + } + StructF1 struct { + StructF + X string + } + + // These embed the above types as a pointer. + StructA2 struct { + *StructA + X string + } + StructB2 struct { + *StructB + X string + } + StructC2 struct { + *StructC + X string + } + StructD2 struct { + *StructD + X string + } + StructE2 struct { + *StructE + X string + } + StructF2 struct { + *StructF + X string + } + + StructNo struct{ X string } // Equal method (with interface argument) on non-satisfying receiver + + AssignA func() int + AssignB struct{ A int } + AssignC chan bool + AssignD <-chan bool +) + +func (x StructA) Equal(y StructA) bool { return true } +func (x *StructB) Equal(y *StructB) bool { return true } +func (x StructC) Equal(y InterfaceA) bool { return true } +func (x StructC) InterfaceA() {} +func (x *StructD) Equal(y InterfaceA) bool { return true } +func (x *StructD) InterfaceA() {} +func (x *StructE) Equal(y InterfaceA) bool { return true } +func (x StructE) InterfaceA() {} +func (x StructF) Equal(y InterfaceA) bool { return true } +func (x *StructF) InterfaceA() {} +func (x StructNo) Equal(y InterfaceA) bool { return true } + +func (x AssignA) Equal(y func() int) bool { return true } +func (x AssignB) Equal(y struct{ A int }) bool { return true } +func (x AssignC) Equal(y chan bool) bool { return true } +func (x AssignD) Equal(y <-chan bool) bool { return true } + +var _ = func( + a StructA, b StructB, c StructC, d StructD, e StructE, f StructF, + ap *StructA, bp *StructB, cp *StructC, dp *StructD, ep *StructE, fp *StructF, + a1 StructA1, b1 StructB1, c1 StructC1, d1 StructD1, e1 StructE1, f1 StructF1, + a2 StructA2, b2 StructB2, c2 StructC2, d2 StructD2, e2 StructE2, f2 StructF1, +) { + a.Equal(a) + b.Equal(&b) + c.Equal(c) + d.Equal(&d) + e.Equal(e) + f.Equal(&f) + + ap.Equal(*ap) + bp.Equal(bp) + cp.Equal(*cp) + dp.Equal(dp) + ep.Equal(*ep) + fp.Equal(fp) + + a1.Equal(a1.StructA) + b1.Equal(&b1.StructB) + c1.Equal(c1) + d1.Equal(&d1) + e1.Equal(e1) + f1.Equal(&f1) + + a2.Equal(*a2.StructA) + b2.Equal(b2.StructB) + c2.Equal(c2) + d2.Equal(&d2) + e2.Equal(e2) + f2.Equal(&f2) +} + +type ( + privateStruct struct{ Public, private int } + PublicStruct struct{ Public, private int } + ParentStructA struct{ privateStruct } + ParentStructB struct{ PublicStruct } + ParentStructC struct { + privateStruct + Public, private int + } + ParentStructD struct { + PublicStruct + Public, private int + } + ParentStructE struct { + privateStruct + PublicStruct + } + ParentStructF struct { + privateStruct + PublicStruct + Public, private int + } + ParentStructG struct { + *privateStruct + } + ParentStructH struct { + *PublicStruct + } + ParentStructI struct { + *privateStruct + *PublicStruct + } + ParentStructJ struct { + *privateStruct + *PublicStruct + Public PublicStruct + private privateStruct + } +) + +func NewParentStructG() *ParentStructG { + return &ParentStructG{new(privateStruct)} +} +func NewParentStructH() *ParentStructH { + return &ParentStructH{new(PublicStruct)} +} +func NewParentStructI() *ParentStructI { + return &ParentStructI{new(privateStruct), new(PublicStruct)} +} +func NewParentStructJ() *ParentStructJ { + return &ParentStructJ{ + privateStruct: new(privateStruct), PublicStruct: new(PublicStruct), + } +} +func (s *privateStruct) SetPrivate(i int) { s.private = i } +func (s *PublicStruct) SetPrivate(i int) { s.private = i } +func (s *ParentStructC) SetPrivate(i int) { s.private = i } +func (s *ParentStructD) SetPrivate(i int) { s.private = i } +func (s *ParentStructF) SetPrivate(i int) { s.private = i } +func (s *ParentStructA) PrivateStruct() *privateStruct { return &s.privateStruct } +func (s *ParentStructC) PrivateStruct() *privateStruct { return &s.privateStruct } +func (s *ParentStructE) PrivateStruct() *privateStruct { return &s.privateStruct } +func (s *ParentStructF) PrivateStruct() *privateStruct { return &s.privateStruct } +func (s *ParentStructG) PrivateStruct() *privateStruct { return s.privateStruct } +func (s *ParentStructI) PrivateStruct() *privateStruct { return s.privateStruct } +func (s *ParentStructJ) PrivateStruct() *privateStruct { return s.privateStruct } +func (s *ParentStructJ) Private() *privateStruct { return &s.private } diff --git a/vendor/github.com/google/go-cmp/cmp/internal/value/format.go b/vendor/github.com/google/go-cmp/cmp/internal/value/format.go new file mode 100644 index 000000000..abaeca89e --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/internal/value/format.go @@ -0,0 +1,259 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +// Package value provides functionality for reflect.Value types. +package value + +import ( + "fmt" + "reflect" + "strings" + "unicode" + "unicode/utf8" +) + +// formatFakePointers controls whether to substitute pointer addresses with nil. +// This is used for deterministic testing. +var formatFakePointers = false + +var stringerIface = reflect.TypeOf((*fmt.Stringer)(nil)).Elem() + +// Format formats the value v as a string. +// +// This is similar to fmt.Sprintf("%+v", v) except this: +// * Prints the type unless it can be elided +// * Avoids printing struct fields that are zero +// * Prints a nil-slice as being nil, not empty +// * Prints map entries in deterministic order +func Format(v reflect.Value, useStringer bool) string { + return formatAny(v, formatConfig{useStringer, true, true, !formatFakePointers}, nil) +} + +type formatConfig struct { + useStringer bool // Should the String method be used if available? + printType bool // Should we print the type before the value? + followPointers bool // Should we recursively follow pointers? + realPointers bool // Should we print the real address of pointers? +} + +func formatAny(v reflect.Value, conf formatConfig, visited map[uintptr]bool) string { + // TODO: Should this be a multi-line printout in certain situations? + + if !v.IsValid() { + return "" + } + if conf.useStringer && v.Type().Implements(stringerIface) && v.CanInterface() { + if (v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface) && v.IsNil() { + return "" + } + return fmt.Sprintf("%q", v.Interface().(fmt.Stringer).String()) + } + + switch v.Kind() { + case reflect.Bool: + return formatPrimitive(v.Type(), v.Bool(), conf) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return formatPrimitive(v.Type(), v.Int(), conf) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + if v.Type().PkgPath() == "" || v.Kind() == reflect.Uintptr { + // Unnamed uints are usually bytes or words, so use hexadecimal. + return formatPrimitive(v.Type(), formatHex(v.Uint()), conf) + } + return formatPrimitive(v.Type(), v.Uint(), conf) + case reflect.Float32, reflect.Float64: + return formatPrimitive(v.Type(), v.Float(), conf) + case reflect.Complex64, reflect.Complex128: + return formatPrimitive(v.Type(), v.Complex(), conf) + case reflect.String: + return formatPrimitive(v.Type(), fmt.Sprintf("%q", v), conf) + case reflect.UnsafePointer, reflect.Chan, reflect.Func: + return formatPointer(v, conf) + case reflect.Ptr: + if v.IsNil() { + if conf.printType { + return fmt.Sprintf("(%v)(nil)", v.Type()) + } + return "" + } + if visited[v.Pointer()] || !conf.followPointers { + return formatPointer(v, conf) + } + visited = insertPointer(visited, v.Pointer()) + return "&" + formatAny(v.Elem(), conf, visited) + case reflect.Interface: + if v.IsNil() { + if conf.printType { + return fmt.Sprintf("%v(nil)", v.Type()) + } + return "" + } + return formatAny(v.Elem(), conf, visited) + case reflect.Slice: + if v.IsNil() { + if conf.printType { + return fmt.Sprintf("%v(nil)", v.Type()) + } + return "" + } + if visited[v.Pointer()] { + return formatPointer(v, conf) + } + visited = insertPointer(visited, v.Pointer()) + fallthrough + case reflect.Array: + var ss []string + subConf := conf + subConf.printType = v.Type().Elem().Kind() == reflect.Interface + for i := 0; i < v.Len(); i++ { + s := formatAny(v.Index(i), subConf, visited) + ss = append(ss, s) + } + s := fmt.Sprintf("{%s}", strings.Join(ss, ", ")) + if conf.printType { + return v.Type().String() + s + } + return s + case reflect.Map: + if v.IsNil() { + if conf.printType { + return fmt.Sprintf("%v(nil)", v.Type()) + } + return "" + } + if visited[v.Pointer()] { + return formatPointer(v, conf) + } + visited = insertPointer(visited, v.Pointer()) + + var ss []string + subConf := conf + subConf.printType = v.Type().Elem().Kind() == reflect.Interface + for _, k := range SortKeys(v.MapKeys()) { + sk := formatAny(k, formatConfig{realPointers: conf.realPointers}, visited) + sv := formatAny(v.MapIndex(k), subConf, visited) + ss = append(ss, fmt.Sprintf("%s: %s", sk, sv)) + } + s := fmt.Sprintf("{%s}", strings.Join(ss, ", ")) + if conf.printType { + return v.Type().String() + s + } + return s + case reflect.Struct: + var ss []string + subConf := conf + subConf.printType = true + for i := 0; i < v.NumField(); i++ { + vv := v.Field(i) + if isZero(vv) { + continue // Elide zero value fields + } + name := v.Type().Field(i).Name + subConf.useStringer = conf.useStringer && isExported(name) + s := formatAny(vv, subConf, visited) + ss = append(ss, fmt.Sprintf("%s: %s", name, s)) + } + s := fmt.Sprintf("{%s}", strings.Join(ss, ", ")) + if conf.printType { + return v.Type().String() + s + } + return s + default: + panic(fmt.Sprintf("%v kind not handled", v.Kind())) + } +} + +func formatPrimitive(t reflect.Type, v interface{}, conf formatConfig) string { + if conf.printType && t.PkgPath() != "" { + return fmt.Sprintf("%v(%v)", t, v) + } + return fmt.Sprintf("%v", v) +} + +func formatPointer(v reflect.Value, conf formatConfig) string { + p := v.Pointer() + if !conf.realPointers { + p = 0 // For deterministic printing purposes + } + s := formatHex(uint64(p)) + if conf.printType { + return fmt.Sprintf("(%v)(%s)", v.Type(), s) + } + return s +} + +func formatHex(u uint64) string { + var f string + switch { + case u <= 0xff: + f = "0x%02x" + case u <= 0xffff: + f = "0x%04x" + case u <= 0xffffff: + f = "0x%06x" + case u <= 0xffffffff: + f = "0x%08x" + case u <= 0xffffffffff: + f = "0x%010x" + case u <= 0xffffffffffff: + f = "0x%012x" + case u <= 0xffffffffffffff: + f = "0x%014x" + case u <= 0xffffffffffffffff: + f = "0x%016x" + } + return fmt.Sprintf(f, u) +} + +// insertPointer insert p into m, allocating m if necessary. +func insertPointer(m map[uintptr]bool, p uintptr) map[uintptr]bool { + if m == nil { + m = make(map[uintptr]bool) + } + m[p] = true + return m +} + +// isZero reports whether v is the zero value. +// This does not rely on Interface and so can be used on unexported fields. +func isZero(v reflect.Value) bool { + switch v.Kind() { + case reflect.Bool: + return v.Bool() == false + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Complex64, reflect.Complex128: + return v.Complex() == 0 + case reflect.String: + return v.String() == "" + case reflect.UnsafePointer: + return v.Pointer() == 0 + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Ptr, reflect.Map, reflect.Slice: + return v.IsNil() + case reflect.Array: + for i := 0; i < v.Len(); i++ { + if !isZero(v.Index(i)) { + return false + } + } + return true + case reflect.Struct: + for i := 0; i < v.NumField(); i++ { + if !isZero(v.Field(i)) { + return false + } + } + return true + } + return false +} + +// isExported reports whether the identifier is exported. +func isExported(id string) bool { + r, _ := utf8.DecodeRuneInString(id) + return unicode.IsUpper(r) +} diff --git a/vendor/github.com/google/go-cmp/cmp/internal/value/format_test.go b/vendor/github.com/google/go-cmp/cmp/internal/value/format_test.go new file mode 100644 index 000000000..6498854f2 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/internal/value/format_test.go @@ -0,0 +1,91 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +package value + +import ( + "bytes" + "io" + "reflect" + "testing" +) + +func TestFormat(t *testing.T) { + type key struct { + a int + b string + c chan bool + } + + tests := []struct { + in interface{} + want string + }{{ + in: []int{}, + want: "[]int{}", + }, { + in: []int(nil), + want: "[]int(nil)", + }, { + in: []int{1, 2, 3, 4, 5}, + want: "[]int{1, 2, 3, 4, 5}", + }, { + in: []interface{}{1, true, "hello", struct{ A, B int }{1, 2}}, + want: "[]interface {}{1, true, \"hello\", struct { A int; B int }{A: 1, B: 2}}", + }, { + in: []struct{ A, B int }{{1, 2}, {0, 4}, {}}, + want: "[]struct { A int; B int }{{A: 1, B: 2}, {B: 4}, {}}", + }, { + in: map[*int]string{new(int): "hello"}, + want: "map[*int]string{0x00: \"hello\"}", + }, { + in: map[key]string{{}: "hello"}, + want: "map[value.key]string{{}: \"hello\"}", + }, { + in: map[key]string{{a: 5, b: "key", c: make(chan bool)}: "hello"}, + want: "map[value.key]string{{a: 5, b: \"key\", c: (chan bool)(0x00)}: \"hello\"}", + }, { + in: map[io.Reader]string{new(bytes.Reader): "hello"}, + want: "map[io.Reader]string{0x00: \"hello\"}", + }, { + in: func() interface{} { + var a = []interface{}{nil} + a[0] = a + return a + }(), + want: "[]interface {}{([]interface {})(0x00)}", + }, { + in: func() interface{} { + type A *A + var a A + a = &a + return a + }(), + want: "&(value.A)(0x00)", + }, { + in: func() interface{} { + type A map[*A]A + a := make(A) + a[&a] = a + return a + }(), + want: "value.A{0x00: 0x00}", + }, { + in: func() interface{} { + var a [2]interface{} + a[0] = &a + return a + }(), + want: "[2]interface {}{&[2]interface {}{(*[2]interface {})(0x00), interface {}(nil)}, interface {}(nil)}", + }} + + formatFakePointers = true + defer func() { formatFakePointers = false }() + for i, tt := range tests { + got := Format(reflect.ValueOf(tt.in), true) + if got != tt.want { + t.Errorf("test %d, Format():\ngot %q\nwant %q", i, got, tt.want) + } + } +} diff --git a/vendor/github.com/google/go-cmp/cmp/internal/value/sort.go b/vendor/github.com/google/go-cmp/cmp/internal/value/sort.go new file mode 100644 index 000000000..ea73cf143 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/internal/value/sort.go @@ -0,0 +1,111 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +package value + +import ( + "fmt" + "math" + "reflect" + "sort" +) + +// SortKeys sorts a list of map keys, deduplicating keys if necessary. +// The type of each value must be comparable. +func SortKeys(vs []reflect.Value) []reflect.Value { + if len(vs) == 0 { + return vs + } + + // Sort the map keys. + sort.Sort(valueSorter(vs)) + + // Deduplicate keys (fails for NaNs). + vs2 := vs[:1] + for _, v := range vs[1:] { + if v.Interface() != vs2[len(vs2)-1].Interface() { + vs2 = append(vs2, v) + } + } + return vs2 +} + +// TODO: Use sort.Slice once Google AppEngine is on Go1.8 or above. +type valueSorter []reflect.Value + +func (vs valueSorter) Len() int { return len(vs) } +func (vs valueSorter) Less(i, j int) bool { return isLess(vs[i], vs[j]) } +func (vs valueSorter) Swap(i, j int) { vs[i], vs[j] = vs[j], vs[i] } + +// isLess is a generic function for sorting arbitrary map keys. +// The inputs must be of the same type and must be comparable. +func isLess(x, y reflect.Value) bool { + switch x.Type().Kind() { + case reflect.Bool: + return !x.Bool() && y.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return x.Int() < y.Int() + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return x.Uint() < y.Uint() + case reflect.Float32, reflect.Float64: + fx, fy := x.Float(), y.Float() + return fx < fy || math.IsNaN(fx) && !math.IsNaN(fy) + case reflect.Complex64, reflect.Complex128: + cx, cy := x.Complex(), y.Complex() + rx, ix, ry, iy := real(cx), imag(cx), real(cy), imag(cy) + if rx == ry || (math.IsNaN(rx) && math.IsNaN(ry)) { + return ix < iy || math.IsNaN(ix) && !math.IsNaN(iy) + } + return rx < ry || math.IsNaN(rx) && !math.IsNaN(ry) + case reflect.Ptr, reflect.UnsafePointer, reflect.Chan: + return x.Pointer() < y.Pointer() + case reflect.String: + return x.String() < y.String() + case reflect.Array: + for i := 0; i < x.Len(); i++ { + if isLess(x.Index(i), y.Index(i)) { + return true + } + if isLess(y.Index(i), x.Index(i)) { + return false + } + } + return false + case reflect.Struct: + for i := 0; i < x.NumField(); i++ { + if isLess(x.Field(i), y.Field(i)) { + return true + } + if isLess(y.Field(i), x.Field(i)) { + return false + } + } + return false + case reflect.Interface: + vx, vy := x.Elem(), y.Elem() + if !vx.IsValid() || !vy.IsValid() { + return !vx.IsValid() && vy.IsValid() + } + tx, ty := vx.Type(), vy.Type() + if tx == ty { + return isLess(x.Elem(), y.Elem()) + } + if tx.Kind() != ty.Kind() { + return vx.Kind() < vy.Kind() + } + if tx.String() != ty.String() { + return tx.String() < ty.String() + } + if tx.PkgPath() != ty.PkgPath() { + return tx.PkgPath() < ty.PkgPath() + } + // This can happen in rare situations, so we fallback to just comparing + // the unique pointer for a reflect.Type. This guarantees deterministic + // ordering within a program, but it is obviously not stable. + return reflect.ValueOf(vx.Type()).Pointer() < reflect.ValueOf(vy.Type()).Pointer() + default: + // Must be Func, Map, or Slice; which are not comparable. + panic(fmt.Sprintf("%T is not comparable", x.Type())) + } +} diff --git a/vendor/github.com/google/go-cmp/cmp/internal/value/sort_test.go b/vendor/github.com/google/go-cmp/cmp/internal/value/sort_test.go new file mode 100644 index 000000000..c5a6bbb12 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/internal/value/sort_test.go @@ -0,0 +1,152 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +package value_test + +import ( + "math" + "reflect" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/internal/value" +) + +func TestSortKeys(t *testing.T) { + type ( + MyString string + MyArray [2]int + MyStruct struct { + A MyString + B MyArray + C chan float64 + } + EmptyStruct struct{} + ) + + opts := []cmp.Option{ + cmp.Comparer(func(x, y float64) bool { + if math.IsNaN(x) && math.IsNaN(y) { + return true + } + return x == y + }), + cmp.Comparer(func(x, y complex128) bool { + rx, ix, ry, iy := real(x), imag(x), real(y), imag(y) + if math.IsNaN(rx) && math.IsNaN(ry) { + rx, ry = 0, 0 + } + if math.IsNaN(ix) && math.IsNaN(iy) { + ix, iy = 0, 0 + } + return rx == ry && ix == iy + }), + cmp.Comparer(func(x, y chan bool) bool { return true }), + cmp.Comparer(func(x, y chan int) bool { return true }), + cmp.Comparer(func(x, y chan float64) bool { return true }), + cmp.Comparer(func(x, y chan interface{}) bool { return true }), + cmp.Comparer(func(x, y *int) bool { return true }), + } + + tests := []struct { + in map[interface{}]bool // Set of keys to sort + want []interface{} + }{{ + in: map[interface{}]bool{1: true, 2: true, 3: true}, + want: []interface{}{1, 2, 3}, + }, { + in: map[interface{}]bool{ + nil: true, + true: true, + false: true, + -5: true, + -55: true, + -555: true, + uint(1): true, + uint(11): true, + uint(111): true, + "abc": true, + "abcd": true, + "abcde": true, + "foo": true, + "bar": true, + MyString("abc"): true, + MyString("abcd"): true, + MyString("abcde"): true, + new(int): true, + new(int): true, + make(chan bool): true, + make(chan bool): true, + make(chan int): true, + make(chan interface{}): true, + math.Inf(+1): true, + math.Inf(-1): true, + 1.2345: true, + 12.345: true, + 123.45: true, + 1234.5: true, + 0 + 0i: true, + 1 + 0i: true, + 2 + 0i: true, + 0 + 1i: true, + 0 + 2i: true, + 0 + 3i: true, + [2]int{2, 3}: true, + [2]int{4, 0}: true, + [2]int{2, 4}: true, + MyArray([2]int{2, 4}): true, + EmptyStruct{}: true, + MyStruct{ + "bravo", [2]int{2, 3}, make(chan float64), + }: true, + MyStruct{ + "alpha", [2]int{3, 3}, make(chan float64), + }: true, + }, + want: []interface{}{ + nil, false, true, + -555, -55, -5, uint(1), uint(11), uint(111), + math.Inf(-1), 1.2345, 12.345, 123.45, 1234.5, math.Inf(+1), + (0 + 0i), (0 + 1i), (0 + 2i), (0 + 3i), (1 + 0i), (2 + 0i), + [2]int{2, 3}, [2]int{2, 4}, [2]int{4, 0}, MyArray([2]int{2, 4}), + make(chan bool), make(chan bool), make(chan int), make(chan interface{}), + new(int), new(int), + "abc", "abcd", "abcde", "bar", "foo", + MyString("abc"), MyString("abcd"), MyString("abcde"), + EmptyStruct{}, + MyStruct{"alpha", [2]int{3, 3}, make(chan float64)}, + MyStruct{"bravo", [2]int{2, 3}, make(chan float64)}, + }, + }, { + // NaN values cannot be properly deduplicated. + // This is okay since map entries with NaN in the keys cannot be + // retrieved anyways. + in: map[interface{}]bool{ + math.NaN(): true, + math.NaN(): true, + complex(0, math.NaN()): true, + complex(0, math.NaN()): true, + complex(math.NaN(), 0): true, + complex(math.NaN(), 0): true, + complex(math.NaN(), math.NaN()): true, + }, + want: []interface{}{ + math.NaN(), math.NaN(), math.NaN(), math.NaN(), + complex(math.NaN(), math.NaN()), complex(math.NaN(), math.NaN()), + complex(math.NaN(), 0), complex(math.NaN(), 0), complex(math.NaN(), 0), complex(math.NaN(), 0), + complex(0, math.NaN()), complex(0, math.NaN()), complex(0, math.NaN()), complex(0, math.NaN()), + }, + }} + + for i, tt := range tests { + keys := append(reflect.ValueOf(tt.in).MapKeys(), reflect.ValueOf(tt.in).MapKeys()...) + var got []interface{} + for _, k := range value.SortKeys(keys) { + got = append(got, k.Interface()) + } + if d := cmp.Diff(got, tt.want, opts...); d != "" { + t.Errorf("test %d, Sort() mismatch (-got +want):\n%s", i, d) + } + } +} diff --git a/vendor/github.com/google/go-cmp/cmp/options.go b/vendor/github.com/google/go-cmp/cmp/options.go new file mode 100644 index 000000000..a4e159ac4 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/options.go @@ -0,0 +1,446 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +package cmp + +import ( + "fmt" + "reflect" + "runtime" + "strings" + + "github.com/google/go-cmp/cmp/internal/function" +) + +// Option configures for specific behavior of Equal and Diff. In particular, +// the fundamental Option functions (Ignore, Transformer, and Comparer), +// configure how equality is determined. +// +// The fundamental options may be composed with filters (FilterPath and +// FilterValues) to control the scope over which they are applied. +// +// The cmp/cmpopts package provides helper functions for creating options that +// may be used with Equal and Diff. +type Option interface { + // filter applies all filters and returns the option that remains. + // Each option may only read s.curPath and call s.callTTBFunc. + // + // An Options is returned only if multiple comparers or transformers + // can apply simultaneously and will only contain values of those types + // or sub-Options containing values of those types. + filter(s *state, vx, vy reflect.Value, t reflect.Type) applicableOption +} + +// applicableOption represents the following types: +// Fundamental: ignore | invalid | *comparer | *transformer +// Grouping: Options +type applicableOption interface { + Option + + // apply executes the option and reports whether the option was applied. + // Each option may mutate s. + apply(s *state, vx, vy reflect.Value) bool +} + +// coreOption represents the following types: +// Fundamental: ignore | invalid | *comparer | *transformer +// Filters: *pathFilter | *valuesFilter +type coreOption interface { + Option + isCore() +} + +type core struct{} + +func (core) isCore() {} + +// Options is a list of Option values that also satisfies the Option interface. +// Helper comparison packages may return an Options value when packing multiple +// Option values into a single Option. When this package processes an Options, +// it will be implicitly expanded into a flat list. +// +// Applying a filter on an Options is equivalent to applying that same filter +// on all individual options held within. +type Options []Option + +func (opts Options) filter(s *state, vx, vy reflect.Value, t reflect.Type) (out applicableOption) { + for _, opt := range opts { + switch opt := opt.filter(s, vx, vy, t); opt.(type) { + case ignore: + return ignore{} // Only ignore can short-circuit evaluation + case invalid: + out = invalid{} // Takes precedence over comparer or transformer + case *comparer, *transformer, Options: + switch out.(type) { + case nil: + out = opt + case invalid: + // Keep invalid + case *comparer, *transformer, Options: + out = Options{out, opt} // Conflicting comparers or transformers + } + } + } + return out +} + +func (opts Options) apply(s *state, _, _ reflect.Value) bool { + const warning = "ambiguous set of applicable options" + const help = "consider using filters to ensure at most one Comparer or Transformer may apply" + var ss []string + for _, opt := range flattenOptions(nil, opts) { + ss = append(ss, fmt.Sprint(opt)) + } + set := strings.Join(ss, "\n\t") + panic(fmt.Sprintf("%s at %#v:\n\t%s\n%s", warning, s.curPath, set, help)) +} + +func (opts Options) String() string { + var ss []string + for _, opt := range opts { + ss = append(ss, fmt.Sprint(opt)) + } + return fmt.Sprintf("Options{%s}", strings.Join(ss, ", ")) +} + +// FilterPath returns a new Option where opt is only evaluated if filter f +// returns true for the current Path in the value tree. +// +// The option passed in may be an Ignore, Transformer, Comparer, Options, or +// a previously filtered Option. +func FilterPath(f func(Path) bool, opt Option) Option { + if f == nil { + panic("invalid path filter function") + } + if opt := normalizeOption(opt); opt != nil { + return &pathFilter{fnc: f, opt: opt} + } + return nil +} + +type pathFilter struct { + core + fnc func(Path) bool + opt Option +} + +func (f pathFilter) filter(s *state, vx, vy reflect.Value, t reflect.Type) applicableOption { + if f.fnc(s.curPath) { + return f.opt.filter(s, vx, vy, t) + } + return nil +} + +func (f pathFilter) String() string { + fn := getFuncName(reflect.ValueOf(f.fnc).Pointer()) + return fmt.Sprintf("FilterPath(%s, %v)", fn, f.opt) +} + +// FilterValues returns a new Option where opt is only evaluated if filter f, +// which is a function of the form "func(T, T) bool", returns true for the +// current pair of values being compared. If the type of the values is not +// assignable to T, then this filter implicitly returns false. +// +// The filter function must be +// symmetric (i.e., agnostic to the order of the inputs) and +// deterministic (i.e., produces the same result when given the same inputs). +// If T is an interface, it is possible that f is called with two values with +// different concrete types that both implement T. +// +// The option passed in may be an Ignore, Transformer, Comparer, Options, or +// a previously filtered Option. +func FilterValues(f interface{}, opt Option) Option { + v := reflect.ValueOf(f) + if !function.IsType(v.Type(), function.ValueFilter) || v.IsNil() { + panic(fmt.Sprintf("invalid values filter function: %T", f)) + } + if opt := normalizeOption(opt); opt != nil { + vf := &valuesFilter{fnc: v, opt: opt} + if ti := v.Type().In(0); ti.Kind() != reflect.Interface || ti.NumMethod() > 0 { + vf.typ = ti + } + return vf + } + return nil +} + +type valuesFilter struct { + core + typ reflect.Type // T + fnc reflect.Value // func(T, T) bool + opt Option +} + +func (f valuesFilter) filter(s *state, vx, vy reflect.Value, t reflect.Type) applicableOption { + if !vx.IsValid() || !vy.IsValid() { + return invalid{} + } + if (f.typ == nil || t.AssignableTo(f.typ)) && s.callTTBFunc(f.fnc, vx, vy) { + return f.opt.filter(s, vx, vy, t) + } + return nil +} + +func (f valuesFilter) String() string { + fn := getFuncName(f.fnc.Pointer()) + return fmt.Sprintf("FilterValues(%s, %v)", fn, f.opt) +} + +// Ignore is an Option that causes all comparisons to be ignored. +// This value is intended to be combined with FilterPath or FilterValues. +// It is an error to pass an unfiltered Ignore option to Equal. +func Ignore() Option { return ignore{} } + +type ignore struct{ core } + +func (ignore) isFiltered() bool { return false } +func (ignore) filter(_ *state, _, _ reflect.Value, _ reflect.Type) applicableOption { return ignore{} } +func (ignore) apply(_ *state, _, _ reflect.Value) bool { return true } +func (ignore) String() string { return "Ignore()" } + +// invalid is a sentinel Option type to indicate that some options could not +// be evaluated due to unexported fields. +type invalid struct{ core } + +func (invalid) filter(_ *state, _, _ reflect.Value, _ reflect.Type) applicableOption { return invalid{} } +func (invalid) apply(s *state, _, _ reflect.Value) bool { + const help = "consider using AllowUnexported or cmpopts.IgnoreUnexported" + panic(fmt.Sprintf("cannot handle unexported field: %#v\n%s", s.curPath, help)) +} + +// Transformer returns an Option that applies a transformation function that +// converts values of a certain type into that of another. +// +// The transformer f must be a function "func(T) R" that converts values of +// type T to those of type R and is implicitly filtered to input values +// assignable to T. The transformer must not mutate T in any way. +// If T and R are the same type, an additional filter must be applied to +// act as the base case to prevent an infinite recursion applying the same +// transform to itself (see the SortedSlice example). +// +// The name is a user provided label that is used as the Transform.Name in the +// transformation PathStep. If empty, an arbitrary name is used. +func Transformer(name string, f interface{}) Option { + v := reflect.ValueOf(f) + if !function.IsType(v.Type(), function.Transformer) || v.IsNil() { + panic(fmt.Sprintf("invalid transformer function: %T", f)) + } + if name == "" { + name = "λ" // Lambda-symbol as place-holder for anonymous transformer + } + if !isValid(name) { + panic(fmt.Sprintf("invalid name: %q", name)) + } + tr := &transformer{name: name, fnc: reflect.ValueOf(f)} + if ti := v.Type().In(0); ti.Kind() != reflect.Interface || ti.NumMethod() > 0 { + tr.typ = ti + } + return tr +} + +type transformer struct { + core + name string + typ reflect.Type // T + fnc reflect.Value // func(T) R +} + +func (tr *transformer) isFiltered() bool { return tr.typ != nil } + +func (tr *transformer) filter(_ *state, _, _ reflect.Value, t reflect.Type) applicableOption { + if tr.typ == nil || t.AssignableTo(tr.typ) { + return tr + } + return nil +} + +func (tr *transformer) apply(s *state, vx, vy reflect.Value) bool { + // Update path before calling the Transformer so that dynamic checks + // will use the updated path. + s.curPath.push(&transform{pathStep{tr.fnc.Type().Out(0)}, tr}) + defer s.curPath.pop() + + vx = s.callTRFunc(tr.fnc, vx) + vy = s.callTRFunc(tr.fnc, vy) + s.compareAny(vx, vy) + return true +} + +func (tr transformer) String() string { + return fmt.Sprintf("Transformer(%s, %s)", tr.name, getFuncName(tr.fnc.Pointer())) +} + +// Comparer returns an Option that determines whether two values are equal +// to each other. +// +// The comparer f must be a function "func(T, T) bool" and is implicitly +// filtered to input values assignable to T. If T is an interface, it is +// possible that f is called with two values of different concrete types that +// both implement T. +// +// The equality function must be: +// • Symmetric: equal(x, y) == equal(y, x) +// • Deterministic: equal(x, y) == equal(x, y) +// • Pure: equal(x, y) does not modify x or y +func Comparer(f interface{}) Option { + v := reflect.ValueOf(f) + if !function.IsType(v.Type(), function.Equal) || v.IsNil() { + panic(fmt.Sprintf("invalid comparer function: %T", f)) + } + cm := &comparer{fnc: v} + if ti := v.Type().In(0); ti.Kind() != reflect.Interface || ti.NumMethod() > 0 { + cm.typ = ti + } + return cm +} + +type comparer struct { + core + typ reflect.Type // T + fnc reflect.Value // func(T, T) bool +} + +func (cm *comparer) isFiltered() bool { return cm.typ != nil } + +func (cm *comparer) filter(_ *state, _, _ reflect.Value, t reflect.Type) applicableOption { + if cm.typ == nil || t.AssignableTo(cm.typ) { + return cm + } + return nil +} + +func (cm *comparer) apply(s *state, vx, vy reflect.Value) bool { + eq := s.callTTBFunc(cm.fnc, vx, vy) + s.report(eq, vx, vy) + return true +} + +func (cm comparer) String() string { + return fmt.Sprintf("Comparer(%s)", getFuncName(cm.fnc.Pointer())) +} + +// AllowUnexported returns an Option that forcibly allows operations on +// unexported fields in certain structs, which are specified by passing in a +// value of each struct type. +// +// Users of this option must understand that comparing on unexported fields +// from external packages is not safe since changes in the internal +// implementation of some external package may cause the result of Equal +// to unexpectedly change. However, it may be valid to use this option on types +// defined in an internal package where the semantic meaning of an unexported +// field is in the control of the user. +// +// For some cases, a custom Comparer should be used instead that defines +// equality as a function of the public API of a type rather than the underlying +// unexported implementation. +// +// For example, the reflect.Type documentation defines equality to be determined +// by the == operator on the interface (essentially performing a shallow pointer +// comparison) and most attempts to compare *regexp.Regexp types are interested +// in only checking that the regular expression strings are equal. +// Both of these are accomplished using Comparers: +// +// Comparer(func(x, y reflect.Type) bool { return x == y }) +// Comparer(func(x, y *regexp.Regexp) bool { return x.String() == y.String() }) +// +// In other cases, the cmpopts.IgnoreUnexported option can be used to ignore +// all unexported fields on specified struct types. +func AllowUnexported(types ...interface{}) Option { + if !supportAllowUnexported { + panic("AllowUnexported is not supported on App Engine Classic or GopherJS") + } + m := make(map[reflect.Type]bool) + for _, typ := range types { + t := reflect.TypeOf(typ) + if t.Kind() != reflect.Struct { + panic(fmt.Sprintf("invalid struct type: %T", typ)) + } + m[t] = true + } + return visibleStructs(m) +} + +type visibleStructs map[reflect.Type]bool + +func (visibleStructs) filter(_ *state, _, _ reflect.Value, _ reflect.Type) applicableOption { + panic("not implemented") +} + +// reporter is an Option that configures how differences are reported. +type reporter interface { + // TODO: Not exported yet. + // + // Perhaps add PushStep and PopStep and change Report to only accept + // a PathStep instead of the full-path? Adding a PushStep and PopStep makes + // it clear that we are traversing the value tree in a depth-first-search + // manner, which has an effect on how values are printed. + + Option + + // Report is called for every comparison made and will be provided with + // the two values being compared, the equality result, and the + // current path in the value tree. It is possible for x or y to be an + // invalid reflect.Value if one of the values is non-existent; + // which is possible with maps and slices. + Report(x, y reflect.Value, eq bool, p Path) +} + +// normalizeOption normalizes the input options such that all Options groups +// are flattened and groups with a single element are reduced to that element. +// Only coreOptions and Options containing coreOptions are allowed. +func normalizeOption(src Option) Option { + switch opts := flattenOptions(nil, Options{src}); len(opts) { + case 0: + return nil + case 1: + return opts[0] + default: + return opts + } +} + +// flattenOptions copies all options in src to dst as a flat list. +// Only coreOptions and Options containing coreOptions are allowed. +func flattenOptions(dst, src Options) Options { + for _, opt := range src { + switch opt := opt.(type) { + case nil: + continue + case Options: + dst = flattenOptions(dst, opt) + case coreOption: + dst = append(dst, opt) + default: + panic(fmt.Sprintf("invalid option type: %T", opt)) + } + } + return dst +} + +// getFuncName returns a short function name from the pointer. +// The string parsing logic works up until Go1.9. +func getFuncName(p uintptr) string { + fnc := runtime.FuncForPC(p) + if fnc == nil { + return "" + } + name := fnc.Name() // E.g., "long/path/name/mypkg.(mytype).(long/path/name/mypkg.myfunc)-fm" + if strings.HasSuffix(name, ")-fm") || strings.HasSuffix(name, ")·fm") { + // Strip the package name from method name. + name = strings.TrimSuffix(name, ")-fm") + name = strings.TrimSuffix(name, ")·fm") + if i := strings.LastIndexByte(name, '('); i >= 0 { + methodName := name[i+1:] // E.g., "long/path/name/mypkg.myfunc" + if j := strings.LastIndexByte(methodName, '.'); j >= 0 { + methodName = methodName[j+1:] // E.g., "myfunc" + } + name = name[:i] + methodName // E.g., "long/path/name/mypkg.(mytype)." + "myfunc" + } + } + if i := strings.LastIndexByte(name, '/'); i >= 0 { + // Strip the package name. + name = name[i+1:] // E.g., "mypkg.(mytype).myfunc" + } + return name +} diff --git a/vendor/github.com/google/go-cmp/cmp/options_test.go b/vendor/github.com/google/go-cmp/cmp/options_test.go new file mode 100644 index 000000000..009b524af --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/options_test.go @@ -0,0 +1,231 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +package cmp + +import ( + "io" + "reflect" + "strings" + "testing" + + ts "github.com/google/go-cmp/cmp/internal/teststructs" +) + +// Test that the creation of Option values with non-sensible inputs produces +// a run-time panic with a decent error message +func TestOptionPanic(t *testing.T) { + type myBool bool + tests := []struct { + label string // Test description + fnc interface{} // Option function to call + args []interface{} // Arguments to pass in + wantPanic string // Expected panic message + }{{ + label: "AllowUnexported", + fnc: AllowUnexported, + args: []interface{}{}, + }, { + label: "AllowUnexported", + fnc: AllowUnexported, + args: []interface{}{1}, + wantPanic: "invalid struct type", + }, { + label: "AllowUnexported", + fnc: AllowUnexported, + args: []interface{}{ts.StructA{}}, + }, { + label: "AllowUnexported", + fnc: AllowUnexported, + args: []interface{}{ts.StructA{}, ts.StructB{}, ts.StructA{}}, + }, { + label: "AllowUnexported", + fnc: AllowUnexported, + args: []interface{}{ts.StructA{}, &ts.StructB{}, ts.StructA{}}, + wantPanic: "invalid struct type", + }, { + label: "Comparer", + fnc: Comparer, + args: []interface{}{5}, + wantPanic: "invalid comparer function", + }, { + label: "Comparer", + fnc: Comparer, + args: []interface{}{func(x, y interface{}) bool { return true }}, + }, { + label: "Comparer", + fnc: Comparer, + args: []interface{}{func(x, y io.Reader) bool { return true }}, + }, { + label: "Comparer", + fnc: Comparer, + args: []interface{}{func(x, y io.Reader) myBool { return true }}, + wantPanic: "invalid comparer function", + }, { + label: "Comparer", + fnc: Comparer, + args: []interface{}{func(x string, y interface{}) bool { return true }}, + wantPanic: "invalid comparer function", + }, { + label: "Comparer", + fnc: Comparer, + args: []interface{}{(func(int, int) bool)(nil)}, + wantPanic: "invalid comparer function", + }, { + label: "Transformer", + fnc: Transformer, + args: []interface{}{"", 0}, + wantPanic: "invalid transformer function", + }, { + label: "Transformer", + fnc: Transformer, + args: []interface{}{"", func(int) int { return 0 }}, + }, { + label: "Transformer", + fnc: Transformer, + args: []interface{}{"", func(bool) bool { return true }}, + }, { + label: "Transformer", + fnc: Transformer, + args: []interface{}{"", func(int) bool { return true }}, + }, { + label: "Transformer", + fnc: Transformer, + args: []interface{}{"", func(int, int) bool { return true }}, + wantPanic: "invalid transformer function", + }, { + label: "Transformer", + fnc: Transformer, + args: []interface{}{"", (func(int) uint)(nil)}, + wantPanic: "invalid transformer function", + }, { + label: "Transformer", + fnc: Transformer, + args: []interface{}{"Func", func(Path) Path { return nil }}, + }, { + label: "Transformer", + fnc: Transformer, + args: []interface{}{"世界", func(int) bool { return true }}, + }, { + label: "Transformer", + fnc: Transformer, + args: []interface{}{"/*", func(int) bool { return true }}, + wantPanic: "invalid name", + }, { + label: "Transformer", + fnc: Transformer, + args: []interface{}{"_", func(int) bool { return true }}, + wantPanic: "invalid name", + }, { + label: "FilterPath", + fnc: FilterPath, + args: []interface{}{(func(Path) bool)(nil), Ignore()}, + wantPanic: "invalid path filter function", + }, { + label: "FilterPath", + fnc: FilterPath, + args: []interface{}{func(Path) bool { return true }, Ignore()}, + }, { + label: "FilterPath", + fnc: FilterPath, + args: []interface{}{func(Path) bool { return true }, &defaultReporter{}}, + wantPanic: "invalid option type", + }, { + label: "FilterPath", + fnc: FilterPath, + args: []interface{}{func(Path) bool { return true }, Options{Ignore(), Ignore()}}, + }, { + label: "FilterPath", + fnc: FilterPath, + args: []interface{}{func(Path) bool { return true }, Options{Ignore(), &defaultReporter{}}}, + wantPanic: "invalid option type", + }, { + label: "FilterValues", + fnc: FilterValues, + args: []interface{}{0, Ignore()}, + wantPanic: "invalid values filter function", + }, { + label: "FilterValues", + fnc: FilterValues, + args: []interface{}{func(x, y int) bool { return true }, Ignore()}, + }, { + label: "FilterValues", + fnc: FilterValues, + args: []interface{}{func(x, y interface{}) bool { return true }, Ignore()}, + }, { + label: "FilterValues", + fnc: FilterValues, + args: []interface{}{func(x, y interface{}) myBool { return true }, Ignore()}, + wantPanic: "invalid values filter function", + }, { + label: "FilterValues", + fnc: FilterValues, + args: []interface{}{func(x io.Reader, y interface{}) bool { return true }, Ignore()}, + wantPanic: "invalid values filter function", + }, { + label: "FilterValues", + fnc: FilterValues, + args: []interface{}{(func(int, int) bool)(nil), Ignore()}, + wantPanic: "invalid values filter function", + }, { + label: "FilterValues", + fnc: FilterValues, + args: []interface{}{func(int, int) bool { return true }, &defaultReporter{}}, + wantPanic: "invalid option type", + }, { + label: "FilterValues", + fnc: FilterValues, + args: []interface{}{func(int, int) bool { return true }, Options{Ignore(), Ignore()}}, + }, { + label: "FilterValues", + fnc: FilterValues, + args: []interface{}{func(int, int) bool { return true }, Options{Ignore(), &defaultReporter{}}}, + wantPanic: "invalid option type", + }} + + for _, tt := range tests { + tRun(t, tt.label, func(t *testing.T) { + var gotPanic string + func() { + defer func() { + if ex := recover(); ex != nil { + if s, ok := ex.(string); ok { + gotPanic = s + } else { + panic(ex) + } + } + }() + var vargs []reflect.Value + for _, arg := range tt.args { + vargs = append(vargs, reflect.ValueOf(arg)) + } + reflect.ValueOf(tt.fnc).Call(vargs) + }() + if tt.wantPanic == "" { + if gotPanic != "" { + t.Fatalf("unexpected panic message: %s", gotPanic) + } + } else { + if !strings.Contains(gotPanic, tt.wantPanic) { + t.Fatalf("panic message:\ngot: %s\nwant: %s", gotPanic, tt.wantPanic) + } + } + }) + } +} + +// TODO: Delete this hack when we drop Go1.6 support. +func tRun(t *testing.T, name string, f func(t *testing.T)) { + type runner interface { + Run(string, func(t *testing.T)) bool + } + var ti interface{} = t + if r, ok := ti.(runner); ok { + r.Run(name, f) + } else { + t.Logf("Test: %s", name) + f(t) + } +} diff --git a/vendor/github.com/google/go-cmp/cmp/path.go b/vendor/github.com/google/go-cmp/cmp/path.go new file mode 100644 index 000000000..0c2eb333f --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/path.go @@ -0,0 +1,293 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +package cmp + +import ( + "fmt" + "reflect" + "strings" + "unicode" + "unicode/utf8" +) + +type ( + // Path is a list of PathSteps describing the sequence of operations to get + // from some root type to the current position in the value tree. + // The first Path element is always an operation-less PathStep that exists + // simply to identify the initial type. + // + // When traversing structs with embedded structs, the embedded struct will + // always be accessed as a field before traversing the fields of the + // embedded struct themselves. That is, an exported field from the + // embedded struct will never be accessed directly from the parent struct. + Path []PathStep + + // PathStep is a union-type for specific operations to traverse + // a value's tree structure. Users of this package never need to implement + // these types as values of this type will be returned by this package. + PathStep interface { + String() string + Type() reflect.Type // Resulting type after performing the path step + isPathStep() + } + + // SliceIndex is an index operation on a slice or array at some index Key. + SliceIndex interface { + PathStep + Key() int // May return -1 if in a split state + + // SplitKeys returns the indexes for indexing into slices in the + // x and y values, respectively. These indexes may differ due to the + // insertion or removal of an element in one of the slices, causing + // all of the indexes to be shifted. If an index is -1, then that + // indicates that the element does not exist in the associated slice. + // + // Key is guaranteed to return -1 if and only if the indexes returned + // by SplitKeys are not the same. SplitKeys will never return -1 for + // both indexes. + SplitKeys() (x int, y int) + + isSliceIndex() + } + // MapIndex is an index operation on a map at some index Key. + MapIndex interface { + PathStep + Key() reflect.Value + isMapIndex() + } + // TypeAssertion represents a type assertion on an interface. + TypeAssertion interface { + PathStep + isTypeAssertion() + } + // StructField represents a struct field access on a field called Name. + StructField interface { + PathStep + Name() string + Index() int + isStructField() + } + // Indirect represents pointer indirection on the parent type. + Indirect interface { + PathStep + isIndirect() + } + // Transform is a transformation from the parent type to the current type. + Transform interface { + PathStep + Name() string + Func() reflect.Value + isTransform() + } +) + +func (pa *Path) push(s PathStep) { + *pa = append(*pa, s) +} + +func (pa *Path) pop() { + *pa = (*pa)[:len(*pa)-1] +} + +// Last returns the last PathStep in the Path. +// If the path is empty, this returns a non-nil PathStep that reports a nil Type. +func (pa Path) Last() PathStep { + if len(pa) > 0 { + return pa[len(pa)-1] + } + return pathStep{} +} + +// String returns the simplified path to a node. +// The simplified path only contains struct field accesses. +// +// For example: +// MyMap.MySlices.MyField +func (pa Path) String() string { + var ss []string + for _, s := range pa { + if _, ok := s.(*structField); ok { + ss = append(ss, s.String()) + } + } + return strings.TrimPrefix(strings.Join(ss, ""), ".") +} + +// GoString returns the path to a specific node using Go syntax. +// +// For example: +// (*root.MyMap["key"].(*mypkg.MyStruct).MySlices)[2][3].MyField +func (pa Path) GoString() string { + var ssPre, ssPost []string + var numIndirect int + for i, s := range pa { + var nextStep PathStep + if i+1 < len(pa) { + nextStep = pa[i+1] + } + switch s := s.(type) { + case *indirect: + numIndirect++ + pPre, pPost := "(", ")" + switch nextStep.(type) { + case *indirect: + continue // Next step is indirection, so let them batch up + case *structField: + numIndirect-- // Automatic indirection on struct fields + case nil: + pPre, pPost = "", "" // Last step; no need for parenthesis + } + if numIndirect > 0 { + ssPre = append(ssPre, pPre+strings.Repeat("*", numIndirect)) + ssPost = append(ssPost, pPost) + } + numIndirect = 0 + continue + case *transform: + ssPre = append(ssPre, s.trans.name+"(") + ssPost = append(ssPost, ")") + continue + case *typeAssertion: + // Elide type assertions immediately following a transform to + // prevent overly verbose path printouts. + // Some transforms return interface{} because of Go's lack of + // generics, but typically take in and return the exact same + // concrete type. Other times, the transform creates an anonymous + // struct, which will be very verbose to print. + if _, ok := nextStep.(*transform); ok { + continue + } + } + ssPost = append(ssPost, s.String()) + } + for i, j := 0, len(ssPre)-1; i < j; i, j = i+1, j-1 { + ssPre[i], ssPre[j] = ssPre[j], ssPre[i] + } + return strings.Join(ssPre, "") + strings.Join(ssPost, "") +} + +type ( + pathStep struct { + typ reflect.Type + } + + sliceIndex struct { + pathStep + xkey, ykey int + } + mapIndex struct { + pathStep + key reflect.Value + } + typeAssertion struct { + pathStep + } + structField struct { + pathStep + name string + idx int + + // These fields are used for forcibly accessing an unexported field. + // pvx, pvy, and field are only valid if unexported is true. + unexported bool + force bool // Forcibly allow visibility + pvx, pvy reflect.Value // Parent values + field reflect.StructField // Field information + } + indirect struct { + pathStep + } + transform struct { + pathStep + trans *transformer + } +) + +func (ps pathStep) Type() reflect.Type { return ps.typ } +func (ps pathStep) String() string { + if ps.typ == nil { + return "" + } + s := ps.typ.String() + if s == "" || strings.ContainsAny(s, "{}\n") { + return "root" // Type too simple or complex to print + } + return fmt.Sprintf("{%s}", s) +} + +func (si sliceIndex) String() string { + switch { + case si.xkey == si.ykey: + return fmt.Sprintf("[%d]", si.xkey) + case si.ykey == -1: + // [5->?] means "I don't know where X[5] went" + return fmt.Sprintf("[%d->?]", si.xkey) + case si.xkey == -1: + // [?->3] means "I don't know where Y[3] came from" + return fmt.Sprintf("[?->%d]", si.ykey) + default: + // [5->3] means "X[5] moved to Y[3]" + return fmt.Sprintf("[%d->%d]", si.xkey, si.ykey) + } +} +func (mi mapIndex) String() string { return fmt.Sprintf("[%#v]", mi.key) } +func (ta typeAssertion) String() string { return fmt.Sprintf(".(%v)", ta.typ) } +func (sf structField) String() string { return fmt.Sprintf(".%s", sf.name) } +func (in indirect) String() string { return "*" } +func (tf transform) String() string { return fmt.Sprintf("%s()", tf.trans.name) } + +func (si sliceIndex) Key() int { + if si.xkey != si.ykey { + return -1 + } + return si.xkey +} +func (si sliceIndex) SplitKeys() (x, y int) { return si.xkey, si.ykey } +func (mi mapIndex) Key() reflect.Value { return mi.key } +func (sf structField) Name() string { return sf.name } +func (sf structField) Index() int { return sf.idx } +func (tf transform) Name() string { return tf.trans.name } +func (tf transform) Func() reflect.Value { return tf.trans.fnc } + +func (pathStep) isPathStep() {} +func (sliceIndex) isSliceIndex() {} +func (mapIndex) isMapIndex() {} +func (typeAssertion) isTypeAssertion() {} +func (structField) isStructField() {} +func (indirect) isIndirect() {} +func (transform) isTransform() {} + +var ( + _ SliceIndex = sliceIndex{} + _ MapIndex = mapIndex{} + _ TypeAssertion = typeAssertion{} + _ StructField = structField{} + _ Indirect = indirect{} + _ Transform = transform{} + + _ PathStep = sliceIndex{} + _ PathStep = mapIndex{} + _ PathStep = typeAssertion{} + _ PathStep = structField{} + _ PathStep = indirect{} + _ PathStep = transform{} +) + +// isExported reports whether the identifier is exported. +func isExported(id string) bool { + r, _ := utf8.DecodeRuneInString(id) + return unicode.IsUpper(r) +} + +// isValid reports whether the identifier is valid. +// Empty and underscore-only strings are not valid. +func isValid(id string) bool { + ok := id != "" && id != "_" + for j, c := range id { + ok = ok && (j > 0 || !unicode.IsDigit(c)) + ok = ok && (c == '_' || unicode.IsLetter(c) || unicode.IsDigit(c)) + } + return ok +} diff --git a/vendor/github.com/google/go-cmp/cmp/reporter.go b/vendor/github.com/google/go-cmp/cmp/reporter.go new file mode 100644 index 000000000..a21d0cded --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/reporter.go @@ -0,0 +1,53 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +package cmp + +import ( + "fmt" + "reflect" + "strings" + + "github.com/google/go-cmp/cmp/internal/value" +) + +type defaultReporter struct { + Option + diffs []string // List of differences, possibly truncated + ndiffs int // Total number of differences + nbytes int // Number of bytes in diffs + nlines int // Number of lines in diffs +} + +var _ reporter = (*defaultReporter)(nil) + +func (r *defaultReporter) Report(x, y reflect.Value, eq bool, p Path) { + if eq { + return // Ignore equal results + } + const maxBytes = 4096 + const maxLines = 256 + r.ndiffs++ + if r.nbytes < maxBytes && r.nlines < maxLines { + sx := value.Format(x, true) + sy := value.Format(y, true) + if sx == sy { + // Stringer is not helpful, so rely on more exact formatting. + sx = value.Format(x, false) + sy = value.Format(y, false) + } + s := fmt.Sprintf("%#v:\n\t-: %s\n\t+: %s\n", p, sx, sy) + r.diffs = append(r.diffs, s) + r.nbytes += len(s) + r.nlines += strings.Count(s, "\n") + } +} + +func (r *defaultReporter) String() string { + s := strings.Join(r.diffs, "") + if r.ndiffs == len(r.diffs) { + return s + } + return fmt.Sprintf("%s... %d more differences ...", s, len(r.diffs)-r.ndiffs) +} diff --git a/vendor/github.com/google/go-cmp/cmp/unsafe_panic.go b/vendor/github.com/google/go-cmp/cmp/unsafe_panic.go new file mode 100644 index 000000000..0d44987f5 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/unsafe_panic.go @@ -0,0 +1,15 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +// +build appengine js + +package cmp + +import "reflect" + +const supportAllowUnexported = false + +func unsafeRetrieveField(reflect.Value, reflect.StructField) reflect.Value { + panic("unsafeRetrieveField is not implemented") +} diff --git a/vendor/github.com/google/go-cmp/cmp/unsafe_reflect.go b/vendor/github.com/google/go-cmp/cmp/unsafe_reflect.go new file mode 100644 index 000000000..81fb82632 --- /dev/null +++ b/vendor/github.com/google/go-cmp/cmp/unsafe_reflect.go @@ -0,0 +1,23 @@ +// Copyright 2017, The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE.md file. + +// +build !appengine,!js + +package cmp + +import ( + "reflect" + "unsafe" +) + +const supportAllowUnexported = true + +// unsafeRetrieveField uses unsafe to forcibly retrieve any field from a struct +// such that the value has read-write permissions. +// +// The parent struct, v, must be addressable, while f must be a StructField +// describing the field to retrieve. +func unsafeRetrieveField(v reflect.Value, f reflect.StructField) reflect.Value { + return reflect.NewAt(f.Type, unsafe.Pointer(v.UnsafeAddr()+f.Offset)).Elem() +} From a56b8fad87cc8faaf3d9e3b312c752b3b7902bc2 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 8 Apr 2018 21:57:52 +0200 Subject: [PATCH 15/30] repository: Improve buffer pooling --- internal/repository/pool.go | 2 +- internal/repository/repository.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/repository/pool.go b/internal/repository/pool.go index 9c7450d5c..b87791f14 100644 --- a/internal/repository/pool.go +++ b/internal/repository/pool.go @@ -8,7 +8,7 @@ import ( var bufPool = sync.Pool{ New: func() interface{} { - return make([]byte, chunker.MinSize) + return make([]byte, chunker.MaxSize/3) }, } diff --git a/internal/repository/repository.go b/internal/repository/repository.go index 3a1ce40c2..4a76f4025 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -214,11 +214,11 @@ func (r *Repository) SaveAndEncrypt(ctx context.Context, t restic.BlobType, data // get buf from the pool ciphertext := getBuf() - defer freeBuf(ciphertext) ciphertext = ciphertext[:0] nonce := crypto.NewRandomNonce() ciphertext = append(ciphertext, nonce...) + defer freeBuf(ciphertext) // encrypt blob ciphertext = r.key.Seal(ciphertext, nonce, data, nil) From b804279fe892a466bd70baa434c4bb90f672507c Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Fri, 20 Apr 2018 21:36:05 +0200 Subject: [PATCH 16/30] integration tests: Don't print anything to stdout --- cmd/restic/integration_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cmd/restic/integration_test.go b/cmd/restic/integration_test.go index 5b4e67e17..8b90c3cd1 100644 --- a/cmd/restic/integration_test.go +++ b/cmd/restic/integration_test.go @@ -52,6 +52,7 @@ func testRunInit(t testing.TB, opts GlobalOptions) { } func testRunBackup(t testing.TB, target []string, opts BackupOptions, gopts GlobalOptions) { + gopts.stdout = ioutil.Discard t.Logf("backing up %v", target) rtest.OK(t, runBackup(opts, gopts, target)) } From 3cd92efdcfccd09bf69fadccb2f6f4fa500577e1 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Tue, 10 Apr 2018 21:42:52 +0200 Subject: [PATCH 17/30] Vendor github.com/mattn/go-isatty --- Gopkg.lock | 8 +- vendor/github.com/mattn/go-isatty/.travis.yml | 9 ++ vendor/github.com/mattn/go-isatty/LICENSE | 9 ++ vendor/github.com/mattn/go-isatty/README.md | 50 ++++++++++ vendor/github.com/mattn/go-isatty/doc.go | 2 + .../mattn/go-isatty/example_test.go | 18 ++++ .../mattn/go-isatty/isatty_appengine.go | 15 +++ .../github.com/mattn/go-isatty/isatty_bsd.go | 18 ++++ .../mattn/go-isatty/isatty_linux.go | 18 ++++ .../mattn/go-isatty/isatty_linux_ppc64x.go | 19 ++++ .../mattn/go-isatty/isatty_others.go | 10 ++ .../mattn/go-isatty/isatty_others_test.go | 19 ++++ .../mattn/go-isatty/isatty_solaris.go | 16 ++++ .../mattn/go-isatty/isatty_windows.go | 94 +++++++++++++++++++ .../mattn/go-isatty/isatty_windows_test.go | 35 +++++++ 15 files changed, 339 insertions(+), 1 deletion(-) create mode 100644 vendor/github.com/mattn/go-isatty/.travis.yml create mode 100644 vendor/github.com/mattn/go-isatty/LICENSE create mode 100644 vendor/github.com/mattn/go-isatty/README.md create mode 100644 vendor/github.com/mattn/go-isatty/doc.go create mode 100644 vendor/github.com/mattn/go-isatty/example_test.go create mode 100644 vendor/github.com/mattn/go-isatty/isatty_appengine.go create mode 100644 vendor/github.com/mattn/go-isatty/isatty_bsd.go create mode 100644 vendor/github.com/mattn/go-isatty/isatty_linux.go create mode 100644 vendor/github.com/mattn/go-isatty/isatty_linux_ppc64x.go create mode 100644 vendor/github.com/mattn/go-isatty/isatty_others.go create mode 100644 vendor/github.com/mattn/go-isatty/isatty_others_test.go create mode 100644 vendor/github.com/mattn/go-isatty/isatty_solaris.go create mode 100644 vendor/github.com/mattn/go-isatty/isatty_windows.go create mode 100644 vendor/github.com/mattn/go-isatty/isatty_windows_test.go diff --git a/Gopkg.lock b/Gopkg.lock index ccbe4f7c8..f08ab4549 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -103,6 +103,12 @@ revision = "8bd9a64bf37eb297b492a4101fb28e80ac0b290f" version = "v1.1.0" +[[projects]] + name = "github.com/mattn/go-isatty" + packages = ["."] + revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39" + version = "v0.0.3" + [[projects]] name = "github.com/minio/minio-go" packages = [".","pkg/credentials","pkg/encrypt","pkg/policy","pkg/s3signer","pkg/s3utils","pkg/set"] @@ -238,6 +244,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "d3d59414a33bb8ecc6d88a681c782a87244a565cc9d0f85615cfa0704c02800a" + inputs-digest = "a270b39954b9dad18c46f097be5816ca58ae1f387940b673b387d30934ce4ed4" solver-name = "gps-cdcl" solver-version = 1 diff --git a/vendor/github.com/mattn/go-isatty/.travis.yml b/vendor/github.com/mattn/go-isatty/.travis.yml new file mode 100644 index 000000000..b9f8b239c --- /dev/null +++ b/vendor/github.com/mattn/go-isatty/.travis.yml @@ -0,0 +1,9 @@ +language: go +go: + - tip + +before_install: + - go get github.com/mattn/goveralls + - go get golang.org/x/tools/cmd/cover +script: + - $HOME/gopath/bin/goveralls -repotoken 3gHdORO5k5ziZcWMBxnd9LrMZaJs8m9x5 diff --git a/vendor/github.com/mattn/go-isatty/LICENSE b/vendor/github.com/mattn/go-isatty/LICENSE new file mode 100644 index 000000000..65dc692b6 --- /dev/null +++ b/vendor/github.com/mattn/go-isatty/LICENSE @@ -0,0 +1,9 @@ +Copyright (c) Yasuhiro MATSUMOTO + +MIT License (Expat) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/mattn/go-isatty/README.md b/vendor/github.com/mattn/go-isatty/README.md new file mode 100644 index 000000000..1e69004bb --- /dev/null +++ b/vendor/github.com/mattn/go-isatty/README.md @@ -0,0 +1,50 @@ +# go-isatty + +[![Godoc Reference](https://godoc.org/github.com/mattn/go-isatty?status.svg)](http://godoc.org/github.com/mattn/go-isatty) +[![Build Status](https://travis-ci.org/mattn/go-isatty.svg?branch=master)](https://travis-ci.org/mattn/go-isatty) +[![Coverage Status](https://coveralls.io/repos/github/mattn/go-isatty/badge.svg?branch=master)](https://coveralls.io/github/mattn/go-isatty?branch=master) +[![Go Report Card](https://goreportcard.com/badge/mattn/go-isatty)](https://goreportcard.com/report/mattn/go-isatty) + +isatty for golang + +## Usage + +```go +package main + +import ( + "fmt" + "github.com/mattn/go-isatty" + "os" +) + +func main() { + if isatty.IsTerminal(os.Stdout.Fd()) { + fmt.Println("Is Terminal") + } else if isatty.IsCygwinTerminal(os.Stdout.Fd()) { + fmt.Println("Is Cygwin/MSYS2 Terminal") + } else { + fmt.Println("Is Not Terminal") + } +} +``` + +## Installation + +``` +$ go get github.com/mattn/go-isatty +``` + +## License + +MIT + +## Author + +Yasuhiro Matsumoto (a.k.a mattn) + +## Thanks + +* k-takata: base idea for IsCygwinTerminal + + https://github.com/k-takata/go-iscygpty diff --git a/vendor/github.com/mattn/go-isatty/doc.go b/vendor/github.com/mattn/go-isatty/doc.go new file mode 100644 index 000000000..17d4f90eb --- /dev/null +++ b/vendor/github.com/mattn/go-isatty/doc.go @@ -0,0 +1,2 @@ +// Package isatty implements interface to isatty +package isatty diff --git a/vendor/github.com/mattn/go-isatty/example_test.go b/vendor/github.com/mattn/go-isatty/example_test.go new file mode 100644 index 000000000..fa8f7e745 --- /dev/null +++ b/vendor/github.com/mattn/go-isatty/example_test.go @@ -0,0 +1,18 @@ +package isatty_test + +import ( + "fmt" + "os" + + "github.com/mattn/go-isatty" +) + +func Example() { + if isatty.IsTerminal(os.Stdout.Fd()) { + fmt.Println("Is Terminal") + } else if isatty.IsCygwinTerminal(os.Stdout.Fd()) { + fmt.Println("Is Cygwin/MSYS2 Terminal") + } else { + fmt.Println("Is Not Terminal") + } +} diff --git a/vendor/github.com/mattn/go-isatty/isatty_appengine.go b/vendor/github.com/mattn/go-isatty/isatty_appengine.go new file mode 100644 index 000000000..9584a9884 --- /dev/null +++ b/vendor/github.com/mattn/go-isatty/isatty_appengine.go @@ -0,0 +1,15 @@ +// +build appengine + +package isatty + +// IsTerminal returns true if the file descriptor is terminal which +// is always false on on appengine classic which is a sandboxed PaaS. +func IsTerminal(fd uintptr) bool { + return false +} + +// IsCygwinTerminal() return true if the file descriptor is a cygwin or msys2 +// terminal. This is also always false on this environment. +func IsCygwinTerminal(fd uintptr) bool { + return false +} diff --git a/vendor/github.com/mattn/go-isatty/isatty_bsd.go b/vendor/github.com/mattn/go-isatty/isatty_bsd.go new file mode 100644 index 000000000..42f2514d1 --- /dev/null +++ b/vendor/github.com/mattn/go-isatty/isatty_bsd.go @@ -0,0 +1,18 @@ +// +build darwin freebsd openbsd netbsd dragonfly +// +build !appengine + +package isatty + +import ( + "syscall" + "unsafe" +) + +const ioctlReadTermios = syscall.TIOCGETA + +// IsTerminal return true if the file descriptor is terminal. +func IsTerminal(fd uintptr) bool { + var termios syscall.Termios + _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, fd, ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0) + return err == 0 +} diff --git a/vendor/github.com/mattn/go-isatty/isatty_linux.go b/vendor/github.com/mattn/go-isatty/isatty_linux.go new file mode 100644 index 000000000..7384cf991 --- /dev/null +++ b/vendor/github.com/mattn/go-isatty/isatty_linux.go @@ -0,0 +1,18 @@ +// +build linux +// +build !appengine,!ppc64,!ppc64le + +package isatty + +import ( + "syscall" + "unsafe" +) + +const ioctlReadTermios = syscall.TCGETS + +// IsTerminal return true if the file descriptor is terminal. +func IsTerminal(fd uintptr) bool { + var termios syscall.Termios + _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, fd, ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0) + return err == 0 +} diff --git a/vendor/github.com/mattn/go-isatty/isatty_linux_ppc64x.go b/vendor/github.com/mattn/go-isatty/isatty_linux_ppc64x.go new file mode 100644 index 000000000..44e5d2130 --- /dev/null +++ b/vendor/github.com/mattn/go-isatty/isatty_linux_ppc64x.go @@ -0,0 +1,19 @@ +// +build linux +// +build ppc64 ppc64le + +package isatty + +import ( + "unsafe" + + syscall "golang.org/x/sys/unix" +) + +const ioctlReadTermios = syscall.TCGETS + +// IsTerminal return true if the file descriptor is terminal. +func IsTerminal(fd uintptr) bool { + var termios syscall.Termios + _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, fd, ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0) + return err == 0 +} diff --git a/vendor/github.com/mattn/go-isatty/isatty_others.go b/vendor/github.com/mattn/go-isatty/isatty_others.go new file mode 100644 index 000000000..ff4de3d9a --- /dev/null +++ b/vendor/github.com/mattn/go-isatty/isatty_others.go @@ -0,0 +1,10 @@ +// +build !windows +// +build !appengine + +package isatty + +// IsCygwinTerminal() return true if the file descriptor is a cygwin or msys2 +// terminal. This is also always false on this environment. +func IsCygwinTerminal(fd uintptr) bool { + return false +} diff --git a/vendor/github.com/mattn/go-isatty/isatty_others_test.go b/vendor/github.com/mattn/go-isatty/isatty_others_test.go new file mode 100644 index 000000000..a2091cf47 --- /dev/null +++ b/vendor/github.com/mattn/go-isatty/isatty_others_test.go @@ -0,0 +1,19 @@ +// +build !windows + +package isatty + +import ( + "os" + "testing" +) + +func TestTerminal(t *testing.T) { + // test for non-panic + IsTerminal(os.Stdout.Fd()) +} + +func TestCygwinPipeName(t *testing.T) { + if IsCygwinTerminal(os.Stdout.Fd()) { + t.Fatal("should be false always") + } +} diff --git a/vendor/github.com/mattn/go-isatty/isatty_solaris.go b/vendor/github.com/mattn/go-isatty/isatty_solaris.go new file mode 100644 index 000000000..1f0c6bf53 --- /dev/null +++ b/vendor/github.com/mattn/go-isatty/isatty_solaris.go @@ -0,0 +1,16 @@ +// +build solaris +// +build !appengine + +package isatty + +import ( + "golang.org/x/sys/unix" +) + +// IsTerminal returns true if the given file descriptor is a terminal. +// see: http://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libbc/libc/gen/common/isatty.c +func IsTerminal(fd uintptr) bool { + var termio unix.Termio + err := unix.IoctlSetTermio(int(fd), unix.TCGETA, &termio) + return err == nil +} diff --git a/vendor/github.com/mattn/go-isatty/isatty_windows.go b/vendor/github.com/mattn/go-isatty/isatty_windows.go new file mode 100644 index 000000000..af51cbcaa --- /dev/null +++ b/vendor/github.com/mattn/go-isatty/isatty_windows.go @@ -0,0 +1,94 @@ +// +build windows +// +build !appengine + +package isatty + +import ( + "strings" + "syscall" + "unicode/utf16" + "unsafe" +) + +const ( + fileNameInfo uintptr = 2 + fileTypePipe = 3 +) + +var ( + kernel32 = syscall.NewLazyDLL("kernel32.dll") + procGetConsoleMode = kernel32.NewProc("GetConsoleMode") + procGetFileInformationByHandleEx = kernel32.NewProc("GetFileInformationByHandleEx") + procGetFileType = kernel32.NewProc("GetFileType") +) + +func init() { + // Check if GetFileInformationByHandleEx is available. + if procGetFileInformationByHandleEx.Find() != nil { + procGetFileInformationByHandleEx = nil + } +} + +// IsTerminal return true if the file descriptor is terminal. +func IsTerminal(fd uintptr) bool { + var st uint32 + r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, fd, uintptr(unsafe.Pointer(&st)), 0) + return r != 0 && e == 0 +} + +// Check pipe name is used for cygwin/msys2 pty. +// Cygwin/MSYS2 PTY has a name like: +// \{cygwin,msys}-XXXXXXXXXXXXXXXX-ptyN-{from,to}-master +func isCygwinPipeName(name string) bool { + token := strings.Split(name, "-") + if len(token) < 5 { + return false + } + + if token[0] != `\msys` && token[0] != `\cygwin` { + return false + } + + if token[1] == "" { + return false + } + + if !strings.HasPrefix(token[2], "pty") { + return false + } + + if token[3] != `from` && token[3] != `to` { + return false + } + + if token[4] != "master" { + return false + } + + return true +} + +// IsCygwinTerminal() return true if the file descriptor is a cygwin or msys2 +// terminal. +func IsCygwinTerminal(fd uintptr) bool { + if procGetFileInformationByHandleEx == nil { + return false + } + + // Cygwin/msys's pty is a pipe. + ft, _, e := syscall.Syscall(procGetFileType.Addr(), 1, fd, 0, 0) + if ft != fileTypePipe || e != 0 { + return false + } + + var buf [2 + syscall.MAX_PATH]uint16 + r, _, e := syscall.Syscall6(procGetFileInformationByHandleEx.Addr(), + 4, fd, fileNameInfo, uintptr(unsafe.Pointer(&buf)), + uintptr(len(buf)*2), 0, 0) + if r == 0 || e != 0 { + return false + } + + l := *(*uint32)(unsafe.Pointer(&buf)) + return isCygwinPipeName(string(utf16.Decode(buf[2 : 2+l/2]))) +} diff --git a/vendor/github.com/mattn/go-isatty/isatty_windows_test.go b/vendor/github.com/mattn/go-isatty/isatty_windows_test.go new file mode 100644 index 000000000..777e8a603 --- /dev/null +++ b/vendor/github.com/mattn/go-isatty/isatty_windows_test.go @@ -0,0 +1,35 @@ +// +build windows + +package isatty + +import ( + "testing" +) + +func TestCygwinPipeName(t *testing.T) { + tests := []struct { + name string + result bool + }{ + {``, false}, + {`\msys-`, false}, + {`\cygwin-----`, false}, + {`\msys-x-PTY5-pty1-from-master`, false}, + {`\cygwin-x-PTY5-from-master`, false}, + {`\cygwin-x-pty2-from-toaster`, false}, + {`\cygwin--pty2-from-master`, false}, + {`\\cygwin-x-pty2-from-master`, false}, + {`\cygwin-x-pty2-from-master-`, true}, // for the feature + {`\cygwin-e022582115c10879-pty4-from-master`, true}, + {`\msys-e022582115c10879-pty4-to-master`, true}, + {`\cygwin-e022582115c10879-pty4-to-master`, true}, + } + + for _, test := range tests { + want := test.result + got := isCygwinPipeName(test.name) + if want != got { + t.Fatalf("isatty(%q): got %v, want %v:", test.name, got, want) + } + } +} From fd12a3af20c0fc7cd296c0f11f02d1f786b07538 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Thu, 19 Apr 2018 15:14:20 +0200 Subject: [PATCH 18/30] Remove old archiver code --- internal/archiver/archive_reader.go | 117 -- internal/archiver/archive_reader_test.go | 206 --- internal/archiver/archiver.go | 877 ----------- internal/archiver/archiver_int_test.go | 145 -- internal/archiver/archiver_test.go | 258 --- internal/archiver/buffer_pool.go | 21 - internal/pipe/doc.go | 2 - internal/pipe/pipe.go | 292 ---- internal/pipe/pipe_test.go | 600 ------- .../walk/testdata/walktree-test-repo.tar.gz | Bin 404224 -> 0 bytes internal/walk/walk.go | 197 --- internal/walk/walk_test.go | 1394 ----------------- 12 files changed, 4109 deletions(-) delete mode 100644 internal/archiver/archive_reader.go delete mode 100644 internal/archiver/archive_reader_test.go delete mode 100644 internal/archiver/archiver.go delete mode 100644 internal/archiver/archiver_int_test.go delete mode 100644 internal/archiver/archiver_test.go delete mode 100644 internal/archiver/buffer_pool.go delete mode 100644 internal/pipe/doc.go delete mode 100644 internal/pipe/pipe.go delete mode 100644 internal/pipe/pipe_test.go delete mode 100644 internal/walk/testdata/walktree-test-repo.tar.gz delete mode 100644 internal/walk/walk.go delete mode 100644 internal/walk/walk_test.go diff --git a/internal/archiver/archive_reader.go b/internal/archiver/archive_reader.go deleted file mode 100644 index fa00a7406..000000000 --- a/internal/archiver/archive_reader.go +++ /dev/null @@ -1,117 +0,0 @@ -package archiver - -import ( - "context" - "io" - "time" - - "github.com/restic/restic/internal/debug" - "github.com/restic/restic/internal/restic" - - "github.com/restic/restic/internal/errors" - - "github.com/restic/chunker" -) - -// Reader allows saving a stream of data to the repository. -type Reader struct { - restic.Repository - - Tags []string - Hostname string - TimeStamp time.Time -} - -// Archive reads data from the reader and saves it to the repo. -func (r *Reader) Archive(ctx context.Context, name string, rd io.Reader, p *restic.Progress) (*restic.Snapshot, restic.ID, error) { - if name == "" { - return nil, restic.ID{}, errors.New("no filename given") - } - debug.Log("start archiving %s", name) - sn, err := restic.NewSnapshot([]string{name}, r.Tags, r.Hostname, r.TimeStamp) - if err != nil { - return nil, restic.ID{}, err - } - - p.Start() - defer p.Done() - - repo := r.Repository - chnker := chunker.New(rd, repo.Config().ChunkerPolynomial) - - ids := restic.IDs{} - var fileSize uint64 - - for { - chunk, err := chnker.Next(getBuf()) - if errors.Cause(err) == io.EOF { - break - } - - if err != nil { - return nil, restic.ID{}, errors.Wrap(err, "chunker.Next()") - } - - id := restic.Hash(chunk.Data) - - if !repo.Index().Has(id, restic.DataBlob) { - _, err := repo.SaveBlob(ctx, restic.DataBlob, chunk.Data, id) - if err != nil { - return nil, restic.ID{}, err - } - debug.Log("saved blob %v (%d bytes)\n", id, chunk.Length) - } else { - debug.Log("blob %v already saved in the repo\n", id) - } - - freeBuf(chunk.Data) - - ids = append(ids, id) - - p.Report(restic.Stat{Bytes: uint64(chunk.Length)}) - fileSize += uint64(chunk.Length) - } - - tree := &restic.Tree{ - Nodes: []*restic.Node{ - { - Name: name, - AccessTime: time.Now(), - ModTime: time.Now(), - Type: "file", - Mode: 0644, - Size: fileSize, - UID: sn.UID, - GID: sn.GID, - User: sn.Username, - Content: ids, - }, - }, - } - - treeID, err := repo.SaveTree(ctx, tree) - if err != nil { - return nil, restic.ID{}, err - } - sn.Tree = &treeID - debug.Log("tree saved as %v", treeID) - - id, err := repo.SaveJSONUnpacked(ctx, restic.SnapshotFile, sn) - if err != nil { - return nil, restic.ID{}, err - } - - debug.Log("snapshot saved as %v", id) - - err = repo.Flush(ctx) - if err != nil { - return nil, restic.ID{}, err - } - - err = repo.SaveIndex(ctx) - if err != nil { - return nil, restic.ID{}, err - } - - return sn, id, nil -} diff --git a/internal/archiver/archive_reader_test.go b/internal/archiver/archive_reader_test.go deleted file mode 100644 index d0fdb06cf..000000000 --- a/internal/archiver/archive_reader_test.go +++ /dev/null @@ -1,206 +0,0 @@ -package archiver - -import ( - "bytes" - "context" - "errors" - "io" - "math/rand" - "testing" - - "github.com/restic/restic/internal/checker" - "github.com/restic/restic/internal/repository" - "github.com/restic/restic/internal/restic" -) - -func loadBlob(t *testing.T, repo restic.Repository, id restic.ID, buf []byte) int { - n, err := repo.LoadBlob(context.TODO(), restic.DataBlob, id, buf) - if err != nil { - t.Fatalf("LoadBlob(%v) returned error %v", id, err) - } - - return n -} - -func checkSavedFile(t *testing.T, repo restic.Repository, treeID restic.ID, name string, rd io.Reader) { - tree, err := repo.LoadTree(context.TODO(), treeID) - if err != nil { - t.Fatalf("LoadTree() returned error %v", err) - } - - if len(tree.Nodes) != 1 { - t.Fatalf("wrong number of nodes for tree, want %v, got %v", 1, len(tree.Nodes)) - } - - node := tree.Nodes[0] - if node.Name != "fakefile" { - t.Fatalf("wrong filename, want %v, got %v", "fakefile", node.Name) - } - - if len(node.Content) == 0 { - t.Fatalf("node.Content has length 0") - } - - // check blobs - for i, id := range node.Content { - size, found := repo.LookupBlobSize(id, restic.DataBlob) - if !found { - t.Fatal("Failed to find blob", id.Str()) - } - - buf := restic.NewBlobBuffer(int(size)) - n := loadBlob(t, repo, id, buf) - if n != len(buf) { - t.Errorf("wrong number of bytes read, want %d, got %d", len(buf), n) - } - - buf2 := make([]byte, int(size)) - _, err := io.ReadFull(rd, buf2) - if err != nil { - t.Fatal(err) - } - - if !bytes.Equal(buf, buf2) { - t.Fatalf("blob %d (%v) is wrong", i, id.Str()) - } - } -} - -// fakeFile returns a reader which yields deterministic pseudo-random data. -func fakeFile(t testing.TB, seed, size int64) io.Reader { - return io.LimitReader(restic.NewRandReader(rand.New(rand.NewSource(seed))), size) -} - -func TestArchiveReader(t *testing.T) { - repo, cleanup := repository.TestRepository(t) - defer cleanup() - - seed := rand.Int63() - size := int64(rand.Intn(50*1024*1024) + 50*1024*1024) - t.Logf("seed is 0x%016x, size is %v", seed, size) - - f := fakeFile(t, seed, size) - - r := &Reader{ - Repository: repo, - Hostname: "localhost", - Tags: []string{"test"}, - } - - sn, id, err := r.Archive(context.TODO(), "fakefile", f, nil) - if err != nil { - t.Fatalf("ArchiveReader() returned error %v", err) - } - - if id.IsNull() { - t.Fatalf("ArchiveReader() returned null ID") - } - - t.Logf("snapshot saved as %v, tree is %v", id.Str(), sn.Tree.Str()) - - checkSavedFile(t, repo, *sn.Tree, "fakefile", fakeFile(t, seed, size)) - - checker.TestCheckRepo(t, repo) -} - -func TestArchiveReaderNull(t *testing.T) { - repo, cleanup := repository.TestRepository(t) - defer cleanup() - - r := &Reader{ - Repository: repo, - Hostname: "localhost", - Tags: []string{"test"}, - } - - sn, id, err := r.Archive(context.TODO(), "fakefile", bytes.NewReader(nil), nil) - if err != nil { - t.Fatalf("ArchiveReader() returned error %v", err) - } - - if id.IsNull() { - t.Fatalf("ArchiveReader() returned null ID") - } - - t.Logf("snapshot saved as %v, tree is %v", id.Str(), sn.Tree.Str()) - - checker.TestCheckRepo(t, repo) -} - -type errReader string - -func (e errReader) Read([]byte) (int, error) { - return 0, errors.New(string(e)) -} - -func countSnapshots(t testing.TB, repo restic.Repository) int { - snapshots := 0 - err := repo.List(context.TODO(), restic.SnapshotFile, func(id restic.ID, size int64) error { - snapshots++ - return nil - }) - if err != nil { - t.Fatal(err) - } - return snapshots -} - -func TestArchiveReaderError(t *testing.T) { - repo, cleanup := repository.TestRepository(t) - defer cleanup() - - r := &Reader{ - Repository: repo, - Hostname: "localhost", - Tags: []string{"test"}, - } - - sn, id, err := r.Archive(context.TODO(), "fakefile", errReader("error returned by reading stdin"), nil) - if err == nil { - t.Errorf("expected error not returned") - } - - if sn != nil { - t.Errorf("Snapshot should be nil, but isn't") - } - - if !id.IsNull() { - t.Errorf("id should be null, but %v returned", id.Str()) - } - - n := countSnapshots(t, repo) - if n > 0 { - t.Errorf("expected zero snapshots, but got %d", n) - } - - checker.TestCheckRepo(t, repo) -} - -func BenchmarkArchiveReader(t *testing.B) { - repo, cleanup := repository.TestRepository(t) - defer cleanup() - - const size = 50 * 1024 * 1024 - - buf := make([]byte, size) - _, err := io.ReadFull(fakeFile(t, 23, size), buf) - if err != nil { - t.Fatal(err) - } - - r := &Reader{ - Repository: repo, - Hostname: "localhost", - Tags: []string{"test"}, - } - - t.SetBytes(size) - t.ResetTimer() - - for i := 0; i < t.N; i++ { - _, _, err := r.Archive(context.TODO(), "fakefile", bytes.NewReader(buf), nil) - if err != nil { - t.Fatal(err) - } - } -} diff --git a/internal/archiver/archiver.go b/internal/archiver/archiver.go deleted file mode 100644 index 55024b53a..000000000 --- a/internal/archiver/archiver.go +++ /dev/null @@ -1,877 +0,0 @@ -package archiver - -import ( - "context" - "encoding/json" - "fmt" - "io" - "os" - "path/filepath" - "sort" - "sync" - "time" - - "github.com/restic/restic/internal/errors" - "github.com/restic/restic/internal/restic" - "github.com/restic/restic/internal/walk" - - "github.com/restic/restic/internal/debug" - "github.com/restic/restic/internal/fs" - "github.com/restic/restic/internal/pipe" - - "github.com/restic/chunker" -) - -const ( - maxConcurrentBlobs = 32 - maxConcurrency = 10 -) - -var archiverPrintWarnings = func(path string, fi os.FileInfo, err error) { - fmt.Fprintf(os.Stderr, "warning for %v: %v", path, err) -} -var archiverAllowAllFiles = func(string, os.FileInfo) bool { return true } - -// Archiver is used to backup a set of directories. -type Archiver struct { - repo restic.Repository - knownBlobs struct { - restic.IDSet - sync.Mutex - } - - blobToken chan struct{} - - Warn func(dir string, fi os.FileInfo, err error) - SelectFilter pipe.SelectFunc - Excludes []string - - WithAccessTime bool -} - -// New returns a new archiver. -func New(repo restic.Repository) *Archiver { - arch := &Archiver{ - repo: repo, - blobToken: make(chan struct{}, maxConcurrentBlobs), - knownBlobs: struct { - restic.IDSet - sync.Mutex - }{ - IDSet: restic.NewIDSet(), - }, - } - - for i := 0; i < maxConcurrentBlobs; i++ { - arch.blobToken <- struct{}{} - } - - arch.Warn = archiverPrintWarnings - arch.SelectFilter = archiverAllowAllFiles - - return arch -} - -// isKnownBlob returns true iff the blob is not yet in the list of known blobs. -// When the blob is not known, false is returned and the blob is added to the -// list. This means that the caller false is returned to is responsible to save -// the blob to the backend. -func (arch *Archiver) isKnownBlob(id restic.ID, t restic.BlobType) bool { - arch.knownBlobs.Lock() - defer arch.knownBlobs.Unlock() - - if arch.knownBlobs.Has(id) { - return true - } - - arch.knownBlobs.Insert(id) - - if arch.repo.Index().Has(id, t) { - return true - } - - return false -} - -// Save stores a blob read from rd in the repository. -func (arch *Archiver) Save(ctx context.Context, t restic.BlobType, data []byte, id restic.ID) error { - debug.Log("Save(%v, %v)\n", t, id) - - if arch.isKnownBlob(id, restic.DataBlob) { - debug.Log("blob %v is known\n", id) - return nil - } - - _, err := arch.repo.SaveBlob(ctx, t, data, id) - if err != nil { - debug.Log("Save(%v, %v): error %v\n", t, id, err) - return err - } - - debug.Log("Save(%v, %v): new blob\n", t, id) - return nil -} - -// SaveTreeJSON stores a tree in the repository. -func (arch *Archiver) SaveTreeJSON(ctx context.Context, tree *restic.Tree) (restic.ID, error) { - data, err := json.Marshal(tree) - if err != nil { - return restic.ID{}, errors.Wrap(err, "Marshal") - } - data = append(data, '\n') - - // check if tree has been saved before - id := restic.Hash(data) - if arch.isKnownBlob(id, restic.TreeBlob) { - return id, nil - } - - return arch.repo.SaveBlob(ctx, restic.TreeBlob, data, id) -} - -func (arch *Archiver) reloadFileIfChanged(node *restic.Node, file fs.File) (*restic.Node, error) { - if !arch.WithAccessTime { - node.AccessTime = node.ModTime - } - - fi, err := file.Stat() - if err != nil { - return nil, errors.Wrap(err, "restic.Stat") - } - - if fi.ModTime().Equal(node.ModTime) { - return node, nil - } - - arch.Warn(node.Path, fi, errors.New("file has changed")) - - node, err = restic.NodeFromFileInfo(node.Path, fi) - if err != nil { - debug.Log("restic.NodeFromFileInfo returned error for %v: %v", node.Path, err) - arch.Warn(node.Path, fi, err) - } - - if !arch.WithAccessTime { - node.AccessTime = node.ModTime - } - - return node, nil -} - -type saveResult struct { - id restic.ID - bytes uint64 -} - -func (arch *Archiver) saveChunk(ctx context.Context, chunk chunker.Chunk, p *restic.Progress, token struct{}, file fs.File, resultChannel chan<- saveResult) { - defer freeBuf(chunk.Data) - - id := restic.Hash(chunk.Data) - err := arch.Save(ctx, restic.DataBlob, chunk.Data, id) - // TODO handle error - if err != nil { - debug.Log("Save(%v) failed: %v", id, err) - fmt.Printf("\nerror while saving data to the repo: %+v\n", err) - panic(err) - } - - p.Report(restic.Stat{Bytes: uint64(chunk.Length)}) - arch.blobToken <- token - resultChannel <- saveResult{id: id, bytes: uint64(chunk.Length)} -} - -func waitForResults(resultChannels [](<-chan saveResult)) ([]saveResult, error) { - results := []saveResult{} - - for _, ch := range resultChannels { - results = append(results, <-ch) - } - - if len(results) != len(resultChannels) { - return nil, errors.Errorf("chunker returned %v chunks, but only %v blobs saved", len(resultChannels), len(results)) - } - - return results, nil -} - -func updateNodeContent(node *restic.Node, results []saveResult) error { - debug.Log("checking size for file %s", node.Path) - - var bytes uint64 - node.Content = make([]restic.ID, len(results)) - - for i, b := range results { - node.Content[i] = b.id - bytes += b.bytes - - debug.Log(" adding blob %s, %d bytes", b.id, b.bytes) - } - - if bytes != node.Size { - fmt.Fprintf(os.Stderr, "warning for %v: expected %d bytes, saved %d bytes\n", node.Path, node.Size, bytes) - } - - debug.Log("SaveFile(%q): %v blobs\n", node.Path, len(results)) - - return nil -} - -// SaveFile stores the content of the file on the backend as a Blob by calling -// Save for each chunk. -func (arch *Archiver) SaveFile(ctx context.Context, p *restic.Progress, node *restic.Node) (*restic.Node, error) { - file, err := fs.Open(node.Path) - if err != nil { - return node, errors.Wrap(err, "Open") - } - defer file.Close() - - debug.RunHook("archiver.SaveFile", node.Path) - - node, err = arch.reloadFileIfChanged(node, file) - if err != nil { - return node, err - } - - chnker := chunker.New(file, arch.repo.Config().ChunkerPolynomial) - resultChannels := [](<-chan saveResult){} - - for { - chunk, err := chnker.Next(getBuf()) - if errors.Cause(err) == io.EOF { - break - } - - if err != nil { - return node, errors.Wrap(err, "chunker.Next") - } - - resCh := make(chan saveResult, 1) - go arch.saveChunk(ctx, chunk, p, <-arch.blobToken, file, resCh) - resultChannels = append(resultChannels, resCh) - } - - results, err := waitForResults(resultChannels) - if err != nil { - return node, err - } - err = updateNodeContent(node, results) - - return node, err -} - -func (arch *Archiver) fileWorker(ctx context.Context, wg *sync.WaitGroup, p *restic.Progress, entCh <-chan pipe.Entry) { - defer func() { - debug.Log("done") - wg.Done() - }() - for { - select { - case e, ok := <-entCh: - if !ok { - // channel is closed - return - } - - debug.Log("got job %v", e) - - // check for errors - if e.Error() != nil { - debug.Log("job %v has errors: %v", e.Path(), e.Error()) - // TODO: integrate error reporting - fmt.Fprintf(os.Stderr, "error for %v: %v\n", e.Path(), e.Error()) - // ignore this file - e.Result() <- nil - p.Report(restic.Stat{Errors: 1}) - continue - } - - node, err := restic.NodeFromFileInfo(e.Fullpath(), e.Info()) - if err != nil { - debug.Log("restic.NodeFromFileInfo returned error for %v: %v", node.Path, err) - arch.Warn(e.Fullpath(), e.Info(), err) - } - - if !arch.WithAccessTime { - node.AccessTime = node.ModTime - } - - // try to use old node, if present - if e.Node != nil { - debug.Log(" %v use old data", e.Path()) - - oldNode := e.Node.(*restic.Node) - // check if all content is still available in the repository - contentMissing := false - for _, blob := range oldNode.Content { - if !arch.repo.Index().Has(blob, restic.DataBlob) { - debug.Log(" %v not using old data, %v is missing", e.Path(), blob) - contentMissing = true - break - } - } - - if !contentMissing { - node.Content = oldNode.Content - debug.Log(" %v content is complete", e.Path()) - } - } else { - debug.Log(" %v no old data", e.Path()) - } - - // otherwise read file normally - if node.Type == "file" && len(node.Content) == 0 { - debug.Log(" read and save %v", e.Path()) - node, err = arch.SaveFile(ctx, p, node) - if err != nil { - fmt.Fprintf(os.Stderr, "error for %v: %v\n", node.Path, err) - arch.Warn(e.Path(), nil, err) - // ignore this file - e.Result() <- nil - p.Report(restic.Stat{Errors: 1}) - continue - } - } else { - // report old data size - p.Report(restic.Stat{Bytes: node.Size}) - } - - debug.Log(" processed %v, %d blobs", e.Path(), len(node.Content)) - e.Result() <- node - p.Report(restic.Stat{Files: 1}) - case <-ctx.Done(): - // pipeline was cancelled - return - } - } -} - -func (arch *Archiver) dirWorker(ctx context.Context, wg *sync.WaitGroup, p *restic.Progress, dirCh <-chan pipe.Dir) { - debug.Log("start") - defer func() { - debug.Log("done") - wg.Done() - }() - for { - select { - case dir, ok := <-dirCh: - if !ok { - // channel is closed - return - } - debug.Log("save dir %v (%d entries), error %v\n", dir.Path(), len(dir.Entries), dir.Error()) - - // ignore dir nodes with errors - if dir.Error() != nil { - fmt.Fprintf(os.Stderr, "error walking dir %v: %v\n", dir.Path(), dir.Error()) - dir.Result() <- nil - p.Report(restic.Stat{Errors: 1}) - continue - } - - tree := restic.NewTree() - - // wait for all content - for _, ch := range dir.Entries { - debug.Log("receiving result from %v", ch) - res := <-ch - - // if we get a nil pointer here, an error has happened while - // processing this entry. Ignore it for now. - if res == nil { - debug.Log("got nil result?") - continue - } - - // else insert node - node := res.(*restic.Node) - - if node.Type == "dir" { - debug.Log("got tree node for %s: %v", node.Path, node.Subtree) - - if node.Subtree == nil { - debug.Log("subtree is nil for node %v", node.Path) - continue - } - - if node.Subtree.IsNull() { - panic("invalid null subtree restic.ID") - } - } - - // insert node into tree, resolve name collisions - name := node.Name - i := 0 - for { - i++ - err := tree.Insert(node) - if err == nil { - break - } - - newName := fmt.Sprintf("%v-%d", name, i) - fmt.Fprintf(os.Stderr, "%v: name collision for %q, renaming to %q\n", filepath.Dir(node.Path), node.Name, newName) - node.Name = newName - } - - } - - node := &restic.Node{} - - if dir.Path() != "" && dir.Info() != nil { - n, err := restic.NodeFromFileInfo(dir.Fullpath(), dir.Info()) - if err != nil { - arch.Warn(dir.Path(), dir.Info(), err) - } - node = n - - if !arch.WithAccessTime { - node.AccessTime = node.ModTime - } - } - - if err := dir.Error(); err != nil { - node.Error = err.Error() - } - - id, err := arch.SaveTreeJSON(ctx, tree) - if err != nil { - panic(err) - } - debug.Log("save tree for %s: %v", dir.Path(), id) - if id.IsNull() { - panic("invalid null subtree restic.ID return from SaveTreeJSON()") - } - - node.Subtree = &id - - debug.Log("sending result to %v", dir.Result()) - - dir.Result() <- node - if dir.Path() != "" { - p.Report(restic.Stat{Dirs: 1}) - } - case <-ctx.Done(): - // pipeline was cancelled - return - } - } -} - -type archivePipe struct { - Old <-chan walk.TreeJob - New <-chan pipe.Job -} - -func copyJobs(ctx context.Context, in <-chan pipe.Job, out chan<- pipe.Job) { - var ( - // disable sending on the outCh until we received a job - outCh chan<- pipe.Job - // enable receiving from in - inCh = in - job pipe.Job - ok bool - ) - - for { - select { - case <-ctx.Done(): - return - case job, ok = <-inCh: - if !ok { - // input channel closed, we're done - debug.Log("input channel closed, we're done") - return - } - inCh = nil - outCh = out - case outCh <- job: - outCh = nil - inCh = in - } - } -} - -type archiveJob struct { - hasOld bool - old walk.TreeJob - new pipe.Job -} - -func (a *archivePipe) compare(ctx context.Context, out chan<- pipe.Job) { - defer func() { - close(out) - debug.Log("done") - }() - - debug.Log("start") - var ( - loadOld, loadNew bool = true, true - ok bool - oldJob walk.TreeJob - newJob pipe.Job - ) - - for { - if loadOld { - oldJob, ok = <-a.Old - // if the old channel is closed, just pass through the new jobs - if !ok { - debug.Log("old channel is closed, copy from new channel") - - // handle remaining newJob - if !loadNew { - out <- archiveJob{new: newJob}.Copy() - } - - copyJobs(ctx, a.New, out) - return - } - - loadOld = false - } - - if loadNew { - newJob, ok = <-a.New - // if the new channel is closed, there are no more files in the current snapshot, return - if !ok { - debug.Log("new channel is closed, we're done") - return - } - - loadNew = false - } - - debug.Log("old job: %v", oldJob.Path) - debug.Log("new job: %v", newJob.Path()) - - // at this point we have received an old job as well as a new job, compare paths - file1 := oldJob.Path - file2 := newJob.Path() - - dir1 := filepath.Dir(file1) - dir2 := filepath.Dir(file2) - - if file1 == file2 { - debug.Log(" same filename %q", file1) - - // send job - out <- archiveJob{hasOld: true, old: oldJob, new: newJob}.Copy() - loadOld = true - loadNew = true - continue - } else if dir1 < dir2 { - debug.Log(" %q < %q, file %q added", dir1, dir2, file2) - // file is new, send new job and load new - loadNew = true - out <- archiveJob{new: newJob}.Copy() - continue - } else if dir1 == dir2 { - if file1 < file2 { - debug.Log(" %q < %q, file %q removed", file1, file2, file1) - // file has been removed, load new old - loadOld = true - continue - } else { - debug.Log(" %q > %q, file %q added", file1, file2, file2) - // file is new, send new job and load new - loadNew = true - out <- archiveJob{new: newJob}.Copy() - continue - } - } - - debug.Log(" %q > %q, file %q removed", file1, file2, file1) - // file has been removed, throw away old job and load new - loadOld = true - } -} - -func (j archiveJob) Copy() pipe.Job { - if !j.hasOld { - return j.new - } - - // handle files - if isRegularFile(j.new.Info()) { - debug.Log(" job %v is file", j.new.Path()) - - // if type has changed, return new job directly - if j.old.Node == nil { - return j.new - } - - // if file is newer, return the new job - if j.old.Node.IsNewer(j.new.Fullpath(), j.new.Info()) { - debug.Log(" job %v is newer", j.new.Path()) - return j.new - } - - debug.Log(" job %v add old data", j.new.Path()) - // otherwise annotate job with old data - e := j.new.(pipe.Entry) - e.Node = j.old.Node - return e - } - - // dirs and other types are just returned - return j.new -} - -const saveIndexTime = 30 * time.Second - -// saveIndexes regularly queries the master index for full indexes and saves them. -func (arch *Archiver) saveIndexes(saveCtx, shutdownCtx context.Context, wg *sync.WaitGroup) { - defer wg.Done() - - ticker := time.NewTicker(saveIndexTime) - defer ticker.Stop() - - for { - select { - case <-saveCtx.Done(): - return - case <-shutdownCtx.Done(): - return - case <-ticker.C: - debug.Log("saving full indexes") - err := arch.repo.SaveFullIndex(saveCtx) - if err != nil { - debug.Log("save indexes returned an error: %v", err) - fmt.Fprintf(os.Stderr, "error saving preliminary index: %v\n", err) - } - } - } -} - -// unique returns a slice that only contains unique strings. -func unique(items []string) []string { - seen := make(map[string]struct{}) - for _, item := range items { - seen[item] = struct{}{} - } - - items = items[:0] - for item := range seen { - items = append(items, item) - } - return items -} - -// baseNameSlice allows sorting paths by basename. -// -// Snapshots have contents sorted by basename, but we receive full paths. -// For the archivePipe to advance them in pairs, we traverse the given -// paths in the same order as the snapshot. -type baseNameSlice []string - -func (p baseNameSlice) Len() int { return len(p) } -func (p baseNameSlice) Less(i, j int) bool { return filepath.Base(p[i]) < filepath.Base(p[j]) } -func (p baseNameSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } - -// Snapshot creates a snapshot of the given paths. If parentrestic.ID is set, this is -// used to compare the files to the ones archived at the time this snapshot was -// taken. -func (arch *Archiver) Snapshot(ctx context.Context, p *restic.Progress, paths, tags []string, hostname string, parentID *restic.ID, time time.Time) (*restic.Snapshot, restic.ID, error) { - paths = unique(paths) - sort.Sort(baseNameSlice(paths)) - - debug.Log("start for %v", paths) - - debug.RunHook("Archiver.Snapshot", nil) - - // signal the whole pipeline to stop - var err error - - p.Start() - defer p.Done() - - // create new snapshot - sn, err := restic.NewSnapshot(paths, tags, hostname, time) - if err != nil { - return nil, restic.ID{}, err - } - sn.Excludes = arch.Excludes - - // make paths absolute - for i, path := range paths { - if p, err := filepath.Abs(path); err == nil { - paths[i] = p - } - } - - jobs := archivePipe{} - - // use parent snapshot (if some was given) - if parentID != nil { - sn.Parent = parentID - - // load parent snapshot - parent, err := restic.LoadSnapshot(ctx, arch.repo, *parentID) - if err != nil { - return nil, restic.ID{}, err - } - - // start walker on old tree - ch := make(chan walk.TreeJob) - go walk.Tree(ctx, arch.repo, *parent.Tree, ch) - jobs.Old = ch - } else { - // use closed channel - ch := make(chan walk.TreeJob) - close(ch) - jobs.Old = ch - } - - // start walker - pipeCh := make(chan pipe.Job) - resCh := make(chan pipe.Result, 1) - go func() { - pipe.Walk(ctx, paths, arch.SelectFilter, pipeCh, resCh) - debug.Log("pipe.Walk done") - }() - jobs.New = pipeCh - - ch := make(chan pipe.Job) - go jobs.compare(ctx, ch) - - var wg sync.WaitGroup - entCh := make(chan pipe.Entry) - dirCh := make(chan pipe.Dir) - - // split - wg.Add(1) - go func() { - pipe.Split(ch, dirCh, entCh) - debug.Log("split done") - close(dirCh) - close(entCh) - wg.Done() - }() - - // run workers - for i := 0; i < maxConcurrency; i++ { - wg.Add(2) - go arch.fileWorker(ctx, &wg, p, entCh) - go arch.dirWorker(ctx, &wg, p, dirCh) - } - - // run index saver - var wgIndexSaver sync.WaitGroup - shutdownCtx, indexShutdown := context.WithCancel(ctx) - wgIndexSaver.Add(1) - go arch.saveIndexes(ctx, shutdownCtx, &wgIndexSaver) - - // wait for all workers to terminate - debug.Log("wait for workers") - wg.Wait() - - // stop index saver - indexShutdown() - wgIndexSaver.Wait() - - debug.Log("workers terminated") - - // flush repository - err = arch.repo.Flush(ctx) - if err != nil { - return nil, restic.ID{}, err - } - - // receive the top-level tree - root := (<-resCh).(*restic.Node) - debug.Log("root node received: %v", root.Subtree) - sn.Tree = root.Subtree - - // load top-level tree again to see if it is empty - toptree, err := arch.repo.LoadTree(ctx, *root.Subtree) - if err != nil { - return nil, restic.ID{}, err - } - - if len(toptree.Nodes) == 0 { - return nil, restic.ID{}, errors.Fatal("no files/dirs saved, refusing to create empty snapshot") - } - - // save index - err = arch.repo.SaveIndex(ctx) - if err != nil { - debug.Log("error saving index: %v", err) - return nil, restic.ID{}, err - } - - debug.Log("saved indexes") - - // save snapshot - id, err := arch.repo.SaveJSONUnpacked(ctx, restic.SnapshotFile, sn) - if err != nil { - return nil, restic.ID{}, err - } - - debug.Log("saved snapshot %v", id) - - return sn, id, nil -} - -func isRegularFile(fi os.FileInfo) bool { - if fi == nil { - return false - } - - return fi.Mode()&(os.ModeType|os.ModeCharDevice) == 0 -} - -// Scan traverses the dirs to collect restic.Stat information while emitting progress -// information with p. -func Scan(dirs []string, filter pipe.SelectFunc, p *restic.Progress) (restic.Stat, error) { - p.Start() - defer p.Done() - - var stat restic.Stat - - for _, dir := range dirs { - debug.Log("Start for %v", dir) - err := fs.Walk(dir, func(str string, fi os.FileInfo, err error) error { - // TODO: integrate error reporting - if err != nil { - fmt.Fprintf(os.Stderr, "error for %v: %v\n", str, err) - return nil - } - if fi == nil { - fmt.Fprintf(os.Stderr, "error for %v: FileInfo is nil\n", str) - return nil - } - - if !filter(str, fi) { - debug.Log("path %v excluded", str) - if fi.IsDir() { - return filepath.SkipDir - } - return nil - } - - s := restic.Stat{} - if fi.IsDir() { - s.Dirs++ - } else { - s.Files++ - - if isRegularFile(fi) { - s.Bytes += uint64(fi.Size()) - } - } - - p.Report(s) - stat.Add(s) - - // TODO: handle error? - return nil - }) - - debug.Log("Done for %v, err: %v", dir, err) - if err != nil { - return restic.Stat{}, errors.Wrap(err, "fs.Walk") - } - } - - return stat, nil -} diff --git a/internal/archiver/archiver_int_test.go b/internal/archiver/archiver_int_test.go deleted file mode 100644 index b1273fee9..000000000 --- a/internal/archiver/archiver_int_test.go +++ /dev/null @@ -1,145 +0,0 @@ -package archiver - -import ( - "context" - "os" - "testing" - - "github.com/restic/restic/internal/pipe" - "github.com/restic/restic/internal/walk" -) - -var treeJobs = []string{ - "foo/baz/subdir", - "foo/baz", - "foo", - "quu/bar/file1", - "quu/bar/file2", - "quu/foo/file1", - "quu/foo/file2", - "quu/foo/file3", - "quu/foo", - "quu/fooz", - "quu", - "yy/a", - "yy/b", - "yy", -} - -var pipeJobs = []string{ - "foo/baz/subdir", - "foo/baz/subdir2", // subdir2 added - "foo/baz", - "foo", - "quu/bar/.file1.swp", // file with . added - "quu/bar/file1", - "quu/bar/file2", - "quu/foo/file1", // file2 removed - "quu/foo/file3", - "quu/foo", - "quu", - "quv/file1", // files added and removed - "quv/file2", - "quv", - "yy", - "zz/file1", // files removed and added at the end - "zz/file2", - "zz", -} - -var resultJobs = []struct { - path string - action string -}{ - {"foo/baz/subdir", "same, not a file"}, - {"foo/baz/subdir2", "new, no old job"}, - {"foo/baz", "same, not a file"}, - {"foo", "same, not a file"}, - {"quu/bar/.file1.swp", "new, no old job"}, - {"quu/bar/file1", "same, not a file"}, - {"quu/bar/file2", "same, not a file"}, - {"quu/foo/file1", "same, not a file"}, - {"quu/foo/file3", "same, not a file"}, - {"quu/foo", "same, not a file"}, - {"quu", "same, not a file"}, - {"quv/file1", "new, no old job"}, - {"quv/file2", "new, no old job"}, - {"quv", "new, no old job"}, - {"yy", "same, not a file"}, - {"zz/file1", "testPipeJob"}, - {"zz/file2", "testPipeJob"}, - {"zz", "testPipeJob"}, -} - -type testPipeJob struct { - path string - err error - fi os.FileInfo - res chan<- pipe.Result -} - -func (j testPipeJob) Path() string { return j.path } -func (j testPipeJob) Fullpath() string { return j.path } -func (j testPipeJob) Error() error { return j.err } -func (j testPipeJob) Info() os.FileInfo { return j.fi } -func (j testPipeJob) Result() chan<- pipe.Result { return j.res } - -func testTreeWalker(ctx context.Context, out chan<- walk.TreeJob) { - for _, e := range treeJobs { - select { - case <-ctx.Done(): - return - case out <- walk.TreeJob{Path: e}: - } - } - - close(out) -} - -func testPipeWalker(ctx context.Context, out chan<- pipe.Job) { - for _, e := range pipeJobs { - select { - case <-ctx.Done(): - return - case out <- testPipeJob{path: e}: - } - } - - close(out) -} - -func TestArchivePipe(t *testing.T) { - ctx := context.TODO() - - treeCh := make(chan walk.TreeJob) - pipeCh := make(chan pipe.Job) - - go testTreeWalker(ctx, treeCh) - go testPipeWalker(ctx, pipeCh) - - p := archivePipe{Old: treeCh, New: pipeCh} - - ch := make(chan pipe.Job) - - go p.compare(ctx, ch) - - i := 0 - for job := range ch { - if job.Path() != resultJobs[i].path { - t.Fatalf("wrong job received: wanted %v, got %v", resultJobs[i], job) - } - - // switch j := job.(type) { - // case archivePipeJob: - // if j.action != resultJobs[i].action { - // t.Fatalf("wrong action for %v detected: wanted %q, got %q", job.Path(), resultJobs[i].action, j.action) - // } - // case testPipeJob: - // if resultJobs[i].action != "testPipeJob" { - // t.Fatalf("unexpected testPipeJob, expected %q: %v", resultJobs[i].action, j) - // } - // } - - i++ - } -} diff --git a/internal/archiver/archiver_test.go b/internal/archiver/archiver_test.go deleted file mode 100644 index 1e8b9355f..000000000 --- a/internal/archiver/archiver_test.go +++ /dev/null @@ -1,258 +0,0 @@ -package archiver_test - -import ( - "bytes" - "context" - "io" - "io/ioutil" - "os" - "path/filepath" - "testing" - "time" - - "github.com/restic/restic/internal/archiver" - "github.com/restic/restic/internal/crypto" - "github.com/restic/restic/internal/fs" - "github.com/restic/restic/internal/repository" - "github.com/restic/restic/internal/restic" - rtest "github.com/restic/restic/internal/test" - - "github.com/restic/restic/internal/errors" - - "github.com/restic/chunker" -) - -var testPol = chunker.Pol(0x3DA3358B4DC173) - -type Rdr interface { - io.ReadSeeker - io.ReaderAt -} - -func benchmarkChunkEncrypt(b testing.TB, buf, buf2 []byte, rd Rdr, key *crypto.Key) { - rd.Seek(0, 0) - ch := chunker.New(rd, testPol) - nonce := crypto.NewRandomNonce() - - for { - chunk, err := ch.Next(buf) - - if errors.Cause(err) == io.EOF { - break - } - - rtest.OK(b, err) - - rtest.Assert(b, uint(len(chunk.Data)) == chunk.Length, - "invalid length: got %d, expected %d", len(chunk.Data), chunk.Length) - - _ = key.Seal(buf2[:0], nonce, chunk.Data, nil) - } -} - -func BenchmarkChunkEncrypt(b *testing.B) { - repo, cleanup := repository.TestRepository(b) - defer cleanup() - - data := rtest.Random(23, 10<<20) // 10MiB - rd := bytes.NewReader(data) - - buf := make([]byte, chunker.MaxSize) - buf2 := make([]byte, chunker.MaxSize) - - b.ResetTimer() - b.SetBytes(int64(len(data))) - - for i := 0; i < b.N; i++ { - benchmarkChunkEncrypt(b, buf, buf2, rd, repo.Key()) - } -} - -func benchmarkChunkEncryptP(b *testing.PB, buf []byte, rd Rdr, key *crypto.Key) { - ch := chunker.New(rd, testPol) - nonce := crypto.NewRandomNonce() - - for { - chunk, err := ch.Next(buf) - if errors.Cause(err) == io.EOF { - break - } - - _ = key.Seal(chunk.Data[:0], nonce, chunk.Data, nil) - } -} - -func BenchmarkChunkEncryptParallel(b *testing.B) { - repo, cleanup := repository.TestRepository(b) - defer cleanup() - - data := rtest.Random(23, 10<<20) // 10MiB - - buf := make([]byte, chunker.MaxSize) - - b.ResetTimer() - b.SetBytes(int64(len(data))) - - b.RunParallel(func(pb *testing.PB) { - for pb.Next() { - rd := bytes.NewReader(data) - benchmarkChunkEncryptP(pb, buf, rd, repo.Key()) - } - }) -} - -func archiveDirectory(b testing.TB) { - repo, cleanup := repository.TestRepository(b) - defer cleanup() - - arch := archiver.New(repo) - - _, id, err := arch.Snapshot(context.TODO(), nil, []string{rtest.BenchArchiveDirectory}, nil, "localhost", nil, time.Now()) - rtest.OK(b, err) - - b.Logf("snapshot archived as %v", id) -} - -func TestArchiveDirectory(t *testing.T) { - if rtest.BenchArchiveDirectory == "" { - t.Skip("benchdir not set, skipping TestArchiveDirectory") - } - - archiveDirectory(t) -} - -func BenchmarkArchiveDirectory(b *testing.B) { - if rtest.BenchArchiveDirectory == "" { - b.Skip("benchdir not set, skipping BenchmarkArchiveDirectory") - } - - for i := 0; i < b.N; i++ { - archiveDirectory(b) - } -} - -func countPacks(t testing.TB, repo restic.Repository, tpe restic.FileType) (n uint) { - err := repo.Backend().List(context.TODO(), tpe, func(restic.FileInfo) error { - n++ - return nil - }) - if err != nil { - t.Fatal(err) - } - - return n -} - -func archiveWithDedup(t testing.TB) { - repo, cleanup := repository.TestRepository(t) - defer cleanup() - - if rtest.BenchArchiveDirectory == "" { - t.Skip("benchdir not set, skipping TestArchiverDedup") - } - - var cnt struct { - before, after, after2 struct { - packs, dataBlobs, treeBlobs uint - } - } - - // archive a few files - sn := archiver.TestSnapshot(t, repo, rtest.BenchArchiveDirectory, nil) - t.Logf("archived snapshot %v", sn.ID().Str()) - - // get archive stats - cnt.before.packs = countPacks(t, repo, restic.DataFile) - cnt.before.dataBlobs = repo.Index().Count(restic.DataBlob) - cnt.before.treeBlobs = repo.Index().Count(restic.TreeBlob) - t.Logf("packs %v, data blobs %v, tree blobs %v", - cnt.before.packs, cnt.before.dataBlobs, cnt.before.treeBlobs) - - // archive the same files again, without parent snapshot - sn2 := archiver.TestSnapshot(t, repo, rtest.BenchArchiveDirectory, nil) - t.Logf("archived snapshot %v", sn2.ID().Str()) - - // get archive stats again - cnt.after.packs = countPacks(t, repo, restic.DataFile) - cnt.after.dataBlobs = repo.Index().Count(restic.DataBlob) - cnt.after.treeBlobs = repo.Index().Count(restic.TreeBlob) - t.Logf("packs %v, data blobs %v, tree blobs %v", - cnt.after.packs, cnt.after.dataBlobs, cnt.after.treeBlobs) - - // if there are more data blobs, something is wrong - if cnt.after.dataBlobs > cnt.before.dataBlobs { - t.Fatalf("TestArchiverDedup: too many data blobs in repository: before %d, after %d", - cnt.before.dataBlobs, cnt.after.dataBlobs) - } - - // archive the same files again, with a parent snapshot - sn3 := archiver.TestSnapshot(t, repo, rtest.BenchArchiveDirectory, sn2.ID()) - t.Logf("archived snapshot %v, parent %v", sn3.ID().Str(), sn2.ID().Str()) - - // get archive stats again - cnt.after2.packs = countPacks(t, repo, restic.DataFile) - cnt.after2.dataBlobs = repo.Index().Count(restic.DataBlob) - cnt.after2.treeBlobs = repo.Index().Count(restic.TreeBlob) - t.Logf("packs %v, data blobs %v, tree blobs %v", - cnt.after2.packs, cnt.after2.dataBlobs, cnt.after2.treeBlobs) - - // if there are more data blobs, something is wrong - if cnt.after2.dataBlobs > cnt.before.dataBlobs { - t.Fatalf("TestArchiverDedup: too many data blobs in repository: before %d, after %d", - cnt.before.dataBlobs, cnt.after2.dataBlobs) - } -} - -func TestArchiveDedup(t *testing.T) { - archiveWithDedup(t) -} - -func TestArchiveEmptySnapshot(t *testing.T) { - repo, cleanup := repository.TestRepository(t) - defer cleanup() - - arch := archiver.New(repo) - - sn, id, err := arch.Snapshot(context.TODO(), nil, []string{"file-does-not-exist-123123213123", "file2-does-not-exist-too-123123123"}, nil, "localhost", nil, time.Now()) - if err == nil { - t.Errorf("expected error for empty snapshot, got nil") - } - - if !id.IsNull() { - t.Errorf("expected null ID for empty snapshot, got %v", id.Str()) - } - - if sn != nil { - t.Errorf("expected null snapshot for empty snapshot, got %v", sn) - } -} - -func TestArchiveNameCollision(t *testing.T) { - repo, cleanup := repository.TestRepository(t) - defer cleanup() - - dir, cleanup := rtest.TempDir(t) - defer cleanup() - - root := filepath.Join(dir, "root") - rtest.OK(t, os.MkdirAll(root, 0755)) - - rtest.OK(t, ioutil.WriteFile(filepath.Join(dir, "testfile"), []byte("testfile1"), 0644)) - rtest.OK(t, ioutil.WriteFile(filepath.Join(dir, "root", "testfile"), []byte("testfile2"), 0644)) - - defer fs.TestChdir(t, root)() - - arch := archiver.New(repo) - - sn, id, err := arch.Snapshot(context.TODO(), nil, []string{"testfile", filepath.Join("..", "testfile")}, nil, "localhost", nil, time.Now()) - rtest.OK(t, err) - - t.Logf("snapshot archived as %v", id) - - tree, err := repo.LoadTree(context.TODO(), *sn.Tree) - rtest.OK(t, err) - - if len(tree.Nodes) != 2 { - t.Fatalf("tree has %d nodes, wanted 2: %v", len(tree.Nodes), tree.Nodes) - } -} diff --git a/internal/archiver/buffer_pool.go b/internal/archiver/buffer_pool.go deleted file mode 100644 index 32df5ab7b..000000000 --- a/internal/archiver/buffer_pool.go +++ /dev/null @@ -1,21 +0,0 @@ -package archiver - -import ( - "sync" - - "github.com/restic/chunker" -) - -var bufPool = sync.Pool{ - New: func() interface{} { - return make([]byte, chunker.MinSize) - }, -} - -func getBuf() []byte { - return bufPool.Get().([]byte) -} - -func freeBuf(data []byte) { - bufPool.Put(data) -} diff --git a/internal/pipe/doc.go b/internal/pipe/doc.go deleted file mode 100644 index ba5fc04ae..000000000 --- a/internal/pipe/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package pipe implements walking a directory in a deterministic order. -package pipe diff --git a/internal/pipe/pipe.go b/internal/pipe/pipe.go deleted file mode 100644 index 1cf7ff6bd..000000000 --- a/internal/pipe/pipe.go +++ /dev/null @@ -1,292 +0,0 @@ -package pipe - -import ( - "context" - "fmt" - "os" - "path/filepath" - "sort" - - "github.com/restic/restic/internal/errors" - - "github.com/restic/restic/internal/debug" - "github.com/restic/restic/internal/fs" -) - -type Result interface{} - -type Job interface { - Path() string - Fullpath() string - Error() error - Info() os.FileInfo - - Result() chan<- Result -} - -type Entry struct { - basedir string - path string - info os.FileInfo - error error - result chan<- Result - - // points to the old node if available, interface{} is used to prevent - // circular import - Node interface{} -} - -func (e Entry) Path() string { return e.path } -func (e Entry) Fullpath() string { return filepath.Join(e.basedir, e.path) } -func (e Entry) Error() error { return e.error } -func (e Entry) Info() os.FileInfo { return e.info } -func (e Entry) Result() chan<- Result { return e.result } - -type Dir struct { - basedir string - path string - error error - info os.FileInfo - - Entries [](<-chan Result) - result chan<- Result -} - -func (e Dir) Path() string { return e.path } -func (e Dir) Fullpath() string { return filepath.Join(e.basedir, e.path) } -func (e Dir) Error() error { return e.error } -func (e Dir) Info() os.FileInfo { return e.info } -func (e Dir) Result() chan<- Result { return e.result } - -// readDirNames reads the directory named by dirname and returns -// a sorted list of directory entries. -// taken from filepath/path.go -func readDirNames(dirname string) ([]string, error) { - f, err := fs.Open(dirname) - if err != nil { - return nil, errors.Wrap(err, "Open") - } - names, err := f.Readdirnames(-1) - _ = f.Close() - if err != nil { - return nil, errors.Wrap(err, "Readdirnames") - } - sort.Strings(names) - return names, nil -} - -// SelectFunc returns true for all items that should be included (files and -// dirs). If false is returned, files are ignored and dirs are not even walked. -type SelectFunc func(item string, fi os.FileInfo) bool - -func walk(ctx context.Context, basedir, dir string, selectFunc SelectFunc, jobs chan<- Job, res chan<- Result) (excluded bool) { - debug.Log("start on %q, basedir %q", dir, basedir) - - relpath, err := filepath.Rel(basedir, dir) - if err != nil { - panic(err) - } - - info, err := fs.Lstat(dir) - if err != nil { - err = errors.Wrap(err, "Lstat") - debug.Log("error for %v: %v, res %p", dir, err, res) - select { - case jobs <- Dir{basedir: basedir, path: relpath, info: info, error: err, result: res}: - case <-ctx.Done(): - } - return - } - - if !selectFunc(dir, info) { - debug.Log("file %v excluded by filter, res %p", dir, res) - excluded = true - return - } - - if !info.IsDir() { - debug.Log("sending file job for %v, res %p", dir, res) - select { - case jobs <- Entry{info: info, basedir: basedir, path: relpath, result: res}: - case <-ctx.Done(): - } - return - } - - debug.RunHook("pipe.readdirnames", dir) - names, err := readDirNames(dir) - if err != nil { - debug.Log("Readdirnames(%v) returned error: %v, res %p", dir, err, res) - select { - case <-ctx.Done(): - case jobs <- Dir{basedir: basedir, path: relpath, info: info, error: err, result: res}: - } - return - } - - // Insert breakpoint to allow testing behaviour with vanishing files - // between Readdir() and lstat() - debug.RunHook("pipe.walk1", relpath) - - entries := make([]<-chan Result, 0, len(names)) - - for _, name := range names { - subpath := filepath.Join(dir, name) - - fi, statErr := fs.Lstat(subpath) - if !selectFunc(subpath, fi) { - debug.Log("file %v excluded by filter", subpath) - continue - } - - ch := make(chan Result, 1) - entries = append(entries, ch) - - if statErr != nil { - statErr = errors.Wrap(statErr, "Lstat") - debug.Log("sending file job for %v, err %v, res %p", subpath, err, res) - select { - case jobs <- Entry{info: fi, error: statErr, basedir: basedir, path: filepath.Join(relpath, name), result: ch}: - case <-ctx.Done(): - return - } - continue - } - - // Insert breakpoint to allow testing behaviour with vanishing files - // between walk and open - debug.RunHook("pipe.walk2", filepath.Join(relpath, name)) - - walk(ctx, basedir, subpath, selectFunc, jobs, ch) - } - - debug.Log("sending dirjob for %q, basedir %q, res %p", dir, basedir, res) - select { - case jobs <- Dir{basedir: basedir, path: relpath, info: info, Entries: entries, result: res}: - case <-ctx.Done(): - } - - return -} - -// cleanupPath is used to clean a path. For a normal path, a slice with just -// the path is returned. For special cases such as "." and "/" the list of -// names within those paths is returned. -func cleanupPath(path string) ([]string, error) { - path = filepath.Clean(path) - if filepath.Dir(path) != path { - return []string{path}, nil - } - - paths, err := readDirNames(path) - if err != nil { - return nil, err - } - - for i, p := range paths { - paths[i] = filepath.Join(path, p) - } - - return paths, nil -} - -// Walk sends a Job for each file and directory it finds below the paths. When -// the channel done is closed, processing stops. -func Walk(ctx context.Context, walkPaths []string, selectFunc SelectFunc, jobs chan<- Job, res chan<- Result) { - var paths []string - - for _, p := range walkPaths { - ps, err := cleanupPath(p) - if err != nil { - fmt.Fprintf(os.Stderr, "Readdirnames(%v): %v, skipping\n", p, err) - debug.Log("Readdirnames(%v) returned error: %v, skipping", p, err) - continue - } - - paths = append(paths, ps...) - } - - debug.Log("start on %v", paths) - defer func() { - debug.Log("output channel closed") - close(jobs) - }() - - entries := make([]<-chan Result, 0, len(paths)) - for _, path := range paths { - debug.Log("start walker for %v", path) - ch := make(chan Result, 1) - excluded := walk(ctx, filepath.Dir(path), path, selectFunc, jobs, ch) - - if excluded { - debug.Log("walker for %v done, it was excluded by the filter", path) - continue - } - - entries = append(entries, ch) - debug.Log("walker for %v done", path) - } - - debug.Log("sending root node, res %p", res) - select { - case <-ctx.Done(): - return - case jobs <- Dir{Entries: entries, result: res}: - } - - debug.Log("walker done") -} - -// Split feeds all elements read from inChan to dirChan and entChan. -func Split(inChan <-chan Job, dirChan chan<- Dir, entChan chan<- Entry) { - debug.Log("start") - defer debug.Log("done") - - inCh := inChan - dirCh := dirChan - entCh := entChan - - var ( - dir Dir - ent Entry - ) - - // deactivate sending until we received at least one job - dirCh = nil - entCh = nil - for { - select { - case job, ok := <-inCh: - if !ok { - // channel is closed - return - } - - if job == nil { - panic("nil job received") - } - - // disable receiving until the current job has been sent - inCh = nil - - switch j := job.(type) { - case Dir: - dir = j - dirCh = dirChan - case Entry: - ent = j - entCh = entChan - default: - panic(fmt.Sprintf("unknown job type %v", j)) - } - case dirCh <- dir: - // disable sending, re-enable receiving - dirCh = nil - inCh = inChan - case entCh <- ent: - // disable sending, re-enable receiving - entCh = nil - inCh = inChan - } - } -} diff --git a/internal/pipe/pipe_test.go b/internal/pipe/pipe_test.go deleted file mode 100644 index 1612987e7..000000000 --- a/internal/pipe/pipe_test.go +++ /dev/null @@ -1,600 +0,0 @@ -package pipe_test - -import ( - "context" - "io/ioutil" - "os" - "path/filepath" - "runtime" - "sync" - "testing" - "time" - - "github.com/restic/restic/internal/debug" - "github.com/restic/restic/internal/pipe" - rtest "github.com/restic/restic/internal/test" -) - -type stats struct { - dirs, files int -} - -func acceptAll(string, os.FileInfo) bool { - return true -} - -func statPath(path string) (stats, error) { - var s stats - - // count files and directories with filepath.Walk() - err := filepath.Walk(rtest.TestWalkerPath, func(p string, fi os.FileInfo, err error) error { - if fi == nil { - return err - } - - if fi.IsDir() { - s.dirs++ - } else { - s.files++ - } - - return err - }) - - return s, err -} - -const maxWorkers = 100 - -func TestPipelineWalkerWithSplit(t *testing.T) { - if rtest.TestWalkerPath == "" { - t.Skipf("walkerpath not set, skipping TestPipelineWalker") - } - - var err error - if !filepath.IsAbs(rtest.TestWalkerPath) { - rtest.TestWalkerPath, err = filepath.Abs(rtest.TestWalkerPath) - rtest.OK(t, err) - } - - before, err := statPath(rtest.TestWalkerPath) - rtest.OK(t, err) - - t.Logf("walking path %s with %d dirs, %d files", rtest.TestWalkerPath, - before.dirs, before.files) - - // account for top level dir - before.dirs++ - - after := stats{} - m := sync.Mutex{} - - worker := func(wg *sync.WaitGroup, done <-chan struct{}, entCh <-chan pipe.Entry, dirCh <-chan pipe.Dir) { - defer wg.Done() - for { - select { - case e, ok := <-entCh: - if !ok { - // channel is closed - return - } - - m.Lock() - after.files++ - m.Unlock() - - e.Result() <- true - - case dir, ok := <-dirCh: - if !ok { - // channel is closed - return - } - - // wait for all content - for _, ch := range dir.Entries { - <-ch - } - - m.Lock() - after.dirs++ - m.Unlock() - - dir.Result() <- true - case <-done: - // pipeline was cancelled - return - } - } - } - - var wg sync.WaitGroup - done := make(chan struct{}) - entCh := make(chan pipe.Entry) - dirCh := make(chan pipe.Dir) - - for i := 0; i < maxWorkers; i++ { - wg.Add(1) - go worker(&wg, done, entCh, dirCh) - } - - jobs := make(chan pipe.Job, 200) - wg.Add(1) - go func() { - pipe.Split(jobs, dirCh, entCh) - close(entCh) - close(dirCh) - wg.Done() - }() - - resCh := make(chan pipe.Result, 1) - pipe.Walk(context.TODO(), []string{rtest.TestWalkerPath}, acceptAll, jobs, resCh) - - // wait for all workers to terminate - wg.Wait() - - // wait for top-level blob - <-resCh - - t.Logf("walked path %s with %d dirs, %d files", rtest.TestWalkerPath, - after.dirs, after.files) - - rtest.Assert(t, before == after, "stats do not match, expected %v, got %v", before, after) -} - -func TestPipelineWalker(t *testing.T) { - if rtest.TestWalkerPath == "" { - t.Skipf("walkerpath not set, skipping TestPipelineWalker") - } - - ctx, cancel := context.WithCancel(context.TODO()) - defer cancel() - - var err error - if !filepath.IsAbs(rtest.TestWalkerPath) { - rtest.TestWalkerPath, err = filepath.Abs(rtest.TestWalkerPath) - rtest.OK(t, err) - } - - before, err := statPath(rtest.TestWalkerPath) - rtest.OK(t, err) - - t.Logf("walking path %s with %d dirs, %d files", rtest.TestWalkerPath, - before.dirs, before.files) - - // account for top level dir - before.dirs++ - - after := stats{} - m := sync.Mutex{} - - worker := func(ctx context.Context, wg *sync.WaitGroup, jobs <-chan pipe.Job) { - defer wg.Done() - for { - select { - case job, ok := <-jobs: - if !ok { - // channel is closed - return - } - rtest.Assert(t, job != nil, "job is nil") - - switch j := job.(type) { - case pipe.Dir: - // wait for all content - for _, ch := range j.Entries { - <-ch - } - - m.Lock() - after.dirs++ - m.Unlock() - - j.Result() <- true - case pipe.Entry: - m.Lock() - after.files++ - m.Unlock() - - j.Result() <- true - } - - case <-ctx.Done(): - // pipeline was cancelled - return - } - } - } - - var wg sync.WaitGroup - jobs := make(chan pipe.Job) - - for i := 0; i < maxWorkers; i++ { - wg.Add(1) - go worker(ctx, &wg, jobs) - } - - resCh := make(chan pipe.Result, 1) - pipe.Walk(ctx, []string{rtest.TestWalkerPath}, acceptAll, jobs, resCh) - - // wait for all workers to terminate - wg.Wait() - - // wait for top-level blob - <-resCh - - t.Logf("walked path %s with %d dirs, %d files", rtest.TestWalkerPath, - after.dirs, after.files) - - rtest.Assert(t, before == after, "stats do not match, expected %v, got %v", before, after) -} - -func createFile(filename, data string) error { - f, err := os.Create(filename) - if err != nil { - return err - } - - defer f.Close() - - _, err = f.Write([]byte(data)) - if err != nil { - return err - } - - return nil -} - -func TestPipeWalkerError(t *testing.T) { - dir, err := ioutil.TempDir("", "restic-test-") - rtest.OK(t, err) - - base := filepath.Base(dir) - - var testjobs = []struct { - path []string - err bool - }{ - {[]string{base, "a", "file_a"}, false}, - {[]string{base, "a"}, false}, - {[]string{base, "b"}, true}, - {[]string{base, "c", "file_c"}, false}, - {[]string{base, "c"}, false}, - {[]string{base}, false}, - {[]string{}, false}, - } - - rtest.OK(t, os.Mkdir(filepath.Join(dir, "a"), 0755)) - rtest.OK(t, os.Mkdir(filepath.Join(dir, "b"), 0755)) - rtest.OK(t, os.Mkdir(filepath.Join(dir, "c"), 0755)) - - rtest.OK(t, createFile(filepath.Join(dir, "a", "file_a"), "file a")) - rtest.OK(t, createFile(filepath.Join(dir, "b", "file_b"), "file b")) - rtest.OK(t, createFile(filepath.Join(dir, "c", "file_c"), "file c")) - - ranHook := false - testdir := filepath.Join(dir, "b") - - // install hook that removes the dir right before readdirnames() - debug.Hook("pipe.readdirnames", func(context interface{}) { - path := context.(string) - - if path != testdir { - return - } - - t.Logf("in hook, removing test file %v", testdir) - ranHook = true - - rtest.OK(t, os.RemoveAll(testdir)) - }) - - ctx, cancel := context.WithCancel(context.TODO()) - - ch := make(chan pipe.Job) - resCh := make(chan pipe.Result, 1) - - go pipe.Walk(ctx, []string{dir}, acceptAll, ch, resCh) - - i := 0 - for job := range ch { - if i == len(testjobs) { - t.Errorf("too many jobs received") - break - } - - p := filepath.Join(testjobs[i].path...) - if p != job.Path() { - t.Errorf("job %d has wrong path: expected %q, got %q", i, p, job.Path()) - } - - if testjobs[i].err { - if job.Error() == nil { - t.Errorf("job %d expected error but got nil", i) - } - } else { - if job.Error() != nil { - t.Errorf("job %d expected no error but got %v", i, job.Error()) - } - } - - i++ - } - - if i != len(testjobs) { - t.Errorf("expected %d jobs, got %d", len(testjobs), i) - } - - cancel() - - rtest.Assert(t, ranHook, "hook did not run") - rtest.OK(t, os.RemoveAll(dir)) -} - -func BenchmarkPipelineWalker(b *testing.B) { - if rtest.TestWalkerPath == "" { - b.Skipf("walkerpath not set, skipping BenchPipelineWalker") - } - - var max time.Duration - m := sync.Mutex{} - - fileWorker := func(ctx context.Context, wg *sync.WaitGroup, ch <-chan pipe.Entry) { - defer wg.Done() - for { - select { - case e, ok := <-ch: - if !ok { - // channel is closed - return - } - - // simulate backup - //time.Sleep(10 * time.Millisecond) - - e.Result() <- true - case <-ctx.Done(): - // pipeline was cancelled - return - } - } - } - - dirWorker := func(ctx context.Context, wg *sync.WaitGroup, ch <-chan pipe.Dir) { - defer wg.Done() - for { - select { - case dir, ok := <-ch: - if !ok { - // channel is closed - return - } - - start := time.Now() - - // wait for all content - for _, ch := range dir.Entries { - <-ch - } - - d := time.Since(start) - m.Lock() - if d > max { - max = d - } - m.Unlock() - - dir.Result() <- true - case <-ctx.Done(): - // pipeline was cancelled - return - } - } - } - - ctx, cancel := context.WithCancel(context.TODO()) - defer cancel() - - for i := 0; i < b.N; i++ { - max = 0 - entCh := make(chan pipe.Entry, 200) - dirCh := make(chan pipe.Dir, 200) - - var wg sync.WaitGroup - b.Logf("starting %d workers", maxWorkers) - for i := 0; i < maxWorkers; i++ { - wg.Add(2) - go dirWorker(ctx, &wg, dirCh) - go fileWorker(ctx, &wg, entCh) - } - - jobs := make(chan pipe.Job, 200) - wg.Add(1) - go func() { - pipe.Split(jobs, dirCh, entCh) - close(entCh) - close(dirCh) - wg.Done() - }() - - resCh := make(chan pipe.Result, 1) - pipe.Walk(ctx, []string{rtest.TestWalkerPath}, acceptAll, jobs, resCh) - - // wait for all workers to terminate - wg.Wait() - - // wait for final result - <-resCh - - b.Logf("max duration for a dir: %v", max) - } -} - -func TestPipelineWalkerMultiple(t *testing.T) { - if rtest.TestWalkerPath == "" { - t.Skipf("walkerpath not set, skipping TestPipelineWalker") - } - - ctx, cancel := context.WithCancel(context.TODO()) - defer cancel() - - paths, err := filepath.Glob(filepath.Join(rtest.TestWalkerPath, "*")) - rtest.OK(t, err) - - before, err := statPath(rtest.TestWalkerPath) - rtest.OK(t, err) - - t.Logf("walking paths %v with %d dirs, %d files", paths, - before.dirs, before.files) - - after := stats{} - m := sync.Mutex{} - - worker := func(ctx context.Context, wg *sync.WaitGroup, jobs <-chan pipe.Job) { - defer wg.Done() - for { - select { - case job, ok := <-jobs: - if !ok { - // channel is closed - return - } - rtest.Assert(t, job != nil, "job is nil") - - switch j := job.(type) { - case pipe.Dir: - // wait for all content - for _, ch := range j.Entries { - <-ch - } - - m.Lock() - after.dirs++ - m.Unlock() - - j.Result() <- true - case pipe.Entry: - m.Lock() - after.files++ - m.Unlock() - - j.Result() <- true - } - - case <-ctx.Done(): - // pipeline was cancelled - return - } - } - } - - var wg sync.WaitGroup - jobs := make(chan pipe.Job) - - for i := 0; i < maxWorkers; i++ { - wg.Add(1) - go worker(ctx, &wg, jobs) - } - - resCh := make(chan pipe.Result, 1) - pipe.Walk(ctx, paths, acceptAll, jobs, resCh) - - // wait for all workers to terminate - wg.Wait() - - // wait for top-level blob - <-resCh - - t.Logf("walked %d paths with %d dirs, %d files", len(paths), after.dirs, after.files) - - rtest.Assert(t, before == after, "stats do not match, expected %v, got %v", before, after) -} - -func dirsInPath(path string) int { - if path == "/" || path == "." || path == "" { - return 0 - } - - n := 0 - for dir := path; dir != "/" && dir != "."; dir = filepath.Dir(dir) { - n++ - } - - return n -} - -func TestPipeWalkerRoot(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skipf("not running TestPipeWalkerRoot on %s", runtime.GOOS) - return - } - - cwd, err := os.Getwd() - rtest.OK(t, err) - - testPaths := []string{ - string(filepath.Separator), - ".", - cwd, - } - - for _, path := range testPaths { - testPipeWalkerRootWithPath(path, t) - } -} - -func testPipeWalkerRootWithPath(path string, t *testing.T) { - pattern := filepath.Join(path, "*") - rootPaths, err := filepath.Glob(pattern) - rtest.OK(t, err) - - for i, p := range rootPaths { - rootPaths[i], err = filepath.Rel(path, p) - rtest.OK(t, err) - } - - t.Logf("paths in %v (pattern %q) expanded to %v items", path, pattern, len(rootPaths)) - - jobCh := make(chan pipe.Job) - var jobs []pipe.Job - - worker := func(wg *sync.WaitGroup) { - defer wg.Done() - for job := range jobCh { - jobs = append(jobs, job) - } - } - - var wg sync.WaitGroup - wg.Add(1) - go worker(&wg) - - filter := func(p string, fi os.FileInfo) bool { - p, err := filepath.Rel(path, p) - rtest.OK(t, err) - return dirsInPath(p) <= 1 - } - - resCh := make(chan pipe.Result, 1) - pipe.Walk(context.TODO(), []string{path}, filter, jobCh, resCh) - - wg.Wait() - - t.Logf("received %d jobs", len(jobs)) - - for i, job := range jobs[:len(jobs)-1] { - path := job.Path() - if path == "." || path == ".." || path == string(filepath.Separator) { - t.Errorf("job %v has invalid path %q", i, path) - } - } - - lastPath := jobs[len(jobs)-1].Path() - if lastPath != "" { - t.Errorf("last job has non-empty path %q", lastPath) - } - - if len(jobs) < len(rootPaths) { - t.Errorf("want at least %v jobs, got %v for path %v\n", len(rootPaths), len(jobs), path) - } -} diff --git a/internal/walk/testdata/walktree-test-repo.tar.gz b/internal/walk/testdata/walktree-test-repo.tar.gz deleted file mode 100644 index 5f3d19c10f85667153c0cf681fdd0a9d0158f30f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 404224 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

IV5`*5kz-5LQ!@5O(fV516D*_iqTw&S2O zKDZHfvV(%H`Q>{9k-V%yzcUo7_;hilbXN2IMU`wwOREg~%-fS*0!BkSfKp*v%O5ev zb@`*GALM0?X_jl6;bP+gy(HtmpI@OA)(F)>hr-^b@&<~YwR=l~&*k@ku>pe%ad%29 z1WXAmWf!|W=%qN8S}sc(*M;N%PZqP4YZ^<4B> z*DTIihL)Nj@g9Yu^=&#X=2-8(cbKsF#jIk8E*)NDX3=RZZJ=fq18j5q5)RdN;~}FX z+R;w>wW?pbQip*Kt^K*WzYeak26AU)&wNS(?5%uR18BChzeNr9d+=iL@{A9q5bxDX zQ$%{0>lrpxzvGw)tRnPtbuh3ZUM_RyY99+A#OE0Y7$=|2T^k()WRTTDDOw{J7+ilJ0LT-|;38)?66#fqXc zAV7hCDhaVy7lCoWANvTzF0}gtpapx%s$5ona^Ft4F;BcNX0|yA-ZC(Dg)j8uu{wQ}UC@P-4vE+$ZZVO~S03U$(YjL@$j;>uL9&);F|y_4-?!DDAm%%uw88sG1qT z?fl+!lB4kIOO_){q8A_p1?sbLUY9j&4e^TXp_Zh05T!HeuKJ?GYDt@B&AOJiOA%2e zip_n}f?|ok_Tp^N>84*-fB{hGZv;ARC2iMwb(dxT>vGvnhdkVhjun~nt7UB&C2Do4 z1QpqU4&_S#m#sajtOqN2^>tU9lFFxNBP(m80S*x3(5wC&Gi=$p|32CvO5w{z_62mpezavB<(2*?#u z_xPrgGomc5|0VagB}Hd)hgjB6j$TZ&;B1tyK5&8#boq;w($m{*t*IeM8E(> zF_$f9xgBvBxrnb5J4uo&13xHSa`k;w7adF$^9c=>H6&k_4akaq&*uKmF@X%l#vVds z8fkihx(>BD!fwV3O(H?MtREJWq33rvD#L~SRSxhAF?QP0+x+lA9CM@B+xV?t58Y== zR#fYkQdu3#$j`M zQ|XB-lbx~aktYPc=XmaMCP6;eJj}|~iwOJE>RiWzgtY2cv1_b~^iz;N{a634%9)9t6A`3bF-UpMf+ znG2Q_Lntb2_!WGcpysr# zpf=UZPJ_VPu-%H)8hR(!6`GXADS!PomM1VA`AUdsr_bGe*sU2}d5p&#U@LbzZcR~7 z-duLGh6l=0lgq2GW3(*?6!gs#bA*6*6MTVC%t57Tz;^1AI~@R>`+R2x$*(HAt#>30 z(DCf1Z+DDRN6+qRx7OEmjyenGFn_XR4kWl&hKJU*XLQ@5^d$b z*aVD0E%dNBio0mUL!{e1nYLoybKM#R)BGA_l~c2{Ua&ol7BG_-8p3cVG=jL2oxnU9 zj)F8i+D8vq)m42tUILMADB)k{Qr24}<%5#2b=Kp{ua5rN;<(3=4SiSDE85&{BaB~+ z9AJPC{#LdVvR+P^AFu8|-g+uJQ3r~>j%LMw@#aR&xXV$yEDn4^$iGW&ovty@br$6r_a|G*qgge#ci){TN`!5P|tfboN9p2fCv{q z5pufg?Xu_)=r-4;BR4g5-N3*r{TdobZgAmO7D}ukQj;0@@kdE+5JX6jS2mGUAzkqO zvALGyF4EEbNCi#i%as!K2S)x%kV;1HbfKi@Rmw1p1Kck6Y9O0N#V7;-&z1OYTyS6R zN1m|iK9!|sU7utZSDK;TT%zM!n*=hQ8s^^WjE1La!{B{c>o`Sbo8{>xLRh(#Bwu|$ z=00bWoN@jM2Gxw?G-5Hvdqv+iCS`N9eabEQ-(9$+IwCT6$0Xvy zSXf+8rvB{HAyBs5qt+QI(B+h)1>$I$z}$XPvF|m*BxnU-t<0=!SC;Eiph_3|;B)Rj zIIF_Eq(*e^CnC=@+J9?M18T%pRm2t9!7D9xZ#!6CZVMaqW)@N=1GRzy zqb=tX+zO;H?NZsigo)z}XP=!7b416U3{$??v97?UN;^<$IOOTrVCo^Hz2MjLMS z))TT{gElzLz9nEiSyUjXBxl5D>pss*I{N4j)+QRKVL&I9h?jriK^^uxHHs5?dI*;+ z9gI6p^w;m!NLhiTL5iDbRPGUbJxm%LN=5SxEufXERul}Qk-Qs|Y@F!&rARUI3bhvD zKk==5r;(sG!2d*$+!Z_1pKOHpMEeBbvwpCIr;m)9v12N^6k!*wGQObyHwCqK+tumg z5xul|{{dHB$r`Fi>>P~gXM(UI{MgX7FBx(69-v80nIO+v)K2gyP*3%2*^N|#L z@z30}7lD@!5wm@ra9UT}lrzf^QzA%B27bOMd3SAxX|UnO5oWjIqU9Gkq)7=185qzbdzCi^^ny*| z{QLck5wgP{iZHz@f-saAj6h@Z`dTt2ty z#Cmb9SaTtN?oFi_*IOlciVSG~Bay2|-==Avh2ayjG4a{=wq1z9?kd1)dOI?Nbhx#C zXmNl}2QsCCU(Emi0RQvR2-B9dkwTnZ3@MWx^i;5i?2`+ zM|sx(KY@55o|etT+Cr|Wg*&ifV2uO@*^HZci0d%n$ucS)7MiNSWkwOuzTk zs#AfX(DkW(eJ1T$PC_p-5^op=q{6r@srybxQL!|#PBPABalc0V5FkUBbCKa|T7aJoo9>0Rv1%$stv_ehCl$q)A=uT35Lk?HDZ zFa+}Q3M%d@!%W#Oti)CQguZ>z)DyL?ol$L)x7XF~Y*tN!ELj>`RDwko$*No)@0Odo z14jiel3RQKpiOxnlYqYMu)~?(MJ`AL2z?l1)zepLikF#@R?Tl#Vn&Hl&d`Xx_&{<_ zxB4_uiPa!<-5w6g*I(_540v$)Q^}`j9xZp}n|I4EM4BkXF(=L)AECD>wms}`=|VP- zCUda>Ys`x)`iw<8%RIv*gypJ3@jOzRdSzguu85+^$jZJK;>ZRd9<$dC&)4%D(_yNs9Vakn1m5`= zO%)B3eQARVq=!|YZ_8Ju93ov5hX6GN)3*xReK&6sGy5TAjwa?LPnq32XbP?XmFh}o zvsvipmw+=jRVF5#2z?N`J^>pdbg6BBw7e)7rp4ZNX5FcFmH2ZO_R6cRFIS3u=i3eT z9G>KH`7b)p4<$F1Qw=Q-Y55`7?saW+>pd)W2|u**{U`HXw0M}&vN;7+t>IA z=Q6g6S&L0TV%m8!)T%U5>G;C~2U{Q=P0aC$FXGp)X4Q4ng^FaV@OM`$; zkZVv_nF3B(HKpAwx)c3$#jX~q90y!~`hDoUv2_ekcz*vsHFUSy+OuBT!&r`U1gZUK zkOAA^(D5pPr9@%W`Zb8e4#~bjBh$2E8DqZjJk)G&Bm_}{ND8^I1nQwH-zkR0KhwN9 zVXU_A4;T2U;|Mqinm@aM`}=Z3;_sm3M%C0ii!b|8}1DSQ{$a zpS{g+jvKl+`b7R!NRS7r)Sr&#PdyaHdfg z{TjL*vl@ooY$O_Ud1&{uZK5|vgaS6`(_(m44g4AQr0uB;6B(<0<2?+?%nYv>wxT%& z=_b0l9iEclhnH{%$xv;B1TXaJZP43=&JB*$my>~nX)*={q!xahqD9YA_2tk0vxV=N zDk*WLjEK6t2R zj*<)hpZ-7jC4ZM*!^H~zkJPH{_tE>F!B^H4LA{4MeJqQ3BQ^XI)w-P|-fAi=g&3+h z9)H&cnjg&RcJ??G(?&bPntWY6)xfvpd!-MC*L3)FZ~MpH*Dkm53}??-#nv?S2(KhB z9}pU^Ntj6^{NuBxQXecUnq1eo&6R^3cJcuAuMPA*GbrsWWJFRxmkc9urZ-E-%XSLr z;zl&&C{WW~C!VRL`SGeqr=lv8Bs>K<%EVMivlxg_EPGA*KF&iz(_zID7)tgKHv%HD zZ9s!#JFIj0;t&3<;tTqxTOYrls~z2u>oWfp`c7}45F)qhUGgD7#vDv0rh(ar+pnN9 zCZz}fhhCgWZEmHq2|TBj^~OII|B|wKRkP5;+Q`M#63 zw{lMhmm22h>yd5S{>oO~Qp-bs>_@T`MLCvO*hFA{CdStbv3asU*XxUJJHC}QcOqet zaIh}RPo7wAc1<`^)0wbd2@`Erb;xRVM$1-B%miJRj7p5v^`LRLULv>t7F|3@M;Dwl zKX_uz)`;zS!tt#<9}&W=@J>G`o<0W$6i6nSzqP&}NL{T5yLLWXA-|ZvnR*)T|M9fg zLn<78gNl_l<^Vroxq%UNeXtZKc&amcB?)1=5;`$Pk=}t@zV==xMCD4R=`tK=6F{tn zvW5?@qrZCkRc`an;bL=scO%WyTe<9%p30mShaO<%h$zIw1g^QSrMFJXUiv6 zQ{Utxr^-VGWiuy-D376_gcgoFZ2ckW{}*mru?OwBhm@ODTq?`)B!}qbp*qz%YiQIA z-g^|s^|HsWkOMN?DAlgOh`oueDyv?MR|N;)S8pc$LtSwRkmtYHVUp*@a7mlCWr8cY0S z4OZygC;cN4`WklMxvnQ8ZT$w9&1lT7Ag=OoPt1qZMwo7hP>G10Rgx5i{O~@@^E@WAkbR&wPcf8rvlHcaGw>&&=SAsUr6}rrU!&fr(vY+W8h*YQoTe zI)S6?5X33$FJiuS7Ke>~Caz|ZO`J$)y04r5u@BM2n3=$ zP}fDt`)e>PlT5SmRaEgtXgJPMa5!X#?9=aR0*ge%UA43&)i{5FOgS9=Tm%b%>zO*JT7sY==k>eX9S;g zp|9h=Ixm(ut@Ld(A6T`ywtDx^YE5J>GT;ER$bu&?uhoP}6<|pYo>Lup$&e!rJh`6m zV*D4Xs+Ubrp^qD7n(0xT&+fKERqKeGy4NJ7Bwx1<3`caonz5j^tadyw19%^ljezM=%*7W$sU1H?1}4gFgb|_6!s81%g$;?3AHf2AYd{+HacQ zTqq|bn)akLITBe}J`J=6uw_A=j84PoOk0lM=&L}w+1p0U{jg+ZlGpb7;GN4OQ8+Kc zuBgq(u*4JiQ!M6ClQ4^RxV39y^qrk*_#f@q1lbk?OTKM+v*X0+->6xHl|dt`4qs;vfZ=Xlo$`htCRbi({{3FwV8&zF_AY ziO@rwS_LeS$-y$q%u~ugJ@(5MYQEucq&lRsX59t*0T z`f9y!&Qd1OUY7xNs+@{TJA)D=XP`DBoN+KVRQGOTGLNhcr7fNElqf)a!9GN%$d->F z@eI(MD5}2(+2b>q9jPEF;{ma}^?^%uGRr<2peCk(3gm8@CeEbK*6rzoBBCF_H6H2tK)ayM!`KiW3vRSF6-%7f`?XL5jwg zo@g&GBRC84RIaao{O~YI=I`C4uWvB!k&8kjcl#q|4=n<|5@iTh;}-?Lv6*0&T=Y)- zDa$9?PcX6q5RFhE?1r~s0FgXfa<>jruO&s6>?HYX>w{%c0 zzbe07Lvs(0tl0u-Zs=jNg5z!H1U)1Tj^%_3>*)gQ4u;1g%x%oKCxQLD*!DN|Xa66x zJ^mXp4#*QSoI3`^1T}sMT#zKB+`$R~4>8Gz^j!EP z;}NfPL(3k_D+kr#8TF9QkU>`JUG;Q&^NK-KXzWV_Eh@}SirX(djIU(QDeYn2+D<2N zTFsQw*3SVF+o#W~(I=qA^a=R6GA{CvyIyF4->MYP1TZ$u^hcE}LgQpJ(qLym$pcfA zf?4ar%peRD_f2L`&lHT*y;jbg=e{Y8)YiHZ*-a+QVp6i-@q~k!6D3UXl%UPx$rNGu z_-#C041p+mjPzLQO}?AqUIiJp7T=)Z-RB7a8Y5rO_e-J{F%oJ9>)jT`6hcj|>RZ=! z7^w~aftyS89;`d!`X>jVvv*sg(!DpL{W0HQc%u_i{Ll~LvTez)I+z>4l-x`Sebscj z880Il6)~)rLUSsWz3lL|63xfwlUV*13D9eSVgifAjM4U|ih>Kuod#X)t1lxvT>fK#=7 z9!FK*bRbUY`VH#Qpvz~T()KSzd;5k@dkHL?tdIxU#cFzga2b(?Z>vs057=yM-PjOU z##7i^Ra=o(;q&;6bNNk!-5r!#!b9oQymQ*nK@@kHJV^NVO?{buM}>t3i(V4+oA`10 zO+Bui77%x0_FL&pUK;Fp0A2H^%Ru}!9swP+50kAa6qfuac-<&E=O}%Ppyisa{*c1D$iFS{cx6gURa7Zjriybi4#>;wH>rsusRT>QuKD3EiKqGXPZkm}=sE`pUxa$z9 zNhr2!KP1WHoc=!Kbo7o#kq7PBPHB|9fukCQVR=a zhqOfdNvH5Okn9#qa<&4Meg1Jlj?rs|Sv#|1>zca(x1iCDaC(@@Khw(y&YXx!#IyXi zYANXPZcLGduJfRKKe8&nn;LZPvsY@kE%Gty=QswYLa^2t^Sm)L{Fo~9BNt&F7 zBL`ILEzYa`t2SpB4H^JY44df1Kl%Tn=KdiXDk@AynMlz_3lGVvr36?_PA!2=->1rUZ5||K-Q8#lmJb&NbBq zF_7ACSNKBkP4dRp$obJ73}!xLau#E4no~6R77V-W=;9{FIsbs1QXW=zYb$re_ZK~I zCap?l&0qI*I4tZw?7w1L3Xf%Dymr@2UKjjEs)I?y;&ve0!`XXinzEZjaOuX&Av7C$cW-+$?Zugh`(#(VK zr^{HRnAK4c*DV#cOEgNM5*heR@z4<3im1`LH`(vYUf0Bkju50k0PHA~1LO;-#rjML zV}8fM(UhgFa)5saG(r}tgr7!>#jvLGSd=dWdbcp|Kg2GY2W*k#LE2+mVmEKverh9X zGdqe3W7lN{R848S6{9WQnGXT>JiNDNfe4lmcRtB$hg&6s$sqRjXx!2o)cyZ8e;E7OSKd_hid{2fbVCEXtSz13cv$7P+qa zq+*!^VlBajNAndwkS}e~9jqLWsIW6|Q>fgvr*p~So@-G=dg3_Vvv|x2=2x{v!mN=6 z7)cuq>7=L2CY&}L%5cYkdJK$xZW@;Rkl2+%xCS%`iBImONIxlIdd%SVV%==SNDTIe z?#Cep-4Nrhzn|^nbas_u+@^dB%zAd(pYb!7HEbGPuOR_61o6%XEHGf;+X4|G2kqL>H0UOs%9Huea3QGTYl{us1)G>db}$j`=fzTKwd zwKot~%+r4WNanLgXk(s)BzUjR{ZP1}_h$|ZeAiDik-DeebeB%a=#2{BHNPBu9XNs2 z-3+NYlX~kKjBEQ*px(xX4Q9{Qd&JU~;&mkqD48&Zu|?)Ji1(Enn409%QlrO3D}D@7 zF^8&umJ93h-Z}uWjL@zAwIW0P`w5f+{Q&(AJ1SYZay5^XQx7?|dLd^7QAB+`ew-M6 zU|SRM;63B^G=m~wa?pda+RWyM;YF5n^QBP)x9g?6+xf-MQ7Gs!T{9*nHQZQ^lYJ)J zJPgy|!MbXRDZ@GBd0&kH-fOo_mn(7^a8qqy7QRh^{j5hK$9;}1hA3`r;7K}qJ8Cj( zo=>BFm{pX;rM-uf2Qx|Wy+fAGZCmW?c##%x{=Km6LO0I9h6(Do@7a7gMa=5v%_XYt z@R4;{Q3(Rmt7Dsjw_SYPi6J_0VL5i@4ZO2gI&6s$dd_a)QM-!$P5{P z2<&E+65qn}w}{hOU~BY?_B*y(jA#_7aABxuTc0eJ8l&feZ#3%m|7;n72Q)o2*U!Q~~9cfvd^UW|(D>4>G#JrFZ<= zb$Il^AAc18M(P2;fR6>QM4-*e?nUVlx@d~!NE_0-v(L%SXisbZCq{`3Z}N9oad9s+ zvQq&|Y6hY9-Z62NM{?@bf)!iV#Yp1O%KiN&#FJ`f+>iqc?Lrwl!tqo~uA@*>1z%)0 zwnO_NQW|sM=SztAdd?9yBd^m+`Y3h-qZ4+iXIHxkE(YT%6&foYDdx{klpa#CvZTE; zTOS;f(h~ZE7XaKcSp%DLmXj5OM;hHm6k}PdljoeevfVH1^{JzxWKtd@xzj5}XX{$~ z@HcGz313eDxZlSv^at}BmNv;NOp>>ESp83?jWbT|uXH!aE@^xXDW+$PlVq`>q)OwMmfS|^> zrLcHx4*Q@E zlQhv0lc*M}&*y!8EKK83?Pnq|9I}gG&WkBTQ^PAQxKB|~LCu6$e-}qjJecvTHvv{< z;e#Bl@^W~J-w}&VmI0PU`6qzq?7H~RbPZl~+7ewgGQU}}!)xth6hcrnPp_Tm?$||MG5s`6 zA56i|z>XiDTDinw5_5uj8V$YxP4ZI%ysoj|D_AFGg>G2~4N|N;KuyBGUQ!TnBR51N ze~V8muaA*l1}h@@;j3yum!VvhC-Av)Kn}<}=>HA@s>&ND-N5q?N`nrJvwGxatAQ|2 zN2SmvZol${HI}>1LVeHn$J|EILwPfxGMNa_WW*1o`Yxym6AEa^?Q-DI7vLZQUWxgQ zN|eq=d>RGVRN9FmvHION^a=NEVPy?Fbxqy=gk5v@5^zneHLPG!U!y~9e8;j1xBQLj zIis4P(@{7Ci1cEi2*mg>oAZs&>v@8Ry6-(+Ra;EdN5o`D*x)&U?R4yIL7S{Lyuf*Ed5o}suzYZp#S|EuZ>gC|rRjTS21 zM-p|-bon1EoknfPJKfqj#36*7E}-A zvum!W{d5|k0m4WBF>+LLhm}S*RjL09ht39O_}^$}6v1- z84*|_S-Sg&6KotKpQ za??Xd3|wy)882)&m3s#~fG`c{5mj085>iLxuzrYykOAMA!+?nXZVH& zz5~7~G@6XsG`mHCK16K2X}mohPuGj5F0~Iw1fD!Zdckcmj6T(NYuAZEO41J7a37;s z9l(zvsn8i(DnI`ZujL?~rb^<;tj-9D z7V;l%m8Zrm;9H*DAxBCGpcwDyHXmxG1k~QTOemR)CeqNA3Nh3aH)E~`XBf61E8qxZ zMX-)qWh>#D_w7t4U!ki?gC1bC1k23?+YQgD2$ijXaTNk5x?Lks&pbE2>YLM8vk#5G zK9(dkNAr8VE!5>hyz-ElhORwA_aZOKFqW$Hkxx|1d9*Hg*Ie{| z56E#CpkZb~cj^|^>0=9y`HX-Dj!_USdaXZpWjOuvZ~_Cu)cH|#VpEFrm_XRZi` zVL8N`sGy_uKtk#cc~H&=}IK zm5G(t0`YuI&bdmxgh?Z~hO()gOimj{Z*VZeOKz)$x8Q-Eij2!vjEod2_N zS5UU-sB$b>&Aa-ESXmQI{QURira>E8v_Rtsg;FOBD@F%q(_iAF5`zTY>PB*}PwwLj z&-CnQEdp*ONBju*NRcCIBvU}MKcP`6u&AFKtEZ3(RR|m8VRPm1h!Uj z)$W&Q{~)t_RCGmcxAMuAewa*C;ecDlSRmyojp(v^aOf2M zEz7x)ClYE!Fc}IW!l&u@5W~yfsXRz*TK~|CPVKhZRrh~HNagw{2!DX6g+xS%w&oOp z5FnU3>4;H@&$4f{KB_`1_fR!BI&n`%UdJ)Y6!exzTS+B5!SuL_H9D^z$rdL;kFPar z0U2JTXj8)i9iALS$n<=}_vfeJFPovZ91_$!93G zl7!izA!?)u6GfvCCOcia(_eEF{g%E!ikbm*AYST$PiA*?(l3SaIgrhW)0!pL37tAy z52=Tx%8x*()LQYL`YF_2KwqhaBqAH6xxO8Y!|QSpTt|Wk3|GJihc?f>5j0xi8l`4! zAHt;&={+uWJlN^_9A{6}fbi%7KuYFvAfa{%k{qw&LNv;AgJ1-WY=TzxguywJLU3G& zH1!MJ0Lvg^6f^dgv?wTz-9*>{NGup$c>+vcc4~-lo2WP3mkICZ(CkJtMwJ~KKvh0F zCjuBNFtb>na(Lf#3uGV8tT5`Z>di$qP9oDF&_NkTE7YW11*m351|TVQOYfF|E@ z9n;aaV17-2si@1rY|5QIc71GuS(~uRa5ja~gmpOd8POF+=4JsSip5-yK>Ihs&+tBNnPDb(qEt z{Lyb;umOoh_hs*h3au#$#6z4qwc93}Jy3WF{4YG|w!M|hnlrD#4^ZR=o&)w~j|hLT z^zM{k&)!KaaWD;AHcDt{g7j;&(tEj@xwUsL^eL{Xl&^OUqM1wft2q1hk{a}^Do$Gv zGAaL|z+Mvaj?fd`4~i)Eev^HNuORLcU_J<3Dqs%up6^?e9#4`hnhFr^tj;QY;cnsB zLERQHx)nA)BDW>C#-Jh;51%75E$|WJ?xyIr9FMx&r^)Wqz=#=4)!ahsUWWV!;+?0* zkCJ?81@qqO?LtWBH_}E838XW>?|wmTq7LLrd)f?O^n?WbypY`u1 zZQ2QG4E-~OTkp%@*dO6dYGvn7$V=L~eiGjti3XSsFXR8b9}Az(c#2n@RIs{Rvb`(j zj2VLy=UL|=A4ib475*5rAJP75hng6Ui<&g)+tVX*m7zco%BF^s8FpaTo&kd9`d z$|(GR>9jQp{l9v6WB7~mGku!k} zpm)oCAV!BX*LmmdP7Q(KZJjO}=gyT&3)49Y$Dm~}vjJPOQOfQ09AE~9Z~Y)ZqhQHc}~V!{nCDv0@;fRh0e>mopEm2UEUp_w)B3@U0bVwFeH<| zNi9HOrfqxGHlfEi`QNZ?)-yn1**6v(U+>+QyU0pEo|}%`31XSL#)OEc<(#MEQW%Fp z!(0G?mSFC_DT7I-v>+aG<06gn6Tf^XqxCl-F-?`yH?BMF@F$?=U49$fA~2dMs?Uf0bZKr?Q5E#f~(Z5xh1Y(b8>ebO=DC~1(=6hA}e^Ag3`#atOFlXn}y zUaHf(IfQ^NU(n^dw{qy*SGif6m$ap39P!Q1ceW2-Zm_%vL;Q0(Ed&ng#K!Xlrj>z! zh4=u@M_KZAK|F2QjKq?87PQhHpl&a*i0@%aG99n5dl__le_V4z;A4=3b^b0CkFoBy zFu>#x6BM`Q_W(mcyuY~K4o48h)>-22#{ys-BuUOcGf6WC8vmVc_lysO&h?MhzO6Uj?HO=>`UL~c( z&A?Y%_-Xo+R;Rr$^!u1%@lvkAE-5)3OqM=%G}~WkCIZ?zdQp4Au_!2EP-2dZ!@)#l z=$8y*3t#ATt59csDL+b(T1_@LK_xw^PGmxp%3zu83G~kdj(wKVK_bg z;Uo&)BEc*b%Ytd+daNN_;<38}`OiZKay0<##s9cnJkihLWFqWXy!R3x*Axx?)PT_6f4M6NE{Vp8X1nQr`o-|Q z*DXoNHxC<;H+E?VZR{zMuDt97bP(F8z$sTj-{`RWtCh>11kpv+q}s9Gmh?~-##bzA zLMhnknaW|Gmk!&Cx4684rgF4SUOD^6JlnRy_3}pFayIAMqO9XMfS>+pcr@P7XaIWB z1&$E*%(HrZA0ruutizEY7kt>2_UaxtThjgv^yvob1p4~;6CplUpBRo)K?R(y_WD7u;=F0Q+5hTo_Drk!fTKyvv;oHWV$aq93~JebI7f!`r>9-_{{ zfhjii`Oq=!ie)ZGp~^OCBZw1%B7Rs>(!IlHKfXfCdt~Z>-#*{M4|WO^vSEy21Uqt! z(TdvcUjhLTuf7!t6cc_LC78AmRfutIyF;pJOrsPrf4={6dWqVr*DhyGdwvEmyim7k zQqzIzQ@%!T|I0=poD-FGE#7!bFh|lTEQCWxE}yx5{f{fH1hF5q`l6&R5)eH6Wf9p) zgzh#uhd&URuxMiG(H){9$}!`1i3W>9^=sfmt!kko6*5FYdl=%xiX56 zL$>@s5#=MTu2Gd(FX<^Xc=j`+tFnIH_glf!V;t*F%iYEMSE@p*SY@4h#Mrz!5vN#a zN+?g^H@`Aixqd+2o>BBPTj;{>3c9Ac3c=NPyWz z)A9swk79C1hCT%GG{yrrTrv8zq%i*7(+rS+Y~3Nqb(m?}oK6FVE_&SQ1MK1@p8=Q} z(0N+x$Fy8%`j$VFv0ZU|YoXeGiGDl69n*R#kwzLfQ>4D4dmNxOB~F`p+bYyNGPzq` zi9ruw!73V_Qh?fMOP!HNzZ{&s!--90XMj$JK!~^WDFjx?NWic$b4&PojQHC%?~2Z- zf^;c~Mm;w-Zej#|TSovIIWe?0O^*yOe=fWsA!^DqWt2Hog?9hbPi9DSEGGuD*Zl@#!Ij$5{n zKp64lyfcyp>J^$1~)q{vs-mmt7tXp%aF_iM@KR= z+MoKY0Ju{j&_VQ#w0IhV2g=iebyYpolW`b6#&a%H_BW1`NVqancjriwp2QvLnjqC| zu9rXWh+HIEWTh)>?kcv67p1#TU!@l$CiQe{VJP|GXXQ;7FOC#ds!t{UP1t$})*emP zWbu}c*%|e8IwTRcM~R;O-6fFdJUjM4_pfMp|DZewFPZWzR%WSMA#+P!J>4BpGtDOY z0P9TO&J0|y;PeS_df_OBi1me89lQvVpq@#swbz5#`SQ^P6?@qOv*?;>twRi-0msa@DLp&{FJP3B!cJ%q zTTn@3U1S}BgF7F5Z#!-S-E@wJJ7_*Qk@Q>Gj3$c+DU7aM-7F%VbH}PulpF7PJFf>6 z{leWS&`-*T6vkiyH(;Zx{_pt?xy+zJ0ZBMCK4)Ta=dQth`PYM5&(`shD*6tj*8seb z-}<=w);#>c;jgR-#eNf+?4X43R-up3&JAvYQWrropP{7)IZWi@9-W8jD3Dteeuqtw zS}-)|ZWbKdo+1$#h=C@k-9I$Yn*ctn4|yKIXZ|FB*tZfjg*8fy0eZie_=eP9Cah=>;p8+{y6EV(RM=X|^N|^(LJT|H*)=Lh5 zJQl@6mm(T=Imt4AAd$ZRwB5j>~y+nFgygwG+fNQkOjO!1PI`5rmr`U?jWy3t#QOoj+{=PUjdfK_5&zZhjhpo=V0Qb%0N3B(VbzMjcr; zhwMG2Md*HmD9-sSyqG0)-2Td1<##{g97xH1rxqB;5Gs_jQqR$h^`c$3FPwrdjdz&% zz;@SAQC+5J#H&A(tn$)!7-qMPM^TQ z`uTuxIl0L6aJ2MGbDzc16D=AhF1yJhf;DLfA)qT8w08hCXr5*&v*GC^86L6@X5DmP zn>G8LFil@tCgS(NYP>4eYdsSq=3AmKjLNng#B^!=gaSj;~(#4X`$&kyq zNW>OqJ(uH~?0OfJ5~-v&#wQ@RfK?WZ!Iyzj%2T`K=y-rwWvPtF+ zQ|iYHWD?Ksp6>mHo>qwiyPDTIkX2f#$|^49+BB8rFe-qCX|i6RJ6&jQF!-YN&`V}& zS4^Zf{5Pe!MJS_pgTl};gJDp?BqRJ7g63a5>nIBOr_KVZqCQkZjgwy}1(zJbd{bxV zbhBegQb;r$$dn!b!MS<05oV5nl!D~M`1}*E3Z2R0k+w9+ho664!cil=%z2F0nTSxK zvEc4z8x|K4RBc#IJ^SDRstGuy`nM8?tR~h6gNfA!M;H@FKU;Q4xKpy@v;^@I2{5{P zOmcbQmW_lsCa;UHIKTbuIsU6{02!bqmhk-gf0e{6#lpF+Fqo-r$sl){)`UsQ>ucyi zgx)lQ!!u{+y80xaQUvE}4h^xte~S=cjcu6Dm}DdYP2ea1N!hK$c50_O$Kb>asFXgu@B3jecaDi|>Vx}A7%J!t ztzH%a&n7hb>)ipqP)ZngYO6-)0XRJsQoZ?$htGzD$b2)C8m<{@*g7l!^FcFH5Y^Z2 zl^3(!-@U5;dyUJ(lrz=tlcOJyW)M@;bwUhyOT4F$Xkk#NYBH1mK6)p5JePvuXH-ct zIZ525Umek!Jj;Y78Qus0!`G!r2Fbg57hV}-JMRR{Hp3MSr$&P(DNZW(2VxaA0-$|t zew0KsFFz^+l~9*Yit@QvxGb4Wrr1Q$=B53>8Fi#zIr>S&TT&)WG#HcKVy}FgJPU#&QsswN^Y8m2Ak8wDgpBuvAx`@d7jG5W7p@XvkB) zD|hM?(N|h3S6I6AJfNa>Q$_es8b|VS?BOAOBTk)_II`R6C;(yhNV6&fpZtEVlGh*Wp<C?A`JJDi!v-Rx#oNkcvpJRe`9Y%?m^J{%jH~ z5V47)m1=mV$32v5ok?JcCUtcZamf5+m-LMZB53r@V9`@eg+Z1jTbilIlwjiD_(fj` z2BNk|DTX+Y^*O-_PNR8oae~RIX|Ulfy#jX$8o0Ngxf5ntfzRoXIn7NxN9UABt9e1? zM)$oRIYH-RQ8vcw^I{4UHRM9$OqH-o0A4R>v2gzUPxa+P+l_7&W4np+ycvU8ri=rtC7Kzb$9aw`mW0PlE^hI9*h#ecNWmoR+m8xEPU& z3|-?$I;zvv^j&GQnUIZ;hcEQUN~|NuWU71`|47Kbp0O1f&V|~b>7V4K3NJ-29CH1YSj0=xVIiHzf#4}Hd&5$2^Rnhfb@UK zo(sP3Pf=bErL{gSMjRYV2J~TTjH2$Y{3e;HqwvYe@0q#DR-BYV;eFt)y7r6P!4_ar zC?vu*Vd|5Jaef-^ZyZfRm_Q7&U_L<}GyOJw*D77dZ=qs!;pXTU1We?CY3Y<-1MLm7 zf(sxehk+K|v;Z|n0UH<0*;v!xs9uV|mFo_$r1eA5KDt?7j7S1KSs zw5Wxqv_9;hp{bR}BWzF&VHNGU`D zcC&>=C*b0_tr+A_=Q&)u`r4oYm9VUS6kTrh34Y-UL=BlnY5!mzU2RL?dM)P~4lV_A zKY{n;sIj!$jckyH-drRZkj0iDCp|zW0W*#=FAUaSGNBD%Nj_eKm<)2Gz|9Po9|Ei{gWG_ zk3yuL+-G{m#IUB-Rz$xJRiZ$6x$3-WfQ zze);h-7EV>y&=Y~^X4OIKN*S?;Qt-jyT6pX)iXg?7?Pc+V>zZ9E6i28)tl$Ptn{nA zVb{A)&o7wrqIrETZR{bu^nq&xhSKg}b2E&eHU?aI&oQC=GJIcY4J&bz8hn^%x%%0AOMd(LGK{EIzQYqZv6Zp@wgc`r+|=!KQZ;_ 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 C^lFp< diff --git a/internal/walk/walk.go b/internal/walk/walk.go deleted file mode 100644 index 41618830a..000000000 --- a/internal/walk/walk.go +++ /dev/null @@ -1,197 +0,0 @@ -package walk - -import ( - "context" - "fmt" - "os" - "path/filepath" - "sync" - - "github.com/restic/restic/internal/debug" - "github.com/restic/restic/internal/restic" -) - -// TreeJob is a job sent from the tree walker. -type TreeJob struct { - Path string - Error error - - Node *restic.Node - Tree *restic.Tree -} - -// TreeWalker traverses a tree in the repository depth-first and sends a job -// for each item (file or dir) that it encounters. -type TreeWalker struct { - ch chan<- loadTreeJob - out chan<- TreeJob -} - -// NewTreeWalker uses ch to load trees from the repository and sends jobs to -// out. -func NewTreeWalker(ch chan<- loadTreeJob, out chan<- TreeJob) *TreeWalker { - return &TreeWalker{ch: ch, out: out} -} - -// Walk starts walking the tree given by id. When the channel done is closed, -// processing stops. -func (tw *TreeWalker) Walk(ctx context.Context, path string, id restic.ID) { - debug.Log("starting on tree %v for %v", id, path) - defer debug.Log("done walking tree %v for %v", id, path) - - resCh := make(chan loadTreeResult, 1) - tw.ch <- loadTreeJob{ - id: id, - res: resCh, - } - - res := <-resCh - if res.err != nil { - select { - case tw.out <- TreeJob{Path: path, Error: res.err}: - case <-ctx.Done(): - return - } - return - } - - tw.walk(ctx, path, res.tree) - - select { - case tw.out <- TreeJob{Path: path, Tree: res.tree}: - case <-ctx.Done(): - return - } -} - -func (tw *TreeWalker) walk(ctx context.Context, path string, tree *restic.Tree) { - debug.Log("start on %q", path) - defer debug.Log("done for %q", path) - - debug.Log("tree %#v", tree) - - // load all subtrees in parallel - results := make([]<-chan loadTreeResult, len(tree.Nodes)) - for i, node := range tree.Nodes { - if node.Type == "dir" { - resCh := make(chan loadTreeResult, 1) - tw.ch <- loadTreeJob{ - id: *node.Subtree, - res: resCh, - } - - results[i] = resCh - } - } - - for i, node := range tree.Nodes { - p := filepath.Join(path, node.Name) - var job TreeJob - - if node.Type == "dir" { - if results[i] == nil { - panic("result chan should not be nil") - } - - res := <-results[i] - if res.err == nil { - tw.walk(ctx, p, res.tree) - } else { - fmt.Fprintf(os.Stderr, "error loading tree: %v\n", res.err) - } - - job = TreeJob{Path: p, Tree: res.tree, Error: res.err} - } else { - job = TreeJob{Path: p, Node: node} - } - - select { - case tw.out <- job: - case <-ctx.Done(): - return - } - } -} - -type loadTreeResult struct { - tree *restic.Tree - err error -} - -type loadTreeJob struct { - id restic.ID - res chan<- loadTreeResult -} - -type treeLoader func(restic.ID) (*restic.Tree, error) - -func loadTreeWorker(ctx context.Context, wg *sync.WaitGroup, in <-chan loadTreeJob, load treeLoader) { - debug.Log("start") - defer debug.Log("exit") - defer wg.Done() - - for { - select { - case <-ctx.Done(): - debug.Log("done channel closed") - return - case job, ok := <-in: - if !ok { - debug.Log("input channel closed, exiting") - return - } - - debug.Log("received job to load tree %v", job.id) - tree, err := load(job.id) - - debug.Log("tree %v loaded, error %v", job.id, err) - - select { - case job.res <- loadTreeResult{tree, err}: - debug.Log("job result sent") - case <-ctx.Done(): - debug.Log("done channel closed before result could be sent") - return - } - } - } -} - -// TreeLoader loads tree objects. -type TreeLoader interface { - LoadTree(context.Context, restic.ID) (*restic.Tree, error) -} - -const loadTreeWorkers = 10 - -// Tree walks the tree specified by id recursively and sends a job for each -// file and directory it finds. When the channel done is closed, processing -// stops. -func Tree(ctx context.Context, repo TreeLoader, id restic.ID, jobCh chan<- TreeJob) { - debug.Log("start on %v, start workers", id) - - load := func(id restic.ID) (*restic.Tree, error) { - tree, err := repo.LoadTree(ctx, id) - if err != nil { - return nil, err - } - return tree, nil - } - - ch := make(chan loadTreeJob) - - var wg sync.WaitGroup - for i := 0; i < loadTreeWorkers; i++ { - wg.Add(1) - go loadTreeWorker(ctx, &wg, ch, load) - } - - tw := NewTreeWalker(ch, jobCh) - tw.Walk(ctx, "", id) - close(jobCh) - - close(ch) - wg.Wait() - - debug.Log("done") -} diff --git a/internal/walk/walk_test.go b/internal/walk/walk_test.go deleted file mode 100644 index b67ae9151..000000000 --- a/internal/walk/walk_test.go +++ /dev/null @@ -1,1394 +0,0 @@ -package walk_test - -import ( - "context" - "os" - "path/filepath" - "strings" - "testing" - "time" - - "github.com/restic/restic/internal/archiver" - "github.com/restic/restic/internal/pipe" - "github.com/restic/restic/internal/repository" - "github.com/restic/restic/internal/restic" - rtest "github.com/restic/restic/internal/test" - "github.com/restic/restic/internal/walk" -) - -func TestWalkTree(t *testing.T) { - repo, cleanup := repository.TestRepository(t) - defer cleanup() - - dirs, err := filepath.Glob(rtest.TestWalkerPath) - rtest.OK(t, err) - - // archive a few files - arch := archiver.New(repo) - sn, _, err := arch.Snapshot(context.TODO(), nil, dirs, nil, "localhost", nil, time.Now()) - rtest.OK(t, err) - - // flush repo, write all packs - rtest.OK(t, repo.Flush(context.Background())) - - // start tree walker - treeJobs := make(chan walk.TreeJob) - go walk.Tree(context.TODO(), repo, *sn.Tree, treeJobs) - - // start filesystem walker - fsJobs := make(chan pipe.Job) - resCh := make(chan pipe.Result, 1) - - f := func(string, os.FileInfo) bool { - return true - } - go pipe.Walk(context.TODO(), dirs, f, fsJobs, resCh) - - for { - // receive fs job - fsJob, fsChOpen := <-fsJobs - rtest.Assert(t, !fsChOpen || fsJob != nil, - "received nil job from filesystem: %v %v", fsJob, fsChOpen) - if fsJob != nil { - rtest.OK(t, fsJob.Error()) - } - - var path string - fsEntries := 1 - switch j := fsJob.(type) { - case pipe.Dir: - path = j.Path() - fsEntries = len(j.Entries) - case pipe.Entry: - path = j.Path() - } - - // receive tree job - treeJob, treeChOpen := <-treeJobs - treeEntries := 1 - - rtest.OK(t, treeJob.Error) - - if treeJob.Tree != nil { - treeEntries = len(treeJob.Tree.Nodes) - } - - rtest.Assert(t, fsChOpen == treeChOpen, - "one channel closed too early: fsChOpen %v, treeChOpen %v", - fsChOpen, treeChOpen) - - if !fsChOpen || !treeChOpen { - break - } - - rtest.Assert(t, filepath.Base(path) == filepath.Base(treeJob.Path), - "paths do not match: %q != %q", filepath.Base(path), filepath.Base(treeJob.Path)) - - rtest.Assert(t, fsEntries == treeEntries, - "wrong number of entries: %v != %v", fsEntries, treeEntries) - } -} - -type delayRepo struct { - repo restic.Repository - delay time.Duration -} - -func (d delayRepo) LoadTree(ctx context.Context, id restic.ID) (*restic.Tree, error) { - time.Sleep(d.delay) - return d.repo.LoadTree(ctx, id) -} - -var repoFixture = filepath.Join("testdata", "walktree-test-repo.tar.gz") - -var walktreeTestItems = []string{ - "testdata/0/0/0/0", - "testdata/0/0/0/1", - "testdata/0/0/0/10", - "testdata/0/0/0/100", - "testdata/0/0/0/101", - "testdata/0/0/0/102", - "testdata/0/0/0/103", - "testdata/0/0/0/104", - "testdata/0/0/0/105", - "testdata/0/0/0/106", - "testdata/0/0/0/107", - "testdata/0/0/0/108", - "testdata/0/0/0/109", - "testdata/0/0/0/11", - "testdata/0/0/0/110", - "testdata/0/0/0/111", - "testdata/0/0/0/112", - "testdata/0/0/0/113", - "testdata/0/0/0/114", - "testdata/0/0/0/115", - "testdata/0/0/0/116", - "testdata/0/0/0/117", - "testdata/0/0/0/118", - "testdata/0/0/0/119", - "testdata/0/0/0/12", - "testdata/0/0/0/120", - "testdata/0/0/0/121", - "testdata/0/0/0/122", - "testdata/0/0/0/123", - "testdata/0/0/0/124", - "testdata/0/0/0/125", - "testdata/0/0/0/126", - "testdata/0/0/0/127", - "testdata/0/0/0/13", - "testdata/0/0/0/14", - "testdata/0/0/0/15", - "testdata/0/0/0/16", - "testdata/0/0/0/17", - "testdata/0/0/0/18", - "testdata/0/0/0/19", - "testdata/0/0/0/2", - "testdata/0/0/0/20", - "testdata/0/0/0/21", - "testdata/0/0/0/22", - "testdata/0/0/0/23", - "testdata/0/0/0/24", - "testdata/0/0/0/25", - "testdata/0/0/0/26", - "testdata/0/0/0/27", - "testdata/0/0/0/28", - "testdata/0/0/0/29", - "testdata/0/0/0/3", - "testdata/0/0/0/30", - "testdata/0/0/0/31", - "testdata/0/0/0/32", - "testdata/0/0/0/33", - "testdata/0/0/0/34", - "testdata/0/0/0/35", - "testdata/0/0/0/36", - "testdata/0/0/0/37", - "testdata/0/0/0/38", - "testdata/0/0/0/39", - "testdata/0/0/0/4", - "testdata/0/0/0/40", - "testdata/0/0/0/41", - "testdata/0/0/0/42", - "testdata/0/0/0/43", - "testdata/0/0/0/44", - "testdata/0/0/0/45", - "testdata/0/0/0/46", - "testdata/0/0/0/47", - "testdata/0/0/0/48", - "testdata/0/0/0/49", - "testdata/0/0/0/5", - "testdata/0/0/0/50", - "testdata/0/0/0/51", - "testdata/0/0/0/52", - "testdata/0/0/0/53", - "testdata/0/0/0/54", - "testdata/0/0/0/55", - "testdata/0/0/0/56", - "testdata/0/0/0/57", - "testdata/0/0/0/58", - "testdata/0/0/0/59", - "testdata/0/0/0/6", - "testdata/0/0/0/60", - "testdata/0/0/0/61", - "testdata/0/0/0/62", - "testdata/0/0/0/63", - "testdata/0/0/0/64", - "testdata/0/0/0/65", - "testdata/0/0/0/66", - "testdata/0/0/0/67", - "testdata/0/0/0/68", - "testdata/0/0/0/69", - "testdata/0/0/0/7", - "testdata/0/0/0/70", - "testdata/0/0/0/71", - "testdata/0/0/0/72", - "testdata/0/0/0/73", - "testdata/0/0/0/74", - "testdata/0/0/0/75", - "testdata/0/0/0/76", - "testdata/0/0/0/77", - "testdata/0/0/0/78", - "testdata/0/0/0/79", - "testdata/0/0/0/8", - "testdata/0/0/0/80", - "testdata/0/0/0/81", - "testdata/0/0/0/82", - "testdata/0/0/0/83", - "testdata/0/0/0/84", - "testdata/0/0/0/85", - "testdata/0/0/0/86", - "testdata/0/0/0/87", - "testdata/0/0/0/88", - "testdata/0/0/0/89", - "testdata/0/0/0/9", - "testdata/0/0/0/90", - "testdata/0/0/0/91", - "testdata/0/0/0/92", - "testdata/0/0/0/93", - "testdata/0/0/0/94", - "testdata/0/0/0/95", - "testdata/0/0/0/96", - "testdata/0/0/0/97", - "testdata/0/0/0/98", - "testdata/0/0/0/99", - "testdata/0/0/0", - "testdata/0/0/1/0", - "testdata/0/0/1/1", - "testdata/0/0/1/10", - "testdata/0/0/1/100", - "testdata/0/0/1/101", - "testdata/0/0/1/102", - "testdata/0/0/1/103", - "testdata/0/0/1/104", - "testdata/0/0/1/105", - "testdata/0/0/1/106", - "testdata/0/0/1/107", - "testdata/0/0/1/108", - "testdata/0/0/1/109", - "testdata/0/0/1/11", - "testdata/0/0/1/110", - "testdata/0/0/1/111", - "testdata/0/0/1/112", - "testdata/0/0/1/113", - "testdata/0/0/1/114", - "testdata/0/0/1/115", - "testdata/0/0/1/116", - "testdata/0/0/1/117", - "testdata/0/0/1/118", - "testdata/0/0/1/119", - "testdata/0/0/1/12", - "testdata/0/0/1/120", - "testdata/0/0/1/121", - "testdata/0/0/1/122", - "testdata/0/0/1/123", - "testdata/0/0/1/124", - "testdata/0/0/1/125", - "testdata/0/0/1/126", - "testdata/0/0/1/127", - "testdata/0/0/1/13", - "testdata/0/0/1/14", - "testdata/0/0/1/15", - "testdata/0/0/1/16", - "testdata/0/0/1/17", - "testdata/0/0/1/18", - "testdata/0/0/1/19", - "testdata/0/0/1/2", - "testdata/0/0/1/20", - "testdata/0/0/1/21", - "testdata/0/0/1/22", - "testdata/0/0/1/23", - "testdata/0/0/1/24", - "testdata/0/0/1/25", - "testdata/0/0/1/26", - "testdata/0/0/1/27", - "testdata/0/0/1/28", - "testdata/0/0/1/29", - "testdata/0/0/1/3", - "testdata/0/0/1/30", - "testdata/0/0/1/31", - "testdata/0/0/1/32", - "testdata/0/0/1/33", - "testdata/0/0/1/34", - "testdata/0/0/1/35", - "testdata/0/0/1/36", - "testdata/0/0/1/37", - "testdata/0/0/1/38", - "testdata/0/0/1/39", - "testdata/0/0/1/4", - "testdata/0/0/1/40", - "testdata/0/0/1/41", - "testdata/0/0/1/42", - "testdata/0/0/1/43", - "testdata/0/0/1/44", - "testdata/0/0/1/45", - "testdata/0/0/1/46", - "testdata/0/0/1/47", - "testdata/0/0/1/48", - "testdata/0/0/1/49", - "testdata/0/0/1/5", - "testdata/0/0/1/50", - "testdata/0/0/1/51", - "testdata/0/0/1/52", - "testdata/0/0/1/53", - "testdata/0/0/1/54", - "testdata/0/0/1/55", - "testdata/0/0/1/56", - "testdata/0/0/1/57", - "testdata/0/0/1/58", - "testdata/0/0/1/59", - "testdata/0/0/1/6", - "testdata/0/0/1/60", - "testdata/0/0/1/61", - "testdata/0/0/1/62", - "testdata/0/0/1/63", - "testdata/0/0/1/64", - "testdata/0/0/1/65", - "testdata/0/0/1/66", - "testdata/0/0/1/67", - "testdata/0/0/1/68", - "testdata/0/0/1/69", - "testdata/0/0/1/7", - "testdata/0/0/1/70", - "testdata/0/0/1/71", - "testdata/0/0/1/72", - "testdata/0/0/1/73", - "testdata/0/0/1/74", - "testdata/0/0/1/75", - "testdata/0/0/1/76", - "testdata/0/0/1/77", - "testdata/0/0/1/78", - "testdata/0/0/1/79", - "testdata/0/0/1/8", - "testdata/0/0/1/80", - "testdata/0/0/1/81", - "testdata/0/0/1/82", - "testdata/0/0/1/83", - "testdata/0/0/1/84", - "testdata/0/0/1/85", - "testdata/0/0/1/86", - "testdata/0/0/1/87", - "testdata/0/0/1/88", - "testdata/0/0/1/89", - "testdata/0/0/1/9", - "testdata/0/0/1/90", - "testdata/0/0/1/91", - "testdata/0/0/1/92", - "testdata/0/0/1/93", - "testdata/0/0/1/94", - "testdata/0/0/1/95", - "testdata/0/0/1/96", - "testdata/0/0/1/97", - "testdata/0/0/1/98", - "testdata/0/0/1/99", - "testdata/0/0/1", - "testdata/0/0/2/0", - "testdata/0/0/2/1", - "testdata/0/0/2/10", - "testdata/0/0/2/100", - "testdata/0/0/2/101", - "testdata/0/0/2/102", - "testdata/0/0/2/103", - "testdata/0/0/2/104", - "testdata/0/0/2/105", - "testdata/0/0/2/106", - "testdata/0/0/2/107", - "testdata/0/0/2/108", - "testdata/0/0/2/109", - "testdata/0/0/2/11", - "testdata/0/0/2/110", - "testdata/0/0/2/111", - "testdata/0/0/2/112", - "testdata/0/0/2/113", - "testdata/0/0/2/114", - "testdata/0/0/2/115", - "testdata/0/0/2/116", - "testdata/0/0/2/117", - "testdata/0/0/2/118", - "testdata/0/0/2/119", - "testdata/0/0/2/12", - "testdata/0/0/2/120", - "testdata/0/0/2/121", - "testdata/0/0/2/122", - "testdata/0/0/2/123", - "testdata/0/0/2/124", - "testdata/0/0/2/125", - "testdata/0/0/2/126", - "testdata/0/0/2/127", - "testdata/0/0/2/13", - "testdata/0/0/2/14", - "testdata/0/0/2/15", - "testdata/0/0/2/16", - "testdata/0/0/2/17", - "testdata/0/0/2/18", - "testdata/0/0/2/19", - "testdata/0/0/2/2", - "testdata/0/0/2/20", - "testdata/0/0/2/21", - "testdata/0/0/2/22", - "testdata/0/0/2/23", - "testdata/0/0/2/24", - "testdata/0/0/2/25", - "testdata/0/0/2/26", - "testdata/0/0/2/27", - "testdata/0/0/2/28", - "testdata/0/0/2/29", - "testdata/0/0/2/3", - "testdata/0/0/2/30", - "testdata/0/0/2/31", - "testdata/0/0/2/32", - "testdata/0/0/2/33", - "testdata/0/0/2/34", - "testdata/0/0/2/35", - "testdata/0/0/2/36", - "testdata/0/0/2/37", - "testdata/0/0/2/38", - "testdata/0/0/2/39", - "testdata/0/0/2/4", - "testdata/0/0/2/40", - "testdata/0/0/2/41", - "testdata/0/0/2/42", - "testdata/0/0/2/43", - "testdata/0/0/2/44", - "testdata/0/0/2/45", - "testdata/0/0/2/46", - "testdata/0/0/2/47", - "testdata/0/0/2/48", - "testdata/0/0/2/49", - "testdata/0/0/2/5", - "testdata/0/0/2/50", - "testdata/0/0/2/51", - "testdata/0/0/2/52", - "testdata/0/0/2/53", - "testdata/0/0/2/54", - "testdata/0/0/2/55", - "testdata/0/0/2/56", - "testdata/0/0/2/57", - "testdata/0/0/2/58", - "testdata/0/0/2/59", - "testdata/0/0/2/6", - "testdata/0/0/2/60", - "testdata/0/0/2/61", - "testdata/0/0/2/62", - "testdata/0/0/2/63", - "testdata/0/0/2/64", - "testdata/0/0/2/65", - "testdata/0/0/2/66", - "testdata/0/0/2/67", - "testdata/0/0/2/68", - "testdata/0/0/2/69", - "testdata/0/0/2/7", - "testdata/0/0/2/70", - "testdata/0/0/2/71", - "testdata/0/0/2/72", - "testdata/0/0/2/73", - "testdata/0/0/2/74", - "testdata/0/0/2/75", - "testdata/0/0/2/76", - "testdata/0/0/2/77", - "testdata/0/0/2/78", - "testdata/0/0/2/79", - "testdata/0/0/2/8", - "testdata/0/0/2/80", - "testdata/0/0/2/81", - "testdata/0/0/2/82", - "testdata/0/0/2/83", - "testdata/0/0/2/84", - "testdata/0/0/2/85", - "testdata/0/0/2/86", - "testdata/0/0/2/87", - "testdata/0/0/2/88", - "testdata/0/0/2/89", - "testdata/0/0/2/9", - "testdata/0/0/2/90", - "testdata/0/0/2/91", - "testdata/0/0/2/92", - "testdata/0/0/2/93", - "testdata/0/0/2/94", - "testdata/0/0/2/95", - "testdata/0/0/2/96", - "testdata/0/0/2/97", - "testdata/0/0/2/98", - "testdata/0/0/2/99", - "testdata/0/0/2", - "testdata/0/0/3/0", - "testdata/0/0/3/1", - "testdata/0/0/3/10", - "testdata/0/0/3/100", - "testdata/0/0/3/101", - "testdata/0/0/3/102", - "testdata/0/0/3/103", - "testdata/0/0/3/104", - "testdata/0/0/3/105", - "testdata/0/0/3/106", - "testdata/0/0/3/107", - "testdata/0/0/3/108", - "testdata/0/0/3/109", - "testdata/0/0/3/11", - "testdata/0/0/3/110", - "testdata/0/0/3/111", - "testdata/0/0/3/112", - "testdata/0/0/3/113", - "testdata/0/0/3/114", - "testdata/0/0/3/115", - "testdata/0/0/3/116", - "testdata/0/0/3/117", - "testdata/0/0/3/118", - "testdata/0/0/3/119", - "testdata/0/0/3/12", - "testdata/0/0/3/120", - "testdata/0/0/3/121", - "testdata/0/0/3/122", - "testdata/0/0/3/123", - "testdata/0/0/3/124", - "testdata/0/0/3/125", - "testdata/0/0/3/126", - "testdata/0/0/3/127", - "testdata/0/0/3/13", - "testdata/0/0/3/14", - "testdata/0/0/3/15", - "testdata/0/0/3/16", - "testdata/0/0/3/17", - "testdata/0/0/3/18", - "testdata/0/0/3/19", - "testdata/0/0/3/2", - "testdata/0/0/3/20", - "testdata/0/0/3/21", - "testdata/0/0/3/22", - "testdata/0/0/3/23", - "testdata/0/0/3/24", - "testdata/0/0/3/25", - "testdata/0/0/3/26", - "testdata/0/0/3/27", - "testdata/0/0/3/28", - "testdata/0/0/3/29", - "testdata/0/0/3/3", - "testdata/0/0/3/30", - "testdata/0/0/3/31", - "testdata/0/0/3/32", - "testdata/0/0/3/33", - "testdata/0/0/3/34", - "testdata/0/0/3/35", - "testdata/0/0/3/36", - "testdata/0/0/3/37", - "testdata/0/0/3/38", - "testdata/0/0/3/39", - "testdata/0/0/3/4", - "testdata/0/0/3/40", - "testdata/0/0/3/41", - "testdata/0/0/3/42", - "testdata/0/0/3/43", - "testdata/0/0/3/44", - "testdata/0/0/3/45", - "testdata/0/0/3/46", - "testdata/0/0/3/47", - "testdata/0/0/3/48", - "testdata/0/0/3/49", - "testdata/0/0/3/5", - "testdata/0/0/3/50", - "testdata/0/0/3/51", - "testdata/0/0/3/52", - "testdata/0/0/3/53", - "testdata/0/0/3/54", - "testdata/0/0/3/55", - "testdata/0/0/3/56", - "testdata/0/0/3/57", - "testdata/0/0/3/58", - "testdata/0/0/3/59", - "testdata/0/0/3/6", - "testdata/0/0/3/60", - "testdata/0/0/3/61", - "testdata/0/0/3/62", - "testdata/0/0/3/63", - "testdata/0/0/3/64", - "testdata/0/0/3/65", - "testdata/0/0/3/66", - "testdata/0/0/3/67", - "testdata/0/0/3/68", - "testdata/0/0/3/69", - "testdata/0/0/3/7", - "testdata/0/0/3/70", - "testdata/0/0/3/71", - "testdata/0/0/3/72", - "testdata/0/0/3/73", - "testdata/0/0/3/74", - "testdata/0/0/3/75", - "testdata/0/0/3/76", - "testdata/0/0/3/77", - "testdata/0/0/3/78", - "testdata/0/0/3/79", - "testdata/0/0/3/8", - "testdata/0/0/3/80", - "testdata/0/0/3/81", - "testdata/0/0/3/82", - "testdata/0/0/3/83", - "testdata/0/0/3/84", - "testdata/0/0/3/85", - "testdata/0/0/3/86", - "testdata/0/0/3/87", - "testdata/0/0/3/88", - "testdata/0/0/3/89", - "testdata/0/0/3/9", - "testdata/0/0/3/90", - "testdata/0/0/3/91", - "testdata/0/0/3/92", - "testdata/0/0/3/93", - "testdata/0/0/3/94", - "testdata/0/0/3/95", - "testdata/0/0/3/96", - "testdata/0/0/3/97", - "testdata/0/0/3/98", - "testdata/0/0/3/99", - "testdata/0/0/3", - "testdata/0/0/4/0", - "testdata/0/0/4/1", - "testdata/0/0/4/10", - "testdata/0/0/4/100", - "testdata/0/0/4/101", - "testdata/0/0/4/102", - "testdata/0/0/4/103", - "testdata/0/0/4/104", - "testdata/0/0/4/105", - "testdata/0/0/4/106", - "testdata/0/0/4/107", - "testdata/0/0/4/108", - "testdata/0/0/4/109", - "testdata/0/0/4/11", - "testdata/0/0/4/110", - "testdata/0/0/4/111", - "testdata/0/0/4/112", - "testdata/0/0/4/113", - "testdata/0/0/4/114", - "testdata/0/0/4/115", - "testdata/0/0/4/116", - "testdata/0/0/4/117", - "testdata/0/0/4/118", - "testdata/0/0/4/119", - "testdata/0/0/4/12", - "testdata/0/0/4/120", - "testdata/0/0/4/121", - "testdata/0/0/4/122", - "testdata/0/0/4/123", - "testdata/0/0/4/124", - "testdata/0/0/4/125", - "testdata/0/0/4/126", - "testdata/0/0/4/127", - "testdata/0/0/4/13", - "testdata/0/0/4/14", - "testdata/0/0/4/15", - "testdata/0/0/4/16", - "testdata/0/0/4/17", - "testdata/0/0/4/18", - "testdata/0/0/4/19", - "testdata/0/0/4/2", - "testdata/0/0/4/20", - "testdata/0/0/4/21", - "testdata/0/0/4/22", - "testdata/0/0/4/23", - "testdata/0/0/4/24", - "testdata/0/0/4/25", - "testdata/0/0/4/26", - "testdata/0/0/4/27", - "testdata/0/0/4/28", - "testdata/0/0/4/29", - "testdata/0/0/4/3", - "testdata/0/0/4/30", - "testdata/0/0/4/31", - "testdata/0/0/4/32", - "testdata/0/0/4/33", - "testdata/0/0/4/34", - "testdata/0/0/4/35", - "testdata/0/0/4/36", - "testdata/0/0/4/37", - "testdata/0/0/4/38", - "testdata/0/0/4/39", - "testdata/0/0/4/4", - "testdata/0/0/4/40", - "testdata/0/0/4/41", - "testdata/0/0/4/42", - "testdata/0/0/4/43", - "testdata/0/0/4/44", - "testdata/0/0/4/45", - "testdata/0/0/4/46", - "testdata/0/0/4/47", - "testdata/0/0/4/48", - "testdata/0/0/4/49", - "testdata/0/0/4/5", - "testdata/0/0/4/50", - "testdata/0/0/4/51", - "testdata/0/0/4/52", - "testdata/0/0/4/53", - "testdata/0/0/4/54", - "testdata/0/0/4/55", - "testdata/0/0/4/56", - "testdata/0/0/4/57", - "testdata/0/0/4/58", - "testdata/0/0/4/59", - "testdata/0/0/4/6", - "testdata/0/0/4/60", - "testdata/0/0/4/61", - "testdata/0/0/4/62", - "testdata/0/0/4/63", - "testdata/0/0/4/64", - "testdata/0/0/4/65", - "testdata/0/0/4/66", - "testdata/0/0/4/67", - "testdata/0/0/4/68", - "testdata/0/0/4/69", - "testdata/0/0/4/7", - "testdata/0/0/4/70", - "testdata/0/0/4/71", - "testdata/0/0/4/72", - "testdata/0/0/4/73", - "testdata/0/0/4/74", - "testdata/0/0/4/75", - "testdata/0/0/4/76", - "testdata/0/0/4/77", - "testdata/0/0/4/78", - "testdata/0/0/4/79", - "testdata/0/0/4/8", - "testdata/0/0/4/80", - "testdata/0/0/4/81", - "testdata/0/0/4/82", - "testdata/0/0/4/83", - "testdata/0/0/4/84", - "testdata/0/0/4/85", - "testdata/0/0/4/86", - "testdata/0/0/4/87", - "testdata/0/0/4/88", - "testdata/0/0/4/89", - "testdata/0/0/4/9", - "testdata/0/0/4/90", - "testdata/0/0/4/91", - "testdata/0/0/4/92", - "testdata/0/0/4/93", - "testdata/0/0/4/94", - "testdata/0/0/4/95", - "testdata/0/0/4/96", - "testdata/0/0/4/97", - "testdata/0/0/4/98", - "testdata/0/0/4/99", - "testdata/0/0/4", - "testdata/0/0/5/0", - "testdata/0/0/5/1", - "testdata/0/0/5/10", - "testdata/0/0/5/100", - "testdata/0/0/5/101", - "testdata/0/0/5/102", - "testdata/0/0/5/103", - "testdata/0/0/5/104", - "testdata/0/0/5/105", - "testdata/0/0/5/106", - "testdata/0/0/5/107", - "testdata/0/0/5/108", - "testdata/0/0/5/109", - "testdata/0/0/5/11", - "testdata/0/0/5/110", - "testdata/0/0/5/111", - "testdata/0/0/5/112", - "testdata/0/0/5/113", - "testdata/0/0/5/114", - "testdata/0/0/5/115", - "testdata/0/0/5/116", - "testdata/0/0/5/117", - "testdata/0/0/5/118", - "testdata/0/0/5/119", - "testdata/0/0/5/12", - "testdata/0/0/5/120", - "testdata/0/0/5/121", - "testdata/0/0/5/122", - "testdata/0/0/5/123", - "testdata/0/0/5/124", - "testdata/0/0/5/125", - "testdata/0/0/5/126", - "testdata/0/0/5/127", - "testdata/0/0/5/13", - "testdata/0/0/5/14", - "testdata/0/0/5/15", - "testdata/0/0/5/16", - "testdata/0/0/5/17", - "testdata/0/0/5/18", - "testdata/0/0/5/19", - "testdata/0/0/5/2", - "testdata/0/0/5/20", - "testdata/0/0/5/21", - "testdata/0/0/5/22", - "testdata/0/0/5/23", - "testdata/0/0/5/24", - "testdata/0/0/5/25", - "testdata/0/0/5/26", - "testdata/0/0/5/27", - "testdata/0/0/5/28", - "testdata/0/0/5/29", - "testdata/0/0/5/3", - "testdata/0/0/5/30", - "testdata/0/0/5/31", - "testdata/0/0/5/32", - "testdata/0/0/5/33", - "testdata/0/0/5/34", - "testdata/0/0/5/35", - "testdata/0/0/5/36", - "testdata/0/0/5/37", - "testdata/0/0/5/38", - "testdata/0/0/5/39", - "testdata/0/0/5/4", - "testdata/0/0/5/40", - "testdata/0/0/5/41", - "testdata/0/0/5/42", - "testdata/0/0/5/43", - "testdata/0/0/5/44", - "testdata/0/0/5/45", - "testdata/0/0/5/46", - "testdata/0/0/5/47", - "testdata/0/0/5/48", - "testdata/0/0/5/49", - "testdata/0/0/5/5", - "testdata/0/0/5/50", - "testdata/0/0/5/51", - "testdata/0/0/5/52", - "testdata/0/0/5/53", - "testdata/0/0/5/54", - "testdata/0/0/5/55", - "testdata/0/0/5/56", - "testdata/0/0/5/57", - "testdata/0/0/5/58", - "testdata/0/0/5/59", - "testdata/0/0/5/6", - "testdata/0/0/5/60", - "testdata/0/0/5/61", - "testdata/0/0/5/62", - "testdata/0/0/5/63", - "testdata/0/0/5/64", - "testdata/0/0/5/65", - "testdata/0/0/5/66", - "testdata/0/0/5/67", - "testdata/0/0/5/68", - "testdata/0/0/5/69", - "testdata/0/0/5/7", - "testdata/0/0/5/70", - "testdata/0/0/5/71", - "testdata/0/0/5/72", - "testdata/0/0/5/73", - "testdata/0/0/5/74", - "testdata/0/0/5/75", - "testdata/0/0/5/76", - "testdata/0/0/5/77", - "testdata/0/0/5/78", - "testdata/0/0/5/79", - "testdata/0/0/5/8", - "testdata/0/0/5/80", - "testdata/0/0/5/81", - "testdata/0/0/5/82", - "testdata/0/0/5/83", - "testdata/0/0/5/84", - "testdata/0/0/5/85", - "testdata/0/0/5/86", - "testdata/0/0/5/87", - "testdata/0/0/5/88", - "testdata/0/0/5/89", - "testdata/0/0/5/9", - "testdata/0/0/5/90", - "testdata/0/0/5/91", - "testdata/0/0/5/92", - "testdata/0/0/5/93", - "testdata/0/0/5/94", - "testdata/0/0/5/95", - "testdata/0/0/5/96", - "testdata/0/0/5/97", - "testdata/0/0/5/98", - "testdata/0/0/5/99", - "testdata/0/0/5", - "testdata/0/0/6/0", - "testdata/0/0/6/1", - "testdata/0/0/6/10", - "testdata/0/0/6/100", - "testdata/0/0/6/101", - "testdata/0/0/6/102", - "testdata/0/0/6/103", - "testdata/0/0/6/104", - "testdata/0/0/6/105", - "testdata/0/0/6/106", - "testdata/0/0/6/107", - "testdata/0/0/6/108", - "testdata/0/0/6/109", - "testdata/0/0/6/11", - "testdata/0/0/6/110", - "testdata/0/0/6/111", - "testdata/0/0/6/112", - "testdata/0/0/6/113", - "testdata/0/0/6/114", - "testdata/0/0/6/115", - "testdata/0/0/6/116", - "testdata/0/0/6/117", - "testdata/0/0/6/118", - "testdata/0/0/6/119", - "testdata/0/0/6/12", - "testdata/0/0/6/120", - "testdata/0/0/6/121", - "testdata/0/0/6/122", - "testdata/0/0/6/123", - "testdata/0/0/6/124", - "testdata/0/0/6/125", - "testdata/0/0/6/126", - "testdata/0/0/6/127", - "testdata/0/0/6/13", - "testdata/0/0/6/14", - "testdata/0/0/6/15", - "testdata/0/0/6/16", - "testdata/0/0/6/17", - "testdata/0/0/6/18", - "testdata/0/0/6/19", - "testdata/0/0/6/2", - "testdata/0/0/6/20", - "testdata/0/0/6/21", - "testdata/0/0/6/22", - "testdata/0/0/6/23", - "testdata/0/0/6/24", - "testdata/0/0/6/25", - "testdata/0/0/6/26", - "testdata/0/0/6/27", - "testdata/0/0/6/28", - "testdata/0/0/6/29", - "testdata/0/0/6/3", - "testdata/0/0/6/30", - "testdata/0/0/6/31", - "testdata/0/0/6/32", - "testdata/0/0/6/33", - "testdata/0/0/6/34", - "testdata/0/0/6/35", - "testdata/0/0/6/36", - "testdata/0/0/6/37", - "testdata/0/0/6/38", - "testdata/0/0/6/39", - "testdata/0/0/6/4", - "testdata/0/0/6/40", - "testdata/0/0/6/41", - "testdata/0/0/6/42", - "testdata/0/0/6/43", - "testdata/0/0/6/44", - "testdata/0/0/6/45", - "testdata/0/0/6/46", - "testdata/0/0/6/47", - "testdata/0/0/6/48", - "testdata/0/0/6/49", - "testdata/0/0/6/5", - "testdata/0/0/6/50", - "testdata/0/0/6/51", - "testdata/0/0/6/52", - "testdata/0/0/6/53", - "testdata/0/0/6/54", - "testdata/0/0/6/55", - "testdata/0/0/6/56", - "testdata/0/0/6/57", - "testdata/0/0/6/58", - "testdata/0/0/6/59", - "testdata/0/0/6/6", - "testdata/0/0/6/60", - "testdata/0/0/6/61", - "testdata/0/0/6/62", - "testdata/0/0/6/63", - "testdata/0/0/6/64", - "testdata/0/0/6/65", - "testdata/0/0/6/66", - "testdata/0/0/6/67", - "testdata/0/0/6/68", - "testdata/0/0/6/69", - "testdata/0/0/6/7", - "testdata/0/0/6/70", - "testdata/0/0/6/71", - "testdata/0/0/6/72", - "testdata/0/0/6/73", - "testdata/0/0/6/74", - "testdata/0/0/6/75", - "testdata/0/0/6/76", - "testdata/0/0/6/77", - "testdata/0/0/6/78", - "testdata/0/0/6/79", - "testdata/0/0/6/8", - "testdata/0/0/6/80", - "testdata/0/0/6/81", - "testdata/0/0/6/82", - "testdata/0/0/6/83", - "testdata/0/0/6/84", - "testdata/0/0/6/85", - "testdata/0/0/6/86", - "testdata/0/0/6/87", - "testdata/0/0/6/88", - "testdata/0/0/6/89", - "testdata/0/0/6/9", - "testdata/0/0/6/90", - "testdata/0/0/6/91", - "testdata/0/0/6/92", - "testdata/0/0/6/93", - "testdata/0/0/6/94", - "testdata/0/0/6/95", - "testdata/0/0/6/96", - "testdata/0/0/6/97", - "testdata/0/0/6/98", - "testdata/0/0/6/99", - "testdata/0/0/6", - "testdata/0/0/7/0", - "testdata/0/0/7/1", - "testdata/0/0/7/10", - "testdata/0/0/7/100", - "testdata/0/0/7/101", - "testdata/0/0/7/102", - "testdata/0/0/7/103", - "testdata/0/0/7/104", - "testdata/0/0/7/105", - "testdata/0/0/7/106", - "testdata/0/0/7/107", - "testdata/0/0/7/108", - "testdata/0/0/7/109", - "testdata/0/0/7/11", - "testdata/0/0/7/110", - "testdata/0/0/7/111", - "testdata/0/0/7/112", - "testdata/0/0/7/113", - "testdata/0/0/7/114", - "testdata/0/0/7/115", - "testdata/0/0/7/116", - "testdata/0/0/7/117", - "testdata/0/0/7/118", - "testdata/0/0/7/119", - "testdata/0/0/7/12", - "testdata/0/0/7/120", - "testdata/0/0/7/121", - "testdata/0/0/7/122", - "testdata/0/0/7/123", - "testdata/0/0/7/124", - "testdata/0/0/7/125", - "testdata/0/0/7/126", - "testdata/0/0/7/127", - "testdata/0/0/7/13", - "testdata/0/0/7/14", - "testdata/0/0/7/15", - "testdata/0/0/7/16", - "testdata/0/0/7/17", - "testdata/0/0/7/18", - "testdata/0/0/7/19", - "testdata/0/0/7/2", - "testdata/0/0/7/20", - "testdata/0/0/7/21", - "testdata/0/0/7/22", - "testdata/0/0/7/23", - "testdata/0/0/7/24", - "testdata/0/0/7/25", - "testdata/0/0/7/26", - "testdata/0/0/7/27", - "testdata/0/0/7/28", - "testdata/0/0/7/29", - "testdata/0/0/7/3", - "testdata/0/0/7/30", - "testdata/0/0/7/31", - "testdata/0/0/7/32", - "testdata/0/0/7/33", - "testdata/0/0/7/34", - "testdata/0/0/7/35", - "testdata/0/0/7/36", - "testdata/0/0/7/37", - "testdata/0/0/7/38", - "testdata/0/0/7/39", - "testdata/0/0/7/4", - "testdata/0/0/7/40", - "testdata/0/0/7/41", - "testdata/0/0/7/42", - "testdata/0/0/7/43", - "testdata/0/0/7/44", - "testdata/0/0/7/45", - "testdata/0/0/7/46", - "testdata/0/0/7/47", - "testdata/0/0/7/48", - "testdata/0/0/7/49", - "testdata/0/0/7/5", - "testdata/0/0/7/50", - "testdata/0/0/7/51", - "testdata/0/0/7/52", - "testdata/0/0/7/53", - "testdata/0/0/7/54", - "testdata/0/0/7/55", - "testdata/0/0/7/56", - "testdata/0/0/7/57", - "testdata/0/0/7/58", - "testdata/0/0/7/59", - "testdata/0/0/7/6", - "testdata/0/0/7/60", - "testdata/0/0/7/61", - "testdata/0/0/7/62", - "testdata/0/0/7/63", - "testdata/0/0/7/64", - "testdata/0/0/7/65", - "testdata/0/0/7/66", - "testdata/0/0/7/67", - "testdata/0/0/7/68", - "testdata/0/0/7/69", - "testdata/0/0/7/7", - "testdata/0/0/7/70", - "testdata/0/0/7/71", - "testdata/0/0/7/72", - "testdata/0/0/7/73", - "testdata/0/0/7/74", - "testdata/0/0/7/75", - "testdata/0/0/7/76", - "testdata/0/0/7/77", - "testdata/0/0/7/78", - "testdata/0/0/7/79", - "testdata/0/0/7/8", - "testdata/0/0/7/80", - "testdata/0/0/7/81", - "testdata/0/0/7/82", - "testdata/0/0/7/83", - "testdata/0/0/7/84", - "testdata/0/0/7/85", - "testdata/0/0/7/86", - "testdata/0/0/7/87", - "testdata/0/0/7/88", - "testdata/0/0/7/89", - "testdata/0/0/7/9", - "testdata/0/0/7/90", - "testdata/0/0/7/91", - "testdata/0/0/7/92", - "testdata/0/0/7/93", - "testdata/0/0/7/94", - "testdata/0/0/7/95", - "testdata/0/0/7/96", - "testdata/0/0/7/97", - "testdata/0/0/7/98", - "testdata/0/0/7/99", - "testdata/0/0/7", - "testdata/0/0/8/0", - "testdata/0/0/8/1", - "testdata/0/0/8/10", - "testdata/0/0/8/100", - "testdata/0/0/8/101", - "testdata/0/0/8/102", - "testdata/0/0/8/103", - "testdata/0/0/8/104", - "testdata/0/0/8/105", - "testdata/0/0/8/106", - "testdata/0/0/8/107", - "testdata/0/0/8/108", - "testdata/0/0/8/109", - "testdata/0/0/8/11", - "testdata/0/0/8/110", - "testdata/0/0/8/111", - "testdata/0/0/8/112", - "testdata/0/0/8/113", - "testdata/0/0/8/114", - "testdata/0/0/8/115", - "testdata/0/0/8/116", - "testdata/0/0/8/117", - "testdata/0/0/8/118", - "testdata/0/0/8/119", - "testdata/0/0/8/12", - "testdata/0/0/8/120", - "testdata/0/0/8/121", - "testdata/0/0/8/122", - "testdata/0/0/8/123", - "testdata/0/0/8/124", - "testdata/0/0/8/125", - "testdata/0/0/8/126", - "testdata/0/0/8/127", - "testdata/0/0/8/13", - "testdata/0/0/8/14", - "testdata/0/0/8/15", - "testdata/0/0/8/16", - "testdata/0/0/8/17", - "testdata/0/0/8/18", - "testdata/0/0/8/19", - "testdata/0/0/8/2", - "testdata/0/0/8/20", - "testdata/0/0/8/21", - "testdata/0/0/8/22", - "testdata/0/0/8/23", - "testdata/0/0/8/24", - "testdata/0/0/8/25", - "testdata/0/0/8/26", - "testdata/0/0/8/27", - "testdata/0/0/8/28", - "testdata/0/0/8/29", - "testdata/0/0/8/3", - "testdata/0/0/8/30", - "testdata/0/0/8/31", - "testdata/0/0/8/32", - "testdata/0/0/8/33", - "testdata/0/0/8/34", - "testdata/0/0/8/35", - "testdata/0/0/8/36", - "testdata/0/0/8/37", - "testdata/0/0/8/38", - "testdata/0/0/8/39", - "testdata/0/0/8/4", - "testdata/0/0/8/40", - "testdata/0/0/8/41", - "testdata/0/0/8/42", - "testdata/0/0/8/43", - "testdata/0/0/8/44", - "testdata/0/0/8/45", - "testdata/0/0/8/46", - "testdata/0/0/8/47", - "testdata/0/0/8/48", - "testdata/0/0/8/49", - "testdata/0/0/8/5", - "testdata/0/0/8/50", - "testdata/0/0/8/51", - "testdata/0/0/8/52", - "testdata/0/0/8/53", - "testdata/0/0/8/54", - "testdata/0/0/8/55", - "testdata/0/0/8/56", - "testdata/0/0/8/57", - "testdata/0/0/8/58", - "testdata/0/0/8/59", - "testdata/0/0/8/6", - "testdata/0/0/8/60", - "testdata/0/0/8/61", - "testdata/0/0/8/62", - "testdata/0/0/8/63", - "testdata/0/0/8/64", - "testdata/0/0/8/65", - "testdata/0/0/8/66", - "testdata/0/0/8/67", - "testdata/0/0/8/68", - "testdata/0/0/8/69", - "testdata/0/0/8/7", - "testdata/0/0/8/70", - "testdata/0/0/8/71", - "testdata/0/0/8/72", - "testdata/0/0/8/73", - "testdata/0/0/8/74", - "testdata/0/0/8/75", - "testdata/0/0/8/76", - "testdata/0/0/8/77", - "testdata/0/0/8/78", - "testdata/0/0/8/79", - "testdata/0/0/8/8", - "testdata/0/0/8/80", - "testdata/0/0/8/81", - "testdata/0/0/8/82", - "testdata/0/0/8/83", - "testdata/0/0/8/84", - "testdata/0/0/8/85", - "testdata/0/0/8/86", - "testdata/0/0/8/87", - "testdata/0/0/8/88", - "testdata/0/0/8/89", - "testdata/0/0/8/9", - "testdata/0/0/8/90", - "testdata/0/0/8/91", - "testdata/0/0/8/92", - "testdata/0/0/8/93", - "testdata/0/0/8/94", - "testdata/0/0/8/95", - "testdata/0/0/8/96", - "testdata/0/0/8/97", - "testdata/0/0/8/98", - "testdata/0/0/8/99", - "testdata/0/0/8", - "testdata/0/0/9/0", - "testdata/0/0/9/1", - "testdata/0/0/9/10", - "testdata/0/0/9/11", - "testdata/0/0/9/12", - "testdata/0/0/9/13", - "testdata/0/0/9/14", - "testdata/0/0/9/15", - "testdata/0/0/9/16", - "testdata/0/0/9/17", - "testdata/0/0/9/18", - "testdata/0/0/9/19", - "testdata/0/0/9/2", - "testdata/0/0/9/20", - "testdata/0/0/9/21", - "testdata/0/0/9/22", - "testdata/0/0/9/23", - "testdata/0/0/9/24", - "testdata/0/0/9/25", - "testdata/0/0/9/26", - "testdata/0/0/9/27", - "testdata/0/0/9/28", - "testdata/0/0/9/29", - "testdata/0/0/9/3", - "testdata/0/0/9/30", - "testdata/0/0/9/31", - "testdata/0/0/9/32", - "testdata/0/0/9/33", - "testdata/0/0/9/34", - "testdata/0/0/9/35", - "testdata/0/0/9/36", - "testdata/0/0/9/37", - "testdata/0/0/9/38", - "testdata/0/0/9/39", - "testdata/0/0/9/4", - "testdata/0/0/9/40", - "testdata/0/0/9/41", - "testdata/0/0/9/42", - "testdata/0/0/9/43", - "testdata/0/0/9/44", - "testdata/0/0/9/45", - "testdata/0/0/9/46", - "testdata/0/0/9/47", - "testdata/0/0/9/48", - "testdata/0/0/9/49", - "testdata/0/0/9/5", - "testdata/0/0/9/50", - "testdata/0/0/9/51", - "testdata/0/0/9/52", - "testdata/0/0/9/53", - "testdata/0/0/9/54", - "testdata/0/0/9/55", - "testdata/0/0/9/56", - "testdata/0/0/9/57", - "testdata/0/0/9/58", - "testdata/0/0/9/59", - "testdata/0/0/9/6", - "testdata/0/0/9/60", - "testdata/0/0/9/61", - "testdata/0/0/9/62", - "testdata/0/0/9/63", - "testdata/0/0/9/64", - "testdata/0/0/9/65", - "testdata/0/0/9/66", - "testdata/0/0/9/67", - "testdata/0/0/9/68", - "testdata/0/0/9/7", - "testdata/0/0/9/8", - "testdata/0/0/9/9", - "testdata/0/0/9", - "testdata/0/0", - "testdata/0", - "testdata", - "", -} - -func TestDelayedWalkTree(t *testing.T) { - repodir, cleanup := rtest.Env(t, repoFixture) - defer cleanup() - - repo := repository.TestOpenLocal(t, repodir) - rtest.OK(t, repo.LoadIndex(context.TODO())) - - root, err := restic.ParseID("937a2f64f736c64ee700c6ab06f840c68c94799c288146a0e81e07f4c94254da") - rtest.OK(t, err) - - dr := delayRepo{repo, 100 * time.Millisecond} - - // start tree walker - treeJobs := make(chan walk.TreeJob) - go walk.Tree(context.TODO(), dr, root, treeJobs) - - i := 0 - for job := range treeJobs { - expectedPath := filepath.Join(strings.Split(walktreeTestItems[i], "/")...) - if job.Path != expectedPath { - t.Fatalf("expected path %q (%v), got %q", walktreeTestItems[i], i, job.Path) - } - i++ - } - - if i != len(walktreeTestItems) { - t.Fatalf("got %d items, expected %v", i, len(walktreeTestItems)) - } -} - -func BenchmarkDelayedWalkTree(t *testing.B) { - repodir, cleanup := rtest.Env(t, repoFixture) - defer cleanup() - - repo := repository.TestOpenLocal(t, repodir) - rtest.OK(t, repo.LoadIndex(context.TODO())) - - root, err := restic.ParseID("937a2f64f736c64ee700c6ab06f840c68c94799c288146a0e81e07f4c94254da") - rtest.OK(t, err) - - dr := delayRepo{repo, 10 * time.Millisecond} - - t.ResetTimer() - - for i := 0; i < t.N; i++ { - // start tree walker - treeJobs := make(chan walk.TreeJob) - go walk.Tree(context.TODO(), dr, root, treeJobs) - - for range treeJobs { - } - } -} From 76b616451fcca84674f514d2bc41122f30bc4834 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Thu, 19 Apr 2018 22:07:28 +0200 Subject: [PATCH 19/30] Remove unneeded code --- internal/restic/node.go | 39 --------------------------------------- 1 file changed, 39 deletions(-) diff --git a/internal/restic/node.go b/internal/restic/node.go index 8cc94be94..d1adf39eb 100644 --- a/internal/restic/node.go +++ b/internal/restic/node.go @@ -517,45 +517,6 @@ func (node Node) sameExtendedAttributes(other Node) bool { return true } -// IsNewer returns true of the file has been updated since the last Stat(). -func (node *Node) IsNewer(path string, fi os.FileInfo) bool { - if node.Type != "file" { - debug.Log("node %v is newer: not file", path) - return true - } - - tpe := nodeTypeFromFileInfo(fi) - if node.Name != fi.Name() || node.Type != tpe { - debug.Log("node %v is newer: name or type changed", path) - return true - } - - size := uint64(fi.Size()) - - extendedStat, ok := toStatT(fi.Sys()) - if !ok { - if !node.ModTime.Equal(fi.ModTime()) || - node.Size != size { - debug.Log("node %v is newer: timestamp or size changed", path) - return true - } - return false - } - - inode := extendedStat.ino() - - if !node.ModTime.Equal(fi.ModTime()) || - !node.ChangeTime.Equal(changeTime(extendedStat)) || - node.Inode != uint64(inode) || - node.Size != size { - debug.Log("node %v is newer: timestamp, size or inode changed", path) - return true - } - - debug.Log("node %v is not newer", path) - return false -} - func (node *Node) fillUser(stat statT) error { node.UID = stat.uid() node.GID = stat.gid() From f279731168104727b1f47a5f9fcf460590f2792d Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Fri, 30 Mar 2018 22:43:18 +0200 Subject: [PATCH 20/30] Add new archiver code --- internal/archiver/archiver.go | 788 ++++++++++++++ internal/archiver/archiver_test.go | 1569 +++++++++++++++++++++++++++ internal/archiver/blob_saver.go | 158 +++ internal/archiver/buffer.go | 90 ++ internal/archiver/file_saver.go | 228 ++++ internal/archiver/index_uploader.go | 53 + internal/archiver/scanner.go | 112 ++ internal/archiver/scanner_test.go | 333 ++++++ internal/archiver/testing.go | 316 ++++++ internal/archiver/testing_test.go | 525 +++++++++ internal/archiver/tree.go | 254 +++++ internal/archiver/tree_test.go | 341 ++++++ 12 files changed, 4767 insertions(+) create mode 100644 internal/archiver/archiver.go create mode 100644 internal/archiver/archiver_test.go create mode 100644 internal/archiver/blob_saver.go create mode 100644 internal/archiver/buffer.go create mode 100644 internal/archiver/file_saver.go create mode 100644 internal/archiver/index_uploader.go create mode 100644 internal/archiver/scanner.go create mode 100644 internal/archiver/scanner_test.go create mode 100644 internal/archiver/testing_test.go create mode 100644 internal/archiver/tree.go create mode 100644 internal/archiver/tree_test.go diff --git a/internal/archiver/archiver.go b/internal/archiver/archiver.go new file mode 100644 index 000000000..143c81e34 --- /dev/null +++ b/internal/archiver/archiver.go @@ -0,0 +1,788 @@ +package archiver + +import ( + "context" + "encoding/json" + "os" + "path" + "runtime" + "sort" + "syscall" + "time" + + "github.com/restic/restic/internal/debug" + "github.com/restic/restic/internal/errors" + "github.com/restic/restic/internal/fs" + "github.com/restic/restic/internal/restic" +) + +// SelectFunc returns true for all items that should be included (files and +// dirs). If false is returned, files are ignored and dirs are not even walked. +type SelectFunc func(item string, fi os.FileInfo) bool + +// ErrorFunc is called when an error during archiving occurs. When nil is +// returned, the archiver continues, otherwise it aborts and passes the error +// up the call stack. +type ErrorFunc func(file string, fi os.FileInfo, err error) error + +// ItemStats collects some statistics about a particular file or directory. +type ItemStats struct { + DataBlobs int // number of new data blobs added for this item + DataSize uint64 // sum of the sizes of all new data blobs + TreeBlobs int // number of new tree blobs added for this item + TreeSize uint64 // sum of the sizes of all new tree blobs +} + +// Add adds other to the current ItemStats. +func (s *ItemStats) Add(other ItemStats) { + s.DataBlobs += other.DataBlobs + s.DataSize += other.DataSize + s.TreeBlobs += other.TreeBlobs + s.TreeSize += other.TreeSize +} + +// Archiver saves a directory structure to the repo. +type Archiver struct { + Repo restic.Repository + Select SelectFunc + FS fs.FS + Options Options + + blobSaver *BlobSaver + fileSaver *FileSaver + + // Error is called for all errors that occur during backup. + Error ErrorFunc + + // CompleteItem is called for all files and dirs once they have been + // processed successfully. The parameter item contains the path as it will + // be in the snapshot after saving. s contains some statistics about this + // particular file/dir. + // + // CompleteItem may be called asynchronously from several different + // goroutines! + CompleteItem func(item string, previous, current *restic.Node, s ItemStats, d time.Duration) + + // StartFile is called when a file is being processed by a worker. + StartFile func(filename string) + + // CompleteBlob is called for all saved blobs for files. + CompleteBlob func(filename string, bytes uint64) + + // WithAtime configures if the access time for files and directories should + // be saved. Enabling it may result in much metadata, so it's off by + // default. + WithAtime bool +} + +// Options is used to configure the archiver. +type Options struct { + // FileReadConcurrency sets how many files are read in concurrently. If + // it's set to zero, at most two files are read in concurrently (which + // turned out to be a good default for most situations). + FileReadConcurrency uint + + // SaveBlobConcurrency sets how many blobs are hashed and saved + // concurrently. If it's set to zero, the default is the number of CPUs + // available in the system. + SaveBlobConcurrency uint +} + +// ApplyDefaults returns a copy of o with the default options set for all unset +// fields. +func (o Options) ApplyDefaults() Options { + if o.FileReadConcurrency == 0 { + // two is a sweet spot for almost all situations. We've done some + // experiments documented here: + // https://github.com/borgbackup/borg/issues/3500 + o.FileReadConcurrency = 2 + } + + if o.SaveBlobConcurrency == 0 { + o.SaveBlobConcurrency = uint(runtime.NumCPU()) + } + + return o +} + +// New initializes a new archiver. +func New(repo restic.Repository, fs fs.FS, opts Options) *Archiver { + arch := &Archiver{ + Repo: repo, + Select: func(string, os.FileInfo) bool { return true }, + FS: fs, + Options: opts.ApplyDefaults(), + + CompleteItem: func(string, *restic.Node, *restic.Node, ItemStats, time.Duration) {}, + StartFile: func(string) {}, + CompleteBlob: func(string, uint64) {}, + } + + return arch +} + +// Valid returns an error if anything is missing. +func (arch *Archiver) Valid() error { + if arch.blobSaver == nil { + return errors.New("blobSaver is nil") + } + + if arch.fileSaver == nil { + return errors.New("fileSaver is nil") + } + + if arch.Repo == nil { + return errors.New("repo is not set") + } + + if arch.Select == nil { + return errors.New("Select is not set") + } + + if arch.FS == nil { + return errors.New("FS is not set") + } + + return nil +} + +// error calls arch.Error if it is set. +func (arch *Archiver) error(item string, fi os.FileInfo, err error) error { + if arch.Error == nil || err == nil { + return err + } + + errf := arch.Error(item, fi, err) + if err != errf { + debug.Log("item %v: error was filtered by handler, before: %q, after: %v", item, err, errf) + } + return errf +} + +// saveTree stores a tree in the repo. It checks the index and the known blobs +// before saving anything. +func (arch *Archiver) saveTree(ctx context.Context, t *restic.Tree) (restic.ID, ItemStats, error) { + var s ItemStats + buf, err := json.Marshal(t) + if err != nil { + return restic.ID{}, s, errors.Wrap(err, "MarshalJSON") + } + + // append a newline so that the data is always consistent (json.Encoder + // adds a newline after each object) + buf = append(buf, '\n') + + b := Buffer{Data: buf} + res := arch.blobSaver.Save(ctx, restic.TreeBlob, b) + if res.Err() != nil { + return restic.ID{}, s, res.Err() + } + + if !res.Known() { + s.TreeBlobs++ + s.TreeSize += uint64(len(buf)) + } + return res.ID(), s, nil +} + +// nodeFromFileInfo returns the restic node from a os.FileInfo. +func (arch *Archiver) nodeFromFileInfo(filename string, fi os.FileInfo) (*restic.Node, error) { + node, err := restic.NodeFromFileInfo(filename, fi) + if !arch.WithAtime { + node.AccessTime = node.ModTime + } + return node, errors.Wrap(err, "NodeFromFileInfo") +} + +// loadSubtree tries to load the subtree referenced by node. In case of an error, nil is returned. +func (arch *Archiver) loadSubtree(ctx context.Context, node *restic.Node) *restic.Tree { + if node == nil || node.Type != "dir" || node.Subtree == nil { + return nil + } + + tree, err := arch.Repo.LoadTree(ctx, *node.Subtree) + if err != nil { + debug.Log("unable to load tree %v: %v", node.Subtree.Str(), err) + // TODO: handle error + return nil + } + + return tree +} + +// SaveDir stores a directory in the repo and returns the node. snPath is the +// path within the current snapshot. +func (arch *Archiver) SaveDir(ctx context.Context, snPath string, fi os.FileInfo, dir string, previous *restic.Tree) (*restic.Node, ItemStats, error) { + debug.Log("%v %v", snPath, dir) + + var s ItemStats + + treeNode, err := arch.nodeFromFileInfo(dir, fi) + if err != nil { + return nil, s, err + } + + names, err := readdirnames(arch.FS, dir) + if err != nil { + return nil, s, err + } + + var futures []FutureNode + + tree := restic.NewTree() + + for _, name := range names { + pathname := arch.FS.Join(dir, name) + oldNode := previous.Find(name) + snItem := join(snPath, name) + fn, excluded, err := arch.Save(ctx, snItem, pathname, oldNode) + + // return error early if possible + if err != nil { + err = arch.error(pathname, fi, err) + if err == nil { + // ignore error + continue + } + + return nil, s, err + } + + if excluded { + continue + } + + futures = append(futures, fn) + } + + for _, fn := range futures { + fn.wait() + + // return the error if it wasn't ignored + if fn.err != nil { + fn.err = arch.error(fn.target, fn.fi, fn.err) + if fn.err == nil { + // ignore error + continue + } + + return nil, s, fn.err + } + + // when the error is ignored, the node could not be saved, so ignore it + if fn.node == nil { + debug.Log("%v excluded: %v", fn.snPath, fn.target) + continue + } + + err := tree.Insert(fn.node) + if err != nil { + return nil, s, err + } + } + + id, treeStats, err := arch.saveTree(ctx, tree) + if err != nil { + return nil, ItemStats{}, err + } + + s.Add(treeStats) + + treeNode.Subtree = &id + return treeNode, s, nil +} + +// FutureNode holds a reference to a node or a FutureFile. +type FutureNode struct { + snPath, target string + + // kept to call the error callback function + absTarget string + fi os.FileInfo + + node *restic.Node + stats ItemStats + err error + + isFile bool + file FutureFile +} + +func (fn *FutureNode) wait() { + if fn.isFile { + // wait for and collect the data for the file + fn.node = fn.file.Node() + fn.err = fn.file.Err() + fn.stats = fn.file.Stats() + } +} + +// Save saves a target (file or directory) to the repo. If the item is +// excluded,this function returns a nil node and error. +// +// Errors and completion is needs to be handled by the caller. +// +// snPath is the path within the current snapshot. +func (arch *Archiver) Save(ctx context.Context, snPath, target string, previous *restic.Node) (fn FutureNode, excluded bool, err error) { + fn = FutureNode{ + snPath: snPath, + target: target, + } + + debug.Log("%v target %q, previous %v", snPath, target, previous) + abstarget, err := arch.FS.Abs(target) + if err != nil { + return FutureNode{}, false, err + } + + fn.absTarget = abstarget + + var fi os.FileInfo + var errFI error + + file, errOpen := arch.FS.OpenFile(target, fs.O_RDONLY|fs.O_NOFOLLOW, 0) + if errOpen == nil { + fi, errFI = file.Stat() + } + + if !arch.Select(abstarget, fi) { + debug.Log("%v is excluded", target) + if file != nil { + _ = file.Close() + } + return FutureNode{}, true, nil + } + + if errOpen != nil { + debug.Log(" open error %#v", errOpen) + // test if the open failed because target is a symbolic link or a socket + if e, ok := errOpen.(*os.PathError); ok && (e.Err == syscall.ELOOP || e.Err == syscall.ENXIO) { + // in this case, redo the stat and carry on + fi, errFI = arch.FS.Lstat(target) + } else { + return FutureNode{}, false, errors.Wrap(errOpen, "OpenFile") + } + } + + if errFI != nil { + _ = file.Close() + return FutureNode{}, false, errors.Wrap(errFI, "Stat") + } + + switch { + case fs.IsRegularFile(fi): + debug.Log(" %v regular file", target) + start := time.Now() + + // use previous node if the file hasn't changed + if previous != nil && !fileChanged(fi, previous) { + debug.Log("%v hasn't changed, returning old node", target) + arch.CompleteItem(snPath, previous, previous, ItemStats{}, time.Since(start)) + arch.CompleteBlob(snPath, previous.Size) + fn.node = previous + _ = file.Close() + return fn, false, nil + } + + fn.isFile = true + // Save will close the file, we don't need to do that + fn.file = arch.fileSaver.Save(ctx, snPath, file, fi, func() { + arch.StartFile(snPath) + }, func(node *restic.Node, stats ItemStats) { + arch.CompleteItem(snPath, previous, node, stats, time.Since(start)) + }) + + file = nil + + case fi.IsDir(): + debug.Log(" %v dir", target) + + snItem := snPath + "/" + start := time.Now() + oldSubtree := arch.loadSubtree(ctx, previous) + fn.node, fn.stats, err = arch.SaveDir(ctx, snPath, fi, target, oldSubtree) + if err == nil { + arch.CompleteItem(snItem, previous, fn.node, fn.stats, time.Since(start)) + } else { + _ = file.Close() + return FutureNode{}, false, err + } + + case fi.Mode()&os.ModeSocket > 0: + debug.Log(" %v is a socket, ignoring", target) + return FutureNode{}, true, nil + + default: + debug.Log(" %v other", target) + + fn.node, err = arch.nodeFromFileInfo(target, fi) + if err != nil { + _ = file.Close() + return FutureNode{}, false, err + } + } + + if file != nil { + err = file.Close() + if err != nil { + return fn, false, errors.Wrap(err, "Close") + } + } + + return fn, false, nil +} + +// fileChanged returns true if the file's content has changed since the node +// was created. +func fileChanged(fi os.FileInfo, node *restic.Node) bool { + if node == nil { + return true + } + + // check type change + if node.Type != "file" { + return true + } + + // check modification timestamp + if !fi.ModTime().Equal(node.ModTime) { + return true + } + + // check size + extFI := fs.ExtendedStat(fi) + if uint64(fi.Size()) != node.Size || uint64(extFI.Size) != node.Size { + return true + } + + // check inode + if node.Inode != extFI.Inode { + return true + } + + return false +} + +// join returns all elements separated with a forward slash. +func join(elem ...string) string { + return path.Join(elem...) +} + +// statDir returns the file info for the directory. Symbolic links are +// resolved. If the target directory is not a directory, an error is returned. +func (arch *Archiver) statDir(dir string) (os.FileInfo, error) { + fi, err := arch.FS.Stat(dir) + if err != nil { + return nil, errors.Wrap(err, "Lstat") + } + + tpe := fi.Mode() & (os.ModeType | os.ModeCharDevice) + if tpe != os.ModeDir { + return fi, errors.Errorf("path is not a directory: %v", dir) + } + + return fi, nil +} + +// SaveTree stores a Tree in the repo, returned is the tree. snPath is the path +// within the current snapshot. +func (arch *Archiver) SaveTree(ctx context.Context, snPath string, atree *Tree, previous *restic.Tree) (*restic.Tree, error) { + debug.Log("%v (%v nodes), parent %v", snPath, len(atree.Nodes), previous) + + tree := restic.NewTree() + + futureNodes := make(map[string]FutureNode) + + for name, subatree := range atree.Nodes { + + // this is a leaf node + if subatree.Path != "" { + fn, excluded, err := arch.Save(ctx, join(snPath, name), subatree.Path, previous.Find(name)) + + if err != nil { + err = arch.error(subatree.Path, fn.fi, err) + if err == nil { + // ignore error + continue + } + return nil, err + } + + if err != nil { + return nil, err + } + + if !excluded { + futureNodes[name] = fn + } + continue + } + + snItem := join(snPath, name) + "/" + start := time.Now() + + oldNode := previous.Find(name) + oldSubtree := arch.loadSubtree(ctx, oldNode) + + // not a leaf node, archive subtree + subtree, err := arch.SaveTree(ctx, join(snPath, name), &subatree, oldSubtree) + if err != nil { + return nil, err + } + + id, nodeStats, err := arch.saveTree(ctx, subtree) + if err != nil { + return nil, err + } + + if subatree.FileInfoPath == "" { + return nil, errors.Errorf("FileInfoPath for %v/%v is empty", snPath, name) + } + + debug.Log("%v, saved subtree %v as %v", snPath, subtree, id.Str()) + + fi, err := arch.statDir(subatree.FileInfoPath) + if err != nil { + return nil, err + } + + debug.Log("%v, dir node data loaded from %v", snPath, subatree.FileInfoPath) + + node, err := arch.nodeFromFileInfo(subatree.FileInfoPath, fi) + if err != nil { + return nil, err + } + + node.Name = name + node.Subtree = &id + + err = tree.Insert(node) + if err != nil { + return nil, err + } + + arch.CompleteItem(snItem, oldNode, node, nodeStats, time.Since(start)) + } + + // process all futures + for name, fn := range futureNodes { + fn.wait() + + // return the error, or ignore it + if fn.err != nil { + fn.err = arch.error(fn.target, fn.fi, fn.err) + if fn.err == nil { + // ignore error + continue + } + + return nil, fn.err + } + + // when the error is ignored, the node could not be saved, so ignore it + if fn.node == nil { + debug.Log("%v excluded: %v", fn.snPath, fn.target) + continue + } + + fn.node.Name = name + + err := tree.Insert(fn.node) + if err != nil { + return nil, err + } + } + + return tree, nil +} + +type fileInfoSlice []os.FileInfo + +func (fi fileInfoSlice) Len() int { + return len(fi) +} + +func (fi fileInfoSlice) Swap(i, j int) { + fi[i], fi[j] = fi[j], fi[i] +} + +func (fi fileInfoSlice) Less(i, j int) bool { + return fi[i].Name() < fi[j].Name() +} + +func readdir(filesystem fs.FS, dir string) ([]os.FileInfo, error) { + f, err := filesystem.OpenFile(dir, fs.O_RDONLY|fs.O_NOFOLLOW, 0) + if err != nil { + return nil, errors.Wrap(err, "Open") + } + + entries, err := f.Readdir(-1) + if err != nil { + _ = f.Close() + return nil, errors.Wrap(err, "Readdir") + } + + err = f.Close() + if err != nil { + return nil, err + } + + sort.Sort(fileInfoSlice(entries)) + return entries, nil +} + +func readdirnames(filesystem fs.FS, dir string) ([]string, error) { + f, err := filesystem.OpenFile(dir, fs.O_RDONLY|fs.O_NOFOLLOW, 0) + if err != nil { + return nil, errors.Wrap(err, "Open") + } + + entries, err := f.Readdirnames(-1) + if err != nil { + _ = f.Close() + return nil, errors.Wrap(err, "Readdirnames") + } + + err = f.Close() + if err != nil { + return nil, err + } + + sort.Sort(sort.StringSlice(entries)) + return entries, nil +} + +// resolveRelativeTargets replaces targets that only contain relative +// directories ("." or "../../") with the contents of the directory. Each +// element of target is processed with fs.Clean(). +func resolveRelativeTargets(fs fs.FS, targets []string) ([]string, error) { + debug.Log("targets before resolving: %v", targets) + result := make([]string, 0, len(targets)) + for _, target := range targets { + target = fs.Clean(target) + pc, _ := pathComponents(fs, target, false) + if len(pc) > 0 { + result = append(result, target) + continue + } + + debug.Log("replacing %q with readdir(%q)", target, target) + entries, err := readdirnames(fs, target) + if err != nil { + return nil, err + } + + for _, name := range entries { + result = append(result, fs.Join(target, name)) + } + } + + debug.Log("targets after resolving: %v", result) + return result, nil +} + +// SnapshotOptions collect attributes for a new snapshot. +type SnapshotOptions struct { + Tags []string + Hostname string + Excludes []string + Time time.Time + ParentSnapshot restic.ID +} + +// loadParentTree loads a tree referenced by snapshot id. If id is null, nil is returned. +func (arch *Archiver) loadParentTree(ctx context.Context, snapshotID restic.ID) *restic.Tree { + if snapshotID.IsNull() { + return nil + } + + debug.Log("load parent snapshot %v", snapshotID) + sn, err := restic.LoadSnapshot(ctx, arch.Repo, snapshotID) + if err != nil { + debug.Log("unable to load snapshot %v: %v", snapshotID, err) + return nil + } + + if sn.Tree == nil { + debug.Log("snapshot %v has empty tree %v", snapshotID) + return nil + } + + debug.Log("load parent tree %v", *sn.Tree) + tree, err := arch.Repo.LoadTree(ctx, *sn.Tree) + if err != nil { + debug.Log("unable to load tree %v: %v", *sn.Tree, err) + return nil + } + return tree +} + +// runWorkers starts the worker pools, which are stopped when the context is cancelled. +func (arch *Archiver) runWorkers(ctx context.Context) { + arch.blobSaver = NewBlobSaver(ctx, arch.Repo, arch.Options.SaveBlobConcurrency) + arch.fileSaver = NewFileSaver(ctx, arch.FS, arch.blobSaver, arch.Repo.Config().ChunkerPolynomial, arch.Options.FileReadConcurrency) + arch.fileSaver.CompleteBlob = arch.CompleteBlob + + arch.fileSaver.NodeFromFileInfo = arch.nodeFromFileInfo +} + +// Snapshot saves several targets and returns a snapshot. +func (arch *Archiver) Snapshot(ctx context.Context, targets []string, opts SnapshotOptions) (*restic.Snapshot, restic.ID, error) { + workerCtx, cancel := context.WithCancel(ctx) + defer cancel() + + arch.runWorkers(workerCtx) + + err := arch.Valid() + if err != nil { + return nil, restic.ID{}, err + } + + cleanTargets, err := resolveRelativeTargets(arch.FS, targets) + if err != nil { + return nil, restic.ID{}, err + } + + atree, err := NewTree(arch.FS, cleanTargets) + if err != nil { + return nil, restic.ID{}, err + } + + start := time.Now() + tree, err := arch.SaveTree(ctx, "/", atree, arch.loadParentTree(ctx, opts.ParentSnapshot)) + if err != nil { + return nil, restic.ID{}, err + } + + rootTreeID, stats, err := arch.saveTree(ctx, tree) + if err != nil { + return nil, restic.ID{}, err + } + + arch.CompleteItem("/", nil, nil, stats, time.Since(start)) + + err = arch.Repo.Flush(ctx) + if err != nil { + return nil, restic.ID{}, err + } + + err = arch.Repo.SaveIndex(ctx) + if err != nil { + return nil, restic.ID{}, err + } + + sn, err := restic.NewSnapshot(targets, opts.Tags, opts.Hostname, opts.Time) + sn.Excludes = opts.Excludes + if !opts.ParentSnapshot.IsNull() { + id := opts.ParentSnapshot + sn.Parent = &id + } + sn.Tree = &rootTreeID + + id, err := arch.Repo.SaveJSONUnpacked(ctx, restic.SnapshotFile, sn) + if err != nil { + return nil, restic.ID{}, err + } + + return sn, id, nil +} diff --git a/internal/archiver/archiver_test.go b/internal/archiver/archiver_test.go new file mode 100644 index 000000000..3f4daf5ec --- /dev/null +++ b/internal/archiver/archiver_test.go @@ -0,0 +1,1569 @@ +package archiver + +import ( + "context" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "runtime" + "strings" + "sync" + "syscall" + "testing" + "time" + + "github.com/restic/restic/internal/checker" + "github.com/restic/restic/internal/fs" + "github.com/restic/restic/internal/repository" + "github.com/restic/restic/internal/restic" + restictest "github.com/restic/restic/internal/test" +) + +func prepareTempdirRepoSrc(t testing.TB, src TestDir) (tempdir string, repo restic.Repository, cleanup func()) { + tempdir, removeTempdir := restictest.TempDir(t) + repo, removeRepository := repository.TestRepository(t) + + TestCreateFiles(t, tempdir, src) + + cleanup = func() { + removeRepository() + removeTempdir() + } + + return tempdir, repo, cleanup +} + +func saveFile(t testing.TB, repo restic.Repository, filename string, filesystem fs.FS) (*restic.Node, ItemStats) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + arch := New(repo, filesystem, Options{}) + arch.runWorkers(ctx) + + var ( + completeCallbackNode *restic.Node + completeCallbackStats ItemStats + completeCallback bool + + startCallback bool + ) + + complete := func(node *restic.Node, stats ItemStats) { + completeCallback = true + completeCallbackNode = node + completeCallbackStats = stats + } + + start := func() { + startCallback = true + } + + file, err := arch.FS.OpenFile(filename, fs.O_RDONLY|fs.O_NOFOLLOW, 0) + if err != nil { + t.Fatal(err) + } + + fi, err := file.Stat() + if err != nil { + t.Fatal(err) + } + + res := arch.fileSaver.Save(ctx, "/", file, fi, start, complete) + if res.Err() != nil { + t.Fatal(res.Err()) + } + + err = repo.Flush(ctx) + if err != nil { + t.Fatal(err) + } + + err = repo.SaveIndex(ctx) + if err != nil { + t.Fatal(err) + } + + if !startCallback { + t.Errorf("start callback did not happen") + } + + if !completeCallback { + t.Errorf("complete callback did not happen") + } + + if completeCallbackNode == nil { + t.Errorf("no node returned for complete callback") + } + + if completeCallbackNode != nil && !res.Node().Equals(*completeCallbackNode) { + t.Errorf("different node returned for complete callback") + } + + if completeCallbackStats != res.Stats() { + t.Errorf("different stats return for complete callback, want:\n %v\ngot:\n %v", res.Stats(), completeCallbackStats) + } + + return res.Node(), res.Stats() +} + +func TestArchiverSaveFile(t *testing.T) { + var tests = []TestFile{ + TestFile{Content: ""}, + TestFile{Content: "foo"}, + TestFile{Content: string(restictest.Random(23, 12*1024*1024+1287898))}, + } + + for _, testfile := range tests { + t.Run("", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + tempdir, repo, cleanup := prepareTempdirRepoSrc(t, TestDir{"file": testfile}) + defer cleanup() + + node, stats := saveFile(t, repo, filepath.Join(tempdir, "file"), fs.Track{fs.Local{}}) + + TestEnsureFileContent(ctx, t, repo, "file", node, testfile) + if stats.DataSize != uint64(len(testfile.Content)) { + t.Errorf("wrong stats returned in DataSize, want %d, got %d", len(testfile.Content), stats.DataSize) + } + if stats.DataBlobs <= 0 && len(testfile.Content) > 0 { + t.Errorf("wrong stats returned in DataBlobs, want > 0, got %d", stats.DataBlobs) + } + if stats.TreeSize != 0 { + t.Errorf("wrong stats returned in TreeSize, want 0, got %d", stats.TreeSize) + } + if stats.TreeBlobs != 0 { + t.Errorf("wrong stats returned in DataBlobs, want 0, got %d", stats.DataBlobs) + } + }) + } +} + +func TestArchiverSaveFileReaderFS(t *testing.T) { + var tests = []struct { + Data string + }{ + {Data: ""}, + {Data: "foo"}, + {Data: string(restictest.Random(23, 12*1024*1024+1287898))}, + } + + for _, test := range tests { + t.Run("", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + repo, cleanup := repository.TestRepository(t) + defer cleanup() + + ts := time.Now() + filename := "xx" + readerFs := &fs.Reader{ + ModTime: ts, + Mode: 0123, + Name: filename, + ReadCloser: ioutil.NopCloser(strings.NewReader(test.Data)), + } + + node, stats := saveFile(t, repo, filename, readerFs) + + TestEnsureFileContent(ctx, t, repo, "file", node, TestFile{Content: test.Data}) + if stats.DataSize != uint64(len(test.Data)) { + t.Errorf("wrong stats returned in DataSize, want %d, got %d", len(test.Data), stats.DataSize) + } + if stats.DataBlobs <= 0 && len(test.Data) > 0 { + t.Errorf("wrong stats returned in DataBlobs, want > 0, got %d", stats.DataBlobs) + } + if stats.TreeSize != 0 { + t.Errorf("wrong stats returned in TreeSize, want 0, got %d", stats.TreeSize) + } + if stats.TreeBlobs != 0 { + t.Errorf("wrong stats returned in DataBlobs, want 0, got %d", stats.DataBlobs) + } + }) + } +} + +func BenchmarkArchiverSaveFileSmall(b *testing.B) { + const fileSize = 4 * 1024 + d := TestDir{"file": TestFile{ + Content: string(restictest.Random(23, fileSize)), + }} + + b.SetBytes(fileSize) + + for i := 0; i < b.N; i++ { + b.StopTimer() + tempdir, repo, cleanup := prepareTempdirRepoSrc(b, d) + b.StartTimer() + + _, stats := saveFile(b, repo, filepath.Join(tempdir, "file"), fs.Track{fs.Local{}}) + + b.StopTimer() + if stats.DataSize != fileSize { + b.Errorf("wrong stats returned in DataSize, want %d, got %d", fileSize, stats.DataSize) + } + if stats.DataBlobs <= 0 { + b.Errorf("wrong stats returned in DataBlobs, want > 0, got %d", stats.DataBlobs) + } + if stats.TreeSize != 0 { + b.Errorf("wrong stats returned in TreeSize, want 0, got %d", stats.TreeSize) + } + if stats.TreeBlobs != 0 { + b.Errorf("wrong stats returned in DataBlobs, want 0, got %d", stats.DataBlobs) + } + cleanup() + b.StartTimer() + } +} + +func BenchmarkArchiverSaveFileLarge(b *testing.B) { + const fileSize = 40*1024*1024 + 1287898 + d := TestDir{"file": TestFile{ + Content: string(restictest.Random(23, fileSize)), + }} + + b.SetBytes(fileSize) + + for i := 0; i < b.N; i++ { + b.StopTimer() + tempdir, repo, cleanup := prepareTempdirRepoSrc(b, d) + b.StartTimer() + + _, stats := saveFile(b, repo, filepath.Join(tempdir, "file"), fs.Track{fs.Local{}}) + + b.StopTimer() + if stats.DataSize != fileSize { + b.Errorf("wrong stats returned in DataSize, want %d, got %d", fileSize, stats.DataSize) + } + if stats.DataBlobs <= 0 { + b.Errorf("wrong stats returned in DataBlobs, want > 0, got %d", stats.DataBlobs) + } + if stats.TreeSize != 0 { + b.Errorf("wrong stats returned in TreeSize, want 0, got %d", stats.TreeSize) + } + if stats.TreeBlobs != 0 { + b.Errorf("wrong stats returned in DataBlobs, want 0, got %d", stats.DataBlobs) + } + cleanup() + b.StartTimer() + } +} + +type blobCountingRepo struct { + restic.Repository + + m sync.Mutex + saved map[restic.BlobHandle]uint +} + +func (repo *blobCountingRepo) SaveBlob(ctx context.Context, t restic.BlobType, buf []byte, id restic.ID) (restic.ID, error) { + id, err := repo.Repository.SaveBlob(ctx, t, buf, id) + h := restic.BlobHandle{ID: id, Type: t} + repo.m.Lock() + repo.saved[h]++ + repo.m.Unlock() + return id, err +} + +func (repo *blobCountingRepo) SaveTree(ctx context.Context, t *restic.Tree) (restic.ID, error) { + id, err := repo.Repository.SaveTree(ctx, t) + h := restic.BlobHandle{ID: id, Type: restic.TreeBlob} + repo.m.Lock() + repo.saved[h]++ + repo.m.Unlock() + fmt.Printf("savetree %v", h) + return id, err +} + +func appendToFile(t testing.TB, filename string, data []byte) { + f, err := os.OpenFile(filename, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + t.Fatal(err) + } + + _, err = f.Write(data) + if err != nil { + _ = f.Close() + t.Fatal(err) + } + + err = f.Close() + if err != nil { + t.Fatal(err) + } +} + +func TestArchiverSaveFileIncremental(t *testing.T) { + tempdir, removeTempdir := restictest.TempDir(t) + defer removeTempdir() + + testRepo, removeRepository := repository.TestRepository(t) + defer removeRepository() + + repo := &blobCountingRepo{ + Repository: testRepo, + saved: make(map[restic.BlobHandle]uint), + } + + data := restictest.Random(23, 512*1024+887898) + testfile := filepath.Join(tempdir, "testfile") + + for i := 0; i < 3; i++ { + appendToFile(t, testfile, data) + node, _ := saveFile(t, repo, testfile, fs.Track{fs.Local{}}) + + t.Logf("node blobs: %v", node.Content) + + for h, n := range repo.saved { + if n > 1 { + t.Errorf("iteration %v: blob %v saved more than once (%d times)", i, h, n) + } + } + } +} + +func save(t testing.TB, filename string, data []byte) { + f, err := os.Create(filename) + if err != nil { + t.Fatal(err) + } + + _, err = f.Write(data) + if err != nil { + t.Fatal(err) + } + + err = f.Sync() + if err != nil { + t.Fatal(err) + } + + err = f.Close() + if err != nil { + t.Fatal(err) + } +} + +func lstat(t testing.TB, name string) os.FileInfo { + fi, err := os.Lstat(name) + if err != nil { + t.Fatal(err) + } + + return fi +} + +func setTimestamp(t testing.TB, filename string, atime, mtime time.Time) { + var utimes = [...]syscall.Timespec{ + syscall.NsecToTimespec(atime.UnixNano()), + syscall.NsecToTimespec(mtime.UnixNano()), + } + + err := syscall.UtimesNano(filename, utimes[:]) + if err != nil { + t.Fatal(err) + } +} + +func remove(t testing.TB, filename string) { + err := os.Remove(filename) + if err != nil { + t.Fatal(err) + } +} + +func nodeFromFI(t testing.TB, filename string, fi os.FileInfo) *restic.Node { + node, err := restic.NodeFromFileInfo(filename, fi) + if err != nil { + t.Fatal(err) + } + + return node +} + +func TestFileChanged(t *testing.T) { + var defaultContent = []byte("foobar") + + var d = 50 * time.Millisecond + if runtime.GOOS == "darwin" { + // on older darwin instances the file system only supports one second + // granularity + d = time.Second + } + + sleep := func() { + time.Sleep(d) + } + + var tests = []struct { + Name string + Content []byte + Modify func(t testing.TB, filename string) + }{ + { + Name: "same-content-new-file", + Modify: func(t testing.TB, filename string) { + remove(t, filename) + sleep() + save(t, filename, defaultContent) + }, + }, + { + Name: "same-content-new-timestamp", + Modify: func(t testing.TB, filename string) { + sleep() + save(t, filename, defaultContent) + }, + }, + { + Name: "other-content", + Modify: func(t testing.TB, filename string) { + remove(t, filename) + sleep() + save(t, filename, []byte("xxxxxx")) + }, + }, + { + Name: "longer-content", + Modify: func(t testing.TB, filename string) { + save(t, filename, []byte("xxxxxxxxxxxxxxxxxxxxxx")) + }, + }, + { + Name: "new-file", + Modify: func(t testing.TB, filename string) { + remove(t, filename) + sleep() + save(t, filename, defaultContent) + }, + }, + } + + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + tempdir, cleanup := restictest.TempDir(t) + defer cleanup() + + filename := filepath.Join(tempdir, "file") + content := defaultContent + if test.Content != nil { + content = test.Content + } + save(t, filename, content) + + fiBefore := lstat(t, filename) + node := nodeFromFI(t, filename, fiBefore) + + if fileChanged(fiBefore, node) { + t.Fatalf("unchanged file detected as changed") + } + + test.Modify(t, filename) + + fiAfter := lstat(t, filename) + if !fileChanged(fiAfter, node) { + t.Fatalf("modified file detected as unchanged") + } + }) + } +} + +func TestFilChangedSpecialCases(t *testing.T) { + tempdir, cleanup := restictest.TempDir(t) + defer cleanup() + + filename := filepath.Join(tempdir, "file") + content := []byte("foobar") + save(t, filename, content) + + t.Run("nil-node", func(t *testing.T) { + fi := lstat(t, filename) + if !fileChanged(fi, nil) { + t.Fatal("nil node detected as unchanged") + } + }) + + t.Run("type-change", func(t *testing.T) { + fi := lstat(t, filename) + node := nodeFromFI(t, filename, fi) + node.Type = "symlink" + if !fileChanged(fi, node) { + t.Fatal("node with changed type detected as unchanged") + } + }) +} + +func TestArchiverSaveDir(t *testing.T) { + const targetNodeName = "targetdir" + + var tests = []struct { + src TestDir + chdir string + target string + want TestDir + }{ + { + src: TestDir{ + "targetfile": TestFile{Content: string(restictest.Random(888, 2*1024*1024+5000))}, + }, + target: ".", + want: TestDir{ + "targetdir": TestDir{ + "targetfile": TestFile{Content: string(restictest.Random(888, 2*1024*1024+5000))}, + }, + }, + }, + { + src: TestDir{ + "targetdir": TestDir{ + "foo": TestFile{Content: "foo"}, + "emptyfile": TestFile{Content: ""}, + "bar": TestFile{Content: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"}, + "largefile": TestFile{Content: string(restictest.Random(888, 2*1024*1024+5000))}, + "largerfile": TestFile{Content: string(restictest.Random(234, 5*1024*1024+5000))}, + }, + }, + target: "targetdir", + }, + { + src: TestDir{ + "foo": TestFile{Content: "foo"}, + "emptyfile": TestFile{Content: ""}, + "bar": TestFile{Content: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"}, + }, + target: ".", + want: TestDir{ + "targetdir": TestDir{ + "foo": TestFile{Content: "foo"}, + "emptyfile": TestFile{Content: ""}, + "bar": TestFile{Content: "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"}, + }, + }, + }, + { + src: TestDir{ + "foo": TestDir{ + "subdir": TestDir{ + "x": TestFile{Content: "xxx"}, + "y": TestFile{Content: "yyyyyyyyyyyyyyyy"}, + "z": TestFile{Content: "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"}, + }, + "file": TestFile{Content: "just a test"}, + }, + }, + chdir: "foo/subdir", + target: "../../", + want: TestDir{ + "targetdir": TestDir{ + "foo": TestDir{ + "subdir": TestDir{ + "x": TestFile{Content: "xxx"}, + "y": TestFile{Content: "yyyyyyyyyyyyyyyy"}, + "z": TestFile{Content: "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"}, + }, + "file": TestFile{Content: "just a test"}, + }, + }, + }, + }, + { + src: TestDir{ + "foo": TestDir{ + "file": TestFile{Content: "just a test"}, + "file2": TestFile{Content: "again"}, + }, + }, + target: "./foo", + want: TestDir{ + "targetdir": TestDir{ + "file": TestFile{Content: "just a test"}, + "file2": TestFile{Content: "again"}, + }, + }, + }, + } + + for _, test := range tests { + t.Run("", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + tempdir, repo, cleanup := prepareTempdirRepoSrc(t, test.src) + defer cleanup() + + arch := New(repo, fs.Track{fs.Local{}}, Options{}) + arch.runWorkers(ctx) + + chdir := tempdir + if test.chdir != "" { + chdir = filepath.Join(chdir, test.chdir) + } + + back := fs.TestChdir(t, chdir) + defer back() + + fi, err := fs.Lstat(test.target) + if err != nil { + t.Fatal(err) + } + + node, stats, err := arch.SaveDir(ctx, "/", fi, test.target, nil) + if err != nil { + t.Fatal(err) + } + + t.Logf("stats: %v", stats) + if stats.DataSize != 0 { + t.Errorf("wrong stats returned in DataSize, want 0, got %d", stats.DataSize) + } + if stats.DataBlobs != 0 { + t.Errorf("wrong stats returned in DataBlobs, want 0, got %d", stats.DataBlobs) + } + if stats.TreeSize <= 0 { + t.Errorf("wrong stats returned in TreeSize, want > 0, got %d", stats.TreeSize) + } + if stats.TreeBlobs <= 0 { + t.Errorf("wrong stats returned in TreeBlobs, want > 0, got %d", stats.TreeBlobs) + } + + node.Name = targetNodeName + tree := &restic.Tree{Nodes: []*restic.Node{node}} + treeID, err := repo.SaveTree(ctx, tree) + if err != nil { + t.Fatal(err) + } + + err = repo.Flush(ctx) + if err != nil { + t.Fatal(err) + } + + err = repo.SaveIndex(ctx) + if err != nil { + t.Fatal(err) + } + + want := test.want + if want == nil { + want = test.src + } + TestEnsureTree(ctx, t, "/", repo, treeID, want) + }) + } +} + +func TestArchiverSaveDirIncremental(t *testing.T) { + tempdir, removeTempdir := restictest.TempDir(t) + defer removeTempdir() + + testRepo, removeRepository := repository.TestRepository(t) + defer removeRepository() + + repo := &blobCountingRepo{ + Repository: testRepo, + saved: make(map[restic.BlobHandle]uint), + } + + appendToFile(t, filepath.Join(tempdir, "testfile"), []byte("foobar")) + + // save the empty directory several times in a row, then have a look if the + // archiver did save the same tree several times + for i := 0; i < 5; i++ { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + arch := New(repo, fs.Track{fs.Local{}}, Options{}) + arch.runWorkers(ctx) + + fi, err := fs.Lstat(tempdir) + if err != nil { + t.Fatal(err) + } + + node, stats, err := arch.SaveDir(ctx, "/", fi, tempdir, nil) + if err != nil { + t.Fatal(err) + } + + if i == 0 { + // operation must have added new tree data + if stats.DataSize != 0 { + t.Errorf("wrong stats returned in DataSize, want 0, got %d", stats.DataSize) + } + if stats.DataBlobs != 0 { + t.Errorf("wrong stats returned in DataBlobs, want 0, got %d", stats.DataBlobs) + } + if stats.TreeSize <= 0 { + t.Errorf("wrong stats returned in TreeSize, want > 0, got %d", stats.TreeSize) + } + if stats.TreeBlobs <= 0 { + t.Errorf("wrong stats returned in TreeBlobs, want > 0, got %d", stats.TreeBlobs) + } + } else { + // operation must not have added any new data + if stats.DataSize != 0 { + t.Errorf("wrong stats returned in DataSize, want 0, got %d", stats.DataSize) + } + if stats.DataBlobs != 0 { + t.Errorf("wrong stats returned in DataBlobs, want 0, got %d", stats.DataBlobs) + } + if stats.TreeSize != 0 { + t.Errorf("wrong stats returned in TreeSize, want 0, got %d", stats.TreeSize) + } + if stats.TreeBlobs != 0 { + t.Errorf("wrong stats returned in TreeBlobs, want 0, got %d", stats.TreeBlobs) + } + } + + t.Logf("node subtree %v", node.Subtree) + + err = repo.Flush(ctx) + if err != nil { + t.Fatal(err) + } + + err = repo.SaveIndex(ctx) + if err != nil { + t.Fatal(err) + } + + for h, n := range repo.saved { + if n > 1 { + t.Errorf("iteration %v: blob %v saved more than once (%d times)", i, h, n) + } + } + } +} + +func TestArchiverSaveTree(t *testing.T) { + symlink := func(from, to string) func(t testing.TB) { + return func(t testing.TB) { + err := os.Symlink(from, to) + if err != nil { + t.Fatal(err) + } + } + } + + var tests = []struct { + src TestDir + prepare func(t testing.TB) + targets []string + want TestDir + }{ + { + src: TestDir{ + "targetfile": TestFile{Content: string("foobar")}, + }, + targets: []string{"targetfile"}, + want: TestDir{ + "targetfile": TestFile{Content: string("foobar")}, + }, + }, + { + src: TestDir{ + "targetfile": TestFile{Content: string("foobar")}, + }, + prepare: symlink("targetfile", "filesymlink"), + targets: []string{"targetfile", "filesymlink"}, + want: TestDir{ + "targetfile": TestFile{Content: string("foobar")}, + "filesymlink": TestSymlink{Target: "targetfile"}, + }, + }, + { + src: TestDir{ + "dir": TestDir{ + "subdir": TestDir{ + "subsubdir": TestDir{ + "targetfile": TestFile{Content: string("foobar")}, + }, + }, + "otherfile": TestFile{Content: string("xxx")}, + }, + }, + prepare: symlink("subdir", filepath.FromSlash("dir/symlink")), + targets: []string{filepath.FromSlash("dir/symlink")}, + want: TestDir{ + "dir": TestDir{ + "symlink": TestSymlink{Target: "subdir"}, + }, + }, + }, + { + src: TestDir{ + "dir": TestDir{ + "subdir": TestDir{ + "subsubdir": TestDir{ + "targetfile": TestFile{Content: string("foobar")}, + }, + }, + "otherfile": TestFile{Content: string("xxx")}, + }, + }, + prepare: symlink("subdir", filepath.FromSlash("dir/symlink")), + targets: []string{filepath.FromSlash("dir/symlink/subsubdir")}, + want: TestDir{ + "dir": TestDir{ + "symlink": TestDir{ + "subsubdir": TestDir{ + "targetfile": TestFile{Content: string("foobar")}, + }, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run("", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + tempdir, repo, cleanup := prepareTempdirRepoSrc(t, test.src) + defer cleanup() + + testFS := fs.Track{fs.Local{}} + + arch := New(repo, testFS, Options{}) + arch.runWorkers(ctx) + + back := fs.TestChdir(t, tempdir) + defer back() + + if test.prepare != nil { + test.prepare(t) + } + + atree, err := NewTree(testFS, test.targets) + if err != nil { + t.Fatal(err) + } + + tree, err := arch.SaveTree(ctx, "/", atree, nil) + if err != nil { + t.Fatal(err) + } + + treeID, err := repo.SaveTree(ctx, tree) + if err != nil { + t.Fatal(err) + } + + err = repo.Flush(ctx) + if err != nil { + t.Fatal(err) + } + + err = repo.SaveIndex(ctx) + if err != nil { + t.Fatal(err) + } + + want := test.want + if want == nil { + want = test.src + } + TestEnsureTree(ctx, t, "/", repo, treeID, want) + }) + } +} + +func TestArchiverSnapshot(t *testing.T) { + var tests = []struct { + name string + src TestDir + want TestDir + chdir string + targets []string + }{ + { + name: "single-file", + src: TestDir{ + "foo": TestFile{Content: "foo"}, + }, + targets: []string{"foo"}, + }, + { + name: "file-current-dir", + src: TestDir{ + "foo": TestFile{Content: "foo"}, + }, + targets: []string{"./foo"}, + }, + { + name: "dir", + src: TestDir{ + "target": TestDir{ + "foo": TestFile{Content: "foo"}, + }, + }, + targets: []string{"target"}, + }, + { + name: "dir-current-dir", + src: TestDir{ + "target": TestDir{ + "foo": TestFile{Content: "foo"}, + }, + }, + targets: []string{"./target"}, + }, + { + name: "content-dir-current-dir", + src: TestDir{ + "target": TestDir{ + "foo": TestFile{Content: "foo"}, + }, + }, + targets: []string{"./target/."}, + }, + { + name: "current-dir", + src: TestDir{ + "target": TestDir{ + "foo": TestFile{Content: "foo"}, + }, + }, + targets: []string{"."}, + }, + { + name: "subdir", + src: TestDir{ + "subdir": TestDir{ + "foo": TestFile{Content: "foo"}, + "subsubdir": TestDir{ + "foo": TestFile{Content: "foo in subsubdir"}, + }, + }, + "other": TestFile{Content: "another file"}, + }, + targets: []string{"subdir"}, + want: TestDir{ + "subdir": TestDir{ + "foo": TestFile{Content: "foo"}, + "subsubdir": TestDir{ + "foo": TestFile{Content: "foo in subsubdir"}, + }, + }, + }, + }, + { + name: "subsubdir", + src: TestDir{ + "subdir": TestDir{ + "foo": TestFile{Content: "foo"}, + "subsubdir": TestDir{ + "foo": TestFile{Content: "foo in subsubdir"}, + }, + }, + "other": TestFile{Content: "another file"}, + }, + targets: []string{"subdir/subsubdir"}, + want: TestDir{ + "subdir": TestDir{ + "subsubdir": TestDir{ + "foo": TestFile{Content: "foo in subsubdir"}, + }, + }, + }, + }, + { + name: "parent-dir", + src: TestDir{ + "subdir": TestDir{ + "foo": TestFile{Content: "foo"}, + }, + "other": TestFile{Content: "another file"}, + }, + chdir: "subdir", + targets: []string{".."}, + }, + { + name: "parent-parent-dir", + src: TestDir{ + "subdir": TestDir{ + "foo": TestFile{Content: "foo"}, + "subsubdir": TestDir{ + "empty": TestFile{Content: ""}, + }, + }, + "other": TestFile{Content: "another file"}, + }, + chdir: "subdir/subsubdir", + targets: []string{"../.."}, + }, + { + name: "parent-parent-dir-slash", + src: TestDir{ + "subdir": TestDir{ + "subsubdir": TestDir{ + "foo": TestFile{Content: "foo"}, + }, + }, + "other": TestFile{Content: "another file"}, + }, + chdir: "subdir/subsubdir", + targets: []string{"../../"}, + want: TestDir{ + "subdir": TestDir{ + "subsubdir": TestDir{ + "foo": TestFile{Content: "foo"}, + }, + }, + "other": TestFile{Content: "another file"}, + }, + }, + { + name: "parent-subdir", + src: TestDir{ + "subdir": TestDir{ + "foo": TestFile{Content: "foo"}, + }, + "other": TestFile{Content: "another file"}, + }, + chdir: "subdir", + targets: []string{"../subdir"}, + want: TestDir{ + "subdir": TestDir{ + "foo": TestFile{Content: "foo"}, + }, + }, + }, + { + name: "parent-parent-dir-subdir", + src: TestDir{ + "subdir": TestDir{ + "subsubdir": TestDir{ + "foo": TestFile{Content: "foo"}, + }, + }, + "other": TestFile{Content: "another file"}, + }, + chdir: "subdir/subsubdir", + targets: []string{"../../subdir/subsubdir"}, + want: TestDir{ + "subdir": TestDir{ + "subsubdir": TestDir{ + "foo": TestFile{Content: "foo"}, + }, + }, + }, + }, + { + name: "included-multiple1", + src: TestDir{ + "subdir": TestDir{ + "subsubdir": TestDir{ + "foo": TestFile{Content: "foo"}, + }, + "other": TestFile{Content: "another file"}, + }, + }, + targets: []string{"subdir", "subdir/subsubdir"}, + }, + { + name: "included-multiple2", + src: TestDir{ + "subdir": TestDir{ + "subsubdir": TestDir{ + "foo": TestFile{Content: "foo"}, + }, + "other": TestFile{Content: "another file"}, + }, + }, + targets: []string{"subdir/subsubdir", "subdir"}, + }, + { + name: "collision", + src: TestDir{ + "subdir": TestDir{ + "foo": TestFile{Content: "foo in subdir"}, + "subsubdir": TestDir{ + "foo": TestFile{Content: "foo in subsubdir"}, + }, + }, + "foo": TestFile{Content: "another file"}, + }, + chdir: "subdir", + targets: []string{".", "../foo"}, + want: TestDir{ + + "foo": TestFile{Content: "foo in subdir"}, + "subsubdir": TestDir{ + "foo": TestFile{Content: "foo in subsubdir"}, + }, + "foo-1": TestFile{Content: "another file"}, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + tempdir, repo, cleanup := prepareTempdirRepoSrc(t, test.src) + defer cleanup() + + arch := New(repo, fs.Track{fs.Local{}}, Options{}) + + chdir := tempdir + if test.chdir != "" { + chdir = filepath.Join(chdir, filepath.FromSlash(test.chdir)) + } + + back := fs.TestChdir(t, chdir) + defer back() + + var targets []string + for _, target := range test.targets { + targets = append(targets, os.ExpandEnv(target)) + } + + t.Logf("targets: %v", targets) + sn, snapshotID, err := arch.Snapshot(ctx, targets, SnapshotOptions{Time: time.Now()}) + if err != nil { + t.Fatal(err) + } + + t.Logf("saved as %v", snapshotID.Str()) + + want := test.want + if want == nil { + want = test.src + } + TestEnsureSnapshot(t, repo, snapshotID, want) + + checker.TestCheckRepo(t, repo) + + // check that the snapshot contains the targets with absolute paths + for i, target := range sn.Paths { + atarget, err := filepath.Abs(test.targets[i]) + if err != nil { + t.Fatal(err) + } + + if target != atarget { + t.Errorf("wrong path in snapshot: want %v, got %v", atarget, target) + } + } + }) + } +} + +func TestArchiverSnapshotSelect(t *testing.T) { + var tests = []struct { + name string + src TestDir + want TestDir + selFn SelectFunc + }{ + { + name: "include-all", + src: TestDir{ + "work": TestDir{ + "foo": TestFile{Content: "foo"}, + "foo.txt": TestFile{Content: "foo text file"}, + "subdir": TestDir{ + "other": TestFile{Content: "other in subdir"}, + "bar.txt": TestFile{Content: "bar.txt in subdir"}, + }, + }, + "other": TestFile{Content: "another file"}, + }, + selFn: func(item string, fi os.FileInfo) bool { + return true + }, + }, + { + name: "exclude-all", + src: TestDir{ + "work": TestDir{ + "foo": TestFile{Content: "foo"}, + "foo.txt": TestFile{Content: "foo text file"}, + "subdir": TestDir{ + "other": TestFile{Content: "other in subdir"}, + "bar.txt": TestFile{Content: "bar.txt in subdir"}, + }, + }, + "other": TestFile{Content: "another file"}, + }, + selFn: func(item string, fi os.FileInfo) bool { + return false + }, + want: TestDir{}, + }, + { + name: "exclude-txt-files", + src: TestDir{ + "work": TestDir{ + "foo": TestFile{Content: "foo"}, + "foo.txt": TestFile{Content: "foo text file"}, + "subdir": TestDir{ + "other": TestFile{Content: "other in subdir"}, + "bar.txt": TestFile{Content: "bar.txt in subdir"}, + }, + }, + "other": TestFile{Content: "another file"}, + }, + want: TestDir{ + "work": TestDir{ + "foo": TestFile{Content: "foo"}, + "subdir": TestDir{ + "other": TestFile{Content: "other in subdir"}, + }, + }, + "other": TestFile{Content: "another file"}, + }, + selFn: func(item string, fi os.FileInfo) bool { + if filepath.Ext(item) == ".txt" { + return false + } + return true + }, + }, + { + name: "exclude-dir", + src: TestDir{ + "work": TestDir{ + "foo": TestFile{Content: "foo"}, + "foo.txt": TestFile{Content: "foo text file"}, + "subdir": TestDir{ + "other": TestFile{Content: "other in subdir"}, + "bar.txt": TestFile{Content: "bar.txt in subdir"}, + }, + }, + "other": TestFile{Content: "another file"}, + }, + want: TestDir{ + "work": TestDir{ + "foo": TestFile{Content: "foo"}, + "foo.txt": TestFile{Content: "foo text file"}, + }, + "other": TestFile{Content: "another file"}, + }, + selFn: func(item string, fi os.FileInfo) bool { + if filepath.Base(item) == "subdir" { + return false + } + return true + }, + }, + { + name: "select-absolute-paths", + src: TestDir{ + "foo": TestFile{Content: "foo"}, + }, + selFn: func(item string, fi os.FileInfo) bool { + return filepath.IsAbs(item) + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + tempdir, repo, cleanup := prepareTempdirRepoSrc(t, test.src) + defer cleanup() + + arch := New(repo, fs.Track{fs.Local{}}, Options{}) + arch.Select = test.selFn + + back := fs.TestChdir(t, tempdir) + defer back() + + targets := []string{"."} + _, snapshotID, err := arch.Snapshot(ctx, targets, SnapshotOptions{Time: time.Now()}) + if err != nil { + t.Fatal(err) + } + + t.Logf("saved as %v", snapshotID.Str()) + + want := test.want + if want == nil { + want = test.src + } + TestEnsureSnapshot(t, repo, snapshotID, want) + + checker.TestCheckRepo(t, repo) + }) + } +} + +// MockFS keeps track which files are read. +type MockFS struct { + fs.FS + + m sync.Mutex + bytesRead map[string]int // tracks bytes read from all opened files +} + +func (m *MockFS) Open(name string) (fs.File, error) { + f, err := m.FS.Open(name) + if err != nil { + return f, err + } + + return MockFile{File: f, fs: m, filename: name}, nil +} + +func (m *MockFS) OpenFile(name string, flag int, perm os.FileMode) (fs.File, error) { + f, err := m.FS.OpenFile(name, flag, perm) + if err != nil { + return f, err + } + + return MockFile{File: f, fs: m, filename: name}, nil +} + +type MockFile struct { + fs.File + filename string + + fs *MockFS +} + +func (f MockFile) Read(p []byte) (int, error) { + n, err := f.File.Read(p) + if n > 0 { + f.fs.m.Lock() + f.fs.bytesRead[f.filename] += n + f.fs.m.Unlock() + } + return n, err +} + +func TestArchiverParent(t *testing.T) { + var tests = []struct { + src TestDir + read map[string]int // tracks number of times a file must have been read + }{ + { + src: TestDir{ + "targetfile": TestFile{Content: string(restictest.Random(888, 2*1024*1024+5000))}, + }, + read: map[string]int{ + "targetfile": 1, + }, + }, + } + + for _, test := range tests { + t.Run("", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + tempdir, repo, cleanup := prepareTempdirRepoSrc(t, test.src) + defer cleanup() + + testFS := &MockFS{ + FS: fs.Track{fs.Local{}}, + bytesRead: make(map[string]int), + } + + arch := New(repo, testFS, Options{}) + + back := fs.TestChdir(t, tempdir) + defer back() + + _, firstSnapshotID, err := arch.Snapshot(ctx, []string{"."}, SnapshotOptions{Time: time.Now()}) + if err != nil { + t.Fatal(err) + } + + t.Logf("first backup saved as %v", firstSnapshotID.Str()) + t.Logf("testfs: %v", testFS) + + // check that all files have been read exactly once + TestWalkFiles(t, ".", test.src, func(filename string, item interface{}) error { + file, ok := item.(TestFile) + if !ok { + return nil + } + + n, ok := testFS.bytesRead[filename] + if !ok { + t.Fatalf("file %v was not read at all", filename) + } + + if n != len(file.Content) { + t.Fatalf("file %v: read %v bytes, wanted %v bytes", filename, n, len(file.Content)) + } + return nil + }) + + opts := SnapshotOptions{ + Time: time.Now(), + ParentSnapshot: firstSnapshotID, + } + _, secondSnapshotID, err := arch.Snapshot(ctx, []string{"."}, opts) + if err != nil { + t.Fatal(err) + } + + // check that all files still been read exactly once + TestWalkFiles(t, ".", test.src, func(filename string, item interface{}) error { + file, ok := item.(TestFile) + if !ok { + return nil + } + + n, ok := testFS.bytesRead[filename] + if !ok { + t.Fatalf("file %v was not read at all", filename) + } + + if n != len(file.Content) { + t.Fatalf("file %v: read %v bytes, wanted %v bytes", filename, n, len(file.Content)) + } + return nil + }) + + t.Logf("second backup saved as %v", secondSnapshotID.Str()) + t.Logf("testfs: %v", testFS) + + checker.TestCheckRepo(t, repo) + }) + } +} + +func TestArchiverErrorReporting(t *testing.T) { + ignoreErrorForBasename := func(basename string) ErrorFunc { + return func(item string, fi os.FileInfo, err error) error { + if filepath.Base(item) == "targetfile" { + t.Logf("ignoring error for targetfile: %v", err) + return nil + } + + t.Errorf("error handler called for unexpected file %v: %v", item, err) + return err + } + } + + chmodUnreadable := func(filename string) func(testing.TB) { + return func(t testing.TB) { + if runtime.GOOS == "windows" { + t.Skip("Skipping this test for windows") + } + + err := os.Chmod(filepath.FromSlash(filename), 0004) + if err != nil { + t.Fatal(err) + } + } + } + + var tests = []struct { + name string + src TestDir + want TestDir + prepare func(t testing.TB) + errFn ErrorFunc + mustError bool + }{ + { + name: "no-error", + src: TestDir{ + "targetfile": TestFile{Content: "foobar"}, + }, + }, + { + name: "file-unreadable", + src: TestDir{ + "targetfile": TestFile{Content: "foobar"}, + }, + prepare: chmodUnreadable("targetfile"), + mustError: true, + }, + { + name: "file-unreadable-ignore-error", + src: TestDir{ + "targetfile": TestFile{Content: "foobar"}, + "other": TestFile{Content: "xxx"}, + }, + want: TestDir{ + "other": TestFile{Content: "xxx"}, + }, + prepare: chmodUnreadable("targetfile"), + errFn: ignoreErrorForBasename("targetfile"), + }, + { + name: "file-subdir-unreadable", + src: TestDir{ + "subdir": TestDir{ + "targetfile": TestFile{Content: "foobar"}, + }, + }, + prepare: chmodUnreadable("subdir/targetfile"), + mustError: true, + }, + { + name: "file-subdir-unreadable-ignore-error", + src: TestDir{ + "subdir": TestDir{ + "targetfile": TestFile{Content: "foobar"}, + "other": TestFile{Content: "xxx"}, + }, + }, + want: TestDir{ + "subdir": TestDir{ + "other": TestFile{Content: "xxx"}, + }, + }, + prepare: chmodUnreadable("subdir/targetfile"), + errFn: ignoreErrorForBasename("targetfile"), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + tempdir, repo, cleanup := prepareTempdirRepoSrc(t, test.src) + defer cleanup() + + back := fs.TestChdir(t, tempdir) + defer back() + + if test.prepare != nil { + test.prepare(t) + } + + arch := New(repo, fs.Track{fs.Local{}}, Options{}) + arch.Error = test.errFn + + _, snapshotID, err := arch.Snapshot(ctx, []string{"."}, SnapshotOptions{Time: time.Now()}) + if test.mustError { + if err != nil { + t.Logf("found expected error (%v), skipping further checks", err) + return + } + + t.Fatalf("expected error not returned by archiver") + return + } + + if err != nil { + t.Fatalf("unexpected error of type %T found: %v", err, err) + } + + t.Logf("saved as %v", snapshotID.Str()) + + want := test.want + if want == nil { + want = test.src + } + TestEnsureSnapshot(t, repo, snapshotID, want) + + checker.TestCheckRepo(t, repo) + }) + } +} diff --git a/internal/archiver/blob_saver.go b/internal/archiver/blob_saver.go new file mode 100644 index 000000000..5e45d7175 --- /dev/null +++ b/internal/archiver/blob_saver.go @@ -0,0 +1,158 @@ +package archiver + +import ( + "context" + "sync" + + "github.com/restic/restic/internal/restic" +) + +// Saver allows saving a blob. +type Saver interface { + SaveBlob(ctx context.Context, t restic.BlobType, data []byte, id restic.ID) (restic.ID, error) + Index() restic.Index +} + +// BlobSaver concurrently saves incoming blobs to the repo. +type BlobSaver struct { + repo Saver + + m sync.Mutex + knownBlobs restic.BlobSet + + ch chan<- saveBlobJob + wg sync.WaitGroup +} + +// NewBlobSaver returns a new blob. A worker pool is started, it is stopped +// when ctx is cancelled. +func NewBlobSaver(ctx context.Context, repo Saver, workers uint) *BlobSaver { + ch := make(chan saveBlobJob, 2*int(workers)) + s := &BlobSaver{ + repo: repo, + knownBlobs: restic.NewBlobSet(), + ch: ch, + } + + for i := uint(0); i < workers; i++ { + s.wg.Add(1) + go s.worker(ctx, &s.wg, ch) + } + + return s +} + +// Save stores a blob in the repo. It checks the index and the known blobs +// before saving anything. The second return parameter is true if the blob was +// previously unknown. +func (s *BlobSaver) Save(ctx context.Context, t restic.BlobType, buf Buffer) FutureBlob { + ch := make(chan saveBlobResponse, 1) + s.ch <- saveBlobJob{BlobType: t, buf: buf, ch: ch} + + return FutureBlob{ch: ch, length: len(buf.Data)} +} + +// FutureBlob is returned by SaveBlob and will return the data once it has been processed. +type FutureBlob struct { + ch <-chan saveBlobResponse + length int + res saveBlobResponse +} + +func (s *FutureBlob) wait() { + res, ok := <-s.ch + if ok { + s.res = res + } +} + +// ID returns the ID of the blob after it has been saved. +func (s *FutureBlob) ID() restic.ID { + s.wait() + return s.res.id +} + +// Known returns whether or not the blob was already known. +func (s *FutureBlob) Known() bool { + s.wait() + return s.res.known +} + +// Err returns the error which may have occurred during save. +func (s *FutureBlob) Err() error { + s.wait() + return s.res.err +} + +// Length returns the length of the blob. +func (s *FutureBlob) Length() int { + return s.length +} + +type saveBlobJob struct { + restic.BlobType + buf Buffer + ch chan<- saveBlobResponse +} + +type saveBlobResponse struct { + id restic.ID + known bool + err error +} + +func (s *BlobSaver) saveBlob(ctx context.Context, t restic.BlobType, buf []byte) saveBlobResponse { + id := restic.Hash(buf) + h := restic.BlobHandle{ID: id, Type: t} + + // check if another goroutine has already saved this blob + known := false + s.m.Lock() + if s.knownBlobs.Has(h) { + known = true + } else { + s.knownBlobs.Insert(h) + known = false + } + s.m.Unlock() + + // blob is already known, nothing to do + if known { + return saveBlobResponse{ + id: id, + known: true, + } + } + + // check if the repo knows this blob + if s.repo.Index().Has(id, t) { + return saveBlobResponse{ + id: id, + known: true, + } + } + + // otherwise we're responsible for saving it + _, err := s.repo.SaveBlob(ctx, t, buf, id) + return saveBlobResponse{ + id: id, + known: false, + err: err, + } +} + +func (s *BlobSaver) worker(ctx context.Context, wg *sync.WaitGroup, jobs <-chan saveBlobJob) { + defer wg.Done() + for { + var job saveBlobJob + select { + case <-ctx.Done(): + return + case job = <-jobs: + } + + job.ch <- s.saveBlob(ctx, job.BlobType, job.buf.Data) + close(job.ch) + job.buf.Release() + } +} diff --git a/internal/archiver/buffer.go b/internal/archiver/buffer.go new file mode 100644 index 000000000..c97d990cf --- /dev/null +++ b/internal/archiver/buffer.go @@ -0,0 +1,90 @@ +package archiver + +import ( + "context" + "sync" +) + +// Buffer is a reusable buffer. After the buffer has been used, Release should +// be called so the underlying slice is put back into the pool. +type Buffer struct { + Data []byte + Put func([]byte) +} + +// Release puts the buffer back into the pool it came from. +func (b Buffer) Release() { + if b.Put != nil { + b.Put(b.Data) + } +} + +// BufferPool implements a limited set of reusable buffers. +type BufferPool struct { + ch chan []byte + chM sync.Mutex + defaultSize int + clearOnce sync.Once +} + +// NewBufferPool initializes a new buffer pool. When the context is cancelled, +// all buffers are released. The pool stores at most max items. New buffers are +// created with defaultSize, buffers that are larger are released and not put +// back. +func NewBufferPool(ctx context.Context, max int, defaultSize int) *BufferPool { + b := &BufferPool{ + ch: make(chan []byte, max), + defaultSize: defaultSize, + } + go func() { + <-ctx.Done() + b.clear() + }() + return b +} + +// Get returns a new buffer, either from the pool or newly allocated. +func (pool *BufferPool) Get() Buffer { + b := Buffer{Put: pool.put} + + pool.chM.Lock() + defer pool.chM.Unlock() + select { + case buf := <-pool.ch: + b.Data = buf + default: + b.Data = make([]byte, pool.defaultSize) + } + + return b +} + +func (pool *BufferPool) put(b []byte) { + pool.chM.Lock() + defer pool.chM.Unlock() + select { + case pool.ch <- b: + default: + } +} + +// Put returns a buffer to the pool for reuse. +func (pool *BufferPool) Put(b Buffer) { + if cap(b.Data) > pool.defaultSize { + return + } + pool.put(b.Data) +} + +// clear empties the buffer so that all items can be garbage collected. +func (pool *BufferPool) clear() { + pool.clearOnce.Do(func() { + ch := pool.ch + pool.chM.Lock() + pool.ch = nil + pool.chM.Unlock() + close(ch) + for range ch { + } + }) +} diff --git a/internal/archiver/file_saver.go b/internal/archiver/file_saver.go new file mode 100644 index 000000000..9a923c6c7 --- /dev/null +++ b/internal/archiver/file_saver.go @@ -0,0 +1,228 @@ +package archiver + +import ( + "context" + "io" + "os" + "sync" + + "github.com/restic/chunker" + "github.com/restic/restic/internal/debug" + "github.com/restic/restic/internal/errors" + "github.com/restic/restic/internal/fs" + "github.com/restic/restic/internal/restic" +) + +// FutureFile is returned by SaveFile and will return the data once it +// has been processed. +type FutureFile struct { + ch <-chan saveFileResponse + res saveFileResponse +} + +func (s *FutureFile) wait() { + res, ok := <-s.ch + if ok { + s.res = res + } +} + +// Node returns the node once it is available. +func (s *FutureFile) Node() *restic.Node { + s.wait() + return s.res.node +} + +// Stats returns the stats for the file once they are available. +func (s *FutureFile) Stats() ItemStats { + s.wait() + return s.res.stats +} + +// Err returns the error in case an error occurred. +func (s *FutureFile) Err() error { + s.wait() + return s.res.err +} + +// FileSaver concurrently saves incoming files to the repo. +type FileSaver struct { + fs fs.FS + blobSaver *BlobSaver + saveFilePool *BufferPool + + pol chunker.Pol + + ch chan<- saveFileJob + wg sync.WaitGroup + + CompleteBlob func(filename string, bytes uint64) + + NodeFromFileInfo func(filename string, fi os.FileInfo) (*restic.Node, error) +} + +// NewFileSaver returns a new file saver. A worker pool with workers is +// started, it is stopped when ctx is cancelled. +func NewFileSaver(ctx context.Context, fs fs.FS, blobSaver *BlobSaver, pol chunker.Pol, workers uint) *FileSaver { + ch := make(chan saveFileJob, workers) + + s := &FileSaver{ + fs: fs, + blobSaver: blobSaver, + saveFilePool: NewBufferPool(ctx, 3*int(workers), chunker.MaxSize/4), + pol: pol, + ch: ch, + + CompleteBlob: func(string, uint64) {}, + } + + for i := uint(0); i < workers; i++ { + s.wg.Add(1) + go s.worker(ctx, &s.wg, ch) + } + + return s +} + +// CompleteFunc is called when the file has been saved. +type CompleteFunc func(*restic.Node, ItemStats) + +// Save stores the file f and returns the data once it has been completed. The +// file is closed by Save. +func (s *FileSaver) Save(ctx context.Context, snPath string, file fs.File, fi os.FileInfo, start func(), complete CompleteFunc) FutureFile { + ch := make(chan saveFileResponse, 1) + s.ch <- saveFileJob{ + snPath: snPath, + file: file, + fi: fi, + start: start, + complete: complete, + ch: ch, + } + + return FutureFile{ch: ch} +} + +type saveFileJob struct { + snPath string + file fs.File + fi os.FileInfo + ch chan<- saveFileResponse + complete CompleteFunc + start func() +} + +type saveFileResponse struct { + node *restic.Node + stats ItemStats + err error +} + +// saveFile stores the file f in the repo, then closes it. +func (s *FileSaver) saveFile(ctx context.Context, chnker *chunker.Chunker, snPath string, f fs.File, fi os.FileInfo, start func()) saveFileResponse { + start() + + stats := ItemStats{} + + debug.Log("%v", snPath) + + node, err := s.NodeFromFileInfo(f.Name(), fi) + if err != nil { + _ = f.Close() + return saveFileResponse{err: err} + } + + if node.Type != "file" { + _ = f.Close() + return saveFileResponse{err: errors.Errorf("node type %q is wrong", node.Type)} + } + + // reuse the chunker + chnker.Reset(f, s.pol) + + var results []FutureBlob + + node.Content = []restic.ID{} + var size uint64 + for { + buf := s.saveFilePool.Get() + chunk, err := chnker.Next(buf.Data) + if errors.Cause(err) == io.EOF { + buf.Release() + break + } + buf.Data = chunk.Data + + size += uint64(chunk.Length) + + if err != nil { + _ = f.Close() + return saveFileResponse{err: err} + } + + // test if the context has been cancelled, return the error + if ctx.Err() != nil { + _ = f.Close() + return saveFileResponse{err: ctx.Err()} + } + + res := s.blobSaver.Save(ctx, restic.DataBlob, buf) + results = append(results, res) + + // test if the context has been cancelled, return the error + if ctx.Err() != nil { + _ = f.Close() + return saveFileResponse{err: ctx.Err()} + } + + s.CompleteBlob(f.Name(), uint64(len(chunk.Data))) + } + + err = f.Close() + if err != nil { + return saveFileResponse{err: err} + } + + for _, res := range results { + // test if the context has been cancelled, return the error + if res.Err() != nil { + return saveFileResponse{err: ctx.Err()} + } + + if !res.Known() { + stats.DataBlobs++ + stats.DataSize += uint64(res.Length()) + } + + node.Content = append(node.Content, res.ID()) + } + + node.Size = size + + return saveFileResponse{ + node: node, + stats: stats, + } +} + +func (s *FileSaver) worker(ctx context.Context, wg *sync.WaitGroup, jobs <-chan saveFileJob) { + // a worker has one chunker which is reused for each file (because it contains a rather large buffer) + chnker := chunker.New(nil, s.pol) + + defer wg.Done() + for { + var job saveFileJob + select { + case <-ctx.Done(): + return + case job = <-jobs: + } + + res := s.saveFile(ctx, chnker, job.snPath, job.file, job.fi, job.start) + if job.complete != nil { + job.complete(res.node, res.stats) + } + job.ch <- res + close(job.ch) + } +} diff --git a/internal/archiver/index_uploader.go b/internal/archiver/index_uploader.go new file mode 100644 index 000000000..c6edb7a01 --- /dev/null +++ b/internal/archiver/index_uploader.go @@ -0,0 +1,53 @@ +package archiver + +import ( + "context" + "time" + + "github.com/restic/restic/internal/debug" + "github.com/restic/restic/internal/repository" + "github.com/restic/restic/internal/restic" +) + +// IndexUploader polls the repo for full indexes and uploads them. +type IndexUploader struct { + restic.Repository + + // Start is called when an index is to be uploaded. + Start func() + + // Complete is called when uploading an index has finished. + Complete func(id restic.ID) +} + +// Upload periodically uploads full indexes to the repo. When shutdown is +// cancelled, the last index upload will finish and then Upload returns. +func (u IndexUploader) Upload(ctx, shutdown context.Context, interval time.Duration) error { + ticker := time.NewTicker(interval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return nil + case <-shutdown.Done(): + return nil + case <-ticker.C: + full := u.Repository.Index().(*repository.MasterIndex).FullIndexes() + for _, idx := range full { + if u.Start != nil { + u.Start() + } + + id, err := repository.SaveIndex(ctx, u.Repository, idx) + if err != nil { + debug.Log("save indexes returned an error: %v", err) + return err + } + if u.Complete != nil { + u.Complete(id) + } + } + } + } +} diff --git a/internal/archiver/scanner.go b/internal/archiver/scanner.go new file mode 100644 index 000000000..000d2d875 --- /dev/null +++ b/internal/archiver/scanner.go @@ -0,0 +1,112 @@ +package archiver + +import ( + "context" + "os" + "path/filepath" + + "github.com/restic/restic/internal/fs" +) + +// Scanner traverses the targets and calls the function Result with cumulated +// stats concerning the files and folders found. Select is used to decide which +// items should be included. Error is called when an error occurs. +type Scanner struct { + FS fs.FS + Select SelectFunc + Error ErrorFunc + Result func(item string, s ScanStats) +} + +// NewScanner initializes a new Scanner. +func NewScanner(fs fs.FS) *Scanner { + return &Scanner{ + FS: fs, + Select: func(item string, fi os.FileInfo) bool { + return true + }, + Error: func(item string, fi os.FileInfo, err error) error { + return err + }, + Result: func(item string, s ScanStats) {}, + } +} + +// ScanStats collect statistics. +type ScanStats struct { + Files, Dirs, Others uint + Bytes uint64 +} + +// Scan traverses the targets. The function Result is called for each new item +// found, the complete result is also returned by Scan. +func (s *Scanner) Scan(ctx context.Context, targets []string) error { + var stats ScanStats + for _, target := range targets { + abstarget, err := s.FS.Abs(target) + if err != nil { + return err + } + + stats, err = s.scan(ctx, stats, abstarget) + if err != nil { + return err + } + + if ctx.Err() != nil { + return ctx.Err() + } + } + + s.Result("", stats) + return nil +} + +func (s *Scanner) scan(ctx context.Context, stats ScanStats, target string) (ScanStats, error) { + if ctx.Err() != nil { + return stats, ctx.Err() + } + + fi, err := s.FS.Lstat(target) + if err != nil { + // ignore error if the target is to be excluded anyway + if !s.Select(target, nil) { + return stats, nil + } + + // else return filtered error + return stats, s.Error(target, fi, err) + } + + if !s.Select(target, fi) { + return stats, nil + } + + switch { + case fi.Mode().IsRegular(): + stats.Files++ + stats.Bytes += uint64(fi.Size()) + case fi.Mode().IsDir(): + if ctx.Err() != nil { + return stats, ctx.Err() + } + + names, err := readdirnames(s.FS, target) + if err != nil { + return stats, s.Error(target, fi, err) + } + + for _, name := range names { + stats, err = s.scan(ctx, stats, filepath.Join(target, name)) + if err != nil { + return stats, err + } + } + stats.Dirs++ + default: + stats.Others++ + } + + s.Result(target, stats) + return stats, nil +} diff --git a/internal/archiver/scanner_test.go b/internal/archiver/scanner_test.go new file mode 100644 index 000000000..91b8d7f63 --- /dev/null +++ b/internal/archiver/scanner_test.go @@ -0,0 +1,333 @@ +package archiver + +import ( + "context" + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/restic/restic/internal/fs" + restictest "github.com/restic/restic/internal/test" +) + +func TestScanner(t *testing.T) { + var tests = []struct { + name string + src TestDir + want map[string]ScanStats + selFn SelectFunc + }{ + { + name: "include-all", + src: TestDir{ + "other": TestFile{Content: "another file"}, + "work": TestDir{ + "foo": TestFile{Content: "foo"}, + "foo.txt": TestFile{Content: "foo text file"}, + "subdir": TestDir{ + "other": TestFile{Content: "other in subdir"}, + "bar.txt": TestFile{Content: "bar.txt in subdir"}, + }, + }, + }, + want: map[string]ScanStats{ + filepath.FromSlash("other"): ScanStats{Files: 1, Bytes: 12}, + filepath.FromSlash("work/foo"): ScanStats{Files: 2, Bytes: 15}, + filepath.FromSlash("work/foo.txt"): ScanStats{Files: 3, Bytes: 28}, + filepath.FromSlash("work/subdir/bar.txt"): ScanStats{Files: 4, Bytes: 45}, + filepath.FromSlash("work/subdir/other"): ScanStats{Files: 5, Bytes: 60}, + filepath.FromSlash("work/subdir"): ScanStats{Files: 5, Dirs: 1, Bytes: 60}, + filepath.FromSlash("work"): ScanStats{Files: 5, Dirs: 2, Bytes: 60}, + filepath.FromSlash("."): ScanStats{Files: 5, Dirs: 3, Bytes: 60}, + filepath.FromSlash(""): ScanStats{Files: 5, Dirs: 3, Bytes: 60}, + }, + }, + { + name: "select-txt", + src: TestDir{ + "other": TestFile{Content: "another file"}, + "work": TestDir{ + "foo": TestFile{Content: "foo"}, + "foo.txt": TestFile{Content: "foo text file"}, + "subdir": TestDir{ + "other": TestFile{Content: "other in subdir"}, + "bar.txt": TestFile{Content: "bar.txt in subdir"}, + }, + }, + }, + selFn: func(item string, fi os.FileInfo) bool { + if fi.IsDir() { + return true + } + + if filepath.Ext(item) == ".txt" { + return true + } + return false + }, + want: map[string]ScanStats{ + filepath.FromSlash("work/foo.txt"): ScanStats{Files: 1, Bytes: 13}, + filepath.FromSlash("work/subdir/bar.txt"): ScanStats{Files: 2, Bytes: 30}, + filepath.FromSlash("work/subdir"): ScanStats{Files: 2, Dirs: 1, Bytes: 30}, + filepath.FromSlash("work"): ScanStats{Files: 2, Dirs: 2, Bytes: 30}, + filepath.FromSlash("."): ScanStats{Files: 2, Dirs: 3, Bytes: 30}, + filepath.FromSlash(""): ScanStats{Files: 2, Dirs: 3, Bytes: 30}, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + tempdir, cleanup := restictest.TempDir(t) + defer cleanup() + + TestCreateFiles(t, tempdir, test.src) + + back := fs.TestChdir(t, tempdir) + defer back() + + cur, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + sc := NewScanner(fs.Track{fs.Local{}}) + if test.selFn != nil { + sc.Select = test.selFn + } + + results := make(map[string]ScanStats) + sc.Result = func(item string, s ScanStats) { + var p string + var err error + + if item != "" { + p, err = filepath.Rel(cur, item) + if err != nil { + panic(err) + } + } + + results[p] = s + } + + err = sc.Scan(ctx, []string{"."}) + if err != nil { + t.Fatal(err) + } + + if !cmp.Equal(test.want, results) { + t.Error(cmp.Diff(test.want, results)) + } + }) + } +} + +func TestScannerError(t *testing.T) { + var tests = []struct { + name string + unix bool + src TestDir + result ScanStats + selFn SelectFunc + errFn func(t testing.TB, item string, fi os.FileInfo, err error) error + resFn func(t testing.TB, item string, s ScanStats) + prepare func(t testing.TB) + }{ + { + name: "no-error", + src: TestDir{ + "other": TestFile{Content: "another file"}, + "work": TestDir{ + "foo": TestFile{Content: "foo"}, + "foo.txt": TestFile{Content: "foo text file"}, + "subdir": TestDir{ + "other": TestFile{Content: "other in subdir"}, + "bar.txt": TestFile{Content: "bar.txt in subdir"}, + }, + }, + }, + result: ScanStats{Files: 5, Dirs: 3, Bytes: 60}, + }, + { + name: "unreadable-dir", + unix: true, + src: TestDir{ + "other": TestFile{Content: "another file"}, + "work": TestDir{ + "foo": TestFile{Content: "foo"}, + "foo.txt": TestFile{Content: "foo text file"}, + "subdir": TestDir{ + "other": TestFile{Content: "other in subdir"}, + "bar.txt": TestFile{Content: "bar.txt in subdir"}, + }, + }, + }, + result: ScanStats{Files: 3, Dirs: 2, Bytes: 28}, + prepare: func(t testing.TB) { + err := os.Chmod(filepath.Join("work", "subdir"), 0000) + if err != nil { + t.Fatal(err) + } + }, + errFn: func(t testing.TB, item string, fi os.FileInfo, err error) error { + if item == filepath.FromSlash("work/subdir") { + return nil + } + + return err + }, + }, + { + name: "removed-item", + src: TestDir{ + "bar": TestFile{Content: "bar"}, + "baz": TestFile{Content: "baz"}, + "foo": TestFile{Content: "foo"}, + "other": TestFile{Content: "other"}, + }, + result: ScanStats{Files: 3, Dirs: 1, Bytes: 11}, + resFn: func(t testing.TB, item string, s ScanStats) { + if item == "bar" { + err := os.Remove("foo") + if err != nil { + t.Fatal(err) + } + } + }, + errFn: func(t testing.TB, item string, fi os.FileInfo, err error) error { + if item == "foo" { + t.Logf("ignoring error for %v: %v", item, err) + return nil + } + + return err + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if test.unix && runtime.GOOS == "windows" { + t.Skipf("skip on windows") + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + tempdir, cleanup := restictest.TempDir(t) + defer cleanup() + + TestCreateFiles(t, tempdir, test.src) + + back := fs.TestChdir(t, tempdir) + defer back() + + cur, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + if test.prepare != nil { + test.prepare(t) + } + + sc := NewScanner(fs.Track{fs.Local{}}) + if test.selFn != nil { + sc.Select = test.selFn + } + + var stats ScanStats + + sc.Result = func(item string, s ScanStats) { + if item == "" { + stats = s + return + } + + if test.resFn != nil { + p, relErr := filepath.Rel(cur, item) + if relErr != nil { + panic(relErr) + } + test.resFn(t, p, s) + } + } + if test.errFn != nil { + sc.Error = func(item string, fi os.FileInfo, err error) error { + p, relErr := filepath.Rel(cur, item) + if relErr != nil { + panic(relErr) + } + + return test.errFn(t, p, fi, err) + } + } + + err = sc.Scan(ctx, []string{"."}) + if err != nil { + t.Fatal(err) + } + + if stats != test.result { + t.Errorf("wrong final result, want\n %#v\ngot:\n %#v", test.result, stats) + } + }) + } +} + +func TestScannerCancel(t *testing.T) { + src := TestDir{ + "bar": TestFile{Content: "bar"}, + "baz": TestFile{Content: "baz"}, + "foo": TestFile{Content: "foo"}, + "other": TestFile{Content: "other"}, + } + + result := ScanStats{Files: 2, Bytes: 6} + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + tempdir, cleanup := restictest.TempDir(t) + defer cleanup() + + TestCreateFiles(t, tempdir, src) + + back := fs.TestChdir(t, tempdir) + defer back() + + cur, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + sc := NewScanner(fs.Track{fs.Local{}}) + var lastStats ScanStats + sc.Result = func(item string, s ScanStats) { + lastStats = s + + if item == filepath.Join(cur, "baz") { + t.Logf("found baz") + cancel() + } + } + + err = sc.Scan(ctx, []string{"."}) + if err == nil { + t.Errorf("did not find expected error") + } + + if err != context.Canceled { + t.Errorf("unexpected error found, want %v, got %v", context.Canceled, err) + } + + if lastStats != result { + t.Errorf("wrong final result, want\n %#v\ngot:\n %#v", result, lastStats) + } +} diff --git a/internal/archiver/testing.go b/internal/archiver/testing.go index d700135b4..e4bace51a 100644 --- a/internal/archiver/testing.go +++ b/internal/archiver/testing.go @@ -2,10 +2,19 @@ package archiver import ( "context" + "io/ioutil" + "os" + "path" + "path/filepath" + "runtime" + "strings" "testing" "time" + "github.com/restic/restic/internal/debug" + "github.com/restic/restic/internal/fs" "github.com/restic/restic/internal/restic" + "github.com/restic/restic/internal/test" ) // TestSnapshot creates a new snapshot of path. @@ -17,3 +26,310 @@ func TestSnapshot(t testing.TB, repo restic.Repository, path string, parent *res } return sn } + +// TestDir describes a directory structure to create for a test. +type TestDir map[string]interface{} + +func (d TestDir) String() string { + return "

" +} + +// TestFile describes a file created for a test. +type TestFile struct { + Content string +} + +func (f TestFile) String() string { + return "" +} + +// TestSymlink describes a symlink created for a test. +type TestSymlink struct { + Target string +} + +func (s TestSymlink) String() string { + return "" +} + +// TestCreateFiles creates a directory structure described by dir at target, +// which must already exist. On Windows, symlinks aren't created. +func TestCreateFiles(t testing.TB, target string, dir TestDir) { + test.Helper(t).Helper() + for name, item := range dir { + targetPath := filepath.Join(target, name) + + switch it := item.(type) { + case TestFile: + err := ioutil.WriteFile(targetPath, []byte(it.Content), 0644) + if err != nil { + t.Fatal(err) + } + case TestSymlink: + if runtime.GOOS == "windows" { + continue + } + + err := fs.Symlink(filepath.FromSlash(it.Target), targetPath) + if err != nil { + t.Fatal(err) + } + case TestDir: + err := fs.Mkdir(targetPath, 0755) + if err != nil { + t.Fatal(err) + } + + TestCreateFiles(t, targetPath, it) + } + } +} + +// TestWalkFunc is used by TestWalkFiles to traverse the dir. When an error is +// returned, traversal stops and the surrounding test is marked as failed. +type TestWalkFunc func(path string, item interface{}) error + +// TestWalkFiles runs fn for each file/directory in dir, the filename will be +// constructed with target as the prefix. Symlinks on Windows are ignored. +func TestWalkFiles(t testing.TB, target string, dir TestDir, fn TestWalkFunc) { + test.Helper(t).Helper() + for name, item := range dir { + targetPath := filepath.Join(target, name) + + err := fn(targetPath, item) + if err != nil { + t.Fatalf("TestWalkFunc returned error for %v: %v", targetPath, err) + return + } + + if dir, ok := item.(TestDir); ok { + TestWalkFiles(t, targetPath, dir, fn) + } + } +} + +// fixpath removes UNC paths (starting with `\\?`) on windows. On Linux, it's a noop. +func fixpath(item string) string { + if runtime.GOOS != "windows" { + return item + } + if strings.HasPrefix(item, `\\?`) { + return item[4:] + } + return item +} + +// TestEnsureFiles tests if the directory structure at target is the same as +// described in dir. +func TestEnsureFiles(t testing.TB, target string, dir TestDir) { + test.Helper(t).Helper() + pathsChecked := make(map[string]struct{}) + + // first, test that all items are there + TestWalkFiles(t, target, dir, func(path string, item interface{}) error { + // ignore symlinks on Windows + if _, ok := item.(TestSymlink); ok && runtime.GOOS == "windows" { + // mark paths and parents as checked + pathsChecked[path] = struct{}{} + for parent := filepath.Dir(path); parent != target; parent = filepath.Dir(parent) { + pathsChecked[parent] = struct{}{} + } + return nil + } + + fi, err := fs.Lstat(path) + if err != nil { + return err + } + + switch node := item.(type) { + case TestDir: + if !fi.IsDir() { + t.Errorf("is not a directory: %v", path) + } + return nil + case TestFile: + if !fs.IsRegularFile(fi) { + t.Errorf("is not a regular file: %v", path) + return nil + } + + content, err := ioutil.ReadFile(path) + if err != nil { + return err + } + + if string(content) != node.Content { + t.Errorf("wrong content for %v, want %q, got %q", path, node.Content, content) + } + case TestSymlink: + if fi.Mode()&os.ModeType != os.ModeSymlink { + t.Errorf("is not a symlink: %v", path) + return nil + } + + target, err := fs.Readlink(path) + if err != nil { + return err + } + + if target != node.Target { + t.Errorf("wrong target for %v, want %v, got %v", path, node.Target, target) + } + } + + pathsChecked[path] = struct{}{} + + for parent := filepath.Dir(path); parent != target; parent = filepath.Dir(parent) { + pathsChecked[parent] = struct{}{} + } + + return nil + }) + + // then, traverse the directory again, looking for additional files + err := fs.Walk(target, func(path string, fi os.FileInfo, err error) error { + if err != nil { + return err + } + + path = fixpath(path) + + if path == target { + return nil + } + + _, ok := pathsChecked[path] + if !ok { + t.Errorf("additional item found: %v %v", path, fi.Mode()) + } + + return nil + }) + if err != nil { + t.Fatal(err) + } +} + +// TestEnsureFileContent checks if the file in the repo is the same as file. +func TestEnsureFileContent(ctx context.Context, t testing.TB, repo restic.Repository, filename string, node *restic.Node, file TestFile) { + if int(node.Size) != len(file.Content) { + t.Fatalf("%v: wrong node size: want %d, got %d", filename, node.Size, len(file.Content)) + return + } + + content := make([]byte, restic.CiphertextLength(len(file.Content))) + pos := 0 + for _, id := range node.Content { + n, err := repo.LoadBlob(ctx, restic.DataBlob, id, content[pos:]) + if err != nil { + t.Fatalf("error loading blob %v: %v", id.Str(), err) + return + } + + pos += n + } + + content = content[:pos] + + if string(content) != file.Content { + t.Fatalf("%v: wrong content returned, want %q, got %q", filename, file.Content, content) + } +} + +// TestEnsureTree checks that the tree ID in the repo matches dir. On Windows, +// Symlinks are ignored. +func TestEnsureTree(ctx context.Context, t testing.TB, prefix string, repo restic.Repository, treeID restic.ID, dir TestDir) { + test.Helper(t).Helper() + + tree, err := repo.LoadTree(ctx, treeID) + if err != nil { + t.Fatal(err) + return + } + + var nodeNames []string + for _, node := range tree.Nodes { + nodeNames = append(nodeNames, node.Name) + } + debug.Log("%v (%v) %v", prefix, treeID.Str(), nodeNames) + + checked := make(map[string]struct{}) + for _, node := range tree.Nodes { + nodePrefix := path.Join(prefix, node.Name) + + entry, ok := dir[node.Name] + if !ok { + t.Errorf("unexpected tree node %q found, want: %#v", node.Name, dir) + return + } + + checked[node.Name] = struct{}{} + + switch e := entry.(type) { + case TestDir: + if node.Type != "dir" { + t.Errorf("tree node %v has wrong type %q, want %q", nodePrefix, node.Type, "dir") + return + } + + if node.Subtree == nil { + t.Errorf("tree node %v has nil subtree", nodePrefix) + return + } + + TestEnsureTree(ctx, t, path.Join(prefix, node.Name), repo, *node.Subtree, e) + case TestFile: + if node.Type != "file" { + t.Errorf("tree node %v has wrong type %q, want %q", nodePrefix, node.Type, "file") + } + TestEnsureFileContent(ctx, t, repo, nodePrefix, node, e) + case TestSymlink: + // skip symlinks on windows + if runtime.GOOS == "windows" { + continue + } + if node.Type != "symlink" { + t.Errorf("tree node %v has wrong type %q, want %q", nodePrefix, node.Type, "file") + } + + if e.Target != node.LinkTarget { + t.Errorf("symlink %v has wrong target, want %q, got %q", nodePrefix, e.Target, node.LinkTarget) + } + } + } + + for name := range dir { + // skip checking symlinks on Windows + entry := dir[name] + if _, ok := entry.(TestSymlink); ok && runtime.GOOS == "windows" { + continue + } + + _, ok := checked[name] + if !ok { + t.Errorf("tree %v: expected node %q not found, has: %v", prefix, name, nodeNames) + } + } +} + +// TestEnsureSnapshot tests if the snapshot in the repo has exactly the same +// structure as dir. On Windows, Symlinks are ignored. +func TestEnsureSnapshot(t testing.TB, repo restic.Repository, snapshotID restic.ID, dir TestDir) { + test.Helper(t).Helper() + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + sn, err := restic.LoadSnapshot(ctx, repo, snapshotID) + if err != nil { + t.Fatal(err) + return + } + + if sn.Tree == nil { + t.Fatal("snapshot has nil tree ID") + return + } + + TestEnsureTree(ctx, t, "/", repo, *sn.Tree, dir) +} diff --git a/internal/archiver/testing_test.go b/internal/archiver/testing_test.go new file mode 100644 index 000000000..e874b8316 --- /dev/null +++ b/internal/archiver/testing_test.go @@ -0,0 +1,525 @@ +package archiver + +import ( + "context" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "runtime" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/restic/restic/internal/fs" + "github.com/restic/restic/internal/repository" + restictest "github.com/restic/restic/internal/test" +) + +// MockT passes through all logging functions from T, but catches Fail(), +// Error/f() and Fatal/f(). It is used to test test helper functions. +type MockT struct { + *testing.T + HasFailed bool +} + +// Fail marks the function as having failed but continues execution. +func (t *MockT) Fail() { + t.T.Log("MockT Fail() called") + t.HasFailed = true +} + +// Fatal is equivalent to Log followed by FailNow. +func (t *MockT) Fatal(args ...interface{}) { + t.T.Logf("MockT Fatal called with %v", args) + t.HasFailed = true +} + +// Fatalf is equivalent to Logf followed by FailNow. +func (t *MockT) Fatalf(msg string, args ...interface{}) { + t.T.Logf("MockT Fatal called: "+msg, args...) + t.HasFailed = true +} + +// Error is equivalent to Log followed by Fail. +func (t *MockT) Error(args ...interface{}) { + t.T.Logf("MockT Error called with %v", args) + t.HasFailed = true +} + +// Errorf is equivalent to Logf followed by Fail. +func (t *MockT) Errorf(msg string, args ...interface{}) { + t.T.Logf("MockT Error called: "+msg, args...) + t.HasFailed = true +} + +func createFilesAt(t testing.TB, targetdir string, files map[string]interface{}) { + for name, item := range files { + target := filepath.Join(targetdir, filepath.FromSlash(name)) + err := fs.MkdirAll(filepath.Dir(target), 0700) + if err != nil { + t.Fatal(err) + } + + switch it := item.(type) { + case TestFile: + err := ioutil.WriteFile(target, []byte(it.Content), 0600) + if err != nil { + t.Fatal(err) + } + case TestSymlink: + // ignore symlinks on windows + if runtime.GOOS == "windows" { + continue + } + err := fs.Symlink(filepath.FromSlash(it.Target), target) + if err != nil { + t.Fatal(err) + } + } + } +} + +func TestTestCreateFiles(t *testing.T) { + var tests = []struct { + dir TestDir + files map[string]interface{} + }{ + { + dir: TestDir{ + "foo": TestFile{Content: "foo"}, + "subdir": TestDir{ + "subfile": TestFile{Content: "bar"}, + }, + "sub": TestDir{ + "subsub": TestDir{ + "link": TestSymlink{Target: "x/y/z"}, + }, + }, + }, + files: map[string]interface{}{ + "foo": TestFile{Content: "foo"}, + "subdir": TestDir{}, + "subdir/subfile": TestFile{Content: "bar"}, + "sub/subsub/link": TestSymlink{Target: "x/y/z"}, + }, + }, + } + + for i, test := range tests { + tempdir, cleanup := restictest.TempDir(t) + defer cleanup() + + t.Run("", func(t *testing.T) { + tempdir := filepath.Join(tempdir, fmt.Sprintf("test-%d", i)) + err := fs.MkdirAll(tempdir, 0700) + if err != nil { + t.Fatal(err) + } + + TestCreateFiles(t, tempdir, test.dir) + + for name, item := range test.files { + // don't check symlinks on windows + if runtime.GOOS == "windows" { + if _, ok := item.(TestSymlink); ok { + continue + } + continue + } + + targetPath := filepath.Join(tempdir, filepath.FromSlash(name)) + fi, err := fs.Lstat(targetPath) + if err != nil { + t.Error(err) + continue + } + + switch node := item.(type) { + case TestFile: + if !fs.IsRegularFile(fi) { + t.Errorf("is not regular file: %v", name) + continue + } + + content, err := ioutil.ReadFile(targetPath) + if err != nil { + t.Error(err) + continue + } + + if string(content) != node.Content { + t.Errorf("wrong content for %v: want %q, got %q", name, node.Content, content) + } + case TestSymlink: + if fi.Mode()&os.ModeType != os.ModeSymlink { + t.Errorf("is not symlink: %v, %o != %o", name, fi.Mode(), os.ModeSymlink) + continue + } + + target, err := fs.Readlink(targetPath) + if err != nil { + t.Error(err) + continue + } + + if target != node.Target { + t.Errorf("wrong target for %v: want %q, got %q", name, node.Target, target) + } + case TestDir: + if !fi.IsDir() { + t.Errorf("is not directory: %v", name) + } + } + } + }) + } +} + +func TestTestWalkFiles(t *testing.T) { + var tests = []struct { + dir TestDir + want map[string]string + }{ + { + dir: TestDir{ + "foo": TestFile{Content: "foo"}, + "subdir": TestDir{ + "subfile": TestFile{Content: "bar"}, + }, + "x": TestDir{ + "y": TestDir{ + "link": TestSymlink{Target: filepath.FromSlash("../../foo")}, + }, + }, + }, + want: map[string]string{ + "foo": "", + "subdir": "", + filepath.FromSlash("subdir/subfile"): "", + "x": "", + filepath.FromSlash("x/y"): "", + filepath.FromSlash("x/y/link"): "", + }, + }, + } + + for _, test := range tests { + t.Run("", func(t *testing.T) { + tempdir, cleanup := restictest.TempDir(t) + defer cleanup() + + got := make(map[string]string) + + TestCreateFiles(t, tempdir, test.dir) + TestWalkFiles(t, tempdir, test.dir, func(path string, item interface{}) error { + p, err := filepath.Rel(tempdir, path) + if err != nil { + return err + } + + got[p] = fmt.Sprintf("%v", item) + return nil + }) + + if !cmp.Equal(test.want, got) { + t.Error(cmp.Diff(test.want, got)) + } + }) + } +} + +func TestTestEnsureFiles(t *testing.T) { + var tests = []struct { + expectFailure bool + files map[string]interface{} + want TestDir + unixOnly bool + }{ + { + files: map[string]interface{}{ + "foo": TestFile{Content: "foo"}, + "subdir/subfile": TestFile{Content: "bar"}, + "x/y/link": TestSymlink{Target: "../../foo"}, + }, + want: TestDir{ + "foo": TestFile{Content: "foo"}, + "subdir": TestDir{ + "subfile": TestFile{Content: "bar"}, + }, + "x": TestDir{ + "y": TestDir{ + "link": TestSymlink{Target: "../../foo"}, + }, + }, + }, + }, + { + expectFailure: true, + files: map[string]interface{}{ + "foo": TestFile{Content: "foo"}, + }, + want: TestDir{ + "foo": TestFile{Content: "foo"}, + "subdir": TestDir{ + "subfile": TestFile{Content: "bar"}, + }, + }, + }, + { + expectFailure: true, + files: map[string]interface{}{ + "foo": TestFile{Content: "foo"}, + "subdir/subfile": TestFile{Content: "bar"}, + }, + want: TestDir{ + "foo": TestFile{Content: "foo"}, + }, + }, + { + expectFailure: true, + files: map[string]interface{}{ + "foo": TestFile{Content: "xxx"}, + }, + want: TestDir{ + "foo": TestFile{Content: "foo"}, + }, + }, + { + expectFailure: true, + files: map[string]interface{}{ + "foo": TestSymlink{Target: "/xxx"}, + }, + want: TestDir{ + "foo": TestFile{Content: "foo"}, + }, + }, + { + expectFailure: true, + unixOnly: true, + files: map[string]interface{}{ + "foo": TestFile{Content: "foo"}, + }, + want: TestDir{ + "foo": TestSymlink{Target: "/xxx"}, + }, + }, + { + expectFailure: true, + unixOnly: true, + files: map[string]interface{}{ + "foo": TestSymlink{Target: "xxx"}, + }, + want: TestDir{ + "foo": TestSymlink{Target: "/yyy"}, + }, + }, + { + expectFailure: true, + files: map[string]interface{}{ + "foo": TestDir{ + "foo": TestFile{Content: "foo"}, + }, + }, + want: TestDir{ + "foo": TestFile{Content: "foo"}, + }, + }, + { + expectFailure: true, + files: map[string]interface{}{ + "foo": TestFile{Content: "foo"}, + }, + want: TestDir{ + "foo": TestDir{ + "foo": TestFile{Content: "foo"}, + }, + }, + }, + } + + for _, test := range tests { + t.Run("", func(t *testing.T) { + if test.unixOnly && runtime.GOOS == "windows" { + t.Skip("skip on Windows") + return + } + + tempdir, cleanup := restictest.TempDir(t) + defer cleanup() + + createFilesAt(t, tempdir, test.files) + + subtestT := testing.TB(t) + if test.expectFailure { + subtestT = &MockT{T: t} + } + + TestEnsureFiles(subtestT, tempdir, test.want) + + if test.expectFailure && !subtestT.(*MockT).HasFailed { + t.Fatal("expected failure of TestEnsureFiles not found") + } + }) + } +} + +func TestTestEnsureSnapshot(t *testing.T) { + var tests = []struct { + expectFailure bool + files map[string]interface{} + want TestDir + unixOnly bool + }{ + { + files: map[string]interface{}{ + "foo": TestFile{Content: "foo"}, + filepath.FromSlash("subdir/subfile"): TestFile{Content: "bar"}, + filepath.FromSlash("x/y/link"): TestSymlink{Target: filepath.FromSlash("../../foo")}, + }, + want: TestDir{ + "target": TestDir{ + "foo": TestFile{Content: "foo"}, + "subdir": TestDir{ + "subfile": TestFile{Content: "bar"}, + }, + "x": TestDir{ + "y": TestDir{ + "link": TestSymlink{Target: filepath.FromSlash("../../foo")}, + }, + }, + }, + }, + }, + { + expectFailure: true, + files: map[string]interface{}{ + "foo": TestFile{Content: "foo"}, + }, + want: TestDir{ + "target": TestDir{ + "bar": TestFile{Content: "foo"}, + }, + }, + }, + { + expectFailure: true, + files: map[string]interface{}{ + "foo": TestFile{Content: "foo"}, + "bar": TestFile{Content: "bar"}, + }, + want: TestDir{ + "target": TestDir{ + "foo": TestFile{Content: "foo"}, + }, + }, + }, + { + expectFailure: true, + files: map[string]interface{}{ + "foo": TestFile{Content: "foo"}, + }, + want: TestDir{ + "target": TestDir{ + "foo": TestFile{Content: "foo"}, + "bar": TestFile{Content: "bar"}, + }, + }, + }, + { + expectFailure: true, + files: map[string]interface{}{ + "foo": TestFile{Content: "foo"}, + }, + want: TestDir{ + "target": TestDir{ + "foo": TestDir{ + "foo": TestFile{Content: "foo"}, + }, + }, + }, + }, + { + expectFailure: true, + files: map[string]interface{}{ + "foo": TestSymlink{Target: filepath.FromSlash("x/y/z")}, + }, + want: TestDir{ + "target": TestDir{ + "foo": TestFile{Content: "foo"}, + }, + }, + }, + { + expectFailure: true, + unixOnly: true, + files: map[string]interface{}{ + "foo": TestSymlink{Target: filepath.FromSlash("x/y/z")}, + }, + want: TestDir{ + "target": TestDir{ + "foo": TestSymlink{Target: filepath.FromSlash("x/y/z2")}, + }, + }, + }, + { + expectFailure: true, + files: map[string]interface{}{ + "foo": TestFile{Content: "foo"}, + }, + want: TestDir{ + "target": TestDir{ + "foo": TestFile{Content: "xxx"}, + }, + }, + }, + } + + for _, test := range tests { + t.Run("", func(t *testing.T) { + if test.unixOnly && runtime.GOOS == "windows" { + t.Skip("skip on Windows") + return + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + tempdir, cleanup := restictest.TempDir(t) + defer cleanup() + + targetDir := filepath.Join(tempdir, "target") + err := fs.Mkdir(targetDir, 0700) + if err != nil { + t.Fatal(err) + } + + createFilesAt(t, targetDir, test.files) + + back := fs.TestChdir(t, targetDir) + defer back() + + repo, cleanup := repository.TestRepository(t) + defer cleanup() + + arch := New(repo) + _, id, err := arch.Snapshot(ctx, nil, []string{"."}, nil, "hostname", nil, time.Now()) + if err != nil { + t.Fatal(err) + } + + t.Logf("snapshot saved as %v", id.Str()) + + subtestT := testing.TB(t) + if test.expectFailure { + subtestT = &MockT{T: t} + } + + TestEnsureSnapshot(subtestT, repo, id, test.want) + + if test.expectFailure && !subtestT.(*MockT).HasFailed { + t.Fatal("expected failure of TestEnsureSnapshot not found") + } + }) + } +} diff --git a/internal/archiver/tree.go b/internal/archiver/tree.go new file mode 100644 index 000000000..8adca2cc3 --- /dev/null +++ b/internal/archiver/tree.go @@ -0,0 +1,254 @@ +package archiver + +import ( + "fmt" + + "github.com/restic/restic/internal/debug" + "github.com/restic/restic/internal/errors" + "github.com/restic/restic/internal/fs" +) + +// Tree recursively defines how a snapshot should look like when +// archived. +// +// When `Path` is set, this is a leaf node and the contents of `Path` should be +// inserted at this point in the tree. +// +// The attribute `Root` is used to distinguish between files/dirs which have +// the same name, but live in a separate directory on the local file system. +// +// `FileInfoPath` is used to extract metadata for intermediate (=non-leaf) +// trees. +type Tree struct { + Nodes map[string]Tree + Path string // where the files/dirs to be saved are found + FileInfoPath string // where the dir can be found that is not included itself, but its subdirs + Root string // parent directory of the tree +} + +// pathComponents returns all path components of p. If a virtual directory +// (volume name on Windows) is added, virtualPrefix is set to true. See the +// tests for examples. +func pathComponents(fs fs.FS, p string, includeRelative bool) (components []string, virtualPrefix bool) { + volume := fs.VolumeName(p) + + if !fs.IsAbs(p) { + if !includeRelative { + p = fs.Join(fs.Separator(), p) + } + } + + p = fs.Clean(p) + + for { + dir, file := fs.Dir(p), fs.Base(p) + + if p == dir { + break + } + + components = append(components, file) + p = dir + } + + // reverse components + for i := len(components)/2 - 1; i >= 0; i-- { + opp := len(components) - 1 - i + components[i], components[opp] = components[opp], components[i] + } + + if volume != "" { + // strip colon + if len(volume) == 2 && volume[1] == ':' { + volume = volume[:1] + } + + components = append([]string{volume}, components...) + virtualPrefix = true + } + + return components, virtualPrefix +} + +// rootDirectory returns the directory which contains the first element of target. +func rootDirectory(fs fs.FS, target string) string { + if target == "" { + return "" + } + + if fs.IsAbs(target) { + return fs.Join(fs.VolumeName(target), fs.Separator()) + } + + target = fs.Clean(target) + pc, _ := pathComponents(fs, target, true) + + rel := "." + for _, c := range pc { + if c == ".." { + rel = fs.Join(rel, c) + } + } + + return rel +} + +// Add adds a new file or directory to the tree. +func (t *Tree) Add(fs fs.FS, path string) error { + if path == "" { + panic("invalid path (empty string)") + } + + if t.Nodes == nil { + t.Nodes = make(map[string]Tree) + } + + pc, virtualPrefix := pathComponents(fs, path, false) + if len(pc) == 0 { + return errors.New("invalid path (no path components)") + } + + name := pc[0] + root := rootDirectory(fs, path) + tree := Tree{Root: root} + + origName := name + i := 0 + for { + other, ok := t.Nodes[name] + if !ok { + break + } + + i++ + if other.Root == root { + tree = other + break + } + + // resolve conflict and try again + name = fmt.Sprintf("%s-%d", origName, i) + continue + } + + if len(pc) > 1 { + subroot := fs.Join(root, origName) + if virtualPrefix { + // use the original root dir if this is a virtual directory (volume name on Windows) + subroot = root + } + err := tree.add(fs, path, subroot, pc[1:]) + if err != nil { + return err + } + tree.FileInfoPath = subroot + } else { + tree.Path = path + } + + t.Nodes[name] = tree + return nil +} + +// add adds a new target path into the tree. +func (t *Tree) add(fs fs.FS, target, root string, pc []string) error { + if len(pc) == 0 { + return errors.Errorf("invalid path %q", target) + } + + if t.Nodes == nil { + t.Nodes = make(map[string]Tree) + } + + name := pc[0] + + if len(pc) == 1 { + tree, ok := t.Nodes[name] + + if !ok { + t.Nodes[name] = Tree{Path: target} + return nil + } + + if tree.Path != "" { + return errors.Errorf("path is already set for target %v", target) + } + tree.Path = target + t.Nodes[name] = tree + return nil + } + + tree := Tree{} + if other, ok := t.Nodes[name]; ok { + tree = other + } + + subroot := fs.Join(root, name) + tree.FileInfoPath = subroot + + err := tree.add(fs, target, subroot, pc[1:]) + if err != nil { + return err + } + t.Nodes[name] = tree + + return nil +} + +func (t Tree) String() string { + return formatTree(t, "") +} + +// formatTree returns a text representation of the tree t. +func formatTree(t Tree, indent string) (s string) { + for name, node := range t.Nodes { + if node.Path != "" { + s += fmt.Sprintf("%v/%v, src %q\n", indent, name, node.Path) + continue + } + s += fmt.Sprintf("%v/%v, root %q, meta %q\n", indent, name, node.Root, node.FileInfoPath) + s += formatTree(node, indent+" ") + } + return s +} + +// prune removes sub-trees of leaf nodes. +func prune(t *Tree) { + // if the current tree is a leaf node (Path is set), remove all nodes, + // those are automatically included anyway. + if t.Path != "" && len(t.Nodes) > 0 { + t.FileInfoPath = "" + t.Nodes = nil + return + } + + for i, subtree := range t.Nodes { + prune(&subtree) + t.Nodes[i] = subtree + } +} + +// NewTree creates a Tree from the target files/directories. +func NewTree(fs fs.FS, targets []string) (*Tree, error) { + debug.Log("targets: %v", targets) + tree := &Tree{} + seen := make(map[string]struct{}) + for _, target := range targets { + target = fs.Clean(target) + + // skip duplicate targets + if _, ok := seen[target]; ok { + continue + } + seen[target] = struct{}{} + + err := tree.Add(fs, target) + if err != nil { + return nil, err + } + } + + prune(tree) + debug.Log("result:\n%v", tree) + return tree, nil +} diff --git a/internal/archiver/tree_test.go b/internal/archiver/tree_test.go new file mode 100644 index 000000000..f50bb510f --- /dev/null +++ b/internal/archiver/tree_test.go @@ -0,0 +1,341 @@ +package archiver + +import ( + "path/filepath" + "runtime" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/restic/restic/internal/fs" +) + +func TestPathComponents(t *testing.T) { + var tests = []struct { + p string + c []string + virtual bool + rel bool + win bool + }{ + { + p: "/foo/bar/baz", + c: []string{"foo", "bar", "baz"}, + }, + { + p: "/foo/bar/baz", + c: []string{"foo", "bar", "baz"}, + rel: true, + }, + { + p: "foo/bar/baz", + c: []string{"foo", "bar", "baz"}, + }, + { + p: "foo/bar/baz", + c: []string{"foo", "bar", "baz"}, + rel: true, + }, + { + p: "../foo/bar/baz", + c: []string{"foo", "bar", "baz"}, + }, + { + p: "../foo/bar/baz", + c: []string{"..", "foo", "bar", "baz"}, + rel: true, + }, + { + p: "c:/foo/bar/baz", + c: []string{"c", "foo", "bar", "baz"}, + virtual: true, + rel: true, + win: true, + }, + { + p: "c:/foo/../bar/baz", + c: []string{"c", "bar", "baz"}, + virtual: true, + win: true, + }, + { + p: `c:\foo\..\bar\baz`, + c: []string{"c", "bar", "baz"}, + virtual: true, + win: true, + }, + { + p: "c:/foo/../bar/baz", + c: []string{"c", "bar", "baz"}, + virtual: true, + rel: true, + win: true, + }, + { + p: `c:\foo\..\bar\baz`, + c: []string{"c", "bar", "baz"}, + virtual: true, + rel: true, + win: true, + }, + } + + for _, test := range tests { + t.Run("", func(t *testing.T) { + if test.win && runtime.GOOS != "windows" { + t.Skip("skip test on unix") + } + + c, v := pathComponents(fs.Local{}, filepath.FromSlash(test.p), test.rel) + if !cmp.Equal(test.c, c) { + t.Error(test.c, c) + } + + if v != test.virtual { + t.Errorf("unexpected virtual prefix count returned, want %v, got %v", test.virtual, v) + } + }) + } +} + +func TestRootDirectory(t *testing.T) { + var tests = []struct { + target string + root string + unix bool + win bool + }{ + {target: ".", root: "."}, + {target: "foo/bar/baz", root: "."}, + {target: "../foo/bar/baz", root: ".."}, + {target: "..", root: ".."}, + {target: "../../..", root: "../../.."}, + {target: "/home/foo", root: "/", unix: true}, + {target: "c:/home/foo", root: "c:/", win: true}, + {target: `c:\home\foo`, root: `c:\`, win: true}, + {target: "//host/share/foo", root: "//host/share/", win: true}, + } + + for _, test := range tests { + t.Run("", func(t *testing.T) { + if test.unix && runtime.GOOS == "windows" { + t.Skip("skip test on windows") + } + if test.win && runtime.GOOS != "windows" { + t.Skip("skip test on unix") + } + + root := rootDirectory(fs.Local{}, filepath.FromSlash(test.target)) + want := filepath.FromSlash(test.root) + if root != want { + t.Fatalf("wrong root directory, want %v, got %v", want, root) + } + }) + } +} + +func TestTree(t *testing.T) { + var tests = []struct { + targets []string + want Tree + unix bool + win bool + mustError bool + }{ + { + targets: []string{"foo"}, + want: Tree{Nodes: map[string]Tree{ + "foo": Tree{Path: "foo", Root: "."}, + }}, + }, + { + targets: []string{"foo", "bar", "baz"}, + want: Tree{Nodes: map[string]Tree{ + "foo": Tree{Path: "foo", Root: "."}, + "bar": Tree{Path: "bar", Root: "."}, + "baz": Tree{Path: "baz", Root: "."}, + }}, + }, + { + targets: []string{"foo/user1", "foo/user2", "foo/other"}, + want: Tree{Nodes: map[string]Tree{ + "foo": Tree{Root: ".", FileInfoPath: "foo", Nodes: map[string]Tree{ + "user1": Tree{Path: filepath.FromSlash("foo/user1")}, + "user2": Tree{Path: filepath.FromSlash("foo/user2")}, + "other": Tree{Path: filepath.FromSlash("foo/other")}, + }}, + }}, + }, + { + targets: []string{"foo/work/user1", "foo/work/user2"}, + want: Tree{Nodes: map[string]Tree{ + "foo": Tree{Root: ".", FileInfoPath: "foo", Nodes: map[string]Tree{ + "work": Tree{FileInfoPath: filepath.FromSlash("foo/work"), Nodes: map[string]Tree{ + "user1": Tree{Path: filepath.FromSlash("foo/work/user1")}, + "user2": Tree{Path: filepath.FromSlash("foo/work/user2")}, + }}, + }}, + }}, + }, + { + targets: []string{"foo/user1", "bar/user1", "foo/other"}, + want: Tree{Nodes: map[string]Tree{ + "foo": Tree{Root: ".", FileInfoPath: "foo", Nodes: map[string]Tree{ + "user1": Tree{Path: filepath.FromSlash("foo/user1")}, + "other": Tree{Path: filepath.FromSlash("foo/other")}, + }}, + "bar": Tree{Root: ".", FileInfoPath: "bar", Nodes: map[string]Tree{ + "user1": Tree{Path: filepath.FromSlash("bar/user1")}, + }}, + }}, + }, + { + targets: []string{"../work"}, + want: Tree{Nodes: map[string]Tree{ + "work": Tree{Root: "..", Path: filepath.FromSlash("../work")}, + }}, + }, + { + targets: []string{"../work/other"}, + want: Tree{Nodes: map[string]Tree{ + "work": Tree{Root: "..", FileInfoPath: filepath.FromSlash("../work"), Nodes: map[string]Tree{ + "other": Tree{Path: filepath.FromSlash("../work/other")}, + }}, + }}, + }, + { + targets: []string{"foo/user1", "../work/other", "foo/user2"}, + want: Tree{Nodes: map[string]Tree{ + "foo": Tree{Root: ".", FileInfoPath: "foo", Nodes: map[string]Tree{ + "user1": Tree{Path: filepath.FromSlash("foo/user1")}, + "user2": Tree{Path: filepath.FromSlash("foo/user2")}, + }}, + "work": Tree{Root: "..", FileInfoPath: filepath.FromSlash("../work"), Nodes: map[string]Tree{ + "other": Tree{Path: filepath.FromSlash("../work/other")}, + }}, + }}, + }, + { + targets: []string{"foo/user1", "../foo/other", "foo/user2"}, + want: Tree{Nodes: map[string]Tree{ + "foo": Tree{Root: ".", FileInfoPath: "foo", Nodes: map[string]Tree{ + "user1": Tree{Path: filepath.FromSlash("foo/user1")}, + "user2": Tree{Path: filepath.FromSlash("foo/user2")}, + }}, + "foo-1": Tree{Root: "..", FileInfoPath: filepath.FromSlash("../foo"), Nodes: map[string]Tree{ + "other": Tree{Path: filepath.FromSlash("../foo/other")}, + }}, + }}, + }, + { + targets: []string{"foo/work", "foo/work/user2"}, + want: Tree{Nodes: map[string]Tree{ + "foo": Tree{Root: ".", FileInfoPath: "foo", Nodes: map[string]Tree{ + "work": Tree{ + Path: filepath.FromSlash("foo/work"), + }, + }}, + }}, + }, + { + targets: []string{"foo/work/user2", "foo/work"}, + want: Tree{Nodes: map[string]Tree{ + "foo": Tree{Root: ".", FileInfoPath: "foo", Nodes: map[string]Tree{ + "work": Tree{ + Path: filepath.FromSlash("foo/work"), + }, + }}, + }}, + }, + { + targets: []string{"foo/work/user2/data/secret", "foo"}, + want: Tree{Nodes: map[string]Tree{ + "foo": Tree{Root: ".", Path: "foo"}, + }}, + }, + { + unix: true, + targets: []string{"/mnt/driveA", "/mnt/driveA/work/driveB"}, + want: Tree{Nodes: map[string]Tree{ + "mnt": Tree{Root: "/", FileInfoPath: filepath.FromSlash("/mnt"), Nodes: map[string]Tree{ + "driveA": Tree{ + Path: filepath.FromSlash("/mnt/driveA"), + }, + }}, + }}, + }, + { + targets: []string{"foo/work/user", "foo/work/user"}, + want: Tree{Nodes: map[string]Tree{ + "foo": Tree{Root: ".", FileInfoPath: "foo", Nodes: map[string]Tree{ + "work": Tree{FileInfoPath: filepath.FromSlash("foo/work"), Nodes: map[string]Tree{ + "user": Tree{Path: filepath.FromSlash("foo/work/user")}, + }}, + }}, + }}, + }, + { + targets: []string{"./foo/work/user", "foo/work/user"}, + want: Tree{Nodes: map[string]Tree{ + "foo": Tree{Root: ".", FileInfoPath: "foo", Nodes: map[string]Tree{ + "work": Tree{FileInfoPath: filepath.FromSlash("foo/work"), Nodes: map[string]Tree{ + "user": Tree{Path: filepath.FromSlash("foo/work/user")}, + }}, + }}, + }}, + }, + { + win: true, + targets: []string{`c:\users\foobar\temp`}, + want: Tree{Nodes: map[string]Tree{ + "c": Tree{Root: `c:\`, FileInfoPath: `c:\`, Nodes: map[string]Tree{ + "users": Tree{FileInfoPath: `c:\users`, Nodes: map[string]Tree{ + "foobar": Tree{FileInfoPath: `c:\users\foobar`, Nodes: map[string]Tree{ + "temp": Tree{Path: `c:\users\foobar\temp`}, + }}, + }}, + }}, + }}, + }, + { + targets: []string{"."}, + mustError: true, + }, + { + targets: []string{".."}, + mustError: true, + }, + { + targets: []string{"../.."}, + mustError: true, + }, + } + + for _, test := range tests { + t.Run("", func(t *testing.T) { + if test.unix && runtime.GOOS == "windows" { + t.Skip("skip test on windows") + } + + if test.win && runtime.GOOS != "windows" { + t.Skip("skip test on unix") + } + + tree, err := NewTree(fs.Local{}, test.targets) + if test.mustError { + if err == nil { + t.Fatal("expected error, got nil") + } + t.Logf("found expected error: %v", err) + return + } + + if err != nil { + t.Fatal(err) + } + + if !cmp.Equal(&test.want, tree) { + t.Error(cmp.Diff(&test.want, tree)) + } + }) + } +} From 38926d85760ab7ced41c088c94237db634088caf Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 25 Mar 2018 23:36:31 +0200 Subject: [PATCH 21/30] Use new archiver code in tests --- cmd/restic/integration_fuse_test.go | 6 +- cmd/restic/integration_test.go | 257 ++++------------------------ internal/archiver/testing.go | 12 +- internal/archiver/testing_test.go | 11 +- internal/checker/checker_test.go | 7 +- 5 files changed, 58 insertions(+), 235 deletions(-) diff --git a/cmd/restic/integration_fuse_test.go b/cmd/restic/integration_fuse_test.go index a341ff4e6..45a9d4eb0 100644 --- a/cmd/restic/integration_fuse_test.go +++ b/cmd/restic/integration_fuse_test.go @@ -171,7 +171,7 @@ func TestMount(t *testing.T) { rtest.SetupTarTestFixture(t, env.testdata, filepath.Join("testdata", "backup-data.tar.gz")) // first backup - testRunBackup(t, []string{env.testdata}, BackupOptions{}, env.gopts) + testRunBackup(t, "", []string{env.testdata}, BackupOptions{}, env.gopts) snapshotIDs := testRunList(t, "snapshots", env.gopts) rtest.Assert(t, len(snapshotIDs) == 1, "expected one snapshot, got %v", snapshotIDs) @@ -179,7 +179,7 @@ func TestMount(t *testing.T) { checkSnapshots(t, env.gopts, repo, env.mountpoint, env.repo, snapshotIDs, 2) // second backup, implicit incremental - testRunBackup(t, []string{env.testdata}, BackupOptions{}, env.gopts) + testRunBackup(t, "", []string{env.testdata}, BackupOptions{}, env.gopts) snapshotIDs = testRunList(t, "snapshots", env.gopts) rtest.Assert(t, len(snapshotIDs) == 2, "expected two snapshots, got %v", snapshotIDs) @@ -188,7 +188,7 @@ func TestMount(t *testing.T) { // third backup, explicit incremental bopts := BackupOptions{Parent: snapshotIDs[0].String()} - testRunBackup(t, []string{env.testdata}, bopts, env.gopts) + testRunBackup(t, "", []string{env.testdata}, bopts, env.gopts) snapshotIDs = testRunList(t, "snapshots", env.gopts) rtest.Assert(t, len(snapshotIDs) == 3, "expected three snapshots, got %v", snapshotIDs) diff --git a/cmd/restic/integration_test.go b/cmd/restic/integration_test.go index 8b90c3cd1..357cd8242 100644 --- a/cmd/restic/integration_test.go +++ b/cmd/restic/integration_test.go @@ -17,9 +17,9 @@ import ( "testing" "time" - "github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/filter" + "github.com/restic/restic/internal/fs" "github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/restic" rtest "github.com/restic/restic/internal/test" @@ -51,9 +51,13 @@ func testRunInit(t testing.TB, opts GlobalOptions) { t.Logf("repository initialized at %v", opts.Repo) } -func testRunBackup(t testing.TB, target []string, opts BackupOptions, gopts GlobalOptions) { +func testRunBackup(t testing.TB, dir string, target []string, opts BackupOptions, gopts GlobalOptions) { gopts.stdout = ioutil.Discard - t.Logf("backing up %v", target) + t.Logf("backing up %v in %v", target, dir) + if dir != "" { + cleanup := fs.TestChdir(t, dir) + defer cleanup() + } rtest.OK(t, runBackup(opts, gopts, target)) } @@ -220,7 +224,7 @@ func TestBackup(t *testing.T) { opts := BackupOptions{} // first backup - testRunBackup(t, []string{env.testdata}, opts, env.gopts) + testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts) snapshotIDs := testRunList(t, "snapshots", env.gopts) rtest.Assert(t, len(snapshotIDs) == 1, "expected one snapshot, got %v", snapshotIDs) @@ -229,7 +233,7 @@ func TestBackup(t *testing.T) { stat1 := dirStats(env.repo) // second backup, implicit incremental - testRunBackup(t, []string{env.testdata}, opts, env.gopts) + testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts) snapshotIDs = testRunList(t, "snapshots", env.gopts) rtest.Assert(t, len(snapshotIDs) == 2, "expected two snapshots, got %v", snapshotIDs) @@ -243,7 +247,7 @@ func TestBackup(t *testing.T) { testRunCheck(t, env.gopts) // third backup, explicit incremental opts.Parent = snapshotIDs[0].String() - testRunBackup(t, []string{env.testdata}, opts, env.gopts) + testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts) snapshotIDs = testRunList(t, "snapshots", env.gopts) rtest.Assert(t, len(snapshotIDs) == 3, "expected three snapshots, got %v", snapshotIDs) @@ -297,198 +301,7 @@ func TestBackupNonExistingFile(t *testing.T) { opts := BackupOptions{} - testRunBackup(t, dirs, opts, env.gopts) -} - -func TestBackupMissingFile1(t *testing.T) { - env, cleanup := withTestEnvironment(t) - defer cleanup() - - datafile := filepath.Join("testdata", "backup-data.tar.gz") - fd, err := os.Open(datafile) - if os.IsNotExist(errors.Cause(err)) { - t.Skipf("unable to find data file %q, skipping", datafile) - return - } - rtest.OK(t, err) - rtest.OK(t, fd.Close()) - - rtest.SetupTarTestFixture(t, env.testdata, datafile) - - testRunInit(t, env.gopts) - globalOptions.stderr = ioutil.Discard - defer func() { - globalOptions.stderr = os.Stderr - }() - - 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 - - rtest.OK(t, os.Remove(filepath.Join(env.testdata, "0", "0", "9", "37"))) - }) - - opts := BackupOptions{} - - testRunBackup(t, []string{env.testdata}, opts, env.gopts) - testRunCheck(t, env.gopts) - - rtest.Assert(t, ranHook, "hook did not run") - debug.RemoveHook("pipe.walk1") -} - -func TestBackupMissingFile2(t *testing.T) { - env, cleanup := withTestEnvironment(t) - defer cleanup() - - datafile := filepath.Join("testdata", "backup-data.tar.gz") - fd, err := os.Open(datafile) - if os.IsNotExist(errors.Cause(err)) { - t.Skipf("unable to find data file %q, skipping", datafile) - return - } - rtest.OK(t, err) - rtest.OK(t, fd.Close()) - - rtest.SetupTarTestFixture(t, env.testdata, datafile) - - testRunInit(t, env.gopts) - - globalOptions.stderr = ioutil.Discard - defer func() { - globalOptions.stderr = os.Stderr - }() - - 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 - - rtest.OK(t, os.Remove(filepath.Join(env.testdata, "0", "0", "9", "37"))) - }) - - opts := BackupOptions{} - - testRunBackup(t, []string{env.testdata}, opts, env.gopts) - testRunCheck(t, env.gopts) - - rtest.Assert(t, ranHook, "hook did not run") - debug.RemoveHook("pipe.walk2") -} - -func TestBackupChangedFile(t *testing.T) { - env, cleanup := withTestEnvironment(t) - defer cleanup() - - datafile := filepath.Join("testdata", "backup-data.tar.gz") - fd, err := os.Open(datafile) - if os.IsNotExist(errors.Cause(err)) { - t.Skipf("unable to find data file %q, skipping", datafile) - return - } - rtest.OK(t, err) - rtest.OK(t, fd.Close()) - - rtest.SetupTarTestFixture(t, env.testdata, datafile) - - testRunInit(t, env.gopts) - - globalOptions.stderr = ioutil.Discard - defer func() { - globalOptions.stderr = os.Stderr - }() - - modFile := filepath.Join(env.testdata, "0", "0", "9", "18") - - ranHook := false - debug.Hook("archiver.SaveFile", func(context interface{}) { - pathname := context.(string) - - if pathname != modFile { - return - } - - t.Logf("in hook, modifying test file %v", modFile) - ranHook = true - - rtest.OK(t, ioutil.WriteFile(modFile, []byte("modified"), 0600)) - }) - - opts := BackupOptions{} - - testRunBackup(t, []string{env.testdata}, opts, env.gopts) - testRunCheck(t, env.gopts) - - rtest.Assert(t, ranHook, "hook did not run") - debug.RemoveHook("archiver.SaveFile") -} - -func TestBackupDirectoryError(t *testing.T) { - env, cleanup := withTestEnvironment(t) - defer cleanup() - - datafile := filepath.Join("testdata", "backup-data.tar.gz") - fd, err := os.Open(datafile) - if os.IsNotExist(errors.Cause(err)) { - t.Skipf("unable to find data file %q, skipping", datafile) - return - } - rtest.OK(t, err) - rtest.OK(t, fd.Close()) - - rtest.SetupTarTestFixture(t, env.testdata, datafile) - - testRunInit(t, env.gopts) - - globalOptions.stderr = ioutil.Discard - defer func() { - globalOptions.stderr = os.Stderr - }() - - ranHook := false - - testdir := filepath.Join(env.testdata, "0", "0", "9") - - // install hook that removes the dir right before readdirnames() - debug.Hook("pipe.readdirnames", func(context interface{}) { - path := context.(string) - - if path != testdir { - return - } - - t.Logf("in hook, removing test file %v", testdir) - ranHook = true - - rtest.OK(t, os.RemoveAll(testdir)) - }) - - testRunBackup(t, []string{filepath.Join(env.testdata, "0", "0")}, BackupOptions{}, env.gopts) - testRunCheck(t, env.gopts) - - rtest.Assert(t, ranHook, "hook did not run") - debug.RemoveHook("pipe.walk2") - - snapshots := testRunList(t, "snapshots", env.gopts) - rtest.Assert(t, len(snapshots) > 0, - "no snapshots found in repo (%v)", datafile) - - files := testRunLs(t, env.gopts, snapshots[0].String()) - - rtest.Assert(t, len(files) > 1, "snapshot is empty") + testRunBackup(t, "", dirs, opts, env.gopts) } func includes(haystack []string, needle string) bool { @@ -553,21 +366,21 @@ func TestBackupExclude(t *testing.T) { opts := BackupOptions{} - testRunBackup(t, []string{datadir}, opts, env.gopts) + testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts) snapshots, snapshotID := lastSnapshot(snapshots, loadSnapshotMap(t, env.gopts)) files := testRunLs(t, env.gopts, snapshotID) rtest.Assert(t, includes(files, filepath.Join(string(filepath.Separator), "testdata", "foo.tar.gz")), "expected file %q in first snapshot, but it's not included", "foo.tar.gz") opts.Excludes = []string{"*.tar.gz"} - testRunBackup(t, []string{datadir}, opts, env.gopts) + testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts) snapshots, snapshotID = lastSnapshot(snapshots, loadSnapshotMap(t, env.gopts)) files = testRunLs(t, env.gopts, snapshotID) rtest.Assert(t, !includes(files, filepath.Join(string(filepath.Separator), "testdata", "foo.tar.gz")), "expected file %q not in first snapshot, but it's included", "foo.tar.gz") opts.Excludes = []string{"*.tar.gz", "private/secret"} - testRunBackup(t, []string{datadir}, opts, env.gopts) + testRunBackup(t, filepath.Dir(env.testdata), []string{"testdata"}, opts, env.gopts) _, snapshotID = lastSnapshot(snapshots, loadSnapshotMap(t, env.gopts)) files = testRunLs(t, env.gopts, snapshotID) rtest.Assert(t, !includes(files, filepath.Join(string(filepath.Separator), "testdata", "foo.tar.gz")), @@ -617,13 +430,13 @@ func TestIncrementalBackup(t *testing.T) { opts := BackupOptions{} - testRunBackup(t, []string{datadir}, opts, env.gopts) + testRunBackup(t, "", []string{datadir}, opts, env.gopts) testRunCheck(t, env.gopts) stat1 := dirStats(env.repo) rtest.OK(t, appendRandomData(testfile, incrementalSecondWrite)) - testRunBackup(t, []string{datadir}, opts, env.gopts) + testRunBackup(t, "", []string{datadir}, opts, env.gopts) testRunCheck(t, env.gopts) stat2 := dirStats(env.repo) if stat2.size-stat1.size > incrementalFirstWrite { @@ -633,7 +446,7 @@ func TestIncrementalBackup(t *testing.T) { rtest.OK(t, appendRandomData(testfile, incrementalThirdWrite)) - testRunBackup(t, []string{datadir}, opts, env.gopts) + testRunBackup(t, "", []string{datadir}, opts, env.gopts) testRunCheck(t, env.gopts) stat3 := dirStats(env.repo) if stat3.size-stat2.size > incrementalFirstWrite { @@ -652,7 +465,7 @@ func TestBackupTags(t *testing.T) { opts := BackupOptions{} - testRunBackup(t, []string{env.testdata}, opts, env.gopts) + testRunBackup(t, "", []string{env.testdata}, opts, env.gopts) testRunCheck(t, env.gopts) newest, _ := testRunSnapshots(t, env.gopts) rtest.Assert(t, newest != nil, "expected a new backup, got nil") @@ -661,7 +474,7 @@ func TestBackupTags(t *testing.T) { parent := newest opts.Tags = []string{"NL"} - testRunBackup(t, []string{env.testdata}, opts, env.gopts) + testRunBackup(t, "", []string{env.testdata}, opts, env.gopts) testRunCheck(t, env.gopts) newest, _ = testRunSnapshots(t, env.gopts) rtest.Assert(t, newest != nil, "expected a new backup, got nil") @@ -684,7 +497,7 @@ func TestTag(t *testing.T) { testRunInit(t, env.gopts) rtest.SetupTarTestFixture(t, env.testdata, datafile) - testRunBackup(t, []string{env.testdata}, BackupOptions{}, env.gopts) + testRunBackup(t, "", []string{env.testdata}, BackupOptions{}, env.gopts) testRunCheck(t, env.gopts) newest, _ := testRunSnapshots(t, env.gopts) rtest.Assert(t, newest != nil, "expected a new backup, got nil") @@ -860,7 +673,7 @@ func TestRestoreFilter(t *testing.T) { opts := BackupOptions{} - testRunBackup(t, []string{env.testdata}, opts, env.gopts) + testRunBackup(t, filepath.Dir(env.testdata), []string{filepath.Base(env.testdata)}, opts, env.gopts) testRunCheck(t, env.gopts) snapshotID := testRunList(t, "snapshots", env.gopts)[0] @@ -900,7 +713,7 @@ func TestRestore(t *testing.T) { opts := BackupOptions{} - testRunBackup(t, []string{env.testdata}, opts, env.gopts) + testRunBackup(t, filepath.Dir(env.testdata), []string{filepath.Base(env.testdata)}, opts, env.gopts) testRunCheck(t, env.gopts) // Restore latest without any filters @@ -923,12 +736,12 @@ func TestRestoreLatest(t *testing.T) { opts := BackupOptions{} - testRunBackup(t, []string{env.testdata}, opts, env.gopts) + testRunBackup(t, filepath.Dir(env.testdata), []string{filepath.Base(env.testdata)}, opts, env.gopts) testRunCheck(t, env.gopts) os.Remove(p) rtest.OK(t, appendRandomData(p, 101)) - testRunBackup(t, []string{env.testdata}, opts, env.gopts) + testRunBackup(t, filepath.Dir(env.testdata), []string{filepath.Base(env.testdata)}, opts, env.gopts) testRunCheck(t, env.gopts) // Restore latest without any filters @@ -939,13 +752,13 @@ func TestRestoreLatest(t *testing.T) { p1 := filepath.Join(env.testdata, "p1/testfile.c") rtest.OK(t, os.MkdirAll(filepath.Dir(p1), 0755)) rtest.OK(t, appendRandomData(p1, 102)) - testRunBackup(t, []string{filepath.Dir(p1)}, opts, env.gopts) + testRunBackup(t, env.testdata, []string{"p1"}, opts, env.gopts) testRunCheck(t, env.gopts) p2 := filepath.Join(env.testdata, "p2/testfile.c") rtest.OK(t, os.MkdirAll(filepath.Dir(p2), 0755)) rtest.OK(t, appendRandomData(p2, 103)) - testRunBackup(t, []string{filepath.Dir(p2)}, opts, env.gopts) + testRunBackup(t, env.testdata, []string{"p2"}, opts, env.gopts) testRunCheck(t, env.gopts) p1rAbs := filepath.Join(env.base, "restore1", "p1/testfile.c") @@ -1018,7 +831,7 @@ func TestRestoreNoMetadataOnIgnoredIntermediateDirs(t *testing.T) { opts := BackupOptions{} - testRunBackup(t, []string{env.testdata}, opts, env.gopts) + testRunBackup(t, filepath.Dir(env.testdata), []string{filepath.Base(env.testdata)}, opts, env.gopts) testRunCheck(t, env.gopts) snapshotID := testRunList(t, "snapshots", env.gopts)[0] @@ -1056,7 +869,7 @@ func TestFind(t *testing.T) { opts := BackupOptions{} - testRunBackup(t, []string{env.testdata}, opts, env.gopts) + testRunBackup(t, "", []string{env.testdata}, opts, env.gopts) testRunCheck(t, env.gopts) results := testRunFind(t, false, env.gopts, "unexistingfile") @@ -1096,7 +909,7 @@ func TestFindJSON(t *testing.T) { opts := BackupOptions{} - testRunBackup(t, []string{env.testdata}, opts, env.gopts) + testRunBackup(t, "", []string{env.testdata}, opts, env.gopts) testRunCheck(t, env.gopts) results := testRunFind(t, true, env.gopts, "unexistingfile") @@ -1199,13 +1012,13 @@ func TestPrune(t *testing.T) { rtest.SetupTarTestFixture(t, env.testdata, datafile) opts := BackupOptions{} - testRunBackup(t, []string{filepath.Join(env.testdata, "0", "0", "9")}, opts, env.gopts) + testRunBackup(t, "", []string{filepath.Join(env.testdata, "0", "0", "9")}, opts, env.gopts) firstSnapshot := testRunList(t, "snapshots", env.gopts) rtest.Assert(t, len(firstSnapshot) == 1, "expected one snapshot, got %v", firstSnapshot) - testRunBackup(t, []string{filepath.Join(env.testdata, "0", "0", "9", "2")}, opts, env.gopts) - testRunBackup(t, []string{filepath.Join(env.testdata, "0", "0", "9", "3")}, opts, env.gopts) + testRunBackup(t, "", []string{filepath.Join(env.testdata, "0", "0", "9", "2")}, opts, env.gopts) + testRunBackup(t, "", []string{filepath.Join(env.testdata, "0", "0", "9", "3")}, opts, env.gopts) snapshotIDs := testRunList(t, "snapshots", env.gopts) rtest.Assert(t, len(snapshotIDs) == 3, @@ -1239,7 +1052,7 @@ func TestHardLink(t *testing.T) { opts := BackupOptions{} // first backup - testRunBackup(t, []string{env.testdata}, opts, env.gopts) + testRunBackup(t, filepath.Dir(env.testdata), []string{filepath.Base(env.testdata)}, opts, env.gopts) snapshotIDs := testRunList(t, "snapshots", env.gopts) rtest.Assert(t, len(snapshotIDs) == 1, "expected one snapshot, got %v", snapshotIDs) @@ -1333,7 +1146,7 @@ func TestQuietBackup(t *testing.T) { opts := BackupOptions{} env.gopts.Quiet = false - testRunBackup(t, []string{env.testdata}, opts, env.gopts) + testRunBackup(t, "", []string{env.testdata}, opts, env.gopts) snapshotIDs := testRunList(t, "snapshots", env.gopts) rtest.Assert(t, len(snapshotIDs) == 1, "expected one snapshot, got %v", snapshotIDs) @@ -1341,7 +1154,7 @@ func TestQuietBackup(t *testing.T) { testRunCheck(t, env.gopts) env.gopts.Quiet = true - testRunBackup(t, []string{env.testdata}, opts, env.gopts) + testRunBackup(t, "", []string{env.testdata}, opts, env.gopts) snapshotIDs = testRunList(t, "snapshots", env.gopts) rtest.Assert(t, len(snapshotIDs) == 2, "expected two snapshots, got %v", snapshotIDs) diff --git a/internal/archiver/testing.go b/internal/archiver/testing.go index e4bace51a..bdb122d69 100644 --- a/internal/archiver/testing.go +++ b/internal/archiver/testing.go @@ -19,8 +19,16 @@ import ( // TestSnapshot creates a new snapshot of path. func TestSnapshot(t testing.TB, repo restic.Repository, path string, parent *restic.ID) *restic.Snapshot { - arch := New(repo) - sn, _, err := arch.Snapshot(context.TODO(), nil, []string{path}, []string{"test"}, "localhost", parent, time.Now()) + arch := New(repo, fs.Local{}, Options{}) + opts := SnapshotOptions{ + Time: time.Now(), + Hostname: "localhost", + Tags: []string{"test"}, + } + if parent != nil { + opts.ParentSnapshot = *parent + } + sn, _, err := arch.Snapshot(context.TODO(), []string{path}, opts) if err != nil { t.Fatal(err) } diff --git a/internal/archiver/testing_test.go b/internal/archiver/testing_test.go index e874b8316..2f0a5f5d8 100644 --- a/internal/archiver/testing_test.go +++ b/internal/archiver/testing_test.go @@ -496,14 +496,19 @@ func TestTestEnsureSnapshot(t *testing.T) { createFilesAt(t, targetDir, test.files) - back := fs.TestChdir(t, targetDir) + back := fs.TestChdir(t, tempdir) defer back() repo, cleanup := repository.TestRepository(t) defer cleanup() - arch := New(repo) - _, id, err := arch.Snapshot(ctx, nil, []string{"."}, nil, "hostname", nil, time.Now()) + arch := New(repo, fs.Local{}, Options{}) + opts := SnapshotOptions{ + Time: time.Now(), + Hostname: "localhost", + Tags: []string{"test"}, + } + _, id, err := arch.Snapshot(ctx, []string{"."}, opts) if err != nil { t.Fatal(err) } diff --git a/internal/checker/checker_test.go b/internal/checker/checker_test.go index 601407636..09ff15a10 100644 --- a/internal/checker/checker_test.go +++ b/internal/checker/checker_test.go @@ -9,7 +9,6 @@ import ( "path/filepath" "sort" "testing" - "time" "github.com/restic/restic/internal/archiver" "github.com/restic/restic/internal/checker" @@ -326,10 +325,8 @@ func TestCheckerModifiedData(t *testing.T) { repo, cleanup := repository.TestRepository(t) defer cleanup() - arch := archiver.New(repo) - _, id, err := arch.Snapshot(context.TODO(), nil, []string{"."}, nil, "localhost", nil, time.Now()) - test.OK(t, err) - t.Logf("archived as %v", id.Str()) + sn := archiver.TestSnapshot(t, repo, ".", nil) + t.Logf("archived as %v", sn.ID().Str()) beError := &errorBackend{Backend: repo.Backend()} checkRepo := repository.New(beError) From a5c0cf23244c6da77434cc93a2eb3aa80c5dd1e4 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Tue, 10 Apr 2018 23:52:32 +0200 Subject: [PATCH 22/30] Add workaround for symlinked temp dir on darwin Chdir to the tempdir, then use os.Getwd() to get the name that filepath.Abs() uses (and stores in the Snapshot). --- cmd/restic/integration_test.go | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/cmd/restic/integration_test.go b/cmd/restic/integration_test.go index 357cd8242..88c154f5c 100644 --- a/cmd/restic/integration_test.go +++ b/cmd/restic/integration_test.go @@ -736,12 +736,22 @@ func TestRestoreLatest(t *testing.T) { opts := BackupOptions{} - testRunBackup(t, filepath.Dir(env.testdata), []string{filepath.Base(env.testdata)}, opts, env.gopts) + // chdir manually here so we can get the current directory. This is not the + // same as the temp dir returned by ioutil.TempDir() on darwin. + back := fs.TestChdir(t, filepath.Dir(env.testdata)) + defer back() + + curdir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + testRunBackup(t, "", []string{filepath.Base(env.testdata)}, opts, env.gopts) testRunCheck(t, env.gopts) os.Remove(p) rtest.OK(t, appendRandomData(p, 101)) - testRunBackup(t, filepath.Dir(env.testdata), []string{filepath.Base(env.testdata)}, opts, env.gopts) + testRunBackup(t, "", []string{filepath.Base(env.testdata)}, opts, env.gopts) testRunCheck(t, env.gopts) // Restore latest without any filters @@ -749,16 +759,18 @@ func TestRestoreLatest(t *testing.T) { rtest.OK(t, testFileSize(filepath.Join(env.base, "restore0", "testdata", "testfile.c"), int64(101))) // Setup test files in different directories backed up in different snapshots - p1 := filepath.Join(env.testdata, "p1/testfile.c") + p1 := filepath.Join(curdir, filepath.FromSlash("p1/testfile.c")) + rtest.OK(t, os.MkdirAll(filepath.Dir(p1), 0755)) rtest.OK(t, appendRandomData(p1, 102)) - testRunBackup(t, env.testdata, []string{"p1"}, opts, env.gopts) + testRunBackup(t, "", []string{"p1"}, opts, env.gopts) testRunCheck(t, env.gopts) - p2 := filepath.Join(env.testdata, "p2/testfile.c") + p2 := filepath.Join(curdir, filepath.FromSlash("p2/testfile.c")) + rtest.OK(t, os.MkdirAll(filepath.Dir(p2), 0755)) rtest.OK(t, appendRandomData(p2, 103)) - testRunBackup(t, env.testdata, []string{"p2"}, opts, env.gopts) + testRunBackup(t, "", []string{"p2"}, opts, env.gopts) testRunCheck(t, env.gopts) p1rAbs := filepath.Join(env.base, "restore1", "p1/testfile.c") From 9fac2ca832459801a730ad3cfcf24af2c23db22d Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sat, 21 Apr 2018 22:07:14 +0200 Subject: [PATCH 23/30] Add flags to set verbosity --- cmd/restic/global.go | 17 +++++++++++++---- cmd/restic/main.go | 15 +++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/cmd/restic/global.go b/cmd/restic/global.go index 0c3d805b2..627e4c8f9 100644 --- a/cmd/restic/global.go +++ b/cmd/restic/global.go @@ -43,6 +43,8 @@ type GlobalOptions struct { Repo string PasswordFile string Quiet bool + Verbose bool + Debug bool NoLock bool JSON bool CacheDir string @@ -59,6 +61,13 @@ type GlobalOptions struct { stdout io.Writer stderr io.Writer + // verbosity is set as follows: + // 0 means: don't print any messages except errors, this is used when --quiet is specified + // 1 is the default: print essential messages + // 2 means: print more messages, report minor things, this is used when --verbose is specified + // 3 means: print very detailed debug messages, this is used when --debug is specified + verbosity uint + Options []string extended options.Options @@ -81,6 +90,8 @@ func init() { f.StringVarP(&globalOptions.Repo, "repo", "r", os.Getenv("RESTIC_REPOSITORY"), "repository to backup to or restore from (default: $RESTIC_REPOSITORY)") f.StringVarP(&globalOptions.PasswordFile, "password-file", "p", os.Getenv("RESTIC_PASSWORD_FILE"), "read the repository password from a file (default: $RESTIC_PASSWORD_FILE)") f.BoolVarP(&globalOptions.Quiet, "quiet", "q", false, "do not output comprehensive progress report") + f.BoolVarP(&globalOptions.Verbose, "verbose", "v", false, "be verbose") + f.BoolVar(&globalOptions.Debug, "debug", false, "be very verbose") f.BoolVar(&globalOptions.NoLock, "no-lock", false, "do not lock the repo, this allows some operations on read-only repos") f.BoolVarP(&globalOptions.JSON, "json", "", false, "set output mode to JSON for commands that support it") f.StringVar(&globalOptions.CacheDir, "cache-dir", "", "set the cache directory") @@ -173,11 +184,9 @@ func Printf(format string, args ...interface{}) { // Verbosef calls Printf to write the message when the verbose flag is set. func Verbosef(format string, args ...interface{}) { - if globalOptions.Quiet { - return + if globalOptions.verbosity >= 1 { + Printf(format, args...) } - - Printf(format, args...) } // PrintProgress wraps fmt.Printf to handle the difference in writing progress diff --git a/cmd/restic/main.go b/cmd/restic/main.go index ca1067cda..c1a42bd90 100644 --- a/cmd/restic/main.go +++ b/cmd/restic/main.go @@ -30,6 +30,21 @@ directories in an encrypted repository stored on different backends. DisableAutoGenTag: true, PersistentPreRunE: func(c *cobra.Command, args []string) error { + // set verbosity + globalOptions.verbosity = 1 + if globalOptions.Quiet && (globalOptions.Verbose || globalOptions.Debug) { + return errors.Fatal("--quiet and --verbose or --debug cannot be specified at the same time") + } + + switch { + case globalOptions.Quiet: + globalOptions.verbosity = 0 + case globalOptions.Verbose: + globalOptions.verbosity = 2 + case globalOptions.Debug: + globalOptions.verbosity = 3 + } + // parse extended options opts, err := options.Parse(globalOptions.Options) if err != nil { From 1af96fc6dd782176983386f8fa602552da9f5fbf Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 22 Apr 2018 11:57:11 +0200 Subject: [PATCH 24/30] Add termstatus --- internal/ui/termstatus/status.go | 281 +++++++++++++++++++++ internal/ui/termstatus/terminal_posix.go | 33 +++ internal/ui/termstatus/terminal_unix.go | 34 +++ internal/ui/termstatus/terminal_windows.go | 131 ++++++++++ 4 files changed, 479 insertions(+) create mode 100644 internal/ui/termstatus/status.go create mode 100644 internal/ui/termstatus/terminal_posix.go create mode 100644 internal/ui/termstatus/terminal_unix.go create mode 100644 internal/ui/termstatus/terminal_windows.go diff --git a/internal/ui/termstatus/status.go b/internal/ui/termstatus/status.go new file mode 100644 index 000000000..25fdcc341 --- /dev/null +++ b/internal/ui/termstatus/status.go @@ -0,0 +1,281 @@ +package termstatus + +import ( + "bufio" + "bytes" + "context" + "fmt" + "io" + "os" + "strings" +) + +// Terminal is used to write messages and display status lines which can be +// updated. When the output is redirected to a file, the status lines are not +// printed. +type Terminal struct { + wr *bufio.Writer + fd uintptr + errWriter io.Writer + buf *bytes.Buffer + msg chan message + status chan status + canUpdateStatus bool + clearLines clearLinesFunc +} + +type clearLinesFunc func(wr io.Writer, fd uintptr, n int) + +type message struct { + line string + err bool +} + +type status struct { + lines []string +} + +type fder interface { + Fd() uintptr +} + +// New returns a new Terminal for wr. A goroutine is started to update the +// terminal. It is terminated when ctx is cancelled. When wr is redirected to +// a file (e.g. via shell output redirection) or is just an io.Writer (not the +// open *os.File for stdout), no status lines are printed. The status lines and +// normal output (via Print/Printf) are written to wr, error messages are +// written to errWriter. +func New(wr io.Writer, errWriter io.Writer) *Terminal { + t := &Terminal{ + wr: bufio.NewWriter(wr), + errWriter: errWriter, + buf: bytes.NewBuffer(nil), + msg: make(chan message), + status: make(chan status), + } + + if d, ok := wr.(fder); ok && canUpdateStatus(d.Fd()) { + // only use the fancy status code when we're running on a real terminal. + t.canUpdateStatus = true + t.fd = d.Fd() + t.clearLines = clearLines(wr, t.fd) + } + + return t +} + +// Run updates the screen. It should be run in a separate goroutine. When +// ctx is cancelled, the status lines are cleanly removed. +func (t *Terminal) Run(ctx context.Context) { + if t.canUpdateStatus { + t.run(ctx) + return + } + + t.runWithoutStatus(ctx) +} + +func countLines(buf []byte) int { + lines := 0 + sc := bufio.NewScanner(bytes.NewReader(buf)) + for sc.Scan() { + lines++ + } + return lines +} + +type stringWriter interface { + WriteString(string) (int, error) +} + +// run listens on the channels and updates the terminal screen. +func (t *Terminal) run(ctx context.Context) { + statusBuf := bytes.NewBuffer(nil) + statusLines := 0 + for { + select { + case <-ctx.Done(): + t.undoStatus(statusLines) + + err := t.wr.Flush() + if err != nil { + fmt.Fprintf(os.Stderr, "flush failed: %v\n", err) + } + + return + + case msg := <-t.msg: + t.undoStatus(statusLines) + + var dst io.Writer + if msg.err { + dst = t.errWriter + + // assume t.wr and t.errWriter are different, so we need to + // flush the removal of the status lines first. + err := t.wr.Flush() + if err != nil { + fmt.Fprintf(os.Stderr, "flush failed: %v\n", err) + } + } else { + dst = t.wr + } + + var err error + if w, ok := dst.(stringWriter); ok { + _, err = w.WriteString(msg.line) + } else { + _, err = dst.Write([]byte(msg.line)) + } + + if err != nil { + fmt.Fprintf(os.Stderr, "write failed: %v\n", err) + continue + } + + _, err = t.wr.Write(statusBuf.Bytes()) + if err != nil { + fmt.Fprintf(os.Stderr, "write failed: %v\n", err) + } + + err = t.wr.Flush() + if err != nil { + fmt.Fprintf(os.Stderr, "flush failed: %v\n", err) + } + + case stat := <-t.status: + t.undoStatus(statusLines) + + statusBuf.Reset() + for _, line := range stat.lines { + statusBuf.WriteString(line) + } + statusLines = len(stat.lines) + + _, err := t.wr.Write(statusBuf.Bytes()) + if err != nil { + fmt.Fprintf(os.Stderr, "write failed: %v\n", err) + } + + err = t.wr.Flush() + if err != nil { + fmt.Fprintf(os.Stderr, "flush failed: %v\n", err) + } + } + } +} + +// runWithoutStatus listens on the channels and just prints out the messages, +// without status lines. +func (t *Terminal) runWithoutStatus(ctx context.Context) { + for { + select { + case <-ctx.Done(): + return + case msg := <-t.msg: + var err error + var flush func() error + + var dst io.Writer + if msg.err { + dst = t.errWriter + } else { + dst = t.wr + flush = t.wr.Flush + } + + if w, ok := dst.(stringWriter); ok { + _, err = w.WriteString(msg.line) + } else { + _, err = dst.Write([]byte(msg.line)) + } + + if err != nil { + fmt.Fprintf(os.Stderr, "write failed: %v\n", err) + } + + if flush == nil { + continue + } + + err = flush() + if err != nil { + fmt.Fprintf(os.Stderr, "flush failed: %v\n", err) + } + + case _ = <-t.status: + // discard status lines + } + } +} + +func (t *Terminal) undoStatus(lines int) { + if lines == 0 { + return + } + + lines-- + t.clearLines(t.wr, t.fd, lines) +} + +// Print writes a line to the terminal. +func (t *Terminal) Print(line string) { + // make sure the line ends with a line break + if line[len(line)-1] != '\n' { + line += "\n" + } + + t.msg <- message{line: line} +} + +// Printf uses fmt.Sprintf to write a line to the terminal. +func (t *Terminal) Printf(msg string, args ...interface{}) { + s := fmt.Sprintf(msg, args...) + t.Print(s) +} + +// Error writes an error to the terminal. +func (t *Terminal) Error(line string) { + // make sure the line ends with a line break + if line[len(line)-1] != '\n' { + line += "\n" + } + + t.msg <- message{line: line, err: true} +} + +// Errorf uses fmt.Sprintf to write an error line to the terminal. +func (t *Terminal) Errorf(msg string, args ...interface{}) { + s := fmt.Sprintf(msg, args...) + t.Error(s) +} + +// SetStatus updates the status lines. +func (t *Terminal) SetStatus(lines []string) { + if len(lines) == 0 { + return + } + + width, _, err := getTermSize(t.fd) + if err != nil || width < 0 { + // use 80 columns by default + width = 80 + } + + // make sure that all lines have a line break and are not too long + for i, line := range lines { + line = strings.TrimRight(line, "\n") + + if len(line) >= width-2 { + line = line[:width-2] + } + line += "\n" + lines[i] = line + } + + // make sure the last line does not have a line break + last := len(lines) - 1 + lines[last] = strings.TrimRight(lines[last], "\n") + + t.status <- status{lines: lines} +} diff --git a/internal/ui/termstatus/terminal_posix.go b/internal/ui/termstatus/terminal_posix.go new file mode 100644 index 000000000..6b86e0d43 --- /dev/null +++ b/internal/ui/termstatus/terminal_posix.go @@ -0,0 +1,33 @@ +package termstatus + +import ( + "fmt" + "io" + "os" +) + +const ( + posixMoveCursorHome = "\r" + posixMoveCursorUp = "\x1b[1A" + posixClearLine = "\x1b[2K" +) + +// posixClearLines will clear the current line and the n lines above. +// Afterwards the cursor is positioned at the start of the first cleared line. +func posixClearLines(wr io.Writer, fd uintptr, n int) { + // clear current line + _, err := wr.Write([]byte(posixMoveCursorHome + posixClearLine)) + if err != nil { + fmt.Fprintf(os.Stderr, "write failed: %v\n", err) + return + } + + for ; n > 0; n-- { + // clear current line and move on line up + _, err := wr.Write([]byte(posixMoveCursorUp + posixClearLine)) + if err != nil { + fmt.Fprintf(os.Stderr, "write failed: %v\n", err) + return + } + } +} diff --git a/internal/ui/termstatus/terminal_unix.go b/internal/ui/termstatus/terminal_unix.go new file mode 100644 index 000000000..52db49a17 --- /dev/null +++ b/internal/ui/termstatus/terminal_unix.go @@ -0,0 +1,34 @@ +// +build !windows + +package termstatus + +import ( + "io" + "syscall" + "unsafe" + + isatty "github.com/mattn/go-isatty" +) + +// clearLines will clear the current line and the n lines above. Afterwards the +// cursor is positioned at the start of the first cleared line. +func clearLines(wr io.Writer, fd uintptr) clearLinesFunc { + return posixClearLines +} + +// canUpdateStatus returns true if status lines can be printed, the process +// output is not redirected to a file or pipe. +func canUpdateStatus(fd uintptr) bool { + return isatty.IsTerminal(fd) +} + +// getTermSize returns the dimensions of the given terminal. +// the code is taken from "golang.org/x/crypto/ssh/terminal" +func getTermSize(fd uintptr) (width, height int, err error) { + var dimensions [4]uint16 + + if _, _, err := syscall.Syscall6(syscall.SYS_IOCTL, fd, uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&dimensions)), 0, 0, 0); err != 0 { + return -1, -1, err + } + return int(dimensions[1]), int(dimensions[0]), nil +} diff --git a/internal/ui/termstatus/terminal_windows.go b/internal/ui/termstatus/terminal_windows.go new file mode 100644 index 000000000..56910c67e --- /dev/null +++ b/internal/ui/termstatus/terminal_windows.go @@ -0,0 +1,131 @@ +// +build windows + +package termstatus + +import ( + "io" + "syscall" + "unsafe" +) + +// clearLines clears the current line and n lines above it. +func clearLines(wr io.Writer, fd uintptr) clearLinesFunc { + // easy case, the terminal is cmd or psh, without redirection + if isWindowsTerminal(fd) { + return windowsClearLines + } + + // check if the output file type is a pipe (0x0003) + if getFileType(fd) != fileTypePipe { + // return empty func, update state is not possible on this terminal + return func(io.Writer, uintptr, int) {} + } + + // assume we're running in mintty/cygwin + return posixClearLines +} + +var kernel32 = syscall.NewLazyDLL("kernel32.dll") + +var ( + procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo") + procSetConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition") + procFillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW") + procFillConsoleOutputAttribute = kernel32.NewProc("FillConsoleOutputAttribute") + procGetConsoleMode = kernel32.NewProc("GetConsoleMode") + procGetFileType = kernel32.NewProc("GetFileType") +) + +type ( + short int16 + word uint16 + dword uint32 + + coord struct { + x short + y short + } + smallRect struct { + left short + top short + right short + bottom short + } + consoleScreenBufferInfo struct { + size coord + cursorPosition coord + attributes word + window smallRect + maximumWindowSize coord + } +) + +// windowsClearLines clears the current line and n lines above it. +func windowsClearLines(wr io.Writer, fd uintptr, n int) { + var info consoleScreenBufferInfo + procGetConsoleScreenBufferInfo.Call(fd, uintptr(unsafe.Pointer(&info))) + + for i := 0; i <= n; i++ { + // clear the line + cursor := coord{ + x: info.window.left, + y: info.cursorPosition.y - short(i), + } + var count, w dword + count = dword(info.size.x) + procFillConsoleOutputAttribute.Call(fd, uintptr(info.attributes), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&w))) + procFillConsoleOutputCharacter.Call(fd, uintptr(' '), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&w))) + } + + // move cursor up by n lines and to the first column + info.cursorPosition.y -= short(n) + info.cursorPosition.x = 0 + procSetConsoleCursorPosition.Call(fd, uintptr(*(*int32)(unsafe.Pointer(&info.cursorPosition)))) +} + +// getTermSize returns the dimensions of the given terminal. +// the code is taken from "golang.org/x/crypto/ssh/terminal" +func getTermSize(fd uintptr) (width, height int, err error) { + var info consoleScreenBufferInfo + _, _, e := syscall.Syscall(procGetConsoleScreenBufferInfo.Addr(), 2, fd, uintptr(unsafe.Pointer(&info)), 0) + if e != 0 { + return 0, 0, error(e) + } + return int(info.size.x), int(info.size.y), nil +} + +// isWindowsTerminal return true if the file descriptor is a windows terminal (cmd, psh). +func isWindowsTerminal(fd uintptr) bool { + var st uint32 + r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, fd, uintptr(unsafe.Pointer(&st)), 0) + return r != 0 && e == 0 +} + +const fileTypePipe = 0x0003 + +// getFileType returns the file type for the given fd. +// https://msdn.microsoft.com/de-de/library/windows/desktop/aa364960(v=vs.85).aspx +func getFileType(fd uintptr) int { + r, _, e := syscall.Syscall(procGetFileType.Addr(), 1, fd, 0, 0) + if e != 0 { + return 0 + } + return int(r) +} + +// canUpdateStatus returns true if status lines can be printed, the process +// output is not redirected to a file or pipe. +func canUpdateStatus(fd uintptr) bool { + // easy case, the terminal is cmd or psh, without redirection + if isWindowsTerminal(fd) { + return true + } + + // check if the output file type is a pipe (0x0003) + if getFileType(fd) != fileTypePipe { + return false + } + + // assume we're running in mintty/cygwin + return true +} From c703d21d5524835f8405f52b2d3ce6cee3e04df9 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 22 Apr 2018 14:21:44 +0200 Subject: [PATCH 25/30] Vendor gopkg.in/tomb.v2 --- Gopkg.lock | 8 +- vendor/gopkg.in/tomb.v2/LICENSE | 29 +++ vendor/gopkg.in/tomb.v2/README.md | 4 + vendor/gopkg.in/tomb.v2/context.go | 74 +++++++ vendor/gopkg.in/tomb.v2/context16.go | 74 +++++++ vendor/gopkg.in/tomb.v2/context16_test.go | 177 ++++++++++++++++ vendor/gopkg.in/tomb.v2/context_test.go | 176 ++++++++++++++++ vendor/gopkg.in/tomb.v2/tomb.go | 237 ++++++++++++++++++++++ vendor/gopkg.in/tomb.v2/tomb_test.go | 183 +++++++++++++++++ 9 files changed, 961 insertions(+), 1 deletion(-) create mode 100644 vendor/gopkg.in/tomb.v2/LICENSE create mode 100644 vendor/gopkg.in/tomb.v2/README.md create mode 100644 vendor/gopkg.in/tomb.v2/context.go create mode 100644 vendor/gopkg.in/tomb.v2/context16.go create mode 100644 vendor/gopkg.in/tomb.v2/context16_test.go create mode 100644 vendor/gopkg.in/tomb.v2/context_test.go create mode 100644 vendor/gopkg.in/tomb.v2/tomb.go create mode 100644 vendor/gopkg.in/tomb.v2/tomb_test.go diff --git a/Gopkg.lock b/Gopkg.lock index f08ab4549..0426c622b 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -235,6 +235,12 @@ revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a" version = "v1.0.0" +[[projects]] + branch = "v2" + name = "gopkg.in/tomb.v2" + packages = ["."] + revision = "d5d1b5820637886def9eef33e03a27a9f166942c" + [[projects]] name = "gopkg.in/yaml.v2" packages = ["."] @@ -244,6 +250,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "a270b39954b9dad18c46f097be5816ca58ae1f387940b673b387d30934ce4ed4" + inputs-digest = "44a8f2ed127a6eaa38c1449b97d298fc703c961617bd93565b89bcc6c9a41483" solver-name = "gps-cdcl" solver-version = 1 diff --git a/vendor/gopkg.in/tomb.v2/LICENSE b/vendor/gopkg.in/tomb.v2/LICENSE new file mode 100644 index 000000000..a4249bb31 --- /dev/null +++ b/vendor/gopkg.in/tomb.v2/LICENSE @@ -0,0 +1,29 @@ +tomb - support for clean goroutine termination in Go. + +Copyright (c) 2010-2011 - Gustavo Niemeyer + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/gopkg.in/tomb.v2/README.md b/vendor/gopkg.in/tomb.v2/README.md new file mode 100644 index 000000000..e7f282b5a --- /dev/null +++ b/vendor/gopkg.in/tomb.v2/README.md @@ -0,0 +1,4 @@ +Installation and usage +---------------------- + +See [gopkg.in/tomb.v2](https://gopkg.in/tomb.v2) for documentation and usage details. diff --git a/vendor/gopkg.in/tomb.v2/context.go b/vendor/gopkg.in/tomb.v2/context.go new file mode 100644 index 000000000..f0fe56f5c --- /dev/null +++ b/vendor/gopkg.in/tomb.v2/context.go @@ -0,0 +1,74 @@ +// +build go1.7 + +package tomb + +import ( + "context" +) + +// WithContext returns a new tomb that is killed when the provided parent +// context is canceled, and a copy of parent with a replaced Done channel +// that is closed when either the tomb is dying or the parent is canceled. +// The returned context may also be obtained via the tomb's Context method. +func WithContext(parent context.Context) (*Tomb, context.Context) { + var t Tomb + t.init() + if parent.Done() != nil { + go func() { + select { + case <-t.Dying(): + case <-parent.Done(): + t.Kill(parent.Err()) + } + }() + } + t.parent = parent + child, cancel := context.WithCancel(parent) + t.addChild(parent, child, cancel) + return &t, child +} + +// Context returns a context that is a copy of the provided parent context with +// a replaced Done channel that is closed when either the tomb is dying or the +// parent is cancelled. +// +// If parent is nil, it defaults to the parent provided via WithContext, or an +// empty background parent if the tomb wasn't created via WithContext. +func (t *Tomb) Context(parent context.Context) context.Context { + t.init() + t.m.Lock() + defer t.m.Unlock() + + if parent == nil { + if t.parent == nil { + t.parent = context.Background() + } + parent = t.parent.(context.Context) + } + + if child, ok := t.child[parent]; ok { + return child.context.(context.Context) + } + + child, cancel := context.WithCancel(parent) + t.addChild(parent, child, cancel) + return child +} + +func (t *Tomb) addChild(parent context.Context, child context.Context, cancel func()) { + if t.reason != ErrStillAlive { + cancel() + return + } + if t.child == nil { + t.child = make(map[interface{}]childContext) + } + t.child[parent] = childContext{child, cancel, child.Done()} + for parent, child := range t.child { + select { + case <-child.done: + delete(t.child, parent) + default: + } + } +} diff --git a/vendor/gopkg.in/tomb.v2/context16.go b/vendor/gopkg.in/tomb.v2/context16.go new file mode 100644 index 000000000..d47d83a5a --- /dev/null +++ b/vendor/gopkg.in/tomb.v2/context16.go @@ -0,0 +1,74 @@ +// +build !go1.7 + +package tomb + +import ( + "golang.org/x/net/context" +) + +// WithContext returns a new tomb that is killed when the provided parent +// context is canceled, and a copy of parent with a replaced Done channel +// that is closed when either the tomb is dying or the parent is canceled. +// The returned context may also be obtained via the tomb's Context method. +func WithContext(parent context.Context) (*Tomb, context.Context) { + var t Tomb + t.init() + if parent.Done() != nil { + go func() { + select { + case <-t.Dying(): + case <-parent.Done(): + t.Kill(parent.Err()) + } + }() + } + t.parent = parent + child, cancel := context.WithCancel(parent) + t.addChild(parent, child, cancel) + return &t, child +} + +// Context returns a context that is a copy of the provided parent context with +// a replaced Done channel that is closed when either the tomb is dying or the +// parent is cancelled. +// +// If parent is nil, it defaults to the parent provided via WithContext, or an +// empty background parent if the tomb wasn't created via WithContext. +func (t *Tomb) Context(parent context.Context) context.Context { + t.init() + t.m.Lock() + defer t.m.Unlock() + + if parent == nil { + if t.parent == nil { + t.parent = context.Background() + } + parent = t.parent.(context.Context) + } + + if child, ok := t.child[parent]; ok { + return child.context.(context.Context) + } + + child, cancel := context.WithCancel(parent) + t.addChild(parent, child, cancel) + return child +} + +func (t *Tomb) addChild(parent context.Context, child context.Context, cancel func()) { + if t.reason != ErrStillAlive { + cancel() + return + } + if t.child == nil { + t.child = make(map[interface{}]childContext) + } + t.child[parent] = childContext{child, cancel, child.Done()} + for parent, child := range t.child { + select { + case <-child.done: + delete(t.child, parent) + default: + } + } +} diff --git a/vendor/gopkg.in/tomb.v2/context16_test.go b/vendor/gopkg.in/tomb.v2/context16_test.go new file mode 100644 index 000000000..ad155f37d --- /dev/null +++ b/vendor/gopkg.in/tomb.v2/context16_test.go @@ -0,0 +1,177 @@ +// +build !go1.7 + +package tomb_test + +import ( + "testing" + "time" + + "golang.org/x/net/context" + + "gopkg.in/tomb.v2" +) + +func TestWithContext(t *testing.T) { + parent1, cancel1 := context.WithCancel(context.Background()) + + tb, child1 := tomb.WithContext(parent1) + + if !tb.Alive() { + t.Fatalf("WithContext returned dead tomb") + } + if tb.Context(parent1) != child1 { + t.Fatalf("Context returned different context for same parent") + } + if tb.Context(nil) != child1 { + t.Fatalf("Context returned different context for nil parent") + } + select { + case <-child1.Done(): + t.Fatalf("Tomb's child context was born dead") + default: + } + + parent2, cancel2 := context.WithCancel(context.WithValue(context.Background(), "parent", "parent2")) + child2 := tb.Context(parent2) + + if tb.Context(parent2) != child2 { + t.Fatalf("Context returned different context for same parent") + } + if child2.Value("parent") != "parent2" { + t.Fatalf("Child context didn't inherit its parent's properties") + } + select { + case <-child2.Done(): + t.Fatalf("Tomb's child context was born dead") + default: + } + + cancel2() + + select { + case <-child2.Done(): + case <-time.After(5 * time.Second): + t.Fatalf("Tomb's child context didn't die after parent was canceled") + } + if !tb.Alive() { + t.Fatalf("Canceling unrelated parent context killed tomb") + } + + parent3 := context.WithValue(context.Background(), "parent", "parent3") + child3 := tb.Context(parent3) + + if child3.Value("parent") != "parent3" { + t.Fatalf("Child context didn't inherit its parent's properties") + } + select { + case <-child3.Done(): + t.Fatalf("Tomb's child context was born dead") + default: + } + + cancel1() + + select { + case <-tb.Dying(): + case <-time.After(5 * time.Second): + t.Fatalf("Canceling parent context did not kill tomb") + } + + if tb.Err() != context.Canceled { + t.Fatalf("tomb should be %v, got %v", context.Canceled, tb.Err()) + } + + if tb.Context(parent1) == child1 || tb.Context(parent3) == child3 { + t.Fatalf("Tomb is dead and shouldn't be tracking children anymore") + } + select { + case <-child3.Done(): + case <-time.After(5 * time.Second): + t.Fatalf("Child context didn't die after tomb's death") + } + + parent4 := context.WithValue(context.Background(), "parent", "parent4") + child4 := tb.Context(parent4) + + select { + case <-child4.Done(): + case <-time.After(5 * time.Second): + t.Fatalf("Child context should be born canceled") + } + + childnil := tb.Context(nil) + select { + case <-childnil.Done(): + default: + t.Fatalf("Child context should be born canceled") + } +} + +func TestContextNoParent(t *testing.T) { + var tb tomb.Tomb + + parent2, cancel2 := context.WithCancel(context.WithValue(context.Background(), "parent", "parent2")) + child2 := tb.Context(parent2) + + if tb.Context(parent2) != child2 { + t.Fatalf("Context returned different context for same parent") + } + if child2.Value("parent") != "parent2" { + t.Fatalf("Child context didn't inherit its parent's properties") + } + select { + case <-child2.Done(): + t.Fatalf("Tomb's child context was born dead") + default: + } + + cancel2() + + select { + case <-child2.Done(): + default: + t.Fatalf("Tomb's child context didn't die after parent was canceled") + } + if !tb.Alive() { + t.Fatalf("Canceling unrelated parent context killed tomb") + } + + parent3 := context.WithValue(context.Background(), "parent", "parent3") + child3 := tb.Context(parent3) + + if child3.Value("parent") != "parent3" { + t.Fatalf("Child context didn't inherit its parent's properties") + } + select { + case <-child3.Done(): + t.Fatalf("Tomb's child context was born dead") + default: + } + + tb.Kill(nil) + + if tb.Context(parent3) == child3 { + t.Fatalf("Tomb is dead and shouldn't be tracking children anymore") + } + select { + case <-child3.Done(): + default: + t.Fatalf("Child context didn't die after tomb's death") + } + + parent4 := context.WithValue(context.Background(), "parent", "parent4") + child4 := tb.Context(parent4) + + select { + case <-child4.Done(): + default: + t.Fatalf("Child context should be born canceled") + } + + childnil := tb.Context(nil) + select { + case <-childnil.Done(): + default: + t.Fatalf("Child context should be born canceled") + } +} diff --git a/vendor/gopkg.in/tomb.v2/context_test.go b/vendor/gopkg.in/tomb.v2/context_test.go new file mode 100644 index 000000000..537548386 --- /dev/null +++ b/vendor/gopkg.in/tomb.v2/context_test.go @@ -0,0 +1,176 @@ +// +build go1.7 + +package tomb_test + +import ( + "context" + "testing" + "time" + + "gopkg.in/tomb.v2" +) + +func TestWithContext(t *testing.T) { + parent1, cancel1 := context.WithCancel(context.Background()) + + tb, child1 := tomb.WithContext(parent1) + + if !tb.Alive() { + t.Fatalf("WithContext returned dead tomb") + } + if tb.Context(parent1) != child1 { + t.Fatalf("Context returned different context for same parent") + } + if tb.Context(nil) != child1 { + t.Fatalf("Context returned different context for nil parent") + } + select { + case <-child1.Done(): + t.Fatalf("Tomb's child context was born dead") + default: + } + + parent2, cancel2 := context.WithCancel(context.WithValue(context.Background(), "parent", "parent2")) + child2 := tb.Context(parent2) + + if tb.Context(parent2) != child2 { + t.Fatalf("Context returned different context for same parent") + } + if child2.Value("parent") != "parent2" { + t.Fatalf("Child context didn't inherit its parent's properties") + } + select { + case <-child2.Done(): + t.Fatalf("Tomb's child context was born dead") + default: + } + + cancel2() + + select { + case <-child2.Done(): + default: + t.Fatalf("Tomb's child context didn't die after parent was canceled") + } + if !tb.Alive() { + t.Fatalf("Canceling unrelated parent context killed tomb") + } + + parent3 := context.WithValue(context.Background(), "parent", "parent3") + child3 := tb.Context(parent3) + + if child3.Value("parent") != "parent3" { + t.Fatalf("Child context didn't inherit its parent's properties") + } + select { + case <-child3.Done(): + t.Fatalf("Tomb's child context was born dead") + default: + } + + cancel1() + + select { + case <-tb.Dying(): + case <-time.After(5 * time.Second): + t.Fatalf("Canceling parent context did not kill tomb") + } + + if tb.Err() != context.Canceled { + t.Fatalf("tomb should be %v, got %v", context.Canceled, tb.Err()) + } + + if tb.Context(parent1) == child1 || tb.Context(parent3) == child3 { + t.Fatalf("Tomb is dead and shouldn't be tracking children anymore") + } + select { + case <-child3.Done(): + default: + t.Fatalf("Child context didn't die after tomb's death") + } + + parent4 := context.WithValue(context.Background(), "parent", "parent4") + child4 := tb.Context(parent4) + + select { + case <-child4.Done(): + default: + t.Fatalf("Child context should be born canceled") + } + + childnil := tb.Context(nil) + select { + case <-childnil.Done(): + default: + t.Fatalf("Child context should be born canceled") + } +} + +func TestContextNoParent(t *testing.T) { + var tb tomb.Tomb + + parent2, cancel2 := context.WithCancel(context.WithValue(context.Background(), "parent", "parent2")) + child2 := tb.Context(parent2) + + if tb.Context(parent2) != child2 { + t.Fatalf("Context returned different context for same parent") + } + if child2.Value("parent") != "parent2" { + t.Fatalf("Child context didn't inherit its parent's properties") + } + select { + case <-child2.Done(): + t.Fatalf("Tomb's child context was born dead") + default: + } + + cancel2() + + select { + case <-child2.Done(): + default: + t.Fatalf("Tomb's child context didn't die after parent was canceled") + } + if !tb.Alive() { + t.Fatalf("Canceling unrelated parent context killed tomb") + } + + parent3 := context.WithValue(context.Background(), "parent", "parent3") + child3 := tb.Context(parent3) + + if child3.Value("parent") != "parent3" { + t.Fatalf("Child context didn't inherit its parent's properties") + } + select { + case <-child3.Done(): + t.Fatalf("Tomb's child context was born dead") + default: + } + + tb.Kill(nil) + + if tb.Context(parent3) == child3 { + t.Fatalf("Tomb is dead and shouldn't be tracking children anymore") + } + select { + case <-child3.Done(): + default: + t.Fatalf("Child context didn't die after tomb's death") + } + + parent4 := context.WithValue(context.Background(), "parent", "parent4") + child4 := tb.Context(parent4) + + select { + case <-child4.Done(): + default: + t.Fatalf("Child context should be born canceled") + } + + childnil := tb.Context(nil) + select { + case <-childnil.Done(): + default: + t.Fatalf("Child context should be born canceled") + } +} diff --git a/vendor/gopkg.in/tomb.v2/tomb.go b/vendor/gopkg.in/tomb.v2/tomb.go new file mode 100644 index 000000000..069b3058b --- /dev/null +++ b/vendor/gopkg.in/tomb.v2/tomb.go @@ -0,0 +1,237 @@ +// Copyright (c) 2011 - Gustavo Niemeyer +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// * Neither the name of the copyright holder nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +// The tomb package handles clean goroutine tracking and termination. +// +// The zero value of a Tomb is ready to handle the creation of a tracked +// goroutine via its Go method, and then any tracked goroutine may call +// the Go method again to create additional tracked goroutines at +// any point. +// +// If any of the tracked goroutines returns a non-nil error, or the +// Kill or Killf method is called by any goroutine in the system (tracked +// or not), the tomb Err is set, Alive is set to false, and the Dying +// channel is closed to flag that all tracked goroutines are supposed +// to willingly terminate as soon as possible. +// +// Once all tracked goroutines terminate, the Dead channel is closed, +// and Wait unblocks and returns the first non-nil error presented +// to the tomb via a result or an explicit Kill or Killf method call, +// or nil if there were no errors. +// +// It is okay to create further goroutines via the Go method while +// the tomb is in a dying state. The final dead state is only reached +// once all tracked goroutines terminate, at which point calling +// the Go method again will cause a runtime panic. +// +// Tracked functions and methods that are still running while the tomb +// is in dying state may choose to return ErrDying as their error value. +// This preserves the well established non-nil error convention, but is +// understood by the tomb as a clean termination. The Err and Wait +// methods will still return nil if all observed errors were either +// nil or ErrDying. +// +// For background and a detailed example, see the following blog post: +// +// http://blog.labix.org/2011/10/09/death-of-goroutines-under-control +// +package tomb + +import ( + "errors" + "fmt" + "sync" +) + +// A Tomb tracks the lifecycle of one or more goroutines as alive, +// dying or dead, and the reason for their death. +// +// See the package documentation for details. +type Tomb struct { + m sync.Mutex + alive int + dying chan struct{} + dead chan struct{} + reason error + + // context.Context is available in Go 1.7+. + parent interface{} + child map[interface{}]childContext +} + +type childContext struct { + context interface{} + cancel func() + done <-chan struct{} +} + +var ( + ErrStillAlive = errors.New("tomb: still alive") + ErrDying = errors.New("tomb: dying") +) + +func (t *Tomb) init() { + t.m.Lock() + if t.dead == nil { + t.dead = make(chan struct{}) + t.dying = make(chan struct{}) + t.reason = ErrStillAlive + } + t.m.Unlock() +} + +// Dead returns the channel that can be used to wait until +// all goroutines have finished running. +func (t *Tomb) Dead() <-chan struct{} { + t.init() + return t.dead +} + +// Dying returns the channel that can be used to wait until +// t.Kill is called. +func (t *Tomb) Dying() <-chan struct{} { + t.init() + return t.dying +} + +// Wait blocks until all goroutines have finished running, and +// then returns the reason for their death. +func (t *Tomb) Wait() error { + t.init() + <-t.dead + t.m.Lock() + reason := t.reason + t.m.Unlock() + return reason +} + +// Go runs f in a new goroutine and tracks its termination. +// +// If f returns a non-nil error, t.Kill is called with that +// error as the death reason parameter. +// +// It is f's responsibility to monitor the tomb and return +// appropriately once it is in a dying state. +// +// It is safe for the f function to call the Go method again +// to create additional tracked goroutines. Once all tracked +// goroutines return, the Dead channel is closed and the +// Wait method unblocks and returns the death reason. +// +// Calling the Go method after all tracked goroutines return +// causes a runtime panic. For that reason, calling the Go +// method a second time out of a tracked goroutine is unsafe. +func (t *Tomb) Go(f func() error) { + t.init() + t.m.Lock() + defer t.m.Unlock() + select { + case <-t.dead: + panic("tomb.Go called after all goroutines terminated") + default: + } + t.alive++ + go t.run(f) +} + +func (t *Tomb) run(f func() error) { + err := f() + t.m.Lock() + defer t.m.Unlock() + t.alive-- + if t.alive == 0 || err != nil { + t.kill(err) + if t.alive == 0 { + close(t.dead) + } + } +} + +// Kill puts the tomb in a dying state for the given reason, +// closes the Dying channel, and sets Alive to false. +// +// Althoguh Kill may be called multiple times, only the first +// non-nil error is recorded as the death reason. +// +// If reason is ErrDying, the previous reason isn't replaced +// even if nil. It's a runtime error to call Kill with ErrDying +// if t is not in a dying state. +func (t *Tomb) Kill(reason error) { + t.init() + t.m.Lock() + defer t.m.Unlock() + t.kill(reason) +} + +func (t *Tomb) kill(reason error) { + if reason == ErrStillAlive { + panic("tomb: Kill with ErrStillAlive") + } + if reason == ErrDying { + if t.reason == ErrStillAlive { + panic("tomb: Kill with ErrDying while still alive") + } + return + } + if t.reason == ErrStillAlive { + t.reason = reason + close(t.dying) + for _, child := range t.child { + child.cancel() + } + t.child = nil + return + } + if t.reason == nil { + t.reason = reason + return + } +} + +// Killf calls the Kill method with an error built providing the received +// parameters to fmt.Errorf. The generated error is also returned. +func (t *Tomb) Killf(f string, a ...interface{}) error { + err := fmt.Errorf(f, a...) + t.Kill(err) + return err +} + +// Err returns the death reason, or ErrStillAlive if the tomb +// is not in a dying or dead state. +func (t *Tomb) Err() (reason error) { + t.init() + t.m.Lock() + reason = t.reason + t.m.Unlock() + return +} + +// Alive returns true if the tomb is not in a dying or dead state. +func (t *Tomb) Alive() bool { + return t.Err() == ErrStillAlive +} diff --git a/vendor/gopkg.in/tomb.v2/tomb_test.go b/vendor/gopkg.in/tomb.v2/tomb_test.go new file mode 100644 index 000000000..a1064dffe --- /dev/null +++ b/vendor/gopkg.in/tomb.v2/tomb_test.go @@ -0,0 +1,183 @@ +package tomb_test + +import ( + "errors" + "gopkg.in/tomb.v2" + "reflect" + "testing" +) + +func nothing() error { return nil } + +func TestNewTomb(t *testing.T) { + tb := &tomb.Tomb{} + checkState(t, tb, false, false, tomb.ErrStillAlive) +} + +func TestGo(t *testing.T) { + tb := &tomb.Tomb{} + alive := make(chan bool) + tb.Go(func() error { + alive <- true + tb.Go(func() error { + alive <- true + <-tb.Dying() + return nil + }) + <-tb.Dying() + return nil + }) + <-alive + <-alive + checkState(t, tb, false, false, tomb.ErrStillAlive) + tb.Kill(nil) + tb.Wait() + checkState(t, tb, true, true, nil) +} + +func TestGoErr(t *testing.T) { + first := errors.New("first error") + second := errors.New("first error") + tb := &tomb.Tomb{} + alive := make(chan bool) + tb.Go(func() error { + alive <- true + tb.Go(func() error { + alive <- true + return first + }) + <-tb.Dying() + return second + }) + <-alive + <-alive + tb.Wait() + checkState(t, tb, true, true, first) +} + +func TestGoPanic(t *testing.T) { + // ErrDying being used properly, after a clean death. + tb := &tomb.Tomb{} + tb.Go(nothing) + tb.Wait() + defer func() { + err := recover() + if err != "tomb.Go called after all goroutines terminated" { + t.Fatalf("Wrong panic on post-death tomb.Go call: %v", err) + } + checkState(t, tb, true, true, nil) + }() + tb.Go(nothing) +} + +func TestKill(t *testing.T) { + // a nil reason flags the goroutine as dying + tb := &tomb.Tomb{} + tb.Kill(nil) + checkState(t, tb, true, false, nil) + + // a non-nil reason now will override Kill + err := errors.New("some error") + tb.Kill(err) + checkState(t, tb, true, false, err) + + // another non-nil reason won't replace the first one + tb.Kill(errors.New("ignore me")) + checkState(t, tb, true, false, err) + + tb.Go(nothing) + tb.Wait() + checkState(t, tb, true, true, err) +} + +func TestKillf(t *testing.T) { + tb := &tomb.Tomb{} + + err := tb.Killf("BO%s", "OM") + if s := err.Error(); s != "BOOM" { + t.Fatalf(`Killf("BO%s", "OM"): want "BOOM", got %q`, s) + } + checkState(t, tb, true, false, err) + + // another non-nil reason won't replace the first one + tb.Killf("ignore me") + checkState(t, tb, true, false, err) + + tb.Go(nothing) + tb.Wait() + checkState(t, tb, true, true, err) +} + +func TestErrDying(t *testing.T) { + // ErrDying being used properly, after a clean death. + tb := &tomb.Tomb{} + tb.Kill(nil) + tb.Kill(tomb.ErrDying) + checkState(t, tb, true, false, nil) + + // ErrDying being used properly, after an errorful death. + err := errors.New("some error") + tb.Kill(err) + tb.Kill(tomb.ErrDying) + checkState(t, tb, true, false, err) + + // ErrDying being used badly, with an alive tomb. + tb = &tomb.Tomb{} + defer func() { + err := recover() + if err != "tomb: Kill with ErrDying while still alive" { + t.Fatalf("Wrong panic on Kill(ErrDying): %v", err) + } + checkState(t, tb, false, false, tomb.ErrStillAlive) + }() + tb.Kill(tomb.ErrDying) +} + +func TestKillErrStillAlivePanic(t *testing.T) { + tb := &tomb.Tomb{} + defer func() { + err := recover() + if err != "tomb: Kill with ErrStillAlive" { + t.Fatalf("Wrong panic on Kill(ErrStillAlive): %v", err) + } + checkState(t, tb, false, false, tomb.ErrStillAlive) + }() + tb.Kill(tomb.ErrStillAlive) +} + +func checkState(t *testing.T, tb *tomb.Tomb, wantDying, wantDead bool, wantErr error) { + select { + case <-tb.Dying(): + if !wantDying { + t.Error("<-Dying: should block") + } + default: + if wantDying { + t.Error("<-Dying: should not block") + } + } + seemsDead := false + select { + case <-tb.Dead(): + if !wantDead { + t.Error("<-Dead: should block") + } + seemsDead = true + default: + if wantDead { + t.Error("<-Dead: should not block") + } + } + if err := tb.Err(); err != wantErr { + t.Errorf("Err: want %#v, got %#v", wantErr, err) + } + if wantDead && seemsDead { + waitErr := tb.Wait() + switch { + case waitErr == tomb.ErrStillAlive: + t.Errorf("Wait should not return ErrStillAlive") + case !reflect.DeepEqual(waitErr, wantErr): + t.Errorf("Wait: want %#v, got %#v", wantErr, waitErr) + } + } +} From 0e78ac92d875e85c43aba9b315baca4be93d1383 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sun, 22 Apr 2018 11:57:20 +0200 Subject: [PATCH 26/30] Use new archiver code for backup --- cmd/restic/cmd_backup.go | 589 +++++++++++++---------------- cmd/restic/global.go | 6 +- cmd/restic/integration_test.go | 20 +- cmd/restic/main.go | 14 +- internal/archiver/archiver_test.go | 2 - internal/ui/backup.go | 343 +++++++++++++++++ internal/ui/message.go | 45 +++ internal/ui/stdio_wrapper.go | 86 +++++ internal/ui/stdio_wrapper_test.go | 95 +++++ 9 files changed, 863 insertions(+), 337 deletions(-) create mode 100644 internal/ui/backup.go create mode 100644 internal/ui/message.go create mode 100644 internal/ui/stdio_wrapper.go create mode 100644 internal/ui/stdio_wrapper_test.go diff --git a/cmd/restic/cmd_backup.go b/cmd/restic/cmd_backup.go index 5620e88e9..cf1bb3b1b 100644 --- a/cmd/restic/cmd_backup.go +++ b/cmd/restic/cmd_backup.go @@ -2,21 +2,24 @@ package main import ( "bufio" - "fmt" + "context" "io" "os" - "path" - "path/filepath" + "strconv" "strings" "time" "github.com/spf13/cobra" + tomb "gopkg.in/tomb.v2" "github.com/restic/restic/internal/archiver" "github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/fs" + "github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/restic" + "github.com/restic/restic/internal/ui" + "github.com/restic/restic/internal/ui/termstatus" ) var cmdBackup = &cobra.Command{ @@ -42,11 +45,16 @@ given as the arguments. return errors.Fatal("cannot use both `--stdin` and `--files-from -`") } - if backupOptions.Stdin { - return readBackupFromStdin(backupOptions, globalOptions, args) - } + var t tomb.Tomb + term := termstatus.New(globalOptions.stdout, globalOptions.stderr) + t.Go(func() error { term.Run(t.Context(globalOptions.ctx)); return nil }) - return runBackup(backupOptions, globalOptions, args) + err := runBackup(backupOptions, globalOptions, term, args) + if err != nil { + return err + } + t.Kill(nil) + return t.Wait() }, } @@ -90,127 +98,6 @@ func init() { f.BoolVar(&backupOptions.WithAtime, "with-atime", false, "store the atime for all files and directories") } -func newScanProgress(gopts GlobalOptions) *restic.Progress { - if gopts.Quiet { - return nil - } - - p := restic.NewProgress() - p.OnUpdate = func(s restic.Stat, d time.Duration, ticker bool) { - if IsProcessBackground() { - return - } - - PrintProgress("[%s] %d directories, %d files, %s", formatDuration(d), s.Dirs, s.Files, formatBytes(s.Bytes)) - } - - p.OnDone = func(s restic.Stat, d time.Duration, ticker bool) { - PrintProgress("scanned %d directories, %d files in %s\n", s.Dirs, s.Files, formatDuration(d)) - } - - return p -} - -func newArchiveProgress(gopts GlobalOptions, todo restic.Stat) *restic.Progress { - if gopts.Quiet { - return nil - } - - archiveProgress := restic.NewProgress() - - var bps, eta uint64 - itemsTodo := todo.Files + todo.Dirs - - archiveProgress.OnUpdate = func(s restic.Stat, d time.Duration, ticker bool) { - if IsProcessBackground() { - return - } - - sec := uint64(d / time.Second) - if todo.Bytes > 0 && sec > 0 && ticker { - bps = s.Bytes / sec - if s.Bytes >= todo.Bytes { - eta = 0 - } else if bps > 0 { - eta = (todo.Bytes - s.Bytes) / bps - } - } - - itemsDone := s.Files + s.Dirs - - status1 := fmt.Sprintf("[%s] %s %s / %s %d / %d items %d errors ", - formatDuration(d), - formatPercent(s.Bytes, todo.Bytes), - formatBytes(s.Bytes), formatBytes(todo.Bytes), - itemsDone, itemsTodo, - s.Errors) - status2 := fmt.Sprintf("ETA %s ", formatSeconds(eta)) - - if w := stdoutTerminalWidth(); w > 0 { - maxlen := w - len(status2) - 1 - - if maxlen < 4 { - status1 = "" - } else if len(status1) > maxlen { - status1 = status1[:maxlen-4] - status1 += "... " - } - } - - PrintProgress("%s%s", status1, status2) - } - - archiveProgress.OnDone = func(s restic.Stat, d time.Duration, ticker bool) { - fmt.Printf("\nduration: %s\n", formatDuration(d)) - } - - return archiveProgress -} - -func newArchiveStdinProgress(gopts GlobalOptions) *restic.Progress { - if gopts.Quiet { - return nil - } - - archiveProgress := restic.NewProgress() - - var bps uint64 - - archiveProgress.OnUpdate = func(s restic.Stat, d time.Duration, ticker bool) { - if IsProcessBackground() { - return - } - - sec := uint64(d / time.Second) - if s.Bytes > 0 && sec > 0 && ticker { - bps = s.Bytes / sec - } - - status1 := fmt.Sprintf("[%s] %s %s/s", formatDuration(d), - formatBytes(s.Bytes), - formatBytes(bps)) - - if w := stdoutTerminalWidth(); w > 0 { - maxlen := w - len(status1) - - if maxlen < 4 { - status1 = "" - } else if len(status1) > maxlen { - status1 = status1[:maxlen-4] - status1 += "... " - } - } - - PrintProgress("%s", status1) - } - - archiveProgress.OnDone = func(s restic.Stat, d time.Duration, ticker bool) { - fmt.Printf("\nduration: %s\n", formatDuration(d)) - } - - return archiveProgress -} - // filterExisting returns a slice of all existing items, or an error if no // items exist at all. func filterExisting(items []string) (result []string, err error) { @@ -231,72 +118,10 @@ func filterExisting(items []string) (result []string, err error) { return } -func readBackupFromStdin(opts BackupOptions, gopts GlobalOptions, args []string) error { - if len(args) != 0 { - return errors.Fatal("when reading from stdin, no additional files can be specified") - } - - fn := opts.StdinFilename - - if fn == "" { - return errors.Fatal("filename for backup from stdin must not be empty") - } - - if filepath.Base(fn) != fn || path.Base(fn) != fn { - return errors.Fatal("filename is invalid (may not contain a directory, slash or backslash)") - } - - var t time.Time - if opts.TimeStamp != "" { - parsedT, err := time.Parse("2006-01-02 15:04:05", opts.TimeStamp) - if err != nil { - return err - } - t = parsedT - } else { - t = time.Now() - } - - if gopts.password == "" { - return errors.Fatal("unable to read password from stdin when data is to be read from stdin, use --password-file or $RESTIC_PASSWORD") - } - - repo, err := OpenRepository(gopts) - if err != nil { - return err - } - - lock, err := lockRepo(repo) - defer unlockRepo(lock) - if err != nil { - return err - } - - err = repo.LoadIndex(gopts.ctx) - if err != nil { - return err - } - - r := &archiver.Reader{ - Repository: repo, - Tags: opts.Tags, - Hostname: opts.Hostname, - TimeStamp: t, - } - - _, id, err := r.Archive(gopts.ctx, fn, os.Stdin, newArchiveStdinProgress(gopts)) - if err != nil { - return err - } - - Verbosef("archived as %v\n", id.Str()) - return nil -} - -// readFromFile will read all lines from the given filename and write them to a -// string array, if filename is empty readFromFile returns and empty string -// array. If filename is a dash (-), readFromFile will read the lines from -// the standard input. +// readFromFile will read all lines from the given filename and return them as +// a string array, if filename is empty readFromFile returns and empty string +// array. If filename is a dash (-), readFromFile will read the lines from the +// standard input. func readLinesFromFile(filename string) ([]string, error) { if filename == "" { return nil, nil @@ -335,47 +160,45 @@ func readLinesFromFile(filename string) ([]string, error) { return lines, nil } -func runBackup(opts BackupOptions, gopts GlobalOptions, args []string) error { +// Check returns an error when an invalid combination of options was set. +func (opts BackupOptions) Check(gopts GlobalOptions, args []string) error { if opts.FilesFrom == "-" && gopts.password == "" { return errors.Fatal("unable to read password from stdin when data is to be read from stdin, use --password-file or $RESTIC_PASSWORD") } - fromfile, err := readLinesFromFile(opts.FilesFrom) - if err != nil { - return err - } - - // merge files from files-from into normal args so we can reuse the normal - // args checks and have the ability to use both files-from and args at the - // same time - args = append(args, fromfile...) - if len(args) == 0 { - return errors.Fatal("nothing to backup, please specify target files/dirs") - } - - target := make([]string, 0, len(args)) - for _, d := range args { - if a, err := filepath.Abs(d); err == nil { - d = a + if opts.Stdin { + if opts.FilesFrom != "" { + return errors.Fatal("--stdin and --files-from cannot be used together") + } + + if len(args) > 0 { + return errors.Fatal("--stdin was specified and files/dirs were listed as arguments") } - target = append(target, d) } - target, err = filterExisting(target) - if err != nil { - return err - } - - // rejectFuncs collect functions that can reject items from the backup - var rejectFuncs []RejectFunc + return nil +} +// collectRejectFuncs returns a list of all functions which may reject data +// from being saved in a snapshot +func collectRejectFuncs(opts BackupOptions, repo *repository.Repository, targets []string) (fs []RejectFunc, err error) { // allowed devices if opts.ExcludeOtherFS { - f, err := rejectByDevice(target) + f, err := rejectByDevice(targets) if err != nil { - return err + return nil, err } - rejectFuncs = append(rejectFuncs, f) + fs = append(fs, f) + } + + // exclude restic cache + if repo.Cache != nil { + f, err := rejectResticCache(repo) + if err != nil { + return nil, err + } + + fs = append(fs, f) } // add patterns from file @@ -384,7 +207,7 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, args []string) error { } if len(opts.Excludes) > 0 { - rejectFuncs = append(rejectFuncs, rejectByPattern(opts.Excludes)) + fs = append(fs, rejectByPattern(opts.Excludes)) } if opts.ExcludeCaches { @@ -394,111 +217,17 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, args []string) error { for _, spec := range opts.ExcludeIfPresent { f, err := rejectIfPresent(spec) if err != nil { - return err + return nil, err } - rejectFuncs = append(rejectFuncs, f) + fs = append(fs, f) } - repo, err := OpenRepository(gopts) - if err != nil { - return err - } - - lock, err := lockRepo(repo) - defer unlockRepo(lock) - if err != nil { - return err - } - - // exclude restic cache - if repo.Cache != nil { - f, err := rejectResticCache(repo) - if err != nil { - return err - } - - rejectFuncs = append(rejectFuncs, f) - } - - err = repo.LoadIndex(gopts.ctx) - if err != nil { - return err - } - - var parentSnapshotID *restic.ID - - // Force using a parent - if !opts.Force && opts.Parent != "" { - id, err := restic.FindSnapshot(repo, opts.Parent) - if err != nil { - return errors.Fatalf("invalid id %q: %v", opts.Parent, err) - } - - parentSnapshotID = &id - } - - // Find last snapshot to set it as parent, if not already set - if !opts.Force && parentSnapshotID == nil { - id, err := restic.FindLatestSnapshot(gopts.ctx, repo, target, []restic.TagList{}, opts.Hostname) - if err == nil { - parentSnapshotID = &id - } else if err != restic.ErrNoSnapshotFound { - return err - } - } - - if parentSnapshotID != nil { - Verbosef("using parent snapshot %v\n", parentSnapshotID.Str()) - } - - Verbosef("scan %v\n", target) - - selectFilter := func(item string, fi os.FileInfo) bool { - for _, reject := range rejectFuncs { - if reject(item, fi) { - return false - } - } - return true - } - - var stat restic.Stat - if !gopts.Quiet { - stat, err = archiver.Scan(target, selectFilter, newScanProgress(gopts)) - if err != nil { - return err - } - } - - arch := archiver.New(repo) - arch.Excludes = opts.Excludes - arch.SelectFilter = selectFilter - arch.WithAccessTime = opts.WithAtime - - arch.Warn = func(dir string, fi os.FileInfo, err error) { - // TODO: make ignoring errors configurable - Warnf("%s\rwarning for %s: %v\n", ClearLine(), dir, err) - } - - timeStamp := time.Now() - if opts.TimeStamp != "" { - timeStamp, err = time.Parse(TimeFormat, opts.TimeStamp) - if err != nil { - return errors.Fatalf("error in time option: %v\n", err) - } - } - - _, id, err := arch.Snapshot(gopts.ctx, newArchiveProgress(gopts, stat), target, opts.Tags, opts.Hostname, parentSnapshotID, timeStamp) - if err != nil { - return err - } - - Verbosef("snapshot %s saved\n", id.Str()) - - return nil + return fs, nil } +// readExcludePatternsFromFiles reads all exclude files and returns the list of +// exclude patterns. func readExcludePatternsFromFiles(excludeFiles []string) []string { var excludes []string for _, filename := range excludeFiles { @@ -540,3 +269,217 @@ func readExcludePatternsFromFiles(excludeFiles []string) []string { } return excludes } + +// collectTargets returns a list of target files/dirs from several sources. +func collectTargets(opts BackupOptions, args []string) (targets []string, err error) { + if opts.Stdin { + return nil, nil + } + + fromfile, err := readLinesFromFile(opts.FilesFrom) + if err != nil { + return nil, err + } + + // merge files from files-from into normal args so we can reuse the normal + // args checks and have the ability to use both files-from and args at the + // same time + args = append(args, fromfile...) + if len(args) == 0 && !opts.Stdin { + return nil, errors.Fatal("nothing to backup, please specify target files/dirs") + } + + targets = args + targets, err = filterExisting(targets) + if err != nil { + return nil, err + } + + return targets, nil +} + +// parent returns the ID of the parent snapshot. If there is none, nil is +// returned. +func findParentSnapshot(ctx context.Context, repo restic.Repository, opts BackupOptions, targets []string) (parentID *restic.ID, err error) { + // Force using a parent + if !opts.Force && opts.Parent != "" { + id, err := restic.FindSnapshot(repo, opts.Parent) + if err != nil { + return nil, errors.Fatalf("invalid id %q: %v", opts.Parent, err) + } + + parentID = &id + } + + // Find last snapshot to set it as parent, if not already set + if !opts.Force && parentID == nil { + id, err := restic.FindLatestSnapshot(ctx, repo, targets, []restic.TagList{}, opts.Hostname) + if err == nil { + parentID = &id + } else if err != restic.ErrNoSnapshotFound { + return nil, err + } + } + + return parentID, nil +} + +func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Terminal, args []string) error { + err := opts.Check(gopts, args) + if err != nil { + return err + } + + targets, err := collectTargets(opts, args) + if err != nil { + return err + } + + var t tomb.Tomb + + p := ui.NewBackup(term, gopts.verbosity) + + // use the terminal for stdout/stderr + prevStdout, prevStderr := gopts.stdout, gopts.stderr + defer func() { + gopts.stdout, gopts.stderr = prevStdout, prevStderr + }() + gopts.stdout, gopts.stderr = p.Stdout(), p.Stderr() + + if s, ok := os.LookupEnv("RESTIC_PROGRESS_FPS"); ok { + fps, err := strconv.Atoi(s) + if err == nil && fps >= 1 { + if fps > 60 { + fps = 60 + } + p.MinUpdatePause = time.Second / time.Duration(fps) + } + } + + t.Go(func() error { return p.Run(t.Context(gopts.ctx)) }) + + p.V("open repository") + repo, err := OpenRepository(gopts) + if err != nil { + return err + } + + p.V("lock repository") + lock, err := lockRepo(repo) + defer unlockRepo(lock) + if err != nil { + return err + } + + // rejectFuncs collect functions that can reject items from the backup + rejectFuncs, err := collectRejectFuncs(opts, repo, targets) + if err != nil { + return err + } + + p.V("load index files") + err = repo.LoadIndex(gopts.ctx) + if err != nil { + return err + } + + parentSnapshotID, err := findParentSnapshot(gopts.ctx, repo, opts, targets) + if err != nil { + return err + } + + if parentSnapshotID != nil { + p.V("using parent snapshot %v\n", parentSnapshotID.Str()) + } + + selectFilter := func(item string, fi os.FileInfo) bool { + for _, reject := range rejectFuncs { + if reject(item, fi) { + return false + } + } + return true + } + + timeStamp := time.Now() + if opts.TimeStamp != "" { + timeStamp, err = time.Parse(TimeFormat, opts.TimeStamp) + if err != nil { + return errors.Fatalf("error in time option: %v\n", err) + } + } + + var targetFS fs.FS = fs.Local{} + if opts.Stdin { + p.V("read data from stdin") + targetFS = &fs.Reader{ + ModTime: timeStamp, + Name: opts.StdinFilename, + Mode: 0644, + ReadCloser: os.Stdin, + } + targets = []string{opts.StdinFilename} + } + + sc := archiver.NewScanner(targetFS) + sc.Select = selectFilter + sc.Error = p.ScannerError + sc.Result = p.ReportTotal + + p.V("start scan") + t.Go(func() error { return sc.Scan(t.Context(gopts.ctx), targets) }) + + arch := archiver.New(repo, targetFS, archiver.Options{}) + arch.Select = selectFilter + arch.WithAtime = opts.WithAtime + arch.Error = p.Error + arch.CompleteItem = p.CompleteItemFn + arch.StartFile = p.StartFile + arch.CompleteBlob = p.CompleteBlob + + if parentSnapshotID == nil { + parentSnapshotID = &restic.ID{} + } + + snapshotOpts := archiver.SnapshotOptions{ + Excludes: opts.Excludes, + Tags: opts.Tags, + Time: timeStamp, + Hostname: opts.Hostname, + ParentSnapshot: *parentSnapshotID, + } + + uploader := archiver.IndexUploader{ + Repository: repo, + Start: func() { + p.VV("uploading intermediate index") + }, + Complete: func(id restic.ID) { + p.V("uploaded intermediate index %v", id.Str()) + }, + } + + t.Go(func() error { + return uploader.Upload(gopts.ctx, t.Context(gopts.ctx), 30*time.Second) + }) + + p.V("start backup") + _, id, err := arch.Snapshot(gopts.ctx, targets, snapshotOpts) + if err != nil { + return err + } + + p.Finish() + p.P("snapshot %s saved\n", id.Str()) + + // cleanly shutdown all running goroutines + t.Kill(nil) + + // let's see if one returned an error + err = t.Wait() + if err != nil { + return err + } + + return nil +} diff --git a/cmd/restic/global.go b/cmd/restic/global.go index 627e4c8f9..8c89a4d80 100644 --- a/cmd/restic/global.go +++ b/cmd/restic/global.go @@ -43,8 +43,7 @@ type GlobalOptions struct { Repo string PasswordFile string Quiet bool - Verbose bool - Debug bool + Verbose int NoLock bool JSON bool CacheDir string @@ -90,8 +89,7 @@ func init() { f.StringVarP(&globalOptions.Repo, "repo", "r", os.Getenv("RESTIC_REPOSITORY"), "repository to backup to or restore from (default: $RESTIC_REPOSITORY)") f.StringVarP(&globalOptions.PasswordFile, "password-file", "p", os.Getenv("RESTIC_PASSWORD_FILE"), "read the repository password from a file (default: $RESTIC_PASSWORD_FILE)") f.BoolVarP(&globalOptions.Quiet, "quiet", "q", false, "do not output comprehensive progress report") - f.BoolVarP(&globalOptions.Verbose, "verbose", "v", false, "be verbose") - f.BoolVar(&globalOptions.Debug, "debug", false, "be very verbose") + f.CountVarP(&globalOptions.Verbose, "verbose", "v", "be verbose (specify --verbose multiple times or level `n`)") f.BoolVar(&globalOptions.NoLock, "no-lock", false, "do not lock the repo, this allows some operations on read-only repos") f.BoolVarP(&globalOptions.JSON, "json", "", false, "set output mode to JSON for commands that support it") f.StringVar(&globalOptions.CacheDir, "cache-dir", "", "set the cache directory") diff --git a/cmd/restic/integration_test.go b/cmd/restic/integration_test.go index 88c154f5c..36a7670b1 100644 --- a/cmd/restic/integration_test.go +++ b/cmd/restic/integration_test.go @@ -3,6 +3,7 @@ package main import ( "bufio" "bytes" + "context" "crypto/rand" "encoding/json" "fmt" @@ -23,6 +24,8 @@ import ( "github.com/restic/restic/internal/repository" "github.com/restic/restic/internal/restic" rtest "github.com/restic/restic/internal/test" + "github.com/restic/restic/internal/ui/termstatus" + "golang.org/x/sync/errgroup" ) func parseIDsFromReader(t testing.TB, rd io.Reader) restic.IDs { @@ -52,13 +55,28 @@ func testRunInit(t testing.TB, opts GlobalOptions) { } func testRunBackup(t testing.TB, dir string, target []string, opts BackupOptions, gopts GlobalOptions) { + ctx, cancel := context.WithCancel(gopts.ctx) + defer cancel() + + var wg errgroup.Group + term := termstatus.New(gopts.stdout, gopts.stderr) + wg.Go(func() error { term.Run(ctx); return nil }) + gopts.stdout = ioutil.Discard t.Logf("backing up %v in %v", target, dir) if dir != "" { cleanup := fs.TestChdir(t, dir) defer cleanup() } - rtest.OK(t, runBackup(opts, gopts, target)) + + rtest.OK(t, runBackup(opts, gopts, term, target)) + + cancel() + + err := wg.Wait() + if err != nil { + t.Fatal(err) + } } func testRunList(t testing.TB, tpe string, opts GlobalOptions) restic.IDs { diff --git a/cmd/restic/main.go b/cmd/restic/main.go index c1a42bd90..01a902b1d 100644 --- a/cmd/restic/main.go +++ b/cmd/restic/main.go @@ -30,19 +30,19 @@ directories in an encrypted repository stored on different backends. DisableAutoGenTag: true, PersistentPreRunE: func(c *cobra.Command, args []string) error { - // set verbosity + // set verbosity, default is one globalOptions.verbosity = 1 - if globalOptions.Quiet && (globalOptions.Verbose || globalOptions.Debug) { - return errors.Fatal("--quiet and --verbose or --debug cannot be specified at the same time") + if globalOptions.Quiet && (globalOptions.Verbose > 1) { + return errors.Fatal("--quiet and --verbose cannot be specified at the same time") } switch { + case globalOptions.Verbose >= 2: + globalOptions.verbosity = 3 + case globalOptions.Verbose > 0: + globalOptions.verbosity = 2 case globalOptions.Quiet: globalOptions.verbosity = 0 - case globalOptions.Verbose: - globalOptions.verbosity = 2 - case globalOptions.Debug: - globalOptions.verbosity = 3 } // parse extended options diff --git a/internal/archiver/archiver_test.go b/internal/archiver/archiver_test.go index 3f4daf5ec..a8557ef2a 100644 --- a/internal/archiver/archiver_test.go +++ b/internal/archiver/archiver_test.go @@ -2,7 +2,6 @@ package archiver import ( "context" - "fmt" "io/ioutil" "os" "path/filepath" @@ -274,7 +273,6 @@ func (repo *blobCountingRepo) SaveTree(ctx context.Context, t *restic.Tree) (res repo.m.Lock() repo.saved[h]++ repo.m.Unlock() - fmt.Printf("savetree %v", h) return id, err } diff --git a/internal/ui/backup.go b/internal/ui/backup.go new file mode 100644 index 000000000..ebd56b8bc --- /dev/null +++ b/internal/ui/backup.go @@ -0,0 +1,343 @@ +package ui + +import ( + "context" + "fmt" + "os" + "sort" + "sync" + "time" + + "github.com/restic/restic/internal/archiver" + "github.com/restic/restic/internal/restic" + "github.com/restic/restic/internal/ui/termstatus" +) + +type counter struct { + Files, Dirs uint + Bytes uint64 +} + +type fileWorkerMessage struct { + filename string + done bool +} + +// Backup reports progress for the `backup` command. +type Backup struct { + *Message + *StdioWrapper + + MinUpdatePause time.Duration + + term *termstatus.Terminal + v uint + start time.Time + + totalBytes uint64 + + totalCh chan counter + processedCh chan counter + errCh chan struct{} + workerCh chan fileWorkerMessage + + summary struct { + sync.Mutex + Files, Dirs struct { + New uint + Changed uint + Unchanged uint + } + archiver.ItemStats + } +} + +// NewBackup returns a new backup progress reporter. +func NewBackup(term *termstatus.Terminal, verbosity uint) *Backup { + return &Backup{ + Message: NewMessage(term, verbosity), + StdioWrapper: NewStdioWrapper(term), + term: term, + v: verbosity, + start: time.Now(), + + // limit to 60fps by default + MinUpdatePause: time.Second / 60, + + totalCh: make(chan counter), + processedCh: make(chan counter), + errCh: make(chan struct{}), + workerCh: make(chan fileWorkerMessage), + } +} + +// Run regularly updates the status lines. It should be called in a separate +// goroutine. +func (b *Backup) Run(ctx context.Context) error { + var ( + lastUpdate time.Time + total, processed counter + errors uint + started bool + currentFiles = make(map[string]struct{}) + secondsRemaining uint64 + ) + + t := time.NewTicker(time.Second) + defer t.Stop() + + for { + select { + case <-ctx.Done(): + return nil + case t, ok := <-b.totalCh: + if ok { + total = t + started = true + } else { + // scan has finished + b.totalCh = nil + b.totalBytes = total.Bytes + } + case s := <-b.processedCh: + processed.Files += s.Files + processed.Dirs += s.Dirs + processed.Bytes += s.Bytes + started = true + case <-b.errCh: + errors++ + started = true + case m := <-b.workerCh: + if m.done { + delete(currentFiles, m.filename) + } else { + currentFiles[m.filename] = struct{}{} + } + case <-t.C: + if !started { + continue + } + + if b.totalCh == nil { + secs := float64(time.Since(b.start) / time.Second) + todo := float64(total.Bytes - processed.Bytes) + secondsRemaining = uint64(secs / float64(processed.Bytes) * todo) + } + } + + // limit update frequency + if time.Since(lastUpdate) < b.MinUpdatePause { + continue + } + lastUpdate = time.Now() + + b.update(total, processed, errors, currentFiles, secondsRemaining) + } +} + +// update updates the status lines. +func (b *Backup) update(total, processed counter, errors uint, currentFiles map[string]struct{}, secs uint64) { + var status string + if total.Files == 0 && total.Dirs == 0 { + // no total count available yet + status = fmt.Sprintf("[%s] %v files, %s, %d errors", + formatDuration(time.Since(b.start)), + processed.Files, formatBytes(processed.Bytes), errors, + ) + } else { + var eta string + + if secs > 0 { + eta = fmt.Sprintf(" ETA %s", formatSeconds(secs)) + } + + // include totals + status = fmt.Sprintf("[%s] %s %v files %s, total %v files %v, %d errors%s", + formatDuration(time.Since(b.start)), + formatPercent(processed.Bytes, total.Bytes), + processed.Files, + formatBytes(processed.Bytes), + total.Files, + formatBytes(total.Bytes), + errors, + eta, + ) + } + + lines := make([]string, 0, len(currentFiles)+1) + for filename := range currentFiles { + lines = append(lines, filename) + } + sort.Sort(sort.StringSlice(lines)) + lines = append([]string{status}, lines...) + + b.term.SetStatus(lines) +} + +// ScannerError is the error callback function for the scanner, it prints the +// error in verbose mode and returns nil. +func (b *Backup) ScannerError(item string, fi os.FileInfo, err error) error { + b.V("scan: %v\n", err) + return nil +} + +// Error is the error callback function for the archiver, it prints the error and returns nil. +func (b *Backup) Error(item string, fi os.FileInfo, err error) error { + b.E("error: %v\n", err) + b.errCh <- struct{}{} + return nil +} + +// StartFile is called when a file is being processed by a worker. +func (b *Backup) StartFile(filename string) { + b.workerCh <- fileWorkerMessage{ + filename: filename, + } +} + +// CompleteBlob is called for all saved blobs for files. +func (b *Backup) CompleteBlob(filename string, bytes uint64) { + b.processedCh <- counter{Bytes: bytes} +} + +func formatPercent(numerator uint64, denominator uint64) string { + if denominator == 0 { + return "" + } + + percent := 100.0 * float64(numerator) / float64(denominator) + + if percent > 100 { + percent = 100 + } + + return fmt.Sprintf("%3.2f%%", percent) +} + +func formatSeconds(sec uint64) string { + hours := sec / 3600 + sec -= hours * 3600 + min := sec / 60 + sec -= min * 60 + if hours > 0 { + return fmt.Sprintf("%d:%02d:%02d", hours, min, sec) + } + + return fmt.Sprintf("%d:%02d", min, sec) +} + +func formatDuration(d time.Duration) string { + sec := uint64(d / time.Second) + return formatSeconds(sec) +} + +func formatBytes(c uint64) string { + b := float64(c) + switch { + case c > 1<<40: + return fmt.Sprintf("%.3f TiB", b/(1<<40)) + case c > 1<<30: + return fmt.Sprintf("%.3f GiB", b/(1<<30)) + case c > 1<<20: + return fmt.Sprintf("%.3f MiB", b/(1<<20)) + case c > 1<<10: + return fmt.Sprintf("%.3f KiB", b/(1<<10)) + default: + return fmt.Sprintf("%d B", c) + } +} + +// CompleteItemFn is the status callback function for the archiver when a +// file/dir has been saved successfully. +func (b *Backup) CompleteItemFn(item string, previous, current *restic.Node, s archiver.ItemStats, d time.Duration) { + b.summary.Lock() + b.summary.ItemStats.Add(s) + b.summary.Unlock() + + if current == nil { + return + } + + switch current.Type { + case "file": + b.processedCh <- counter{Files: 1} + b.workerCh <- fileWorkerMessage{ + filename: item, + done: true, + } + case "dir": + b.processedCh <- counter{Dirs: 1} + } + + if current.Type == "dir" { + if previous == nil { + b.VV("new %v, saved in %.3fs (%v added, %v metadata)", item, d.Seconds(), formatBytes(s.DataSize), formatBytes(s.TreeSize)) + b.summary.Lock() + b.summary.Dirs.New++ + b.summary.Unlock() + return + } + + if previous.Equals(*current) { + b.VV("unchanged %v", item) + b.summary.Lock() + b.summary.Dirs.Unchanged++ + b.summary.Unlock() + } else { + b.VV("modified %v, saved in %.3fs (%v added, %v metadata)", item, d.Seconds(), formatBytes(s.DataSize), formatBytes(s.TreeSize)) + b.summary.Lock() + b.summary.Dirs.Changed++ + b.summary.Unlock() + } + + } else if current.Type == "file" { + + b.workerCh <- fileWorkerMessage{ + done: true, + filename: item, + } + + if previous == nil { + b.VV("new %v, saved in %.3fs (%v added)", item, d.Seconds(), formatBytes(s.DataSize)) + b.summary.Lock() + b.summary.Files.New++ + b.summary.Unlock() + return + } + + if previous.Equals(*current) { + b.VV("unchanged %v", item) + b.summary.Lock() + b.summary.Files.Unchanged++ + b.summary.Unlock() + } else { + b.VV("modified %v, saved in %.3fs (%v added)", item, d.Seconds(), formatBytes(s.DataSize)) + b.summary.Lock() + b.summary.Files.Changed++ + b.summary.Unlock() + } + } +} + +// ReportTotal sets the total stats up to now +func (b *Backup) ReportTotal(item string, s archiver.ScanStats) { + b.totalCh <- counter{Files: s.Files, Dirs: s.Dirs, Bytes: s.Bytes} + + if item == "" { + b.V("scan finished in %.3fs", time.Since(b.start).Seconds()) + close(b.totalCh) + return + } +} + +// Finish prints the finishing messages. +func (b *Backup) Finish() { + b.V("processed %s in %s", formatBytes(b.totalBytes), formatDuration(time.Since(b.start))) + b.V("\n") + b.V("Files: %5d new, %5d changed, %5d unmodified\n", b.summary.Files.New, b.summary.Files.Changed, b.summary.Files.Unchanged) + b.V("Dirs: %5d new, %5d changed, %5d unmodified\n", b.summary.Dirs.New, b.summary.Dirs.Changed, b.summary.Dirs.Unchanged) + b.VV("Data Blobs: %5d new\n", b.summary.ItemStats.DataBlobs) + b.VV("Tree Blobs: %5d new\n", b.summary.ItemStats.TreeBlobs) + b.V("Added: %-5s\n", formatBytes(b.summary.ItemStats.DataSize+b.summary.ItemStats.TreeSize)) + b.V("\n") +} diff --git a/internal/ui/message.go b/internal/ui/message.go new file mode 100644 index 000000000..75e54b019 --- /dev/null +++ b/internal/ui/message.go @@ -0,0 +1,45 @@ +package ui + +import "github.com/restic/restic/internal/ui/termstatus" + +// Message reports progress with messages of different verbosity. +type Message struct { + term *termstatus.Terminal + v uint +} + +// NewMessage returns a message progress reporter with underlying terminal +// term. +func NewMessage(term *termstatus.Terminal, verbosity uint) *Message { + return &Message{ + term: term, + v: verbosity, + } +} + +// E reports an error +func (m *Message) E(msg string, args ...interface{}) { + m.term.Errorf(msg, args...) +} + +// P prints a message if verbosity >= 1, this is used for normal messages which +// are not errors. +func (m *Message) P(msg string, args ...interface{}) { + if m.v >= 1 { + m.term.Printf(msg, args...) + } +} + +// V prints a message if verbosity >= 2, this is used for verbose messages. +func (m *Message) V(msg string, args ...interface{}) { + if m.v >= 2 { + m.term.Printf(msg, args...) + } +} + +// VV prints a message if verbosity >= 3, this is used for debug messages. +func (m *Message) VV(msg string, args ...interface{}) { + if m.v >= 3 { + m.term.Printf(msg, args...) + } +} diff --git a/internal/ui/stdio_wrapper.go b/internal/ui/stdio_wrapper.go new file mode 100644 index 000000000..eccaefb7b --- /dev/null +++ b/internal/ui/stdio_wrapper.go @@ -0,0 +1,86 @@ +package ui + +import ( + "bytes" + "io" + + "github.com/restic/restic/internal/ui/termstatus" +) + +// StdioWrapper provides stdout and stderr integration with termstatus. +type StdioWrapper struct { + stdout *lineWriter + stderr *lineWriter +} + +// NewStdioWrapper initializes a new stdio wrapper that can be used in place of +// os.Stdout or os.Stderr. +func NewStdioWrapper(term *termstatus.Terminal) *StdioWrapper { + return &StdioWrapper{ + stdout: newLineWriter(term.Print), + stderr: newLineWriter(term.Error), + } +} + +// Stdout returns a writer that is line buffered and can be used in place of +// os.Stdout. On Close(), the remaining bytes are written, followed by a line +// break. +func (w *StdioWrapper) Stdout() io.WriteCloser { + return w.stdout +} + +// Stderr returns a writer that is line buffered and can be used in place of +// os.Stderr. On Close(), the remaining bytes are written, followed by a line +// break. +func (w *StdioWrapper) Stderr() io.WriteCloser { + return w.stderr +} + +type lineWriter struct { + buf *bytes.Buffer + print func(string) +} + +var _ io.WriteCloser = &lineWriter{} + +func newLineWriter(print func(string)) *lineWriter { + return &lineWriter{buf: bytes.NewBuffer(nil), print: print} +} + +func (w *lineWriter) Write(data []byte) (n int, err error) { + n, err = w.buf.Write(data) + if err != nil { + return n, err + } + + // look for line breaks + buf := w.buf.Bytes() + skip := 0 + for i := 0; i < len(buf); { + if buf[i] == '\n' { + // found line + w.print(string(buf[:i+1])) + buf = buf[i+1:] + skip += i + 1 + i = 0 + continue + } + + i++ + } + + _ = w.buf.Next(skip) + + return n, err +} + +func (w *lineWriter) Flush() error { + if w.buf.Len() > 0 { + w.print(string(append(w.buf.Bytes(), '\n'))) + } + return nil +} + +func (w *lineWriter) Close() error { + return w.Flush() +} diff --git a/internal/ui/stdio_wrapper_test.go b/internal/ui/stdio_wrapper_test.go new file mode 100644 index 000000000..fc071f992 --- /dev/null +++ b/internal/ui/stdio_wrapper_test.go @@ -0,0 +1,95 @@ +package ui + +import ( + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestStdioWrapper(t *testing.T) { + var tests = []struct { + inputs [][]byte + outputs []string + }{ + { + inputs: [][]byte{ + []byte("foo"), + }, + outputs: []string{ + "foo\n", + }, + }, + { + inputs: [][]byte{ + []byte("foo"), + []byte("bar"), + []byte("\n"), + []byte("baz"), + }, + outputs: []string{ + "foobar\n", + "baz\n", + }, + }, + { + inputs: [][]byte{ + []byte("foo"), + []byte("bar\nbaz\n"), + []byte("bump\n"), + }, + outputs: []string{ + "foobar\n", + "baz\n", + "bump\n", + }, + }, + { + inputs: [][]byte{ + []byte("foo"), + []byte("bar\nbaz\n"), + []byte("bum"), + []byte("p\nx"), + []byte("x"), + []byte("x"), + []byte("z"), + }, + outputs: []string{ + "foobar\n", + "baz\n", + "bump\n", + "xxxz\n", + }, + }, + } + + for _, test := range tests { + t.Run("", func(t *testing.T) { + var lines []string + print := func(s string) { + lines = append(lines, s) + } + + w := newLineWriter(print) + + for _, data := range test.inputs { + n, err := w.Write(data) + if err != nil { + t.Fatal(err) + } + + if n != len(data) { + t.Errorf("invalid length returned by Write, want %d, got %d", len(data), n) + } + } + + err := w.Close() + if err != nil { + t.Fatal(err) + } + + if !cmp.Equal(test.outputs, lines) { + t.Error(cmp.Diff(test.outputs, lines)) + } + }) + } +} From 1449d7dc29eae36954c1190f57004a1f2b35e09f Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sat, 28 Apr 2018 14:36:24 +0200 Subject: [PATCH 27/30] Remove background checking code --- cmd/restic/background.go | 9 --------- cmd/restic/background_linux.go | 21 --------------------- 2 files changed, 30 deletions(-) delete mode 100644 cmd/restic/background.go delete mode 100644 cmd/restic/background_linux.go diff --git a/cmd/restic/background.go b/cmd/restic/background.go deleted file mode 100644 index 2f115adfd..000000000 --- a/cmd/restic/background.go +++ /dev/null @@ -1,9 +0,0 @@ -// +build !linux - -package main - -// IsProcessBackground should return true if it is running in the background or false if not -func IsProcessBackground() bool { - //TODO: Check if the process are running in the background in other OS than linux - return false -} diff --git a/cmd/restic/background_linux.go b/cmd/restic/background_linux.go deleted file mode 100644 index b9a2a2f00..000000000 --- a/cmd/restic/background_linux.go +++ /dev/null @@ -1,21 +0,0 @@ -package main - -import ( - "syscall" - "unsafe" - - "github.com/restic/restic/internal/debug" -) - -// IsProcessBackground returns true if it is running in the background or false if not -func IsProcessBackground() bool { - var pid int - _, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(syscall.Stdin), syscall.TIOCGPGRP, uintptr(unsafe.Pointer(&pid))) - - if err != 0 { - debug.Log("Can't check if we are in the background. Using default behaviour. Error: %s\n", err.Error()) - return false - } - - return pid != syscall.Getpgrp() -} From 16c314ab7f7806f387af648e3099d18711fb1713 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sat, 28 Apr 2018 15:24:36 +0200 Subject: [PATCH 28/30] termstatus: Don't print status if in background --- internal/ui/termstatus/background.go | 9 +++++++++ internal/ui/termstatus/background_linux.go | 21 +++++++++++++++++++++ internal/ui/termstatus/status.go | 12 ++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 internal/ui/termstatus/background.go create mode 100644 internal/ui/termstatus/background_linux.go diff --git a/internal/ui/termstatus/background.go b/internal/ui/termstatus/background.go new file mode 100644 index 000000000..e371c18df --- /dev/null +++ b/internal/ui/termstatus/background.go @@ -0,0 +1,9 @@ +// +build !linux + +package termstatus + +// IsProcessBackground reports whether the current process is running in the +// background. Not implemented for this platform. +func IsProcessBackground() bool { + return false +} diff --git a/internal/ui/termstatus/background_linux.go b/internal/ui/termstatus/background_linux.go new file mode 100644 index 000000000..f99091128 --- /dev/null +++ b/internal/ui/termstatus/background_linux.go @@ -0,0 +1,21 @@ +package termstatus + +import ( + "syscall" + "unsafe" + + "github.com/restic/restic/internal/debug" +) + +// IsProcessBackground reports whether the current process is running in the background. +func IsProcessBackground() bool { + var pid int + _, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(syscall.Stdin), syscall.TIOCGPGRP, uintptr(unsafe.Pointer(&pid))) + + if err != 0 { + debug.Log("Can't check if we are in the background. Using default behaviour. Error: %s\n", err.Error()) + return false + } + + return pid != syscall.Getpgrp() +} diff --git a/internal/ui/termstatus/status.go b/internal/ui/termstatus/status.go index 25fdcc341..6682430e8 100644 --- a/internal/ui/termstatus/status.go +++ b/internal/ui/termstatus/status.go @@ -95,6 +95,10 @@ func (t *Terminal) run(ctx context.Context) { for { select { case <-ctx.Done(): + if IsProcessBackground() { + // ignore all messages, do nothing, we are in the background process group + continue + } t.undoStatus(statusLines) err := t.wr.Flush() @@ -105,6 +109,10 @@ func (t *Terminal) run(ctx context.Context) { return case msg := <-t.msg: + if IsProcessBackground() { + // ignore all messages, do nothing, we are in the background process group + continue + } t.undoStatus(statusLines) var dst io.Writer @@ -144,6 +152,10 @@ func (t *Terminal) run(ctx context.Context) { } case stat := <-t.status: + if IsProcessBackground() { + // ignore all messages, do nothing, we are in the background process group + continue + } t.undoStatus(statusLines) statusBuf.Reset() From 6b12b92339b199dd7f72f831b0dad569d5d24106 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sat, 28 Apr 2018 15:53:53 +0200 Subject: [PATCH 29/30] Add entry to changelog --- changelog/unreleased/issue-549 | 36 ++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 changelog/unreleased/issue-549 diff --git a/changelog/unreleased/issue-549 b/changelog/unreleased/issue-549 new file mode 100644 index 000000000..01cb38144 --- /dev/null +++ b/changelog/unreleased/issue-549 @@ -0,0 +1,36 @@ +Enhancement: Rework archiver code + +The core archiver code and the complementary code for the `backup` command was +rewritten completely. This resolves very annoying issues such as 549. + +Basically, with the old code, restic took the last path component of each +to-be-saved file or directory as the top-level file/directory within the +snapshot. This meant that when called as `restic backup /home/user/foo`, the +snapshot would contain the files in the directory `/home/user/foo` as `/foo`. + +This is not the case any more with the new archiver code. Now, restic works +very similar to what `tar` does: When restic is called with an absolute path to +save, then it'll preserve the directory structure within the snapshot. For the +example above, the snapshot would contain the files in the directory within +`/home/user/foo` in the snapshot. For relative directories, it only preserves +the relative path components. So `restic backup user/foo` will save the files +as `/user/foo` in the snapshot. + +While we were at it, the status display and notification system was completely +rewritten. By default, restic now shows which files are currently read (unless +`--quiet` is specified) in a multi-line status display. + +The `backup` command also gained a new option: `--verbose`. It can be specified +once (which prints a bit more detail what restic is doing) or twice (which +prints a line for each file/directory restic encountered, together with some +statistics). + +https://github.com/restic/restic/issues/549 +https://github.com/restic/restic/issues/1286 +https://github.com/restic/restic/issues/446 +https://github.com/restic/restic/issues/1344 +https://github.com/restic/restic/issues/1416 +https://github.com/restic/restic/issues/1456 +https://github.com/restic/restic/issues/1145 +https://github.com/restic/restic/issues/1160 +https://github.com/restic/restic/pull/1494 From c3cc5d7cee97d62033257702b9549ff849152557 Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Sat, 28 Apr 2018 16:19:16 +0200 Subject: [PATCH 30/30] Update docs --- doc/010_introduction.rst | 3 + doc/020_installation.rst | 14 ++- doc/030_preparing_a_new_repo.rst | 22 +++-- doc/040_backup.rst | 155 ++++++++++++++++++++++--------- doc/045_working_with_repos.rst | 22 ++--- doc/050_restore.rst | 12 +-- doc/060_forget.rst | 8 +- doc/070_encryption.rst | 10 +- doc/075_scripting.rst | 6 +- doc/100_references.rst | 39 +++----- doc/110_talks.rst | 34 +++++++ doc/cache.rst | 36 ------- doc/conf.py | 2 +- doc/index.rst | 1 + doc/manual_rest.rst | 75 ++++++++------- 15 files changed, 255 insertions(+), 184 deletions(-) create mode 100644 doc/110_talks.rst delete mode 100644 doc/cache.rst diff --git a/doc/010_introduction.rst b/doc/010_introduction.rst index 6128c6662..5c213f6cd 100644 --- a/doc/010_introduction.rst +++ b/doc/010_introduction.rst @@ -14,3 +14,6 @@ Introduction ############ +Restic is a fast and secure backup program. In the following sections, we will +present typical workflows, starting with installing, preparing a new +repository, and making the first backup. diff --git a/doc/020_installation.rst b/doc/020_installation.rst index df10fcb84..6cab0c9a1 100644 --- a/doc/020_installation.rst +++ b/doc/020_installation.rst @@ -145,9 +145,17 @@ Admin rights. Docker Container **************** +We're maintaining a bare docker container with just a few files and the restic +binary, you can get it with `docker pull` like this: + +.. code-block:: console + + $ docker pull restic/restic + .. note:: - | A docker container is available as a contribution (Thank you!). - | You can find it at https://github.com/Lobaro/restic-backup-docker + | Another docker container which offers more configuration options is + | available as a contribution (Thank you!). You can find it at + | https://github.com/Lobaro/restic-backup-docker From Source *********** @@ -173,7 +181,7 @@ You can easily cross-compile restic for all supported platforms, just supply the target OS and platform via the command-line options like this (for Windows and FreeBSD respectively): -:: +.. code-block:: console $ go run build.go --goos windows --goarch amd64 diff --git a/doc/030_preparing_a_new_repo.rst b/doc/030_preparing_a_new_repo.rst index 7d865d9a5..2133ec7c2 100644 --- a/doc/030_preparing_a_new_repo.rst +++ b/doc/030_preparing_a_new_repo.rst @@ -15,20 +15,24 @@ Preparing a new repository ########################## The place where your backups will be saved at is called a "repository". -This chapter explains how to create ("init") such a repository. +This chapter explains how to create ("init") such a repository. The repository +can be stored locally, or on some remote server or service. We'll first cover +using a local repository, the remaining sections of this chapter cover all the +other options. You can skip to the next chapter once you've read the relevant +section here. Local ***** -In order to create a repository at ``/tmp/backup``, run the following +In order to create a repository at ``/srv/restic-repo``, run the following command and enter the same password twice: .. code-block:: console - $ restic init --repo /tmp/backup + $ restic init --repo /srv/restic-repo enter password for new backend: enter password again: - created restic backend 085b3c76b9 at /tmp/backup + created restic backend 085b3c76b9 at /srv/restic-repo Please note that knowledge of your password is required to access the repository. Losing your password means that your data is irrecoverably lost. @@ -55,10 +59,10 @@ simply be achieved by changing the URL scheme in the ``init`` command: .. code-block:: console - $ restic -r sftp:user@host:/tmp/backup init + $ restic -r sftp:user@host:/srv/restic-repo init enter password for new backend: enter password again: - created restic backend f1c6108821 at sftp:user@host:/tmp/backup + created restic backend f1c6108821 at sftp:user@host:/srv/restic-repo Please note that knowledge of your password is required to access the repository. Losing your password means that your data is irrecoverably lost. @@ -87,7 +91,7 @@ specify the user name in this case): :: - $ restic -r sftp:foo:/tmp/backup init + $ restic -r sftp:foo:/srv/restic-repo init You can also add an entry with a special host name which does not exist, just for use with restic, and use the ``Hostname`` option to set the @@ -104,7 +108,7 @@ Then use it in the backend specification: :: - $ restic -r sftp:restic-backup-host:/tmp/backup init + $ restic -r sftp:restic-backup-host:/srv/restic-repo init Last, if you'd like to use an entirely different program to create the SFTP connection, you can specify the command to be run with the option @@ -509,5 +513,5 @@ On MSYS2, you can install ``winpty`` as follows: .. code-block:: console $ pacman -S winpty - $ winpty restic -r /tmp/backup init + $ winpty restic -r /srv/restic-repo init diff --git a/doc/040_backup.rst b/doc/040_backup.rst index a181879d5..badc836ba 100644 --- a/doc/040_backup.rst +++ b/doc/040_backup.rst @@ -21,43 +21,88 @@ again: .. code-block:: console - $ restic -r /tmp/backup backup ~/work + $ restic -r /srv/restic-repo --verbose backup ~/work + open repository enter password for repository: - scan [/home/user/work] - scanned 764 directories, 1816 files in 0:00 - [0:29] 100.00% 54.732 MiB/s 1.582 GiB / 1.582 GiB 2580 / 2580 items 0 errors ETA 0:00 - duration: 0:29, 54.47MiB/s + password is correct + lock repository + load index files + start scan + start backup + scan finished in 1.837s + processed 1.720 GiB in 0:12 + Files: 5307 new, 0 changed, 0 unmodified + Dirs: 1867 new, 0 changed, 0 unmodified + Added: 1.700 GiB snapshot 40dc1520 saved As you can see, restic created a backup of the directory and was pretty fast! The specific snapshot just created is identified by a sequence of hexadecimal characters, ``40dc1520`` in this case. +If you don't pass the ``--verbose`` option, restic will print less data (but +you'll still get a nice live status display). + If you run the command again, restic will create another snapshot of your data, but this time it's even faster. This is de-duplication at work! .. code-block:: console - $ restic -r /tmp/backup backup ~/work + $ restic -r /srv/restic-repo backup --verbose ~/work + open repository enter password for repository: - using parent snapshot 40dc1520aa6a07b7b3ae561786770a01951245d2367241e71e9485f18ae8228c - scan [/home/user/work] - scanned 764 directories, 1816 files in 0:00 - [0:00] 100.00% 0B/s 1.582 GiB / 1.582 GiB 2580 / 2580 items 0 errors ETA 0:00 - duration: 0:00, 6572.38MiB/s + password is correct + lock repository + load index files + using parent snapshot d875ae93 + start scan + start backup + scan finished in 1.881s + processed 1.720 GiB in 0:03 + Files: 0 new, 0 changed, 5307 unmodified + Dirs: 0 new, 0 changed, 1867 unmodified + Added: 0 B snapshot 79766175 saved -You can even backup individual files in the same repository. +You can even backup individual files in the same repository (not passing +``--verbose`` means less output): .. code-block:: console - $ restic -r /tmp/backup backup ~/work.txt - scan [/home/user/work.txt] - scanned 0 directories, 1 files in 0:00 - [0:00] 100.00% 0B/s 220B / 220B 1 / 1 items 0 errors ETA 0:00 - duration: 0:00, 0.03MiB/s - snapshot 31f7bd63 saved + $ restic -r /srv/restic-repo backup ~/work.txt + enter password for repository: + password is correct + snapshot 249d0210 saved + +If you're interested in what restic does, pass ``--verbose`` twice (or +``--verbose 2``) to display detailed information about each file and directory +restic encounters: + +.. code-block:: console + + $ echo 'more data foo bar' >> ~/work.txt + + $ restic -r /srv/restic-repo backup --verbose --verbose ~/work.txt + open repository + enter password for repository: + password is correct + lock repository + load index files + using parent snapshot f3f8d56b + start scan + start backup + scan finished in 2.115s + modified /home/user/work.txt, saved in 0.007s (22 B added) + modified /home/user/, saved in 0.008s (0 B added, 378 B metadata) + modified /home/, saved in 0.009s (0 B added, 375 B metadata) + processed 22 B in 0:02 + Files: 0 new, 1 changed, 0 unmodified + Dirs: 0 new, 2 changed, 0 unmodified + Data Blobs: 1 new + Tree Blobs: 3 new + Added: 1.116 KiB + snapshot 8dc503fc saved In fact several hosts may use the same repository to backup directories and files leading to a greater de-duplication. @@ -87,33 +132,53 @@ the exclude options are: - ``--exclude-if-present`` Specified one or more times to exclude a folders content if it contains a given file (optionally having a given header) -Basic example: + Let's say we have a file called ``excludes.txt`` with the following content: -.. code-block:: console - - $ cat exclude +:: # exclude go-files *.go # exclude foo/x/y/z/bar foo/x/bar foo/bar foo/**/bar - $ restic -r /tmp/backup backup ~/work --exclude="*.c" --exclude-file=exclude + +It can be used like this: + +.. code-block:: console + + $ restic -r /srv/restic-repo backup ~/work --exclude="*.c" --exclude-file=excludes.txt + +This instruct restic to exclude files matching the following criteria: + + * All files matching ``*.go`` (second line in ``excludes.txt``) + * All files and sub-directories named ``bar`` which reside somewhere below a directory called ``foo`` (fourth line in ``excludes.txt``) + * All files matching ``*.c`` (parameter ``--exclude``) Please see ``restic help backup`` for more specific information about each exclude option. Patterns use `filepath.Glob `__ internally, -see `filepath.Match `__ for syntax. -Patterns are tested against the full path of a file/dir to be saved, not only -against the relative path below the argument given to restic backup. -Patterns need to match on complete path components. (``foo`` matches -``/dir1/foo/dir2/file`` and ``/dir/foo`` but does not match ``/dir/foobar`` or -``barfoo``.) A trailing ``/`` is ignored. A leading ``/`` anchors the -pattern at the root directory. (``/bin`` matches ``/bin/bash`` but does not -match ``/usr/bin/restic``.) Regular wildcards cannot be used to match over the -directory separator ``/``. (``b*ash`` matches ``/bin/bash`` but does not match -``/bin/ash``.) However ``**`` matches arbitrary subdirectories. (``foo/**/bar`` -matches ``/dir1/foo/dir2/bar/file``, ``/foo/bar/file`` and ``/tmp/foo/bar``.) -Environment-variables in exclude-files are expanded with -`os.ExpandEnv `__. +see `filepath.Match `__ for +syntax. Patterns are tested against the full path of a file/dir to be saved, +even if restic is passed a relative path to save. Environment-variables in +exclude-files are expanded with `os.ExpandEnv `__. + +Patterns need to match on complete path components. For example, the pattern ``foo``: + + * matches ``/dir1/foo/dir2/file`` and ``/dir/foo`` + * does not match ``/dir/foobar`` or ``barfoo`` + +A trailing ``/`` is ignored, a leading ``/`` anchors the +pattern at the root directory. This means, ``/bin`` matches ``/bin/bash`` but +does not match ``/usr/bin/restic``. + +Regular wildcards cannot be used to match over the +directory separator ``/``. For example: ``b*ash`` matches ``/bin/bash`` but does not match +``/bin/ash``. + +For this, the special wildcard ``**`` can be used to match arbitrary +sub-directories: The pattern ``foo/**/bar`` matches: + + * ``/dir1/foo/dir2/bar/file`` + * ``/foo/bar/file`` + * ``/tmp/foo/bar`` By specifying the option ``--one-file-system`` you can instruct restic to only backup files from the file systems the initially specified files @@ -122,15 +187,15 @@ backup ``/sys`` or ``/dev`` on a Linux system: .. code-block:: console - $ restic -r /tmp/backup backup --one-file-system / + $ restic -r /srv/restic-repo backup --one-file-system / By using the ``--files-from`` option you can read the files you want to backup from a file. This is especially useful if a lot of files have to be backed up that are not in the same folder or are maybe pre-filtered by other software. -For example maybe you want to backup files that have a certain filename -in them: +For example maybe you want to backup files which have a name that matches a +certain pattern: .. code-block:: console @@ -140,14 +205,14 @@ You can then use restic to backup the filtered files: .. code-block:: console - $ restic -r /tmp/backup backup --files-from /tmp/files_to_backup + $ restic -r /srv/restic-repo backup --files-from /tmp/files_to_backup Incidentally you can also combine ``--files-from`` with the normal files args: .. code-block:: console - $ restic -r /tmp/backup backup --files-from /tmp/files_to_backup /tmp/some_additional_file + $ restic -r /srv/restic-repo backup --files-from /tmp/files_to_backup /tmp/some_additional_file Paths in the listing file can be absolute or relative. @@ -159,7 +224,7 @@ and displays a small statistic, just pass the command two snapshot IDs: .. code-block:: console - $ restic -r /tmp/backup diff 5845b002 2ab627a6 + $ restic -r /srv/restic-repo diff 5845b002 2ab627a6 password is correct comparing snapshot ea657ce5 to 2ab627a6: @@ -206,7 +271,7 @@ this mode of operation, just supply the option ``--stdin`` to the .. code-block:: console - $ mysqldump [...] | restic -r /tmp/backup backup --stdin + $ mysqldump [...] | restic -r /srv/restic-repo backup --stdin This creates a new snapshot of the output of ``mysqldump``. You can then use e.g. the fuse mounting option (see below) to mount the repository @@ -217,7 +282,7 @@ specified with ``--stdin-filename``, e.g. like this: .. code-block:: console - $ mysqldump [...] | restic -r /tmp/backup backup --stdin --stdin-filename production.sql + $ mysqldump [...] | restic -r /srv/restic-repo backup --stdin --stdin-filename production.sql Tags for backup *************** @@ -227,7 +292,7 @@ information. Just specify the tags for a snapshot one by one with ``--tag``: .. code-block:: console - $ restic -r /tmp/backup backup --tag projectX --tag foo --tag bar ~/work + $ restic -r /srv/restic-repo backup --tag projectX --tag foo --tag bar ~/work [...] The tags can later be used to keep (or forget) snapshots with the ``forget`` diff --git a/doc/045_working_with_repos.rst b/doc/045_working_with_repos.rst index 5ee39ea26..773234ca0 100644 --- a/doc/045_working_with_repos.rst +++ b/doc/045_working_with_repos.rst @@ -22,7 +22,7 @@ Now, you can list all the snapshots stored in the repository: .. code-block:: console - $ restic -r /tmp/backup snapshots + $ restic -r /srv/restic-repo snapshots enter password for repository: ID Date Host Tags Directory ---------------------------------------------------------------------- @@ -36,7 +36,7 @@ You can filter the listing by directory path: .. code-block:: console - $ restic -r /tmp/backup snapshots --path="/srv" + $ restic -r /srv/restic-repo snapshots --path="/srv" enter password for repository: ID Date Host Tags Directory ---------------------------------------------------------------------- @@ -47,7 +47,7 @@ Or filter by host: .. code-block:: console - $ restic -r /tmp/backup snapshots --host luigi + $ restic -r /srv/restic-repo snapshots --host luigi enter password for repository: ID Date Host Tags Directory ---------------------------------------------------------------------- @@ -74,7 +74,7 @@ backup data is consistent and the integrity is unharmed: .. code-block:: console - $ restic -r /tmp/backup check + $ restic -r /srv/restic-repo check Load indexes ciphertext verification failed @@ -83,7 +83,7 @@ yield the same error: .. code-block:: console - $ restic -r /tmp/backup restore 79766175 --target /tmp/restore-work + $ restic -r /srv/restic-repo restore 79766175 --target /tmp/restore-work Load indexes ciphertext verification failed @@ -93,7 +93,7 @@ data files: .. code-block:: console - $ restic -r /tmp/backup check --read-data + $ restic -r /srv/restic-repo check --read-data load indexes check all packs check snapshots, trees and blobs @@ -107,9 +107,9 @@ commands check all repository data files over 5 separate invocations: .. code-block:: console - $ restic -r /tmp/backup check --read-data-subset=1/5 - $ restic -r /tmp/backup check --read-data-subset=2/5 - $ restic -r /tmp/backup check --read-data-subset=3/5 - $ restic -r /tmp/backup check --read-data-subset=4/5 - $ restic -r /tmp/backup check --read-data-subset=5/5 + $ restic -r /srv/restic-repo check --read-data-subset=1/5 + $ restic -r /srv/restic-repo check --read-data-subset=2/5 + $ restic -r /srv/restic-repo check --read-data-subset=3/5 + $ restic -r /srv/restic-repo check --read-data-subset=4/5 + $ restic -r /srv/restic-repo check --read-data-subset=5/5 diff --git a/doc/050_restore.rst b/doc/050_restore.rst index 50c02c760..35e27e730 100644 --- a/doc/050_restore.rst +++ b/doc/050_restore.rst @@ -23,7 +23,7 @@ command to restore the contents of the latest snapshot to .. code-block:: console - $ restic -r /tmp/backup restore 79766175 --target /tmp/restore-work + $ restic -r /srv/restic-repo restore 79766175 --target /tmp/restore-work enter password for repository: restoring to /tmp/restore-work @@ -33,7 +33,7 @@ backup for a specific host, path or both. .. code-block:: console - $ restic -r /tmp/backup restore latest --target /tmp/restore-art --path "/home/art" --host luigi + $ restic -r /srv/restic-repo restore latest --target /tmp/restore-art --path "/home/art" --host luigi enter password for repository: restoring to /tmp/restore-art @@ -42,7 +42,7 @@ files in the snapshot. For example, to restore a single file: .. code-block:: console - $ restic -r /tmp/backup restore 79766175 --target /tmp/restore-work --include /work/foo + $ restic -r /srv/restic-repo restore 79766175 --target /tmp/restore-work --include /work/foo enter password for repository: restoring to /tmp/restore-work @@ -58,9 +58,9 @@ command to serve the repository with FUSE: .. code-block:: console $ mkdir /mnt/restic - $ restic -r /tmp/backup mount /mnt/restic + $ restic -r /srv/restic-repo mount /mnt/restic enter password for repository: - Now serving /tmp/backup at /mnt/restic + Now serving /srv/restic-repo at /mnt/restic Don't forget to umount after quitting! Mounting repositories via FUSE is not possible on OpenBSD, Solaris/illumos @@ -80,4 +80,4 @@ the data directly. This can be achieved by using the `dump` command, like this: .. code-block:: console - $ restic -r /tmp/backup dump latest production.sql | mysql + $ restic -r /srv/restic-repo dump latest production.sql | mysql diff --git a/doc/060_forget.rst b/doc/060_forget.rst index 2a3595952..ab5274758 100644 --- a/doc/060_forget.rst +++ b/doc/060_forget.rst @@ -35,7 +35,7 @@ repository like this: .. code-block:: console - $ restic -r /tmp/backup snapshots + $ restic -r /srv/restic-repo snapshots enter password for repository: ID Date Host Tags Directory ---------------------------------------------------------------------- @@ -50,7 +50,7 @@ command and specify the snapshot ID on the command line: .. code-block:: console - $ restic -r /tmp/backup forget bdbd3439 + $ restic -r /srv/restic-repo forget bdbd3439 enter password for repository: removed snapshot d3f01f63 @@ -58,7 +58,7 @@ Afterwards this snapshot is removed: .. code-block:: console - $ restic -r /tmp/backup snapshots + $ restic -r /srv/restic-repo snapshots enter password for repository: ID Date Host Tags Directory ---------------------------------------------------------------------- @@ -73,7 +73,7 @@ command must be run: .. code-block:: console - $ restic -r /tmp/backup prune + $ restic -r /srv/restic-repo prune enter password for repository: counting files in repo diff --git a/doc/070_encryption.rst b/doc/070_encryption.rst index c0889b852..a7b8716ac 100644 --- a/doc/070_encryption.rst +++ b/doc/070_encryption.rst @@ -16,8 +16,8 @@ Encryption *"The design might not be perfect, but it’s good. Encryption is a first-class feature, -the implementation looks sane and I guess the deduplication trade-off is worth it. So… I’m going to use restic for -my personal backups.*" `Filippo Valsorda`_ +the implementation looks sane and I guess the deduplication trade-off is worth +it. So… I’m going to use restic for my personal backups.*" `Filippo Valsorda`_ .. _Filippo Valsorda: https://blog.filippo.io/restic-cryptography/ @@ -31,19 +31,19 @@ per repository. In fact, you can use the ``list``, ``add``, ``remove``, and .. code-block:: console - $ restic -r /tmp/backup key list + $ restic -r /srv/restic-repo key list enter password for repository: ID User Host Created ---------------------------------------------------------------------- *eb78040b username kasimir 2015-08-12 13:29:57 - $ restic -r /tmp/backup key add + $ restic -r /srv/restic-repo key add enter password for repository: enter password for new key: enter password again: saved new key as - $ restic -r backup key list + $ restic -r /srv/restic-repo key list enter password for repository: ID User Host Created ---------------------------------------------------------------------- diff --git a/doc/075_scripting.rst b/doc/075_scripting.rst index c0a73d9b5..712a70244 100644 --- a/doc/075_scripting.rst +++ b/doc/075_scripting.rst @@ -26,10 +26,10 @@ times. The command ``snapshots`` may be used for this purpose: .. code-block:: console - $ restic -r /tmp/backup snapshots - Fatal: unable to open config file: Stat: stat /tmp/backup/config: no such file or directory + $ restic -r /srv/restic-repo snapshots + Fatal: unable to open config file: Stat: stat /srv/restic-repo/config: no such file or directory Is there a repository at the following location? - /tmp/backup + /srv/restic-repo If a repository does not exist, restic will return a non-zero exit code and print an error message. Note that restic will also return a non-zero diff --git a/doc/100_references.rst b/doc/100_references.rst index be22defa0..23ae2956e 100644 --- a/doc/100_references.rst +++ b/doc/100_references.rst @@ -625,14 +625,15 @@ are deleted, the particular snapshot vanished and all snapshots depending on data that has been added in the snapshot cannot be restored completely. Restic is not designed to detect this attack. +****** Local Cache -=========== +****** In order to speed up certain operations, restic manages a local cache of data. This document describes the data structures for the local cache with version 1. Versions --------- +======== The cache directory is selected according to the `XDG base dir specification `__. @@ -646,12 +647,21 @@ a lower version number is found the cache is recreated with the current version. If a higher version number is found the cache is ignored and left as is. -Snapshots and Indexes ---------------------- +Snapshots, Data and Indexes +=========================== Snapshot, Data and Index files are cached in the sub-directories ``snapshots``, ``data`` and ``index``, as read from the repository. +Expiry +====== + +Whenever a cache directory for a repo is used, that directory's modification +timestamp is updated to the current time. By looking at the modification +timestamps of the repo cache directories it is easy to decide which directories +are old and haven't been used in a long time. Those are probably stale and can +be removed. + ************ REST Backend @@ -798,24 +808,3 @@ Returns "200 OK" if the blob with the given name and type has been deleted from the repository, an HTTP error otherwise. -***** -Talks -***** - -The following talks will be or have been given about restic: - -- 2016-01-31: Lightning Talk at the Go Devroom at FOSDEM 2016, - Brussels, Belgium -- 2016-01-29: `restic - Backups mal - richtig `__: - Public lecture in German at `CCC Cologne - e.V. `__ in Cologne, Germany -- 2015-08-23: `A Solution to the Backup - Inconvenience `__: - Lecture at `FROSCON 2015 `__ in Bonn, Germany -- 2015-02-01: `Lightning Talk at FOSDEM - 2015 `__: A - short introduction (with slightly outdated command line) -- 2015-01-27: `Talk about restic at CCC - Aachen `__ - (in German) diff --git a/doc/110_talks.rst b/doc/110_talks.rst new file mode 100644 index 000000000..06952896f --- /dev/null +++ b/doc/110_talks.rst @@ -0,0 +1,34 @@ +.. + Normally, there are no heading levels assigned to certain characters as the structure is + determined from the succession of headings. However, this convention is used in Python’s + Style Guide for documenting which you may follow: + + # with overline, for parts + * for chapters + = for sections + - for subsections + ^ for subsubsections + " for paragraphs + + +##### +Talks +##### + +The following talks will be or have been given about restic: + +- 2016-01-31: Lightning Talk at the Go Devroom at FOSDEM 2016, + Brussels, Belgium +- 2016-01-29: `restic - Backups mal + richtig `__: + Public lecture in German at `CCC Cologne + e.V. `__ in Cologne, Germany +- 2015-08-23: `A Solution to the Backup + Inconvenience `__: + Lecture at `FROSCON 2015 `__ in Bonn, Germany +- 2015-02-01: `Lightning Talk at FOSDEM + 2015 `__: A + short introduction (with slightly outdated command line) +- 2015-01-27: `Talk about restic at CCC + Aachen `__ + (in German) diff --git a/doc/cache.rst b/doc/cache.rst deleted file mode 100644 index a39a1e76c..000000000 --- a/doc/cache.rst +++ /dev/null @@ -1,36 +0,0 @@ -Local Cache -=========== - -In order to speed up certain operations, restic manages a local cache of data. -This document describes the data structures for the local cache with version 1. - -Versions --------- - -The cache directory is selected according to the `XDG base dir specification -`__. -Each repository has its own cache sub-directory, consting of the repository ID -which is chosen at ``init``. All cache directories for different repos are -independent of each other. - -The cache dir for a repo contains a file named ``version``, which contains a -single ASCII integer line that stands for the current version of the cache. If -a lower version number is found the cache is recreated with the current -version. If a higher version number is found the cache is ignored and left as -is. - -Snapshots, Data and Indexes ---------------------------- - -Snapshot, Data and Index files are cached in the sub-directories ``snapshots``, -``data`` and ``index``, as read from the repository. - -Expiry ------- - -Whenever a cache directory for a repo is used, that directory's modification -timestamp is updated to the current time. By looking at the modification -timestamps of the repo cache directories it is easy to decide which directories -are old and haven't been used in a long time. Those are probably stale and can -be removed. - diff --git a/doc/conf.py b/doc/conf.py index 3f7c66158..3c0af927b 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -35,7 +35,7 @@ master_doc = 'index' # General information about the project. project = 'restic' -copyright = '2017, restic authors' +copyright = '2018, restic authors' author = 'fd0' # The version info for the project you're documenting, acts as replacement for diff --git a/doc/index.rst b/doc/index.rst index a5f82e284..68f86c398 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -16,5 +16,6 @@ Restic Documentation 080_examples 090_participating 100_references + 110_talks faq manual_rest diff --git a/doc/manual_rest.rst b/doc/manual_rest.rst index a53e34869..540a6d60a 100644 --- a/doc/manual_rest.rst +++ b/doc/manual_rest.rst @@ -19,6 +19,7 @@ Usage help is available: backup Create a new backup of files and/or directories cat Print internal objects to stdout check Check the repository for errors + diff Show differences between two snapshots dump Print a backed-up file to stdout find Find a file or directory forget Remove snapshots from the repository @@ -39,24 +40,24 @@ Usage help is available: version Print version information Flags: - --cacert stringSlice path to load root certificates from (default: use system certificates) - --cache-dir string set the cache directory - -h, --help help for restic - --json set output mode to JSON for commands that support it - --limit-download int limits downloads to a maximum rate in KiB/s. (default: unlimited) - --limit-upload int limits uploads to a maximum rate in KiB/s. (default: unlimited) - --no-cache do not use a local cache - --no-lock do not lock the repo, this allows some operations on read-only repos - -o, --option key=value set extended option (key=value, can be specified multiple times) - -p, --password-file string read the repository password from a file (default: $RESTIC_PASSWORD_FILE) - -q, --quiet do not output comprehensive progress report - -r, --repo string repository to backup to or restore from (default: $RESTIC_REPOSITORY) + --cacert stringSlice path to load root certificates from (default: use system certificates) + --cache-dir string set the cache directory + --cleanup-cache auto remove old cache directories + -h, --help help for restic + --json set output mode to JSON for commands that support it + --limit-download int limits downloads to a maximum rate in KiB/s. (default: unlimited) + --limit-upload int limits uploads to a maximum rate in KiB/s. (default: unlimited) + --no-cache do not use a local cache + --no-lock do not lock the repo, this allows some operations on read-only repos + -o, --option key=value set extended option (key=value, can be specified multiple times) + -p, --password-file string read the repository password from a file (default: $RESTIC_PASSWORD_FILE) + -q, --quiet do not output comprehensive progress report + -r, --repo string repository to backup to or restore from (default: $RESTIC_REPOSITORY) --tls-client-cert string path to a file containing PEM encoded TLS client certificate and private key - + -v, --verbose count[=-1] be verbose (can be specified multiple times) Use "restic [command] --help" for more information about a command. - Similar to programs such as ``git``, restic has a number of sub-commands. You can see these commands in the listing above. Each sub-command may have own command-line options, and there is a help @@ -87,21 +88,23 @@ command: --stdin-filename string file name to use when reading from stdin (default "stdin") --tag tag add a tag for the new snapshot (can be specified multiple times) --time string time of the backup (ex. '2012-11-01 22:08:41') (default: now) + --with-atime store the atime for all files and directories Global Flags: - --cacert stringSlice path to load root certificates from (default: use system certificates) - --cache-dir string set the cache directory - --json set output mode to JSON for commands that support it - --limit-download int limits downloads to a maximum rate in KiB/s. (default: unlimited) - --limit-upload int limits uploads to a maximum rate in KiB/s. (default: unlimited) - --no-cache do not use a local cache - --no-lock do not lock the repo, this allows some operations on read-only repos - -o, --option key=value set extended option (key=value, can be specified multiple times) - -p, --password-file string read the repository password from a file (default: $RESTIC_PASSWORD_FILE) - -q, --quiet do not output comprehensive progress report - -r, --repo string repository to backup to or restore from (default: $RESTIC_REPOSITORY) - --tls-client-cert string path to a TLS client certificate - --tls-client-key string path to a TLS client certificate key + --cacert stringSlice path to load root certificates from (default: use system certificates) + --cache-dir string set the cache directory + --cleanup-cache auto remove old cache directories + --json set output mode to JSON for commands that support it + --limit-download int limits downloads to a maximum rate in KiB/s. (default: unlimited) + --limit-upload int limits uploads to a maximum rate in KiB/s. (default: unlimited) + --no-cache do not use a local cache + --no-lock do not lock the repo, this allows some operations on read-only repos + -o, --option key=value set extended option (key=value, can be specified multiple times) + -p, --password-file string read the repository password from a file (default: $RESTIC_PASSWORD_FILE) + -q, --quiet do not output comprehensive progress report + -r, --repo string repository to backup to or restore from (default: $RESTIC_REPOSITORY) + --tls-client-cert string path to a file containing PEM encoded TLS client certificate and private key + -v, --verbose n[=-1] be verbose (specify --verbose multiple times or level n) Subcommand that support showing progress information such as ``backup``, ``check`` and ``prune`` will do so unless the quiet flag ``-q`` or @@ -128,7 +131,7 @@ command does that: .. code-block:: console - $ restic -r /tmp/backup tag --set NL --set CH 590c8fc8 + $ restic -r /srv/restic-repo tag --set NL --set CH 590c8fc8 create exclusive lock for repository modified tags on 1 snapshots @@ -141,19 +144,19 @@ So we can add and remove tags incrementally like this: .. code-block:: console - $ restic -r /tmp/backup tag --tag NL --remove CH + $ restic -r /srv/restic-repo tag --tag NL --remove CH create exclusive lock for repository modified tags on 1 snapshots - $ restic -r /tmp/backup tag --tag NL --add UK + $ restic -r /srv/restic-repo tag --tag NL --add UK create exclusive lock for repository modified tags on 1 snapshots - $ restic -r /tmp/backup tag --tag NL --remove NL + $ restic -r /srv/restic-repo tag --tag NL --remove NL create exclusive lock for repository modified tags on 1 snapshots - $ restic -r /tmp/backup tag --tag NL --add SOMETHING + $ restic -r /srv/restic-repo tag --tag NL --add SOMETHING no snapshots were modified Under the hood @@ -170,7 +173,7 @@ locks with the following command: .. code-block:: console - $ restic -r /tmp/backup list snapshots + $ restic -r /srv/restic-repo list snapshots d369ccc7d126594950bf74f0a348d5d98d9e99f3215082eb69bf02dc9b3e464c The ``find`` command searches for a given @@ -191,7 +194,7 @@ objects or their raw content. .. code-block:: console - $ restic -r /tmp/backup cat snapshot d369ccc7d126594950bf74f0a348d5d98d9e99f3215082eb69bf02dc9b3e464c + $ restic -r /srv/restic-repo cat snapshot d369ccc7d126594950bf74f0a348d5d98d9e99f3215082eb69bf02dc9b3e464c enter password for repository: { "time": "2015-08-12T12:52:44.091448856+02:00", @@ -242,7 +245,7 @@ lists all snapshots as JSON and uses ``jq`` to pretty-print the result: .. code-block:: console - $ restic -r /tmp/backup snapshots --json | jq . + $ restic -r /srv/restic-repo snapshots --json | jq . [ { "time": "2017-03-11T09:57:43.26630619+01:00", @@ -283,7 +286,7 @@ instead of the default, set the environment variable like this: .. code-block:: console $ export TMPDIR=/var/tmp/restic-tmp - $ restic -r /tmp/backup backup ~/work + $ restic -r /srv/restic-repo backup ~/work