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

370 lines
14 KiB
C++

/*
SPDX-FileCopyrightText: 2006-2020 David Faure <faure@kde.org>
SPDX-License-Identifier: LGPL-2.0-or-later
*/
#include "setupxdgdirs.h"
#include <locale.h>
#include <QTest>
#include <KConfig>
#include <KConfigGroup>
#include <KDesktopFile>
#include <kbuildsycoca_p.h>
#include <ksycoca.h>
#include <kapplicationtrader.h>
#include <kservicegroup.h>
#include <QFile>
#include <QSignalSpy>
#include <QStandardPaths>
#include <QThread>
#include <QDebug>
#include <QLoggingCategory>
#include <QMimeDatabase>
enum class ExpectedResult {
NoResults,
FakeApplicationOnly,
FakeSchemeHandlerOnly,
FakeApplicationAndOthers,
NotFakeApplication,
};
Q_DECLARE_METATYPE(ExpectedResult)
class KApplicationTraderTest : public QObject
{
Q_OBJECT
public:
private Q_SLOTS:
void initTestCase();
void testTraderConstraints_data();
void testTraderConstraints();
void testQueryByMimeType();
void testThreads();
void testTraderQueryMustRebuildSycoca();
void testSetPreferredService();
void cleanupTestCase();
private:
QString createFakeApplication(const QString &filename, const QString &name, const QMap<QString, QString> &extraFields = {});
void checkResult(const KService::List &offers, ExpectedResult expectedResult);
QString m_fakeApplication;
QString m_fakeSchemeHandler;
QString m_fakeGnomeApplication;
QStringList m_createdDesktopFiles;
};
QTEST_MAIN(KApplicationTraderTest)
extern KSERVICE_EXPORT int ksycoca_ms_between_checks;
void KApplicationTraderTest::initTestCase()
{
// Set up a layer in the bin dir so ksycoca finds the desktop files created by createFakeApplication
// Note that we still need /usr in there so that mimetypes are found
setupXdgDirs();
qputenv("XDG_CURRENT_DESKTOP", "KDE");
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");
bool hasNonCLocale = (setlocale(LC_ALL, nullptr) == QByteArray("fr_FR.utf8"));
if (!hasNonCLocale) {
qDebug() << "Setting locale to fr_FR.utf8 failed";
}
// Ensure no leftovers from other tests
QDir(QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation)).removeRecursively();
// Create some fake services for the tests below, and ensure they are in ksycoca.
// fakeservice_deleteme: deleted and recreated by testDeletingService, don't use in other tests
createFakeApplication(QStringLiteral("fakeservice_deleteme.desktop"), QStringLiteral("DeleteMe"));
// fakeapplication
m_fakeApplication = createFakeApplication(QStringLiteral("fakeapplication.desktop"), QStringLiteral("FakeApplication"));
m_fakeApplication = QFileInfo(m_fakeApplication).canonicalFilePath();
m_fakeSchemeHandler = createFakeApplication(QStringLiteral("fakeschemehandler.desktop"),
QStringLiteral("FakeSchemeHandler"),
{{QStringLiteral("MimeType"), QStringLiteral("text/plain;x-scheme-handler/someprotocol")}});
m_fakeSchemeHandler = QFileInfo(m_fakeSchemeHandler).canonicalFilePath();
// fakegnomeapplication (do not show in Plasma). Should never be returned. To test the filtering code in queryByMimeType.
QMap<QString, QString> fields;
fields.insert(QStringLiteral("OnlyShowIn"), QStringLiteral("Gnome"));
m_fakeGnomeApplication = createFakeApplication(QStringLiteral("fakegnomeapplication.desktop"), QStringLiteral("FakeApplicationGnome"), fields);
m_fakeGnomeApplication = QFileInfo(m_fakeGnomeApplication).canonicalFilePath();
ksycoca_ms_between_checks = 0;
}
void KApplicationTraderTest::cleanupTestCase()
{
for (const QString &file : std::as_const(m_createdDesktopFiles)) {
QFile::remove(file);
}
}
// Helper method for all the trader tests
static bool offerListHasService(const KService::List &offers, const QString &entryPath)
{
bool found = false;
for (const auto &service : offers) {
if (service->entryPath() == entryPath) {
if (found) { // should be there only once
qWarning("ERROR: %s was found twice in the list", qPrintable(entryPath));
return false; // make test fail
}
found = true;
}
}
return found;
}
void KApplicationTraderTest::checkResult(const KService::List &offers, ExpectedResult expectedResult)
{
switch (expectedResult) {
case ExpectedResult::NoResults:
if (!offers.isEmpty()) {
qWarning() << "Got" << offers.count() << "unexpected results, including" << offers.at(0)->entryPath();
}
QCOMPARE(offers.count(), 0);
break;
case ExpectedResult::FakeApplicationOnly:
if (offers.count() != 1) {
for (const auto &service : offers) {
qWarning() << " " << service->entryPath();
}
}
QCOMPARE(offers.count(), 1);
QCOMPARE(offers.at(0)->entryPath(), m_fakeApplication);
break;
case ExpectedResult::FakeSchemeHandlerOnly:
if (offers.count() != 1) {
for (const auto &service : offers) {
qWarning() << " " << service->entryPath();
}
}
QCOMPARE(offers.count(), 1);
QCOMPARE(offers.at(0)->entryPath(), m_fakeSchemeHandler);
break;
case ExpectedResult::FakeApplicationAndOthers:
QVERIFY(!offers.isEmpty());
if (!offerListHasService(offers, m_fakeApplication)) {
qWarning() << m_fakeApplication << "not found. Here's what we have:";
for (const auto &service : offers) {
qWarning() << " " << service->entryPath();
}
}
QVERIFY(offerListHasService(offers, m_fakeApplication));
break;
case ExpectedResult::NotFakeApplication:
QVERIFY(!offerListHasService(offers, m_fakeApplication));
break;
}
QVERIFY(!offerListHasService(offers, m_fakeGnomeApplication));
}
using FF = KApplicationTrader::FilterFunc;
Q_DECLARE_METATYPE(KApplicationTrader::FilterFunc)
void KApplicationTraderTest::testTraderConstraints_data()
{
QTest::addColumn<KApplicationTrader::FilterFunc>("filterFunc");
QTest::addColumn<ExpectedResult>("expectedResult");
QTest::newRow("no_constraint") << FF([](const KService::Ptr &) {
return true;
}) << ExpectedResult::FakeApplicationAndOthers;
// == tests
FF name_comparison = [](const KService::Ptr &serv) {
return serv->name() == QLatin1String("FakeApplication");
};
QTest::newRow("name_comparison") << name_comparison << ExpectedResult::FakeApplicationOnly;
FF isDontExist = [](const KService::Ptr &serv) {
return serv->name() == QLatin1String("IDontExist");
};
QTest::newRow("no_such_name") << isDontExist << ExpectedResult::NoResults;
FF no_such_name_by_case = [](const KService::Ptr &serv) {
return serv->name() == QLatin1String("fakeapplication");
};
QTest::newRow("no_such_name_by_case") << no_such_name_by_case << ExpectedResult::NoResults;
// Name =~ 'fAkEaPPlicaTion'
FF match_case_insensitive = [](const KService::Ptr &serv) {
return serv->name().compare(QLatin1String{"fAkEaPPlicaTion"}, Qt::CaseInsensitive) == 0;
};
QTest::newRow("match_case_insensitive") << match_case_insensitive << ExpectedResult::FakeApplicationOnly;
// 'FakeApp' ~ Name
FF is_contained_in = [](const KService::Ptr &serv) {
return serv->name().contains(QLatin1String{"FakeApp"});
};
QTest::newRow("is_contained_in") << is_contained_in << ExpectedResult::FakeApplicationOnly;
// 'FakeApplicationNot' ~ Name
FF is_contained_in_fail = [](const KService::Ptr &serv) {
return serv->name().contains(QLatin1String{"FakeApplicationNot"});
};
QTest::newRow("is_contained_in_fail") << is_contained_in_fail << ExpectedResult::NoResults;
// 'faKeApP' ~~ Name
FF is_contained_in_case_insensitive = [](const KService::Ptr &serv) {
return serv->name().contains(QLatin1String{"faKeApP"}, Qt::CaseInsensitive);
};
QTest::newRow("is_contained_in_case_insensitive") << is_contained_in_case_insensitive << ExpectedResult::FakeApplicationOnly;
// 'faKeApPp' ~ Name
FF is_contained_in_case_in_fail = [](const KService::Ptr &serv) {
return serv->name().contains(QLatin1String{"faKeApPp"}, Qt::CaseInsensitive);
};
QTest::newRow("is_contained_in_case_in_fail") << is_contained_in_case_in_fail << ExpectedResult::NoResults;
// 'FkApli' subseq Name
FF subseq = [](const KService::Ptr &serv) {
return KApplicationTrader::isSubsequence(QStringLiteral("FkApli"), serv->name());
};
QTest::newRow("subseq") << subseq << ExpectedResult::FakeApplicationOnly;
// 'fkApli' subseq Name
FF subseq_fail = [](const KService::Ptr &serv) {
return KApplicationTrader::isSubsequence(QStringLiteral("fkApli"), serv->name());
};
QTest::newRow("subseq_fail") << subseq_fail << ExpectedResult::NoResults;
// 'fkApLI' ~subseq Name
FF subseq_case_insensitive = [](const KService::Ptr &serv) {
return KApplicationTrader::isSubsequence(QStringLiteral("fkApLI"), serv->name(), Qt::CaseInsensitive);
};
QTest::newRow("subseq_case_insensitive") << subseq_case_insensitive << ExpectedResult::FakeApplicationOnly;
// 'fk_Apli' ~subseq Name
FF subseq_case_insensitive_fail = [](const KService::Ptr &serv) {
return KApplicationTrader::isSubsequence(QStringLiteral("fk_Apli"), serv->name(), Qt::CaseInsensitive);
};
QTest::newRow("subseq_case_insensitive_fail") << subseq_case_insensitive_fail << ExpectedResult::NoResults;
// Test another property, parsed as a double
FF testVersion = [](const KService::Ptr &serv) {
double d = serv->property<double>(QStringLiteral("X-KDE-Version"));
return d > 5.559 && d < 5.561;
};
QTest::newRow("float_parsing") << testVersion << ExpectedResult::FakeApplicationAndOthers;
}
void KApplicationTraderTest::testTraderConstraints()
{
QFETCH(KApplicationTrader::FilterFunc, filterFunc);
QFETCH(ExpectedResult, expectedResult);
const KService::List offers = KApplicationTrader::query(filterFunc);
checkResult(offers, expectedResult);
}
void KApplicationTraderTest::testQueryByMimeType()
{
KService::List offers;
// Without constraint
offers = KApplicationTrader::queryByMimeType(QStringLiteral("text/plain"));
checkResult(offers, ExpectedResult::FakeApplicationAndOthers);
offers = KApplicationTrader::queryByMimeType(QStringLiteral("image/png"));
checkResult(offers, ExpectedResult::NotFakeApplication);
offers = KApplicationTrader::queryByMimeType(QStringLiteral("x-scheme-handler/someprotocol"));
checkResult(offers, ExpectedResult::FakeSchemeHandlerOnly);
QTest::ignoreMessage(QtWarningMsg, "KApplicationTrader: mimeType \"no/such/mimetype\" not found");
offers = KApplicationTrader::queryByMimeType(QStringLiteral("no/such/mimetype"));
checkResult(offers, ExpectedResult::NoResults);
// With constraint
FF isFakeApplication = [](const KService::Ptr &serv) {
return serv->name() == QLatin1String("FakeApplication");
};
offers = KApplicationTrader::queryByMimeType(QStringLiteral("text/plain"), isFakeApplication);
checkResult(offers, ExpectedResult::FakeApplicationOnly);
FF isDontExist = [](const KService::Ptr &serv) {
return serv->name() == QLatin1String("IDontExist");
};
offers = KApplicationTrader::queryByMimeType(QStringLiteral("text/plain"), isDontExist);
checkResult(offers, ExpectedResult::NoResults);
}
QString KApplicationTraderTest::createFakeApplication(const QString &filename, const QString &name, const QMap<QString, QString> &extraFields)
{
const QString fakeService = QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation) + QLatin1Char('/') + filename;
QFile::remove(fakeService);
m_createdDesktopFiles << fakeService;
KDesktopFile file(fakeService);
KConfigGroup group = file.desktopGroup();
group.writeEntry("Name", name);
group.writeEntry("Type", "Application");
group.writeEntry("Exec", "ls");
group.writeEntry("Categories", "FakeCategory");
group.writeEntry("X-KDE-Version", "5.56");
group.writeEntry("MimeType", "text/plain;");
for (auto it = extraFields.begin(); it != extraFields.end(); ++it) {
group.writeEntry(it.key(), it.value());
}
return fakeService;
}
#include <QFutureSynchronizer>
#include <QThreadPool>
#include <QtConcurrentRun>
// Testing for concurrent access to ksycoca from multiple threads
// Use thread-sanitizer to see the data races
void KApplicationTraderTest::testThreads()
{
QThreadPool::globalInstance()->setMaxThreadCount(10);
QFutureSynchronizer<void> sync;
// Can't use data-driven tests here, QTestLib isn't threadsafe.
sync.addFuture(QtConcurrent::run(&KApplicationTraderTest::testQueryByMimeType, this));
sync.addFuture(QtConcurrent::run(&KApplicationTraderTest::testQueryByMimeType, this));
sync.waitForFinished();
}
void KApplicationTraderTest::testTraderQueryMustRebuildSycoca()
{
auto filter = [](const KService::Ptr &serv) {
return serv->name() == QLatin1String("MustRebuild");
};
QCOMPARE(KApplicationTrader::query(filter).count(), 0);
createFakeApplication(QStringLiteral("fakeservice_querymustrebuild.desktop"), QStringLiteral("MustRebuild"));
KService::List offers = KApplicationTrader::query(filter);
QCOMPARE(offers.count(), 1);
}
void KApplicationTraderTest::testSetPreferredService()
{
const KService::Ptr oldPref = KApplicationTrader::preferredService(QLatin1String("text/plain"));
const KService::Ptr newPref = KService::serviceByDesktopPath(m_fakeApplication);
KApplicationTrader::setPreferredService(QLatin1String("text/plain"), newPref);
QCOMPARE(KApplicationTrader::preferredService(QLatin1String("text/plain"))->entryPath(), m_fakeApplication);
KApplicationTrader::setPreferredService(QLatin1String("text/plain"), oldPref);
QCOMPARE(KApplicationTrader::preferredService(QLatin1String("text/plain"))->storageId(), oldPref->storageId());
}
#include "kapplicationtradertest.moc"