diff options
Diffstat (limited to 'fs/sync/sync_test.go')
| -rw-r--r-- | fs/sync/sync_test.go | 3108 |
1 files changed, 3108 insertions, 0 deletions
diff --git a/fs/sync/sync_test.go b/fs/sync/sync_test.go new file mode 100644 index 0000000..9865ecc --- /dev/null +++ b/fs/sync/sync_test.go @@ -0,0 +1,3108 @@ +// Test sync/copy/move + +package sync + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "os" + "os/exec" + "runtime" + "sort" + "strings" + "testing" + "time" + + mutex "sync" // renamed as "sync" already in use + + _ "github.com/mewsen/rclone-studip-backend-oot/backend/studip" + _ "github.com/rclone/rclone/backend/all" // import all backends + "github.com/rclone/rclone/cmd/bisync/bilib" + "github.com/rclone/rclone/fs" + "github.com/rclone/rclone/fs/accounting" + "github.com/rclone/rclone/fs/filter" + "github.com/rclone/rclone/fs/fserrors" + "github.com/rclone/rclone/fs/hash" + "github.com/rclone/rclone/fs/operations" + "github.com/rclone/rclone/fstest" + "github.com/rclone/rclone/lib/transform" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/text/unicode/norm" +) + +// Some times used in the tests +var ( + t1 = fstest.Time("2001-02-03T04:05:06.499999999Z") + t2 = fstest.Time("2011-12-25T12:59:59.123456789Z") + t3 = fstest.Time("2011-12-30T12:59:59.000000000Z") +) + +// TestMain drives the tests +func TestMain(m *testing.M) { + fstest.TestMain(m) +} + +// Check dry run is working +func TestCopyWithDryRun(t *testing.T) { + ctx := context.Background() + ctx, ci := fs.AddConfig(ctx) + r := fstest.NewRun(t) + file1 := r.WriteFile("sub dir/hello world", "hello world", t1) + r.Mkdir(ctx, r.Fremote) + + ci.DryRun = true + ctx = predictDstFromLogger(ctx) + err := CopyDir(ctx, r.Fremote, r.Flocal, false) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) // error expected here because dry-run + require.NoError(t, err) + + r.CheckLocalItems(t, file1) + r.CheckRemoteItems(t) +} + +// Now without dry run +func TestCopy(t *testing.T) { + ctx := context.Background() + r := fstest.NewRun(t) + file1 := r.WriteFile("sub dir/hello world", "hello world", t1) + _, err := operations.SetDirModTime(ctx, r.Flocal, nil, "sub dir", t2) + if err != nil && !errors.Is(err, fs.ErrorNotImplemented) { + require.NoError(t, err) + } + r.Mkdir(ctx, r.Fremote) + + ctx = predictDstFromLogger(ctx) + err = CopyDir(ctx, r.Fremote, r.Flocal, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + r.CheckLocalItems(t, file1) + r.CheckRemoteItems(t, file1) + + // Check that the modtimes of the directories are as expected + r.CheckDirectoryModTimes(t, "sub dir") +} + +func testCopyMetadata(t *testing.T, createEmptySrcDirs bool) { + ctx := context.Background() + ctx, ci := fs.AddConfig(ctx) + ci.Metadata = true + r := fstest.NewRun(t) + features := r.Fremote.Features() + + if !features.ReadMetadata && !features.WriteMetadata && !features.UserMetadata && + !features.ReadDirMetadata && !features.WriteDirMetadata && !features.UserDirMetadata { + t.Skip("Skipping as metadata not supported") + } + + if createEmptySrcDirs && !features.CanHaveEmptyDirectories { + t.Skip("Skipping as can't have empty directories") + } + + const content = "hello metadata world!" + const dirPath = "metadata sub dir" + const emptyDirPath = "empty metadata sub dir" + const filePath = dirPath + "/hello metadata world" + + fileMetadata := fs.Metadata{ + // System metadata supported by all backends + "mtime": t1.Format(time.RFC3339Nano), + // User metadata + "potato": "jersey", + } + + dirMetadata := fs.Metadata{ + // System metadata supported by all backends + "mtime": t2.Format(time.RFC3339Nano), + // User metadata + "potato": "king edward", + } + + // Make the directory with metadata - may fall back to Mkdir + _, err := operations.MkdirMetadata(ctx, r.Flocal, dirPath, dirMetadata) + require.NoError(t, err) + + // Make the empty directory with metadata - may fall back to Mkdir + _, err = operations.MkdirMetadata(ctx, r.Flocal, emptyDirPath, dirMetadata) + require.NoError(t, err) + + // Upload the file with metadata + in := io.NopCloser(bytes.NewBufferString(content)) + _, err = operations.Rcat(ctx, r.Flocal, filePath, in, t1, fileMetadata) + require.NoError(t, err) + file1 := fstest.NewItem(filePath, content, t1) + + // Reset the time of the directory + _, err = operations.SetDirModTime(ctx, r.Flocal, nil, dirPath, t2) + if err != nil && !errors.Is(err, fs.ErrorNotImplemented) { + require.NoError(t, err) + } + + ctx = predictDstFromLogger(ctx) + err = CopyDir(ctx, r.Fremote, r.Flocal, createEmptySrcDirs) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + r.CheckLocalItems(t, file1) + r.CheckRemoteItems(t, file1) + + // Check that the modtimes of the directories are as expected + r.CheckDirectoryModTimes(t, dirPath) + + // Check that the metadata on the directory and file is correct + if features.WriteMetadata && features.ReadMetadata { + fstest.CheckEntryMetadata(ctx, t, r.Fremote, fstest.NewObject(ctx, t, r.Fremote, filePath), fileMetadata) + } + if features.WriteDirMetadata && features.ReadDirMetadata { + fstest.CheckEntryMetadata(ctx, t, r.Fremote, fstest.NewDirectory(ctx, t, r.Fremote, dirPath), dirMetadata) + } + if !createEmptySrcDirs { + // dir must not exist + _, err := fstest.NewDirectoryRetries(ctx, t, r.Fremote, emptyDirPath, 1) + assert.Error(t, err, "Not expecting to find empty directory") + assert.True(t, errors.Is(err, fs.ErrorDirNotFound), fmt.Sprintf("expecting wrapped %#v not: %#v", fs.ErrorDirNotFound, err)) + } else { + // dir must exist + dir := fstest.NewDirectory(ctx, t, r.Fremote, emptyDirPath) + if features.ReadDirMetadata { + fstest.CheckEntryMetadata(ctx, t, r.Fremote, dir, dirMetadata) + } + } +} + +func TestCopyMetadata(t *testing.T) { + testCopyMetadata(t, true) +} + +func TestCopyMetadataNoEmptyDirs(t *testing.T) { + testCopyMetadata(t, false) +} + +func TestCopyMissingDirectory(t *testing.T) { + ctx := context.Background() + r := fstest.NewRun(t) + r.Mkdir(ctx, r.Fremote) + + nonExistingFs, err := fs.NewFs(ctx, "/non-existing") + if err != nil { + t.Fatal(err) + } + + ctx = predictDstFromLogger(ctx) + err = CopyDir(ctx, r.Fremote, nonExistingFs, false) + require.Error(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) +} + +// Now with --no-traverse +func TestCopyNoTraverse(t *testing.T) { + ctx := context.Background() + ctx, ci := fs.AddConfig(ctx) + r := fstest.NewRun(t) + + ci.NoTraverse = true + + file1 := r.WriteFile("sub dir/hello world", "hello world", t1) + + ctx = predictDstFromLogger(ctx) + err := CopyDir(ctx, r.Fremote, r.Flocal, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + r.CheckLocalItems(t, file1) + r.CheckRemoteItems(t, file1) +} + +func TestCopyNoTraverseDeadlock(t *testing.T) { + r := fstest.NewRun(t) + if !r.Fremote.Features().IsLocal { + t.Skip("Only runs on local") + } + const nFiles = 200 + t1 := fstest.Time("2001-02-03T04:05:06.499999999Z") + + // Create lots of source files. + items := make([]fstest.Item, nFiles) + for i := range items { + name := fmt.Sprintf("file%d.txt", i) + items[i] = r.WriteFile(name, fmt.Sprintf("content%d", i), t1) + } + r.CheckLocalItems(t, items...) + + // Set --no-traverse + ctx, ci := fs.AddConfig(context.Background()) + ci.NoTraverse = true + + // Initial copy to establish destination. + require.NoError(t, CopyDir(ctx, r.Fremote, r.Flocal, false)) + r.CheckRemoteItems(t, items...) + + // Second copy which shouldn't deadlock + require.NoError(t, CopyDir(ctx, r.Flocal, r.Fremote, false)) + r.CheckRemoteItems(t, items...) +} + +// Now with --check-first +func TestCopyCheckFirst(t *testing.T) { + ctx := context.Background() + ctx, ci := fs.AddConfig(ctx) + r := fstest.NewRun(t) + + ci.CheckFirst = true + + file1 := r.WriteFile("sub dir/hello world", "hello world", t1) + + ctx = predictDstFromLogger(ctx) + err := CopyDir(ctx, r.Fremote, r.Flocal, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + r.CheckLocalItems(t, file1) + r.CheckRemoteItems(t, file1) +} + +// Now with --no-traverse +func TestSyncNoTraverse(t *testing.T) { + ctx := context.Background() + ctx, ci := fs.AddConfig(ctx) + r := fstest.NewRun(t) + + ci.NoTraverse = true + + file1 := r.WriteFile("sub dir/hello world", "hello world", t1) + + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + err := Sync(ctx, r.Fremote, r.Flocal, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + r.CheckLocalItems(t, file1) + r.CheckRemoteItems(t, file1) +} + +// Test copy with depth +func TestCopyWithDepth(t *testing.T) { + ctx := context.Background() + ctx, ci := fs.AddConfig(ctx) + r := fstest.NewRun(t) + file1 := r.WriteFile("sub dir/hello world", "hello world", t1) + file2 := r.WriteFile("hello world2", "hello world2", t2) + + // Check the MaxDepth too + ci.MaxDepth = 1 + + ctx = predictDstFromLogger(ctx) + err := CopyDir(ctx, r.Fremote, r.Flocal, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + r.CheckLocalItems(t, file1, file2) + r.CheckRemoteItems(t, file2) +} + +// Test copy with files from +func testCopyWithFilesFrom(t *testing.T, noTraverse bool) { + ctx := context.Background() + ctx, ci := fs.AddConfig(ctx) + r := fstest.NewRun(t) + file1 := r.WriteFile("potato2", "hello world", t1) + file2 := r.WriteFile("hello world2", "hello world2", t2) + + // Set the --files-from equivalent + f, err := filter.NewFilter(nil) + require.NoError(t, err) + require.NoError(t, f.AddFile("potato2")) + require.NoError(t, f.AddFile("notfound")) + + // Change the active filter + ctx = filter.ReplaceConfig(ctx, f) + + ci.NoTraverse = noTraverse + + ctx = predictDstFromLogger(ctx) + err = CopyDir(ctx, r.Fremote, r.Flocal, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + r.CheckLocalItems(t, file1, file2) + r.CheckRemoteItems(t, file1) +} +func TestCopyWithFilesFrom(t *testing.T) { testCopyWithFilesFrom(t, false) } +func TestCopyWithFilesFromAndNoTraverse(t *testing.T) { testCopyWithFilesFrom(t, true) } + +// Test copy empty directories +func TestCopyEmptyDirectories(t *testing.T) { + ctx := context.Background() + r := fstest.NewRun(t) + file1 := r.WriteFile("sub dir/hello world", "hello world", t1) + _, err := operations.MkdirModTime(ctx, r.Flocal, "sub dir2/sub sub dir2", t2) + require.NoError(t, err) + _, err = operations.SetDirModTime(ctx, r.Flocal, nil, "sub dir2", t2) + require.NoError(t, err) + r.Mkdir(ctx, r.Fremote) + + // Set the modtime on "sub dir" to something specific + // Without this it fails on the CI and in VirtualBox with variances of up to 10mS + _, err = operations.SetDirModTime(ctx, r.Flocal, nil, "sub dir", t1) + require.NoError(t, err) + + ctx = predictDstFromLogger(ctx) + err = CopyDir(ctx, r.Fremote, r.Flocal, true) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + r.CheckRemoteListing( + t, + []fstest.Item{ + file1, + }, + []string{ + "sub dir", + "sub dir2", + "sub dir2/sub sub dir2", + }, + ) + + // Check that the modtimes of the directories are as expected + r.CheckDirectoryModTimes(t, "sub dir", "sub dir2", "sub dir2/sub sub dir2") +} + +// Test copy empty directories when we are configured not to create them +func TestCopyNoEmptyDirectories(t *testing.T) { + ctx := context.Background() + r := fstest.NewRun(t) + file1 := r.WriteFile("sub dir/hello world", "hello world", t1) + err := operations.Mkdir(ctx, r.Flocal, "sub dir2") + require.NoError(t, err) + _, err = operations.MkdirModTime(ctx, r.Flocal, "sub dir2/sub sub dir2", t2) + require.NoError(t, err) + r.Mkdir(ctx, r.Fremote) + + err = CopyDir(ctx, r.Fremote, r.Flocal, false) + require.NoError(t, err) + + r.CheckRemoteListing( + t, + []fstest.Item{ + file1, + }, + []string{ + "sub dir", + }, + ) +} + +// Test move empty directories +func TestMoveEmptyDirectories(t *testing.T) { + ctx := context.Background() + r := fstest.NewRun(t) + file1 := r.WriteFile("sub dir/hello world", "hello world", t1) + _, err := operations.MkdirModTime(ctx, r.Flocal, "sub dir2", t2) + require.NoError(t, err) + subDir := fstest.NewDirectory(ctx, t, r.Flocal, "sub dir") + subDirT := subDir.ModTime(ctx) + r.Mkdir(ctx, r.Fremote) + + ctx = predictDstFromLogger(ctx) + err = MoveDir(ctx, r.Fremote, r.Flocal, false, true) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + r.CheckRemoteListing( + t, + []fstest.Item{ + file1, + }, + []string{ + "sub dir", + "sub dir2", + }, + ) + + // Check that the modtimes of the directories are as expected + r.CheckDirectoryModTimes(t, "sub dir2") + // Note that "sub dir" mod time is updated when file1 is deleted from it + // So check it more manually + got := fstest.NewDirectory(ctx, t, r.Fremote, "sub dir") + fstest.CheckDirModTime(ctx, t, r.Fremote, got, subDirT) +} + +// Test that --no-update-dir-modtime is working +func TestSyncNoUpdateDirModtime(t *testing.T) { + r := fstest.NewRun(t) + if r.Fremote.Features().DirSetModTime == nil { + t.Skip("Skipping test as backend does not support DirSetModTime") + } + + ctx, ci := fs.AddConfig(context.Background()) + ci.NoUpdateDirModTime = true + const name = "sub dir no update dir modtime" + + // Set the modtime on name to something specific + _, err := operations.MkdirModTime(ctx, r.Flocal, name, t1) + require.NoError(t, err) + + // Create the remote directory with the current time + require.NoError(t, r.Fremote.Mkdir(ctx, name)) + + // Read its modification time + wantT := fstest.NewDirectory(ctx, t, r.Fremote, name).ModTime(ctx) + + ctx = predictDstFromLogger(ctx) + err = Sync(ctx, r.Fremote, r.Flocal, true) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + r.CheckRemoteListing( + t, + []fstest.Item{}, + []string{ + name, + }, + ) + + // Read the new directory modification time - it should not have changed + gotT := fstest.NewDirectory(ctx, t, r.Fremote, name).ModTime(ctx) + fstest.AssertTimeEqualWithPrecision(t, name, wantT, gotT, r.Fremote.Precision()) +} + +// Test move empty directories when we are not configured to create them +func TestMoveNoEmptyDirectories(t *testing.T) { + ctx := context.Background() + r := fstest.NewRun(t) + file1 := r.WriteFile("sub dir/hello world", "hello world", t1) + err := operations.Mkdir(ctx, r.Flocal, "sub dir2") + require.NoError(t, err) + r.Mkdir(ctx, r.Fremote) + + err = MoveDir(ctx, r.Fremote, r.Flocal, false, false) + require.NoError(t, err) + + r.CheckRemoteListing( + t, + []fstest.Item{ + file1, + }, + []string{ + "sub dir", + }, + ) +} + +// Test sync empty directories +func TestSyncEmptyDirectories(t *testing.T) { + ctx := context.Background() + r := fstest.NewRun(t) + file1 := r.WriteFile("sub dir/hello world", "hello world", t1) + _, err := operations.MkdirModTime(ctx, r.Flocal, "sub dir2", t2) + require.NoError(t, err) + + // Set the modtime on "sub dir" to something specific + // Without this it fails on the CI and in VirtualBox with variances of up to 10mS + _, err = operations.SetDirModTime(ctx, r.Flocal, nil, "sub dir", t1) + require.NoError(t, err) + + r.Mkdir(ctx, r.Fremote) + + ctx = predictDstFromLogger(ctx) + err = Sync(ctx, r.Fremote, r.Flocal, true) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + r.CheckRemoteListing( + t, + []fstest.Item{ + file1, + }, + []string{ + "sub dir", + "sub dir2", + }, + ) + + // Check that the modtimes of the directories are as expected + r.CheckDirectoryModTimes(t, "sub dir", "sub dir2") +} + +// Test delayed mod time setting +func TestSyncSetDelayedModTimes(t *testing.T) { + ctx := context.Background() + r := fstest.NewRun(t) + + if !r.Fremote.Features().DirModTimeUpdatesOnWrite { + t.Skip("Backend doesn't have DirModTimeUpdatesOnWrite set") + } + + // Create directories without timestamps + require.NoError(t, r.Flocal.Mkdir(ctx, "a1/b1/c1/d1/e1/f1")) + require.NoError(t, r.Flocal.Mkdir(ctx, "a1/b2/c1/d1/e1/f1")) + require.NoError(t, r.Flocal.Mkdir(ctx, "a1/b1/c1/d2/e1/f1")) + require.NoError(t, r.Flocal.Mkdir(ctx, "a1/b1/c1/d2/e1/f2")) + + dirs := []string{ + "a1", + "a1/b1", + "a1/b1/c1", + "a1/b1/c1/d1", + "a1/b1/c1/d1/e1", + "a1/b1/c1/d1/e1/f1", + "a1/b1/c1/d2", + "a1/b1/c1/d2/e1", + "a1/b1/c1/d2/e1/f1", + "a1/b1/c1/d2/e1/f2", + "a1/b2", + "a1/b2/c1", + "a1/b2/c1/d1", + "a1/b2/c1/d1/e1", + "a1/b2/c1/d1/e1/f1", + } + r.CheckLocalListing(t, []fstest.Item{}, dirs) + + // Timestamp the directories in reverse order + ts := t1 + for i := len(dirs) - 1; i >= 0; i-- { + dir := dirs[i] + _, err := operations.SetDirModTime(ctx, r.Flocal, nil, dir, ts) + require.NoError(t, err) + ts = ts.Add(time.Minute) + } + + r.Mkdir(ctx, r.Fremote) + + ctx = predictDstFromLogger(ctx) + err := Sync(ctx, r.Fremote, r.Flocal, true) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + r.CheckRemoteListing(t, []fstest.Item{}, dirs) + + // Check that the modtimes of the directories are as expected + r.CheckDirectoryModTimes(t, dirs...) +} + +// Test sync empty directories when we are not configured to create them +func TestSyncNoEmptyDirectories(t *testing.T) { + ctx := context.Background() + r := fstest.NewRun(t) + file1 := r.WriteFile("sub dir/hello world", "hello world", t1) + err := operations.Mkdir(ctx, r.Flocal, "sub dir2") + require.NoError(t, err) + r.Mkdir(ctx, r.Fremote) + + err = Sync(ctx, r.Fremote, r.Flocal, false) + require.NoError(t, err) + + r.CheckRemoteListing( + t, + []fstest.Item{ + file1, + }, + []string{ + "sub dir", + }, + ) +} + +// Test a server-side copy if possible, or the backup path if not +func TestServerSideCopy(t *testing.T) { + ctx := context.Background() + r := fstest.NewRun(t) + file1 := r.WriteObject(ctx, "sub dir/hello world", "hello world", t1) + r.CheckRemoteItems(t, file1) + + FremoteCopy, _, finaliseCopy, err := fstest.RandomRemote() + require.NoError(t, err) + defer finaliseCopy() + t.Logf("Server side copy (if possible) %v -> %v", r.Fremote, FremoteCopy) + + ctx = predictDstFromLogger(ctx) + err = CopyDir(ctx, FremoteCopy, r.Fremote, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + fstest.CheckItems(t, FremoteCopy, file1) +} + +// Test copying a file over itself +func TestCopyOverSelf(t *testing.T) { + ctx := context.Background() + r := fstest.NewRun(t) + file1 := r.WriteObject(ctx, "sub dir/hello world", "hello world", t1) + r.CheckRemoteItems(t, file1) + file2 := r.WriteFile("sub dir/hello world", "hello world again", t2) + r.CheckLocalItems(t, file2) + + ctx = predictDstFromLogger(ctx) + err := CopyDir(ctx, r.Fremote, r.Flocal, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + r.CheckRemoteItems(t, file2) +} + +// Test server-side copying a file over itself +func TestServerSideCopyOverSelf(t *testing.T) { + ctx := context.Background() + r := fstest.NewRun(t) + file1 := r.WriteObject(ctx, "sub dir/hello world", "hello world", t1) + r.CheckRemoteItems(t, file1) + + FremoteCopy, _, finaliseCopy, err := fstest.RandomRemote() + require.NoError(t, err) + defer finaliseCopy() + t.Logf("Server side copy (if possible) %v -> %v", r.Fremote, FremoteCopy) + + ctx = predictDstFromLogger(ctx) + err = CopyDir(ctx, FremoteCopy, r.Fremote, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, FremoteCopy, r.Fremote, operations.GetLoggerOpt(ctx).JSON, t) + fstest.CheckItems(t, FremoteCopy, file1) + + file2 := r.WriteObject(ctx, "sub dir/hello world", "hello world again", t2) + r.CheckRemoteItems(t, file2) + + ctx = predictDstFromLogger(ctx) + err = CopyDir(ctx, FremoteCopy, r.Fremote, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, FremoteCopy, r.Fremote, operations.GetLoggerOpt(ctx).JSON, t) + fstest.CheckItems(t, FremoteCopy, file2) +} + +// Test moving a file over itself +func TestMoveOverSelf(t *testing.T) { + ctx := context.Background() + r := fstest.NewRun(t) + file1 := r.WriteObject(ctx, "sub dir/hello world", "hello world", t1) + r.CheckRemoteItems(t, file1) + file2 := r.WriteFile("sub dir/hello world", "hello world again", t2) + r.CheckLocalItems(t, file2) + + ctx = predictDstFromLogger(ctx) + err := MoveDir(ctx, r.Fremote, r.Flocal, false, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + r.CheckLocalItems(t) + r.CheckRemoteItems(t, file2) +} + +// Test server-side moving a file over itself +func TestServerSideMoveOverSelf(t *testing.T) { + ctx := context.Background() + r := fstest.NewRun(t) + file1 := r.WriteObject(ctx, "sub dir/hello world", "hello world", t1) + r.CheckRemoteItems(t, file1) + + FremoteCopy, _, finaliseCopy, err := fstest.RandomRemote() + require.NoError(t, err) + defer finaliseCopy() + t.Logf("Server side copy (if possible) %v -> %v", r.Fremote, FremoteCopy) + + ctx = predictDstFromLogger(ctx) + err = CopyDir(ctx, FremoteCopy, r.Fremote, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, FremoteCopy, r.Fremote, operations.GetLoggerOpt(ctx).JSON, t) + fstest.CheckItems(t, FremoteCopy, file1) + + file2 := r.WriteObject(ctx, "sub dir/hello world", "hello world again", t2) + r.CheckRemoteItems(t, file2) + + // ctx = predictDstFromLogger(ctx) + err = MoveDir(ctx, FremoteCopy, r.Fremote, false, false) + require.NoError(t, err) + // testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) // not currently supported + r.CheckRemoteItems(t) + fstest.CheckItems(t, FremoteCopy, file2) + + // check that individual file moves also work without MoveDir + file3 := r.WriteObject(ctx, "sub dir/hello world", "hello world a third time", t3) + r.CheckRemoteItems(t, file3) + + ctx = predictDstFromLogger(ctx) + fs.Debugf(nil, "testing file moves") + err = moveDir(ctx, FremoteCopy, r.Fremote, false, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, FremoteCopy, r.Fremote, operations.GetLoggerOpt(ctx).JSON, t) + r.CheckRemoteItems(t) + fstest.CheckItems(t, FremoteCopy, file3) +} + +// Check that if the local file doesn't exist when we copy it up, +// nothing happens to the remote file +func TestCopyAfterDelete(t *testing.T) { + ctx := context.Background() + r := fstest.NewRun(t) + file1 := r.WriteObject(ctx, "sub dir/hello world", "hello world", t1) + r.CheckLocalItems(t) + r.CheckRemoteItems(t, file1) + + err := operations.Mkdir(ctx, r.Flocal, "") + require.NoError(t, err) + + ctx = predictDstFromLogger(ctx) + err = CopyDir(ctx, r.Fremote, r.Flocal, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + r.CheckLocalItems(t) + r.CheckRemoteItems(t, file1) +} + +// Check the copy downloading a file +func TestCopyRedownload(t *testing.T) { + ctx := context.Background() + r := fstest.NewRun(t) + file1 := r.WriteObject(ctx, "sub dir/hello world", "hello world", t1) + r.CheckRemoteItems(t, file1) + + ctx = predictDstFromLogger(ctx) + err := CopyDir(ctx, r.Flocal, r.Fremote, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + // Test with combined precision of local and remote as we copied it there and back + r.CheckLocalListing(t, []fstest.Item{file1}, nil) +} + +// Create a file and sync it. Change the last modified date and resync. +// If we're only doing sync by size and checksum, we expect nothing to +// to be transferred on the second sync. +func TestSyncBasedOnCheckSum(t *testing.T) { + ctx := context.Background() + ctx, ci := fs.AddConfig(ctx) + r := fstest.NewRun(t) + ci.CheckSum = true + + file1 := r.WriteFile("check sum", "-", t1) + r.CheckLocalItems(t, file1) + + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + err := Sync(ctx, r.Fremote, r.Flocal, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + // We should have transferred exactly one file. + assert.Equal(t, toyFileTransfers(r), accounting.GlobalStats().GetTransfers()) + r.CheckRemoteItems(t, file1) + + // Change last modified date only + file2 := r.WriteFile("check sum", "-", t2) + r.CheckLocalItems(t, file2) + + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + err = Sync(ctx, r.Fremote, r.Flocal, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + // We should have transferred no files + assert.Equal(t, int64(0), accounting.GlobalStats().GetTransfers()) + r.CheckLocalItems(t, file2) + r.CheckRemoteItems(t, file1) +} + +// Create a file and sync it. Change the last modified date and the +// file contents but not the size. If we're only doing sync by size +// only, we expect nothing to be transferred on the second sync. +func TestSyncSizeOnly(t *testing.T) { + ctx := context.Background() + ctx, ci := fs.AddConfig(ctx) + r := fstest.NewRun(t) + ci.SizeOnly = true + + file1 := r.WriteFile("sizeonly", "potato", t1) + r.CheckLocalItems(t, file1) + + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + err := Sync(ctx, r.Fremote, r.Flocal, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + // We should have transferred exactly one file. + assert.Equal(t, toyFileTransfers(r), accounting.GlobalStats().GetTransfers()) + r.CheckRemoteItems(t, file1) + + // Update mtime, md5sum but not length of file + file2 := r.WriteFile("sizeonly", "POTATO", t2) + r.CheckLocalItems(t, file2) + + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + err = Sync(ctx, r.Fremote, r.Flocal, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + // We should have transferred no files + assert.Equal(t, int64(0), accounting.GlobalStats().GetTransfers()) + r.CheckLocalItems(t, file2) + r.CheckRemoteItems(t, file1) +} + +// Create a file and sync it. Keep the last modified date but change +// the size. With --ignore-size we expect nothing to be +// transferred on the second sync. +func TestSyncIgnoreSize(t *testing.T) { + ctx := context.Background() + ctx, ci := fs.AddConfig(ctx) + r := fstest.NewRun(t) + ci.IgnoreSize = true + + file1 := r.WriteFile("ignore-size", "contents", t1) + r.CheckLocalItems(t, file1) + + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + err := Sync(ctx, r.Fremote, r.Flocal, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + // We should have transferred exactly one file. + assert.Equal(t, toyFileTransfers(r), accounting.GlobalStats().GetTransfers()) + r.CheckRemoteItems(t, file1) + + // Update size but not date of file + file2 := r.WriteFile("ignore-size", "longer contents but same date", t1) + r.CheckLocalItems(t, file2) + + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + err = Sync(ctx, r.Fremote, r.Flocal, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + // We should have transferred no files + assert.Equal(t, int64(0), accounting.GlobalStats().GetTransfers()) + r.CheckLocalItems(t, file2) + r.CheckRemoteItems(t, file1) +} + +func TestSyncIgnoreTimes(t *testing.T) { + ctx := context.Background() + ctx, ci := fs.AddConfig(ctx) + r := fstest.NewRun(t) + file1 := r.WriteBoth(ctx, "existing", "potato", t1) + r.CheckRemoteItems(t, file1) + + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + err := Sync(ctx, r.Fremote, r.Flocal, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + // We should have transferred exactly 0 files because the + // files were identical. + assert.Equal(t, int64(0), accounting.GlobalStats().GetTransfers()) + + ci.IgnoreTimes = true + + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + err = Sync(ctx, r.Fremote, r.Flocal, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + // We should have transferred exactly one file even though the + // files were identical. + assert.Equal(t, toyFileTransfers(r), accounting.GlobalStats().GetTransfers()) + + r.CheckLocalItems(t, file1) + r.CheckRemoteItems(t, file1) +} + +func TestSyncIgnoreExisting(t *testing.T) { + ctx := context.Background() + ctx, ci := fs.AddConfig(ctx) + r := fstest.NewRun(t) + file1 := r.WriteFile("existing", "potato", t1) + + ci.IgnoreExisting = true + + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + err := Sync(ctx, r.Fremote, r.Flocal, false) + require.NoError(t, err) + r.CheckLocalItems(t, file1) + r.CheckRemoteItems(t, file1) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + // Change everything + r.WriteFile("existing", "newpotatoes", t2) + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + err = Sync(ctx, r.Fremote, r.Flocal, false) + require.NoError(t, err) + // Items should not change + r.CheckRemoteItems(t, file1) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) +} + +func TestSyncIgnoreErrors(t *testing.T) { + ctx := context.Background() + ctx, ci := fs.AddConfig(ctx) + r := fstest.NewRun(t) + ci.IgnoreErrors = true + file1 := r.WriteFile("a/potato2", "------------------------------------------------------------", t1) + file2 := r.WriteObject(ctx, "b/potato", "SMALLER BUT SAME DATE", t2) + file3 := r.WriteBoth(ctx, "c/non empty space", "AhHa!", t2) + require.NoError(t, operations.Mkdir(ctx, r.Fremote, "d")) + + r.CheckLocalListing( + t, + []fstest.Item{ + file1, + file3, + }, + []string{ + "a", + "c", + }, + ) + r.CheckRemoteListing( + t, + []fstest.Item{ + file2, + file3, + }, + []string{ + "b", + "c", + "d", + }, + ) + + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + _ = fs.CountError(ctx, errors.New("boom")) + assert.NoError(t, Sync(ctx, r.Fremote, r.Flocal, false)) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + r.CheckLocalListing( + t, + []fstest.Item{ + file1, + file3, + }, + []string{ + "a", + "c", + }, + ) + r.CheckRemoteListing( + t, + []fstest.Item{ + file1, + file3, + }, + []string{ + "a", + "c", + }, + ) +} + +func TestSyncAfterChangingModtimeOnly(t *testing.T) { + ctx := context.Background() + ctx, ci := fs.AddConfig(ctx) + r := fstest.NewRun(t) + file1 := r.WriteFile("empty space", "-", t2) + file2 := r.WriteObject(ctx, "empty space", "-", t1) + + r.CheckLocalItems(t, file1) + r.CheckRemoteItems(t, file2) + + ci.DryRun = true + + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + err := Sync(ctx, r.Fremote, r.Flocal, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + r.CheckLocalItems(t, file1) + r.CheckRemoteItems(t, file2) + + ci.DryRun = false + + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + err = Sync(ctx, r.Fremote, r.Flocal, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + r.CheckLocalItems(t, file1) + r.CheckRemoteItems(t, file1) +} + +func TestSyncAfterChangingModtimeOnlyWithNoUpdateModTime(t *testing.T) { + ctx := context.Background() + ctx, ci := fs.AddConfig(ctx) + r := fstest.NewRun(t) + + if r.Fremote.Hashes().Count() == 0 { + t.Logf("Can't check this if no hashes supported") + return + } + + ci.NoUpdateModTime = true + + file1 := r.WriteFile("empty space", "-", t2) + file2 := r.WriteObject(ctx, "empty space", "-", t1) + + r.CheckLocalItems(t, file1) + r.CheckRemoteItems(t, file2) + + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + err := Sync(ctx, r.Fremote, r.Flocal, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + r.CheckLocalItems(t, file1) + r.CheckRemoteItems(t, file2) +} + +func TestSyncDoesntUpdateModtime(t *testing.T) { + ctx := context.Background() + r := fstest.NewRun(t) + if fs.GetModifyWindow(ctx, r.Fremote) == fs.ModTimeNotSupported { + t.Skip("Can't run this test on fs which doesn't support mod time") + } + + file1 := r.WriteFile("foo", "foo", t2) + file2 := r.WriteObject(ctx, "foo", "bar", t1) + + r.CheckLocalItems(t, file1) + r.CheckRemoteItems(t, file2) + + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + err := Sync(ctx, r.Fremote, r.Flocal, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + r.CheckLocalItems(t, file1) + r.CheckRemoteItems(t, file1) + + // We should have transferred exactly one file, not set the mod time + assert.Equal(t, toyFileTransfers(r), accounting.GlobalStats().GetTransfers()) +} + +func TestSyncAfterAddingAFile(t *testing.T) { + ctx := context.Background() + r := fstest.NewRun(t) + file1 := r.WriteBoth(ctx, "empty space", "-", t2) + file2 := r.WriteFile("potato", "------------------------------------------------------------", t3) + + r.CheckLocalItems(t, file1, file2) + r.CheckRemoteItems(t, file1) + + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + err := Sync(ctx, r.Fremote, r.Flocal, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + r.CheckLocalItems(t, file1, file2) + r.CheckRemoteItems(t, file1, file2) +} + +func TestSyncAfterChangingFilesSizeOnly(t *testing.T) { + ctx := context.Background() + r := fstest.NewRun(t) + file1 := r.WriteObject(ctx, "potato", "------------------------------------------------------------", t3) + file2 := r.WriteFile("potato", "smaller but same date", t3) + r.CheckRemoteItems(t, file1) + r.CheckLocalItems(t, file2) + + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + err := Sync(ctx, r.Fremote, r.Flocal, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + r.CheckLocalItems(t, file2) + r.CheckRemoteItems(t, file2) +} + +// Sync after changing a file's contents, changing modtime but length +// remaining the same +func TestSyncAfterChangingContentsOnly(t *testing.T) { + ctx := context.Background() + r := fstest.NewRun(t) + var file1 fstest.Item + if r.Fremote.Precision() == fs.ModTimeNotSupported { + t.Logf("ModTimeNotSupported so forcing file to be a different size") + file1 = r.WriteObject(ctx, "potato", "different size to make sure it syncs", t3) + } else { + file1 = r.WriteObject(ctx, "potato", "smaller but same date", t3) + } + file2 := r.WriteFile("potato", "SMALLER BUT SAME DATE", t2) + r.CheckRemoteItems(t, file1) + r.CheckLocalItems(t, file2) + + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + err := Sync(ctx, r.Fremote, r.Flocal, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + r.CheckLocalItems(t, file2) + r.CheckRemoteItems(t, file2) +} + +// Sync after removing a file and adding a file --dry-run +func TestSyncAfterRemovingAFileAndAddingAFileDryRun(t *testing.T) { + ctx := context.Background() + ctx, ci := fs.AddConfig(ctx) + r := fstest.NewRun(t) + file1 := r.WriteFile("potato2", "------------------------------------------------------------", t1) + file2 := r.WriteObject(ctx, "potato", "SMALLER BUT SAME DATE", t2) + file3 := r.WriteBoth(ctx, "empty space", "-", t2) + + ci.DryRun = true + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + err := Sync(ctx, r.Fremote, r.Flocal, false) + ci.DryRun = false + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + r.CheckLocalItems(t, file3, file1) + r.CheckRemoteItems(t, file3, file2) +} + +// Sync after removing a file and adding a file +func testSyncAfterRemovingAFileAndAddingAFile(ctx context.Context, t *testing.T) { + r := fstest.NewRun(t) + file1 := r.WriteFile("potato2", "------------------------------------------------------------", t1) + file2 := r.WriteObject(ctx, "potato", "SMALLER BUT SAME DATE", t2) + file3 := r.WriteBoth(ctx, "empty space", "-", t2) + r.CheckRemoteItems(t, file2, file3) + r.CheckLocalItems(t, file1, file3) + + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + err := Sync(ctx, r.Fremote, r.Flocal, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + r.CheckLocalItems(t, file1, file3) + r.CheckRemoteItems(t, file1, file3) +} + +func TestSyncAfterRemovingAFileAndAddingAFile(t *testing.T) { + testSyncAfterRemovingAFileAndAddingAFile(context.Background(), t) +} + +// Sync after removing a file and adding a file +func testSyncAfterRemovingAFileAndAddingAFileSubDir(ctx context.Context, t *testing.T) { + r := fstest.NewRun(t) + file1 := r.WriteFile("a/potato2", "------------------------------------------------------------", t1) + file2 := r.WriteObject(ctx, "b/potato", "SMALLER BUT SAME DATE", t2) + file3 := r.WriteBoth(ctx, "c/non empty space", "AhHa!", t2) + require.NoError(t, operations.Mkdir(ctx, r.Fremote, "d")) + require.NoError(t, operations.Mkdir(ctx, r.Fremote, "d/e")) + + r.CheckLocalListing( + t, + []fstest.Item{ + file1, + file3, + }, + []string{ + "a", + "c", + }, + ) + r.CheckRemoteListing( + t, + []fstest.Item{ + file2, + file3, + }, + []string{ + "b", + "c", + "d", + "d/e", + }, + ) + + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + err := Sync(ctx, r.Fremote, r.Flocal, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + r.CheckLocalListing( + t, + []fstest.Item{ + file1, + file3, + }, + []string{ + "a", + "c", + }, + ) + r.CheckRemoteListing( + t, + []fstest.Item{ + file1, + file3, + }, + []string{ + "a", + "c", + }, + ) +} + +func TestSyncAfterRemovingAFileAndAddingAFileSubDir(t *testing.T) { + testSyncAfterRemovingAFileAndAddingAFileSubDir(context.Background(), t) +} + +// Sync after removing a file and adding a file with IO Errors +func TestSyncAfterRemovingAFileAndAddingAFileSubDirWithErrors(t *testing.T) { + ctx := context.Background() + r := fstest.NewRun(t) + file1 := r.WriteFile("a/potato2", "------------------------------------------------------------", t1) + file2 := r.WriteObject(ctx, "b/potato", "SMALLER BUT SAME DATE", t2) + file3 := r.WriteBoth(ctx, "c/non empty space", "AhHa!", t2) + require.NoError(t, operations.Mkdir(ctx, r.Fremote, "d")) + + r.CheckLocalListing( + t, + []fstest.Item{ + file1, + file3, + }, + []string{ + "a", + "c", + }, + ) + r.CheckRemoteListing( + t, + []fstest.Item{ + file2, + file3, + }, + []string{ + "b", + "c", + "d", + }, + ) + + ctx = predictDstFromLogger(ctx) + accounting.GlobalStats().ResetCounters() + _ = fs.CountError(ctx, errors.New("boom")) + err := Sync(ctx, r.Fremote, r.Flocal, false) + assert.Equal(t, fs.ErrorNotDeleting, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + accounting.GlobalStats().ResetCounters() + + r.CheckLocalListing( + t, + []fstest.Item{ + file1, + file3, + }, + []string{ + "a", + "c", + }, + ) + r.CheckRemoteListing( + t, + []fstest.Item{ + file1, + file2, + file3, + }, + []string{ + "a", + "b", + "c", + "d", + }, + ) +} + +// Sync test delete after +func TestSyncDeleteAfter(t *testing.T) { + ctx := context.Background() + ci := fs.GetConfig(ctx) + // This is the default so we've checked this already + // check it is the default + require.Equal(t, ci.DeleteMode, fs.DeleteModeAfter, "Didn't default to --delete-after") +} + +// Sync test delete during +func TestSyncDeleteDuring(t *testing.T) { + ctx := context.Background() + ctx, ci := fs.AddConfig(ctx) + ci.DeleteMode = fs.DeleteModeDuring + + testSyncAfterRemovingAFileAndAddingAFile(ctx, t) +} + +// Sync test delete before +func TestSyncDeleteBefore(t *testing.T) { + ctx := context.Background() + ctx, ci := fs.AddConfig(ctx) + ci.DeleteMode = fs.DeleteModeBefore + + testSyncAfterRemovingAFileAndAddingAFile(ctx, t) +} + +// Copy test delete before - shouldn't delete anything +func TestCopyDeleteBefore(t *testing.T) { + ctx := context.Background() + ctx, ci := fs.AddConfig(ctx) + r := fstest.NewRun(t) + + ci.DeleteMode = fs.DeleteModeBefore + + file1 := r.WriteObject(ctx, "potato", "hopefully not deleted", t1) + file2 := r.WriteFile("potato2", "hopefully copied in", t1) + r.CheckRemoteItems(t, file1) + r.CheckLocalItems(t, file2) + + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + err := CopyDir(ctx, r.Fremote, r.Flocal, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + r.CheckRemoteItems(t, file1, file2) + r.CheckLocalItems(t, file2) +} + +// Test with exclude +func TestSyncWithExclude(t *testing.T) { + ctx := context.Background() + r := fstest.NewRun(t) + file1 := r.WriteBoth(ctx, "potato2", "------------------------------------------------------------", t1) + file2 := r.WriteBoth(ctx, "empty space", "-", t2) + file3 := r.WriteFile("enormous", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", t1) // 100 bytes + r.CheckRemoteItems(t, file1, file2) + r.CheckLocalItems(t, file1, file2, file3) + + fi, err := filter.NewFilter(nil) + require.NoError(t, err) + fi.Opt.MaxSize = 40 + ctx = filter.ReplaceConfig(ctx, fi) + + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + err = Sync(ctx, r.Fremote, r.Flocal, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + r.CheckRemoteItems(t, file2, file1) + + // Now sync the other way round and check enormous doesn't get + // deleted as it is excluded from the sync + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + err = Sync(ctx, r.Flocal, r.Fremote, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + r.CheckLocalItems(t, file2, file1, file3) +} + +// Test with exclude and delete excluded +func TestSyncWithExcludeAndDeleteExcluded(t *testing.T) { + ctx := context.Background() + r := fstest.NewRun(t) + file1 := r.WriteBoth(ctx, "potato2", "------------------------------------------------------------", t1) // 60 bytes + file2 := r.WriteBoth(ctx, "empty space", "-", t2) + file3 := r.WriteBoth(ctx, "enormous", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", t1) // 100 bytes + r.CheckRemoteItems(t, file1, file2, file3) + r.CheckLocalItems(t, file1, file2, file3) + + fi, err := filter.NewFilter(nil) + require.NoError(t, err) + fi.Opt.MaxSize = 40 + fi.Opt.DeleteExcluded = true + ctx = filter.ReplaceConfig(ctx, fi) + + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + err = Sync(ctx, r.Fremote, r.Flocal, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + r.CheckRemoteItems(t, file2) + + // Check sync the other way round to make sure enormous gets + // deleted even though it is excluded + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + err = Sync(ctx, r.Flocal, r.Fremote, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + r.CheckLocalItems(t, file2) +} + +// Test with UpdateOlder set +func TestSyncWithUpdateOlder(t *testing.T) { + ctx := context.Background() + ctx, ci := fs.AddConfig(ctx) + r := fstest.NewRun(t) + if fs.GetModifyWindow(ctx, r.Fremote) == fs.ModTimeNotSupported { + t.Skip("Can't run this test on fs which doesn't support mod time") + } + t2plus := t2.Add(time.Second / 2) + t2minus := t2.Add(time.Second / 2) + oneF := r.WriteFile("one", "one", t1) + twoF := r.WriteFile("two", "two", t3) + threeF := r.WriteFile("three", "three", t2) + fourF := r.WriteFile("four", "four", t2) + fiveF := r.WriteFile("five", "five", t2) + r.CheckLocalItems(t, oneF, twoF, threeF, fourF, fiveF) + oneO := r.WriteObject(ctx, "one", "ONE", t2) + twoO := r.WriteObject(ctx, "two", "TWO", t2) + threeO := r.WriteObject(ctx, "three", "THREE", t2plus) + fourO := r.WriteObject(ctx, "four", "FOURFOUR", t2minus) + r.CheckRemoteItems(t, oneO, twoO, threeO, fourO) + + ci.UpdateOlder = true + ci.ModifyWindow = fs.Duration(fs.ModTimeNotSupported) + + ctx = predictDstFromLogger(ctx) + err := Sync(ctx, r.Fremote, r.Flocal, false) + require.NoError(t, err) + r.CheckRemoteItems(t, oneO, twoF, threeO, fourF, fiveF) + // testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) // no modtime + + if r.Fremote.Hashes().Count() == 0 { + t.Logf("Skip test with --checksum as no hashes supported") + return + } + + // now enable checksum + ci.CheckSum = true + + err = Sync(ctx, r.Fremote, r.Flocal, false) + require.NoError(t, err) + r.CheckRemoteItems(t, oneO, twoF, threeF, fourF, fiveF) +} + +// Test with a max transfer duration +func testSyncWithMaxDuration(t *testing.T, cutoffMode fs.CutoffMode) { + ctx := context.Background() + ctx, ci := fs.AddConfig(ctx) + if *fstest.RemoteName != "" { + t.Skip("Skipping test on non local remote") + } + r := fstest.NewRun(t) + + maxDuration := fs.Duration(250 * time.Millisecond) + ci.MaxDuration = maxDuration + ci.CutoffMode = cutoffMode + ci.CheckFirst = true + ci.OrderBy = "size" + ci.Transfers = 1 + ci.Checkers = 1 + bytesPerSecond := 10 * 1024 + accounting.TokenBucket.SetBwLimit(fs.BwPair{Tx: fs.SizeSuffix(bytesPerSecond), Rx: fs.SizeSuffix(bytesPerSecond)}) + defer accounting.TokenBucket.SetBwLimit(fs.BwPair{Tx: -1, Rx: -1}) + + // write one small file which we expect to transfer and one big one which we don't + file1 := r.WriteFile("file1", string(make([]byte, 16)), t1) + file2 := r.WriteFile("file2", string(make([]byte, 50*1024)), t1) + r.CheckLocalItems(t, file1, file2) + r.CheckRemoteItems(t) + + if runtime.GOOS == "darwin" { + r.Flocal.Features().Disable("Copy") // macOS cloning is too fast for this test! + if r.Fremote.Features().IsLocal { + r.Fremote.Features().Disable("Copy") // macOS cloning is too fast for this test! + } + } + accounting.GlobalStats().ResetCounters() + // ctx = predictDstFromLogger(ctx) // not currently supported (but tests do pass for CutoffModeSoft) + startTime := time.Now() + err := Sync(ctx, r.Fremote, r.Flocal, false) + // testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + require.True(t, errors.Is(err, ErrorMaxDurationReached)) + + if cutoffMode == fs.CutoffModeHard { + r.CheckRemoteItems(t, file1) + assert.Equal(t, int64(1), accounting.GlobalStats().GetTransfers()) + } else { + r.CheckRemoteItems(t, file1, file2) + assert.Equal(t, int64(2), accounting.GlobalStats().GetTransfers()) + } + + elapsed := time.Since(startTime) + const maxTransferTime = 20 * time.Second + + what := fmt.Sprintf("expecting elapsed time %v between %v and %v", elapsed, maxDuration, maxTransferTime) + assert.True(t, elapsed >= time.Duration(maxDuration), what) + assert.True(t, elapsed < maxTransferTime, what) +} + +func TestSyncWithMaxDuration(t *testing.T) { + t.Run("Hard", func(t *testing.T) { + testSyncWithMaxDuration(t, fs.CutoffModeHard) + }) + t.Run("Soft", func(t *testing.T) { + testSyncWithMaxDuration(t, fs.CutoffModeSoft) + }) +} + +// Test with TrackRenames set +func TestSyncWithTrackRenames(t *testing.T) { + ctx := context.Background() + ctx, ci := fs.AddConfig(ctx) + r := fstest.NewRun(t) + + ci.TrackRenames = true + defer func() { + ci.TrackRenames = false + }() + + haveHash := r.Fremote.Hashes().Overlap(r.Flocal.Hashes()).GetOne() != hash.None + canTrackRenames := haveHash && operations.CanServerSideMove(r.Fremote) + t.Logf("Can track renames: %v", canTrackRenames) + + f1 := r.WriteFile("potato", "Potato Content", t1) + f2 := r.WriteFile("yam", "Yam Content", t2) + + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + require.NoError(t, Sync(ctx, r.Fremote, r.Flocal, false)) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + r.CheckRemoteItems(t, f1, f2) + r.CheckLocalItems(t, f1, f2) + + // Now rename locally. + f2 = r.RenameFile(f2, "yaml") + + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + require.NoError(t, Sync(ctx, r.Fremote, r.Flocal, false)) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + r.CheckRemoteItems(t, f1, f2) + + // Check we renamed something if we should have + if canTrackRenames { + renames := accounting.GlobalStats().Renames(0) + assert.Equal(t, canTrackRenames, renames != 0, fmt.Sprintf("canTrackRenames=%v, renames=%d", canTrackRenames, renames)) + } +} + +func TestParseRenamesStrategyModtime(t *testing.T) { + for _, test := range []struct { + in string + want trackRenamesStrategy + wantErr bool + }{ + {"", 0, false}, + {"modtime", trackRenamesStrategyModtime, false}, + {"hash", trackRenamesStrategyHash, false}, + {"size", 0, false}, + {"modtime,hash", trackRenamesStrategyModtime | trackRenamesStrategyHash, false}, + {"hash,modtime,size", trackRenamesStrategyModtime | trackRenamesStrategyHash, false}, + {"size,boom", 0, true}, + } { + got, err := parseTrackRenamesStrategy(test.in) + assert.Equal(t, test.want, got, test.in) + assert.Equal(t, test.wantErr, err != nil, test.in) + } +} + +func TestRenamesStrategyModtime(t *testing.T) { + both := trackRenamesStrategyHash | trackRenamesStrategyModtime + hash := trackRenamesStrategyHash + modTime := trackRenamesStrategyModtime + + assert.True(t, both.hash()) + assert.True(t, both.modTime()) + assert.True(t, hash.hash()) + assert.False(t, hash.modTime()) + assert.False(t, modTime.hash()) + assert.True(t, modTime.modTime()) +} + +func TestSyncWithTrackRenamesStrategyModtime(t *testing.T) { + ctx := context.Background() + ctx, ci := fs.AddConfig(ctx) + r := fstest.NewRun(t) + + ci.TrackRenames = true + ci.TrackRenamesStrategy = "modtime" + + canTrackRenames := operations.CanServerSideMove(r.Fremote) && r.Fremote.Precision() != fs.ModTimeNotSupported + t.Logf("Can track renames: %v", canTrackRenames) + + f1 := r.WriteFile("potato", "Potato Content", t1) + f2 := r.WriteFile("yam", "Yam Content", t2) + + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + require.NoError(t, Sync(ctx, r.Fremote, r.Flocal, false)) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + r.CheckRemoteItems(t, f1, f2) + r.CheckLocalItems(t, f1, f2) + + // Now rename locally. + f2 = r.RenameFile(f2, "yaml") + + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + require.NoError(t, Sync(ctx, r.Fremote, r.Flocal, false)) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + r.CheckRemoteItems(t, f1, f2) + + // Check we renamed something if we should have + if canTrackRenames { + renames := accounting.GlobalStats().Renames(0) + assert.Equal(t, canTrackRenames, renames != 0, fmt.Sprintf("canTrackRenames=%v, renames=%d", canTrackRenames, renames)) + } +} + +func TestSyncWithTrackRenamesStrategyLeaf(t *testing.T) { + ctx := context.Background() + ctx, ci := fs.AddConfig(ctx) + r := fstest.NewRun(t) + + ci.TrackRenames = true + ci.TrackRenamesStrategy = "leaf" + + canTrackRenames := operations.CanServerSideMove(r.Fremote) && r.Fremote.Precision() != fs.ModTimeNotSupported + t.Logf("Can track renames: %v", canTrackRenames) + + f1 := r.WriteFile("potato", "Potato Content", t1) + f2 := r.WriteFile("sub/yam", "Yam Content", t2) + + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + require.NoError(t, Sync(ctx, r.Fremote, r.Flocal, false)) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + r.CheckRemoteItems(t, f1, f2) + r.CheckLocalItems(t, f1, f2) + + // Now rename locally. + f2 = r.RenameFile(f2, "yam") + + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + require.NoError(t, Sync(ctx, r.Fremote, r.Flocal, false)) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + r.CheckRemoteItems(t, f1, f2) + + // Check we renamed something if we should have + if canTrackRenames { + renames := accounting.GlobalStats().Renames(0) + assert.Equal(t, canTrackRenames, renames != 0, fmt.Sprintf("canTrackRenames=%v, renames=%d", canTrackRenames, renames)) + } +} + +func toyFileTransfers(r *fstest.Run) int64 { + remote := r.Fremote.Name() + transfers := 1 + if strings.HasPrefix(remote, "TestChunker") && strings.HasSuffix(remote, "S3") { + transfers++ // Extra Copy because S3 emulates Move as Copy+Delete. + } + return int64(transfers) +} + +// Test a server-side move if possible, or the backup path if not +func testServerSideMove(ctx context.Context, t *testing.T, r *fstest.Run, withFilter, testDeleteEmptyDirs bool) { + FremoteMove, _, finaliseMove, err := fstest.RandomRemote() + require.NoError(t, err) + defer finaliseMove() + + file1 := r.WriteBoth(ctx, "potato2", "------------------------------------------------------------", t1) + file2 := r.WriteBoth(ctx, "empty space", "-", t2) + file3u := r.WriteBoth(ctx, "potato3", "------------------------------------------------------------ UPDATED", t2) + + if testDeleteEmptyDirs { + err := operations.Mkdir(ctx, r.Fremote, "tomatoDir") + require.NoError(t, err) + } + + r.CheckRemoteItems(t, file2, file1, file3u) + + t.Logf("Server side move (if possible) %v -> %v", r.Fremote, FremoteMove) + + // Write just one file in the new remote + r.WriteObjectTo(ctx, FremoteMove, "empty space", "-", t2, false) + file3 := r.WriteObjectTo(ctx, FremoteMove, "potato3", "------------------------------------------------------------", t1, false) + fstest.CheckItems(t, FremoteMove, file2, file3) + + // Do server-side move + accounting.GlobalStats().ResetCounters() + // ctx = predictDstFromLogger(ctx) // not currently supported -- doesn't list all contents of dir. + err = MoveDir(ctx, FremoteMove, r.Fremote, testDeleteEmptyDirs, false) + require.NoError(t, err) + // testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + if withFilter { + r.CheckRemoteItems(t, file2) + } else { + r.CheckRemoteItems(t) + } + + if testDeleteEmptyDirs { + r.CheckRemoteListing(t, nil, []string{}) + } + + fstest.CheckItems(t, FremoteMove, file2, file1, file3u) + + // Create a new empty remote for stuff to be moved into + FremoteMove2, _, finaliseMove2, err := fstest.RandomRemote() + require.NoError(t, err) + defer finaliseMove2() + + if testDeleteEmptyDirs { + err := operations.Mkdir(ctx, FremoteMove, "tomatoDir") + require.NoError(t, err) + } + + // Move it back to a new empty remote, dst does not exist this time + accounting.GlobalStats().ResetCounters() + // ctx = predictDstFromLogger(ctx) + err = MoveDir(ctx, FremoteMove2, FremoteMove, testDeleteEmptyDirs, false) + require.NoError(t, err) + // testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + if withFilter { + fstest.CheckItems(t, FremoteMove2, file1, file3u) + fstest.CheckItems(t, FremoteMove, file2) + } else { + fstest.CheckItems(t, FremoteMove2, file2, file1, file3u) + fstest.CheckItems(t, FremoteMove) + } + + if testDeleteEmptyDirs { + fstest.CheckListingWithPrecision(t, FremoteMove, nil, []string{}, fs.GetModifyWindow(ctx, r.Fremote)) + } +} + +// Test MoveDir on Local +func TestServerSideMoveLocal(t *testing.T) { + ctx := context.Background() + r := fstest.NewRun(t) + f1 := r.WriteFile("dir1/file1.txt", "hello", t1) + f2 := r.WriteFile("dir2/file2.txt", "hello again", t2) + r.CheckLocalItems(t, f1, f2) + + dir1, err := fs.NewFs(ctx, r.Flocal.Root()+"/dir1") + require.NoError(t, err) + dir2, err := fs.NewFs(ctx, r.Flocal.Root()+"/dir2") + require.NoError(t, err) + err = MoveDir(ctx, dir2, dir1, false, false) + require.NoError(t, err) +} + +// Test move +func TestMoveWithDeleteEmptySrcDirs(t *testing.T) { + ctx := context.Background() + r := fstest.NewRun(t) + file1 := r.WriteFile("sub dir/hello world", "hello world", t1) + file2 := r.WriteFile("nested/sub dir/file", "nested", t1) + r.Mkdir(ctx, r.Fremote) + + // run move with --delete-empty-src-dirs + ctx = predictDstFromLogger(ctx) + err := MoveDir(ctx, r.Fremote, r.Flocal, true, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + r.CheckLocalListing( + t, + nil, + []string{}, + ) + r.CheckRemoteItems(t, file1, file2) +} + +func TestMoveWithoutDeleteEmptySrcDirs(t *testing.T) { + ctx := context.Background() + r := fstest.NewRun(t) + file1 := r.WriteFile("sub dir/hello world", "hello world", t1) + file2 := r.WriteFile("nested/sub dir/file", "nested", t1) + r.Mkdir(ctx, r.Fremote) + + ctx = predictDstFromLogger(ctx) + err := MoveDir(ctx, r.Fremote, r.Flocal, false, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + r.CheckLocalListing( + t, + nil, + []string{ + "sub dir", + "nested", + "nested/sub dir", + }, + ) + r.CheckRemoteItems(t, file1, file2) +} + +func TestMoveWithIgnoreExisting(t *testing.T) { + ctx := context.Background() + ctx, ci := fs.AddConfig(ctx) + r := fstest.NewRun(t) + file1 := r.WriteFile("existing", "potato", t1) + file2 := r.WriteFile("existing-b", "tomato", t1) + + ci.IgnoreExisting = true + + accounting.GlobalStats().ResetCounters() + // ctx = predictDstFromLogger(ctx) + err := MoveDir(ctx, r.Fremote, r.Flocal, false, false) + require.NoError(t, err) + // testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + r.CheckLocalListing( + t, + []fstest.Item{}, + []string{}, + ) + r.CheckRemoteListing( + t, + []fstest.Item{ + file1, + file2, + }, + []string{}, + ) + + // Recreate first file with modified content + file1b := r.WriteFile("existing", "newpotatoes", t2) + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + err = MoveDir(ctx, r.Fremote, r.Flocal, false, false) + require.NoError(t, err) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + // Source items should still exist in modified state + r.CheckLocalListing( + t, + []fstest.Item{ + file1b, + }, + []string{}, + ) + // Dest items should not have changed + r.CheckRemoteListing( + t, + []fstest.Item{ + file1, + file2, + }, + []string{}, + ) +} + +// Test a server-side move if possible, or the backup path if not +func TestServerSideMove(t *testing.T) { + ctx := context.Background() + r := fstest.NewRun(t) + testServerSideMove(ctx, t, r, false, false) +} + +// Test a server-side move if possible, or the backup path if not +func TestServerSideMoveWithFilter(t *testing.T) { + ctx := context.Background() + r := fstest.NewRun(t) + + fi, err := filter.NewFilter(nil) + require.NoError(t, err) + fi.Opt.MinSize = 40 + ctx = filter.ReplaceConfig(ctx, fi) + + testServerSideMove(ctx, t, r, true, false) +} + +// Test a server-side move if possible +func TestServerSideMoveDeleteEmptySourceDirs(t *testing.T) { + ctx := context.Background() + r := fstest.NewRun(t) + testServerSideMove(ctx, t, r, false, true) +} + +// Test a server-side move with overlap +func TestServerSideMoveOverlap(t *testing.T) { + ctx := context.Background() + r := fstest.NewRun(t) + + if r.Fremote.Features().DirMove != nil { + t.Skip("Skipping test as remote supports DirMove") + } + + subRemoteName := r.FremoteName + "/rclone-move-test" + FremoteMove, err := fs.NewFs(ctx, subRemoteName) + require.NoError(t, err) + + file1 := r.WriteObject(ctx, "potato2", "------------------------------------------------------------", t1) + r.CheckRemoteItems(t, file1) + + // Subdir move with no filters should return ErrorCantMoveOverlapping + // ctx = predictDstFromLogger(ctx) + err = MoveDir(ctx, FremoteMove, r.Fremote, false, false) + assert.EqualError(t, err, fs.ErrorOverlapping.Error()) + // testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + // Now try with a filter which should also fail with ErrorCantMoveOverlapping + fi, err := filter.NewFilter(nil) + require.NoError(t, err) + fi.Opt.MinSize = 40 + ctx = filter.ReplaceConfig(ctx, fi) + + // ctx = predictDstFromLogger(ctx) + err = MoveDir(ctx, FremoteMove, r.Fremote, false, false) + assert.EqualError(t, err, fs.ErrorOverlapping.Error()) + // testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) +} + +// Test a sync with overlap +func TestSyncOverlap(t *testing.T) { + ctx := context.Background() + r := fstest.NewRun(t) + + subRemoteName := r.FremoteName + "/rclone-sync-test" + FremoteSync, err := fs.NewFs(ctx, subRemoteName) + require.NoError(t, err) + + checkErr := func(err error) { + require.Error(t, err) + assert.True(t, fserrors.IsFatalError(err)) + assert.Equal(t, fs.ErrorOverlapping.Error(), err.Error()) + } + + ctx = predictDstFromLogger(ctx) + checkErr(Sync(ctx, FremoteSync, r.Fremote, false)) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + ctx = predictDstFromLogger(ctx) + checkErr(Sync(ctx, r.Fremote, FremoteSync, false)) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + ctx = predictDstFromLogger(ctx) + checkErr(Sync(ctx, r.Fremote, r.Fremote, false)) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + ctx = predictDstFromLogger(ctx) + checkErr(Sync(ctx, FremoteSync, FremoteSync, false)) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) +} + +// Test a sync with filtered overlap +func TestSyncOverlapWithFilter(t *testing.T) { + ctx := context.Background() + r := fstest.NewRun(t) + + fi, err := filter.NewFilter(nil) + require.NoError(t, err) + require.NoError(t, fi.Add(false, "/rclone-sync-test/")) + require.NoError(t, fi.Add(false, "*/layer2/")) + fi.Opt.ExcludeFile = []string{".ignore"} + filterCtx := filter.ReplaceConfig(ctx, fi) + + subRemoteName := r.FremoteName + "/rclone-sync-test" + FremoteSync, err := fs.NewFs(ctx, subRemoteName) + require.NoError(t, FremoteSync.Mkdir(ctx, "")) + require.NoError(t, err) + + subRemoteName2 := r.FremoteName + "/rclone-sync-test-include/layer2" + FremoteSync2, err := fs.NewFs(ctx, subRemoteName2) + require.NoError(t, FremoteSync2.Mkdir(ctx, "")) + require.NoError(t, err) + + subRemoteName3 := r.FremoteName + "/rclone-sync-test-ignore-file" + FremoteSync3, err := fs.NewFs(ctx, subRemoteName3) + require.NoError(t, FremoteSync3.Mkdir(ctx, "")) + require.NoError(t, err) + r.WriteObject(context.Background(), "rclone-sync-test-ignore-file/.ignore", "-", t1) + + checkErr := func(err error) { + require.Error(t, err) + assert.True(t, fserrors.IsFatalError(err)) + assert.Equal(t, fs.ErrorOverlapping.Error(), err.Error()) + accounting.GlobalStats().ResetCounters() + } + + checkNoErr := func(err error) { + require.NoError(t, err) + } + + accounting.GlobalStats().ResetCounters() + filterCtx = predictDstFromLogger(filterCtx) + checkNoErr(Sync(filterCtx, FremoteSync, r.Fremote, false)) + checkErr(Sync(ctx, FremoteSync, r.Fremote, false)) + checkNoErr(Sync(filterCtx, r.Fremote, FremoteSync, false)) + testLoggerVsLsf(filterCtx, r.Fremote, r.Flocal, operations.GetLoggerOpt(filterCtx).JSON, t) + filterCtx = predictDstFromLogger(filterCtx) + checkErr(Sync(ctx, r.Fremote, FremoteSync, false)) + testLoggerVsLsf(filterCtx, r.Fremote, r.Flocal, operations.GetLoggerOpt(filterCtx).JSON, t) + filterCtx = predictDstFromLogger(filterCtx) + checkErr(Sync(filterCtx, r.Fremote, r.Fremote, false)) + testLoggerVsLsf(filterCtx, r.Fremote, r.Flocal, operations.GetLoggerOpt(filterCtx).JSON, t) + filterCtx = predictDstFromLogger(filterCtx) + checkErr(Sync(ctx, r.Fremote, r.Fremote, false)) + testLoggerVsLsf(filterCtx, r.Fremote, r.Flocal, operations.GetLoggerOpt(filterCtx).JSON, t) + filterCtx = predictDstFromLogger(filterCtx) + checkErr(Sync(filterCtx, FremoteSync, FremoteSync, false)) + testLoggerVsLsf(filterCtx, r.Fremote, r.Flocal, operations.GetLoggerOpt(filterCtx).JSON, t) + filterCtx = predictDstFromLogger(filterCtx) + checkErr(Sync(ctx, FremoteSync, FremoteSync, false)) + testLoggerVsLsf(filterCtx, r.Fremote, r.Flocal, operations.GetLoggerOpt(filterCtx).JSON, t) + filterCtx = predictDstFromLogger(filterCtx) + + checkNoErr(Sync(filterCtx, FremoteSync2, r.Fremote, false)) + testLoggerVsLsf(filterCtx, r.Fremote, r.Flocal, operations.GetLoggerOpt(filterCtx).JSON, t) + filterCtx = predictDstFromLogger(filterCtx) + checkErr(Sync(ctx, FremoteSync2, r.Fremote, false)) + testLoggerVsLsf(filterCtx, r.Fremote, r.Flocal, operations.GetLoggerOpt(filterCtx).JSON, t) + filterCtx = predictDstFromLogger(filterCtx) + checkNoErr(Sync(filterCtx, r.Fremote, FremoteSync2, false)) + testLoggerVsLsf(filterCtx, r.Fremote, r.Flocal, operations.GetLoggerOpt(filterCtx).JSON, t) + filterCtx = predictDstFromLogger(filterCtx) + checkErr(Sync(ctx, r.Fremote, FremoteSync2, false)) + testLoggerVsLsf(filterCtx, r.Fremote, r.Flocal, operations.GetLoggerOpt(filterCtx).JSON, t) + filterCtx = predictDstFromLogger(filterCtx) + checkErr(Sync(filterCtx, FremoteSync2, FremoteSync2, false)) + testLoggerVsLsf(filterCtx, r.Fremote, r.Flocal, operations.GetLoggerOpt(filterCtx).JSON, t) + filterCtx = predictDstFromLogger(filterCtx) + checkErr(Sync(ctx, FremoteSync2, FremoteSync2, false)) + testLoggerVsLsf(filterCtx, r.Fremote, r.Flocal, operations.GetLoggerOpt(filterCtx).JSON, t) + filterCtx = predictDstFromLogger(filterCtx) + + checkNoErr(Sync(filterCtx, FremoteSync3, r.Fremote, false)) + testLoggerVsLsf(filterCtx, r.Fremote, r.Flocal, operations.GetLoggerOpt(filterCtx).JSON, t) + filterCtx = predictDstFromLogger(filterCtx) + checkErr(Sync(ctx, FremoteSync3, r.Fremote, false)) + testLoggerVsLsf(filterCtx, r.Fremote, r.Flocal, operations.GetLoggerOpt(filterCtx).JSON, t) + filterCtx = predictDstFromLogger(filterCtx) + // Destination is excluded so this test makes no sense + // checkNoErr(Sync(filterCtx, r.Fremote, FremoteSync3, false)) + checkErr(Sync(ctx, r.Fremote, FremoteSync3, false)) + testLoggerVsLsf(filterCtx, r.Fremote, r.Flocal, operations.GetLoggerOpt(filterCtx).JSON, t) + filterCtx = predictDstFromLogger(filterCtx) + checkErr(Sync(filterCtx, FremoteSync3, FremoteSync3, false)) + testLoggerVsLsf(filterCtx, r.Fremote, r.Flocal, operations.GetLoggerOpt(filterCtx).JSON, t) + filterCtx = predictDstFromLogger(filterCtx) + checkErr(Sync(ctx, FremoteSync3, FremoteSync3, false)) + testLoggerVsLsf(filterCtx, r.Fremote, r.Flocal, operations.GetLoggerOpt(filterCtx).JSON, t) +} + +// Test with CompareDest set +func TestSyncCompareDest(t *testing.T) { + ctx := context.Background() + ctx, ci := fs.AddConfig(ctx) + r := fstest.NewRun(t) + + ci.CompareDest = []string{r.FremoteName + "/CompareDest"} + + fdst, err := fs.NewFs(ctx, r.FremoteName+"/dst") + require.NoError(t, err) + + // check empty dest, empty compare + file1 := r.WriteFile("one", "one", t1) + r.CheckLocalItems(t, file1) + + accounting.GlobalStats().ResetCounters() + // ctx = predictDstFromLogger(ctx) // not currently supported due to duplicate equal() checks + err = Sync(ctx, fdst, r.Flocal, false) + // testLoggerVsLsf(ctx, fdst, operations.GetLoggerOpt(ctx).JSON, t) + require.NoError(t, err) + + file1dst := file1 + file1dst.Path = "dst/one" + + r.CheckRemoteItems(t, file1dst) + + // check old dest, empty compare + file1b := r.WriteFile("one", "onet2", t2) + r.CheckRemoteItems(t, file1dst) + r.CheckLocalItems(t, file1b) + + accounting.GlobalStats().ResetCounters() + // ctx = predictDstFromLogger(ctx) + err = Sync(ctx, fdst, r.Flocal, false) + // testLoggerVsLsf(ctx, fdst, operations.GetLoggerOpt(ctx).JSON, t) + require.NoError(t, err) + + file1bdst := file1b + file1bdst.Path = "dst/one" + + r.CheckRemoteItems(t, file1bdst) + + // check old dest, new compare + file3 := r.WriteObject(ctx, "dst/one", "one", t1) + file2 := r.WriteObject(ctx, "CompareDest/one", "onet2", t2) + file1c := r.WriteFile("one", "onet2", t2) + r.CheckRemoteItems(t, file2, file3) + r.CheckLocalItems(t, file1c) + + accounting.GlobalStats().ResetCounters() + // ctx = predictDstFromLogger(ctx) + err = Sync(ctx, fdst, r.Flocal, false) + // testLoggerVsLsf(ctx, fdst, operations.GetLoggerOpt(ctx).JSON, t) + require.NoError(t, err) + + r.CheckRemoteItems(t, file2, file3) + + // check empty dest, new compare + file4 := r.WriteObject(ctx, "CompareDest/two", "two", t2) + file5 := r.WriteFile("two", "two", t2) + r.CheckRemoteItems(t, file2, file3, file4) + r.CheckLocalItems(t, file1c, file5) + + accounting.GlobalStats().ResetCounters() + // ctx = predictDstFromLogger(ctx) + err = Sync(ctx, fdst, r.Flocal, false) + // testLoggerVsLsf(ctx, fdst, operations.GetLoggerOpt(ctx).JSON, t) + require.NoError(t, err) + + r.CheckRemoteItems(t, file2, file3, file4) + + // check new dest, new compare + accounting.GlobalStats().ResetCounters() + // ctx = predictDstFromLogger(ctx) + err = Sync(ctx, fdst, r.Flocal, false) + // testLoggerVsLsf(ctx, fdst, operations.GetLoggerOpt(ctx).JSON, t) + require.NoError(t, err) + + r.CheckRemoteItems(t, file2, file3, file4) + + // Work out if we actually have hashes for uploaded files + haveHash := false + if ht := fdst.Hashes().GetOne(); ht != hash.None { + file2obj, err := fdst.NewObject(ctx, "one") + if err == nil { + file2objHash, err := file2obj.Hash(ctx, ht) + if err == nil { + haveHash = file2objHash != "" + } + } + } + + // check new dest, new compare, src timestamp differs + // + // we only check this if we the file we uploaded previously + // actually has a hash otherwise the differing timestamp is + // always copied. + if haveHash { + file5b := r.WriteFile("two", "two", t3) + r.CheckLocalItems(t, file1c, file5b) + + accounting.GlobalStats().ResetCounters() + // ctx = predictDstFromLogger(ctx) + err = Sync(ctx, fdst, r.Flocal, false) + // testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + require.NoError(t, err) + + r.CheckRemoteItems(t, file2, file3, file4) + } else { + t.Log("No hash on uploaded file so skipping compare timestamp test") + } + + // check empty dest, old compare + file5c := r.WriteFile("two", "twot3", t3) + r.CheckRemoteItems(t, file2, file3, file4) + r.CheckLocalItems(t, file1c, file5c) + + accounting.GlobalStats().ResetCounters() + // ctx = predictDstFromLogger(ctx) + err = Sync(ctx, fdst, r.Flocal, false) + // testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + require.NoError(t, err) + + file5cdst := file5c + file5cdst.Path = "dst/two" + + r.CheckRemoteItems(t, file2, file3, file4, file5cdst) +} + +// Test with multiple CompareDest +func TestSyncMultipleCompareDest(t *testing.T) { + ctx := context.Background() + ctx, ci := fs.AddConfig(ctx) + r := fstest.NewRun(t) + precision := fs.GetModifyWindow(ctx, r.Fremote, r.Flocal) + + ci.CompareDest = []string{r.FremoteName + "/pre-dest1", r.FremoteName + "/pre-dest2"} + + // check empty dest, new compare + fsrc1 := r.WriteFile("1", "1", t1) + fsrc2 := r.WriteFile("2", "2", t1) + fsrc3 := r.WriteFile("3", "3", t1) + r.CheckLocalItems(t, fsrc1, fsrc2, fsrc3) + + fdest1 := r.WriteObject(ctx, "pre-dest1/1", "1", t1) + fdest2 := r.WriteObject(ctx, "pre-dest2/2", "2", t1) + r.CheckRemoteItems(t, fdest1, fdest2) + + accounting.GlobalStats().ResetCounters() + fdst, err := fs.NewFs(ctx, r.FremoteName+"/dest") + require.NoError(t, err) + // ctx = predictDstFromLogger(ctx) + require.NoError(t, Sync(ctx, fdst, r.Flocal, false)) + // testLoggerVsLsf(ctx, fdst, operations.GetLoggerOpt(ctx).JSON, t) + + fdest3 := fsrc3 + fdest3.Path = "dest/3" + + fstest.CheckItemsWithPrecision(t, fdst, precision, fsrc3) + r.CheckRemoteItems(t, fdest1, fdest2, fdest3) +} + +// Test with CopyDest set +func TestSyncCopyDest(t *testing.T) { + ctx := context.Background() + ctx, ci := fs.AddConfig(ctx) + r := fstest.NewRun(t) + + if r.Fremote.Features().Copy == nil { + t.Skip("Skipping test as remote does not support server-side copy") + } + + ci.CopyDest = []string{r.FremoteName + "/CopyDest"} + + fdst, err := fs.NewFs(ctx, r.FremoteName+"/dst") + require.NoError(t, err) + + // check empty dest, empty copy + file1 := r.WriteFile("one", "one", t1) + r.CheckLocalItems(t, file1) + + accounting.GlobalStats().ResetCounters() + // ctx = predictDstFromLogger(ctx) + err = Sync(ctx, fdst, r.Flocal, false) + // testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) // not currently supported + require.NoError(t, err) + + file1dst := file1 + file1dst.Path = "dst/one" + + r.CheckRemoteItems(t, file1dst) + + // check old dest, empty copy + file1b := r.WriteFile("one", "onet2", t2) + r.CheckRemoteItems(t, file1dst) + r.CheckLocalItems(t, file1b) + + accounting.GlobalStats().ResetCounters() + // ctx = predictDstFromLogger(ctx) + err = Sync(ctx, fdst, r.Flocal, false) + // testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + require.NoError(t, err) + + file1bdst := file1b + file1bdst.Path = "dst/one" + + r.CheckRemoteItems(t, file1bdst) + + // check old dest, new copy, backup-dir + + ci.BackupDir = r.FremoteName + "/BackupDir" + + file3 := r.WriteObject(ctx, "dst/one", "one", t1) + file2 := r.WriteObject(ctx, "CopyDest/one", "onet2", t2) + file1c := r.WriteFile("one", "onet2", t2) + r.CheckRemoteItems(t, file2, file3) + r.CheckLocalItems(t, file1c) + + accounting.GlobalStats().ResetCounters() + // ctx = predictDstFromLogger(ctx) + err = Sync(ctx, fdst, r.Flocal, false) + // testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + require.NoError(t, err) + + file2dst := file2 + file2dst.Path = "dst/one" + file3.Path = "BackupDir/one" + + r.CheckRemoteItems(t, file2, file2dst, file3) + ci.BackupDir = "" + + // check empty dest, new copy + file4 := r.WriteObject(ctx, "CopyDest/two", "two", t2) + file5 := r.WriteFile("two", "two", t2) + r.CheckRemoteItems(t, file2, file2dst, file3, file4) + r.CheckLocalItems(t, file1c, file5) + + accounting.GlobalStats().ResetCounters() + // ctx = predictDstFromLogger(ctx) + // testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + err = Sync(ctx, fdst, r.Flocal, false) + require.NoError(t, err) + + file4dst := file4 + file4dst.Path = "dst/two" + + r.CheckRemoteItems(t, file2, file2dst, file3, file4, file4dst) + + // check new dest, new copy + accounting.GlobalStats().ResetCounters() + // ctx = predictDstFromLogger(ctx) + err = Sync(ctx, fdst, r.Flocal, false) + // testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + require.NoError(t, err) + + r.CheckRemoteItems(t, file2, file2dst, file3, file4, file4dst) + + // check empty dest, old copy + file6 := r.WriteObject(ctx, "CopyDest/three", "three", t2) + file7 := r.WriteFile("three", "threet3", t3) + r.CheckRemoteItems(t, file2, file2dst, file3, file4, file4dst, file6) + r.CheckLocalItems(t, file1c, file5, file7) + + accounting.GlobalStats().ResetCounters() + // ctx = predictDstFromLogger(ctx) + err = Sync(ctx, fdst, r.Flocal, false) + // testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + require.NoError(t, err) + + file7dst := file7 + file7dst.Path = "dst/three" + + r.CheckRemoteItems(t, file2, file2dst, file3, file4, file4dst, file6, file7dst) +} + +// Test with BackupDir set +func testSyncBackupDir(t *testing.T, backupDir string, suffix string, suffixKeepExtension bool) { + ctx := context.Background() + ctx, ci := fs.AddConfig(ctx) + r := fstest.NewRun(t) + + if !operations.CanServerSideMove(r.Fremote) { + t.Skip("Skipping test as remote does not support server-side move") + } + r.Mkdir(ctx, r.Fremote) + + if backupDir != "" { + ci.BackupDir = r.FremoteName + "/" + backupDir + backupDir += "/" + } else { + ci.BackupDir = "" + backupDir = "dst/" + // Exclude the suffix from the sync otherwise the sync + // deletes the old backup files + flt, err := filter.NewFilter(nil) + require.NoError(t, err) + require.NoError(t, flt.AddRule("- *"+suffix)) + // Change the active filter + ctx = filter.ReplaceConfig(ctx, flt) + } + ci.Suffix = suffix + ci.SuffixKeepExtension = suffixKeepExtension + + // Make the setup so we have one, two, three in the dest + // and one (different), two (same) in the source + file1 := r.WriteObject(ctx, "dst/one", "one", t1) + file2 := r.WriteObject(ctx, "dst/two", "two", t1) + file3 := r.WriteObject(ctx, "dst/three.txt", "three", t1) + file2a := r.WriteFile("two", "two", t1) + file1a := r.WriteFile("one", "oneA", t2) + + r.CheckRemoteItems(t, file1, file2, file3) + r.CheckLocalItems(t, file1a, file2a) + + fdst, err := fs.NewFs(ctx, r.FremoteName+"/dst") + require.NoError(t, err) + + accounting.GlobalStats().ResetCounters() + err = Sync(ctx, fdst, r.Flocal, false) + require.NoError(t, err) + + // one should be moved to the backup dir and the new one installed + file1.Path = backupDir + "one" + suffix + file1a.Path = "dst/one" + // two should be unchanged + // three should be moved to the backup dir + if suffixKeepExtension { + file3.Path = backupDir + "three" + suffix + ".txt" + } else { + file3.Path = backupDir + "three.txt" + suffix + } + + r.CheckRemoteItems(t, file1, file2, file3, file1a) + + // Now check what happens if we do it again + // Restore a different three and update one in the source + file3a := r.WriteObject(ctx, "dst/three.txt", "threeA", t2) + file1b := r.WriteFile("one", "oneBB", t3) + r.CheckRemoteItems(t, file1, file2, file3, file1a, file3a) + + // This should delete three and overwrite one again, checking + // the files got overwritten correctly in backup-dir + accounting.GlobalStats().ResetCounters() + err = Sync(ctx, fdst, r.Flocal, false) + require.NoError(t, err) + + // one should be moved to the backup dir and the new one installed + file1a.Path = backupDir + "one" + suffix + file1b.Path = "dst/one" + // two should be unchanged + // three should be moved to the backup dir + if suffixKeepExtension { + file3a.Path = backupDir + "three" + suffix + ".txt" + } else { + file3a.Path = backupDir + "three.txt" + suffix + } + + r.CheckRemoteItems(t, file1b, file2, file3a, file1a) +} + +func TestSyncBackupDir(t *testing.T) { + testSyncBackupDir(t, "backup", "", false) +} + +func TestSyncBackupDirWithSuffix(t *testing.T) { + testSyncBackupDir(t, "backup", ".bak", false) +} + +func TestSyncBackupDirWithSuffixKeepExtension(t *testing.T) { + testSyncBackupDir(t, "backup", "-2019-01-01", true) +} + +func TestSyncBackupDirSuffixOnly(t *testing.T) { + testSyncBackupDir(t, "", ".bak", false) +} + +// Test with Suffix set +func testSyncSuffix(t *testing.T, suffix string, suffixKeepExtension bool) { + ctx := context.Background() + ctx, ci := fs.AddConfig(ctx) + r := fstest.NewRun(t) + + if !operations.CanServerSideMove(r.Fremote) { + t.Skip("Skipping test as remote does not support server-side move") + } + r.Mkdir(ctx, r.Fremote) + + ci.Suffix = suffix + ci.SuffixKeepExtension = suffixKeepExtension + + // Make the setup so we have one, two, three in the dest + // and one (different), two (same) in the source + file1 := r.WriteObject(ctx, "dst/one", "one", t1) + file2 := r.WriteObject(ctx, "dst/two", "two", t1) + file3 := r.WriteObject(ctx, "dst/three.txt", "three", t1) + file2a := r.WriteFile("two", "two", t1) + file1a := r.WriteFile("one", "oneA", t2) + file3a := r.WriteFile("three.txt", "threeA", t1) + + r.CheckRemoteItems(t, file1, file2, file3) + r.CheckLocalItems(t, file1a, file2a, file3a) + + fdst, err := fs.NewFs(ctx, r.FremoteName+"/dst") + require.NoError(t, err) + + accounting.GlobalStats().ResetCounters() + err = operations.CopyFile(ctx, fdst, r.Flocal, "one", "one") + require.NoError(t, err) + err = operations.CopyFile(ctx, fdst, r.Flocal, "two", "two") + require.NoError(t, err) + err = operations.CopyFile(ctx, fdst, r.Flocal, "three.txt", "three.txt") + require.NoError(t, err) + + // one should be moved to the backup dir and the new one installed + file1.Path = "dst/one" + suffix + file1a.Path = "dst/one" + // two should be unchanged + // three should be moved to the backup dir + if suffixKeepExtension { + file3.Path = "dst/three" + suffix + ".txt" + } else { + file3.Path = "dst/three.txt" + suffix + } + file3a.Path = "dst/three.txt" + + r.CheckRemoteItems(t, file1, file2, file3, file1a, file3a) + + // Now check what happens if we do it again + // Restore a different three and update one in the source + file3b := r.WriteFile("three.txt", "threeBDifferentSize", t3) + file1b := r.WriteFile("one", "oneBB", t3) + r.CheckRemoteItems(t, file1, file2, file3, file1a, file3a) + + // This should delete three and overwrite one again, checking + // the files got overwritten correctly in backup-dir + accounting.GlobalStats().ResetCounters() + err = operations.CopyFile(ctx, fdst, r.Flocal, "one", "one") + require.NoError(t, err) + err = operations.CopyFile(ctx, fdst, r.Flocal, "two", "two") + require.NoError(t, err) + err = operations.CopyFile(ctx, fdst, r.Flocal, "three.txt", "three.txt") + require.NoError(t, err) + + // one should be moved to the backup dir and the new one installed + file1a.Path = "dst/one" + suffix + file1b.Path = "dst/one" + // two should be unchanged + // three should be moved to the backup dir + if suffixKeepExtension { + file3a.Path = "dst/three" + suffix + ".txt" + } else { + file3a.Path = "dst/three.txt" + suffix + } + file3b.Path = "dst/three.txt" + + r.CheckRemoteItems(t, file1b, file3b, file2, file3a, file1a) +} +func TestSyncSuffix(t *testing.T) { testSyncSuffix(t, ".bak", false) } +func TestSyncSuffixKeepExtension(t *testing.T) { testSyncSuffix(t, "-2019-01-01", true) } + +// Check we can sync two files with differing UTF-8 representations +func TestSyncUTFNorm(t *testing.T) { + ctx := context.Background() + if runtime.GOOS == "darwin" { + t.Skip("Can't test UTF normalization on OS X") + } + + r := fstest.NewRun(t) + + // Two strings with different unicode normalization (from OS X) + Encoding1 := "Testêé" + Encoding2 := "Testêé" + assert.NotEqual(t, Encoding1, Encoding2) + assert.Equal(t, norm.NFC.String(Encoding1), norm.NFC.String(Encoding2)) + + file1 := r.WriteFile(Encoding1, "This is a test", t1) + r.CheckLocalItems(t, file1) + + file2 := r.WriteObject(ctx, Encoding2, "This is a old test", t2) + r.CheckRemoteItems(t, file2) + + accounting.GlobalStats().ResetCounters() + // ctx = predictDstFromLogger(ctx) + err := Sync(ctx, r.Fremote, r.Flocal, false) + // testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) // can't test this on macOS + require.NoError(t, err) + + // We should have transferred exactly one file, but kept the + // normalized state of the file. + assert.Equal(t, toyFileTransfers(r), accounting.GlobalStats().GetTransfers()) + r.CheckLocalItems(t, file1) + file1.Path = file2.Path + r.CheckRemoteItems(t, file1) +} + +// Test --immutable +func TestSyncImmutable(t *testing.T) { + ctx := context.Background() + ctx, ci := fs.AddConfig(ctx) + r := fstest.NewRun(t) + + ci.Immutable = true + + // Create file on source + file1 := r.WriteFile("existing", "potato", t1) + r.CheckLocalItems(t, file1) + r.CheckRemoteItems(t) + + // Should succeed + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + err := Sync(ctx, r.Fremote, r.Flocal, false) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + require.NoError(t, err) + r.CheckLocalItems(t, file1) + r.CheckRemoteItems(t, file1) + + // Modify file data and timestamp on source + file2 := r.WriteFile("existing", "tomatoes", t2) + r.CheckLocalItems(t, file2) + r.CheckRemoteItems(t, file1) + + // Should fail with ErrorImmutableModified and not modify local or remote files + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + err = Sync(ctx, r.Fremote, r.Flocal, false) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + assert.EqualError(t, err, fs.ErrorImmutableModified.Error()) + r.CheckLocalItems(t, file2) + r.CheckRemoteItems(t, file1) +} + +// Test --ignore-case-sync +func TestSyncIgnoreCase(t *testing.T) { + ctx := context.Background() + ctx, ci := fs.AddConfig(ctx) + r := fstest.NewRun(t) + + // Only test if filesystems are case sensitive + if r.Fremote.Features().CaseInsensitive || r.Flocal.Features().CaseInsensitive { + t.Skip("Skipping test as local or remote are case-insensitive") + } + + ci.IgnoreCaseSync = true + + // Create files with different filename casing + file1 := r.WriteFile("existing", "potato", t1) + r.CheckLocalItems(t, file1) + file2 := r.WriteObject(ctx, "EXISTING", "potato", t1) + r.CheckRemoteItems(t, file2) + + // Should not copy files that are differently-cased but otherwise identical + accounting.GlobalStats().ResetCounters() + // ctx = predictDstFromLogger(ctx) + err := Sync(ctx, r.Fremote, r.Flocal, false) + // testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) // can't test this on macOS + require.NoError(t, err) + r.CheckLocalItems(t, file1) + r.CheckRemoteItems(t, file2) +} + +// Test --fix-case +func TestFixCase(t *testing.T) { + ctx := context.Background() + ctx, ci := fs.AddConfig(ctx) + r := fstest.NewRun(t) + + // Only test if remote is case insensitive + if !r.Fremote.Features().CaseInsensitive { + t.Skip("Skipping test as local or remote are case-sensitive") + } + + ci.FixCase = true + + // Create files with different filename casing + file1a := r.WriteFile("existing", "potato", t1) + file1b := r.WriteFile("existingbutdifferent", "donut", t1) + file1c := r.WriteFile("subdira/subdirb/subdirc/hello", "donut", t1) + file1d := r.WriteFile("subdira/subdirb/subdirc/subdird/filewithoutcasedifferences", "donut", t1) + r.CheckLocalItems(t, file1a, file1b, file1c, file1d) + file2a := r.WriteObject(ctx, "EXISTING", "potato", t1) + file2b := r.WriteObject(ctx, "EXISTINGBUTDIFFERENT", "lemonade", t1) + file2c := r.WriteObject(ctx, "SUBDIRA/subdirb/SUBDIRC/HELLO", "lemonade", t1) + file2d := r.WriteObject(ctx, "SUBDIRA/subdirb/SUBDIRC/subdird/filewithoutcasedifferences", "lemonade", t1) + r.CheckRemoteItems(t, file2a, file2b, file2c, file2d) + + // Should force rename of dest file that is differently-cased + accounting.GlobalStats().ResetCounters() + err := Sync(ctx, r.Fremote, r.Flocal, false) + require.NoError(t, err) + r.CheckLocalItems(t, file1a, file1b, file1c, file1d) + r.CheckRemoteItems(t, file1a, file1b, file1c, file1d) +} + +// Test that aborting on --max-transfer works +func TestMaxTransfer(t *testing.T) { + ctx := context.Background() + ctx, ci := fs.AddConfig(ctx) + ci.MaxTransfer = 3 * 1024 + ci.Transfers = 1 + ci.Checkers = 1 + ci.CutoffMode = fs.CutoffModeHard + + test := func(t *testing.T, cutoff fs.CutoffMode) { + r := fstest.NewRun(t) + ci.CutoffMode = cutoff + + if r.Fremote.Name() != "local" { + t.Skip("This test only runs on local") + } + + // Create file on source + file1 := r.WriteFile("file1", string(make([]byte, 5*1024)), t1) + file2 := r.WriteFile("file2", string(make([]byte, 2*1024)), t1) + file3 := r.WriteFile("file3", string(make([]byte, 3*1024)), t1) + r.CheckLocalItems(t, file1, file2, file3) + r.CheckRemoteItems(t) + + if runtime.GOOS == "darwin" { + // disable server-side copies as they don't count towards transfer size stats + r.Flocal.Features().Disable("Copy") + if r.Fremote.Features().IsLocal { + r.Fremote.Features().Disable("Copy") + } + } + + accounting.GlobalStats().ResetCounters() + + // ctx = predictDstFromLogger(ctx) // not currently supported + err := Sync(ctx, r.Fremote, r.Flocal, false) + // testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + expectedErr := fserrors.FsError(accounting.ErrorMaxTransferLimitReachedFatal) + if cutoff != fs.CutoffModeHard { + expectedErr = accounting.ErrorMaxTransferLimitReachedGraceful + } + fserrors.Count(expectedErr) + assert.Equal(t, expectedErr, err) + } + + t.Run("Hard", func(t *testing.T) { test(t, fs.CutoffModeHard) }) + t.Run("Soft", func(t *testing.T) { test(t, fs.CutoffModeSoft) }) + t.Run("Cautious", func(t *testing.T) { test(t, fs.CutoffModeCautious) }) +} + +func testSyncConcurrent(t *testing.T, subtest string) { + const ( + NFILES = 20 + NCHECKERS = 4 + NTRANSFERS = 4 + ) + + ctx, ci := fs.AddConfig(context.Background()) + ci.Checkers = NCHECKERS + ci.Transfers = NTRANSFERS + + r := fstest.NewRun(t) + stats := accounting.GlobalStats() + + itemsBefore := []fstest.Item{} + itemsAfter := []fstest.Item{} + for i := range NFILES { + nameBoth := fmt.Sprintf("both%d", i) + nameOnly := fmt.Sprintf("only%d", i) + switch subtest { + case "delete": + fileBoth := r.WriteBoth(ctx, nameBoth, "potato", t1) + fileOnly := r.WriteObject(ctx, nameOnly, "potato", t1) + itemsBefore = append(itemsBefore, fileBoth, fileOnly) + itemsAfter = append(itemsAfter, fileBoth) + case "truncate": + fileBoth := r.WriteBoth(ctx, nameBoth, "potato", t1) + fileFull := r.WriteObject(ctx, nameOnly, "potato", t1) + fileEmpty := r.WriteFile(nameOnly, "", t1) + itemsBefore = append(itemsBefore, fileBoth, fileFull) + itemsAfter = append(itemsAfter, fileBoth, fileEmpty) + } + } + + r.CheckRemoteItems(t, itemsBefore...) + stats.ResetErrors() + ctx = predictDstFromLogger(ctx) + err := Sync(ctx, r.Fremote, r.Flocal, false) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + if errors.Is(err, fs.ErrorCantUploadEmptyFiles) { + t.Skipf("Skip test because remote cannot upload empty files") + } + assert.NoError(t, err, "Sync must not return a error") + assert.False(t, stats.Errored(), "Low level errors must not have happened") + r.CheckRemoteItems(t, itemsAfter...) +} + +func TestSyncConcurrentDelete(t *testing.T) { + testSyncConcurrent(t, "delete") +} + +func TestSyncConcurrentTruncate(t *testing.T) { + testSyncConcurrent(t, "truncate") +} + +// Test that sync replaces dir modtimes in dst if they've changed +func testSyncReplaceDirModTime(t *testing.T, copyEmptySrcDirs bool) { + accounting.GlobalStats().ResetCounters() + ctx, _ := fs.AddConfig(context.Background()) + r := fstest.NewRun(t) + + file1 := r.WriteFile("file1", "file1", t2) + file2 := r.WriteFile("test_dir1/file2", "file2", t2) + file3 := r.WriteFile("test_dir2/sub_dir/file3", "file3", t2) + r.CheckLocalItems(t, file1, file2, file3) + + _, err := operations.MkdirModTime(ctx, r.Flocal, "empty_dir", t2) + require.NoError(t, err) + + // A directory that's empty on both src and dst + _, err = operations.MkdirModTime(ctx, r.Flocal, "empty_on_remote", t2) + require.NoError(t, err) + _, err = operations.MkdirModTime(ctx, r.Fremote, "empty_on_remote", t2) + require.NoError(t, err) + + // set logging + // (this checks log output as DirModtime operations do not yet have stats, and r.CheckDirectoryModTimes also does not tell us what actions were taken) + oldLogLevel := fs.GetConfig(context.Background()).LogLevel + defer func() { fs.GetConfig(context.Background()).LogLevel = oldLogLevel }() // reset to old val after test + // need to do this as fs.Infof only respects the globalConfig + fs.GetConfig(context.Background()).LogLevel = fs.LogLevelInfo + + // First run + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + output := bilib.CaptureOutput(func() { + err := CopyDir(ctx, r.Fremote, r.Flocal, copyEmptySrcDirs) + require.NoError(t, err) + }) + require.NotNil(t, output) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + + // Save all dirs + dirs := []string{"test_dir1", "test_dir2", "test_dir2/sub_dir", "empty_on_remote"} + if copyEmptySrcDirs { + dirs = append(dirs, "empty_dir") + } + + // Change dir modtimes + for _, dir := range dirs { + _, err := operations.SetDirModTime(ctx, r.Flocal, nil, dir, t1) + if err != nil && !errors.Is(err, fs.ErrorNotImplemented) { + require.NoError(t, err) + } + } + + // Run again + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + output = bilib.CaptureOutput(func() { + err := CopyDir(ctx, r.Fremote, r.Flocal, copyEmptySrcDirs) + require.NoError(t, err) + }) + require.NotNil(t, output) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + r.CheckLocalItems(t, file1, file2, file3) + r.CheckRemoteItems(t, file1, file2, file3) + + // Check that the modtimes of the directories are as expected + r.CheckDirectoryModTimes(t, dirs...) +} + +func TestSyncReplaceDirModTime(t *testing.T) { + testSyncReplaceDirModTime(t, false) +} + +func TestSyncReplaceDirModTimeWithEmptyDirs(t *testing.T) { + testSyncReplaceDirModTime(t, true) +} + +// Tests that nothing is transferred when src and dst already match +// Run the same sync twice, ensure no action is taken the second time +func testNothingToTransfer(t *testing.T, copyEmptySrcDirs bool) { + accounting.GlobalStats().ResetCounters() + ctx, _ := fs.AddConfig(context.Background()) + r := fstest.NewRun(t) + file1 := r.WriteFile("sub dir/hello world", "hello world", t1) + file2 := r.WriteFile("sub dir2/very/very/very/very/very/nested/subdir/hello world", "hello world", t1) + r.CheckLocalItems(t, file1, file2) + _, err := operations.SetDirModTime(ctx, r.Flocal, nil, "sub dir", t2) + if err != nil && !errors.Is(err, fs.ErrorNotImplemented) { + require.NoError(t, err) + } + r.Mkdir(ctx, r.Fremote) + _, err = operations.MkdirModTime(ctx, r.Fremote, "sub dir", t3) + require.NoError(t, err) + + // set logging + // (this checks log output as DirModtime operations do not yet have stats, and r.CheckDirectoryModTimes also does not tell us what actions were taken) + oldLogLevel := fs.GetConfig(context.Background()).LogLevel + defer func() { fs.GetConfig(context.Background()).LogLevel = oldLogLevel }() // reset to old val after test + // need to do this as fs.Infof only respects the globalConfig + fs.GetConfig(context.Background()).LogLevel = fs.LogLevelInfo + + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + output := bilib.CaptureOutput(func() { + err = CopyDir(ctx, r.Fremote, r.Flocal, copyEmptySrcDirs) + require.NoError(t, err) + }) + require.NotNil(t, output) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + r.CheckLocalItems(t, file1, file2) + r.CheckRemoteItems(t, file1, file2) + // Check that the modtimes of the directories are as expected + r.CheckDirectoryModTimes(t, "sub dir", "sub dir2", "sub dir2/very", "sub dir2/very/very", "sub dir2/very/very/very/very/very/nested/subdir") + + // check that actions were taken + assert.True(t, strings.Contains(string(output), "Copied"), `expected to find at least one "Copied" log: `+string(output)) + if r.Fremote.Features().DirSetModTime != nil || r.Fremote.Features().MkdirMetadata != nil { + assert.True(t, strings.Contains(string(output), "Set directory modification time"), `expected to find at least one "Set directory modification time" log: `+string(output)) + } + assert.False(t, strings.Contains(string(output), "There was nothing to transfer"), `expected to find no "There was nothing to transfer" logs, but found one: `+string(output)) + assert.True(t, accounting.GlobalStats().GetTransfers() >= 2) + + // run it again and make sure no actions were taken + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + output = bilib.CaptureOutput(func() { + err = CopyDir(ctx, r.Fremote, r.Flocal, copyEmptySrcDirs) + require.NoError(t, err) + }) + require.NotNil(t, output) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + r.CheckLocalItems(t, file1, file2) + r.CheckRemoteItems(t, file1, file2) + // Check that the modtimes of the directories are as expected + r.CheckDirectoryModTimes(t, "sub dir", "sub dir2", "sub dir2/very", "sub dir2/very/very", "sub dir2/very/very/very/very/very/nested/subdir") + + // check that actions were NOT taken + assert.False(t, strings.Contains(string(output), "Copied"), `expected to find no "Copied" logs, but found one: `+string(output)) + if r.Fremote.Features().DirSetModTime != nil || r.Fremote.Features().MkdirMetadata != nil { + assert.False(t, strings.Contains(string(output), "Set directory modification time"), `expected to find no "Set directory modification time" logs, but found one: `+string(output)) + assert.False(t, strings.Contains(string(output), "Updated directory metadata"), `expected to find no "Updated directory metadata" logs, but found one: `+string(output)) + assert.False(t, strings.Contains(string(output), "directory"), `expected to find no "directory"-related logs, but found one: `+string(output)) // catch-all + } + assert.True(t, strings.Contains(string(output), "There was nothing to transfer"), `expected to find a "There was nothing to transfer" log: `+string(output)) + assert.Equal(t, int64(0), accounting.GlobalStats().GetTransfers()) + + // check nested empty dir behavior (FIXME: probably belongs in a separate test) + if r.Fremote.Features().DirSetModTime == nil && r.Fremote.Features().MkdirMetadata == nil { + return + } + file3 := r.WriteFile("sub dir2/sub dir3/hello world", "hello again, world", t1) + _, err = operations.SetDirModTime(ctx, r.Flocal, nil, "sub dir2", t1) + assert.NoError(t, err) + _, err = operations.SetDirModTime(ctx, r.Fremote, nil, "sub dir2", t1) + assert.NoError(t, err) + _, err = operations.MkdirModTime(ctx, r.Flocal, "sub dirEmpty/sub dirEmpty2", t2) + assert.NoError(t, err) + _, err = operations.SetDirModTime(ctx, r.Flocal, nil, "sub dirEmpty", t2) + assert.NoError(t, err) + + accounting.GlobalStats().ResetCounters() + ctx = predictDstFromLogger(ctx) + output = bilib.CaptureOutput(func() { + err = CopyDir(ctx, r.Fremote, r.Flocal, copyEmptySrcDirs) + require.NoError(t, err) + }) + require.NotNil(t, output) + testLoggerVsLsf(ctx, r.Fremote, r.Flocal, operations.GetLoggerOpt(ctx).JSON, t) + r.CheckLocalItems(t, file1, file2, file3) + r.CheckRemoteItems(t, file1, file2, file3) + // Check that the modtimes of the directories are as expected + r.CheckDirectoryModTimes(t, "sub dir", "sub dir2", "sub dir2/very", "sub dir2/very/very", "sub dir2/very/very/very/very/very/nested/subdir", "sub dir2/sub dir3") + if copyEmptySrcDirs { + r.CheckDirectoryModTimes(t, "sub dirEmpty", "sub dirEmpty/sub dirEmpty2") + assert.True(t, strings.Contains(string(output), "sub dirEmpty:"), `expected to find at least one "sub dirEmpty:" log: `+string(output)) + } else { + assert.False(t, strings.Contains(string(output), "sub dirEmpty:"), `expected to find no "sub dirEmpty:" logs, but found one (empty dir was synced and shouldn't have been): `+string(output)) + } + assert.True(t, strings.Contains(string(output), "sub dir3:"), `expected to find at least one "sub dir3:" log: `+string(output)) + assert.False(t, strings.Contains(string(output), "sub dir2/very:"), `expected to find no "sub dir2/very:" logs, but found one (unmodified dir was marked modified): `+string(output)) +} + +func TestNothingToTransferWithEmptyDirs(t *testing.T) { + testNothingToTransfer(t, true) +} + +func TestNothingToTransferWithoutEmptyDirs(t *testing.T) { + testNothingToTransfer(t, false) +} + +// for testing logger: +func predictDstFromLogger(ctx context.Context) context.Context { + opt := operations.NewLoggerOpt() + var lock mutex.Mutex + + opt.LoggerFn = func(ctx context.Context, sigil operations.Sigil, src, dst fs.DirEntry, err error) { + lock.Lock() + defer lock.Unlock() + + // ignore dirs for our purposes here + if err == fs.ErrorIsDir { + return + } + winner := operations.WinningSide(ctx, sigil, src, dst, err) + if winner.Obj != nil { + file := winner.Obj + obj, ok := file.(fs.ObjectInfo) + checksum := "" + timeFormat := "2006-01-02 15:04:05" + if ok { + if obj.Fs().Hashes().GetOne() == hash.MD5 { + // skip if no MD5 + checksum, _ = obj.Hash(ctx, hash.MD5) + } + timeFormat = operations.FormatForLSFPrecision(obj.Fs().Precision()) + } + errMsg := "" + if winner.Err != nil { + errMsg = ";" + winner.Err.Error() + } + operations.SyncFprintf(opt.JSON, "%s;%s;%v;%s%s\n", file.ModTime(ctx).Local().Format(timeFormat), checksum, file.Size(), transform.Path(ctx, file.Remote(), false), errMsg) // TODO: should the transform be handled in the sync instead of here? + } + } + return operations.WithSyncLogger(ctx, opt) +} + +func DstLsf(ctx context.Context, Fremote fs.Fs) *bytes.Buffer { + opt := operations.ListJSONOpt{ + NoModTime: false, + NoMimeType: true, + DirsOnly: false, + FilesOnly: true, + Recurse: true, + ShowHash: true, + HashTypes: []string{"MD5"}, + } + + var list operations.ListFormat + + list.SetSeparator(";") + timeFormat := operations.FormatForLSFPrecision(Fremote.Precision()) + if Fremote.Precision() == fs.ModTimeNotSupported { + timeFormat = "none" + } + list.AddModTime(timeFormat) + list.AddHash(hash.MD5) + list.AddSize() + list.AddPath() + + out := new(bytes.Buffer) + + err := operations.ListJSON(ctx, Fremote, "", &opt, func(item *operations.ListJSONItem) error { + _, _ = fmt.Fprintln(out, list.Format(item)) + return nil + }) + if err != nil { + fs.Errorf(Fremote, "ListJSON error: %v", err) + } + + return out +} + +func LoggerMatchesLsf(logger, lsf *bytes.Buffer) error { + loggerSplit := bytes.Split(logger.Bytes(), []byte("\n")) + sort.SliceStable(loggerSplit, func(i int, j int) bool { return string(loggerSplit[i]) < string(loggerSplit[j]) }) + lsfSplit := bytes.Split(lsf.Bytes(), []byte("\n")) + sort.SliceStable(lsfSplit, func(i int, j int) bool { return string(lsfSplit[i]) < string(lsfSplit[j]) }) + + loggerJoined := bytes.Join(loggerSplit, []byte("\n")) + lsfJoined := bytes.Join(lsfSplit, []byte("\n")) + + if bytes.Equal(loggerJoined, lsfJoined) { + return nil + } + Diff(string(loggerJoined), string(lsfJoined)) + return fmt.Errorf("logger does not match lsf! \nlogger: \n%s \nlsf: \n%s", loggerJoined, lsfJoined) +} + +func Diff(rev1, rev2 string) { + fmt.Printf("Diff of %q and %q\n", "logger", "lsf") + cmd := exec.Command("bash", "-c", fmt.Sprintf(`diff <(echo "%s") <(echo "%s")`, rev1, rev2)) + out, _ := cmd.Output() + _, _ = os.Stdout.Write(out) +} + +func testLoggerVsLsf(ctx context.Context, fdst, fsrc fs.Fs, logger *bytes.Buffer, t *testing.T) { + var newlogger bytes.Buffer + canTestModtime := fs.GetModifyWindow(ctx, fdst) != fs.ModTimeNotSupported + canTestHash := fdst.Hashes().Contains(hash.MD5) + if !canTestHash || !canTestModtime { + loggerSplit := bytes.Split(logger.Bytes(), []byte("\n")) + for i, line := range loggerSplit { + elements := bytes.Split(line, []byte(";")) + if len(elements) >= 2 { + if !canTestModtime { + elements[0] = []byte("none") + } + if !canTestHash { + elements[1] = []byte("") + } + } + loggerSplit[i] = bytes.Join(elements, []byte(";")) + } + newlogger.Write(bytes.Join(loggerSplit, []byte("\n"))) + } else { + newlogger.Write(logger.Bytes()) + } + + if fsrc.Precision() == fdst.Precision() && fsrc.Hashes().Contains(hash.MD5) && canTestHash { + lsf := DstLsf(ctx, fdst) + err := LoggerMatchesLsf(&newlogger, lsf) + require.NoError(t, err) + } +} |
