testsyncmove.cpp 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943
  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. bool itemSuccessful(const ItemCompletedSpy &spy, const QString &path, const SyncInstructions instr)
  32. {
  33. auto item = spy.findItem(path);
  34. return item->_status == SyncFileItem::Success && item->_instruction == instr;
  35. }
  36. bool itemConflict(const ItemCompletedSpy &spy, const QString &path)
  37. {
  38. auto item = spy.findItem(path);
  39. return item->_status == SyncFileItem::Conflict && item->_instruction == CSYNC_INSTRUCTION_CONFLICT;
  40. }
  41. bool itemSuccessfulMove(const ItemCompletedSpy &spy, const QString &path)
  42. {
  43. return itemSuccessful(spy, path, CSYNC_INSTRUCTION_RENAME);
  44. }
  45. QStringList findConflicts(const FileInfo &dir)
  46. {
  47. QStringList conflicts;
  48. for (const auto &item : dir.children) {
  49. if (item.name.contains("(conflicted copy")) {
  50. conflicts.append(item.path());
  51. }
  52. }
  53. return conflicts;
  54. }
  55. bool expectAndWipeConflict(FileModifier &local, FileInfo state, const QString path)
  56. {
  57. PathComponents pathComponents(path);
  58. auto base = state.find(pathComponents.parentDirComponents());
  59. if (!base)
  60. return false;
  61. for (const auto &item : base->children) {
  62. if (item.name.startsWith(pathComponents.fileName()) && item.name.contains("(conflicted copy")) {
  63. local.remove(item.path());
  64. return true;
  65. }
  66. }
  67. return false;
  68. }
  69. class TestSyncMove : public QObject
  70. {
  71. Q_OBJECT
  72. private slots:
  73. void testRemoteChangeInMovedFolder()
  74. {
  75. // issue #5192
  76. FakeFolder fakeFolder{ FileInfo{ QString(), { FileInfo{ QStringLiteral("folder"), { FileInfo{ QStringLiteral("folderA"), { { QStringLiteral("file.txt"), 400 } } }, QStringLiteral("folderB") } } } } };
  77. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  78. // Edit a file in a moved directory.
  79. fakeFolder.remoteModifier().setContents("folder/folderA/file.txt", 'a');
  80. fakeFolder.remoteModifier().rename("folder/folderA", "folder/folderB/folderA");
  81. fakeFolder.syncOnce();
  82. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  83. auto oldState = fakeFolder.currentLocalState();
  84. QVERIFY(oldState.find("folder/folderB/folderA/file.txt"));
  85. QVERIFY(!oldState.find("folder/folderA/file.txt"));
  86. // This sync should not remove the file
  87. fakeFolder.syncOnce();
  88. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  89. QCOMPARE(fakeFolder.currentLocalState(), oldState);
  90. }
  91. void testSelectiveSyncMovedFolder()
  92. {
  93. // issue #5224
  94. FakeFolder fakeFolder{ FileInfo{ QString(), { FileInfo{ QStringLiteral("parentFolder"), { FileInfo{ QStringLiteral("subFolderA"), { { QStringLiteral("fileA.txt"), 400 } } }, FileInfo{ QStringLiteral("subFolderB"), { { QStringLiteral("fileB.txt"), 400 } } } } } } } };
  95. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  96. auto expectedServerState = fakeFolder.currentRemoteState();
  97. // Remove subFolderA with selectiveSync:
  98. fakeFolder.syncEngine().journal()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList,
  99. { "parentFolder/subFolderA/" });
  100. fakeFolder.syncEngine().journal()->schedulePathForRemoteDiscovery(QByteArrayLiteral("parentFolder/subFolderA/"));
  101. fakeFolder.syncOnce();
  102. {
  103. // Nothing changed on the server
  104. QCOMPARE(fakeFolder.currentRemoteState(), expectedServerState);
  105. // The local state should not have subFolderA
  106. auto remoteState = fakeFolder.currentRemoteState();
  107. remoteState.remove("parentFolder/subFolderA");
  108. QCOMPARE(fakeFolder.currentLocalState(), remoteState);
  109. }
  110. // Rename parentFolder on the server
  111. fakeFolder.remoteModifier().rename("parentFolder", "parentFolderRenamed");
  112. expectedServerState = fakeFolder.currentRemoteState();
  113. fakeFolder.syncOnce();
  114. {
  115. QCOMPARE(fakeFolder.currentRemoteState(), expectedServerState);
  116. auto remoteState = fakeFolder.currentRemoteState();
  117. // The subFolderA should still be there on the server.
  118. QVERIFY(remoteState.find("parentFolderRenamed/subFolderA/fileA.txt"));
  119. // But not on the client because of the selective sync
  120. remoteState.remove("parentFolderRenamed/subFolderA");
  121. QCOMPARE(fakeFolder.currentLocalState(), remoteState);
  122. }
  123. // Rename it again, locally this time.
  124. fakeFolder.localModifier().rename("parentFolderRenamed", "parentThirdName");
  125. fakeFolder.syncOnce();
  126. {
  127. auto remoteState = fakeFolder.currentRemoteState();
  128. // The subFolderA should still be there on the server.
  129. QVERIFY(remoteState.find("parentThirdName/subFolderA/fileA.txt"));
  130. // But not on the client because of the selective sync
  131. remoteState.remove("parentThirdName/subFolderA");
  132. QCOMPARE(fakeFolder.currentLocalState(), remoteState);
  133. expectedServerState = fakeFolder.currentRemoteState();
  134. ItemCompletedSpy completeSpy(fakeFolder);
  135. fakeFolder.syncOnce(); // This sync should do nothing
  136. QCOMPARE(completeSpy.count(), 0);
  137. QCOMPARE(fakeFolder.currentRemoteState(), expectedServerState);
  138. QCOMPARE(fakeFolder.currentLocalState(), remoteState);
  139. }
  140. }
  141. void testLocalMoveDetection()
  142. {
  143. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  144. int nPUT = 0;
  145. int nDELETE = 0;
  146. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &, QIODevice *) {
  147. if (op == QNetworkAccessManager::PutOperation)
  148. ++nPUT;
  149. if (op == QNetworkAccessManager::DeleteOperation)
  150. ++nDELETE;
  151. return nullptr;
  152. });
  153. // For directly editing the remote checksum
  154. FileInfo &remoteInfo = fakeFolder.remoteModifier();
  155. // Simple move causing a remote rename
  156. fakeFolder.localModifier().rename("A/a1", "A/a1m");
  157. QVERIFY(fakeFolder.syncOnce());
  158. QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
  159. QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(remoteInfo));
  160. QCOMPARE(nPUT, 0);
  161. // Move-and-change, causing a upload and delete
  162. fakeFolder.localModifier().rename("A/a2", "A/a2m");
  163. fakeFolder.localModifier().appendByte("A/a2m");
  164. QVERIFY(fakeFolder.syncOnce());
  165. QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
  166. QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(remoteInfo));
  167. QCOMPARE(nPUT, 1);
  168. QCOMPARE(nDELETE, 1);
  169. // Move-and-change, mtime+content only
  170. fakeFolder.localModifier().rename("B/b1", "B/b1m");
  171. fakeFolder.localModifier().setContents("B/b1m", 'C');
  172. QVERIFY(fakeFolder.syncOnce());
  173. QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
  174. QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(remoteInfo));
  175. QCOMPARE(nPUT, 2);
  176. QCOMPARE(nDELETE, 2);
  177. // Move-and-change, size+content only
  178. auto mtime = fakeFolder.remoteModifier().find("B/b2")->lastModified;
  179. fakeFolder.localModifier().rename("B/b2", "B/b2m");
  180. fakeFolder.localModifier().appendByte("B/b2m");
  181. fakeFolder.localModifier().setModTime("B/b2m", mtime);
  182. QVERIFY(fakeFolder.syncOnce());
  183. QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
  184. QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(remoteInfo));
  185. QCOMPARE(nPUT, 3);
  186. QCOMPARE(nDELETE, 3);
  187. // Move-and-change, content only -- c1 has no checksum, so we fail to detect this!
  188. // NOTE: This is an expected failure.
  189. mtime = fakeFolder.remoteModifier().find("C/c1")->lastModified;
  190. fakeFolder.localModifier().rename("C/c1", "C/c1m");
  191. fakeFolder.localModifier().setContents("C/c1m", 'C');
  192. fakeFolder.localModifier().setModTime("C/c1m", mtime);
  193. QVERIFY(fakeFolder.syncOnce());
  194. QCOMPARE(nPUT, 3);
  195. QCOMPARE(nDELETE, 3);
  196. QVERIFY(!(fakeFolder.currentLocalState() == remoteInfo));
  197. // cleanup, and upload a file that will have a checksum in the db
  198. fakeFolder.localModifier().remove("C/c1m");
  199. fakeFolder.localModifier().insert("C/c3");
  200. QVERIFY(fakeFolder.syncOnce());
  201. QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
  202. QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(remoteInfo));
  203. QCOMPARE(nPUT, 4);
  204. QCOMPARE(nDELETE, 4);
  205. // Move-and-change, content only, this time while having a checksum
  206. mtime = fakeFolder.remoteModifier().find("C/c3")->lastModified;
  207. fakeFolder.localModifier().rename("C/c3", "C/c3m");
  208. fakeFolder.localModifier().setContents("C/c3m", 'C');
  209. fakeFolder.localModifier().setModTime("C/c3m", mtime);
  210. QVERIFY(fakeFolder.syncOnce());
  211. QCOMPARE(nPUT, 5);
  212. QCOMPARE(nDELETE, 5);
  213. QCOMPARE(fakeFolder.currentLocalState(), remoteInfo);
  214. QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(remoteInfo));
  215. }
  216. void testDuplicateFileId_data()
  217. {
  218. QTest::addColumn<QString>("prefix");
  219. // There have been bugs related to how the original
  220. // folder and the folder with the duplicate tree are
  221. // ordered. Test both cases here.
  222. QTest::newRow("first ordering") << "O"; // "O" > "A"
  223. QTest::newRow("second ordering") << "0"; // "0" < "A"
  224. }
  225. // If the same folder is shared in two different ways with the same
  226. // user, the target user will see duplicate file ids. We need to make
  227. // sure the move detection and sync still do the right thing in that
  228. // case.
  229. void testDuplicateFileId()
  230. {
  231. QFETCH(QString, prefix);
  232. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  233. auto &remote = fakeFolder.remoteModifier();
  234. remote.mkdir("A/W");
  235. remote.insert("A/W/w1");
  236. remote.mkdir("A/Q");
  237. // Duplicate every entry in A under O/A
  238. remote.mkdir(prefix);
  239. remote.children[prefix].addChild(remote.children["A"]);
  240. // This already checks that the rename detection doesn't get
  241. // horribly confused if we add new files that have the same
  242. // fileid as existing ones
  243. QVERIFY(fakeFolder.syncOnce());
  244. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  245. OperationCounter counter;
  246. fakeFolder.setServerOverride(counter.functor());
  247. // Try a remote file move
  248. remote.rename("A/a1", "A/W/a1m");
  249. remote.rename(prefix + "/A/a1", prefix + "/A/W/a1m");
  250. QVERIFY(fakeFolder.syncOnce());
  251. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  252. QCOMPARE(counter.nGET, 0);
  253. // And a remote directory move
  254. remote.rename("A/W", "A/Q/W");
  255. remote.rename(prefix + "/A/W", prefix + "/A/Q/W");
  256. QVERIFY(fakeFolder.syncOnce());
  257. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  258. QCOMPARE(counter.nGET, 0);
  259. // Partial file removal (in practice, A/a2 may be moved to O/a2, but we don't care)
  260. remote.rename(prefix + "/A/a2", prefix + "/a2");
  261. remote.remove("A/a2");
  262. QVERIFY(fakeFolder.syncOnce());
  263. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  264. QCOMPARE(counter.nGET, 0);
  265. // Local change plus remote move at the same time
  266. fakeFolder.localModifier().appendByte(prefix + "/a2");
  267. remote.rename(prefix + "/a2", prefix + "/a3");
  268. QVERIFY(fakeFolder.syncOnce());
  269. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  270. QCOMPARE(counter.nGET, 1);
  271. counter.reset();
  272. // remove localy, and remote move at the same time
  273. fakeFolder.localModifier().remove("A/Q/W/a1m");
  274. remote.rename("A/Q/W/a1m", "A/Q/W/a1p");
  275. remote.rename(prefix + "/A/Q/W/a1m", prefix + "/A/Q/W/a1p");
  276. QVERIFY(fakeFolder.syncOnce());
  277. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  278. QCOMPARE(counter.nGET, 1);
  279. counter.reset();
  280. }
  281. void testMovePropagation()
  282. {
  283. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  284. auto &local = fakeFolder.localModifier();
  285. auto &remote = fakeFolder.remoteModifier();
  286. OperationCounter counter;
  287. fakeFolder.setServerOverride(counter.functor());
  288. // Move
  289. {
  290. counter.reset();
  291. local.rename("A/a1", "A/a1m");
  292. remote.rename("B/b1", "B/b1m");
  293. ItemCompletedSpy completeSpy(fakeFolder);
  294. QVERIFY(fakeFolder.syncOnce());
  295. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  296. QCOMPARE(counter.nGET, 0);
  297. QCOMPARE(counter.nPUT, 0);
  298. QCOMPARE(counter.nMOVE, 1);
  299. QCOMPARE(counter.nDELETE, 0);
  300. QVERIFY(itemSuccessfulMove(completeSpy, "A/a1m"));
  301. QVERIFY(itemSuccessfulMove(completeSpy, "B/b1m"));
  302. QCOMPARE(completeSpy.findItem("A/a1m")->_file, QStringLiteral("A/a1"));
  303. QCOMPARE(completeSpy.findItem("A/a1m")->_renameTarget, QStringLiteral("A/a1m"));
  304. QCOMPARE(completeSpy.findItem("B/b1m")->_file, QStringLiteral("B/b1"));
  305. QCOMPARE(completeSpy.findItem("B/b1m")->_renameTarget, QStringLiteral("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. ItemCompletedSpy completeSpy(fakeFolder);
  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. ItemCompletedSpy completeSpy(fakeFolder);
  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. ItemCompletedSpy completeSpy(fakeFolder);
  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. QCOMPARE(completeSpy.findItem("AM")->_file, QStringLiteral("A"));
  429. QCOMPARE(completeSpy.findItem("AM")->_renameTarget, QStringLiteral("AM"));
  430. QCOMPARE(completeSpy.findItem("BM")->_file, QStringLiteral("B"));
  431. QCOMPARE(completeSpy.findItem("BM")->_renameTarget, QStringLiteral("BM"));
  432. }
  433. // Folder move with contents touched on the same side
  434. {
  435. counter.reset();
  436. local.setContents("AM/a2m", 'C');
  437. // We must change the modtime for it is likely that it did not change between sync.
  438. // (Previous version of the client (<=2.5) would not need this because it was always doing
  439. // checksum comparison for all renames. But newer version no longer does it if the file is
  440. // renamed because the parent folder is renamed)
  441. local.setModTime("AM/a2m", QDateTime::currentDateTimeUtc().addDays(3));
  442. local.rename("AM", "A2");
  443. remote.setContents("BM/b2m", 'C');
  444. remote.rename("BM", "B2");
  445. ItemCompletedSpy completeSpy(fakeFolder);
  446. QVERIFY(fakeFolder.syncOnce());
  447. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  448. QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(fakeFolder.currentRemoteState()));
  449. QCOMPARE(counter.nGET, 1);
  450. QCOMPARE(counter.nPUT, 1);
  451. QCOMPARE(counter.nMOVE, 1);
  452. QCOMPARE(counter.nDELETE, 0);
  453. QCOMPARE(remote.find("A2/a2m")->contentChar, 'C');
  454. QCOMPARE(remote.find("B2/b2m")->contentChar, 'C');
  455. QVERIFY(itemSuccessfulMove(completeSpy, "A2"));
  456. QVERIFY(itemSuccessfulMove(completeSpy, "B2"));
  457. }
  458. // Folder rename with contents touched on the other tree
  459. counter.reset();
  460. remote.setContents("A2/a2m", 'D');
  461. // setContents alone may not produce updated mtime if the test is fast
  462. // and since we don't use checksums here, that matters.
  463. remote.appendByte("A2/a2m");
  464. local.rename("A2", "A3");
  465. local.setContents("B2/b2m", 'D');
  466. local.appendByte("B2/b2m");
  467. remote.rename("B2", "B3");
  468. QVERIFY(fakeFolder.syncOnce());
  469. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  470. QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(fakeFolder.currentRemoteState()));
  471. QCOMPARE(counter.nGET, 1);
  472. QCOMPARE(counter.nPUT, 1);
  473. QCOMPARE(counter.nMOVE, 1);
  474. QCOMPARE(counter.nDELETE, 0);
  475. QCOMPARE(remote.find("A3/a2m")->contentChar, 'D');
  476. QCOMPARE(remote.find("B3/b2m")->contentChar, 'D');
  477. // Folder rename with contents touched on both ends
  478. counter.reset();
  479. remote.setContents("A3/a2m", 'R');
  480. remote.appendByte("A3/a2m");
  481. local.setContents("A3/a2m", 'L');
  482. local.appendByte("A3/a2m");
  483. local.appendByte("A3/a2m");
  484. local.rename("A3", "A4");
  485. remote.setContents("B3/b2m", 'R');
  486. remote.appendByte("B3/b2m");
  487. local.setContents("B3/b2m", 'L');
  488. local.appendByte("B3/b2m");
  489. local.appendByte("B3/b2m");
  490. remote.rename("B3", "B4");
  491. QVERIFY(fakeFolder.syncOnce());
  492. auto currentLocal = fakeFolder.currentLocalState();
  493. auto conflicts = findConflicts(currentLocal.children["A4"]);
  494. QCOMPARE(conflicts.size(), 1);
  495. for (const auto& c : conflicts) {
  496. QCOMPARE(currentLocal.find(c)->contentChar, 'L');
  497. local.remove(c);
  498. }
  499. conflicts = findConflicts(currentLocal.children["B4"]);
  500. QCOMPARE(conflicts.size(), 1);
  501. for (const auto& c : conflicts) {
  502. QCOMPARE(currentLocal.find(c)->contentChar, 'L');
  503. local.remove(c);
  504. }
  505. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  506. QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(fakeFolder.currentRemoteState()));
  507. QCOMPARE(counter.nGET, 2);
  508. QCOMPARE(counter.nPUT, 0);
  509. QCOMPARE(counter.nMOVE, 1);
  510. QCOMPARE(counter.nDELETE, 0);
  511. QCOMPARE(remote.find("A4/a2m")->contentChar, 'R');
  512. QCOMPARE(remote.find("B4/b2m")->contentChar, 'R');
  513. // Rename a folder and rename the contents at the same time
  514. counter.reset();
  515. local.rename("A4/a2m", "A4/a2m2");
  516. local.rename("A4", "A5");
  517. remote.rename("B4/b2m", "B4/b2m2");
  518. remote.rename("B4", "B5");
  519. QVERIFY(fakeFolder.syncOnce());
  520. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  521. QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(fakeFolder.currentRemoteState()));
  522. QCOMPARE(counter.nGET, 0);
  523. QCOMPARE(counter.nPUT, 0);
  524. QCOMPARE(counter.nMOVE, 2);
  525. QCOMPARE(counter.nDELETE, 0);
  526. }
  527. // These renames can be troublesome on windows
  528. void testRenameCaseOnly()
  529. {
  530. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  531. auto &local = fakeFolder.localModifier();
  532. auto &remote = fakeFolder.remoteModifier();
  533. OperationCounter counter;
  534. fakeFolder.setServerOverride(counter.functor());
  535. local.rename("A/a1", "A/A1");
  536. remote.rename("A/a2", "A/A2");
  537. QVERIFY(fakeFolder.syncOnce());
  538. QCOMPARE(fakeFolder.currentLocalState(), remote);
  539. QCOMPARE(printDbData(fakeFolder.dbState()), printDbData(fakeFolder.currentRemoteState()));
  540. QCOMPARE(counter.nGET, 0);
  541. QCOMPARE(counter.nPUT, 0);
  542. QCOMPARE(counter.nMOVE, 1);
  543. QCOMPARE(counter.nDELETE, 0);
  544. }
  545. // Check interaction of moves with file type changes
  546. void testMoveAndTypeChange()
  547. {
  548. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  549. auto &local = fakeFolder.localModifier();
  550. auto &remote = fakeFolder.remoteModifier();
  551. // Touch on one side, rename and mkdir on the other
  552. {
  553. local.appendByte("A/a1");
  554. remote.rename("A/a1", "A/a1mq");
  555. remote.mkdir("A/a1");
  556. remote.appendByte("B/b1");
  557. local.rename("B/b1", "B/b1mq");
  558. local.mkdir("B/b1");
  559. ItemCompletedSpy completeSpy(fakeFolder);
  560. QVERIFY(fakeFolder.syncOnce());
  561. // BUG: This doesn't behave right
  562. //QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  563. }
  564. }
  565. // https://github.com/owncloud/client/issues/6629#issuecomment-402450691
  566. // When a file is moved and the server mtime was not in sync, the local mtime should be kept
  567. void testMoveAndMTimeChange()
  568. {
  569. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  570. OperationCounter counter;
  571. fakeFolder.setServerOverride(counter.functor());
  572. // Changing the mtime on the server (without invalidating the etag)
  573. fakeFolder.remoteModifier().find("A/a1")->lastModified = QDateTime::currentDateTimeUtc().addSecs(-50000);
  574. fakeFolder.remoteModifier().find("A/a2")->lastModified = QDateTime::currentDateTimeUtc().addSecs(-40000);
  575. // Move a few files
  576. fakeFolder.remoteModifier().rename("A/a1", "A/a1_server_renamed");
  577. fakeFolder.localModifier().rename("A/a2", "A/a2_local_renamed");
  578. QVERIFY(fakeFolder.syncOnce());
  579. QCOMPARE(counter.nGET, 0);
  580. QCOMPARE(counter.nPUT, 0);
  581. QCOMPARE(counter.nMOVE, 1);
  582. QCOMPARE(counter.nDELETE, 0);
  583. // Another sync should do nothing
  584. QVERIFY(fakeFolder.syncOnce());
  585. QCOMPARE(counter.nGET, 0);
  586. QCOMPARE(counter.nPUT, 0);
  587. QCOMPARE(counter.nMOVE, 1);
  588. QCOMPARE(counter.nDELETE, 0);
  589. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  590. }
  591. // Test for https://github.com/owncloud/client/issues/6694
  592. void testInvertFolderHierarchy()
  593. {
  594. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  595. fakeFolder.remoteModifier().mkdir("A/Empty");
  596. fakeFolder.remoteModifier().mkdir("A/Empty/Foo");
  597. fakeFolder.remoteModifier().mkdir("C/AllEmpty");
  598. fakeFolder.remoteModifier().mkdir("C/AllEmpty/Bar");
  599. fakeFolder.remoteModifier().insert("A/Empty/f1");
  600. fakeFolder.remoteModifier().insert("A/Empty/Foo/f2");
  601. fakeFolder.remoteModifier().mkdir("C/AllEmpty/f3");
  602. fakeFolder.remoteModifier().mkdir("C/AllEmpty/Bar/f4");
  603. QVERIFY(fakeFolder.syncOnce());
  604. OperationCounter counter;
  605. fakeFolder.setServerOverride(counter.functor());
  606. // "Empty" is after "A", alphabetically
  607. fakeFolder.localModifier().rename("A/Empty", "Empty");
  608. fakeFolder.localModifier().rename("A", "Empty/A");
  609. // "AllEmpty" is before "C", alphabetically
  610. fakeFolder.localModifier().rename("C/AllEmpty", "AllEmpty");
  611. fakeFolder.localModifier().rename("C", "AllEmpty/C");
  612. auto expectedState = fakeFolder.currentLocalState();
  613. QVERIFY(fakeFolder.syncOnce());
  614. QCOMPARE(fakeFolder.currentLocalState(), expectedState);
  615. QCOMPARE(fakeFolder.currentRemoteState(), expectedState);
  616. QCOMPARE(counter.nDELETE, 0);
  617. QCOMPARE(counter.nGET, 0);
  618. QCOMPARE(counter.nPUT, 0);
  619. // Now, the revert, but "crossed"
  620. fakeFolder.localModifier().rename("Empty/A", "A");
  621. fakeFolder.localModifier().rename("AllEmpty/C", "C");
  622. fakeFolder.localModifier().rename("Empty", "C/Empty");
  623. fakeFolder.localModifier().rename("AllEmpty", "A/AllEmpty");
  624. expectedState = fakeFolder.currentLocalState();
  625. QVERIFY(fakeFolder.syncOnce());
  626. QCOMPARE(fakeFolder.currentLocalState(), expectedState);
  627. QCOMPARE(fakeFolder.currentRemoteState(), expectedState);
  628. QCOMPARE(counter.nDELETE, 0);
  629. QCOMPARE(counter.nGET, 0);
  630. QCOMPARE(counter.nPUT, 0);
  631. // Reverse on remote
  632. fakeFolder.remoteModifier().rename("A/AllEmpty", "AllEmpty");
  633. fakeFolder.remoteModifier().rename("C/Empty", "Empty");
  634. fakeFolder.remoteModifier().rename("C", "AllEmpty/C");
  635. fakeFolder.remoteModifier().rename("A", "Empty/A");
  636. expectedState = fakeFolder.currentRemoteState();
  637. QVERIFY(fakeFolder.syncOnce());
  638. QCOMPARE(fakeFolder.currentLocalState(), expectedState);
  639. QCOMPARE(fakeFolder.currentRemoteState(), expectedState);
  640. QCOMPARE(counter.nDELETE, 0);
  641. QCOMPARE(counter.nGET, 0);
  642. QCOMPARE(counter.nPUT, 0);
  643. }
  644. void testDeepHierarchy_data()
  645. {
  646. QTest::addColumn<bool>("local");
  647. QTest::newRow("remote") << false;
  648. QTest::newRow("local") << true;
  649. }
  650. void testDeepHierarchy()
  651. {
  652. QFETCH(bool, local);
  653. FakeFolder fakeFolder { FileInfo::A12_B12_C12_S12() };
  654. auto &modifier = local ? fakeFolder.localModifier() : fakeFolder.remoteModifier();
  655. modifier.mkdir("FolA");
  656. modifier.mkdir("FolA/FolB");
  657. modifier.mkdir("FolA/FolB/FolC");
  658. modifier.mkdir("FolA/FolB/FolC/FolD");
  659. modifier.mkdir("FolA/FolB/FolC/FolD/FolE");
  660. modifier.insert("FolA/FileA.txt");
  661. modifier.insert("FolA/FolB/FileB.txt");
  662. modifier.insert("FolA/FolB/FolC/FileC.txt");
  663. modifier.insert("FolA/FolB/FolC/FolD/FileD.txt");
  664. modifier.insert("FolA/FolB/FolC/FolD/FolE/FileE.txt");
  665. QVERIFY(fakeFolder.syncOnce());
  666. OperationCounter counter;
  667. fakeFolder.setServerOverride(counter.functor());
  668. modifier.insert("FolA/FileA2.txt");
  669. modifier.insert("FolA/FolB/FileB2.txt");
  670. modifier.insert("FolA/FolB/FolC/FileC2.txt");
  671. modifier.insert("FolA/FolB/FolC/FolD/FileD2.txt");
  672. modifier.insert("FolA/FolB/FolC/FolD/FolE/FileE2.txt");
  673. modifier.rename("FolA", "FolA_Renamed");
  674. modifier.rename("FolA_Renamed/FolB", "FolB_Renamed");
  675. modifier.rename("FolB_Renamed/FolC", "FolA");
  676. modifier.rename("FolA/FolD", "FolA/FolD_Renamed");
  677. modifier.mkdir("FolB_Renamed/New");
  678. modifier.rename("FolA/FolD_Renamed/FolE", "FolB_Renamed/New/FolE");
  679. auto expected = local ? fakeFolder.currentLocalState() : fakeFolder.currentRemoteState();
  680. QVERIFY(fakeFolder.syncOnce());
  681. QCOMPARE(fakeFolder.currentLocalState(), expected);
  682. QCOMPARE(fakeFolder.currentRemoteState(), expected);
  683. QCOMPARE(counter.nDELETE, local ? 1 : 0); // FolC was is renamed to an existing name, so it is not considered as renamed
  684. // There was 5 inserts
  685. QCOMPARE(counter.nGET, local ? 0 : 5);
  686. QCOMPARE(counter.nPUT, local ? 5 : 0);
  687. }
  688. void renameOnBothSides()
  689. {
  690. FakeFolder fakeFolder { FileInfo::A12_B12_C12_S12() };
  691. OperationCounter counter;
  692. fakeFolder.setServerOverride(counter.functor());
  693. // Test that renaming a file within a directory that was renamed on the other side actually do a rename.
  694. // 1) move the folder alphabeticaly before
  695. fakeFolder.remoteModifier().rename("A/a1", "A/a1m");
  696. fakeFolder.localModifier().rename("A", "_A");
  697. fakeFolder.localModifier().rename("B/b1", "B/b1m");
  698. fakeFolder.remoteModifier().rename("B", "_B");
  699. QVERIFY(fakeFolder.syncOnce());
  700. QCOMPARE(fakeFolder.currentRemoteState(), fakeFolder.currentRemoteState());
  701. QVERIFY(fakeFolder.currentRemoteState().find("_A/a1m"));
  702. QVERIFY(fakeFolder.currentRemoteState().find("_B/b1m"));
  703. QCOMPARE(counter.nDELETE, 0);
  704. QCOMPARE(counter.nGET, 0);
  705. QCOMPARE(counter.nPUT, 0);
  706. QCOMPARE(counter.nMOVE, 2);
  707. counter.reset();
  708. // 2) move alphabetically after
  709. fakeFolder.remoteModifier().rename("_A/a2", "_A/a2m");
  710. fakeFolder.localModifier().rename("_B/b2", "_B/b2m");
  711. fakeFolder.localModifier().rename("_A", "S/A");
  712. fakeFolder.remoteModifier().rename("_B", "S/B");
  713. QVERIFY(fakeFolder.syncOnce());
  714. QCOMPARE(fakeFolder.currentRemoteState(), fakeFolder.currentRemoteState());
  715. QVERIFY(fakeFolder.currentRemoteState().find("S/A/a2m"));
  716. QVERIFY(fakeFolder.currentRemoteState().find("S/B/b2m"));
  717. QCOMPARE(counter.nDELETE, 0);
  718. QCOMPARE(counter.nGET, 0);
  719. QCOMPARE(counter.nPUT, 0);
  720. QCOMPARE(counter.nMOVE, 2);
  721. }
  722. void moveFileToDifferentFolderOnBothSides()
  723. {
  724. FakeFolder fakeFolder { FileInfo::A12_B12_C12_S12() };
  725. OperationCounter counter;
  726. fakeFolder.setServerOverride(counter.functor());
  727. // Test that moving a file within to different folder on both side does the right thing.
  728. fakeFolder.remoteModifier().rename("B/b1", "A/b1");
  729. fakeFolder.localModifier().rename("B/b1", "C/b1");
  730. fakeFolder.localModifier().rename("B/b2", "A/b2");
  731. fakeFolder.remoteModifier().rename("B/b2", "C/b2");
  732. QVERIFY(fakeFolder.syncOnce());
  733. QCOMPARE(fakeFolder.currentRemoteState(), fakeFolder.currentRemoteState());
  734. QVERIFY(fakeFolder.currentRemoteState().find("A/b1"));
  735. QVERIFY(fakeFolder.currentRemoteState().find("C/b1"));
  736. QVERIFY(fakeFolder.currentRemoteState().find("A/b2"));
  737. QVERIFY(fakeFolder.currentRemoteState().find("C/b2"));
  738. QCOMPARE(counter.nMOVE, 0); // Unfortunately, we can't really make a move in this case
  739. QCOMPARE(counter.nGET, 2);
  740. QCOMPARE(counter.nPUT, 2);
  741. QCOMPARE(counter.nDELETE, 0);
  742. counter.reset();
  743. }
  744. // Test that deletes don't run before renames
  745. void testRenameParallelism()
  746. {
  747. FakeFolder fakeFolder{ FileInfo{} };
  748. fakeFolder.remoteModifier().mkdir("A");
  749. fakeFolder.remoteModifier().insert("A/file");
  750. QVERIFY(fakeFolder.syncOnce());
  751. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  752. fakeFolder.localModifier().mkdir("B");
  753. fakeFolder.localModifier().rename("A/file", "B/file");
  754. fakeFolder.localModifier().remove("A");
  755. QVERIFY(fakeFolder.syncOnce());
  756. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  757. }
  758. void testMovedWithError_data()
  759. {
  760. QTest::addColumn<Vfs::Mode>("vfsMode");
  761. QTest::newRow("Vfs::Off") << Vfs::Off;
  762. QTest::newRow("Vfs::WithSuffix") << Vfs::WithSuffix;
  763. #ifdef Q_OS_WIN32
  764. if (isVfsPluginAvailable(Vfs::WindowsCfApi))
  765. {
  766. QTest::newRow("Vfs::WindowsCfApi") << Vfs::WindowsCfApi;
  767. } else {
  768. QWARN("Skipping Vfs::WindowsCfApi");
  769. }
  770. #endif
  771. }
  772. void testMovedWithError()
  773. {
  774. QFETCH(Vfs::Mode, vfsMode);
  775. const auto getName = [vfsMode] (const QString &s)
  776. {
  777. if (vfsMode == Vfs::WithSuffix)
  778. {
  779. return QStringLiteral("%1" APPLICATION_DOTVIRTUALFILE_SUFFIX).arg(s);
  780. }
  781. return s;
  782. };
  783. const QString src = "folder/folderA/file.txt";
  784. const QString dest = "folder/folderB/file.txt";
  785. FakeFolder fakeFolder{ FileInfo{ QString(), { FileInfo{ QStringLiteral("folder"), { FileInfo{ QStringLiteral("folderA"), { { QStringLiteral("file.txt"), 400 } } }, QStringLiteral("folderB") } } } } };
  786. auto syncOpts = fakeFolder.syncEngine().syncOptions();
  787. syncOpts._parallelNetworkJobs = 0;
  788. fakeFolder.syncEngine().setSyncOptions(syncOpts);
  789. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  790. if (vfsMode != Vfs::Off)
  791. {
  792. auto vfs = QSharedPointer<Vfs>(createVfsFromPlugin(vfsMode).release());
  793. QVERIFY(vfs);
  794. fakeFolder.switchToVfs(vfs);
  795. fakeFolder.syncJournal().internalPinStates().setForPath("", PinState::OnlineOnly);
  796. // make files virtual
  797. fakeFolder.syncOnce();
  798. }
  799. fakeFolder.serverErrorPaths().append(src, 403);
  800. fakeFolder.localModifier().rename(getName(src), getName(dest));
  801. QVERIFY(!fakeFolder.currentLocalState().find(getName(src)));
  802. QVERIFY(fakeFolder.currentLocalState().find(getName(dest)));
  803. QVERIFY(fakeFolder.currentRemoteState().find(src));
  804. QVERIFY(!fakeFolder.currentRemoteState().find(dest));
  805. // sync1 file gets detected as error, instruction is still NEW_FILE
  806. fakeFolder.syncOnce();
  807. // sync2 file is in error state, checkErrorBlacklisting sets instruction to IGNORED
  808. fakeFolder.syncOnce();
  809. if (vfsMode != Vfs::Off)
  810. {
  811. fakeFolder.syncJournal().internalPinStates().setForPath("", PinState::AlwaysLocal);
  812. fakeFolder.syncOnce();
  813. }
  814. QVERIFY(!fakeFolder.currentLocalState().find(src));
  815. QVERIFY(fakeFolder.currentLocalState().find(getName(dest)));
  816. if (vfsMode == Vfs::WithSuffix)
  817. {
  818. // the placeholder was not restored as it is still in error state
  819. QVERIFY(!fakeFolder.currentLocalState().find(dest));
  820. }
  821. QVERIFY(fakeFolder.currentRemoteState().find(src));
  822. QVERIFY(!fakeFolder.currentRemoteState().find(dest));
  823. }
  824. };
  825. QTEST_GUILESS_MAIN(TestSyncMove)
  826. #include "testsyncmove.moc"