File indexing completed on 2024-05-05 04:01:41

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"