File indexing completed on 2024-05-05 16:22:33

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