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