summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRobert Griebl <robert.griebl@qt.io>2024-03-02 22:24:02 +0100
committerRobert Griebl <robert.griebl@qt.io>2024-03-05 12:30:26 +0100
commit96e290e617e20127002ce3e3a5f151d128cc3189 (patch)
treebaf8e735bae4a30eff5f02f44ecfad0dce60cea9
parent607b74cfea26d45285c36314592927e03c9dfeff (diff)
tests: add a test for the new package-server tool
Also fixes three bugs found in the server with those tests. Change-Id: Ic2ca10cc2ee66d644c97b4b613e59521dfcec7eb Pick-to: 6.7 Reviewed-by: Dominik Holland <dominik.holland@qt.io>
-rw-r--r--src/tools/package-server/package-server.cpp1
-rw-r--r--src/tools/package-server/pshttpinterface.cpp2
-rw-r--r--src/tools/package-server/pspackages.cpp12
-rw-r--r--tests/auto/CMakeLists.txt3
-rw-r--r--tests/auto/package-server-tool/CMakeLists.txt11
-rw-r--r--tests/auto/package-server-tool/tst_package-server-tool.cpp281
6 files changed, 306 insertions, 4 deletions
diff --git a/src/tools/package-server/package-server.cpp b/src/tools/package-server/package-server.cpp
index 1d425b55..f625ce43 100644
--- a/src/tools/package-server/package-server.cpp
+++ b/src/tools/package-server/package-server.cpp
@@ -38,7 +38,6 @@ int main(int argc, char **argv)
try {
auto cfg = std::make_unique<PSConfiguration>();
cfg->parse(a.arguments());
-
colorOut() << ColorPrint::bgreen << QCoreApplication::applicationName() << ColorPrint::reset;
colorOut() << "> Data directory: " << ColorPrint::bmagenta << cfg->dataDirectory.absolutePath() << ColorPrint::reset;
colorOut() << "> Project: " << ColorPrint::bcyan << cfg->projectId << ColorPrint::reset;
diff --git a/src/tools/package-server/pshttpinterface.cpp b/src/tools/package-server/pshttpinterface.cpp
index de99f764..a0eb6703 100644
--- a/src/tools/package-server/pshttpinterface.cpp
+++ b/src/tools/package-server/pshttpinterface.cpp
@@ -216,7 +216,7 @@ void PSHttpInterface::setupRouting(PSPackages *packages)
<< " [" << (architecture.isEmpty() ? u"<any>"_s : architecture) << "] (no match)";
}
return QJsonObject { { u"status"_s, removeCount ? u"ok"_s : u"fail"_s },
- { u"removed"_s, QString::number(removeCount) } };
+ { u"removed"_s, removeCount } };
});
d->server->route(u"/category/list"_s, GetOrPost, [packages](const QHttpServerRequest &) {
diff --git a/src/tools/package-server/pspackages.cpp b/src/tools/package-server/pspackages.cpp
index 2e3083ca..7e06fa99 100644
--- a/src/tools/package-server/pspackages.cpp
+++ b/src/tools/package-server/pspackages.cpp
@@ -25,6 +25,11 @@
#if defined(Q_OS_UNIX)
# include <csignal>
# define AM_PS_SIGNALS { SIGTERM, SIGINT, SIGFPE, SIGSEGV, SIGPIPE, SIGABRT, SIGQUIT }
+# if defined(QT_AM_COVERAGE)
+extern "C" {
+# include <gcov.h>
+}
+# endif
#else
# include <windows.h>
# define AM_PS_SIGNALS { SIGTERM, SIGINT }
@@ -74,7 +79,6 @@ void PSPackages::initialize()
// make sure to always clean up the lock file, even if we crash
d->lockFilePath = d->lockFile->fileName().toLocal8Bit();
-
UnixSignalHandler::instance()->install(UnixSignalHandler::RawSignalHandler, AM_PS_SIGNALS,
[this](int sig) {
UnixSignalHandler::instance()->resetToDefault(sig);
@@ -85,7 +89,11 @@ void PSPackages::initialize()
#else
if (!d->lockFilePath.isEmpty())
::unlink(d->lockFilePath.constData());
- ::kill(0, sig);
+
+# if defined(QT_AM_COVERAGE)
+ __gcov_dump();
+# endif
+ ::kill(::getpid(), sig);
#endif
});
diff --git a/tests/auto/CMakeLists.txt b/tests/auto/CMakeLists.txt
index cdf4e29c..fd503c0c 100644
--- a/tests/auto/CMakeLists.txt
+++ b/tests/auto/CMakeLists.txt
@@ -32,4 +32,7 @@ if (LINUX)
if (TARGET Qt::DBus)
add_subdirectory(controller-tool)
endif()
+ if (QT_FEATURE_am_package_server)
+ add_subdirectory(package-server-tool)
+ endif()
endif()
diff --git a/tests/auto/package-server-tool/CMakeLists.txt b/tests/auto/package-server-tool/CMakeLists.txt
new file mode 100644
index 00000000..d104cc7a
--- /dev/null
+++ b/tests/auto/package-server-tool/CMakeLists.txt
@@ -0,0 +1,11 @@
+
+qt_internal_add_test(tst_package-server-tool
+ SOURCES
+ ../error-checking.h
+ tst_package-server-tool.cpp
+ DEFINES
+ AM_TESTDATA_DIR="${CMAKE_CURRENT_BINARY_DIR}/../../data/"
+ LIBRARIES
+ Qt::Network
+ Qt::AppManCommonPrivate
+)
diff --git a/tests/auto/package-server-tool/tst_package-server-tool.cpp b/tests/auto/package-server-tool/tst_package-server-tool.cpp
new file mode 100644
index 00000000..4f54c45d
--- /dev/null
+++ b/tests/auto/package-server-tool/tst_package-server-tool.cpp
@@ -0,0 +1,281 @@
+// Copyright (C) 2023 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
+
+#include <QtCore>
+#include <QtTest>
+#include <QDir>
+#include <QString>
+#include <QTemporaryDir>
+#include <QUrl>
+#include <QUrlQuery>
+#include <QNetworkAccessManager>
+#include <QStringBuilder>
+
+#if defined(Q_OS_LINUX)
+# include <sys/prctl.h>
+# include <signal.h>
+#endif
+
+#include "utilities.h"
+
+
+using namespace Qt::StringLiterals;
+
+static QString findAppManTool(const QString &toolName)
+{
+ static const QStringList possibleLocations = {
+ QCoreApplication::applicationDirPath() + u"/../../../bin"_s,
+ QLibraryInfo::path(QLibraryInfo::BinariesPath)
+ };
+
+ for (const QString &possibleLocation : possibleLocations) {
+ QFileInfo fi(possibleLocation + u'/' + toolName);
+ if (fi.exists() && fi.isExecutable())
+ return fi.absoluteFilePath();
+ }
+ return { };
+}
+
+class tst_PackageServerTool : public QObject
+{
+ Q_OBJECT
+
+public:
+ tst_PackageServerTool();
+
+private slots:
+ void initTestCase();
+ void cleanupTestCase();
+
+ void httpInterface();
+
+private:
+ QTemporaryDir m_tmpDir;
+ QProcess m_psProcess;
+ QUrl m_url;
+ QNetworkAccessManager m_nam;
+};
+
+
+tst_PackageServerTool::tst_PackageServerTool()
+{ }
+
+void tst_PackageServerTool::initTestCase()
+{
+#if !defined(Q_OS_LINUX)
+ QSKIP("This test is only supported on Linux");
+#endif
+
+ if (!QDir(QString::fromLatin1(AM_TESTDATA_DIR "/packages")).exists())
+ QSKIP("No test packages available in the data/ directory");
+
+ auto verbose = qEnvironmentVariableIsSet("AM_VERBOSE_TEST");
+ qInfo() << "Verbose mode is" << (verbose ? "on" : "off") << "(change by (un)setting $AM_VERBOSE_TEST)";
+
+ const QString psTool = findAppManTool(u"appman-package-server"_s);
+ QVERIFY(!psTool.isEmpty());
+
+ QVERIFY(m_tmpDir.isValid());
+ QVERIFY(QDir(m_tmpDir.path()).mkpath(u".packages"_s));
+ QVERIFY(QFile::copy(QString::fromLatin1(AM_TESTDATA_DIR "/packages/test.appkg"),
+ m_tmpDir.path() + u"/.packages/com.pelagicore.test_all.ampkg"_s));
+
+ m_psProcess.setProgram(psTool);
+ QStringList args = { u"--dd"_s, m_tmpDir.path() };
+ m_psProcess.setArguments(args);
+ QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
+ env.insert(u"AM_FORCE_COLOR_OUTPUT"_s, u"off"_s);
+ m_psProcess.setProcessEnvironment(env);
+
+#if defined(Q_OS_LINUX)
+ m_psProcess.setChildProcessModifier([]() {
+ // at least on Linux we can make sure that the package-server is always killed
+ ::prctl(PR_SET_PDEATHSIG, SIGKILL);
+ });
+#endif
+ m_psProcess.start();
+
+ QVERIFY(m_psProcess.waitForStarted());
+ QVERIFY(m_psProcess.waitForReadyRead());
+ QTest::qWait(500 * QtAM::timeoutFactor());
+
+ const auto err = QString::fromLocal8Bit(m_psProcess.readAllStandardError());
+ QCOMPARE(err, QString { });
+ const auto outLines = QString::fromLocal8Bit(m_psProcess.readAllStandardOutput()).split(u'\n', Qt::SkipEmptyParts);
+
+ const QStringList expectedLines = {
+ u"Qt ApplicationManager Package Server"_s,
+ u"> Data directory: "_s + m_tmpDir.path(),
+ u"> Project: PROJECT"_s,
+ u"> Verify developer signature on upload: no"_s,
+ u"> Add store signature on download: no"_s,
+ u"> Scanning .packages"_s,
+ u" + adding com.pelagicore.test [all]"_s,
+ u"> HTTP server listening on: "_s // + <ip>:<port>
+ };
+
+ QCOMPARE(outLines.size(), expectedLines.size());
+ auto sameLines = expectedLines.size() - 1;
+ QCOMPARE(outLines.first(sameLines), expectedLines.first(sameLines));
+ QVERIFY(outLines.last().startsWith(expectedLines.last()));
+ m_url = u"http://"_s + outLines.last().mid(expectedLines.last().length());
+ QVERIFY(m_url.isValid());
+ qInfo() << "packager-server URL:" << m_url.toString();
+}
+
+void tst_PackageServerTool::cleanupTestCase()
+{
+ if (m_psProcess.state() != QProcess::NotRunning) {
+ m_psProcess.terminate();
+ QVERIFY(m_psProcess.waitForFinished());
+ }
+}
+
+void tst_PackageServerTool::httpInterface()
+{
+ // we cannot use test data here, as the steps have to be done in order and most
+ // cannot be repeated
+
+ static QByteArray iconPng;
+ static QByteArray testAmpkg;
+
+ if (iconPng.isEmpty()) {
+ QFile f(QString::fromLatin1(AM_TESTDATA_DIR "/icon.png"));
+ QVERIFY(f.open(QIODevice::ReadOnly));
+ iconPng = f.readAll();
+ QVERIFY(!iconPng.isEmpty());
+ }
+
+ if (testAmpkg.isEmpty()) {
+ QFile f(QString::fromLatin1(AM_TESTDATA_DIR "/packages/test.appkg"));
+ QVERIFY(f.open(QIODevice::ReadOnly));
+ testAmpkg = f.readAll();
+ QVERIFY(!testAmpkg.isEmpty());
+ }
+
+ static const auto Get = QNetworkAccessManager::GetOperation;
+ static const auto Post = QNetworkAccessManager::PostOperation;
+ static const auto Put = QNetworkAccessManager::PutOperation;
+
+ std::vector<std::tuple<QByteArray, QByteArray, QUrlQuery, QNetworkAccessManager::Operation, int, QVariant>> testCases = {
+
+ { "hello-no-project", "/hello", QUrlQuery(), Get, 200,
+ QJsonObject { { u"status"_s, u"incompatible-project-id"_s } } },
+
+ { "hello", "/hello", QUrlQuery({ { u"project-id"_s, u"PROJECT"_s } }), Get, 200,
+ QJsonObject { { u"status"_s, u"ok"_s } } },
+
+ { "categories", "/category/list", QUrlQuery(), Get, 200,
+ QJsonArray { u"test-category"_s } },
+
+ { "packages", "/package/list", QUrlQuery(), Get, 200,
+ QJsonArray { QJsonObject {
+ { u"architecture"_s, u""_s },
+ { u"id"_s, u"com.pelagicore.test"_s },
+ { u"categories"_s, QJsonArray { u"test-category"_s } },
+ { u"iconUrl"_s, u"package/icon?id=com.pelagicore.test"_s },
+ { u"names"_s, QJsonObject { { u"de"_s, u"Hallo"_s }, { u"en"_s, u"Hello"_s } } },
+ { u"descriptions"_s, QJsonObject { } },
+ { u"version"_s, u"1.0"_s },
+ } } },
+
+ { "no-icon", "/package/icon", QUrlQuery(), Get, 404,
+ QByteArray { } },
+
+ { "icon", "/package/icon", QUrlQuery { { u"id"_s, u"com.pelagicore.test"_s} }, Get, 200,
+ iconPng },
+
+ { "no-download", "/package/download", QUrlQuery(), Get, 404,
+ QByteArray { } },
+
+ { "download", "/package/download", QUrlQuery { { u"id"_s, u"com.pelagicore.test"_s} }, Get, 200,
+ testAmpkg },
+
+ { "remove-non-existent", "/package/remove", QUrlQuery({ { u"id"_s, u"com.pelagicore.test"_s }, { u"architecture"_s, u"x86_64"_s } }), Post, 200,
+ QJsonObject { { u"status"_s, u"fail"_s }, { u"removed"_s, 0 } } },
+
+ { "remove", "/package/remove", QUrlQuery({ { u"id"_s, u"com.pelagicore.test"_s } }), Post, 200,
+ QJsonObject { { u"status"_s, u"ok"_s }, { u"removed"_s, 1 } } },
+
+ { "no-categories", "/category/list", QUrlQuery(), Get, 200,
+ QJsonArray { } },
+
+ { "no-packages", "/package/list", QUrlQuery(), Get, 200,
+ QJsonArray { } },
+
+ { "upload", "/package/upload", QUrlQuery(), Put, 200,
+ QJsonObject {
+ { u"status"_s, u"ok"_s },
+ { u"result"_s, u"added"_s },
+ { u"id"_s, u"com.pelagicore.test"_s },
+ { u"architecture"_s, u"all"_s },
+ } },
+
+ { "upload-again", "/package/upload", QUrlQuery(), Put, 200,
+ QJsonObject {
+ { u"status"_s, u"ok"_s },
+ { u"result"_s, u"no changes"_s },
+ { u"id"_s, u"com.pelagicore.test"_s },
+ { u"architecture"_s, u"all"_s },
+ } },
+
+ };
+
+ for (const auto &testCase : testCases) {
+ const auto &name = std::get<0>(testCase);
+ const auto &path = std::get<1>(testCase);
+ const auto &query = std::get<2>(testCase);
+ const auto &operation = std::get<3>(testCase);
+ const auto &expectedStatusCode = std::get<4>(testCase);
+ const auto &expectedReply = std::get<5>(testCase);
+
+ qInfo() << "tag:" << name.constData();
+
+ auto url = m_url;
+ url.setPath(QString::fromLatin1(path));
+ url.setQuery(query);
+ QNetworkRequest req(url);
+ QNetworkReply *reply = nullptr;
+ switch (operation) {
+ case QNetworkAccessManager::GetOperation:
+ reply = m_nam.get(req);
+ break;
+ case QNetworkAccessManager::PostOperation:
+ req.setHeader(QNetworkRequest::ContentTypeHeader, u"application/x-www-form-urlencoded"_s);
+ reply = m_nam.post(req, QByteArray());
+ break;
+ case QNetworkAccessManager::PutOperation:
+ reply = m_nam.put(req, testAmpkg); // hardcoded to keep the tuple small
+ break;
+ default:
+ break;
+ }
+ QVERIFY(reply);
+ QTRY_VERIFY_WITH_TIMEOUT(reply->isFinished(), 3000 * QtAM::timeoutFactor());
+ QCOMPARE(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(), expectedStatusCode);
+
+ switch (expectedReply.userType()) {
+ case QMetaType::QJsonArray:
+ case QMetaType::QJsonObject: {
+ auto json = QJsonDocument::fromJson(reply->readAll());
+ QVERIFY(!json.isNull());
+ QJsonDocument expectedJson;
+ if (expectedReply.userType() == QMetaType::QJsonArray)
+ expectedJson.setArray(expectedReply.toJsonArray());
+ else
+ expectedJson.setObject(expectedReply.toJsonObject());
+ QCOMPARE(json, expectedJson);
+ break;
+ }
+ case QMetaType::QByteArray:
+ QCOMPARE(reply->readAll(), expectedReply.toByteArray());
+ break;
+ default:
+ break;
+ }
+ }
+}
+
+QTEST_GUILESS_MAIN(tst_PackageServerTool)
+
+#include "tst_package-server-tool.moc"