// Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include #include #include #include #include #ifdef QT_BUILD_INTERNAL #include #endif #include #include #include #include #include #include #include #include #include #include #if defined(Q_OS_WIN) # include // for SetFileAttributes #endif #include #include using namespace Qt::StringLiterals; #define WAITTIME 1000 // Will try to wait for the condition while allowing event processing // for a maximum of 5 seconds. #define TRY_WAIT(expr, timedOut) \ do { \ *timedOut = true; \ const int step = 50; \ for (int __i = 0; __i < 5000; __i += step) { \ if (expr) { \ *timedOut = false; \ break; \ } \ QTest::qWait(step); \ } \ } while(0) Q_DECLARE_METATYPE(QDir::Filters) Q_DECLARE_METATYPE(QFileDevice::Permissions) Q_LOGGING_CATEGORY(lcFileSystemModel, "qt.widgets.tests.qfilesystemmodel") class tst_QFileSystemModel : public QObject { Q_OBJECT private slots: void initTestCase(); void cleanup(); void indexPath(); void rootPath(); void readOnly(); void iconProvider(); void rowCount(); void rowsInserted_data(); void rowsInserted(); void rowsRemoved_data(); void rowsRemoved(); void dataChanged_data(); void dataChanged(); void filters_data(); void filters(); void nameFilters(); void setData_data(); void setData(); void sortPersistentIndex(); void sort_data(); void sort(); void mkdir(); void deleteFile(); void deleteDirectory(); void caseSensitivity(); void drives_data(); void drives(); void dirsBeforeFiles(); void roleNames_data(); void roleNames(); void permissions_data(); void permissions(); void doNotUnwatchOnFailedRmdir(); void specialFiles(); void fileInfo(); protected: bool createFiles(QFileSystemModel *model, const QString &test_path, const QStringList &initial_files, int existingFileCount = 0, const QStringList &initial_dirs = QStringList()); QModelIndex prepareTestModelRoot(QFileSystemModel *model, const QString &test_path, QSignalSpy **spy2 = nullptr, QSignalSpy **spy3 = nullptr); private: QString flatDirTestPath; QTemporaryDir m_tempDir; }; void tst_QFileSystemModel::cleanup() { QDir dir(flatDirTestPath); if (dir.exists()) { const QDir::Filters filters = QDir::AllEntries | QDir::System | QDir::Hidden | QDir::NoDotAndDotDot; const QFileInfoList list = dir.entryInfoList(filters); for (const QFileInfo &fi : list) { if (fi.isDir()) { QVERIFY(dir.rmdir(fi.fileName())); } else { QFile dead(fi.absoluteFilePath()); dead.setPermissions(QFile::ReadUser | QFile::ReadOwner | QFile::ExeOwner | QFile::ExeUser | QFile::WriteUser | QFile::WriteOwner | QFile::WriteOther); QVERIFY(dead.remove()); } } QVERIFY(dir.entryInfoList(filters).isEmpty()); } } void tst_QFileSystemModel::initTestCase() { QVERIFY2(m_tempDir.isValid(), qPrintable(m_tempDir.errorString())); flatDirTestPath = m_tempDir.path(); } void tst_QFileSystemModel::indexPath() { #if !defined(Q_OS_WIN) QScopedPointer model(new QFileSystemModel); QAbstractItemModelTester tester(model.get()); tester.setUseFetchMore(false); int depth = QDir::currentPath().count('/'); model->setRootPath(QDir::currentPath()); QString backPath; for (int i = 0; i <= depth * 2 + 1; ++i) { backPath += "../"; QModelIndex idx = model->index(backPath); QVERIFY(i != depth - 1 ? idx.isValid() : !idx.isValid()); } #endif } void tst_QFileSystemModel::rootPath() { QScopedPointer model(new QFileSystemModel); QAbstractItemModelTester tester(model.get()); tester.setUseFetchMore(false); QCOMPARE(model->rootPath(), QString(QDir().path())); QSignalSpy rootChanged(model.data(), &QFileSystemModel::rootPathChanged); QModelIndex root = model->setRootPath(model->rootPath()); root = model->setRootPath("this directory shouldn't exist"); QCOMPARE(rootChanged.size(), 0); QString oldRootPath = model->rootPath(); const QStringList documentPaths = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation); QVERIFY(!documentPaths.isEmpty()); QString documentPath = documentPaths.front(); // In particular on Linux, ~/Documents (the first // DocumentsLocation) may not exist, so choose ~ in that case: if (!QFile::exists(documentPath)) { documentPath = QDir::homePath(); qWarning("%s: first documentPath \"%s\" does not exist. Using ~ (\"%s\") instead.", Q_FUNC_INFO, qPrintable(documentPaths.front()), qPrintable(documentPath)); } root = model->setRootPath(documentPath); QTRY_VERIFY(model->rowCount(root) >= 0); QCOMPARE(model->rootPath(), QString(documentPath)); QCOMPARE(rootChanged.size(), oldRootPath == model->rootPath() ? 0 : 1); QCOMPARE(model->rootDirectory().absolutePath(), documentPath); model->setRootPath(QDir::rootPath()); int oldCount = rootChanged.size(); oldRootPath = model->rootPath(); root = model->setRootPath(documentPath + QLatin1String("/.")); QTRY_VERIFY(model->rowCount(root) >= 0); QCOMPARE(model->rootPath(), documentPath); QCOMPARE(rootChanged.size(), oldRootPath == model->rootPath() ? oldCount : oldCount + 1); QCOMPARE(model->rootDirectory().absolutePath(), documentPath); QDir newdir = documentPath; if (newdir.cdUp()) { oldCount = rootChanged.size(); oldRootPath = model->rootPath(); root = model->setRootPath(documentPath + QLatin1String("/..")); QTRY_VERIFY(model->rowCount(root) >= 0); QCOMPARE(model->rootPath(), newdir.path()); QCOMPARE(rootChanged.size(), oldCount + 1); QCOMPARE(model->rootDirectory().absolutePath(), newdir.path()); } #ifdef Q_OS_WIN // check case insensitive root node on windows, tests QTBUG-71701 QModelIndex index = model->setRootPath(uR"(\\localhost\c$)"_s); QVERIFY(index.isValid()); QCOMPARE(model->rootPath(), u"//localhost/c$"_s); index = model->setRootPath(uR"(\\localhost\C$)"_s); QVERIFY(index.isValid()); QCOMPARE(model->rootPath(), u"//localhost/C$"_s); index = model->setRootPath(uR"(\\LOCALHOST\C$)"_s); QVERIFY(index.isValid()); QCOMPARE(model->rootPath(), u"//LOCALHOST/C$"_s); #endif } void tst_QFileSystemModel::readOnly() { QScopedPointer model(new QFileSystemModel); QAbstractItemModelTester tester(model.get()); tester.setUseFetchMore(false); QCOMPARE(model->isReadOnly(), true); QTemporaryFile file(flatDirTestPath + QStringLiteral("/XXXXXX.dat")); QVERIFY2(file.open(), qPrintable(file.errorString())); const QString fileName = file.fileName(); file.close(); const QFileInfo fileInfo(fileName); QTRY_VERIFY(QDir(flatDirTestPath).entryInfoList().contains(fileInfo)); QModelIndex root = model->setRootPath(flatDirTestPath); QTRY_VERIFY(model->rowCount(root) > 0); // ItemIsEditable should change, ItemNeverHasChildren should not change QVERIFY(!(model->flags(model->index(fileName)) & Qt::ItemIsEditable)); QVERIFY(model->flags(model->index(fileName)) & Qt::ItemNeverHasChildren); model->setReadOnly(false); QCOMPARE(model->isReadOnly(), false); QVERIFY(model->flags(model->index(fileName)) & Qt::ItemIsEditable); QVERIFY(model->flags(model->index(fileName)) & Qt::ItemNeverHasChildren); } class CustomFileIconProvider : public QFileIconProvider { public: CustomFileIconProvider() : QFileIconProvider() { auto style = QApplication::style(); mb = style->standardIcon(QStyle::SP_MessageBoxCritical); dvd = style->standardIcon(QStyle::SP_DriveDVDIcon); } QIcon icon(const QFileInfo &info) const override { if (info.isDir()) return mb; return QFileIconProvider::icon(info); } QIcon icon(IconType type) const override { if (type == QFileIconProvider::Folder) return dvd; return QFileIconProvider::icon(type); } private: QIcon mb; QIcon dvd; }; void tst_QFileSystemModel::iconProvider() { QScopedPointer model(new QFileSystemModel); QAbstractItemModelTester tester(model.get()); tester.setUseFetchMore(false); QVERIFY(model->iconProvider()); QScopedPointer provider(new QFileIconProvider); model->setIconProvider(provider.data()); QCOMPARE(model->iconProvider(), provider.data()); model->setIconProvider(nullptr); provider.reset(); QScopedPointer myModel(new QFileSystemModel); const QStringList documentPaths = QStandardPaths::standardLocations(QStandardPaths::DocumentsLocation); QVERIFY(!documentPaths.isEmpty()); myModel->setRootPath(documentPaths.constFirst()); //We change the provider, icons must be updated provider.reset(new CustomFileIconProvider); myModel->setIconProvider(provider.data()); QPixmap mb = QApplication::style()->standardIcon(QStyle::SP_MessageBoxCritical).pixmap(50, 50); QCOMPARE(myModel->fileIcon(myModel->index(QDir::homePath())).pixmap(50, 50), mb); } bool tst_QFileSystemModel::createFiles(QFileSystemModel *model, const QString &test_path, const QStringList &initial_files, int existingFileCount, const QStringList &initial_dirs) { qCDebug(lcFileSystemModel) << (model->rowCount(model->index(test_path))) << existingFileCount << initial_files; bool timedOut = false; TRY_WAIT((model->rowCount(model->index(test_path)) == existingFileCount), &timedOut); if (timedOut) return false; QDir dir(test_path); if (!dir.exists()) { qWarning() << "error" << test_path << "doesn't exist"; return false; } for (const auto &initial_dir : initial_dirs) { if (!dir.mkdir(initial_dir)) { qWarning() << "error" << "failed to make" << initial_dir; return false; } qCDebug(lcFileSystemModel) << test_path + '/' + initial_dir << (QFile::exists(test_path + '/' + initial_dir)); } for (const auto &initial_file : initial_files) { QFile file(test_path + '/' + initial_file); if (!file.open(QIODevice::WriteOnly | QIODevice::Append)) { qDebug() << "failed to open file" << initial_file; return false; } if (!file.resize(1024 + file.size())) { qDebug() << "failed to resize file" << initial_file; return false; } if (!file.flush()) { qDebug() << "failed to flush file" << initial_file; return false; } file.close(); #if defined(Q_OS_WIN) if (initial_file[0] == '.') { const QString hiddenFile = QDir::toNativeSeparators(file.fileName()); const auto nativeHiddenFile = reinterpret_cast(hiddenFile.utf16()); DWORD currentAttributes = ::GetFileAttributes(nativeHiddenFile); if (currentAttributes == 0xFFFFFFFF) { qErrnoWarning("failed to get file attributes: %s", qPrintable(hiddenFile)); return false; } if (!::SetFileAttributes(nativeHiddenFile, currentAttributes | FILE_ATTRIBUTE_HIDDEN)) { qErrnoWarning("failed to set file hidden: %s", qPrintable(hiddenFile)); return false; } } #endif qCDebug(lcFileSystemModel) << test_path + '/' + initial_file << (QFile::exists(test_path + '/' + initial_file)); } return true; } QModelIndex tst_QFileSystemModel::prepareTestModelRoot(QFileSystemModel *model, const QString &test_path, QSignalSpy **spy2, QSignalSpy **spy3) { if (model->rowCount(model->index(test_path)) != 0) return QModelIndex(); if (spy2) *spy2 = new QSignalSpy(model, &QFileSystemModel::rowsInserted); if (spy3) *spy3 = new QSignalSpy(model, &QFileSystemModel::rowsAboutToBeInserted); QStringList files = { "b", "d", "f", "h", "j", ".a", ".c", ".e", ".g" }; if (!createFiles(model, test_path, files)) return QModelIndex(); QModelIndex root = model->setRootPath(test_path); if (!root.isValid()) return QModelIndex(); bool timedOut = false; TRY_WAIT(model->rowCount(root) == 5, &timedOut); if (timedOut) return QModelIndex(); return root; } void tst_QFileSystemModel::rowCount() { QSignalSpy *spy2 = nullptr; QSignalSpy *spy3 = nullptr; QScopedPointer model(new QFileSystemModel); QAbstractItemModelTester tester(model.get()); tester.setUseFetchMore(false); QModelIndex root = prepareTestModelRoot(model.data(), flatDirTestPath, &spy2, &spy3); QVERIFY(root.isValid()); QVERIFY(spy2 && spy2->size() > 0); QVERIFY(spy3 && spy3->size() > 0); } void tst_QFileSystemModel::rowsInserted_data() { QTest::addColumn("count"); QTest::addColumn("ascending"); for (int i = 0; i < 4; ++i) { const QByteArray iB = QByteArray::number(i); QTest::newRow(("Qt::AscendingOrder " + iB).constData()) << i << Qt::AscendingOrder; QTest::newRow(("Qt::DescendingOrder " + iB).constData()) << i << Qt::DescendingOrder; } } static inline QString lastEntry(const QModelIndex &root) { const QAbstractItemModel *model = root.model(); return model->index(model->rowCount(root) - 1, 0, root).data().toString(); } void tst_QFileSystemModel::rowsInserted() { const QString tmp = flatDirTestPath; QScopedPointer model(new QFileSystemModel); QAbstractItemModelTester tester(model.get()); tester.setUseFetchMore(false); QModelIndex root = prepareTestModelRoot(model.data(), tmp); QVERIFY(root.isValid()); QFETCH(Qt::SortOrder, ascending); QFETCH(int, count); model->sort(0, ascending); QSignalSpy spy0(model.data(), &QAbstractItemModel::rowsInserted); QSignalSpy spy1(model.data(), &QAbstractItemModel::rowsAboutToBeInserted); int oldCount = model->rowCount(root); QStringList files; for (int i = 0; i < count; ++i) files.append(QLatin1Char('c') + QString::number(i)); QVERIFY(createFiles(model.data(), tmp, files, 5)); QTRY_COMPARE(model->rowCount(root), oldCount + count); int totalRowsInserted = 0; for (int i = 0; i < spy0.size(); ++i) { int start = spy0[i].value(1).toInt(); int end = spy0[i].value(2).toInt(); totalRowsInserted += end - start + 1; } QCOMPARE(totalRowsInserted, count); const QString expected = ascending == Qt::AscendingOrder ? QStringLiteral("j") : QStringLiteral("b"); QTRY_COMPARE(lastEntry(root), expected); if (spy0.size() > 0) { if (count == 0) QCOMPARE(spy0.size(), 0); else QVERIFY(spy0.size() >= 1); } if (count == 0) QCOMPARE(spy1.size(), 0); else QVERIFY(spy1.size() >= 1); QVERIFY(createFiles(model.data(), tmp, QStringList(".hidden_file"), 5 + count)); if (count != 0) QTRY_VERIFY(spy0.size() >= 1); else QTRY_COMPARE(spy0.size(), 0); if (count != 0) QTRY_VERIFY(spy1.size() >= 1); else QTRY_COMPARE(spy1.size(), 0); } void tst_QFileSystemModel::rowsRemoved_data() { rowsInserted_data(); } void tst_QFileSystemModel::rowsRemoved() { const QString tmp = flatDirTestPath; QScopedPointer model(new QFileSystemModel); QAbstractItemModelTester tester(model.get()); tester.setUseFetchMore(false); QModelIndex root = prepareTestModelRoot(model.data(), tmp); QVERIFY(root.isValid()); QFETCH(int, count); QFETCH(Qt::SortOrder, ascending); model->sort(0, ascending); QSignalSpy spy0(model.data(), &QAbstractItemModel::rowsRemoved); QSignalSpy spy1(model.data(), &QAbstractItemModel::rowsAboutToBeRemoved); int oldCount = model->rowCount(root); for (int i = count - 1; i >= 0; --i) { const QString fileName = model->index(i, 0, root).data().toString(); qCDebug(lcFileSystemModel) << "removing" << fileName; QVERIFY(QFile::remove(tmp + QLatin1Char('/') + fileName)); } for (int i = 0 ; i < 10; ++i) { if (count != 0) { if (i == 10 || spy0.size() != 0) { QVERIFY(spy0.size() >= 1); QVERIFY(spy1.size() >= 1); } } else { if (i == 10 || spy0.size() == 0) { QCOMPARE(spy0.size(), 0); QCOMPARE(spy1.size(), 0); } } QStringList lst; for (int i = 0; i < model->rowCount(root); ++i) lst.append(model->index(i, 0, root).data().toString()); if (model->rowCount(root) == oldCount - count) break; qCDebug(lcFileSystemModel) << "still have:" << lst << QFile::exists(tmp + QLatin1String("/.a")); QDir tmpLister(tmp); qCDebug(lcFileSystemModel) << tmpLister.entryList(); } QTRY_COMPARE(model->rowCount(root), oldCount - count); QVERIFY(QFile::exists(tmp + QLatin1String("/.a"))); QVERIFY(QFile::remove(tmp + QLatin1String("/.a"))); QVERIFY(QFile::remove(tmp + QLatin1String("/.c"))); if (count != 0) { QVERIFY(spy0.size() >= 1); QVERIFY(spy1.size() >= 1); } else { QCOMPARE(spy0.size(), 0); QCOMPARE(spy1.size(), 0); } } void tst_QFileSystemModel::dataChanged_data() { rowsInserted_data(); } void tst_QFileSystemModel::dataChanged() { QSKIP("This can't be tested right now since we don't watch files, only directories."); const QString tmp = flatDirTestPath; QScopedPointer model(new QFileSystemModel); QAbstractItemModelTester tester(model.get()); tester.setUseFetchMore(false); QModelIndex root = prepareTestModelRoot(model.data(), tmp); QVERIFY(root.isValid()); QFETCH(int, count); QFETCH(Qt::SortOrder, ascending); model->sort(0, ascending); QSignalSpy spy(model.data(), &QAbstractItemModel::dataChanged); QStringList files; for (int i = 0; i < count; ++i) files.append(model->index(i, 0, root).data().toString()); createFiles(model.data(), tmp, files); QTest::qWait(WAITTIME); if (count != 0) QVERIFY(spy.size() >= 1); else QCOMPARE(spy.size(), 0); } void tst_QFileSystemModel::filters_data() { QTest::addColumn("files"); QTest::addColumn("dirs"); QTest::addColumn("dirFilters"); QTest::addColumn("nameFilters"); QTest::addColumn("rowCount"); const QStringList abcList{QLatin1String("a"), QLatin1String("b"), QLatin1String("c")}; const QStringList zList{QLatin1String("Z")}; QTest::newRow("no dirs") << abcList << QStringList() << QDir::Filters(QDir::Dirs) << QStringList() << 2; QTest::newRow("no dirs - dot") << abcList << QStringList() << (QDir::Dirs | QDir::NoDot) << QStringList() << 1; QTest::newRow("no dirs - dotdot") << abcList << QStringList() << (QDir::Dirs | QDir::NoDotDot) << QStringList() << 1; QTest::newRow("no dirs - dotanddotdot") << abcList << QStringList() << (QDir::Dirs | QDir::NoDotAndDotDot) << QStringList() << 0; QTest::newRow("one dir - dot") << abcList << zList << (QDir::Dirs | QDir::NoDot) << QStringList() << 2; QTest::newRow("one dir - dotdot") << abcList << zList << (QDir::Dirs | QDir::NoDotDot) << QStringList() << 2; QTest::newRow("one dir - dotanddotdot") << abcList << zList << (QDir::Dirs | QDir::NoDotAndDotDot) << QStringList() << 1; QTest::newRow("one dir") << abcList << zList << QDir::Filters(QDir::Dirs) << QStringList() << 3; QTest::newRow("no dir + hidden") << abcList << QStringList() << (QDir::Dirs | QDir::Hidden) << QStringList() << 2; QTest::newRow("dir+hid+files") << abcList << QStringList() << (QDir::Dirs | QDir::Files | QDir::Hidden) << QStringList() << 5; QTest::newRow("dir+file+hid-dot .A") << abcList << QStringList{QLatin1String(".A")} << (QDir::Dirs | QDir::Files | QDir::Hidden | QDir::NoDotAndDotDot) << QStringList() << 4; QTest::newRow("dir+files+hid+dot A") << abcList << QStringList{QLatin1String("AFolder")} << (QDir::Dirs | QDir::Files | QDir::Hidden | QDir::NoDotAndDotDot) << QStringList{QLatin1String("A*")} << 2; QTest::newRow("dir+files+hid+dot+cas1") << abcList << zList << (QDir::Dirs | QDir::Files | QDir::Hidden | QDir::NoDotAndDotDot | QDir::CaseSensitive) << zList << 1; QTest::newRow("dir+files+hid+dot+cas2") << abcList << zList << (QDir::Dirs | QDir::Files | QDir::Hidden | QDir::NoDotAndDotDot | QDir::CaseSensitive) << QStringList{QLatin1String("a")} << 1; QTest::newRow("dir+files+hid+dot+cas+alldir") << abcList << zList << (QDir::Dirs | QDir::Files | QDir::Hidden | QDir::NoDotAndDotDot | QDir::CaseSensitive | QDir::AllDirs) << zList << 1; QTest::newRow("case sensitive") << QStringList{QLatin1String("Antiguagdb"), QLatin1String("Antiguamtd"), QLatin1String("Antiguamtp"), QLatin1String("afghanistangdb"), QLatin1String("afghanistanmtd")} << QStringList() << QDir::Filters(QDir::Files) << QStringList() << 5; } void tst_QFileSystemModel::filters() { QString tmp = flatDirTestPath; QScopedPointer model(new QFileSystemModel); QAbstractItemModelTester tester(model.get()); tester.setUseFetchMore(false); QVERIFY(createFiles(model.data(), tmp, QStringList())); QModelIndex root = model->setRootPath(tmp); QFETCH(QStringList, files); QFETCH(QStringList, dirs); QFETCH(QDir::Filters, dirFilters); QFETCH(QStringList, nameFilters); QFETCH(int, rowCount); if (nameFilters.size() > 0) model->setNameFilters(nameFilters); model->setNameFilterDisables(false); model->setFilter(dirFilters); QVERIFY(createFiles(model.data(), tmp, files, 0, dirs)); QTRY_COMPARE(model->rowCount(root), rowCount); // Make sure that we do what QDir does QDir xFactor(tmp); QStringList dirEntries; if (nameFilters.size() > 0) dirEntries = xFactor.entryList(nameFilters, dirFilters); else dirEntries = xFactor.entryList(dirFilters); QCOMPARE(dirEntries.size(), rowCount); QStringList modelEntries; for (int i = 0; i < rowCount; ++i) modelEntries.append(model->data(model->index(i, 0, root), QFileSystemModel::FileNameRole).toString()); std::sort(dirEntries.begin(), dirEntries.end()); std::sort(modelEntries.begin(), modelEntries.end()); QCOMPARE(dirEntries, modelEntries); #ifdef Q_OS_LINUX if (files.size() >= 3 && rowCount >= 3 && rowCount != 5) { QString fileName1 = (tmp + '/' + files.at(0)); QString fileName2 = (tmp + '/' + files.at(1)); QString fileName3 = (tmp + '/' + files.at(2)); QFile::Permissions originalPermissions = QFile::permissions(fileName1); QVERIFY(QFile::setPermissions(fileName1, QFile::WriteOwner)); QVERIFY(QFile::setPermissions(fileName2, QFile::ReadOwner)); QVERIFY(QFile::setPermissions(fileName3, QFile::ExeOwner)); model->setFilter((QDir::Files | QDir::Readable)); QTRY_COMPARE(model->rowCount(root), 1); model->setFilter((QDir::Files | QDir::Writable)); QTRY_COMPARE(model->rowCount(root), 1); model->setFilter((QDir::Files | QDir::Executable)); QTRY_COMPARE(model->rowCount(root), 1); // reset permissions QVERIFY(QFile::setPermissions(fileName1, originalPermissions)); QVERIFY(QFile::setPermissions(fileName2, originalPermissions)); QVERIFY(QFile::setPermissions(fileName3, originalPermissions)); } #endif } void tst_QFileSystemModel::nameFilters() { QStringList list; list << "a" << "b" << "c"; QScopedPointer model(new QFileSystemModel); QAbstractItemModelTester tester(model.get()); tester.setUseFetchMore(false); model->setNameFilters(list); model->setNameFilterDisables(false); QCOMPARE(model->nameFilters(), list); QString tmp = flatDirTestPath; QVERIFY(createFiles(model.data(), tmp, list)); QModelIndex root = model->setRootPath(tmp); QTRY_COMPARE(model->rowCount(root), 3); QStringList filters; filters << "a" << "b"; model->setNameFilters(filters); QTRY_COMPARE(model->rowCount(root), 2); } void tst_QFileSystemModel::setData_data() { QTest::addColumn("subdirName"); QTest::addColumn("files"); QTest::addColumn("oldFileName"); QTest::addColumn("newFileName"); QTest::addColumn("success"); /*QTest::newRow("outside current dir") << (QStringList() << "a" << "b" << "c") << flatDirTestPath + '/' + "a" << QDir::temp().absolutePath() + '/' + "a" << false; */ const QStringList abcList{QLatin1String("a"), QLatin1String("b"), QLatin1String("c")}; QTest::newRow("in current dir") << QString() << abcList << "a" << "d" << true; QTest::newRow("in subdir") << "s" << abcList << "a" << "d" << true; } void tst_QFileSystemModel::setData() { QScopedPointer model(new QFileSystemModel); QAbstractItemModelTester tester(model.get()); tester.setUseFetchMore(false); QSignalSpy spy(model.data(), &QFileSystemModel::fileRenamed); QFETCH(QString, subdirName); QFETCH(QStringList, files); QFETCH(QString, oldFileName); QFETCH(QString, newFileName); QFETCH(bool, success); QString tmp = flatDirTestPath; if (!subdirName.isEmpty()) { QDir dir(tmp); QVERIFY(dir.mkdir(subdirName)); tmp.append('/' + subdirName); } QVERIFY(createFiles(model.data(), tmp, files)); QModelIndex tmpIdx = model->setRootPath(flatDirTestPath); if (!subdirName.isEmpty()) { tmpIdx = model->index(tmp); model->fetchMore(tmpIdx); } QTRY_COMPARE(model->rowCount(tmpIdx), files.size()); QModelIndex idx = model->index(tmp + '/' + oldFileName); QCOMPARE(idx.isValid(), true); QCOMPARE(model->setData(idx, newFileName), false); model->setReadOnly(false); QCOMPARE(model->setData(idx, newFileName), success); model->setReadOnly(true); if (success) { QCOMPARE(spy.size(), 1); QList arguments = spy.takeFirst(); QCOMPARE(model->data(idx, QFileSystemModel::FileNameRole).toString(), newFileName); QCOMPARE(model->fileInfo(idx).filePath(), tmp + '/' + newFileName); QCOMPARE(model->index(arguments.at(0).toString()), model->index(tmp)); QCOMPARE(arguments.at(1).toString(), oldFileName); QCOMPARE(arguments.at(2).toString(), newFileName); QCOMPARE(QFile::rename(tmp + '/' + newFileName, tmp + '/' + oldFileName), true); } QTRY_COMPARE(model->rowCount(tmpIdx), files.size()); // cleanup if (!subdirName.isEmpty()) QVERIFY(QDir(tmp).removeRecursively()); } void tst_QFileSystemModel::sortPersistentIndex() { QTemporaryFile file(flatDirTestPath + QStringLiteral("/XXXXXX.dat")); QVERIFY2(file.open(), qPrintable(file.errorString())); const QFileInfo fileInfo(file.fileName()); file.close(); QTRY_VERIFY(QDir(flatDirTestPath).entryInfoList().contains(fileInfo)); QScopedPointer model(new QFileSystemModel); QAbstractItemModelTester tester(model.get()); tester.setUseFetchMore(false); QModelIndex root = model->setRootPath(flatDirTestPath); QTRY_VERIFY(model->rowCount(root) > 0); QPersistentModelIndex idx = model->index(0, 1, root); model->sort(0, Qt::AscendingOrder); model->sort(0, Qt::DescendingOrder); QVERIFY(idx.column() != 0); } class MyFriendFileSystemModel : public QFileSystemModel { friend class tst_QFileSystemModel; Q_DECLARE_PRIVATE(QFileSystemModel) }; void tst_QFileSystemModel::sort_data() { QTest::addColumn("fileDialogMode"); QTest::newRow("standard usage") << false; QTest::newRow("QFileDialog usage") << true; } void tst_QFileSystemModel::sort() { QFETCH(bool, fileDialogMode); QScopedPointer myModel(new MyFriendFileSystemModel); QTreeView tree; tree.setWindowTitle(QTest::currentTestFunction()); if (fileDialogMode && QTestPrivate::isRunningArmOnX86()) QSKIP("Crashes in QEMU. QTBUG-70572"); #ifdef QT_BUILD_INTERNAL if (fileDialogMode) myModel->d_func()->disableRecursiveSort = true; #endif QDir dir(flatDirTestPath); const QString dirPath = dir.absolutePath(); //Create a file that will be at the end when sorting by name (For Mac, the default) //but if we sort by size descending it will be the first QFile tempFile(dirPath + "/plop2.txt"); tempFile.open(QIODevice::WriteOnly | QIODevice::Text); QTextStream out(&tempFile); out << "The magic number is: " << 49 << "\n"; tempFile.close(); QFile tempFile2(dirPath + "/plop.txt"); tempFile2.open(QIODevice::WriteOnly | QIODevice::Text); QTextStream out2(&tempFile2); out2 << "The magic number is : " << 49 << " but i write some stuff in the file \n"; tempFile2.close(); myModel->setRootPath(""); myModel->setFilter(QDir::AllEntries | QDir::System | QDir::Hidden); tree.setSortingEnabled(true); tree.setModel(myModel.data()); tree.show(); tree.resize(800, 800); QVERIFY(QTest::qWaitForWindowExposed(&tree)); tree.header()->setSortIndicator(1, Qt::DescendingOrder); tree.header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); QStringList dirsToOpen; do { dirsToOpen << dir.absolutePath(); } while (dir.cdUp()); for (int i = dirsToOpen.size() -1 ; i > 0 ; --i) { QString path = dirsToOpen[i]; tree.expand(myModel->index(path, 0)); } tree.expand(myModel->index(dirPath, 0)); QModelIndex parent = myModel->index(dirPath, 0); QList expectedOrder; expectedOrder << tempFile2.fileName() << tempFile.fileName() << dirPath + QChar('/') + ".." << dirPath + QChar('/') + "."; if (fileDialogMode) { QTRY_COMPARE(myModel->rowCount(parent), expectedOrder.size()); // File dialog Mode means sub trees are not sorted, only the current root. // There's no way we can check that the sub tree is "not sorted"; just check if it // has the same contents of the expected list QList actualRows; for(int i = 0; i < myModel->rowCount(parent); ++i) { actualRows << dirPath + QChar('/') + myModel->index(i, 1, parent).data(QFileSystemModel::FileNameRole).toString(); } std::sort(expectedOrder.begin(), expectedOrder.end()); std::sort(actualRows.begin(), actualRows.end()); QCOMPARE(actualRows, expectedOrder); } else { for(int i = 0; i < myModel->rowCount(parent); ++i) { QTRY_COMPARE(dirPath + QChar('/') + myModel->index(i, 1, parent).data(QFileSystemModel::FileNameRole).toString(), expectedOrder.at(i)); } } } void tst_QFileSystemModel::mkdir() { QString tmp = flatDirTestPath; QString newFolderPath = QDir::toNativeSeparators(tmp + '/' + "NewFoldermkdirtest4"); QScopedPointer model(new QFileSystemModel); QAbstractItemModelTester tester(model.get()); tester.setUseFetchMore(false); QModelIndex tmpDir = model->index(tmp); QVERIFY(tmpDir.isValid()); QDir bestatic(newFolderPath); if (bestatic.exists()) { if (!bestatic.rmdir(newFolderPath)) qWarning() << "unable to remove" << newFolderPath; QTest::qWait(WAITTIME); } model->mkdir(tmpDir, "NewFoldermkdirtest3"); model->mkdir(tmpDir, "NewFoldermkdirtest5"); QModelIndex idx = model->mkdir(tmpDir, "NewFoldermkdirtest4"); QVERIFY(idx.isValid()); int oldRow = idx.row(); idx = model->index(newFolderPath); QVERIFY(idx.isValid()); QVERIFY(model->remove(idx)); QVERIFY(!bestatic.exists()); QVERIFY(0 != idx.row()); QCOMPARE(oldRow, idx.row()); } void tst_QFileSystemModel::deleteFile() { QString newFilePath = QDir::temp().filePath("NewFileDeleteTest"); QFile newFile(newFilePath); if (newFile.exists()) { if (!newFile.remove()) qWarning() << "unable to remove" << newFilePath; QTest::qWait(WAITTIME); } if (!newFile.open(QIODevice::WriteOnly | QIODevice::Text)) { qWarning() << "unable to create" << newFilePath; } newFile.close(); QScopedPointer model(new QFileSystemModel); QAbstractItemModelTester tester(model.get()); tester.setUseFetchMore(false); QModelIndex idx = model->index(newFilePath); QVERIFY(idx.isValid()); QVERIFY(model->remove(idx)); QVERIFY(!newFile.exists()); } void tst_QFileSystemModel::deleteDirectory() { // QTBUG-65683: Verify that directories can be removed recursively despite // file system watchers being active on them or their sub-directories (Windows). // Create a temporary directory, a nested directory and expand a treeview // to show them to ensure watcher creation. Then delete the directory. QTemporaryDir dirToBeDeleted(flatDirTestPath + QStringLiteral("/deleteDirectory-XXXXXX")); QVERIFY(dirToBeDeleted.isValid()); const QString dirToBeDeletedPath = dirToBeDeleted.path(); const QString nestedTestDir = QStringLiteral("test"); QVERIFY(QDir(dirToBeDeletedPath).mkpath(nestedTestDir)); const QString nestedTestDirPath = dirToBeDeletedPath + QLatin1Char('/') + nestedTestDir; QFile testFile(nestedTestDirPath + QStringLiteral("/test.txt")); QVERIFY(testFile.open(QIODevice::WriteOnly | QIODevice::Text)); testFile.write("Hello\n"); testFile.close(); QFileSystemModel model; const QModelIndex rootIndex = model.setRootPath(flatDirTestPath); QTreeView treeView; treeView.setWindowTitle(QTest::currentTestFunction()); treeView.setModel(&model); treeView.setRootIndex(rootIndex); const QModelIndex dirToBeDeletedPathIndex = model.index(dirToBeDeletedPath); QVERIFY(dirToBeDeletedPathIndex.isValid()); treeView.setExpanded(dirToBeDeletedPathIndex, true); const QModelIndex nestedTestDirIndex = model.index(nestedTestDirPath); QVERIFY(nestedTestDirIndex.isValid()); treeView.setExpanded(nestedTestDirIndex, true); treeView.show(); QVERIFY(QTest::qWaitForWindowExposed(&treeView)); QVERIFY(model.remove(dirToBeDeletedPathIndex)); dirToBeDeleted.setAutoRemove(false); } static QString flipCase(QString s) { for (int i = 0, size = s.size(); i < size; ++i) { const QChar c = s.at(i); if (c.isUpper()) s[i] = c.toLower(); else if (c.isLower()) s[i] = c.toUpper(); } return s; } void tst_QFileSystemModel::caseSensitivity() { QString tmp = flatDirTestPath; QStringList files; files << "a" << "c" << "C"; QScopedPointer model(new QFileSystemModel); QAbstractItemModelTester tester(model.get()); tester.setUseFetchMore(false); QVERIFY(createFiles(model.data(), tmp, files)); QModelIndex root = model->index(tmp); QStringList paths; QModelIndexList indexes; QCOMPARE(model->rowCount(root), 0); for (int i = 0; i < files.size(); ++i) { const QString path = tmp + '/' + files.at(i); const QModelIndex index = model->index(path); QVERIFY(index.isValid()); paths.append(path); indexes.append(index); } if (!QFileSystemEngine::isCaseSensitive()) { // QTBUG-31103, QTBUG-64147: Verify that files can be accessed by paths with fLipPeD case. for (int i = 0; i < paths.size(); ++i) { const QModelIndex flippedCaseIndex = model->index(flipCase(paths.at(i))); QCOMPARE(indexes.at(i), flippedCaseIndex); } } } void tst_QFileSystemModel::drives_data() { QTest::addColumn("path"); QTest::newRow("current") << QDir::currentPath(); QTest::newRow("slash") << "/"; QTest::newRow("My Computer") << "My Computer"; } void tst_QFileSystemModel::drives() { QFETCH(QString, path); QFileSystemModel model; model.setRootPath(path); model.fetchMore(QModelIndex()); const QFileInfoList drives = QDir::drives(); int driveCount = 0; for (const QFileInfo& driveRoot : drives) { if (driveRoot.exists()) driveCount++; } QTRY_COMPARE(model.rowCount(), driveCount); } void tst_QFileSystemModel::dirsBeforeFiles() { auto diagnosticMsg = [](int row, const QFileInfo &left, const QFileInfo &right) -> QByteArray { QString message; QDebug(&message).noquote() << "Unexpected sort order at #" << row << ':' << left << right; return message.toLocal8Bit(); }; QTemporaryDir testDir(flatDirTestPath); QVERIFY2(testDir.isValid(), qPrintable(testDir.errorString())); QDir dir(testDir.path()); const int itemCount = 3; for (int i = 0; i < itemCount; ++i) { QLatin1Char c('a' + char(i)); QVERIFY(dir.mkdir(c + QLatin1String("-dir"))); QFile file(dir.filePath(c + QLatin1String("-file"))); QVERIFY(file.open(QIODevice::ReadWrite)); file.close(); } QScopedPointer model(new QFileSystemModel); QAbstractItemModelTester tester(model.get()); tester.setUseFetchMore(false); QModelIndex root = model->setRootPath(dir.absolutePath()); // Wait for model to be notified by the file system watcher QTRY_COMPARE(model->rowCount(root), 2 * itemCount); // sort explicitly - dirs before files (except on macOS), and then by name model->sort(0); // Ensure that no file occurs before any directory (see QFileSystemModelSorter): for (int i = 1, count = model->rowCount(root); i < count; ++i) { const QFileInfo previous = model->fileInfo(model->index(i - 1, 0, root)); const QFileInfo current = model->fileInfo(model->index(i, 0, root)); #ifndef Q_OS_MAC QVERIFY2(!(previous.isFile() && current.isDir()), diagnosticMsg(i, previous, current).constData()); #else QVERIFY2(previous.fileName() < current.fileName(), diagnosticMsg(i, previous, current).constData()); #endif } } void tst_QFileSystemModel::roleNames_data() { QTest::addColumn("role"); QTest::addColumn("roleName"); QTest::newRow("decoration") << int(Qt::DecorationRole) << QByteArray("fileIcon"); QTest::newRow("display") << int(Qt::DisplayRole) << QByteArray("display"); QTest::newRow("fileIcon") << int(QFileSystemModel::FileIconRole) << QByteArray("fileIcon"); QTest::newRow("filePath") << int(QFileSystemModel::FilePathRole) << QByteArray("filePath"); QTest::newRow("fileName") << int(QFileSystemModel::FileNameRole) << QByteArray("fileName"); QTest::newRow("filePermissions") << int(QFileSystemModel::FilePermissions) << QByteArray("filePermissions"); } void tst_QFileSystemModel::roleNames() { QFileSystemModel model; QHash roles = model.roleNames(); QFETCH(int, role); QVERIFY(roles.contains(role)); QFETCH(QByteArray, roleName); QCOMPARE(roles.contains(role), true); QCOMPARE(roles.value(role), roleName); } static inline QByteArray permissionRowName(bool readOnly, int permission) { QByteArray result = readOnly ? QByteArrayLiteral("ro") : QByteArrayLiteral("rw"); result += QByteArrayLiteral("-0"); result += QByteArray::number(permission, 16); return result; } void tst_QFileSystemModel::permissions_data() { QTest::addColumn("permissions"); QTest::addColumn("readOnly"); static const int permissions[] = { QFile::WriteOwner, QFile::ReadOwner, QFile::WriteOwner|QFile::ReadOwner, }; for (int permission : permissions) { QTest::newRow(permissionRowName(false, permission).constData()) << QFileDevice::Permissions(permission) << false; QTest::newRow(permissionRowName(true, permission).constData()) << QFileDevice::Permissions(permission) << true; } } void tst_QFileSystemModel::permissions() // checks QTBUG-20503 { QFETCH(QFileDevice::Permissions, permissions); QFETCH(bool, readOnly); const QString tmp = flatDirTestPath; const QString file = tmp + QLatin1String("/f"); QScopedPointer model(new QFileSystemModel); QAbstractItemModelTester tester(model.get()); tester.setUseFetchMore(false); QVERIFY(createFiles(model.data(), tmp, QStringList{QLatin1String("f")})); QVERIFY(QFile::setPermissions(file, permissions)); const QModelIndex root = model->setRootPath(tmp); model->setReadOnly(readOnly); QCOMPARE(model->isReadOnly(), readOnly); QTRY_COMPARE(model->rowCount(root), 1); const QFile::Permissions modelPermissions = model->permissions(model->index(0, 0, root)); const QFile::Permissions modelFileInfoPermissions = model->fileInfo(model->index(0, 0, root)).permissions(); const QFile::Permissions fileInfoPermissions = QFileInfo(file).permissions(); QCOMPARE(modelPermissions, modelFileInfoPermissions); QCOMPARE(modelFileInfoPermissions, fileInfoPermissions); QCOMPARE(fileInfoPermissions, modelPermissions); } void tst_QFileSystemModel::doNotUnwatchOnFailedRmdir() { const QString tmp = flatDirTestPath; QFileSystemModel model; const QTemporaryDir tempDir(tmp + '/' + QStringLiteral("doNotUnwatchOnFailedRmdir-XXXXXX")); QVERIFY(tempDir.isValid()); const QModelIndex rootIndex = model.setRootPath(tempDir.path()); // create a file in the directory so to prevent it from deletion { QFile file(tempDir.path() + '/' + QStringLiteral("file1")); QVERIFY(file.open(QIODevice::WriteOnly)); } QCOMPARE(model.rmdir(rootIndex), false); // create another file { QFile file(tempDir.path() + '/' + QStringLiteral("file2")); QVERIFY(file.open(QIODevice::WriteOnly)); } // the model must now detect this second file QTRY_COMPARE(model.rowCount(rootIndex), 2); } static QSet fileListUnderIndex(const QFileSystemModel *model, const QModelIndex &parent) { QSet fileNames; const int rowCount = model->rowCount(parent); for (int i = 0; i < rowCount; ++i) fileNames.insert(model->index(i, 0, parent).data(QFileSystemModel::FileNameRole).toString()); return fileNames; } void tst_QFileSystemModel::specialFiles() { #ifndef Q_OS_UNIX QSKIP("Not implemented"); #endif #ifdef Q_OS_ANDROID QSKIP("Android does not allow access to root filesystem"); #endif QFileSystemModel model; model.setFilter(QDir::AllEntries | QDir::System | QDir::Hidden); // Can't simply verify if the model returns a valid model index for a special file // as it will always return a valid index for existing files, // even if the file is not visible with the given filter. const QModelIndex rootIndex = model.setRootPath(QStringLiteral("/dev/")); const QString testFileName = QStringLiteral("null"); QTRY_VERIFY(fileListUnderIndex(&model, rootIndex).contains(testFileName)); model.setFilter(QDir::AllEntries | QDir::Hidden); QTRY_VERIFY(!fileListUnderIndex(&model, rootIndex).contains(testFileName)); } void tst_QFileSystemModel::fileInfo() { QFileSystemModel model; QModelIndex idx; QVERIFY(model.fileInfo(idx).filePath().isEmpty()); const QString dirPath = flatDirTestPath; QDir dir(dirPath); const QString subdir = QStringLiteral("subdir"); QVERIFY(dir.mkdir(subdir)); const QString subdirPath = dir.absoluteFilePath(subdir); idx = model.setRootPath(subdirPath); QCOMPARE(model.fileInfo(idx), QFileInfo(subdirPath)); idx = model.setRootPath(dirPath); QCOMPARE(model.fileInfo(idx), QFileInfo(dirPath)); } QTEST_MAIN(tst_QFileSystemModel) #include "tst_qfilesystemmodel.moc"