testchunkingng.cpp 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  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. /* Upload a 1/3 of a file of given size.
  12. * fakeFolder needs to be synchronized */
  13. static void partialUpload(FakeFolder &fakeFolder, const QString &name, int size)
  14. {
  15. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  16. QCOMPARE(fakeFolder.uploadState().children.count(), 0); // The state should be clean
  17. fakeFolder.localModifier().insert(name, size);
  18. // Abort when the upload is at 1/3
  19. int sizeWhenAbort = -1;
  20. auto con = QObject::connect(&fakeFolder.syncEngine(), &SyncEngine::transmissionProgress,
  21. [&](const ProgressInfo &progress) {
  22. if (progress.completedSize() > (progress.totalSize() /3 )) {
  23. sizeWhenAbort = progress.completedSize();
  24. fakeFolder.syncEngine().abort();
  25. }
  26. });
  27. QVERIFY(!fakeFolder.syncOnce()); // there should have been an error
  28. QObject::disconnect(con);
  29. QVERIFY(sizeWhenAbort > 0);
  30. QVERIFY(sizeWhenAbort < size);
  31. QCOMPARE(fakeFolder.uploadState().children.count(), 1); // the transfer was done with chunking
  32. auto upStateChildren = fakeFolder.uploadState().children.first().children;
  33. QCOMPARE(sizeWhenAbort, std::accumulate(upStateChildren.cbegin(), upStateChildren.cend(), 0,
  34. [](int s, const FileInfo &i) { return s + i.size; }));
  35. }
  36. class TestChunkingNG : public QObject
  37. {
  38. Q_OBJECT
  39. private slots:
  40. void testFileUpload() {
  41. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  42. fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"chunking", "1.0"} } } });
  43. const int size = 300 * 1000 * 1000; // 300 MB
  44. fakeFolder.localModifier().insert("A/a0", size);
  45. QVERIFY(fakeFolder.syncOnce());
  46. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  47. QCOMPARE(fakeFolder.uploadState().children.count(), 1); // the transfer was done with chunking
  48. QCOMPARE(fakeFolder.currentRemoteState().find("A/a0")->size, size);
  49. // Check that another upload of the same file also work.
  50. fakeFolder.localModifier().appendByte("A/a0");
  51. QVERIFY(fakeFolder.syncOnce());
  52. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  53. QCOMPARE(fakeFolder.uploadState().children.count(), 2); // the transfer was done with chunking
  54. }
  55. void testResume () {
  56. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  57. fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"chunking", "1.0"} } } });
  58. const int size = 300 * 1000 * 1000; // 300 MB
  59. partialUpload(fakeFolder, "A/a0", size);
  60. QCOMPARE(fakeFolder.uploadState().children.count(), 1);
  61. auto chunkingId = fakeFolder.uploadState().children.first().name;
  62. const auto &chunkMap = fakeFolder.uploadState().children.first().children;
  63. quint64 uploadedSize = std::accumulate(chunkMap.begin(), chunkMap.end(), 0LL, [](quint64 s, const FileInfo &f) { return s + f.size; });
  64. QVERIFY(uploadedSize > 50 * 1000 * 1000); // at least 50 MB
  65. // Add a fake file to make sure it gets deleted
  66. fakeFolder.uploadState().children.first().insert("10000", size);
  67. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
  68. if (op == QNetworkAccessManager::PutOperation) {
  69. // Test that we properly resuming and are not sending past data again.
  70. Q_ASSERT(request.rawHeader("OC-Chunk-Offset").toULongLong() >= uploadedSize);
  71. }
  72. return nullptr;
  73. });
  74. QVERIFY(fakeFolder.syncOnce());
  75. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  76. QCOMPARE(fakeFolder.currentRemoteState().find("A/a0")->size, size);
  77. // The same chunk id was re-used
  78. QCOMPARE(fakeFolder.uploadState().children.count(), 1);
  79. QCOMPARE(fakeFolder.uploadState().children.first().name, chunkingId);
  80. }
  81. // Check what happens when we abort during the final MOVE and the
  82. // the final MOVE takes longer than the abort-delay
  83. void testLateAbortHard()
  84. {
  85. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  86. fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ { "chunking", "1.0" } } }, { "checksums", QVariantMap{ { "supportedTypes", QStringList() << "SHA1" } } } });
  87. const int size = 150 * 1000 * 1000;
  88. // Make the MOVE never reply, but trigger a client-abort and apply the change remotely
  89. auto parent = new QObject;
  90. QByteArray moveChecksumHeader;
  91. int nGET = 0;
  92. int responseDelay = 10000; // bigger than abort-wait timeout
  93. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
  94. if (request.attribute(QNetworkRequest::CustomVerbAttribute) == "MOVE") {
  95. QTimer::singleShot(50, parent, [&]() { fakeFolder.syncEngine().abort(); });
  96. moveChecksumHeader = request.rawHeader("OC-Checksum");
  97. return new DelayedReply<FakeChunkMoveReply>(responseDelay, fakeFolder.uploadState(), fakeFolder.remoteModifier(), op, request, parent);
  98. } else if (op == QNetworkAccessManager::GetOperation) {
  99. nGET++;
  100. }
  101. return nullptr;
  102. });
  103. // Test 1: NEW file aborted
  104. fakeFolder.localModifier().insert("A/a0", size);
  105. QVERIFY(!fakeFolder.syncOnce()); // error: abort!
  106. // Now the next sync gets a NEW/NEW conflict and since there's no checksum
  107. // it just becomes a UPDATE_METADATA
  108. auto checkEtagUpdated = [&](SyncFileItemVector &items) {
  109. QCOMPARE(items.size(), 1);
  110. QCOMPARE(items[0]->_file, QLatin1String("A"));
  111. SyncJournalFileRecord record;
  112. QVERIFY(fakeFolder.syncJournal().getFileRecord(QByteArray("A/a0"), &record));
  113. QCOMPARE(record._etag, fakeFolder.remoteModifier().find("A/a0")->etag.toUtf8());
  114. };
  115. auto connection = connect(&fakeFolder.syncEngine(), &SyncEngine::aboutToPropagate, checkEtagUpdated);
  116. QVERIFY(fakeFolder.syncOnce());
  117. disconnect(connection);
  118. QCOMPARE(nGET, 0);
  119. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  120. // Test 2: modified file upload aborted
  121. fakeFolder.localModifier().appendByte("A/a0");
  122. QVERIFY(!fakeFolder.syncOnce()); // error: abort!
  123. // An EVAL/EVAL conflict is also UPDATE_METADATA when there's no checksums
  124. connection = connect(&fakeFolder.syncEngine(), &SyncEngine::aboutToPropagate, checkEtagUpdated);
  125. QVERIFY(fakeFolder.syncOnce());
  126. disconnect(connection);
  127. QCOMPARE(nGET, 0);
  128. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  129. // Test 3: modified file upload aborted, with good checksums
  130. fakeFolder.localModifier().appendByte("A/a0");
  131. QVERIFY(!fakeFolder.syncOnce()); // error: abort!
  132. // Set the remote checksum -- the test setup doesn't do it automatically
  133. QVERIFY(!moveChecksumHeader.isEmpty());
  134. fakeFolder.remoteModifier().find("A/a0")->checksums = moveChecksumHeader;
  135. QVERIFY(fakeFolder.syncOnce());
  136. disconnect(connection);
  137. QCOMPARE(nGET, 0); // no new download, just a metadata update!
  138. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  139. // Test 4: New file, that gets deleted locally before the next sync
  140. fakeFolder.localModifier().insert("A/a3", size);
  141. QVERIFY(!fakeFolder.syncOnce()); // error: abort!
  142. fakeFolder.localModifier().remove("A/a3");
  143. // bug: in this case we must expect a re-download of A/A3
  144. QVERIFY(fakeFolder.syncOnce());
  145. QCOMPARE(nGET, 1);
  146. QVERIFY(fakeFolder.currentLocalState().find("A/a3"));
  147. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  148. }
  149. // Check what happens when we abort during the final MOVE and the
  150. // the final MOVE is short enough for the abort-delay to help
  151. void testLateAbortRecoverable()
  152. {
  153. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  154. fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ { "chunking", "1.0" } } }, { "checksums", QVariantMap{ { "supportedTypes", QStringList() << "SHA1" } } } });
  155. const int size = 150 * 1000 * 1000;
  156. // Make the MOVE never reply, but trigger a client-abort and apply the change remotely
  157. auto parent = new QObject;
  158. QByteArray moveChecksumHeader;
  159. int nGET = 0;
  160. int responseDelay = 2000; // smaller than abort-wait timeout
  161. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
  162. if (request.attribute(QNetworkRequest::CustomVerbAttribute) == "MOVE") {
  163. QTimer::singleShot(50, parent, [&]() { fakeFolder.syncEngine().abort(); });
  164. moveChecksumHeader = request.rawHeader("OC-Checksum");
  165. return new DelayedReply<FakeChunkMoveReply>(responseDelay, fakeFolder.uploadState(), fakeFolder.remoteModifier(), op, request, parent);
  166. } else if (op == QNetworkAccessManager::GetOperation) {
  167. nGET++;
  168. }
  169. return nullptr;
  170. });
  171. // Test 1: NEW file aborted
  172. fakeFolder.localModifier().insert("A/a0", size);
  173. QVERIFY(fakeFolder.syncOnce());
  174. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  175. // Test 2: modified file upload aborted
  176. fakeFolder.localModifier().appendByte("A/a0");
  177. QVERIFY(fakeFolder.syncOnce());
  178. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  179. }
  180. // We modify the file locally after it has been partially uploaded
  181. void testRemoveStale1() {
  182. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  183. fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"chunking", "1.0"} } } });
  184. const int size = 300 * 1000 * 1000; // 300 MB
  185. partialUpload(fakeFolder, "A/a0", size);
  186. QCOMPARE(fakeFolder.uploadState().children.count(), 1);
  187. auto chunkingId = fakeFolder.uploadState().children.first().name;
  188. fakeFolder.localModifier().setContents("A/a0", 'B');
  189. fakeFolder.localModifier().appendByte("A/a0");
  190. QVERIFY(fakeFolder.syncOnce());
  191. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  192. QCOMPARE(fakeFolder.currentRemoteState().find("A/a0")->size, size + 1);
  193. // A different chunk id was used, and the previous one is removed
  194. QCOMPARE(fakeFolder.uploadState().children.count(), 1);
  195. QVERIFY(fakeFolder.uploadState().children.first().name != chunkingId);
  196. }
  197. // We remove the file locally after it has been partially uploaded
  198. void testRemoveStale2() {
  199. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  200. fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"chunking", "1.0"} } } });
  201. const int size = 300 * 1000 * 1000; // 300 MB
  202. partialUpload(fakeFolder, "A/a0", size);
  203. QCOMPARE(fakeFolder.uploadState().children.count(), 1);
  204. fakeFolder.localModifier().remove("A/a0");
  205. QVERIFY(fakeFolder.syncOnce());
  206. QCOMPARE(fakeFolder.uploadState().children.count(), 0);
  207. }
  208. void testCreateConflictWhileSyncing() {
  209. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  210. fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"chunking", "1.0"} } } });
  211. const int size = 150 * 1000 * 1000; // 150 MB
  212. // Put a file on the server and download it.
  213. fakeFolder.remoteModifier().insert("A/a0", size);
  214. QVERIFY(fakeFolder.syncOnce());
  215. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  216. // Modify the file localy and start the upload
  217. fakeFolder.localModifier().setContents("A/a0", 'B');
  218. fakeFolder.localModifier().appendByte("A/a0");
  219. // But in the middle of the sync, modify the file on the server
  220. QMetaObject::Connection con = QObject::connect(&fakeFolder.syncEngine(), &SyncEngine::transmissionProgress,
  221. [&](const ProgressInfo &progress) {
  222. if (progress.completedSize() > (progress.totalSize() / 2 )) {
  223. fakeFolder.remoteModifier().setContents("A/a0", 'C');
  224. QObject::disconnect(con);
  225. }
  226. });
  227. QVERIFY(!fakeFolder.syncOnce());
  228. // There was a precondition failed error, this means wen need to sync again
  229. QCOMPARE(fakeFolder.syncEngine().isAnotherSyncNeeded(), ImmediateFollowUp);
  230. QCOMPARE(fakeFolder.uploadState().children.count(), 1); // We did not clean the chunks at this point
  231. // Now we will download the server file and create a conflict
  232. QVERIFY(fakeFolder.syncOnce());
  233. auto localState = fakeFolder.currentLocalState();
  234. // A0 is the one from the server
  235. QCOMPARE(localState.find("A/a0")->size, size);
  236. QCOMPARE(localState.find("A/a0")->contentChar, 'C');
  237. // There is a conflict file with our version
  238. auto &stateAChildren = localState.find("A")->children;
  239. auto it = std::find_if(stateAChildren.cbegin(), stateAChildren.cend(), [&](const FileInfo &fi) {
  240. return fi.name.startsWith("a0_conflict");
  241. });
  242. QVERIFY(it != stateAChildren.cend());
  243. QCOMPARE(it->contentChar, 'B');
  244. QCOMPARE(it->size, size+1);
  245. // Remove the conflict file so the comparison works!
  246. fakeFolder.localModifier().remove("A/" + it->name);
  247. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  248. QCOMPARE(fakeFolder.uploadState().children.count(), 0); // The last sync cleaned the chunks
  249. }
  250. void testModifyLocalFileWhileUploading() {
  251. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  252. fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"chunking", "1.0"} } } });
  253. const int size = 150 * 1000 * 1000; // 150 MB
  254. fakeFolder.localModifier().insert("A/a0", size);
  255. // middle of the sync, modify the file
  256. QMetaObject::Connection con = QObject::connect(&fakeFolder.syncEngine(), &SyncEngine::transmissionProgress,
  257. [&](const ProgressInfo &progress) {
  258. if (progress.completedSize() > (progress.totalSize() / 2 )) {
  259. fakeFolder.localModifier().setContents("A/a0", 'B');
  260. fakeFolder.localModifier().appendByte("A/a0");
  261. QObject::disconnect(con);
  262. }
  263. });
  264. QVERIFY(!fakeFolder.syncOnce());
  265. // There should be a followup sync
  266. QCOMPARE(fakeFolder.syncEngine().isAnotherSyncNeeded(), ImmediateFollowUp);
  267. QCOMPARE(fakeFolder.uploadState().children.count(), 1); // We did not clean the chunks at this point
  268. auto chunkingId = fakeFolder.uploadState().children.first().name;
  269. // Now we make a new sync which should upload the file for good.
  270. QVERIFY(fakeFolder.syncOnce());
  271. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  272. QCOMPARE(fakeFolder.currentRemoteState().find("A/a0")->size, size+1);
  273. // A different chunk id was used, and the previous one is removed
  274. QCOMPARE(fakeFolder.uploadState().children.count(), 1);
  275. QVERIFY(fakeFolder.uploadState().children.first().name != chunkingId);
  276. }
  277. void testResumeServerDeletedChunks() {
  278. FakeFolder fakeFolder{FileInfo::A12_B12_C12_S12()};
  279. fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ {"chunking", "1.0"} } } });
  280. const int size = 300 * 1000 * 1000; // 300 MB
  281. partialUpload(fakeFolder, "A/a0", size);
  282. QCOMPARE(fakeFolder.uploadState().children.count(), 1);
  283. auto chunkingId = fakeFolder.uploadState().children.first().name;
  284. // Delete the chunks on the server
  285. fakeFolder.uploadState().children.clear();
  286. QVERIFY(fakeFolder.syncOnce());
  287. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  288. QCOMPARE(fakeFolder.currentRemoteState().find("A/a0")->size, size);
  289. // A different chunk id was used
  290. QCOMPARE(fakeFolder.uploadState().children.count(), 1);
  291. QVERIFY(fakeFolder.uploadState().children.first().name != chunkingId);
  292. }
  293. // Check what happens when the connection is dropped on the PUT (non-chunking) or MOVE (chunking)
  294. // for on the issue #5106
  295. void connectionDroppedBeforeEtagRecieved_data()
  296. {
  297. QTest::addColumn<bool>("chunking");
  298. QTest::newRow("big file") << true;
  299. QTest::newRow("small file") << false;
  300. }
  301. void connectionDroppedBeforeEtagRecieved()
  302. {
  303. QFETCH(bool, chunking);
  304. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  305. fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ { "chunking", "1.0" } } }, { "checksums", QVariantMap{ { "supportedTypes", QStringList() << "SHA1" } } } });
  306. const int size = chunking ? 150 * 1000 * 1000 : 300;
  307. // Make the MOVE never reply, but trigger a client-abort and apply the change remotely
  308. QByteArray checksumHeader;
  309. int nGET = 0;
  310. QScopedValueRollback<int> setHttpTimeout(AbstractNetworkJob::httpTimeout, 1);
  311. int responseDelay = AbstractNetworkJob::httpTimeout * 1000 * 1000; // much bigger than http timeout (so a timeout will occur)
  312. // This will perform the operation on the server, but the reply will not come to the client
  313. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *outgoingData) -> QNetworkReply * {
  314. if (!chunking && op == QNetworkAccessManager::PutOperation) {
  315. checksumHeader = request.rawHeader("OC-Checksum");
  316. return new DelayedReply<FakePutReply>(responseDelay, fakeFolder.remoteModifier(), op, request, outgoingData->readAll(), &fakeFolder.syncEngine());
  317. } else if (chunking && request.attribute(QNetworkRequest::CustomVerbAttribute) == "MOVE") {
  318. checksumHeader = request.rawHeader("OC-Checksum");
  319. return new DelayedReply<FakeChunkMoveReply>(responseDelay, fakeFolder.uploadState(), fakeFolder.remoteModifier(), op, request, &fakeFolder.syncEngine());
  320. } else if (op == QNetworkAccessManager::GetOperation) {
  321. nGET++;
  322. }
  323. return nullptr;
  324. });
  325. // Test 1: a NEW file
  326. fakeFolder.localModifier().insert("A/a0", size);
  327. QVERIFY(!fakeFolder.syncOnce()); // timeout!
  328. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); // but the upload succeeded
  329. QVERIFY(!checksumHeader.isEmpty());
  330. fakeFolder.remoteModifier().find("A/a0")->checksums = checksumHeader; // The test system don't do that automatically
  331. // Should be resolved properly
  332. QVERIFY(fakeFolder.syncOnce());
  333. QCOMPARE(nGET, 0);
  334. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  335. // Test 2: Modify the file further
  336. fakeFolder.localModifier().appendByte("A/a0");
  337. QVERIFY(!fakeFolder.syncOnce()); // timeout!
  338. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); // but the upload succeeded
  339. fakeFolder.remoteModifier().find("A/a0")->checksums = checksumHeader;
  340. // modify again, should not cause conflict
  341. fakeFolder.localModifier().appendByte("A/a0");
  342. QVERIFY(!fakeFolder.syncOnce()); // now it's trying to upload the modified file
  343. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  344. fakeFolder.remoteModifier().find("A/a0")->checksums = checksumHeader;
  345. QVERIFY(fakeFolder.syncOnce());
  346. QCOMPARE(nGET, 0);
  347. }
  348. };
  349. QTEST_GUILESS_MAIN(TestChunkingNG)
  350. #include "testchunkingng.moc"