Files
RedBear-OS/local/recipes/kde/kf6-kservice/source/autotests/kservicetest.cpp
T
2026-04-14 10:51:06 +01:00

576 lines
22 KiB
C++

/*
SPDX-FileCopyrightText: 2006 David Faure <faure@kde.org>
SPDX-FileCopyrightText: 2022 Harald Sitter <sitter@kde.org>
SPDX-License-Identifier: LGPL-2.0-only
*/
#include "kservicetest.h"
#include "setupxdgdirs.h"
#include <locale.h>
#include <QTest>
#include <../src/services/kserviceutil_p.h> // for KServiceUtilPrivate
#include <KConfig>
#include <KConfigGroup>
#include <KDesktopFile>
#include <kapplicationtrader.h>
#include <kbuildsycoca_p.h>
#include <ksycoca.h>
#include <KPluginMetaData>
#include <kservicegroup.h>
#include <QFile>
#include <QSignalSpy>
#include <QStandardPaths>
#include <QThread>
#include <QDebug>
#include <QLoggingCategory>
#include <QMimeDatabase>
QTEST_MAIN(KServiceTest)
extern KSERVICE_EXPORT int ksycoca_ms_between_checks;
static void eraseProfiles()
{
QString profilerc = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1String{"/profilerc"};
if (!profilerc.isEmpty()) {
QFile::remove(profilerc);
}
profilerc = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QLatin1String{"/servicetype_profilerc"};
if (!profilerc.isEmpty()) {
QFile::remove(profilerc);
}
}
void KServiceTest::initTestCase()
{
// Set up a layer in the bin dir so ksycoca finds the Application servicetypes
setupXdgDirs();
QStandardPaths::setTestModeEnabled(true);
// A non-C locale is necessary for some tests.
// This locale must have the following properties:
// - some character other than dot as decimal separator
// If it cannot be set, locale-dependent tests are skipped.
setlocale(LC_ALL, "fr_FR.utf8");
QLocale::setDefault(QLocale(QStringLiteral("fr_FR")));
m_hasNonCLocale = (setlocale(LC_ALL, nullptr) == QByteArray("fr_FR.utf8"));
if (!m_hasNonCLocale) {
qDebug() << "Setting locale to fr_FR.utf8 failed";
}
eraseProfiles();
if (!KSycoca::isAvailable()) {
runKBuildSycoca();
}
// Create some fake services for the tests below, and ensure they are in ksycoca.
bool mustUpdateKSycoca = false;
// org.kde.deleteme.desktop: deleted and recreated by testDeletingService, don't use in other tests
const QString deleteMeApp = QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation) + QLatin1String("/org.kde.deleteme.desktop");
if (!QFile::exists(deleteMeApp)) {
QVERIFY(QDir().mkpath(QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation)));
const QString src = QFINDTESTDATA("org.kde.deleteme.desktop");
QVERIFY(!src.isEmpty());
QVERIFY2(QFile::copy(src, deleteMeApp), qPrintable(deleteMeApp));
qDebug() << "Created" << deleteMeApp;
mustUpdateKSycoca = true;
}
// org.kde.faketestapp.desktop
const QString testApp = QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation) + QLatin1String("/org.kde.faketestapp.desktop");
if (!QFile::exists(testApp)) {
QVERIFY(QDir().mkpath(QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation)));
const QString src = QFINDTESTDATA("org.kde.faketestapp.desktop");
QVERIFY(!src.isEmpty());
QVERIFY2(QFile::copy(src, testApp), qPrintable(testApp));
qDebug() << "Created" << testApp;
mustUpdateKSycoca = true;
}
// otherfakeapp.desktop
const QString otherTestApp = QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation) + QLatin1String("/org.kde.otherfakeapp.desktop");
if (!QFile::exists(otherTestApp)) {
QVERIFY(QDir().mkpath(QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation)));
const QString src = QFINDTESTDATA("org.kde.otherfakeapp.desktop");
QVERIFY(!src.isEmpty());
QVERIFY2(QFile::copy(src, otherTestApp), qPrintable(otherTestApp));
qDebug() << "Created" << otherTestApp;
mustUpdateKSycoca = true;
}
// testnames.desktop
const QString namesTestApp = QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation) + QLatin1String("/org.kde.testnames.desktop");
if (!QFile::exists(namesTestApp)) {
QVERIFY(QDir().mkpath(QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation)));
const QString src = QFINDTESTDATA("org.kde.testnames.desktop");
QVERIFY(!src.isEmpty());
QVERIFY2(QFile::copy(src, namesTestApp), qPrintable(namesTestApp));
qDebug() << "Created" << namesTestApp;
mustUpdateKSycoca = true;
}
if (mustUpdateKSycoca) {
// Update ksycoca in ~/.qttest after creating the above
runKBuildSycoca(true);
}
QVERIFY(KService::serviceByDesktopName(QStringLiteral("org.kde.faketestapp")));
QVERIFY(KService::serviceByDesktopName(QStringLiteral("org.kde.otherfakeapp")));
QVERIFY(KService::serviceByDesktopName(QStringLiteral("org.kde.testnames")));
}
void KServiceTest::runKBuildSycoca(bool noincremental)
{
QSignalSpy spy(KSycoca::self(), &KSycoca::databaseChanged);
KBuildSycoca builder;
QVERIFY(builder.recreate(!noincremental));
if (spy.isEmpty()) {
qDebug() << "waiting for signal";
QVERIFY(spy.wait(10000));
qDebug() << "got signal";
}
}
void KServiceTest::cleanupTestCase()
{
KBuildSycoca builder;
builder.recreate();
}
void KServiceTest::testByName()
{
if (!KSycoca::isAvailable()) {
QSKIP("ksycoca not available");
}
const QString filePath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("applications/org.kde.faketestapp.desktop"));
KService::Ptr myService = KService::serviceByDesktopPath(filePath);
QVERIFY(myService);
QCOMPARE(myService->name(), QStringLiteral("Konsole"));
}
void KServiceTest::testConstructorFullPath()
{
const QString filePath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("applications/org.kde.faketestapp.desktop"));
QVERIFY(QFile::exists(filePath));
KService service(filePath);
QVERIFY(service.isValid());
QCOMPARE(service.name(), QStringLiteral("Konsole"));
}
void KServiceTest::testConstructorKDesktopFile() // as happens inside kbuildsycoca.cpp
{
const QString filePath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("applications/org.kde.faketestapp.desktop"));
KDesktopFile desktopFile(filePath);
QCOMPARE(KService(&desktopFile, filePath).name(), QStringLiteral("Konsole"));
}
void KServiceTest::testCopyConstructor()
{
const QString filePath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("applications/org.kde.faketestapp.desktop"));
QVERIFY(QFile::exists(filePath));
KDesktopFile desktopFile(filePath);
// Test making a copy of a KService that will go out of scope
KService::Ptr service;
{
KService origService(&desktopFile);
service = new KService(origService);
}
QVERIFY(service->isValid());
QCOMPARE(service->name(), QStringLiteral("Konsole"));
}
void KServiceTest::testCopyInvalidService()
{
KService::Ptr service;
{
KService origService{QString()}; // this still sets a d_ptr, so no problem;
QVERIFY(!origService.isValid());
service = new KService(origService);
}
QVERIFY(!service->isValid());
}
void KServiceTest::testProperty()
{
ksycoca_ms_between_checks = 0;
// Let's try creating a desktop file and ensuring it's noticed by the timestamp check
QTest::qWait(1000);
QString fakeAppPath = KService::newServicePath(false, QStringLiteral("org.kde.foo"));
if (!QFile::exists(fakeAppPath)) {
KDesktopFile file(fakeAppPath);
KConfigGroup group = file.desktopGroup();
group.writeEntry("Name", "Foo");
group.writeEntry("Type", "Application");
group.writeEntry("X-Flatpak-RenamedFrom", "foo;bar;");
group.writeEntry("Exec", "bla");
group.writeEntry("X-GNOME-UsesNotifications", true);
qDebug() << "created" << fakeAppPath;
}
KService::Ptr fakeApp = KService::serviceByDesktopName(QStringLiteral("org.kde.foo"));
QVERIFY(fakeApp);
QStringList expectedRename{QStringLiteral("foo"), QStringLiteral("bar")};
QCOMPARE(fakeApp->property<QStringList>(QStringLiteral("X-Flatpak-RenamedFrom")), expectedRename);
QCOMPARE(fakeApp->property<bool>(QStringLiteral("X-GNOME-UsesNotifications")), true);
QVERIFY(!fakeApp->property<QString>(QStringLiteral("Name")).isEmpty());
QVERIFY(fakeApp->property<QString>(QStringLiteral("Name[fr]")).isEmpty());
// Restore value
ksycoca_ms_between_checks = 1500;
}
void KServiceTest::testAllServices()
{
if (!KSycoca::isAvailable()) {
QSKIP("ksycoca not available");
}
const KService::List lst = KService::allServices();
QVERIFY(!lst.isEmpty());
bool foundTestApp = false;
for (const KService::Ptr &service : lst) {
QVERIFY(service->isType(KST_KService));
const QString name = service->name();
const QString entryPath = service->entryPath();
if (entryPath.contains(QLatin1String{"fake"})) {
qDebug() << name << "entryPath=" << entryPath << "menuId=" << service->menuId();
}
QVERIFY(!name.isEmpty());
QVERIFY(!entryPath.isEmpty());
KService::Ptr lookedupService = KService::serviceByDesktopPath(entryPath);
QVERIFY(lookedupService); // not null
QCOMPARE(lookedupService->entryPath(), entryPath);
if (service->isApplication()) {
const QString menuId = service->menuId();
if (menuId.isEmpty()) {
qWarning("%s has an empty menuId!", qPrintable(entryPath));
} else if (menuId == QLatin1String{"org.kde.faketestapp.desktop"}) {
foundTestApp = true;
}
QVERIFY(!menuId.isEmpty());
lookedupService = KService::serviceByMenuId(menuId);
QVERIFY(lookedupService); // not null
QCOMPARE(lookedupService->menuId(), menuId);
}
}
QVERIFY(foundTestApp);
}
void KServiceTest::testByStorageId()
{
if (!KSycoca::isAvailable()) {
QSKIP("ksycoca not available");
}
QVERIFY(!QStandardPaths::locate(QStandardPaths::ApplicationsLocation, QStringLiteral("org.kde.faketestapp.desktop")).isEmpty());
QVERIFY(KService::serviceByMenuId(QStringLiteral("org.kde.faketestapp.desktop")));
QVERIFY(!KService::serviceByMenuId(QStringLiteral("org.kde.faketestapp"))); // doesn't work, extension mandatory
QVERIFY(!KService::serviceByMenuId(QStringLiteral("faketestapp.desktop"))); // doesn't work, full filename mandatory
QVERIFY(KService::serviceByStorageId(QStringLiteral("org.kde.faketestapp.desktop")));
QVERIFY(KService::serviceByStorageId(QStringLiteral("org.kde.faketestapp")));
QVERIFY(KService::serviceByDesktopName(QStringLiteral("org.kde.faketestapp")));
QCOMPARE(KService::serviceByDesktopName(QStringLiteral("org.kde.faketestapp"))->menuId(), QStringLiteral("org.kde.faketestapp.desktop"));
}
void KServiceTest::testSubseqConstraints()
{
auto test = [](const char *pattern, const char *text, bool sensitive) {
return KApplicationTrader::isSubsequence(QString::fromLatin1(pattern), QString::fromLatin1(text), sensitive ? Qt::CaseSensitive : Qt::CaseInsensitive);
};
// Case Sensitive
QVERIFY2(!test("", "", 1), "both empty");
QVERIFY2(!test("", "something", 1), "empty pattern");
QVERIFY2(!test("something", "", 1), "empty text");
QVERIFY2(test("lngfile", "somereallylongfile", 1), "match ending");
QVERIFY2(test("somelong", "somereallylongfile", 1), "match beginning");
QVERIFY2(test("reallylong", "somereallylongfile", 1), "match middle");
QVERIFY2(test("across", "a 23 c @#! r o01 o 5 s_s", 1), "match across");
QVERIFY2(!test("nocigar", "soclosebutnociga", 1), "close but no match");
QVERIFY2(!test("god", "dog", 1), "incorrect letter order");
QVERIFY2(!test("mismatch", "mIsMaTcH", 1), "case sensitive mismatch");
// Case insensitive
QVERIFY2(test("mismatch", "mIsMaTcH", 0), "case insensitive match");
QVERIFY2(test("tryhards", "Try Your Hardest", 0), "uppercase text");
QVERIFY2(test("TRYHARDS", "try your hardest", 0), "uppercase pattern");
}
void KServiceTest::testActionsAndDataStream()
{
KService::Ptr service = KService::serviceByStorageId(QStringLiteral("org.kde.faketestapp.desktop"));
QVERIFY(service);
QVERIFY(service->property<QString>(QStringLiteral("Name[fr]")).isEmpty());
const QList<KServiceAction> actions = service->actions();
QCOMPARE(actions.count(), 2); // NewWindow, NewTab
const KServiceAction newTabAction = actions.at(1);
QCOMPARE(newTabAction.name(), QStringLiteral("NewTab"));
QCOMPARE(newTabAction.exec(), QStringLiteral("konsole --new-tab"));
QCOMPARE(newTabAction.icon(), QStringLiteral("tab-new"));
QCOMPARE(newTabAction.noDisplay(), false);
QVERIFY(!newTabAction.isSeparator());
QCOMPARE(newTabAction.service()->name(), service->name());
}
void KServiceTest::testServiceGroups()
{
KServiceGroup::Ptr root = KServiceGroup::root();
QVERIFY(root);
qDebug() << root->groupEntries().count();
KServiceGroup::Ptr group = root;
QVERIFY(group);
const KServiceGroup::List list = group->entries(true, // sorted
true, // exclude no display entries,
false, // allow separators
true); // sort by generic name
qDebug() << list.count();
for (KServiceGroup::SPtr s : list) {
qDebug() << s->name() << s->entryPath();
}
// No unit test here yet, but at least this can be valgrinded for errors.
}
void KServiceTest::testDeletingService()
{
// workaround unexplained inotify issue (in CI only...)
QTest::qWait(1000);
const QString serviceName = QStringLiteral("org.kde.deleteme");
KService::Ptr fakeService = KService::serviceByDesktopName(serviceName);
QVERIFY(fakeService); // see initTestCase; it should be found.
// Test deleting a service
const QString servPath = fakeService->locateLocal();
QVERIFY(QFile::exists(servPath));
QFile::remove(servPath);
runKBuildSycoca();
ksycoca_ms_between_checks = 0; // need it to check the ksycoca mtime
QVERIFY(!KService::serviceByDesktopName(serviceName)); // not in ksycoca anymore
// Restore value
ksycoca_ms_between_checks = 1500;
QVERIFY(fakeService); // the whole point of refcounting is that this KService instance is still valid.
QVERIFY(!QFile::exists(servPath));
// Recreate it, for future tests
const QString deleteMeApp = QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation) + QLatin1String("/org.kde.deleteme.desktop");
const QString src = QFINDTESTDATA("org.kde.deleteme.desktop");
QVERIFY2(QFile::copy(src, deleteMeApp), qPrintable(deleteMeApp));
runKBuildSycoca();
if (QThread::currentThread() != QCoreApplication::instance()->thread()) {
m_sycocaUpdateDone.ref();
}
}
#include <QFutureSynchronizer>
#include <QThreadPool>
#include <QtConcurrentRun>
// Testing for concurrent access to ksycoca from multiple threads
// It's especially interesting to run this test as ./kservicetest testThreads
// so that even the ksycoca initialization is happening from N threads at the same time.
// Use valgrind --tool=helgrind to see the race conditions.
void KServiceTest::testReaderThreads()
{
QThreadPool::globalInstance()->setMaxThreadCount(10);
QFutureSynchronizer<void> sync;
sync.addFuture(QtConcurrent::run(&KServiceTest::testAllServices, this));
sync.addFuture(QtConcurrent::run(&KServiceTest::testAllServices, this));
sync.addFuture(QtConcurrent::run(&KServiceTest::testAllServices, this));
sync.addFuture(QtConcurrent::run(&KServiceTest::testAllServices, this));
sync.addFuture(QtConcurrent::run(&KServiceTest::testAllServices, this));
sync.waitForFinished();
QThreadPool::globalInstance()->setMaxThreadCount(1); // delete those threads
}
void KServiceTest::testThreads()
{
QThreadPool::globalInstance()->setMaxThreadCount(10);
QFutureSynchronizer<void> sync;
sync.addFuture(QtConcurrent::run(&KServiceTest::testAllServices, this));
sync.addFuture(QtConcurrent::run(&KServiceTest::testDeletingService, this));
// process events (DBus, inotify...), until we get all expected signals
QTRY_COMPARE_WITH_TIMEOUT(m_sycocaUpdateDone.loadRelaxed(), 1, 15000); // not using a bool, just to silence helgrind
qDebug() << "Joining all threads";
sync.waitForFinished();
}
void KServiceTest::testCompleteBaseName()
{
// dots in filename before .desktop extension:
QCOMPARE(KServiceUtilPrivate::completeBaseName(QStringLiteral("/home/x/.local/share/applications/org.kde.fakeapp.desktop")),
QStringLiteral("org.kde.fakeapp"));
}
void KServiceTest::testEntryPathToName()
{
QCOMPARE(KService(QStringLiteral("c.desktop")).name(), QStringLiteral("c"));
QCOMPARE(KService(QStringLiteral("a.b.c.desktop")).name(), QStringLiteral("a.b.c")); // dots in filename before .desktop extension
QCOMPARE(KService(QStringLiteral("/hallo/a.b.c.desktop")).name(), QStringLiteral("a.b.c"));
}
void KServiceTest::testAliasFor()
{
if (!KSycoca::isAvailable()) {
QSKIP("ksycoca not available");
}
KService::Ptr testapp = KService::serviceByDesktopName(QStringLiteral("org.kde.faketestapp"));
QVERIFY(testapp);
QCOMPARE(testapp->aliasFor(), QStringLiteral("org.kde.okular"));
}
void KServiceTest::testMimeType()
{
if (!KSycoca::isAvailable()) {
QSKIP("ksycoca not available");
}
KService::Ptr testapp = KService::serviceByDesktopName(QStringLiteral("org.kde.otherfakeapp"));
QVERIFY(testapp);
QCOMPARE(testapp->mimeTypes(), {QStringLiteral("application/pdf")});
}
void KServiceTest::testProtocols()
{
if (!KSycoca::isAvailable()) {
QSKIP("ksycoca not available");
}
KService::Ptr testapp = KService::serviceByDesktopName(QStringLiteral("org.kde.otherfakeapp"));
QVERIFY(testapp);
QStringList expectedProtocols{QStringLiteral("http"), QStringLiteral("tel")};
QCOMPARE(testapp->supportedProtocols(), expectedProtocols);
}
void KServiceTest::testServiceActionService()
{
if (!KSycoca::isAvailable()) {
QSKIP("ksycoca not available");
}
const QString filePath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("applications/org.kde.faketestapp.desktop"));
QVERIFY(QFile::exists(filePath));
KService service(filePath);
QVERIFY(service.isValid());
const KServiceAction action = service.actions().first();
QCOMPARE(action.service()->property<bool>(QStringLiteral("DBusActivatable")), true);
QCOMPARE(action.service()->actions().size(), 2);
}
void KServiceTest::testUntranslatedNames()
{
const QString name = QStringLiteral("Name");
const QString genericName = QStringLiteral("GenericName");
QLatin1String translationPostfix(" trans");
KService::Ptr app = KService::serviceByDesktopName(QStringLiteral("org.kde.testnames"));
QVERIFY(app);
QVERIFY(app->isValid());
QCOMPARE(app->untranslatedName(), name);
QCOMPARE(app->untranslatedGenericName(), genericName);
QCOMPARE(app->name(), name + translationPostfix);
QCOMPARE(app->genericName(), genericName + translationPostfix);
// Property access
QCOMPARE(app->property<QString>(QStringLiteral("UntranslatedName")), name);
QCOMPARE(app->property<QString>(QStringLiteral("UntranslatedGenericName")), genericName);
}
void KServiceTest::testStartupNotify()
{
{
QString fakeAppPath = KService::newServicePath(false, QStringLiteral("org.kde.startupnotify1"));
KDesktopFile file(fakeAppPath);
KConfigGroup group = file.desktopGroup();
group.writeEntry("Name", "Foo");
group.writeEntry("Type", "Application");
group.writeEntry("StartupNotify", true);
group.sync();
KService fakeApp(fakeAppPath);
QVERIFY(fakeApp.isValid());
QVERIFY(fakeApp.startupNotify().value());
}
{
QString fakeAppPath = KService::newServicePath(false, QStringLiteral("org.kde.startupnotify2"));
KDesktopFile file(fakeAppPath);
KConfigGroup group = file.desktopGroup();
group.writeEntry("Name", "Foo");
group.writeEntry("Type", "Application");
group.writeEntry("StartupNotify", false);
group.sync();
KService fakeApp(fakeAppPath);
QVERIFY(fakeApp.isValid());
QVERIFY(!fakeApp.startupNotify().value());
}
{
QString fakeAppPath = KService::newServicePath(false, QStringLiteral("org.kde.startupnotify1"));
KDesktopFile file(fakeAppPath);
KConfigGroup group = file.desktopGroup();
group.writeEntry("Name", "Foo");
group.writeEntry("Type", "Application");
group.writeEntry("X-KDE-StartupNotify", true);
group.sync();
KService fakeApp(fakeAppPath);
QVERIFY(fakeApp.isValid());
QVERIFY(fakeApp.startupNotify().value());
}
{
QString fakeAppPath = KService::newServicePath(false, QStringLiteral("org.kde.startupnotify2"));
KDesktopFile file(fakeAppPath);
KConfigGroup group = file.desktopGroup();
group.writeEntry("Name", "Foo");
group.writeEntry("Type", "Application");
group.writeEntry("X-KDE-StartupNotify", false);
group.sync();
KService fakeApp(fakeAppPath);
QVERIFY(fakeApp.isValid());
QVERIFY(!fakeApp.startupNotify().value());
}
{
QString fakeAppPath = KService::newServicePath(false, QStringLiteral("org.kde.startupnotify3"));
KDesktopFile file(fakeAppPath);
KConfigGroup group = file.desktopGroup();
group.writeEntry("Name", "Foo");
group.writeEntry("Type", "Application");
group.sync();
KService fakeApp(fakeAppPath);
QVERIFY(fakeApp.isValid());
QVERIFY(!fakeApp.startupNotify().has_value());
}
}
#include "moc_kservicetest.cpp"