X-Git-Url: https://cloud.milkyroute.net/gitweb/dolphin.git/blobdiff_plain/40c71d5ce0f6978ce48eb67fd73ed211bab4e887..d0f0f1f2e2644e3738c03374d062dab9acc61eaf:/src/tests/kfileitemmodeltest.cpp diff --git a/src/tests/kfileitemmodeltest.cpp b/src/tests/kfileitemmodeltest.cpp index 9eee50d44..d224e1a2e 100644 --- a/src/tests/kfileitemmodeltest.cpp +++ b/src/tests/kfileitemmodeltest.cpp @@ -20,7 +20,8 @@ #include -#include +#include + #include "kitemviews/kfileitemmodel.h" #include "kitemviews/private/kfileitemmodeldirlister.h" #include "testdir.h" @@ -28,25 +29,26 @@ void myMessageOutput(QtMsgType type, const char* msg) { switch (type) { - case QtDebugMsg: - break; - case QtWarningMsg: - break; - case QtCriticalMsg: - fprintf(stderr, "Critical: %s\n", msg); - break; - case QtFatalMsg: - fprintf(stderr, "Fatal: %s\n", msg); - abort(); - default: + case QtDebugMsg: + break; + case QtWarningMsg: break; + case QtCriticalMsg: + fprintf(stderr, "Critical: %s\n", msg); + break; + case QtFatalMsg: + fprintf(stderr, "Fatal: %s\n", msg); + abort(); + default: + break; } - } +} namespace { const int DefaultTimeout = 5000; }; +Q_DECLARE_METATYPE(KItemRange) Q_DECLARE_METATYPE(KItemRangeList) Q_DECLARE_METATYPE(QList) @@ -67,18 +69,34 @@ private slots: void testSetData(); void testSetDataWithModifiedSortRole_data(); void testSetDataWithModifiedSortRole(); + void testChangeSortRole(); + void testResortAfterChangingName(); void testModelConsistencyWhenInsertingItems(); void testItemRangeConsistencyWhenInsertingItems(); void testExpandItems(); void testExpandParentItems(); + void testMakeExpandedItemHidden(); + void testRemoveFilteredExpandedItems(); void testSorting(); - void testIndexForKeyboardSearch(); - void testNameFilter(); + void testEmptyPath(); + void testRefreshExpandedItem(); + void testRemoveHiddenItems(); + void collapseParentOfHiddenItems(); + void removeParentOfHiddenItems(); + void testGeneralParentChildRelationships(); + void testNameRoleGroups(); + void testNameRoleGroupsWithExpandedItems(); + void testInconsistentModel(); + void testChangeRolesForFilteredItems(); + void testChangeSortRoleWhileFiltering(); + void testRefreshFilteredItems(); + void testCollapseFolderWhileLoading(); + void testCreateMimeData(); + void testDeleteFileMoreThanOnce(); private: - bool isModelConsistent() const; QStringList itemsInModel() const; private: @@ -99,6 +117,9 @@ void KFileItemModelTest::init() m_testDir = new TestDir(); m_model = new KFileItemModel(); m_model->m_dirLister->setAutoUpdate(false); + + // Reduce the timer interval to make the test run faster. + m_model->m_resortAllItemsTimer->setInterval(0); } void KFileItemModelTest::cleanup() @@ -113,14 +134,15 @@ void KFileItemModelTest::cleanup() void KFileItemModelTest::testDefaultRoles() { const QSet roles = m_model->roles(); - QCOMPARE(roles.count(), 2); - QVERIFY(roles.contains("name")); + QCOMPARE(roles.count(), 3); + QVERIFY(roles.contains("text")); QVERIFY(roles.contains("isDir")); + QVERIFY(roles.contains("isLink")); } void KFileItemModelTest::testDefaultSortRole() { - QCOMPARE(m_model->sortRole(), QByteArray("name")); + QCOMPARE(m_model->sortRole(), QByteArray("text")); QStringList files; files << "c.txt" << "a.txt" << "b.txt"; @@ -131,9 +153,9 @@ void KFileItemModelTest::testDefaultSortRole() QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); QCOMPARE(m_model->count(), 3); - QCOMPARE(m_model->data(0)["name"].toString(), QString("a.txt")); - QCOMPARE(m_model->data(1)["name"].toString(), QString("b.txt")); - QCOMPARE(m_model->data(2)["name"].toString(), QString("c.txt")); + QCOMPARE(m_model->data(0)["text"].toString(), QString("a.txt")); + QCOMPARE(m_model->data(1)["text"].toString(), QString("b.txt")); + QCOMPARE(m_model->data(2)["text"].toString(), QString("c.txt")); } void KFileItemModelTest::testDefaultGroupedSorting() @@ -152,7 +174,7 @@ void KFileItemModelTest::testNewItems() QCOMPARE(m_model->count(), 3); - QVERIFY(isModelConsistent()); + QVERIFY(m_model->isConsistent()); } void KFileItemModelTest::testRemoveItems() @@ -162,13 +184,13 @@ void KFileItemModelTest::testRemoveItems() m_model->loadDirectory(m_testDir->url()); QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); QCOMPARE(m_model->count(), 2); - QVERIFY(isModelConsistent()); + QVERIFY(m_model->isConsistent()); m_testDir->removeFile("a.txt"); m_model->m_dirLister->updateDirectory(m_testDir->url()); QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsRemoved(KItemRangeList)), DefaultTimeout)); QCOMPARE(m_model->count(), 1); - QVERIFY(isModelConsistent()); + QVERIFY(m_model->isConsistent()); } void KFileItemModelTest::testDirLoadingCompleted() @@ -177,7 +199,7 @@ void KFileItemModelTest::testDirLoadingCompleted() QSignalSpy itemsInsertedSpy(m_model, SIGNAL(itemsInserted(KItemRangeList))); QSignalSpy itemsRemovedSpy(m_model, SIGNAL(itemsRemoved(KItemRangeList))); - m_testDir->createFiles(QStringList() << "a.txt" << "b.txt" << "c.txt"); + m_testDir->createFiles({"a.txt", "b.txt", "c.txt"}); m_model->loadDirectory(m_testDir->url()); QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(directoryLoadingCompleted()), DefaultTimeout)); @@ -186,7 +208,7 @@ void KFileItemModelTest::testDirLoadingCompleted() QCOMPARE(itemsRemovedSpy.count(), 0); QCOMPARE(m_model->count(), 3); - m_testDir->createFiles(QStringList() << "d.txt" << "e.txt"); + m_testDir->createFiles({"d.txt", "e.txt"}); m_model->m_dirLister->updateDirectory(m_testDir->url()); QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(directoryLoadingCompleted()), DefaultTimeout)); QCOMPARE(loadingCompletedSpy.count(), 2); @@ -211,7 +233,7 @@ void KFileItemModelTest::testDirLoadingCompleted() QCOMPARE(itemsRemovedSpy.count(), 2); QCOMPARE(m_model->count(), 4); - QVERIFY(isModelConsistent()); + QVERIFY(m_model->isConsistent()); } void KFileItemModelTest::testSetData() @@ -232,7 +254,7 @@ void KFileItemModelTest::testSetData() values = m_model->data(0); QCOMPARE(values.value("customRole1").toString(), QString("Test1")); QCOMPARE(values.value("customRole2").toString(), QString("Test2")); - QVERIFY(isModelConsistent()); + QVERIFY(m_model->isConsistent()); } void KFileItemModelTest::testSetDataWithModifiedSortRole_data() @@ -269,7 +291,9 @@ void KFileItemModelTest::testSetDataWithModifiedSortRole() // Changing the value of a sort-role must result in // a reordering of the items. - QCOMPARE(m_model->sortRole(), QByteArray("name")); + QCOMPARE(m_model->sortRole(), QByteArray("text")); + m_model->setSortRole("rating"); + QCOMPARE(m_model->sortRole(), QByteArray("rating")); QStringList files; files << "a.txt" << "b.txt" << "c.txt"; @@ -295,7 +319,6 @@ void KFileItemModelTest::testSetDataWithModifiedSortRole() ratingC.insert("rating", 6); m_model->setData(2, ratingC); - m_model->setSortRole("rating"); QCOMPARE(m_model->data(0).value("rating").toInt(), 2); QCOMPARE(m_model->data(1).value("rating").toInt(), 4); QCOMPARE(m_model->data(2).value("rating").toInt(), 6); @@ -313,7 +336,82 @@ void KFileItemModelTest::testSetDataWithModifiedSortRole() QCOMPARE(m_model->data(0).value("rating").toInt(), ratingIndex0); QCOMPARE(m_model->data(1).value("rating").toInt(), ratingIndex1); QCOMPARE(m_model->data(2).value("rating").toInt(), ratingIndex2); - QVERIFY(isModelConsistent()); + QVERIFY(m_model->isConsistent()); +} + +void KFileItemModelTest::testChangeSortRole() +{ + QCOMPARE(m_model->sortRole(), QByteArray("text")); + + QStringList files; + files << "a.txt" << "b.jpg" << "c.txt"; + m_testDir->createFiles(files); + + m_model->loadDirectory(m_testDir->url()); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(itemsInModel(), QStringList() << "a.txt" << "b.jpg" << "c.txt"); + + // Simulate that KFileItemModelRolesUpdater determines the mime type. + // Resorting the files by 'type' will only work immediately if their + // mime types are known. + for (int index = 0; index < m_model->count(); ++index) { + m_model->fileItem(index).determineMimeType(); + } + + // Now: sort by type. + QSignalSpy spyItemsMoved(m_model, SIGNAL(itemsMoved(KItemRange,QList))); + m_model->setSortRole("type"); + QCOMPARE(m_model->sortRole(), QByteArray("type")); + QVERIFY(!spyItemsMoved.isEmpty()); + + // The actual order of the files might depend on the translation of the + // result of KFileItem::mimeComment() in the user's language. + QStringList version1; + version1 << "b.jpg" << "a.txt" << "c.txt"; + + QStringList version2; + version2 << "a.txt" << "c.txt" << "b.jpg"; + + const bool ok1 = (itemsInModel() == version1); + const bool ok2 = (itemsInModel() == version2); + + QVERIFY(ok1 || ok2); +} + +void KFileItemModelTest::testResortAfterChangingName() +{ + // We sort by size in a directory where all files have the same size. + // Therefore, the files are sorted by their names. + m_model->setSortRole("size"); + + QStringList files; + files << "a.txt" << "b.txt" << "c.txt"; + m_testDir->createFiles(files); + + m_model->loadDirectory(m_testDir->url()); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(itemsInModel(), QStringList() << "a.txt" << "b.txt" << "c.txt"); + + // We rename a.txt to d.txt. Even though the size has not changed at all, + // the model must re-sort the items. + QHash data; + data.insert("text", "d.txt"); + m_model->setData(0, data); + + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsMoved(KItemRange,QList)), DefaultTimeout)); + QCOMPARE(itemsInModel(), QStringList() << "b.txt" << "c.txt" << "d.txt"); + + // We rename d.txt back to a.txt using the dir lister's refreshItems() signal. + const KFileItem fileItemD = m_model->fileItem(2); + KFileItem fileItemA = fileItemD; + QUrl urlA = fileItemA.url().adjusted(QUrl::RemoveFilename); + urlA.setPath(urlA.path() + "a.txt"); + fileItemA.setUrl(urlA); + + m_model->slotRefreshItems({qMakePair(fileItemD, fileItemA)}); + + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsMoved(KItemRange,QList)), DefaultTimeout)); + QCOMPARE(itemsInModel(), QStringList() << "a.txt" << "b.txt" << "c.txt"); } void KFileItemModelTest::testModelConsistencyWhenInsertingItems() @@ -352,7 +450,7 @@ void KFileItemModelTest::testModelConsistencyWhenInsertingItems() QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); } - QVERIFY(isModelConsistent()); + QVERIFY(m_model->isConsistent()); } QCOMPARE(m_model->count(), 201); @@ -408,7 +506,8 @@ void KFileItemModelTest::testExpandItems() // KFileItemModel::expansionLevelsCompare(const KFileItem& a, const KFileItem& b) // yields the correct result for "a/a/1" and "a/a-1/", whis is non-trivial because they share the // first three characters. - QSet modelRoles = m_model->roles(); + QSet originalModelRoles = m_model->roles(); + QSet modelRoles = originalModelRoles; modelRoles << "isExpanded" << "isExpandable" << "expandedParentsCount"; m_model->setRoles(modelRoles); @@ -417,8 +516,10 @@ void KFileItemModelTest::testExpandItems() m_testDir->createFiles(files); // Store the URLs of all folders in a set. - QSet allFolders; - allFolders << KUrl(m_testDir->name() + "a") << KUrl(m_testDir->name() + "a/a") << KUrl(m_testDir->name() + "a/a-1"); + QSet allFolders; + allFolders << QUrl::fromLocalFile(m_testDir->name() + 'a') + << QUrl::fromLocalFile(m_testDir->name() + "a/a") + << QUrl::fromLocalFile(m_testDir->name() + "a/a-1"); m_model->loadDirectory(m_testDir->url()); QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); @@ -436,7 +537,7 @@ void KFileItemModelTest::testExpandItems() QVERIFY(m_model->isExpanded(0)); QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); QCOMPARE(m_model->count(), 3); // 3 items: "a/", "a/a/", "a/a-1/" - QCOMPARE(m_model->expandedDirectories(), QSet() << KUrl(m_testDir->name() + "a")); + QCOMPARE(m_model->expandedDirectories(), QSet() << QUrl::fromLocalFile(m_testDir->name() + 'a')); QCOMPARE(spyInserted.count(), 1); KItemRangeList itemRangeList = spyInserted.takeFirst().at(0).value(); @@ -452,7 +553,7 @@ void KFileItemModelTest::testExpandItems() QVERIFY(m_model->isExpanded(1)); QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); QCOMPARE(m_model->count(), 4); // 4 items: "a/", "a/a/", "a/a/1", "a/a-1/" - QCOMPARE(m_model->expandedDirectories(), QSet() << KUrl(m_testDir->name() + "a") << KUrl(m_testDir->name() + "a/a")); + QCOMPARE(m_model->expandedDirectories(), QSet() << QUrl::fromLocalFile(m_testDir->name() + 'a') << QUrl::fromLocalFile(m_testDir->name() + "a/a")); QCOMPARE(spyInserted.count(), 1); itemRangeList = spyInserted.takeFirst().at(0).value(); @@ -481,11 +582,12 @@ void KFileItemModelTest::testExpandItems() m_model->setExpanded(0, false); QVERIFY(!m_model->isExpanded(0)); QCOMPARE(m_model->count(), 1); - QVERIFY(!m_model->expandedDirectories().contains(KUrl(m_testDir->name() + "a"))); // TODO: Make sure that child URLs are also removed + QVERIFY(!m_model->expandedDirectories().contains(QUrl::fromLocalFile(m_testDir->name() + 'a'))); // TODO: Make sure that child URLs are also removed QCOMPARE(spyRemoved.count(), 1); itemRangeList = spyRemoved.takeFirst().at(0).value(); QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(1, 4)); // 4 items removed + QVERIFY(m_model->isConsistent()); // Clear the model, reload the folder and try to restore the expanded folders. m_model->clear(); @@ -502,10 +604,11 @@ void KFileItemModelTest::testExpandItems() QVERIFY(m_model->isExpanded(3)); QVERIFY(!m_model->isExpanded(4)); QCOMPARE(m_model->expandedDirectories(), allFolders); + QVERIFY(m_model->isConsistent()); // Move to a sub folder, then call restoreExpandedFolders() *before* going back. // This is how DolphinView restores the expanded folders when navigating in history. - m_model->loadDirectory(KUrl(m_testDir->name() + "a/a/")); + m_model->loadDirectory(QUrl::fromLocalFile(m_testDir->name() + "a/a/")); QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(directoryLoadingCompleted()), DefaultTimeout)); QCOMPARE(m_model->count(), 1); // 1 item: "1" m_model->restoreExpandedDirectories(allFolders); @@ -513,6 +616,18 @@ void KFileItemModelTest::testExpandItems() QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(directoryLoadingCompleted()), DefaultTimeout)); QCOMPARE(m_model->count(), 5); // 5 items: "a/", "a/a/", "a/a/1", "a/a-1/", "a/a-1/1" QCOMPARE(m_model->expandedDirectories(), allFolders); + + // Remove all expanded items by changing the roles + spyRemoved.clear(); + m_model->setRoles(originalModelRoles); + QVERIFY(!m_model->isExpanded(0)); + QCOMPARE(m_model->count(), 1); + QVERIFY(!m_model->expandedDirectories().contains(QUrl::fromLocalFile(m_testDir->name() + 'a'))); + + QCOMPARE(spyRemoved.count(), 1); + itemRangeList = spyRemoved.takeFirst().at(0).value(); + QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(1, 4)); // 4 items removed + QVERIFY(m_model->isConsistent()); } void KFileItemModelTest::testExpandParentItems() @@ -541,7 +656,7 @@ void KFileItemModelTest::testExpandParentItems() QVERIFY(m_model->expandedDirectories().empty()); // Expand the parents of "a2/b2/c2". - m_model->expandParentDirectories(KUrl(m_testDir->name() + "a2/b2/c2")); + m_model->expandParentDirectories(QUrl::fromLocalFile(m_testDir->name() + "a2/b2/c2")); QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(directoryLoadingCompleted()), DefaultTimeout)); // The model should now contain "a 1/", "a2/", "a2/b2/", and "a2/b2/c2/". @@ -553,7 +668,7 @@ void KFileItemModelTest::testExpandParentItems() QVERIFY(!m_model->isExpanded(3)); // Expand the parents of "a 1/b1". - m_model->expandParentDirectories(KUrl(m_testDir->name() + "a 1/b1")); + m_model->expandParentDirectories(QUrl::fromLocalFile(m_testDir->name() + "a 1/b1")); QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(directoryLoadingCompleted()), DefaultTimeout)); // The model should now contain "a 1/", "a 1/b1/", "a2/", "a2/b2", and "a2/b2/c2/". @@ -564,6 +679,123 @@ void KFileItemModelTest::testExpandParentItems() QVERIFY(m_model->isExpanded(2)); QVERIFY(m_model->isExpanded(3)); QVERIFY(!m_model->isExpanded(4)); + QVERIFY(m_model->isConsistent()); + + // Expand "a 1/b1/". + m_model->setExpanded(1, true); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(directoryLoadingCompleted()), DefaultTimeout)); + QCOMPARE(m_model->count(), 6); + QVERIFY(m_model->isExpanded(0)); + QVERIFY(m_model->isExpanded(1)); + QVERIFY(!m_model->isExpanded(2)); + QVERIFY(m_model->isExpanded(3)); + QVERIFY(m_model->isExpanded(4)); + QVERIFY(!m_model->isExpanded(5)); + QVERIFY(m_model->isConsistent()); + + // Collapse "a 1/b1/" again, and verify that the previous state is restored. + m_model->setExpanded(1, false); + QCOMPARE(m_model->count(), 5); + QVERIFY(m_model->isExpanded(0)); + QVERIFY(!m_model->isExpanded(1)); + QVERIFY(m_model->isExpanded(2)); + QVERIFY(m_model->isExpanded(3)); + QVERIFY(!m_model->isExpanded(4)); + QVERIFY(m_model->isConsistent()); +} + +/** + * Renaming an expanded folder by prepending its name with a dot makes it + * hidden. Verify that this does not cause an inconsistent model state and + * a crash later on, see https://bugs.kde.org/show_bug.cgi?id=311947 + */ +void KFileItemModelTest::testMakeExpandedItemHidden() +{ + QSet modelRoles = m_model->roles(); + modelRoles << "isExpanded" << "isExpandable" << "expandedParentsCount"; + m_model->setRoles(modelRoles); + + QStringList files; + m_testDir->createFile("1a/2a/3a"); + m_testDir->createFile("1a/2a/3b"); + m_testDir->createFile("1a/2b"); + m_testDir->createFile("1b"); + + m_model->loadDirectory(m_testDir->url()); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + + // So far, the model contains only "1a/" and "1b". + QCOMPARE(m_model->count(), 2); + m_model->setExpanded(0, true); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + + // Now "1a/2a" and "1a/2b" have appeared. + QCOMPARE(m_model->count(), 4); + m_model->setExpanded(1, true); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(m_model->count(), 6); + + // Rename "1a/2" and make it hidden. + const QUrl oldUrl = QUrl::fromLocalFile(m_model->fileItem(0).url().path() + "/2a"); + const QUrl newUrl = QUrl::fromLocalFile(m_model->fileItem(0).url().path() + "/.2a"); + + KIO::SimpleJob* job = KIO::rename(oldUrl, newUrl, KIO::HideProgressInfo); + bool ok = job->exec(); + QVERIFY(ok); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsRemoved(KItemRangeList)), DefaultTimeout)); + + // "1a/2" and its subfolders have disappeared now. + QVERIFY(m_model->isConsistent()); + QCOMPARE(m_model->count(), 3); + + m_model->setExpanded(0, false); + QCOMPARE(m_model->count(), 2); + +} + +void KFileItemModelTest::testRemoveFilteredExpandedItems() +{ + QSet originalModelRoles = m_model->roles(); + QSet modelRoles = originalModelRoles; + modelRoles << "isExpanded" << "isExpandable" << "expandedParentsCount"; + m_model->setRoles(modelRoles); + + QStringList files; + files << "folder/child" << "file"; // missing folders are created automatically + m_testDir->createFiles(files); + + m_model->loadDirectory(m_testDir->url()); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + + // So far, the model contains only "folder/" and "file". + QCOMPARE(m_model->count(), 2); + QVERIFY(m_model->isExpandable(0)); + QVERIFY(!m_model->isExpandable(1)); + QVERIFY(!m_model->isExpanded(0)); + QVERIFY(!m_model->isExpanded(1)); + QCOMPARE(itemsInModel(), QStringList() << "folder" << "file"); + + // Expand "folder" -> "folder/child" becomes visible. + m_model->setExpanded(0, true); + QVERIFY(m_model->isExpanded(0)); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(itemsInModel(), QStringList() << "folder" << "child" << "file"); + + // Add a name filter. + m_model->setNameFilter("f"); + QCOMPARE(itemsInModel(), QStringList() << "folder" << "file"); + + m_model->setNameFilter("fo"); + QCOMPARE(itemsInModel(), QStringList() << "folder"); + + // Remove all expanded items by changing the roles + m_model->setRoles(originalModelRoles); + QVERIFY(!m_model->isExpanded(0)); + QCOMPARE(itemsInModel(), QStringList() << "folder"); + + // Remove the name filter and verify that "folder/child" does not reappear. + m_model->setNameFilter(QString()); + QCOMPARE(itemsInModel(), QStringList() << "folder" << "file"); } void KFileItemModelTest::testSorting() @@ -572,7 +804,7 @@ void KFileItemModelTest::testSorting() QDateTime now = QDateTime::currentDateTime(); QSet roles; - roles.insert("name"); + roles.insert("text"); roles.insert("isExpanded"); roles.insert("isExpandable"); roles.insert("expandedParentsCount"); @@ -592,16 +824,16 @@ void KFileItemModelTest::testSorting() m_model->loadDirectory(m_testDir->url()); QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); - int index = m_model->index(KUrl(m_testDir->url().url() + "c")); + int index = m_model->index(QUrl(m_testDir->url().url() + 'c')); m_model->setExpanded(index, true); QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); - index = m_model->index(KUrl(m_testDir->url().url() + "c/c-2")); + index = m_model->index(QUrl(m_testDir->url().url() + "c/c-2")); m_model->setExpanded(index, true); QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); // Default: Sort by Name, ascending - QCOMPARE(m_model->sortRole(), QByteArray("name")); + QCOMPARE(m_model->sortRole(), QByteArray("text")); QCOMPARE(m_model->sortOrder(), Qt::AscendingOrder); QVERIFY(m_model->sortDirectoriesFirst()); QVERIFY(!m_model->showHiddenFiles()); @@ -611,21 +843,24 @@ void KFileItemModelTest::testSorting() // Sort by Name, ascending, 'Sort Folders First' disabled m_model->setSortDirectoriesFirst(false); - QCOMPARE(m_model->sortRole(), QByteArray("name")); + QCOMPARE(m_model->sortRole(), QByteArray("text")); QCOMPARE(m_model->sortOrder(), Qt::AscendingOrder); QCOMPARE(itemsInModel(), QStringList() << "a" << "b" << "c" << "c-1" << "c-2" << "c-3" << "d" << "e"); QCOMPARE(spyItemsMoved.count(), 1); - QCOMPARE(spyItemsMoved.takeFirst().at(1).value >(), QList() << 2 << 4 << 5 << 3 << 0 << 1 << 6 << 7); + QCOMPARE(spyItemsMoved.first().at(0).value(), KItemRange(0, 6)); + QCOMPARE(spyItemsMoved.takeFirst().at(1).value >(), QList() << 2 << 4 << 5 << 3 << 0 << 1); // Sort by Name, descending m_model->setSortDirectoriesFirst(true); m_model->setSortOrder(Qt::DescendingOrder); - QCOMPARE(m_model->sortRole(), QByteArray("name")); + QCOMPARE(m_model->sortRole(), QByteArray("text")); QCOMPARE(m_model->sortOrder(), Qt::DescendingOrder); QCOMPARE(itemsInModel(), QStringList() << "c" << "c-2" << "c-3" << "c-1" << "e" << "d" << "b" << "a"); QCOMPARE(spyItemsMoved.count(), 2); - QCOMPARE(spyItemsMoved.takeFirst().at(1).value >(), QList() << 4 << 5 << 0 << 3 << 1 << 2 << 6 << 7); - QCOMPARE(spyItemsMoved.takeFirst().at(1).value >(), QList() << 0 << 1 << 2 << 3 << 7 << 6 << 5 << 4); + QCOMPARE(spyItemsMoved.first().at(0).value(), KItemRange(0, 6)); + QCOMPARE(spyItemsMoved.takeFirst().at(1).value >(), QList() << 4 << 5 << 0 << 3 << 1 << 2); + QCOMPARE(spyItemsMoved.first().at(0).value(), KItemRange(4, 4)); + QCOMPARE(spyItemsMoved.takeFirst().at(1).value >(), QList() << 7 << 6 << 5 << 4); // Sort by Date, descending m_model->setSortDirectoriesFirst(true); @@ -634,7 +869,8 @@ void KFileItemModelTest::testSorting() QCOMPARE(m_model->sortOrder(), Qt::DescendingOrder); QCOMPARE(itemsInModel(), QStringList() << "c" << "c-2" << "c-3" << "c-1" << "b" << "d" << "a" << "e"); QCOMPARE(spyItemsMoved.count(), 1); - QCOMPARE(spyItemsMoved.takeFirst().at(1).value >(), QList() << 0 << 1 << 2 << 3 << 7 << 5 << 4 << 6); + QCOMPARE(spyItemsMoved.first().at(0).value(), KItemRange(4, 4)); + QCOMPARE(spyItemsMoved.takeFirst().at(1).value >(), QList() << 7 << 5 << 4 << 6); // Sort by Date, ascending m_model->setSortOrder(Qt::AscendingOrder); @@ -642,7 +878,8 @@ void KFileItemModelTest::testSorting() QCOMPARE(m_model->sortOrder(), Qt::AscendingOrder); QCOMPARE(itemsInModel(), QStringList() << "c" << "c-2" << "c-3" << "c-1" << "e" << "a" << "d" << "b"); QCOMPARE(spyItemsMoved.count(), 1); - QCOMPARE(spyItemsMoved.takeFirst().at(1).value >(), QList() << 0 << 1 << 2 << 3 << 7 << 6 << 5 << 4); + QCOMPARE(spyItemsMoved.first().at(0).value(), KItemRange(4, 4)); + QCOMPARE(spyItemsMoved.takeFirst().at(1).value >(), QList() << 7 << 6 << 5 << 4); // Sort by Date, ascending, 'Sort Folders First' disabled m_model->setSortDirectoriesFirst(false); @@ -651,14 +888,16 @@ void KFileItemModelTest::testSorting() QVERIFY(!m_model->sortDirectoriesFirst()); QCOMPARE(itemsInModel(), QStringList() << "e" << "a" << "c" << "c-1" << "c-2" << "c-3" << "d" << "b"); QCOMPARE(spyItemsMoved.count(), 1); - QCOMPARE(spyItemsMoved.takeFirst().at(1).value >(), QList() << 2 << 4 << 5 << 3 << 0 << 1 << 6 << 7); + QCOMPARE(spyItemsMoved.first().at(0).value(), KItemRange(0, 6)); + QCOMPARE(spyItemsMoved.takeFirst().at(1).value >(), QList() << 2 << 4 << 5 << 3 << 0 << 1); // Sort by Name, ascending, 'Sort Folders First' disabled - m_model->setSortRole("name"); + m_model->setSortRole("text"); QCOMPARE(m_model->sortOrder(), Qt::AscendingOrder); QVERIFY(!m_model->sortDirectoriesFirst()); QCOMPARE(itemsInModel(), QStringList() << "a" << "b" << "c" << "c-1" << "c-2" << "c-3" << "d" << "e"); QCOMPARE(spyItemsMoved.count(), 1); + QCOMPARE(spyItemsMoved.first().at(0).value(), KItemRange(0, 8)); QCOMPARE(spyItemsMoved.takeFirst().at(1).value >(), QList() << 7 << 0 << 2 << 3 << 4 << 5 << 6 << 1); // Sort by Size, ascending, 'Sort Folders First' disabled @@ -668,19 +907,15 @@ void KFileItemModelTest::testSorting() QVERIFY(!m_model->sortDirectoriesFirst()); QCOMPARE(itemsInModel(), QStringList() << "c" << "c-2" << "c-3" << "c-1" << "a" << "b" << "e" << "d"); QCOMPARE(spyItemsMoved.count(), 1); + QCOMPARE(spyItemsMoved.first().at(0).value(), KItemRange(0, 8)); QCOMPARE(spyItemsMoved.takeFirst().at(1).value >(), QList() << 4 << 5 << 0 << 3 << 1 << 2 << 7 << 6); - QSKIP("2 tests of testSorting() are temporary deactivated as in KFileItemModel resortAllItems() " - "always emits a itemsMoved() signal. Before adjusting the tests think about probably introducing " - "another signal", SkipSingle); - // Internal note: Check comment in KFileItemModel::resortAllItems() for details. - // In 'Sort by Size' mode, folders are always first -> changing 'Sort Folders First' does not resort the model m_model->setSortDirectoriesFirst(true); QCOMPARE(m_model->sortRole(), QByteArray("size")); QCOMPARE(m_model->sortOrder(), Qt::AscendingOrder); QVERIFY(m_model->sortDirectoriesFirst()); - QCOMPARE(itemsInModel(), QStringList() << "c" << "a" << "b" << "e" << "d"); + QCOMPARE(itemsInModel(), QStringList() << "c" << "c-2" << "c-3" << "c-1" << "a" << "b" << "e" << "d"); QCOMPARE(spyItemsMoved.count(), 0); // Sort by Size, descending, 'Sort Folders First' enabled @@ -688,9 +923,10 @@ void KFileItemModelTest::testSorting() QCOMPARE(m_model->sortRole(), QByteArray("size")); QCOMPARE(m_model->sortOrder(), Qt::DescendingOrder); QVERIFY(m_model->sortDirectoriesFirst()); - QCOMPARE(itemsInModel(), QStringList() << "c" << "d" << "e" << "b" << "a"); + QCOMPARE(itemsInModel(), QStringList() << "c" << "c-2" << "c-3" << "c-1" << "d" << "e" << "b" << "a"); QCOMPARE(spyItemsMoved.count(), 1); - QCOMPARE(spyItemsMoved.takeFirst().at(1).value >(), QList() << 0 << 4 << 3 << 2 << 1); + QCOMPARE(spyItemsMoved.first().at(0).value(), KItemRange(4, 4)); + QCOMPARE(spyItemsMoved.takeFirst().at(1).value >(), QList() << 7 << 6 << 5 << 4); // TODO: Sort by other roles; show/hide hidden files } @@ -773,34 +1009,723 @@ void KFileItemModelTest::testNameFilter() QCOMPARE(m_model->count(), 5); } -bool KFileItemModelTest::isModelConsistent() const +/** + * Verifies that we do not crash when adding a KFileItem with an empty path. + * Before this issue was fixed, KFileItemModel::expandedParentsCountCompare() + * tried to always read the first character of the path, even if the path is empty. + */ +void KFileItemModelTest::testEmptyPath() +{ + QSet roles; + roles.insert("text"); + roles.insert("isExpanded"); + roles.insert("isExpandable"); + roles.insert("expandedParentsCount"); + m_model->setRoles(roles); + + const QUrl emptyUrl; + QVERIFY(emptyUrl.path().isEmpty()); + + const QUrl url("file:///test/"); + + KFileItemList items; + items << KFileItem(emptyUrl, QString(), KFileItem::Unknown) << KFileItem(url, QString(), KFileItem::Unknown); + m_model->slotItemsAdded(emptyUrl, items); + m_model->slotCompleted(); +} + +/** + * Verifies that the 'isExpanded' state of folders does not change when the + * 'refreshItems' signal is received, see https://bugs.kde.org/show_bug.cgi?id=299675. + */ +void KFileItemModelTest::testRefreshExpandedItem() +{ + QSet modelRoles = m_model->roles(); + modelRoles << "isExpanded" << "isExpandable" << "expandedParentsCount"; + m_model->setRoles(modelRoles); + + QStringList files; + files << "a/1" << "a/2" << "3" << "4"; + m_testDir->createFiles(files); + + m_model->loadDirectory(m_testDir->url()); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(m_model->count(), 3); // "a/", "3", "4" + + m_model->setExpanded(0, true); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(m_model->count(), 5); // "a/", "a/1", "a/2", "3", "4" + QVERIFY(m_model->isExpanded(0)); + + QSignalSpy spyItemsChanged(m_model, SIGNAL(itemsChanged(KItemRangeList,QSet))); + + const KFileItem item = m_model->fileItem(0); + m_model->slotRefreshItems({qMakePair(item, item)}); + QVERIFY(!spyItemsChanged.isEmpty()); + + QCOMPARE(m_model->count(), 5); // "a/", "a/1", "a/2", "3", "4" + QVERIFY(m_model->isExpanded(0)); +} + +/** + * Verify that removing hidden files and folders from the model does not + * result in a crash, see https://bugs.kde.org/show_bug.cgi?id=314046 + */ +void KFileItemModelTest::testRemoveHiddenItems() +{ + m_testDir->createDir(".a"); + m_testDir->createDir(".b"); + m_testDir->createDir("c"); + m_testDir->createDir("d"); + m_testDir->createFiles({".f", ".g", "h", "i"}); + + QSignalSpy spyItemsInserted(m_model, SIGNAL(itemsInserted(KItemRangeList))); + QSignalSpy spyItemsRemoved(m_model, SIGNAL(itemsRemoved(KItemRangeList))); + + m_model->setShowHiddenFiles(true); + m_model->loadDirectory(m_testDir->url()); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(itemsInModel(), QStringList() << ".a" << ".b" << "c" << "d" <<".f" << ".g" << "h" << "i"); + QCOMPARE(spyItemsInserted.count(), 1); + QCOMPARE(spyItemsRemoved.count(), 0); + KItemRangeList itemRangeList = spyItemsInserted.takeFirst().at(0).value(); + QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(0, 8)); + + m_model->setShowHiddenFiles(false); + QCOMPARE(itemsInModel(), QStringList() << "c" << "d" << "h" << "i"); + QCOMPARE(spyItemsInserted.count(), 0); + QCOMPARE(spyItemsRemoved.count(), 1); + itemRangeList = spyItemsRemoved.takeFirst().at(0).value(); + QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(0, 2) << KItemRange(4, 2)); + + m_model->setShowHiddenFiles(true); + QCOMPARE(itemsInModel(), QStringList() << ".a" << ".b" << "c" << "d" <<".f" << ".g" << "h" << "i"); + QCOMPARE(spyItemsInserted.count(), 1); + QCOMPARE(spyItemsRemoved.count(), 0); + itemRangeList = spyItemsInserted.takeFirst().at(0).value(); + QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(0, 2) << KItemRange(2, 2)); + + m_model->clear(); + QCOMPARE(itemsInModel(), QStringList()); + QCOMPARE(spyItemsInserted.count(), 0); + QCOMPARE(spyItemsRemoved.count(), 1); + itemRangeList = spyItemsRemoved.takeFirst().at(0).value(); + QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(0, 8)); + + // Hiding hidden files makes the dir lister emit its itemsDeleted signal. + // Verify that this does not make the model crash. + m_model->setShowHiddenFiles(false); +} + +/** + * Verify that filtered items are removed when their parent is collapsed. + */ +void KFileItemModelTest::collapseParentOfHiddenItems() { - if (m_model->m_items.count() != m_model->m_itemData.count()) { - return false; + QSet modelRoles = m_model->roles(); + modelRoles << "isExpanded" << "isExpandable" << "expandedParentsCount"; + m_model->setRoles(modelRoles); + + QStringList files; + files << "a/1" << "a/b/1" << "a/b/c/1" << "a/b/c/d/1"; + m_testDir->createFiles(files); + + m_model->loadDirectory(m_testDir->url()); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(m_model->count(), 1); // Only "a/" + + // Expand "a/". + m_model->setExpanded(0, true); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(m_model->count(), 3); // 3 items: "a/", "a/b/", "a/1" + + // Expand "a/b/". + m_model->setExpanded(1, true); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(m_model->count(), 5); // 5 items: "a/", "a/b/", "a/b/c", "a/b/1", "a/1" + + // Expand "a/b/c/". + m_model->setExpanded(2, true); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(m_model->count(), 7); // 7 items: "a/", "a/b/", "a/b/c", "a/b/c/d/", "a/b/c/1", "a/b/1", "a/1" + + // Set a name filter that matches nothing -> only the expanded folders remain. + m_model->setNameFilter("xyz"); + QCOMPARE(m_model->count(), 3); + QCOMPARE(itemsInModel(), QStringList() << "a" << "b" << "c"); + + // Collapse the folder "a/". + QSignalSpy spyItemsRemoved(m_model, SIGNAL(itemsRemoved(KItemRangeList))); + m_model->setExpanded(0, false); + QCOMPARE(spyItemsRemoved.count(), 1); + QCOMPARE(m_model->count(), 1); + QCOMPARE(itemsInModel(), QStringList() << "a"); + + // Remove the filter -> no files should appear (and we should not get a crash). + m_model->setNameFilter(QString()); + QCOMPARE(m_model->count(), 1); +} + +/** + * Verify that filtered items are removed when their parent is deleted. + */ +void KFileItemModelTest::removeParentOfHiddenItems() +{ + QSet modelRoles = m_model->roles(); + modelRoles << "isExpanded" << "isExpandable" << "expandedParentsCount"; + m_model->setRoles(modelRoles); + + QStringList files; + files << "a/1" << "a/b/1" << "a/b/c/1" << "a/b/c/d/1"; + m_testDir->createFiles(files); + + m_model->loadDirectory(m_testDir->url()); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(m_model->count(), 1); // Only "a/" + + // Expand "a/". + m_model->setExpanded(0, true); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(m_model->count(), 3); // 3 items: "a/", "a/b/", "a/1" + + // Expand "a/b/". + m_model->setExpanded(1, true); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(m_model->count(), 5); // 5 items: "a/", "a/b/", "a/b/c", "a/b/1", "a/1" + + // Expand "a/b/c/". + m_model->setExpanded(2, true); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(m_model->count(), 7); // 7 items: "a/", "a/b/", "a/b/c", "a/b/c/d/", "a/b/c/1", "a/b/1", "a/1" + + // Set a name filter that matches nothing -> only the expanded folders remain. + m_model->setNameFilter("xyz"); + QCOMPARE(m_model->count(), 3); + QCOMPARE(itemsInModel(), QStringList() << "a" << "b" << "c"); + + // Simulate the deletion of the directory "a/b/". + QSignalSpy spyItemsRemoved(m_model, SIGNAL(itemsRemoved(KItemRangeList))); + m_model->slotItemsDeleted(KFileItemList() << m_model->fileItem(1)); + QCOMPARE(spyItemsRemoved.count(), 1); + QCOMPARE(m_model->count(), 1); + QCOMPARE(itemsInModel(), QStringList() << "a"); + + // Remove the filter -> only the file "a/1" should appear. + m_model->setNameFilter(QString()); + QCOMPARE(m_model->count(), 2); + QCOMPARE(itemsInModel(), QStringList() << "a" << "1"); +} + +/** + * Create a tree structure where parent-child relationships can not be + * determined by parsing the URLs, and verify that KFileItemModel + * handles them correctly. + */ +void KFileItemModelTest::testGeneralParentChildRelationships() +{ + QSet modelRoles = m_model->roles(); + modelRoles << "isExpanded" << "isExpandable" << "expandedParentsCount"; + m_model->setRoles(modelRoles); + + QStringList files; + files << "parent1/realChild1/realGrandChild1" << "parent2/realChild2/realGrandChild2"; + m_testDir->createFiles(files); + + m_model->loadDirectory(m_testDir->url()); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(itemsInModel(), QStringList() << "parent1" << "parent2"); + + // Expand all folders. + m_model->setExpanded(0, true); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "parent2"); + + m_model->setExpanded(1, true); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "realGrandChild1" << "parent2"); + + m_model->setExpanded(3, true); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "realGrandChild1" << "parent2" << "realChild2"); + + m_model->setExpanded(4, true); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "realGrandChild1" << "parent2" << "realChild2" << "realGrandChild2"); + + // Add some more children and grand-children. + const QUrl parent1 = m_model->fileItem(0).url(); + const QUrl parent2 = m_model->fileItem(3).url(); + const QUrl realChild1 = m_model->fileItem(1).url(); + const QUrl realChild2 = m_model->fileItem(4).url(); + + m_model->slotItemsAdded(parent1, KFileItemList() << KFileItem(QUrl("child1"), QString(), KFileItem::Unknown)); + m_model->slotCompleted(); + QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "realGrandChild1" << "child1" << "parent2" << "realChild2" << "realGrandChild2"); + + m_model->slotItemsAdded(parent2, KFileItemList() << KFileItem(QUrl("child2"), QString(), KFileItem::Unknown)); + m_model->slotCompleted(); + QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "realGrandChild1" << "child1" << "parent2" << "realChild2" << "realGrandChild2" << "child2"); + + m_model->slotItemsAdded(realChild1, KFileItemList() << KFileItem(QUrl("grandChild1"), QString(), KFileItem::Unknown)); + m_model->slotCompleted(); + QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "grandChild1" << "realGrandChild1" << "child1" << "parent2" << "realChild2" << "realGrandChild2" << "child2"); + + m_model->slotItemsAdded(realChild1, KFileItemList() << KFileItem(QUrl("grandChild1"), QString(), KFileItem::Unknown)); + m_model->slotCompleted(); + QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "grandChild1" << "realGrandChild1" << "child1" << "parent2" << "realChild2" << "realGrandChild2" << "child2"); + + m_model->slotItemsAdded(realChild2, KFileItemList() << KFileItem(QUrl("grandChild2"), QString(), KFileItem::Unknown)); + m_model->slotCompleted(); + QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "grandChild1" << "realGrandChild1" << "child1" << "parent2" << "realChild2" << "grandChild2" << "realGrandChild2" << "child2"); + + // Set a name filter that matches nothing -> only expanded folders remain. + QSignalSpy itemsRemovedSpy(m_model, SIGNAL(itemsRemoved(KItemRangeList))); + m_model->setNameFilter("xyz"); + QCOMPARE(itemsInModel(), QStringList() << "parent1" << "realChild1" << "parent2" << "realChild2"); + QCOMPARE(itemsRemovedSpy.count(), 1); + QList arguments = itemsRemovedSpy.takeFirst(); + KItemRangeList itemRangeList = arguments.at(0).value(); + QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(2, 3) << KItemRange(7, 3)); + + // Collapse "parent1". + m_model->setExpanded(0, false); + QCOMPARE(itemsInModel(), QStringList() << "parent1" << "parent2" << "realChild2"); + QCOMPARE(itemsRemovedSpy.count(), 1); + arguments = itemsRemovedSpy.takeFirst(); + itemRangeList = arguments.at(0).value(); + QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(1, 1)); + + // Remove "parent2". + m_model->slotItemsDeleted(KFileItemList() << m_model->fileItem(1)); + QCOMPARE(itemsInModel(), QStringList() << "parent1"); + QCOMPARE(itemsRemovedSpy.count(), 1); + arguments = itemsRemovedSpy.takeFirst(); + itemRangeList = arguments.at(0).value(); + QCOMPARE(itemRangeList, KItemRangeList() << KItemRange(1, 2)); + + // Clear filter, verify that no items reappear. + m_model->setNameFilter(QString()); + QCOMPARE(itemsInModel(), QStringList() << "parent1"); +} + +void KFileItemModelTest::testNameRoleGroups() +{ + QStringList files; + files << "b.txt" << "c.txt" << "d.txt" << "e.txt"; + + m_testDir->createFiles(files); + + m_model->setGroupedSorting(true); + m_model->loadDirectory(m_testDir->url()); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(itemsInModel(), QStringList() << "b.txt" << "c.txt" << "d.txt" << "e.txt"); + + QList > expectedGroups; + expectedGroups << QPair(0, QLatin1String("B")); + expectedGroups << QPair(1, QLatin1String("C")); + expectedGroups << QPair(2, QLatin1String("D")); + expectedGroups << QPair(3, QLatin1String("E")); + QCOMPARE(m_model->groups(), expectedGroups); + + // Rename d.txt to a.txt. + QHash data; + data.insert("text", "a.txt"); + m_model->setData(2, data); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsMoved(KItemRange,QList)), DefaultTimeout)); + QCOMPARE(itemsInModel(), QStringList() << "a.txt" << "b.txt" << "c.txt" << "e.txt"); + + expectedGroups.clear(); + expectedGroups << QPair(0, QLatin1String("A")); + expectedGroups << QPair(1, QLatin1String("B")); + expectedGroups << QPair(2, QLatin1String("C")); + expectedGroups << QPair(3, QLatin1String("E")); + QCOMPARE(m_model->groups(), expectedGroups); + + // Rename c.txt to d.txt. + data.insert("text", "d.txt"); + m_model->setData(2, data); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(groupsChanged()), DefaultTimeout)); + QCOMPARE(itemsInModel(), QStringList() << "a.txt" << "b.txt" << "d.txt" << "e.txt"); + + expectedGroups.clear(); + expectedGroups << QPair(0, QLatin1String("A")); + expectedGroups << QPair(1, QLatin1String("B")); + expectedGroups << QPair(2, QLatin1String("D")); + expectedGroups << QPair(3, QLatin1String("E")); + QCOMPARE(m_model->groups(), expectedGroups); + + // Change d.txt back to c.txt, but this time using the dir lister's refreshItems() signal. + const KFileItem fileItemD = m_model->fileItem(2); + KFileItem fileItemC = fileItemD; + QUrl urlC = fileItemC.url().adjusted(QUrl::RemoveFilename); + urlC.setPath(urlC.path() + "c.txt"); + fileItemC.setUrl(urlC); + + m_model->slotRefreshItems({qMakePair(fileItemD, fileItemC)}); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(groupsChanged()), DefaultTimeout)); + QCOMPARE(itemsInModel(), QStringList() << "a.txt" << "b.txt" << "c.txt" << "e.txt"); + + expectedGroups.clear(); + expectedGroups << QPair(0, QLatin1String("A")); + expectedGroups << QPair(1, QLatin1String("B")); + expectedGroups << QPair(2, QLatin1String("C")); + expectedGroups << QPair(3, QLatin1String("E")); + QCOMPARE(m_model->groups(), expectedGroups); +} + +void KFileItemModelTest::testNameRoleGroupsWithExpandedItems() +{ + QSet modelRoles = m_model->roles(); + modelRoles << "isExpanded" << "isExpandable" << "expandedParentsCount"; + m_model->setRoles(modelRoles); + + QStringList files; + files << "a/b.txt" << "a/c.txt" << "d/e.txt" << "d/f.txt"; + + m_testDir->createFiles(files); + + m_model->setGroupedSorting(true); + m_model->loadDirectory(m_testDir->url()); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(itemsInModel(), QStringList() << "a" << "d"); + + QList > expectedGroups; + expectedGroups << QPair(0, QLatin1String("A")); + expectedGroups << QPair(1, QLatin1String("D")); + QCOMPARE(m_model->groups(), expectedGroups); + + // Verify that expanding "a" and "d" will not change the groups (except for the index of "D"). + expectedGroups.clear(); + expectedGroups << QPair(0, QLatin1String("A")); + expectedGroups << QPair(3, QLatin1String("D")); + + m_model->setExpanded(0, true); + QVERIFY(m_model->isExpanded(0)); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(itemsInModel(), QStringList() << "a" << "b.txt" << "c.txt" << "d"); + QCOMPARE(m_model->groups(), expectedGroups); + + m_model->setExpanded(3, true); + QVERIFY(m_model->isExpanded(3)); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(itemsInModel(), QStringList() << "a" << "b.txt" << "c.txt" << "d" << "e.txt" << "f.txt"); + QCOMPARE(m_model->groups(), expectedGroups); +} + +void KFileItemModelTest::testInconsistentModel() +{ + QSet modelRoles = m_model->roles(); + modelRoles << "isExpanded" << "isExpandable" << "expandedParentsCount"; + m_model->setRoles(modelRoles); + + QStringList files; + files << "a/b/c1.txt" << "a/b/c2.txt"; + + m_testDir->createFiles(files); + + m_model->loadDirectory(m_testDir->url()); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(itemsInModel(), QStringList() << "a"); + + // Expand "a/" and "a/b/". + m_model->setExpanded(0, true); + QVERIFY(m_model->isExpanded(0)); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(itemsInModel(), QStringList() << "a" << "b"); + + m_model->setExpanded(1, true); + QVERIFY(m_model->isExpanded(1)); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(itemsInModel(), QStringList() << "a" << "b" << "c1.txt" << "c2.txt"); + + // Add the files "c1.txt" and "c2.txt" to the model also as top-level items. + // Such a thing can in principle happen when performing a search, and there + // are files which + // (a) match the search string, and + // (b) are children of a folder that matches the search string and is expanded. + // + // Note that the first item in the list of added items must be new (i.e., not + // in the model yet). Otherwise, KFileItemModel::slotItemsAdded() will see that + // it receives items that are in the model already and ignore them. + QUrl url(m_model->directory().url() + "/a2"); + KFileItem newItem(KFileItem::Unknown, KFileItem::Unknown, url); + + KFileItemList items; + items << newItem << m_model->fileItem(2) << m_model->fileItem(3); + m_model->slotItemsAdded(m_model->directory(), items); + m_model->slotCompleted(); + QCOMPARE(itemsInModel(), QStringList() << "a" << "b" << "c1.txt" << "c2.txt" << "a2" << "c1.txt" << "c2.txt"); + + m_model->setExpanded(0, false); + + // Test that the right items have been removed, see + // https://bugs.kde.org/show_bug.cgi?id=324371 + QCOMPARE(itemsInModel(), QStringList() << "a" << "a2" << "c1.txt" << "c2.txt"); + + // Test that resorting does not cause a crash, see + // https://bugs.kde.org/show_bug.cgi?id=325359 + // The crash is not 100% reproducible, but Valgrind will report an invalid memory access. + m_model->resortAllItems(); + +} + +void KFileItemModelTest::testChangeRolesForFilteredItems() +{ + QSet modelRoles = m_model->roles(); + modelRoles << "owner"; + m_model->setRoles(modelRoles); + + QStringList files; + files << "a.txt" << "aa.txt" << "aaa.txt"; + m_testDir->createFiles(files); + + m_model->loadDirectory(m_testDir->url()); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(itemsInModel(), QStringList() << "a.txt" << "aa.txt" << "aaa.txt"); + + for (int index = 0; index < m_model->count(); ++index) { + // All items should have the "text" and "owner" roles, but not "group". + QVERIFY(m_model->data(index).contains("text")); + QVERIFY(m_model->data(index).contains("owner")); + QVERIFY(!m_model->data(index).contains("group")); } - for (int i = 0; i < m_model->count(); ++i) { - const KFileItem item = m_model->fileItem(i); - if (item.isNull()) { - qWarning() << "Item" << i << "is null"; - return false; - } + // Add a filter, such that only "aaa.txt" remains in the model. + m_model->setNameFilter("aaa"); + QCOMPARE(itemsInModel(), QStringList() << "aaa.txt"); - const int itemIndex = m_model->index(item); - if (itemIndex != i) { - qWarning() << "Item" << i << "has a wrong index:" << itemIndex; - return false; - } + // Add the "group" role. + modelRoles << "group"; + m_model->setRoles(modelRoles); + + // Modify the filter, such that "aa.txt" reappears, and verify that all items have the expected roles. + m_model->setNameFilter("aa"); + QCOMPARE(itemsInModel(), QStringList() << "aa.txt" << "aaa.txt"); + + for (int index = 0; index < m_model->count(); ++index) { + // All items should have the "text", "owner", and "group" roles. + QVERIFY(m_model->data(index).contains("text")); + QVERIFY(m_model->data(index).contains("owner")); + QVERIFY(m_model->data(index).contains("group")); } - return true; + // Remove the "owner" role. + modelRoles.remove("owner"); + m_model->setRoles(modelRoles); + + // Clear the filter, and verify that all items have the expected roles + m_model->setNameFilter(QString()); + QCOMPARE(itemsInModel(), QStringList() << "a.txt" << "aa.txt" << "aaa.txt"); + + for (int index = 0; index < m_model->count(); ++index) { + // All items should have the "text" and "group" roles, but now "owner". + QVERIFY(m_model->data(index).contains("text")); + QVERIFY(!m_model->data(index).contains("owner")); + QVERIFY(m_model->data(index).contains("group")); + } +} + +void KFileItemModelTest::testChangeSortRoleWhileFiltering() +{ + KFileItemList items; + + KIO::UDSEntry entry; + entry.insert(KIO::UDSEntry::UDS_FILE_TYPE, 0100000); // S_IFREG might not be defined on non-Unix platforms. + entry.insert(KIO::UDSEntry::UDS_ACCESS, 07777); + entry.insert(KIO::UDSEntry::UDS_SIZE, 0); + entry.insert(KIO::UDSEntry::UDS_MODIFICATION_TIME, 0); + entry.insert(KIO::UDSEntry::UDS_GROUP, "group"); + entry.insert(KIO::UDSEntry::UDS_ACCESS_TIME, 0); + + entry.insert(KIO::UDSEntry::UDS_NAME, "a.txt"); + entry.insert(KIO::UDSEntry::UDS_USER, "user-b"); + items.append(KFileItem(entry, m_testDir->url(), false, true)); + + entry.insert(KIO::UDSEntry::UDS_NAME, "b.txt"); + entry.insert(KIO::UDSEntry::UDS_USER, "user-c"); + items.append(KFileItem(entry, m_testDir->url(), false, true)); + + entry.insert(KIO::UDSEntry::UDS_NAME, "c.txt"); + entry.insert(KIO::UDSEntry::UDS_USER, "user-a"); + items.append(KFileItem(entry, m_testDir->url(), false, true)); + + m_model->slotItemsAdded(m_testDir->url(), items); + m_model->slotCompleted(); + + QCOMPARE(itemsInModel(), QStringList() << "a.txt" << "b.txt" << "c.txt"); + + // Add a filter. + m_model->setNameFilter("a"); + QCOMPARE(itemsInModel(), QStringList() << "a.txt"); + + // Sort by "owner". + m_model->setSortRole("owner"); + + // Clear the filter, and verify that the items are sorted correctly. + m_model->setNameFilter(QString()); + QCOMPARE(itemsInModel(), QStringList() << "c.txt" << "a.txt" << "b.txt"); +} + +void KFileItemModelTest::testRefreshFilteredItems() +{ + QStringList files; + files << "a.txt" << "b.txt" << "c.jpg" << "d.jpg"; + m_testDir->createFiles(files); + + m_model->loadDirectory(m_testDir->url()); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(itemsInModel(), QStringList() << "a.txt" << "b.txt" << "c.jpg" << "d.jpg"); + + const KFileItem fileItemC = m_model->fileItem(2); + + // Show only the .txt files. + m_model->setNameFilter(".txt"); + QCOMPARE(itemsInModel(), QStringList() << "a.txt" << "b.txt"); + + // Rename one of the .jpg files. + KFileItem fileItemE = fileItemC; + QUrl urlE = fileItemE.url().adjusted(QUrl::RemoveFilename); + urlE.setPath(urlE.path() + "e.jpg"); + fileItemE.setUrl(urlE); + + m_model->slotRefreshItems({qMakePair(fileItemC, fileItemE)}); + + // Show all files again, and verify that the model has updated the file name. + m_model->setNameFilter(QString()); + QCOMPARE(itemsInModel(), QStringList() << "a.txt" << "b.txt" << "d.jpg" << "e.jpg"); +} + +void KFileItemModelTest::testCreateMimeData() +{ + QSet modelRoles = m_model->roles(); + modelRoles << "isExpanded" << "isExpandable" << "expandedParentsCount"; + m_model->setRoles(modelRoles); + + QStringList files; + files << "a/1"; + m_testDir->createFiles(files); + + m_model->loadDirectory(m_testDir->url()); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(itemsInModel(), QStringList() << "a"); + + // Expand "a/". + m_model->setExpanded(0, true); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(itemsInModel(), QStringList() << "a" << "1"); + + // Verify that creating the MIME data for a child of an expanded folder does + // not cause a crash, see https://bugs.kde.org/show_bug.cgi?id=329119 + KItemSet selection; + selection.insert(1); + QMimeData* mimeData = m_model->createMimeData(selection); + delete mimeData; +} + +void KFileItemModelTest::testCollapseFolderWhileLoading() +{ + QSet modelRoles = m_model->roles(); + modelRoles << "isExpanded" << "isExpandable" << "expandedParentsCount"; + m_model->setRoles(modelRoles); + + QStringList files; + files << "a2/b/c1.txt"; + m_testDir->createFiles(files); + + m_model->loadDirectory(m_testDir->url()); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(itemsInModel(), QStringList() << "a2"); + + // Expand "a2/". + m_model->setExpanded(0, true); + QVERIFY(m_model->isExpanded(0)); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(itemsInModel(), QStringList() << "a2" << "b"); + + // Expand "a2/b/". + m_model->setExpanded(1, true); + QVERIFY(m_model->isExpanded(1)); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(itemsInModel(), QStringList() << "a2" << "b" << "c1.txt"); + + // Simulate that a new item "c2.txt" appears, but that the dir lister's completed() + // signal is not emitted yet. + const KFileItem fileItemC1 = m_model->fileItem(2); + KFileItem fileItemC2 = fileItemC1; + QUrl urlC2 = fileItemC2.url(); + urlC2.adjusted(QUrl::RemoveFilename); + urlC2.setPath(urlC2.path() + "c2.txt"); + fileItemC2.setUrl(urlC2); + + const QUrl urlB = m_model->fileItem(1).url(); + m_model->slotItemsAdded(urlB, KFileItemList() << fileItemC2); + QCOMPARE(itemsInModel(), QStringList() << "a2" << "b" << "c1.txt"); + + // Collapse "a2/". This should also remove all its (indirect) children from + // the model and from the model's m_pendingItemsToInsert member. + m_model->setExpanded(0, false); + QCOMPARE(itemsInModel(), QStringList() << "a2"); + + // Simulate that the dir lister's completed() signal is emitted. If "c2.txt" + // is still in m_pendingItemsToInsert, then we might get a crash, see + // https://bugs.kde.org/show_bug.cgi?id=332102. Even if the crash is not + // reproducible here, Valgrind will complain, and the item "c2.txt" will appear + // without parent in the model. + m_model->slotCompleted(); + QCOMPARE(itemsInModel(), QStringList() << "a2"); + + // Expand "a2/" again. + m_model->setExpanded(0, true); + QVERIFY(m_model->isExpanded(0)); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(itemsInModel(), QStringList() << "a2" << "b"); + + // Now simulate that a new folder "a1/" is appears, but that the dir lister's + // completed() signal is not emitted yet. + const KFileItem fileItemA2 = m_model->fileItem(0); + KFileItem fileItemA1 = fileItemA2; + QUrl urlA1 = fileItemA1.url().adjusted(QUrl::RemoveFilename); + urlA1.setPath(urlA1.path() + "a1"); + fileItemA1.setUrl(urlA1); + + m_model->slotItemsAdded(m_model->directory(), KFileItemList() << fileItemA1); + QCOMPARE(itemsInModel(), QStringList() << "a2" << "b"); + + // Collapse "a2/". Note that this will cause "a1/" to be added to the model, + // i.e., the index of "a2/" will change from 0 to 1. Check that this does not + // confuse the code which collapses the folder. + m_model->setExpanded(0, false); + QCOMPARE(itemsInModel(), QStringList() << "a1" << "a2"); + QVERIFY(!m_model->isExpanded(0)); + QVERIFY(!m_model->isExpanded(1)); +} + +void KFileItemModelTest::testDeleteFileMoreThanOnce() +{ + QStringList files; + files << "a.txt" << "b.txt" << "c.txt" << "d.txt"; + m_testDir->createFiles(files); + + m_model->loadDirectory(m_testDir->url()); + QVERIFY(QTest::kWaitForSignal(m_model, SIGNAL(itemsInserted(KItemRangeList)), DefaultTimeout)); + QCOMPARE(itemsInModel(), QStringList() << "a.txt" << "b.txt" << "c.txt" << "d.txt"); + + const KFileItem fileItemB = m_model->fileItem(1); + + // Tell the model that a list of items has been deleted, where "b.txt" appears twice in the list. + KFileItemList list; + list << fileItemB << fileItemB; + m_model->slotItemsDeleted(list); + + QVERIFY(m_model->isConsistent()); + QCOMPARE(itemsInModel(), QStringList() << "a.txt" << "c.txt" << "d.txt"); } QStringList KFileItemModelTest::itemsInModel() const { QStringList items; for (int i = 0; i < m_model->count(); i++) { - items << m_model->data(i).value("name").toString(); + items << m_model->fileItem(i).text(); } return items; }