mirror of
https://github.com/kopia/kopia.git
synced 2026-05-14 01:37:07 -04:00
feat(cli): compute snapshot diff stats (#4444)
Added functionality to calculate aggregate statistics when comparing what's changed between snapshots using kopia diff Statistics collected during snapshot diff computation includes: - files added/removed/modified - dirs added/removed/modified - files/dirs with metadata changes but same underlying content (OID) Testing approach: Added a test for verifying stats collected when comparing two directories with the same objectID but metadata changes across snapshots (dir mode, dir mod time, dir owner, etc), expectation is all the appropriate dir stats fields are updated. Added another test for verifying stats collected when comparing two directories with similar file contents but the metadata for the files have changed between snapshots but content remains unchanged. Expectation is all the relevant file level stats fields are updated. Existing tests have been updated due to stats now being printed in addition to previous output.
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
@@ -63,7 +65,19 @@ func (c *commandDiff) run(ctx context.Context, rep repo.Repository) error {
|
||||
}
|
||||
|
||||
if isDir1 {
|
||||
return errors.Wrap(d.Compare(ctx, ent1, ent2), "error comparing directories")
|
||||
snapshotDiffStats, err := d.Compare(ctx, ent1, ent2)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error comparing directories")
|
||||
}
|
||||
|
||||
b, err := json.Marshal(snapshotDiffStats)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error marshaling computed snapshot diff stats")
|
||||
}
|
||||
|
||||
fmt.Fprintf(c.out.stdout(), "%s", b) //nolint:errcheck
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return errors.New("comparing files not implemented yet")
|
||||
|
||||
@@ -21,18 +21,47 @@
|
||||
|
||||
var log = logging.Module("diff")
|
||||
|
||||
// EntryTypeStats accumulates specific stats for the snapshots being compared.
|
||||
type EntryTypeStats struct {
|
||||
Added uint32 `json:"added"`
|
||||
Removed uint32 `json:"removed"`
|
||||
Modified uint32 `json:"modified"`
|
||||
|
||||
// aggregate stats
|
||||
SameContentButDifferentMetadata uint32 `json:"sameContentButDifferentMetadata"`
|
||||
|
||||
// stats categorized based on metadata
|
||||
SameContentButDifferentMode uint32 `json:"sameContentButDifferentMode"`
|
||||
SameContentButDifferentModificationTime uint32 `json:"sameContentButDifferentModificationTime"`
|
||||
SameContentButDifferentUserOwner uint32 `json:"sameContentButDifferentUserOwner"`
|
||||
SameContentButDifferentGroupOwner uint32 `json:"sameContentButDifferentGroupOwner"`
|
||||
}
|
||||
|
||||
// Stats accumulates stats between snapshots being compared.
|
||||
type Stats struct {
|
||||
FileEntries EntryTypeStats `json:"fileEntries"`
|
||||
DirectoryEntries EntryTypeStats `json:"directoryEntries"`
|
||||
}
|
||||
|
||||
// Comparer outputs diff information between two filesystems.
|
||||
type Comparer struct {
|
||||
out io.Writer
|
||||
tmpDir string
|
||||
|
||||
stats Stats
|
||||
out io.Writer
|
||||
tmpDir string
|
||||
DiffCommand string
|
||||
DiffArguments []string
|
||||
}
|
||||
|
||||
// Compare compares two filesystem entries and emits their diff information.
|
||||
func (c *Comparer) Compare(ctx context.Context, e1, e2 fs.Entry) error {
|
||||
return c.compareEntry(ctx, e1, e2, ".")
|
||||
func (c *Comparer) Compare(ctx context.Context, e1, e2 fs.Entry) (Stats, error) {
|
||||
c.stats = Stats{}
|
||||
|
||||
err := c.compareEntry(ctx, e1, e2, ".")
|
||||
if err != nil {
|
||||
return c.stats, err
|
||||
}
|
||||
|
||||
return c.stats, errors.Wrap(err, "error comparing fs entries")
|
||||
}
|
||||
|
||||
// Close removes all temporary files used by the comparer.
|
||||
@@ -76,23 +105,34 @@ func (c *Comparer) compareDirectories(ctx context.Context, dir1, dir2 fs.Directo
|
||||
//nolint:gocyclo
|
||||
func (c *Comparer) compareEntry(ctx context.Context, e1, e2 fs.Entry, path string) error {
|
||||
// see if we have the same object IDs, which implies identical objects, thanks to content-addressable-storage
|
||||
if h1, ok := e1.(object.HasObjectID); ok {
|
||||
if h2, ok := e2.(object.HasObjectID); ok {
|
||||
if h1.ObjectID() == h2.ObjectID() {
|
||||
log(ctx).Debugf("unchanged %v", path)
|
||||
return nil
|
||||
h1, e1HasObjectID := e1.(object.HasObjectID)
|
||||
h2, e2HasObjectID := e2.(object.HasObjectID)
|
||||
|
||||
if e1HasObjectID && e2HasObjectID {
|
||||
if h1.ObjectID() == h2.ObjectID() {
|
||||
if _, isDir := e1.(fs.Directory); isDir {
|
||||
c.compareDirMetadataAndComputeStats(ctx, e1, e2, path)
|
||||
} else {
|
||||
c.compareFileMetadataAndComputeStats(ctx, e1, e2, path)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if e1 == nil {
|
||||
if dir2, isDir2 := e2.(fs.Directory); isDir2 {
|
||||
c.output("added directory %v\n", path)
|
||||
|
||||
c.stats.DirectoryEntries.Added++
|
||||
|
||||
return c.compareDirectories(ctx, nil, dir2, path)
|
||||
}
|
||||
|
||||
c.output("added file %v (%v bytes)\n", path, e2.Size())
|
||||
|
||||
c.stats.FileEntries.Added++
|
||||
|
||||
if f, ok := e2.(fs.File); ok {
|
||||
if err := c.compareFiles(ctx, nil, f, path); err != nil {
|
||||
return err
|
||||
@@ -105,11 +145,16 @@ func (c *Comparer) compareEntry(ctx context.Context, e1, e2 fs.Entry, path strin
|
||||
if e2 == nil {
|
||||
if dir1, isDir1 := e1.(fs.Directory); isDir1 {
|
||||
c.output("removed directory %v\n", path)
|
||||
|
||||
c.stats.DirectoryEntries.Removed++
|
||||
|
||||
return c.compareDirectories(ctx, dir1, nil, path)
|
||||
}
|
||||
|
||||
c.output("removed file %v (%v bytes)\n", path, e1.Size())
|
||||
|
||||
c.stats.FileEntries.Removed++
|
||||
|
||||
if f, ok := e1.(fs.File); ok {
|
||||
if err := c.compareFiles(ctx, f, nil, path); err != nil {
|
||||
return err
|
||||
@@ -119,7 +164,7 @@ func (c *Comparer) compareEntry(ctx context.Context, e1, e2 fs.Entry, path strin
|
||||
return nil
|
||||
}
|
||||
|
||||
compareEntry(e1, e2, path, c.out)
|
||||
c.compareEntryMetadata(e1, e2, path)
|
||||
|
||||
dir1, isDir1 := e1.(fs.Directory)
|
||||
dir2, isDir2 := e2.(fs.Directory)
|
||||
@@ -137,6 +182,8 @@ func (c *Comparer) compareEntry(ctx context.Context, e1, e2 fs.Entry, path strin
|
||||
if isDir2 {
|
||||
// left is non-directory, right is a directory
|
||||
log(ctx).Infof("changed %v from non-directory to a directory", path)
|
||||
c.output("changed %v from non-directory to a directory\n", path)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -144,6 +191,8 @@ func (c *Comparer) compareEntry(ctx context.Context, e1, e2 fs.Entry, path strin
|
||||
if f2, ok := e2.(fs.File); ok {
|
||||
c.output("changed %v at %v (size %v -> %v)\n", path, e2.ModTime().String(), e1.Size(), e2.Size())
|
||||
|
||||
c.stats.FileEntries.Modified++
|
||||
|
||||
if err := c.compareFiles(ctx, f1, f2, path); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -153,18 +202,81 @@ func (c *Comparer) compareEntry(ctx context.Context, e1, e2 fs.Entry, path strin
|
||||
return nil
|
||||
}
|
||||
|
||||
func compareEntry(e1, e2 fs.Entry, fullpath string, out io.Writer) bool {
|
||||
func (c *Comparer) compareDirMetadataAndComputeStats(ctx context.Context, e1, e2 fs.Entry, path string) {
|
||||
// check for metadata changes pertaining to directories given that content hasn't changed and gather aggregate statistics
|
||||
equal := true
|
||||
|
||||
if m1, m2 := e1.Mode(), e2.Mode(); m1 != m2 {
|
||||
equal = false
|
||||
c.stats.DirectoryEntries.SameContentButDifferentMode++
|
||||
}
|
||||
|
||||
if mt1, mt2 := e1.ModTime(), e2.ModTime(); !mt1.Equal(mt2) {
|
||||
equal = false
|
||||
c.stats.DirectoryEntries.SameContentButDifferentModificationTime++
|
||||
}
|
||||
|
||||
o1, o2 := e1.Owner(), e2.Owner()
|
||||
if o1.UserID != o2.UserID {
|
||||
equal = false
|
||||
c.stats.DirectoryEntries.SameContentButDifferentUserOwner++
|
||||
}
|
||||
|
||||
if o1.GroupID != o2.GroupID {
|
||||
equal = false
|
||||
c.stats.DirectoryEntries.SameContentButDifferentGroupOwner++
|
||||
}
|
||||
|
||||
if !equal {
|
||||
c.stats.DirectoryEntries.SameContentButDifferentMetadata++
|
||||
|
||||
log(ctx).Debugf("content unchanged but metadata has been modified: %v", path)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Comparer) compareFileMetadataAndComputeStats(ctx context.Context, e1, e2 fs.Entry, path string) {
|
||||
// check for metadata changes pertaining to files given that content hasn't changed and gather aggregate statistics
|
||||
equal := true
|
||||
if m1, m2 := e1.Mode(), e2.Mode(); m1 != m2 {
|
||||
equal = false
|
||||
c.stats.FileEntries.SameContentButDifferentMode++
|
||||
}
|
||||
|
||||
if mt1, mt2 := e1.ModTime(), e2.ModTime(); !mt1.Equal(mt2) {
|
||||
equal = false
|
||||
c.stats.FileEntries.SameContentButDifferentModificationTime++
|
||||
}
|
||||
|
||||
o1, o2 := e1.Owner(), e2.Owner()
|
||||
if o1.UserID != o2.UserID {
|
||||
equal = false
|
||||
c.stats.FileEntries.SameContentButDifferentUserOwner++
|
||||
}
|
||||
|
||||
if o1.GroupID != o2.GroupID {
|
||||
equal = false
|
||||
c.stats.FileEntries.SameContentButDifferentGroupOwner++
|
||||
}
|
||||
|
||||
if !equal {
|
||||
c.stats.FileEntries.SameContentButDifferentMetadata++
|
||||
|
||||
log(ctx).Debugf("content unchanged but metadata has been modified: %v", path)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Comparer) compareEntryMetadata(e1, e2 fs.Entry, fullpath string) bool {
|
||||
if e1 == e2 { // in particular e1 == nil && e2 == nil
|
||||
return true
|
||||
}
|
||||
|
||||
if e1 == nil {
|
||||
fmt.Fprintln(out, fullpath, "does not exist in source directory") //nolint:errcheck
|
||||
c.output("%v does not exist in source directory\n", fullpath)
|
||||
return false
|
||||
}
|
||||
|
||||
if e2 == nil {
|
||||
fmt.Fprintln(out, fullpath, "does not exist in destination directory") //nolint:errcheck
|
||||
c.output("%v does not exist in destination directory\n", fullpath)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -173,32 +285,43 @@ func compareEntry(e1, e2 fs.Entry, fullpath string, out io.Writer) bool {
|
||||
if m1, m2 := e1.Mode(), e2.Mode(); m1 != m2 {
|
||||
equal = false
|
||||
|
||||
fmt.Fprintln(out, fullpath, "modes differ: ", m1, m2) //nolint:errcheck
|
||||
c.output("%v modes differ: %v %v\n", fullpath, m1, m2)
|
||||
}
|
||||
|
||||
if s1, s2 := e1.Size(), e2.Size(); s1 != s2 {
|
||||
equal = false
|
||||
|
||||
fmt.Fprintln(out, fullpath, "sizes differ: ", s1, s2) //nolint:errcheck
|
||||
c.output("%v sizes differ: %v %v\n", fullpath, s1, s2)
|
||||
}
|
||||
|
||||
if mt1, mt2 := e1.ModTime(), e2.ModTime(); !mt1.Equal(mt2) {
|
||||
equal = false
|
||||
|
||||
fmt.Fprintln(out, fullpath, "modification times differ: ", mt1, mt2) //nolint:errcheck
|
||||
c.output("%v modification times differ: %v %v\n", fullpath, mt1, mt2)
|
||||
}
|
||||
|
||||
o1, o2 := e1.Owner(), e2.Owner()
|
||||
if o1.UserID != o2.UserID {
|
||||
equal = false
|
||||
|
||||
fmt.Fprintln(out, fullpath, "owner users differ: ", o1.UserID, o2.UserID) //nolint:errcheck
|
||||
c.output("%v owner users differ: %v %v\n", fullpath, o1.UserID, o2.UserID)
|
||||
}
|
||||
|
||||
if o1.GroupID != o2.GroupID {
|
||||
equal = false
|
||||
|
||||
fmt.Fprintln(out, fullpath, "owner groups differ: ", o1.GroupID, o2.GroupID) //nolint:errcheck
|
||||
c.output("%v owner groups differ: %v %v\n", fullpath, o1.GroupID, o2.GroupID)
|
||||
}
|
||||
|
||||
_, isDir1 := e1.(fs.Directory)
|
||||
_, isDir2 := e2.(fs.Directory)
|
||||
|
||||
if !equal {
|
||||
if isDir1 && isDir2 {
|
||||
c.stats.DirectoryEntries.Modified++
|
||||
} else {
|
||||
c.stats.FileEntries.Modified++
|
||||
}
|
||||
}
|
||||
|
||||
// don't compare filesystem boundaries (e1.Device()), it's pretty useless and is not stored in backups
|
||||
@@ -297,6 +420,12 @@ func downloadFile(ctx context.Context, f fs.File, fname string) error {
|
||||
return errors.Wrap(iocopy.JustCopy(dst, src), "error downloading file")
|
||||
}
|
||||
|
||||
// Stats returns aggregated statistics computed during snapshot comparison
|
||||
// must be invoked after a call to Compare which populates ComparerStats struct.
|
||||
func (c *Comparer) Stats() Stats {
|
||||
return c.stats
|
||||
}
|
||||
|
||||
func (c *Comparer) output(msg string, args ...interface{}) {
|
||||
fmt.Fprintf(c.out, msg, args...) //nolint:errcheck
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
|
||||
"github.com/kopia/kopia/fs"
|
||||
"github.com/kopia/kopia/internal/diff"
|
||||
"github.com/kopia/kopia/repo/content/index"
|
||||
"github.com/kopia/kopia/repo/object"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -24,6 +26,8 @@ type testBaseEntry struct {
|
||||
modtime time.Time
|
||||
mode os.FileMode
|
||||
name string
|
||||
owner fs.OwnerInfo
|
||||
oid object.ID
|
||||
}
|
||||
|
||||
func (f *testBaseEntry) IsDir() bool { return false }
|
||||
@@ -32,8 +36,9 @@ func (f *testBaseEntry) Close() {}
|
||||
func (f *testBaseEntry) Name() string { return f.name }
|
||||
func (f *testBaseEntry) ModTime() time.Time { return f.modtime }
|
||||
func (f *testBaseEntry) Sys() interface{} { return nil }
|
||||
func (f *testBaseEntry) Owner() fs.OwnerInfo { return fs.OwnerInfo{UserID: 1000, GroupID: 1000} }
|
||||
func (f *testBaseEntry) Owner() fs.OwnerInfo { return f.owner }
|
||||
func (f *testBaseEntry) Device() fs.DeviceInfo { return fs.DeviceInfo{Dev: 1} }
|
||||
func (f *testBaseEntry) ObjectID() object.ID { return f.oid }
|
||||
|
||||
func (f *testBaseEntry) Mode() os.FileMode {
|
||||
if f.mode == 0 {
|
||||
@@ -92,9 +97,18 @@ func TestCompareEmptyDirectories(t *testing.T) {
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
dmodtime := time.Date(2023, time.April, 12, 10, 30, 0, 0, time.UTC)
|
||||
dir1 := createTestDirectory("testDir1", dmodtime)
|
||||
dir2 := createTestDirectory("testDir2", dmodtime)
|
||||
dirModTime := time.Date(2023, time.April, 12, 10, 30, 0, 0, time.UTC)
|
||||
dirOwnerInfo := fs.OwnerInfo{UserID: 1000, GroupID: 1000}
|
||||
dirMode := os.FileMode(0o777)
|
||||
|
||||
cid, _ := index.IDFromHash("p", []byte("sdkjfn"))
|
||||
dirObjectID1 := object.DirectObjectID(cid)
|
||||
|
||||
cid, _ = index.IDFromHash("i", []byte("dfjlgn"))
|
||||
dirObjectID2 := object.DirectObjectID(cid)
|
||||
|
||||
dir1 := createTestDirectory("testDir1", dirModTime, dirOwnerInfo, dirMode, dirObjectID1)
|
||||
dir2 := createTestDirectory("testDir2", dirModTime, dirOwnerInfo, dirMode, dirObjectID2)
|
||||
|
||||
c, err := diff.NewComparer(&buf)
|
||||
require.NoError(t, err)
|
||||
@@ -103,9 +117,12 @@ func TestCompareEmptyDirectories(t *testing.T) {
|
||||
_ = c.Close()
|
||||
})
|
||||
|
||||
err = c.Compare(ctx, dir1, dir2)
|
||||
expectedStats := diff.Stats{}
|
||||
actualStats, err := c.Compare(ctx, dir1, dir2)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, buf.String())
|
||||
require.Equal(t, expectedStats, actualStats)
|
||||
}
|
||||
|
||||
func TestCompareIdenticalDirectories(t *testing.T) {
|
||||
@@ -113,21 +130,41 @@ func TestCompareIdenticalDirectories(t *testing.T) {
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
dmodtime := time.Date(2023, time.April, 12, 10, 30, 0, 0, time.UTC)
|
||||
fmodtime := time.Date(2023, time.April, 12, 10, 30, 0, 0, time.UTC)
|
||||
dirModTime := time.Date(2023, time.April, 12, 10, 30, 0, 0, time.UTC)
|
||||
dirOwnerInfo := fs.OwnerInfo{UserID: 1000, GroupID: 1000}
|
||||
dirMode := os.FileMode(0o777)
|
||||
fileModTime := time.Date(2023, time.April, 12, 10, 30, 0, 0, time.UTC)
|
||||
|
||||
cid, _ := index.IDFromHash("p", []byte("sdkjfn"))
|
||||
dirObjectID1 := object.DirectObjectID(cid)
|
||||
|
||||
cid, _ = index.IDFromHash("i", []byte("dfjlgn"))
|
||||
dirObjectID2 := object.DirectObjectID(cid)
|
||||
|
||||
file1 := &testFile{testBaseEntry: testBaseEntry{modtime: fileModTime, name: "file1.txt"}, content: "abcdefghij"}
|
||||
file2 := &testFile{testBaseEntry: testBaseEntry{modtime: fileModTime, name: "file2.txt"}, content: "klmnopqrstuvwxyz"}
|
||||
|
||||
dir1 := createTestDirectory(
|
||||
"testDir1",
|
||||
dmodtime,
|
||||
&testFile{testBaseEntry: testBaseEntry{modtime: fmodtime, name: "file1.txt"}, content: "abcdefghij"},
|
||||
&testFile{testBaseEntry: testBaseEntry{modtime: fmodtime, name: "file2.txt"}, content: "klmnopqrstuvwxyz"},
|
||||
dirModTime,
|
||||
dirOwnerInfo,
|
||||
dirMode,
|
||||
dirObjectID1,
|
||||
file1,
|
||||
file2,
|
||||
)
|
||||
dir2 := createTestDirectory(
|
||||
"testDir2",
|
||||
dmodtime,
|
||||
&testFile{testBaseEntry: testBaseEntry{modtime: fmodtime, name: "file1.txt"}, content: "abcdefghij"},
|
||||
&testFile{testBaseEntry: testBaseEntry{modtime: fmodtime, name: "file2.txt"}, content: "klmnopqrstuvwxyz"},
|
||||
dirModTime,
|
||||
dirOwnerInfo,
|
||||
dirMode,
|
||||
dirObjectID2,
|
||||
file1,
|
||||
file2,
|
||||
)
|
||||
|
||||
expectedStats := diff.Stats{}
|
||||
|
||||
c, err := diff.NewComparer(&buf)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -135,9 +172,11 @@ func TestCompareIdenticalDirectories(t *testing.T) {
|
||||
_ = c.Close()
|
||||
})
|
||||
|
||||
err = c.Compare(ctx, dir1, dir2)
|
||||
actualStats, err := c.Compare(ctx, dir1, dir2)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, buf.String())
|
||||
require.Equal(t, expectedStats, actualStats)
|
||||
}
|
||||
|
||||
func TestCompareDifferentDirectories(t *testing.T) {
|
||||
@@ -145,19 +184,34 @@ func TestCompareDifferentDirectories(t *testing.T) {
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
dmodtime := time.Date(2023, time.April, 12, 10, 30, 0, 0, time.UTC)
|
||||
fmodtime := time.Date(2023, time.April, 12, 10, 30, 0, 0, time.UTC)
|
||||
dirModTime := time.Date(2023, time.April, 12, 10, 30, 0, 0, time.UTC)
|
||||
fileModTime := time.Date(2023, time.April, 12, 10, 30, 0, 0, time.UTC)
|
||||
dirOwnerInfo := fs.OwnerInfo{UserID: 1000, GroupID: 1000}
|
||||
dirMode := os.FileMode(0o777)
|
||||
|
||||
cid, _ := index.IDFromHash("p", []byte("sdkjfn"))
|
||||
dirObjectID1 := object.DirectObjectID(cid)
|
||||
|
||||
cid, _ = index.IDFromHash("i", []byte("dfjlgn"))
|
||||
dirObjectID2 := object.DirectObjectID(cid)
|
||||
|
||||
dir1 := createTestDirectory(
|
||||
"testDir1",
|
||||
dmodtime,
|
||||
&testFile{testBaseEntry: testBaseEntry{modtime: fmodtime, name: "file1.txt"}, content: "abcdefghij"},
|
||||
&testFile{testBaseEntry: testBaseEntry{modtime: fmodtime, name: "file2.txt"}, content: "klmnopqrstuvwxyz"},
|
||||
dirModTime,
|
||||
dirOwnerInfo,
|
||||
dirMode,
|
||||
dirObjectID1,
|
||||
&testFile{testBaseEntry: testBaseEntry{modtime: fileModTime, name: "file1.txt"}, content: "abcdefghij"},
|
||||
&testFile{testBaseEntry: testBaseEntry{modtime: fileModTime, name: "file2.txt"}, content: "klmnopqrstuvwxyz"},
|
||||
)
|
||||
dir2 := createTestDirectory(
|
||||
"testDir2",
|
||||
dmodtime,
|
||||
&testFile{testBaseEntry: testBaseEntry{modtime: fmodtime, name: "file3.txt"}, content: "abcdefghij1"},
|
||||
&testFile{testBaseEntry: testBaseEntry{modtime: fmodtime, name: "file4.txt"}, content: "klmnopqrstuvwxyz2"},
|
||||
dirModTime,
|
||||
dirOwnerInfo,
|
||||
dirMode,
|
||||
dirObjectID2,
|
||||
&testFile{testBaseEntry: testBaseEntry{modtime: fileModTime, name: "file3.txt"}, content: "abcdefghij1"},
|
||||
&testFile{testBaseEntry: testBaseEntry{modtime: fileModTime, name: "file4.txt"}, content: "klmnopqrstuvwxyz2"},
|
||||
)
|
||||
|
||||
c, err := diff.NewComparer(&buf)
|
||||
@@ -167,12 +221,18 @@ func TestCompareDifferentDirectories(t *testing.T) {
|
||||
_ = c.Close()
|
||||
})
|
||||
|
||||
expectedStats := diff.Stats{}
|
||||
expectedStats.FileEntries.Added = 2
|
||||
expectedStats.FileEntries.Removed = 2
|
||||
|
||||
expectedOutput := "added file ./file3.txt (11 bytes)\nadded file ./file4.txt (17 bytes)\n" +
|
||||
"removed file ./file1.txt (10 bytes)\n" +
|
||||
"removed file ./file2.txt (16 bytes)\n"
|
||||
|
||||
err = c.Compare(ctx, dir1, dir2)
|
||||
actualStats, err := c.Compare(ctx, dir1, dir2)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedStats, actualStats)
|
||||
require.Equal(t, expectedOutput, buf.String())
|
||||
}
|
||||
|
||||
@@ -181,22 +241,40 @@ func TestCompareDifferentDirectories_DirTimeDiff(t *testing.T) {
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
dmodtime1 := time.Date(2023, time.April, 12, 10, 30, 0, 0, time.UTC)
|
||||
dmodtime2 := time.Date(2022, time.April, 12, 10, 30, 0, 0, time.UTC)
|
||||
fmodtime := time.Date(2023, time.April, 12, 10, 30, 0, 0, time.UTC)
|
||||
fileModTime := time.Date(2023, time.April, 12, 10, 30, 0, 0, time.UTC)
|
||||
dirModTime1 := time.Date(2023, time.April, 12, 10, 30, 0, 0, time.UTC)
|
||||
dirModTime2 := time.Date(2022, time.April, 12, 10, 30, 0, 0, time.UTC)
|
||||
dirOwnerInfo := fs.OwnerInfo{UserID: 1000, GroupID: 1000}
|
||||
dirMode := os.FileMode(0o777)
|
||||
|
||||
cid, _ := index.IDFromHash("p", []byte("sdkjfn"))
|
||||
dirObjectID1 := object.DirectObjectID(cid)
|
||||
|
||||
cid, _ = index.IDFromHash("i", []byte("dfjlgn"))
|
||||
dirObjectID2 := object.DirectObjectID(cid)
|
||||
|
||||
dir1 := createTestDirectory(
|
||||
"testDir1",
|
||||
dmodtime1,
|
||||
&testFile{testBaseEntry: testBaseEntry{modtime: fmodtime, name: "file1.txt"}, content: "abcdefghij"},
|
||||
&testFile{testBaseEntry: testBaseEntry{modtime: fmodtime, name: "file2.txt"}, content: "klmnopqrstuvwxyz"},
|
||||
dirModTime1,
|
||||
dirOwnerInfo,
|
||||
dirMode,
|
||||
dirObjectID1,
|
||||
&testFile{testBaseEntry: testBaseEntry{modtime: fileModTime, name: "file1.txt"}, content: "abcdefghij"},
|
||||
&testFile{testBaseEntry: testBaseEntry{modtime: fileModTime, name: "file2.txt"}, content: "klmnopqrstuvwxyz"},
|
||||
)
|
||||
dir2 := createTestDirectory(
|
||||
"testDir2",
|
||||
dmodtime2,
|
||||
&testFile{testBaseEntry: testBaseEntry{modtime: fmodtime, name: "file1.txt"}, content: "abcdefghij"},
|
||||
&testFile{testBaseEntry: testBaseEntry{modtime: fmodtime, name: "file2.txt"}, content: "klmnopqrstuvwxyz"},
|
||||
dirModTime2,
|
||||
dirOwnerInfo,
|
||||
dirMode,
|
||||
dirObjectID2,
|
||||
&testFile{testBaseEntry: testBaseEntry{modtime: fileModTime, name: "file1.txt"}, content: "abcdefghij"},
|
||||
&testFile{testBaseEntry: testBaseEntry{modtime: fileModTime, name: "file2.txt"}, content: "klmnopqrstuvwxyz"},
|
||||
)
|
||||
|
||||
expectedStats := diff.Stats{}
|
||||
expectedStats.DirectoryEntries.Modified = 1
|
||||
|
||||
c, err := diff.NewComparer(&buf)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -204,10 +282,12 @@ func TestCompareDifferentDirectories_DirTimeDiff(t *testing.T) {
|
||||
_ = c.Close()
|
||||
})
|
||||
|
||||
expectedOutput := ". modification times differ: 2023-04-12 10:30:00 +0000 UTC 2022-04-12 10:30:00 +0000 UTC\n"
|
||||
err = c.Compare(ctx, dir1, dir2)
|
||||
expectedOutput := ". modification times differ: 2023-04-12 10:30:00 +0000 UTC 2022-04-12 10:30:00 +0000 UTC\n"
|
||||
actualStats, err := c.Compare(ctx, dir1, dir2)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedOutput, buf.String())
|
||||
require.Equal(t, expectedStats, actualStats)
|
||||
}
|
||||
|
||||
func TestCompareDifferentDirectories_FileTimeDiff(t *testing.T) {
|
||||
@@ -215,18 +295,33 @@ func TestCompareDifferentDirectories_FileTimeDiff(t *testing.T) {
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
fmodtime1 := time.Date(2023, time.April, 12, 10, 30, 0, 0, time.UTC)
|
||||
fmodtime2 := time.Date(2022, time.April, 12, 10, 30, 0, 0, time.UTC)
|
||||
dmodtime := time.Date(2023, time.April, 12, 10, 30, 0, 0, time.UTC)
|
||||
fileModTime1 := time.Date(2023, time.April, 12, 10, 30, 0, 0, time.UTC)
|
||||
fileModTime2 := time.Date(2022, time.April, 12, 10, 30, 0, 0, time.UTC)
|
||||
dirModTime := time.Date(2023, time.April, 12, 10, 30, 0, 0, time.UTC)
|
||||
dirOwnerInfo := fs.OwnerInfo{UserID: 1000, GroupID: 1000}
|
||||
dirMode := os.FileMode(0o700)
|
||||
|
||||
cid, _ := index.IDFromHash("p", []byte("sdkjfn"))
|
||||
OID1 := object.DirectObjectID(cid)
|
||||
|
||||
cid, _ = index.IDFromHash("i", []byte("hvhjb"))
|
||||
OID2 := object.DirectObjectID(cid)
|
||||
|
||||
dir1 := createTestDirectory(
|
||||
"testDir1",
|
||||
dmodtime,
|
||||
&testFile{testBaseEntry: testBaseEntry{modtime: fmodtime1, name: "file1.txt"}, content: "abcdefghij"},
|
||||
dirModTime,
|
||||
dirOwnerInfo,
|
||||
dirMode,
|
||||
OID1,
|
||||
&testFile{testBaseEntry: testBaseEntry{modtime: fileModTime1, name: "file1.txt", oid: OID1}, content: "abcdefghij"},
|
||||
)
|
||||
dir2 := createTestDirectory(
|
||||
"testDir2",
|
||||
dmodtime,
|
||||
&testFile{testBaseEntry: testBaseEntry{modtime: fmodtime2, name: "file1.txt"}, content: "abcdefghij"},
|
||||
dirModTime,
|
||||
dirOwnerInfo,
|
||||
dirMode,
|
||||
OID2,
|
||||
&testFile{testBaseEntry: testBaseEntry{modtime: fileModTime2, name: "file1.txt", oid: OID2}, content: "abcdefghij"},
|
||||
)
|
||||
|
||||
c, err := diff.NewComparer(&buf)
|
||||
@@ -236,13 +331,141 @@ func TestCompareDifferentDirectories_FileTimeDiff(t *testing.T) {
|
||||
_ = c.Close()
|
||||
})
|
||||
|
||||
expectedOutput := "./file1.txt modification times differ: 2023-04-12 10:30:00 +0000 UTC 2022-04-12 10:30:00 +0000 UTC\n"
|
||||
expectedStats := diff.Stats{}
|
||||
expectedStats.FileEntries.Modified = 1
|
||||
|
||||
expectedOutput := "./file1.txt modification times differ: 2023-04-12 10:30:00 +0000 UTC 2022-04-12 10:30:00 +0000 UTC\n"
|
||||
|
||||
actualStats, err := c.Compare(ctx, dir1, dir2)
|
||||
|
||||
err = c.Compare(ctx, dir1, dir2)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedOutput, buf.String())
|
||||
require.Equal(t, expectedStats, actualStats)
|
||||
}
|
||||
|
||||
func createTestDirectory(name string, modtime time.Time, files ...fs.Entry) *testDirectory {
|
||||
return &testDirectory{testBaseEntry: testBaseEntry{modtime: modtime, name: name}, files: files}
|
||||
func TestCompareFileWithIdenticalContentsButDiffFileMetadata(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
fileModTime1 := time.Date(2023, time.April, 12, 10, 30, 0, 0, time.UTC)
|
||||
fileModTime2 := time.Date(2022, time.April, 12, 10, 30, 0, 0, time.UTC)
|
||||
|
||||
fileOwnerinfo1 := fs.OwnerInfo{UserID: 1000, GroupID: 1000}
|
||||
fileOwnerinfo2 := fs.OwnerInfo{UserID: 1001, GroupID: 1002}
|
||||
|
||||
dirOwnerInfo := fs.OwnerInfo{UserID: 1000, GroupID: 1000}
|
||||
dirMode := os.FileMode(0o777)
|
||||
dirModTime := time.Date(2023, time.April, 12, 10, 30, 0, 0, time.UTC)
|
||||
|
||||
cid, _ := index.IDFromHash("p", []byte("sdkjfn"))
|
||||
dirObjectID1 := object.DirectObjectID(cid)
|
||||
|
||||
cid, _ = index.IDFromHash("i", []byte("dfjlgn"))
|
||||
dirObjectID2 := object.DirectObjectID(cid)
|
||||
|
||||
dir1 := createTestDirectory(
|
||||
"testDir1",
|
||||
dirModTime,
|
||||
dirOwnerInfo,
|
||||
dirMode,
|
||||
dirObjectID1,
|
||||
&testFile{testBaseEntry: testBaseEntry{name: "file1.txt", modtime: fileModTime1, oid: object.ID{}, owner: fileOwnerinfo1, mode: 0o700}, content: "abcdefghij"},
|
||||
)
|
||||
|
||||
dir2 := createTestDirectory(
|
||||
"testDir2",
|
||||
dirModTime,
|
||||
dirOwnerInfo,
|
||||
dirMode,
|
||||
dirObjectID2,
|
||||
&testFile{testBaseEntry: testBaseEntry{name: "file1.txt", modtime: fileModTime2, oid: object.ID{}, owner: fileOwnerinfo2, mode: 0o777}, content: "abcdefghij"},
|
||||
)
|
||||
|
||||
c, err := diff.NewComparer(&buf)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
_ = c.Close()
|
||||
})
|
||||
|
||||
expectedStats := diff.Stats{
|
||||
FileEntries: diff.EntryTypeStats{
|
||||
SameContentButDifferentMetadata: 1,
|
||||
SameContentButDifferentModificationTime: 1,
|
||||
SameContentButDifferentMode: 1,
|
||||
SameContentButDifferentUserOwner: 1,
|
||||
SameContentButDifferentGroupOwner: 1,
|
||||
},
|
||||
}
|
||||
|
||||
actualStats, err := c.Compare(ctx, dir1, dir2)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, buf.String())
|
||||
require.Equal(t, expectedStats, actualStats)
|
||||
}
|
||||
|
||||
func TestCompareIdenticalDirectoriesWithDiffDirectoryMetadata(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
dirModTime1 := time.Date(2023, time.April, 12, 10, 30, 0, 0, time.UTC)
|
||||
dirModTime2 := time.Date(2022, time.April, 12, 10, 30, 0, 0, time.UTC)
|
||||
|
||||
dirOwnerInfo1 := fs.OwnerInfo{UserID: 1000, GroupID: 1000}
|
||||
dirOwnerInfo2 := fs.OwnerInfo{UserID: 1001, GroupID: 1002}
|
||||
|
||||
dirMode1 := os.FileMode(0o644)
|
||||
dirMode2 := os.FileMode(0o777)
|
||||
|
||||
fileModTime := time.Date(2023, time.April, 12, 10, 30, 0, 0, time.UTC)
|
||||
|
||||
cid, _ := index.IDFromHash("p", []byte("sdkjfn"))
|
||||
dirObjectID := object.DirectObjectID(cid)
|
||||
|
||||
dir1 := createTestDirectory(
|
||||
"testDir1",
|
||||
dirModTime1,
|
||||
dirOwnerInfo1,
|
||||
dirMode1,
|
||||
dirObjectID,
|
||||
&testFile{testBaseEntry: testBaseEntry{name: "file1.txt", modtime: fileModTime}, content: "abcdefghij"},
|
||||
)
|
||||
|
||||
dir2 := createTestDirectory(
|
||||
"testDir2",
|
||||
dirModTime2,
|
||||
dirOwnerInfo2,
|
||||
dirMode2,
|
||||
dirObjectID,
|
||||
&testFile{testBaseEntry: testBaseEntry{name: "file1.txt", modtime: fileModTime}, content: "abcdefghij"},
|
||||
)
|
||||
c, err := diff.NewComparer(&buf)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
_ = c.Close()
|
||||
})
|
||||
|
||||
expectedStats := diff.Stats{
|
||||
DirectoryEntries: diff.EntryTypeStats{
|
||||
SameContentButDifferentMetadata: 1,
|
||||
SameContentButDifferentModificationTime: 1,
|
||||
SameContentButDifferentMode: 1,
|
||||
SameContentButDifferentUserOwner: 1,
|
||||
SameContentButDifferentGroupOwner: 1,
|
||||
},
|
||||
}
|
||||
|
||||
actualStats, err := c.Compare(ctx, dir1, dir2)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, buf.String())
|
||||
require.Equal(t, expectedStats, actualStats)
|
||||
}
|
||||
|
||||
func createTestDirectory(name string, modtime time.Time, owner fs.OwnerInfo, mode os.FileMode, oid object.ID, files ...fs.Entry) *testDirectory {
|
||||
return &testDirectory{testBaseEntry: testBaseEntry{modtime: modtime, name: name, owner: owner, mode: mode, oid: oid}, files: files}
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ func compareDirs(t *testing.T, source, restoreDir string) {
|
||||
require.NoError(t, err)
|
||||
|
||||
cmp.DiffCommand = "cmp"
|
||||
_ = cmp.Compare(ctx, s, r)
|
||||
cmp.Compare(ctx, s, r)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -310,7 +310,7 @@ func CompareDirs(t *testing.T, source, destination string) {
|
||||
e2, err := localfs.NewEntry(destination)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = c.Compare(ctx, e1, e2)
|
||||
_, err = c.Compare(ctx, e1, e2)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user