/* SPDX-FileCopyrightText: 2016 Volker Krause SPDX-License-Identifier: MIT */ #include "repository_test_base.h" #include "test-config.h" #include #include #include #include #include #include #include #include #include #include namespace KSyntaxHighlighting { class NullHighlighter : public AbstractHighlighter { public: using AbstractHighlighter::highlightLine; void applyFormat(int offset, int length, const Format &format) override { Q_UNUSED(offset); Q_UNUSED(length); // only here to ensure we don't crash format.isDefaultTextStyle(theme()); format.textColor(theme()); } }; class RepositoryTest : public RepositoryTestBase { Q_OBJECT private Q_SLOTS: void testDefinitionByExtension_data() { definitionByExtensionTestData(); } void testDefinitionByExtension() { QFETCH(QString, fileName); QFETCH(QString, definitionName); definitionByExtensionTest(fileName, definitionName); } void testDefinitionsForFileName_data() { definitionsForFileNameTestData(); } void testDefinitionsForFileName() { QFETCH(QString, fileName); QFETCH(QStringList, definitionNames); definitionsForFileNameTest(fileName, definitionNames); } void testDefinitionForMimeType_data() { definitionForMimeTypeTestData(); } void testDefinitionForMimeType() { QFETCH(QString, mimeTypeName); QFETCH(QString, definitionName); definitionForMimeTypeTest(mimeTypeName, definitionName); } void testDefinitionsForMimeType_data() { definitionsForMimeTypeTestData(); } void testDefinitionsForMimeType() { QFETCH(QString, mimeTypeName); QFETCH(QStringList, definitionNames); definitionsForMimeTypeTest(mimeTypeName, definitionNames); } void testLoadAll() { for (const auto &def : m_repo.definitions()) { QVERIFY(!def.name().isEmpty()); QVERIFY(!def.translatedName().isEmpty()); QVERIFY(!def.isValid() || !def.section().isEmpty()); QVERIFY(!def.isValid() || !def.translatedSection().isEmpty()); // indirectly trigger loading, as we can't reach that from public API // if the loading fails the highlighter will produce empty states NullHighlighter hl; State initialState; hl.setDefinition(def); const auto state = hl.highlightLine(u"This should not crash } ] ) !", initialState); QVERIFY(!def.isValid() || state != initialState || def.name() == QLatin1String("Broken Syntax")); } } void testMetaData() { auto def = m_repo.definitionForName(QLatin1String("Alerts")); QVERIFY(def.isValid()); QVERIFY(def.extensions().isEmpty()); QVERIFY(def.mimeTypes().isEmpty()); QVERIFY(def.version() >= 1.11f); QVERIFY(def.isHidden()); QCOMPARE(def.section(), QLatin1String("Other")); QCOMPARE(def.license(), QLatin1String("MIT")); QVERIFY(def.author().contains(QLatin1String("Dominik"))); QFileInfo fi(def.filePath()); QVERIFY(fi.isAbsolute()); QVERIFY(def.filePath().endsWith(QLatin1String("alert.xml"))); def = m_repo.definitionForName(QLatin1String("C++")); QVERIFY(def.isValid()); QCOMPARE(def.section(), QLatin1String("Sources")); QCOMPARE(def.indenter(), QLatin1String("cstyle")); QCOMPARE(def.style(), QLatin1String("C++")); QVERIFY(def.mimeTypes().contains(QLatin1String("text/x-c++hdr"))); QVERIFY(def.extensions().contains(QLatin1String("*.h"))); QCOMPARE(def.priority(), 9); def = m_repo.definitionForName(QLatin1String("Apache Configuration")); QVERIFY(def.isValid()); QVERIFY(def.extensions().contains(QLatin1String("httpd.conf"))); QVERIFY(def.extensions().contains(QLatin1String(".htaccess*"))); } void testGeneralMetaData() { auto def = m_repo.definitionForName(QLatin1String("C++")); QVERIFY(def.isValid()); QVERIFY(!def.indentationBasedFoldingEnabled()); // comment markers QCOMPARE(def.singleLineCommentMarker(), QLatin1String("//")); QCOMPARE(def.singleLineCommentPosition(), KSyntaxHighlighting::CommentPosition::AfterWhitespace); const auto cppMultiLineCommentMarker = QPair(QLatin1String("/*"), QLatin1String("*/")); QCOMPARE(def.multiLineCommentMarker(), cppMultiLineCommentMarker); def = m_repo.definitionForName(QLatin1String("Python")); QVERIFY(def.isValid()); // indentation QVERIFY(def.indentationBasedFoldingEnabled()); QCOMPARE(def.foldingIgnoreList(), QStringList() << QLatin1String("(?:\\s+|\\s*#.*)")); // keyword lists QVERIFY(!def.keywordLists().isEmpty()); QVERIFY(def.keywordList(QLatin1String("operators")).contains(QLatin1String("and"))); QVERIFY(!def.keywordList(QLatin1String("does not exits")).contains(QLatin1String("and"))); } void testFormatData() { auto def = m_repo.definitionForName(QLatin1String("ChangeLog")); QVERIFY(def.isValid()); auto formats = def.formats(); QVERIFY(!formats.isEmpty()); // verify that the formats are sorted, such that the order matches the order of the itemDatas in the xml files. auto sortComparator = [](const KSyntaxHighlighting::Format &lhs, const KSyntaxHighlighting::Format &rhs) { return lhs.id() < rhs.id(); }; QVERIFY(std::is_sorted(formats.begin(), formats.end(), sortComparator)); // check all names are listed QStringList formatNames; for (const auto &format : std::as_const(formats)) { formatNames.append(format.name()); } const QStringList expectedItemDatas = {QStringLiteral("Normal Text"), QStringLiteral("Name"), QStringLiteral("E-Mail"), QStringLiteral("Date"), QStringLiteral("Entry")}; QCOMPARE(formatNames, expectedItemDatas); } void testIncludedDefinitions() { auto def = m_repo.definitionForName(QLatin1String("PHP (HTML)")); QVERIFY(def.isValid()); auto defs = def.includedDefinitions(); QStringList expectedDefinitionNames = {QStringLiteral("PHP/PHP"), QStringLiteral("Alerts"), QStringLiteral("Comments"), QStringLiteral("CSS/PHP"), QStringLiteral("JavaScript/PHP"), QStringLiteral("JavaScript React (JSX)/PHP"), QStringLiteral("JavaScript"), QStringLiteral("TypeScript/PHP"), QStringLiteral("Mustache/Handlebars (HTML)/PHP"), QStringLiteral("Doxygen"), QStringLiteral("Modelines"), QStringLiteral("SPDX-Comments"), QStringLiteral("HTML"), QStringLiteral("CSS"), QStringLiteral("SQL (MySQL)"), QStringLiteral("JavaScript React (JSX)"), QStringLiteral("TypeScript"), QStringLiteral("Mustache/Handlebars (HTML)")}; QStringList definitionNames; for (auto d : defs) { QVERIFY(d.isValid()); definitionNames.push_back(d.name()); } // work on sorted lists to make test more stable to changes in structure of XML files std::sort(expectedDefinitionNames.begin(), expectedDefinitionNames.end()); std::sort(definitionNames.begin(), definitionNames.end()); QCOMPARE(definitionNames, expectedDefinitionNames); } void testIncludedFormats() { // ensure we have a efficient numbering of formats // do this just with one example definition that includes a lot of stuff // before: checked for all definitions we have, that is n^2 run-time wise Repository repo; initRepositorySearchPaths(repo); auto def = repo.definitionForName(QLatin1String("PHP (HTML)")); QVERIFY(def.isValid()); auto includedDefs = def.includedDefinitions(); includedDefs.push_front(def); // collect all formats, shall be numbered from 1.. QSet formatIds; for (const auto &d : std::as_const(includedDefs)) { const auto formats = d.formats(); for (const auto &format : formats) { // no duplicates QVERIFY(!formatIds.contains(format.id())); formatIds.insert(format.id()); } } // ensure all ids are there from 1..size // this is no longer feasible, as we e.g. skip the "Comments" // syntax definition in includedDefinitions as it is not contributing // any context/rule // for (int i = 1; i <= formatIds.size(); ++i) { // QVERIFY(formatIds.contains(i)); //} } void testReload() { auto def = m_repo.definitionForName(QLatin1String("QML")); QVERIFY(!m_repo.definitions().isEmpty()); QVERIFY(def.isValid()); NullHighlighter hl; hl.setDefinition(def); auto oldState = hl.highlightLine(u"/* TODO this should not crash */", State()); m_repo.reload(); QVERIFY(!m_repo.definitions().isEmpty()); QVERIFY(!def.isValid()); hl.highlightLine(u"/* TODO this should not crash */", State()); hl.highlightLine(u"/* FIXME neither should this crash */", oldState); QVERIFY(hl.definition().isValid()); QCOMPARE(hl.definition().name(), QLatin1String("QML")); } void testLifetime() { // common mistake with value-type like Repo API, make sure this doesn'T crash NullHighlighter hl; { Repository repo; hl.setDefinition(repo.definitionForName(QLatin1String("C++"))); hl.setTheme(repo.defaultTheme()); } hl.highlightLine(u"/**! @todo this should not crash .*/", State()); } void testCustomPath() { QString testInputPath = QStringLiteral(TESTSRCDIR "/input"); Repository repo; QVERIFY(repo.customSearchPaths().empty()); repo.addCustomSearchPath(testInputPath); QCOMPARE(repo.customSearchPaths().size(), 1); QCOMPARE(repo.customSearchPaths()[0], testInputPath); auto customDefinition = repo.definitionForName(QLatin1String("Test Syntax")); QVERIFY(customDefinition.isValid()); auto customTheme = repo.theme(QLatin1String("Test Theme")); QVERIFY(customTheme.isValid()); } void testInvalidDefinition() { Definition def; QVERIFY(!def.isValid()); QVERIFY(def.filePath().isEmpty()); QCOMPARE(def.name(), QLatin1String("None")); QVERIFY(def.section().isEmpty()); QVERIFY(def.translatedSection().isEmpty()); QVERIFY(def.mimeTypes().isEmpty()); QVERIFY(def.extensions().isEmpty()); QCOMPARE(def.version(), 0); QCOMPARE(def.priority(), 0); QVERIFY(!def.isHidden()); QVERIFY(def.style().isEmpty()); QVERIFY(def.indenter().isEmpty()); QVERIFY(def.author().isEmpty()); QVERIFY(def.license().isEmpty()); QVERIFY(!def.foldingEnabled()); QVERIFY(!def.indentationBasedFoldingEnabled()); QVERIFY(def.foldingIgnoreList().isEmpty()); QVERIFY(def.keywordLists().isEmpty()); QVERIFY(def.formats().isEmpty()); QVERIFY(def.includedDefinitions().isEmpty()); QVERIFY(def.singleLineCommentMarker().isEmpty()); QCOMPARE(def.singleLineCommentPosition(), KSyntaxHighlighting::CommentPosition::StartOfLine); const auto emptyPair = QPair(); QCOMPARE(def.multiLineCommentMarker(), emptyPair); QVERIFY(def.characterEncodings().isEmpty()); for (QChar c : QStringLiteral("\t !%&()*+,-./:;<=>?[\\]^{|}~")) { QVERIFY(def.isWordDelimiter(c)); QVERIFY(def.isWordWrapDelimiter(c)); } } void testDelimiters() { auto def = m_repo.definitionForName(QLatin1String("LaTeX")); QVERIFY(def.isValid()); // check that backslash '\' and '*' are removed for (QChar c : QStringLiteral("\t !%&()+,-./:;<=>?[]^{|}~")) { QVERIFY(def.isWordDelimiter(c)); } QVERIFY(!def.isWordDelimiter(QLatin1Char('\\'))); // check where breaking a line is valid for (QChar c : QStringLiteral(",{}[]")) { QVERIFY(def.isWordWrapDelimiter(c)); } } void testFoldingEnabled() { // test invalid folding Definition def; QVERIFY(!def.isValid()); QVERIFY(!def.foldingEnabled()); QVERIFY(!def.indentationBasedFoldingEnabled()); // test no folding def = m_repo.definitionForName(QLatin1String("ChangeLog")); QVERIFY(def.isValid()); QVERIFY(!def.foldingEnabled()); QVERIFY(!def.indentationBasedFoldingEnabled()); // C++ itself has no regions, but it includes ISO C++ def = m_repo.definitionForName(QLatin1String("C++")); QVERIFY(def.isValid()); QVERIFY(def.foldingEnabled()); QVERIFY(!def.indentationBasedFoldingEnabled()); // ISO C++ itself has folding regions def = m_repo.definitionForName(QLatin1String("ISO C++")); QVERIFY(def.isValid()); QVERIFY(def.foldingEnabled()); QVERIFY(!def.indentationBasedFoldingEnabled()); // Python has indentation based folding def = m_repo.definitionForName(QLatin1String("Python")); QVERIFY(def.isValid()); QVERIFY(def.foldingEnabled()); QVERIFY(def.indentationBasedFoldingEnabled()); } void testCharacterEncodings() { auto def = m_repo.definitionForName(QLatin1String("LaTeX")); QVERIFY(def.isValid()); const auto encodings = def.characterEncodings(); QVERIFY(!encodings.isEmpty()); QVERIFY(encodings.contains({QChar(196), QLatin1String("\\\"{A}")})); QVERIFY(encodings.contains({QChar(227), QLatin1String("\\~{a}")})); } void testIncludeKeywordLists() { Repository repo; QTemporaryDir dir; // forge a syntax file { QVERIFY(QDir(dir.path()).mkpath(QLatin1String("syntax"))); const char syntax[] = R"xml( c a b a c b d e##AAA e f f )xml"; QFile file(dir.path() + QLatin1String("/syntax/a.xml")); QVERIFY(file.open(QIODevice::WriteOnly)); QTextStream stream(&file); stream << syntax; } repo.addCustomSearchPath(dir.path()); auto def = repo.definitionForName(QLatin1String("AAA")); QCOMPARE(def.name(), QLatin1String("AAA")); auto klist1 = def.keywordList(QLatin1String("a")); auto klist2 = def.keywordList(QLatin1String("b")); auto klist3 = def.keywordList(QLatin1String("c")); // internal QHash is arbitrarily ordered and undeterministic auto &klist = klist1.size() == 3 ? klist1 : klist2.size() == 3 ? klist2 : klist3; QCOMPARE(klist.size(), 3); QVERIFY(klist.contains(QLatin1String("a"))); QVERIFY(klist.contains(QLatin1String("b"))); QVERIFY(klist.contains(QLatin1String("c"))); klist = def.keywordList(QLatin1String("d")); QCOMPARE(klist.size(), 3); QVERIFY(klist.contains(QLatin1String("d"))); QVERIFY(klist.contains(QLatin1String("e"))); QVERIFY(klist.contains(QLatin1String("f"))); klist = def.keywordList(QLatin1String("e")); QCOMPARE(klist.size(), 2); QVERIFY(klist.contains(QLatin1String("e"))); QVERIFY(klist.contains(QLatin1String("f"))); klist = def.keywordList(QLatin1String("f")); QCOMPARE(klist.size(), 1); QVERIFY(klist.contains(QLatin1String("f"))); } void testKeywordListModification() { auto def = m_repo.definitionForName(QLatin1String("Python")); QVERIFY(def.isValid()); const QStringList &lists = def.keywordLists(); QVERIFY(!lists.isEmpty()); const QString &listName = lists.first(); const QStringList keywords = def.keywordList(listName); QStringList modified = keywords; modified.append(QLatin1String("test")); QVERIFY(def.setKeywordList(listName, modified) == true); QCOMPARE(keywords + QStringList(QLatin1String("test")), def.keywordList(listName)); const QString &unexistedName = QLatin1String("unexisted-keyword-name"); QVERIFY(lists.contains(unexistedName) == false); QVERIFY(def.setKeywordList(unexistedName, QStringList()) == false); } void testAutomaticThemeSelection() { QPalette palette; palette.setColor(QPalette::Base, QColor(27, 30, 32)); palette.setColor(QPalette::Highlight, QColor(61, 174, 253)); auto theme = m_repo.themeForPalette(palette); QCOMPARE(theme.name(), QStringLiteral("Breeze Dark")); } }; } QTEST_GUILESS_MAIN(KSyntaxHighlighting::RepositoryTest) #include "repository_test.moc"