#ifndef QAPPIMAGE_UPDATE_TESTS_HPP_INCLUDED
#define QAPPIMAGE_UPDATE_TESTS_HPP_INCLUDED
#include <QTest>
#include <QSignalSpy>
#include <QTemporaryDir>
#include <QAppImageUpdate>
#include <QScopedPointer>
#include <QDebug>
#include <QStringList>
#include <QCoreApplication>
#include <QJsonObject>
#include <QtConcurrent>
#include <QFuture>
#include <QEventLoop>
#include "SimpleDownload.hpp"
class QAppImageUpdateTests : public QObject {
Q_OBJECT
QScopedPointer<QTemporaryDir> m_TempDir;
QStringList m_Available;
private slots:
void initTestCase(void) {
SimpleDownload downloader;
m_TempDir.reset(new QTemporaryDir);
if(!m_TempDir->isValid()) {
QFAIL("Cannot create temporary directory");
emit finished();
return;
}
/// The testable AppImages
QStringList urls;
/// Github based AppImage Update
urls /* Small AppImages */
<< "https://github.com/AppImage/AppImageKit/releases/download/10/appimagetool-x86_64.AppImage"
#ifdef QUICK_TEST
;
#else
/* Slightly larger AppImage */
<< "https://github.com/antony-jr/AppImageUpdater/releases/download/14/AppImageUpdater-9b4000e-x86_64.AppImage"
/* Largest AppImage */
/* Temporarily Disable FreeCAD update check because it has broken release right now
as of 19/02/2021.
<< "https://github.com/FreeCAD/FreeCAD/releases/download/0.18.2/FreeCAD_0.18-16117-Linux-Conda_Py3Qt5_glibc2.12-x86_64.AppImage"*/
;
/// Bintray based AppImage Update
/// Bintray is now shutdown so no need to
/// test them.
//
//urls /* Large AppImage < 200 MiB */
// << "https://bintray.com/probono/AppImages/download_file?file_path=Blender-2.78-x86_64.AppImage"
// << "https://bintray.com/probono/AppImages/download_file?file_path=FreeCAD-0.17.git201709021132.glibc2.17-x86_64.AppImage";
/// Direct zsync AppImage Update
urls << "https://releases.openclonk.org/snapshots/2020-08-08T17:14:06Z-master-dc43c2b72/OpenClonk-x86_64.AppImage";
/// Gitlab zsync AppImage Update
urls << "https://gitlab.com/probono/QtQuickApp/-/jobs/73879740/artifacts/raw/QtQuickApp-x86_64.AppImage";
/// Torrent Based Update supported AppImage.
urls << "https://github.com/antony-jr/ShareMyHost/releases/download/1/ShareMyHost-a3b2973-x86_64.AppImage";
/// AppImage Update without range request support
/// Cannot find anything for now. Please add one if you find it.
#endif // QUICK TEST
/// Download the required testing AppImages
int count = 1;
for(auto iter = urls.begin(),
end = urls.end();
iter != end;
++iter) {
QString path = m_TempDir->path() + "/" + QString::number(count) + ".AppImage";
if(!downloader.download(*iter, path)) {
m_Available << path;
} else {
QWARN("Download Failed");
}
++count;
}
if(m_Available.size() == 0) {
QFAIL("No AppImages to test");
}
return;
}
void actionGetEmbeddedInfo() {
QAppImageUpdate updater;
updater.setAppImage(m_Available.at(0));
connect(&updater, &QAppImageUpdate::error, this, &QAppImageUpdateTests::defaultErrorHandler);
QSignalSpy spyInfo(&updater, SIGNAL(finished(QJsonObject, short)));
updater.start(QAppImageUpdate::Action::GetEmbeddedInfo);
spyInfo.wait(10 * 1000);
QVERIFY(spyInfo.count() == 1);
/* Get resultant QJsonObject and Compare. */
auto sg = spyInfo.takeFirst();
QJsonObject result = sg.at(0).toJsonObject();
short action = sg.at(1).toInt();
QVERIFY(action == QAppImageUpdate::Action::GetEmbeddedInfo);
/* Check if the result has a json sub-object called 'FileInformation'.
* If so then compare it with our test case file information.
*/
auto fileInfo = result["FileInformation"].toObject();
/* If the file info is empty then fail. */
QVERIFY(!fileInfo.isEmpty());
auto updateInfo = result["UpdateInformation"].toObject();
/* if the update info is empty then fail. */
QVERIFY(!updateInfo.isEmpty());
/* both are not empty , Check the value for isEmpty in the resultant. */
QVERIFY(!result["isEmpty"].toBool());
return;
}
void actionGetEmbeddedInfoAll(void) {
short action = 0;
QJsonObject result,fileInfo,updateInfo;
QList<QVariant> sg;
QAppImageUpdate updater;
connect(&updater, &QAppImageUpdate::error, this, &QAppImageUpdateTests::defaultErrorHandler);
QSignalSpy spyInfo(&updater, SIGNAL(finished(QJsonObject, short)));
for(auto iter = m_Available.begin(),
end = m_Available.end();
iter != end;
++iter) {
updater.setAppImage(*iter);
qInfo().noquote() << "GetEmbeddedInfo(" << *iter << ")";
updater.start(QAppImageUpdate::Action::GetEmbeddedInfo);
spyInfo.wait(10 * 1000);
QCOMPARE(spyInfo.count(), 1);
/* Get resultant QJsonObject and Compare. */
sg = spyInfo.takeFirst();
result = sg.at(0).toJsonObject();
action = sg.at(1).toInt();
QVERIFY(action == QAppImageUpdate::Action::GetEmbeddedInfo);
/* Check if the result has a json sub-object called 'FileInformation'.
* If so then compare it with our test case file information.
*/
fileInfo = result["FileInformation"].toObject();
/* If the file info is empty then fail. */
QVERIFY(!fileInfo.isEmpty());
updateInfo = result["UpdateInformation"].toObject();
/* if the update info is empty then fail. */
QVERIFY(!updateInfo.isEmpty());
/* both are not empty , Check the value for isEmpty in the resultant. */
QVERIFY(!result["isEmpty"].toBool());
}
}
void actionCheckForUpdate() {
QAppImageUpdate updater;
updater.setAppImage(m_Available.at(0));
connect(&updater, &QAppImageUpdate::error, this, &QAppImageUpdateTests::defaultErrorHandler);
QEventLoop loop;
QSignalSpy spyInfo(&updater, SIGNAL(finished(QJsonObject, short)));
connect(&updater, &QAppImageUpdate::finished, &loop, &QEventLoop::quit, Qt::QueuedConnection);
updater.start(QAppImageUpdate::Action::CheckForUpdate);
loop.exec();
QVERIFY(spyInfo.count() == 1);
auto sg = spyInfo.takeFirst();
QJsonObject result = sg.at(0).toJsonObject();
short action = sg.at(1).toInt();
QVERIFY(action == QAppImageUpdate::Action::CheckForUpdate);
QVERIFY(result.contains("UpdateAvailable"));
}
void actionCheckForUpdateAll() {
short action = 0;
QJsonObject result;
QList<QVariant> sg;
QAppImageUpdate updater;
connect(&updater, &QAppImageUpdate::error, this, &QAppImageUpdateTests::defaultErrorHandler);
QSignalSpy spyInfo(&updater, SIGNAL(finished(QJsonObject, short)));
QEventLoop loop;
connect(&updater, &QAppImageUpdate::finished, &loop, &QEventLoop::quit, Qt::QueuedConnection);
for(auto iter = m_Available.begin(),
end = m_Available.end();
iter != end;
++iter) {
updater.setAppImage(*iter);
qInfo().noquote() << "CheckForUpdate(" << *iter << ")";
updater.start(QAppImageUpdate::Action::CheckForUpdate);
loop.exec();
QCOMPARE(spyInfo.count(), 1);
sg = spyInfo.takeFirst();
result = sg.at(0).toJsonObject();
action = sg.at(1).toInt();
QVERIFY(action == QAppImageUpdate::Action::CheckForUpdate);
QVERIFY(result.contains("UpdateAvailable"));
}
}
void actionUpdate() {
short action = 0;
QJsonObject result;
QList<QVariant> sg;
QAppImageUpdate updater;
connect(&updater, &QAppImageUpdate::error, this, &QAppImageUpdateTests::defaultErrorHandler);
QSignalSpy spyInfo(&updater, SIGNAL(finished(QJsonObject, short)));
QEventLoop loop;
connect(&updater, &QAppImageUpdate::finished, &loop, &QEventLoop::quit, Qt::QueuedConnection);
updater.setAppImage(m_Available.at(0));
updater.start(QAppImageUpdate::Action::Update);
loop.exec();
QCOMPARE(spyInfo.count(), 1);
sg = spyInfo.takeFirst();
result = sg.at(0).toJsonObject();
action = sg.at(1).toInt();
QVERIFY(action == QAppImageUpdate::Action::Update);
QVERIFY(result.contains("NewVersionSha1Hash"));
/// Remove the appimage if its updated.
QFile::remove(result["NewVersionPath"].toString());
}
// Test the default action sequence
// CheckForUpdate -> Update
// Make sure that exactly the required signals are
// emitted.
void actionSequenceAll() {
short action = 0;
QJsonObject result;
QList<QVariant> sg;
QAppImageUpdate updater;
connect(&updater, &QAppImageUpdate::error, this, &QAppImageUpdateTests::defaultErrorHandler);
QSignalSpy spyInfo(&updater, SIGNAL(finished(QJsonObject, short)));
QEventLoop loop;
connect(&updater, &QAppImageUpdate::finished, &loop, &QEventLoop::quit, Qt::QueuedConnection);
for(auto iter = m_Available.begin(),
end = m_Available.end();
iter != end;
++iter) {
updater.setAppImage(*iter);
qInfo().noquote() << "Update(" << *iter << ")";
updater.start(QAppImageUpdate::Action::CheckForUpdate);
loop.exec();
QCOMPARE(spyInfo.count(), 1);
sg = spyInfo.takeFirst();
result = sg.at(0).toJsonObject();
action = sg.at(1).toInt();
QVERIFY(action == QAppImageUpdate::Action::CheckForUpdate);
QVERIFY(result.contains("UpdateAvailable"));
bool torrentSupported = result["TorrentSupported"].toBool();
auto remoteSha1 = result["RemoteSha1Hash"].toString();
// Now Update
updater.start();
loop.exec();
QCOMPARE(spyInfo.count(), 1);
sg = spyInfo.takeFirst();
result = sg.at(0).toJsonObject();
action = sg.at(1).toInt();
QVERIFY(action == QAppImageUpdate::Action::Update);
QCOMPARE(remoteSha1, result["NewVersionSha1Hash"].toString());
/// Should not use torrent if it supports it.
if(torrentSupported ) {
QCOMPARE(false, result["UsedTorrent"].toBool());
}
/// Remove all appimage if its updated.
QFile::remove(result["NewVersionPath"].toString());
}
}
// Same asa actionSequenceAll but with non single
// threaded mode.
void actionSequenceAllNonSingleThreaded() {
short action = 0;
QJsonObject result;
QList<QVariant> sg;
QAppImageUpdate updater(/*singleThreaded=*/false);
connect(&updater, &QAppImageUpdate::error, this, &QAppImageUpdateTests::defaultErrorHandler);
QSignalSpy spyInfo(&updater, SIGNAL(finished(QJsonObject, short)));
QEventLoop loop;
connect(&updater, &QAppImageUpdate::finished, &loop, &QEventLoop::quit, Qt::QueuedConnection);
for(auto iter = m_Available.begin(),
end = m_Available.end();
iter != end;
++iter) {
updater.setAppImage(*iter);
qInfo().noquote() << "Update(" << *iter << ")";
updater.start(QAppImageUpdate::Action::CheckForUpdate);
loop.exec();
QCOMPARE(spyInfo.count(), 1);
sg = spyInfo.takeFirst();
result = sg.at(0).toJsonObject();
action = sg.at(1).toInt();
QVERIFY(action == QAppImageUpdate::Action::CheckForUpdate);
QVERIFY(result.contains("UpdateAvailable"));
bool torrentSupported = result["TorrentSupported"].toBool();
auto remoteSha1 = result["RemoteSha1Hash"].toString();
// Now Update
updater.start();
loop.exec();
QCOMPARE(spyInfo.count(), 1);
sg = spyInfo.takeFirst();
result = sg.at(0).toJsonObject();
action = sg.at(1).toInt();
QVERIFY(action == QAppImageUpdate::Action::Update);
QCOMPARE(remoteSha1, result["NewVersionSha1Hash"].toString());
/// Should not use torrent if it supports it.
if(torrentSupported ) {
QCOMPARE(false, result["UsedTorrent"].toBool());
}
/// Remove all appimage if its updated.
QFile::remove(result["NewVersionPath"].toString());
}
}
void actionCancelGetEmbeddedInfo() {
QAppImageUpdate updater;
updater.setAppImage(m_Available.at(0));
connect(&updater, &QAppImageUpdate::error, this, &QAppImageUpdateTests::defaultErrorHandler);
QSignalSpy spyInfo(&updater, SIGNAL(finished(QJsonObject, short)));
QSignalSpy cancelInfo(&updater, SIGNAL(canceled(short)));
updater.start(QAppImageUpdate::Action::GetEmbeddedInfo);
updater.cancel();
cancelInfo.wait(10 * 1000);
/// We should only have the cancel signal emitted.
QCOMPARE(spyInfo.count(), 0);
QCOMPARE(cancelInfo.count(), 1);
/* Get resultant QJsonObject and Compare. */
auto sg = cancelInfo.takeFirst();
short action = sg.at(0).toInt();
QVERIFY(action == QAppImageUpdate::Action::GetEmbeddedInfo);
return;
}
void actionCancelCheckForUpdate() {
QAppImageUpdate updater;
updater.setAppImage(m_Available.at(0));
connect(&updater, &QAppImageUpdate::error, this, &QAppImageUpdateTests::defaultErrorHandler);
QEventLoop loop;
QSignalSpy spyInfo(&updater, SIGNAL(finished(QJsonObject, short)));
QSignalSpy cancelInfo(&updater, SIGNAL(canceled(short)));
connect(&updater, &QAppImageUpdate::finished, &loop, &QEventLoop::quit, Qt::QueuedConnection);
connect(&updater, &QAppImageUpdate::canceled, &loop, &QEventLoop::quit, Qt::QueuedConnection);
updater.start(QAppImageUpdate::Action::CheckForUpdate);
updater.cancel();
loop.exec();
/// We should only have the cancel signal emitted.
QCOMPARE(spyInfo.count(), 0);
QCOMPARE(cancelInfo.count(), 1);
auto sg = cancelInfo.takeFirst();
short action = sg.at(0).toInt();
QVERIFY(action == QAppImageUpdate::Action::CheckForUpdate);
}
void actionCancelUpdate() {
short action = 0;
QJsonObject result;
QList<QVariant> sg;
QAppImageUpdate updater;
connect(&updater, &QAppImageUpdate::error, this, &QAppImageUpdateTests::defaultErrorHandler);
QSignalSpy spyInfo(&updater, SIGNAL(finished(QJsonObject, short)));
QSignalSpy cancelInfo(&updater, SIGNAL(canceled(short)));
QEventLoop loop;
connect(&updater, &QAppImageUpdate::finished, &loop, &QEventLoop::quit, Qt::QueuedConnection);
connect(&updater, &QAppImageUpdate::canceled, &loop, &QEventLoop::quit, Qt::QueuedConnection);
updater.setAppImage(m_Available.at(0));
/// Cancel as soon as the delta writer starts up
connect(&updater, &QAppImageUpdate::started, &updater, &QAppImageUpdate::cancel, Qt::DirectConnection);
/// Start the updater.
updater.start();
loop.exec();
/// We should only have the cancel signal emitted.
QCOMPARE(spyInfo.count(), 0);
QCOMPARE(cancelInfo.count(), 1);
sg = cancelInfo.takeFirst();
action = sg.at(0).toInt();
QVERIFY(action == QAppImageUpdate::Action::Update);
}
#ifdef DECENTRALIZED_UPDATE_ENABLED
void actionUpdateWithTorrent() {
short action = 0;
QJsonObject result;
QList<QVariant> sg;
QAppImageUpdate updater;
connect(&updater, &QAppImageUpdate::error, this, &QAppImageUpdateTests::defaultErrorHandler);
QSignalSpy spyInfo(&updater, SIGNAL(finished(QJsonObject, short)));
QEventLoop loop;
connect(&updater, &QAppImageUpdate::finished, &loop, &QEventLoop::quit, Qt::QueuedConnection);
//// Search for all test files which supports torrent update and
//// Update it with torrents.
for(auto iter = m_Available.begin(),
end = m_Available.end();
iter != end;
++iter) {
updater.setAppImage(*iter);
qInfo().noquote() << "Update(" << *iter << ")";
updater.start(QAppImageUpdate::Action::CheckForUpdate);
loop.exec();
QCOMPARE(spyInfo.count(), 1);
sg = spyInfo.takeFirst();
result = sg.at(0).toJsonObject();
action = sg.at(1).toInt();
QVERIFY(action == QAppImageUpdate::Action::CheckForUpdate);
QVERIFY(result.contains("UpdateAvailable"));
auto remoteSha1 = result["RemoteSha1Hash"].toString();
if(result["TorrentSupported"].toBool()) {
// Now Update With Torrent confirmation.
updater.start(QAppImageUpdate::Action::UpdateWithTorrent);
loop.exec();
QCOMPARE(spyInfo.count(), 1);
sg = spyInfo.takeFirst();
result = sg.at(0).toJsonObject();
action = sg.at(1).toInt();
QVERIFY(action == QAppImageUpdate::Action::UpdateWithTorrent);
QCOMPARE(remoteSha1, result["NewVersionSha1Hash"].toString());
QCOMPARE(true, result["UsedTorrent"].toBool());
QFile::remove(result["NewVersionPath"].toString());
}
}
}
#endif // DECENTRALIZED_UPDATE_ENABLED
//// This should not crash the test
//// That is the test.
void destructWhileUpdate() {
{
QAppImageUpdate updater;
QEventLoop startLoop;
connect(&updater, &QAppImageUpdate::started, &startLoop, &QEventLoop::quit, Qt::QueuedConnection);
connect(&updater, &QAppImageUpdate::error, this, &QAppImageUpdateTests::defaultErrorHandler);
updater.setAppImage(m_Available.at(0));
updater.start();
// Destruct as soon as the update starts.
startLoop.exec();
}
}
/// I have no idea on how to test thread safety,
// so we are just gonna call setAppImage and
// start from multiple threads.
//
// The expected result is,
//
// The first thread to call setAppImage and start
// will succeed and all other threads calls will be
// invalid and will not affect the object or segfaults.
//
// So the QSignalSpy should have only one signal that
// is finished. and not multiple signals.
void threadSafety() {
QAppImageUpdate updater;
QEventLoop loop;
connect(&updater, &QAppImageUpdate::error, this, &QAppImageUpdateTests::defaultErrorHandler);
QSignalSpy spyInfo(&updater, SIGNAL(finished(QJsonObject, short)));
connect(&updater, &QAppImageUpdate::finished, &loop, &QEventLoop::quit, Qt::QueuedConnection);
auto function = [&]() {
updater.setAppImage(m_Available.at(0));
updater.start();
};
auto future1 = new QFuture<void>;
auto future2 = new QFuture<void>;
auto future3 = new QFuture<void>;
auto future4 = new QFuture<void>;
*future1 = QtConcurrent::run(function);
*future2 = QtConcurrent::run(function);
*future3 = QtConcurrent::run(function);
*future4 = QtConcurrent::run(function);
/// Wait for all futures to end;
while(future1->isRunning() || future2->isRunning() ||
future3->isRunning() || future4->isRunning()) {
QCoreApplication::processEvents();
}
delete future1;
delete future2;
delete future3;
delete future4;
if(spyInfo.count() < 1) {
if(!spyInfo.wait()) {
loop.exec();
}
}
QCOMPARE(spyInfo.count(), 1);
auto sg = spyInfo.takeFirst();
QJsonObject result = sg.at(0).toJsonObject();
short action = sg.at(1).toInt();
QVERIFY(action == QAppImageUpdate::Action::Update);
}
void cleanupTestCase(void) {
m_TempDir->remove();
emit finished();
return;
}
protected slots:
void defaultErrorHandler(short code, short action) {
Q_UNUSED(action);
auto scode = QAppImageUpdate::errorCodeToString(code);
scode.prepend("error:: ");
QFAIL(QTest::toString(scode));
return;
}
Q_SIGNALS:
void finished(void);
};
#endif
↑ V779 Unreachable code detected. It is possible that an error is present.