File indexing completed on 2024-04-21 14:59:41

0001 /*
0002     This file is part of the KDE project
0003     SPDX-FileCopyrightText: 2006-2007 David Faure <faure@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include <array>
0009 
0010 #include "jobuidelegatefactory.h"
0011 #include "kdirmodeltest.h"
0012 #include <KDirWatch>
0013 #include <kdirlister.h>
0014 #include <kdirnotify.h>
0015 #include <kio/chmodjob.h>
0016 #include <kio/copyjob.h>
0017 #include <kio/deletejob.h>
0018 #include <kio/simplejob.h>
0019 #include <kprotocolinfo.h>
0020 
0021 #include <QDebug>
0022 #include <QMimeData>
0023 #include <QSignalSpy>
0024 #include <QUrl>
0025 
0026 #ifdef Q_OS_UNIX
0027 #include <utime.h>
0028 #endif
0029 #include "kiotesthelper.h"
0030 #include "mockcoredelegateextensions.h"
0031 
0032 QTEST_MAIN(KDirModelTest)
0033 
0034 static QString specialChars()
0035 {
0036 #ifndef Q_OS_WIN
0037     return QStringLiteral(" special chars%:.pdf");
0038 #else
0039     return QStringLiteral(" special chars%.pdf");
0040 #endif
0041 }
0042 
0043 Q_DECLARE_METATYPE(KFileItemList)
0044 
0045 void KDirModelTest::initTestCase()
0046 {
0047     qputenv("LC_ALL", "en_US.UTF-8");
0048 
0049     QStandardPaths::setTestModeEnabled(true);
0050 
0051     qRegisterMetaType<KFileItemList>("KFileItemList");
0052 
0053     m_dirModelForExpand = nullptr;
0054     m_dirModel = nullptr;
0055     s_referenceTimeStamp = QDateTime::currentDateTime().addSecs(-30); // 30 seconds ago
0056     m_topLevelFileNames << QStringLiteral("toplevelfile_1") << QStringLiteral("toplevelfile_2") << QStringLiteral("toplevelfile_3") << specialChars();
0057     recreateTestData();
0058 
0059     fillModel(false);
0060 }
0061 
0062 void KDirModelTest::recreateTestData()
0063 {
0064     if (m_tempDir) {
0065         qDebug() << "Deleting old tempdir" << m_tempDir->path();
0066         m_tempDir.reset();
0067         qApp->processEvents(); // process inotify events so they don't pollute us later on
0068     }
0069 
0070     m_tempDir = std::make_unique<QTemporaryDir>(homeTmpDir());
0071     qDebug() << "new tmp dir:" << m_tempDir->path();
0072     // Create test data:
0073     /*
0074      * PATH/toplevelfile_1
0075      * PATH/toplevelfile_2
0076      * PATH/toplevelfile_3
0077      * PATH/special chars%:.pdf
0078      * PATH/.hiddenfile
0079      * PATH/.hiddenfile2
0080      * PATH/subdir
0081      * PATH/subdir/testfile
0082      * PATH/subdir/testsymlink
0083      * PATH/subdir/subsubdir
0084      * PATH/subdir/subsubdir/testfile
0085      * PATH/subdir/hasChildren
0086      * PATH/subdir/hasChildren/emptyDir
0087      * PATH/subdir/hasChildren/hiddenfileDir
0088      * PATH/subdir/hasChildren/hiddenfileDir/.hidden
0089      * PATH/subdir/hasChildren/hiddenDirDir
0090      * PATH/subdir/hasChildren/hiddenDirDir/.hidden
0091      * PATH/subdir/hasChildren/symlinkDir
0092      * PATH/subdir/hasChildren/symlinkDir/link
0093      * PATH/subdir/hasChildren/pipeDir
0094      * PATH/subdir/hasChildren/pipeDir/pipe
0095      */
0096     const QString path = m_tempDir->path() + '/';
0097     for (const QString &f : std::as_const(m_topLevelFileNames)) {
0098         createTestFile(path + f);
0099     }
0100     createTestFile(path + ".hiddenfile");
0101     createTestFile(path + ".hiddenfile2");
0102     createTestDirectory(path + "subdir");
0103     createTestDirectory(path + "subdir/subsubdir", NoSymlink);
0104     createTestDirectory(path + "subdir/hasChildren", Empty);
0105     createTestDirectory(path + "subdir/hasChildren/emptyDir", Empty);
0106     createTestDirectory(path + "subdir/hasChildren/hiddenfileDir", Empty);
0107     createTestFile(path + "subdir/hasChildren/hiddenfileDir/.hidden");
0108     createTestDirectory(path + "subdir/hasChildren/hiddenDirDir", Empty);
0109     createTestDirectory(path + "subdir/hasChildren/hiddenDirDir/.hidden", Empty);
0110     createTestDirectory(path + "subdir/hasChildren/symlinkDir", Empty);
0111     createTestSymlink(path + "subdir/hasChildren/symlinkDir/link", QString(path + "toplevelfile_1").toUtf8());
0112     createTestDirectory(path + "subdir/hasChildren/pipeDir", Empty);
0113     createTestPipe(path + "subdir/hasChildren/pipeDir/pipe");
0114 
0115     m_dirIndex = QModelIndex();
0116     m_fileIndex = QModelIndex();
0117     m_secondFileIndex = QModelIndex();
0118 }
0119 
0120 void KDirModelTest::cleanupTestCase()
0121 {
0122     m_tempDir.reset();
0123 
0124     delete m_dirModel;
0125     m_dirModel = nullptr;
0126 
0127     delete m_dirModelForExpand;
0128     m_dirModelForExpand = nullptr;
0129 }
0130 
0131 void KDirModelTest::fillModel(bool reload, bool expectAllIndexes)
0132 {
0133     if (!m_dirModel) {
0134         m_dirModel = new KDirModel;
0135     }
0136     m_dirModel->dirLister()->setAutoErrorHandlingEnabled(false);
0137     const QString path = m_tempDir->path() + '/';
0138     KDirLister *dirLister = m_dirModel->dirLister();
0139     qDebug() << "Calling openUrl";
0140     m_dirModel->openUrl(QUrl::fromLocalFile(path), reload ? KDirModel::Reload : KDirModel::NoFlags);
0141     QSignalSpy spyCompleted(dirLister, qOverload<>(&KCoreDirLister::completed));
0142     QVERIFY(spyCompleted.wait());
0143 
0144     if (expectAllIndexes) {
0145         collectKnownIndexes();
0146     }
0147 }
0148 
0149 // Called after test function
0150 void KDirModelTest::cleanup()
0151 {
0152     if (m_dirModel) {
0153         disconnect(m_dirModel->dirLister(), nullptr, this, nullptr);
0154         m_dirModel->dirLister()->setNameFilter(QString());
0155         m_dirModel->dirLister()->setMimeFilter(QStringList());
0156         m_dirModel->dirLister()->emitChanges();
0157     }
0158 }
0159 
0160 void KDirModelTest::collectKnownIndexes()
0161 {
0162     m_dirIndex = QModelIndex();
0163     m_fileIndex = QModelIndex();
0164     m_secondFileIndex = QModelIndex();
0165     // Create the indexes once and for all
0166     // The trouble is that the order of listing is undefined, one can get 1/2/3/subdir or subdir/3/2/1 for instance.
0167     for (int row = 0; row < m_topLevelFileNames.count() + 1 /*subdir*/; ++row) {
0168         QModelIndex idx = m_dirModel->index(row, 0, QModelIndex());
0169         QVERIFY(idx.isValid());
0170         KFileItem item = m_dirModel->itemForIndex(idx);
0171         qDebug() << item.url() << "isDir=" << item.isDir();
0172         QString fileName = item.url().fileName();
0173         if (item.isDir()) {
0174             m_dirIndex = idx;
0175         } else if (fileName == QLatin1String("toplevelfile_1")) {
0176             m_fileIndex = idx;
0177         } else if (fileName == QLatin1String("toplevelfile_2")) {
0178             m_secondFileIndex = idx;
0179         } else if (fileName.startsWith(QLatin1String(" special"))) {
0180             m_specialFileIndex = idx;
0181         }
0182     }
0183     QVERIFY(m_dirIndex.isValid());
0184     QVERIFY(m_fileIndex.isValid());
0185     QVERIFY(m_secondFileIndex.isValid());
0186     QVERIFY(m_specialFileIndex.isValid());
0187 
0188     // Now list subdir/
0189     QSignalSpy spyCompleted(m_dirModel->dirLister(), qOverload<>(&KCoreDirLister::completed));
0190     QVERIFY(m_dirModel->canFetchMore(m_dirIndex));
0191     m_dirModel->fetchMore(m_dirIndex);
0192     qDebug() << "Listing subdir/";
0193     if (spyCompleted.isEmpty()) {
0194         QVERIFY(spyCompleted.wait());
0195     }
0196 
0197     // Index of a file inside a directory (subdir/testfile)
0198     QModelIndex subdirIndex;
0199     m_fileInDirIndex = QModelIndex();
0200     for (int row = 0; row < 4; ++row) {
0201         QModelIndex idx = m_dirModel->index(row, 0, m_dirIndex);
0202         const KFileItem item = m_dirModel->itemForIndex(idx);
0203         if (item.isDir() && item.name() == QLatin1String("subsubdir")) {
0204             subdirIndex = idx;
0205         } else if (item.name() == QLatin1String("testfile")) {
0206             m_fileInDirIndex = idx;
0207         }
0208     }
0209 
0210     // List subdir/subsubdir
0211     QVERIFY(m_dirModel->canFetchMore(subdirIndex));
0212     qDebug() << "Listing subdir/subsubdir";
0213     spyCompleted.clear();
0214     m_dirModel->fetchMore(subdirIndex);
0215     QVERIFY(spyCompleted.wait());
0216 
0217     // Index of ... well, subdir/subsubdir/testfile
0218     m_fileInSubdirIndex = m_dirModel->index(0, 0, subdirIndex);
0219 }
0220 
0221 void KDirModelTest::testRowCount()
0222 {
0223     const int topLevelRowCount = m_dirModel->rowCount();
0224     QCOMPARE(topLevelRowCount, m_topLevelFileNames.count() + 1 /*subdir*/);
0225     const int subdirRowCount = m_dirModel->rowCount(m_dirIndex);
0226     QCOMPARE(subdirRowCount, 4);
0227 
0228     QVERIFY(m_fileIndex.isValid());
0229     const int fileRowCount = m_dirModel->rowCount(m_fileIndex); // #176555
0230     QCOMPARE(fileRowCount, 0);
0231 }
0232 
0233 void KDirModelTest::testIndex()
0234 {
0235     QVERIFY(m_dirModel->hasChildren());
0236 
0237     // Index of the first file
0238     QVERIFY(m_fileIndex.isValid());
0239     QCOMPARE(m_fileIndex.model(), static_cast<const QAbstractItemModel *>(m_dirModel));
0240     // QCOMPARE(m_fileIndex.row(), 0);
0241     QCOMPARE(m_fileIndex.column(), 0);
0242     QVERIFY(!m_fileIndex.parent().isValid());
0243     QVERIFY(!m_dirModel->hasChildren(m_fileIndex));
0244 
0245     // Index of a directory
0246     QVERIFY(m_dirIndex.isValid());
0247     QCOMPARE(m_dirIndex.model(), static_cast<const QAbstractItemModel *>(m_dirModel));
0248     // QCOMPARE(m_dirIndex.row(), 3); // ordering isn't guaranteed
0249     QCOMPARE(m_dirIndex.column(), 0);
0250     QVERIFY(!m_dirIndex.parent().isValid());
0251     QVERIFY(m_dirModel->hasChildren(m_dirIndex));
0252 
0253     // Index of a file inside a directory (subdir/testfile)
0254     QVERIFY(m_fileInDirIndex.isValid());
0255     QCOMPARE(m_fileInDirIndex.model(), static_cast<const QAbstractItemModel *>(m_dirModel));
0256     // QCOMPARE(m_fileInDirIndex.row(), 0); // ordering isn't guaranteed
0257     QCOMPARE(m_fileInDirIndex.column(), 0);
0258     QVERIFY(m_fileInDirIndex.parent() == m_dirIndex);
0259     QVERIFY(!m_dirModel->hasChildren(m_fileInDirIndex));
0260 
0261     // Index of subdir/subsubdir/testfile
0262     QVERIFY(m_fileInSubdirIndex.isValid());
0263     QCOMPARE(m_fileInSubdirIndex.model(), static_cast<const QAbstractItemModel *>(m_dirModel));
0264     QCOMPARE(m_fileInSubdirIndex.row(), 0); // we can check it because it's the only file there
0265     QCOMPARE(m_fileInSubdirIndex.column(), 0);
0266     QVERIFY(m_fileInSubdirIndex.parent().parent() == m_dirIndex);
0267     QVERIFY(!m_dirModel->hasChildren(m_fileInSubdirIndex));
0268 
0269     // Test sibling() by going from subdir/testfile to subdir/subsubdir
0270     const QModelIndex subsubdirIndex = m_fileInSubdirIndex.parent();
0271     QVERIFY(subsubdirIndex.isValid());
0272     QModelIndex sibling1 = m_dirModel->sibling(subsubdirIndex.row(), 0, m_fileInDirIndex);
0273     QVERIFY(sibling1.isValid());
0274     QVERIFY(sibling1 == subsubdirIndex);
0275     QVERIFY(m_dirModel->hasChildren(subsubdirIndex));
0276 
0277     // Invalid sibling call
0278     QVERIFY(!m_dirModel->sibling(2, 0, m_fileInSubdirIndex).isValid());
0279 
0280     // Test index() with a valid parent (dir).
0281     QModelIndex index2 = m_dirModel->index(m_fileInSubdirIndex.row(), m_fileInSubdirIndex.column(), subsubdirIndex);
0282     QVERIFY(index2.isValid());
0283     QVERIFY(index2 == m_fileInSubdirIndex);
0284 
0285     // Test index() with a non-parent (file).
0286     QModelIndex index3 = m_dirModel->index(m_fileInSubdirIndex.row(), m_fileInSubdirIndex.column(), m_fileIndex);
0287     QVERIFY(!index3.isValid());
0288 }
0289 
0290 void KDirModelTest::testNames()
0291 {
0292     QString fileName = m_dirModel->data(m_fileIndex, Qt::DisplayRole).toString();
0293     QCOMPARE(fileName, QString("toplevelfile_1"));
0294 
0295     QString specialFileName = m_dirModel->data(m_specialFileIndex, Qt::DisplayRole).toString();
0296     QCOMPARE(specialFileName, specialChars());
0297 
0298     QString dirName = m_dirModel->data(m_dirIndex, Qt::DisplayRole).toString();
0299     QCOMPARE(dirName, QString("subdir"));
0300 
0301     QString fileInDirName = m_dirModel->data(m_fileInDirIndex, Qt::DisplayRole).toString();
0302     QCOMPARE(fileInDirName, QString("testfile"));
0303 
0304     QString fileInSubdirName = m_dirModel->data(m_fileInSubdirIndex, Qt::DisplayRole).toString();
0305     QCOMPARE(fileInSubdirName, QString("testfile"));
0306 }
0307 
0308 void KDirModelTest::testItemForIndex()
0309 {
0310     // root item
0311     KFileItem rootItem = m_dirModel->itemForIndex(QModelIndex());
0312     QVERIFY(!rootItem.isNull());
0313     QCOMPARE(rootItem.name(), QString("."));
0314 
0315     KFileItem fileItem = m_dirModel->itemForIndex(m_fileIndex);
0316     QVERIFY(!fileItem.isNull());
0317     QCOMPARE(fileItem.name(), QString("toplevelfile_1"));
0318     QVERIFY(!fileItem.isDir());
0319     QCOMPARE(fileItem.url().toLocalFile(), QString(m_tempDir->path() + "/toplevelfile_1"));
0320 
0321     KFileItem dirItem = m_dirModel->itemForIndex(m_dirIndex);
0322     QVERIFY(!dirItem.isNull());
0323     QCOMPARE(dirItem.name(), QString("subdir"));
0324     QVERIFY(dirItem.isDir());
0325     QCOMPARE(dirItem.url().toLocalFile(), QString(m_tempDir->path() + "/subdir"));
0326 
0327     KFileItem fileInDirItem = m_dirModel->itemForIndex(m_fileInDirIndex);
0328     QVERIFY(!fileInDirItem.isNull());
0329     QCOMPARE(fileInDirItem.name(), QString("testfile"));
0330     QVERIFY(!fileInDirItem.isDir());
0331     QCOMPARE(fileInDirItem.url().toLocalFile(), QString(m_tempDir->path() + "/subdir/testfile"));
0332 
0333     KFileItem fileInSubdirItem = m_dirModel->itemForIndex(m_fileInSubdirIndex);
0334     QVERIFY(!fileInSubdirItem.isNull());
0335     QCOMPARE(fileInSubdirItem.name(), QString("testfile"));
0336     QVERIFY(!fileInSubdirItem.isDir());
0337     QCOMPARE(fileInSubdirItem.url().toLocalFile(), QString(m_tempDir->path() + "/subdir/subsubdir/testfile"));
0338 }
0339 
0340 void KDirModelTest::testIndexForItem()
0341 {
0342     KFileItem rootItem = m_dirModel->itemForIndex(QModelIndex());
0343     QModelIndex rootIndex = m_dirModel->indexForItem(rootItem);
0344     QVERIFY(!rootIndex.isValid());
0345 
0346     KFileItem fileItem = m_dirModel->itemForIndex(m_fileIndex);
0347     QModelIndex fileIndex = m_dirModel->indexForItem(fileItem);
0348     QCOMPARE(fileIndex, m_fileIndex);
0349 
0350     KFileItem dirItem = m_dirModel->itemForIndex(m_dirIndex);
0351     QModelIndex dirIndex = m_dirModel->indexForItem(dirItem);
0352     QCOMPARE(dirIndex, m_dirIndex);
0353 
0354     KFileItem fileInDirItem = m_dirModel->itemForIndex(m_fileInDirIndex);
0355     QModelIndex fileInDirIndex = m_dirModel->indexForItem(fileInDirItem);
0356     QCOMPARE(fileInDirIndex, m_fileInDirIndex);
0357 
0358     KFileItem fileInSubdirItem = m_dirModel->itemForIndex(m_fileInSubdirIndex);
0359     QModelIndex fileInSubdirIndex = m_dirModel->indexForItem(fileInSubdirItem);
0360     QCOMPARE(fileInSubdirIndex, m_fileInSubdirIndex);
0361 }
0362 
0363 void KDirModelTest::testData()
0364 {
0365     // First file
0366     QModelIndex idx1col0 = m_dirModel->index(m_fileIndex.row(), 0, QModelIndex());
0367     QCOMPARE(idx1col0.data().toString(), QString("toplevelfile_1"));
0368     QModelIndex idx1col1 = m_dirModel->index(m_fileIndex.row(), 1, QModelIndex());
0369     QString size1 = m_dirModel->data(idx1col1, Qt::DisplayRole).toString();
0370     QCOMPARE(size1, QString("11 B"));
0371 
0372     KFileItem item = m_dirModel->data(m_fileIndex, KDirModel::FileItemRole).value<KFileItem>();
0373     KFileItem fileItem = m_dirModel->itemForIndex(m_fileIndex);
0374     QCOMPARE(item, fileItem);
0375 
0376     QCOMPARE(m_dirModel->data(m_fileIndex, KDirModel::ChildCountRole).toInt(), (int)KDirModel::ChildCountUnknown);
0377 
0378     // Second file
0379     QModelIndex idx2col0 = m_dirModel->index(m_secondFileIndex.row(), 0, QModelIndex());
0380     QString display2 = m_dirModel->data(idx2col0, Qt::DisplayRole).toString();
0381     QCOMPARE(display2, QString("toplevelfile_2"));
0382 
0383     // Subdir: check child count
0384     QCOMPARE(m_dirModel->data(m_dirIndex, KDirModel::ChildCountRole).toInt(), 4);
0385 
0386     // Subsubdir: check child count
0387     QCOMPARE(m_dirModel->data(m_fileInSubdirIndex.parent(), KDirModel::ChildCountRole).toInt(), 1);
0388 }
0389 
0390 void KDirModelTest::testIcon()
0391 {
0392     /**
0393      * Create test data only used in testIcon()
0394      * PATH/subdirwithcustomicon
0395      * PATH/subdirwithcustomicon/.directory
0396      * PATH/subdirwithcustomicon/exampleIcon.svg
0397      * PATH/subdirwithcustomicon/subdirwithinvalidicon
0398      * PATH/subdirwithcustomicon/subdirwithinvalidicon/.directory
0399      * PATH/subdirwithcustomicon/subdirwithinvalidsvgicon
0400      * PATH/subdirwithcustomicon/subdirwithinvalidsvgicon/.directory
0401      */
0402     const QString path(m_tempDir->path() + '/');
0403 
0404     const QString subdirWithCustomIconPath(path + "subdirwithcustomicon/");
0405     createTestDirectory(path + "subdirwithcustomicon/", Empty);
0406 
0407     const QString exampleIconPath = subdirWithCustomIconPath + "exampleIcon.svg";
0408     const QByteArray exampleSvgIconContent(
0409         "<svg version='1.1' width='100' height='100' xmlns='http://www.w3.org/2000/svg'>"
0410         "<rect width='100%' height='100%' fill='red' />"
0411         "</svg>");
0412     const QByteArray directoryFileContent(
0413         "[Desktop Entry]\n"
0414         "Icon="
0415         + exampleIconPath.toLatin1());
0416 
0417     struct Entry {
0418         bool isDir;
0419         QString name;
0420         QByteArray content;
0421     };
0422 
0423     const std::array<Entry, 4> topLevelContent{{
0424         {false, exampleIconPath, exampleSvgIconContent},
0425         {false, subdirWithCustomIconPath + ".directory", directoryFileContent},
0426         {true, subdirWithCustomIconPath + "subdirwithinvalidicon", QByteArray()},
0427         {true, subdirWithCustomIconPath + "subdirwithinvalidsvgicon", QByteArray()},
0428     }};
0429 
0430     for (const auto &entry : topLevelContent) {
0431         if (entry.isDir) {
0432             createTestDirectory(entry.name, Empty);
0433         } else {
0434             createTestFile(entry.name, false, entry.content);
0435         }
0436     }
0437 
0438     // Add an extra suffix to the end of the path to make it become invalid
0439     const QByteArray directoryFileWithInvalidIconContent(
0440         "[Desktop Entry]\n"
0441         "Icon="
0442         + exampleIconPath.toLatin1() + ".png");
0443     createTestFile(subdirWithCustomIconPath + "subdirwithinvalidicon/.directory", false, directoryFileWithInvalidIconContent);
0444 
0445     // Add an extra ".svg" to the end of the path to make it become invalid
0446     const QByteArray directoryFileWithInvalidSvgIconContent(
0447         "[Desktop Entry]\n"
0448         "Icon="
0449         + exampleIconPath.toLatin1() + ".svg");
0450     createTestFile(subdirWithCustomIconPath + "subdirwithinvalidsvgicon/.directory", false, directoryFileWithInvalidSvgIconContent);
0451 
0452     // Refresh the directory after adding files
0453     KDirLister *dirLister = m_dirModel->dirLister();
0454     QSignalSpy completedSpy(dirLister, qOverload<>(&KCoreDirLister::completed));
0455     dirLister->openUrl(QUrl::fromLocalFile(path), KDirLister::Reload);
0456     QVERIFY(completedSpy.wait());
0457     completedSpy.clear();
0458 
0459     QModelIndex dirWithIconIndex; // subdirwithcustomicon
0460 
0461     for (int row = 0; row < m_topLevelFileNames.count() + 2 /*subdir, subdirwithinvalidicon*/; ++row) {
0462         const QModelIndex idx = m_dirModel->index(row, 0, QModelIndex());
0463         QVERIFY(idx.isValid());
0464         const KFileItem item = m_dirModel->itemForIndex(idx);
0465         if (item.isDir() && item.name() == QStringLiteral("subdirwithcustomicon")) {
0466             dirWithIconIndex = idx;
0467             break;
0468         }
0469     }
0470 
0471     QVERIFY(dirWithIconIndex.isValid());
0472 
0473     // Now list subdirwithcustomicon/
0474     QVERIFY(m_dirModel->canFetchMore(dirWithIconIndex));
0475     qDebug() << "Listing subdirwithcustomicon";
0476     m_dirModel->fetchMore(dirWithIconIndex);
0477     QVERIFY(completedSpy.wait());
0478     completedSpy.clear();
0479 
0480     // Indexes of files inside a directory
0481     QModelIndex dirWithInvalidIconIndex; // subdirwithcustomicon/subdirwithinvalidicon
0482     QModelIndex dirWithInvalidSvgIconIndex; // subdirwithcustomicon/subdirwithinvalidsvgicon
0483 
0484     for (int row = 0; row < static_cast<int>(topLevelContent.size()); ++row) {
0485         QModelIndex idx = m_dirModel->index(row, 0, dirWithIconIndex);
0486         const KFileItem item = m_dirModel->itemForIndex(idx);
0487         if (item.isDir()) {
0488             if (item.name() == QLatin1String("subdirwithinvalidicon")) {
0489                 dirWithInvalidIconIndex = idx;
0490             } else if (item.name() == QLatin1String("subdirwithinvalidsvgicon")) {
0491                 dirWithInvalidSvgIconIndex = idx;
0492             }
0493         }
0494     }
0495 
0496     QVERIFY(dirWithInvalidIconIndex.isValid());
0497     QVERIFY(dirWithInvalidSvgIconIndex.isValid());
0498 
0499     // Create an expected icon image
0500     const QSize comparedSize(100, 100);
0501     const QImage expectedIconImage = QIcon(exampleIconPath).pixmap(comparedSize).toImage();
0502 
0503     // Test the custom icon is correctly loaded for the directory
0504     const QIcon icon = m_dirModel->data(dirWithIconIndex, Qt::DecorationRole).value<QIcon>();
0505     QCOMPARE(m_dirModel->itemForIndex(dirWithIconIndex).iconName(), exampleIconPath);
0506     QVERIFY(icon.pixmap(comparedSize).toImage() == expectedIconImage);
0507 
0508     // Test "unknown" icon will be used when the icon path refers to an invalid icon
0509     const QIcon icon2 = m_dirModel->data(dirWithInvalidIconIndex, Qt::DecorationRole).value<QIcon>();
0510     QCOMPARE(icon2.name(), "unknown");
0511 
0512     // Test "unknown" icon will be used when the icon path refers to an invalid svg icon
0513     const QIcon icon3 = m_dirModel->data(dirWithInvalidSvgIconIndex, Qt::DecorationRole).value<QIcon>();
0514     QCOMPARE(icon3.name(), "unknown");
0515 
0516     // Delete the folder
0517     QDir dir(subdirWithCustomIconPath);
0518     QVERIFY(dir.removeRecursively());
0519 
0520     // Refresh the directory again after deleting the folder
0521     dirLister->openUrl(QUrl::fromLocalFile(path), KDirLister::Reload);
0522     QVERIFY(completedSpy.wait());
0523     completedSpy.clear();
0524 }
0525 
0526 void KDirModelTest::testReload()
0527 {
0528     fillModel(true);
0529     testItemForIndex();
0530 }
0531 
0532 // We want more info than just "the values differ", if they do.
0533 /* clang-format off */
0534 #define COMPARE_INDEXES(a, b) \
0535     QCOMPARE(a.row(), b.row()); \
0536     QCOMPARE(a.column(), b.column()); \
0537     QCOMPARE(a.model(), b.model()); \
0538     QCOMPARE(a.parent().isValid(), b.parent().isValid()); \
0539     QCOMPARE(a, b);
0540 /* clang-format on */
0541 
0542 void KDirModelTest::testModifyFile()
0543 {
0544     const QString file = m_tempDir->path() + "/toplevelfile_2";
0545 
0546     QSignalSpy spyDataChanged(m_dirModel, &QAbstractItemModel::dataChanged);
0547 
0548     // "Touch" the file
0549     setTimeStamp(file, s_referenceTimeStamp.addSecs(20));
0550 
0551     // In stat mode, kdirwatch doesn't notice file changes; we need to trigger it
0552     // by creating a file.
0553     // createTestFile(m_tempDir->path() + "/toplevelfile_5");
0554     KDirWatch::self()->setDirty(m_tempDir->path());
0555 
0556     // Wait for KDirWatch to notify the change (especially when using Stat)
0557     if (spyDataChanged.isEmpty()) {
0558         QVERIFY(spyDataChanged.wait());
0559     }
0560 
0561     // If we come here, then dataChanged() was emitted - all good.
0562     const QVariantList dataChanged = spyDataChanged[0];
0563     QModelIndex receivedIndex = dataChanged[0].value<QModelIndex>();
0564     COMPARE_INDEXES(receivedIndex, m_secondFileIndex);
0565     receivedIndex = dataChanged[1].value<QModelIndex>();
0566     QCOMPARE(receivedIndex.row(), m_secondFileIndex.row()); // only compare row; column is count-1
0567 }
0568 
0569 void KDirModelTest::testRenameFile()
0570 {
0571     const QUrl url = QUrl::fromLocalFile(m_tempDir->path() + "/toplevelfile_2");
0572     const QUrl newUrl = QUrl::fromLocalFile(m_tempDir->path() + "/toplevelfile_2_renamed");
0573 
0574     QSignalSpy spyDataChanged(m_dirModel, &QAbstractItemModel::dataChanged);
0575 
0576     KIO::SimpleJob *job = KIO::rename(url, newUrl, KIO::HideProgressInfo);
0577     QVERIFY(job->exec());
0578 
0579     // Wait for the DBUS signal from KDirNotify, it's the one the triggers dataChanged
0580     if (spyDataChanged.isEmpty()) {
0581         QVERIFY(spyDataChanged.wait());
0582     }
0583 
0584     // If we come here, then dataChanged() was emitted - all good.
0585     QCOMPARE(spyDataChanged.count(), 1);
0586     COMPARE_INDEXES(spyDataChanged[0][0].value<QModelIndex>(), m_secondFileIndex);
0587     QModelIndex receivedIndex = spyDataChanged[0][1].value<QModelIndex>();
0588     QCOMPARE(receivedIndex.row(), m_secondFileIndex.row()); // only compare row; column is count-1
0589 
0590     // check renaming happened
0591     QCOMPARE(m_dirModel->itemForIndex(m_secondFileIndex).url().toString(), newUrl.toString());
0592 
0593     // check that KDirLister::cachedItemForUrl won't give a bad name if copying that item (#195385)
0594     KFileItem cachedItem = KDirLister::cachedItemForUrl(newUrl);
0595     QVERIFY(!cachedItem.isNull());
0596     QCOMPARE(cachedItem.name(), QString("toplevelfile_2_renamed"));
0597     QCOMPARE(cachedItem.entry().stringValue(KIO::UDSEntry::UDS_NAME), QString("toplevelfile_2_renamed"));
0598 
0599     // Put things back to normal
0600     spyDataChanged.clear();
0601     job = KIO::rename(newUrl, url, KIO::HideProgressInfo);
0602     QVERIFY(job->exec());
0603     // Wait for the DBUS signal from KDirNotify, it's the one the triggers dataChanged
0604     if (spyDataChanged.isEmpty()) {
0605         QVERIFY(spyDataChanged.wait());
0606     }
0607     QCOMPARE(m_dirModel->itemForIndex(m_secondFileIndex).url().toString(), url.toString());
0608 }
0609 
0610 void KDirModelTest::testMoveDirectory()
0611 {
0612     testMoveDirectory(QStringLiteral("subdir"));
0613 }
0614 
0615 void KDirModelTest::testMoveDirectory(const QString &dir /*just a dir name, no slash*/)
0616 {
0617     const QString path = m_tempDir->path() + '/';
0618     const QString srcdir = path + dir;
0619     QVERIFY(QDir(srcdir).exists());
0620     QTemporaryDir destDir(homeTmpDir());
0621     const QString dest = destDir.path() + '/';
0622     QVERIFY(QDir(dest).exists());
0623 
0624     // Move
0625     qDebug() << "Moving" << srcdir << "to" << dest;
0626     KIO::CopyJob *job = KIO::move(QUrl::fromLocalFile(srcdir), QUrl::fromLocalFile(dest), KIO::HideProgressInfo);
0627     job->setUiDelegate(nullptr);
0628     job->setUiDelegateExtension(nullptr);
0629     QVERIFY(job->exec());
0630 
0631     // wait for kdirnotify
0632     QSignalSpy spyRowsRemoved(m_dirModel, &QAbstractItemModel::rowsRemoved);
0633     if (spyRowsRemoved.isEmpty()) {
0634         QVERIFY(spyRowsRemoved.wait());
0635     }
0636 
0637     QVERIFY(!m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir")).isValid());
0638     QVERIFY(!m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir_renamed")).isValid());
0639 
0640     // Move back
0641     qDebug() << "Moving" << dest + dir << "back to" << srcdir;
0642     job = KIO::move(QUrl::fromLocalFile(dest + dir), QUrl::fromLocalFile(srcdir), KIO::HideProgressInfo);
0643     job->setUiDelegate(nullptr);
0644     job->setUiDelegateExtension(nullptr);
0645     QVERIFY(job->exec());
0646 
0647     QSignalSpy spyRowsInserted(m_dirModel, &QAbstractItemModel::rowsInserted);
0648     if (spyRowsInserted.isEmpty()) {
0649         QVERIFY(spyRowsInserted.wait());
0650     }
0651 
0652     QVERIFY(QDir(srcdir).exists());
0653 
0654     // m_dirIndex is invalid after the above...
0655     fillModel(true);
0656 }
0657 
0658 void KDirModelTest::testRenameDirectory() // #172945, #174703, (and #180156)
0659 {
0660     const QString path = m_tempDir->path() + '/';
0661     const QUrl url = QUrl::fromLocalFile(path + "subdir");
0662     const QUrl newUrl = QUrl::fromLocalFile(path + "subdir_renamed");
0663 
0664     // For #180156 we need a second kdirmodel, viewing the subdir being renamed.
0665     // I'm abusing m_dirModelForExpand for that purpose.
0666     delete m_dirModelForExpand;
0667     m_dirModelForExpand = new KDirModel;
0668     KDirLister *dirListerForExpand = m_dirModelForExpand->dirLister();
0669     QSignalSpy spyCompleted(dirListerForExpand, qOverload<>(&KCoreDirLister::completed));
0670     dirListerForExpand->openUrl(url); // async
0671     QVERIFY(spyCompleted.wait());
0672 
0673     // Now do the renaming
0674     QSignalSpy spyDataChanged(m_dirModel, &QAbstractItemModel::dataChanged);
0675     KIO::SimpleJob *job = KIO::rename(url, newUrl, KIO::HideProgressInfo);
0676     QVERIFY(job->exec());
0677 
0678     // Wait for the DBUS signal from KDirNotify, it's the one the triggers dataChanged
0679     if (spyDataChanged.isEmpty()) {
0680         QVERIFY(spyDataChanged.wait());
0681     }
0682     spyDataChanged.clear();
0683 
0684     // If we come here, then dataChanged() was emitted - all good.
0685     // QCOMPARE(spyDataChanged.count(), 1); // it was in fact emitted 5 times...
0686     // COMPARE_INDEXES(spyDataChanged[0][0].value<QModelIndex>(), m_dirIndex);
0687     // QModelIndex receivedIndex = spyDataChanged[0][1].value<QModelIndex>();
0688     // QCOMPARE(receivedIndex.row(), m_dirIndex.row()); // only compare row; column is count-1
0689 
0690     // check renaming happened
0691     QCOMPARE(m_dirModel->itemForIndex(m_dirIndex).url().toString(), newUrl.toString());
0692     qDebug() << newUrl << "indexForUrl=" << m_dirModel->indexForUrl(newUrl) << "m_dirIndex=" << m_dirIndex;
0693     QCOMPARE(m_dirModel->indexForUrl(newUrl), m_dirIndex);
0694     QVERIFY(m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir_renamed")).isValid());
0695     QVERIFY(m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir_renamed/testfile")).isValid());
0696     QVERIFY(m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir_renamed/subsubdir")).isValid());
0697     QVERIFY(m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir_renamed/subsubdir/testfile")).isValid());
0698 
0699     // Check the other kdirmodel got redirected
0700     QCOMPARE(dirListerForExpand->url().toLocalFile(), QString(path + "subdir_renamed"));
0701 
0702     qDebug() << "calling testMoveDirectory(subdir_renamed)";
0703 
0704     // Test moving the renamed directory; if something inside KDirModel
0705     // wasn't properly updated by the renaming, this would detect it and crash (#180673)
0706     testMoveDirectory(QStringLiteral("subdir_renamed"));
0707 
0708     // Put things back to normal
0709     job = KIO::rename(newUrl, url, KIO::HideProgressInfo);
0710     QVERIFY(job->exec());
0711     // Wait for the DBUS signal from KDirNotify, it's the one the triggers dataChanged
0712     if (spyDataChanged.isEmpty()) {
0713         QVERIFY(spyDataChanged.wait());
0714     }
0715     QCOMPARE(m_dirModel->itemForIndex(m_dirIndex).url().toString(), url.toString());
0716 
0717     QCOMPARE(m_dirModel->itemForIndex(m_dirIndex).url().toString(), url.toString());
0718     QCOMPARE(m_dirModel->indexForUrl(url), m_dirIndex);
0719     QVERIFY(m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir")).isValid());
0720     QVERIFY(m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir/testfile")).isValid());
0721     QVERIFY(m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir/subsubdir")).isValid());
0722     QVERIFY(m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir/subsubdir/testfile")).isValid());
0723     QVERIFY(!m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir_renamed")).isValid());
0724     QVERIFY(!m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir_renamed/testfile")).isValid());
0725     QVERIFY(!m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir_renamed/subsubdir")).isValid());
0726     QVERIFY(!m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir_renamed/subsubdir/testfile")).isValid());
0727 
0728     // TODO INVESTIGATE
0729     // QCOMPARE(dirListerForExpand->url().toLocalFile(), path+"subdir");
0730 
0731     delete m_dirModelForExpand;
0732     m_dirModelForExpand = nullptr;
0733 }
0734 
0735 void KDirModelTest::testRenameDirectoryInCache() // #188807
0736 {
0737     // Ensure the stuff is in cache.
0738     fillModel(true);
0739     const QString path = m_tempDir->path() + '/';
0740     QVERIFY(!m_dirModel->dirLister()->findByUrl(QUrl::fromLocalFile(path)).isNull());
0741 
0742     // No more dirmodel nor dirlister.
0743     delete m_dirModel;
0744     m_dirModel = nullptr;
0745 
0746     // Now let's rename a directory that is in KCoreDirListerCache
0747     const QUrl url = QUrl::fromLocalFile(path);
0748     QUrl newUrl = url.adjusted(QUrl::StripTrailingSlash);
0749     newUrl.setPath(newUrl.path() + "_renamed");
0750     qDebug() << newUrl;
0751     KIO::SimpleJob *job = KIO::rename(url, newUrl, KIO::HideProgressInfo);
0752     QVERIFY(job->exec());
0753 
0754     // Put things back to normal
0755     job = KIO::rename(newUrl, url, KIO::HideProgressInfo);
0756     QVERIFY(job->exec());
0757 
0758     // KDirNotify emits FileRenamed for each rename() above, which in turn
0759     // re-lists the directory. We need to wait for both signals to be emitted
0760     // otherwise the dirlister will not be in the state we expect.
0761     QTest::qWait(200);
0762 
0763     fillModel(true);
0764 
0765     QVERIFY(m_dirIndex.isValid());
0766     KFileItem rootItem = m_dirModel->dirLister()->findByUrl(QUrl::fromLocalFile(path));
0767     QVERIFY(!rootItem.isNull());
0768 }
0769 
0770 void KDirModelTest::testChmodDirectory() // #53397
0771 {
0772     QSignalSpy spyDataChanged(m_dirModel, &QAbstractItemModel::dataChanged);
0773     const QString path = m_tempDir->path() + '/';
0774     KFileItem rootItem = m_dirModel->itemForIndex(QModelIndex());
0775     const mode_t origPerm = rootItem.permissions();
0776     mode_t newPerm = origPerm ^ S_IWGRP;
0777     // const QFile::Permissions origPerm = rootItem.filePermissions();
0778     // QVERIFY(origPerm & QFile::ReadOwner);
0779     // const QFile::Permissions newPerm = origPerm ^ QFile::WriteGroup;
0780     QVERIFY(newPerm != origPerm);
0781     KIO::Job *job = KIO::chmod({rootItem}, newPerm, S_IWGRP /*TODO: QFile::WriteGroup*/, QString(), QString(), false, KIO::HideProgressInfo);
0782     job->setUiDelegate(nullptr);
0783     QVERIFY(job->exec());
0784     // ChmodJob doesn't talk to KDirNotify, kpropertiesdialog does.
0785     // [this allows to group notifications after all the changes one can make in the dialog]
0786     org::kde::KDirNotify::emitFilesChanged(QList<QUrl>{QUrl::fromLocalFile(path)});
0787     // Wait for the DBUS signal from KDirNotify, it's the one the triggers rowsRemoved
0788     if (spyDataChanged.isEmpty()) {
0789         QVERIFY(spyDataChanged.wait());
0790     }
0791 
0792     // If we come here, then dataChanged() was emitted - all good.
0793     QCOMPARE(spyDataChanged.count(), 1);
0794     QModelIndex receivedIndex = spyDataChanged[0][0].value<QModelIndex>();
0795     qDebug() << "receivedIndex" << receivedIndex;
0796     QVERIFY(!receivedIndex.isValid());
0797 
0798     const KFileItem newRootItem = m_dirModel->itemForIndex(QModelIndex());
0799     QVERIFY(!newRootItem.isNull());
0800     QCOMPARE(QString::number(newRootItem.permissions(), 16), QString::number(newPerm, 16));
0801 }
0802 
0803 enum {
0804     NoFlag = 0,
0805     NewDir = 1, // whether to re-create a new QTemporaryDir completely, to avoid cached fileitems
0806     ListFinalDir = 2, // whether to list the target dir at the same time, like k3b, for #193364
0807     Recreate = 4,
0808     CacheSubdir = 8, // put subdir in the cache before expandToUrl
0809     // flags, next item is 16!
0810 };
0811 
0812 void KDirModelTest::testExpandToUrl_data()
0813 {
0814     QTest::addColumn<int>("flags"); // see enum above
0815     QTest::addColumn<QString>("expandToPath"); // relative path
0816     QTest::addColumn<QStringList>("expectedExpandSignals");
0817 
0818     QTest::newRow("the root, nothing to do") << int(NoFlag) << QString() << QStringList();
0819     QTest::newRow(".") << int(NoFlag) << "." << (QStringList());
0820     QTest::newRow("subdir") << int(NoFlag) << "subdir" << QStringList{QStringLiteral("subdir")};
0821     QTest::newRow("subdir/.") << int(NoFlag) << "subdir/." << QStringList{QStringLiteral("subdir")};
0822 
0823     const QString subsubdir = QStringLiteral("subdir/subsubdir");
0824     // Must list root, emit expand for subdir, list subdir, emit expand for subsubdir.
0825     QTest::newRow("subdir/subsubdir") << int(NoFlag) << subsubdir << QStringList{QStringLiteral("subdir"), subsubdir};
0826 
0827     // Must list root, emit expand for subdir, list subdir, emit expand for subsubdir, list subsubdir.
0828     const QString subsubdirfile = subsubdir + "/testfile";
0829     QTest::newRow("subdir/subsubdir/testfile sync") << int(NoFlag) << subsubdirfile << QStringList{QStringLiteral("subdir"), subsubdir, subsubdirfile};
0830 
0831 #ifndef Q_OS_WIN
0832     // Expand a symlink to a directory (#219547)
0833     const QString dirlink = m_tempDir->path() + "/dirlink";
0834     createTestSymlink(dirlink, "subdir"); // dirlink -> subdir
0835     QVERIFY(QFileInfo(dirlink).isSymLink());
0836     // If this test fails, your first move should be to enable all debug output and see if KDirWatch says inotify failed
0837     QTest::newRow("dirlink") << int(NoFlag) << "dirlink/subsubdir" << QStringList{QStringLiteral("dirlink"), QStringLiteral("dirlink/subsubdir")};
0838 #endif
0839 
0840     // Do a cold-cache test too, but nowadays it doesn't change anything anymore,
0841     // apart from testing different code paths inside KDirLister.
0842     QTest::newRow("subdir/subsubdir/testfile with reload") << int(NewDir) << subsubdirfile << QStringList{QStringLiteral("subdir"), subsubdir, subsubdirfile};
0843 
0844     QTest::newRow("hold dest dir") // #193364
0845         << int(NewDir | ListFinalDir) << subsubdirfile << QStringList{QStringLiteral("subdir"), subsubdir, subsubdirfile};
0846 
0847     // Put subdir in cache too (#175035)
0848     QTest::newRow("hold subdir and dest dir") << int(NewDir | CacheSubdir | ListFinalDir | Recreate) << subsubdirfile
0849                                               << QStringList{QStringLiteral("subdir"), subsubdir, subsubdirfile};
0850 
0851     // Make sure the last test has the Recreate option set, for the subsequent test methods.
0852 }
0853 
0854 void KDirModelTest::testExpandToUrl()
0855 {
0856     QFETCH(int, flags);
0857     QFETCH(QString, expandToPath); // relative
0858     QFETCH(QStringList, expectedExpandSignals);
0859 
0860     if (flags & NewDir) {
0861         recreateTestData();
0862         // WARNING! m_dirIndex, m_fileIndex, m_secondFileIndex etc. are not valid anymore after this point!
0863     }
0864 
0865     const QString path = m_tempDir->path() + '/';
0866     if (flags & CacheSubdir) {
0867         // This way, the listDir for subdir will find items in cache, and will schedule a CachedItemsJob
0868         m_dirModel->dirLister()->openUrl(QUrl::fromLocalFile(path + "subdir"));
0869         QSignalSpy completedSpy(m_dirModel->dirLister(), qOverload<>(&KCoreDirLister::completed));
0870         QVERIFY(completedSpy.wait(2000));
0871     }
0872     if (flags & ListFinalDir) {
0873         // This way, the last listDir will find items in cache, and will schedule a CachedItemsJob
0874         m_dirModel->dirLister()->openUrl(QUrl::fromLocalFile(path + "subdir/subsubdir"));
0875         QSignalSpy completedSpy(m_dirModel->dirLister(), qOverload<>(&KCoreDirLister::completed));
0876         QVERIFY(completedSpy.wait(2000));
0877     }
0878 
0879     if (!m_dirModelForExpand || (flags & NewDir)) {
0880         delete m_dirModelForExpand;
0881         m_dirModelForExpand = new KDirModel;
0882         connect(m_dirModelForExpand, &KDirModel::expand, this, &KDirModelTest::slotExpand);
0883         connect(m_dirModelForExpand, &QAbstractItemModel::rowsInserted, this, &KDirModelTest::slotRowsInserted);
0884         KDirLister *dirListerForExpand = m_dirModelForExpand->dirLister();
0885         dirListerForExpand->openUrl(QUrl::fromLocalFile(path), KDirLister::NoFlags); // async
0886     }
0887     m_rowsInsertedEmitted = false;
0888     m_expectedExpandSignals = expectedExpandSignals;
0889     m_nextExpectedExpandSignals = 0;
0890     QSignalSpy spyExpand(m_dirModelForExpand, &KDirModel::expand);
0891     m_urlToExpandTo = QUrl::fromLocalFile(path + expandToPath);
0892     // If KDirModel doesn't know this URL yet, then we want to see rowsInserted signals
0893     // being emitted, so that the slots can get the index to that url then.
0894     m_expectRowsInserted = !expandToPath.isEmpty() && !m_dirModelForExpand->indexForUrl(m_urlToExpandTo).isValid();
0895     QVERIFY(QFileInfo::exists(m_urlToExpandTo.toLocalFile()));
0896     m_dirModelForExpand->expandToUrl(m_urlToExpandTo);
0897     if (expectedExpandSignals.isEmpty()) {
0898         QTest::qWait(20); // to make sure we process queued connection calls, otherwise spyExpand.count() is always 0 even if there's a bug...
0899         QCOMPARE(spyExpand.count(), 0);
0900     } else {
0901         if (spyExpand.count() < expectedExpandSignals.count()) {
0902             QTRY_COMPARE(spyExpand.count(), expectedExpandSignals.count());
0903         }
0904         if (m_expectRowsInserted) {
0905             QVERIFY(m_rowsInsertedEmitted);
0906         }
0907     }
0908 
0909     // Now it should exist
0910     if (!expandToPath.isEmpty() && expandToPath != QLatin1String(".")) {
0911         qDebug() << "Do I know" << m_urlToExpandTo << "?";
0912         QVERIFY(m_dirModelForExpand->indexForUrl(m_urlToExpandTo).isValid());
0913     }
0914 
0915     if (flags & ListFinalDir) {
0916         testUpdateParentAfterExpand();
0917     }
0918 
0919     if (flags & Recreate) {
0920         // Clean up, for the next tests
0921         recreateTestData();
0922         fillModel(false);
0923     }
0924 }
0925 
0926 void KDirModelTest::slotExpand(const QModelIndex &index)
0927 {
0928     QVERIFY(index.isValid());
0929     const QString path = m_tempDir->path() + '/';
0930     KFileItem item = m_dirModelForExpand->itemForIndex(index);
0931     QVERIFY(!item.isNull());
0932     qDebug() << item.url().toLocalFile();
0933     QCOMPARE(item.url().toLocalFile(), QString(path + m_expectedExpandSignals[m_nextExpectedExpandSignals++]));
0934 
0935     // if rowsInserted wasn't emitted yet, then any proxy model would be unable to do anything with index at this point
0936     if (item.url() == m_urlToExpandTo) {
0937         QVERIFY(m_dirModelForExpand->indexForUrl(m_urlToExpandTo).isValid());
0938         if (m_expectRowsInserted) {
0939             QVERIFY(m_rowsInsertedEmitted);
0940         }
0941     }
0942 }
0943 
0944 void KDirModelTest::slotRowsInserted(const QModelIndex &, int, int)
0945 {
0946     m_rowsInsertedEmitted = true;
0947 }
0948 
0949 // This code is called by testExpandToUrl
0950 void KDirModelTest::testUpdateParentAfterExpand() // #193364
0951 {
0952     const QString path = m_tempDir->path() + '/';
0953     const QString file = path + "subdir/aNewFile";
0954     qDebug() << "Creating" << file;
0955     QVERIFY(!QFile::exists(file));
0956     createTestFile(file);
0957     QSignalSpy spyRowsInserted(m_dirModelForExpand, &QAbstractItemModel::rowsInserted);
0958     QVERIFY(spyRowsInserted.wait(1000));
0959 }
0960 
0961 void KDirModelTest::testFilter()
0962 {
0963     QVERIFY(m_dirIndex.isValid());
0964     const int oldTopLevelRowCount = m_dirModel->rowCount();
0965     const int oldSubdirRowCount = m_dirModel->rowCount(m_dirIndex);
0966 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 100)
0967     QSignalSpy spyItemsFilteredByMime(m_dirModel->dirLister(), &KCoreDirLister::itemsFilteredByMime);
0968 #endif
0969     QSignalSpy spyItemsDeleted(m_dirModel->dirLister(), &KCoreDirLister::itemsDeleted);
0970     QSignalSpy spyRowsRemoved(m_dirModel, &QAbstractItemModel::rowsRemoved);
0971     m_dirModel->dirLister()->setNameFilter(QStringLiteral("toplevel*"));
0972     QCOMPARE(m_dirModel->rowCount(), oldTopLevelRowCount); // no change yet
0973     QCOMPARE(m_dirModel->rowCount(m_dirIndex), oldSubdirRowCount); // no change yet
0974     m_dirModel->dirLister()->emitChanges();
0975 
0976     QCOMPARE(m_dirModel->rowCount(), 4); // 3 toplevel* files, one subdir
0977     QCOMPARE(m_dirModel->rowCount(m_dirIndex), 2); // the files get filtered out, subsubdir and hasChildren are remaining
0978 
0979     // In the subdir, we can get rowsRemoved signals like (1,2) or (0,0)+(2,2),
0980     // depending on the order of the files in the model.
0981     // So QCOMPARE(spyRowsRemoved.count(), 3) is fragile, we rather need
0982     // to sum up the removed rows per parent directory.
0983     QMap<QString, int> rowsRemovedPerDir;
0984     for (int i = 0; i < spyRowsRemoved.count(); ++i) {
0985         const QVariantList args = spyRowsRemoved[i];
0986         const QModelIndex parentIdx = args[0].value<QModelIndex>();
0987         QString dirName;
0988         if (parentIdx.isValid()) {
0989             const KFileItem item = m_dirModel->itemForIndex(parentIdx);
0990             dirName = item.name();
0991         } else {
0992             dirName = QStringLiteral("root");
0993         }
0994         rowsRemovedPerDir[dirName] += args[2].toInt() - args[1].toInt() + 1;
0995         // qDebug() << parentIdx << args[1].toInt() << args[2].toInt();
0996     }
0997     QCOMPARE(rowsRemovedPerDir.count(), 3); // once for every dir
0998     QCOMPARE(rowsRemovedPerDir.value("root"), 1); // one from toplevel ('special chars')
0999     QCOMPARE(rowsRemovedPerDir.value("subdir"), 2); // two from subdir
1000     QCOMPARE(rowsRemovedPerDir.value("subsubdir"), 1); // one from subsubdir
1001     QCOMPARE(spyItemsDeleted.count(), 3); // once for every dir
1002     QCOMPARE(spyItemsDeleted[0][0].value<KFileItemList>().count(), 1); // one from toplevel ('special chars')
1003     QCOMPARE(spyItemsDeleted[1][0].value<KFileItemList>().count(), 2); // two from subdir
1004     QCOMPARE(spyItemsDeleted[2][0].value<KFileItemList>().count(), 1); // one from subsubdir
1005 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 100)
1006     QCOMPARE(spyItemsFilteredByMime.count(), 0);
1007 #endif
1008     spyItemsDeleted.clear();
1009 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 100)
1010     spyItemsFilteredByMime.clear();
1011 #endif
1012 
1013     // Reset the filter
1014     qDebug() << "reset to no filter";
1015     m_dirModel->dirLister()->setNameFilter(QString());
1016     m_dirModel->dirLister()->emitChanges();
1017 
1018     QCOMPARE(m_dirModel->rowCount(), oldTopLevelRowCount);
1019     QCOMPARE(m_dirModel->rowCount(m_dirIndex), oldSubdirRowCount);
1020     QCOMPARE(spyItemsDeleted.count(), 0);
1021 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 100)
1022     QCOMPARE(spyItemsFilteredByMime.count(), 0);
1023 #endif
1024 
1025     // The order of things changed because of filtering.
1026     // Fill again, so that m_fileIndex etc. are correct again.
1027     fillModel(true);
1028 }
1029 
1030 void KDirModelTest::testFilterPatterns()
1031 {
1032     QVERIFY(m_dirIndex.isValid());
1033 
1034     const int oldTopLevelRowCount = m_dirModel->rowCount();
1035     const int oldSubdirRowCount = m_dirModel->rowCount(m_dirIndex);
1036 
1037     // matching without wildcard -> no files
1038     m_dirModel->dirLister()->setNameFilter(QStringLiteral("toplevel"));
1039     QCOMPARE(m_dirModel->rowCount(), oldTopLevelRowCount); // no change yet
1040     QCOMPARE(m_dirModel->rowCount(m_dirIndex), oldSubdirRowCount); // no change yet
1041     m_dirModel->dirLister()->emitChanges();
1042 
1043     QCOMPARE(m_dirModel->rowCount(), 1); // the files get filtered out, subdir
1044     QCOMPARE(m_dirModel->rowCount(m_dirIndex), 2); // the files get filtered out, subsubdir and hasChildren
1045 
1046     // Reset the filter
1047     m_dirModel->dirLister()->setNameFilter(QString());
1048     m_dirModel->dirLister()->emitChanges();
1049 
1050     // Back to original state
1051     QCOMPARE(m_dirModel->rowCount(), oldTopLevelRowCount);
1052     QCOMPARE(m_dirModel->rowCount(m_dirIndex), oldSubdirRowCount);
1053 
1054     m_dirModel->dirLister()->setNameFilter(QStringLiteral("toplevel*")); // Matching with a wildcard
1055     QCOMPARE(m_dirModel->rowCount(), oldTopLevelRowCount); // no change yet
1056     QCOMPARE(m_dirModel->rowCount(m_dirIndex), oldSubdirRowCount); // no change yet
1057     m_dirModel->dirLister()->emitChanges();
1058 
1059     QCOMPARE(m_dirModel->rowCount(), 4); // 3 files, one subdir with "toplevel*" in the name
1060     QCOMPARE(m_dirModel->rowCount(m_dirIndex), 2); // the files get filtered out, subsubdir and hasChildren are remaining
1061 
1062     m_dirModel->dirLister()->setNameFilter(QString());
1063     m_dirModel->dirLister()->emitChanges();
1064 
1065     QCOMPARE(m_dirModel->rowCount(), oldTopLevelRowCount);
1066     QCOMPARE(m_dirModel->rowCount(m_dirIndex), oldSubdirRowCount);
1067 
1068     // The order of things changed because of filtering.
1069     // Fill again, so that m_fileIndex etc. are correct again.
1070     fillModel(true);
1071 }
1072 
1073 void KDirModelTest::testMimeFilter()
1074 {
1075     QVERIFY(m_dirIndex.isValid());
1076     const int oldTopLevelRowCount = m_dirModel->rowCount();
1077     const int oldSubdirRowCount = m_dirModel->rowCount(m_dirIndex);
1078 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 100)
1079     QSignalSpy spyItemsFilteredByMime(m_dirModel->dirLister(), &KCoreDirLister::itemsFilteredByMime);
1080 #endif
1081     QSignalSpy spyItemsDeleted(m_dirModel->dirLister(), &KCoreDirLister::itemsDeleted);
1082     QSignalSpy spyRowsRemoved(m_dirModel, &QAbstractItemModel::rowsRemoved);
1083     m_dirModel->dirLister()->setMimeFilter(QStringList{QStringLiteral("application/pdf")});
1084     QCOMPARE(m_dirModel->rowCount(), oldTopLevelRowCount); // no change yet
1085     QCOMPARE(m_dirModel->rowCount(m_dirIndex), oldSubdirRowCount); // no change yet
1086     m_dirModel->dirLister()->emitChanges();
1087 
1088     QCOMPARE(m_dirModel->rowCount(), 1); // 1 pdf files, no subdir anymore
1089 
1090     QVERIFY(spyRowsRemoved.count() >= 1); // depends on contiguity...
1091     QVERIFY(spyItemsDeleted.count() >= 1); // once for every dir
1092 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 100)
1093     // Maybe it would make sense to have those items in itemsFilteredByMime,
1094     // but well, for the only existing use of that signal (MIME type filter plugin),
1095     // it's not really necessary, the plugin has seen those files before anyway.
1096     // The signal is mostly useful for the case of listing a dir with a MIME type filter set.
1097     // QCOMPARE(spyItemsFilteredByMime.count(), 1);
1098     // QCOMPARE(spyItemsFilteredByMime[0][0].value<KFileItemList>().count(), 4);
1099     spyItemsFilteredByMime.clear();
1100 #endif
1101     spyItemsDeleted.clear();
1102 
1103     // Reset the filter
1104     qDebug() << "reset to no filter";
1105     m_dirModel->dirLister()->setMimeFilter(QStringList());
1106     m_dirModel->dirLister()->emitChanges();
1107 
1108     QCOMPARE(m_dirModel->rowCount(), oldTopLevelRowCount);
1109     QCOMPARE(spyItemsDeleted.count(), 0);
1110 #if KIOCORE_BUILD_DEPRECATED_SINCE(5, 100)
1111     QCOMPARE(spyItemsFilteredByMime.count(), 0);
1112 #endif
1113 
1114     // The order of things changed because of filtering.
1115     // Fill again, so that m_fileIndex etc. are correct again.
1116     fillModel(true);
1117 }
1118 
1119 void KDirModelTest::testShowHiddenFiles() // #174788
1120 {
1121     KDirLister *dirLister = m_dirModel->dirLister();
1122 
1123     QSignalSpy spyRowsRemoved(m_dirModel, &QAbstractItemModel::rowsRemoved);
1124     QSignalSpy spyNewItems(dirLister, &KCoreDirLister::newItems);
1125     QSignalSpy spyRowsInserted(m_dirModel, &QAbstractItemModel::rowsInserted);
1126     dirLister->setShowHiddenFiles(true);
1127     dirLister->emitChanges();
1128     const int numberOfDotFiles = 2;
1129     QCOMPARE(spyNewItems.count(), 1);
1130     QCOMPARE(spyNewItems[0][0].value<KFileItemList>().count(), numberOfDotFiles);
1131     QCOMPARE(spyRowsInserted.count(), 1);
1132     QCOMPARE(spyRowsRemoved.count(), 0);
1133     spyNewItems.clear();
1134     spyRowsInserted.clear();
1135 
1136     dirLister->setShowHiddenFiles(false);
1137     dirLister->emitChanges();
1138     QCOMPARE(spyNewItems.count(), 0);
1139     QCOMPARE(spyRowsInserted.count(), 0);
1140     QCOMPARE(spyRowsRemoved.count(), 1);
1141 }
1142 
1143 void KDirModelTest::testMultipleSlashes()
1144 {
1145     const QString path = m_tempDir->path() + '/';
1146 
1147     QModelIndex index = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir//testfile"));
1148     QVERIFY(index.isValid());
1149 
1150     index = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir//subsubdir//"));
1151     QVERIFY(index.isValid());
1152 
1153     index = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir///subsubdir////testfile"));
1154     QVERIFY(index.isValid());
1155 }
1156 
1157 void KDirModelTest::testUrlWithRef() // #171117
1158 {
1159     const QString path = m_tempDir->path() + '/';
1160     KDirLister *dirLister = m_dirModel->dirLister();
1161     QUrl url = QUrl::fromLocalFile(path);
1162     url.setFragment(QStringLiteral("ref"));
1163     QVERIFY(url.url().endsWith(QLatin1String("#ref")));
1164     QSignalSpy spyCompleted(dirLister, qOverload<>(&KCoreDirLister::completed));
1165     dirLister->openUrl(url, KDirLister::NoFlags);
1166     QVERIFY(spyCompleted.wait());
1167 
1168     QCOMPARE(dirLister->url().toString(), url.toString(QUrl::StripTrailingSlash));
1169     collectKnownIndexes();
1170 }
1171 
1172 // void KDirModelTest::testFontUrlWithHost() // #160057 --> moved to kio_fonts (kfontinst/kio/autotests)
1173 
1174 void KDirModelTest::testRemoteUrlWithHost() // #178416
1175 {
1176     if (!KProtocolInfo::isKnownProtocol(QStringLiteral("remote"))) {
1177         QSKIP("kio_remote not installed");
1178     }
1179     QUrl url(QStringLiteral("remote://foo"));
1180     KDirLister *dirLister = m_dirModel->dirLister();
1181     QSignalSpy spyCompleted(dirLister, qOverload<>(&KCoreDirLister::completed));
1182     dirLister->openUrl(url, KDirLister::NoFlags);
1183     QVERIFY(spyCompleted.wait());
1184 
1185     QCOMPARE(dirLister->url().toString(), QString("remote://foo"));
1186 }
1187 
1188 void KDirModelTest::testZipFile() // # 171721
1189 {
1190     const QString path = QFileInfo(QFINDTESTDATA("wronglocalsizes.zip")).absolutePath();
1191     KDirLister *dirLister = m_dirModel->dirLister();
1192     QSignalSpy spyCompleted(dirLister, qOverload<>(&KCoreDirLister::completed));
1193     dirLister->openUrl(QUrl::fromLocalFile(path), KDirLister::NoFlags);
1194     QVERIFY(spyCompleted.wait());
1195 
1196     QUrl zipUrl(QUrl::fromLocalFile(path));
1197     zipUrl.setPath(zipUrl.path() + "/wronglocalsizes.zip"); // just a zip file lying here for other reasons
1198 
1199     QVERIFY(QFile::exists(zipUrl.toLocalFile()));
1200     zipUrl.setScheme(QStringLiteral("zip"));
1201     QModelIndex index = m_dirModel->indexForUrl(zipUrl);
1202     QVERIFY(!index.isValid()); // protocol mismatch, can't find it!
1203     zipUrl.setScheme(QStringLiteral("file"));
1204     index = m_dirModel->indexForUrl(zipUrl);
1205     QVERIFY(index.isValid());
1206 }
1207 
1208 class MyDirLister : public KDirLister
1209 {
1210 public:
1211     void emitItemsDeleted(const KFileItemList &items)
1212     {
1213         Q_EMIT itemsDeleted(items);
1214     }
1215 };
1216 
1217 void KDirModelTest::testBug196695()
1218 {
1219     KFileItem rootItem(QUrl::fromLocalFile(m_tempDir->path()), QString(), KFileItem::Unknown);
1220     KFileItem childItem(QUrl::fromLocalFile(QString(m_tempDir->path() + "/toplevelfile_1")), QString(), KFileItem::Unknown);
1221 
1222     KFileItemList list;
1223     // Important: the root item must not be first in the list to trigger bug 196695
1224     list << childItem << rootItem;
1225 
1226     MyDirLister *dirLister = static_cast<MyDirLister *>(m_dirModel->dirLister());
1227     dirLister->emitItemsDeleted(list);
1228 
1229     fillModel(true);
1230 }
1231 
1232 void KDirModelTest::testMimeData()
1233 {
1234     QModelIndex index0 = m_dirModel->index(0, 0);
1235     QVERIFY(index0.isValid());
1236     QModelIndex index1 = m_dirModel->index(1, 0);
1237     QVERIFY(index1.isValid());
1238     QList<QModelIndex> indexes;
1239     indexes << index0 << index1;
1240     QMimeData *mimeData = m_dirModel->mimeData(indexes);
1241     QVERIFY(mimeData);
1242     QVERIFY(mimeData->hasUrls());
1243     const QList<QUrl> urls = mimeData->urls();
1244     QCOMPARE(urls.count(), indexes.count());
1245     delete mimeData;
1246 }
1247 
1248 void KDirModelTest::testDotHiddenFile_data()
1249 {
1250     QTest::addColumn<QStringList>("fileContents");
1251     QTest::addColumn<QStringList>("expectedListing");
1252 
1253     const QStringList allItems{QStringLiteral("toplevelfile_1"),
1254                                QStringLiteral("toplevelfile_2"),
1255                                QStringLiteral("toplevelfile_3"),
1256                                specialChars(),
1257                                QStringLiteral("subdir")};
1258     QTest::newRow("empty_file") << (QStringList{}) << allItems;
1259 
1260     QTest::newRow("simple_name") << (QStringList{QStringLiteral("toplevelfile_1")}) << QStringList(allItems.mid(1));
1261 
1262     QStringList allButSpecialChars = allItems;
1263     allButSpecialChars.removeAt(3);
1264     QTest::newRow("special_chars") << (QStringList{specialChars()}) << allButSpecialChars;
1265 
1266     QStringList allButSubdir = allItems;
1267     allButSubdir.removeAt(4);
1268     QTest::newRow("subdir") << (QStringList{QStringLiteral("subdir")}) << allButSubdir;
1269 
1270     QTest::newRow("many_lines")
1271         << (QStringList{QStringLiteral("subdir"), QStringLiteral("toplevelfile_1"), QStringLiteral("toplevelfile_3"), QStringLiteral("toplevelfile_2")})
1272         << QStringList{specialChars()};
1273 }
1274 
1275 void KDirModelTest::testDotHiddenFile()
1276 {
1277     QFETCH(QStringList, fileContents);
1278     QFETCH(QStringList, expectedListing);
1279 
1280     const QString path = m_tempDir->path() + '/';
1281     const QString dotHiddenFile = path + ".hidden";
1282     QTest::qWait(1000); // mtime-based cache, so we need to wait for 1 second
1283     QFile dh(dotHiddenFile);
1284     QVERIFY(dh.open(QIODevice::WriteOnly));
1285     dh.write(fileContents.join('\n').toUtf8());
1286     dh.close();
1287 
1288     // Do it twice: once to read from the file and once to use the cache
1289     for (int i = 0; i < 2; ++i) {
1290         fillModel(true, false);
1291         QStringList files;
1292         for (int row = 0; row < m_dirModel->rowCount(); ++row) {
1293             files.append(m_dirModel->index(row, KDirModel::Name).data().toString());
1294         }
1295         files.sort();
1296         expectedListing.sort();
1297         QCOMPARE(files, expectedListing);
1298     }
1299 
1300     dh.remove();
1301 }
1302 
1303 void KDirModelTest::testShowRoot()
1304 {
1305     KDirModel dirModel;
1306     const QUrl homeUrl = QUrl::fromLocalFile(QDir::homePath());
1307     const QUrl fsRootUrl = QUrl(QStringLiteral("file:///"));
1308 
1309     // openUrl("/", ShowRoot) should create a "/" item
1310     dirModel.openUrl(fsRootUrl, KDirModel::ShowRoot);
1311     QTRY_COMPARE(dirModel.rowCount(), 1);
1312     const QModelIndex rootIndex = dirModel.index(0, 0);
1313     QVERIFY(rootIndex.isValid());
1314     QCOMPARE(rootIndex.data().toString(), QStringLiteral("/"));
1315     QVERIFY(!dirModel.parent(rootIndex).isValid());
1316     QCOMPARE(dirModel.itemForIndex(rootIndex).url(), QUrl(QStringLiteral("file:///")));
1317     QCOMPARE(dirModel.itemForIndex(rootIndex).name(), QStringLiteral("/"));
1318 
1319     // expandToUrl should work
1320     dirModel.expandToUrl(homeUrl);
1321     QTRY_VERIFY(dirModel.indexForUrl(homeUrl).isValid());
1322 
1323     // test itemForIndex and indexForUrl
1324     QCOMPARE(dirModel.itemForIndex(QModelIndex()).url(), QUrl());
1325     QVERIFY(!dirModel.indexForUrl(QUrl()).isValid());
1326     const QUrl slashUrl = QUrl::fromLocalFile(QStringLiteral("/"));
1327     QCOMPARE(dirModel.indexForUrl(slashUrl), rootIndex);
1328 
1329     // switching to another URL should also show a root node
1330     QSignalSpy spyRowsRemoved(&dirModel, &QAbstractItemModel::rowsRemoved);
1331     const QUrl tempUrl = QUrl::fromLocalFile(QDir::tempPath());
1332     dirModel.openUrl(tempUrl, KDirModel::ShowRoot);
1333     QTRY_COMPARE(dirModel.rowCount(), 1);
1334     QCOMPARE(spyRowsRemoved.count(), 1);
1335     const QModelIndex newRootIndex = dirModel.index(0, 0);
1336     QVERIFY(newRootIndex.isValid());
1337     QCOMPARE(newRootIndex.data().toString(), QFileInfo(QDir::tempPath()).fileName());
1338     QVERIFY(!dirModel.parent(newRootIndex).isValid());
1339     QVERIFY(!dirModel.indexForUrl(slashUrl).isValid());
1340     QCOMPARE(dirModel.itemForIndex(newRootIndex).url(), tempUrl);
1341 }
1342 
1343 void KDirModelTest::testShowRootWithTrailingSlash()
1344 {
1345     // GIVEN
1346     KDirModel dirModel;
1347     const QUrl homeUrl = QUrl::fromLocalFile(QDir::homePath() + QLatin1Char('/'));
1348 
1349     // WHEN
1350     dirModel.openUrl(homeUrl, KDirModel::ShowRoot);
1351     QTRY_VERIFY(dirModel.indexForUrl(homeUrl).isValid());
1352 }
1353 
1354 void KDirModelTest::testShowRootAndExpandToUrl()
1355 {
1356     // call expandToUrl without waiting for initial listing of root node
1357     KDirModel dirModel;
1358     dirModel.openUrl(QUrl(QStringLiteral("file:///")), KDirModel::ShowRoot);
1359     const QUrl homeUrl = QUrl::fromLocalFile(QDir::homePath());
1360     dirModel.expandToUrl(homeUrl);
1361     QTRY_VERIFY(dirModel.indexForUrl(homeUrl).isValid());
1362 }
1363 
1364 void KDirModelTest::testHasChildren_data()
1365 {
1366     QTest::addColumn<bool>("dirsOnly");
1367     QTest::addColumn<bool>("withHidden");
1368 
1369     QTest::newRow("with_files_and_no_hidden") << false << false;
1370     QTest::newRow("dirs_only_and_no_hidden") << true << false;
1371     QTest::newRow("with_files_and_hidden") << false << true;
1372     QTest::newRow("dirs_only_with_hidden") << true << true;
1373 }
1374 
1375 // Test hasChildren without first populating the dirs
1376 void KDirModelTest::testHasChildren()
1377 {
1378     QFETCH(bool, dirsOnly);
1379     QFETCH(bool, withHidden);
1380 
1381     m_dirModel->dirLister()->setDirOnlyMode(dirsOnly);
1382     m_dirModel->dirLister()->setShowHiddenFiles(withHidden);
1383     fillModel(true, false);
1384 
1385     QVERIFY(m_dirModel->hasChildren());
1386 
1387     auto findDir = [this](const QModelIndex &parentIndex, const QString &name) {
1388         for (int row = 0; row < m_dirModel->rowCount(parentIndex); ++row) {
1389             QModelIndex idx = m_dirModel->index(row, 0, parentIndex);
1390             if (m_dirModel->itemForIndex(idx).isDir() && m_dirModel->itemForIndex(idx).name() == name) {
1391                 return idx;
1392             }
1393         }
1394         return QModelIndex();
1395     };
1396 
1397     m_dirIndex = findDir(QModelIndex(), "subdir");
1398     QVERIFY(m_dirIndex.isValid());
1399     QVERIFY(m_dirModel->hasChildren(m_dirIndex));
1400 
1401     auto listDir = [this](const QModelIndex &index) {
1402         QSignalSpy completedSpy(m_dirModel->dirLister(), qOverload<>(&KDirLister::completed));
1403         m_dirModel->fetchMore(index);
1404         return completedSpy.wait();
1405     };
1406     // Now list subdir/
1407     QVERIFY(listDir(m_dirIndex));
1408 
1409     const QModelIndex subsubdirIndex = findDir(m_dirIndex, "subsubdir");
1410     QVERIFY(subsubdirIndex.isValid());
1411     QCOMPARE(m_dirModel->hasChildren(subsubdirIndex), !dirsOnly);
1412 
1413     const QModelIndex hasChildrenDirIndex = findDir(m_dirIndex, "hasChildren");
1414     QVERIFY(hasChildrenDirIndex.isValid());
1415     QVERIFY(m_dirModel->hasChildren(hasChildrenDirIndex));
1416 
1417     // Now list hasChildren/
1418     QVERIFY(listDir(hasChildrenDirIndex));
1419 
1420     QModelIndex testDirIndex = findDir(hasChildrenDirIndex, "emptyDir");
1421     QVERIFY(testDirIndex.isValid());
1422     QVERIFY(!m_dirModel->hasChildren(testDirIndex));
1423 
1424     testDirIndex = findDir(hasChildrenDirIndex, "hiddenfileDir");
1425     QVERIFY(testDirIndex.isValid());
1426     QCOMPARE(m_dirModel->hasChildren(testDirIndex), !dirsOnly && withHidden);
1427 
1428     testDirIndex = findDir(hasChildrenDirIndex, "hiddenDirDir");
1429     QVERIFY(testDirIndex.isValid());
1430     QCOMPARE(m_dirModel->hasChildren(testDirIndex), withHidden);
1431 
1432     testDirIndex = findDir(hasChildrenDirIndex, "pipeDir");
1433     QVERIFY(testDirIndex.isValid());
1434     QCOMPARE(m_dirModel->hasChildren(testDirIndex), !dirsOnly);
1435 
1436     testDirIndex = findDir(hasChildrenDirIndex, "symlinkDir");
1437     QVERIFY(testDirIndex.isValid());
1438     QCOMPARE(m_dirModel->hasChildren(testDirIndex), !dirsOnly);
1439 
1440     m_dirModel->dirLister()->setDirOnlyMode(false);
1441     m_dirModel->dirLister()->setShowHiddenFiles(false);
1442 }
1443 
1444 void KDirModelTest::testInvalidUrl()
1445 {
1446     QSignalSpy completedSpy(m_dirModel->dirLister(), qOverload<>(&KCoreDirLister::completed));
1447     m_dirModel->openUrl(QUrl(":/"));
1448     // currently ends up in KCoreDirLister::handleError. TODO: add error signal to KDirModel
1449 }
1450 
1451 void KDirModelTest::testDeleteFile()
1452 {
1453     fillModel(true);
1454 
1455     QVERIFY(m_fileIndex.isValid());
1456     const int oldTopLevelRowCount = m_dirModel->rowCount();
1457     const QString path = m_tempDir->path() + '/';
1458     const QString file = path + "toplevelfile_1";
1459     const QUrl url = QUrl::fromLocalFile(file);
1460 
1461     QSignalSpy spyRowsRemoved(m_dirModel, &QAbstractItemModel::rowsRemoved);
1462 
1463     KIO::DeleteJob *job = KIO::del(url, KIO::HideProgressInfo);
1464     QVERIFY(job->exec());
1465 
1466     // Wait for the DBUS signal from KDirNotify, it's the one the triggers rowsRemoved
1467     if (spyRowsRemoved.isEmpty()) {
1468         QVERIFY(spyRowsRemoved.wait());
1469     }
1470 
1471     // If we come here, then rowsRemoved() was emitted - all good.
1472     const int topLevelRowCount = m_dirModel->rowCount();
1473     QCOMPARE(topLevelRowCount, oldTopLevelRowCount - 1); // one less than before
1474     QCOMPARE(spyRowsRemoved.count(), 1);
1475     QCOMPARE(spyRowsRemoved[0][1].toInt(), m_fileIndex.row());
1476     QCOMPARE(spyRowsRemoved[0][2].toInt(), m_fileIndex.row());
1477 
1478     QModelIndex fileIndex = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "toplevelfile_1"));
1479     QVERIFY(!fileIndex.isValid());
1480 
1481     // Recreate the file, for consistency in the next tests
1482     // So the second part of this test is a "testCreateFile"
1483     createTestFile(file);
1484     // Tricky problem - KDirLister::openUrl will emit items from cache
1485     // and then schedule an update; so just calling fillModel would
1486     // not wait enough, it would abort due to not finding toplevelfile_1
1487     // in the items from cache. This progressive-emitting behavior is fine
1488     // for GUIs but not for unit tests ;-)
1489     fillModel(true, false);
1490     fillModel(false);
1491 }
1492 
1493 void KDirModelTest::testDeleteFileWhileListing() // doesn't really test that yet, the kdirwatch deleted signal comes too late
1494 {
1495     const int oldTopLevelRowCount = m_dirModel->rowCount();
1496     const QString path = m_tempDir->path() + '/';
1497     const QString file = path + "toplevelfile_1";
1498     const QUrl url = QUrl::fromLocalFile(file);
1499 
1500     KDirLister *dirLister = m_dirModel->dirLister();
1501     QSignalSpy spyCompleted(dirLister, qOverload<>(&KCoreDirLister::completed));
1502     dirLister->openUrl(QUrl::fromLocalFile(path), KDirLister::NoFlags);
1503     if (!spyCompleted.isEmpty()) {
1504         QSKIP("listing completed too early");
1505     }
1506     QSignalSpy spyRowsRemoved(m_dirModel, &QAbstractItemModel::rowsRemoved);
1507     KIO::DeleteJob *job = KIO::del(url, KIO::HideProgressInfo);
1508     QVERIFY(job->exec());
1509 
1510     if (spyCompleted.isEmpty()) {
1511         QVERIFY(spyCompleted.wait());
1512     }
1513     QVERIFY(spyRowsRemoved.wait(1000));
1514 
1515     const int topLevelRowCount = m_dirModel->rowCount();
1516     QCOMPARE(topLevelRowCount, oldTopLevelRowCount - 1); // one less than before
1517     QCOMPARE(spyRowsRemoved.count(), 1);
1518     QCOMPARE(spyRowsRemoved[0][1].toInt(), m_fileIndex.row());
1519     QCOMPARE(spyRowsRemoved[0][2].toInt(), m_fileIndex.row());
1520 
1521     QModelIndex fileIndex = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "toplevelfile_1"));
1522     QVERIFY(!fileIndex.isValid());
1523 
1524     qDebug() << "Test done, recreating file";
1525 
1526     // Recreate the file, for consistency in the next tests
1527     // So the second part of this test is a "testCreateFile"
1528     createTestFile(file);
1529     fillModel(true, false); // see testDeleteFile
1530     fillModel(false);
1531 }
1532 
1533 void KDirModelTest::testOverwriteFileWithDir() // #151851 c4
1534 {
1535     fillModel(false);
1536     const QString path = m_tempDir->path() + '/';
1537     const QString dir = path + "subdir";
1538     const QString file = path + "toplevelfile_1";
1539     const int oldTopLevelRowCount = m_dirModel->rowCount();
1540 
1541     bool removalWithinTopLevel = false;
1542     bool dataChangedAtFirstLevel = false;
1543     auto rrc = connect(m_dirModel, &KDirModel::rowsRemoved, this, [&removalWithinTopLevel](const QModelIndex &index) {
1544         if (!index.isValid()) {
1545             // yes, that's what we have been waiting for
1546             removalWithinTopLevel = true;
1547         }
1548     });
1549     auto dcc = connect(m_dirModel, &KDirModel::dataChanged, this, [&dataChangedAtFirstLevel](const QModelIndex &index) {
1550         if (index.isValid() && !index.parent().isValid()) {
1551             // a change of a node whose parent is root, yay, that's it
1552             dataChangedAtFirstLevel = true;
1553         }
1554     });
1555 
1556     KIO::Job *job = KIO::move(QUrl::fromLocalFile(dir), QUrl::fromLocalFile(file), KIO::HideProgressInfo);
1557     delete KIO::delegateExtension<KIO::AskUserActionInterface *>(job);
1558     auto *askUserHandler = new MockAskUserInterface(job->uiDelegate());
1559     askUserHandler->m_renameResult = KIO::Result_Overwrite;
1560     QVERIFY(job->exec());
1561 
1562     QCOMPARE(askUserHandler->m_askUserRenameCalled, 1);
1563 
1564     // Wait for a removal within the top level (that's for the old file going away), and also
1565     // for a dataChanged which notifies us that a file has become a directory
1566 
1567     QTRY_VERIFY(removalWithinTopLevel);
1568     QTRY_VERIFY(dataChangedAtFirstLevel);
1569 
1570     m_dirModel->disconnect(rrc);
1571     m_dirModel->disconnect(dcc);
1572 
1573     // If we come here, then rowsRemoved() was emitted - all good.
1574     const int topLevelRowCount = m_dirModel->rowCount();
1575     QCOMPARE(topLevelRowCount, oldTopLevelRowCount - 1); // one less than before
1576 
1577     QVERIFY(!m_dirModel->indexForUrl(QUrl::fromLocalFile(dir)).isValid());
1578     QModelIndex newIndex = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "toplevelfile_1"));
1579     QVERIFY(newIndex.isValid());
1580     KFileItem newItem = m_dirModel->itemForIndex(newIndex);
1581     QVERIFY(newItem.isDir()); // yes, the file is a dir now ;-)
1582 
1583     qDebug() << "========= Test done, recreating test data =========";
1584 
1585     recreateTestData();
1586     fillModel(false);
1587 }
1588 
1589 void KDirModelTest::testDeleteFiles()
1590 {
1591     const int oldTopLevelRowCount = m_dirModel->rowCount();
1592     const QString file = m_tempDir->path() + "/toplevelfile_";
1593     QList<QUrl> urls;
1594     urls << QUrl::fromLocalFile(file + '1') << QUrl::fromLocalFile(file + '2') << QUrl::fromLocalFile(file + '3');
1595 
1596     QSignalSpy spyRowsRemoved(m_dirModel, &QAbstractItemModel::rowsRemoved);
1597 
1598     KIO::DeleteJob *job = KIO::del(urls, KIO::HideProgressInfo);
1599     QVERIFY(job->exec());
1600 
1601     int numRowsRemoved = 0;
1602     while (numRowsRemoved < 3) {
1603         QTest::qWait(20);
1604 
1605         numRowsRemoved = 0;
1606         for (int sigNum = 0; sigNum < spyRowsRemoved.count(); ++sigNum) {
1607             numRowsRemoved += spyRowsRemoved[sigNum][2].toInt() - spyRowsRemoved[sigNum][1].toInt() + 1;
1608         }
1609         qDebug() << "numRowsRemoved=" << numRowsRemoved;
1610     }
1611 
1612     const int topLevelRowCount = m_dirModel->rowCount();
1613     QCOMPARE(topLevelRowCount, oldTopLevelRowCount - 3); // three less than before
1614 
1615     qDebug() << "Recreating test data";
1616     recreateTestData();
1617     qDebug() << "Re-filling model";
1618     fillModel(false);
1619 }
1620 
1621 // A renaming that looks more like a deletion to the model
1622 void KDirModelTest::testRenameFileToHidden() // #174721
1623 {
1624     const QUrl url = QUrl::fromLocalFile(m_tempDir->path() + "/toplevelfile_2");
1625     const QUrl newUrl = QUrl::fromLocalFile(m_tempDir->path() + "/.toplevelfile_2");
1626 
1627     QSignalSpy spyDataChanged(m_dirModel, &QAbstractItemModel::dataChanged);
1628     QSignalSpy spyRowsRemoved(m_dirModel, &QAbstractItemModel::rowsRemoved);
1629     QSignalSpy spyRowsInserted(m_dirModel, &QAbstractItemModel::rowsInserted);
1630 
1631     KIO::SimpleJob *job = KIO::rename(url, newUrl, KIO::HideProgressInfo);
1632     QVERIFY(job->exec());
1633 
1634     // Wait for the DBUS signal from KDirNotify, it's the one the triggers KDirLister
1635     if (spyRowsRemoved.isEmpty()) {
1636         QVERIFY(spyRowsRemoved.wait());
1637     }
1638 
1639     // If we come here, then rowsRemoved() was emitted - all good.
1640     QCOMPARE(spyDataChanged.count(), 0);
1641     QCOMPARE(spyRowsRemoved.count(), 1);
1642     QCOMPARE(spyRowsInserted.count(), 0);
1643     COMPARE_INDEXES(spyRowsRemoved[0][0].value<QModelIndex>(), QModelIndex()); // parent is invalid
1644     const int row = spyRowsRemoved[0][1].toInt();
1645     QCOMPARE(row, m_secondFileIndex.row()); // only compare row
1646 
1647     spyRowsRemoved.clear();
1648 
1649     // Put things back to normal, should make the file reappear
1650     job = KIO::rename(newUrl, url, KIO::HideProgressInfo);
1651     QVERIFY(job->exec());
1652     // Wait for the DBUS signal from KDirNotify, it's the one the triggers KDirLister
1653     if (spyRowsInserted.isEmpty()) {
1654         QVERIFY(spyRowsInserted.wait());
1655     }
1656     QCOMPARE(spyDataChanged.count(), 0);
1657     QCOMPARE(spyRowsRemoved.count(), 0);
1658     QCOMPARE(spyRowsInserted.count(), 1);
1659     const int newRow = spyRowsInserted[0][1].toInt();
1660     m_secondFileIndex = m_dirModel->index(newRow, 0);
1661     QVERIFY(m_secondFileIndex.isValid());
1662     QCOMPARE(m_dirModel->itemForIndex(m_secondFileIndex).url().toString(), url.toString());
1663 }
1664 
1665 void KDirModelTest::testDeleteDirectory()
1666 {
1667     const QString path = m_tempDir->path() + '/';
1668     const QUrl url = QUrl::fromLocalFile(path + "subdir/subsubdir");
1669 
1670     QSignalSpy spyRowsRemoved(m_dirModel, &QAbstractItemModel::rowsRemoved);
1671 
1672     QSignalSpy spyDirWatchDeleted(KDirWatch::self(), &KDirWatch::deleted);
1673 
1674     KIO::DeleteJob *job = KIO::del(url, KIO::HideProgressInfo);
1675     QVERIFY(job->exec());
1676 
1677     // Wait for the DBUS signal from KDirNotify, it's the one the triggers rowsRemoved
1678     if (spyRowsRemoved.isEmpty()) {
1679         QVERIFY(spyRowsRemoved.wait());
1680     }
1681 
1682     // If we come here, then rowsRemoved() was emitted - all good.
1683     QCOMPARE(spyRowsRemoved.count(), 1);
1684 
1685     QModelIndex deletedDirIndex = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir/subsubdir"));
1686     QVERIFY(!deletedDirIndex.isValid());
1687     QModelIndex dirIndex = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir"));
1688     QVERIFY(dirIndex.isValid());
1689 
1690     // Not emitted because DeleteJobPrivate::currentSourceStated calls stopDirScan
1691     // We don't need it anyway, we have the DBus signal
1692     // QCOMPARE(spyDirWatchDeleted.count(), 1);
1693 }
1694 
1695 void KDirModelTest::testDeleteCurrentDirectory()
1696 {
1697     const int oldTopLevelRowCount = m_dirModel->rowCount();
1698     const QString path = m_tempDir->path() + '/';
1699     const QUrl url = QUrl::fromLocalFile(path);
1700 
1701     QSignalSpy spyRowsRemoved(m_dirModel, &QAbstractItemModel::rowsRemoved);
1702 
1703     KDirWatch::self()->statistics();
1704 
1705     KIO::DeleteJob *job = KIO::del(url, KIO::HideProgressInfo);
1706     QVERIFY(job->exec());
1707 
1708     // Wait for the DBUS signal from KDirNotify, it's the one the triggers rowsRemoved
1709     if (spyRowsRemoved.isEmpty()) {
1710         QVERIFY(spyRowsRemoved.wait());
1711     }
1712 
1713     // If we come here, then rowsRemoved() was emitted - all good.
1714     QTRY_COMPARE(m_dirModel->rowCount(), 0); // empty
1715 
1716     // We can get rowsRemoved for subdirs first, since kdirwatch notices that.
1717     QVERIFY(spyRowsRemoved.count() >= 1);
1718 
1719     // Look for the signal(s) that had QModelIndex() as parent.
1720     int i;
1721     int numDeleted = 0;
1722     for (i = 0; i < spyRowsRemoved.count(); ++i) {
1723         const int from = spyRowsRemoved[i][1].toInt();
1724         const int to = spyRowsRemoved[i][2].toInt();
1725         qDebug() << spyRowsRemoved[i][0].value<QModelIndex>() << from << to;
1726         if (!spyRowsRemoved[i][0].value<QModelIndex>().isValid()) {
1727             numDeleted += (to - from) + 1;
1728         }
1729     }
1730 
1731     QCOMPARE(numDeleted, oldTopLevelRowCount);
1732 
1733     QModelIndex fileIndex = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "toplevelfile_1"));
1734     QVERIFY(!fileIndex.isValid());
1735 }
1736 
1737 void KDirModelTest::testQUrlHash()
1738 {
1739     const int count = 3000;
1740     // Prepare an array of QUrls so that url constructing isn't part of the timing
1741     QVector<QUrl> urls;
1742     urls.resize(count);
1743     for (int i = 0; i < count; ++i) {
1744         urls[i] = QUrl("http://www.kde.org/path/" + QString::number(i));
1745     }
1746     QHash<QUrl, int> qurlHash;
1747     QHash<QUrl, int> kurlHash;
1748     QElapsedTimer dt;
1749     dt.start();
1750     for (int i = 0; i < count; ++i) {
1751         qurlHash.insert(urls[i], i);
1752     }
1753     // qDebug() << "inserting" << count << "urls into QHash using old qHash:" << dt.elapsed() << "msecs";
1754     dt.start();
1755     for (int i = 0; i < count; ++i) {
1756         kurlHash.insert(urls[i], i);
1757     }
1758     // qDebug() << "inserting" << count << "urls into QHash using new qHash:" << dt.elapsed() << "msecs";
1759     // Nice results: for count=30000 I got 4515 (before) and 103 (after)
1760 
1761     dt.start();
1762     for (int i = 0; i < count; ++i) {
1763         QCOMPARE(qurlHash.value(urls[i]), i);
1764     }
1765     // qDebug() << "looking up" << count << "urls into QHash using old qHash:" << dt.elapsed() << "msecs";
1766     dt.start();
1767     for (int i = 0; i < count; ++i) {
1768         QCOMPARE(kurlHash.value(urls[i]), i);
1769     }
1770     // qDebug() << "looking up" << count << "urls into QHash using new qHash:" << dt.elapsed() << "msecs";
1771     // Nice results: for count=30000 I got 4296 (before) and 63 (after)
1772 }
1773 
1774 #include "moc_kdirmodeltest.cpp"