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
path: root/test
diff options
context:
space:
mode:
authorChristian Kamm <mail@ckamm.de>2017-10-27 11:29:31 +0300
committerChristian Kamm <mail@ckamm.de>2017-10-27 11:29:31 +0300
commit05c1bfb6cf5a890572de41a59da5e719d7c0432c (patch)
tree52407f7a203fd90239ae029305b255daf063c1a9 /test
parent17126de5c703e935d2de9285a880211f0a120e35 (diff)
parent2d2ec2a57639c47f05d2534c5043b130a50fb673 (diff)
Merge remote-tracking branch 'origin/2.4'
Diffstat (limited to 'test')
-rw-r--r--test/CMakeLists.txt1
-rw-r--r--test/syncenginetestutils.h56
-rw-r--r--test/testchunkingng.cpp130
-rw-r--r--test/testsyncengine.cpp220
-rw-r--r--test/testsyncfilestatustracker.cpp3
-rw-r--r--test/testsyncmove.cpp569
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"