File indexing completed on 2025-02-02 14:22:23
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"