File indexing completed on 2024-04-28 04:01:10

0001 /*
0002     SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org>
0003 
0004     SPDX-License-Identifier: MIT
0005 */
0006 
0007 #include "repository_test_base.h"
0008 #include "test-config.h"
0009 
0010 #include <KSyntaxHighlighting/AbstractHighlighter>
0011 #include <KSyntaxHighlighting/Definition>
0012 #include <KSyntaxHighlighting/Format>
0013 #include <KSyntaxHighlighting/Repository>
0014 #include <KSyntaxHighlighting/State>
0015 #include <KSyntaxHighlighting/Theme>
0016 
0017 #include <QFileInfo>
0018 #include <QPalette>
0019 #include <QTest>
0020 
0021 #include <algorithm>
0022 
0023 namespace KSyntaxHighlighting
0024 {
0025 class NullHighlighter : public AbstractHighlighter
0026 {
0027 public:
0028     using AbstractHighlighter::highlightLine;
0029     void applyFormat(int offset, int length, const Format &format) override
0030     {
0031         Q_UNUSED(offset);
0032         Q_UNUSED(length);
0033         // only here to ensure we don't crash
0034         format.isDefaultTextStyle(theme());
0035         format.textColor(theme());
0036     }
0037 };
0038 
0039 class RepositoryTest : public RepositoryTestBase
0040 {
0041     Q_OBJECT
0042 private Q_SLOTS:
0043     void testDefinitionByExtension_data()
0044     {
0045         definitionByExtensionTestData();
0046     }
0047 
0048     void testDefinitionByExtension()
0049     {
0050         QFETCH(QString, fileName);
0051         QFETCH(QString, definitionName);
0052 
0053         definitionByExtensionTest(fileName, definitionName);
0054     }
0055 
0056     void testDefinitionsForFileName_data()
0057     {
0058         definitionsForFileNameTestData();
0059     }
0060 
0061     void testDefinitionsForFileName()
0062     {
0063         QFETCH(QString, fileName);
0064         QFETCH(QStringList, definitionNames);
0065 
0066         definitionsForFileNameTest(fileName, definitionNames);
0067     }
0068 
0069     void testDefinitionForMimeType_data()
0070     {
0071         definitionForMimeTypeTestData();
0072     }
0073 
0074     void testDefinitionForMimeType()
0075     {
0076         QFETCH(QString, mimeTypeName);
0077         QFETCH(QString, definitionName);
0078 
0079         definitionForMimeTypeTest(mimeTypeName, definitionName);
0080     }
0081 
0082     void testDefinitionsForMimeType_data()
0083     {
0084         definitionsForMimeTypeTestData();
0085     }
0086 
0087     void testDefinitionsForMimeType()
0088     {
0089         QFETCH(QString, mimeTypeName);
0090         QFETCH(QStringList, definitionNames);
0091 
0092         definitionsForMimeTypeTest(mimeTypeName, definitionNames);
0093     }
0094 
0095     void testLoadAll()
0096     {
0097         for (const auto &def : m_repo.definitions()) {
0098             QVERIFY(!def.name().isEmpty());
0099             QVERIFY(!def.translatedName().isEmpty());
0100             QVERIFY(!def.isValid() || !def.section().isEmpty());
0101             QVERIFY(!def.isValid() || !def.translatedSection().isEmpty());
0102             // indirectly trigger loading, as we can't reach that from public API
0103             // if the loading fails the highlighter will produce empty states
0104             NullHighlighter hl;
0105             State initialState;
0106             hl.setDefinition(def);
0107             const auto state = hl.highlightLine(u"This should not crash } ] ) !", initialState);
0108             QVERIFY(!def.isValid() || state != initialState || def.name() == QLatin1String("Broken Syntax"));
0109         }
0110     }
0111 
0112     void testMetaData()
0113     {
0114         auto def = m_repo.definitionForName(QLatin1String("Alerts"));
0115         QVERIFY(def.isValid());
0116         QVERIFY(def.extensions().isEmpty());
0117         QVERIFY(def.mimeTypes().isEmpty());
0118         QVERIFY(def.version() >= 1.11f);
0119         QVERIFY(def.isHidden());
0120         QCOMPARE(def.section(), QLatin1String("Other"));
0121         QCOMPARE(def.license(), QLatin1String("MIT"));
0122         QVERIFY(def.author().contains(QLatin1String("Dominik")));
0123         QFileInfo fi(def.filePath());
0124         QVERIFY(fi.isAbsolute());
0125         QVERIFY(def.filePath().endsWith(QLatin1String("alert.xml")));
0126 
0127         def = m_repo.definitionForName(QLatin1String("C++"));
0128         QVERIFY(def.isValid());
0129         QCOMPARE(def.section(), QLatin1String("Sources"));
0130         QCOMPARE(def.indenter(), QLatin1String("cstyle"));
0131         QCOMPARE(def.style(), QLatin1String("C++"));
0132         QVERIFY(def.mimeTypes().contains(QLatin1String("text/x-c++hdr")));
0133         QVERIFY(def.extensions().contains(QLatin1String("*.h")));
0134         QCOMPARE(def.priority(), 9);
0135 
0136         def = m_repo.definitionForName(QLatin1String("Apache Configuration"));
0137         QVERIFY(def.isValid());
0138         QVERIFY(def.extensions().contains(QLatin1String("httpd.conf")));
0139         QVERIFY(def.extensions().contains(QLatin1String(".htaccess*")));
0140     }
0141 
0142     void testGeneralMetaData()
0143     {
0144         auto def = m_repo.definitionForName(QLatin1String("C++"));
0145         QVERIFY(def.isValid());
0146         QVERIFY(!def.indentationBasedFoldingEnabled());
0147 
0148         // comment markers
0149         QCOMPARE(def.singleLineCommentMarker(), QLatin1String("//"));
0150         QCOMPARE(def.singleLineCommentPosition(), KSyntaxHighlighting::CommentPosition::AfterWhitespace);
0151         const auto cppMultiLineCommentMarker = QPair<QString, QString>(QLatin1String("/*"), QLatin1String("*/"));
0152         QCOMPARE(def.multiLineCommentMarker(), cppMultiLineCommentMarker);
0153 
0154         def = m_repo.definitionForName(QLatin1String("Python"));
0155         QVERIFY(def.isValid());
0156 
0157         // indentation
0158         QVERIFY(def.indentationBasedFoldingEnabled());
0159         QCOMPARE(def.foldingIgnoreList(), QStringList() << QLatin1String("(?:\\s+|\\s*#.*)"));
0160 
0161         // keyword lists
0162         QVERIFY(!def.keywordLists().isEmpty());
0163         QVERIFY(def.keywordList(QLatin1String("operators")).contains(QLatin1String("and")));
0164         QVERIFY(!def.keywordList(QLatin1String("does not exits")).contains(QLatin1String("and")));
0165     }
0166 
0167     void testFormatData()
0168     {
0169         auto def = m_repo.definitionForName(QLatin1String("ChangeLog"));
0170         QVERIFY(def.isValid());
0171         auto formats = def.formats();
0172         QVERIFY(!formats.isEmpty());
0173 
0174         // verify that the formats are sorted, such that the order matches the order of the itemDatas in the xml files.
0175         auto sortComparator = [](const KSyntaxHighlighting::Format &lhs, const KSyntaxHighlighting::Format &rhs) {
0176             return lhs.id() < rhs.id();
0177         };
0178         QVERIFY(std::is_sorted(formats.begin(), formats.end(), sortComparator));
0179 
0180         // check all names are listed
0181         QStringList formatNames;
0182         for (const auto &format : std::as_const(formats)) {
0183             formatNames.append(format.name());
0184         }
0185 
0186         const QStringList expectedItemDatas = {QStringLiteral("Normal Text"),
0187                                                QStringLiteral("Name"),
0188                                                QStringLiteral("E-Mail"),
0189                                                QStringLiteral("Date"),
0190                                                QStringLiteral("Entry")};
0191         QCOMPARE(formatNames, expectedItemDatas);
0192     }
0193 
0194     void testIncludedDefinitions()
0195     {
0196         auto def = m_repo.definitionForName(QLatin1String("PHP (HTML)"));
0197         QVERIFY(def.isValid());
0198         auto defs = def.includedDefinitions();
0199 
0200         QStringList expectedDefinitionNames = {QStringLiteral("PHP/PHP"),
0201                                                QStringLiteral("Alerts"),
0202                                                QStringLiteral("Comments"),
0203                                                QStringLiteral("CSS/PHP"),
0204                                                QStringLiteral("JavaScript/PHP"),
0205                                                QStringLiteral("JavaScript React (JSX)/PHP"),
0206                                                QStringLiteral("JavaScript"),
0207                                                QStringLiteral("TypeScript/PHP"),
0208                                                QStringLiteral("Mustache/Handlebars (HTML)/PHP"),
0209                                                QStringLiteral("Doxygen"),
0210                                                QStringLiteral("Modelines"),
0211                                                QStringLiteral("SPDX-Comments"),
0212                                                QStringLiteral("HTML"),
0213                                                QStringLiteral("CSS"),
0214                                                QStringLiteral("SQL (MySQL)"),
0215                                                QStringLiteral("JavaScript React (JSX)"),
0216                                                QStringLiteral("TypeScript"),
0217                                                QStringLiteral("Mustache/Handlebars (HTML)")};
0218         QStringList definitionNames;
0219         for (auto d : defs) {
0220             QVERIFY(d.isValid());
0221             definitionNames.push_back(d.name());
0222         }
0223 
0224         // work on sorted lists to make test more stable to changes in structure of XML files
0225         std::sort(expectedDefinitionNames.begin(), expectedDefinitionNames.end());
0226         std::sort(definitionNames.begin(), definitionNames.end());
0227         QCOMPARE(definitionNames, expectedDefinitionNames);
0228     }
0229 
0230     void testIncludedFormats()
0231     {
0232         // ensure we have a efficient numbering of formats
0233         // do this just with one example definition that includes a lot of stuff
0234         // before: checked for all definitions we have, that is n^2 run-time wise
0235         Repository repo;
0236         initRepositorySearchPaths(repo);
0237         auto def = repo.definitionForName(QLatin1String("PHP (HTML)"));
0238         QVERIFY(def.isValid());
0239         auto includedDefs = def.includedDefinitions();
0240         includedDefs.push_front(def);
0241 
0242         // collect all formats, shall be numbered from 1..
0243         QSet<int> formatIds;
0244         for (const auto &d : std::as_const(includedDefs)) {
0245             const auto formats = d.formats();
0246             for (const auto &format : formats) {
0247                 // no duplicates
0248                 QVERIFY(!formatIds.contains(format.id()));
0249                 formatIds.insert(format.id());
0250             }
0251         }
0252 
0253         // ensure all ids are there from 1..size
0254         // this is no longer feasible, as we e.g. skip the "Comments"
0255         // syntax definition in includedDefinitions as it is not contributing
0256         // any context/rule
0257         // for (int i = 1; i <= formatIds.size(); ++i) {
0258         //    QVERIFY(formatIds.contains(i));
0259         //}
0260     }
0261 
0262     void testReload()
0263     {
0264         auto def = m_repo.definitionForName(QLatin1String("QML"));
0265         QVERIFY(!m_repo.definitions().isEmpty());
0266         QVERIFY(def.isValid());
0267 
0268         NullHighlighter hl;
0269         hl.setDefinition(def);
0270         auto oldState = hl.highlightLine(u"/* TODO this should not crash */", State());
0271 
0272         m_repo.reload();
0273         QVERIFY(!m_repo.definitions().isEmpty());
0274         QVERIFY(!def.isValid());
0275 
0276         hl.highlightLine(u"/* TODO this should not crash */", State());
0277         hl.highlightLine(u"/* FIXME neither should this crash */", oldState);
0278         QVERIFY(hl.definition().isValid());
0279         QCOMPARE(hl.definition().name(), QLatin1String("QML"));
0280     }
0281 
0282     void testLifetime()
0283     {
0284         // common mistake with value-type like Repo API, make sure this doesn'T crash
0285         NullHighlighter hl;
0286         {
0287             Repository repo;
0288             hl.setDefinition(repo.definitionForName(QLatin1String("C++")));
0289             hl.setTheme(repo.defaultTheme());
0290         }
0291         hl.highlightLine(u"/**! @todo this should not crash .*/", State());
0292     }
0293 
0294     void testCustomPath()
0295     {
0296         QString testInputPath = QStringLiteral(TESTSRCDIR "/input");
0297 
0298         Repository repo;
0299         QVERIFY(repo.customSearchPaths().empty());
0300         repo.addCustomSearchPath(testInputPath);
0301         QCOMPARE(repo.customSearchPaths().size(), 1);
0302         QCOMPARE(repo.customSearchPaths()[0], testInputPath);
0303 
0304         auto customDefinition = repo.definitionForName(QLatin1String("Test Syntax"));
0305         QVERIFY(customDefinition.isValid());
0306         auto customTheme = repo.theme(QLatin1String("Test Theme"));
0307         QVERIFY(customTheme.isValid());
0308     }
0309 
0310     void testInvalidDefinition()
0311     {
0312         Definition def;
0313         QVERIFY(!def.isValid());
0314         QVERIFY(def.filePath().isEmpty());
0315         QCOMPARE(def.name(), QLatin1String("None"));
0316         QVERIFY(def.section().isEmpty());
0317         QVERIFY(def.translatedSection().isEmpty());
0318         QVERIFY(def.mimeTypes().isEmpty());
0319         QVERIFY(def.extensions().isEmpty());
0320         QCOMPARE(def.version(), 0);
0321         QCOMPARE(def.priority(), 0);
0322         QVERIFY(!def.isHidden());
0323         QVERIFY(def.style().isEmpty());
0324         QVERIFY(def.indenter().isEmpty());
0325         QVERIFY(def.author().isEmpty());
0326         QVERIFY(def.license().isEmpty());
0327         QVERIFY(!def.foldingEnabled());
0328         QVERIFY(!def.indentationBasedFoldingEnabled());
0329         QVERIFY(def.foldingIgnoreList().isEmpty());
0330         QVERIFY(def.keywordLists().isEmpty());
0331         QVERIFY(def.formats().isEmpty());
0332         QVERIFY(def.includedDefinitions().isEmpty());
0333         QVERIFY(def.singleLineCommentMarker().isEmpty());
0334         QCOMPARE(def.singleLineCommentPosition(), KSyntaxHighlighting::CommentPosition::StartOfLine);
0335         const auto emptyPair = QPair<QString, QString>();
0336         QCOMPARE(def.multiLineCommentMarker(), emptyPair);
0337         QVERIFY(def.characterEncodings().isEmpty());
0338 
0339         for (QChar c : QStringLiteral("\t !%&()*+,-./:;<=>?[\\]^{|}~")) {
0340             QVERIFY(def.isWordDelimiter(c));
0341             QVERIFY(def.isWordWrapDelimiter(c));
0342         }
0343     }
0344 
0345     void testDelimiters()
0346     {
0347         auto def = m_repo.definitionForName(QLatin1String("LaTeX"));
0348         QVERIFY(def.isValid());
0349 
0350         // check that backslash '\' and '*' are removed
0351         for (QChar c : QStringLiteral("\t !%&()+,-./:;<=>?[]^{|}~")) {
0352             QVERIFY(def.isWordDelimiter(c));
0353         }
0354         QVERIFY(!def.isWordDelimiter(QLatin1Char('\\')));
0355 
0356         // check where breaking a line is valid
0357         for (QChar c : QStringLiteral(",{}[]")) {
0358             QVERIFY(def.isWordWrapDelimiter(c));
0359         }
0360     }
0361 
0362     void testFoldingEnabled()
0363     {
0364         // test invalid folding
0365         Definition def;
0366         QVERIFY(!def.isValid());
0367         QVERIFY(!def.foldingEnabled());
0368         QVERIFY(!def.indentationBasedFoldingEnabled());
0369 
0370         // test no folding
0371         def = m_repo.definitionForName(QLatin1String("ChangeLog"));
0372         QVERIFY(def.isValid());
0373         QVERIFY(!def.foldingEnabled());
0374         QVERIFY(!def.indentationBasedFoldingEnabled());
0375 
0376         // C++ itself has no regions, but it includes ISO C++
0377         def = m_repo.definitionForName(QLatin1String("C++"));
0378         QVERIFY(def.isValid());
0379         QVERIFY(def.foldingEnabled());
0380         QVERIFY(!def.indentationBasedFoldingEnabled());
0381 
0382         // ISO C++ itself has folding regions
0383         def = m_repo.definitionForName(QLatin1String("ISO C++"));
0384         QVERIFY(def.isValid());
0385         QVERIFY(def.foldingEnabled());
0386         QVERIFY(!def.indentationBasedFoldingEnabled());
0387 
0388         // Python has indentation based folding
0389         def = m_repo.definitionForName(QLatin1String("Python"));
0390         QVERIFY(def.isValid());
0391         QVERIFY(def.foldingEnabled());
0392         QVERIFY(def.indentationBasedFoldingEnabled());
0393     }
0394 
0395     void testCharacterEncodings()
0396     {
0397         auto def = m_repo.definitionForName(QLatin1String("LaTeX"));
0398         QVERIFY(def.isValid());
0399         const auto encodings = def.characterEncodings();
0400         QVERIFY(!encodings.isEmpty());
0401         QVERIFY(encodings.contains({QChar(196), QLatin1String("\\\"{A}")}));
0402         QVERIFY(encodings.contains({QChar(227), QLatin1String("\\~{a}")}));
0403     }
0404 
0405     void testIncludeKeywordLists()
0406     {
0407         Repository repo;
0408         QTemporaryDir dir;
0409 
0410         // forge a syntax file
0411         {
0412             QVERIFY(QDir(dir.path()).mkpath(QLatin1String("syntax")));
0413 
0414             const char syntax[] = R"xml(<?xml version="1.0" encoding="UTF-8"?>
0415             <!DOCTYPE language SYSTEM "language.dtd">
0416             <language version="1" kateversion="5.1" name="AAA" section="Other" extensions="*.a" mimetype="" author="" license="MIT">
0417               <highlighting>
0418                 <list name="a">
0419                   <include>c</include>
0420                   <item>a</item>
0421                 </list>
0422                 <list name="b">
0423                   <item>b</item>
0424                   <include>a</include>
0425                 </list>
0426                 <list name="c">
0427                   <item>c</item>
0428                   <include>b</include>
0429                 </list>
0430 
0431                 <list name="d">
0432                   <item>d</item>
0433                   <include>e##AAA</include>
0434                 </list>
0435                 <list name="e">
0436                   <item>e</item>
0437                   <include>f</include>
0438                 </list>
0439                 <list name="f">
0440                   <item>f</item>
0441                 </list>
0442                 <contexts>
0443                   <context attribute="Normal Text" lineEndContext="#stay" name="Normal Text">
0444                     <keyword attribute="x" context="#stay" String="a" />
0445                   </context>
0446                 </contexts>
0447                 <itemDatas>
0448                   <itemData name="Normal Text" defStyleNum="dsNormal"/>
0449                   <itemData name="x" defStyleNum="dsAlert" />
0450                 </itemDatas>
0451               </highlighting>
0452             </language>
0453             )xml";
0454 
0455             QFile file(dir.path() + QLatin1String("/syntax/a.xml"));
0456             QVERIFY(file.open(QIODevice::WriteOnly));
0457             QTextStream stream(&file);
0458             stream << syntax;
0459         }
0460 
0461         repo.addCustomSearchPath(dir.path());
0462         auto def = repo.definitionForName(QLatin1String("AAA"));
0463         QCOMPARE(def.name(), QLatin1String("AAA"));
0464 
0465         auto klist1 = def.keywordList(QLatin1String("a"));
0466         auto klist2 = def.keywordList(QLatin1String("b"));
0467         auto klist3 = def.keywordList(QLatin1String("c"));
0468 
0469         // internal QHash<QString, KeywordList> is arbitrarily ordered and undeterministic
0470         auto &klist = klist1.size() == 3 ? klist1 : klist2.size() == 3 ? klist2 : klist3;
0471 
0472         QCOMPARE(klist.size(), 3);
0473         QVERIFY(klist.contains(QLatin1String("a")));
0474         QVERIFY(klist.contains(QLatin1String("b")));
0475         QVERIFY(klist.contains(QLatin1String("c")));
0476 
0477         klist = def.keywordList(QLatin1String("d"));
0478         QCOMPARE(klist.size(), 3);
0479         QVERIFY(klist.contains(QLatin1String("d")));
0480         QVERIFY(klist.contains(QLatin1String("e")));
0481         QVERIFY(klist.contains(QLatin1String("f")));
0482 
0483         klist = def.keywordList(QLatin1String("e"));
0484         QCOMPARE(klist.size(), 2);
0485         QVERIFY(klist.contains(QLatin1String("e")));
0486         QVERIFY(klist.contains(QLatin1String("f")));
0487 
0488         klist = def.keywordList(QLatin1String("f"));
0489         QCOMPARE(klist.size(), 1);
0490         QVERIFY(klist.contains(QLatin1String("f")));
0491     }
0492 
0493     void testKeywordListModification()
0494     {
0495         auto def = m_repo.definitionForName(QLatin1String("Python"));
0496         QVERIFY(def.isValid());
0497 
0498         const QStringList &lists = def.keywordLists();
0499         QVERIFY(!lists.isEmpty());
0500 
0501         const QString &listName = lists.first();
0502         const QStringList keywords = def.keywordList(listName);
0503 
0504         QStringList modified = keywords;
0505         modified.append(QLatin1String("test"));
0506 
0507         QVERIFY(def.setKeywordList(listName, modified) == true);
0508         QCOMPARE(keywords + QStringList(QLatin1String("test")), def.keywordList(listName));
0509 
0510         const QString &unexistedName = QLatin1String("unexisted-keyword-name");
0511         QVERIFY(lists.contains(unexistedName) == false);
0512         QVERIFY(def.setKeywordList(unexistedName, QStringList()) == false);
0513     }
0514 
0515     void testAutomaticThemeSelection()
0516     {
0517         QPalette palette;
0518         palette.setColor(QPalette::Base, QColor(27, 30, 32));
0519         palette.setColor(QPalette::Highlight, QColor(61, 174, 253));
0520         auto theme = m_repo.themeForPalette(palette);
0521         QCOMPARE(theme.name(), QStringLiteral("Breeze Dark"));
0522     }
0523 };
0524 }
0525 
0526 QTEST_GUILESS_MAIN(KSyntaxHighlighting::RepositoryTest)
0527 
0528 #include "repository_test.moc"