Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/owncloud/client.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/common/syncjournaldb.cpp13
-rw-r--r--src/common/syncjournaldb.h2
-rw-r--r--src/csync/csync_reconcile.cpp117
-rw-r--r--src/csync/csync_update.cpp71
-rw-r--r--test/CMakeLists.txt1
-rw-r--r--test/syncenginetestutils.h20
-rw-r--r--test/testsyncengine.cpp170
-rw-r--r--test/testsyncmove.cpp569
8 files changed, 715 insertions, 248 deletions
diff --git a/src/common/syncjournaldb.cpp b/src/common/syncjournaldb.cpp
index b827657c7..b263cdc16 100644
--- a/src/common/syncjournaldb.cpp
+++ b/src/common/syncjournaldb.cpp
@@ -1094,15 +1094,10 @@ bool SyncJournalDb::getFileRecordByInode(quint64 inode, SyncJournalFileRecord *r
return true;
}
-bool SyncJournalDb::getFileRecordByFileId(const QByteArray &fileId, SyncJournalFileRecord *rec)
+bool SyncJournalDb::getFileRecordsByFileId(const QByteArray &fileId, const std::function<void(const SyncJournalFileRecord &)> &rowCallback)
{
QMutexLocker locker(&_mutex);
- // Reset the output var in case the caller is reusing it.
- Q_ASSERT(rec);
- rec->_path.clear();
- Q_ASSERT(!rec->isValid());
-
if (fileId.isEmpty() || _metadataTableIsEmpty)
return true; // no error, yet nothing found (rec->isValid() == false)
@@ -1116,8 +1111,10 @@ bool SyncJournalDb::getFileRecordByFileId(const QByteArray &fileId, SyncJournalF
return false;
}
- if (_getFileRecordQueryByFileId->next()) {
- fillFileRecordFromGetQuery(*rec, *_getFileRecordQueryByFileId);
+ while (_getFileRecordQueryByFileId->next()) {
+ SyncJournalFileRecord rec;
+ fillFileRecordFromGetQuery(rec, *_getFileRecordQueryByFileId);
+ rowCallback(rec);
}
return true;
diff --git a/src/common/syncjournaldb.h b/src/common/syncjournaldb.h
index cfdced9a3..6baa1388e 100644
--- a/src/common/syncjournaldb.h
+++ b/src/common/syncjournaldb.h
@@ -58,7 +58,7 @@ public:
bool getFileRecord(const QString &filename, SyncJournalFileRecord *rec) { return getFileRecord(filename.toUtf8(), rec); }
bool getFileRecord(const QByteArray &filename, SyncJournalFileRecord *rec);
bool getFileRecordByInode(quint64 inode, SyncJournalFileRecord *rec);
- bool getFileRecordByFileId(const QByteArray &fileId, SyncJournalFileRecord *rec);
+ bool getFileRecordsByFileId(const QByteArray &fileId, const std::function<void(const SyncJournalFileRecord &)> &rowCallback);
bool getFilesBelowPath(const QByteArray &path, const std::function<void(const SyncJournalFileRecord&)> &rowCallback);
bool setFileRecord(const SyncJournalFileRecord &record);
diff --git a/src/csync/csync_reconcile.cpp b/src/csync/csync_reconcile.cpp
index 52ebba36f..92aa563fe 100644
--- a/src/csync/csync_reconcile.cpp
+++ b/src/csync/csync_reconcile.cpp
@@ -104,14 +104,17 @@ static bool _csync_is_collision_safe_hash(const char *checksum_header)
* source and the destination, have been changed, the newer file wins.
*/
static int _csync_merge_algorithm_visitor(csync_file_stat_t *cur, CSYNC * ctx) {
+ csync_s::FileMap *our_tree = nullptr;
csync_s::FileMap *other_tree = nullptr;
/* we need the opposite tree! */
switch (ctx->current) {
case LOCAL_REPLICA:
+ our_tree = &ctx->local.files;
other_tree = &ctx->remote.files;
break;
case REMOTE_REPLICA:
+ our_tree = &ctx->remote.files;
other_tree = &ctx->local.files;
break;
default:
@@ -152,40 +155,51 @@ static int _csync_merge_algorithm_visitor(csync_file_stat_t *cur, CSYNC * ctx) {
cur->instruction = CSYNC_INSTRUCTION_REMOVE;
break;
case CSYNC_INSTRUCTION_EVAL_RENAME: {
- OCC::SyncJournalFileRecord base;
- if(ctx->current == LOCAL_REPLICA ) {
- /* use the old name to find the "other" node */
- ctx->statedb->getFileRecordByInode(cur->inode, &base);
- qCDebug(lcReconcile, "Finding opposite temp through inode %" PRIu64 ": %s",
- cur->inode, base.isValid() ? "true":"false");
- } else {
- ASSERT( ctx->current == REMOTE_REPLICA );
- ctx->statedb->getFileRecordByFileId(cur->file_id, &base);
- qCDebug(lcReconcile, "Finding opposite temp through file ID %s: %s",
- cur->file_id.constData(), base.isValid() ? "true":"false");
- }
+ // By default, the EVAL_RENAME decays into a NEW
+ cur->instruction = CSYNC_INSTRUCTION_NEW;
+
+ bool processedRename = false;
+ auto renameCandidateProcessing = [&](const OCC::SyncJournalFileRecord &base) {
+ if (processedRename)
+ return;
+ if (!base.isValid())
+ return;
- if( base.isValid() ) {
/* First, check that the file is NOT in our tree (another file with the same name was added) */
- csync_s::FileMap *our_tree = ctx->current == REMOTE_REPLICA ? &ctx->remote.files : &ctx->local.files;
- if (our_tree->findFile(base._path)) {
- qCDebug(lcReconcile, "Origin found in our tree : %s", base._path.constData());
+ if (our_tree->findFile(base._path)) {
+ qCDebug(lcReconcile, "Origin found in our tree : %s", base._path.constData());
} else {
- /* Find the temporar file in the other tree.
+ /* Find the potential rename source file in the other tree.
* If the renamed file could not be found in the opposite tree, that is because it
* is not longer existing there, maybe because it was renamed or deleted.
* The journal is cleaned up later after propagation.
*/
other = other_tree->findFile(base._path);
- qCDebug(lcReconcile, "Temporary opposite (%s) %s",
- base._path.constData() , other ? "found": "not found" );
+ qCDebug(lcReconcile, "Rename origin in other tree (%s) %s",
+ base._path.constData(), other ? "found" : "not found");
}
if(!other) {
- cur->instruction = CSYNC_INSTRUCTION_NEW;
- } else if (other->instruction == CSYNC_INSTRUCTION_NONE
- || other->instruction == CSYNC_INSTRUCTION_UPDATE_METADATA
- || cur->type == CSYNC_FTW_TYPE_DIR) {
+ // Stick with the NEW
+ return;
+ } else if (other->instruction == CSYNC_INSTRUCTION_RENAME) {
+ // Some other EVAL_RENAME already claimed other.
+ // We do nothing: maybe a different candidate for
+ // other is found as well?
+ qCDebug(lcReconcile, "Other has already been renamed to %s",
+ other->rename_path.constData());
+ } else if (cur->type == CSYNC_FTW_TYPE_DIR
+ // The local replica is reconciled first, so the remote tree would
+ // have either NONE or UPDATE_METADATA if the remote file is safe to
+ // move.
+ // In the remote replica, REMOVE is also valid (local has already
+ // been reconciled). NONE can still happen if the whole parent dir
+ // was set to REMOVE by the local reconcile.
+ || other->instruction == CSYNC_INSTRUCTION_NONE
+ || other->instruction == CSYNC_INSTRUCTION_UPDATE_METADATA
+ || other->instruction == CSYNC_INSTRUCTION_REMOVE) {
+ qCDebug(lcReconcile, "Switching %s to RENAME to %s",
+ other->path.constData(), cur->path.constData());
other->instruction = CSYNC_INSTRUCTION_RENAME;
other->rename_path = cur->path;
if( !cur->file_id.isEmpty() ) {
@@ -193,24 +207,44 @@ static int _csync_merge_algorithm_visitor(csync_file_stat_t *cur, CSYNC * ctx) {
}
other->inode = cur->inode;
cur->instruction = CSYNC_INSTRUCTION_NONE;
- } else if (other->instruction == CSYNC_INSTRUCTION_REMOVE) {
- other->instruction = CSYNC_INSTRUCTION_RENAME;
- other->rename_path = cur->path;
+ // We have consumed 'other': exit this loop to not consume another one.
+ processedRename = true;
+ } else if (our_tree->findFile(csync_rename_adjust_path(ctx, other->path)) == cur) {
+ // If we're here, that means that the other side's reconcile will be able
+ // to work against cur: The filename itself didn't change, only a parent
+ // directory was renamed! In that case it's safe to ignore the rename
+ // since the parent directory rename will already deal with it.
- if( !cur->file_id.isEmpty() ) {
- other->file_id = cur->file_id;
- }
- other->inode = cur->inode;
+ // Local: The remote reconcile will be able to deal with this.
+ // Remote: The local replica has already dealt with this.
+ // See the EVAL_RENAME case when other was found directly.
+ qCDebug(lcReconcile, "File in a renamed directory, other side's instruction: %d",
+ other->instruction);
cur->instruction = CSYNC_INSTRUCTION_NONE;
- } else if (other->instruction == CSYNC_INSTRUCTION_NEW) {
- qCDebug(lcReconcile, "OOOO=> NEW detected in other tree!");
- cur->instruction = CSYNC_INSTRUCTION_CONFLICT;
} else {
- assert(other->type != CSYNC_FTW_TYPE_DIR);
- cur->instruction = CSYNC_INSTRUCTION_NONE;
- other->instruction = CSYNC_INSTRUCTION_SYNC;
+ // This can, for instance, happen when there was a local change in other
+ // and the instruction in the local tree is NEW while cur has EVAL_RENAME
+ // due to a remote move of the same file. In these scenarios we just
+ // want the instruction to stay NEW.
+ qCDebug(lcReconcile, "Other already has instruction %d",
+ other->instruction);
}
+ };
+
+ if (ctx->current == LOCAL_REPLICA) {
+ /* use the old name to find the "other" node */
+ OCC::SyncJournalFileRecord base;
+ qCDebug(lcReconcile, "Finding rename origin through inode %" PRIu64 "",
+ cur->inode);
+ ctx->statedb->getFileRecordByInode(cur->inode, &base);
+ renameCandidateProcessing(base);
+ } else {
+ ASSERT(ctx->current == REMOTE_REPLICA);
+ qCDebug(lcReconcile, "Finding rename origin through file ID %s",
+ cur->file_id.constData());
+ ctx->statedb->getFileRecordsByFileId(cur->file_id, renameCandidateProcessing);
}
+
break;
}
default:
@@ -310,10 +344,19 @@ static int _csync_merge_algorithm_visitor(csync_file_stat_t *cur, CSYNC * ctx) {
break;
case CSYNC_INSTRUCTION_IGNORE:
cur->instruction = CSYNC_INSTRUCTION_IGNORE;
- break;
+ break;
default:
break;
}
+ // Ensure we're not leaving discovery-only instructions
+ // in place. This can happen, for instance, when other's
+ // instruction is EVAL_RENAME because the parent dir was renamed.
+ // NEW is safer than EVAL because it will end up with
+ // propagation unless it's changed by something, and EVAL and
+ // NEW are treated equivalently during reconcile.
+ if (cur->instruction == CSYNC_INSTRUCTION_EVAL)
+ cur->instruction = CSYNC_INSTRUCTION_NEW;
+ break;
default:
break;
}
diff --git a/src/csync/csync_update.cpp b/src/csync/csync_update.cpp
index 0271b1c56..82be908e3 100644
--- a/src/csync/csync_update.cpp
+++ b/src/csync/csync_update.cpp
@@ -297,41 +297,60 @@ static int _csync_detect_update(CSYNC *ctx, std::unique_ptr<csync_file_stat_t> f
} else {
/* Remote Replica Rename check */
- OCC::SyncJournalFileRecord base;
- if(!ctx->statedb->getFileRecordByFileId(fs->file_id, &base)) {
- ctx->status_code = CSYNC_STATUS_UNSUCCESSFUL;
- return -1;
- }
- if (base.isValid()) { /* tmp existing at all */
+ fs->instruction = CSYNC_INSTRUCTION_NEW;
+
+ bool done = false;
+ auto renameCandidateProcessing = [&](const OCC::SyncJournalFileRecord &base) {
+ if (done)
+ return;
+ if (!base.isValid())
+ return;
+
+ // Some things prohibit rename detection entirely.
+ // Since we don't do the same checks again in reconcile, we can't
+ // just skip the candidate, but have to give up completely.
if (base._type != fs->type) {
- qCWarning(lcUpdate, "file types different is not!");
- fs->instruction = CSYNC_INSTRUCTION_NEW;
- goto out;
+ qCWarning(lcUpdate, "file types different, not a rename");
+ done = true;
+ return;
}
- qCDebug(lcUpdate, "remote rename detected based on fileid %s --> %s", base._path.constData(), fs->path.constData());
- fs->instruction = CSYNC_INSTRUCTION_EVAL_RENAME;
+ if (fs->type != CSYNC_FTW_TYPE_DIR && base._etag != fs->etag) {
+ /* File with different etag, don't do a rename, but download the file again */
+ qCWarning(lcUpdate, "file etag different, not a rename");
+ done = true;
+ return;
+ }
+
+ // Record directory renames
if (fs->type == CSYNC_FTW_TYPE_DIR) {
- csync_rename_record(ctx, base._path, fs->path);
- } else {
- if( base._etag != fs->etag ) {
- /* CSYNC_LOG(CSYNC_LOG_PRIORITY_DEBUG, "ETags are different!"); */
- /* File with different etag, don't do a rename, but download the file again */
- fs->instruction = CSYNC_INSTRUCTION_NEW;
+ // If the same folder was already renamed by a different entry,
+ // skip to the next candidate
+ if (ctx->renames.folder_renamed_to.count(base._path) > 0) {
+ qCWarning(lcUpdate, "folder already has a rename entry, skipping");
+ return;
}
+ csync_rename_record(ctx, base._path, fs->path);
}
- goto out;
- } else {
- /* file not found in statedb */
- fs->instruction = CSYNC_INSTRUCTION_NEW;
+ qCDebug(lcUpdate, "remote rename detected based on fileid %s --> %s", base._path.constData(), fs->path.constData());
+ fs->instruction = CSYNC_INSTRUCTION_EVAL_RENAME;
+ done = true;
+ };
- if (fs->type == CSYNC_FTW_TYPE_DIR && ctx->current == REMOTE_REPLICA && ctx->callbacks.checkSelectiveSyncNewFolderHook) {
- if (ctx->callbacks.checkSelectiveSyncNewFolderHook(ctx->callbacks.update_callback_userdata, fs->path, fs->remotePerm)) {
- return 1;
- }
+ if (!ctx->statedb->getFileRecordsByFileId(fs->file_id, renameCandidateProcessing)) {
+ ctx->status_code = CSYNC_STATUS_UNSUCCESSFUL;
+ return -1;
+ }
+
+ if (fs->instruction == CSYNC_INSTRUCTION_NEW
+ && fs->type == CSYNC_FTW_TYPE_DIR
+ && ctx->current == REMOTE_REPLICA
+ && ctx->callbacks.checkSelectiveSyncNewFolderHook) {
+ if (ctx->callbacks.checkSelectiveSyncNewFolderHook(ctx->callbacks.update_callback_userdata, fs->path, fs->remotePerm)) {
+ return 1;
}
- goto out;
}
+ goto out;
}
}
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 14235a49d..e529c86a9 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -46,6 +46,7 @@ owncloud_add_test(ExcludedFiles "")
owncloud_add_test(FileSystem "")
owncloud_add_test(Utility "")
owncloud_add_test(SyncEngine "syncenginetestutils.h")
+owncloud_add_test(SyncMove "syncenginetestutils.h")
owncloud_add_test(SyncFileStatusTracker "syncenginetestutils.h")
owncloud_add_test(ChunkingNg "syncenginetestutils.h")
owncloud_add_test(UploadReset "syncenginetestutils.h")
diff --git a/test/syncenginetestutils.h b/test/syncenginetestutils.h
index 376b47496..e6d4a395e 100644
--- a/test/syncenginetestutils.h
+++ b/test/syncenginetestutils.h
@@ -161,12 +161,15 @@ public:
FileInfo(const QString &name, qint64 size) : name{name}, isDir{false}, size{size} { }
FileInfo(const QString &name, qint64 size, char contentChar) : name{name}, isDir{false}, size{size}, contentChar{contentChar} { }
FileInfo(const QString &name, const std::initializer_list<FileInfo> &children) : name{name} {
- QString p = path();
- for (const auto &source : children) {
- auto &dest = this->children[source.name] = source;
- dest.parentPath = p;
- dest.fixupParentPathRecursively();
- }
+ for (const auto &source : children)
+ addChild(source);
+ }
+
+ void addChild(const FileInfo &info)
+ {
+ auto &dest = this->children[info.name] = info;
+ dest.parentPath = path();
+ dest.fixupParentPathRecursively();
}
void remove(const QString &relativePath) override {
@@ -952,6 +955,11 @@ private:
} else {
QFile f{diskChild.filePath()};
f.open(QFile::ReadOnly);
+ auto content = f.read(1);
+ if (content.size() == 0) {
+ qWarning() << "Empty file at:" << diskChild.filePath();
+ continue;
+ }
char contentChar = f.read(1).at(0);
templateFi.children.insert(diskChild.fileName(), FileInfo{diskChild.fileName(), diskChild.size(), contentChar});
}
diff --git a/test/testsyncengine.cpp b/test/testsyncengine.cpp
index 591ca653f..f1ed98941 100644
--- a/test/testsyncengine.cpp
+++ b/test/testsyncengine.cpp
@@ -141,98 +141,6 @@ private slots:
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
}
- void testRemoteChangeInMovedFolder() {
- // issue #5192
- FakeFolder fakeFolder{FileInfo{ QString(), {
- FileInfo { QStringLiteral("folder"), {
- FileInfo{ QStringLiteral("folderA"), { { QStringLiteral("file.txt"), 400 } } },
- QStringLiteral("folderB")
- }
- }}}};
-
- QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
-
- // Edit a file in a moved directory.
- fakeFolder.remoteModifier().setContents("folder/folderA/file.txt", 'a');
- fakeFolder.remoteModifier().rename("folder/folderA", "folder/folderB/folderA");
- fakeFolder.syncOnce();
- QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
- auto oldState = fakeFolder.currentLocalState();
- QVERIFY(oldState.find("folder/folderB/folderA/file.txt"));
- QVERIFY(!oldState.find("folder/folderA/file.txt"));
-
- // This sync should not remove the file
- fakeFolder.syncOnce();
- QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
- QCOMPARE(fakeFolder.currentLocalState(), oldState);
-
- }
-
- void testSelectiveSyncModevFolder() {
- // issue #5224
- FakeFolder fakeFolder{FileInfo{ QString(), {
- FileInfo { QStringLiteral("parentFolder"), {
- FileInfo{ QStringLiteral("subFolderA"), { { QStringLiteral("fileA.txt"), 400 } } },
- FileInfo{ QStringLiteral("subFolderB"), { { QStringLiteral("fileB.txt"), 400 } } }
- }
- }}}};
-
- QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
- auto expectedServerState = fakeFolder.currentRemoteState();
-
- // Remove subFolderA with selectiveSync:
- fakeFolder.syncEngine().journal()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList,
- {"parentFolder/subFolderA/"});
- fakeFolder.syncEngine().journal()->avoidReadFromDbOnNextSync(QByteArrayLiteral("parentFolder/subFolderA/"));
-
- fakeFolder.syncOnce();
-
- {
- // Nothing changed on the server
- QCOMPARE(fakeFolder.currentRemoteState(), expectedServerState);
- // The local state should not have subFolderA
- auto remoteState = fakeFolder.currentRemoteState();
- remoteState.remove("parentFolder/subFolderA");
- QCOMPARE(fakeFolder.currentLocalState(), remoteState);
- }
-
- // Rename parentFolder on the server
- fakeFolder.remoteModifier().rename("parentFolder", "parentFolderRenamed");
- expectedServerState = fakeFolder.currentRemoteState();
- fakeFolder.syncOnce();
-
- {
- QCOMPARE(fakeFolder.currentRemoteState(), expectedServerState);
- auto remoteState = fakeFolder.currentRemoteState();
- // The subFolderA should still be there on the server.
- QVERIFY(remoteState.find("parentFolderRenamed/subFolderA/fileA.txt"));
- // But not on the client because of the selective sync
- remoteState.remove("parentFolderRenamed/subFolderA");
- QCOMPARE(fakeFolder.currentLocalState(), remoteState);
- }
-
- // Rename it again, locally this time.
- fakeFolder.localModifier().rename("parentFolderRenamed", "parentThirdName");
- fakeFolder.syncOnce();
-
- {
- auto remoteState = fakeFolder.currentRemoteState();
- // The subFolderA should still be there on the server.
- QVERIFY(remoteState.find("parentThirdName/subFolderA/fileA.txt"));
- // But not on the client because of the selective sync
- remoteState.remove("parentThirdName/subFolderA");
- QCOMPARE(fakeFolder.currentLocalState(), remoteState);
-
- expectedServerState = fakeFolder.currentRemoteState();
- QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
- fakeFolder.syncOnce(); // This sync should do nothing
- QCOMPARE(completeSpy.count(), 0);
-
- QCOMPARE(fakeFolder.currentRemoteState(), expectedServerState);
- QCOMPARE(fakeFolder.currentLocalState(), remoteState);
- }
- }
-
void testSelectiveSyncBug() {
// issue owncloud/enterprise#1965: files from selective-sync ignored
// folders are uploaded anyway is some circumstances.
@@ -536,84 +444,6 @@ private slots:
QCOMPARE(n507, 3);
}
- void testLocalMove()
- {
- FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
-
- int nPUT = 0;
- int nDELETE = 0;
- fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &) {
- if (op == QNetworkAccessManager::PutOperation)
- ++nPUT;
- if (op == QNetworkAccessManager::DeleteOperation)
- ++nDELETE;
- return nullptr;
- });
-
- // For directly editing the remote checksum
- FileInfo &remoteInfo = fakeFolder.remoteModifier();
-
- // Simple move causing a remote rename
- fakeFolder.localModifier().rename("A/a1", "A/a1m");
- QVERIFY(fakeFolder.syncOnce());
- QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
- QCOMPARE(nPUT, 0);
-
- // Move-and-change, causing a upload and delete
- fakeFolder.localModifier().rename("A/a2", "A/a2m");
- fakeFolder.localModifier().appendByte("A/a2m");
- QVERIFY(fakeFolder.syncOnce());
- QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
- QCOMPARE(nPUT, 1);
- QCOMPARE(nDELETE, 1);
-
- // Move-and-change, mtime+content only
- fakeFolder.localModifier().rename("B/b1", "B/b1m");
- fakeFolder.localModifier().setContents("B/b1m", 'C');
- QVERIFY(fakeFolder.syncOnce());
- QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
- QCOMPARE(nPUT, 2);
- QCOMPARE(nDELETE, 2);
-
- // Move-and-change, size+content only
- auto mtime = fakeFolder.remoteModifier().find("B/b2")->lastModified;
- fakeFolder.localModifier().rename("B/b2", "B/b2m");
- fakeFolder.localModifier().appendByte("B/b2m");
- fakeFolder.localModifier().setModTime("B/b2m", mtime);
- QVERIFY(fakeFolder.syncOnce());
- QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
- QCOMPARE(nPUT, 3);
- QCOMPARE(nDELETE, 3);
-
- // Move-and-change, content only -- c1 has no checksum, so we fail to detect this!
- mtime = fakeFolder.remoteModifier().find("C/c1")->lastModified;
- fakeFolder.localModifier().rename("C/c1", "C/c1m");
- fakeFolder.localModifier().setContents("C/c1m", 'C');
- fakeFolder.localModifier().setModTime("C/c1m", mtime);
- QVERIFY(fakeFolder.syncOnce());
- QCOMPARE(nPUT, 3);
- QCOMPARE(nDELETE, 3);
- QVERIFY(!(fakeFolder.currentLocalState() == remoteInfo));
-
- // cleanup, and upload a file that will have a checksum in the db
- fakeFolder.localModifier().remove("C/c1m");
- fakeFolder.localModifier().insert("C/c3");
- QVERIFY(fakeFolder.syncOnce());
- QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
- QCOMPARE(nPUT, 4);
- QCOMPARE(nDELETE, 4);
-
- // Move-and-change, content only, this time while having a checksum
- mtime = fakeFolder.remoteModifier().find("C/c3")->lastModified;
- fakeFolder.localModifier().rename("C/c3", "C/c3m");
- fakeFolder.localModifier().setContents("C/c3m", 'C');
- fakeFolder.localModifier().setModTime("C/c3m", mtime);
- QVERIFY(fakeFolder.syncOnce());
- QCOMPARE(nPUT, 5);
- QCOMPARE(nDELETE, 5);
- QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
- }
-
// Checks whether downloads with bad checksums are accepted
void testChecksumValidation()
{
diff --git a/test/testsyncmove.cpp b/test/testsyncmove.cpp
new file mode 100644
index 000000000..11b354ad4
--- /dev/null
+++ b/test/testsyncmove.cpp
@@ -0,0 +1,569 @@
+/*
+ * This software is in the public domain, furnished "as is", without technical
+ * support, and with no warranty, express or implied, as to its usefulness for
+ * any purpose.
+ *
+ */
+
+#include <QtTest>
+#include "syncenginetestutils.h"
+#include <syncengine.h>
+
+using namespace OCC;
+
+SyncFileItemPtr findItem(const QSignalSpy &spy, const QString &path)
+{
+ for (const QList<QVariant> &args : spy) {
+ auto item = args[0].value<SyncFileItemPtr>();
+ if (item->destination() == path)
+ return item;
+ }
+ return SyncFileItemPtr(new SyncFileItem);
+}
+
+bool itemSuccessful(const QSignalSpy &spy, const QString &path, const csync_instructions_e instr)
+{
+ auto item = findItem(spy, path);
+ return item->_status == SyncFileItem::Success && item->_instruction == instr;
+}
+
+bool itemConflict(const QSignalSpy &spy, const QString &path)
+{
+ auto item = findItem(spy, path);
+ return item->_status == SyncFileItem::Conflict && item->_instruction == CSYNC_INSTRUCTION_CONFLICT;
+}
+
+bool itemSuccessfulMove(const QSignalSpy &spy, const QString &path)
+{
+ return itemSuccessful(spy, path, CSYNC_INSTRUCTION_RENAME);
+}
+
+QStringList findConflicts(const FileInfo &dir)
+{
+ QStringList conflicts;
+ for (const auto &item : dir.children) {
+ if (item.name.contains("conflict")) {
+ conflicts.append(item.path());
+ }
+ }
+ return conflicts;
+}
+
+bool expectAndWipeConflict(FileModifier &local, FileInfo state, const QString path)
+{
+ PathComponents pathComponents(path);
+ auto base = state.find(pathComponents.parentDirComponents());
+ if (!base)
+ return false;
+ for (const auto &item : base->children) {
+ if (item.name.startsWith(pathComponents.fileName()) && item.name.contains("_conflict")) {
+ local.remove(item.path());
+ return true;
+ }
+ }
+ return false;
+}
+
+class TestSyncMove : public QObject
+{
+ Q_OBJECT
+
+private slots:
+ void testRemoteChangeInMovedFolder()
+ {
+ // issue #5192
+ FakeFolder fakeFolder{ FileInfo{ QString(), { FileInfo{ QStringLiteral("folder"), { FileInfo{ QStringLiteral("folderA"), { { QStringLiteral("file.txt"), 400 } } }, QStringLiteral("folderB") } } } } };
+
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+
+ // Edit a file in a moved directory.
+ fakeFolder.remoteModifier().setContents("folder/folderA/file.txt", 'a');
+ fakeFolder.remoteModifier().rename("folder/folderA", "folder/folderB/folderA");
+ fakeFolder.syncOnce();
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+ auto oldState = fakeFolder.currentLocalState();
+ QVERIFY(oldState.find("folder/folderB/folderA/file.txt"));
+ QVERIFY(!oldState.find("folder/folderA/file.txt"));
+
+ // This sync should not remove the file
+ fakeFolder.syncOnce();
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+ QCOMPARE(fakeFolder.currentLocalState(), oldState);
+ }
+
+ void testSelectiveSyncMovedFolder()
+ {
+ // issue #5224
+ FakeFolder fakeFolder{ FileInfo{ QString(), { FileInfo{ QStringLiteral("parentFolder"), { FileInfo{ QStringLiteral("subFolderA"), { { QStringLiteral("fileA.txt"), 400 } } }, FileInfo{ QStringLiteral("subFolderB"), { { QStringLiteral("fileB.txt"), 400 } } } } } } } };
+
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+ auto expectedServerState = fakeFolder.currentRemoteState();
+
+ // Remove subFolderA with selectiveSync:
+ fakeFolder.syncEngine().journal()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList,
+ { "parentFolder/subFolderA/" });
+ fakeFolder.syncEngine().journal()->avoidReadFromDbOnNextSync(QByteArrayLiteral("parentFolder/subFolderA/"));
+
+ fakeFolder.syncOnce();
+
+ {
+ // Nothing changed on the server
+ QCOMPARE(fakeFolder.currentRemoteState(), expectedServerState);
+ // The local state should not have subFolderA
+ auto remoteState = fakeFolder.currentRemoteState();
+ remoteState.remove("parentFolder/subFolderA");
+ QCOMPARE(fakeFolder.currentLocalState(), remoteState);
+ }
+
+ // Rename parentFolder on the server
+ fakeFolder.remoteModifier().rename("parentFolder", "parentFolderRenamed");
+ expectedServerState = fakeFolder.currentRemoteState();
+ fakeFolder.syncOnce();
+
+ {
+ QCOMPARE(fakeFolder.currentRemoteState(), expectedServerState);
+ auto remoteState = fakeFolder.currentRemoteState();
+ // The subFolderA should still be there on the server.
+ QVERIFY(remoteState.find("parentFolderRenamed/subFolderA/fileA.txt"));
+ // But not on the client because of the selective sync
+ remoteState.remove("parentFolderRenamed/subFolderA");
+ QCOMPARE(fakeFolder.currentLocalState(), remoteState);
+ }
+
+ // Rename it again, locally this time.
+ fakeFolder.localModifier().rename("parentFolderRenamed", "parentThirdName");
+ fakeFolder.syncOnce();
+
+ {
+ auto remoteState = fakeFolder.currentRemoteState();
+ // The subFolderA should still be there on the server.
+ QVERIFY(remoteState.find("parentThirdName/subFolderA/fileA.txt"));
+ // But not on the client because of the selective sync
+ remoteState.remove("parentThirdName/subFolderA");
+ QCOMPARE(fakeFolder.currentLocalState(), remoteState);
+
+ expectedServerState = fakeFolder.currentRemoteState();
+ QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
+ fakeFolder.syncOnce(); // This sync should do nothing
+ QCOMPARE(completeSpy.count(), 0);
+
+ QCOMPARE(fakeFolder.currentRemoteState(), expectedServerState);
+ QCOMPARE(fakeFolder.currentLocalState(), remoteState);
+ }
+ }
+
+ void testLocalMoveDetection()
+ {
+ FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
+
+ int nPUT = 0;
+ int nDELETE = 0;
+ fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &) {
+ if (op == QNetworkAccessManager::PutOperation)
+ ++nPUT;
+ if (op == QNetworkAccessManager::DeleteOperation)
+ ++nDELETE;
+ return nullptr;
+ });
+
+ // For directly editing the remote checksum
+ FileInfo &remoteInfo = fakeFolder.remoteModifier();
+
+ // Simple move causing a remote rename
+ fakeFolder.localModifier().rename("A/a1", "A/a1m");
+ QVERIFY(fakeFolder.syncOnce());
+ QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
+ QCOMPARE(nPUT, 0);
+
+ // Move-and-change, causing a upload and delete
+ fakeFolder.localModifier().rename("A/a2", "A/a2m");
+ fakeFolder.localModifier().appendByte("A/a2m");
+ QVERIFY(fakeFolder.syncOnce());
+ QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
+ QCOMPARE(nPUT, 1);
+ QCOMPARE(nDELETE, 1);
+
+ // Move-and-change, mtime+content only
+ fakeFolder.localModifier().rename("B/b1", "B/b1m");
+ fakeFolder.localModifier().setContents("B/b1m", 'C');
+ QVERIFY(fakeFolder.syncOnce());
+ QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
+ QCOMPARE(nPUT, 2);
+ QCOMPARE(nDELETE, 2);
+
+ // Move-and-change, size+content only
+ auto mtime = fakeFolder.remoteModifier().find("B/b2")->lastModified;
+ fakeFolder.localModifier().rename("B/b2", "B/b2m");
+ fakeFolder.localModifier().appendByte("B/b2m");
+ fakeFolder.localModifier().setModTime("B/b2m", mtime);
+ QVERIFY(fakeFolder.syncOnce());
+ QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
+ QCOMPARE(nPUT, 3);
+ QCOMPARE(nDELETE, 3);
+
+ // Move-and-change, content only -- c1 has no checksum, so we fail to detect this!
+ // NOTE: This is an expected failure.
+ mtime = fakeFolder.remoteModifier().find("C/c1")->lastModified;
+ fakeFolder.localModifier().rename("C/c1", "C/c1m");
+ fakeFolder.localModifier().setContents("C/c1m", 'C');
+ fakeFolder.localModifier().setModTime("C/c1m", mtime);
+ QVERIFY(fakeFolder.syncOnce());
+ QCOMPARE(nPUT, 3);
+ QCOMPARE(nDELETE, 3);
+ QVERIFY(!(fakeFolder.currentLocalState() == remoteInfo));
+
+ // cleanup, and upload a file that will have a checksum in the db
+ fakeFolder.localModifier().remove("C/c1m");
+ fakeFolder.localModifier().insert("C/c3");
+ QVERIFY(fakeFolder.syncOnce());
+ QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
+ QCOMPARE(nPUT, 4);
+ QCOMPARE(nDELETE, 4);
+
+ // Move-and-change, content only, this time while having a checksum
+ mtime = fakeFolder.remoteModifier().find("C/c3")->lastModified;
+ fakeFolder.localModifier().rename("C/c3", "C/c3m");
+ fakeFolder.localModifier().setContents("C/c3m", 'C');
+ fakeFolder.localModifier().setModTime("C/c3m", mtime);
+ QVERIFY(fakeFolder.syncOnce());
+ QCOMPARE(nPUT, 5);
+ QCOMPARE(nDELETE, 5);
+ QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
+ }
+
+ // If the same folder is shared in two different ways with the same
+ // user, the target user will see duplicate file ids. We need to make
+ // sure the move detection and sync still do the right thing in that
+ // case.
+ void testDuplicateFileId()
+ {
+ FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
+ auto &remote = fakeFolder.remoteModifier();
+
+ remote.mkdir("A/W");
+ remote.insert("A/W/w1");
+ remote.mkdir("A/Q");
+
+ // Duplicate every entry in A under O/A
+ remote.mkdir("O");
+ remote.children["O"].addChild(remote.children["A"]);
+
+ // This already checks that the rename detection doesn't get
+ // horribly confused if we add new files that have the same
+ // fileid as existing ones
+ QVERIFY(fakeFolder.syncOnce());
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+
+ int nGET = 0;
+ fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &) {
+ if (op == QNetworkAccessManager::GetOperation)
+ ++nGET;
+ return nullptr;
+ });
+
+ // Try a remote file move
+ remote.rename("A/a1", "A/W/a1m");
+ remote.rename("O/A/a1", "O/A/W/a1m");
+ QVERIFY(fakeFolder.syncOnce());
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+ QCOMPARE(nGET, 0);
+
+ // And a remote directory move
+ remote.rename("A/W", "A/Q/W");
+ remote.rename("O/A/W", "O/A/Q/W");
+ QVERIFY(fakeFolder.syncOnce());
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+ QCOMPARE(nGET, 0);
+
+ // Partial file removal (in practice, A/a2 may be moved to O/a2, but we don't care)
+ remote.rename("O/A/a2", "O/a2");
+ remote.remove("A/a2");
+ QVERIFY(fakeFolder.syncOnce());
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+ QCOMPARE(nGET, 0);
+
+ // Local change plus remote move at the same time
+ fakeFolder.localModifier().appendByte("O/a2");
+ remote.rename("O/a2", "O/a3");
+ QVERIFY(fakeFolder.syncOnce());
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+ QCOMPARE(nGET, 1);
+ }
+
+ void testMovePropagation()
+ {
+ FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
+ auto &local = fakeFolder.localModifier();
+ auto &remote = fakeFolder.remoteModifier();
+
+ int nGET = 0;
+ int nPUT = 0;
+ int nMOVE = 0;
+ int nDELETE = 0;
+ fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &req) {
+ if (op == QNetworkAccessManager::GetOperation)
+ ++nGET;
+ if (op == QNetworkAccessManager::PutOperation)
+ ++nPUT;
+ if (op == QNetworkAccessManager::DeleteOperation)
+ ++nDELETE;
+ if (req.attribute(QNetworkRequest::CustomVerbAttribute) == "MOVE")
+ ++nMOVE;
+ return nullptr;
+ });
+ auto resetCounters = [&]() {
+ nGET = nPUT = nMOVE = nDELETE = 0;
+ };
+
+ // Move
+ {
+ resetCounters();
+ local.rename("A/a1", "A/a1m");
+ remote.rename("B/b1", "B/b1m");
+ QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
+ QVERIFY(fakeFolder.syncOnce());
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+ QCOMPARE(nGET, 0);
+ QCOMPARE(nPUT, 0);
+ QCOMPARE(nMOVE, 1);
+ QCOMPARE(nDELETE, 0);
+ QVERIFY(itemSuccessfulMove(completeSpy, "A/a1m"));
+ QVERIFY(itemSuccessfulMove(completeSpy, "B/b1m"));
+ }
+
+ // Touch+Move on same side
+ resetCounters();
+ local.rename("A/a2", "A/a2m");
+ local.setContents("A/a2m", 'A');
+ remote.rename("B/b2", "B/b2m");
+ remote.setContents("B/b2m", 'A');
+ QVERIFY(fakeFolder.syncOnce());
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+ QCOMPARE(nGET, 1);
+ QCOMPARE(nPUT, 1);
+ QCOMPARE(nMOVE, 0);
+ QCOMPARE(nDELETE, 1);
+ QCOMPARE(remote.find("A/a2m")->contentChar, 'A');
+ QCOMPARE(remote.find("B/b2m")->contentChar, 'A');
+
+ // Touch+Move on opposite sides
+ resetCounters();
+ local.rename("A/a1m", "A/a1m2");
+ remote.setContents("A/a1m", 'B');
+ remote.rename("B/b1m", "B/b1m2");
+ local.setContents("B/b1m", 'B');
+ QVERIFY(fakeFolder.syncOnce());
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+ QCOMPARE(nGET, 2);
+ QCOMPARE(nPUT, 2);
+ QCOMPARE(nMOVE, 0);
+ QCOMPARE(nDELETE, 0);
+ // All these files existing afterwards is debatable. Should we propagate
+ // the rename in one direction and grab the new contents in the other?
+ // Currently there's no propagation job that would do that, and this does
+ // at least not lose data.
+ QCOMPARE(remote.find("A/a1m")->contentChar, 'B');
+ QCOMPARE(remote.find("B/b1m")->contentChar, 'B');
+ QCOMPARE(remote.find("A/a1m2")->contentChar, 'W');
+ QCOMPARE(remote.find("B/b1m2")->contentChar, 'W');
+
+ // Touch+create on one side, move on the other
+ {
+ resetCounters();
+ local.appendByte("A/a1m");
+ local.insert("A/a1mt");
+ remote.rename("A/a1m", "A/a1mt");
+ remote.appendByte("B/b1m");
+ remote.insert("B/b1mt");
+ local.rename("B/b1m", "B/b1mt");
+ QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
+ QVERIFY(fakeFolder.syncOnce());
+ QVERIFY(expectAndWipeConflict(local, fakeFolder.currentLocalState(), "A/a1mt"));
+ QVERIFY(expectAndWipeConflict(local, fakeFolder.currentLocalState(), "B/b1mt"));
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+ QCOMPARE(nGET, 3);
+ QCOMPARE(nPUT, 1);
+ QCOMPARE(nMOVE, 0);
+ QCOMPARE(nDELETE, 0);
+ QVERIFY(itemSuccessful(completeSpy, "A/a1m", CSYNC_INSTRUCTION_NEW));
+ QVERIFY(itemSuccessful(completeSpy, "B/b1m", CSYNC_INSTRUCTION_NEW));
+ QVERIFY(itemConflict(completeSpy, "A/a1mt"));
+ QVERIFY(itemConflict(completeSpy, "B/b1mt"));
+ }
+
+ // Create new on one side, move to new on the other
+ {
+ resetCounters();
+ local.insert("A/a1N", 13);
+ remote.rename("A/a1mt", "A/a1N");
+ remote.insert("B/b1N", 13);
+ local.rename("B/b1mt", "B/b1N");
+ QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
+ QVERIFY(fakeFolder.syncOnce());
+ QVERIFY(expectAndWipeConflict(local, fakeFolder.currentLocalState(), "A/a1N"));
+ QVERIFY(expectAndWipeConflict(local, fakeFolder.currentLocalState(), "B/b1N"));
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+ QCOMPARE(nGET, 2);
+ QCOMPARE(nPUT, 0);
+ QCOMPARE(nMOVE, 0);
+ QCOMPARE(nDELETE, 1);
+ QVERIFY(itemSuccessful(completeSpy, "A/a1mt", CSYNC_INSTRUCTION_REMOVE));
+ QVERIFY(itemSuccessful(completeSpy, "B/b1mt", CSYNC_INSTRUCTION_REMOVE));
+ QVERIFY(itemConflict(completeSpy, "A/a1N"));
+ QVERIFY(itemConflict(completeSpy, "B/b1N"));
+ }
+
+ // Local move, remote move
+ resetCounters();
+ local.rename("C/c1", "C/c1mL");
+ remote.rename("C/c1", "C/c1mR");
+ QVERIFY(fakeFolder.syncOnce());
+ // end up with both files
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+ QCOMPARE(nGET, 1);
+ QCOMPARE(nPUT, 1);
+ QCOMPARE(nMOVE, 0);
+ QCOMPARE(nDELETE, 0);
+
+ // Rename/rename conflict on a folder
+ resetCounters();
+ remote.rename("C", "CMR");
+ local.rename("C", "CML");
+ QVERIFY(fakeFolder.syncOnce());
+ // End up with both folders
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+ QCOMPARE(nGET, 3); // 3 files in C
+ QCOMPARE(nPUT, 3);
+ QCOMPARE(nMOVE, 0);
+ QCOMPARE(nDELETE, 0);
+
+ // Folder move
+ {
+ resetCounters();
+ local.rename("A", "AM");
+ remote.rename("B", "BM");
+ QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
+ QVERIFY(fakeFolder.syncOnce());
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+ QCOMPARE(nGET, 0);
+ QCOMPARE(nPUT, 0);
+ QCOMPARE(nMOVE, 1);
+ QCOMPARE(nDELETE, 0);
+ QVERIFY(itemSuccessfulMove(completeSpy, "AM"));
+ QVERIFY(itemSuccessfulMove(completeSpy, "BM"));
+ }
+
+ // Folder move with contents touched on the same side
+ {
+ resetCounters();
+ local.setContents("AM/a2m", 'C');
+ local.rename("AM", "A2");
+ remote.setContents("BM/b2m", 'C');
+ remote.rename("BM", "B2");
+ QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
+ QVERIFY(fakeFolder.syncOnce());
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+ QCOMPARE(nGET, 1);
+ QCOMPARE(nPUT, 1);
+ QCOMPARE(nMOVE, 1);
+ QCOMPARE(nDELETE, 0);
+ QCOMPARE(remote.find("A2/a2m")->contentChar, 'C');
+ QCOMPARE(remote.find("B2/b2m")->contentChar, 'C');
+ QVERIFY(itemSuccessfulMove(completeSpy, "A2"));
+ QVERIFY(itemSuccessfulMove(completeSpy, "B2"));
+ }
+
+ // Folder rename with contents touched on the other tree
+ resetCounters();
+ remote.setContents("A2/a2m", 'D');
+ // setContents alone may not produce updated mtime if the test is fast
+ // and since we don't use checksums here, that matters.
+ remote.appendByte("A2/a2m");
+ local.rename("A2", "A3");
+ local.setContents("B2/b2m", 'D');
+ local.appendByte("B2/b2m");
+ remote.rename("B2", "B3");
+ QVERIFY(fakeFolder.syncOnce());
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+ QCOMPARE(nGET, 1);
+ QCOMPARE(nPUT, 1);
+ QCOMPARE(nMOVE, 1);
+ QCOMPARE(nDELETE, 0);
+ QCOMPARE(remote.find("A3/a2m")->contentChar, 'D');
+ QCOMPARE(remote.find("B3/b2m")->contentChar, 'D');
+
+ // Folder rename with contents touched on both ends
+ resetCounters();
+ remote.setContents("A3/a2m", 'R');
+ remote.appendByte("A3/a2m");
+ local.setContents("A3/a2m", 'L');
+ local.appendByte("A3/a2m");
+ local.appendByte("A3/a2m");
+ local.rename("A3", "A4");
+ remote.setContents("B3/b2m", 'R');
+ remote.appendByte("B3/b2m");
+ local.setContents("B3/b2m", 'L');
+ local.appendByte("B3/b2m");
+ local.appendByte("B3/b2m");
+ remote.rename("B3", "B4");
+ QVERIFY(fakeFolder.syncOnce());
+ auto currentLocal = fakeFolder.currentLocalState();
+ auto conflicts = findConflicts(currentLocal.children["A4"]);
+ QCOMPARE(conflicts.size(), 1);
+ for (auto c : conflicts) {
+ QCOMPARE(currentLocal.find(c)->contentChar, 'L');
+ local.remove(c);
+ }
+ conflicts = findConflicts(currentLocal.children["B4"]);
+ QCOMPARE(conflicts.size(), 1);
+ for (auto c : conflicts) {
+ QCOMPARE(currentLocal.find(c)->contentChar, 'L');
+ local.remove(c);
+ }
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+ QCOMPARE(nGET, 2);
+ QCOMPARE(nPUT, 0);
+ QCOMPARE(nMOVE, 1);
+ QCOMPARE(nDELETE, 0);
+ QCOMPARE(remote.find("A4/a2m")->contentChar, 'R');
+ QCOMPARE(remote.find("B4/b2m")->contentChar, 'R');
+
+ // Rename a folder and rename the contents at the same time
+ resetCounters();
+ local.rename("A4/a2m", "A4/a2m2");
+ local.rename("A4", "A5");
+ remote.rename("B4/b2m", "B4/b2m2");
+ remote.rename("B4", "B5");
+ QVERIFY(fakeFolder.syncOnce());
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+ QCOMPARE(nGET, 0);
+ QCOMPARE(nPUT, 0);
+ QCOMPARE(nMOVE, 2);
+ QCOMPARE(nDELETE, 0);
+ }
+
+ // Check interaction of moves with file type changes
+ void testMoveAndTypeChange()
+ {
+ FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
+ auto &local = fakeFolder.localModifier();
+ auto &remote = fakeFolder.remoteModifier();
+
+ // Touch on one side, rename and mkdir on the other
+ {
+ local.appendByte("A/a1");
+ remote.rename("A/a1", "A/a1mq");
+ remote.mkdir("A/a1");
+ remote.appendByte("B/b1");
+ local.rename("B/b1", "B/b1mq");
+ local.mkdir("B/b1");
+ QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
+ QVERIFY(fakeFolder.syncOnce());
+ // BUG: This doesn't behave right
+ //QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+ }
+ }
+};
+
+QTEST_GUILESS_MAIN(TestSyncMove)
+#include "testsyncmove.moc"