File indexing completed on 2024-04-14 14:19:05

0001 /*
0002     This file is part of the KDE project
0003     SPDX-FileCopyrightText: 2014 Alex Richardson <arichardson.kde@gmail.com>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "kcoreaddons_debug.h"
0009 #include <QDebug>
0010 #include <QJsonArray>
0011 #include <QJsonDocument>
0012 #include <QJsonObject>
0013 #include <QObject>
0014 #include <QProcess>
0015 #include <QTemporaryFile>
0016 #include <QTest>
0017 #include <kcoreaddons_export.h>
0018 
0019 namespace QTest
0020 {
0021 template<>
0022 inline char *toString(const QJsonValue &val)
0023 {
0024     // simply reuse the QDebug representation
0025     QString result;
0026     QDebug(&result) << val;
0027     return QTest::toString(result);
0028 }
0029 
0030 }
0031 
0032 class DesktopToJsonTest : public QObject
0033 {
0034     Q_OBJECT
0035 
0036 private:
0037     void compareJson(const QJsonObject &actual, const QJsonObject &expected)
0038     {
0039         for (auto it = actual.constBegin(); it != actual.constEnd(); ++it) {
0040             if (expected.constFind(it.key()) == expected.constEnd()) {
0041                 qCritical() << "Result has key" << it.key() << "which is not expected!";
0042                 QFAIL("Invalid output");
0043             }
0044             if (it.value().isObject() && expected.value(it.key()).isObject()) {
0045                 compareJson(it.value().toObject(), expected.value(it.key()).toObject());
0046             } else {
0047                 QCOMPARE(it.value(), expected.value(it.key()));
0048             }
0049         }
0050         for (auto it = expected.constBegin(); it != expected.constEnd(); ++it) {
0051             if (actual.constFind(it.key()) == actual.constEnd()) {
0052                 qCritical() << "Result is missing key" << it.key();
0053                 QFAIL("Invalid output");
0054             }
0055             if (it.value().isObject() && actual.value(it.key()).isObject()) {
0056                 compareJson(it.value().toObject(), actual.value(it.key()).toObject());
0057             } else {
0058                 QCOMPARE(it.value(), actual.value(it.key()));
0059             }
0060         }
0061     }
0062 
0063 private Q_SLOTS:
0064 
0065     void testDesktopToJson_data()
0066     {
0067         QTest::addColumn<QByteArray>("input");
0068         QTest::addColumn<QJsonObject>("expectedResult");
0069         QTest::addColumn<bool>("compatibilityMode");
0070         QTest::addColumn<QStringList>("serviceTypes");
0071 
0072         QJsonObject expectedResult;
0073         QJsonObject kpluginObj;
0074         QByteArray input =
0075             // include an insignificant group
0076             "[Some Group]\n"
0077             "Foo=Bar\n"
0078             "\n"
0079             "[Desktop Entry]\n"
0080             // only data inside [Desktop Entry] should be included
0081             "Name=Example\n"
0082             // empty lines
0083             "\n"
0084             " \n"
0085             // make sure translations are included:
0086             "Name[de_DE]=Beispiel\n"
0087             // ignore comments:
0088             "#Comment=Comment\n"
0089             "  #Comment=Comment\n"
0090             "Categories=foo;bar;a\\;b\n"
0091             // As the case is significant, the keys Name and NAME are not equivalent:
0092             "CaseSensitive=ABC\n"
0093             "CASESENSITIVE=abc\n"
0094             // Space before and after the equals sign should be ignored:
0095             "SpacesBeforeEq   =foo\n"
0096             "SpacesAfterEq=   foo\n"
0097             //  Space before and after the equals sign should be ignored; the = sign is the actual delimiter.
0098             // TODO: error in spec (spaces before and after the key??)
0099             "   SpacesBeforeKey=foo\n"
0100             "SpacesAfterKey   =foo\n"
0101             // ignore trailing spaces
0102             "TrailingSpaces=foo   \n"
0103             // However spaces in the value are significant:
0104             "SpacesInValue=Hello, World!\n"
0105             //  The escape sequences \s, \n, \t, \r, and \\ are supported for values of
0106             // type string and localestring, meaning ASCII space, newline, tab,
0107             // carriage return, and backslash, respectively:
0108             "EscapeSequences=So\\sme esc\\nap\\te se\\\\qu\\re\\\\nces\n" // make sure that the last n is a literal n not a newline!
0109             // the standard keys that are used by plugins, make sure correct types are used:
0110             "X-KDE-PluginInfo-Category=Examples\n" // string key
0111             "X-KDE-PluginInfo-Version=1.0\n"
0112         // The multiple values should be separated by a semicolon and the value of the key
0113         // may be optionally terminated by a semicolon. Trailing empty strings must always
0114         // be terminated with a semicolon. Semicolons in these values need to be escaped using \;.
0115 #if KCOREADDONS_BUILD_DEPRECATED_SINCE(5, 79)
0116             "X-KDE-PluginInfo-Depends=foo,bar,esc\\,aped\n" // string list key
0117 #endif
0118             "X-KDE-ServiceTypes=\n" // empty string list
0119             "X-KDE-PluginInfo-EnabledByDefault=true\n" // bool key
0120             // now start a new group
0121             "[New Group]\n"
0122             "InWrongGroup=true\n";
0123 
0124         expectedResult[QStringLiteral("Categories")] = QStringLiteral("foo;bar;a\\;b");
0125         expectedResult[QStringLiteral("CaseSensitive")] = QStringLiteral("ABC");
0126         expectedResult[QStringLiteral("CASESENSITIVE")] = QStringLiteral("abc");
0127         expectedResult[QStringLiteral("SpacesBeforeEq")] = QStringLiteral("foo");
0128         expectedResult[QStringLiteral("SpacesAfterEq")] = QStringLiteral("foo");
0129         expectedResult[QStringLiteral("SpacesBeforeKey")] = QStringLiteral("foo");
0130         expectedResult[QStringLiteral("SpacesAfterKey")] = QStringLiteral("foo");
0131         expectedResult[QStringLiteral("TrailingSpaces")] = QStringLiteral("foo");
0132         expectedResult[QStringLiteral("SpacesInValue")] = QStringLiteral("Hello, World!");
0133         expectedResult[QStringLiteral("EscapeSequences")] = QStringLiteral("So me esc\nap\te se\\qu\re\\nces");
0134         kpluginObj[QStringLiteral("Name")] = QStringLiteral("Example");
0135         kpluginObj[QStringLiteral("Name[de_DE]")] = QStringLiteral("Beispiel");
0136         kpluginObj[QStringLiteral("Category")] = QStringLiteral("Examples");
0137 #if KCOREADDONS_BUILD_DEPRECATED_SINCE(5, 79)
0138         kpluginObj[QStringLiteral("Dependencies")] =
0139             QJsonArray::fromStringList(QStringList() << QStringLiteral("foo") << QStringLiteral("bar") << QStringLiteral("esc,aped"));
0140 #endif
0141         kpluginObj[QStringLiteral("ServiceTypes")] = QJsonArray::fromStringList(QStringList());
0142         kpluginObj[QStringLiteral("EnabledByDefault")] = true;
0143         kpluginObj[QStringLiteral("Version")] = QStringLiteral("1.0");
0144         QJsonObject compatResult = expectedResult;
0145         compatResult[QStringLiteral("Name")] = QStringLiteral("Example");
0146         compatResult[QStringLiteral("Name[de_DE]")] = QStringLiteral("Beispiel");
0147         compatResult[QStringLiteral("X-KDE-PluginInfo-Category")] = QStringLiteral("Examples");
0148         compatResult[QStringLiteral("X-KDE-PluginInfo-Version")] = QStringLiteral("1.0");
0149 #if KCOREADDONS_BUILD_DEPRECATED_SINCE(5, 79)
0150         compatResult[QStringLiteral("X-KDE-PluginInfo-Depends")] =
0151             QJsonArray::fromStringList(QStringList() << QStringLiteral("foo") << QStringLiteral("bar") << QStringLiteral("esc,aped"));
0152 #endif
0153         compatResult[QStringLiteral("X-KDE-ServiceTypes")] = QJsonArray::fromStringList(QStringList());
0154         compatResult[QStringLiteral("X-KDE-PluginInfo-EnabledByDefault")] = true;
0155 
0156         expectedResult[QStringLiteral("KPlugin")] = kpluginObj;
0157 
0158         QTest::newRow("newFormat") << input << expectedResult << false << QStringList();
0159         QTest::newRow("compatFormat") << input << compatResult << true << QStringList();
0160 
0161         // test conversion of a currently existing .desktop file (excluding most of the translations):
0162         QByteArray kdevInput =
0163             "[Desktop Entry]\n"
0164             "Type = Service\n"
0165             "Icon=text-x-c++src\n"
0166             "Exec=blubb\n"
0167             "Comment=C/C++ Language Support\n"
0168             "Comment[fr]=Prise en charge du langage C/C++\n"
0169             "Comment[it]=Supporto al linguaggio C/C++\n"
0170             "Name=C++ Support\n"
0171             "Name[fi]=C++-tuki\n"
0172             "Name[fr]=Prise en charge du C++\n"
0173             "GenericName=Language Support\n"
0174             "GenericName[sl]=Podpora jeziku\n"
0175             "ServiceTypes=KDevelop/NonExistentPlugin\n"
0176             "X-KDE-Library=kdevcpplanguagesupport\n"
0177             "X-KDE-PluginInfo-Name=kdevcppsupport\n"
0178             "X-KDE-PluginInfo-Category=Language Support\n"
0179             "X-KDevelop-Version=1\n"
0180             "X-KDevelop-Language=C++\n"
0181             "X-KDevelop-Args=CPP\n"
0182             "X-KDevelop-Interfaces=ILanguageSupport\n"
0183             "X-KDevelop-SupportedMimeTypes=text/x-chdr,text/x-c++hdr,text/x-csrc,text/x-c++src\n"
0184             "X-KDevelop-Mode=NoGUI\n"
0185             "X-KDevelop-LoadMode=AlwaysOn";
0186 
0187         QJsonParseError e;
0188         QJsonObject kdevExpected = QJsonDocument::fromJson(
0189                                        "{\n"
0190                                        " \"GenericName\": \"Language Support\",\n"
0191                                        " \"GenericName[sl]\": \"Podpora jeziku\",\n"
0192                                        " \"KPlugin\": {\n"
0193                                        "     \"Category\": \"Language Support\",\n"
0194                                        "     \"Description\": \"C/C++ Language Support\",\n"
0195                                        "     \"Description[fr]\": \"Prise en charge du langage C/C++\",\n"
0196                                        "     \"Description[it]\": \"Supporto al linguaggio C/C++\",\n"
0197                                        "     \"Icon\": \"text-x-c++src\",\n"
0198                                        "     \"Id\": \"kdevcppsupport\",\n"
0199                                        "     \"Name\": \"C++ Support\",\n"
0200                                        "     \"Name[fi]\": \"C++-tuki\",\n"
0201                                        "     \"Name[fr]\": \"Prise en charge du C++\",\n"
0202                                        "     \"ServiceTypes\": [ \"KDevelop/NonExistentPlugin\" ]\n"
0203                                        " },\n"
0204                                        " \"X-KDevelop-Args\": \"CPP\",\n"
0205                                        " \"X-KDevelop-Interfaces\": \"ILanguageSupport\",\n"
0206                                        " \"X-KDevelop-Language\": \"C++\",\n"
0207                                        " \"X-KDevelop-LoadMode\": \"AlwaysOn\",\n"
0208                                        " \"X-KDevelop-Mode\": \"NoGUI\",\n"
0209                                        " \"X-KDevelop-SupportedMimeTypes\": \"text/x-chdr,text/x-c++hdr,text/x-csrc,text/x-c++src\",\n"
0210                                        " \"X-KDevelop-Version\": \"1\"\n"
0211                                        "}\n",
0212                                        &e)
0213                                        .object();
0214         QCOMPARE(e.error, QJsonParseError::NoError);
0215         QTest::newRow("kdevcpplanguagesupport no servicetype") << kdevInput << kdevExpected << false << QStringList();
0216 
0217         QJsonObject kdevExpectedWithServiceType =
0218             QJsonDocument::fromJson(
0219                 "{\n"
0220                 " \"GenericName\": \"Language Support\",\n"
0221                 " \"GenericName[sl]\": \"Podpora jeziku\",\n"
0222                 " \"KPlugin\": {\n"
0223                 "     \"Category\": \"Language Support\",\n"
0224                 "     \"Description\": \"C/C++ Language Support\",\n"
0225                 "     \"Description[fr]\": \"Prise en charge du langage C/C++\",\n"
0226                 "     \"Description[it]\": \"Supporto al linguaggio C/C++\",\n"
0227                 "     \"Icon\": \"text-x-c++src\",\n"
0228                 "     \"Id\": \"kdevcppsupport\",\n"
0229                 "     \"Name\": \"C++ Support\",\n"
0230                 "     \"Name[fi]\": \"C++-tuki\",\n"
0231                 "     \"Name[fr]\": \"Prise en charge du C++\",\n"
0232                 "     \"ServiceTypes\": [ \"KDevelop/NonExistentPlugin\" ]\n"
0233                 " },\n"
0234                 " \"X-KDevelop-Args\": \"CPP\",\n"
0235                 " \"X-KDevelop-Interfaces\": [\"ILanguageSupport\"],\n"
0236                 " \"X-KDevelop-Language\": \"C++\",\n"
0237                 " \"X-KDevelop-LoadMode\": \"AlwaysOn\",\n"
0238                 " \"X-KDevelop-Mode\": \"NoGUI\",\n"
0239                 " \"X-KDevelop-SupportedMimeTypes\": [\"text/x-chdr\", \"text/x-c++hdr\", \"text/x-csrc\", \"text/x-c++src\"],\n"
0240                 " \"X-KDevelop-Version\": 1\n"
0241                 "}\n",
0242                 &e)
0243                 .object();
0244         QCOMPARE(e.error, QJsonParseError::NoError);
0245         const QString kdevServiceTypePath = QFINDTESTDATA("data/servicetypes/fake-kdevelopplugin.desktop");
0246         QVERIFY(!kdevServiceTypePath.isEmpty());
0247         QTest::newRow("kdevcpplanguagesupport with servicetype") << kdevInput << kdevExpectedWithServiceType << false << QStringList(kdevServiceTypePath);
0248         // test conversion of the X-KDE-PluginInfo-Author + X-KDE-PluginInfo-Email key:
0249         QByteArray authorInput =
0250             "[Desktop Entry]\n"
0251             "Type=Service\n"
0252             "X-KDE-PluginInfo-Author=Foo Bar\n"
0253             "X-KDE-PluginInfo-Email=foo.bar@baz.com\n";
0254 
0255         QJsonObject authorsExpected = QJsonDocument::fromJson(
0256                                           "{\n"
0257                                           " \"KPlugin\": {\n"
0258                                           "     \"Authors\": [ { \"Name\": \"Foo Bar\", \"Email\": \"foo.bar@baz.com\" } ]\n"
0259                                           " }\n }\n",
0260                                           &e)
0261                                           .object();
0262         QCOMPARE(e.error, QJsonParseError::NoError);
0263         QTest::newRow("authors") << authorInput << authorsExpected << false << QStringList();
0264 
0265         // test case-insensitive conversion of boolean keys
0266         const QString boolServiceType = QFINDTESTDATA("data/servicetypes/bool-servicetype.desktop");
0267         QVERIFY(!boolServiceType.isEmpty());
0268 
0269         QByteArray boolInput1 = "[Desktop Entry]\nType=Service\nX-Test-Bool=true\n";
0270         QByteArray boolInput2 = "[Desktop Entry]\nType=Service\nX-Test-Bool=TRue\n";
0271         QByteArray boolInput3 = "[Desktop Entry]\nType=Service\nX-Test-Bool=false\n";
0272         QByteArray boolInput4 = "[Desktop Entry]\nType=Service\nX-Test-Bool=FALse\n";
0273 
0274         auto boolResultTrue = QJsonDocument::fromJson("{\"KPlugin\":{},\"X-Test-Bool\": true}", &e).object();
0275         QCOMPARE(e.error, QJsonParseError::NoError);
0276         auto boolResultFalse = QJsonDocument::fromJson("{\"KPlugin\":{},\"X-Test-Bool\": false}", &e).object();
0277         QCOMPARE(e.error, QJsonParseError::NoError);
0278         QTest::newRow("bool true") << boolInput1 << boolResultTrue << false << QStringList(boolServiceType);
0279         QTest::newRow("bool TRue") << boolInput2 << boolResultTrue << false << QStringList(boolServiceType);
0280         QTest::newRow("bool false") << boolInput3 << boolResultFalse << false << QStringList(boolServiceType);
0281         QTest::newRow("bool FALse") << boolInput4 << boolResultFalse << false << QStringList(boolServiceType);
0282 
0283         // test conversion of kcookiejar.desktop (for some reason the wrong boolean values were committed)
0284         QByteArray kcookiejarInput =
0285             "[Desktop Entry]\n"
0286             "Type= Service\n"
0287             "Name=Cookie Jar\n"
0288             "Comment=Stores network cookies\n"
0289             "X-KDE-ServiceTypes=KDEDModule\n"
0290             "X-KDE-Library=kf5/kded/kcookiejar\n"
0291             "X-KDE-Kded-autoload=false\n"
0292             "X-KDE-Kded-load-on-demand=true\n";
0293         auto kcookiejarResult = QJsonDocument::fromJson(
0294                                     "{\n"
0295                                     "  \"KPlugin\": {\n"
0296                                     "    \"Description\": \"Stores network cookies\",\n"
0297                                     "    \"Name\": \"Cookie Jar\",\n"
0298                                     "    \"ServiceTypes\": [\n"
0299                                     "      \"KDEDModule\"\n"
0300                                     "    ]\n"
0301                                     "  },\n"
0302                                     "\"X-KDE-Kded-autoload\": false,\n"
0303                                     "\"X-KDE-Kded-load-on-demand\": true\n"
0304                                     "}\n",
0305                                     &e)
0306                                     .object();
0307         const QString kdedmoduleServiceType = QFINDTESTDATA("data/servicetypes/fake-kdedmodule.desktop");
0308         QVERIFY(!kdedmoduleServiceType.isEmpty());
0309         QTest::newRow("kcookiejar") << kcookiejarInput << kcookiejarResult << false << QStringList(kdedmoduleServiceType);
0310     }
0311 
0312     void testDesktopToJson()
0313     {
0314         QTemporaryFile output;
0315         QTemporaryFile inputFile;
0316         QVERIFY(inputFile.open());
0317         QVERIFY(output.open()); // create the file
0318         QFETCH(QByteArray, input);
0319         QFETCH(QJsonObject, expectedResult);
0320         QFETCH(bool, compatibilityMode);
0321         QFETCH(QStringList, serviceTypes);
0322         output.close();
0323         inputFile.write(input);
0324         inputFile.flush();
0325         inputFile.close();
0326 
0327         QProcess proc;
0328         proc.setProgram(QStringLiteral(DESKTOP_TO_JSON_EXE));
0329         QStringList arguments = QStringList() << QStringLiteral("-i") << inputFile.fileName() << QStringLiteral("-o") << output.fileName();
0330         if (compatibilityMode) {
0331             arguments << QStringLiteral("-c");
0332         }
0333         for (const QString &s : std::as_const(serviceTypes)) {
0334             arguments << QStringLiteral("-s") << s;
0335         }
0336         proc.setArguments(arguments);
0337         proc.start();
0338         QVERIFY(proc.waitForFinished(10000));
0339         QByteArray errorOut = proc.readAllStandardError();
0340         if (!errorOut.isEmpty()) {
0341             qCWarning(KCOREADDONS_DEBUG).nospace() << "desktoptojson STDERR:\n\n" << errorOut.constData() << "\n";
0342         }
0343         QCOMPARE(proc.exitCode(), 0);
0344         QVERIFY(output.open());
0345         QByteArray jsonString = output.readAll();
0346         QJsonParseError e;
0347         QJsonDocument doc = QJsonDocument::fromJson(jsonString, &e);
0348         QCOMPARE(e.error, QJsonParseError::NoError);
0349         QJsonObject result = doc.object();
0350         compareJson(result, expectedResult);
0351         QVERIFY(!QTest::currentTestFailed());
0352     }
0353 };
0354 
0355 QTEST_MAIN(DesktopToJsonTest)
0356 
0357 #include "desktoptojsontest.moc"