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"