// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: 2026 A-La-Karte Contributors #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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()) { v = v.value().variant(); } return v; } static QVariantMap unwrapVariantMap(QVariant v) { v = unwrapDbusVariant(v); if (v.metaType() == QMetaType::fromType()) { const QDBusArgument arg = v.value(); return qdbus_cast(arg); } if (v.canConvert()) { return v.toMap(); } return {}; } } class DbusSmokeTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void cleanupTestCase(); void pingGameCenter(); void gameCenterUniqueness(); 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/ALaKarte/GameCenter1"), QStringLiteral("org.kde.GameCenter1"), m_bus); QVERIFY(iface.isValid()); iface.setTimeout(2000); const QDBusReply reply = iface.call(QStringLiteral("Ping")); QVERIFY(reply.isValid()); QCOMPARE(reply.value(), QStringLiteral("ok")); } void DbusSmokeTest::gameCenterUniqueness() { pingGameCenter(); QVERIFY(m_bus.interface()); const QDBusReply ownerBeforeReply = m_bus.interface()->serviceOwner(QStringLiteral("org.kde.GameCenter1")); QVERIFY(ownerBeforeReply.isValid()); const QString ownerBefore = ownerBeforeReply.value(); QVERIFY(!ownerBefore.isEmpty()); QProcess second; startDaemon(second, m_gamecenterPath); QVERIFY(second.waitForStarted(5000)); const bool finished = second.waitForFinished(5000); if (!finished) { stopProcess(second); } QVERIFY(finished); QCOMPARE(second.exitStatus(), QProcess::NormalExit); const QDBusReply ownerAfterReply = m_bus.interface()->serviceOwner(QStringLiteral("org.kde.GameCenter1")); QVERIFY(ownerAfterReply.isValid()); QCOMPARE(ownerAfterReply.value(), ownerBefore); pingGameCenter(); } 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 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 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 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 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 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 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 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 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 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"