/* SPDX-FileCopyrightText: 2016 Volker Krause SPDX-License-Identifier: MIT */ #include "test-config.h" #include #include #include #include #include #include #include "state_p.h" #include #include #include #include #include #include using namespace KSyntaxHighlighting; class TestHighlighter : public AbstractHighlighter { public: int highlightFile(const QString &inFileName, const QString &outFileName) { QFile outFile(outFileName); if (!outFile.open(QFile::WriteOnly | QFile::Truncate)) { qWarning() << "Failed to open output file" << outFileName << ":" << outFile.errorString(); return 0; } m_out.setDevice(&outFile); QFile f(inFileName); if (!f.open(QFile::ReadOnly)) { qWarning() << "Failed to open input file" << inFileName << ":" << f.errorString(); return 0; } QTextStream in(&f); State state; while (in.readLineInto(&m_currentLine)) { state = highlightLine(m_currentLine, state); m_out << "
\n"; } m_out.flush(); auto *stateData = StateData::get(state); return stateData ? stateData->size() : 0; } protected: void applyFormat(int offset, int length, const Format &format) override { if (format.name().isEmpty()) { m_out << "" << QStringView(m_currentLine).mid(offset, length) << ""; } else { m_out << "<" << format.name() << ">" << QStringView(m_currentLine).mid(offset, length) << ""; } } private: QTextStream m_out; QString m_currentLine; }; class TestHighlighterTest : public QObject { Q_OBJECT public: explicit TestHighlighterTest(QObject *parent = nullptr) : QObject(parent) , m_repo(nullptr) { } private: Repository *m_repo; std::map m_coveredDefinitions; private Q_SLOTS: void initTestCase() { QStandardPaths::setTestModeEnabled(true); m_repo = new Repository; initRepositorySearchPaths(*m_repo); } void cleanupTestCase() { QFile coveredList(QLatin1String(TESTBUILDDIR "/covered-definitions.txt")); QFile uncoveredList(QLatin1String(TESTBUILDDIR "/uncovered-definition.txt")); QVERIFY(coveredList.open(QFile::WriteOnly)); QVERIFY(uncoveredList.open(QFile::WriteOnly)); int count = 0; for (const auto &def : m_repo->definitions()) { if (!def.isValid()) { continue; } ++count; if (m_coveredDefinitions.find(def.name()) != m_coveredDefinitions.end()) { coveredList.write(def.name().toUtf8() + '\n'); } else { uncoveredList.write(def.name().toUtf8() + '\n'); } } qDebug() << "Syntax definitions with test coverage:" << ((float)m_coveredDefinitions.size() * 100.0f / (float)count) << "%"; // we don't want multiple tests for the same highlighting // tests should be consolidated into one useful file per highlighting // the update script for https://kate-editor.org/syntax/ will check that no duplicated output is there, too bool duplicates = false; for (const auto &entry : std::as_const(m_coveredDefinitions)) { if (entry.second.size() <= 1) { continue; } // abort and tell about duplicated test cases! qWarning() << "Multiple unit tests for the language " << entry.first; for (const auto &testCase : entry.second) { qWarning() << " - " << testCase; } duplicates = true; } if (duplicates) { QFAIL("Multiple unit tests for the same language found, see for details the output above!"); } delete m_repo; m_repo = nullptr; } void testHighlight_data() { QTest::addColumn("inFile"); QTest::addColumn("outFile"); QTest::addColumn("refFile"); QTest::addColumn("syntax"); const QDir dir(QStringLiteral(TESTSRCDIR "/input")); for (const auto &fileName : dir.entryList(QDir::Files | QDir::NoSymLinks | QDir::Readable | QDir::Hidden, QDir::Name)) { // skip .clang-format file we use to avoid formatting test files if (fileName == QLatin1String(".clang-format")) { continue; } const auto inFile = dir.absoluteFilePath(fileName); if (inFile.endsWith(QLatin1String(".syntax"))) { continue; } QString syntax; QFile syntaxOverride(inFile + QStringLiteral(".syntax")); if (syntaxOverride.exists() && syntaxOverride.open(QFile::ReadOnly)) { syntax = QString::fromUtf8(syntaxOverride.readAll()).trimmed(); } QTest::newRow(fileName.toUtf8().constData()) << inFile << (QStringLiteral(TESTBUILDDIR "/output/") + fileName + QStringLiteral(".ref")) << (QStringLiteral(TESTSRCDIR "/reference/") + fileName + QStringLiteral(".ref")) << syntax; } // cleanup before we test QDir(QStringLiteral(TESTBUILDDIR "/output/")).removeRecursively(); QDir().mkpath(QStringLiteral(TESTBUILDDIR "/output/")); } void testHighlight() { QFETCH(QString, inFile); QFETCH(QString, outFile); QFETCH(QString, refFile); QFETCH(QString, syntax); QVERIFY(m_repo); auto def = m_repo->definitionForFileName(inFile); if (!syntax.isEmpty()) { def = m_repo->definitionForName(syntax); } TestHighlighter highlighter; highlighter.setTheme(m_repo->defaultTheme()); QVERIFY(highlighter.theme().isValid()); QVERIFY(def.isValid()); qDebug() << "Using syntax" << def.name(); m_coveredDefinitions[def.name()].push_back(inFile); highlighter.setDefinition(def); int lastStackSize = highlighter.highlightFile(inFile, outFile); // arbitrary condition to detect syntaxes that stack contexts indefinitely // instead of going back to the parent QVERIFY2(lastStackSize < 15, qPrintable(def.name() + QStringLiteral("seems to stack contexts indefinitely, perhaps #pop's are missing?"))); /** * compare results */ compareFiles(refFile, outFile); } }; QTEST_GUILESS_MAIN(TestHighlighterTest) #include "testhighlighter.moc"