File indexing completed on 2024-04-14 03:50:04
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_scalableDuplicates_data() 0243 { 0244 test_scalable_data(false); 0245 } 0246 0247 void test_scalableDuplicates() 0248 { 0249 QFETCH(QList<std::shared_ptr<Dir>>, dirs); 0250 0251 QList<std::shared_ptr<Dir>> scalableDirs; 0252 for (auto dir : dirs) { 0253 switch (dir->type) { 0254 case KIconLoaderDummy::Scalable: 0255 scalableDirs << dir; 0256 break; 0257 case KIconLoaderDummy::Fixed: 0258 // Not of interest in this test. 0259 break; 0260 case KIconLoaderDummy::Threshold: 0261 QVERIFY2(false, "Test does not support threshold icons right now."); 0262 } 0263 } 0264 0265 QHash<QString, QList<QFileInfo>> scalableIcons; 0266 for (auto dir : scalableDirs) { 0267 for (auto iconInfo : dir->allIcons()) { 0268 scalableIcons[iconInfo.completeBaseName()].append(iconInfo); 0269 } 0270 } 0271 0272 QHash<QString, QList<QFileInfo>> duplicatedScalableIcons; 0273 for (auto icon : scalableIcons.keys()) { 0274 auto list = scalableIcons[icon]; 0275 if (list.size() > 1) { 0276 duplicatedScalableIcons[icon] = list; 0277 } 0278 } 0279 0280 // Assert that there is only one scalable version per icon name. 0281 // Otherwise apps/32/klipper.svg OR apps/48/klipper.svg may be used. 0282 if (!duplicatedScalableIcons.empty()) { 0283 QString msg; 0284 QTextStream stream(&msg); 0285 stream << "Duplicated scalable icons:\n"; 0286 for (const auto &icon : duplicatedScalableIcons.keys()) { 0287 stream << QStringLiteral(" %1:").arg(icon) << '\n'; 0288 for (const auto &info : duplicatedScalableIcons[icon]) { 0289 stream << QStringLiteral(" %1").arg(info.absoluteFilePath()) << '\n'; 0290 } 0291 } 0292 stream.flush(); 0293 QFAIL(qPrintable(msg)); 0294 } 0295 } 0296 }; 0297 0298 QTEST_GUILESS_MAIN(ScalableTest) 0299 0300 #include "scalabletest.moc"