tests: add dbus smoke + stop-launching regression

This commit is contained in:
Marco Allegretti 2026-02-12 14:52:34 +01:00
parent e9071c2b86
commit b0d09be497
4 changed files with 1118 additions and 0 deletions

View file

@ -26,6 +26,7 @@ include(ECMPoQmTools)
include(ECMDeprecationSettings) include(ECMDeprecationSettings)
include(ECMQmlModule) include(ECMQmlModule)
include(ECMAddAppIcon) include(ECMAddAppIcon)
include(CTest)
include(FeatureSummary) include(FeatureSummary)
ecm_setup_version(${PROJECT_VERSION} ecm_setup_version(${PROJECT_VERSION}
@ -114,6 +115,10 @@ add_subdirectory(src)
add_subdirectory(src/krunner) add_subdirectory(src/krunner)
add_subdirectory(icons) add_subdirectory(icons)
if (BUILD_TESTING)
add_subdirectory(tests)
endif()
ki18n_install(po) ki18n_install(po)
install(FILES org.kde.alakarte.desktop DESTINATION ${KDE_INSTALL_APPDIR}) install(FILES org.kde.alakarte.desktop DESTINATION ${KDE_INSTALL_APPDIR})

45
tests/CMakeLists.txt Normal file
View file

@ -0,0 +1,45 @@
find_package(Qt6 ${QT_MIN_VERSION} QUIET COMPONENTS Test)
if (NOT Qt6Test_FOUND OR NOT TARGET Qt6::Test)
return()
endif()
add_executable(alakarte_dbus_smoketest
dbus_smoketest.cpp
)
target_link_libraries(alakarte_dbus_smoketest PRIVATE
Qt6::Core
Qt6::DBus
Qt6::Test
)
add_test(NAME alakarte_dbus_smoketest
COMMAND alakarte_dbus_smoketest
--gamecenter $<TARGET_FILE:alakarte_gamecenter>
--runnerd $<TARGET_FILE:alakarte_runnerd>
--inputd $<TARGET_FILE:alakarte_inputd>
)
set_tests_properties(alakarte_dbus_smoketest PROPERTIES
TIMEOUT 30
)
add_executable(alakarte_stop_launching_regression_test
stop_launching_regression_test.cpp
)
target_link_libraries(alakarte_stop_launching_regression_test PRIVATE
Qt6::Core
Qt6::DBus
Qt6::Test
)
add_test(NAME alakarte_stop_launching_regression_test
COMMAND alakarte_stop_launching_regression_test
--gamecenter $<TARGET_FILE:alakarte_gamecenter>
)
set_tests_properties(alakarte_stop_launching_regression_test PROPERTIES
TIMEOUT 60
)

438
tests/dbus_smoketest.cpp Normal file
View file

@ -0,0 +1,438 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
#include <QCoreApplication>
#include <QDBusArgument>
#include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <QDBusInterface>
#include <QDBusReply>
#include <QDBusVariant>
#include <QDir>
#include <QElapsedTimer>
#include <QProcess>
#include <QProcessEnvironment>
#include <QStandardPaths>
#include <QStringList>
#include <QUuid>
#include <QtTest>
namespace
{
static QString takeArgValue(const QStringList &args, const QString &key)
{
const int idx = args.indexOf(key);
if (idx < 0) {
return {};
}
if (idx + 1 >= args.size()) {
return {};
}
return args.at(idx + 1);
}
static bool waitForService(QDBusConnection &bus, const QString &service, int timeoutMs)
{
if (!bus.isConnected() || !bus.interface()) {
return false;
}
QElapsedTimer t;
t.start();
while (t.elapsed() < timeoutMs) {
if (bus.interface()->isServiceRegistered(service)) {
return true;
}
QTest::qWait(50);
}
return false;
}
static QVariant unwrapDbusVariant(QVariant v)
{
if (v.metaType() == QMetaType::fromType<QDBusVariant>()) {
v = v.value<QDBusVariant>().variant();
}
return v;
}
static QVariantMap unwrapVariantMap(QVariant v)
{
v = unwrapDbusVariant(v);
if (v.metaType() == QMetaType::fromType<QDBusArgument>()) {
const QDBusArgument arg = v.value<QDBusArgument>();
return qdbus_cast<QVariantMap>(arg);
}
if (v.canConvert<QVariantMap>()) {
return v.toMap();
}
return {};
}
}
class DbusSmokeTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void initTestCase();
void cleanupTestCase();
void pingGameCenter();
void pingRunner();
void pingInput();
void runnerResolveLaunchNative();
void runnerGameProfiles();
private:
QString m_gamecenterPath;
QString m_runnerdPath;
QString m_inputdPath;
QString m_dbusAddress;
QProcess m_dbusDaemon;
QProcess m_gamecenter;
QProcess m_runnerd;
QProcess m_inputd;
QString m_testXdgBase;
QDBusConnection m_bus = QDBusConnection::connectToBus(QString(), QStringLiteral("alakarte_test"));
bool startPrivateBus();
bool haveDbusDaemon() const;
void startDaemon(QProcess &p, const QString &program);
void stopProcess(QProcess &p);
};
bool DbusSmokeTest::haveDbusDaemon() const
{
return !QStandardPaths::findExecutable(QStringLiteral("dbus-daemon")).isEmpty();
}
bool DbusSmokeTest::startPrivateBus()
{
const QString dbusDaemon = QStandardPaths::findExecutable(QStringLiteral("dbus-daemon"));
if (dbusDaemon.isEmpty()) {
return false;
}
m_dbusDaemon.setProgram(dbusDaemon);
m_dbusDaemon.setArguments({QStringLiteral("--session"), QStringLiteral("--nofork"), QStringLiteral("--print-address=1")});
m_dbusDaemon.setProcessChannelMode(QProcess::MergedChannels);
m_dbusDaemon.start();
if (!m_dbusDaemon.waitForStarted(5000)) {
return false;
}
if (!m_dbusDaemon.waitForReadyRead(5000)) {
return false;
}
m_dbusAddress = QString::fromUtf8(m_dbusDaemon.readLine()).trimmed();
if (m_dbusAddress.isEmpty()) {
return false;
}
QDBusConnection::disconnectFromBus(QStringLiteral("alakarte_test"));
m_bus = QDBusConnection::connectToBus(m_dbusAddress, QStringLiteral("alakarte_test"));
return m_bus.isConnected();
}
void DbusSmokeTest::startDaemon(QProcess &p, const QString &program)
{
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert(QStringLiteral("DBUS_SESSION_BUS_ADDRESS"), m_dbusAddress);
if (!m_testXdgBase.isEmpty()) {
env.insert(QStringLiteral("XDG_DATA_HOME"), m_testXdgBase + QStringLiteral("/data"));
env.insert(QStringLiteral("XDG_CONFIG_HOME"), m_testXdgBase + QStringLiteral("/config"));
env.insert(QStringLiteral("XDG_CACHE_HOME"), m_testXdgBase + QStringLiteral("/cache"));
}
p.setProcessEnvironment(env);
p.setProgram(program);
p.setArguments({});
p.setProcessChannelMode(QProcess::MergedChannels);
p.start();
}
void DbusSmokeTest::stopProcess(QProcess &p)
{
if (p.state() == QProcess::NotRunning) {
return;
}
p.terminate();
if (!p.waitForFinished(3000)) {
p.kill();
p.waitForFinished(3000);
}
}
void DbusSmokeTest::initTestCase()
{
m_gamecenterPath = qApp ? qApp->property("alakarte_test_gamecenter").toString() : QString();
m_runnerdPath = qApp ? qApp->property("alakarte_test_runnerd").toString() : QString();
m_inputdPath = qApp ? qApp->property("alakarte_test_inputd").toString() : QString();
if (m_gamecenterPath.isEmpty() || m_runnerdPath.isEmpty() || m_inputdPath.isEmpty()) {
const QStringList args = QCoreApplication::arguments();
if (m_gamecenterPath.isEmpty()) {
m_gamecenterPath = takeArgValue(args, QStringLiteral("--gamecenter"));
}
if (m_runnerdPath.isEmpty()) {
m_runnerdPath = takeArgValue(args, QStringLiteral("--runnerd"));
}
if (m_inputdPath.isEmpty()) {
m_inputdPath = takeArgValue(args, QStringLiteral("--inputd"));
}
}
QVERIFY2(!m_gamecenterPath.isEmpty(), "--gamecenter is required");
QVERIFY2(!m_runnerdPath.isEmpty(), "--runnerd is required");
QVERIFY2(!m_inputdPath.isEmpty(), "--inputd is required");
if (!haveDbusDaemon()) {
QSKIP("dbus-daemon not found");
}
if (!startPrivateBus()) {
QSKIP("failed to start private dbus session");
}
m_testXdgBase = QDir::tempPath() + QStringLiteral("/alakarte-test-xdg-") + QUuid::createUuid().toString(QUuid::WithoutBraces);
QVERIFY(QDir().mkpath(m_testXdgBase + QStringLiteral("/data")));
QVERIFY(QDir().mkpath(m_testXdgBase + QStringLiteral("/config")));
QVERIFY(QDir().mkpath(m_testXdgBase + QStringLiteral("/cache")));
startDaemon(m_runnerd, m_runnerdPath);
QVERIFY(m_runnerd.waitForStarted(5000));
startDaemon(m_inputd, m_inputdPath);
QVERIFY(m_inputd.waitForStarted(5000));
startDaemon(m_gamecenter, m_gamecenterPath);
QVERIFY(m_gamecenter.waitForStarted(5000));
QVERIFY2(waitForService(m_bus, QStringLiteral("org.kde.ALaKarte.Runner1"), 5000), "Runner1 service did not appear on the bus");
QVERIFY2(waitForService(m_bus, QStringLiteral("org.kde.ALaKarte.Input1"), 5000), "Input1 service did not appear on the bus");
QVERIFY2(waitForService(m_bus, QStringLiteral("org.kde.GameCenter1"), 5000), "GameCenter1 service did not appear on the bus");
}
void DbusSmokeTest::cleanupTestCase()
{
stopProcess(m_gamecenter);
stopProcess(m_inputd);
stopProcess(m_runnerd);
stopProcess(m_dbusDaemon);
QDBusConnection::disconnectFromBus(QStringLiteral("alakarte_test"));
if (!m_testXdgBase.isEmpty()) {
QDir(m_testXdgBase).removeRecursively();
m_testXdgBase.clear();
}
}
void DbusSmokeTest::pingGameCenter()
{
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/GameCenter1"), QStringLiteral("org.kde.GameCenter1"), m_bus);
QVERIFY(iface.isValid());
iface.setTimeout(2000);
const QDBusReply<QString> reply = iface.call(QStringLiteral("Ping"));
QVERIFY(reply.isValid());
QCOMPARE(reply.value(), QStringLiteral("ok"));
}
void DbusSmokeTest::pingRunner()
{
QDBusInterface iface(QStringLiteral("org.kde.ALaKarte.Runner1"),
QStringLiteral("/org/kde/ALaKarte/Runner1"),
QStringLiteral("org.kde.ALaKarte.Runner1"),
m_bus);
QVERIFY(iface.isValid());
iface.setTimeout(2000);
const QDBusReply<QString> reply = iface.call(QStringLiteral("Ping"));
QVERIFY(reply.isValid());
QCOMPARE(reply.value(), QStringLiteral("ok"));
}
void DbusSmokeTest::pingInput()
{
QDBusInterface iface(QStringLiteral("org.kde.ALaKarte.Input1"),
QStringLiteral("/org/kde/ALaKarte/Input1"),
QStringLiteral("org.kde.ALaKarte.Input1"),
m_bus);
QVERIFY(iface.isValid());
iface.setTimeout(2000);
const QDBusReply<QString> reply = iface.call(QStringLiteral("Ping"));
QVERIFY(reply.isValid());
QCOMPARE(reply.value(), QStringLiteral("ok"));
}
void DbusSmokeTest::runnerResolveLaunchNative()
{
QDBusInterface iface(QStringLiteral("org.kde.ALaKarte.Runner1"),
QStringLiteral("/org/kde/ALaKarte/Runner1"),
QStringLiteral("org.kde.ALaKarte.Runner1"),
m_bus);
QVERIFY(iface.isValid());
iface.setTimeout(2000);
QVariantMap spec;
spec.insert(QStringLiteral("program"), QStringLiteral("/bin/true"));
spec.insert(QStringLiteral("args"), QStringList{});
const QDBusReply<QVariantMap> reply = iface.call(QStringLiteral("ResolveLaunch"), spec);
QVERIFY(reply.isValid());
const QVariantMap out = reply.value();
QVERIFY(out.value(QStringLiteral("ok")).toBool());
QCOMPARE(out.value(QStringLiteral("finalProgram")).toString(), QStringLiteral("/bin/true"));
QCOMPARE(out.value(QStringLiteral("finalArgs")).toStringList(), QStringList{});
}
void DbusSmokeTest::runnerGameProfiles()
{
QDBusInterface iface(QStringLiteral("org.kde.ALaKarte.Runner1"),
QStringLiteral("/org/kde/ALaKarte/Runner1"),
QStringLiteral("org.kde.ALaKarte.Runner1"),
m_bus);
QVERIFY(iface.isValid());
iface.setTimeout(2000);
const QString gameId = QStringLiteral("alakarte-test-game");
{
const QDBusReply<QVariantMap> reply = iface.call(QStringLiteral("ClearGameProfile"), gameId);
QVERIFY(reply.isValid());
QVERIFY(reply.value().value(QStringLiteral("ok")).toBool());
}
{
QVariantMap spec;
spec.insert(QStringLiteral("gameId"), gameId);
spec.insert(QStringLiteral("runner"), QStringLiteral("wine"));
spec.insert(QStringLiteral("runnerPath"), QStringLiteral("/bin/true"));
spec.insert(QStringLiteral("envOverrides"), QVariantMap{{QStringLiteral("ALAKARTE_TEST_VAR"), QStringLiteral("1")}});
spec.insert(QStringLiteral("extraArgs"), QStringList{QStringLiteral("--profile-arg")});
spec.insert(QStringLiteral("dllOverrides"),
QVariantMap{{QStringLiteral("d3d11"), QStringLiteral("native,builtin")}, {QStringLiteral("dxgi"), QStringLiteral("native,builtin")}});
const QDBusReply<QVariantMap> reply = iface.call(QStringLiteral("SetGameProfile"), spec);
QVERIFY(reply.isValid());
const QVariantMap out = reply.value();
QVERIFY(out.value(QStringLiteral("ok")).toBool());
const QVariantMap profile = unwrapVariantMap(out.value(QStringLiteral("profile")));
QCOMPARE(profile.value(QStringLiteral("gameId")).toString(), gameId);
const QVariantMap env = unwrapVariantMap(profile.value(QStringLiteral("envOverrides")));
QCOMPARE(env.value(QStringLiteral("ALAKARTE_TEST_VAR")).toString(), QStringLiteral("1"));
QCOMPARE(profile.value(QStringLiteral("extraArgs")).toStringList(), QStringList{QStringLiteral("--profile-arg")});
const QVariantMap dll = unwrapVariantMap(profile.value(QStringLiteral("dllOverrides")));
QCOMPARE(dll.value(QStringLiteral("d3d11")).toString(), QStringLiteral("native,builtin"));
QCOMPARE(dll.value(QStringLiteral("dxgi")).toString(), QStringLiteral("native,builtin"));
}
{
const QDBusReply<QVariantMap> reply = iface.call(QStringLiteral("GetGameProfile"), gameId);
QVERIFY(reply.isValid());
const QVariantMap out = reply.value();
QVERIFY(out.value(QStringLiteral("ok")).toBool());
const QVariantMap profile = unwrapVariantMap(out.value(QStringLiteral("profile")));
QCOMPARE(profile.value(QStringLiteral("gameId")).toString(), gameId);
QCOMPARE(profile.value(QStringLiteral("extraArgs")).toStringList(), QStringList{QStringLiteral("--profile-arg")});
const QVariantMap dll = unwrapVariantMap(profile.value(QStringLiteral("dllOverrides")));
QCOMPARE(dll.value(QStringLiteral("d3d11")).toString(), QStringLiteral("native,builtin"));
QCOMPARE(dll.value(QStringLiteral("dxgi")).toString(), QStringLiteral("native,builtin"));
}
{
QVariantMap spec;
spec.insert(QStringLiteral("gameId"), gameId);
spec.insert(QStringLiteral("useGameProfile"), true);
spec.insert(QStringLiteral("program"), QStringLiteral("/bin/true"));
spec.insert(QStringLiteral("args"), QStringList{QStringLiteral("--base-arg")});
spec.insert(QStringLiteral("extraArgs"), QStringList{QStringLiteral("--spec-arg")});
const QDBusReply<QVariantMap> reply = iface.call(QStringLiteral("ResolveLaunch"), spec);
QVERIFY(reply.isValid());
const QVariantMap out = reply.value();
QVERIFY(out.value(QStringLiteral("ok")).toBool());
QCOMPARE(out.value(QStringLiteral("finalProgram")).toString(), QStringLiteral("/bin/true"));
const QStringList expectedFinalArgs = QStringList({
QStringLiteral("/bin/true"),
QStringLiteral("--base-arg"),
QStringLiteral("--profile-arg"),
QStringLiteral("--spec-arg"),
});
QCOMPARE(out.value(QStringLiteral("finalArgs")).toStringList(), expectedFinalArgs);
const QVariantMap env = unwrapVariantMap(out.value(QStringLiteral("effectiveEnv")));
QCOMPARE(env.value(QStringLiteral("ALAKARTE_TEST_VAR")).toString(), QStringLiteral("1"));
QCOMPARE(env.value(QStringLiteral("WINEDLLOVERRIDES")).toString(), QStringLiteral("d3d11=native,builtin;dxgi=native,builtin"));
}
{
const QDBusReply<QVariantList> reply = iface.call(QStringLiteral("ListGameProfiles"));
QVERIFY(reply.isValid());
const QVariantList list = reply.value();
bool found = false;
for (const QVariant &v : list) {
const QVariantMap m = unwrapVariantMap(v);
if (m.value(QStringLiteral("gameId")).toString() == gameId) {
found = true;
break;
}
}
QVERIFY(found);
}
{
const QDBusReply<QVariantMap> reply = iface.call(QStringLiteral("ClearGameProfile"), gameId);
QVERIFY(reply.isValid());
QVERIFY(reply.value().value(QStringLiteral("ok")).toBool());
}
}
int main(int argc, char **argv)
{
QCoreApplication app(argc, argv);
const QStringList args = app.arguments();
const QString gamecenterPath = takeArgValue(args, QStringLiteral("--gamecenter"));
const QString runnerdPath = takeArgValue(args, QStringLiteral("--runnerd"));
const QString inputdPath = takeArgValue(args, QStringLiteral("--inputd"));
app.setProperty("alakarte_test_gamecenter", gamecenterPath);
app.setProperty("alakarte_test_runnerd", runnerdPath);
app.setProperty("alakarte_test_inputd", inputdPath);
QStringList filtered;
filtered.reserve(args.size());
if (!args.isEmpty()) {
filtered.push_back(args.first());
}
for (int i = 1; i < args.size(); ++i) {
const QString a = args.at(i);
if (a == QLatin1String("--gamecenter") || a == QLatin1String("--runnerd")) {
++i;
continue;
}
if (a == QLatin1String("--inputd")) {
++i;
continue;
}
filtered.push_back(a);
}
DbusSmokeTest tc;
return QTest::qExec(&tc, filtered);
}
#include "dbus_smoketest.moc"

View file

@ -0,0 +1,630 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// SPDX-FileCopyrightText: 2026 A-La-Karte Contributors
#include <QCoreApplication>
#include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <QDBusInterface>
#include <QDBusReply>
#include <QDBusVariant>
#include <QDir>
#include <QElapsedTimer>
#include <QFile>
#include <QFileInfo>
#include <QProcess>
#include <QProcessEnvironment>
#include <QStandardPaths>
#include <QStringList>
#include <QTemporaryDir>
#include <QUuid>
#include <QtTest>
#include <signal.h>
#include <sys/types.h>
namespace
{
static QString takeArgValue(const QStringList &args, const QString &key)
{
const int idx = args.indexOf(key);
if (idx < 0) {
return {};
}
if (idx + 1 >= args.size()) {
return {};
}
return args.at(idx + 1);
}
static bool waitForService(QDBusConnection &bus, const QString &service, int timeoutMs)
{
if (!bus.isConnected() || !bus.interface()) {
return false;
}
QElapsedTimer t;
t.start();
while (t.elapsed() < timeoutMs) {
if (bus.interface()->isServiceRegistered(service)) {
return true;
}
QTest::qWait(50);
}
return false;
}
static QByteArray readProcFile(const QString &path, qint64 maxSize = 65536)
{
QFile f(path);
if (!f.open(QIODevice::ReadOnly)) {
return {};
}
return f.read(maxSize);
}
static bool containsNullSeparatedEntry(const QByteArray &blob, const QByteArray &needle)
{
if (blob.isEmpty() || needle.isEmpty()) {
return false;
}
int pos = -1;
while (true) {
pos = blob.indexOf(needle, pos + 1);
if (pos < 0) {
return false;
}
const bool startOk = (pos == 0) || (blob.at(pos - 1) == '\0');
const int endPos = pos + needle.size();
const bool endOk = (endPos == blob.size()) || (blob.at(endPos) == '\0');
if (startOk && endOk) {
return true;
}
}
}
static QList<uint> pidsWithEnvironEntry(const QByteArray &needle)
{
QList<uint> out;
const QDir proc(QStringLiteral("/proc"));
const QStringList entries = proc.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
for (const QString &entry : entries) {
bool ok = false;
const uint pid = entry.toUInt(&ok);
if (!ok || pid <= 1) {
continue;
}
const QByteArray env = readProcFile(QStringLiteral("/proc/%1/environ").arg(pid));
if (containsNullSeparatedEntry(env, needle)) {
out.push_back(pid);
}
}
return out;
}
static bool waitForAnyEnvironEntry(const QByteArray &needle, int timeoutMs)
{
QElapsedTimer t;
t.start();
while (t.elapsed() < timeoutMs) {
if (!pidsWithEnvironEntry(needle).isEmpty()) {
return true;
}
QTest::qWait(50);
}
return !pidsWithEnvironEntry(needle).isEmpty();
}
static bool waitForNoEnvironEntry(const QByteArray &needle, int timeoutMs)
{
QElapsedTimer t;
t.start();
while (t.elapsed() < timeoutMs) {
if (pidsWithEnvironEntry(needle).isEmpty()) {
return true;
}
QTest::qWait(150);
}
return pidsWithEnvironEntry(needle).isEmpty();
}
static void killPids(const QList<uint> &pids)
{
for (uint pid : pids) {
if (pid > 1) {
::kill(static_cast<pid_t>(pid), SIGKILL);
}
}
}
static void stopProcess(QProcess &p)
{
if (p.state() == QProcess::NotRunning) {
return;
}
p.terminate();
if (!p.waitForFinished(3000)) {
p.kill();
p.waitForFinished(3000);
}
}
static bool waitForUnitNotActive(const QString &unitName, int timeoutMs)
{
QDBusConnection bus = QDBusConnection::sessionBus();
if (!bus.isConnected()) {
return false;
}
QDBusInterface manager(QStringLiteral("org.freedesktop.systemd1"),
QStringLiteral("/org/freedesktop/systemd1"),
QStringLiteral("org.freedesktop.systemd1.Manager"),
bus);
if (!manager.isValid()) {
return false;
}
QElapsedTimer t;
t.start();
while (t.elapsed() < timeoutMs) {
const QDBusReply<QDBusObjectPath> unitReply = manager.call(QStringLiteral("GetUnit"), unitName);
if (!unitReply.isValid()) {
if (unitReply.error().name() == QLatin1String("org.freedesktop.systemd1.NoSuchUnit")) {
return true;
}
QTest::qWait(100);
continue;
}
QDBusInterface props(QStringLiteral("org.freedesktop.systemd1"), unitReply.value().path(), QStringLiteral("org.freedesktop.DBus.Properties"), bus);
if (!props.isValid()) {
return true;
}
const QDBusReply<QVariant> stateReply =
props.call(QStringLiteral("Get"), QStringLiteral("org.freedesktop.systemd1.Unit"), QStringLiteral("ActiveState"));
if (!stateReply.isValid()) {
return true;
}
QVariant v = stateReply.value();
if (v.canConvert<QDBusVariant>()) {
v = v.value<QDBusVariant>().variant();
}
const QString state = v.toString();
if (state != QLatin1String("active") && state != QLatin1String("activating") && state != QLatin1String("deactivating")) {
return true;
}
QTest::qWait(100);
}
const QDBusReply<QDBusObjectPath> unitReply = manager.call(QStringLiteral("GetUnit"), unitName);
if (!unitReply.isValid() && unitReply.error().name() == QLatin1String("org.freedesktop.systemd1.NoSuchUnit")) {
return true;
}
return false;
}
}
class StopLaunchingRegressionTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void initTestCase();
void cleanupTestCase();
void stopWhileLaunchingSteam();
void stopByGameIdWhileLaunchingSteam();
void stopWhileLaunchingLutris();
void stopByGameIdWhileLaunchingLutris();
void stopDirectLaunch();
void stopByGameIdDirectLaunch();
private:
QString m_gamecenterPath;
QProcess m_gamecenter;
QString m_testXdgBase;
QTemporaryDir m_stubBin;
QString m_markerValue;
};
void StopLaunchingRegressionTest::initTestCase()
{
m_gamecenterPath = qApp->property("alakarte_test_gamecenter").toString();
if (m_gamecenterPath.isEmpty()) {
const QStringList args = QCoreApplication::arguments();
m_gamecenterPath = takeArgValue(args, QStringLiteral("--gamecenter"));
}
QVERIFY2(!m_gamecenterPath.isEmpty(), "--gamecenter is required");
QDBusConnection bus = QDBusConnection::sessionBus();
if (!bus.isConnected() || !bus.interface()) {
QSKIP("session bus not available");
}
QDBusInterface systemd(QStringLiteral("org.freedesktop.systemd1"),
QStringLiteral("/org/freedesktop/systemd1"),
QStringLiteral("org.freedesktop.systemd1.Manager"),
bus);
if (!systemd.isValid()) {
QSKIP("systemd user manager not available on session bus");
}
if (bus.interface()->isServiceRegistered(QStringLiteral("org.kde.GameCenter1"))) {
QSKIP("org.kde.GameCenter1 already registered on the session bus");
}
if (!m_stubBin.isValid()) {
QSKIP("failed to create temporary directory for stub executables");
}
m_markerValue = QUuid::createUuid().toString(QUuid::WithoutBraces);
const QByteArray markerNeedle = (QStringLiteral("ALAKARTE_TEST_MARKER=") + m_markerValue).toUtf8();
killPids(pidsWithEnvironEntry(markerNeedle));
m_testXdgBase = QDir::tempPath() + QStringLiteral("/alakarte-stop-launching-test-xdg-") + QUuid::createUuid().toString(QUuid::WithoutBraces);
QVERIFY(QDir().mkpath(m_testXdgBase + QStringLiteral("/data")));
QVERIFY(QDir().mkpath(m_testXdgBase + QStringLiteral("/config")));
QVERIFY(QDir().mkpath(m_testXdgBase + QStringLiteral("/cache")));
const QString steamPath = m_stubBin.path() + QStringLiteral("/steam");
{
QFile steam(steamPath);
QVERIFY(steam.open(QIODevice::WriteOnly | QIODevice::Truncate));
const QByteArray script =
"#!/bin/sh\n"
"appid=\"\"\n"
"while [ $# -gt 0 ]; do\n"
" if [ \"$1\" = \"-applaunch\" ] && [ $# -ge 2 ]; then appid=\"$2\"; break; fi\n"
" shift\n"
"done\n"
"if [ -z \"$appid\" ]; then appid=\"123456\"; fi\n"
"marker=\"${ALAKARTE_TEST_MARKER_VALUE}\"\n"
"nohup sh -c \"sleep 1; env SteamAppId=$appid SteamGameId=$appid ALAKARTE_TEST_MARKER=$marker /bin/sleep 60\" >/dev/null 2>&1 &\n"
"exit 0\n";
QVERIFY(steam.write(script) == script.size());
}
QVERIFY(QFile::setPermissions(steamPath,
QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ExeOwner | QFileDevice::ReadGroup | QFileDevice::ExeGroup
| QFileDevice::ReadOther | QFileDevice::ExeOther));
const QString lutrisPath = m_stubBin.path() + QStringLiteral("/lutris");
{
QFile lutris(lutrisPath);
QVERIFY(lutris.open(QIODevice::WriteOnly | QIODevice::Truncate));
const QByteArray script =
"#!/bin/sh\n"
"id=\"\"\n"
"if [ $# -ge 1 ]; then\n"
" id=\"$1\"\n"
"fi\n"
"case \"$id\" in\n"
" lutris:rungameid/*) id=\"${id##*/}\" ;;\n"
" lutris:rungame/*) id=\"${id##*/}\" ;;\n"
"esac\n"
"if [ -z \"$id\" ]; then id=\"test-lutris\"; fi\n"
"marker=\"${ALAKARTE_TEST_MARKER_VALUE}\"\n"
"nohup sh -c \"sleep 1; env LUTRIS_GAME_SLUG=$id ALAKARTE_TEST_MARKER=$marker /bin/sleep 60\" >/dev/null 2>&1 &\n"
"exit 0\n";
QVERIFY(lutris.write(script) == script.size());
}
QVERIFY(QFile::setPermissions(lutrisPath,
QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ExeOwner | QFileDevice::ReadGroup | QFileDevice::ExeGroup
| QFileDevice::ReadOther | QFileDevice::ExeOther));
QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
env.insert(QStringLiteral("PATH"), m_stubBin.path() + QStringLiteral(":") + env.value(QStringLiteral("PATH")));
env.insert(QStringLiteral("ALAKARTE_TEST_MARKER_VALUE"), m_markerValue);
env.insert(QStringLiteral("XDG_DATA_HOME"), m_testXdgBase + QStringLiteral("/data"));
env.insert(QStringLiteral("XDG_CONFIG_HOME"), m_testXdgBase + QStringLiteral("/config"));
env.insert(QStringLiteral("XDG_CACHE_HOME"), m_testXdgBase + QStringLiteral("/cache"));
m_gamecenter.setProcessEnvironment(env);
m_gamecenter.setProgram(m_gamecenterPath);
m_gamecenter.setArguments({});
m_gamecenter.setProcessChannelMode(QProcess::MergedChannels);
m_gamecenter.start();
QVERIFY(m_gamecenter.waitForStarted(5000));
QVERIFY2(waitForService(bus, QStringLiteral("org.kde.GameCenter1"), 5000), "GameCenter1 service did not appear on the session bus");
}
void StopLaunchingRegressionTest::cleanupTestCase()
{
stopProcess(m_gamecenter);
if (!m_markerValue.isEmpty()) {
const QByteArray markerNeedle = (QStringLiteral("ALAKARTE_TEST_MARKER=") + m_markerValue).toUtf8();
killPids(pidsWithEnvironEntry(markerNeedle));
m_markerValue.clear();
}
if (!m_testXdgBase.isEmpty()) {
QDir(m_testXdgBase).removeRecursively();
m_testXdgBase.clear();
}
}
void StopLaunchingRegressionTest::stopWhileLaunchingSteam()
{
QDBusConnection bus = QDBusConnection::sessionBus();
if (!bus.isConnected()) {
QSKIP("session bus not available");
}
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/GameCenter1"), QStringLiteral("org.kde.GameCenter1"), bus);
QVERIFY2(iface.isValid(), "GameCenter1 DBus interface not valid");
const QString appId = QStringLiteral("987654");
QVariantMap spec;
spec.insert(QStringLiteral("provider"), QStringLiteral("steam"));
spec.insert(QStringLiteral("command"), QStringLiteral("steam -applaunch %1").arg(appId));
spec.insert(QStringLiteral("gameId"), QStringLiteral("test-steam-%1").arg(appId));
QDBusReply<QString> launchReply = iface.call(QStringLiteral("Launch"), spec);
QVERIFY2(launchReply.isValid(), qPrintable(launchReply.error().message()));
const QString sessionId = launchReply.value();
QVERIFY2(!sessionId.isEmpty(), "Launch returned empty sessionId");
const QString unitName = QStringLiteral("alakarte-game-%1.scope").arg(sessionId);
QDBusReply<void> stopReply = iface.call(QStringLiteral("Stop"), sessionId);
QVERIFY2(stopReply.isValid(), qPrintable(stopReply.error().message()));
const QByteArray markerNeedle = (QStringLiteral("ALAKARTE_TEST_MARKER=") + m_markerValue).toUtf8();
waitForAnyEnvironEntry(markerNeedle, 5000);
const bool cleaned = waitForNoEnvironEntry(markerNeedle, 15000);
if (!cleaned) {
const QList<uint> pids = pidsWithEnvironEntry(markerNeedle);
killPids(pids);
}
QVERIFY2(cleaned, "marker process still alive after Stop() during Launching");
QVERIFY2(waitForUnitNotActive(unitName, 15000), "systemd unit still active after Stop() during Launching");
}
void StopLaunchingRegressionTest::stopByGameIdWhileLaunchingSteam()
{
QDBusConnection bus = QDBusConnection::sessionBus();
if (!bus.isConnected()) {
QSKIP("session bus not available");
}
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/GameCenter1"), QStringLiteral("org.kde.GameCenter1"), bus);
QVERIFY2(iface.isValid(), "GameCenter1 DBus interface not valid");
const QByteArray markerNeedle = (QStringLiteral("ALAKARTE_TEST_MARKER=") + m_markerValue).toUtf8();
killPids(pidsWithEnvironEntry(markerNeedle));
const QString appId = QStringLiteral("876543");
const QString gameId = QStringLiteral("test-steam-stop-by-gameid-%1").arg(QUuid::createUuid().toString(QUuid::WithoutBraces));
QVariantMap spec;
spec.insert(QStringLiteral("provider"), QStringLiteral("steam"));
spec.insert(QStringLiteral("command"), QStringLiteral("steam -applaunch %1").arg(appId));
spec.insert(QStringLiteral("gameId"), gameId);
QDBusReply<QString> launchReply = iface.call(QStringLiteral("Launch"), spec);
QVERIFY2(launchReply.isValid(), qPrintable(launchReply.error().message()));
const QString sessionId = launchReply.value();
QVERIFY2(!sessionId.isEmpty(), "Launch returned empty sessionId");
const QString unitName = QStringLiteral("alakarte-game-%1.scope").arg(sessionId);
QDBusReply<void> stopReply = iface.call(QStringLiteral("StopByGameId"), gameId);
QVERIFY2(stopReply.isValid(), qPrintable(stopReply.error().message()));
waitForAnyEnvironEntry(markerNeedle, 5000);
const bool cleaned = waitForNoEnvironEntry(markerNeedle, 15000);
if (!cleaned) {
const QList<uint> pids = pidsWithEnvironEntry(markerNeedle);
killPids(pids);
}
QVERIFY2(cleaned, "marker process still alive after StopByGameId() during Launching");
QVERIFY2(waitForUnitNotActive(unitName, 15000), "systemd unit still active after StopByGameId() during Launching");
}
void StopLaunchingRegressionTest::stopWhileLaunchingLutris()
{
QDBusConnection bus = QDBusConnection::sessionBus();
if (!bus.isConnected()) {
QSKIP("session bus not available");
}
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/GameCenter1"), QStringLiteral("org.kde.GameCenter1"), bus);
QVERIFY2(iface.isValid(), "GameCenter1 DBus interface not valid");
const QByteArray markerNeedle = (QStringLiteral("ALAKARTE_TEST_MARKER=") + m_markerValue).toUtf8();
killPids(pidsWithEnvironEntry(markerNeedle));
const QString lutrisId = QStringLiteral("rungameid/%1").arg(QStringLiteral("lutris-test-1"));
QVariantMap spec;
spec.insert(QStringLiteral("provider"), QStringLiteral("lutris"));
spec.insert(QStringLiteral("command"), QStringLiteral("lutris lutris:%1").arg(lutrisId));
spec.insert(QStringLiteral("gameId"), QStringLiteral("test-lutris-%1").arg(QUuid::createUuid().toString(QUuid::WithoutBraces)));
QDBusReply<QString> launchReply = iface.call(QStringLiteral("Launch"), spec);
QVERIFY2(launchReply.isValid(), qPrintable(launchReply.error().message()));
const QString sessionId = launchReply.value();
QVERIFY2(!sessionId.isEmpty(), "Launch returned empty sessionId");
const QString unitName = QStringLiteral("alakarte-game-%1.scope").arg(sessionId);
QDBusReply<void> stopReply = iface.call(QStringLiteral("Stop"), sessionId);
QVERIFY2(stopReply.isValid(), qPrintable(stopReply.error().message()));
waitForAnyEnvironEntry(markerNeedle, 5000);
const bool cleaned = waitForNoEnvironEntry(markerNeedle, 15000);
if (!cleaned) {
killPids(pidsWithEnvironEntry(markerNeedle));
}
QVERIFY2(cleaned, "marker process still alive after Stop() during Launching (lutris)");
QVERIFY2(waitForUnitNotActive(unitName, 15000), "systemd unit still active after Stop() during Launching (lutris)");
}
void StopLaunchingRegressionTest::stopByGameIdWhileLaunchingLutris()
{
QDBusConnection bus = QDBusConnection::sessionBus();
if (!bus.isConnected()) {
QSKIP("session bus not available");
}
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/GameCenter1"), QStringLiteral("org.kde.GameCenter1"), bus);
QVERIFY2(iface.isValid(), "GameCenter1 DBus interface not valid");
const QByteArray markerNeedle = (QStringLiteral("ALAKARTE_TEST_MARKER=") + m_markerValue).toUtf8();
killPids(pidsWithEnvironEntry(markerNeedle));
const QString gameId = QStringLiteral("test-lutris-stop-by-gameid-%1").arg(QUuid::createUuid().toString(QUuid::WithoutBraces));
QVariantMap spec;
spec.insert(QStringLiteral("provider"), QStringLiteral("lutris"));
spec.insert(QStringLiteral("command"), QStringLiteral("lutris lutris:rungameid/%1").arg(QStringLiteral("lutris-test-2")));
spec.insert(QStringLiteral("gameId"), gameId);
QDBusReply<QString> launchReply = iface.call(QStringLiteral("Launch"), spec);
QVERIFY2(launchReply.isValid(), qPrintable(launchReply.error().message()));
const QString sessionId = launchReply.value();
QVERIFY2(!sessionId.isEmpty(), "Launch returned empty sessionId");
const QString unitName = QStringLiteral("alakarte-game-%1.scope").arg(sessionId);
QDBusReply<void> stopReply = iface.call(QStringLiteral("StopByGameId"), gameId);
QVERIFY2(stopReply.isValid(), qPrintable(stopReply.error().message()));
waitForAnyEnvironEntry(markerNeedle, 5000);
const bool cleaned = waitForNoEnvironEntry(markerNeedle, 15000);
if (!cleaned) {
killPids(pidsWithEnvironEntry(markerNeedle));
}
QVERIFY2(cleaned, "marker process still alive after StopByGameId() during Launching (lutris)");
QVERIFY2(waitForUnitNotActive(unitName, 15000), "systemd unit still active after StopByGameId() during Launching (lutris)");
}
void StopLaunchingRegressionTest::stopDirectLaunch()
{
QDBusConnection bus = QDBusConnection::sessionBus();
if (!bus.isConnected()) {
QSKIP("session bus not available");
}
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/GameCenter1"), QStringLiteral("org.kde.GameCenter1"), bus);
QVERIFY2(iface.isValid(), "GameCenter1 DBus interface not valid");
const QByteArray markerNeedle = (QStringLiteral("ALAKARTE_TEST_MARKER=") + m_markerValue).toUtf8();
killPids(pidsWithEnvironEntry(markerNeedle));
const QString gameId = QStringLiteral("test-direct-stop-%1").arg(QUuid::createUuid().toString(QUuid::WithoutBraces));
const QString envExe = QStandardPaths::findExecutable(QStringLiteral("env"));
if (envExe.isEmpty()) {
QSKIP("env executable not found");
}
QVariantMap spec;
spec.insert(QStringLiteral("provider"), QStringLiteral("manual"));
spec.insert(QStringLiteral("program"), envExe);
spec.insert(QStringLiteral("args"),
QStringList{QStringLiteral("ALAKARTE_TEST_MARKER=%1").arg(m_markerValue), QStringLiteral("/bin/sleep"), QStringLiteral("60")});
spec.insert(QStringLiteral("gameId"), gameId);
QDBusReply<QString> launchReply = iface.call(QStringLiteral("Launch"), spec);
QVERIFY2(launchReply.isValid(), qPrintable(launchReply.error().message()));
const QString sessionId = launchReply.value();
QVERIFY2(!sessionId.isEmpty(), "Launch returned empty sessionId");
const QString unitName = QStringLiteral("alakarte-game-%1.scope").arg(sessionId);
QVERIFY2(waitForAnyEnvironEntry(markerNeedle, 5000), "direct launch marker process did not appear");
QDBusReply<void> stopReply = iface.call(QStringLiteral("Stop"), sessionId);
QVERIFY2(stopReply.isValid(), qPrintable(stopReply.error().message()));
const bool cleaned = waitForNoEnvironEntry(markerNeedle, 15000);
if (!cleaned) {
killPids(pidsWithEnvironEntry(markerNeedle));
}
QVERIFY2(cleaned, "marker process still alive after Stop() (direct launch)");
QVERIFY2(waitForUnitNotActive(unitName, 15000), "systemd unit still active after Stop() (direct launch)");
}
void StopLaunchingRegressionTest::stopByGameIdDirectLaunch()
{
QDBusConnection bus = QDBusConnection::sessionBus();
if (!bus.isConnected()) {
QSKIP("session bus not available");
}
QDBusInterface iface(QStringLiteral("org.kde.GameCenter1"), QStringLiteral("/org/kde/GameCenter1"), QStringLiteral("org.kde.GameCenter1"), bus);
QVERIFY2(iface.isValid(), "GameCenter1 DBus interface not valid");
const QByteArray markerNeedle = (QStringLiteral("ALAKARTE_TEST_MARKER=") + m_markerValue).toUtf8();
killPids(pidsWithEnvironEntry(markerNeedle));
const QString gameId = QStringLiteral("test-direct-stop-by-gameid-%1").arg(QUuid::createUuid().toString(QUuid::WithoutBraces));
const QString envExe = QStandardPaths::findExecutable(QStringLiteral("env"));
if (envExe.isEmpty()) {
QSKIP("env executable not found");
}
QVariantMap spec;
spec.insert(QStringLiteral("provider"), QStringLiteral("manual"));
spec.insert(QStringLiteral("program"), envExe);
spec.insert(QStringLiteral("args"),
QStringList{QStringLiteral("ALAKARTE_TEST_MARKER=%1").arg(m_markerValue), QStringLiteral("/bin/sleep"), QStringLiteral("60")});
spec.insert(QStringLiteral("gameId"), gameId);
QDBusReply<QString> launchReply = iface.call(QStringLiteral("Launch"), spec);
QVERIFY2(launchReply.isValid(), qPrintable(launchReply.error().message()));
const QString sessionId = launchReply.value();
QVERIFY2(!sessionId.isEmpty(), "Launch returned empty sessionId");
const QString unitName = QStringLiteral("alakarte-game-%1.scope").arg(sessionId);
QVERIFY2(waitForAnyEnvironEntry(markerNeedle, 5000), "direct launch marker process did not appear");
QDBusReply<void> stopReply = iface.call(QStringLiteral("StopByGameId"), gameId);
QVERIFY2(stopReply.isValid(), qPrintable(stopReply.error().message()));
const bool cleaned = waitForNoEnvironEntry(markerNeedle, 15000);
if (!cleaned) {
killPids(pidsWithEnvironEntry(markerNeedle));
}
QVERIFY2(cleaned, "marker process still alive after StopByGameId() (direct launch)");
QVERIFY2(waitForUnitNotActive(unitName, 15000), "systemd unit still active after StopByGameId() (direct launch)");
}
int main(int argc, char **argv)
{
QCoreApplication app(argc, argv);
const QStringList args = app.arguments();
const QString gamecenterPath = takeArgValue(args, QStringLiteral("--gamecenter"));
app.setProperty("alakarte_test_gamecenter", gamecenterPath);
QStringList filtered;
filtered.reserve(args.size());
if (!args.isEmpty()) {
filtered.push_back(args.first());
}
for (int i = 1; i < args.size(); ++i) {
const QString a = args.at(i);
if (a == QLatin1String("--gamecenter")) {
++i;
continue;
}
filtered.push_back(a);
}
StopLaunchingRegressionTest tc;
return QTest::qExec(&tc, filtered);
}
#include "stop_launching_regression_test.moc"