File indexing completed on 2024-05-19 16:41:38

0001 /*
0002     SPDX-FileCopyrightText: 2016-2021 Harald Sitter <sitter@kde.org>
0003     SPDX-FileCopyrightText: 2022 Alexander Lohnau <alexander.lohnau@gmx.de>
0004 
0005     SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0006 */
0007 
0008 #include <QDebug>
0009 #include <QDir>
0010 #include <QFile>
0011 #include <QObject>
0012 #include <QStandardPaths>
0013 #include <QTest>
0014 #include <QThread>
0015 
0016 #include <KSycoca>
0017 
0018 #include "../servicerunner.h"
0019 
0020 #include <clocale>
0021 #include <optional>
0022 #include <sys/types.h>
0023 #include <unistd.h>
0024 
0025 class ServiceRunnerTest : public QObject
0026 {
0027     Q_OBJECT
0028 private Q_SLOTS:
0029     void initTestCase();
0030     void cleanupTestCase();
0031 
0032     void testExcutableExactMatch();
0033     void testKonsoleVsYakuakeComment();
0034     void testSystemSettings();
0035     void testSystemSettings2();
0036     void testCategories();
0037     void testJumpListActions();
0038     void testINotifyUsage();
0039     void testSpecialArgs();
0040     void testEnv();
0041 };
0042 
0043 void ServiceRunnerTest::initTestCase()
0044 {
0045     QStandardPaths::setTestModeEnabled(true);
0046 
0047     auto appsPath = QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation);
0048     QDir(appsPath).removeRecursively();
0049     QVERIFY(QDir().mkpath(appsPath));
0050     auto fixtureDir = QDir(QFINDTESTDATA("fixtures"));
0051     const auto infoList = fixtureDir.entryInfoList(QDir::Files);
0052     for (const auto &fileInfo : infoList) {
0053         auto source = fileInfo.absoluteFilePath();
0054         auto target = appsPath + QDir::separator() + fileInfo.fileName();
0055         QVERIFY2(QFile::copy(fileInfo.absoluteFilePath(), target), qPrintable(QStringLiteral("can't copy %1 => %2").arg(source, target)));
0056     }
0057 
0058     setlocale(LC_ALL, "C.utf8");
0059 
0060     KSycoca::self()->ensureCacheValid();
0061 
0062     // Make sure noDisplay behaves consistently WRT OnlyShowIn etc.
0063     QVERIFY(setenv("XDG_CURRENT_DESKTOP", "KDE", 1) == 0);
0064     // NOTE: noDisplay also includes X-KDE-OnlyShowOnQtPlatforms which is a bit harder to fake
0065     //       and not currently under testing anyway.
0066 }
0067 
0068 void ServiceRunnerTest::cleanupTestCase()
0069 {
0070 }
0071 
0072 void ServiceRunnerTest::testExcutableExactMatch()
0073 {
0074     ServiceRunner runner(this, KPluginMetaData(), QVariantList());
0075     Plasma::RunnerContext context;
0076     context.setQuery(QStringLiteral("Virtual Machine Manager ServiceRunnerTest")); // virt-manager.desktop
0077 
0078     runner.match(context);
0079     auto matches = context.matches();
0080     QVERIFY(std::any_of(matches.cbegin(), matches.cend(), [](const Plasma::QueryMatch &match) {
0081         return match.text() == QLatin1String("Virtual Machine Manager ServiceRunnerTest") && match.relevance() == 1;
0082     }));
0083 }
0084 
0085 void ServiceRunnerTest::testKonsoleVsYakuakeComment()
0086 {
0087     // Yakuake has konsole mentioned in comment, should be rated lower.
0088     ServiceRunner runner(this, KPluginMetaData(), QVariantList());
0089     Plasma::RunnerContext context;
0090     context.setQuery(QStringLiteral("kons"));
0091 
0092     runner.match(context);
0093 
0094     bool konsoleFound = false;
0095     bool yakuakeFound = false;
0096     const auto matches = context.matches();
0097     for (const auto &match : matches) {
0098         qDebug() << "matched" << match.text();
0099         if (!match.text().contains(QLatin1String("ServiceRunnerTest"))) {
0100             continue;
0101         }
0102 
0103         if (match.text() == QLatin1String("Konsole ServiceRunnerTest")) {
0104             QCOMPARE(match.relevance(), 0.99);
0105             konsoleFound = true;
0106         } else if (match.text() == QLatin1String("Yakuake ServiceRunnerTest")) {
0107             // Rates lower because it doesn't have it in the name.
0108             QCOMPARE(match.relevance(), 0.59);
0109             yakuakeFound = true;
0110         }
0111     }
0112     QVERIFY(konsoleFound);
0113     QVERIFY(yakuakeFound);
0114 }
0115 
0116 void ServiceRunnerTest::testSystemSettings()
0117 {
0118     // In 5.9.0 'System Settings' suddenly didn't come back as a match for 'settings' anymore.
0119     // System Settings has a noKDE version and a KDE version, if the noKDE version is encountered
0120     // first it will be added to the seen cache, however disqualification of already seen items
0121     // may then also disqualify the KDE version of system settings on account of having already
0122     // seen it. This test makes sure we find the right version.
0123     ServiceRunner runner(this, KPluginMetaData(), QVariantList());
0124     Plasma::RunnerContext context;
0125     context.setQuery(QStringLiteral("settings"));
0126 
0127     runner.match(context);
0128 
0129     bool systemSettingsFound = false;
0130     bool foreignSystemSettingsFound = false;
0131     const auto matches = context.matches();
0132     for (const auto &match : matches) {
0133         qDebug() << "matched" << match.text();
0134         if (match.text() == QLatin1String("System Settings ServiceRunnerTest")) {
0135             systemSettingsFound = true;
0136         }
0137         if (match.text() == QLatin1String("KDE System Settings ServiceRunnerTest")) {
0138             foreignSystemSettingsFound = true;
0139         }
0140     }
0141     QVERIFY(systemSettingsFound);
0142     QVERIFY(!foreignSystemSettingsFound);
0143 }
0144 
0145 void ServiceRunnerTest::testSystemSettings2()
0146 {
0147     ServiceRunner runner(this, KPluginMetaData(), QVariantList());
0148     Plasma::RunnerContext context;
0149     context.setQuery(QStringLiteral("sy"));
0150 
0151     runner.match(context);
0152 
0153     bool systemSettingsFound = false;
0154     bool foreignSystemSettingsFound = false;
0155     const auto matches = context.matches();
0156     for (const auto &match : matches) {
0157         qDebug() << "matched" << match.text();
0158         if (match.text() == QLatin1String("System Settings ServiceRunnerTest")) {
0159             systemSettingsFound = true;
0160         }
0161         if (match.text() == QLatin1String("KDE System Settings ServiceRunnerTest")) {
0162             foreignSystemSettingsFound = true;
0163         }
0164     }
0165     QVERIFY(systemSettingsFound);
0166     QVERIFY(!foreignSystemSettingsFound);
0167 }
0168 
0169 void ServiceRunnerTest::testCategories()
0170 {
0171     ServiceRunner runner(this, KPluginMetaData(), QVariantList());
0172     Plasma::RunnerContext context;
0173 
0174     context.setQuery(QStringLiteral("System"));
0175     runner.match(context);
0176     auto matches = context.matches();
0177     QVERIFY(std::any_of(matches.cbegin(), matches.cend(), [](const Plasma::QueryMatch &match) {
0178         return match.text() == QLatin1String("Konsole ServiceRunnerTest") && match.relevance() == 0.64;
0179     }));
0180 
0181     // Multiple categories, this should still match, but now as relevant
0182     context.setQuery(QStringLiteral("System KDE TerminalEmulator"));
0183     runner.match(context);
0184     matches = context.matches();
0185     QVERIFY(std::any_of(matches.cbegin(), matches.cend(), [](const Plasma::QueryMatch &match) {
0186         return match.text() == QLatin1String("Konsole ServiceRunnerTest") && match.relevance() == 0.44;
0187     }));
0188 
0189     // Multiple categories but at least one doesn't match
0190     context.setQuery(QStringLiteral("System KDE Office"));
0191     runner.match(context);
0192     matches = context.matches();
0193     QVERIFY(std::none_of(matches.cbegin(), matches.cend(), [](const Plasma::QueryMatch &match) {
0194         return match.text() == QLatin1String("Konsole ServiceRunnerTest");
0195     }));
0196 
0197     // Query too short to match any category
0198     context.setQuery(QStringLiteral("Dumm"));
0199     runner.match(context);
0200     matches = context.matches();
0201     QVERIFY(matches.isEmpty());
0202 }
0203 
0204 void ServiceRunnerTest::testJumpListActions()
0205 {
0206     ServiceRunner runner(this, KPluginMetaData(), QVariantList());
0207     Plasma::RunnerContext context;
0208 
0209     context.setQuery(QStringLiteral("open a new window")); // org.kde.konsole.desktop
0210     runner.match(context);
0211     auto matches = context.matches();
0212     QVERIFY(std::any_of(matches.cbegin(), matches.cend(), [](const Plasma::QueryMatch &match) {
0213         return match.text() == QLatin1String("Open a New Window - Konsole ServiceRunnerTest") && match.relevance() == 0.65;
0214     }));
0215 
0216     context.setQuery(QStringLiteral("new window"));
0217     runner.match(context);
0218     matches = context.matches();
0219     QVERIFY(std::any_of(matches.cbegin(), matches.cend(), [](const Plasma::QueryMatch &match) {
0220         return match.text() == QLatin1String("Open a New Window - Konsole ServiceRunnerTest") && match.relevance() == 0.5;
0221     }));
0222 
0223     context.setQuery(QStringLiteral("new windows"));
0224     runner.match(context);
0225     matches = context.matches();
0226     QVERIFY(std::none_of(matches.cbegin(), matches.cend(), [](const Plasma::QueryMatch &match) {
0227         return match.text() == QLatin1String("Open a New Window - Konsole ServiceRunnerTest");
0228     }));
0229 }
0230 
0231 void ServiceRunnerTest::testINotifyUsage()
0232 {
0233     auto inotifyCount = []() -> uint {
0234         uint count = 0;
0235         const QDir procDir(QStringLiteral("/proc/%1/fd").arg(getpid()));
0236         for (const auto &fileInfo : procDir.entryInfoList()) {
0237             if (fileInfo.symLinkTarget().endsWith(QStringLiteral("anon_inode:inotify"))) {
0238                 ++count;
0239             }
0240         }
0241         return count;
0242     };
0243 
0244     const uint originalCount = inotifyCount();
0245 
0246     // We'll run this in a new thread so KDirWatch would be led to create a new thread-local watch instance.
0247     // The expectation here is that this KDW instance is not persistently claiming an inotify instance.
0248     bool inotifyCountCool = false;
0249     auto thread = QThread::create([&] {
0250         ServiceRunner runner(nullptr, KPluginMetaData(), QVariantList());
0251         Plasma::RunnerContext context;
0252         context.setQuery(QStringLiteral("settings"));
0253 
0254         runner.match(context);
0255 
0256         QCOMPARE(inotifyCount(), originalCount);
0257         inotifyCountCool = true;
0258     });
0259     thread->start();
0260     thread->wait();
0261     thread->deleteLater();
0262 
0263     QVERIFY(inotifyCountCool);
0264 }
0265 
0266 void ServiceRunnerTest::testSpecialArgs()
0267 {
0268     ServiceRunner runner(this, KPluginMetaData(), QVariantList());
0269     Plasma::RunnerContext context;
0270 
0271     context.setQuery(QStringLiteral("kpat"));
0272     runner.match(context);
0273     auto matches = context.matches();
0274     QVERIFY(std::any_of(matches.cbegin(), matches.cend(), [](const Plasma::QueryMatch &match) {
0275         // Should have no -qwindowtitle at the end. Because we use DesktopExecParser, we have a "true" as an exec which is available on all systems
0276         return match.id().endsWith(QLatin1String("/bin/true"));
0277     }));
0278 }
0279 
0280 void ServiceRunnerTest::testEnv()
0281 {
0282     ServiceRunner runner(this, KPluginMetaData(), QVariantList());
0283     Plasma::RunnerContext context;
0284 
0285     context.setQuery(QStringLiteral("audacity"));
0286     runner.match(context);
0287     auto matches = context.matches();
0288     QVERIFY(std::any_of(matches.cbegin(), matches.cend(), [](const Plasma::QueryMatch &match) {
0289         // Because we use DesktopExecParser, we have a "true" as an exec which is available on all systems
0290         return match.id().endsWith(QLatin1String("/bin/true"));
0291     }));
0292 }
0293 
0294 QTEST_MAIN(ServiceRunnerTest)
0295 
0296 #include "servicerunnertest.moc"