diff options
-rw-r--r-- | src/common/syncjournaldb.cpp | 13 | ||||
-rw-r--r-- | src/common/syncjournaldb.h | 2 | ||||
-rw-r--r-- | src/csync/csync_reconcile.cpp | 117 | ||||
-rw-r--r-- | src/csync/csync_update.cpp | 71 | ||||
-rw-r--r-- | test/CMakeLists.txt | 1 | ||||
-rw-r--r-- | test/syncenginetestutils.h | 20 | ||||
-rw-r--r-- | test/testsyncengine.cpp | 170 | ||||
-rw-r--r-- | test/testsyncmove.cpp | 569 |
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" |