File indexing completed on 2025-03-23 03:46:11
0001 /* 0002 SPDX-FileCopyrightText: 2016 Volker Krause <vkrause@kde.org> 0003 0004 SPDX-License-Identifier: MIT 0005 */ 0006 0007 #include "test-config.h" 0008 0009 #include <KSyntaxHighlighting/AbstractHighlighter> 0010 #include <KSyntaxHighlighting/Definition> 0011 #include <KSyntaxHighlighting/Format> 0012 #include <KSyntaxHighlighting/Repository> 0013 #include <KSyntaxHighlighting/State> 0014 #include <KSyntaxHighlighting/Theme> 0015 0016 #include "state_p.h" 0017 0018 #include <QDir> 0019 #include <QFile> 0020 #include <QObject> 0021 #include <QTest> 0022 #include <QTextStream> 0023 0024 #include <map> 0025 0026 using namespace KSyntaxHighlighting; 0027 0028 class TestHighlighter : public AbstractHighlighter 0029 { 0030 public: 0031 int highlightFile(const QString &inFileName, const QString &outFileName) 0032 { 0033 QFile outFile(outFileName); 0034 if (!outFile.open(QFile::WriteOnly | QFile::Truncate)) { 0035 qWarning() << "Failed to open output file" << outFileName << ":" << outFile.errorString(); 0036 return 0; 0037 } 0038 m_out.setDevice(&outFile); 0039 0040 QFile f(inFileName); 0041 if (!f.open(QFile::ReadOnly)) { 0042 qWarning() << "Failed to open input file" << inFileName << ":" << f.errorString(); 0043 return 0; 0044 } 0045 0046 QTextStream in(&f); 0047 State state; 0048 while (in.readLineInto(&m_currentLine)) { 0049 state = highlightLine(m_currentLine, state); 0050 m_out << "<br/>\n"; 0051 } 0052 0053 m_out.flush(); 0054 auto *stateData = StateData::get(state); 0055 return stateData ? stateData->size() : 0; 0056 } 0057 0058 protected: 0059 void applyFormat(int offset, int length, const Format &format) override 0060 { 0061 if (format.name().isEmpty()) { 0062 m_out << "<dsNormal>" << QStringView(m_currentLine).mid(offset, length) << "</dsNormal>"; 0063 } else { 0064 m_out << "<" << format.name() << ">" << QStringView(m_currentLine).mid(offset, length) << "</" << format.name() << ">"; 0065 } 0066 } 0067 0068 private: 0069 QTextStream m_out; 0070 QString m_currentLine; 0071 }; 0072 0073 class TestHighlighterTest : public QObject 0074 { 0075 Q_OBJECT 0076 public: 0077 explicit TestHighlighterTest(QObject *parent = nullptr) 0078 : QObject(parent) 0079 , m_repo(nullptr) 0080 { 0081 } 0082 0083 private: 0084 Repository *m_repo; 0085 std::map<QString, QStringList> m_coveredDefinitions; 0086 0087 private Q_SLOTS: 0088 void initTestCase() 0089 { 0090 QStandardPaths::setTestModeEnabled(true); 0091 m_repo = new Repository; 0092 initRepositorySearchPaths(*m_repo); 0093 } 0094 0095 void cleanupTestCase() 0096 { 0097 QFile coveredList(QLatin1String(TESTBUILDDIR "/covered-definitions.txt")); 0098 QFile uncoveredList(QLatin1String(TESTBUILDDIR "/uncovered-definition.txt")); 0099 QVERIFY(coveredList.open(QFile::WriteOnly)); 0100 QVERIFY(uncoveredList.open(QFile::WriteOnly)); 0101 0102 int count = 0; 0103 for (const auto &def : m_repo->definitions()) { 0104 if (!def.isValid()) { 0105 continue; 0106 } 0107 ++count; 0108 if (m_coveredDefinitions.find(def.name()) != m_coveredDefinitions.end()) { 0109 coveredList.write(def.name().toUtf8() + '\n'); 0110 } else { 0111 uncoveredList.write(def.name().toUtf8() + '\n'); 0112 } 0113 } 0114 0115 qDebug() << "Syntax definitions with test coverage:" << ((float)m_coveredDefinitions.size() * 100.0f / (float)count) << "%"; 0116 0117 // we don't want multiple tests for the same highlighting 0118 // tests should be consolidated into one useful file per highlighting 0119 // the update script for https://kate-editor.org/syntax/ will check that no duplicated output is there, too 0120 bool duplicates = false; 0121 for (const auto &entry : std::as_const(m_coveredDefinitions)) { 0122 if (entry.second.size() <= 1) { 0123 continue; 0124 } 0125 0126 // abort and tell about duplicated test cases! 0127 qWarning() << "Multiple unit tests for the language " << entry.first; 0128 for (const auto &testCase : entry.second) { 0129 qWarning() << " - " << testCase; 0130 } 0131 duplicates = true; 0132 } 0133 if (duplicates) { 0134 QFAIL("Multiple unit tests for the same language found, see for details the output above!"); 0135 } 0136 0137 delete m_repo; 0138 m_repo = nullptr; 0139 } 0140 0141 void testHighlight_data() 0142 { 0143 QTest::addColumn<QString>("inFile"); 0144 QTest::addColumn<QString>("outFile"); 0145 QTest::addColumn<QString>("refFile"); 0146 QTest::addColumn<QString>("syntax"); 0147 0148 const QDir dir(QStringLiteral(TESTSRCDIR "/input")); 0149 for (const auto &fileName : dir.entryList(QDir::Files | QDir::NoSymLinks | QDir::Readable | QDir::Hidden, QDir::Name)) { 0150 // skip .clang-format file we use to avoid formatting test files 0151 if (fileName == QLatin1String(".clang-format")) { 0152 continue; 0153 } 0154 0155 const auto inFile = dir.absoluteFilePath(fileName); 0156 if (inFile.endsWith(QLatin1String(".syntax"))) { 0157 continue; 0158 } 0159 0160 QString syntax; 0161 QFile syntaxOverride(inFile + QStringLiteral(".syntax")); 0162 if (syntaxOverride.exists() && syntaxOverride.open(QFile::ReadOnly)) { 0163 syntax = QString::fromUtf8(syntaxOverride.readAll()).trimmed(); 0164 } 0165 0166 QTest::newRow(fileName.toUtf8().constData()) << inFile << (QStringLiteral(TESTBUILDDIR "/output/") + fileName + QStringLiteral(".ref")) 0167 << (QStringLiteral(TESTSRCDIR "/reference/") + fileName + QStringLiteral(".ref")) << syntax; 0168 } 0169 0170 // cleanup before we test 0171 QDir(QStringLiteral(TESTBUILDDIR "/output/")).removeRecursively(); 0172 QDir().mkpath(QStringLiteral(TESTBUILDDIR "/output/")); 0173 } 0174 0175 void testHighlight() 0176 { 0177 QFETCH(QString, inFile); 0178 QFETCH(QString, outFile); 0179 QFETCH(QString, refFile); 0180 QFETCH(QString, syntax); 0181 QVERIFY(m_repo); 0182 0183 auto def = m_repo->definitionForFileName(inFile); 0184 if (!syntax.isEmpty()) { 0185 def = m_repo->definitionForName(syntax); 0186 } 0187 0188 TestHighlighter highlighter; 0189 highlighter.setTheme(m_repo->defaultTheme()); 0190 QVERIFY(highlighter.theme().isValid()); 0191 0192 QVERIFY(def.isValid()); 0193 qDebug() << "Using syntax" << def.name(); 0194 m_coveredDefinitions[def.name()].push_back(inFile); 0195 highlighter.setDefinition(def); 0196 int lastStackSize = highlighter.highlightFile(inFile, outFile); 0197 // arbitrary condition to detect syntaxes that stack contexts indefinitely 0198 // instead of going back to the parent 0199 QVERIFY2(lastStackSize < 15, qPrintable(def.name() + QStringLiteral("seems to stack contexts indefinitely, perhaps #pop's are missing?"))); 0200 0201 /** 0202 * compare results 0203 */ 0204 compareFiles(refFile, outFile); 0205 } 0206 }; 0207 0208 QTEST_GUILESS_MAIN(TestHighlighterTest) 0209 0210 #include "testhighlighter.moc"