testsyncmove.cpp 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792
  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. struct OperationCounter {
  12. int nGET = 0;
  13. int nPUT = 0;
  14. int nMOVE = 0;
  15. int nDELETE = 0;
  16. void reset() { *this = {}; }
  17. auto functor() {
  18. return [&](QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *) {
  19. if (op == QNetworkAccessManager::GetOperation)
  20. ++nGET;
  21. if (op == QNetworkAccessManager::PutOperation)
  22. ++nPUT;
  23. if (op == QNetworkAccessManager::DeleteOperation)
  24. ++nDELETE;
  25. if (req.attribute(QNetworkRequest::CustomVerbAttribute) == "MOVE")
  26. ++nMOVE;
  27. return nullptr;
  28. };
  29. }
  30. };
  31. SyncFileItemPtr findItem(const QSignalSpy &spy, const QString &path)
  32. {
  33. for (const QList<QVariant> &args : spy) {
  34. auto item = args[0].value<SyncFileItemPtr>();
  35. if (item->destination() == path)
  36. return item;
  37. }
  38. return SyncFileItemPtr(new SyncFileItem);
  39. }
  40. bool itemSuccessful(const QSignalSpy &spy, const QString &path, const csync_instructions_e instr)
  41. {
  42. auto item = findItem(spy, path);
  43. return item->_status == SyncFileItem::Success && item->_instruction == instr;
  44. }
  45. bool itemConflict(const QSignalSpy &spy, const QString &path)
  46. {
  47. auto item = findItem(spy, path);
  48. return item->_status == SyncFileItem::Conflict && item->_instruction == CSYNC_INSTRUCTION_CONFLICT;
  49. }
  50. bool itemSuccessfulMove(const QSignalSpy &spy, const QString &path)
  51. {
  52. return itemSuccessful(spy, path, CSYNC_INSTRUCTION_RENAME);
  53. }
  54. QStringList findConflicts(const FileInfo &dir)
  55. {
  56. QStringList conflicts;
  57. for (const auto &item : dir.children) {
  58. if (item.name.contains("(conflicted copy")) {
  59. conflicts.append(item.path());
  60. }
  61. }
  62. return conflicts;
  63. }
  64. bool expectAndWipeConflict(FileModifier &local, FileInfo state, const QString path)
  65. {
  66. PathComponents pathComponents(path);
  67. auto base = state.find(pathComponents.parentDirComponents());
  68. if (!base)
  69. return false;
  70. for (const auto &item : base->children) {
  71. if (item.name.startsWith(pathComponents.fileName()) && item.name.contains("(conflicted copy")) {
  72. local.remove(item.path());
  73. return true;
  74. }
  75. }
  76. return false;
  77. }
  78. class TestSyncMove : public QObject
  79. {
  80. Q_OBJECT
  81. private slots:
  82. void testRemoteChangeInMovedFolder()
  83. {
  84. // issue #5192
  85. FakeFolder fakeFolder{ FileInfo{ QString(), { FileInfo{ QStringLiteral("folder"), { FileInfo{ QStringLiteral("folderA"), { { QStringLiteral("file.txt"), 400 } } }, QStringLiteral("folderB") } } } } };
  86. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  87. // Edit a file in a moved directory.
  88. fakeFolder.remoteModifier().setContents("folder/folderA/file.txt", 'a');
  89. fakeFolder.remoteModifier().rename("folder/folderA", "folder/folderB/folderA");
  90. fakeFolder.syncOnce();
  91. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  92. auto oldState = fakeFolder.currentLocalState();
  93. QVERIFY(oldState.find("folder/folderB/folderA/file.txt"));
  94. QVERIFY(!oldState.find("folder/folderA/file.txt"));
  95. // This sync should not remove the file
  96. fakeFolder.syncOnce();
  97. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  98. QCOMPARE(fakeFolder.currentLocalState(), oldState);
  99. }
  100. void testSelectiveSyncMovedFolder()
  101. {
  102. // issue #5224
  103. FakeFolder fakeFolder{ FileInfo{ QString(), { FileInfo{ QStringLiteral("parentFolder"), { FileInfo{ QStringLiteral("subFolderA"), { { QStringLiteral("fileA.txt"), 400 } } }, FileInfo{ QStringLiteral("subFolderB"), { { QStringLiteral("fileB.txt"), 400 } } } } } } } };
  104. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  105. auto expectedServerState = fakeFolder.currentRemoteState();
  106. // Remove subFolderA with selectiveSync:
  107. fakeFolder.syncEngine().journal()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList,
  108. { "parentFolder/subFolderA/" });
  109. fakeFolder.syncEngine().journal()->schedulePathForRemoteDiscovery(QByteArrayLiteral("parentFolder/subFolderA/"));
  110. fakeFolder.syncOnce();
  111. {
  112. // Nothing changed on the server
  113. QCOMPARE(fakeFolder.currentRemoteState(), expectedServerState);
  114. // The local state should not have subFolderA
  115. auto remoteState = fakeFolder.currentRemoteState();
  116. remoteState.remove("parentFolder/subFolderA");
  117. QCOMPARE(fakeFolder.currentLocalState(), remoteState);
  118. }
  119. // Rename parentFolder on the server
  120. fakeFolder.remoteModifier().rename("parentFolder", "parentFolderRenamed");
  121. expectedServerState = fakeFolder.currentRemoteState();
  122. fakeFolder.syncOnce();
  123. {
  124. QCOMPARE(fakeFolder.currentRemoteState(), expectedServerState);
  125. auto remoteState = fakeFolder.currentRemoteState();
  126. // The subFolderA should still be there on the server.
  127. QVERIFY(remoteState.find("parentFolderRenamed/subFolderA/fileA.txt"));
  128. // But not on the client because of the selective sync
  129. remoteState.remove("parentFolderRenamed/subFolderA");
  130. QCOMPARE(fakeFolder.currentLocalState(), remoteState);
  131. }
  132. // Rename it again, locally this time.
  133. fakeFolder.localModifier().rename("parentFolderRenamed", "parentThirdName");
  134. fakeFolder.syncOnce();
  135. {
  136. auto remoteState = fakeFolder.currentRemoteState();
  137. // The subFolderA should still be there on the server.
  138. QVERIFY(remoteState.find("parentThirdName/subFolderA/fileA.txt"));
  139. // But not on the client because of the selective sync
  140. remoteState.remove("parentThirdName/subFolderA");
  141. QCOMPARE(fakeFolder.currentLocalState(), remoteState);
  142. expectedServerState = fakeFolder.currentRemoteState();
  143. QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
  144. fakeFolder.syncOnce(); // This sync should do nothing
  145. QCOMPARE(completeSpy.count(), 0);
  146. QCOMPARE(fakeFolder.currentRemoteState(), expectedServerState);
  147. QCOMPARE(fakeFolder.currentLocalState(), remoteState);
  148. }
  149. }
  150. void testLocalMoveDetection()
  151. {
  152. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  153. int nPUT = 0;
  154. int nDELETE = 0;
  155. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &, QIODevice *) {
  156. if (op == QNetworkAccessManager::PutOperation)
  157. ++nPUT;
  158. if (op == QNetworkAccessManager::DeleteOperation)
  159. ++nDELETE;
  160. return nullptr;
  161. });
  162. // For directly editing the remote checksum
  163. FileInfo &remoteInfo = fakeFolder.remoteModifier();
  164. // Simple move causing a remote rename
  165. fakeFolder.localModifier().rename("A/a1", "A/a1m");
  166. QVERIFY(fakeFolder.syncOnce());
  167. QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
  168. QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(remoteInfo));
  169. QCOMPARE(nPUT, 0);
  170. // Move-and-change, causing a upload and delete
  171. fakeFolder.localModifier().rename("A/a2", "A/a2m");
  172. fakeFolder.localModifier().appendByte("A/a2m");
  173. QVERIFY(fakeFolder.syncOnce());
  174. QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
  175. QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(remoteInfo));
  176. QCOMPARE(nPUT, 1);
  177. QCOMPARE(nDELETE, 1);
  178. // Move-and-change, mtime+content only
  179. fakeFolder.localModifier().rename("B/b1", "B/b1m");
  180. fakeFolder.localModifier().setContents("B/b1m", 'C');
  181. QVERIFY(fakeFolder.syncOnce());
  182. QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
  183. QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(remoteInfo));
  184. QCOMPARE(nPUT, 2);
  185. QCOMPARE(nDELETE, 2);
  186. // Move-and-change, size+content only
  187. auto mtime = fakeFolder.remoteModifier().find("B/b2")->lastModified;
  188. fakeFolder.localModifier().rename("B/b2", "B/b2m");
  189. fakeFolder.localModifier().appendByte("B/b2m");
  190. fakeFolder.localModifier().setModTime("B/b2m", mtime);
  191. QVERIFY(fakeFolder.syncOnce());
  192. QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
  193. QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(remoteInfo));
  194. QCOMPARE(nPUT, 3);
  195. QCOMPARE(nDELETE, 3);
  196. // Move-and-change, content only -- c1 has no checksum, so we fail to detect this!
  197. // NOTE: This is an expected failure.
  198. mtime = fakeFolder.remoteModifier().find("C/c1")->lastModified;
  199. fakeFolder.localModifier().rename("C/c1", "C/c1m");
  200. fakeFolder.localModifier().setContents("C/c1m", 'C');
  201. fakeFolder.localModifier().setModTime("C/c1m", mtime);
  202. QVERIFY(fakeFolder.syncOnce());
  203. QCOMPARE(nPUT, 3);
  204. QCOMPARE(nDELETE, 3);
  205. QVERIFY(!(fakeFolder.currentLocalState() == remoteInfo));
  206. // cleanup, and upload a file that will have a checksum in the db
  207. fakeFolder.localModifier().remove("C/c1m");
  208. fakeFolder.localModifier().insert("C/c3");
  209. QVERIFY(fakeFolder.syncOnce());
  210. QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
  211. QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(remoteInfo));
  212. QCOMPARE(nPUT, 4);
  213. QCOMPARE(nDELETE, 4);
  214. // Move-and-change, content only, this time while having a checksum
  215. mtime = fakeFolder.remoteModifier().find("C/c3")->lastModified;
  216. fakeFolder.localModifier().rename("C/c3", "C/c3m");
  217. fakeFolder.localModifier().setContents("C/c3m", 'C');
  218. fakeFolder.localModifier().setModTime("C/c3m", mtime);
  219. QVERIFY(fakeFolder.syncOnce());
  220. QCOMPARE(nPUT, 5);
  221. QCOMPARE(nDELETE, 5);
  222. QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
  223. QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(remoteInfo));
  224. }
  225. void testDuplicateFileId_data()
  226. {
  227. QTest::addColumn<QString>("prefix");
  228. // There have been bugs related to how the original
  229. // folder and the folder with the duplicate tree are
  230. // ordered. Test both cases here.
  231. QTest::newRow("first ordering") << "O"; // "O" > "A"
  232. QTest::newRow("second ordering") << "0"; // "0" < "A"
  233. }
  234. // If the same folder is shared in two different ways with the same
  235. // user, the target user will see duplicate file ids. We need to make
  236. // sure the move detection and sync still do the right thing in that
  237. // case.
  238. void testDuplicateFileId()
  239. {
  240. QFETCH(QString, prefix);
  241. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  242. auto &remote = fakeFolder.remoteModifier();
  243. remote.mkdir("A/W");
  244. remote.insert("A/W/w1");
  245. remote.mkdir("A/Q");
  246. // Duplicate every entry in A under O/A
  247. remote.mkdir(prefix);
  248. remote.children[prefix].addChild(remote.children["A"]);
  249. // This already checks that the rename detection doesn't get
  250. // horribly confused if we add new files that have the same
  251. // fileid as existing ones
  252. QVERIFY(fakeFolder.syncOnce());
  253. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  254. int nGET = 0;
  255. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &, QIODevice *) {
  256. if (op == QNetworkAccessManager::GetOperation)
  257. ++nGET;
  258. return nullptr;
  259. });
  260. // Try a remote file move
  261. remote.rename("A/a1", "A/W/a1m");
  262. remote.rename(prefix + "/A/a1", prefix + "/A/W/a1m");
  263. QVERIFY(fakeFolder.syncOnce());
  264. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  265. QCOMPARE(nGET, 0);
  266. // And a remote directory move
  267. remote.rename("A/W", "A/Q/W");
  268. remote.rename(prefix + "/A/W", prefix + "/A/Q/W");
  269. QVERIFY(fakeFolder.syncOnce());
  270. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  271. QCOMPARE(nGET, 0);
  272. // Partial file removal (in practice, A/a2 may be moved to O/a2, but we don't care)
  273. remote.rename(prefix + "/A/a2", prefix + "/a2");
  274. remote.remove("A/a2");
  275. QVERIFY(fakeFolder.syncOnce());
  276. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  277. QCOMPARE(nGET, 0);
  278. // Local change plus remote move at the same time
  279. fakeFolder.localModifier().appendByte(prefix + "/a2");
  280. remote.rename(prefix + "/a2", prefix + "/a3");
  281. QVERIFY(fakeFolder.syncOnce());
  282. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  283. QCOMPARE(nGET, 1);
  284. }
  285. void testMovePropagation()
  286. {
  287. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  288. auto &local = fakeFolder.localModifier();
  289. auto &remote = fakeFolder.remoteModifier();
  290. OperationCounter counter;
  291. fakeFolder.setServerOverride(counter.functor());
  292. // Move
  293. {
  294. counter.reset();
  295. local.rename("A/a1", "A/a1m");
  296. remote.rename("B/b1", "B/b1m");
  297. QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
  298. QVERIFY(fakeFolder.syncOnce());
  299. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  300. QCOMPARE(counter.nGET, 0);
  301. QCOMPARE(counter.nPUT, 0);
  302. QCOMPARE(counter.nMOVE, 1);
  303. QCOMPARE(counter.nDELETE, 0);
  304. QVERIFY(itemSuccessfulMove(completeSpy, "A/a1m"));
  305. QVERIFY(itemSuccessfulMove(completeSpy, "B/b1m"));
  306. }
  307. // Touch+Move on same side
  308. counter.reset();
  309. local.rename("A/a2", "A/a2m");
  310. local.setContents("A/a2m", 'A');
  311. remote.rename("B/b2", "B/b2m");
  312. remote.setContents("B/b2m", 'A');
  313. QVERIFY(fakeFolder.syncOnce());
  314. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  315. QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(fakeFolder.currentRemoteState()));
  316. QCOMPARE(counter.nGET, 1);
  317. QCOMPARE(counter.nPUT, 1);
  318. QCOMPARE(counter.nMOVE, 0);
  319. QCOMPARE(counter.nDELETE, 1);
  320. QCOMPARE(remote.find("A/a2m")->contentChar, 'A');
  321. QCOMPARE(remote.find("B/b2m")->contentChar, 'A');
  322. // Touch+Move on opposite sides
  323. counter.reset();
  324. local.rename("A/a1m", "A/a1m2");
  325. remote.setContents("A/a1m", 'B');
  326. remote.rename("B/b1m", "B/b1m2");
  327. local.setContents("B/b1m", 'B');
  328. QVERIFY(fakeFolder.syncOnce());
  329. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  330. QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(fakeFolder.currentRemoteState()));
  331. QCOMPARE(counter.nGET, 2);
  332. QCOMPARE(counter.nPUT, 2);
  333. QCOMPARE(counter.nMOVE, 0);
  334. QCOMPARE(counter.nDELETE, 0);
  335. // All these files existing afterwards is debatable. Should we propagate
  336. // the rename in one direction and grab the new contents in the other?
  337. // Currently there's no propagation job that would do that, and this does
  338. // at least not lose data.
  339. QCOMPARE(remote.find("A/a1m")->contentChar, 'B');
  340. QCOMPARE(remote.find("B/b1m")->contentChar, 'B');
  341. QCOMPARE(remote.find("A/a1m2")->contentChar, 'W');
  342. QCOMPARE(remote.find("B/b1m2")->contentChar, 'W');
  343. // Touch+create on one side, move on the other
  344. {
  345. counter.reset();
  346. local.appendByte("A/a1m");
  347. local.insert("A/a1mt");
  348. remote.rename("A/a1m", "A/a1mt");
  349. remote.appendByte("B/b1m");
  350. remote.insert("B/b1mt");
  351. local.rename("B/b1m", "B/b1mt");
  352. QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
  353. QVERIFY(fakeFolder.syncOnce());
  354. QVERIFY(expectAndWipeConflict(local, fakeFolder.currentLocalState(), "A/a1mt"));
  355. QVERIFY(expectAndWipeConflict(local, fakeFolder.currentLocalState(), "B/b1mt"));
  356. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  357. QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(fakeFolder.currentRemoteState()));
  358. QCOMPARE(counter.nGET, 3);
  359. QCOMPARE(counter.nPUT, 1);
  360. QCOMPARE(counter.nMOVE, 0);
  361. QCOMPARE(counter.nDELETE, 0);
  362. QVERIFY(itemSuccessful(completeSpy, "A/a1m", CSYNC_INSTRUCTION_NEW));
  363. QVERIFY(itemSuccessful(completeSpy, "B/b1m", CSYNC_INSTRUCTION_NEW));
  364. QVERIFY(itemConflict(completeSpy, "A/a1mt"));
  365. QVERIFY(itemConflict(completeSpy, "B/b1mt"));
  366. }
  367. // Create new on one side, move to new on the other
  368. {
  369. counter.reset();
  370. local.insert("A/a1N", 13);
  371. remote.rename("A/a1mt", "A/a1N");
  372. remote.insert("B/b1N", 13);
  373. local.rename("B/b1mt", "B/b1N");
  374. QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
  375. QVERIFY(fakeFolder.syncOnce());
  376. QVERIFY(expectAndWipeConflict(local, fakeFolder.currentLocalState(), "A/a1N"));
  377. QVERIFY(expectAndWipeConflict(local, fakeFolder.currentLocalState(), "B/b1N"));
  378. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  379. QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(fakeFolder.currentRemoteState()));
  380. QCOMPARE(counter.nGET, 2);
  381. QCOMPARE(counter.nPUT, 0);
  382. QCOMPARE(counter.nMOVE, 0);
  383. QCOMPARE(counter.nDELETE, 1);
  384. QVERIFY(itemSuccessful(completeSpy, "A/a1mt", CSYNC_INSTRUCTION_REMOVE));
  385. QVERIFY(itemSuccessful(completeSpy, "B/b1mt", CSYNC_INSTRUCTION_REMOVE));
  386. QVERIFY(itemConflict(completeSpy, "A/a1N"));
  387. QVERIFY(itemConflict(completeSpy, "B/b1N"));
  388. }
  389. // Local move, remote move
  390. counter.reset();
  391. local.rename("C/c1", "C/c1mL");
  392. remote.rename("C/c1", "C/c1mR");
  393. QVERIFY(fakeFolder.syncOnce());
  394. // end up with both files
  395. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  396. QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(fakeFolder.currentRemoteState()));
  397. QCOMPARE(counter.nGET, 1);
  398. QCOMPARE(counter.nPUT, 1);
  399. QCOMPARE(counter.nMOVE, 0);
  400. QCOMPARE(counter.nDELETE, 0);
  401. // Rename/rename conflict on a folder
  402. counter.reset();
  403. remote.rename("C", "CMR");
  404. local.rename("C", "CML");
  405. QVERIFY(fakeFolder.syncOnce());
  406. // End up with both folders
  407. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  408. QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(fakeFolder.currentRemoteState()));
  409. QCOMPARE(counter.nGET, 3); // 3 files in C
  410. QCOMPARE(counter.nPUT, 3);
  411. QCOMPARE(counter.nMOVE, 0);
  412. QCOMPARE(counter.nDELETE, 0);
  413. // Folder move
  414. {
  415. counter.reset();
  416. local.rename("A", "AM");
  417. remote.rename("B", "BM");
  418. QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
  419. QVERIFY(fakeFolder.syncOnce());
  420. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  421. QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(fakeFolder.currentRemoteState()));
  422. QCOMPARE(counter.nGET, 0);
  423. QCOMPARE(counter.nPUT, 0);
  424. QCOMPARE(counter.nMOVE, 1);
  425. QCOMPARE(counter.nDELETE, 0);
  426. QVERIFY(itemSuccessfulMove(completeSpy, "AM"));
  427. QVERIFY(itemSuccessfulMove(completeSpy, "BM"));
  428. }
  429. // Folder move with contents touched on the same side
  430. {
  431. counter.reset();
  432. local.setContents("AM/a2m", 'C');
  433. // We must change the modtime for it is likely that it did not change between sync.
  434. // (Previous version of the client (<=2.5) would not need this because it was always doing
  435. // checksum comparison for all renames. But newer version no longer does it if the file is
  436. // renamed because the parent folder is renamed)
  437. local.setModTime("AM/a2m", QDateTime::currentDateTimeUtc().addDays(3));
  438. local.rename("AM", "A2");
  439. remote.setContents("BM/b2m", 'C');
  440. remote.rename("BM", "B2");
  441. QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
  442. QVERIFY(fakeFolder.syncOnce());
  443. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  444. QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(fakeFolder.currentRemoteState()));
  445. QCOMPARE(counter.nGET, 1);
  446. QCOMPARE(counter.nPUT, 1);
  447. QCOMPARE(counter.nMOVE, 1);
  448. QCOMPARE(counter.nDELETE, 0);
  449. QCOMPARE(remote.find("A2/a2m")->contentChar, 'C');
  450. QCOMPARE(remote.find("B2/b2m")->contentChar, 'C');
  451. QVERIFY(itemSuccessfulMove(completeSpy, "A2"));
  452. QVERIFY(itemSuccessfulMove(completeSpy, "B2"));
  453. }
  454. // Folder rename with contents touched on the other tree
  455. counter.reset();
  456. remote.setContents("A2/a2m", 'D');
  457. // setContents alone may not produce updated mtime if the test is fast
  458. // and since we don't use checksums here, that matters.
  459. remote.appendByte("A2/a2m");
  460. local.rename("A2", "A3");
  461. local.setContents("B2/b2m", 'D');
  462. local.appendByte("B2/b2m");
  463. remote.rename("B2", "B3");
  464. QVERIFY(fakeFolder.syncOnce());
  465. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  466. QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(fakeFolder.currentRemoteState()));
  467. QCOMPARE(counter.nGET, 1);
  468. QCOMPARE(counter.nPUT, 1);
  469. QCOMPARE(counter.nMOVE, 1);
  470. QCOMPARE(counter.nDELETE, 0);
  471. QCOMPARE(remote.find("A3/a2m")->contentChar, 'D');
  472. QCOMPARE(remote.find("B3/b2m")->contentChar, 'D');
  473. // Folder rename with contents touched on both ends
  474. counter.reset();
  475. remote.setContents("A3/a2m", 'R');
  476. remote.appendByte("A3/a2m");
  477. local.setContents("A3/a2m", 'L');
  478. local.appendByte("A3/a2m");
  479. local.appendByte("A3/a2m");
  480. local.rename("A3", "A4");
  481. remote.setContents("B3/b2m", 'R');
  482. remote.appendByte("B3/b2m");
  483. local.setContents("B3/b2m", 'L');
  484. local.appendByte("B3/b2m");
  485. local.appendByte("B3/b2m");
  486. remote.rename("B3", "B4");
  487. QVERIFY(fakeFolder.syncOnce());
  488. auto currentLocal = fakeFolder.currentLocalState();
  489. auto conflicts = findConflicts(currentLocal.children["A4"]);
  490. QCOMPARE(conflicts.size(), 1);
  491. for (const auto& c : conflicts) {
  492. QCOMPARE(currentLocal.find(c)->contentChar, 'L');
  493. local.remove(c);
  494. }
  495. conflicts = findConflicts(currentLocal.children["B4"]);
  496. QCOMPARE(conflicts.size(), 1);
  497. for (const auto& c : conflicts) {
  498. QCOMPARE(currentLocal.find(c)->contentChar, 'L');
  499. local.remove(c);
  500. }
  501. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  502. QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(fakeFolder.currentRemoteState()));
  503. QCOMPARE(counter.nGET, 2);
  504. QCOMPARE(counter.nPUT, 0);
  505. QCOMPARE(counter.nMOVE, 1);
  506. QCOMPARE(counter.nDELETE, 0);
  507. QCOMPARE(remote.find("A4/a2m")->contentChar, 'R');
  508. QCOMPARE(remote.find("B4/b2m")->contentChar, 'R');
  509. // Rename a folder and rename the contents at the same time
  510. counter.reset();
  511. local.rename("A4/a2m", "A4/a2m2");
  512. local.rename("A4", "A5");
  513. remote.rename("B4/b2m", "B4/b2m2");
  514. remote.rename("B4", "B5");
  515. QVERIFY(fakeFolder.syncOnce());
  516. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  517. QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(fakeFolder.currentRemoteState()));
  518. QCOMPARE(counter.nGET, 0);
  519. QCOMPARE(counter.nPUT, 0);
  520. QCOMPARE(counter.nMOVE, 2);
  521. QCOMPARE(counter.nDELETE, 0);
  522. }
  523. // Check interaction of moves with file type changes
  524. void testMoveAndTypeChange()
  525. {
  526. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  527. auto &local = fakeFolder.localModifier();
  528. auto &remote = fakeFolder.remoteModifier();
  529. // Touch on one side, rename and mkdir on the other
  530. {
  531. local.appendByte("A/a1");
  532. remote.rename("A/a1", "A/a1mq");
  533. remote.mkdir("A/a1");
  534. remote.appendByte("B/b1");
  535. local.rename("B/b1", "B/b1mq");
  536. local.mkdir("B/b1");
  537. QSignalSpy completeSpy(&fakeFolder.syncEngine(), SIGNAL(itemCompleted(const SyncFileItemPtr &)));
  538. QVERIFY(fakeFolder.syncOnce());
  539. // BUG: This doesn't behave right
  540. //QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  541. }
  542. }
  543. // https://github.com/owncloud/client/issues/6629#issuecomment-402450691
  544. // When a file is moved and the server mtime was not in sync, the local mtime should be kept
  545. void testMoveAndMTimeChange()
  546. {
  547. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  548. OperationCounter counter;
  549. fakeFolder.setServerOverride(counter.functor());
  550. // Changing the mtime on the server (without invalidating the etag)
  551. fakeFolder.remoteModifier().find("A/a1")->lastModified = QDateTime::currentDateTimeUtc().addSecs(-50000);
  552. fakeFolder.remoteModifier().find("A/a2")->lastModified = QDateTime::currentDateTimeUtc().addSecs(-40000);
  553. // Move a few files
  554. fakeFolder.remoteModifier().rename("A/a1", "A/a1_server_renamed");
  555. fakeFolder.localModifier().rename("A/a2", "A/a2_local_renamed");
  556. QVERIFY(fakeFolder.syncOnce());
  557. QCOMPARE(counter.nGET, 0);
  558. QCOMPARE(counter.nPUT, 0);
  559. QCOMPARE(counter.nMOVE, 1);
  560. QCOMPARE(counter.nDELETE, 0);
  561. // Another sync should do nothing
  562. QVERIFY(fakeFolder.syncOnce());
  563. QCOMPARE(counter.nGET, 0);
  564. QCOMPARE(counter.nPUT, 0);
  565. QCOMPARE(counter.nMOVE, 1);
  566. QCOMPARE(counter.nDELETE, 0);
  567. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  568. }
  569. // Test for https://github.com/owncloud/client/issues/6694
  570. void testInvertFolderHierarchy()
  571. {
  572. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  573. fakeFolder.remoteModifier().mkdir("A/Empty");
  574. fakeFolder.remoteModifier().mkdir("A/Empty/Foo");
  575. fakeFolder.remoteModifier().mkdir("C/AllEmpty");
  576. fakeFolder.remoteModifier().mkdir("C/AllEmpty/Bar");
  577. fakeFolder.remoteModifier().insert("A/Empty/f1");
  578. fakeFolder.remoteModifier().insert("A/Empty/Foo/f2");
  579. fakeFolder.remoteModifier().mkdir("C/AllEmpty/f3");
  580. fakeFolder.remoteModifier().mkdir("C/AllEmpty/Bar/f4");
  581. QVERIFY(fakeFolder.syncOnce());
  582. OperationCounter counter;
  583. fakeFolder.setServerOverride(counter.functor());
  584. // "Empty" is after "A", alphabetically
  585. fakeFolder.localModifier().rename("A/Empty", "Empty");
  586. fakeFolder.localModifier().rename("A", "Empty/A");
  587. // "AllEmpty" is before "C", alphabetically
  588. fakeFolder.localModifier().rename("C/AllEmpty", "AllEmpty");
  589. fakeFolder.localModifier().rename("C", "AllEmpty/C");
  590. auto expectedState = fakeFolder.currentLocalState();
  591. QVERIFY(fakeFolder.syncOnce());
  592. QCOMPARE(fakeFolder.currentLocalState(), expectedState);
  593. QCOMPARE(fakeFolder.currentRemoteState(), expectedState);
  594. QCOMPARE(counter.nDELETE, 0);
  595. QCOMPARE(counter.nGET, 0);
  596. QCOMPARE(counter.nPUT, 0);
  597. // Now, the revert, but "crossed"
  598. fakeFolder.localModifier().rename("Empty/A", "A");
  599. fakeFolder.localModifier().rename("AllEmpty/C", "C");
  600. fakeFolder.localModifier().rename("Empty", "C/Empty");
  601. fakeFolder.localModifier().rename("AllEmpty", "A/AllEmpty");
  602. expectedState = fakeFolder.currentLocalState();
  603. QVERIFY(fakeFolder.syncOnce());
  604. QCOMPARE(fakeFolder.currentLocalState(), expectedState);
  605. QCOMPARE(fakeFolder.currentRemoteState(), expectedState);
  606. QCOMPARE(counter.nDELETE, 0);
  607. QCOMPARE(counter.nGET, 0);
  608. QCOMPARE(counter.nPUT, 0);
  609. // Reverse on remote
  610. fakeFolder.remoteModifier().rename("A/AllEmpty", "AllEmpty");
  611. fakeFolder.remoteModifier().rename("C/Empty", "Empty");
  612. fakeFolder.remoteModifier().rename("C", "AllEmpty/C");
  613. fakeFolder.remoteModifier().rename("A", "Empty/A");
  614. expectedState = fakeFolder.currentRemoteState();
  615. QVERIFY(fakeFolder.syncOnce());
  616. QCOMPARE(fakeFolder.currentLocalState(), expectedState);
  617. QCOMPARE(fakeFolder.currentRemoteState(), expectedState);
  618. QCOMPARE(counter.nDELETE, 0);
  619. QCOMPARE(counter.nGET, 0);
  620. QCOMPARE(counter.nPUT, 0);
  621. }
  622. void testDeepHierarchy_data()
  623. {
  624. QTest::addColumn<bool>("local");
  625. QTest::newRow("remote") << false;
  626. QTest::newRow("local") << true;
  627. }
  628. void testDeepHierarchy()
  629. {
  630. QFETCH(bool, local);
  631. FakeFolder fakeFolder { FileInfo::A12_B12_C12_S12() };
  632. auto &modifier = local ? fakeFolder.localModifier() : fakeFolder.remoteModifier();
  633. modifier.mkdir("FolA");
  634. modifier.mkdir("FolA/FolB");
  635. modifier.mkdir("FolA/FolB/FolC");
  636. modifier.mkdir("FolA/FolB/FolC/FolD");
  637. modifier.mkdir("FolA/FolB/FolC/FolD/FolE");
  638. modifier.insert("FolA/FileA.txt");
  639. modifier.insert("FolA/FolB/FileB.txt");
  640. modifier.insert("FolA/FolB/FolC/FileC.txt");
  641. modifier.insert("FolA/FolB/FolC/FolD/FileD.txt");
  642. modifier.insert("FolA/FolB/FolC/FolD/FolE/FileE.txt");
  643. QVERIFY(fakeFolder.syncOnce());
  644. OperationCounter counter;
  645. fakeFolder.setServerOverride(counter.functor());
  646. modifier.insert("FolA/FileA2.txt");
  647. modifier.insert("FolA/FolB/FileB2.txt");
  648. modifier.insert("FolA/FolB/FolC/FileC2.txt");
  649. modifier.insert("FolA/FolB/FolC/FolD/FileD2.txt");
  650. modifier.insert("FolA/FolB/FolC/FolD/FolE/FileE2.txt");
  651. modifier.rename("FolA", "FolA_Renamed");
  652. modifier.rename("FolA_Renamed/FolB", "FolB_Renamed");
  653. modifier.rename("FolB_Renamed/FolC", "FolA");
  654. modifier.rename("FolA/FolD", "FolA/FolD_Renamed");
  655. modifier.mkdir("FolB_Renamed/New");
  656. modifier.rename("FolA/FolD_Renamed/FolE", "FolB_Renamed/New/FolE");
  657. auto expected = local ? fakeFolder.currentLocalState() : fakeFolder.currentRemoteState();
  658. QVERIFY(fakeFolder.syncOnce());
  659. QCOMPARE(fakeFolder.currentLocalState(), expected);
  660. QCOMPARE(fakeFolder.currentRemoteState(), expected);
  661. QCOMPARE(counter.nDELETE, local ? 1 : 0); // FolC was is renamed to an existing name, so it is not considered as renamed
  662. // There was 5 inserts
  663. QCOMPARE(counter.nGET, local ? 0 : 5);
  664. QCOMPARE(counter.nPUT, local ? 5 : 0);
  665. }
  666. void renameOnBothSides()
  667. {
  668. FakeFolder fakeFolder { FileInfo::A12_B12_C12_S12() };
  669. OperationCounter counter;
  670. fakeFolder.setServerOverride(counter.functor());
  671. // Test that renaming a file within a directory that was renamed on the other side actually do a rename.
  672. // 1) move the folder alphabeticaly before
  673. fakeFolder.remoteModifier().rename("A/a1", "A/a1m");
  674. fakeFolder.localModifier().rename("A", "_A");
  675. fakeFolder.localModifier().rename("B/b1", "B/b1m");
  676. fakeFolder.remoteModifier().rename("B", "_B");
  677. QVERIFY(fakeFolder.syncOnce());
  678. QCOMPARE(fakeFolder.currentRemoteState(), fakeFolder.currentRemoteState());
  679. QVERIFY(fakeFolder.currentRemoteState().find("_A/a1m"));
  680. QVERIFY(fakeFolder.currentRemoteState().find("_B/b1m"));
  681. QCOMPARE(counter.nDELETE, 0);
  682. QCOMPARE(counter.nGET, 0);
  683. QCOMPARE(counter.nPUT, 0);
  684. QCOMPARE(counter.nMOVE, 2);
  685. counter.reset();
  686. // 2) move alphabetically after
  687. fakeFolder.remoteModifier().rename("_A/a2", "_A/a2m");
  688. fakeFolder.localModifier().rename("_B/b2", "_B/b2m");
  689. fakeFolder.localModifier().rename("_A", "S/A");
  690. fakeFolder.remoteModifier().rename("_B", "S/B");
  691. QVERIFY(fakeFolder.syncOnce());
  692. QCOMPARE(fakeFolder.currentRemoteState(), fakeFolder.currentRemoteState());
  693. QVERIFY(fakeFolder.currentRemoteState().find("S/A/a2m"));
  694. QVERIFY(fakeFolder.currentRemoteState().find("S/B/b2m"));
  695. QCOMPARE(counter.nDELETE, 0);
  696. QCOMPARE(counter.nGET, 0);
  697. QCOMPARE(counter.nPUT, 0);
  698. QCOMPARE(counter.nMOVE, 2);
  699. }
  700. };
  701. QTEST_GUILESS_MAIN(TestSyncMove)
  702. #include "testsyncmove.moc"