File indexing completed on 2025-02-02 14:22:24
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, QVector<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::isValidColor(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 ©right : 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 QVector<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 QVERIFY(QColor::isValidColor(editorColors.value(key).toString())); 0313 } 0314 0315 // verify custom-styles if any 0316 { 0317 QVector<QPair<QString, QString>> invalidCustomStyles; 0318 const auto customStyles = obj.value(QLatin1String("custom-styles")).toObject(); 0319 for (auto it = customStyles.constBegin(); it != customStyles.constEnd(); ++it) { 0320 // get definitions for this language 0321 const auto &lang = it.key(); 0322 const auto def = m_repo.definitionForName(lang); 0323 QVERIFY2(def.isValid(), qPrintable(QStringLiteral("Definition %1 does not exist").arg(lang))); 0324 0325 const QVector<Format> fmts = def.formats(); 0326 QSet<QString> fmtNames; 0327 fmtNames.reserve(fmts.size()); 0328 for (const auto &fmt : fmts) { 0329 fmtNames.insert(fmt.name()); 0330 } 0331 0332 // get custom style names for `lang` in this theme 0333 // and make sure the language definition contain that name 0334 const auto customStylesForLang = it.value().toObject(); 0335 for (auto csIt = customStylesForLang.constBegin(); csIt != customStylesForLang.constEnd(); ++csIt) { 0336 // make sure the text style is present in language definition formats 0337 const auto &textStyleName = csIt.key(); 0338 0339 // wasn't found, append it to the vector 0340 // we will later print this and fail the test 0341 if (!fmtNames.contains(textStyleName)) { 0342 invalidCustomStyles.append({lang, textStyleName}); 0343 continue; 0344 } 0345 0346 // now verify this text style 0347 const auto entry = csIt.value().toObject(); 0348 QVector<QString> unknown; 0349 verifyStyle(entry, textStyleName, unknown); 0350 if (!unknown.isEmpty()) { 0351 qWarning() << "Unknown entries found in custom-styles for " << lang << ": " << unknown; 0352 } 0353 QVERIFY(unknown.isEmpty()); 0354 } 0355 } 0356 0357 if (!invalidCustomStyles.isEmpty()) { 0358 qWarning() << "Unknown styles found: " << invalidCustomStyles; 0359 } 0360 QVERIFY(invalidCustomStyles.isEmpty()); 0361 } 0362 0363 // the theme must be available in our repository, too 0364 const auto theme = m_repo.theme(themeName); 0365 QVERIFY(theme.isValid()); 0366 QVERIFY(theme.name() == themeName); 0367 0368 // we have one fixed theme showcase 0369 const QString inFile(QStringLiteral(TESTSRCDIR "/input/themes/showcase.cpp")); 0370 0371 // render some example HTML for the theme, we use that e.g. to show-case the themes on our website 0372 const QString outFile(QStringLiteral(TESTBUILDDIR "/theme.html.output/") + QFileInfo(theme.filePath()).baseName() + QStringLiteral(".html")); 0373 HtmlHighlighter highlighter; 0374 highlighter.setTheme(theme); 0375 QVERIFY(highlighter.theme().isValid()); 0376 highlighter.setDefinition(m_repo.definitionForFileName(inFile)); 0377 highlighter.setOutputFile(outFile); 0378 highlighter.highlightFile(inFile); 0379 } 0380 }; 0381 } 0382 0383 QTEST_GUILESS_MAIN(KSyntaxHighlighting::ThemeTest) 0384 0385 #include "theme_test.moc"