File indexing completed on 2024-03-24 15:24:28
0001 /* 0002 Copyright 2017 Harald Sitter <sitter@kde.org> 0003 Copyright 2017 Sune Vuorela <sune@kde.org> 0004 0005 This library is free software; you can redistribute it and/or 0006 modify it under the terms of the GNU Lesser General Public 0007 License as published by the Free Software Foundation; either 0008 version 2.1 of the License, or (at your option) version 3, or any 0009 later version accepted by the membership of KDE e.V. (or its 0010 successor approved by the membership of KDE e.V.), which shall 0011 act as a proxy defined in Section 6 of version 3 of the license. 0012 0013 This library is distributed in the hope that it will be useful, 0014 but WITHOUT ANY WARRANTY; without even the implied warranty of 0015 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 0016 Lesser General Public License for more details. 0017 0018 You should have received a copy of the GNU Lesser General Public 0019 License along with this library. If not, see <http://www.gnu.org/licenses/>. 0020 */ 0021 0022 #include <QDirIterator> 0023 #include <QObject> 0024 #include <QTest> 0025 0026 #include "testhelpers.h" 0027 #include <QSettings> // parsing the ini files as desktop files 0028 0029 // lift a bit of code from KIconLoader to get the unit test running without tier 3 libraries 0030 class KIconLoaderDummy : public QObject 0031 { 0032 Q_OBJECT 0033 public: 0034 enum Context { 0035 Invalid = -1, 0036 Any, 0037 Action, 0038 Application, 0039 Device, 0040 FileSystem, 0041 MimeType, 0042 Animation, 0043 Category, 0044 Emblem, 0045 Emote, 0046 International, 0047 Place, 0048 StatusIcon, 0049 }; 0050 Q_ENUM(Context) 0051 enum Type { 0052 Fixed, 0053 Scalable, 0054 Threshold, 0055 }; 0056 Q_ENUM(Type) 0057 }; 0058 0059 /** 0060 * Represents icon directory to conduct simple icon lookup within. 0061 */ 0062 class Dir 0063 { 0064 public: 0065 Dir(const QSettings &cg, const QString &themeDir_) 0066 : themeDir(themeDir_) 0067 , path(cg.group()) 0068 , size(cg.value("Size", 0).toInt()) 0069 , contextString(cg.value("Context", QString()).toString()) 0070 , context(parseContext(contextString)) 0071 , type(parseType(cg.value("Type", QStringLiteral("Threshold")).toString())) 0072 { 0073 QVERIFY2(!contextString.isEmpty(), 0074 QStringLiteral("Missing 'Context' key in file %1, config group '[%2]'").arg(cg.fileName(), cg.group()).toLatin1().constData()); 0075 QVERIFY2(context != -1, 0076 QStringLiteral("Don't know how to handle 'Context=%1' in file %2, config group '[%3]'") 0077 .arg(contextString, cg.fileName(), cg.group()) 0078 .toLatin1() 0079 .constData()); 0080 } 0081 0082 static QMetaEnum findEnum(const char *name) 0083 { 0084 auto mo = KIconLoaderDummy::staticMetaObject; 0085 for (int i = 0; i < mo.enumeratorCount(); ++i) { 0086 auto enumerator = mo.enumerator(i); 0087 if (strcmp(enumerator.name(), name) == 0) { 0088 return KIconLoaderDummy::staticMetaObject.enumerator(i); 0089 } 0090 } 0091 Q_ASSERT(false); // failed to resolve enum 0092 return QMetaEnum(); 0093 } 0094 0095 static QMetaEnum typeEnum() 0096 { 0097 static auto e = findEnum("Type"); 0098 return e; 0099 } 0100 0101 static KIconLoaderDummy::Context parseContext(const QString &string) 0102 { 0103 // Can't use QMetaEnum as the enum names are singular, the entry values are plural though. 0104 static QHash<QString, int> hash{ 0105 {QStringLiteral("Actions"), KIconLoaderDummy::Action}, 0106 {QStringLiteral("Animations"), KIconLoaderDummy::Animation}, 0107 {QStringLiteral("Applications"), KIconLoaderDummy::Application}, 0108 {QStringLiteral("Categories"), KIconLoaderDummy::Category}, 0109 {QStringLiteral("Devices"), KIconLoaderDummy::Device}, 0110 {QStringLiteral("Emblems"), KIconLoaderDummy::Emblem}, 0111 {QStringLiteral("Emotes"), KIconLoaderDummy::Emote}, 0112 {QStringLiteral("MimeTypes"), KIconLoaderDummy::MimeType}, 0113 {QStringLiteral("Places"), KIconLoaderDummy::Place}, 0114 {QStringLiteral("Status"), KIconLoaderDummy::StatusIcon}, 0115 }; 0116 const auto value = hash.value(string, KIconLoaderDummy::Invalid); 0117 return static_cast<KIconLoaderDummy::Context>(value); // the caller will check that it wasn't "Invalid" 0118 } 0119 0120 static KIconLoaderDummy::Type parseType(const QString &string) 0121 { 0122 bool ok; 0123 auto v = (KIconLoaderDummy::Type)typeEnum().keyToValue(string.toLatin1().constData(), &ok); 0124 Q_ASSERT(ok); 0125 return v; 0126 } 0127 0128 /** 0129 * @returns list of all icon's fileinfo (first level only, selected types 0130 * only) 0131 */ 0132 QList<QFileInfo> allIcons() 0133 { 0134 QList<QFileInfo> icons; 0135 auto iconDir = QStringLiteral("%1/%2").arg(themeDir).arg(path); 0136 QDirIterator it(iconDir); 0137 while (it.hasNext()) { 0138 it.next(); 0139 auto suffix = it.fileInfo().suffix(); 0140 if (suffix != "svg" && suffix != "svgz" && suffix != "png") { 0141 continue; // Probably not an icon. 0142 } 0143 icons << it.fileInfo(); 0144 } 0145 return icons; 0146 } 0147 0148 QString themeDir; 0149 QString path; 0150 uint size; 0151 QString contextString; 0152 KIconLoaderDummy::Context context; 0153 KIconLoaderDummy::Type type; 0154 }; 0155 0156 // Declare so we can put them into the QTest data table. 0157 Q_DECLARE_METATYPE(KIconLoaderDummy::Context) 0158 Q_DECLARE_METATYPE(std::shared_ptr<Dir>) 0159 0160 class ScalableTest : public QObject 0161 { 0162 Q_OBJECT 0163 0164 private Q_SLOTS: 0165 void test_scalable_data(bool checkInherits=true) 0166 { 0167 for (auto dir : ICON_DIRS) { 0168 QString themeDir = PROJECT_SOURCE_DIR + QStringLiteral("/") + dir; 0169 0170 QHash<KIconLoaderDummy::Context, QList<std::shared_ptr<Dir>>> contextHash; 0171 QHash<KIconLoaderDummy::Context, QString> contextStringHash; 0172 0173 QSettings config(themeDir + "/index.theme", QSettings::IniFormat); 0174 auto keys = config.allKeys(); 0175 0176 config.beginGroup("Icon Theme"); 0177 auto directoryPaths = config.value("Directories", QString()).toStringList(); 0178 directoryPaths += config.value("ScaledDirectories", QString()).toStringList(); 0179 config.endGroup(); 0180 0181 QVERIFY(!directoryPaths.isEmpty()); 0182 for (auto directoryPath : directoryPaths) { 0183 config.beginGroup(directoryPath); 0184 QVERIFY2(keys.contains(directoryPath + "/Size"), 0185 QStringLiteral("The theme %1 has an entry 'Directories' which specifies '%2' as directory, but it has no associated entry '%2/Size'") 0186 .arg(themeDir + "/index.theme", directoryPath) 0187 .toLatin1() 0188 .constData()); 0189 auto dir = std::make_shared<Dir>(config, themeDir); 0190 config.endGroup(); 0191 contextHash[dir->context].append(dir); 0192 contextStringHash[dir->context] = (dir->contextString); 0193 } 0194 0195 // Also check in the normal theme if it's listed in Inherits 0196 config.beginGroup("Icon Theme"); 0197 auto inherits = config.value("Inherits", QString()).toStringList(); 0198 if (checkInherits && inherits.contains(QStringLiteral("breeze"))) { 0199 QString inheritedDir = PROJECT_SOURCE_DIR + QStringLiteral("/icons"); 0200 0201 QSettings inheritedConfig(inheritedDir + "/index.theme", QSettings::IniFormat); 0202 auto inheritedKeys = inheritedConfig.allKeys(); 0203 0204 inheritedConfig.beginGroup("Icon Theme"); 0205 auto inheritedPaths = config.value("Directories", QString()).toStringList(); 0206 inheritedPaths += config.value("ScaledDirectories", QString()).toStringList(); 0207 inheritedConfig.endGroup(); 0208 0209 QVERIFY(!inheritedPaths.empty()); 0210 for (const auto& path : inheritedPaths) { 0211 inheritedConfig.beginGroup(path); 0212 QVERIFY2( 0213 inheritedKeys.contains(path + "/Size"), 0214 QStringLiteral("The theme %1 has an entry 'Directories' which specifies '%2' as directory, but it has no associated entry '%2/Size'") 0215 .arg(inheritedDir + "/index.theme", path) 0216 .toLatin1() 0217 .constData()); 0218 auto dir = std::make_shared<Dir>(inheritedConfig, inheritedDir); 0219 inheritedConfig.endGroup(); 0220 contextHash[dir->context].append(dir); 0221 contextStringHash[dir->context] = (dir->contextString); 0222 } 0223 } 0224 config.endGroup(); 0225 0226 QTest::addColumn<KIconLoaderDummy::Context>("context"); 0227 QTest::addColumn<QList<std::shared_ptr<Dir>>>("dirs"); 0228 0229 for (auto key : contextHash.keys()) { 0230 if (key != KIconLoaderDummy::Application) { 0231 qDebug() << "Only testing Application context for now."; 0232 continue; 0233 } 0234 // FIXME: go through qenum to stringify the bugger 0235 // Gets rid of the stupid second hash 0236 auto contextId = QString(QLatin1String(dir) + ":" + contextStringHash[key]).toLatin1(); 0237 QTest::newRow(contextId.constData()) << key << contextHash[key]; 0238 } 0239 } 0240 } 0241 0242 void test_scalable() 0243 { 0244 QFETCH(KIconLoaderDummy::Context, context); 0245 QFETCH(QList<std::shared_ptr<Dir>>, dirs); 0246 0247 QList<std::shared_ptr<Dir>> fixedDirs; 0248 QList<std::shared_ptr<Dir>> scalableDirs; 0249 for (auto dir : dirs) { 0250 switch (dir->type) { 0251 case KIconLoaderDummy::Scalable: 0252 scalableDirs << dir; 0253 break; 0254 case KIconLoaderDummy::Fixed: 0255 fixedDirs << dir; 0256 break; 0257 case KIconLoaderDummy::Threshold: 0258 QVERIFY2(false, "Test does not support threshold icons right now."); 0259 } 0260 } 0261 0262 // FIXME: context should be translated through qenum 0263 switch (context) { 0264 case KIconLoaderDummy::Application: 0265 // Treat this as a problem. 0266 QVERIFY2(!scalableDirs.empty(), "This icon context has no scalable directory at all!"); 0267 break; 0268 default: 0269 qWarning() << "All context but Application are whitelisted from having a scalable directory."; 0270 return; 0271 } 0272 0273 QStringList fixedIcons; 0274 for (auto dir : fixedDirs) { 0275 for (auto iconInfo : dir->allIcons()) { 0276 fixedIcons << iconInfo.completeBaseName(); 0277 } 0278 } 0279 0280 QHash<QString, QList<QFileInfo>> scalableIcons; 0281 for (auto dir : scalableDirs) { 0282 for (auto iconInfo : dir->allIcons()) { 0283 scalableIcons[iconInfo.completeBaseName()].append(iconInfo); 0284 } 0285 } 0286 0287 QStringList notScalableIcons; 0288 for (auto fixed : fixedIcons) { 0289 if (scalableIcons.keys().contains(fixed)) { 0290 continue; 0291 } 0292 notScalableIcons << fixed; 0293 } 0294 0295 // Assert that each icon has a scalable variant. 0296 if (notScalableIcons.empty()) { 0297 return; 0298 } 0299 notScalableIcons.removeDuplicates(); 0300 QFAIL(QStringLiteral("The following icons are not available in a scalable directory:\n %1").arg(notScalableIcons.join("\n ")).toLatin1().constData()); 0301 } 0302 0303 void test_scalableDuplicates_data() 0304 { 0305 test_scalable_data(false); 0306 } 0307 0308 void test_scalableDuplicates() 0309 { 0310 QFETCH(QList<std::shared_ptr<Dir>>, dirs); 0311 0312 QList<std::shared_ptr<Dir>> scalableDirs; 0313 for (auto dir : dirs) { 0314 switch (dir->type) { 0315 case KIconLoaderDummy::Scalable: 0316 scalableDirs << dir; 0317 break; 0318 case KIconLoaderDummy::Fixed: 0319 // Not of interest in this test. 0320 break; 0321 case KIconLoaderDummy::Threshold: 0322 QVERIFY2(false, "Test does not support threshold icons right now."); 0323 } 0324 } 0325 0326 QHash<QString, QList<QFileInfo>> scalableIcons; 0327 for (auto dir : scalableDirs) { 0328 for (auto iconInfo : dir->allIcons()) { 0329 scalableIcons[iconInfo.completeBaseName()].append(iconInfo); 0330 } 0331 } 0332 0333 QHash<QString, QList<QFileInfo>> duplicatedScalableIcons; 0334 for (auto icon : scalableIcons.keys()) { 0335 auto list = scalableIcons[icon]; 0336 if (list.size() > 1) { 0337 duplicatedScalableIcons[icon] = list; 0338 } 0339 } 0340 0341 // Assert that there is only one scalable version per icon name. 0342 // Otherwise apps/32/klipper.svg OR apps/48/klipper.svg may be used. 0343 if (!duplicatedScalableIcons.empty()) { 0344 QString msg; 0345 QTextStream stream(&msg); 0346 stream << "Duplicated scalable icons:\n"; 0347 for (const auto &icon : duplicatedScalableIcons.keys()) { 0348 stream << QStringLiteral(" %1:").arg(icon) << '\n'; 0349 for (const auto &info : duplicatedScalableIcons[icon]) { 0350 stream << QStringLiteral(" %1").arg(info.absoluteFilePath()) << '\n'; 0351 } 0352 } 0353 stream.flush(); 0354 QFAIL(qPrintable(msg)); 0355 } 0356 } 0357 }; 0358 0359 QTEST_GUILESS_MAIN(ScalableTest) 0360 0361 #include "scalabletest.moc"