diff options
author | Christian Kamm <mail@ckamm.de> | 2017-10-27 11:29:31 +0300 |
---|---|---|
committer | Christian Kamm <mail@ckamm.de> | 2017-10-27 11:29:31 +0300 |
commit | 05c1bfb6cf5a890572de41a59da5e719d7c0432c (patch) | |
tree | 52407f7a203fd90239ae029305b255daf063c1a9 /test | |
parent | 17126de5c703e935d2de9285a880211f0a120e35 (diff) | |
parent | 2d2ec2a57639c47f05d2534c5043b130a50fb673 (diff) |
Merge remote-tracking branch 'origin/2.4'
Diffstat (limited to 'test')
-rw-r--r-- | test/CMakeLists.txt | 1 | ||||
-rw-r--r-- | test/syncenginetestutils.h | 56 | ||||
-rw-r--r-- | test/testchunkingng.cpp | 130 | ||||
-rw-r--r-- | test/testsyncengine.cpp | 220 | ||||
-rw-r--r-- | test/testsyncfilestatustracker.cpp | 3 | ||||
-rw-r--r-- | test/testsyncmove.cpp | 569 |
6 files changed, 816 insertions, 163 deletions
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 7327212ae..b62a6ee01 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 e061de222..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 { @@ -433,6 +436,8 @@ public: abort(); return; } + fileInfo->lastModified = OCC::Utility::qDateTimeFromTime_t(request.rawHeader("X-OC-Mtime").toLongLong()); + remoteRootFileInfo.find(fileName, /*invalidate_etags=*/true); QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection); } @@ -597,6 +602,9 @@ public: size -= len; return len; } + + // useful to be public for testing + using QNetworkReply::setRawHeader; }; @@ -606,8 +614,10 @@ class FakeChunkMoveReply : public QNetworkReply FileInfo *fileInfo; public: FakeChunkMoveReply(FileInfo &uploadsFileInfo, FileInfo &remoteRootFileInfo, - QNetworkAccessManager::Operation op, const QNetworkRequest &request, - QObject *parent) : QNetworkReply{parent} { + QNetworkAccessManager::Operation op, const QNetworkRequest &request, + quint64 delayMs, QObject *parent) + : QNetworkReply{ parent } + { setRequest(request); setUrl(request.url()); setOperation(op); @@ -662,7 +672,10 @@ public: abort(); return; } - QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection); + fileInfo->lastModified = OCC::Utility::qDateTimeFromTime_t(request.rawHeader("X-OC-Mtime").toLongLong()); + remoteRootFileInfo.find(fileName, /*invalidate_etags=*/true); + + QTimer::singleShot(delayMs, this, &FakeChunkMoveReply::respond); } Q_INVOKABLE void respond() { @@ -713,6 +726,24 @@ public: int _httpErrorCode; }; +// A reply that never responds +class FakeHangingReply : public QNetworkReply +{ + Q_OBJECT +public: + FakeHangingReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent) + : QNetworkReply(parent) + { + setRequest(request); + setUrl(request.url()); + setOperation(op); + open(QIODevice::ReadOnly); + } + + void abort() override {} + qint64 readData(char *, qint64) override { return 0; } +}; + class FakeQNAM : public QNetworkAccessManager { public: @@ -765,7 +796,7 @@ protected: else if (verb == QLatin1String("MOVE") && !isUpload) return new FakeMoveReply{info, op, request, this}; else if (verb == QLatin1String("MOVE") && isUpload) - return new FakeChunkMoveReply{info, _remoteRootFileInfo, op, request, this}; + return new FakeChunkMoveReply{ info, _remoteRootFileInfo, op, request, 0, this }; else { qDebug() << verb << outgoingData; Q_UNREACHABLE(); @@ -924,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/testchunkingng.cpp b/test/testchunkingng.cpp index ed30f8954..16c6856ac 100644 --- a/test/testchunkingng.cpp +++ b/test/testchunkingng.cpp @@ -85,6 +85,136 @@ private slots: QCOMPARE(fakeFolder.uploadState().children.first().name, chunkingId); } + // Check what happens when we abort during the final MOVE and the + // the final MOVE takes longer than the abort-delay + void testLateAbortHard() + { + FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() }; + fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ { "chunking", "1.0" } } }, { "checksums", QVariantMap{ { "supportedTypes", QStringList() << "SHA1" } } } }); + const int size = 150 * 1000 * 1000; + + // Make the MOVE never reply, but trigger a client-abort and apply the change remotely + auto parent = new QObject; + QByteArray moveChecksumHeader; + int nGET = 0; + int responseDelay = 10000; // bigger than abort-wait timeout + fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request) -> QNetworkReply * { + if (request.attribute(QNetworkRequest::CustomVerbAttribute) == "MOVE") { + QTimer::singleShot(50, parent, [&]() { fakeFolder.syncEngine().abort(); }); + moveChecksumHeader = request.rawHeader("OC-Checksum"); + return new FakeChunkMoveReply(fakeFolder.uploadState(), fakeFolder.remoteModifier(), op, request, responseDelay, parent); + } else if (op == QNetworkAccessManager::GetOperation) { + nGET++; + } + return nullptr; + }); + + + // Test 1: NEW file aborted + fakeFolder.localModifier().insert("A/a0", size); + QVERIFY(!fakeFolder.syncOnce()); // error: abort! + + // Now the next sync gets a NEW/NEW conflict and since there's no checksum + // it just becomes a UPDATE_METADATA + auto checkEtagUpdated = [&](SyncFileItemVector &items) { + QCOMPARE(items.size(), 1); + QCOMPARE(items[0]->_file, QLatin1String("A")); + SyncJournalFileRecord record; + QVERIFY(fakeFolder.syncJournal().getFileRecord(QByteArray("A/a0"), &record)); + QCOMPARE(record._etag, fakeFolder.remoteModifier().find("A/a0")->etag.toUtf8()); + }; + auto connection = connect(&fakeFolder.syncEngine(), &SyncEngine::aboutToPropagate, checkEtagUpdated); + QVERIFY(fakeFolder.syncOnce()); + disconnect(connection); + QCOMPARE(nGET, 0); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + + + // Test 2: modified file upload aborted + fakeFolder.localModifier().appendByte("A/a0"); + QVERIFY(!fakeFolder.syncOnce()); // error: abort! + + // An EVAL/EVAL conflict is also UPDATE_METADATA when there's no checksums + connection = connect(&fakeFolder.syncEngine(), &SyncEngine::aboutToPropagate, checkEtagUpdated); + QVERIFY(fakeFolder.syncOnce()); + disconnect(connection); + QCOMPARE(nGET, 0); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + + + // Test 3: modified file upload aborted, with good checksums + fakeFolder.localModifier().appendByte("A/a0"); + QVERIFY(!fakeFolder.syncOnce()); // error: abort! + + // Set the remote checksum -- the test setup doesn't do it automatically + QVERIFY(!moveChecksumHeader.isEmpty()); + fakeFolder.remoteModifier().find("A/a0")->checksums = moveChecksumHeader; + + // This time it's a real conflict, we have a remote checksum! + connection = connect(&fakeFolder.syncEngine(), &SyncEngine::aboutToPropagate, [&](SyncFileItemVector &items) { + SyncFileItemPtr a0; + for (auto &item : items) { + if (item->_file == "A/a0") + a0 = item; + } + + QVERIFY(a0); + QCOMPARE(a0->_instruction, CSYNC_INSTRUCTION_CONFLICT); + }); + QVERIFY(fakeFolder.syncOnce()); + disconnect(connection); + QCOMPARE(nGET, 0); // no new download, just a metadata update! + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + + + // Test 4: New file, that gets deleted locally before the next sync + fakeFolder.localModifier().insert("A/a3", size); + QVERIFY(!fakeFolder.syncOnce()); // error: abort! + fakeFolder.localModifier().remove("A/a3"); + + // bug: in this case we must expect a re-download of A/A3 + QVERIFY(fakeFolder.syncOnce()); + QCOMPARE(nGET, 1); + QVERIFY(fakeFolder.currentLocalState().find("A/a3")); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + } + + // Check what happens when we abort during the final MOVE and the + // the final MOVE is short enough for the abort-delay to help + void testLateAbortRecoverable() + { + FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() }; + fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ { "chunking", "1.0" } } }, { "checksums", QVariantMap{ { "supportedTypes", QStringList() << "SHA1" } } } }); + const int size = 150 * 1000 * 1000; + + // Make the MOVE never reply, but trigger a client-abort and apply the change remotely + auto parent = new QObject; + QByteArray moveChecksumHeader; + int nGET = 0; + int responseDelay = 2000; // smaller than abort-wait timeout + fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request) -> QNetworkReply * { + if (request.attribute(QNetworkRequest::CustomVerbAttribute) == "MOVE") { + QTimer::singleShot(50, parent, [&]() { fakeFolder.syncEngine().abort(); }); + moveChecksumHeader = request.rawHeader("OC-Checksum"); + return new FakeChunkMoveReply(fakeFolder.uploadState(), fakeFolder.remoteModifier(), op, request, responseDelay, parent); + } else if (op == QNetworkAccessManager::GetOperation) { + nGET++; + } + return nullptr; + }); + + + // Test 1: NEW file aborted + fakeFolder.localModifier().insert("A/a0", size); + QVERIFY(fakeFolder.syncOnce()); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + + // Test 2: modified file upload aborted + fakeFolder.localModifier().appendByte("A/a0"); + QVERIFY(fakeFolder.syncOnce()); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + } + // We modify the file locally after it has been partially uploaded void testRemoveStale1() { diff --git a/test/testsyncengine.cpp b/test/testsyncengine.cpp index 139fbf42b..e68fed8bb 100644 --- a/test/testsyncengine.cpp +++ b/test/testsyncengine.cpp @@ -16,7 +16,7 @@ bool itemDidComplete(const QSignalSpy &spy, const QString &path) for(const QList<QVariant> &args : spy) { auto item = args[0].value<SyncFileItemPtr>(); if (item->destination() == path) - return true; + return item->_instruction != CSYNC_INSTRUCTION_NONE && item->_instruction != CSYNC_INSTRUCTION_UPDATE_METADATA; } return false; } @@ -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,82 +444,88 @@ private slots: QCOMPARE(n507, 3); } - void testLocalMove() + // Checks whether downloads with bad checksums are accepted + void testChecksumValidation() { FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() }; + QObject parent; - int nPUT = 0; - int nDELETE = 0; - fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &) { - if (op == QNetworkAccessManager::PutOperation) - ++nPUT; - if (op == QNetworkAccessManager::DeleteOperation) - ++nDELETE; + QByteArray checksumValue; + QByteArray contentMd5Value; + + fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request) -> QNetworkReply * { + if (op == QNetworkAccessManager::GetOperation) { + auto reply = new FakeGetReply(fakeFolder.remoteModifier(), op, request, &parent); + if (!checksumValue.isNull()) + reply->setRawHeader("OC-Checksum", checksumValue); + if (!contentMd5Value.isNull()) + reply->setRawHeader("Content-MD5", contentMd5Value); + return reply; + } 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"); + // Basic case + fakeFolder.remoteModifier().create("A/a3", 16, 'A'); QVERIFY(fakeFolder.syncOnce()); - QCOMPARE(fakeFolder.currentLocalState(), remoteInfo); - QCOMPARE(nPUT, 0); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + + // Bad OC-Checksum + checksumValue = "SHA1:bad"; + fakeFolder.remoteModifier().create("A/a4", 16, 'A'); + QVERIFY(!fakeFolder.syncOnce()); - // Move-and-change, causing a upload and delete - fakeFolder.localModifier().rename("A/a2", "A/a2m"); - fakeFolder.localModifier().appendByte("A/a2m"); + // Good OC-Checksum + checksumValue = "SHA1:19b1928d58a2030d08023f3d7054516dbc186f20"; // printf 'A%.0s' {1..16} | sha1sum - QVERIFY(fakeFolder.syncOnce()); - QCOMPARE(fakeFolder.currentLocalState(), remoteInfo); - QCOMPARE(nPUT, 1); - QCOMPARE(nDELETE, 1); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + checksumValue = QByteArray(); + + // Bad Content-MD5 + contentMd5Value = "bad"; + fakeFolder.remoteModifier().create("A/a5", 16, 'A'); + QVERIFY(!fakeFolder.syncOnce()); - // Move-and-change, mtime+content only - fakeFolder.localModifier().rename("B/b1", "B/b1m"); - fakeFolder.localModifier().setContents("B/b1m", 'C'); + // Good Content-MD5 + contentMd5Value = "d8a73157ce10cd94a91c2079fc9a92c8"; // printf 'A%.0s' {1..16} | md5sum - 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); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + + // OC-Checksum has preference + checksumValue = "garbage"; + // contentMd5Value is still good + fakeFolder.remoteModifier().create("A/a6", 16, 'A'); + QVERIFY(!fakeFolder.syncOnce()); + } + + // Tests the behavior of invalid filename detection + void testInvalidFilenameRegex() + { + FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() }; + + // For current servers, no characters are forbidden + fakeFolder.syncEngine().account()->setServerVersion("10.0.0"); + fakeFolder.localModifier().insert("A/\\:?*\"<>|.txt"); 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); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + + // For legacy servers, some characters were forbidden by the client + fakeFolder.syncEngine().account()->setServerVersion("8.0.0"); + fakeFolder.localModifier().insert("B/\\:?*\"<>|.txt"); QVERIFY(fakeFolder.syncOnce()); - QCOMPARE(nPUT, 3); - QCOMPARE(nDELETE, 3); - QVERIFY(!(fakeFolder.currentLocalState() == remoteInfo)); + QVERIFY(!fakeFolder.currentRemoteState().find("B/\\:?*\"<>|.txt")); - // cleanup, and upload a file that will have a checksum in the db - fakeFolder.localModifier().remove("C/c1m"); - fakeFolder.localModifier().insert("C/c3"); + // We can override that by setting the capability + fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ { "invalidFilenameRegex", "" } } } }); 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); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + + // Check that new servers also accept the capability + fakeFolder.syncEngine().account()->setServerVersion("10.0.0"); + fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ { "invalidFilenameRegex", "my[fgh]ile" } } } }); + fakeFolder.localModifier().insert("C/myfile.txt"); QVERIFY(fakeFolder.syncOnce()); - QCOMPARE(nPUT, 5); - QCOMPARE(nDELETE, 5); - QCOMPARE(fakeFolder.currentLocalState(), remoteInfo); + QVERIFY(!fakeFolder.currentRemoteState().find("C/myfile.txt")); } // Check correct behavior when local discovery is partially drawn from the db diff --git a/test/testsyncfilestatustracker.cpp b/test/testsyncfilestatustracker.cpp index ed828857d..bbc80f386 100644 --- a/test/testsyncfilestatustracker.cpp +++ b/test/testsyncfilestatustracker.cpp @@ -436,6 +436,8 @@ private slots: fakeFolder.remoteModifier().appendByte("S/s1"); fakeFolder.remoteModifier().insert("B/b3"); fakeFolder.remoteModifier().find("B/b3")->extraDavProperties = "<oc:share-types><oc:share-type>0</oc:share-type></oc:share-types>"; + fakeFolder.remoteModifier().find("A/a1")->isShared = true; // becomes shared + fakeFolder.remoteModifier().find("A", true); // change the etags of the parent StatusPushSpy statusSpy(fakeFolder.syncEngine()); @@ -458,6 +460,7 @@ private slots: QCOMPARE(statusSpy.statusOf("S/s1"), sharedUpToDateStatus); QCOMPARE(statusSpy.statusOf("B/b1").shared(), false); QCOMPARE(statusSpy.statusOf("B/b3"), sharedUpToDateStatus); + QCOMPARE(statusSpy.statusOf("A/a1"), sharedUpToDateStatus); QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); } 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" |