diff options
author | Olivier Goffart <ogoffart@woboq.com> | 2018-06-25 18:47:52 +0300 |
---|---|---|
committer | Markus Goetz <markus@woboq.com> | 2019-03-20 14:30:35 +0300 |
commit | 3093ef55d288cebe8ad01a52d6a71e88a1260afc (patch) | |
tree | 11406558a3aa6dc3af88721b4139771e4d0494eb | |
parent | ed1e534d24f871a0ab6ea02e4c88eefe3126bf59 (diff) |
Upload: asynchronious operations
Implements https://github.com/owncloud/core/pull/31851
-rw-r--r-- | src/common/syncjournaldb.h | 6 | ||||
-rw-r--r-- | src/libsync/owncloudpropagator.cpp | 14 | ||||
-rw-r--r-- | src/libsync/propagateupload.cpp | 30 | ||||
-rw-r--r-- | src/libsync/propagateupload.h | 6 | ||||
-rw-r--r-- | src/libsync/propagateuploadng.cpp | 12 | ||||
-rw-r--r-- | src/libsync/propagateuploadv1.cpp | 2 | ||||
-rw-r--r-- | test/CMakeLists.txt | 1 | ||||
-rw-r--r-- | test/syncenginetestutils.h | 89 | ||||
-rw-r--r-- | test/testasyncop.cpp | 236 |
9 files changed, 349 insertions, 47 deletions
diff --git a/src/common/syncjournaldb.h b/src/common/syncjournaldb.h index e3888290c..c9deb28b4 100644 --- a/src/common/syncjournaldb.h +++ b/src/common/syncjournaldb.h @@ -123,9 +123,9 @@ public: struct PollInfo { - QString _file; - QString _url; - qint64 _modtime; + QString _file; // The relative path of a file + QString _url; // the poll url. (This pollinfo is invalid if _url is empty) + qint64 _modtime; // The modtime of the file being uploaded }; DownloadInfo getDownloadInfo(const QString &file); diff --git a/src/libsync/owncloudpropagator.cpp b/src/libsync/owncloudpropagator.cpp index ca49752e0..f2381a7fe 100644 --- a/src/libsync/owncloudpropagator.cpp +++ b/src/libsync/owncloudpropagator.cpp @@ -1017,12 +1017,12 @@ void CleanupPollsJob::start() auto info = _pollInfos.first(); _pollInfos.pop_front(); SyncJournalFileRecord record; - if (_journal->getFileRecord(info._file, &record) && record.isValid()) { - SyncFileItemPtr item = SyncFileItem::fromSyncJournalFileRecord(record); - PollJob *job = new PollJob(_account, info._url, item, _journal, _localPath, this); - connect(job, &PollJob::finishedSignal, this, &CleanupPollsJob::slotPollFinished); - job->start(); - } + SyncFileItemPtr item(new SyncFileItem); + item->_file = info._file; + item->_modtime = info._modtime; + PollJob *job = new PollJob(_account, info._url, item, _journal, _localPath, this); + connect(job, &PollJob::finishedSignal, this, &CleanupPollsJob::slotPollFinished); + job->start(); } void CleanupPollsJob::slotPollFinished() @@ -1044,7 +1044,7 @@ void CleanupPollsJob::slotPollFinished() deleteLater(); return; } - // TODO: Is syncfilestatustracker notified somehow? + _journal->setUploadInfo(job->_item->_file, SyncJournalDb::UploadInfo()); } // Continue with the next entry, or finish start(); diff --git a/src/libsync/propagateupload.cpp b/src/libsync/propagateupload.cpp index 05ddd6566..306b9c394 100644 --- a/src/libsync/propagateupload.cpp +++ b/src/libsync/propagateupload.cpp @@ -98,7 +98,7 @@ void PollJob::start() QUrl finalUrl = QUrl::fromUserInput(accountUrl.scheme() + QLatin1String("://") + accountUrl.authority() + (path().startsWith('/') ? QLatin1String("") : QLatin1String("/")) + path()); sendRequest("GET", finalUrl); - connect(reply(), &QNetworkReply::downloadProgress, this, &AbstractNetworkJob::resetTimeout); + connect(reply(), &QNetworkReply::downloadProgress, this, &AbstractNetworkJob::resetTimeout, Qt::UniqueConnection); AbstractNetworkJob::start(); } @@ -123,14 +123,14 @@ bool PollJob::finished() emit finishedSignal(); return true; } - start(); + QTimer::singleShot(8 * 1000, this, &PollJob::start); return false; } QByteArray jsonData = reply()->readAll().trimmed(); - qCInfo(lcPollJob) << ">" << jsonData << "<" << reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); QJsonParseError jsonParseError; - QJsonObject status = QJsonDocument::fromJson(jsonData, &jsonParseError).object(); + QJsonObject json = QJsonDocument::fromJson(jsonData, &jsonParseError).object(); + qCInfo(lcPollJob) << ">" << jsonData << "<" << reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() << json << jsonParseError.errorString(); if (jsonParseError.error != QJsonParseError::NoError) { _item->_errorString = tr("Invalid JSON reply from the poll URL"); _item->_status = SyncFileItem::NormalError; @@ -138,16 +138,23 @@ bool PollJob::finished() return true; } - if (status["unfinished"].toBool()) { - start(); + auto status = json["status"].toString(); + if (status == QLatin1String("init") || status == QLatin1String("started")) { + QTimer::singleShot(5 * 1000, this, &PollJob::start); return false; } - _item->_errorString = status["error"].toString(); - _item->_status = _item->_errorString.isEmpty() ? SyncFileItem::Success : SyncFileItem::NormalError; - _item->_fileId = status["fileid"].toString().toUtf8(); - _item->_etag = status["etag"].toString().toUtf8(); _item->_responseTimeStamp = responseTimestamp(); + _item->_httpErrorCode = json["errorCode"].toInt(); + + if (status == QLatin1String("finished")) { + _item->_status = SyncFileItem::Success; + _item->_fileId = json["fileId"].toString().toUtf8(); + _item->_etag = parseEtag(json["ETag"].toString().toUtf8()); + } else { // error + _item->_status = classifyError(QNetworkReply::UnknownContentError, _item->_httpErrorCode); + _item->_errorString = json["errorMessage"].toString(); + } SyncJournalDb::PollInfo info; info._file = _item->_file; @@ -573,9 +580,10 @@ void PropagateUploadFileCommon::abortWithError(SyncFileItem::Status status, cons QMap<QByteArray, QByteArray> PropagateUploadFileCommon::headers() { QMap<QByteArray, QByteArray> headers; - headers[QByteArrayLiteral("OC-Async")] = QByteArrayLiteral("1"); headers[QByteArrayLiteral("Content-Type")] = QByteArrayLiteral("application/octet-stream"); headers[QByteArrayLiteral("X-OC-Mtime")] = QByteArray::number(qint64(_item->_modtime)); + if (qEnvironmentVariableIntValue("OWNCLOUD_LAZYOPS")) + headers[QByteArrayLiteral("OC-LazyOps")] = QByteArrayLiteral("true"); if (_item->_file.contains(QLatin1String(".sys.admin#recall#"))) { // This is a file recall triggered by the admin. Note: the diff --git a/src/libsync/propagateupload.h b/src/libsync/propagateupload.h index 022d10e2e..cfcb6b0fb 100644 --- a/src/libsync/propagateupload.h +++ b/src/libsync/propagateupload.h @@ -151,8 +151,8 @@ signals: /** * @brief This job implements the asynchronous PUT * - * If the server replies to a PUT with a OC-Finish-Poll url, we will query this url until the server - * replies with an etag. https://github.com/owncloud/core/issues/12097 + * If the server replies to a PUT with a OC-JobStatus-Location path, we will query this url until the server + * replies with an etag. * @ingroup libsync */ class PollJob : public AbstractNetworkJob @@ -293,7 +293,7 @@ protected: */ static void adjustLastJobTimeout(AbstractNetworkJob *job, qint64 fileSize); - // Bases headers that need to be sent with every chunk + /** Bases headers that need to be sent on the PUT, or in the MOVE for chunking-ng */ QMap<QByteArray, QByteArray> headers(); }; diff --git a/src/libsync/propagateuploadng.cpp b/src/libsync/propagateuploadng.cpp index 98da05348..c8e3adb0a 100644 --- a/src/libsync/propagateuploadng.cpp +++ b/src/libsync/propagateuploadng.cpp @@ -743,6 +743,18 @@ void PropagateUploadFileNG::slotMoveJobFinished() commonErrorHandling(job); return; } + + if (_item->_httpErrorCode == 202) { + QString path = QString::fromUtf8(job->reply()->rawHeader("OC-JobStatus-Location")); + if (path.isEmpty()) { + done(SyncFileItem::NormalError, tr("Poll URL missing")); + return; + } + _finished = true; + startPollJob(path); + return; + } + if (_item->_httpErrorCode != 201 && _item->_httpErrorCode != 204) { abortWithError(SyncFileItem::NormalError, tr("Unexpected return code from server (%1)").arg(_item->_httpErrorCode)); return; diff --git a/src/libsync/propagateuploadv1.cpp b/src/libsync/propagateuploadv1.cpp index 05aa7a442..9f0d2e580 100644 --- a/src/libsync/propagateuploadv1.cpp +++ b/src/libsync/propagateuploadv1.cpp @@ -212,7 +212,7 @@ void PropagateUploadFileV1::slotPutFinished() // The server needs some time to process the request and provide us with a poll URL if (_item->_httpErrorCode == 202) { - QString path = QString::fromUtf8(job->reply()->rawHeader("OC-Finish-Poll")); + QString path = QString::fromUtf8(job->reply()->rawHeader("OC-JobStatus-Location")); if (path.isEmpty()) { done(SyncFileItem::NormalError, tr("Poll URL missing")); return; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 3c192bd6b..89c2b2712 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -50,6 +50,7 @@ owncloud_add_test(SyncFileStatusTracker "syncenginetestutils.h") owncloud_add_test(Download "syncenginetestutils.h") owncloud_add_test(ChunkingNg "syncenginetestutils.h") owncloud_add_test(Zsync "syncenginetestutils.h") +owncloud_add_test(AsyncOp "syncenginetestutils.h") owncloud_add_test(UploadReset "syncenginetestutils.h") owncloud_add_test(AllFilesDeleted "syncenginetestutils.h") owncloud_add_test(Blacklist "syncenginetestutils.h") diff --git a/test/syncenginetestutils.h b/test/syncenginetestutils.h index 00f75134e..a15c01bab 100644 --- a/test/syncenginetestutils.h +++ b/test/syncenginetestutils.h @@ -45,7 +45,7 @@ inline QString getFilePathFromUrl(const QUrl &url) { inline QString generateEtag() { - return QString::number(QDateTime::currentDateTimeUtc().toMSecsSinceEpoch(), 16); + return QString::number(QDateTime::currentDateTimeUtc().toMSecsSinceEpoch(), 16) + QByteArray::number(qrand(), 16); } inline QByteArray generateFileId() { return QByteArray::number(qrand(), 16); @@ -266,7 +266,7 @@ public: auto file = it->find(std::move(pathComponents).subComponents(), invalidateEtags); if (file && invalidateEtags) { // Update parents on the way back - etag = file->etag; + etag = generateEtag(); } return file; } @@ -336,7 +336,6 @@ public: QMap<QString, FileInfo> children; QString parentPath; -private: FileInfo *findInvalidatingEtags(PathComponents pathComponents) { return find(std::move(pathComponents), true); } @@ -459,24 +458,25 @@ public: setUrl(request.url()); setOperation(op); open(QIODevice::ReadOnly); + fileInfo = perform(remoteRootFileInfo, request, putPayload); + QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection); + } + static FileInfo *perform(FileInfo &remoteRootFileInfo, const QNetworkRequest &request, const QByteArray &putPayload) + { QString fileName = getFilePathFromUrl(request.url()); Q_ASSERT(!fileName.isEmpty()); - if ((fileInfo = remoteRootFileInfo.find(fileName))) { + FileInfo *fileInfo = remoteRootFileInfo.find(fileName); + if (fileInfo) { fileInfo->size = putPayload.size(); fileInfo->contentChar = putPayload.at(0); } else { // Assume that the file is filled with the same character fileInfo = remoteRootFileInfo.create(fileName, putPayload.size(), putPayload.at(0)); } - - if (!fileInfo) { - abort(); - return; - } fileInfo->lastModified = OCC::Utility::qDateTimeFromTime_t(request.rawHeader("X-OC-Mtime").toLongLong()); remoteRootFileInfo.find(fileName, /*invalidate_etags=*/true); - QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection); + return fileInfo; } Q_INVOKABLE virtual void respond() @@ -744,7 +744,16 @@ public: setUrl(request.url()); setOperation(op); open(QIODevice::ReadOnly); + fileInfo = perform(uploadsFileInfo, remoteRootFileInfo, request); + if (!fileInfo) { + QTimer::singleShot(0, this, &FakeChunkMoveReply::respondPreconditionFailed); + } else { + QTimer::singleShot(0, this, &FakeChunkMoveReply::respond); + } + } + static FileInfo *perform(FileInfo &uploadsFileInfo, FileInfo &remoteRootFileInfo, const QNetworkRequest &request) + { QString source = getFilePathFromUrl(request.url()); bool zsync = false; Q_ASSERT(!source.isEmpty()); @@ -783,7 +792,7 @@ public: ++count; prev = chunkName.toLongLong() + x.size; } - QCOMPARE(sourceFolder->children.count(), count); // There should not be holes or extra files + Q_ASSERT(sourceFolder->children.count() == count); // There should not be holes or extra files // For zsync, get the size from the header, and allow no-chunk uploads (shrinking files) if (zsync) { @@ -795,13 +804,12 @@ public: } // NOTE: This does not actually assemble the file data from the chunks! - - if ((fileInfo = remoteRootFileInfo.find(fileName))) { - QVERIFY(request.hasRawHeader("If")); // The client should put this header + FileInfo *fileInfo = remoteRootFileInfo.find(fileName); + if (fileInfo) { + Q_ASSERT(request.hasRawHeader("If")); // The client should put this header if (request.rawHeader("If") != QByteArray("<" + request.rawHeader("Destination") + "> ([\"" + fileInfo->etag.toLatin1() + "\"])")) { - QMetaObject::invokeMethod(this, "respondPreconditionFailed", Qt::QueuedConnection); - return; + return nullptr; } fileInfo->size = size; fileInfo->contentChar = payload; @@ -810,15 +818,10 @@ public: // Assume that the file is filled with the same character fileInfo = remoteRootFileInfo.create(fileName, size, payload); } - - if (!fileInfo) { - abort(); - return; - } fileInfo->lastModified = OCC::Utility::qDateTimeFromTime_t(request.rawHeader("X-OC-Mtime").toLongLong()); remoteRootFileInfo.find(fileName, /*invalidate_etags=*/true); - QTimer::singleShot(0, this, &FakeChunkMoveReply::respond); + return fileInfo; } Q_INVOKABLE virtual void respond() @@ -940,6 +943,48 @@ public: qint64 readData(char *, qint64) override { return 0; } }; +class FakePayloadReply : public QNetworkReply +{ + Q_OBJECT +public: + FakePayloadReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request, + const QByteArray &body, QObject *parent) + : QNetworkReply{ parent } + , _body(body) + { + setRequest(request); + setUrl(request.url()); + setOperation(op); + open(QIODevice::ReadOnly); + QTimer::singleShot(10, this, &FakePayloadReply::respond); + } + + void respond() + { + setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200); + setHeader(QNetworkRequest::ContentLengthHeader, _body.size()); + emit metaDataChanged(); + emit readyRead(); + setFinished(true); + emit finished(); + } + + void abort() override {} + qint64 readData(char *buf, qint64 max) override + { + max = qMin<qint64>(max, _body.size()); + memcpy(buf, _body.constData(), max); + _body = _body.mid(max); + return max; + } + qint64 bytesAvailable() const override + { + return _body.size(); + } + QByteArray _body; +}; + + class FakeErrorReply : public QNetworkReply { Q_OBJECT diff --git a/test/testasyncop.cpp b/test/testasyncop.cpp new file mode 100644 index 000000000..930fd382f --- /dev/null +++ b/test/testasyncop.cpp @@ -0,0 +1,236 @@ +/* + * This software is in the public domain, furnished "as is", without technical + * support, and with no warranty, express or implied, as to its usefulness for + * any purpose. + * + */ + +#include <QtTest> +#include "syncenginetestutils.h" +#include <syncengine.h> + +using namespace OCC; + +class FakeAsyncReply : public QNetworkReply +{ + Q_OBJECT + QByteArray _pollLocation; + +public: + FakeAsyncReply(const QByteArray &pollLocation, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent) + : QNetworkReply{ parent } + , _pollLocation(pollLocation) + { + setRequest(request); + setUrl(request.url()); + setOperation(op); + open(QIODevice::ReadOnly); + + QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection); + } + + Q_INVOKABLE void respond() + { + setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 202); + setRawHeader("OC-JobStatus-Location", _pollLocation); + emit metaDataChanged(); + emit finished(); + } + + void abort() override {} + qint64 readData(char *, qint64) override { return 0; } +}; + + +class TestAsyncOp : public QObject +{ + Q_OBJECT + +private slots: + + void asyncUploadOperations() + { + FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() }; + fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ { "chunking", "1.0" } } } }); + // Reduce max chunk size a bit so we get more chunks + SyncOptions options; + options._maxChunkSize = 20 * 1000; + fakeFolder.syncEngine().setSyncOptions(options); + int nGET = 0; + + // This test is made of several testcases. + // the testCases maps a filename to a couple of callback. + // When a file is uploaded, the fake server will always return the 202 code, and will set + // the `perform` functor to what needs to be done to complete the transaction. + // The testcase consist of the `pollRequest` which will be called when the sync engine + // calls the poll url. + struct TestCase + { + using PollRequest_t = std::function<QNetworkReply *(TestCase *, const QNetworkRequest &request)>; + PollRequest_t pollRequest; + std::function<FileInfo *()> perform = nullptr; + }; + QHash<QString, TestCase> testCases; + + fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *outgoingData) -> QNetworkReply * { + auto path = request.url().path(); + + if (op == QNetworkAccessManager::GetOperation && path.startsWith("/async-poll/")) { + auto file = path.mid(sizeof("/async-poll/") - 1); + Q_ASSERT(testCases.contains(file)); + auto &testCase = testCases[file]; + return testCase.pollRequest(&testCase, request); + } + + if (op == QNetworkAccessManager::PutOperation && !path.contains("/uploads/")) { + // Not chunking + auto file = getFilePathFromUrl(request.url()); + Q_ASSERT(testCases.contains(file)); + auto &testCase = testCases[file]; + Q_ASSERT(!testCase.perform); + auto putPayload = outgoingData->readAll(); + testCase.perform = [putPayload, request, &fakeFolder] { + return FakePutReply::perform(fakeFolder.remoteModifier(), request, putPayload); + }; + return new FakeAsyncReply("/async-poll/" + file.toUtf8(), op, request, &fakeFolder.syncEngine()); + } else if (request.attribute(QNetworkRequest::CustomVerbAttribute) == "MOVE") { + QString file = getFilePathFromUrl(QUrl::fromEncoded(request.rawHeader("Destination"))); + Q_ASSERT(testCases.contains(file)); + auto &testCase = testCases[file]; + Q_ASSERT(!testCase.perform); + testCase.perform = [request, &fakeFolder] { + return FakeChunkMoveReply::perform(fakeFolder.uploadState(), fakeFolder.remoteModifier(), request); + }; + return new FakeAsyncReply("/async-poll/" + file.toUtf8(), op, request, &fakeFolder.syncEngine()); + } else if (op == QNetworkAccessManager::GetOperation) { + nGET++; + } + return nullptr; + }); + + + // Callback to be used to finalize the transaction and return the success + auto successCallback = [](TestCase *tc, const QNetworkRequest &request) { + tc->pollRequest = [](auto...) -> QNetworkReply * { std::abort(); }; // shall no longer be called + FileInfo *info = tc->perform(); + QByteArray body = "{ \"status\":\"finished\", \"ETag\":\"\\\"" + info->etag.toUtf8() + "\\\"\", \"fileId\":\"" + info->fileId + "\"}\n"; + return new FakePayloadReply(QNetworkAccessManager::GetOperation, request, body, nullptr); + }; + // Callback that never finishes + auto waitForeverCallback = [](TestCase *, const QNetworkRequest &request) { + QByteArray body = "{\"status\":\"started\"}\n"; + return new FakePayloadReply(QNetworkAccessManager::GetOperation, request, body, nullptr); + }; + // Callback that simulate an error. + auto errorCallback = [](TestCase *tc, const QNetworkRequest &request) { + tc->pollRequest = [](auto...) -> QNetworkReply * { std::abort(); }; // shall no longer be called; + QByteArray body = "{\"status\":\"error\",\"errorCode\":500,\"errorMessage\":\"TestingErrors\"}\n"; + return new FakePayloadReply(QNetworkAccessManager::GetOperation, request, body, nullptr); + }; + // This lambda takes another functor as a parameter, and returns a callback that will + // tell the client needs to poll again, and further call to the poll url will call the + // given callback + auto waitAndChain = [](const TestCase::PollRequest_t &chain) { + return [chain](TestCase *tc, const QNetworkRequest &request) { + tc->pollRequest = chain; + QByteArray body = "{\"status\":\"started\"}\n"; + return new FakePayloadReply(QNetworkAccessManager::GetOperation, request, body, nullptr); + }; + }; + + // Create a testcase by creating a file of a given size locally and assigning it a callback + auto insertFile = [&](const QString &file, int size, TestCase::PollRequest_t cb) { + fakeFolder.localModifier().insert(file, size); + testCases[file] = { std::move(cb) }; + }; + fakeFolder.localModifier().mkdir("success"); + insertFile("success/chunked_success", options._maxChunkSize * 3, successCallback); + insertFile("success/single_success", 300, successCallback); + insertFile("success/chunked_patience", options._maxChunkSize * 3, + waitAndChain(waitAndChain(successCallback))); + insertFile("success/single_patience", 300, + waitAndChain(waitAndChain(successCallback))); + fakeFolder.localModifier().mkdir("err"); + insertFile("err/chunked_error", options._maxChunkSize * 3, errorCallback); + insertFile("err/single_error", 300, errorCallback); + insertFile("err/chunked_error2", options._maxChunkSize * 3, waitAndChain(errorCallback)); + insertFile("err/single_error2", 300, waitAndChain(errorCallback)); + + // First sync should finish by itself. + // All the things in "success/" should be transfered, the things in "err/" not + QVERIFY(!fakeFolder.syncOnce()); + QCOMPARE(nGET, 0); + QCOMPARE(*fakeFolder.currentLocalState().find("success"), + *fakeFolder.currentRemoteState().find("success")); + testCases.clear(); + testCases["err/chunked_error"] = { successCallback }; + testCases["err/chunked_error2"] = { successCallback }; + testCases["err/single_error"] = { successCallback }; + testCases["err/single_error2"] = { successCallback }; + + fakeFolder.localModifier().mkdir("waiting"); + insertFile("waiting/small", 300, waitForeverCallback); + insertFile("waiting/willNotConflict", 300, waitForeverCallback); + insertFile("waiting/big", options._maxChunkSize * 3, + waitAndChain(waitAndChain([&](TestCase *tc, const QNetworkRequest &request) { + QTimer::singleShot(0, &fakeFolder.syncEngine(), &SyncEngine::abort); + return waitAndChain(waitForeverCallback)(tc, request); + }))); + + fakeFolder.syncJournal().wipeErrorBlacklist(); + + // This second sync will redo the files that had errors + // But the waiting folder will not complete before it is aborted. + QVERIFY(!fakeFolder.syncOnce()); + QCOMPARE(nGET, 0); + QCOMPARE(*fakeFolder.currentLocalState().find("err"), + *fakeFolder.currentRemoteState().find("err")); + + testCases["waiting/small"].pollRequest = waitAndChain(waitAndChain(successCallback)); + testCases["waiting/big"].pollRequest = waitAndChain(successCallback); + testCases["waiting/willNotConflict"].pollRequest = + [&fakeFolder, &successCallback](TestCase *tc, const QNetworkRequest &request) { + auto &remoteModifier = fakeFolder.remoteModifier(); // successCallback destroys the capture + auto reply = successCallback(tc, request); + // This is going to succeed, and after we just change the file. + // This should not be a conflict, but this should be downloaded in the + // next sync + remoteModifier.appendByte("waiting/willNotConflict"); + return reply; + }; + + + int nPUT = 0; + int nMOVE = 0; + int nDELETE = 0; + fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * { + auto path = request.url().path(); + if (op == QNetworkAccessManager::GetOperation && path.startsWith("/async-poll/")) { + auto file = path.mid(sizeof("/async-poll/") - 1); + Q_ASSERT(testCases.contains(file)); + auto &testCase = testCases[file]; + return testCase.pollRequest(&testCase, request); + } else if (op == QNetworkAccessManager::PutOperation) { + nPUT++; + } else if (op == QNetworkAccessManager::GetOperation) { + nGET++; + } else if (op == QNetworkAccessManager::DeleteOperation) { + nDELETE++; + } else if (request.attribute(QNetworkRequest::CustomVerbAttribute) == "MOVE") { + nMOVE++; + } + return nullptr; + }); + + // This last sync will do the waiting stuff + QVERIFY(fakeFolder.syncOnce()); + QCOMPARE(nGET, 1); // "waiting/willNotConflict" + QCOMPARE(nPUT, 0); + QCOMPARE(nMOVE, 0); + QCOMPARE(nDELETE, 0); + QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState()); + } +}; + +QTEST_GUILESS_MAIN(TestAsyncOp) +#include "testasyncop.moc" |