File indexing completed on 2024-04-21 03:54:51

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     QSignalSpy spyItemsFilteredByMime(m_dirModel->dirLister(), &KCoreDirLister::itemsFilteredByMime);
0967     QSignalSpy spyItemsDeleted(m_dirModel->dirLister(), &KCoreDirLister::itemsDeleted);
0968     QSignalSpy spyRowsRemoved(m_dirModel, &QAbstractItemModel::rowsRemoved);
0969     m_dirModel->dirLister()->setNameFilter(QStringLiteral("toplevel*"));
0970     QCOMPARE(m_dirModel->rowCount(), oldTopLevelRowCount); // no change yet
0971     QCOMPARE(m_dirModel->rowCount(m_dirIndex), oldSubdirRowCount); // no change yet
0972     m_dirModel->dirLister()->emitChanges();
0973 
0974     QCOMPARE(m_dirModel->rowCount(), 4); // 3 toplevel* files, one subdir
0975     QCOMPARE(m_dirModel->rowCount(m_dirIndex), 2); // the files get filtered out, subsubdir and hasChildren are remaining
0976 
0977     // In the subdir, we can get rowsRemoved signals like (1,2) or (0,0)+(2,2),
0978     // depending on the order of the files in the model.
0979     // So QCOMPARE(spyRowsRemoved.count(), 3) is fragile, we rather need
0980     // to sum up the removed rows per parent directory.
0981     QMap<QString, int> rowsRemovedPerDir;
0982     for (int i = 0; i < spyRowsRemoved.count(); ++i) {
0983         const QVariantList args = spyRowsRemoved[i];
0984         const QModelIndex parentIdx = args[0].value<QModelIndex>();
0985         QString dirName;
0986         if (parentIdx.isValid()) {
0987             const KFileItem item = m_dirModel->itemForIndex(parentIdx);
0988             dirName = item.name();
0989         } else {
0990             dirName = QStringLiteral("root");
0991         }
0992         rowsRemovedPerDir[dirName] += args[2].toInt() - args[1].toInt() + 1;
0993         // qDebug() << parentIdx << args[1].toInt() << args[2].toInt();
0994     }
0995     QCOMPARE(rowsRemovedPerDir.count(), 3); // once for every dir
0996     QCOMPARE(rowsRemovedPerDir.value("root"), 1); // one from toplevel ('special chars')
0997     QCOMPARE(rowsRemovedPerDir.value("subdir"), 2); // two from subdir
0998     QCOMPARE(rowsRemovedPerDir.value("subsubdir"), 1); // one from subsubdir
0999     QCOMPARE(spyItemsDeleted.count(), 3); // once for every dir
1000     QCOMPARE(spyItemsDeleted[0][0].value<KFileItemList>().count(), 1); // one from toplevel ('special chars')
1001     QCOMPARE(spyItemsDeleted[1][0].value<KFileItemList>().count(), 2); // two from subdir
1002     QCOMPARE(spyItemsDeleted[2][0].value<KFileItemList>().count(), 1); // one from subsubdir
1003     QCOMPARE(spyItemsFilteredByMime.count(), 0);
1004     spyItemsDeleted.clear();
1005     spyItemsFilteredByMime.clear();
1006 
1007     // Reset the filter
1008     qDebug() << "reset to no filter";
1009     m_dirModel->dirLister()->setNameFilter(QString());
1010     m_dirModel->dirLister()->emitChanges();
1011 
1012     QCOMPARE(m_dirModel->rowCount(), oldTopLevelRowCount);
1013     QCOMPARE(m_dirModel->rowCount(m_dirIndex), oldSubdirRowCount);
1014     QCOMPARE(spyItemsDeleted.count(), 0);
1015     QCOMPARE(spyItemsFilteredByMime.count(), 0);
1016 
1017     // The order of things changed because of filtering.
1018     // Fill again, so that m_fileIndex etc. are correct again.
1019     fillModel(true);
1020 }
1021 
1022 void KDirModelTest::testFilterPatterns()
1023 {
1024     QVERIFY(m_dirIndex.isValid());
1025 
1026     const int oldTopLevelRowCount = m_dirModel->rowCount();
1027     const int oldSubdirRowCount = m_dirModel->rowCount(m_dirIndex);
1028 
1029     // matching without wildcard -> no files
1030     m_dirModel->dirLister()->setNameFilter(QStringLiteral("toplevel"));
1031     QCOMPARE(m_dirModel->rowCount(), oldTopLevelRowCount); // no change yet
1032     QCOMPARE(m_dirModel->rowCount(m_dirIndex), oldSubdirRowCount); // no change yet
1033     m_dirModel->dirLister()->emitChanges();
1034 
1035     QCOMPARE(m_dirModel->rowCount(), 1); // the files get filtered out, subdir
1036     QCOMPARE(m_dirModel->rowCount(m_dirIndex), 2); // the files get filtered out, subsubdir and hasChildren
1037 
1038     // Reset the filter
1039     m_dirModel->dirLister()->setNameFilter(QString());
1040     m_dirModel->dirLister()->emitChanges();
1041 
1042     // Back to original state
1043     QCOMPARE(m_dirModel->rowCount(), oldTopLevelRowCount);
1044     QCOMPARE(m_dirModel->rowCount(m_dirIndex), oldSubdirRowCount);
1045 
1046     m_dirModel->dirLister()->setNameFilter(QStringLiteral("toplevel*")); // Matching with a wildcard
1047     QCOMPARE(m_dirModel->rowCount(), oldTopLevelRowCount); // no change yet
1048     QCOMPARE(m_dirModel->rowCount(m_dirIndex), oldSubdirRowCount); // no change yet
1049     m_dirModel->dirLister()->emitChanges();
1050 
1051     QCOMPARE(m_dirModel->rowCount(), 4); // 3 files, one subdir with "toplevel*" in the name
1052     QCOMPARE(m_dirModel->rowCount(m_dirIndex), 2); // the files get filtered out, subsubdir and hasChildren are remaining
1053 
1054     m_dirModel->dirLister()->setNameFilter(QString());
1055     m_dirModel->dirLister()->emitChanges();
1056 
1057     QCOMPARE(m_dirModel->rowCount(), oldTopLevelRowCount);
1058     QCOMPARE(m_dirModel->rowCount(m_dirIndex), oldSubdirRowCount);
1059 
1060     // The order of things changed because of filtering.
1061     // Fill again, so that m_fileIndex etc. are correct again.
1062     fillModel(true);
1063 }
1064 
1065 void KDirModelTest::testMimeFilter()
1066 {
1067     QVERIFY(m_dirIndex.isValid());
1068     const int oldTopLevelRowCount = m_dirModel->rowCount();
1069     const int oldSubdirRowCount = m_dirModel->rowCount(m_dirIndex);
1070     QSignalSpy spyItemsFilteredByMime(m_dirModel->dirLister(), &KCoreDirLister::itemsFilteredByMime);
1071     QSignalSpy spyItemsDeleted(m_dirModel->dirLister(), &KCoreDirLister::itemsDeleted);
1072     QSignalSpy spyRowsRemoved(m_dirModel, &QAbstractItemModel::rowsRemoved);
1073     m_dirModel->dirLister()->setMimeFilter({QStringLiteral("application/pdf")});
1074     QCOMPARE(m_dirModel->rowCount(), oldTopLevelRowCount); // no change yet
1075     QCOMPARE(m_dirModel->rowCount(m_dirIndex), oldSubdirRowCount); // no change yet
1076     m_dirModel->dirLister()->emitChanges();
1077 
1078     QCOMPARE(m_dirModel->rowCount(), 1); // 1 pdf files, no subdir anymore
1079 
1080     QVERIFY(spyRowsRemoved.count() >= 1); // depends on contiguity...
1081     QVERIFY(spyItemsDeleted.count() >= 1); // once for every dir
1082     QCOMPARE(spyItemsFilteredByMime.count(), 3);
1083     spyItemsDeleted.clear();
1084     spyItemsFilteredByMime.clear();
1085 
1086     // Reset the filter
1087     m_dirModel->dirLister()->setMimeFilter({});
1088     m_dirModel->dirLister()->emitChanges();
1089 
1090     QCOMPARE(m_dirModel->rowCount(), oldTopLevelRowCount);
1091     QCOMPARE(spyItemsDeleted.count(), 0);
1092     QCOMPARE(spyItemsFilteredByMime.count(), 0);
1093 
1094     // The order of things changed because of filtering.
1095     // Fill again, so that m_fileIndex etc. are correct again.
1096     fillModel(true);
1097 }
1098 
1099 void KDirModelTest::testMimeExcludeFilter()
1100 {
1101     QVERIFY(m_dirIndex.isValid());
1102     const int oldTopLevelRowCount = m_dirModel->rowCount();
1103     const int oldSubdirRowCount = m_dirModel->rowCount(m_dirIndex);
1104     QSignalSpy spyItemsFilteredByMime(m_dirModel->dirLister(), &KCoreDirLister::itemsFilteredByMime);
1105     QSignalSpy spyItemsDeleted(m_dirModel->dirLister(), &KCoreDirLister::itemsDeleted);
1106     QSignalSpy spyRowsRemoved(m_dirModel, &QAbstractItemModel::rowsRemoved);
1107     m_dirModel->dirLister()->setMimeExcludeFilter({QStringLiteral("application/pdf")});
1108     QCOMPARE(m_dirModel->rowCount(), oldTopLevelRowCount); // no change yet
1109     QCOMPARE(m_dirModel->rowCount(m_dirIndex), oldSubdirRowCount); // no change yet
1110     m_dirModel->dirLister()->emitChanges();
1111 
1112     QCOMPARE(m_dirModel->rowCount(), oldTopLevelRowCount - 1); // no pdf files anymore
1113 
1114     QCOMPARE(spyRowsRemoved.count(), 1); // one pdf file removed ...
1115     QCOMPARE(spyItemsDeleted.count(), 1);
1116     QCOMPARE(spyItemsFilteredByMime.count(), 1);
1117     QCOMPARE(spyItemsFilteredByMime[0][0].value<KFileItemList>().count(), 1);
1118     spyItemsFilteredByMime.clear();
1119     spyItemsDeleted.clear();
1120 
1121     // Reset the exclude filter
1122     m_dirModel->dirLister()->setMimeExcludeFilter({});
1123     m_dirModel->dirLister()->emitChanges();
1124 
1125     QCOMPARE(m_dirModel->rowCount(), oldTopLevelRowCount);
1126     QCOMPARE(spyItemsDeleted.count(), 0);
1127     QCOMPARE(spyItemsFilteredByMime.count(), 0);
1128 
1129     // The order of things changed because of filtering.
1130     // Fill again, so that m_fileIndex etc. are correct again.
1131     fillModel(true);
1132 }
1133 
1134 void KDirModelTest::testShowHiddenFiles() // #174788
1135 {
1136     KDirLister *dirLister = m_dirModel->dirLister();
1137 
1138     QSignalSpy spyRowsRemoved(m_dirModel, &QAbstractItemModel::rowsRemoved);
1139     QSignalSpy spyNewItems(dirLister, &KCoreDirLister::newItems);
1140     QSignalSpy spyRowsInserted(m_dirModel, &QAbstractItemModel::rowsInserted);
1141     dirLister->setShowHiddenFiles(true);
1142     dirLister->emitChanges();
1143     const int numberOfDotFiles = 2;
1144     QCOMPARE(spyNewItems.count(), 1);
1145     QCOMPARE(spyNewItems[0][0].value<KFileItemList>().count(), numberOfDotFiles);
1146     QCOMPARE(spyRowsInserted.count(), 1);
1147     QCOMPARE(spyRowsRemoved.count(), 0);
1148     spyNewItems.clear();
1149     spyRowsInserted.clear();
1150 
1151     dirLister->setShowHiddenFiles(false);
1152     dirLister->emitChanges();
1153     QCOMPARE(spyNewItems.count(), 0);
1154     QCOMPARE(spyRowsInserted.count(), 0);
1155     QCOMPARE(spyRowsRemoved.count(), 1);
1156 }
1157 
1158 void KDirModelTest::testMultipleSlashes()
1159 {
1160     const QString path = m_tempDir->path() + '/';
1161 
1162     QModelIndex index = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir//testfile"));
1163     QVERIFY(index.isValid());
1164 
1165     index = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir//subsubdir//"));
1166     QVERIFY(index.isValid());
1167 
1168     index = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir///subsubdir////testfile"));
1169     QVERIFY(index.isValid());
1170 }
1171 
1172 void KDirModelTest::testUrlWithRef() // #171117
1173 {
1174     const QString path = m_tempDir->path() + '/';
1175     KDirLister *dirLister = m_dirModel->dirLister();
1176     QUrl url = QUrl::fromLocalFile(path);
1177     url.setFragment(QStringLiteral("ref"));
1178     QVERIFY(url.url().endsWith(QLatin1String("#ref")));
1179     QSignalSpy spyCompleted(dirLister, qOverload<>(&KCoreDirLister::completed));
1180     dirLister->openUrl(url, KDirLister::NoFlags);
1181     QVERIFY(spyCompleted.wait());
1182 
1183     QCOMPARE(dirLister->url().toString(), url.toString(QUrl::StripTrailingSlash));
1184     collectKnownIndexes();
1185 }
1186 
1187 // void KDirModelTest::testFontUrlWithHost() // #160057 --> moved to kio_fonts (kfontinst/kio/autotests)
1188 
1189 void KDirModelTest::testRemoteUrlWithHost() // #178416
1190 {
1191     if (!KProtocolInfo::isKnownProtocol(QStringLiteral("remote"))) {
1192         QSKIP("kio_remote not installed");
1193     }
1194     QUrl url(QStringLiteral("remote://foo"));
1195     KDirLister *dirLister = m_dirModel->dirLister();
1196     QSignalSpy spyCompleted(dirLister, qOverload<>(&KCoreDirLister::completed));
1197     dirLister->openUrl(url, KDirLister::NoFlags);
1198     QVERIFY(spyCompleted.wait());
1199 
1200     QCOMPARE(dirLister->url().toString(), QString("remote://foo"));
1201 }
1202 
1203 void KDirModelTest::testZipFile() // # 171721
1204 {
1205     const QString path = QFileInfo(QFINDTESTDATA("wronglocalsizes.zip")).absolutePath();
1206     KDirLister *dirLister = m_dirModel->dirLister();
1207     QSignalSpy spyCompleted(dirLister, qOverload<>(&KCoreDirLister::completed));
1208     dirLister->openUrl(QUrl::fromLocalFile(path), KDirLister::NoFlags);
1209     QVERIFY(spyCompleted.wait());
1210 
1211     QUrl zipUrl(QUrl::fromLocalFile(path));
1212     zipUrl.setPath(zipUrl.path() + "/wronglocalsizes.zip"); // just a zip file lying here for other reasons
1213 
1214     QVERIFY(QFile::exists(zipUrl.toLocalFile()));
1215     zipUrl.setScheme(QStringLiteral("zip"));
1216     QModelIndex index = m_dirModel->indexForUrl(zipUrl);
1217     QVERIFY(!index.isValid()); // protocol mismatch, can't find it!
1218     zipUrl.setScheme(QStringLiteral("file"));
1219     index = m_dirModel->indexForUrl(zipUrl);
1220     QVERIFY(index.isValid());
1221 }
1222 
1223 class MyDirLister : public KDirLister
1224 {
1225 public:
1226     void emitItemsDeleted(const KFileItemList &items)
1227     {
1228         Q_EMIT itemsDeleted(items);
1229     }
1230 };
1231 
1232 void KDirModelTest::testBug196695()
1233 {
1234     KFileItem rootItem(QUrl::fromLocalFile(m_tempDir->path()), QString(), KFileItem::Unknown);
1235     KFileItem childItem(QUrl::fromLocalFile(QString(m_tempDir->path() + "/toplevelfile_1")), QString(), KFileItem::Unknown);
1236 
1237     KFileItemList list;
1238     // Important: the root item must not be first in the list to trigger bug 196695
1239     list << childItem << rootItem;
1240 
1241     MyDirLister *dirLister = static_cast<MyDirLister *>(m_dirModel->dirLister());
1242     dirLister->emitItemsDeleted(list);
1243 
1244     fillModel(true);
1245 }
1246 
1247 void KDirModelTest::testMimeData()
1248 {
1249     QModelIndex index0 = m_dirModel->index(0, 0);
1250     QVERIFY(index0.isValid());
1251     QModelIndex index1 = m_dirModel->index(1, 0);
1252     QVERIFY(index1.isValid());
1253     QList<QModelIndex> indexes;
1254     indexes << index0 << index1;
1255     QMimeData *mimeData = m_dirModel->mimeData(indexes);
1256     QVERIFY(mimeData);
1257     QVERIFY(mimeData->hasUrls());
1258     const QList<QUrl> urls = mimeData->urls();
1259     QCOMPARE(urls.count(), indexes.count());
1260     delete mimeData;
1261 }
1262 
1263 void KDirModelTest::testDotHiddenFile_data()
1264 {
1265     QTest::addColumn<QStringList>("fileContents");
1266     QTest::addColumn<QStringList>("expectedListing");
1267 
1268     const QStringList allItems{QStringLiteral("toplevelfile_1"),
1269                                QStringLiteral("toplevelfile_2"),
1270                                QStringLiteral("toplevelfile_3"),
1271                                specialChars(),
1272                                QStringLiteral("subdir")};
1273     QTest::newRow("empty_file") << (QStringList{}) << allItems;
1274 
1275     QTest::newRow("simple_name") << (QStringList{QStringLiteral("toplevelfile_1")}) << QStringList(allItems.mid(1));
1276 
1277     QStringList allButSpecialChars = allItems;
1278     allButSpecialChars.removeAt(3);
1279     QTest::newRow("special_chars") << (QStringList{specialChars()}) << allButSpecialChars;
1280 
1281     QStringList allButSubdir = allItems;
1282     allButSubdir.removeAt(4);
1283     QTest::newRow("subdir") << (QStringList{QStringLiteral("subdir")}) << allButSubdir;
1284 
1285     QTest::newRow("many_lines")
1286         << (QStringList{QStringLiteral("subdir"), QStringLiteral("toplevelfile_1"), QStringLiteral("toplevelfile_3"), QStringLiteral("toplevelfile_2")})
1287         << QStringList{specialChars()};
1288 }
1289 
1290 void KDirModelTest::testDotHiddenFile()
1291 {
1292     QFETCH(QStringList, fileContents);
1293     QFETCH(QStringList, expectedListing);
1294 
1295     const QString path = m_tempDir->path() + '/';
1296     const QString dotHiddenFile = path + ".hidden";
1297     QTest::qWait(1000); // mtime-based cache, so we need to wait for 1 second
1298     QFile dh(dotHiddenFile);
1299     QVERIFY(dh.open(QIODevice::WriteOnly));
1300     dh.write(fileContents.join('\n').toUtf8());
1301     dh.close();
1302 
1303     // Do it twice: once to read from the file and once to use the cache
1304     for (int i = 0; i < 2; ++i) {
1305         fillModel(true, false);
1306         QStringList files;
1307         for (int row = 0; row < m_dirModel->rowCount(); ++row) {
1308             files.append(m_dirModel->index(row, KDirModel::Name).data().toString());
1309         }
1310         files.sort();
1311         expectedListing.sort();
1312         QCOMPARE(files, expectedListing);
1313     }
1314 
1315     dh.remove();
1316 }
1317 
1318 void KDirModelTest::testShowRoot()
1319 {
1320     KDirModel dirModel;
1321     const QUrl homeUrl = QUrl::fromLocalFile(QDir::homePath());
1322     const QUrl fsRootUrl = QUrl(QStringLiteral("file:///"));
1323 
1324     // openUrl("/", ShowRoot) should create a "/" item
1325     dirModel.openUrl(fsRootUrl, KDirModel::ShowRoot);
1326     QTRY_COMPARE(dirModel.rowCount(), 1);
1327     const QModelIndex rootIndex = dirModel.index(0, 0);
1328     QVERIFY(rootIndex.isValid());
1329     QCOMPARE(rootIndex.data().toString(), QStringLiteral("/"));
1330     QVERIFY(!dirModel.parent(rootIndex).isValid());
1331     QCOMPARE(dirModel.itemForIndex(rootIndex).url(), QUrl(QStringLiteral("file:///")));
1332     QCOMPARE(dirModel.itemForIndex(rootIndex).name(), QStringLiteral("/"));
1333 
1334     // expandToUrl should work
1335     dirModel.expandToUrl(homeUrl);
1336     QTRY_VERIFY(dirModel.indexForUrl(homeUrl).isValid());
1337 
1338     // test itemForIndex and indexForUrl
1339     QCOMPARE(dirModel.itemForIndex(QModelIndex()).url(), QUrl());
1340     QVERIFY(!dirModel.indexForUrl(QUrl()).isValid());
1341     const QUrl slashUrl = QUrl::fromLocalFile(QStringLiteral("/"));
1342     QCOMPARE(dirModel.indexForUrl(slashUrl), rootIndex);
1343 
1344     // switching to another URL should also show a root node
1345     QSignalSpy spyRowsRemoved(&dirModel, &QAbstractItemModel::rowsRemoved);
1346     const QUrl tempUrl = QUrl::fromLocalFile(QDir::tempPath());
1347     dirModel.openUrl(tempUrl, KDirModel::ShowRoot);
1348     QTRY_COMPARE(dirModel.rowCount(), 1);
1349     QCOMPARE(spyRowsRemoved.count(), 1);
1350     const QModelIndex newRootIndex = dirModel.index(0, 0);
1351     QVERIFY(newRootIndex.isValid());
1352     QCOMPARE(newRootIndex.data().toString(), QFileInfo(QDir::tempPath()).fileName());
1353     QVERIFY(!dirModel.parent(newRootIndex).isValid());
1354     QVERIFY(!dirModel.indexForUrl(slashUrl).isValid());
1355     QCOMPARE(dirModel.itemForIndex(newRootIndex).url(), tempUrl);
1356 }
1357 
1358 void KDirModelTest::testShowRootWithTrailingSlash()
1359 {
1360     // GIVEN
1361     KDirModel dirModel;
1362     const QUrl homeUrl = QUrl::fromLocalFile(QDir::homePath() + QLatin1Char('/'));
1363 
1364     // WHEN
1365     dirModel.openUrl(homeUrl, KDirModel::ShowRoot);
1366     QTRY_VERIFY(dirModel.indexForUrl(homeUrl).isValid());
1367 }
1368 
1369 void KDirModelTest::testShowRootAndExpandToUrl()
1370 {
1371     // call expandToUrl without waiting for initial listing of root node
1372     KDirModel dirModel;
1373     dirModel.openUrl(QUrl(QStringLiteral("file:///")), KDirModel::ShowRoot);
1374     const QUrl homeUrl = QUrl::fromLocalFile(QDir::homePath());
1375     dirModel.expandToUrl(homeUrl);
1376     QTRY_VERIFY(dirModel.indexForUrl(homeUrl).isValid());
1377 }
1378 
1379 void KDirModelTest::testHasChildren_data()
1380 {
1381     QTest::addColumn<bool>("dirsOnly");
1382     QTest::addColumn<bool>("withHidden");
1383 
1384     QTest::newRow("with_files_and_no_hidden") << false << false;
1385     QTest::newRow("dirs_only_and_no_hidden") << true << false;
1386     QTest::newRow("with_files_and_hidden") << false << true;
1387     QTest::newRow("dirs_only_with_hidden") << true << true;
1388 }
1389 
1390 // Test hasChildren without first populating the dirs
1391 void KDirModelTest::testHasChildren()
1392 {
1393     QFETCH(bool, dirsOnly);
1394     QFETCH(bool, withHidden);
1395 
1396     m_dirModel->dirLister()->setDirOnlyMode(dirsOnly);
1397     m_dirModel->dirLister()->setShowHiddenFiles(withHidden);
1398     fillModel(true, false);
1399 
1400     QVERIFY(m_dirModel->hasChildren());
1401 
1402     auto findDir = [this](const QModelIndex &parentIndex, const QString &name) {
1403         for (int row = 0; row < m_dirModel->rowCount(parentIndex); ++row) {
1404             QModelIndex idx = m_dirModel->index(row, 0, parentIndex);
1405             if (m_dirModel->itemForIndex(idx).isDir() && m_dirModel->itemForIndex(idx).name() == name) {
1406                 return idx;
1407             }
1408         }
1409         return QModelIndex();
1410     };
1411 
1412     m_dirIndex = findDir(QModelIndex(), "subdir");
1413     QVERIFY(m_dirIndex.isValid());
1414     QVERIFY(m_dirModel->hasChildren(m_dirIndex));
1415 
1416     auto listDir = [this](const QModelIndex &index) {
1417         QSignalSpy completedSpy(m_dirModel->dirLister(), qOverload<>(&KDirLister::completed));
1418         m_dirModel->fetchMore(index);
1419         return completedSpy.wait();
1420     };
1421     // Now list subdir/
1422     QVERIFY(listDir(m_dirIndex));
1423 
1424     const QModelIndex subsubdirIndex = findDir(m_dirIndex, "subsubdir");
1425     QVERIFY(subsubdirIndex.isValid());
1426     QCOMPARE(m_dirModel->hasChildren(subsubdirIndex), !dirsOnly);
1427 
1428     const QModelIndex hasChildrenDirIndex = findDir(m_dirIndex, "hasChildren");
1429     QVERIFY(hasChildrenDirIndex.isValid());
1430     QVERIFY(m_dirModel->hasChildren(hasChildrenDirIndex));
1431 
1432     // Now list hasChildren/
1433     QVERIFY(listDir(hasChildrenDirIndex));
1434 
1435     QModelIndex testDirIndex = findDir(hasChildrenDirIndex, "emptyDir");
1436     QVERIFY(testDirIndex.isValid());
1437     QVERIFY(!m_dirModel->hasChildren(testDirIndex));
1438 
1439     testDirIndex = findDir(hasChildrenDirIndex, "hiddenfileDir");
1440     QVERIFY(testDirIndex.isValid());
1441     QCOMPARE(m_dirModel->hasChildren(testDirIndex), !dirsOnly && withHidden);
1442 
1443     testDirIndex = findDir(hasChildrenDirIndex, "hiddenDirDir");
1444     QVERIFY(testDirIndex.isValid());
1445     QCOMPARE(m_dirModel->hasChildren(testDirIndex), withHidden);
1446 
1447     testDirIndex = findDir(hasChildrenDirIndex, "pipeDir");
1448     QVERIFY(testDirIndex.isValid());
1449     QCOMPARE(m_dirModel->hasChildren(testDirIndex), !dirsOnly);
1450 
1451     testDirIndex = findDir(hasChildrenDirIndex, "symlinkDir");
1452     QVERIFY(testDirIndex.isValid());
1453     QCOMPARE(m_dirModel->hasChildren(testDirIndex), !dirsOnly);
1454 
1455     m_dirModel->dirLister()->setDirOnlyMode(false);
1456     m_dirModel->dirLister()->setShowHiddenFiles(false);
1457 }
1458 
1459 void KDirModelTest::testInvalidUrl()
1460 {
1461     QSignalSpy completedSpy(m_dirModel->dirLister(), qOverload<>(&KCoreDirLister::completed));
1462     m_dirModel->openUrl(QUrl(":/"));
1463     // currently ends up in KCoreDirLister::handleError. TODO: add error signal to KDirModel
1464 }
1465 
1466 void KDirModelTest::testDeleteFile()
1467 {
1468     fillModel(true);
1469 
1470     QVERIFY(m_fileIndex.isValid());
1471     const int oldTopLevelRowCount = m_dirModel->rowCount();
1472     const QString path = m_tempDir->path() + '/';
1473     const QString file = path + "toplevelfile_1";
1474     const QUrl url = QUrl::fromLocalFile(file);
1475 
1476     QSignalSpy spyRowsRemoved(m_dirModel, &QAbstractItemModel::rowsRemoved);
1477 
1478     KIO::DeleteJob *job = KIO::del(url, KIO::HideProgressInfo);
1479     QVERIFY(job->exec());
1480 
1481     // Wait for the DBUS signal from KDirNotify, it's the one the triggers rowsRemoved
1482     if (spyRowsRemoved.isEmpty()) {
1483         QVERIFY(spyRowsRemoved.wait());
1484     }
1485 
1486     // If we come here, then rowsRemoved() was emitted - all good.
1487     const int topLevelRowCount = m_dirModel->rowCount();
1488     QCOMPARE(topLevelRowCount, oldTopLevelRowCount - 1); // one less than before
1489     QCOMPARE(spyRowsRemoved.count(), 1);
1490     QCOMPARE(spyRowsRemoved[0][1].toInt(), m_fileIndex.row());
1491     QCOMPARE(spyRowsRemoved[0][2].toInt(), m_fileIndex.row());
1492 
1493     QModelIndex fileIndex = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "toplevelfile_1"));
1494     QVERIFY(!fileIndex.isValid());
1495 
1496     // Recreate the file, for consistency in the next tests
1497     // So the second part of this test is a "testCreateFile"
1498     createTestFile(file);
1499     // Tricky problem - KDirLister::openUrl will emit items from cache
1500     // and then schedule an update; so just calling fillModel would
1501     // not wait enough, it would abort due to not finding toplevelfile_1
1502     // in the items from cache. This progressive-emitting behavior is fine
1503     // for GUIs but not for unit tests ;-)
1504     fillModel(true, false);
1505     fillModel(false);
1506 }
1507 
1508 void KDirModelTest::testDeleteFileWhileListing() // doesn't really test that yet, the kdirwatch deleted signal comes too late
1509 {
1510     const int oldTopLevelRowCount = m_dirModel->rowCount();
1511     const QString path = m_tempDir->path() + '/';
1512     const QString file = path + "toplevelfile_1";
1513     const QUrl url = QUrl::fromLocalFile(file);
1514 
1515     KDirLister *dirLister = m_dirModel->dirLister();
1516     QSignalSpy spyCompleted(dirLister, qOverload<>(&KCoreDirLister::completed));
1517     dirLister->openUrl(QUrl::fromLocalFile(path), KDirLister::NoFlags);
1518     if (!spyCompleted.isEmpty()) {
1519         QSKIP("listing completed too early");
1520     }
1521     QSignalSpy spyRowsRemoved(m_dirModel, &QAbstractItemModel::rowsRemoved);
1522     KIO::DeleteJob *job = KIO::del(url, KIO::HideProgressInfo);
1523     QVERIFY(job->exec());
1524 
1525     if (spyCompleted.isEmpty()) {
1526         QVERIFY(spyCompleted.wait());
1527     }
1528     QVERIFY(spyRowsRemoved.wait(1000));
1529 
1530     const int topLevelRowCount = m_dirModel->rowCount();
1531     QCOMPARE(topLevelRowCount, oldTopLevelRowCount - 1); // one less than before
1532     QCOMPARE(spyRowsRemoved.count(), 1);
1533     QCOMPARE(spyRowsRemoved[0][1].toInt(), m_fileIndex.row());
1534     QCOMPARE(spyRowsRemoved[0][2].toInt(), m_fileIndex.row());
1535 
1536     QModelIndex fileIndex = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "toplevelfile_1"));
1537     QVERIFY(!fileIndex.isValid());
1538 
1539     qDebug() << "Test done, recreating file";
1540 
1541     // Recreate the file, for consistency in the next tests
1542     // So the second part of this test is a "testCreateFile"
1543     createTestFile(file);
1544     fillModel(true, false); // see testDeleteFile
1545     fillModel(false);
1546 }
1547 
1548 void KDirModelTest::testOverwriteFileWithDir() // #151851 c4
1549 {
1550     fillModel(false);
1551     const QString path = m_tempDir->path() + '/';
1552     const QString dir = path + "subdir";
1553     const QString file = path + "toplevelfile_1";
1554     const int oldTopLevelRowCount = m_dirModel->rowCount();
1555 
1556     bool removalWithinTopLevel = false;
1557     bool dataChangedAtFirstLevel = false;
1558     auto rrc = connect(m_dirModel, &KDirModel::rowsRemoved, this, [&removalWithinTopLevel](const QModelIndex &index) {
1559         if (!index.isValid()) {
1560             // yes, that's what we have been waiting for
1561             removalWithinTopLevel = true;
1562         }
1563     });
1564     auto dcc = connect(m_dirModel, &KDirModel::dataChanged, this, [&dataChangedAtFirstLevel](const QModelIndex &index) {
1565         if (index.isValid() && !index.parent().isValid()) {
1566             // a change of a node whose parent is root, yay, that's it
1567             dataChangedAtFirstLevel = true;
1568         }
1569     });
1570 
1571     KIO::Job *job = KIO::move(QUrl::fromLocalFile(dir), QUrl::fromLocalFile(file), KIO::HideProgressInfo);
1572     delete KIO::delegateExtension<KIO::AskUserActionInterface *>(job);
1573     auto *askUserHandler = new MockAskUserInterface(job->uiDelegate());
1574     askUserHandler->m_renameResult = KIO::Result_Overwrite;
1575     QVERIFY(job->exec());
1576 
1577     QCOMPARE(askUserHandler->m_askUserRenameCalled, 1);
1578 
1579     // Wait for a removal within the top level (that's for the old file going away), and also
1580     // for a dataChanged which notifies us that a file has become a directory
1581 
1582     QTRY_VERIFY(removalWithinTopLevel);
1583     QTRY_VERIFY(dataChangedAtFirstLevel);
1584 
1585     m_dirModel->disconnect(rrc);
1586     m_dirModel->disconnect(dcc);
1587 
1588     // If we come here, then rowsRemoved() was emitted - all good.
1589     const int topLevelRowCount = m_dirModel->rowCount();
1590     QCOMPARE(topLevelRowCount, oldTopLevelRowCount - 1); // one less than before
1591 
1592     QVERIFY(!m_dirModel->indexForUrl(QUrl::fromLocalFile(dir)).isValid());
1593     QModelIndex newIndex = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "toplevelfile_1"));
1594     QVERIFY(newIndex.isValid());
1595     KFileItem newItem = m_dirModel->itemForIndex(newIndex);
1596     QVERIFY(newItem.isDir()); // yes, the file is a dir now ;-)
1597 
1598     qDebug() << "========= Test done, recreating test data =========";
1599 
1600     recreateTestData();
1601     fillModel(false);
1602 }
1603 
1604 void KDirModelTest::testDeleteFiles()
1605 {
1606     const int oldTopLevelRowCount = m_dirModel->rowCount();
1607     const QString file = m_tempDir->path() + "/toplevelfile_";
1608     QList<QUrl> urls;
1609     urls << QUrl::fromLocalFile(file + '1') << QUrl::fromLocalFile(file + '2') << QUrl::fromLocalFile(file + '3');
1610 
1611     QSignalSpy spyRowsRemoved(m_dirModel, &QAbstractItemModel::rowsRemoved);
1612 
1613     KIO::DeleteJob *job = KIO::del(urls, KIO::HideProgressInfo);
1614     QVERIFY(job->exec());
1615 
1616     int numRowsRemoved = 0;
1617     while (numRowsRemoved < 3) {
1618         QTest::qWait(20);
1619 
1620         numRowsRemoved = 0;
1621         for (int sigNum = 0; sigNum < spyRowsRemoved.count(); ++sigNum) {
1622             numRowsRemoved += spyRowsRemoved[sigNum][2].toInt() - spyRowsRemoved[sigNum][1].toInt() + 1;
1623         }
1624         qDebug() << "numRowsRemoved=" << numRowsRemoved;
1625     }
1626 
1627     const int topLevelRowCount = m_dirModel->rowCount();
1628     QCOMPARE(topLevelRowCount, oldTopLevelRowCount - 3); // three less than before
1629 
1630     qDebug() << "Recreating test data";
1631     recreateTestData();
1632     qDebug() << "Re-filling model";
1633     fillModel(false);
1634 }
1635 
1636 // A renaming that looks more like a deletion to the model
1637 void KDirModelTest::testRenameFileToHidden() // #174721
1638 {
1639     const QUrl url = QUrl::fromLocalFile(m_tempDir->path() + "/toplevelfile_2");
1640     const QUrl newUrl = QUrl::fromLocalFile(m_tempDir->path() + "/.toplevelfile_2");
1641 
1642     QSignalSpy spyDataChanged(m_dirModel, &QAbstractItemModel::dataChanged);
1643     QSignalSpy spyRowsRemoved(m_dirModel, &QAbstractItemModel::rowsRemoved);
1644     QSignalSpy spyRowsInserted(m_dirModel, &QAbstractItemModel::rowsInserted);
1645 
1646     KIO::SimpleJob *job = KIO::rename(url, newUrl, KIO::HideProgressInfo);
1647     QVERIFY(job->exec());
1648 
1649     // Wait for the DBUS signal from KDirNotify, it's the one the triggers KDirLister
1650     if (spyRowsRemoved.isEmpty()) {
1651         QVERIFY(spyRowsRemoved.wait());
1652     }
1653 
1654     // If we come here, then rowsRemoved() was emitted - all good.
1655     QCOMPARE(spyDataChanged.count(), 0);
1656     QCOMPARE(spyRowsRemoved.count(), 1);
1657     QCOMPARE(spyRowsInserted.count(), 0);
1658     COMPARE_INDEXES(spyRowsRemoved[0][0].value<QModelIndex>(), QModelIndex()); // parent is invalid
1659     const int row = spyRowsRemoved[0][1].toInt();
1660     QCOMPARE(row, m_secondFileIndex.row()); // only compare row
1661 
1662     spyRowsRemoved.clear();
1663 
1664     // Put things back to normal, should make the file reappear
1665     job = KIO::rename(newUrl, url, KIO::HideProgressInfo);
1666     QVERIFY(job->exec());
1667     // Wait for the DBUS signal from KDirNotify, it's the one the triggers KDirLister
1668     if (spyRowsInserted.isEmpty()) {
1669         QVERIFY(spyRowsInserted.wait());
1670     }
1671     QCOMPARE(spyDataChanged.count(), 0);
1672     QCOMPARE(spyRowsRemoved.count(), 0);
1673     QCOMPARE(spyRowsInserted.count(), 1);
1674     const int newRow = spyRowsInserted[0][1].toInt();
1675     m_secondFileIndex = m_dirModel->index(newRow, 0);
1676     QVERIFY(m_secondFileIndex.isValid());
1677     QCOMPARE(m_dirModel->itemForIndex(m_secondFileIndex).url().toString(), url.toString());
1678 }
1679 
1680 void KDirModelTest::testDeleteDirectory()
1681 {
1682     const QString path = m_tempDir->path() + '/';
1683     const QUrl url = QUrl::fromLocalFile(path + "subdir/subsubdir");
1684 
1685     QSignalSpy spyRowsRemoved(m_dirModel, &QAbstractItemModel::rowsRemoved);
1686 
1687     QSignalSpy spyDirWatchDeleted(KDirWatch::self(), &KDirWatch::deleted);
1688 
1689     KIO::DeleteJob *job = KIO::del(url, KIO::HideProgressInfo);
1690     QVERIFY(job->exec());
1691 
1692     // Wait for the DBUS signal from KDirNotify, it's the one the triggers rowsRemoved
1693     if (spyRowsRemoved.isEmpty()) {
1694         QVERIFY(spyRowsRemoved.wait());
1695     }
1696 
1697     // If we come here, then rowsRemoved() was emitted - all good.
1698     QCOMPARE(spyRowsRemoved.count(), 1);
1699 
1700     QModelIndex deletedDirIndex = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir/subsubdir"));
1701     QVERIFY(!deletedDirIndex.isValid());
1702     QModelIndex dirIndex = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir"));
1703     QVERIFY(dirIndex.isValid());
1704 
1705     // Not emitted because DeleteJobPrivate::currentSourceStated calls stopDirScan
1706     // We don't need it anyway, we have the DBus signal
1707     // QCOMPARE(spyDirWatchDeleted.count(), 1);
1708 }
1709 
1710 void KDirModelTest::testDeleteCurrentDirectory()
1711 {
1712     const int oldTopLevelRowCount = m_dirModel->rowCount();
1713     const QString path = m_tempDir->path() + '/';
1714     const QUrl url = QUrl::fromLocalFile(path);
1715 
1716     QSignalSpy spyRowsRemoved(m_dirModel, &QAbstractItemModel::rowsRemoved);
1717 
1718     KIO::DeleteJob *job = KIO::del(url, KIO::HideProgressInfo);
1719     QVERIFY(job->exec());
1720 
1721     // Wait for the DBUS signal from KDirNotify, it's the one the triggers rowsRemoved
1722     if (spyRowsRemoved.isEmpty()) {
1723         QVERIFY(spyRowsRemoved.wait());
1724     }
1725 
1726     // If we come here, then rowsRemoved() was emitted - all good.
1727     QTRY_COMPARE(m_dirModel->rowCount(), 0); // empty
1728 
1729     // We can get rowsRemoved for subdirs first, since kdirwatch notices that.
1730     QVERIFY(spyRowsRemoved.count() >= 1);
1731 
1732     // Look for the signal(s) that had QModelIndex() as parent.
1733     int i;
1734     int numDeleted = 0;
1735     for (i = 0; i < spyRowsRemoved.count(); ++i) {
1736         const int from = spyRowsRemoved[i][1].toInt();
1737         const int to = spyRowsRemoved[i][2].toInt();
1738         qDebug() << spyRowsRemoved[i][0].value<QModelIndex>() << from << to;
1739         if (!spyRowsRemoved[i][0].value<QModelIndex>().isValid()) {
1740             numDeleted += (to - from) + 1;
1741         }
1742     }
1743 
1744     QCOMPARE(numDeleted, oldTopLevelRowCount);
1745 
1746     QModelIndex fileIndex = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "toplevelfile_1"));
1747     QVERIFY(!fileIndex.isValid());
1748 }
1749 
1750 void KDirModelTest::testQUrlHash()
1751 {
1752     const int count = 3000;
1753     // Prepare an array of QUrls so that url constructing isn't part of the timing
1754     QList<QUrl> urls;
1755     urls.resize(count);
1756     for (int i = 0; i < count; ++i) {
1757         urls[i] = QUrl("http://www.kde.org/path/" + QString::number(i));
1758     }
1759     QHash<QUrl, int> qurlHash;
1760     QHash<QUrl, int> kurlHash;
1761     QElapsedTimer dt;
1762     dt.start();
1763     for (int i = 0; i < count; ++i) {
1764         qurlHash.insert(urls[i], i);
1765     }
1766     // qDebug() << "inserting" << count << "urls into QHash using old qHash:" << dt.elapsed() << "msecs";
1767     dt.start();
1768     for (int i = 0; i < count; ++i) {
1769         kurlHash.insert(urls[i], i);
1770     }
1771     // qDebug() << "inserting" << count << "urls into QHash using new qHash:" << dt.elapsed() << "msecs";
1772     // Nice results: for count=30000 I got 4515 (before) and 103 (after)
1773 
1774     dt.start();
1775     for (int i = 0; i < count; ++i) {
1776         QCOMPARE(qurlHash.value(urls[i]), i);
1777     }
1778     // qDebug() << "looking up" << count << "urls into QHash using old qHash:" << dt.elapsed() << "msecs";
1779     dt.start();
1780     for (int i = 0; i < count; ++i) {
1781         QCOMPARE(kurlHash.value(urls[i]), i);
1782     }
1783     // qDebug() << "looking up" << count << "urls into QHash using new qHash:" << dt.elapsed() << "msecs";
1784     // Nice results: for count=30000 I got 4296 (before) and 63 (after)
1785 }
1786 
1787 #include "moc_kdirmodeltest.cpp"