WIP: Add an updater
This commit is contained in:
parent
7a07077daf
commit
b39ab44d4c
|
@ -26,10 +26,11 @@ set(HEADER_FILES
|
|||
misc/conversion.h
|
||||
misc/compat.h
|
||||
models/checklistmodel.h
|
||||
tests/mockreply.h
|
||||
resources/qtconfigarguments.h
|
||||
resources/resources.h
|
||||
resources/importplugin.h)
|
||||
set(SRC_FILES misc/dialogutils.cpp misc/desktoputils.cpp models/checklistmodel.cpp resources/qtconfigarguments.cpp
|
||||
set(SRC_FILES misc/dialogutils.cpp misc/desktoputils.cpp models/checklistmodel.cpp tests/mockreply.cpp resources/qtconfigarguments.cpp
|
||||
resources/resources.cpp)
|
||||
set(RES_FILES resources/qtutilsicons.qrc)
|
||||
|
||||
|
@ -42,6 +43,7 @@ set(WIDGETS_HEADER_FILES
|
|||
settingsdialog/optionpage.h
|
||||
settingsdialog/settingsdialog.h
|
||||
settingsdialog/qtsettings.h
|
||||
setup/updater.h
|
||||
widgets/buttonoverlay.h
|
||||
widgets/clearcombobox.h
|
||||
widgets/clearlineedit.h
|
||||
|
@ -61,6 +63,7 @@ set(WIDGETS_SRC_FILES
|
|||
settingsdialog/optionpage.cpp
|
||||
settingsdialog/settingsdialog.cpp
|
||||
settingsdialog/qtsettings.cpp
|
||||
setup/updater.cpp
|
||||
widgets/buttonoverlay.cpp
|
||||
widgets/clearcombobox.cpp
|
||||
widgets/clearlineedit.cpp
|
||||
|
@ -176,6 +179,17 @@ else ()
|
|||
message(STATUS "D-Bus notifications disabled")
|
||||
endif ()
|
||||
|
||||
# configure whether setup tools are enabled; if not functions/classes under setup become no-ops
|
||||
option(SETUP_TOOLS "enables setup tools; makes likely sense to disable when distributing via a package" ON)
|
||||
if (SETUP_TOOLS)
|
||||
set_property(
|
||||
SOURCE setup/updater.cpp
|
||||
APPEND
|
||||
PROPERTY COMPILE_DEFINITIONS ${META_PROJECT_VARNAME}_SETUP_TOOLS_ENABLED)
|
||||
list(APPEND QT_TESTS setup)
|
||||
list(APPEND ADDITIONAL_QT_MODULES Network)
|
||||
endif ()
|
||||
|
||||
# include modules to apply configuration
|
||||
include(BasicConfig)
|
||||
include(QtGuiConfig)
|
||||
|
|
|
@ -0,0 +1,369 @@
|
|||
#include "./updater.h"
|
||||
|
||||
#include "../misc/compat.h"
|
||||
#include "../tests/mockreply.h"
|
||||
|
||||
#include "resources/config.h"
|
||||
|
||||
#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
|
||||
#include <c++utilities/application/argumentparser.h>
|
||||
|
||||
#include <QDebug>
|
||||
#include <QFile>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QRegularExpression>
|
||||
#include <QStringBuilder>
|
||||
#include <QVersionNumber>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
#endif
|
||||
|
||||
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
|
||||
#define QT_UTILITIES_VERSION_SUFFIX QString()
|
||||
#else
|
||||
#define QT_UTILITIES_VERSION_SUFFIX QStringLiteral("-qt5")
|
||||
#endif
|
||||
|
||||
namespace QtUtilities {
|
||||
|
||||
/*!
|
||||
* \brief
|
||||
*/
|
||||
void handleUpdate()
|
||||
{
|
||||
}
|
||||
|
||||
static QFile fileForRequest(const QString &dir, const QNetworkRequest &request)
|
||||
{
|
||||
auto url = request.url();
|
||||
auto path = url.path();
|
||||
auto query = url.query();
|
||||
for (const auto c : {QChar('/'), QChar('&')}){
|
||||
path.replace(c, QChar('-'));
|
||||
query.replace(c, QChar('-'));
|
||||
}
|
||||
return QFile(dir % QChar('/') % url.host() % QChar('-') % path % QChar('-') % query % QStringLiteral(".txt"));
|
||||
}
|
||||
|
||||
struct NetworkReplyData {
|
||||
explicit NetworkReplyData();
|
||||
~NetworkReplyData();
|
||||
|
||||
static NetworkReplyData current(QObject *sender);
|
||||
|
||||
QNetworkReply *reply;
|
||||
QByteArray response;
|
||||
};
|
||||
|
||||
NetworkReplyData::NetworkReplyData()
|
||||
: reply(nullptr)
|
||||
{
|
||||
}
|
||||
|
||||
NetworkReplyData::~NetworkReplyData()
|
||||
{
|
||||
if (reply) {
|
||||
reply->deleteLater();
|
||||
}
|
||||
}
|
||||
|
||||
NetworkReplyData NetworkReplyData::current(QObject *sender)
|
||||
{
|
||||
// assign reply and read the response
|
||||
auto d = NetworkReplyData();
|
||||
d.reply = qobject_cast<QNetworkReply *>(sender);
|
||||
if (!d.reply) {
|
||||
qDebug() << "Update check: sender is no QNetworkReply";
|
||||
return d;
|
||||
}
|
||||
if (d.reply->error() == QNetworkReply::NoError) {
|
||||
d.response = d.reply->readAll();
|
||||
}
|
||||
|
||||
// dump the response for debugging/testing if enabled
|
||||
static const auto dumpDir = qEnvironmentVariable(PROJECT_VARNAME_UPPER "_UPDATER_DUMP_RESPONSES");
|
||||
if (!dumpDir.isEmpty() && !qobject_cast<MockReply *>(sender)) {
|
||||
auto file = fileForRequest(dumpDir, d.reply->request());
|
||||
if (!file.open(QFile::WriteOnly) || !file.write(d.response)) {
|
||||
qDebug() << "Update check: Unable to dump response to '" << file.fileName() << "': " << file.errorString();
|
||||
}
|
||||
file.close();
|
||||
}
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
struct UpdateNotifierPrivate {
|
||||
QNetworkAccessManager *nm = nullptr;
|
||||
QVersionNumber currentVersion = QVersionNumber();
|
||||
QRegularExpression gitHubRegex = QRegularExpression(QStringLiteral(".*/github.com/([^/]+)/([^/]+)(/.*)?"));
|
||||
QRegularExpression assetRegex = QRegularExpression();
|
||||
QString newVersion;
|
||||
QString additionalInfo;
|
||||
QString error;
|
||||
QUrl downloadUrl;
|
||||
QUrl releasesUrl;
|
||||
bool inProgress = false;
|
||||
bool updateAvailable = false;
|
||||
bool verbose = false;
|
||||
};
|
||||
|
||||
UpdateNotifier::UpdateNotifier(QObject *parent)
|
||||
: QObject(parent)
|
||||
#ifdef QT_UTILITIES_SETUP_TOOLS_ENABLED
|
||||
, m_p(std::make_unique<UpdateNotifierPrivate>())
|
||||
#endif
|
||||
{
|
||||
#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
|
||||
return;
|
||||
#else
|
||||
m_p->verbose = qEnvironmentVariableIntValue(PROJECT_VARNAME_UPPER "_UPDATER_VERBOSE");
|
||||
|
||||
const auto &appInfo = CppUtilities::applicationInfo;
|
||||
const auto url = QString::fromUtf8(appInfo.url);
|
||||
const auto gitHubMatch = m_p->gitHubRegex.match(url);
|
||||
const auto gitHubOrga = gitHubMatch.captured(1);
|
||||
const auto gitHubRepo = gitHubMatch.captured(2);
|
||||
if (gitHubOrga.isNull() || gitHubRepo.isNull()) {
|
||||
return;
|
||||
}
|
||||
const auto expectedBinaryName = gitHubRepo % QT_UTILITIES_VERSION_SUFFIX;
|
||||
|
||||
m_p->releasesUrl = QStringLiteral("https://api.github.com/repos/") % gitHubOrga % QChar('/') % gitHubRepo % QStringLiteral("/releases?per_page=25");
|
||||
m_p->currentVersion = QVersionNumber::fromString(QUtf8StringView(appInfo.version));
|
||||
#if defined(Q_OS_WIN64)
|
||||
m_p->assetRegex = QRegularExpression(expectedBinaryName + QStringLiteral("-.*-x86_64-w64-mingw32\\.exe\\..+"));
|
||||
#elif defined(Q_OS_WIN32)
|
||||
m_p->assetRegex = QRegularExpression(expectedBinaryName + QStringLiteral("-.*-i686-w64-mingw32\\.exe\\..+"));
|
||||
#elif defined(__GNUC__) && defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID) && defined(__x86_64__)
|
||||
m_p->assetRegex = QRegularExpression(expectedBinaryName + QStringLiteral("-.*-x86_64-pc-linux-gnu\\..+"));
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
UpdateNotifier::~UpdateNotifier()
|
||||
{
|
||||
}
|
||||
|
||||
bool UpdateNotifier::isSupported() const
|
||||
{
|
||||
#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
|
||||
return false;
|
||||
#else
|
||||
return !m_p->assetRegex.pattern().isEmpty();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool UpdateNotifier::isInProgress() const
|
||||
{
|
||||
#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
|
||||
return false;
|
||||
#else
|
||||
return m_p->inProgress;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool UpdateNotifier::isUpdateAvailable() const
|
||||
{
|
||||
#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
|
||||
return false;
|
||||
#else
|
||||
return m_p->updateAvailable;
|
||||
#endif
|
||||
}
|
||||
|
||||
const QString &UpdateNotifier::newVersion() const
|
||||
{
|
||||
#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
|
||||
static const auto v = QString();
|
||||
return v;
|
||||
#else
|
||||
return m_p->newVersion;
|
||||
#endif
|
||||
}
|
||||
|
||||
const QString &UpdateNotifier::additionalInfo() const
|
||||
{
|
||||
#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
|
||||
static const auto v = QString();
|
||||
return v;
|
||||
#else
|
||||
return m_p->additionalInfo;
|
||||
#endif
|
||||
}
|
||||
|
||||
const QString &UpdateNotifier::error() const
|
||||
{
|
||||
#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
|
||||
static const auto v = QString();
|
||||
return v;
|
||||
#else
|
||||
return m_p->error;
|
||||
#endif
|
||||
}
|
||||
|
||||
const QUrl &UpdateNotifier::downloadUrl() const
|
||||
{
|
||||
#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
|
||||
static const auto v = QUrl();
|
||||
return v;
|
||||
#else
|
||||
return m_p->downloadUrl;
|
||||
#endif
|
||||
}
|
||||
|
||||
void UpdateNotifier::setNetworkAccessManager(QNetworkAccessManager *nm)
|
||||
{
|
||||
#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
|
||||
Q_UNUSED(nm)
|
||||
#else
|
||||
m_p->nm = nm;
|
||||
#endif
|
||||
}
|
||||
|
||||
void UpdateNotifier::setError(const QString &context, QNetworkReply *reply)
|
||||
{
|
||||
m_p->error = context + reply->errorString();
|
||||
emit inProgressChanged(m_p->inProgress = false);
|
||||
}
|
||||
|
||||
void UpdateNotifier::setError(const QString &context, const QJsonParseError &jsonError, const QByteArray &response, QNetworkReply *)
|
||||
{
|
||||
m_p->error = context % jsonError.errorString() % QChar(' ') % QChar('(') % tr("at offset %1").arg(jsonError.offset) % QChar(')');
|
||||
if (!response.isEmpty()) {
|
||||
m_p->error += QStringLiteral("\nResponse was: ");
|
||||
m_p->error += QString::fromUtf8(response);
|
||||
}
|
||||
emit inProgressChanged(m_p->inProgress = false);
|
||||
}
|
||||
|
||||
void UpdateNotifier::checkForUpdate()
|
||||
{
|
||||
#ifndef QT_UTILITIES_SETUP_TOOLS_ENABLED
|
||||
return;
|
||||
#else
|
||||
if (!m_p->nm || m_p->inProgress) {
|
||||
return;
|
||||
}
|
||||
emit inProgressChanged(m_p->inProgress = true);
|
||||
auto *const reply = makeRequest(QNetworkRequest(m_p->releasesUrl));
|
||||
connect(reply, &QNetworkReply::finished, this, &UpdateNotifier::readReleases);
|
||||
#endif
|
||||
}
|
||||
|
||||
void UpdateNotifier::readReleases()
|
||||
{
|
||||
const auto d = NetworkReplyData::current(sender());
|
||||
switch (d.reply->error()) {
|
||||
case QNetworkReply::NoError: {
|
||||
// parse JSON
|
||||
auto jsonError = QJsonParseError();
|
||||
const auto replyDoc = QJsonDocument::fromJson(d.response, &jsonError);
|
||||
if (jsonError.error != QJsonParseError::NoError) {
|
||||
setError(tr("Unable to parse releases: "), jsonError, d.response, d.reply);
|
||||
return;
|
||||
}
|
||||
#if !defined(QT_JSON_READONLY)
|
||||
if (m_p->verbose) {
|
||||
qDebug().noquote() << "Update check: found releases: " << QString::fromUtf8(replyDoc.toJson(QJsonDocument::Indented));
|
||||
}
|
||||
#endif
|
||||
const auto replyArray = replyDoc.array();
|
||||
for (const auto &releaseInfoVal : replyArray) {
|
||||
const auto releaseInfo = releaseInfoVal.toObject();
|
||||
const auto tag = releaseInfo.value(QLatin1String("tag_name")).toString();
|
||||
const auto version = QVersionNumber::fromString(tag.startsWith(QChar('v')) ? tag.mid(1) : tag);
|
||||
if (!version.isNull() && version > m_p->currentVersion) {
|
||||
const auto assets = releaseInfo.value(QLatin1String("assets"));
|
||||
m_p->newVersion = version.toString();
|
||||
if (assets.isArray()) {
|
||||
processAssets(assets.toArray());
|
||||
emit inProgressChanged(m_p->inProgress = false);
|
||||
} else {
|
||||
queryRelease(releaseInfo.value(QLatin1String("assets_url")).toString());
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (m_p->verbose) {
|
||||
qDebug() << "Update check: skipping release: " << tag;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case QNetworkReply::OperationCanceledError:
|
||||
emit inProgressChanged(m_p->inProgress = false);
|
||||
return;
|
||||
default:
|
||||
setError(tr("Unable to request releases: "), d.reply);
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateNotifier::queryRelease(const QUrl &releaseUrl)
|
||||
{
|
||||
auto *const reply = makeRequest(QNetworkRequest(releaseUrl));
|
||||
connect(reply, &QNetworkReply::finished, this, &UpdateNotifier::readRelease);
|
||||
}
|
||||
|
||||
void UpdateNotifier::readRelease()
|
||||
{
|
||||
const auto d = NetworkReplyData::current(sender());
|
||||
switch (d.reply->error()) {
|
||||
case QNetworkReply::NoError: {
|
||||
// parse JSON
|
||||
auto jsonError = QJsonParseError();
|
||||
const auto replyDoc = QJsonDocument::fromJson(d.response, &jsonError);
|
||||
if (jsonError.error != QJsonParseError::NoError) {
|
||||
setError(tr("Unable to parse release: "), jsonError, d.response, d.reply);
|
||||
return;
|
||||
}
|
||||
#if !defined(QT_JSON_READONLY)
|
||||
if (m_p->verbose) {
|
||||
qDebug().noquote() << "Update check: found release info: " << QString::fromUtf8(replyDoc.toJson(QJsonDocument::Indented));
|
||||
}
|
||||
#endif
|
||||
processAssets(replyDoc.object().value(QLatin1String("assets")).toArray());
|
||||
emit inProgressChanged(m_p->inProgress = false);
|
||||
break;
|
||||
}
|
||||
case QNetworkReply::OperationCanceledError:
|
||||
emit inProgressChanged(m_p->inProgress = false);
|
||||
return;
|
||||
default:
|
||||
setError(tr("Unable to request release: "), d.reply);
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateNotifier::processAssets(const QJsonArray &assets)
|
||||
{
|
||||
for (const auto &assetVal : assets) {
|
||||
const auto asset = assetVal.toObject();
|
||||
const auto assetName = asset.value(QLatin1String("name")).toString();
|
||||
if (!assetName.isEmpty() && m_p->assetRegex.match(assetName).hasMatch()) {
|
||||
m_p->downloadUrl = asset.value(QLatin1String("browser_download_url")).toString();
|
||||
m_p->updateAvailable = true;
|
||||
emit updateAvailable(m_p->newVersion, m_p->additionalInfo);
|
||||
return;
|
||||
}
|
||||
if (m_p->verbose) {
|
||||
qDebug() << "Update check: skipping asset: " << assetName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QNetworkReply *UpdateNotifier::makeRequest(const QNetworkRequest &request)
|
||||
{
|
||||
static const auto mockDir = qEnvironmentVariable(PROJECT_VARNAME_UPPER "_UPDATER_MOCK_RESPONSES");
|
||||
if (m_p->nm && mockDir.isEmpty()) {
|
||||
return m_p->nm->get(request);
|
||||
} else {
|
||||
// mock the response for debugging/testing if enabled
|
||||
auto file = fileForRequest(mockDir, request);
|
||||
qDebug() << "Update check: loading mock response from: " << file.fileName();
|
||||
return MockReply::forFile(request, std::move(file));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace QtUtilities
|
|
@ -0,0 +1,70 @@
|
|||
#ifndef QT_UTILITIES_SETUP_UPDATER_H
|
||||
#define QT_UTILITIES_SETUP_UPDATER_H
|
||||
|
||||
#include "../global.h"
|
||||
#include "../settingsdialog/optionpage.h" // TODO: UpdateOptionPage
|
||||
|
||||
#include <QObject>
|
||||
#include <QUrl>
|
||||
|
||||
#include <memory>
|
||||
|
||||
QT_FORWARD_DECLARE_CLASS(QJsonParseError)
|
||||
QT_FORWARD_DECLARE_CLASS(QJsonArray)
|
||||
QT_FORWARD_DECLARE_CLASS(QNetworkAccessManager)
|
||||
QT_FORWARD_DECLARE_CLASS(QNetworkReply)
|
||||
QT_FORWARD_DECLARE_CLASS(QNetworkRequest)
|
||||
|
||||
namespace QtUtilities {
|
||||
|
||||
QT_UTILITIES_EXPORT void handleUpdate();
|
||||
|
||||
struct UpdateNotifierPrivate;
|
||||
|
||||
class QT_UTILITIES_EXPORT UpdateNotifier : public QObject {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(bool supported READ isSupported)
|
||||
Q_PROPERTY(bool inProgress READ isInProgress NOTIFY inProgressChanged)
|
||||
Q_PROPERTY(bool updateAvailable READ isUpdateAvailable)
|
||||
Q_PROPERTY(QString newVersion READ newVersion)
|
||||
Q_PROPERTY(QString additionalInfo READ additionalInfo)
|
||||
Q_PROPERTY(QString error READ error)
|
||||
Q_PROPERTY(QUrl downloadUrl READ downloadUrl)
|
||||
|
||||
public:
|
||||
explicit UpdateNotifier(QObject *parent = nullptr);
|
||||
~UpdateNotifier() override;
|
||||
|
||||
bool isSupported() const;
|
||||
bool isInProgress() const;
|
||||
bool isUpdateAvailable() const;
|
||||
const QString &newVersion() const;
|
||||
const QString &additionalInfo() const;
|
||||
const QString &error() const;
|
||||
const QUrl &downloadUrl() const;
|
||||
void setNetworkAccessManager(QNetworkAccessManager *nm);
|
||||
|
||||
public Q_SLOTS:
|
||||
void checkForUpdate();
|
||||
|
||||
Q_SIGNALS:
|
||||
void inProgressChanged(bool inProgress);
|
||||
void updateAvailable(const QString &version, const QString &additionalInfo);
|
||||
|
||||
private Q_SLOTS:
|
||||
void setError(const QString &context, QNetworkReply *reply);
|
||||
void setError(const QString &context, const QJsonParseError &jsonError, const QByteArray &response, QNetworkReply *reply);
|
||||
void readReleases();
|
||||
void queryRelease(const QUrl &releaseUrl);
|
||||
void readRelease();
|
||||
void processAssets(const QJsonArray &assets);
|
||||
|
||||
private:
|
||||
QNetworkReply *makeRequest(const QNetworkRequest &request);
|
||||
|
||||
std::unique_ptr<UpdateNotifierPrivate> m_p;
|
||||
};
|
||||
|
||||
} // namespace QtUtilities
|
||||
|
||||
#endif // QT_UTILITIES_SETUP_UPDATER_H
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,101 @@
|
|||
#include "./mockreply.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QTimer>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace QtUtilities {
|
||||
|
||||
struct MockReplyPrivate {
|
||||
QByteArray bufferArray;
|
||||
std::string_view bufferView;
|
||||
const char *pos;
|
||||
qint64 bytesLeft;
|
||||
};
|
||||
|
||||
MockReply::MockReply(const QByteArray &buffer, int delay, QObject *parent)
|
||||
: QNetworkReply(parent)
|
||||
, m_d(std::make_unique<MockReplyPrivate>())
|
||||
{
|
||||
m_d->bufferArray = buffer;
|
||||
m_d->bufferView = std::string_view(m_d->bufferArray.data(), static_cast<std::size_t>(m_d->bufferArray.size()));
|
||||
m_d->pos = m_d->bufferView.data();
|
||||
m_d->bytesLeft = static_cast<qint64>(m_d->bufferView.size());
|
||||
|
||||
setOpenMode(QIODevice::ReadOnly);
|
||||
QTimer::singleShot(delay, this, &MockReply::emitFinished);
|
||||
}
|
||||
|
||||
MockReply::~MockReply()
|
||||
{
|
||||
}
|
||||
|
||||
void MockReply::abort()
|
||||
{
|
||||
close();
|
||||
}
|
||||
|
||||
void MockReply::close()
|
||||
{
|
||||
}
|
||||
|
||||
qint64 MockReply::bytesAvailable() const
|
||||
{
|
||||
return m_d->bytesLeft;
|
||||
}
|
||||
|
||||
bool MockReply::isSequential() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
qint64 MockReply::size() const
|
||||
{
|
||||
return static_cast<qint64>(m_d->bufferView.size());
|
||||
}
|
||||
|
||||
qint64 MockReply::readData(char *data, qint64 maxlen)
|
||||
{
|
||||
if (!m_d->bytesLeft) {
|
||||
return -1;
|
||||
}
|
||||
const auto bytesToRead = std::min<qint64>(m_d->bytesLeft, maxlen);
|
||||
if (!bytesToRead) {
|
||||
return 0;
|
||||
}
|
||||
std::copy(m_d->pos, m_d->pos + bytesToRead, data);
|
||||
m_d->pos += bytesToRead;
|
||||
m_d->bytesLeft -= bytesToRead;
|
||||
return bytesToRead;
|
||||
}
|
||||
|
||||
MockReply *MockReply::forFile(const QNetworkRequest &request, QFile &&file, int delay)
|
||||
{
|
||||
auto buffer = QByteArray();
|
||||
if (file.exists()) {
|
||||
file.open(QFile::ReadOnly);
|
||||
buffer = file.readAll();
|
||||
file.close();
|
||||
}
|
||||
auto *const reply = new MockReply(buffer, delay);
|
||||
reply->setRequest(request);
|
||||
return reply;
|
||||
}
|
||||
|
||||
void MockReply::emitFinished()
|
||||
{
|
||||
if (m_d->bufferView.empty()) {
|
||||
setError(QNetworkReply::InternalServerError, QStringLiteral("No mock reply available for this request."));
|
||||
setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 404);
|
||||
setAttribute(QNetworkRequest::HttpReasonPhraseAttribute, QLatin1String("Not found"));
|
||||
} else {
|
||||
setError(QNetworkReply::NoError, QString());
|
||||
setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200);
|
||||
setAttribute(QNetworkRequest::HttpReasonPhraseAttribute, QLatin1String("OK"));
|
||||
}
|
||||
setFinished(true);
|
||||
emit finished();
|
||||
}
|
||||
|
||||
} // namespace QtUtilities
|
|
@ -0,0 +1,49 @@
|
|||
#ifndef QT_UTILITIES_TESTS_MOCK_REPLY_H
|
||||
#define QT_UTILITIES_TESTS_MOCK_REPLY_H
|
||||
|
||||
#include "../global.h"
|
||||
|
||||
#include <QNetworkReply>
|
||||
|
||||
#include <memory>
|
||||
|
||||
QT_FORWARD_DECLARE_CLASS(QFile)
|
||||
|
||||
namespace QtUtilities {
|
||||
|
||||
struct MockReplyPrivate;
|
||||
|
||||
/*!
|
||||
* \brief The MockedReply class provides a fake QNetworkReply which will just return data from a specified buffer.
|
||||
*/
|
||||
class QT_UTILITIES_EXPORT MockReply : public QNetworkReply {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
~MockReply() override;
|
||||
|
||||
public Q_SLOTS:
|
||||
void abort() override;
|
||||
|
||||
public:
|
||||
void close() override;
|
||||
qint64 bytesAvailable() const override;
|
||||
bool isSequential() const override;
|
||||
qint64 size() const override;
|
||||
qint64 readData(char *data, qint64 maxlen) override;
|
||||
|
||||
static MockReply *forFile(const QNetworkRequest &request, QFile &&file, int delay = 0);
|
||||
|
||||
protected:
|
||||
QT_UTILITIES_EXPORT MockReply(const QByteArray &buffer, int delay, QObject *parent = nullptr);
|
||||
|
||||
private Q_SLOTS:
|
||||
void emitFinished();
|
||||
|
||||
private:
|
||||
std::unique_ptr<MockReplyPrivate> m_d;
|
||||
};
|
||||
|
||||
} // namespace QtUtilities
|
||||
|
||||
#endif // QT_UTILITIES_TESTS_HELPER_H
|
|
@ -0,0 +1,89 @@
|
|||
#include "../setup/updater.h"
|
||||
|
||||
#include "resources/config.h"
|
||||
|
||||
#include <c++utilities/application/argumentparser.h>
|
||||
#include <c++utilities/conversion/conversionexception.h>
|
||||
#include <c++utilities/conversion/stringconversion.h>
|
||||
|
||||
#include <QEventLoop>
|
||||
#include <QTimer>
|
||||
#include <QNetworkAccessManager>
|
||||
|
||||
#include <QtTest/QtTest>
|
||||
|
||||
using namespace QtUtilities;
|
||||
|
||||
class SetupTests : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
private Q_SLOTS:
|
||||
void initTestCase();
|
||||
void testUpdateNotifier();
|
||||
|
||||
private:
|
||||
double m_timeoutFactor = 1.0;
|
||||
};
|
||||
|
||||
void SetupTests::initTestCase()
|
||||
{
|
||||
CppUtilities::applicationInfo.url = "https://github.com/Martchus/syncthingtray";
|
||||
CppUtilities::applicationInfo.version = "1.4.0";
|
||||
|
||||
if (!qEnvironmentVariableIsSet(PROJECT_VARNAME_UPPER "_UPDATER_VERBOSE")) {
|
||||
qputenv(PROJECT_VARNAME_UPPER "_UPDATER_VERBOSE", "1");
|
||||
}
|
||||
|
||||
if (const auto timeoutFactorEnv = qgetenv(PROJECT_VARNAME_UPPER "_TEST_TIMEOUT_FACTOR"); !timeoutFactorEnv.isEmpty()) {
|
||||
try {
|
||||
m_timeoutFactor = CppUtilities::stringToNumber<double>(timeoutFactorEnv.data());
|
||||
qDebug() << "using timeout factor: " << m_timeoutFactor;
|
||||
} catch (const CppUtilities::ConversionException &) {
|
||||
qDebug() << "ignoring invalid " PROJECT_VARNAME_UPPER "_TEST_TIMEOUT_FACTOR";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SetupTests::testUpdateNotifier()
|
||||
{
|
||||
auto updateNotifier = UpdateNotifier();
|
||||
if (!updateNotifier.isSupported()) {
|
||||
qDebug() << "skipping: UpdateNotifier() is not supported";
|
||||
return;
|
||||
}
|
||||
|
||||
auto eventLoop = QEventLoop();
|
||||
auto timeout = QTimer();
|
||||
timeout.setInterval(static_cast<int>(5000 * m_timeoutFactor));
|
||||
timeout.setSingleShot(true);
|
||||
|
||||
auto networkAccessManager = QNetworkAccessManager();
|
||||
auto newVersionFromSignal = QString();
|
||||
updateNotifier.setNetworkAccessManager(&networkAccessManager);
|
||||
updateNotifier.checkForUpdate();
|
||||
|
||||
QObject::connect(&updateNotifier, &UpdateNotifier::inProgressChanged, &eventLoop, &QEventLoop::quit);
|
||||
QObject::connect(&updateNotifier, &UpdateNotifier::updateAvailable, &updateNotifier, [&newVersionFromSignal] (const QString &version, const QString &additionalInfo) {
|
||||
Q_UNUSED(additionalInfo)
|
||||
newVersionFromSignal = version;
|
||||
});
|
||||
QObject::connect(&timeout, &QTimer::timeout, &eventLoop, &QEventLoop::quit);
|
||||
|
||||
timeout.start();
|
||||
eventLoop.exec();
|
||||
timeout.stop();
|
||||
|
||||
QCOMPARE(updateNotifier.error(), QString());
|
||||
qDebug() << "download URL: " << updateNotifier.downloadUrl();
|
||||
QCOMPARE(updateNotifier.downloadUrl().host(), QStringLiteral("github.com"));
|
||||
QVERIFY2(updateNotifier.downloadUrl().path().startsWith(QLatin1String("/Martchus/syncthingtray/releases/download/")), "download URL contains expected path");
|
||||
QVERIFY2(updateNotifier.isUpdateAvailable(), "update considered available");
|
||||
QVERIFY2(!updateNotifier.isInProgress(), "not timed out");
|
||||
QVERIFY2(!updateNotifier.newVersion().isEmpty(), "new version assigned");
|
||||
QVERIFY2(!newVersionFromSignal.isEmpty(), "updateAvailable() was emitted with non-empty version");
|
||||
QCOMPARE(updateNotifier.newVersion(), newVersionFromSignal);
|
||||
QVERIFY2(!QVersionNumber::fromString(newVersionFromSignal).isNull(), "version is parsable");
|
||||
}
|
||||
|
||||
QTEST_MAIN(SetupTests)
|
||||
#include "setup.moc"
|
Loading…
Reference in New Issue