package archiver import ( "context" "os" "path" "path/filepath" "runtime" "sort" "strings" "testing" "time" "github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/fs" "github.com/restic/restic/internal/restic" ) // 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, fs.Local{}, Options{}) opts := SnapshotOptions{ Time: time.Now(), Hostname: "localhost", Tags: []string{"test"}, } if parent != nil { sn, err := restic.LoadSnapshot(context.TODO(), repo, *parent) if err != nil { t.Fatal(err) } opts.ParentSnapshot = sn } sn, _, _, err := arch.Snapshot(context.TODO(), []string{path}, opts) if err != nil { t.Fatal(err) } return sn } // TestDir describes a directory structure to create for a test. type TestDir map[string]interface{} func (d TestDir) String() string { return "<Dir>" } // TestFile describes a file created for a test. type TestFile struct { Content string } func (f TestFile) String() string { return "<File>" } // TestSymlink describes a symlink created for a test. type TestSymlink struct { Target string } func (s TestSymlink) String() string { return "<Symlink>" } // TestHardlink describes a hardlink created for a test. type TestHardlink struct { Target string } func (s TestHardlink) String() string { return "<Hardlink>" } // 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) { t.Helper() // ensure a stable order such that it can be guaranteed that a hardlink target already exists var names []string for name := range dir { names = append(names, name) } sort.Strings(names) for _, name := range names { item := dir[name] targetPath := filepath.Join(target, name) switch it := item.(type) { case TestFile: err := os.WriteFile(targetPath, []byte(it.Content), 0644) if err != nil { t.Fatal(err) } case TestSymlink: err := fs.Symlink(filepath.FromSlash(it.Target), targetPath) if err != nil { t.Fatal(err) } case TestHardlink: err := fs.Link(filepath.Join(target, 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) { 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) { t.Helper() pathsChecked := make(map[string]struct{}) // first, test that all items are there TestWalkFiles(t, target, dir, func(path string, item interface{}) error { 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 !fi.Mode().IsRegular() { t.Errorf("is not a regular file: %v", path) return nil } content, err := os.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 := filepath.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.BlobLoader, 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, len(file.Content)) pos := 0 for _, id := range node.Content { part, err := repo.LoadBlob(ctx, restic.DataBlob, id, content[pos:]) if err != nil { t.Fatalf("error loading blob %v: %v", id.Str(), err) return } copy(content[pos:pos+len(part)], part) pos += len(part) } 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.BlobLoader, treeID restic.ID, dir TestDir) { t.Helper() tree, err := restic.LoadTree(ctx, repo, 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 != restic.NodeTypeDir { 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 != restic.NodeTypeFile { t.Errorf("tree node %v has wrong type %q, want %q", nodePrefix, node.Type, "file") } TestEnsureFileContent(ctx, t, repo, nodePrefix, node, e) case TestSymlink: if node.Type != restic.NodeTypeSymlink { t.Errorf("tree node %v has wrong type %q, want %q", nodePrefix, node.Type, "symlink") } 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 { _, 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) { 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) }