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:
Juha Vuolle 2023-07-06 11:59:04 +03:00
parent e560adef21
commit e9f703ed3b
5 changed files with 181 additions and 32 deletions

View File

@ -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;

View File

@ -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;

View File

@ -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 = {};

View File

@ -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:

View File

@ -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"