Warning, file /frameworks/kcoreaddons/autotests/kpluginmetadatatest.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

0001 /*
0002     SPDX-FileCopyrightText: 2014 Alex Richardson <arichardson.kde@gmail.com>
0003 
0004     SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0005 */
0006 
0007 #include <QJsonArray>
0008 #include <QJsonDocument>
0009 #include <QJsonParseError>
0010 #include <QPluginLoader>
0011 #include <QRegularExpression>
0012 #include <QStandardPaths>
0013 #include <QTest>
0014 
0015 #include "kcoreaddons_debug.h"
0016 #include <kaboutdata.h>
0017 #include <kpluginloader.h>
0018 #include <kpluginmetadata.h>
0019 
0020 #include <QLocale>
0021 #include <QLoggingCategory>
0022 
0023 namespace QTest
0024 {
0025 template<>
0026 inline char *toString(const QJsonValue &val)
0027 {
0028     // simply reuse the QDebug representation
0029     QString result;
0030     QDebug(&result) << val;
0031     return QTest::toString(result);
0032 }
0033 }
0034 
0035 class LibraryPathRestorer
0036 {
0037 public:
0038     explicit LibraryPathRestorer(const QStringList &paths)
0039         : mPaths(paths)
0040     {
0041     }
0042     ~LibraryPathRestorer()
0043     {
0044         QCoreApplication::setLibraryPaths(mPaths);
0045     }
0046 
0047 private:
0048     QStringList mPaths;
0049 };
0050 
0051 class KPluginMetaDataTest : public QObject
0052 {
0053     Q_OBJECT
0054     bool m_canMessage = false;
0055 
0056     void doMessagesWorkInternal()
0057     {
0058     }
0059 
0060     Q_REQUIRED_RESULT bool doMessagesWork()
0061     {
0062         // Q_SKIP returns, but since this is called in multiple tests we want to return a bool so the caller can
0063         // return easily.
0064         auto internalCheck = [this] {
0065             // Make sure output is well formed AND generated. To that end we cannot run this test when any of the
0066             // overriding environment variables are set.
0067             // https://bugs.kde.org/show_bug.cgi?id=387006
0068             if (qEnvironmentVariableIsSet("QT_MESSAGE_PATTERN")) {
0069                 QSKIP("QT_MESSAGE_PATTERN prevents warning expectations from matching");
0070             }
0071             if (qEnvironmentVariableIsSet("QT_LOGGING_RULES")) {
0072                 QSKIP("QT_LOGGING_RULES prevents warning expectations from matching");
0073             }
0074             if (qEnvironmentVariableIsSet("QT_LOGGING_CONF")) {
0075                 QSKIP("QT_LOGGING_CONF prevents warning expectations from matching");
0076             }
0077             m_canMessage = true;
0078             // Ensure all frameworks output is enabled so the expectations can match.
0079             // qtlogging.ini may have disabled it but we can fix that because setFilterRules overrides the ini files.
0080             QLoggingCategory::setFilterRules(QStringLiteral("kf.*=true"));
0081         };
0082         internalCheck();
0083         return m_canMessage;
0084     }
0085 private Q_SLOTS:
0086 
0087     void testFromPluginLoader()
0088     {
0089         QString location;
0090         location = QPluginLoader(QStringLiteral("jsonplugin")).fileName();
0091         QVERIFY2(!location.isEmpty(), "Could not find jsonplugin");
0092 
0093         // now that this file is translated we need to read it instead of hardcoding the contents here
0094         QString jsonLocation = QFINDTESTDATA("jsonplugin.json");
0095         QVERIFY2(!jsonLocation.isEmpty(), "Could not find jsonplugin.json");
0096         QFile jsonFile(jsonLocation);
0097         QVERIFY(jsonFile.open(QFile::ReadOnly));
0098         QJsonParseError e;
0099         QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonFile.readAll(), &e);
0100         QCOMPARE(e.error, QJsonParseError::NoError);
0101 
0102         location = QFileInfo(location).absoluteFilePath();
0103 
0104         KPluginMetaData fromQPluginLoader(QPluginLoader(QStringLiteral("jsonplugin")));
0105         KPluginMetaData fromFullPath(location);
0106         KPluginMetaData fromRelativePath(QStringLiteral("jsonplugin"));
0107         KPluginMetaData fromRawData(jsonDoc.object(), location);
0108 
0109         auto description = QStringLiteral("This is a plugin");
0110 
0111 #if KCOREADDONS_BUILD_DEPRECATED_SINCE(5, 86)
0112         KPluginMetaData fromKPluginLoader(KPluginLoader(QStringLiteral("jsonplugin")));
0113         QVERIFY(fromKPluginLoader.isValid());
0114         QCOMPARE(fromKPluginLoader.description(), description);
0115         QCOMPARE(fromKPluginLoader, fromKPluginLoader);
0116         QCOMPARE(fromQPluginLoader, fromKPluginLoader);
0117         QCOMPARE(fromKPluginLoader, fromQPluginLoader);
0118         QCOMPARE(fromKPluginLoader, fromFullPath);
0119         QCOMPARE(fromKPluginLoader, fromRawData);
0120         QCOMPARE(fromFullPath, fromKPluginLoader);
0121         QCOMPARE(fromRawData, fromKPluginLoader);
0122         QVERIFY(!KPluginMetaData(KPluginLoader(QStringLiteral("doesnotexist"))).isValid());
0123 #endif
0124 
0125         QVERIFY(fromQPluginLoader.isValid());
0126         QCOMPARE(fromQPluginLoader.description(), description);
0127         QVERIFY(fromFullPath.isValid());
0128         QCOMPARE(fromFullPath.description(), description);
0129         QVERIFY(fromRelativePath.isValid());
0130         QCOMPARE(fromRelativePath.description(), description);
0131         QVERIFY(fromRawData.isValid());
0132         QCOMPARE(fromRawData.description(), description);
0133 
0134         // check operator==
0135         QCOMPARE(fromRawData, fromRawData);
0136         QCOMPARE(fromQPluginLoader, fromQPluginLoader);
0137         QCOMPARE(fromFullPath, fromFullPath);
0138 
0139         QCOMPARE(fromQPluginLoader, fromFullPath);
0140         QCOMPARE(fromQPluginLoader, fromRawData);
0141 
0142         QCOMPARE(fromFullPath, fromQPluginLoader);
0143         QCOMPARE(fromFullPath, fromRawData);
0144 
0145         QCOMPARE(fromRawData, fromQPluginLoader);
0146         QCOMPARE(fromRawData, fromFullPath);
0147 
0148         QVERIFY(!KPluginMetaData(QPluginLoader(QStringLiteral("doesnotexist"))).isValid());
0149         QVERIFY(!KPluginMetaData(QJsonObject(), QString()).isValid());
0150     }
0151 
0152     void testAllKeys()
0153     {
0154         QJsonParseError e;
0155         QJsonObject jo = QJsonDocument::fromJson(
0156                              "{\n"
0157                              " \"KPlugin\": {\n"
0158                              " \"Name\": \"Date and Time\",\n"
0159                              " \"Description\": \"Date and time by timezone\",\n"
0160                              " \"Icon\": \"preferences-system-time\",\n"
0161                              " \"Authors\": { \"Name\": \"Aaron Seigo\", \"Email\": \"aseigo@kde.org\" },\n"
0162                              " \"Translators\": { \"Name\": \"No One\", \"Email\": \"no.one@kde.org\" },\n"
0163                              " \"OtherContributors\": { \"Name\": \"No One\", \"Email\": \"no.one@kde.org\" },\n"
0164                              " \"Category\": \"Date and Time\",\n"
0165                              " \"Dependencies\": [ \"foo\", \"bar\"],\n"
0166                              " \"EnabledByDefault\": \"true\",\n"
0167                              " \"ExtraInformation\": \"Something else\",\n"
0168                              " \"License\": \"LGPL\",\n"
0169                              " \"Copyright\": \"(c) Alex Richardson 2015\",\n"
0170                              " \"Id\": \"time\",\n"
0171                              " \"Version\": \"1.0\",\n"
0172                              " \"Website\": \"https://plasma.kde.org/\",\n"
0173                              " \"MimeTypes\": [ \"image/png\" ],\n"
0174                              " \"ServiceTypes\": [\"Plasma/DataEngine\"]\n"
0175                              " }\n}\n",
0176                              &e)
0177                              .object();
0178         QCOMPARE(e.error, QJsonParseError::NoError);
0179         KPluginMetaData m(jo, QString());
0180         QVERIFY(m.isValid());
0181         QCOMPARE(m.pluginId(), QStringLiteral("time"));
0182         QCOMPARE(m.name(), QStringLiteral("Date and Time"));
0183         QCOMPARE(m.description(), QStringLiteral("Date and time by timezone"));
0184 #if KCOREADDONS_BUILD_DEPRECATED_SINCE(5, 87)
0185         QCOMPARE(m.extraInformation(), QStringLiteral("Something else"));
0186 #endif
0187         QCOMPARE(m.iconName(), QStringLiteral("preferences-system-time"));
0188         QCOMPARE(m.category(), QStringLiteral("Date and Time"));
0189 #if KCOREADDONS_BUILD_DEPRECATED_SINCE(5, 79)
0190         QCOMPARE(m.dependencies(), QStringList() << QStringLiteral("foo") << QStringLiteral("bar"));
0191 #endif
0192         QCOMPARE(m.authors().size(), 1);
0193         QCOMPARE(m.authors().constFirst().name(), QStringLiteral("Aaron Seigo"));
0194         QCOMPARE(m.authors().constFirst().emailAddress(), QStringLiteral("aseigo@kde.org"));
0195         QCOMPARE(m.translators().size(), 1);
0196         QCOMPARE(m.translators().constFirst().name(), QStringLiteral("No One"));
0197         QCOMPARE(m.translators().constFirst().emailAddress(), QStringLiteral("no.one@kde.org"));
0198         QCOMPARE(m.otherContributors().size(), 1);
0199         QCOMPARE(m.otherContributors().constFirst().name(), QStringLiteral("No One"));
0200         QCOMPARE(m.otherContributors().constFirst().emailAddress(), QStringLiteral("no.one@kde.org"));
0201         QVERIFY(m.isEnabledByDefault());
0202         QCOMPARE(m.license(), QStringLiteral("LGPL"));
0203         QCOMPARE(m.copyrightText(), QStringLiteral("(c) Alex Richardson 2015"));
0204         QCOMPARE(m.version(), QStringLiteral("1.0"));
0205         QCOMPARE(m.website(), QStringLiteral("https://plasma.kde.org/"));
0206 #if KCOREADDONS_BUILD_DEPRECATED_SINCE(5, 89)
0207         QCOMPARE(m.serviceTypes(), QStringList() << QStringLiteral("Plasma/DataEngine"));
0208 #endif
0209         QCOMPARE(m.mimeTypes(), QStringList() << QStringLiteral("image/png"));
0210     }
0211 
0212     void testTranslations()
0213     {
0214         QJsonParseError e;
0215         QJsonObject jo = QJsonDocument::fromJson(
0216                              "{ \"KPlugin\": {\n"
0217                              "\"Name\": \"Name\",\n"
0218                              "\"Name[de]\": \"Name (de)\",\n"
0219                              "\"Name[de_DE]\": \"Name (de_DE)\",\n"
0220                              "\"Description\": \"Description\",\n"
0221                              "\"Description[de]\": \"Beschreibung (de)\",\n"
0222                              "\"Description[de_DE]\": \"Beschreibung (de_DE)\"\n"
0223                              "}\n}",
0224                              &e)
0225                              .object();
0226         KPluginMetaData m(jo, QString());
0227         QLocale::setDefault(QLocale::c());
0228         QCOMPARE(m.name(), QStringLiteral("Name"));
0229         QCOMPARE(m.description(), QStringLiteral("Description"));
0230 
0231         QLocale::setDefault(QLocale(QStringLiteral("de_DE")));
0232         QCOMPARE(m.name(), QStringLiteral("Name (de_DE)"));
0233         QCOMPARE(m.description(), QStringLiteral("Beschreibung (de_DE)"));
0234 
0235         QLocale::setDefault(QLocale(QStringLiteral("de_CH")));
0236         QCOMPARE(m.name(), QStringLiteral("Name (de)"));
0237         QCOMPARE(m.description(), QStringLiteral("Beschreibung (de)"));
0238 
0239         QLocale::setDefault(QLocale(QStringLiteral("fr_FR")));
0240         QCOMPARE(m.name(), QStringLiteral("Name"));
0241         QCOMPARE(m.description(), QStringLiteral("Description"));
0242     }
0243 
0244     void testReadStringList()
0245     {
0246         if (!doMessagesWork()) {
0247             return;
0248         }
0249         QJsonParseError e;
0250         QJsonObject jo = QJsonDocument::fromJson(
0251                              "{\n"
0252                              "\"String\": \"foo\",\n"
0253                              "\"OneArrayEntry\": [ \"foo\" ],\n"
0254                              "\"Bool\": true,\n" // make sure booleans are accepted
0255                              "\"QuotedBool\": \"true\",\n" // make sure booleans are accepted
0256                              "\"Number\": 12345,\n" // number should also work
0257                              "\"QuotedNumber\": \"12345\",\n" // number should also work
0258                              "\"EmptyArray\": [],\n"
0259                              "\"NumberArray\": [1, 2, 3],\n"
0260                              "\"BoolArray\": [true, false, true],\n"
0261                              "\"StringArray\": [\"foo\", \"bar\"],\n"
0262                              "\"Null\": null,\n" // should return empty list
0263                              "\"QuotedNull\": \"null\",\n" // this is okay, it is a string
0264                              "\"ArrayWithNull\": [ \"foo\", null, \"bar\"],\n" // TODO: null is converted to empty string, is this okay?
0265                              "\"Object\": { \"foo\": \"bar\" }\n" // should return empty list
0266                              "}",
0267                              &e)
0268                              .object();
0269         QCOMPARE(e.error, QJsonParseError::NoError);
0270         QTest::ignoreMessage(QtWarningMsg, QRegularExpression(QStringLiteral("Expected JSON property ")));
0271         KPluginMetaData data(jo, QStringLiteral("test"));
0272         QCOMPARE(data.value(QStringLiteral("String"), QStringList()), QStringList(QStringLiteral("foo")));
0273         QCOMPARE(data.value(QStringLiteral("OneArrayEntry"), QStringList()), QStringList(QStringLiteral("foo")));
0274         QCOMPARE(data.value(QStringLiteral("Bool"), QStringList()), QStringList(QStringLiteral("true")));
0275         QCOMPARE(data.value(QStringLiteral("QuotedBool"), QStringList()), QStringList(QStringLiteral("true")));
0276         QCOMPARE(data.value(QStringLiteral("Number"), QStringList()), QStringList(QStringLiteral("12345")));
0277         QCOMPARE(data.value(QStringLiteral("QuotedNumber"), QStringList()), QStringList(QStringLiteral("12345")));
0278         QCOMPARE(data.value(QStringLiteral("EmptyArray"), QStringList()), QStringList());
0279         QCOMPARE(data.value(QStringLiteral("NumberArray"), QStringList()), QStringList() << QStringLiteral("1") << QStringLiteral("2") << QStringLiteral("3"));
0280         QCOMPARE(data.value(QStringLiteral("BoolArray"), QStringList()),
0281                  QStringList() << QStringLiteral("true") << QStringLiteral("false") << QStringLiteral("true"));
0282         QCOMPARE(data.value(QStringLiteral("StringArray"), QStringList()), QStringList() << QStringLiteral("foo") << QStringLiteral("bar"));
0283         QCOMPARE(data.value(QStringLiteral("Null"), QStringList()), QStringList());
0284         QCOMPARE(data.value(QStringLiteral("QuotedNull"), QStringList()), QStringList(QStringLiteral("null")));
0285         QCOMPARE(data.value(QStringLiteral("ArrayWithNull"), QStringList()), QStringList() << QStringLiteral("foo") << QString() << QStringLiteral("bar"));
0286         QCOMPARE(data.value(QStringLiteral("Object"), QStringList()), QStringList());
0287     }
0288 
0289 #if KCOREADDONS_BUILD_DEPRECATED_SINCE(5, 91)
0290     void testFromDesktopFile()
0291     {
0292         const QString dfile = QFINDTESTDATA("data/fakeplugin.desktop");
0293         KPluginMetaData md = KPluginMetaData::fromDesktopFile(dfile);
0294         QVERIFY(md.isValid());
0295         QCOMPARE(md.pluginId(), QStringLiteral("fakeplugin"));
0296         QCOMPARE(md.fileName(), QStringLiteral("fakeplugin"));
0297         QCOMPARE(md.metaDataFileName(), dfile);
0298         QCOMPARE(md.iconName(), QStringLiteral("preferences-system-time"));
0299         QCOMPARE(md.license(), QStringLiteral("LGPL"));
0300         QCOMPARE(md.website(), QStringLiteral("https://kde.org/"));
0301         QCOMPARE(md.category(), QStringLiteral("Examples"));
0302         QCOMPARE(md.version(), QStringLiteral("1.0"));
0303 #if KCOREADDONS_BUILD_DEPRECATED_SINCE(5, 79)
0304         QCOMPARE(md.dependencies(), QStringList());
0305 #endif
0306         QCOMPARE(md.isHidden(), false);
0307 #if KCOREADDONS_BUILD_DEPRECATED_SINCE(5, 89)
0308         QCOMPARE(md.serviceTypes(), QStringList(QStringLiteral("KService/NSA")));
0309 #endif
0310         QCOMPARE(md.mimeTypes(), QStringList() << QStringLiteral("image/png") << QStringLiteral("application/pdf"));
0311 
0312         auto kp = md.rawData()[QStringLiteral("KPlugin")].toObject();
0313         QStringList formFactors = kp.value(QStringLiteral("FormFactors")).toVariant().toStringList();
0314         QCOMPARE(formFactors, QStringList() << QStringLiteral("mediacenter") << QStringLiteral("desktop"));
0315         QCOMPARE(md.formFactors(), QStringList() << QStringLiteral("mediacenter") << QStringLiteral("desktop"));
0316 
0317         const QString dfilehidden = QFINDTESTDATA("data/hiddenplugin.desktop");
0318         KPluginMetaData mdhidden = KPluginMetaData::fromDesktopFile(dfilehidden);
0319         QVERIFY(mdhidden.isValid());
0320         QCOMPARE(mdhidden.isHidden(), true);
0321     }
0322 
0323     void twoStepsParseTest()
0324     {
0325         QStandardPaths::setTestModeEnabled(true);
0326         const QString dfile = QFINDTESTDATA("data/twostepsparsetest.desktop");
0327         const QString typesPath = QFINDTESTDATA("data/servicetypes/example-servicetype.desktop");
0328         KPluginMetaData md = KPluginMetaData::fromDesktopFile(dfile, QStringList() << typesPath);
0329         QVERIFY(md.isValid());
0330         QStringList list = md.value(QStringLiteral("X-Test-List"), QStringList());
0331         QCOMPARE(list, QStringList({QStringLiteral("first"), QStringLiteral("second")}));
0332     }
0333 
0334     void testServiceTypes_data()
0335     {
0336         const QString kdevServiceTypePath = QFINDTESTDATA("data/servicetypes/fake-kdevelopplugin.desktop");
0337         const QString invalidServiceTypePath = QFINDTESTDATA("data/servicetypes/invalid-servicetype.desktop");
0338         const QString exampleServiceTypePath = QFINDTESTDATA("data/servicetypes/example-servicetype.desktop");
0339         QVERIFY(!kdevServiceTypePath.isEmpty());
0340         QVERIFY(!invalidServiceTypePath.isEmpty());
0341         QVERIFY(!exampleServiceTypePath.isEmpty());
0342     }
0343 
0344     void testServiceType()
0345     {
0346         if (!doMessagesWork()) {
0347             return;
0348         }
0349         const QString typesPath = QFINDTESTDATA("data/servicetypes/example-servicetype.desktop");
0350         QVERIFY(!typesPath.isEmpty());
0351         const QString inputPath = QFINDTESTDATA("data/servicetypes/example-input.desktop");
0352         QVERIFY(!inputPath.isEmpty());
0353         QTest::ignoreMessage(
0354             QtWarningMsg,
0355             // We also print out a list of paths we searched in. With the ".+" we ensure that they are printed out,
0356             // but don't make fragile assumptions on the exact message
0357             QRegularExpression(QStringLiteral("Unable to find service type for service \"bar/foo\" listed in \"") + inputPath + QLatin1String("\" .+")));
0358         KPluginMetaData md = KPluginMetaData::fromDesktopFile(inputPath, QStringList() << typesPath);
0359         QVERIFY(md.isValid());
0360         QCOMPARE(md.name(), QStringLiteral("Example"));
0361 #if KCOREADDONS_BUILD_DEPRECATED_SINCE(5, 89)
0362         QCOMPARE(md.serviceTypes(), QStringList() << QStringLiteral("example/servicetype") << QStringLiteral("bar/foo"));
0363 #endif
0364         QCOMPARE(md.rawData().size(), 8);
0365         QVERIFY(md.rawData().value(QStringLiteral("KPlugin")).isObject());
0366         QCOMPARE(md.rawData().value(QStringLiteral("X-Test-Integer")), QJsonValue(42));
0367         QCOMPARE(md.rawData().value(QStringLiteral("X-Test-Bool")), QJsonValue(true));
0368         QCOMPARE(md.rawData().value(QStringLiteral("X-Test-Double")), QJsonValue(42.42));
0369         QCOMPARE(md.rawData().value(QStringLiteral("X-Test-String")), QJsonValue(QStringLiteral("foobar")));
0370         QCOMPARE(md.rawData().value(QStringLiteral("X-Test-List")),
0371                  QJsonValue(
0372                      QJsonArray::fromStringList(QStringList() << QStringLiteral("a") << QStringLiteral("b") << QStringLiteral("c") << QStringLiteral("def"))));
0373         QCOMPARE(md.rawData().value(QStringLiteral("X-Test-Size")), QJsonValue(QStringLiteral("10,20"))); // QSize no longer supported (and also no longer used)
0374         QCOMPARE(md.rawData().value(QStringLiteral("X-Test-Unknown")), QJsonValue(QStringLiteral("true"))); // unknown property -> string
0375 #if KCOREADDONS_BUILD_DEPRECATED_SINCE(5, 88)
0376         const QString charOverloadVlaue = md.value(QStringLiteral("X-Test-Unknown"), "true");
0377         QCOMPARE(charOverloadVlaue, QStringLiteral("true"));
0378 #endif
0379     }
0380 
0381     void testBadGroupsInServiceType()
0382     {
0383         if (!doMessagesWork()) {
0384             return;
0385         }
0386         const QString typesPath = QFINDTESTDATA("data/servicetypes/bad-groups-servicetype.desktop");
0387         QVERIFY(!typesPath.isEmpty());
0388         const QString inputPath = QFINDTESTDATA("data/servicetypes/bad-groups-input.desktop");
0389         QVERIFY(!inputPath.isEmpty());
0390         QTest::ignoreMessage(QtWarningMsg, "Illegal .desktop group definition (does not end with ']'): \"[PropertyDef::MissingTerminator\"");
0391         QTest::ignoreMessage(QtWarningMsg, "Illegal .desktop group definition (does not end with ']'): \"[PropertyDef::\"");
0392         QTest::ignoreMessage(QtWarningMsg, "Illegal .desktop group definition (does not end with ']'): \"[\"");
0393         QTest::ignoreMessage(QtWarningMsg, "Read empty .desktop file group name! Invalid file?");
0394         QTest::ignoreMessage(QtWarningMsg,
0395                              QRegularExpression(QStringLiteral("Skipping invalid group \"\" in service type \".*/bad-groups-servicetype.desktop\"")));
0396         QTest::ignoreMessage(QtWarningMsg,
0397                              QRegularExpression(QStringLiteral("Skipping invalid group \"DoesNotStartWithPropertyDef::SomeOtherProperty\" in service type "
0398                                                                "\".+/data/servicetypes/bad-groups-servicetype.desktop\"")));
0399         QTest::ignoreMessage(QtWarningMsg, "Could not find Type= key in group \"PropertyDef::MissingType\"");
0400         QTest::ignoreMessage(QtWarningMsg,
0401                              QRegularExpression(QStringLiteral("Property type \"integer\" is not a known QVariant type. Found while parsing property "
0402                                                                "definition for \"InvalidType\" in \".+/data/servicetypes/bad-groups-servicetype.desktop\"")));
0403         QTest::ignoreMessage(QtWarningMsg,
0404                              QRegularExpression(QStringLiteral(".+/data/servicetypes/bad-groups-input.desktop:\\d+: Key name is missing: \"=11\"")));
0405         QTest::ignoreMessage(QtWarningMsg,
0406                              QRegularExpression(QStringLiteral(".+/data/servicetypes/bad-groups-input.desktop:\\d+: Key name is missing: \"=13\"")));
0407         QTest::ignoreMessage(QtWarningMsg,
0408                              QRegularExpression(QStringLiteral(".+/data/servicetypes/bad-groups-input.desktop:\\d+: Key name is missing: \"=14\"")));
0409         KPluginMetaData md = KPluginMetaData::fromDesktopFile(inputPath, QStringList() << typesPath);
0410         QVERIFY(md.isValid());
0411         QCOMPARE(md.name(), QStringLiteral("Bad Groups"));
0412         QCOMPARE(md.rawData().size(), 8);
0413         QCOMPARE(md.rawData().value(QStringLiteral("ThisIsOkay")), QJsonValue(10)); // integer
0414         // 11 is empty group
0415         QCOMPARE(md.rawData().value(QStringLiteral("MissingTerminator")), QJsonValue(12)); // accept missing group terminator (for now) -> integer
0416         // 13 is empty group name
0417         // 14 is empty group name
0418         QCOMPARE(md.rawData().value(QStringLiteral("SomeOtherProperty")),
0419                  QJsonValue(QStringLiteral("15"))); // does not start with PropertyDef:: -> fall back to string
0420         QCOMPARE(md.rawData().value(QStringLiteral("TrailingSpacesAreOkay")), QJsonValue(16)); // accept trailing spaces in group name -> integer
0421         QCOMPARE(md.rawData().value(QStringLiteral("MissingType")), QJsonValue(QStringLiteral("17"))); // Type= missing -> fall back to string
0422         QCOMPARE(md.rawData().value(QStringLiteral("InvalidType")), QJsonValue(QStringLiteral("18"))); // Type= is invalid -> fall back to string
0423         QCOMPARE(md.rawData().value(QStringLiteral("ThisIsOkayAgain")), QJsonValue(19)); // valid definition after invalid ones should still work -> integer
0424     }
0425 #endif
0426 
0427     void testJSONMetadata()
0428     {
0429         const QString inputPath = QFINDTESTDATA("data/testmetadata.json");
0430         KPluginMetaData md = KPluginMetaData::fromJsonFile(inputPath);
0431         QVERIFY(md.isValid());
0432         QCOMPARE(md.name(), QStringLiteral("Test"));
0433 
0434         QCOMPARE(md.value(QStringLiteral("X-Plasma-MainScript")), QStringLiteral("ui/main.qml"));
0435         QJsonArray expected;
0436         expected.append(QStringLiteral("Export"));
0437         QCOMPARE(md.rawData().value(QStringLiteral("X-Purpose-PluginTypes")).toArray(), expected);
0438         QCOMPARE(md.value(QStringLiteral("SomeInt"), 24), 42);
0439         QCOMPARE(md.value(QStringLiteral("SomeIntAsString"), 24), 42);
0440         QCOMPARE(md.value(QStringLiteral("SomeStringNotAInt"), 24), 24);
0441         QCOMPARE(md.value(QStringLiteral("DoesNotExist"), 24), 24);
0442 
0443         QVERIFY(md.value(QStringLiteral("SomeBool"), false));
0444         QVERIFY(!md.value(QStringLiteral("SomeBoolThatIsFalse"), true));
0445         QVERIFY(md.value(QStringLiteral("SomeBoolAsString"), false));
0446         QVERIFY(md.value(QStringLiteral("DoesNotExist"), true));
0447     }
0448 
0449     void testPathIsAbsolute_data()
0450     {
0451         QTest::addColumn<QString>("inputAbsolute");
0452         QTest::addColumn<QString>("pluginPath");
0453 
0454 #if KCOREADDONS_BUILD_DEPRECATED_SINCE(5, 91)
0455         // The .desktop file has X-KDE-Library, so .fileName() returns  different file
0456         QTest::newRow("desktop") << QFINDTESTDATA("data/fakeplugin.desktop") << QStringLiteral("fakeplugin");
0457 #endif
0458         // But for the .json based plugin both are the same.
0459         QTest::newRow("json") << QFINDTESTDATA("data/testmetadata.json") << QFINDTESTDATA("data/testmetadata.json");
0460         // And also for the library with embedded JSON metadata.
0461         QPluginLoader shlibLoader(QCoreApplication::applicationDirPath() + QStringLiteral("/jsonplugin"));
0462         QVERIFY2(!shlibLoader.fileName().isEmpty(), "Could not find jsonplugin");
0463         QString shlibPath = QFileInfo(shlibLoader.fileName()).absoluteFilePath();
0464         QTest::newRow("library") << shlibPath << shlibPath;
0465     }
0466 
0467     void testPathIsAbsolute()
0468     {
0469         // Test that the fileName() accessor always returns an absolute path if it was used.
0470         QFETCH(QString, inputAbsolute);
0471         QVERIFY2(QDir::isAbsolutePath(inputAbsolute), qPrintable(inputAbsolute));
0472         QFETCH(QString, pluginPath);
0473 
0474         const auto createMetaData = [](const QString &path) {
0475 #if KCOREADDONS_BUILD_DEPRECATED_SINCE(5, 91)
0476             return KPluginMetaData(path);
0477 #else
0478             if (path.endsWith(QLatin1String(".json"))) {
0479                 return KPluginMetaData::fromJsonFile(path);
0480             } else {
0481                 return KPluginMetaData(path);
0482             }
0483 #endif
0484         };
0485 
0486         KPluginMetaData mdAbsolute = createMetaData(inputAbsolute);
0487         QVERIFY(mdAbsolute.isValid());
0488         QCOMPARE(mdAbsolute.metaDataFileName(), inputAbsolute);
0489         QCOMPARE(mdAbsolute.fileName(), pluginPath);
0490 
0491         // All files that have been opened should be stored as absolute paths.
0492         QString inputRelative;
0493         if (QLibrary::isLibrary(inputAbsolute)) {
0494             // We have a plugin without namespace, with the code path below we would end up with
0495             // a path relative to the PWD, but we want to check a path relative to the plugin dir.
0496             // Because of that we simply use the baseName of the file.
0497             inputRelative = QFileInfo(inputAbsolute).baseName();
0498         } else {
0499             inputRelative = QDir::current().relativeFilePath(inputAbsolute);
0500         }
0501         QVERIFY2(QDir::isRelativePath(inputRelative), qPrintable(inputRelative));
0502         KPluginMetaData mdRelative = createMetaData(inputRelative);
0503         QVERIFY(mdRelative.isValid());
0504         QCOMPARE(mdRelative.metaDataFileName(), inputAbsolute);
0505         QCOMPARE(mdRelative.fileName(), pluginPath);
0506 
0507         // Check that creating it with the parsed JSON object and a path keeps the path unchanged
0508         const QJsonObject json = mdAbsolute.rawData();
0509         QString pluginRelative = QDir::current().relativeFilePath(pluginPath);
0510         QVERIFY2(QDir::isRelativePath(pluginRelative), qPrintable(pluginRelative));
0511         // TODO: KF6: no need to test both constructors once they are merged into one overload.
0512         KPluginMetaData mdFromJson1(json, pluginRelative, inputRelative);
0513         QCOMPARE(mdFromJson1.metaDataFileName(), inputRelative);
0514         // We should not be normalizing files that have not been openened, so both arguments should be unchanged.
0515         QCOMPARE(mdFromJson1.fileName(), pluginRelative);
0516         KPluginMetaData mdFromJson2(json, inputRelative);
0517         QCOMPARE(mdFromJson2.metaDataFileName(), inputRelative);
0518         QCOMPARE(mdFromJson2.fileName(), inputRelative);
0519     }
0520 
0521     void testFindPlugins()
0522     {
0523         QTemporaryDir temp;
0524         QVERIFY(temp.isValid());
0525         QDir dir(temp.path());
0526         QVERIFY(dir.mkdir(QStringLiteral("kpluginmetadatatest")));
0527         QVERIFY(dir.cd(QStringLiteral("kpluginmetadatatest")));
0528         for (const QString &name : {QStringLiteral("jsonplugin"), QStringLiteral("unversionedplugin"), QStringLiteral("jsonplugin2")}) {
0529             const QString pluginPath = QPluginLoader(name).fileName();
0530             QVERIFY2(!pluginPath.isEmpty(), qPrintable(pluginPath));
0531             QVERIFY2(QFile::copy(pluginPath, dir.absoluteFilePath(QFileInfo(pluginPath).fileName())),
0532                      qPrintable(dir.absoluteFilePath(QFileInfo(pluginPath).fileName())));
0533         }
0534 
0535         LibraryPathRestorer restorer(QCoreApplication::libraryPaths());
0536         // we only want plugins from our temporary dir
0537         QCoreApplication::setLibraryPaths(QStringList() << temp.path());
0538 
0539         auto sortPlugins = [](const KPluginMetaData &a, const KPluginMetaData &b) {
0540             return a.pluginId() < b.pluginId();
0541         };
0542         // it should find jsonplugin and jsonplugin2 since unversionedplugin does not have any meta data
0543         auto plugins = KPluginMetaData::findPlugins(QStringLiteral("kpluginmetadatatest"));
0544         std::sort(plugins.begin(), plugins.end(), sortPlugins);
0545         QCOMPARE(plugins.size(), 2);
0546         QCOMPARE(plugins[0].pluginId(), QStringLiteral("foobar")); // ID is not the filename, it is set in the JSON metadata
0547         QCOMPARE(plugins[0].description(), QStringLiteral("This is another plugin"));
0548         QCOMPARE(plugins[1].pluginId(), QStringLiteral("jsonplugin"));
0549         QCOMPARE(plugins[1].description(), QStringLiteral("This is a plugin"));
0550 
0551         // filter accepts none
0552         plugins = KPluginMetaData::findPlugins(QStringLiteral("kpluginmetadatatest"), [](const KPluginMetaData &) {
0553             return false;
0554         });
0555         std::sort(plugins.begin(), plugins.end(), sortPlugins);
0556         QCOMPARE(plugins.size(), 0);
0557 
0558         // filter accepts all
0559         plugins = KPluginMetaData::findPlugins(QStringLiteral("kpluginmetadatatest"), [](const KPluginMetaData &) {
0560             return true;
0561         });
0562         std::sort(plugins.begin(), plugins.end(), sortPlugins);
0563         QCOMPARE(plugins.size(), 2);
0564         QCOMPARE(plugins[0].description(), QStringLiteral("This is another plugin"));
0565         QCOMPARE(plugins[1].description(), QStringLiteral("This is a plugin"));
0566 
0567         // mimetype filter. Only one match, jsonplugin2 is specific to text/html.
0568         auto supportTextPlain = [](const KPluginMetaData &metaData) {
0569             return metaData.supportsMimeType(QLatin1String("text/plain"));
0570         };
0571         plugins = KPluginMetaData::findPlugins(QStringLiteral("kpluginmetadatatest"), supportTextPlain);
0572         QCOMPARE(plugins.size(), 1);
0573         QCOMPARE(plugins[0].description(), QStringLiteral("This is a plugin"));
0574 
0575         // mimetype filter. Two matches, both support text/html, via inheritance.
0576         auto supportTextHtml = [](const KPluginMetaData &metaData) {
0577             return metaData.supportsMimeType(QLatin1String("text/html"));
0578         };
0579         plugins = KPluginMetaData::findPlugins(QStringLiteral("kpluginmetadatatest"), supportTextHtml);
0580         std::sort(plugins.begin(), plugins.end(), sortPlugins);
0581         QCOMPARE(plugins.size(), 2);
0582         QCOMPARE(plugins[0].description(), QStringLiteral("This is another plugin"));
0583         QCOMPARE(plugins[1].description(), QStringLiteral("This is a plugin"));
0584 
0585         // mimetype filter with invalid mimetype
0586         auto supportDoesNotExist = [](const KPluginMetaData &metaData) {
0587             return metaData.supportsMimeType(QLatin1String("does/not/exist"));
0588         };
0589         plugins = KPluginMetaData::findPlugins(QStringLiteral("kpluginmetadatatest"), supportDoesNotExist);
0590         QCOMPARE(plugins.size(), 0);
0591 
0592         // invalid std::function as filter
0593         plugins = KPluginMetaData::findPlugins(QStringLiteral("kpluginmetadatatest"));
0594         std::sort(plugins.begin(), plugins.end(), sortPlugins);
0595         QCOMPARE(plugins.size(), 2);
0596         QCOMPARE(plugins[0].description(), QStringLiteral("This is another plugin"));
0597         QCOMPARE(plugins[1].description(), QStringLiteral("This is a plugin"));
0598 
0599         // by plugin id
0600         KPluginMetaData plugin = KPluginMetaData::findPluginById(dir.absolutePath(), QStringLiteral("foobar"));
0601         QVERIFY(plugin.isValid());
0602         QCOMPARE(plugin.description(), QStringLiteral("This is another plugin"));
0603 
0604         // by plugin invalid id
0605         plugin = KPluginMetaData::findPluginById(dir.absolutePath(), QStringLiteral("invalidid"));
0606         QVERIFY(!plugin.isValid());
0607 
0608         // absolute path, no filter
0609         plugins = KPluginMetaData::findPlugins(dir.absolutePath());
0610         std::sort(plugins.begin(), plugins.end(), sortPlugins);
0611         QCOMPARE(plugins.size(), 2);
0612         QCOMPARE(plugins[0].description(), QStringLiteral("This is another plugin"));
0613         QCOMPARE(plugins[1].description(), QStringLiteral("This is a plugin"));
0614 
0615         // This plugin has no explicit pluginId and will fall back to basename of file
0616         const KPluginMetaData validPlugin = KPluginMetaData::findPluginById(dir.absolutePath(), QStringLiteral("jsonplugin"));
0617         QVERIFY(validPlugin.isValid());
0618         QCOMPARE(plugins[0].description(), QStringLiteral("This is another plugin"));
0619 
0620         // The basename matches, but the pluginId does not match
0621         const KPluginMetaData nonMatchingPluginId = KPluginMetaData::findPluginById(dir.absolutePath(), QStringLiteral("jsonplugin2"));
0622         QVERIFY(!nonMatchingPluginId.isValid());
0623 
0624         const KPluginMetaData nonExistingPlugin = KPluginMetaData::findPluginById(dir.absolutePath(), QStringLiteral("invalidid"));
0625         QVERIFY(!nonExistingPlugin.isValid());
0626     }
0627 
0628     void testStaticPlugins()
0629     {
0630         QCOMPARE(QPluginLoader::staticPlugins().count(), 0);
0631 
0632         const auto plugins = KPluginMetaData::findPlugins(QStringLiteral("staticnamespace"));
0633         QCOMPARE(plugins.count(), 1);
0634 
0635         QCOMPARE(plugins.first().description(), QStringLiteral("This is a plugin"));
0636         QCOMPARE(plugins.first().fileName(), QStringLiteral("staticnamespace/static_jsonplugin_cmake_macro"));
0637     }
0638 
0639     void testPluginsWithoutMetaData()
0640     {
0641         KPluginMetaData emptyMetaData(QStringLiteral("namespace/pluginwithoutmetadata"), KPluginMetaData::AllowEmptyMetaData);
0642         QVERIFY(emptyMetaData.isValid());
0643         QCOMPARE(emptyMetaData.pluginId(), QStringLiteral("pluginwithoutmetadata"));
0644 
0645         const auto plugins = KPluginMetaData::findPlugins(QStringLiteral("namespace"), {}, KPluginMetaData::AllowEmptyMetaData);
0646         QCOMPARE(plugins.count(), 2);
0647         for (auto plugin : plugins) {
0648             if (plugin.pluginId() == QLatin1String("pluginwithoutmetadata")) {
0649                 QVERIFY(plugin.isValid());
0650                 QVERIFY(plugin.rawData().isEmpty());
0651             } else if (plugin.pluginId() == QLatin1String("jsonplugin_cmake_macro")) {
0652                 QVERIFY(plugin.isValid());
0653                 QVERIFY(!plugin.rawData().isEmpty());
0654             } else {
0655                 Q_UNREACHABLE();
0656             }
0657         }
0658     }
0659 
0660     void testStaticPluginsWithoutMetadata()
0661     {
0662         QVERIFY(KPluginMetaData::findPlugins(QStringLiteral("staticnamespace3")).isEmpty());
0663         const auto plugins = KPluginMetaData::findPlugins(QStringLiteral("staticnamespace3"), {}, KPluginMetaData::AllowEmptyMetaData);
0664         QCOMPARE(plugins.count(), 1);
0665         QVERIFY(plugins.first().isValid());
0666         QCOMPARE(plugins.first().pluginId(), QStringLiteral("static_plugin_without_metadata"));
0667     }
0668 
0669     void testReverseDomainNotationPluginId()
0670     {
0671         KPluginMetaData data(QStringLiteral("org.kde.test"));
0672         QVERIFY(data.isValid());
0673         QCOMPARE(data.pluginId(), QStringLiteral("org.kde.test"));
0674     }
0675 
0676     void testFindingPluginInAppDirFirst()
0677     {
0678         const QString originalPluginPath = QPluginLoader(QStringLiteral("namespace/jsonplugin_cmake_macro")).fileName();
0679         const QString pluginFileName = QFileInfo(originalPluginPath).fileName();
0680         const QString pluginNamespace = QStringLiteral("somepluginnamespace");
0681         const QString pluginAppDir = QCoreApplication::applicationDirPath() + QLatin1Char('/') + pluginNamespace;
0682         QDir(pluginAppDir).mkpath(QStringLiteral("."));
0683         const QString pluginAppPath = pluginAppDir + QLatin1Char('/') + pluginFileName;
0684         QFile::remove(pluginAppPath);
0685 
0686         QVERIFY(QFile::copy(originalPluginPath, pluginAppPath));
0687 
0688         QTemporaryDir temp;
0689         QVERIFY(temp.isValid());
0690         QDir dir(temp.path());
0691         QVERIFY(dir.mkdir(pluginNamespace));
0692         QVERIFY(dir.cd(pluginNamespace));
0693 
0694         const QString pluginInNamespacePath = dir.absoluteFilePath(pluginFileName);
0695         QVERIFY(QFile::copy(originalPluginPath, pluginInNamespacePath));
0696 
0697         LibraryPathRestorer restorer(QCoreApplication::libraryPaths());
0698         QCoreApplication::setLibraryPaths(QStringList() << temp.path());
0699 
0700         // Our plugin in the applicationDirPath should come first
0701         const QString relativePathWithNamespace = QStringLiteral("somepluginnamespace/jsonplugin_cmake_macro");
0702         KPluginMetaData data(relativePathWithNamespace);
0703         QVERIFY(data.isValid());
0704         QCOMPARE(data.fileName(), pluginAppPath);
0705 
0706         // The other one must be valid
0707         QVERIFY(KPluginMetaData(pluginInNamespacePath).isValid());
0708         // And after removing the plugin in the applicationDirPath, it should be found
0709         QVERIFY(QFile::remove(pluginAppPath));
0710         QCOMPARE(KPluginMetaData(relativePathWithNamespace).fileName(), pluginInNamespacePath);
0711     }
0712 };
0713 
0714 QTEST_MAIN(KPluginMetaDataTest)
0715 
0716 #include "kpluginmetadatatest.moc"