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

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 #include <htmlhighlighter.h>
0016 
0017 #include <QDebug>
0018 #include <QDirIterator>
0019 #include <QFileInfo>
0020 #include <QJsonArray>
0021 #include <QJsonObject>
0022 #include <QJsonParseError>
0023 #include <QObject>
0024 #include <QRegularExpression>
0025 #include <QStandardPaths>
0026 #include <QTest>
0027 
0028 namespace KSyntaxHighlighting
0029 {
0030 class FormatCollector : public AbstractHighlighter
0031 {
0032 public:
0033     using AbstractHighlighter::highlightLine;
0034     void applyFormat(int offset, int length, const Format &format) override
0035     {
0036         Q_UNUSED(offset);
0037         Q_UNUSED(length);
0038         formatMap.insert(format.name(), format);
0039     }
0040     QHash<QString, Format> formatMap;
0041 };
0042 
0043 class ThemeTest : public QObject
0044 {
0045     Q_OBJECT
0046 private:
0047     Repository m_repo;
0048 
0049 private Q_SLOTS:
0050     void initTestCase()
0051     {
0052         QStandardPaths::setTestModeEnabled(true);
0053         initRepositorySearchPaths(m_repo);
0054     }
0055 
0056     void testThemes()
0057     {
0058         QVERIFY(!m_repo.themes().isEmpty());
0059         for (const auto &theme : m_repo.themes()) {
0060             QVERIFY(theme.isValid());
0061             QVERIFY(!theme.name().isEmpty());
0062             QVERIFY(!theme.filePath().isEmpty());
0063             QVERIFY(QFileInfo::exists(theme.filePath()));
0064             QVERIFY(m_repo.theme(theme.name()).isValid());
0065         }
0066     }
0067 
0068     void testFormat_data()
0069     {
0070         QTest::addColumn<QString>("themeName");
0071         QTest::newRow("default") << "Breeze Light";
0072         QTest::newRow("dark") << "Breeze Dark";
0073         QTest::newRow("print") << "Printing";
0074     }
0075 
0076     void testFormat()
0077     {
0078         QFETCH(QString, themeName);
0079 
0080         // somewhat complicated way to get proper Format objects
0081         FormatCollector collector;
0082         collector.setDefinition(m_repo.definitionForName(QLatin1String("QML")));
0083         const auto t = m_repo.theme(themeName);
0084         QVERIFY(t.isValid());
0085         collector.setTheme(t);
0086         collector.highlightLine(u"normal + property real foo: 3.14", State());
0087 
0088         QVERIFY(collector.formatMap.size() >= 4);
0089         // qDebug() << collector.formatMap.keys();
0090 
0091         // normal text
0092         auto f = collector.formatMap.value(QLatin1String("Normal Text"));
0093         QVERIFY(f.isValid());
0094         QVERIFY(f.textStyle() == Theme::Normal);
0095         QVERIFY(f.isDefaultTextStyle(t));
0096         QVERIFY(!f.hasTextColor(t));
0097         QVERIFY(!f.hasBackgroundColor(t));
0098         QVERIFY(f.id() > 0);
0099 
0100         // visually identical to normal text with Printing theme
0101         f = collector.formatMap.value(QLatin1String("Symbol"));
0102         QVERIFY(f.isValid());
0103         QCOMPARE(f.textStyle(), Theme::Operator);
0104         if (themeName == QLatin1String("Printing")) {
0105             QVERIFY(f.isDefaultTextStyle(t));
0106             QVERIFY(!f.hasTextColor(t));
0107         } else {
0108             QVERIFY(!f.isDefaultTextStyle(t));
0109             QVERIFY(f.hasTextColor(t));
0110         }
0111         QVERIFY(f.id() > 0);
0112 
0113         // visually different to normal text
0114         f = collector.formatMap.value(QLatin1String("Keywords"));
0115         QVERIFY(f.isValid());
0116         QCOMPARE(f.textStyle(), Theme::Keyword);
0117         QVERIFY(!f.isDefaultTextStyle(t));
0118         QVERIFY(f.isBold(t));
0119         QVERIFY(f.id() > 0);
0120 
0121         f = collector.formatMap.value(QLatin1String("Float"));
0122         QVERIFY(f.isValid());
0123         QCOMPARE(f.textStyle(), Theme::Float);
0124         QVERIFY(!f.isDefaultTextStyle(t));
0125         QVERIFY(f.hasTextColor(t));
0126         QVERIFY(f.id() > 0);
0127     }
0128 
0129     void testBreezeLightTheme()
0130     {
0131         Theme t = m_repo.theme(QLatin1String("Breeze Light"));
0132         QVERIFY(t.isValid());
0133 
0134         // Themes compiled in as resource are never writable
0135         QVERIFY(t.isReadOnly());
0136 
0137         // make sure all editor colors are properly read
0138         QCOMPARE(t.editorColor(Theme::BackgroundColor), QColor("#ffffff").rgba());
0139         QCOMPARE(t.editorColor(Theme::TextSelection), QColor("#94caef").rgba());
0140         QCOMPARE(t.editorColor(Theme::CurrentLine), QColor("#f8f7f6").rgba());
0141         QCOMPARE(t.editorColor(Theme::SearchHighlight), QColor("#ffff00").rgba());
0142         QCOMPARE(t.editorColor(Theme::ReplaceHighlight), QColor("#00ff00").rgba());
0143         QCOMPARE(t.editorColor(Theme::BracketMatching), QColor("#ffff00").rgba());
0144         QCOMPARE(t.editorColor(Theme::TabMarker), QColor("#d2d2d2").rgba());
0145         QCOMPARE(t.editorColor(Theme::SpellChecking), QColor("#bf0303").rgba());
0146         QCOMPARE(t.editorColor(Theme::IndentationLine), QColor("#d2d2d2").rgba());
0147         QCOMPARE(t.editorColor(Theme::IconBorder), QColor("#f0f0f0").rgba());
0148         QCOMPARE(t.editorColor(Theme::CodeFolding), QColor("#94caef").rgba());
0149         QCOMPARE(t.editorColor(Theme::LineNumbers), QColor("#a0a0a0").rgba());
0150         QCOMPARE(t.editorColor(Theme::CurrentLineNumber), QColor("#1e1e1e").rgba());
0151         QCOMPARE(t.editorColor(Theme::WordWrapMarker), QColor("#ededed").rgba());
0152         QCOMPARE(t.editorColor(Theme::ModifiedLines), QColor("#fdbc4b").rgba());
0153         QCOMPARE(t.editorColor(Theme::SavedLines), QColor("#2ecc71").rgba());
0154         QCOMPARE(t.editorColor(Theme::Separator), QColor("#d5d5d5").rgba());
0155         QCOMPARE(t.editorColor(Theme::MarkBookmark), QColor("#0000ff").rgba());
0156         QCOMPARE(t.editorColor(Theme::MarkBreakpointActive), QColor("#ff0000").rgba());
0157         QCOMPARE(t.editorColor(Theme::MarkBreakpointReached), QColor("#ffff00").rgba());
0158         QCOMPARE(t.editorColor(Theme::MarkBreakpointDisabled), QColor("#ff00ff").rgba());
0159         QCOMPARE(t.editorColor(Theme::MarkExecution), QColor("#a0a0a4").rgba());
0160         QCOMPARE(t.editorColor(Theme::MarkWarning), QColor("#00ff00").rgba());
0161         QCOMPARE(t.editorColor(Theme::MarkError), QColor("#ff0000").rgba());
0162         QCOMPARE(t.editorColor(Theme::TemplateBackground), QColor("#d6d2d0").rgba());
0163         QCOMPARE(t.editorColor(Theme::TemplatePlaceholder), QColor("#baf8ce").rgba());
0164         QCOMPARE(t.editorColor(Theme::TemplateFocusedPlaceholder), QColor("#76da98").rgba());
0165         QCOMPARE(t.editorColor(Theme::TemplateReadOnlyPlaceholder), QColor("#f6e6e6").rgba());
0166     }
0167 
0168     void testInvalidTheme()
0169     {
0170         // somewhat complicated way to get proper Format objects
0171         FormatCollector collector;
0172         collector.setDefinition(m_repo.definitionForName(QLatin1String("QML")));
0173         collector.highlightLine(u"normal + property real foo: 3.14", State());
0174 
0175         QVERIFY(collector.formatMap.size() >= 4);
0176         auto f = collector.formatMap.value(QLatin1String("Normal Text"));
0177         QVERIFY(f.isValid());
0178         QVERIFY(f.isDefaultTextStyle(Theme()));
0179         QVERIFY(!f.hasTextColor(Theme()));
0180         QVERIFY(!f.hasBackgroundColor(Theme()));
0181     }
0182 
0183     void testThemeIntegrity_data()
0184     {
0185         // cleanup before we test
0186         QDir(QStringLiteral(TESTBUILDDIR "/theme.html.output/")).removeRecursively();
0187         QDir().mkpath(QStringLiteral(TESTBUILDDIR "/theme.html.output/"));
0188 
0189         QTest::addColumn<QString>("themeFileName");
0190 
0191         QDirIterator it(QStringLiteral(":/org.kde.syntax-highlighting/themes"), QStringList() << QLatin1String("*.theme"), QDir::Files);
0192         while (it.hasNext()) {
0193             const QString fileName = it.next();
0194             QTest::newRow(fileName.toLatin1().data()) << fileName;
0195         }
0196     }
0197 
0198     static bool isColorEntry(const QString &entry)
0199     {
0200         static const QLatin1String predefColorEntries[] = {QLatin1String("text-color"),
0201                                                            QLatin1String("selected-text-color"),
0202                                                            QLatin1String("background-color"),
0203                                                            QLatin1String("selected-background-color")};
0204 
0205         return std::find(std::begin(predefColorEntries), std::end(predefColorEntries), entry) != std::end(predefColorEntries);
0206     }
0207 
0208     static bool isFontStyleEntry(const QString &entry)
0209     {
0210         static const QLatin1String predefColorEntries[] = {QLatin1String("bold"),
0211                                                            QLatin1String("italic"),
0212                                                            QLatin1String("underline"),
0213                                                            QLatin1String("strike-through")};
0214 
0215         return std::find(std::begin(predefColorEntries), std::end(predefColorEntries), entry) != std::end(predefColorEntries);
0216     }
0217 
0218     void verifyStyle(const QJsonObject &textStyle, const QString &textStyleName, QList<QString> &unknown)
0219     {
0220         const QStringList definedColors = textStyle.keys();
0221         for (const auto &key : definedColors) {
0222             const QString context = textStyleName + QLatin1Char('/') + key + QLatin1Char('=') + textStyle.value(key).toString();
0223             if (isColorEntry(key)) {
0224                 QVERIFY2(QColor::isValidColorName(textStyle.value(key).toString()), context.toLatin1().data());
0225             } else if (isFontStyleEntry(key)) {
0226                 QVERIFY2(textStyle.value(key).isBool(), context.toLatin1().data());
0227             } else {
0228                 unknown.append(textStyleName + QLatin1Char('/') + key);
0229             }
0230         }
0231     }
0232 
0233     void testThemeIntegrity()
0234     {
0235         QFETCH(QString, themeFileName);
0236 
0237         QFile loadFile(themeFileName);
0238         QVERIFY(loadFile.open(QIODevice::ReadOnly));
0239         const QByteArray jsonData = loadFile.readAll();
0240         QJsonParseError parseError;
0241         QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &parseError);
0242         if (parseError.error != QJsonParseError::NoError) {
0243             qWarning() << "Failed to parse theme file:" << parseError.errorString();
0244             QVERIFY(false);
0245         }
0246 
0247         QJsonObject obj = jsonDoc.object();
0248 
0249         // verify metadata
0250         QVERIFY(obj.contains(QLatin1String("metadata")));
0251         const QJsonObject metadata = obj.value(QLatin1String("metadata")).toObject();
0252         QVERIFY(metadata.contains(QLatin1String("name")));
0253         const auto themeName = metadata.value(QLatin1String("name")).toString();
0254         QVERIFY(!themeName.isEmpty());
0255         QVERIFY(metadata.contains(QLatin1String("revision")));
0256         QVERIFY(metadata.value(QLatin1String("revision")).toInt() > 0);
0257 
0258         // verify licensing part of the metadata
0259 
0260         // ensure we have some copyright text attributes like "SPDX-FileCopyrightText: 2020 Christoph Cullmann <cullmann@kde.org>"
0261         QVERIFY(metadata.contains(QLatin1String("copyright")));
0262         const auto copyrights = metadata.value(QLatin1String("copyright")).toArray();
0263         QVERIFY(!copyrights.empty());
0264         for (const auto &copyright : copyrights) {
0265             static const QRegularExpression copyrightRegex(QLatin1String("SPDX-FileCopyrightText: \\d{4} "));
0266             QVERIFY(copyright.toString().indexOf(copyrightRegex) == 0);
0267         }
0268 
0269         // ensure the theme is MIT licensed with a proper SPDX identifier
0270         // we always compile all themes into the library as resources, we want to have a "pure" MIT licensed library!
0271         QVERIFY(metadata.contains(QLatin1String("license")));
0272         QVERIFY(metadata.value(QLatin1String("license")).toString() == QLatin1String("SPDX-License-Identifier: MIT"));
0273 
0274         // verify completeness of text styles
0275         const auto metaEnum = QMetaEnum::fromType<Theme::TextStyle>();
0276         QVERIFY(obj.contains(QLatin1String("text-styles")));
0277         const QJsonObject textStyles = obj.value(QLatin1String("text-styles")).toObject();
0278         for (int i = 0; i < metaEnum.keyCount(); ++i) {
0279             QCOMPARE(i, metaEnum.value(i));
0280             const QString textStyleName = QLatin1String(metaEnum.key(i));
0281             QVERIFY(textStyles.contains(textStyleName));
0282             const QJsonObject textStyle = textStyles.value(textStyleName).toObject();
0283             QVERIFY(textStyle.contains(QLatin1String("text-color")));
0284 
0285             // verify valid entry
0286             QList<QString> unknown;
0287             verifyStyle(textStyle, textStyleName, unknown);
0288             if (!unknown.isEmpty()) {
0289                 qWarning() << "Unknown entries found in text-styles: " << unknown;
0290             }
0291             QVERIFY(unknown.isEmpty());
0292         }
0293 
0294         // editor area colors
0295         const auto metaEnumColor = QMetaEnum::fromType<Theme::EditorColorRole>();
0296         QStringList requiredEditorColors;
0297         for (int i = 0; i < metaEnumColor.keyCount(); ++i) {
0298             Q_ASSERT(i == metaEnumColor.value(i));
0299             requiredEditorColors.append(QLatin1String(metaEnumColor.key(i)));
0300         }
0301         std::sort(requiredEditorColors.begin(), requiredEditorColors.end());
0302 
0303         // verify all editor colors are defined - not more, not less
0304         QVERIFY(obj.contains(QLatin1String("editor-colors")));
0305         const QJsonObject editorColors = obj.value(QLatin1String("editor-colors")).toObject();
0306         QStringList definedEditorColors = editorColors.keys();
0307         std::sort(definedEditorColors.begin(), definedEditorColors.end());
0308         QCOMPARE(definedEditorColors, requiredEditorColors);
0309 
0310         // verify all editor colors are valid
0311         for (const auto &key : requiredEditorColors) {
0312             auto color = editorColors.value(key).toString();
0313             QVERIFY2(QColor::isValidColorName(color), color.toStdString().c_str());
0314         }
0315 
0316         // verify custom-styles if any
0317         {
0318             QList<QPair<QString, QString>> invalidCustomStyles;
0319             const auto customStyles = obj.value(QLatin1String("custom-styles")).toObject();
0320             for (auto it = customStyles.constBegin(); it != customStyles.constEnd(); ++it) {
0321                 // get definitions for this language
0322                 const auto &lang = it.key();
0323                 const auto def = m_repo.definitionForName(lang);
0324                 QVERIFY2(def.isValid(), qPrintable(QStringLiteral("Definition %1 does not exist").arg(lang)));
0325 
0326                 const QList<Format> fmts = def.formats();
0327                 QSet<QString> fmtNames;
0328                 fmtNames.reserve(fmts.size());
0329                 for (const auto &fmt : fmts) {
0330                     fmtNames.insert(fmt.name());
0331                 }
0332 
0333                 // get custom style names for `lang` in this theme
0334                 // and make sure the language definition contain that name
0335                 const auto customStylesForLang = it.value().toObject();
0336                 for (auto csIt = customStylesForLang.constBegin(); csIt != customStylesForLang.constEnd(); ++csIt) {
0337                     // make sure the text style is present in language definition formats
0338                     const auto &textStyleName = csIt.key();
0339 
0340                     // wasn't found, append it to the vector
0341                     // we will later print this and fail the test
0342                     if (!fmtNames.contains(textStyleName)) {
0343                         invalidCustomStyles.append({lang, textStyleName});
0344                         continue;
0345                     }
0346 
0347                     // now verify this text style
0348                     const auto entry = csIt.value().toObject();
0349                     QList<QString> unknown;
0350                     verifyStyle(entry, textStyleName, unknown);
0351                     if (!unknown.isEmpty()) {
0352                         qWarning() << "Unknown entries found in custom-styles for " << lang << ": " << unknown;
0353                     }
0354                     QVERIFY(unknown.isEmpty());
0355                 }
0356             }
0357 
0358             if (!invalidCustomStyles.isEmpty()) {
0359                 qWarning() << "Unknown styles found: " << invalidCustomStyles;
0360             }
0361             QVERIFY(invalidCustomStyles.isEmpty());
0362         }
0363 
0364         // the theme must be available in our repository, too
0365         const auto theme = m_repo.theme(themeName);
0366         QVERIFY(theme.isValid());
0367         QVERIFY(theme.name() == themeName);
0368 
0369         // we have one fixed theme showcase
0370         const QString inFile(QStringLiteral(TESTSRCDIR "/input/themes/showcase.cpp"));
0371 
0372         // render some example HTML for the theme, we use that e.g. to show-case the themes on our website
0373         const QString outFile(QStringLiteral(TESTBUILDDIR "/theme.html.output/") + QFileInfo(theme.filePath()).baseName() + QStringLiteral(".html"));
0374         HtmlHighlighter highlighter;
0375         highlighter.setTheme(theme);
0376         QVERIFY(highlighter.theme().isValid());
0377         highlighter.setDefinition(m_repo.definitionForFileName(inFile));
0378         highlighter.setOutputFile(outFile);
0379         highlighter.highlightFile(inFile);
0380     }
0381 };
0382 }
0383 
0384 QTEST_GUILESS_MAIN(KSyntaxHighlighting::ThemeTest)
0385 
0386 #include "theme_test.moc"