testsyncmove.cpp 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676
  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. // We must change the modtime for it is likely that it did not change between sync.
  417. // (Previous version of the client (<=2.5) would not need this because it was always doing
  418. // checksum comparison for all renames. But newer version no longer does it if the file is
  419. // renamed because the parent folder is renamed)
  420. local.setModTime("AM/a2m", QDateTime::currentDateTimeUtc().addDays(3));
  421. local.rename("AM", "A2");
  422. remote.setContents("BM/b2m", 'C');
  423. remote.rename("BM", "B2");
  424. QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
  425. QVERIFY(fakeFolder.syncOnce());
  426. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  427. QCOMPARE(nGET, 1);
  428. QCOMPARE(nPUT, 1);
  429. QCOMPARE(nMOVE, 1);
  430. QCOMPARE(nDELETE, 0);
  431. QCOMPARE(remote.find("A2/a2m")->contentChar, 'C');
  432. QCOMPARE(remote.find("B2/b2m")->contentChar, 'C');
  433. QVERIFY(itemSuccessfulMove(completeSpy, "A2"));
  434. QVERIFY(itemSuccessfulMove(completeSpy, "B2"));
  435. }
  436. // Folder rename with contents touched on the other tree
  437. resetCounters();
  438. remote.setContents("A2/a2m", 'D');
  439. // setContents alone may not produce updated mtime if the test is fast
  440. // and since we don't use checksums here, that matters.
  441. remote.appendByte("A2/a2m");
  442. local.rename("A2", "A3");
  443. local.setContents("B2/b2m", 'D');
  444. local.appendByte("B2/b2m");
  445. remote.rename("B2", "B3");
  446. QVERIFY(fakeFolder.syncOnce());
  447. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  448. QCOMPARE(nGET, 1);
  449. QCOMPARE(nPUT, 1);
  450. QCOMPARE(nMOVE, 1);
  451. QCOMPARE(nDELETE, 0);
  452. QCOMPARE(remote.find("A3/a2m")->contentChar, 'D');
  453. QCOMPARE(remote.find("B3/b2m")->contentChar, 'D');
  454. // Folder rename with contents touched on both ends
  455. resetCounters();
  456. remote.setContents("A3/a2m", 'R');
  457. remote.appendByte("A3/a2m");
  458. local.setContents("A3/a2m", 'L');
  459. local.appendByte("A3/a2m");
  460. local.appendByte("A3/a2m");
  461. local.rename("A3", "A4");
  462. remote.setContents("B3/b2m", 'R');
  463. remote.appendByte("B3/b2m");
  464. local.setContents("B3/b2m", 'L');
  465. local.appendByte("B3/b2m");
  466. local.appendByte("B3/b2m");
  467. remote.rename("B3", "B4");
  468. QVERIFY(fakeFolder.syncOnce());
  469. auto currentLocal = fakeFolder.currentLocalState();
  470. auto conflicts = findConflicts(currentLocal.children["A4"]);
  471. QCOMPARE(conflicts.size(), 1);
  472. for (const auto& c : conflicts) {
  473. QCOMPARE(currentLocal.find(c)->contentChar, 'L');
  474. local.remove(c);
  475. }
  476. conflicts = findConflicts(currentLocal.children["B4"]);
  477. QCOMPARE(conflicts.size(), 1);
  478. for (const auto& c : conflicts) {
  479. QCOMPARE(currentLocal.find(c)->contentChar, 'L');
  480. local.remove(c);
  481. }
  482. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  483. QCOMPARE(nGET, 2);
  484. QCOMPARE(nPUT, 0);
  485. QCOMPARE(nMOVE, 1);
  486. QCOMPARE(nDELETE, 0);
  487. QCOMPARE(remote.find("A4/a2m")->contentChar, 'R');
  488. QCOMPARE(remote.find("B4/b2m")->contentChar, 'R');
  489. // Rename a folder and rename the contents at the same time
  490. resetCounters();
  491. local.rename("A4/a2m", "A4/a2m2");
  492. local.rename("A4", "A5");
  493. remote.rename("B4/b2m", "B4/b2m2");
  494. remote.rename("B4", "B5");
  495. QVERIFY(fakeFolder.syncOnce());
  496. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  497. QCOMPARE(nGET, 0);
  498. QCOMPARE(nPUT, 0);
  499. QCOMPARE(nMOVE, 2);
  500. QCOMPARE(nDELETE, 0);
  501. }
  502. // Check interaction of moves with file type changes
  503. void testMoveAndTypeChange()
  504. {
  505. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  506. auto &local = fakeFolder.localModifier();
  507. auto &remote = fakeFolder.remoteModifier();
  508. // Touch on one side, rename and mkdir on the other
  509. {
  510. local.appendByte("A/a1");
  511. remote.rename("A/a1", "A/a1mq");
  512. remote.mkdir("A/a1");
  513. remote.appendByte("B/b1");
  514. local.rename("B/b1", "B/b1mq");
  515. local.mkdir("B/b1");
  516. QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
  517. QVERIFY(fakeFolder.syncOnce());
  518. // BUG: This doesn't behave right
  519. //QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  520. }
  521. }
  522. // https://github.com/owncloud/client/issues/6629#issuecomment-402450691
  523. // When a file is moved and the server mtime was not in sync, the local mtime should be kept
  524. void testMoveAndMTimeChange()
  525. {
  526. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  527. int nPUT = 0;
  528. int nDELETE = 0;
  529. int nGET = 0;
  530. int nMOVE = 0;
  531. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *) {
  532. if (op == QNetworkAccessManager::PutOperation)
  533. ++nPUT;
  534. if (op == QNetworkAccessManager::DeleteOperation)
  535. ++nDELETE;
  536. if (op == QNetworkAccessManager::GetOperation)
  537. ++nGET;
  538. if (req.attribute(QNetworkRequest::CustomVerbAttribute) == "MOVE")
  539. ++nMOVE;
  540. return nullptr;
  541. });
  542. // Changing the mtime on the server (without invalidating the etag)
  543. fakeFolder.remoteModifier().find("A/a1")->lastModified = QDateTime::currentDateTimeUtc().addSecs(-50000);
  544. fakeFolder.remoteModifier().find("A/a2")->lastModified = QDateTime::currentDateTimeUtc().addSecs(-40000);
  545. // Move a few files
  546. fakeFolder.remoteModifier().rename("A/a1", "A/a1_server_renamed");
  547. fakeFolder.localModifier().rename("A/a2", "A/a2_local_renamed");
  548. QVERIFY(fakeFolder.syncOnce());
  549. QCOMPARE(nGET, 0);
  550. QCOMPARE(nPUT, 0);
  551. QCOMPARE(nMOVE, 1);
  552. QCOMPARE(nDELETE, 0);
  553. // Another sync should do nothing
  554. QVERIFY(fakeFolder.syncOnce());
  555. QCOMPARE(nGET, 0);
  556. QCOMPARE(nPUT, 0);
  557. QCOMPARE(nMOVE, 1);
  558. QCOMPARE(nDELETE, 0);
  559. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  560. }
  561. // Test for https://github.com/owncloud/client/issues/6694
  562. void testInvertFolderHierarchy()
  563. {
  564. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  565. fakeFolder.remoteModifier().mkdir("A/Empty");
  566. fakeFolder.remoteModifier().mkdir("A/Empty/Foo");
  567. fakeFolder.remoteModifier().mkdir("C/AllEmpty");
  568. fakeFolder.remoteModifier().mkdir("C/AllEmpty/Bar");
  569. QVERIFY(fakeFolder.syncOnce());
  570. // "Empty" is after "A", alphabetically
  571. fakeFolder.localModifier().rename("A/Empty", "Empty");
  572. fakeFolder.localModifier().rename("A", "Empty/A");
  573. // "AllEmpty" is before "C", alphabetically
  574. fakeFolder.localModifier().rename("C/AllEmpty", "AllEmpty");
  575. fakeFolder.localModifier().rename("C", "AllEmpty/C");
  576. auto expectedState = fakeFolder.currentLocalState();
  577. QVERIFY(fakeFolder.syncOnce());
  578. QCOMPARE(fakeFolder.currentLocalState(), expectedState);
  579. QCOMPARE(fakeFolder.currentRemoteState(), expectedState);
  580. // Now, the revert, but "crossed"
  581. fakeFolder.localModifier().rename("Empty/A", "A");
  582. fakeFolder.localModifier().rename("AllEmpty/C", "C");
  583. fakeFolder.localModifier().rename("Empty", "C/Empty");
  584. fakeFolder.localModifier().rename("AllEmpty", "A/AllEmpty");
  585. expectedState = fakeFolder.currentLocalState();
  586. QVERIFY(fakeFolder.syncOnce());
  587. QCOMPARE(fakeFolder.currentLocalState(), expectedState);
  588. QCOMPARE(fakeFolder.currentRemoteState(), expectedState);
  589. // Reverse on remote
  590. fakeFolder.remoteModifier().rename("A/AllEmpty", "AllEmpty");
  591. fakeFolder.remoteModifier().rename("C/Empty", "Empty");
  592. fakeFolder.remoteModifier().rename("C", "AllEmpty/C");
  593. fakeFolder.remoteModifier().rename("A", "Empty/A");
  594. expectedState = fakeFolder.currentRemoteState();
  595. QVERIFY(fakeFolder.syncOnce());
  596. QCOMPARE(fakeFolder.currentLocalState(), expectedState);
  597. QCOMPARE(fakeFolder.currentRemoteState(), expectedState);
  598. }
  599. };
  600. QTEST_GUILESS_MAIN(TestSyncMove)
  601. #include "testsyncmove.moc"