testasyncop.cpp 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  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. class FakeAsyncReply : public FakeReply
  12. {
  13. Q_OBJECT
  14. QByteArray _pollLocation;
  15. public:
  16. FakeAsyncReply(const QByteArray &pollLocation, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
  17. : FakeReply { parent }
  18. , _pollLocation(pollLocation)
  19. {
  20. setRequest(request);
  21. setUrl(request.url());
  22. setOperation(op);
  23. open(QIODevice::ReadOnly);
  24. QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
  25. }
  26. Q_INVOKABLE void respond()
  27. {
  28. setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 202);
  29. setRawHeader("OC-JobStatus-Location", _pollLocation);
  30. emit metaDataChanged();
  31. emit finished();
  32. }
  33. void abort() override {}
  34. qint64 readData(char *, qint64) override { return 0; }
  35. };
  36. class TestAsyncOp : public QObject
  37. {
  38. Q_OBJECT
  39. private slots:
  40. void asyncUploadOperations()
  41. {
  42. FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
  43. fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ { "chunking", "1.0" } } } });
  44. // Reduce max chunk size a bit so we get more chunks
  45. SyncOptions options;
  46. options._maxChunkSize = 20 * 1000;
  47. fakeFolder.syncEngine().setSyncOptions(options);
  48. int nGET = 0;
  49. // This test is made of several testcases.
  50. // the testCases maps a filename to a couple of callback.
  51. // When a file is uploaded, the fake server will always return the 202 code, and will set
  52. // the `perform` functor to what needs to be done to complete the transaction.
  53. // The testcase consist of the `pollRequest` which will be called when the sync engine
  54. // calls the poll url.
  55. struct TestCase
  56. {
  57. using PollRequest_t = std::function<QNetworkReply *(TestCase *, const QNetworkRequest &request)>;
  58. PollRequest_t pollRequest;
  59. std::function<FileInfo *()> perform = nullptr;
  60. };
  61. QHash<QString, TestCase> testCases;
  62. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *outgoingData) -> QNetworkReply * {
  63. auto path = request.url().path();
  64. if (op == QNetworkAccessManager::GetOperation && path.startsWith("/async-poll/")) {
  65. auto file = path.mid(sizeof("/async-poll/") - 1);
  66. Q_ASSERT(testCases.contains(file));
  67. auto &testCase = testCases[file];
  68. return testCase.pollRequest(&testCase, request);
  69. }
  70. if (op == QNetworkAccessManager::PutOperation && !path.contains("/uploads/")) {
  71. // Not chunking
  72. auto file = getFilePathFromUrl(request.url());
  73. Q_ASSERT(testCases.contains(file));
  74. auto &testCase = testCases[file];
  75. Q_ASSERT(!testCase.perform);
  76. auto putPayload = outgoingData->readAll();
  77. testCase.perform = [putPayload, request, &fakeFolder] {
  78. return FakePutReply::perform(fakeFolder.remoteModifier(), request, putPayload);
  79. };
  80. return new FakeAsyncReply("/async-poll/" + file.toUtf8(), op, request, &fakeFolder.syncEngine());
  81. } else if (request.attribute(QNetworkRequest::CustomVerbAttribute) == "MOVE") {
  82. QString file = getFilePathFromUrl(QUrl::fromEncoded(request.rawHeader("Destination")));
  83. Q_ASSERT(testCases.contains(file));
  84. auto &testCase = testCases[file];
  85. Q_ASSERT(!testCase.perform);
  86. testCase.perform = [request, &fakeFolder] {
  87. return FakeChunkMoveReply::perform(fakeFolder.uploadState(), fakeFolder.remoteModifier(), request);
  88. };
  89. return new FakeAsyncReply("/async-poll/" + file.toUtf8(), op, request, &fakeFolder.syncEngine());
  90. } else if (op == QNetworkAccessManager::GetOperation) {
  91. nGET++;
  92. }
  93. return nullptr;
  94. });
  95. // Callback to be used to finalize the transaction and return the success
  96. auto successCallback = [](TestCase *tc, const QNetworkRequest &request) {
  97. tc->pollRequest = [](TestCase *, const QNetworkRequest &) -> QNetworkReply * { std::abort(); }; // shall no longer be called
  98. FileInfo *info = tc->perform();
  99. QByteArray body = R"({ "status":"finished", "ETag":"\")" + info->etag + R"(\"", "fileId":")" + info->fileId + "\"}\n";
  100. return new FakePayloadReply(QNetworkAccessManager::GetOperation, request, body, nullptr);
  101. };
  102. // Callback that never finishes
  103. auto waitForeverCallback = [](TestCase *, const QNetworkRequest &request) {
  104. QByteArray body = "{\"status\":\"started\"}\n";
  105. return new FakePayloadReply(QNetworkAccessManager::GetOperation, request, body, nullptr);
  106. };
  107. // Callback that simulate an error.
  108. auto errorCallback = [](TestCase *tc, const QNetworkRequest &request) {
  109. tc->pollRequest = [](TestCase *, const QNetworkRequest &) -> QNetworkReply * { std::abort(); }; // shall no longer be called;
  110. QByteArray body = "{\"status\":\"error\",\"errorCode\":500,\"errorMessage\":\"TestingErrors\"}\n";
  111. return new FakePayloadReply(QNetworkAccessManager::GetOperation, request, body, nullptr);
  112. };
  113. // This lambda takes another functor as a parameter, and returns a callback that will
  114. // tell the client needs to poll again, and further call to the poll url will call the
  115. // given callback
  116. auto waitAndChain = [](const TestCase::PollRequest_t &chain) {
  117. return [chain](TestCase *tc, const QNetworkRequest &request) {
  118. tc->pollRequest = chain;
  119. QByteArray body = "{\"status\":\"started\"}\n";
  120. return new FakePayloadReply(QNetworkAccessManager::GetOperation, request, body, nullptr);
  121. };
  122. };
  123. // Create a testcase by creating a file of a given size locally and assigning it a callback
  124. auto insertFile = [&](const QString &file, qint64 size, TestCase::PollRequest_t cb) {
  125. fakeFolder.localModifier().insert(file, size);
  126. testCases[file] = { std::move(cb) };
  127. };
  128. fakeFolder.localModifier().mkdir("success");
  129. insertFile("success/chunked_success", options._maxChunkSize * 3, successCallback);
  130. insertFile("success/single_success", 300, successCallback);
  131. insertFile("success/chunked_patience", options._maxChunkSize * 3,
  132. waitAndChain(waitAndChain(successCallback)));
  133. insertFile("success/single_patience", 300,
  134. waitAndChain(waitAndChain(successCallback)));
  135. fakeFolder.localModifier().mkdir("err");
  136. insertFile("err/chunked_error", options._maxChunkSize * 3, errorCallback);
  137. insertFile("err/single_error", 300, errorCallback);
  138. insertFile("err/chunked_error2", options._maxChunkSize * 3, waitAndChain(errorCallback));
  139. insertFile("err/single_error2", 300, waitAndChain(errorCallback));
  140. // First sync should finish by itself.
  141. // All the things in "success/" should be transfered, the things in "err/" not
  142. QVERIFY(!fakeFolder.syncOnce());
  143. QCOMPARE(nGET, 0);
  144. QCOMPARE(*fakeFolder.currentLocalState().find("success"),
  145. *fakeFolder.currentRemoteState().find("success"));
  146. testCases.clear();
  147. testCases["err/chunked_error"] = { successCallback };
  148. testCases["err/chunked_error2"] = { successCallback };
  149. testCases["err/single_error"] = { successCallback };
  150. testCases["err/single_error2"] = { successCallback };
  151. fakeFolder.localModifier().mkdir("waiting");
  152. insertFile("waiting/small", 300, waitForeverCallback);
  153. insertFile("waiting/willNotConflict", 300, waitForeverCallback);
  154. insertFile("waiting/big", options._maxChunkSize * 3,
  155. waitAndChain(waitAndChain([&](TestCase *tc, const QNetworkRequest &request) {
  156. QTimer::singleShot(0, &fakeFolder.syncEngine(), &SyncEngine::abort);
  157. return waitAndChain(waitForeverCallback)(tc, request);
  158. })));
  159. QVERIFY(fakeFolder.syncJournal().wipeErrorBlacklist() != -1);
  160. // This second sync will redo the files that had errors
  161. // But the waiting folder will not complete before it is aborted.
  162. QVERIFY(!fakeFolder.syncOnce());
  163. QCOMPARE(nGET, 0);
  164. QCOMPARE(*fakeFolder.currentLocalState().find("err"),
  165. *fakeFolder.currentRemoteState().find("err"));
  166. testCases["waiting/small"].pollRequest = waitAndChain(waitAndChain(successCallback));
  167. testCases["waiting/big"].pollRequest = waitAndChain(successCallback);
  168. testCases["waiting/willNotConflict"].pollRequest =
  169. [&fakeFolder, &successCallback](TestCase *tc, const QNetworkRequest &request) {
  170. auto &remoteModifier = fakeFolder.remoteModifier(); // successCallback destroys the capture
  171. auto reply = successCallback(tc, request);
  172. // This is going to succeed, and after we just change the file.
  173. // This should not be a conflict, but this should be downloaded in the
  174. // next sync
  175. remoteModifier.appendByte("waiting/willNotConflict");
  176. return reply;
  177. };
  178. int nPUT = 0;
  179. int nMOVE = 0;
  180. int nDELETE = 0;
  181. fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
  182. auto path = request.url().path();
  183. if (op == QNetworkAccessManager::GetOperation && path.startsWith("/async-poll/")) {
  184. auto file = path.mid(sizeof("/async-poll/") - 1);
  185. Q_ASSERT(testCases.contains(file));
  186. auto &testCase = testCases[file];
  187. return testCase.pollRequest(&testCase, request);
  188. } else if (op == QNetworkAccessManager::PutOperation) {
  189. nPUT++;
  190. } else if (op == QNetworkAccessManager::GetOperation) {
  191. nGET++;
  192. } else if (op == QNetworkAccessManager::DeleteOperation) {
  193. nDELETE++;
  194. } else if (request.attribute(QNetworkRequest::CustomVerbAttribute) == "MOVE") {
  195. nMOVE++;
  196. }
  197. return nullptr;
  198. });
  199. // This last sync will do the waiting stuff
  200. QVERIFY(fakeFolder.syncOnce());
  201. QCOMPARE(nGET, 1); // "waiting/willNotConflict"
  202. QCOMPARE(nPUT, 0);
  203. QCOMPARE(nMOVE, 0);
  204. QCOMPARE(nDELETE, 0);
  205. QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
  206. }
  207. };
  208. QTEST_GUILESS_MAIN(TestAsyncOp)
  209. #include "testasyncop.moc"