1 /*
2     This file is part of the KDE project
3     SPDX-FileCopyrightText: 2006-2007 David Faure <faure@kde.org>
4 
5     SPDX-License-Identifier: LGPL-2.0-or-later
6 */
7 
8 #include "kdirmodeltest.h"
9 #include "jobuidelegatefactory.h"
10 #include <KDirWatch>
11 #include <kdirlister.h>
12 #include <kdirnotify.h>
13 #include <kio/chmodjob.h>
14 #include <kio/copyjob.h>
15 #include <kio/deletejob.h>
16 #include <kio/job.h>
17 #include <kprotocolinfo.h>
18 
19 #include <QDebug>
20 #include <QMimeData>
21 #include <QSignalSpy>
22 #include <QUrl>
23 
24 #ifdef Q_OS_UNIX
25 #include <utime.h>
26 #endif
27 #include "kiotesthelper.h"
28 #include "mockcoredelegateextensions.h"
29 
QTEST_MAIN(KDirModelTest)30 QTEST_MAIN(KDirModelTest)
31 
32 #ifndef USE_QTESTEVENTLOOP
33 #define exitLoop quit
34 #endif
35 
36 static QString specialChars()
37 {
38 #ifndef Q_OS_WIN
39     return QStringLiteral(" special chars%:.pdf");
40 #else
41     return QStringLiteral(" special chars%.pdf");
42 #endif
43 }
44 
Q_DECLARE_METATYPE(KFileItemList)45 Q_DECLARE_METATYPE(KFileItemList)
46 
47 void KDirModelTest::initTestCase()
48 {
49     qputenv("LC_ALL", "en_US.UTF-8");
50     // To avoid a runtime dependency on klauncher
51     qputenv("KDE_FORK_SLAVES", "yes");
52 
53     QStandardPaths::setTestModeEnabled(true);
54 
55     qRegisterMetaType<KFileItemList>("KFileItemList");
56 
57     m_dirModelForExpand = nullptr;
58     m_dirModel = nullptr;
59     s_referenceTimeStamp = QDateTime::currentDateTime().addSecs(-30); // 30 seconds ago
60     m_topLevelFileNames << QStringLiteral("toplevelfile_1") << QStringLiteral("toplevelfile_2") << QStringLiteral("toplevelfile_3") << specialChars();
61     recreateTestData();
62 
63     fillModel(false);
64 }
65 
recreateTestData()66 void KDirModelTest::recreateTestData()
67 {
68     if (m_tempDir) {
69         qDebug() << "Deleting old tempdir" << m_tempDir->path();
70         m_tempDir.reset();
71         qApp->processEvents(); // process inotify events so they don't pollute us later on
72     }
73 
74     m_tempDir = std::make_unique<QTemporaryDir>(homeTmpDir());
75     qDebug() << "new tmp dir:" << m_tempDir->path();
76     // Create test data:
77     /*
78      * PATH/toplevelfile_1
79      * PATH/toplevelfile_2
80      * PATH/toplevelfile_3
81      * PATH/special chars%:.pdf
82      * PATH/.hiddenfile
83      * PATH/.hiddenfile2
84      * PATH/subdir
85      * PATH/subdir/testfile
86      * PATH/subdir/testsymlink
87      * PATH/subdir/subsubdir
88      * PATH/subdir/subsubdir/testfile
89      * PATH/subdir/hasChildren
90      * PATH/subdir/hasChildren/emptyDir
91      * PATH/subdir/hasChildren/hiddenfileDir
92      * PATH/subdir/hasChildren/hiddenfileDir/.hidden
93      * PATH/subdir/hasChildren/hiddenDirDir
94      * PATH/subdir/hasChildren/hiddenDirDir/.hidden
95      * PATH/subdir/hasChildren/symlinkDir
96      * PATH/subdir/hasChildren/symlinkDir/link
97      * PATH/subdir/hasChildren/pipeDir
98      * PATH/subdir/hasChildren/pipeDir/pipe
99      */
100     const QString path = m_tempDir->path() + '/';
101     for (const QString &f : std::as_const(m_topLevelFileNames)) {
102         createTestFile(path + f);
103     }
104     createTestFile(path + ".hiddenfile");
105     createTestFile(path + ".hiddenfile2");
106     createTestDirectory(path + "subdir");
107     createTestDirectory(path + "subdir/subsubdir", NoSymlink);
108     createTestDirectory(path + "subdir/hasChildren", Empty);
109     createTestDirectory(path + "subdir/hasChildren/emptyDir", Empty);
110     createTestDirectory(path + "subdir/hasChildren/hiddenfileDir", Empty);
111     createTestFile(path + "subdir/hasChildren/hiddenfileDir/.hidden");
112     createTestDirectory(path + "subdir/hasChildren/hiddenDirDir", Empty);
113     createTestDirectory(path + "subdir/hasChildren/hiddenDirDir/.hidden", Empty);
114     createTestDirectory(path + "subdir/hasChildren/symlinkDir", Empty);
115     createTestSymlink(path + "subdir/hasChildren/symlinkDir/link", QString(path + "toplevelfile_1").toUtf8());
116     createTestDirectory(path + "subdir/hasChildren/pipeDir", Empty);
117     createTestPipe(path + "subdir/hasChildren/pipeDir/pipe");
118 
119     m_dirIndex = QModelIndex();
120     m_fileIndex = QModelIndex();
121     m_secondFileIndex = QModelIndex();
122 }
123 
cleanupTestCase()124 void KDirModelTest::cleanupTestCase()
125 {
126     m_tempDir.reset();
127 
128     delete m_dirModel;
129     m_dirModel = nullptr;
130 
131     delete m_dirModelForExpand;
132     m_dirModelForExpand = nullptr;
133 }
134 
fillModel(bool reload,bool expectAllIndexes)135 void KDirModelTest::fillModel(bool reload, bool expectAllIndexes)
136 {
137     if (!m_dirModel) {
138         m_dirModel = new KDirModel;
139     }
140     m_dirModel->dirLister()->setAutoErrorHandlingEnabled(false);
141     const QString path = m_tempDir->path() + '/';
142     KDirLister *dirLister = m_dirModel->dirLister();
143     qDebug() << "Calling openUrl";
144     m_dirModel->openUrl(QUrl::fromLocalFile(path), reload ? KDirModel::Reload : KDirModel::NoFlags);
145     connect(dirLister, qOverload<>(&KCoreDirLister::completed), this, &KDirModelTest::slotListingCompleted);
146     qDebug() << "enterLoop, waiting for completed()";
147     enterLoop();
148 
149     if (expectAllIndexes) {
150         collectKnownIndexes();
151     }
152     disconnect(dirLister, qOverload<>(&KCoreDirLister::completed), this, &KDirModelTest::slotListingCompleted);
153 }
154 
155 // Called after test function
cleanup()156 void KDirModelTest::cleanup()
157 {
158     if (m_dirModel) {
159         disconnect(m_dirModel, nullptr, &m_eventLoop, nullptr);
160         disconnect(m_dirModel->dirLister(), nullptr, this, nullptr);
161         m_dirModel->dirLister()->setNameFilter(QString());
162         m_dirModel->dirLister()->setMimeFilter(QStringList());
163         m_dirModel->dirLister()->emitChanges();
164     }
165 }
166 
collectKnownIndexes()167 void KDirModelTest::collectKnownIndexes()
168 {
169     m_dirIndex = QModelIndex();
170     m_fileIndex = QModelIndex();
171     m_secondFileIndex = QModelIndex();
172     // Create the indexes once and for all
173     // The trouble is that the order of listing is undefined, one can get 1/2/3/subdir or subdir/3/2/1 for instance.
174     for (int row = 0; row < m_topLevelFileNames.count() + 1 /*subdir*/; ++row) {
175         QModelIndex idx = m_dirModel->index(row, 0, QModelIndex());
176         QVERIFY(idx.isValid());
177         KFileItem item = m_dirModel->itemForIndex(idx);
178         qDebug() << item.url() << "isDir=" << item.isDir();
179         QString fileName = item.url().fileName();
180         if (item.isDir()) {
181             m_dirIndex = idx;
182         } else if (fileName == QLatin1String("toplevelfile_1")) {
183             m_fileIndex = idx;
184         } else if (fileName == QLatin1String("toplevelfile_2")) {
185             m_secondFileIndex = idx;
186         } else if (fileName.startsWith(QLatin1String(" special"))) {
187             m_specialFileIndex = idx;
188         }
189     }
190     QVERIFY(m_dirIndex.isValid());
191     QVERIFY(m_fileIndex.isValid());
192     QVERIFY(m_secondFileIndex.isValid());
193     QVERIFY(m_specialFileIndex.isValid());
194 
195     // Now list subdir/
196     QVERIFY(m_dirModel->canFetchMore(m_dirIndex));
197     m_dirModel->fetchMore(m_dirIndex);
198     qDebug() << "Listing subdir/";
199     enterLoop();
200 
201     // Index of a file inside a directory (subdir/testfile)
202     QModelIndex subdirIndex;
203     m_fileInDirIndex = QModelIndex();
204     for (int row = 0; row < 4; ++row) {
205         QModelIndex idx = m_dirModel->index(row, 0, m_dirIndex);
206         const KFileItem item = m_dirModel->itemForIndex(idx);
207         if (item.isDir() && item.name() == QLatin1String("subsubdir")) {
208             subdirIndex = idx;
209         } else if (item.name() == QLatin1String("testfile")) {
210             m_fileInDirIndex = idx;
211         }
212     }
213 
214     // List subdir/subsubdir
215     QVERIFY(m_dirModel->canFetchMore(subdirIndex));
216     qDebug() << "Listing subdir/subsubdir";
217     m_dirModel->fetchMore(subdirIndex);
218     enterLoop();
219 
220     // Index of ... well, subdir/subsubdir/testfile
221     m_fileInSubdirIndex = m_dirModel->index(0, 0, subdirIndex);
222 }
223 
enterLoop()224 void KDirModelTest::enterLoop()
225 {
226 #ifdef USE_QTESTEVENTLOOP
227     m_eventLoop.enterLoop(10 /*seconds max*/);
228     QVERIFY(!m_eventLoop.timeout());
229 #else
230     m_eventLoop.exec();
231 #endif
232 }
233 
slotListingCompleted()234 void KDirModelTest::slotListingCompleted()
235 {
236     qDebug();
237 #ifdef USE_QTESTEVENTLOOP
238     m_eventLoop.exitLoop();
239 #else
240     m_eventLoop.quit();
241 #endif
242 }
243 
testRowCount()244 void KDirModelTest::testRowCount()
245 {
246     const int topLevelRowCount = m_dirModel->rowCount();
247     QCOMPARE(topLevelRowCount, m_topLevelFileNames.count() + 1 /*subdir*/);
248     const int subdirRowCount = m_dirModel->rowCount(m_dirIndex);
249     QCOMPARE(subdirRowCount, 4);
250 
251     QVERIFY(m_fileIndex.isValid());
252     const int fileRowCount = m_dirModel->rowCount(m_fileIndex); // #176555
253     QCOMPARE(fileRowCount, 0);
254 }
255 
testIndex()256 void KDirModelTest::testIndex()
257 {
258     QVERIFY(m_dirModel->hasChildren());
259 
260     // Index of the first file
261     QVERIFY(m_fileIndex.isValid());
262     QCOMPARE(m_fileIndex.model(), static_cast<const QAbstractItemModel *>(m_dirModel));
263     // QCOMPARE(m_fileIndex.row(), 0);
264     QCOMPARE(m_fileIndex.column(), 0);
265     QVERIFY(!m_fileIndex.parent().isValid());
266     QVERIFY(!m_dirModel->hasChildren(m_fileIndex));
267 
268     // Index of a directory
269     QVERIFY(m_dirIndex.isValid());
270     QCOMPARE(m_dirIndex.model(), static_cast<const QAbstractItemModel *>(m_dirModel));
271     // QCOMPARE(m_dirIndex.row(), 3); // ordering isn't guaranteed
272     QCOMPARE(m_dirIndex.column(), 0);
273     QVERIFY(!m_dirIndex.parent().isValid());
274     QVERIFY(m_dirModel->hasChildren(m_dirIndex));
275 
276     // Index of a file inside a directory (subdir/testfile)
277     QVERIFY(m_fileInDirIndex.isValid());
278     QCOMPARE(m_fileInDirIndex.model(), static_cast<const QAbstractItemModel *>(m_dirModel));
279     // QCOMPARE(m_fileInDirIndex.row(), 0); // ordering isn't guaranteed
280     QCOMPARE(m_fileInDirIndex.column(), 0);
281     QVERIFY(m_fileInDirIndex.parent() == m_dirIndex);
282     QVERIFY(!m_dirModel->hasChildren(m_fileInDirIndex));
283 
284     // Index of subdir/subsubdir/testfile
285     QVERIFY(m_fileInSubdirIndex.isValid());
286     QCOMPARE(m_fileInSubdirIndex.model(), static_cast<const QAbstractItemModel *>(m_dirModel));
287     QCOMPARE(m_fileInSubdirIndex.row(), 0); // we can check it because it's the only file there
288     QCOMPARE(m_fileInSubdirIndex.column(), 0);
289     QVERIFY(m_fileInSubdirIndex.parent().parent() == m_dirIndex);
290     QVERIFY(!m_dirModel->hasChildren(m_fileInSubdirIndex));
291 
292     // Test sibling() by going from subdir/testfile to subdir/subsubdir
293     const QModelIndex subsubdirIndex = m_fileInSubdirIndex.parent();
294     QVERIFY(subsubdirIndex.isValid());
295     QModelIndex sibling1 = m_dirModel->sibling(subsubdirIndex.row(), 0, m_fileInDirIndex);
296     QVERIFY(sibling1.isValid());
297     QVERIFY(sibling1 == subsubdirIndex);
298     QVERIFY(m_dirModel->hasChildren(subsubdirIndex));
299 
300     // Invalid sibling call
301     QVERIFY(!m_dirModel->sibling(2, 0, m_fileInSubdirIndex).isValid());
302 
303     // Test index() with a valid parent (dir).
304     QModelIndex index2 = m_dirModel->index(m_fileInSubdirIndex.row(), m_fileInSubdirIndex.column(), subsubdirIndex);
305     QVERIFY(index2.isValid());
306     QVERIFY(index2 == m_fileInSubdirIndex);
307 
308     // Test index() with a non-parent (file).
309     QModelIndex index3 = m_dirModel->index(m_fileInSubdirIndex.row(), m_fileInSubdirIndex.column(), m_fileIndex);
310     QVERIFY(!index3.isValid());
311 }
312 
testNames()313 void KDirModelTest::testNames()
314 {
315     QString fileName = m_dirModel->data(m_fileIndex, Qt::DisplayRole).toString();
316     QCOMPARE(fileName, QString("toplevelfile_1"));
317 
318     QString specialFileName = m_dirModel->data(m_specialFileIndex, Qt::DisplayRole).toString();
319     QCOMPARE(specialFileName, specialChars());
320 
321     QString dirName = m_dirModel->data(m_dirIndex, Qt::DisplayRole).toString();
322     QCOMPARE(dirName, QString("subdir"));
323 
324     QString fileInDirName = m_dirModel->data(m_fileInDirIndex, Qt::DisplayRole).toString();
325     QCOMPARE(fileInDirName, QString("testfile"));
326 
327     QString fileInSubdirName = m_dirModel->data(m_fileInSubdirIndex, Qt::DisplayRole).toString();
328     QCOMPARE(fileInSubdirName, QString("testfile"));
329 }
330 
testItemForIndex()331 void KDirModelTest::testItemForIndex()
332 {
333     // root item
334     KFileItem rootItem = m_dirModel->itemForIndex(QModelIndex());
335     QVERIFY(!rootItem.isNull());
336     QCOMPARE(rootItem.name(), QString("."));
337 
338     KFileItem fileItem = m_dirModel->itemForIndex(m_fileIndex);
339     QVERIFY(!fileItem.isNull());
340     QCOMPARE(fileItem.name(), QString("toplevelfile_1"));
341     QVERIFY(!fileItem.isDir());
342     QCOMPARE(fileItem.url().toLocalFile(), QString(m_tempDir->path() + "/toplevelfile_1"));
343 
344     KFileItem dirItem = m_dirModel->itemForIndex(m_dirIndex);
345     QVERIFY(!dirItem.isNull());
346     QCOMPARE(dirItem.name(), QString("subdir"));
347     QVERIFY(dirItem.isDir());
348     QCOMPARE(dirItem.url().toLocalFile(), QString(m_tempDir->path() + "/subdir"));
349 
350     KFileItem fileInDirItem = m_dirModel->itemForIndex(m_fileInDirIndex);
351     QVERIFY(!fileInDirItem.isNull());
352     QCOMPARE(fileInDirItem.name(), QString("testfile"));
353     QVERIFY(!fileInDirItem.isDir());
354     QCOMPARE(fileInDirItem.url().toLocalFile(), QString(m_tempDir->path() + "/subdir/testfile"));
355 
356     KFileItem fileInSubdirItem = m_dirModel->itemForIndex(m_fileInSubdirIndex);
357     QVERIFY(!fileInSubdirItem.isNull());
358     QCOMPARE(fileInSubdirItem.name(), QString("testfile"));
359     QVERIFY(!fileInSubdirItem.isDir());
360     QCOMPARE(fileInSubdirItem.url().toLocalFile(), QString(m_tempDir->path() + "/subdir/subsubdir/testfile"));
361 }
362 
testIndexForItem()363 void KDirModelTest::testIndexForItem()
364 {
365     KFileItem rootItem = m_dirModel->itemForIndex(QModelIndex());
366     QModelIndex rootIndex = m_dirModel->indexForItem(rootItem);
367     QVERIFY(!rootIndex.isValid());
368 
369     KFileItem fileItem = m_dirModel->itemForIndex(m_fileIndex);
370     QModelIndex fileIndex = m_dirModel->indexForItem(fileItem);
371     QCOMPARE(fileIndex, m_fileIndex);
372 
373     KFileItem dirItem = m_dirModel->itemForIndex(m_dirIndex);
374     QModelIndex dirIndex = m_dirModel->indexForItem(dirItem);
375     QCOMPARE(dirIndex, m_dirIndex);
376 
377     KFileItem fileInDirItem = m_dirModel->itemForIndex(m_fileInDirIndex);
378     QModelIndex fileInDirIndex = m_dirModel->indexForItem(fileInDirItem);
379     QCOMPARE(fileInDirIndex, m_fileInDirIndex);
380 
381     KFileItem fileInSubdirItem = m_dirModel->itemForIndex(m_fileInSubdirIndex);
382     QModelIndex fileInSubdirIndex = m_dirModel->indexForItem(fileInSubdirItem);
383     QCOMPARE(fileInSubdirIndex, m_fileInSubdirIndex);
384 }
385 
testData()386 void KDirModelTest::testData()
387 {
388     // First file
389     QModelIndex idx1col0 = m_dirModel->index(m_fileIndex.row(), 0, QModelIndex());
390     QCOMPARE(idx1col0.data().toString(), QString("toplevelfile_1"));
391     QModelIndex idx1col1 = m_dirModel->index(m_fileIndex.row(), 1, QModelIndex());
392     QString size1 = m_dirModel->data(idx1col1, Qt::DisplayRole).toString();
393     QCOMPARE(size1, QString("11 B"));
394 
395     KFileItem item = m_dirModel->data(m_fileIndex, KDirModel::FileItemRole).value<KFileItem>();
396     KFileItem fileItem = m_dirModel->itemForIndex(m_fileIndex);
397     QCOMPARE(item, fileItem);
398 
399     QCOMPARE(m_dirModel->data(m_fileIndex, KDirModel::ChildCountRole).toInt(), (int)KDirModel::ChildCountUnknown);
400 
401     // Second file
402     QModelIndex idx2col0 = m_dirModel->index(m_secondFileIndex.row(), 0, QModelIndex());
403     QString display2 = m_dirModel->data(idx2col0, Qt::DisplayRole).toString();
404     QCOMPARE(display2, QString("toplevelfile_2"));
405 
406     // Subdir: check child count
407     QCOMPARE(m_dirModel->data(m_dirIndex, KDirModel::ChildCountRole).toInt(), 4);
408 
409     // Subsubdir: check child count
410     QCOMPARE(m_dirModel->data(m_fileInSubdirIndex.parent(), KDirModel::ChildCountRole).toInt(), 1);
411 }
412 
testReload()413 void KDirModelTest::testReload()
414 {
415     fillModel(true);
416     testItemForIndex();
417 }
418 
419 // We want more info than just "the values differ", if they do.
420 /* clang-format off */
421 #define COMPARE_INDEXES(a, b) \
422     QCOMPARE(a.row(), b.row()); \
423     QCOMPARE(a.column(), b.column()); \
424     QCOMPARE(a.model(), b.model()); \
425     QCOMPARE(a.parent().isValid(), b.parent().isValid()); \
426     QCOMPARE(a, b);
427 /* clang-format on */
428 
testModifyFile()429 void KDirModelTest::testModifyFile()
430 {
431     const QString file = m_tempDir->path() + "/toplevelfile_2";
432 
433 #if 1
434     QSignalSpy spyDataChanged(m_dirModel, &QAbstractItemModel::dataChanged);
435 #else
436     ModelSpy modelSpy(m_dirModel);
437 #endif
438     connect(m_dirModel, &QAbstractItemModel::dataChanged, &m_eventLoop, &QTestEventLoop::exitLoop);
439 
440     // "Touch" the file
441     setTimeStamp(file, s_referenceTimeStamp.addSecs(20));
442 
443     // In stat mode, kdirwatch doesn't notice file changes; we need to trigger it
444     // by creating a file.
445     // createTestFile(m_tempDir->path() + "/toplevelfile_5");
446     KDirWatch::self()->setDirty(m_tempDir->path());
447 
448     // Wait for KDirWatch to notify the change (especially when using Stat)
449     enterLoop();
450 
451     // If we come here, then dataChanged() was emitted - all good.
452     const QVariantList dataChanged = spyDataChanged[0];
453     QModelIndex receivedIndex = dataChanged[0].value<QModelIndex>();
454     COMPARE_INDEXES(receivedIndex, m_secondFileIndex);
455     receivedIndex = dataChanged[1].value<QModelIndex>();
456     QCOMPARE(receivedIndex.row(), m_secondFileIndex.row()); // only compare row; column is count-1
457 
458     disconnect(m_dirModel, &QAbstractItemModel::dataChanged, &m_eventLoop, &QTestEventLoop::exitLoop);
459 }
460 
testRenameFile()461 void KDirModelTest::testRenameFile()
462 {
463     const QUrl url = QUrl::fromLocalFile(m_tempDir->path() + "/toplevelfile_2");
464     const QUrl newUrl = QUrl::fromLocalFile(m_tempDir->path() + "/toplevelfile_2_renamed");
465 
466     QSignalSpy spyDataChanged(m_dirModel, &QAbstractItemModel::dataChanged);
467     connect(m_dirModel, &QAbstractItemModel::dataChanged, &m_eventLoop, &QTestEventLoop::exitLoop);
468 
469     KIO::SimpleJob *job = KIO::rename(url, newUrl, KIO::HideProgressInfo);
470     QVERIFY(job->exec());
471 
472     // Wait for the DBUS signal from KDirNotify, it's the one the triggers dataChanged
473     enterLoop();
474 
475     // If we come here, then dataChanged() was emitted - all good.
476     QCOMPARE(spyDataChanged.count(), 1);
477     COMPARE_INDEXES(spyDataChanged[0][0].value<QModelIndex>(), m_secondFileIndex);
478     QModelIndex receivedIndex = spyDataChanged[0][1].value<QModelIndex>();
479     QCOMPARE(receivedIndex.row(), m_secondFileIndex.row()); // only compare row; column is count-1
480 
481     // check renaming happened
482     QCOMPARE(m_dirModel->itemForIndex(m_secondFileIndex).url().toString(), newUrl.toString());
483 
484     // check that KDirLister::cachedItemForUrl won't give a bad name if copying that item (#195385)
485     KFileItem cachedItem = KDirLister::cachedItemForUrl(newUrl);
486     QVERIFY(!cachedItem.isNull());
487     QCOMPARE(cachedItem.name(), QString("toplevelfile_2_renamed"));
488     QCOMPARE(cachedItem.entry().stringValue(KIO::UDSEntry::UDS_NAME), QString("toplevelfile_2_renamed"));
489 
490     // Put things back to normal
491     job = KIO::rename(newUrl, url, KIO::HideProgressInfo);
492     QVERIFY(job->exec());
493     // Wait for the DBUS signal from KDirNotify, it's the one the triggers dataChanged
494     enterLoop();
495     QCOMPARE(m_dirModel->itemForIndex(m_secondFileIndex).url().toString(), url.toString());
496 
497     disconnect(m_dirModel, &QAbstractItemModel::dataChanged, &m_eventLoop, &QTestEventLoop::exitLoop);
498 }
499 
testMoveDirectory()500 void KDirModelTest::testMoveDirectory()
501 {
502     testMoveDirectory(QStringLiteral("subdir"));
503 }
504 
testMoveDirectory(const QString & dir)505 void KDirModelTest::testMoveDirectory(const QString &dir /*just a dir name, no slash*/)
506 {
507     const QString path = m_tempDir->path() + '/';
508     const QString srcdir = path + dir;
509     QVERIFY(QDir(srcdir).exists());
510     QTemporaryDir destDir(homeTmpDir());
511     const QString dest = destDir.path() + '/';
512     QVERIFY(QDir(dest).exists());
513 
514     connect(m_dirModel, &QAbstractItemModel::rowsRemoved, &m_eventLoop, &QTestEventLoop::exitLoop);
515 
516     // Move
517     qDebug() << "Moving" << srcdir << "to" << dest;
518     KIO::CopyJob *job = KIO::move(QUrl::fromLocalFile(srcdir), QUrl::fromLocalFile(dest), KIO::HideProgressInfo);
519     job->setUiDelegate(nullptr);
520     job->setUiDelegateExtension(nullptr);
521     QVERIFY(job->exec());
522 
523     // wait for kdirnotify
524     enterLoop();
525 
526     disconnect(m_dirModel, &QAbstractItemModel::rowsRemoved, &m_eventLoop, &QTestEventLoop::exitLoop);
527 
528     QVERIFY(!m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir")).isValid());
529     QVERIFY(!m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir_renamed")).isValid());
530 
531     connect(m_dirModel, &QAbstractItemModel::rowsInserted, &m_eventLoop, &QTestEventLoop::exitLoop);
532 
533     // Move back
534     qDebug() << "Moving" << dest + dir << "back to" << srcdir;
535     job = KIO::move(QUrl::fromLocalFile(dest + dir), QUrl::fromLocalFile(srcdir), KIO::HideProgressInfo);
536     job->setUiDelegate(nullptr);
537     job->setUiDelegateExtension(nullptr);
538     QVERIFY(job->exec());
539 
540     enterLoop();
541 
542     QVERIFY(QDir(srcdir).exists());
543     disconnect(m_dirModel, &QAbstractItemModel::rowsInserted, &m_eventLoop, &QTestEventLoop::exitLoop);
544 
545     // m_dirIndex is invalid after the above...
546     fillModel(true);
547 }
548 
testRenameDirectory()549 void KDirModelTest::testRenameDirectory() // #172945, #174703, (and #180156)
550 {
551     const QString path = m_tempDir->path() + '/';
552     const QUrl url = QUrl::fromLocalFile(path + "subdir");
553     const QUrl newUrl = QUrl::fromLocalFile(path + "subdir_renamed");
554 
555     // For #180156 we need a second kdirmodel, viewing the subdir being renamed.
556     // I'm abusing m_dirModelForExpand for that purpose.
557     delete m_dirModelForExpand;
558     m_dirModelForExpand = new KDirModel;
559     KDirLister *dirListerForExpand = m_dirModelForExpand->dirLister();
560     connect(dirListerForExpand, qOverload<>(&KDirLister::completed), this, &KDirModelTest::slotListingCompleted);
561     dirListerForExpand->openUrl(url); // async
562     enterLoop();
563 
564     // Now do the renaming
565     QSignalSpy spyDataChanged(m_dirModel, &QAbstractItemModel::dataChanged);
566     connect(m_dirModel, &QAbstractItemModel::dataChanged, &m_eventLoop, &QTestEventLoop::exitLoop);
567     KIO::SimpleJob *job = KIO::rename(url, newUrl, KIO::HideProgressInfo);
568     QVERIFY(job->exec());
569 
570     // Wait for the DBUS signal from KDirNotify, it's the one the triggers dataChanged
571     enterLoop();
572 
573     // If we come here, then dataChanged() was emitted - all good.
574     // QCOMPARE(spyDataChanged.count(), 1); // it was in fact emitted 5 times...
575     // COMPARE_INDEXES(spyDataChanged[0][0].value<QModelIndex>(), m_dirIndex);
576     // QModelIndex receivedIndex = spyDataChanged[0][1].value<QModelIndex>();
577     // QCOMPARE(receivedIndex.row(), m_dirIndex.row()); // only compare row; column is count-1
578 
579     // check renaming happened
580     QCOMPARE(m_dirModel->itemForIndex(m_dirIndex).url().toString(), newUrl.toString());
581     qDebug() << newUrl << "indexForUrl=" << m_dirModel->indexForUrl(newUrl) << "m_dirIndex=" << m_dirIndex;
582     QCOMPARE(m_dirModel->indexForUrl(newUrl), m_dirIndex);
583     QVERIFY(m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir_renamed")).isValid());
584     QVERIFY(m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir_renamed/testfile")).isValid());
585     QVERIFY(m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir_renamed/subsubdir")).isValid());
586     QVERIFY(m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir_renamed/subsubdir/testfile")).isValid());
587 
588     // Check the other kdirmodel got redirected
589     QCOMPARE(dirListerForExpand->url().toLocalFile(), QString(path + "subdir_renamed"));
590 
591     qDebug() << "calling testMoveDirectory(subdir_renamed)";
592 
593     // Test moving the renamed directory; if something inside KDirModel
594     // wasn't properly updated by the renaming, this would detect it and crash (#180673)
595     testMoveDirectory(QStringLiteral("subdir_renamed"));
596 
597     // Put things back to normal
598     job = KIO::rename(newUrl, url, KIO::HideProgressInfo);
599     QVERIFY(job->exec());
600     // Wait for the DBUS signal from KDirNotify, it's the one the triggers dataChanged
601     enterLoop();
602     QCOMPARE(m_dirModel->itemForIndex(m_dirIndex).url().toString(), url.toString());
603 
604     disconnect(m_dirModel, &QAbstractItemModel::dataChanged, &m_eventLoop, &QTestEventLoop::exitLoop);
605 
606     QCOMPARE(m_dirModel->itemForIndex(m_dirIndex).url().toString(), url.toString());
607     QCOMPARE(m_dirModel->indexForUrl(url), m_dirIndex);
608     QVERIFY(m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir")).isValid());
609     QVERIFY(m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir/testfile")).isValid());
610     QVERIFY(m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir/subsubdir")).isValid());
611     QVERIFY(m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir/subsubdir/testfile")).isValid());
612     QVERIFY(!m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir_renamed")).isValid());
613     QVERIFY(!m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir_renamed/testfile")).isValid());
614     QVERIFY(!m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir_renamed/subsubdir")).isValid());
615     QVERIFY(!m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir_renamed/subsubdir/testfile")).isValid());
616 
617     // TODO INVESTIGATE
618     // QCOMPARE(dirListerForExpand->url().toLocalFile(), path+"subdir");
619 
620     delete m_dirModelForExpand;
621     m_dirModelForExpand = nullptr;
622 }
623 
testRenameDirectoryInCache()624 void KDirModelTest::testRenameDirectoryInCache() // #188807
625 {
626     // Ensure the stuff is in cache.
627     fillModel(true);
628     const QString path = m_tempDir->path() + '/';
629     QVERIFY(!m_dirModel->dirLister()->findByUrl(QUrl::fromLocalFile(path)).isNull());
630 
631     // No more dirmodel nor dirlister.
632     delete m_dirModel;
633     m_dirModel = nullptr;
634 
635     // Now let's rename a directory that is in KCoreDirListerCache
636     const QUrl url = QUrl::fromLocalFile(path);
637     QUrl newUrl = url.adjusted(QUrl::StripTrailingSlash);
638     newUrl.setPath(newUrl.path() + "_renamed");
639     qDebug() << newUrl;
640     KIO::SimpleJob *job = KIO::rename(url, newUrl, KIO::HideProgressInfo);
641     QVERIFY(job->exec());
642 
643     // Put things back to normal
644     job = KIO::rename(newUrl, url, KIO::HideProgressInfo);
645     QVERIFY(job->exec());
646 
647     // KDirNotify emits FileRenamed for each rename() above, which in turn
648     // re-lists the directory. We need to wait for both signals to be emitted
649     // otherwise the dirlister will not be in the state we expect.
650     QTest::qWait(200);
651 
652     fillModel(true);
653 
654     QVERIFY(m_dirIndex.isValid());
655     KFileItem rootItem = m_dirModel->dirLister()->findByUrl(QUrl::fromLocalFile(path));
656     QVERIFY(!rootItem.isNull());
657 }
658 
testChmodDirectory()659 void KDirModelTest::testChmodDirectory() // #53397
660 {
661     QSignalSpy spyDataChanged(m_dirModel, &QAbstractItemModel::dataChanged);
662     connect(m_dirModel, &QAbstractItemModel::dataChanged, &m_eventLoop, &QTestEventLoop::exitLoop);
663     const QString path = m_tempDir->path() + '/';
664     KFileItem rootItem = m_dirModel->itemForIndex(QModelIndex());
665     const mode_t origPerm = rootItem.permissions();
666     mode_t newPerm = origPerm ^ S_IWGRP;
667     // const QFile::Permissions origPerm = rootItem.filePermissions();
668     // QVERIFY(origPerm & QFile::ReadOwner);
669     // const QFile::Permissions newPerm = origPerm ^ QFile::WriteGroup;
670     QVERIFY(newPerm != origPerm);
671     KIO::Job *job = KIO::chmod({rootItem}, newPerm, S_IWGRP /*TODO: QFile::WriteGroup*/, QString(), QString(), false, KIO::HideProgressInfo);
672     job->setUiDelegate(nullptr);
673     QVERIFY(job->exec());
674     // ChmodJob doesn't talk to KDirNotify, kpropertiesdialog does.
675     // [this allows to group notifications after all the changes one can make in the dialog]
676     org::kde::KDirNotify::emitFilesChanged(QList<QUrl>{QUrl::fromLocalFile(path)});
677     // Wait for the DBUS signal from KDirNotify, it's the one the triggers rowsRemoved
678     enterLoop();
679 
680     // If we come here, then dataChanged() was emitted - all good.
681     QCOMPARE(spyDataChanged.count(), 1);
682     QModelIndex receivedIndex = spyDataChanged[0][0].value<QModelIndex>();
683     qDebug() << "receivedIndex" << receivedIndex;
684     QVERIFY(!receivedIndex.isValid());
685 
686     const KFileItem newRootItem = m_dirModel->itemForIndex(QModelIndex());
687     QVERIFY(!newRootItem.isNull());
688     QCOMPARE(QString::number(newRootItem.permissions(), 16), QString::number(newPerm, 16));
689 
690     disconnect(m_dirModel, &QAbstractItemModel::dataChanged, &m_eventLoop, &QTestEventLoop::exitLoop);
691 }
692 
693 enum {
694     NoFlag = 0,
695     NewDir = 1, // whether to re-create a new QTemporaryDir completely, to avoid cached fileitems
696     ListFinalDir = 2, // whether to list the target dir at the same time, like k3b, for #193364
697     Recreate = 4,
698     CacheSubdir = 8, // put subdir in the cache before expandToUrl
699     // flags, next item is 16!
700 };
701 
testExpandToUrl_data()702 void KDirModelTest::testExpandToUrl_data()
703 {
704     QTest::addColumn<int>("flags"); // see enum above
705     QTest::addColumn<QString>("expandToPath"); // relative path
706     QTest::addColumn<QStringList>("expectedExpandSignals");
707 
708     QTest::newRow("the root, nothing to do") << int(NoFlag) << QString() << QStringList();
709     QTest::newRow(".") << int(NoFlag) << "." << (QStringList());
710     QTest::newRow("subdir") << int(NoFlag) << "subdir" << QStringList{QStringLiteral("subdir")};
711     QTest::newRow("subdir/.") << int(NoFlag) << "subdir/." << QStringList{QStringLiteral("subdir")};
712 
713     const QString subsubdir = QStringLiteral("subdir/subsubdir");
714     // Must list root, emit expand for subdir, list subdir, emit expand for subsubdir.
715     QTest::newRow("subdir/subsubdir") << int(NoFlag) << subsubdir << QStringList{QStringLiteral("subdir"), subsubdir};
716 
717     // Must list root, emit expand for subdir, list subdir, emit expand for subsubdir, list subsubdir.
718     const QString subsubdirfile = subsubdir + "/testfile";
719     QTest::newRow("subdir/subsubdir/testfile sync") << int(NoFlag) << subsubdirfile << QStringList{QStringLiteral("subdir"), subsubdir, subsubdirfile};
720 
721 #ifndef Q_OS_WIN
722     // Expand a symlink to a directory (#219547)
723     const QString dirlink = m_tempDir->path() + "/dirlink";
724     createTestSymlink(dirlink, "subdir"); // dirlink -> subdir
725     QVERIFY(QFileInfo(dirlink).isSymLink());
726     // If this test fails, your first move should be to enable all debug output and see if KDirWatch says inotify failed
727     QTest::newRow("dirlink") << int(NoFlag) << "dirlink/subsubdir" << QStringList{QStringLiteral("dirlink"), QStringLiteral("dirlink/subsubdir")};
728 #endif
729 
730     // Do a cold-cache test too, but nowadays it doesn't change anything anymore,
731     // apart from testing different code paths inside KDirLister.
732     QTest::newRow("subdir/subsubdir/testfile with reload") << int(NewDir) << subsubdirfile << QStringList{QStringLiteral("subdir"), subsubdir, subsubdirfile};
733 
734     QTest::newRow("hold dest dir") // #193364
735         << int(NewDir | ListFinalDir) << subsubdirfile << QStringList{QStringLiteral("subdir"), subsubdir, subsubdirfile};
736 
737     // Put subdir in cache too (#175035)
738     QTest::newRow("hold subdir and dest dir") << int(NewDir | CacheSubdir | ListFinalDir | Recreate) << subsubdirfile
739                                               << QStringList{QStringLiteral("subdir"), subsubdir, subsubdirfile};
740 
741     // Make sure the last test has the Recreate option set, for the subsequent test methods.
742 }
743 
testExpandToUrl()744 void KDirModelTest::testExpandToUrl()
745 {
746     QFETCH(int, flags);
747     QFETCH(QString, expandToPath); // relative
748     QFETCH(QStringList, expectedExpandSignals);
749 
750     if (flags & NewDir) {
751         recreateTestData();
752         // WARNING! m_dirIndex, m_fileIndex, m_secondFileIndex etc. are not valid anymore after this point!
753     }
754 
755     const QString path = m_tempDir->path() + '/';
756     if (flags & CacheSubdir) {
757         // This way, the listDir for subdir will find items in cache, and will schedule a CachedItemsJob
758         m_dirModel->dirLister()->openUrl(QUrl::fromLocalFile(path + "subdir"));
759         QSignalSpy completedSpy(m_dirModel->dirLister(), qOverload<>(&KCoreDirLister::completed));
760         QVERIFY(completedSpy.wait(2000));
761     }
762     if (flags & ListFinalDir) {
763         // This way, the last listDir will find items in cache, and will schedule a CachedItemsJob
764         m_dirModel->dirLister()->openUrl(QUrl::fromLocalFile(path + "subdir/subsubdir"));
765         QSignalSpy completedSpy(m_dirModel->dirLister(), qOverload<>(&KCoreDirLister::completed));
766         QVERIFY(completedSpy.wait(2000));
767     }
768 
769     if (!m_dirModelForExpand || (flags & NewDir)) {
770         delete m_dirModelForExpand;
771         m_dirModelForExpand = new KDirModel;
772         connect(m_dirModelForExpand, &KDirModel::expand, this, &KDirModelTest::slotExpand);
773         connect(m_dirModelForExpand, &QAbstractItemModel::rowsInserted, this, &KDirModelTest::slotRowsInserted);
774         KDirLister *dirListerForExpand = m_dirModelForExpand->dirLister();
775         dirListerForExpand->openUrl(QUrl::fromLocalFile(path), KDirLister::NoFlags); // async
776     }
777     m_rowsInsertedEmitted = false;
778     m_expectedExpandSignals = expectedExpandSignals;
779     m_nextExpectedExpandSignals = 0;
780     QSignalSpy spyExpand(m_dirModelForExpand, &KDirModel::expand);
781     m_urlToExpandTo = QUrl::fromLocalFile(path + expandToPath);
782     // If KDirModel doesn't know this URL yet, then we want to see rowsInserted signals
783     // being emitted, so that the slots can get the index to that url then.
784     m_expectRowsInserted = !expandToPath.isEmpty() && !m_dirModelForExpand->indexForUrl(m_urlToExpandTo).isValid();
785     QVERIFY(QFileInfo::exists(m_urlToExpandTo.toLocalFile()));
786     m_dirModelForExpand->expandToUrl(m_urlToExpandTo);
787     if (expectedExpandSignals.isEmpty()) {
788         QTest::qWait(20); // to make sure we process queued connection calls, otherwise spyExpand.count() is always 0 even if there's a bug...
789         QCOMPARE(spyExpand.count(), 0);
790     } else {
791         if (spyExpand.count() < expectedExpandSignals.count()) {
792             enterLoop();
793             QCOMPARE(spyExpand.count(), expectedExpandSignals.count());
794         }
795         if (m_expectRowsInserted) {
796             QVERIFY(m_rowsInsertedEmitted);
797         }
798     }
799 
800     // Now it should exist
801     if (!expandToPath.isEmpty() && expandToPath != QLatin1String(".")) {
802         qDebug() << "Do I know" << m_urlToExpandTo << "?";
803         QVERIFY(m_dirModelForExpand->indexForUrl(m_urlToExpandTo).isValid());
804     }
805 
806     if (flags & ListFinalDir) {
807         testUpdateParentAfterExpand();
808     }
809 
810     if (flags & Recreate) {
811         // Clean up, for the next tests
812         recreateTestData();
813         fillModel(false);
814     }
815 }
816 
slotExpand(const QModelIndex & index)817 void KDirModelTest::slotExpand(const QModelIndex &index)
818 {
819     QVERIFY(index.isValid());
820     const QString path = m_tempDir->path() + '/';
821     KFileItem item = m_dirModelForExpand->itemForIndex(index);
822     QVERIFY(!item.isNull());
823     qDebug() << item.url().toLocalFile();
824     QCOMPARE(item.url().toLocalFile(), QString(path + m_expectedExpandSignals[m_nextExpectedExpandSignals++]));
825 
826     // if rowsInserted wasn't emitted yet, then any proxy model would be unable to do anything with index at this point
827     if (item.url() == m_urlToExpandTo) {
828         QVERIFY(m_dirModelForExpand->indexForUrl(m_urlToExpandTo).isValid());
829         if (m_expectRowsInserted) {
830             QVERIFY(m_rowsInsertedEmitted);
831         }
832     }
833 
834     if (m_nextExpectedExpandSignals == m_expectedExpandSignals.count()) {
835         m_eventLoop.exitLoop(); // done
836     }
837 }
838 
slotRowsInserted(const QModelIndex &,int,int)839 void KDirModelTest::slotRowsInserted(const QModelIndex &, int, int)
840 {
841     m_rowsInsertedEmitted = true;
842 }
843 
844 // This code is called by testExpandToUrl
testUpdateParentAfterExpand()845 void KDirModelTest::testUpdateParentAfterExpand() // #193364
846 {
847     const QString path = m_tempDir->path() + '/';
848     const QString file = path + "subdir/aNewFile";
849     qDebug() << "Creating" << file;
850     QVERIFY(!QFile::exists(file));
851     createTestFile(file);
852     QSignalSpy spyRowsInserted(m_dirModelForExpand, &QAbstractItemModel::rowsInserted);
853     QVERIFY(spyRowsInserted.wait(1000));
854 }
855 
testFilter()856 void KDirModelTest::testFilter()
857 {
858     QVERIFY(m_dirIndex.isValid());
859     const int oldTopLevelRowCount = m_dirModel->rowCount();
860     const int oldSubdirRowCount = m_dirModel->rowCount(m_dirIndex);
861     QSignalSpy spyItemsFilteredByMime(m_dirModel->dirLister(), &KCoreDirLister::itemsFilteredByMime);
862     QSignalSpy spyItemsDeleted(m_dirModel->dirLister(), &KCoreDirLister::itemsDeleted);
863     QSignalSpy spyRowsRemoved(m_dirModel, &QAbstractItemModel::rowsRemoved);
864     m_dirModel->dirLister()->setNameFilter(QStringLiteral("toplevel*"));
865     QCOMPARE(m_dirModel->rowCount(), oldTopLevelRowCount); // no change yet
866     QCOMPARE(m_dirModel->rowCount(m_dirIndex), oldSubdirRowCount); // no change yet
867     m_dirModel->dirLister()->emitChanges();
868 
869     QCOMPARE(m_dirModel->rowCount(), 4); // 3 toplevel* files, one subdir
870     QCOMPARE(m_dirModel->rowCount(m_dirIndex), 2); // the files get filtered out, subsubdir and hasChildren are remaining
871 
872     // In the subdir, we can get rowsRemoved signals like (1,2) or (0,0)+(2,2),
873     // depending on the order of the files in the model.
874     // So QCOMPARE(spyRowsRemoved.count(), 3) is fragile, we rather need
875     // to sum up the removed rows per parent directory.
876     QMap<QString, int> rowsRemovedPerDir;
877     for (int i = 0; i < spyRowsRemoved.count(); ++i) {
878         const QVariantList args = spyRowsRemoved[i];
879         const QModelIndex parentIdx = args[0].value<QModelIndex>();
880         QString dirName;
881         if (parentIdx.isValid()) {
882             const KFileItem item = m_dirModel->itemForIndex(parentIdx);
883             dirName = item.name();
884         } else {
885             dirName = QStringLiteral("root");
886         }
887         rowsRemovedPerDir[dirName] += args[2].toInt() - args[1].toInt() + 1;
888         // qDebug() << parentIdx << args[1].toInt() << args[2].toInt();
889     }
890     QCOMPARE(rowsRemovedPerDir.count(), 3); // once for every dir
891     QCOMPARE(rowsRemovedPerDir.value("root"), 1); // one from toplevel ('special chars')
892     QCOMPARE(rowsRemovedPerDir.value("subdir"), 2); // two from subdir
893     QCOMPARE(rowsRemovedPerDir.value("subsubdir"), 1); // one from subsubdir
894     QCOMPARE(spyItemsDeleted.count(), 3); // once for every dir
895     QCOMPARE(spyItemsDeleted[0][0].value<KFileItemList>().count(), 1); // one from toplevel ('special chars')
896     QCOMPARE(spyItemsDeleted[1][0].value<KFileItemList>().count(), 2); // two from subdir
897     QCOMPARE(spyItemsDeleted[2][0].value<KFileItemList>().count(), 1); // one from subsubdir
898     QCOMPARE(spyItemsFilteredByMime.count(), 0);
899     spyItemsDeleted.clear();
900     spyItemsFilteredByMime.clear();
901 
902     // Reset the filter
903     qDebug() << "reset to no filter";
904     m_dirModel->dirLister()->setNameFilter(QString());
905     m_dirModel->dirLister()->emitChanges();
906 
907     QCOMPARE(m_dirModel->rowCount(), oldTopLevelRowCount);
908     QCOMPARE(m_dirModel->rowCount(m_dirIndex), oldSubdirRowCount);
909     QCOMPARE(spyItemsDeleted.count(), 0);
910     QCOMPARE(spyItemsFilteredByMime.count(), 0);
911 
912     // The order of things changed because of filtering.
913     // Fill again, so that m_fileIndex etc. are correct again.
914     fillModel(true);
915 }
916 
testFilterPatterns()917 void KDirModelTest::testFilterPatterns()
918 {
919     QVERIFY(m_dirIndex.isValid());
920 
921     const int oldTopLevelRowCount = m_dirModel->rowCount();
922     const int oldSubdirRowCount = m_dirModel->rowCount(m_dirIndex);
923 
924     m_dirModel->dirLister()->setNameFilter(QStringLiteral("toplevel"));
925     QCOMPARE(m_dirModel->rowCount(), oldTopLevelRowCount); // no change yet
926     QCOMPARE(m_dirModel->rowCount(m_dirIndex), oldSubdirRowCount); // no change yet
927     m_dirModel->dirLister()->emitChanges();
928 
929     QCOMPARE(m_dirModel->rowCount(), 4); // 3 files, one subdir with "toplevel" in the name
930     QCOMPARE(m_dirModel->rowCount(m_dirIndex), 2); // the files get filtered out, subsubdir and hasChildren are remaining
931 
932     // Reset the filter
933     m_dirModel->dirLister()->setNameFilter(QString());
934     m_dirModel->dirLister()->emitChanges();
935 
936     // Back to original state
937     QCOMPARE(m_dirModel->rowCount(), oldTopLevelRowCount);
938     QCOMPARE(m_dirModel->rowCount(m_dirIndex), oldSubdirRowCount);
939 
940     m_dirModel->dirLister()->setNameFilter(QStringLiteral("toplevel*")); // Matching with a wildcard
941     QCOMPARE(m_dirModel->rowCount(), oldTopLevelRowCount); // no change yet
942     QCOMPARE(m_dirModel->rowCount(m_dirIndex), oldSubdirRowCount); // no change yet
943     m_dirModel->dirLister()->emitChanges();
944 
945     QCOMPARE(m_dirModel->rowCount(), 4); // 3 files, one subdir with "toplevel*" in the name
946     QCOMPARE(m_dirModel->rowCount(m_dirIndex), 2); // the files get filtered out, subsubdir and hasChildren are remaining
947 
948     m_dirModel->dirLister()->setNameFilter(QString());
949     m_dirModel->dirLister()->emitChanges();
950 
951     QCOMPARE(m_dirModel->rowCount(), oldTopLevelRowCount);
952     QCOMPARE(m_dirModel->rowCount(m_dirIndex), oldSubdirRowCount);
953 
954     // The order of things changed because of filtering.
955     // Fill again, so that m_fileIndex etc. are correct again.
956     fillModel(true);
957 }
958 
testMimeFilter()959 void KDirModelTest::testMimeFilter()
960 {
961     QVERIFY(m_dirIndex.isValid());
962     const int oldTopLevelRowCount = m_dirModel->rowCount();
963     const int oldSubdirRowCount = m_dirModel->rowCount(m_dirIndex);
964     QSignalSpy spyItemsFilteredByMime(m_dirModel->dirLister(), &KCoreDirLister::itemsFilteredByMime);
965     QSignalSpy spyItemsDeleted(m_dirModel->dirLister(), &KCoreDirLister::itemsDeleted);
966     QSignalSpy spyRowsRemoved(m_dirModel, &QAbstractItemModel::rowsRemoved);
967     m_dirModel->dirLister()->setMimeFilter(QStringList{QStringLiteral("application/pdf")});
968     QCOMPARE(m_dirModel->rowCount(), oldTopLevelRowCount); // no change yet
969     QCOMPARE(m_dirModel->rowCount(m_dirIndex), oldSubdirRowCount); // no change yet
970     m_dirModel->dirLister()->emitChanges();
971 
972     QCOMPARE(m_dirModel->rowCount(), 1); // 1 pdf files, no subdir anymore
973 
974     QVERIFY(spyRowsRemoved.count() >= 1); // depends on contiguity...
975     QVERIFY(spyItemsDeleted.count() >= 1); // once for every dir
976     // Maybe it would make sense to have those items in itemsFilteredByMime,
977     // but well, for the only existing use of that signal (MIME type filter plugin),
978     // it's not really necessary, the plugin has seen those files before anyway.
979     // The signal is mostly useful for the case of listing a dir with a MIME type filter set.
980     // QCOMPARE(spyItemsFilteredByMime.count(), 1);
981     // QCOMPARE(spyItemsFilteredByMime[0][0].value<KFileItemList>().count(), 4);
982     spyItemsDeleted.clear();
983     spyItemsFilteredByMime.clear();
984 
985     // Reset the filter
986     qDebug() << "reset to no filter";
987     m_dirModel->dirLister()->setMimeFilter(QStringList());
988     m_dirModel->dirLister()->emitChanges();
989 
990     QCOMPARE(m_dirModel->rowCount(), oldTopLevelRowCount);
991     QCOMPARE(spyItemsDeleted.count(), 0);
992     QCOMPARE(spyItemsFilteredByMime.count(), 0);
993 
994     // The order of things changed because of filtering.
995     // Fill again, so that m_fileIndex etc. are correct again.
996     fillModel(true);
997 }
998 
testShowHiddenFiles()999 void KDirModelTest::testShowHiddenFiles() // #174788
1000 {
1001     KDirLister *dirLister = m_dirModel->dirLister();
1002 
1003     QSignalSpy spyRowsRemoved(m_dirModel, &QAbstractItemModel::rowsRemoved);
1004     QSignalSpy spyNewItems(dirLister, &KCoreDirLister::newItems);
1005     QSignalSpy spyRowsInserted(m_dirModel, &QAbstractItemModel::rowsInserted);
1006     dirLister->setShowingDotFiles(true);
1007     dirLister->emitChanges();
1008     const int numberOfDotFiles = 2;
1009     QCOMPARE(spyNewItems.count(), 1);
1010     QCOMPARE(spyNewItems[0][0].value<KFileItemList>().count(), numberOfDotFiles);
1011     QCOMPARE(spyRowsInserted.count(), 1);
1012     QCOMPARE(spyRowsRemoved.count(), 0);
1013     spyNewItems.clear();
1014     spyRowsInserted.clear();
1015 
1016     dirLister->setShowingDotFiles(false);
1017     dirLister->emitChanges();
1018     QCOMPARE(spyNewItems.count(), 0);
1019     QCOMPARE(spyRowsInserted.count(), 0);
1020     QCOMPARE(spyRowsRemoved.count(), 1);
1021 }
1022 
testMultipleSlashes()1023 void KDirModelTest::testMultipleSlashes()
1024 {
1025     const QString path = m_tempDir->path() + '/';
1026 
1027     QModelIndex index = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir//testfile"));
1028     QVERIFY(index.isValid());
1029 
1030     index = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir//subsubdir//"));
1031     QVERIFY(index.isValid());
1032 
1033     index = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir///subsubdir////testfile"));
1034     QVERIFY(index.isValid());
1035 }
1036 
testUrlWithRef()1037 void KDirModelTest::testUrlWithRef() // #171117
1038 {
1039     const QString path = m_tempDir->path() + '/';
1040     KDirLister *dirLister = m_dirModel->dirLister();
1041     QUrl url = QUrl::fromLocalFile(path);
1042     url.setFragment(QStringLiteral("ref"));
1043     QVERIFY(url.url().endsWith(QLatin1String("#ref")));
1044     dirLister->openUrl(url, KDirLister::NoFlags);
1045     connect(dirLister, qOverload<>(&KCoreDirLister::completed), this, &KDirModelTest::slotListingCompleted);
1046     enterLoop();
1047 
1048     QCOMPARE(dirLister->url().toString(), url.toString(QUrl::StripTrailingSlash));
1049     collectKnownIndexes();
1050     disconnect(dirLister, qOverload<>(&KCoreDirLister::completed), this, &KDirModelTest::slotListingCompleted);
1051 }
1052 
1053 // void KDirModelTest::testFontUrlWithHost() // #160057 --> moved to kio_fonts (kfontinst/kio/autotests)
1054 
testRemoteUrlWithHost()1055 void KDirModelTest::testRemoteUrlWithHost() // #178416
1056 {
1057     if (!KProtocolInfo::isKnownProtocol(QStringLiteral("remote"))) {
1058         QSKIP("kio_remote not installed");
1059     }
1060     QUrl url(QStringLiteral("remote://foo"));
1061     KDirLister *dirLister = m_dirModel->dirLister();
1062     dirLister->openUrl(url, KDirLister::NoFlags);
1063     connect(dirLister, qOverload<>(&KCoreDirLister::completed), this, &KDirModelTest::slotListingCompleted);
1064     enterLoop();
1065 
1066     QCOMPARE(dirLister->url().toString(), QString("remote://foo"));
1067 }
1068 
testZipFile()1069 void KDirModelTest::testZipFile() // # 171721
1070 {
1071     const QString path = QFileInfo(QFINDTESTDATA("wronglocalsizes.zip")).absolutePath();
1072     KDirLister *dirLister = m_dirModel->dirLister();
1073     dirLister->openUrl(QUrl::fromLocalFile(path), KDirLister::NoFlags);
1074     connect(dirLister, qOverload<>(&KCoreDirLister::completed), this, &KDirModelTest::slotListingCompleted);
1075     enterLoop();
1076     disconnect(dirLister, qOverload<>(&KCoreDirLister::completed), this, &KDirModelTest::slotListingCompleted);
1077 
1078     QUrl zipUrl(QUrl::fromLocalFile(path));
1079     zipUrl.setPath(zipUrl.path() + "/wronglocalsizes.zip"); // just a zip file lying here for other reasons
1080 
1081     QVERIFY(QFile::exists(zipUrl.toLocalFile()));
1082     zipUrl.setScheme(QStringLiteral("zip"));
1083     QModelIndex index = m_dirModel->indexForUrl(zipUrl);
1084     QVERIFY(!index.isValid()); // protocol mismatch, can't find it!
1085     zipUrl.setScheme(QStringLiteral("file"));
1086     index = m_dirModel->indexForUrl(zipUrl);
1087     QVERIFY(index.isValid());
1088 }
1089 
1090 class MyDirLister : public KDirLister
1091 {
1092 public:
emitItemsDeleted(const KFileItemList & items)1093     void emitItemsDeleted(const KFileItemList &items)
1094     {
1095         Q_EMIT itemsDeleted(items);
1096     }
1097 };
1098 
testBug196695()1099 void KDirModelTest::testBug196695()
1100 {
1101     KFileItem rootItem(QUrl::fromLocalFile(m_tempDir->path()), QString(), KFileItem::Unknown);
1102     KFileItem childItem(QUrl::fromLocalFile(QString(m_tempDir->path() + "/toplevelfile_1")), QString(), KFileItem::Unknown);
1103 
1104     KFileItemList list;
1105     // Important: the root item must not be first in the list to trigger bug 196695
1106     list << childItem << rootItem;
1107 
1108     MyDirLister *dirLister = static_cast<MyDirLister *>(m_dirModel->dirLister());
1109     dirLister->emitItemsDeleted(list);
1110 
1111     fillModel(true);
1112 }
1113 
testMimeData()1114 void KDirModelTest::testMimeData()
1115 {
1116     QModelIndex index0 = m_dirModel->index(0, 0);
1117     QVERIFY(index0.isValid());
1118     QModelIndex index1 = m_dirModel->index(1, 0);
1119     QVERIFY(index1.isValid());
1120     QList<QModelIndex> indexes;
1121     indexes << index0 << index1;
1122     QMimeData *mimeData = m_dirModel->mimeData(indexes);
1123     QVERIFY(mimeData);
1124     QVERIFY(mimeData->hasUrls());
1125     const QList<QUrl> urls = mimeData->urls();
1126     QCOMPARE(urls.count(), indexes.count());
1127     delete mimeData;
1128 }
1129 
testDotHiddenFile_data()1130 void KDirModelTest::testDotHiddenFile_data()
1131 {
1132     QTest::addColumn<QStringList>("fileContents");
1133     QTest::addColumn<QStringList>("expectedListing");
1134 
1135     const QStringList allItems{QStringLiteral("toplevelfile_1"),
1136                                QStringLiteral("toplevelfile_2"),
1137                                QStringLiteral("toplevelfile_3"),
1138                                specialChars(),
1139                                QStringLiteral("subdir")};
1140     QTest::newRow("empty_file") << (QStringList{}) << allItems;
1141 
1142     QTest::newRow("simple_name") << (QStringList{QStringLiteral("toplevelfile_1")}) << QStringList(allItems.mid(1));
1143 
1144     QStringList allButSpecialChars = allItems;
1145     allButSpecialChars.removeAt(3);
1146     QTest::newRow("special_chars") << (QStringList{specialChars()}) << allButSpecialChars;
1147 
1148     QStringList allButSubdir = allItems;
1149     allButSubdir.removeAt(4);
1150     QTest::newRow("subdir") << (QStringList{QStringLiteral("subdir")}) << allButSubdir;
1151 
1152     QTest::newRow("many_lines")
1153         << (QStringList{QStringLiteral("subdir"), QStringLiteral("toplevelfile_1"), QStringLiteral("toplevelfile_3"), QStringLiteral("toplevelfile_2")})
1154         << QStringList{specialChars()};
1155 }
1156 
testDotHiddenFile()1157 void KDirModelTest::testDotHiddenFile()
1158 {
1159     QFETCH(QStringList, fileContents);
1160     QFETCH(QStringList, expectedListing);
1161 
1162     const QString path = m_tempDir->path() + '/';
1163     const QString dotHiddenFile = path + ".hidden";
1164     QTest::qWait(1000); // mtime-based cache, so we need to wait for 1 second
1165     QFile dh(dotHiddenFile);
1166     QVERIFY(dh.open(QIODevice::WriteOnly));
1167     dh.write(fileContents.join('\n').toUtf8());
1168     dh.close();
1169 
1170     // Do it twice: once to read from the file and once to use the cache
1171     for (int i = 0; i < 2; ++i) {
1172         fillModel(true, false);
1173         QStringList files;
1174         for (int row = 0; row < m_dirModel->rowCount(); ++row) {
1175             files.append(m_dirModel->index(row, KDirModel::Name).data().toString());
1176         }
1177         files.sort();
1178         expectedListing.sort();
1179         QCOMPARE(files, expectedListing);
1180     }
1181 
1182     dh.remove();
1183 }
1184 
testShowRoot()1185 void KDirModelTest::testShowRoot()
1186 {
1187     KDirModel dirModel;
1188     const QUrl homeUrl = QUrl::fromLocalFile(QDir::homePath());
1189     const QUrl fsRootUrl = QUrl(QStringLiteral("file:///"));
1190 
1191     // openUrl("/", ShowRoot) should create a "/" item
1192     dirModel.openUrl(fsRootUrl, KDirModel::ShowRoot);
1193     QTRY_COMPARE(dirModel.rowCount(), 1);
1194     const QModelIndex rootIndex = dirModel.index(0, 0);
1195     QVERIFY(rootIndex.isValid());
1196     QCOMPARE(rootIndex.data().toString(), QStringLiteral("/"));
1197     QVERIFY(!dirModel.parent(rootIndex).isValid());
1198     QCOMPARE(dirModel.itemForIndex(rootIndex).url(), QUrl(QStringLiteral("file:///")));
1199     QCOMPARE(dirModel.itemForIndex(rootIndex).name(), QStringLiteral("/"));
1200 
1201     // expandToUrl should work
1202     dirModel.expandToUrl(homeUrl);
1203     QTRY_VERIFY(dirModel.indexForUrl(homeUrl).isValid());
1204 
1205     // test itemForIndex and indexForUrl
1206     QCOMPARE(dirModel.itemForIndex(QModelIndex()).url(), QUrl());
1207     QVERIFY(!dirModel.indexForUrl(QUrl()).isValid());
1208     const QUrl slashUrl = QUrl::fromLocalFile(QStringLiteral("/"));
1209     QCOMPARE(dirModel.indexForUrl(slashUrl), rootIndex);
1210 
1211     // switching to another URL should also show a root node
1212     QSignalSpy spyRowsRemoved(&dirModel, &QAbstractItemModel::rowsRemoved);
1213     const QUrl tempUrl = QUrl::fromLocalFile(QDir::tempPath());
1214     dirModel.openUrl(tempUrl, KDirModel::ShowRoot);
1215     QTRY_COMPARE(dirModel.rowCount(), 1);
1216     QCOMPARE(spyRowsRemoved.count(), 1);
1217     const QModelIndex newRootIndex = dirModel.index(0, 0);
1218     QVERIFY(newRootIndex.isValid());
1219     QCOMPARE(newRootIndex.data().toString(), QFileInfo(QDir::tempPath()).fileName());
1220     QVERIFY(!dirModel.parent(newRootIndex).isValid());
1221     QVERIFY(!dirModel.indexForUrl(slashUrl).isValid());
1222     QCOMPARE(dirModel.itemForIndex(newRootIndex).url(), tempUrl);
1223 }
1224 
testShowRootWithTrailingSlash()1225 void KDirModelTest::testShowRootWithTrailingSlash()
1226 {
1227     // GIVEN
1228     KDirModel dirModel;
1229     const QUrl homeUrl = QUrl::fromLocalFile(QDir::homePath() + QLatin1Char('/'));
1230 
1231     // WHEN
1232     dirModel.openUrl(homeUrl, KDirModel::ShowRoot);
1233     QTRY_VERIFY(dirModel.indexForUrl(homeUrl).isValid());
1234 }
1235 
testShowRootAndExpandToUrl()1236 void KDirModelTest::testShowRootAndExpandToUrl()
1237 {
1238     // call expandToUrl without waiting for initial listing of root node
1239     KDirModel dirModel;
1240     dirModel.openUrl(QUrl(QStringLiteral("file:///")), KDirModel::ShowRoot);
1241     const QUrl homeUrl = QUrl::fromLocalFile(QDir::homePath());
1242     dirModel.expandToUrl(homeUrl);
1243     QTRY_VERIFY(dirModel.indexForUrl(homeUrl).isValid());
1244 }
1245 
testHasChildren_data()1246 void KDirModelTest::testHasChildren_data()
1247 {
1248     QTest::addColumn<bool>("dirsOnly");
1249     QTest::addColumn<bool>("withHidden");
1250 
1251     QTest::newRow("with_files_and_no_hidden") << false << false;
1252     QTest::newRow("dirs_only_and_no_hidden") << true << false;
1253     QTest::newRow("with_files_and_hidden") << false << true;
1254     QTest::newRow("dirs_only_with_hidden") << true << true;
1255 }
1256 
1257 // Test hasChildren without first populating the dirs
testHasChildren()1258 void KDirModelTest::testHasChildren()
1259 {
1260     QFETCH(bool, dirsOnly);
1261     QFETCH(bool, withHidden);
1262 
1263     m_dirModel->dirLister()->setDirOnlyMode(dirsOnly);
1264     m_dirModel->dirLister()->setShowingDotFiles(withHidden);
1265     fillModel(true, false);
1266 
1267     QVERIFY(m_dirModel->hasChildren());
1268 
1269     auto findDir = [this](const QModelIndex &parentIndex, const QString &name) {
1270         for (int row = 0; row < m_dirModel->rowCount(parentIndex); ++row) {
1271             QModelIndex idx = m_dirModel->index(row, 0, parentIndex);
1272             if (m_dirModel->itemForIndex(idx).isDir() && m_dirModel->itemForIndex(idx).name() == name) {
1273                 return idx;
1274             }
1275         }
1276         return QModelIndex();
1277     };
1278 
1279     m_dirIndex = findDir(QModelIndex(), "subdir");
1280     QVERIFY(m_dirIndex.isValid());
1281     QVERIFY(m_dirModel->hasChildren(m_dirIndex));
1282 
1283     auto listDir = [this](const QModelIndex &index) {
1284         QSignalSpy completedSpy(m_dirModel->dirLister(), qOverload<>(&KDirLister::completed));
1285         m_dirModel->fetchMore(index);
1286         return completedSpy.wait();
1287     };
1288     // Now list subdir/
1289     QVERIFY(listDir(m_dirIndex));
1290 
1291     const QModelIndex subsubdirIndex = findDir(m_dirIndex, "subsubdir");
1292     QVERIFY(subsubdirIndex.isValid());
1293     QCOMPARE(m_dirModel->hasChildren(subsubdirIndex), !dirsOnly);
1294 
1295     const QModelIndex hasChildrenDirIndex = findDir(m_dirIndex, "hasChildren");
1296     QVERIFY(hasChildrenDirIndex.isValid());
1297     QVERIFY(m_dirModel->hasChildren(hasChildrenDirIndex));
1298 
1299     // Now list hasChildren/
1300     QVERIFY(listDir(hasChildrenDirIndex));
1301 
1302     QModelIndex testDirIndex = findDir(hasChildrenDirIndex, "emptyDir");
1303     QVERIFY(testDirIndex.isValid());
1304     QVERIFY(!m_dirModel->hasChildren(testDirIndex));
1305 
1306     testDirIndex = findDir(hasChildrenDirIndex, "hiddenfileDir");
1307     QVERIFY(testDirIndex.isValid());
1308     QCOMPARE(m_dirModel->hasChildren(testDirIndex), !dirsOnly && withHidden);
1309 
1310     testDirIndex = findDir(hasChildrenDirIndex, "hiddenDirDir");
1311     QVERIFY(testDirIndex.isValid());
1312     QCOMPARE(m_dirModel->hasChildren(testDirIndex), withHidden);
1313 
1314     testDirIndex = findDir(hasChildrenDirIndex, "pipeDir");
1315     QVERIFY(testDirIndex.isValid());
1316     QCOMPARE(m_dirModel->hasChildren(testDirIndex), !dirsOnly);
1317 
1318     testDirIndex = findDir(hasChildrenDirIndex, "symlinkDir");
1319     QVERIFY(testDirIndex.isValid());
1320     QCOMPARE(m_dirModel->hasChildren(testDirIndex), !dirsOnly);
1321 
1322     m_dirModel->dirLister()->setDirOnlyMode(false);
1323     m_dirModel->dirLister()->setShowingDotFiles(false);
1324 }
1325 
testInvalidUrl()1326 void KDirModelTest::testInvalidUrl()
1327 {
1328     QSignalSpy completedSpy(m_dirModel->dirLister(), qOverload<>(&KCoreDirLister::completed));
1329     m_dirModel->openUrl(QUrl(":/"));
1330     // currently ends up in KCoreDirLister::handleError. TODO: add error signal to KDirModel
1331 }
1332 
testDeleteFile()1333 void KDirModelTest::testDeleteFile()
1334 {
1335     fillModel(true);
1336 
1337     QVERIFY(m_fileIndex.isValid());
1338     const int oldTopLevelRowCount = m_dirModel->rowCount();
1339     const QString path = m_tempDir->path() + '/';
1340     const QString file = path + "toplevelfile_1";
1341     const QUrl url = QUrl::fromLocalFile(file);
1342 
1343     QSignalSpy spyRowsRemoved(m_dirModel, &QAbstractItemModel::rowsRemoved);
1344     connect(m_dirModel, &QAbstractItemModel::rowsRemoved, &m_eventLoop, &QTestEventLoop::exitLoop);
1345 
1346     KIO::DeleteJob *job = KIO::del(url, KIO::HideProgressInfo);
1347     QVERIFY(job->exec());
1348 
1349     // Wait for the DBUS signal from KDirNotify, it's the one the triggers rowsRemoved
1350     enterLoop();
1351 
1352     // If we come here, then rowsRemoved() was emitted - all good.
1353     const int topLevelRowCount = m_dirModel->rowCount();
1354     QCOMPARE(topLevelRowCount, oldTopLevelRowCount - 1); // one less than before
1355     QCOMPARE(spyRowsRemoved.count(), 1);
1356     QCOMPARE(spyRowsRemoved[0][1].toInt(), m_fileIndex.row());
1357     QCOMPARE(spyRowsRemoved[0][2].toInt(), m_fileIndex.row());
1358     disconnect(m_dirModel, &QAbstractItemModel::rowsRemoved, &m_eventLoop, &QTestEventLoop::exitLoop);
1359 
1360     QModelIndex fileIndex = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "toplevelfile_1"));
1361     QVERIFY(!fileIndex.isValid());
1362 
1363     // Recreate the file, for consistency in the next tests
1364     // So the second part of this test is a "testCreateFile"
1365     createTestFile(file);
1366     // Tricky problem - KDirLister::openUrl will emit items from cache
1367     // and then schedule an update; so just calling fillModel would
1368     // not wait enough, it would abort due to not finding toplevelfile_1
1369     // in the items from cache. This progressive-emitting behavior is fine
1370     // for GUIs but not for unit tests ;-)
1371     fillModel(true, false);
1372     fillModel(false);
1373 }
1374 
testDeleteFileWhileListing()1375 void KDirModelTest::testDeleteFileWhileListing() // doesn't really test that yet, the kdirwatch deleted signal comes too late
1376 {
1377     const int oldTopLevelRowCount = m_dirModel->rowCount();
1378     const QString path = m_tempDir->path() + '/';
1379     const QString file = path + "toplevelfile_1";
1380     const QUrl url = QUrl::fromLocalFile(file);
1381 
1382     KDirLister *dirLister = m_dirModel->dirLister();
1383     QSignalSpy spyCompleted(dirLister, qOverload<>(&KCoreDirLister::completed));
1384     connect(dirLister, qOverload<>(&KCoreDirLister::completed), this, &KDirModelTest::slotListingCompleted);
1385     dirLister->openUrl(QUrl::fromLocalFile(path), KDirLister::NoFlags);
1386     if (!spyCompleted.isEmpty()) {
1387         QSKIP("listing completed too early");
1388     }
1389     QSignalSpy spyRowsRemoved(m_dirModel, &QAbstractItemModel::rowsRemoved);
1390     KIO::DeleteJob *job = KIO::del(url, KIO::HideProgressInfo);
1391     QVERIFY(job->exec());
1392 
1393     if (spyCompleted.isEmpty()) {
1394         enterLoop();
1395     }
1396     QVERIFY(spyRowsRemoved.wait(1000));
1397 
1398     const int topLevelRowCount = m_dirModel->rowCount();
1399     QCOMPARE(topLevelRowCount, oldTopLevelRowCount - 1); // one less than before
1400     QCOMPARE(spyRowsRemoved.count(), 1);
1401     QCOMPARE(spyRowsRemoved[0][1].toInt(), m_fileIndex.row());
1402     QCOMPARE(spyRowsRemoved[0][2].toInt(), m_fileIndex.row());
1403 
1404     QModelIndex fileIndex = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "toplevelfile_1"));
1405     QVERIFY(!fileIndex.isValid());
1406 
1407     qDebug() << "Test done, recreating file";
1408 
1409     // Recreate the file, for consistency in the next tests
1410     // So the second part of this test is a "testCreateFile"
1411     createTestFile(file);
1412     fillModel(true, false); // see testDeleteFile
1413     fillModel(false);
1414 }
1415 
testOverwriteFileWithDir()1416 void KDirModelTest::testOverwriteFileWithDir() // #151851 c4
1417 {
1418     fillModel(false);
1419     const QString path = m_tempDir->path() + '/';
1420     const QString dir = path + "subdir";
1421     const QString file = path + "toplevelfile_1";
1422     const int oldTopLevelRowCount = m_dirModel->rowCount();
1423 
1424     bool removalWithinTopLevel = false;
1425     bool dataChangedAtFirstLevel = false;
1426     auto rrc = connect(m_dirModel, &KDirModel::rowsRemoved, this, [&removalWithinTopLevel](const QModelIndex &index) {
1427         if (!index.isValid()) {
1428             // yes, that's what we have been waiting for
1429             removalWithinTopLevel = true;
1430         }
1431     });
1432     auto dcc = connect(m_dirModel, &KDirModel::dataChanged, this, [&dataChangedAtFirstLevel](const QModelIndex &index) {
1433         if (index.isValid() && !index.parent().isValid()) {
1434             // a change of a node whose parent is root, yay, that's it
1435             dataChangedAtFirstLevel = true;
1436         }
1437     });
1438 
1439     connect(m_dirModel, &QAbstractItemModel::rowsRemoved, &m_eventLoop, &QTestEventLoop::exitLoop);
1440 
1441     KIO::Job *job = KIO::move(QUrl::fromLocalFile(dir), QUrl::fromLocalFile(file), KIO::HideProgressInfo);
1442     delete KIO::delegateExtension<KIO::AskUserActionInterface *>(job);
1443     auto *askUserHandler = new MockAskUserInterface(job->uiDelegate());
1444     askUserHandler->m_renameResult = KIO::Result_Overwrite;
1445     QVERIFY(job->exec());
1446 
1447     QCOMPARE(askUserHandler->m_askUserRenameCalled, 1);
1448 
1449     // Wait for a removal within the top level (that's for the old file going away), and also
1450     // for a dataChanged which notifies us that a file has become a directory
1451 
1452     int retries = 0;
1453     while ((!removalWithinTopLevel || !dataChangedAtFirstLevel) && retries < 100) {
1454         QTest::qWait(10);
1455         ++retries;
1456     }
1457     QVERIFY(removalWithinTopLevel);
1458     QVERIFY(dataChangedAtFirstLevel);
1459 
1460     m_dirModel->disconnect(rrc);
1461     m_dirModel->disconnect(dcc);
1462 
1463     // If we come here, then rowsRemoved() was emitted - all good.
1464     const int topLevelRowCount = m_dirModel->rowCount();
1465     QCOMPARE(topLevelRowCount, oldTopLevelRowCount - 1); // one less than before
1466 
1467     QVERIFY(!m_dirModel->indexForUrl(QUrl::fromLocalFile(dir)).isValid());
1468     QModelIndex newIndex = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "toplevelfile_1"));
1469     QVERIFY(newIndex.isValid());
1470     KFileItem newItem = m_dirModel->itemForIndex(newIndex);
1471     QVERIFY(newItem.isDir()); // yes, the file is a dir now ;-)
1472 
1473     qDebug() << "========= Test done, recreating test data =========";
1474 
1475     recreateTestData();
1476     fillModel(false);
1477 }
1478 
testDeleteFiles()1479 void KDirModelTest::testDeleteFiles()
1480 {
1481     const int oldTopLevelRowCount = m_dirModel->rowCount();
1482     const QString file = m_tempDir->path() + "/toplevelfile_";
1483     QList<QUrl> urls;
1484     urls << QUrl::fromLocalFile(file + '1') << QUrl::fromLocalFile(file + '2') << QUrl::fromLocalFile(file + '3');
1485 
1486     QSignalSpy spyRowsRemoved(m_dirModel, &QAbstractItemModel::rowsRemoved);
1487 
1488     KIO::DeleteJob *job = KIO::del(urls, KIO::HideProgressInfo);
1489     QVERIFY(job->exec());
1490 
1491     int numRowsRemoved = 0;
1492     while (numRowsRemoved < 3) {
1493         QTest::qWait(20);
1494 
1495         numRowsRemoved = 0;
1496         for (int sigNum = 0; sigNum < spyRowsRemoved.count(); ++sigNum) {
1497             numRowsRemoved += spyRowsRemoved[sigNum][2].toInt() - spyRowsRemoved[sigNum][1].toInt() + 1;
1498         }
1499         qDebug() << "numRowsRemoved=" << numRowsRemoved;
1500     }
1501 
1502     const int topLevelRowCount = m_dirModel->rowCount();
1503     QCOMPARE(topLevelRowCount, oldTopLevelRowCount - 3); // three less than before
1504 
1505     qDebug() << "Recreating test data";
1506     recreateTestData();
1507     qDebug() << "Re-filling model";
1508     fillModel(false);
1509 }
1510 
1511 // A renaming that looks more like a deletion to the model
testRenameFileToHidden()1512 void KDirModelTest::testRenameFileToHidden() // #174721
1513 {
1514     const QUrl url = QUrl::fromLocalFile(m_tempDir->path() + "/toplevelfile_2");
1515     const QUrl newUrl = QUrl::fromLocalFile(m_tempDir->path() + "/.toplevelfile_2");
1516 
1517     QSignalSpy spyDataChanged(m_dirModel, &QAbstractItemModel::dataChanged);
1518     QSignalSpy spyRowsRemoved(m_dirModel, &QAbstractItemModel::rowsRemoved);
1519     QSignalSpy spyRowsInserted(m_dirModel, &QAbstractItemModel::rowsInserted);
1520     connect(m_dirModel, &QAbstractItemModel::rowsRemoved, &m_eventLoop, &QTestEventLoop::exitLoop);
1521 
1522     KIO::SimpleJob *job = KIO::rename(url, newUrl, KIO::HideProgressInfo);
1523     QVERIFY(job->exec());
1524 
1525     // Wait for the DBUS signal from KDirNotify, it's the one the triggers KDirLister
1526     enterLoop();
1527 
1528     // If we come here, then rowsRemoved() was emitted - all good.
1529     QCOMPARE(spyDataChanged.count(), 0);
1530     QCOMPARE(spyRowsRemoved.count(), 1);
1531     QCOMPARE(spyRowsInserted.count(), 0);
1532     COMPARE_INDEXES(spyRowsRemoved[0][0].value<QModelIndex>(), QModelIndex()); // parent is invalid
1533     const int row = spyRowsRemoved[0][1].toInt();
1534     QCOMPARE(row, m_secondFileIndex.row()); // only compare row
1535 
1536     disconnect(m_dirModel, &QAbstractItemModel::rowsRemoved, &m_eventLoop, &QTestEventLoop::exitLoop);
1537     spyRowsRemoved.clear();
1538 
1539     // Put things back to normal, should make the file reappear
1540     connect(m_dirModel, &QAbstractItemModel::rowsInserted, &m_eventLoop, &QTestEventLoop::exitLoop);
1541     job = KIO::rename(newUrl, url, KIO::HideProgressInfo);
1542     QVERIFY(job->exec());
1543     // Wait for the DBUS signal from KDirNotify, it's the one the triggers KDirLister
1544     enterLoop();
1545     QCOMPARE(spyDataChanged.count(), 0);
1546     QCOMPARE(spyRowsRemoved.count(), 0);
1547     QCOMPARE(spyRowsInserted.count(), 1);
1548     int newRow = spyRowsInserted[0][1].toInt();
1549     m_secondFileIndex = m_dirModel->index(newRow, 0);
1550     QVERIFY(m_secondFileIndex.isValid());
1551     QCOMPARE(m_dirModel->itemForIndex(m_secondFileIndex).url().toString(), url.toString());
1552 }
1553 
testDeleteDirectory()1554 void KDirModelTest::testDeleteDirectory()
1555 {
1556     const QString path = m_tempDir->path() + '/';
1557     const QUrl url = QUrl::fromLocalFile(path + "subdir/subsubdir");
1558 
1559     QSignalSpy spyRowsRemoved(m_dirModel, &QAbstractItemModel::rowsRemoved);
1560     connect(m_dirModel, &QAbstractItemModel::rowsRemoved, &m_eventLoop, &QTestEventLoop::exitLoop);
1561 
1562     QSignalSpy spyDirWatchDeleted(KDirWatch::self(), &KDirWatch::deleted);
1563 
1564     KIO::DeleteJob *job = KIO::del(url, KIO::HideProgressInfo);
1565     QVERIFY(job->exec());
1566 
1567     // Wait for the DBUS signal from KDirNotify, it's the one the triggers rowsRemoved
1568     enterLoop();
1569 
1570     // If we come here, then rowsRemoved() was emitted - all good.
1571     QCOMPARE(spyRowsRemoved.count(), 1);
1572     disconnect(m_dirModel, &QAbstractItemModel::rowsRemoved, &m_eventLoop, &QTestEventLoop::exitLoop);
1573 
1574     QModelIndex deletedDirIndex = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir/subsubdir"));
1575     QVERIFY(!deletedDirIndex.isValid());
1576     QModelIndex dirIndex = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "subdir"));
1577     QVERIFY(dirIndex.isValid());
1578 
1579     // TODO!!! Bug in KDirWatch? ###
1580     // QCOMPARE(spyDirWatchDeleted.count(), 1);
1581 }
1582 
testDeleteCurrentDirectory()1583 void KDirModelTest::testDeleteCurrentDirectory()
1584 {
1585     const int oldTopLevelRowCount = m_dirModel->rowCount();
1586     const QString path = m_tempDir->path() + '/';
1587     const QUrl url = QUrl::fromLocalFile(path);
1588 
1589     QSignalSpy spyRowsRemoved(m_dirModel, &QAbstractItemModel::rowsRemoved);
1590     connect(m_dirModel, &QAbstractItemModel::rowsRemoved, &m_eventLoop, &QTestEventLoop::exitLoop);
1591 
1592     KDirWatch::self()->statistics();
1593 
1594     KIO::DeleteJob *job = KIO::del(url, KIO::HideProgressInfo);
1595     QVERIFY(job->exec());
1596 
1597     // Wait for the DBUS signal from KDirNotify, it's the one the triggers rowsRemoved
1598     enterLoop();
1599 
1600     // If we come here, then rowsRemoved() was emitted - all good.
1601     const int topLevelRowCount = m_dirModel->rowCount();
1602     QCOMPARE(topLevelRowCount, 0); // empty
1603 
1604     // We can get rowsRemoved for subdirs first, since kdirwatch notices that.
1605     QVERIFY(spyRowsRemoved.count() >= 1);
1606 
1607     // Look for the signal(s) that had QModelIndex() as parent.
1608     int i;
1609     int numDeleted = 0;
1610     for (i = 0; i < spyRowsRemoved.count(); ++i) {
1611         const int from = spyRowsRemoved[i][1].toInt();
1612         const int to = spyRowsRemoved[i][2].toInt();
1613         qDebug() << spyRowsRemoved[i][0].value<QModelIndex>() << from << to;
1614         if (!spyRowsRemoved[i][0].value<QModelIndex>().isValid()) {
1615             numDeleted += (to - from) + 1;
1616         }
1617     }
1618 
1619     QCOMPARE(numDeleted, oldTopLevelRowCount);
1620     disconnect(m_dirModel, &QAbstractItemModel::rowsRemoved, &m_eventLoop, &QTestEventLoop::exitLoop);
1621 
1622     QModelIndex fileIndex = m_dirModel->indexForUrl(QUrl::fromLocalFile(path + "toplevelfile_1"));
1623     QVERIFY(!fileIndex.isValid());
1624 }
1625 
testQUrlHash()1626 void KDirModelTest::testQUrlHash()
1627 {
1628     const int count = 3000;
1629     // Prepare an array of QUrls so that url constructing isn't part of the timing
1630     QVector<QUrl> urls;
1631     urls.resize(count);
1632     for (int i = 0; i < count; ++i) {
1633         urls[i] = QUrl("http://www.kde.org/path/" + QString::number(i));
1634     }
1635     QHash<QUrl, int> qurlHash;
1636     QHash<QUrl, int> kurlHash;
1637     QElapsedTimer dt;
1638     dt.start();
1639     for (int i = 0; i < count; ++i) {
1640         qurlHash.insert(urls[i], i);
1641     }
1642     // qDebug() << "inserting" << count << "urls into QHash using old qHash:" << dt.elapsed() << "msecs";
1643     dt.start();
1644     for (int i = 0; i < count; ++i) {
1645         kurlHash.insert(urls[i], i);
1646     }
1647     // qDebug() << "inserting" << count << "urls into QHash using new qHash:" << dt.elapsed() << "msecs";
1648     // Nice results: for count=30000 I got 4515 (before) and 103 (after)
1649 
1650     dt.start();
1651     for (int i = 0; i < count; ++i) {
1652         QCOMPARE(qurlHash.value(urls[i]), i);
1653     }
1654     // qDebug() << "looking up" << count << "urls into QHash using old qHash:" << dt.elapsed() << "msecs";
1655     dt.start();
1656     for (int i = 0; i < count; ++i) {
1657         QCOMPARE(kurlHash.value(urls[i]), i);
1658     }
1659     // qDebug() << "looking up" << count << "urls into QHash using new qHash:" << dt.elapsed() << "msecs";
1660     // Nice results: for count=30000 I got 4296 (before) and 63 (after)
1661 }
1662