Add signals and methods for QRestReply download progress
These include: - readyRead(), signal for indicating new data availability - bytesAvailable(), function for checking available data amount - downloadProgress(), signal for monitoring download progress Task-number: QTBUG-114717 Change-Id: Id6c49530d7857f5c76bd111eba84525137294ea7 Reviewed-by: Marc Mutz <marc.mutz@qt.io> Reviewed-by: Mårten Nordheim <marten.nordheim@qt.io>
This commit is contained in:
parent
e560adef21
commit
e9f703ed3b
@ -33,6 +33,33 @@ Q_DECLARE_LOGGING_CATEGORY(lcQrest)
|
||||
\sa QRestAccessManager, QNetworkReply
|
||||
*/
|
||||
|
||||
/*!
|
||||
\fn void QRestReply::readyRead(QRestReply *reply)
|
||||
|
||||
This signal is emitted when \a reply has received new data.
|
||||
|
||||
\sa body(), bytesAvailable(), isFinished()
|
||||
*/
|
||||
|
||||
/*!
|
||||
\fn void QRestReply::downloadProgress(qint64 bytesReceived,
|
||||
qint64 bytesTotal,
|
||||
QRestReply* reply)
|
||||
|
||||
This signal is emitted to indicate the progress of the download part of
|
||||
this network \a reply.
|
||||
|
||||
The \a bytesReceived parameter indicates the number of bytes received,
|
||||
while \a bytesTotal indicates the total number of bytes expected to be
|
||||
downloaded. If the number of bytes to be downloaded is not known, for
|
||||
instance due to a missing \c Content-Length header, \a bytesTotal
|
||||
will be -1.
|
||||
|
||||
See \l QNetworkReply::downloadProgress() documentation for more details.
|
||||
|
||||
\sa bytesAvailable(), readyRead()
|
||||
*/
|
||||
|
||||
/*!
|
||||
\fn void QRestReply::finished(QRestReply *reply)
|
||||
|
||||
@ -63,6 +90,14 @@ QRestReply::QRestReply(QNetworkReply *reply, QObject *parent)
|
||||
d->networkReply = reply;
|
||||
// Reparent so that destruction of QRestReply destroys QNetworkReply
|
||||
reply->setParent(this);
|
||||
|
||||
QObject::connect(reply, &QNetworkReply::readyRead, this, [this] {
|
||||
emit readyRead(this);
|
||||
});
|
||||
QObject::connect(reply, &QNetworkReply::downloadProgress, this,
|
||||
[this](qint64 bytesReceived, qint64 bytesTotal) {
|
||||
emit downloadProgress(bytesReceived, bytesTotal, this);
|
||||
});
|
||||
}
|
||||
|
||||
/*!
|
||||
@ -155,7 +190,7 @@ std::optional<QJsonArray> QRestReply::jsonArray()
|
||||
calls to get response data will return empty until further data has been
|
||||
received.
|
||||
|
||||
\sa json(), text()
|
||||
\sa json(), text(), bytesAvailable(), readyRead()
|
||||
*/
|
||||
QByteArray QRestReply::body()
|
||||
{
|
||||
@ -294,6 +329,17 @@ bool QRestReply::isFinished() const
|
||||
return d->networkReply->isFinished();
|
||||
}
|
||||
|
||||
/*!
|
||||
Returns the number of bytes available.
|
||||
|
||||
\sa body
|
||||
*/
|
||||
qint64 QRestReply::bytesAvailable() const
|
||||
{
|
||||
Q_D(const QRestReply);
|
||||
return d->networkReply->bytesAvailable();
|
||||
}
|
||||
|
||||
QRestReplyPrivate::QRestReplyPrivate()
|
||||
= default;
|
||||
|
||||
|
@ -35,6 +35,7 @@ public:
|
||||
QString errorString() const;
|
||||
|
||||
bool isFinished() const;
|
||||
qint64 bytesAvailable() const;
|
||||
|
||||
public Q_SLOTS:
|
||||
void abort();
|
||||
@ -42,6 +43,8 @@ public Q_SLOTS:
|
||||
Q_SIGNALS:
|
||||
void finished(QRestReply *reply);
|
||||
void errorOccurred(QRestReply *reply);
|
||||
void readyRead(QRestReply *reply);
|
||||
void downloadProgress(qint64 bytesReceived, qint64 bytesTotal, QRestReply *reply);
|
||||
|
||||
private:
|
||||
friend class QRestAccessManagerPrivate;
|
||||
|
@ -73,31 +73,52 @@ void HttpTestServer::handleDataAvailable()
|
||||
m_request.url.setPort(parts.at(1).toUInt());
|
||||
}
|
||||
HttpData response;
|
||||
ResponseControl control;
|
||||
// Inform the testcase about request and ask for response data
|
||||
m_handler(m_request, response);
|
||||
m_handler(m_request, response, control);
|
||||
|
||||
if (response.respond) {
|
||||
QByteArray responseMessage;
|
||||
responseMessage += "HTTP/1.1 ";
|
||||
responseMessage += QByteArray::number(response.status);
|
||||
QByteArray responseMessage;
|
||||
responseMessage += "HTTP/1.1 ";
|
||||
responseMessage += QByteArray::number(response.status);
|
||||
responseMessage += CRLF;
|
||||
// Insert headers if any
|
||||
for (const auto &[name,value] : response.headers.asKeyValueRange()) {
|
||||
responseMessage += name;
|
||||
responseMessage += value;
|
||||
responseMessage += CRLF;
|
||||
// Insert headers if any
|
||||
for (const auto &[name,value] : response.headers.asKeyValueRange()) {
|
||||
responseMessage += name;
|
||||
responseMessage += value;
|
||||
responseMessage += CRLF;
|
||||
}
|
||||
responseMessage += CRLF;
|
||||
/*
|
||||
qDebug() << "HTTPTestServer received request"
|
||||
<< "\nMethod:" << m_request.method
|
||||
<< "\nHeaders:" << m_request.headers
|
||||
<< "\nBody:" << m_request.body;
|
||||
*/
|
||||
if (control.respond) {
|
||||
if (control.responseChunkSize <= 0) {
|
||||
responseMessage += response.body;
|
||||
// qDebug() << "HTTPTestServer response:" << responseMessage;
|
||||
m_socket->write(responseMessage);
|
||||
} else {
|
||||
// Respond in chunks, first write the headers
|
||||
// qDebug() << "HTTPTestServer response:" << responseMessage;
|
||||
m_socket->write(responseMessage);
|
||||
// Then write bodydata in chunks, while allowing the testcase to process as well
|
||||
QByteArray chunk;
|
||||
while (!response.body.isEmpty()) {
|
||||
chunk = response.body.left(control.responseChunkSize);
|
||||
response.body.remove(0, control.responseChunkSize);
|
||||
// qDebug() << "SERVER writing chunk" << chunk;
|
||||
m_socket->write(chunk);
|
||||
m_socket->flush();
|
||||
m_socket->waitForBytesWritten();
|
||||
// Process events until testcase indicates it's ready for next chunk.
|
||||
// This way we can control the bytes the testcase gets in each chunk
|
||||
control.readyForNextChunk = false;
|
||||
while (!control.readyForNextChunk)
|
||||
QCoreApplication::processEvents();
|
||||
}
|
||||
}
|
||||
responseMessage += CRLF;
|
||||
responseMessage += response.body;
|
||||
|
||||
/*
|
||||
qDebug() << "HTTPTestServer received request"
|
||||
<< "\nMethod:" << m_request.method
|
||||
<< "\nHeaders:" << m_request.headers
|
||||
<< "\nBody:" << m_request.body;
|
||||
qDebug() << "HTTPTestServer sends response:" << responseMessage;
|
||||
*/
|
||||
m_socket->write(responseMessage);
|
||||
}
|
||||
m_socket->disconnectFromHost();
|
||||
m_request = {};
|
||||
|
@ -14,7 +14,6 @@
|
||||
struct HttpData {
|
||||
QUrl url;
|
||||
int status = 0;
|
||||
bool respond = true;
|
||||
QByteArray body;
|
||||
QByteArray method;
|
||||
quint16 port = 0;
|
||||
@ -22,6 +21,13 @@ struct HttpData {
|
||||
QMap<QByteArray, QByteArray> headers;
|
||||
};
|
||||
|
||||
struct ResponseControl
|
||||
{
|
||||
bool respond = true;
|
||||
qsizetype responseChunkSize = -1;
|
||||
bool readyForNextChunk = true;
|
||||
};
|
||||
|
||||
// Simple HTTP server. Currently supports only one concurrent connection
|
||||
class HttpTestServer : public QTcpServer
|
||||
{
|
||||
@ -62,7 +68,8 @@ public:
|
||||
QByteArray fragment;
|
||||
|
||||
// Settable callback for testcase. Gives the received request data, and takes in response data
|
||||
using Handler = std::function<void(const HttpData &request, HttpData &response)>;
|
||||
using Handler = std::function<void(const HttpData &request, HttpData &response,
|
||||
ResponseControl &control)>;
|
||||
void setHandler(const Handler &handler);
|
||||
|
||||
private slots:
|
||||
|
@ -6,6 +6,7 @@
|
||||
#include <QtNetwork/qhttpmultipart.h>
|
||||
#include <QtNetwork/qrestaccessmanager.h>
|
||||
#include <QtNetwork/qauthenticator.h>
|
||||
#include <QtNetwork/qnetworkreply.h>
|
||||
#include <QtNetwork/qrestreply.h>
|
||||
|
||||
#include <QTest>
|
||||
@ -37,6 +38,7 @@ private slots:
|
||||
void body();
|
||||
void json();
|
||||
void text();
|
||||
void download();
|
||||
|
||||
private:
|
||||
void memberHandler(QRestReply *reply);
|
||||
@ -86,7 +88,7 @@ void tst_QRestAccessManager::networkRequestReply()
|
||||
HttpData serverSideRequest; // The request data the server received
|
||||
HttpData serverSideResponse; // The response data the server responds with
|
||||
serverSideResponse.status = 200;
|
||||
server.setHandler([&](HttpData request, HttpData &response) {
|
||||
server.setHandler([&](HttpData request, HttpData &response, ResponseControl&) {
|
||||
serverSideRequest = request;
|
||||
response = serverSideResponse;
|
||||
|
||||
@ -227,8 +229,8 @@ void tst_QRestAccessManager::abort()
|
||||
QTRY_COMPARE(finishedSpy.size(), 1);
|
||||
|
||||
// Abort after request has been sent out
|
||||
server.setHandler([&](HttpData, HttpData &response) {
|
||||
response.respond = false;
|
||||
server.setHandler([&](HttpData, HttpData&, ResponseControl &control) {
|
||||
control.respond = false;
|
||||
manager.abortRequests();
|
||||
});
|
||||
manager.get(request, this, callback);
|
||||
@ -480,7 +482,7 @@ void tst_QRestAccessManager::authentication()
|
||||
QRestReply *replyFromServer = nullptr;
|
||||
|
||||
HttpData serverSideRequest;
|
||||
server.setHandler([&](HttpData request, HttpData &response) {
|
||||
server.setHandler([&](HttpData request, HttpData &response, ResponseControl&) {
|
||||
if (!request.headers.contains("Authorization"_ba)) {
|
||||
response.status = 401;
|
||||
response.headers.insert("WWW-Authenticate: "_ba, "Basic realm=\"secret_place\""_ba);
|
||||
@ -533,7 +535,7 @@ void tst_QRestAccessManager::errors()
|
||||
bool errorSignalReceived = false;
|
||||
|
||||
HttpData serverSideResponse; // The response data the server responds with
|
||||
server.setHandler([&](HttpData, HttpData &response) {
|
||||
server.setHandler([&](HttpData, HttpData &response, ResponseControl &) {
|
||||
response = serverSideResponse;
|
||||
});
|
||||
|
||||
@ -604,7 +606,7 @@ void tst_QRestAccessManager::body()
|
||||
|
||||
HttpData serverSideRequest; // The request data the server received
|
||||
HttpData serverSideResponse; // The response data the server responds with
|
||||
server.setHandler([&](HttpData request, HttpData &response) {
|
||||
server.setHandler([&](HttpData request, HttpData &response, ResponseControl&) {
|
||||
serverSideRequest = request;
|
||||
response = serverSideResponse;
|
||||
});
|
||||
@ -654,7 +656,7 @@ void tst_QRestAccessManager::json()
|
||||
HttpData serverSideRequest; // The request data the server received
|
||||
HttpData serverSideResponse; // The response data the server responds with
|
||||
serverSideResponse.status = 200;
|
||||
server.setHandler([&](HttpData request, HttpData &response) {
|
||||
server.setHandler([&](HttpData request, HttpData &response, ResponseControl&) {
|
||||
serverSideRequest = request;
|
||||
response = serverSideResponse;
|
||||
});
|
||||
@ -723,7 +725,7 @@ void tst_QRestAccessManager::text()
|
||||
HttpData serverSideRequest; // The request data the server received
|
||||
HttpData serverSideResponse; // The response data the server responds with
|
||||
serverSideResponse.status = 200;
|
||||
server.setHandler([&](HttpData request, HttpData &response) {
|
||||
server.setHandler([&](HttpData request, HttpData &response, ResponseControl&) {
|
||||
serverSideRequest = request;
|
||||
response = serverSideResponse;
|
||||
});
|
||||
@ -790,5 +792,75 @@ void tst_QRestAccessManager::text()
|
||||
replyFromServer = nullptr;
|
||||
}
|
||||
|
||||
void tst_QRestAccessManager::download()
|
||||
{
|
||||
// Test case where data is received in chunks.
|
||||
QRestAccessManager manager;
|
||||
manager.setDeletesRepliesOnFinished(false);
|
||||
HttpTestServer server;
|
||||
QTRY_VERIFY(server.isListening());
|
||||
QNetworkRequest request(server.url());
|
||||
HttpData serverSideResponse; // The response data the server responds with
|
||||
constexpr qsizetype dataSize = 1 * 1024 * 1024; // 1 MB
|
||||
QByteArray expectedData{dataSize, 0};
|
||||
for (qsizetype i = 0; i < dataSize; ++i) // initialize the data we download
|
||||
expectedData[i] = i % 100;
|
||||
QByteArray cumulativeReceivedData;
|
||||
qsizetype cumulativeReceivedBytesAvailable = 0;
|
||||
|
||||
serverSideResponse.body = expectedData;
|
||||
ResponseControl *responseControl = nullptr;
|
||||
serverSideResponse.status = 200;
|
||||
// Set content-length header so that underlying QNAM is able to report bytesTotal correctly
|
||||
serverSideResponse.headers.insert("Content-Length: ",
|
||||
QString::number(expectedData.size()).toLatin1());
|
||||
server.setHandler([&](HttpData, HttpData &response, ResponseControl &control) {
|
||||
response = serverSideResponse;
|
||||
responseControl = &control; // store for later
|
||||
control.responseChunkSize = 1024; // tell testserver to send data in chunks of this size
|
||||
});
|
||||
|
||||
QRestReply* reply = manager.get(request, this, [&responseControl](QRestReply */*reply*/){
|
||||
responseControl = nullptr; // all finished, no more need for controlling the response
|
||||
});
|
||||
|
||||
QObject::connect(reply, &QRestReply::readyRead, this, [&](QRestReply *reply) {
|
||||
static bool testOnce = true;
|
||||
if (!reply->isFinished() && testOnce) {
|
||||
// Test once that reading jsonObject or text of an unfinished reply will not work
|
||||
testOnce = false;
|
||||
QTest::ignoreMessage(QtWarningMsg, "Attempt to read json() of an unfinished"
|
||||
" reply, ignoring.");
|
||||
reply->json();
|
||||
QTest::ignoreMessage(QtWarningMsg, "Attempt to read text() of an unfinished reply,"
|
||||
" ignoring.");
|
||||
(void)reply->text();
|
||||
}
|
||||
|
||||
cumulativeReceivedBytesAvailable += reply->bytesAvailable();
|
||||
cumulativeReceivedData += reply->body();
|
||||
// Tell testserver that test is ready for next chunk
|
||||
responseControl->readyForNextChunk = true;
|
||||
});
|
||||
|
||||
qint64 totalBytes = 0;
|
||||
qint64 receivedBytes = 0;
|
||||
QObject::connect(reply, &QRestReply::downloadProgress, this,
|
||||
[&](qint64 bytesReceived, qint64 bytesTotal) {
|
||||
if (totalBytes == 0 && bytesTotal > 0)
|
||||
totalBytes = bytesTotal;
|
||||
receivedBytes = bytesReceived;
|
||||
});
|
||||
QTRY_VERIFY(reply->isFinished());
|
||||
reply->deleteLater();
|
||||
reply = nullptr;
|
||||
// Checks specific for readyRead() and bytesAvailable()
|
||||
QCOMPARE(cumulativeReceivedData, expectedData);
|
||||
QCOMPARE(cumulativeReceivedBytesAvailable, expectedData.size());
|
||||
// Checks specific for downloadProgress()
|
||||
QCOMPARE(totalBytes, expectedData.size());
|
||||
QCOMPARE(receivedBytes, expectedData.size());
|
||||
}
|
||||
|
||||
QTEST_MAIN(tst_QRestAccessManager)
|
||||
#include "tst_qrestaccessmanager.moc"
|
||||
|
Loading…
x
Reference in New Issue
Block a user