testsyncmove.cpp 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582
  1. /*
  2. * This software is in the public domain, furnished "as is", without technical
  3. * support, and with no warranty, express or implied, as to its usefulness for
  4. * any purpose.
  5. *
  6. */
  7. #include <QtTest>
  8. #include "syncenginetestutils.h"
  9. #include <syncengine.h>
  10. using namespace OCC;
  11. SyncFileItemPtr findItem(const QSignalSpy &spy, const QString &path)
  12. {
  13. for (const QList<QVariant> &args : spy) {
  14. auto item = args[0].value<SyncFileItemPtr>();
  15. if (item->destination() == path)
  16. return item;
  17. }
  18. return SyncFileItemPtr(new SyncFileItem);
  19. }
  20. bool itemSuccessful(const QSignalSpy &spy, const QString &path, const csync_instructions_e instr)
  21. {
  22. auto item = findItem(spy, path);
  23. return item->_status == SyncFileItem::Success && item->_instruction == instr;
  24. }
  25. bool itemConflict(const QSignalSpy &spy, const QString &path)
  26. {
  27. auto item = findItem(spy, path);
  28. return item->_status == SyncFileItem::Conflict && item->_instruction == CSYNC_INSTRUCTION_CONFLICT;
  29. }
  30. bool itemSuccessfulMove(const QSignalSpy &spy, const QString &path)
  31. {
  32. return itemSuccessful(spy, path, CSYNC_INSTRUCTION_RENAME);
  33. }
  34. QStringList findConflicts(const FileInfo &dir)
  35. {
  36. QStringList conflicts;
  37. for (const auto &item : dir.children) {
  38. if (item.name.contains("(conflicted copy")) {
  39. conflicts.append(item.path());
  40. }
  41. }
  42. return conflicts;
  43. }
  44. bool expectAndWipeConflict(FileModifier &local, FileInfo state, const QString path)
  45. {
  46. PathComponents pathComponents(path);
  47. auto base = state.find(pathComponents.parentDirComponents());
  48. if (!base)
  49. return false;
  50. for (const auto &item : base->children) {
  51. if (item.name.startsWith(pathComponents.fileName()) && item.name.contains("(conflicted copy")) {
  52. local.remove(item.path());
  53. return true;
  54. }
  55. }
  56. return false;
  57. }
  58. class TestSyncMove : public QObject
  59. {
  60. Q_OBJECT
  61. private slots:
  62. void testRemoteChangeInMovedFolder()
  63. {
  64. // issue #5192
  65. FakeFolder fakeFolder{ FileInfo{ QString(), { FileInfo{ QStringLiteral("folder"), { FileInfo{ QStringLiteral("folderA"), { { QStringLiteral("file.txt"), 400 } } }, QStringLiteral("folderB") } } } } };
  66. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  67. // Edit a file in a moved directory.
  68. fakeFolder.remoteModifier().setContents("folder/folderA/file.txt", 'a');
  69. fakeFolder.remoteModifier().rename("folder/folderA", "folder/folderB/folderA");
  70. fakeFolder.syncOnce();
  71. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  72. auto oldState = fakeFolder.currentLocalState();
  73. QVERIFY(oldState.find("folder/folderB/folderA/file.txt"));
  74. QVERIFY(!oldState.find("folder/folderA/file.txt"));
  75. // This sync should not remove the file
  76. fakeFolder.syncOnce();
  77. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  78. QCOMPARE(fakeFolder.currentLocalState(), oldState);
  79. }
  80. void testSelectiveSyncMovedFolder()
  81. {
  82. // issue #5224
  83. FakeFolder fakeFolder{ FileInfo{ QString(), { FileInfo{ QStringLiteral("parentFolder"), { FileInfo{ QStringLiteral("subFolderA"), { { QStringLiteral("fileA.txt"), 400 } } }, FileInfo{ QStringLiteral("subFolderB"), { { QStringLiteral("fileB.txt"), 400 } } } } } } } };
  84. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  85. auto expectedServerState = fakeFolder.currentRemoteState();
  86. // Remove subFolderA with selectiveSync:
  87. fakeFolder.syncEngine().journal()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList,
  88. { "parentFolder/subFolderA/" });
  89. fakeFolder.syncEngine().journal()->avoidReadFromDbOnNextSync(QByteArrayLiteral("parentFolder/subFolderA/"));
  90. fakeFolder.syncOnce();
  91. {
  92. // Nothing changed on the server
  93. QCOMPARE(fakeFolder.currentRemoteState(), expectedServerState);
  94. // The local state should not have subFolderA
  95. auto remoteState = fakeFolder.currentRemoteState();
  96. remoteState.remove("parentFolder/subFolderA");
  97. QCOMPARE(fakeFolder.currentLocalState(), remoteState);
  98. }
  99. // Rename parentFolder on the server
  100. fakeFolder.remoteModifier().rename("parentFolder", "parentFolderRenamed");
  101. expectedServerState = fakeFolder.currentRemoteState();
  102. fakeFolder.syncOnce();
  103. {
  104. QCOMPARE(fakeFolder.currentRemoteState(), expectedServerState);
  105. auto remoteState = fakeFolder.currentRemoteState();
  106. // The subFolderA should still be there on the server.
  107. QVERIFY(remoteState.find("parentFolderRenamed/subFolderA/fileA.txt"));
  108. // But not on the client because of the selective sync
  109. remoteState.remove("parentFolderRenamed/subFolderA");
  110. QCOMPARE(fakeFolder.currentLocalState(), remoteState);
  111. }
  112. // Rename it again, locally this time.
  113. fakeFolder.localModifier().rename("parentFolderRenamed", "parentThirdName");
  114. fakeFolder.syncOnce();
  115. {
  116. auto remoteState = fakeFolder.currentRemoteState();
  117. // The subFolderA should still be there on the server.
  118. QVERIFY(remoteState.find("parentThirdName/subFolderA/fileA.txt"));
  119. // But not on the client because of the selective sync
  120. remoteState.remove("parentThirdName/subFolderA");
  121. QCOMPARE(fakeFolder.currentLocalState(), remoteState);
  122. expectedServerState = fakeFolder.currentRemoteState();
  123. QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
  124. fakeFolder.syncOnce(); // This sync should do nothing
  125. QCOMPARE(completeSpy.count(), 0);
  126. QCOMPARE(fakeFolder.currentRemoteState(), expectedServerState);
  127. QCOMPARE(fakeFolder.currentLocalState(), remoteState);
  128. }
  129. }
  130. void testLocalMoveDetection()
  131. {
  132. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  133. int nPUT = 0;
  134. int nDELETE = 0;
  135. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &, QIODevice *) {
  136. if (op == QNetworkAccessManager::PutOperation)
  137. ++nPUT;
  138. if (op == QNetworkAccessManager::DeleteOperation)
  139. ++nDELETE;
  140. return nullptr;
  141. });
  142. // For directly editing the remote checksum
  143. FileInfo &remoteInfo = fakeFolder.remoteModifier();
  144. // Simple move causing a remote rename
  145. fakeFolder.localModifier().rename("A/a1", "A/a1m");
  146. QVERIFY(fakeFolder.syncOnce());
  147. QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
  148. QCOMPARE(nPUT, 0);
  149. // Move-and-change, causing a upload and delete
  150. fakeFolder.localModifier().rename("A/a2", "A/a2m");
  151. fakeFolder.localModifier().appendByte("A/a2m");
  152. QVERIFY(fakeFolder.syncOnce());
  153. QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
  154. QCOMPARE(nPUT, 1);
  155. QCOMPARE(nDELETE, 1);
  156. // Move-and-change, mtime+content only
  157. fakeFolder.localModifier().rename("B/b1", "B/b1m");
  158. fakeFolder.localModifier().setContents("B/b1m", 'C');
  159. QVERIFY(fakeFolder.syncOnce());
  160. QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
  161. QCOMPARE(nPUT, 2);
  162. QCOMPARE(nDELETE, 2);
  163. // Move-and-change, size+content only
  164. auto mtime = fakeFolder.remoteModifier().find("B/b2")->lastModified;
  165. fakeFolder.localModifier().rename("B/b2", "B/b2m");
  166. fakeFolder.localModifier().appendByte("B/b2m");
  167. fakeFolder.localModifier().setModTime("B/b2m", mtime);
  168. QVERIFY(fakeFolder.syncOnce());
  169. QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
  170. QCOMPARE(nPUT, 3);
  171. QCOMPARE(nDELETE, 3);
  172. // Move-and-change, content only -- c1 has no checksum, so we fail to detect this!
  173. // NOTE: This is an expected failure.
  174. mtime = fakeFolder.remoteModifier().find("C/c1")->lastModified;
  175. fakeFolder.localModifier().rename("C/c1", "C/c1m");
  176. fakeFolder.localModifier().setContents("C/c1m", 'C');
  177. fakeFolder.localModifier().setModTime("C/c1m", mtime);
  178. QVERIFY(fakeFolder.syncOnce());
  179. QCOMPARE(nPUT, 3);
  180. QCOMPARE(nDELETE, 3);
  181. QVERIFY(!(fakeFolder.currentLocalState() == remoteInfo));
  182. // cleanup, and upload a file that will have a checksum in the db
  183. fakeFolder.localModifier().remove("C/c1m");
  184. fakeFolder.localModifier().insert("C/c3");
  185. QVERIFY(fakeFolder.syncOnce());
  186. QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
  187. QCOMPARE(nPUT, 4);
  188. QCOMPARE(nDELETE, 4);
  189. // Move-and-change, content only, this time while having a checksum
  190. mtime = fakeFolder.remoteModifier().find("C/c3")->lastModified;
  191. fakeFolder.localModifier().rename("C/c3", "C/c3m");
  192. fakeFolder.localModifier().setContents("C/c3m", 'C');
  193. fakeFolder.localModifier().setModTime("C/c3m", mtime);
  194. QVERIFY(fakeFolder.syncOnce());
  195. QCOMPARE(nPUT, 5);
  196. QCOMPARE(nDELETE, 5);
  197. QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
  198. }
  199. void testDuplicateFileId_data()
  200. {
  201. QTest::addColumn<QString>("prefix");
  202. // There have been bugs related to how the original
  203. // folder and the folder with the duplicate tree are
  204. // ordered. Test both cases here.
  205. QTest::newRow("first ordering") << "O"; // "O" > "A"
  206. QTest::newRow("second ordering") << "0"; // "0" < "A"
  207. }
  208. // If the same folder is shared in two different ways with the same
  209. // user, the target user will see duplicate file ids. We need to make
  210. // sure the move detection and sync still do the right thing in that
  211. // case.
  212. void testDuplicateFileId()
  213. {
  214. QFETCH(QString, prefix);
  215. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  216. auto &remote = fakeFolder.remoteModifier();
  217. remote.mkdir("A/W");
  218. remote.insert("A/W/w1");
  219. remote.mkdir("A/Q");
  220. // Duplicate every entry in A under O/A
  221. remote.mkdir(prefix);
  222. remote.children[prefix].addChild(remote.children["A"]);
  223. // This already checks that the rename detection doesn't get
  224. // horribly confused if we add new files that have the same
  225. // fileid as existing ones
  226. QVERIFY(fakeFolder.syncOnce());
  227. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  228. int nGET = 0;
  229. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &, QIODevice *) {
  230. if (op == QNetworkAccessManager::GetOperation)
  231. ++nGET;
  232. return nullptr;
  233. });
  234. // Try a remote file move
  235. remote.rename("A/a1", "A/W/a1m");
  236. remote.rename(prefix + "/A/a1", prefix + "/A/W/a1m");
  237. QVERIFY(fakeFolder.syncOnce());
  238. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  239. QCOMPARE(nGET, 0);
  240. // And a remote directory move
  241. remote.rename("A/W", "A/Q/W");
  242. remote.rename(prefix + "/A/W", prefix + "/A/Q/W");
  243. QVERIFY(fakeFolder.syncOnce());
  244. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  245. QCOMPARE(nGET, 0);
  246. // Partial file removal (in practice, A/a2 may be moved to O/a2, but we don't care)
  247. remote.rename(prefix + "/A/a2", prefix + "/a2");
  248. remote.remove("A/a2");
  249. QVERIFY(fakeFolder.syncOnce());
  250. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  251. QCOMPARE(nGET, 0);
  252. // Local change plus remote move at the same time
  253. fakeFolder.localModifier().appendByte(prefix + "/a2");
  254. remote.rename(prefix + "/a2", prefix + "/a3");
  255. QVERIFY(fakeFolder.syncOnce());
  256. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  257. QCOMPARE(nGET, 1);
  258. }
  259. void testMovePropagation()
  260. {
  261. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  262. auto &local = fakeFolder.localModifier();
  263. auto &remote = fakeFolder.remoteModifier();
  264. int nGET = 0;
  265. int nPUT = 0;
  266. int nMOVE = 0;
  267. int nDELETE = 0;
  268. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *) {
  269. if (op == QNetworkAccessManager::GetOperation)
  270. ++nGET;
  271. if (op == QNetworkAccessManager::PutOperation)
  272. ++nPUT;
  273. if (op == QNetworkAccessManager::DeleteOperation)
  274. ++nDELETE;
  275. if (req.attribute(QNetworkRequest::CustomVerbAttribute) == "MOVE")
  276. ++nMOVE;
  277. return nullptr;
  278. });
  279. auto resetCounters = [&]() {
  280. nGET = nPUT = nMOVE = nDELETE = 0;
  281. };
  282. // Move
  283. {
  284. resetCounters();
  285. local.rename("A/a1", "A/a1m");
  286. remote.rename("B/b1", "B/b1m");
  287. QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
  288. QVERIFY(fakeFolder.syncOnce());
  289. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  290. QCOMPARE(nGET, 0);
  291. QCOMPARE(nPUT, 0);
  292. QCOMPARE(nMOVE, 1);
  293. QCOMPARE(nDELETE, 0);
  294. QVERIFY(itemSuccessfulMove(completeSpy, "A/a1m"));
  295. QVERIFY(itemSuccessfulMove(completeSpy, "B/b1m"));
  296. }
  297. // Touch+Move on same side
  298. resetCounters();
  299. local.rename("A/a2", "A/a2m");
  300. local.setContents("A/a2m", 'A');
  301. remote.rename("B/b2", "B/b2m");
  302. remote.setContents("B/b2m", 'A');
  303. QVERIFY(fakeFolder.syncOnce());
  304. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  305. QCOMPARE(nGET, 1);
  306. QCOMPARE(nPUT, 1);
  307. QCOMPARE(nMOVE, 0);
  308. QCOMPARE(nDELETE, 1);
  309. QCOMPARE(remote.find("A/a2m")->contentChar, 'A');
  310. QCOMPARE(remote.find("B/b2m")->contentChar, 'A');
  311. // Touch+Move on opposite sides
  312. resetCounters();
  313. local.rename("A/a1m", "A/a1m2");
  314. remote.setContents("A/a1m", 'B');
  315. remote.rename("B/b1m", "B/b1m2");
  316. local.setContents("B/b1m", 'B');
  317. QVERIFY(fakeFolder.syncOnce());
  318. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  319. QCOMPARE(nGET, 2);
  320. QCOMPARE(nPUT, 2);
  321. QCOMPARE(nMOVE, 0);
  322. QCOMPARE(nDELETE, 0);
  323. // All these files existing afterwards is debatable. Should we propagate
  324. // the rename in one direction and grab the new contents in the other?
  325. // Currently there's no propagation job that would do that, and this does
  326. // at least not lose data.
  327. QCOMPARE(remote.find("A/a1m")->contentChar, 'B');
  328. QCOMPARE(remote.find("B/b1m")->contentChar, 'B');
  329. QCOMPARE(remote.find("A/a1m2")->contentChar, 'W');
  330. QCOMPARE(remote.find("B/b1m2")->contentChar, 'W');
  331. // Touch+create on one side, move on the other
  332. {
  333. resetCounters();
  334. local.appendByte("A/a1m");
  335. local.insert("A/a1mt");
  336. remote.rename("A/a1m", "A/a1mt");
  337. remote.appendByte("B/b1m");
  338. remote.insert("B/b1mt");
  339. local.rename("B/b1m", "B/b1mt");
  340. QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
  341. QVERIFY(fakeFolder.syncOnce());
  342. QVERIFY(expectAndWipeConflict(local, fakeFolder.currentLocalState(), "A/a1mt"));
  343. QVERIFY(expectAndWipeConflict(local, fakeFolder.currentLocalState(), "B/b1mt"));
  344. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  345. QCOMPARE(nGET, 3);
  346. QCOMPARE(nPUT, 1);
  347. QCOMPARE(nMOVE, 0);
  348. QCOMPARE(nDELETE, 0);
  349. QVERIFY(itemSuccessful(completeSpy, "A/a1m", CSYNC_INSTRUCTION_NEW));
  350. QVERIFY(itemSuccessful(completeSpy, "B/b1m", CSYNC_INSTRUCTION_NEW));
  351. QVERIFY(itemConflict(completeSpy, "A/a1mt"));
  352. QVERIFY(itemConflict(completeSpy, "B/b1mt"));
  353. }
  354. // Create new on one side, move to new on the other
  355. {
  356. resetCounters();
  357. local.insert("A/a1N", 13);
  358. remote.rename("A/a1mt", "A/a1N");
  359. remote.insert("B/b1N", 13);
  360. local.rename("B/b1mt", "B/b1N");
  361. QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
  362. QVERIFY(fakeFolder.syncOnce());
  363. QVERIFY(expectAndWipeConflict(local, fakeFolder.currentLocalState(), "A/a1N"));
  364. QVERIFY(expectAndWipeConflict(local, fakeFolder.currentLocalState(), "B/b1N"));
  365. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  366. QCOMPARE(nGET, 2);
  367. QCOMPARE(nPUT, 0);
  368. QCOMPARE(nMOVE, 0);
  369. QCOMPARE(nDELETE, 1);
  370. QVERIFY(itemSuccessful(completeSpy, "A/a1mt", CSYNC_INSTRUCTION_REMOVE));
  371. QVERIFY(itemSuccessful(completeSpy, "B/b1mt", CSYNC_INSTRUCTION_REMOVE));
  372. QVERIFY(itemConflict(completeSpy, "A/a1N"));
  373. QVERIFY(itemConflict(completeSpy, "B/b1N"));
  374. }
  375. // Local move, remote move
  376. resetCounters();
  377. local.rename("C/c1", "C/c1mL");
  378. remote.rename("C/c1", "C/c1mR");
  379. QVERIFY(fakeFolder.syncOnce());
  380. // end up with both files
  381. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  382. QCOMPARE(nGET, 1);
  383. QCOMPARE(nPUT, 1);
  384. QCOMPARE(nMOVE, 0);
  385. QCOMPARE(nDELETE, 0);
  386. // Rename/rename conflict on a folder
  387. resetCounters();
  388. remote.rename("C", "CMR");
  389. local.rename("C", "CML");
  390. QVERIFY(fakeFolder.syncOnce());
  391. // End up with both folders
  392. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  393. QCOMPARE(nGET, 3); // 3 files in C
  394. QCOMPARE(nPUT, 3);
  395. QCOMPARE(nMOVE, 0);
  396. QCOMPARE(nDELETE, 0);
  397. // Folder move
  398. {
  399. resetCounters();
  400. local.rename("A", "AM");
  401. remote.rename("B", "BM");
  402. QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
  403. QVERIFY(fakeFolder.syncOnce());
  404. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  405. QCOMPARE(nGET, 0);
  406. QCOMPARE(nPUT, 0);
  407. QCOMPARE(nMOVE, 1);
  408. QCOMPARE(nDELETE, 0);
  409. QVERIFY(itemSuccessfulMove(completeSpy, "AM"));
  410. QVERIFY(itemSuccessfulMove(completeSpy, "BM"));
  411. }
  412. // Folder move with contents touched on the same side
  413. {
  414. resetCounters();
  415. local.setContents("AM/a2m", 'C');
  416. local.rename("AM", "A2");
  417. remote.setContents("BM/b2m", 'C');
  418. remote.rename("BM", "B2");
  419. QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
  420. QVERIFY(fakeFolder.syncOnce());
  421. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  422. QCOMPARE(nGET, 1);
  423. QCOMPARE(nPUT, 1);
  424. QCOMPARE(nMOVE, 1);
  425. QCOMPARE(nDELETE, 0);
  426. QCOMPARE(remote.find("A2/a2m")->contentChar, 'C');
  427. QCOMPARE(remote.find("B2/b2m")->contentChar, 'C');
  428. QVERIFY(itemSuccessfulMove(completeSpy, "A2"));
  429. QVERIFY(itemSuccessfulMove(completeSpy, "B2"));
  430. }
  431. // Folder rename with contents touched on the other tree
  432. resetCounters();
  433. remote.setContents("A2/a2m", 'D');
  434. // setContents alone may not produce updated mtime if the test is fast
  435. // and since we don't use checksums here, that matters.
  436. remote.appendByte("A2/a2m");
  437. local.rename("A2", "A3");
  438. local.setContents("B2/b2m", 'D');
  439. local.appendByte("B2/b2m");
  440. remote.rename("B2", "B3");
  441. QVERIFY(fakeFolder.syncOnce());
  442. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  443. QCOMPARE(nGET, 1);
  444. QCOMPARE(nPUT, 1);
  445. QCOMPARE(nMOVE, 1);
  446. QCOMPARE(nDELETE, 0);
  447. QCOMPARE(remote.find("A3/a2m")->contentChar, 'D');
  448. QCOMPARE(remote.find("B3/b2m")->contentChar, 'D');
  449. // Folder rename with contents touched on both ends
  450. resetCounters();
  451. remote.setContents("A3/a2m", 'R');
  452. remote.appendByte("A3/a2m");
  453. local.setContents("A3/a2m", 'L');
  454. local.appendByte("A3/a2m");
  455. local.appendByte("A3/a2m");
  456. local.rename("A3", "A4");
  457. remote.setContents("B3/b2m", 'R');
  458. remote.appendByte("B3/b2m");
  459. local.setContents("B3/b2m", 'L');
  460. local.appendByte("B3/b2m");
  461. local.appendByte("B3/b2m");
  462. remote.rename("B3", "B4");
  463. QVERIFY(fakeFolder.syncOnce());
  464. auto currentLocal = fakeFolder.currentLocalState();
  465. auto conflicts = findConflicts(currentLocal.children["A4"]);
  466. QCOMPARE(conflicts.size(), 1);
  467. for (auto c : conflicts) {
  468. QCOMPARE(currentLocal.find(c)->contentChar, 'L');
  469. local.remove(c);
  470. }
  471. conflicts = findConflicts(currentLocal.children["B4"]);
  472. QCOMPARE(conflicts.size(), 1);
  473. for (auto c : conflicts) {
  474. QCOMPARE(currentLocal.find(c)->contentChar, 'L');
  475. local.remove(c);
  476. }
  477. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  478. QCOMPARE(nGET, 2);
  479. QCOMPARE(nPUT, 0);
  480. QCOMPARE(nMOVE, 1);
  481. QCOMPARE(nDELETE, 0);
  482. QCOMPARE(remote.find("A4/a2m")->contentChar, 'R');
  483. QCOMPARE(remote.find("B4/b2m")->contentChar, 'R');
  484. // Rename a folder and rename the contents at the same time
  485. resetCounters();
  486. local.rename("A4/a2m", "A4/a2m2");
  487. local.rename("A4", "A5");
  488. remote.rename("B4/b2m", "B4/b2m2");
  489. remote.rename("B4", "B5");
  490. QVERIFY(fakeFolder.syncOnce());
  491. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  492. QCOMPARE(nGET, 0);
  493. QCOMPARE(nPUT, 0);
  494. QCOMPARE(nMOVE, 2);
  495. QCOMPARE(nDELETE, 0);
  496. }
  497. // Check interaction of moves with file type changes
  498. void testMoveAndTypeChange()
  499. {
  500. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  501. auto &local = fakeFolder.localModifier();
  502. auto &remote = fakeFolder.remoteModifier();
  503. // Touch on one side, rename and mkdir on the other
  504. {
  505. local.appendByte("A/a1");
  506. remote.rename("A/a1", "A/a1mq");
  507. remote.mkdir("A/a1");
  508. remote.appendByte("B/b1");
  509. local.rename("B/b1", "B/b1mq");
  510. local.mkdir("B/b1");
  511. QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
  512. QVERIFY(fakeFolder.syncOnce());
  513. // BUG: This doesn't behave right
  514. //QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  515. }
  516. }
  517. };
  518. QTEST_GUILESS_MAIN(TestSyncMove)
  519. #include "testsyncmove.moc"