diff --git a/autotests/dfm-io-tests/CMakeLists.txt b/autotests/dfm-io-tests/CMakeLists.txt index e93283f6..3336d999 100644 --- a/autotests/dfm-io-tests/CMakeLists.txt +++ b/autotests/dfm-io-tests/CMakeLists.txt @@ -9,15 +9,21 @@ endif() message(STATUS "Adding unit tests for ${IO_TEST_LIB}") -# Collect test source files -file(GLOB_RECURSE TEST_SRCS - ${CMAKE_CURRENT_SOURCE_DIR}/*.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/*.h +# 共享的测试入口点 +set(TEST_COMMON_SRCS + ${CMAKE_CURRENT_SOURCE_DIR}/test_main.cpp +) + +# 各测试类源文件(不含 main) +set(TEST_CLASS_SRCS + ${CMAKE_CURRENT_SOURCE_DIR}/tst_dfm_io.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/tst_dfilesorter.cpp ) # Create the test executable add_executable(dfm-io-test - ${TEST_SRCS} + ${TEST_COMMON_SRCS} + ${TEST_CLASS_SRCS} ) # Link against the dfm-io library and Qt Test @@ -27,17 +33,11 @@ target_link_libraries(dfm-io-test ) # Add include directories -if(DFM_BUILD_WITH_QT6) - target_include_directories(dfm-io-test - PRIVATE - ${CMAKE_SOURCE_DIR}/src/dfm-io/dfm-io - ) -else() - target_include_directories(dfm-io-test - PRIVATE - ${CMAKE_SOURCE_DIR}/src/dfm-io/dfm-io - ) -endif() +target_include_directories(dfm-io-test + PRIVATE + ${CMAKE_SOURCE_DIR}/src/dfm-io/dfm-io + ${CMAKE_SOURCE_DIR}/src/dfm-io/dfm-io/sort +) # Register the test with CTest add_test(NAME dfm-io-test COMMAND dfm-io-test) diff --git a/autotests/dfm-io-tests/test_main.cpp b/autotests/dfm-io-tests/test_main.cpp new file mode 100644 index 00000000..1432aae5 --- /dev/null +++ b/autotests/dfm-io-tests/test_main.cpp @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include + +// 前置声明测试类 +class tst_DfmIO; +class tst_DFileSorter; + +int main(int argc, char *argv[]) +{ + int status = 0; + + // 运行各测试类 + extern int run_tst_DfmIO(int argc, char *argv[]); + extern int run_tst_DFileSorter(int argc, char *argv[]); + + status |= run_tst_DfmIO(argc, argv); + status |= run_tst_DFileSorter(argc, argv); + + return status; +} diff --git a/autotests/dfm-io-tests/tst_dfilesorter.cpp b/autotests/dfm-io-tests/tst_dfilesorter.cpp new file mode 100644 index 00000000..5308873a --- /dev/null +++ b/autotests/dfm-io-tests/tst_dfilesorter.cpp @@ -0,0 +1,640 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "sort/dfilesorter.h" +#include "sort/dsortkeycache.h" + +using namespace dfmio; + +class tst_DFileSorter : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void cleanupTestCase(); + + // DSortKeyCache 测试 + void sortKeyCache_basic(); + void sortKeyCache_numericMode(); + void sortKeyCache_threadSafety(); + + // DFileSorter 测试 + void fileSorter_sortByName(); + void fileSorter_sortByName_descending(); + void fileSorter_sortBySize(); + void fileSorter_sortBySize_withDirs(); + void fileSorter_sortBySize_descending(); + void fileSorter_sortByLastModified(); + void fileSorter_sortByLastRead(); + void fileSorter_mixDirAndFile(); + void fileSorter_separateDirAndFile(); + void fileSorter_emptyList(); + void fileSorter_singleItem(); + void fileSorter_sortByLastModified_withSymlink(); + void fileSorter_sortByLastRead_withSymlink(); +}; + +void tst_DFileSorter::initTestCase() +{ +} + +void tst_DFileSorter::cleanupTestCase() +{ +} + +// ==================== DSortKeyCache 测试 ==================== + +void tst_DFileSorter::sortKeyCache_basic() +{ + DSortKeyCache &cache = DSortKeyCache::instance(); + + // 相同字符串应返回相同的排序键 + QCollatorSortKey key1 = cache.sortKey("test"); + QCollatorSortKey key2 = cache.sortKey("test"); + QCOMPARE(key1.compare(key2), 0); +} + +void tst_DFileSorter::sortKeyCache_numericMode() +{ + DSortKeyCache &cache = DSortKeyCache::instance(); + + // 数字自然排序:file2 < file10 + QCollatorSortKey key2 = cache.sortKey("file2"); + QCollatorSortKey key10 = cache.sortKey("file10"); + + // file2 应该排在 file10 前面(numericMode 开启) + QVERIFY(key2.compare(key10) < 0); +} + +void tst_DFileSorter::sortKeyCache_threadSafety() +{ + // 测试 thread_local QCollator 的线程安全性 + // 每个线程应该有独立的 QCollator 实例 + DSortKeyCache &cache = DSortKeyCache::instance(); + + // 验证排序键可以正常比较(不假设大小写不敏感) + QCollatorSortKey key1 = cache.sortKey("abc"); + + // 相同字符串返回相同排序键 + QCollatorSortKey key2 = cache.sortKey("abc"); + QCOMPARE(key1.compare(key2), 0); +} + +// ==================== DFileSorter 测试 ==================== + +void tst_DFileSorter::fileSorter_sortByName() +{ + DFileSorter::SortConfig config; + config.role = DFileSorter::SortRole::Name; + config.order = Qt::AscendingOrder; + config.mixDirAndFile = true; + + DFileSorter sorter(config); + + // 创建测试文件列表 + QList> files; + + auto file1 = QSharedPointer::create(); + file1->url = QUrl::fromLocalFile("/test/file10.txt"); + file1->isDir = false; + file1->filesize = 100; + files.append(file1); + + auto file2 = QSharedPointer::create(); + file2->url = QUrl::fromLocalFile("/test/file2.txt"); + file2->isDir = false; + file2->filesize = 200; + files.append(file2); + + auto file3 = QSharedPointer::create(); + file3->url = QUrl::fromLocalFile("/test/file1.txt"); + file3->isDir = false; + file3->filesize = 300; + files.append(file3); + + auto sorted = sorter.sort(std::move(files)); + + // 验证排序结果:file1 < file2 < file10(自然排序) + QCOMPARE(sorted.size(), 3); + QCOMPARE(sorted[0]->url.fileName(), QString("file1.txt")); + QCOMPARE(sorted[1]->url.fileName(), QString("file2.txt")); + QCOMPARE(sorted[2]->url.fileName(), QString("file10.txt")); +} + +void tst_DFileSorter::fileSorter_sortByName_descending() +{ + DFileSorter::SortConfig config; + config.role = DFileSorter::SortRole::Name; + config.order = Qt::DescendingOrder; + config.mixDirAndFile = true; + + DFileSorter sorter(config); + + QList> files; + + auto file1 = QSharedPointer::create(); + file1->url = QUrl::fromLocalFile("/test/a.txt"); + file1->isDir = false; + files.append(file1); + + auto file2 = QSharedPointer::create(); + file2->url = QUrl::fromLocalFile("/test/b.txt"); + file2->isDir = false; + files.append(file2); + + auto file3 = QSharedPointer::create(); + file3->url = QUrl::fromLocalFile("/test/c.txt"); + file3->isDir = false; + files.append(file3); + + auto sorted = sorter.sort(std::move(files)); + + // 降序:c > b > a + QCOMPARE(sorted.size(), 3); + QCOMPARE(sorted[0]->url.fileName(), QString("c.txt")); + QCOMPARE(sorted[1]->url.fileName(), QString("b.txt")); + QCOMPARE(sorted[2]->url.fileName(), QString("a.txt")); +} + +void tst_DFileSorter::fileSorter_sortBySize() +{ + DFileSorter::SortConfig config; + config.role = DFileSorter::SortRole::Size; + config.order = Qt::AscendingOrder; + config.mixDirAndFile = true; + + DFileSorter sorter(config); + + QList> files; + + auto file1 = QSharedPointer::create(); + file1->url = QUrl::fromLocalFile("/test/large.txt"); + file1->isDir = false; + file1->filesize = 1000; + files.append(file1); + + auto file2 = QSharedPointer::create(); + file2->url = QUrl::fromLocalFile("/test/small.txt"); + file2->isDir = false; + file2->filesize = 100; + files.append(file2); + + auto file3 = QSharedPointer::create(); + file3->url = QUrl::fromLocalFile("/test/medium.txt"); + file3->isDir = false; + file3->filesize = 500; + files.append(file3); + + auto sorted = sorter.sort(std::move(files)); + + // 按大小升序:small < medium < large + QCOMPARE(sorted.size(), 3); + QCOMPARE(sorted[0]->url.fileName(), QString("small.txt")); + QCOMPARE(sorted[1]->url.fileName(), QString("medium.txt")); + QCOMPARE(sorted[2]->url.fileName(), QString("large.txt")); +} + +void tst_DFileSorter::fileSorter_sortBySize_withDirs() +{ + // 测试按大小排序时,目录(有效大小 -1)始终排在前面(升序时) + // 这与原 DLocalHelper::fileSizeByEnt 的行为一致 + DFileSorter::SortConfig config; + config.role = DFileSorter::SortRole::Size; + config.order = Qt::AscendingOrder; + config.mixDirAndFile = true; // 混排模式 + + DFileSorter sorter(config); + + QList> files; + + // 目录:大小通常为 4096,但有效大小应为 -1 + auto dir1 = QSharedPointer::create(); + dir1->url = QUrl::fromLocalFile("/test/docs"); + dir1->isDir = true; + dir1->isSymLink = false; + dir1->filesize = 4096; // 实际大小 + files.append(dir1); + + // 小文件:100 字节(小于 4096) + auto file1 = QSharedPointer::create(); + file1->url = QUrl::fromLocalFile("/test/small.txt"); + file1->isDir = false; + file1->filesize = 100; + files.append(file1); + + // 中等文件:2000 字节(小于 4096) + auto file2 = QSharedPointer::create(); + file2->url = QUrl::fromLocalFile("/test/medium.txt"); + file2->isDir = false; + file2->filesize = 2000; + files.append(file2); + + // 大文件:10000 字节(大于 4096) + auto file3 = QSharedPointer::create(); + file3->url = QUrl::fromLocalFile("/test/large.txt"); + file3->isDir = false; + file3->filesize = 10000; + files.append(file3); + + // 另一个目录 + auto dir2 = QSharedPointer::create(); + dir2->url = QUrl::fromLocalFile("/test/apps"); + dir2->isDir = true; + dir2->isSymLink = false; + dir2->filesize = 4096; + files.append(dir2); + + auto sorted = sorter.sort(std::move(files)); + + // 验证:升序时目录(有效大小 -1)在前,然后按文件大小排序 + QCOMPARE(sorted.size(), 5); + // 目录在前(按名称排序,因为大小相同都是 -1) + QCOMPARE(sorted[0]->isDir, true); + QCOMPARE(sorted[0]->url.fileName(), QString("apps")); + QCOMPARE(sorted[1]->isDir, true); + QCOMPARE(sorted[1]->url.fileName(), QString("docs")); + // 文件在后(按大小排序) + QCOMPARE(sorted[2]->isDir, false); + QCOMPARE(sorted[2]->url.fileName(), QString("small.txt")); + QCOMPARE(sorted[3]->isDir, false); + QCOMPARE(sorted[3]->url.fileName(), QString("medium.txt")); + QCOMPARE(sorted[4]->isDir, false); + QCOMPARE(sorted[4]->url.fileName(), QString("large.txt")); +} + +void tst_DFileSorter::fileSorter_sortBySize_descending() +{ + // 测试按大小降序排序 + // 降序时,目录(有效大小 -1)会排到后面 + DFileSorter::SortConfig config; + config.role = DFileSorter::SortRole::Size; + config.order = Qt::DescendingOrder; + config.mixDirAndFile = true; + + DFileSorter sorter(config); + + QList> files; + + // 目录(有效大小 -1) + auto dir1 = QSharedPointer::create(); + dir1->url = QUrl::fromLocalFile("/test/docs"); + dir1->isDir = true; + dir1->isSymLink = false; + dir1->filesize = 4096; + files.append(dir1); + + // 文件 + auto file1 = QSharedPointer::create(); + file1->url = QUrl::fromLocalFile("/test/small.txt"); + file1->isDir = false; + file1->filesize = 100; + files.append(file1); + + auto file2 = QSharedPointer::create(); + file2->url = QUrl::fromLocalFile("/test/large.txt"); + file2->isDir = false; + file2->filesize = 10000; + files.append(file2); + + auto sorted = sorter.sort(std::move(files)); + + // 降序:大文件 > 小文件 > 目录(-1 最小,反转后在最后) + QCOMPARE(sorted.size(), 3); + QCOMPARE(sorted[0]->url.fileName(), QString("large.txt")); // 大文件 + QCOMPARE(sorted[1]->url.fileName(), QString("small.txt")); // 小文件 + QCOMPARE(sorted[2]->isDir, true); // 目录在最后 +} + +void tst_DFileSorter::fileSorter_sortByLastModified() +{ + DFileSorter::SortConfig config; + config.role = DFileSorter::SortRole::LastModified; + config.order = Qt::AscendingOrder; + config.mixDirAndFile = true; + + DFileSorter sorter(config); + + QList> files; + + auto file1 = QSharedPointer::create(); + file1->url = QUrl::fromLocalFile("/test/newest.txt"); + file1->isDir = false; + file1->lastModifed = 3000; + file1->lastModifedNs = 0; + files.append(file1); + + auto file2 = QSharedPointer::create(); + file2->url = QUrl::fromLocalFile("/test/oldest.txt"); + file2->isDir = false; + file2->lastModifed = 1000; + file2->lastModifedNs = 0; + files.append(file2); + + auto file3 = QSharedPointer::create(); + file3->url = QUrl::fromLocalFile("/test/middle.txt"); + file3->isDir = false; + file3->lastModifed = 2000; + file3->lastModifedNs = 0; + files.append(file3); + + auto sorted = sorter.sort(std::move(files)); + + // 按修改时间升序:oldest < middle < newest + QCOMPARE(sorted.size(), 3); + QCOMPARE(sorted[0]->url.fileName(), QString("oldest.txt")); + QCOMPARE(sorted[1]->url.fileName(), QString("middle.txt")); + QCOMPARE(sorted[2]->url.fileName(), QString("newest.txt")); +} + +void tst_DFileSorter::fileSorter_sortByLastRead() +{ + DFileSorter::SortConfig config; + config.role = DFileSorter::SortRole::LastRead; + config.order = Qt::AscendingOrder; + config.mixDirAndFile = true; + + DFileSorter sorter(config); + + QList> files; + + auto file1 = QSharedPointer::create(); + file1->url = QUrl::fromLocalFile("/test/read_newest.txt"); + file1->isDir = false; + file1->lastRead = 3000; + file1->lastReadNs = 0; + files.append(file1); + + auto file2 = QSharedPointer::create(); + file2->url = QUrl::fromLocalFile("/test/read_oldest.txt"); + file2->isDir = false; + file2->lastRead = 1000; + file2->lastReadNs = 0; + files.append(file2); + + auto sorted = sorter.sort(std::move(files)); + + // 按访问时间升序:oldest < newest + QCOMPARE(sorted.size(), 2); + QCOMPARE(sorted[0]->url.fileName(), QString("read_oldest.txt")); + QCOMPARE(sorted[1]->url.fileName(), QString("read_newest.txt")); +} + +void tst_DFileSorter::fileSorter_mixDirAndFile() +{ + DFileSorter::SortConfig config; + config.role = DFileSorter::SortRole::Name; + config.order = Qt::AscendingOrder; + config.mixDirAndFile = true; // 混排模式 + + DFileSorter sorter(config); + + QList> files; + + auto dir1 = QSharedPointer::create(); + dir1->url = QUrl::fromLocalFile("/test/docs"); + dir1->isDir = true; + dir1->isSymLink = false; + files.append(dir1); + + auto file1 = QSharedPointer::create(); + file1->url = QUrl::fromLocalFile("/test/afile.txt"); + file1->isDir = false; + files.append(file1); + + auto dir2 = QSharedPointer::create(); + dir2->url = QUrl::fromLocalFile("/test/apps"); + dir2->isDir = true; + dir2->isSymLink = false; + files.append(dir2); + + auto sorted = sorter.sort(std::move(files)); + + // 混排模式:所有项一起排序 + QCOMPARE(sorted.size(), 3); + QCOMPARE(sorted[0]->url.fileName(), QString("afile.txt")); // a 排最前 + QCOMPARE(sorted[1]->url.fileName(), QString("apps")); + QCOMPARE(sorted[2]->url.fileName(), QString("docs")); +} + +void tst_DFileSorter::fileSorter_separateDirAndFile() +{ + DFileSorter::SortConfig config; + config.role = DFileSorter::SortRole::Name; + config.order = Qt::AscendingOrder; + config.mixDirAndFile = false; // 分离模式:目录在前 + + DFileSorter sorter(config); + + QList> files; + + auto file1 = QSharedPointer::create(); + file1->url = QUrl::fromLocalFile("/test/a_file.txt"); + file1->isDir = false; + files.append(file1); + + auto dir1 = QSharedPointer::create(); + dir1->url = QUrl::fromLocalFile("/test/z_dir"); + dir1->isDir = true; + dir1->isSymLink = false; + files.append(dir1); + + auto file2 = QSharedPointer::create(); + file2->url = QUrl::fromLocalFile("/test/b_file.txt"); + file2->isDir = false; + files.append(file2); + + auto dir2 = QSharedPointer::create(); + dir2->url = QUrl::fromLocalFile("/test/a_dir"); + dir2->isDir = true; + dir2->isSymLink = false; + files.append(dir2); + + auto sorted = sorter.sort(std::move(files)); + + // 分离模式:目录在前(按名称排序),文件在后(按名称排序) + QCOMPARE(sorted.size(), 4); + // 目录在前 + QCOMPARE(sorted[0]->url.fileName(), QString("a_dir")); + QCOMPARE(sorted[1]->url.fileName(), QString("z_dir")); + // 文件在后 + QCOMPARE(sorted[2]->url.fileName(), QString("a_file.txt")); + QCOMPARE(sorted[3]->url.fileName(), QString("b_file.txt")); +} + +void tst_DFileSorter::fileSorter_emptyList() +{ + DFileSorter::SortConfig config; + config.role = DFileSorter::SortRole::Name; + + DFileSorter sorter(config); + + QList> files; + auto sorted = sorter.sort(std::move(files)); + + QVERIFY(sorted.isEmpty()); +} + +void tst_DFileSorter::fileSorter_singleItem() +{ + DFileSorter::SortConfig config; + config.role = DFileSorter::SortRole::Name; + + DFileSorter sorter(config); + + QList> files; + + auto file1 = QSharedPointer::create(); + file1->url = QUrl::fromLocalFile("/test/single.txt"); + file1->isDir = false; + files.append(file1); + + auto sorted = sorter.sort(std::move(files)); + + QCOMPARE(sorted.size(), 1); + QCOMPARE(sorted[0]->url.fileName(), QString("single.txt")); +} + +void tst_DFileSorter::fileSorter_sortByLastModified_withSymlink() +{ + // 测试按修改时间排序时,符号链接使用其指向文件的时间 + // 需要创建临时文件来测试真实场景 + DFileSorter::SortConfig config; + config.role = DFileSorter::SortRole::LastModified; + config.order = Qt::AscendingOrder; + config.mixDirAndFile = true; + + DFileSorter sorter(config); + + // 创建临时目录和文件 + QTemporaryDir tempDir; + QVERIFY(tempDir.isValid()); + + QString oldFile = tempDir.path() + "/old_file.txt"; + QString newFile = tempDir.path() + "/new_file.txt"; + QString linkFile = tempDir.path() + "/link_to_old.txt"; + + // 创建文件 + { + QFile f(oldFile); + f.open(QIODevice::WriteOnly); + f.setFileTime(QDateTime::fromSecsSinceEpoch(1000), QFileDevice::FileModificationTime); + } + { + QFile f(newFile); + f.open(QIODevice::WriteOnly); + f.setFileTime(QDateTime::fromSecsSinceEpoch(3000), QFileDevice::FileModificationTime); + } + + // 创建符号链接指向旧文件 + QFile::link(oldFile, linkFile); + + QList> files; + + // 普通文件:new_file + auto file1 = QSharedPointer::create(); + file1->url = QUrl::fromLocalFile(newFile); + file1->isDir = false; + file1->isSymLink = false; + file1->lastModifed = 3000; + file1->lastModifedNs = 0; + files.append(file1); + + // 符号链接:指向 old_file(时间 1000) + auto symlink1 = QSharedPointer::create(); + symlink1->url = QUrl::fromLocalFile(linkFile); + symlink1->isDir = false; + symlink1->isSymLink = true; + symlink1->symlinkUrl = QUrl::fromLocalFile(oldFile); // 指向目标文件 + symlink1->lastModifed = 2000; // 链接本身的时间(排序时应忽略,使用目标时间 1000) + symlink1->lastModifedNs = 0; + files.append(symlink1); + + auto sorted = sorter.sort(std::move(files)); + + // 按修改时间升序:link_to_old(1000,目标文件时间) < new_file(3000) + QCOMPARE(sorted.size(), 2); + QCOMPARE(sorted[0]->url.fileName(), QString("link_to_old.txt")); + QCOMPARE(sorted[1]->url.fileName(), QString("new_file.txt")); +} + +void tst_DFileSorter::fileSorter_sortByLastRead_withSymlink() +{ + // 测试按访问时间排序时,符号链接使用其指向文件的时间 + DFileSorter::SortConfig config; + config.role = DFileSorter::SortRole::LastRead; + config.order = Qt::AscendingOrder; + config.mixDirAndFile = true; + + DFileSorter sorter(config); + + // 创建临时目录和文件 + QTemporaryDir tempDir; + QVERIFY(tempDir.isValid()); + + QString file1 = tempDir.path() + "/file1.txt"; + QString file2 = tempDir.path() + "/file2.txt"; + QString linkFile = tempDir.path() + "/link_to_file2.txt"; + + // 创建文件并设置访问时间 + { + QFile f(file1); + f.open(QIODevice::WriteOnly); + f.setFileTime(QDateTime::fromSecsSinceEpoch(1000), QFileDevice::FileAccessTime); + } + { + QFile f(file2); + f.open(QIODevice::WriteOnly); + f.setFileTime(QDateTime::fromSecsSinceEpoch(3000), QFileDevice::FileAccessTime); + } + + // 创建符号链接指向 file2 + QFile::link(file2, linkFile); + + QList> files; + + // 普通文件:file1(访问时间 1000) + auto normalFile = QSharedPointer::create(); + normalFile->url = QUrl::fromLocalFile(file1); + normalFile->isDir = false; + normalFile->isSymLink = false; + normalFile->lastRead = 1000; + normalFile->lastReadNs = 0; + files.append(normalFile); + + // 符号链接:指向 file2(访问时间 3000) + auto symlink1 = QSharedPointer::create(); + symlink1->url = QUrl::fromLocalFile(linkFile); + symlink1->isDir = false; + symlink1->isSymLink = true; + symlink1->symlinkUrl = QUrl::fromLocalFile(file2); // 指向目标文件 + symlink1->lastRead = 2000; // 链接本身的时间(排序时应忽略,使用目标时间 3000) + symlink1->lastReadNs = 0; + files.append(symlink1); + + auto sorted = sorter.sort(std::move(files)); + + // 按访问时间升序:file1(1000) < link_to_file2(3000,目标文件时间) + QCOMPARE(sorted.size(), 2); + QCOMPARE(sorted[0]->url.fileName(), QString("file1.txt")); + QCOMPARE(sorted[1]->url.fileName(), QString("link_to_file2.txt")); +} + +int run_tst_DFileSorter(int argc, char *argv[]) +{ + tst_DFileSorter tc; + return QTest::qExec(&tc, argc, argv); +} + +#include "tst_dfilesorter.moc" + diff --git a/autotests/dfm-io-tests/tst_dfm_io.cpp b/autotests/dfm-io-tests/tst_dfm_io.cpp index da783bca..17b33637 100644 --- a/autotests/dfm-io-tests/tst_dfm_io.cpp +++ b/autotests/dfm-io-tests/tst_dfm_io.cpp @@ -30,5 +30,10 @@ void tst_DfmIO::initialization_test() QVERIFY(true); } -QTEST_MAIN(tst_DfmIO) +int run_tst_DfmIO(int argc, char *argv[]) +{ + tst_DfmIO tc; + return QTest::qExec(&tc, argc, argv); +} + #include "tst_dfm_io.moc" diff --git a/src/dfm-io/dfm-io/denumerator.cpp b/src/dfm-io/dfm-io/denumerator.cpp index 6c129566..bc14e046 100644 --- a/src/dfm-io/dfm-io/denumerator.cpp +++ b/src/dfm-io/dfm-io/denumerator.cpp @@ -5,6 +5,7 @@ #include "private/denumerator_p.h" #include "utils/dlocalhelper.h" +#include "sort/dfilesorter.h" #include #include @@ -803,14 +804,29 @@ QList> DEnumerator::fileInfoList() QList> DEnumerator::sortFileInfoList() { - if (!d->fts) - d->openDirByfts(); + // 使用 FTS 遍历但不预排序(传 nullptr 作为比较函数) + if (!d->fts) { + char *paths[2] = { nullptr, nullptr }; + paths[0] = d->filePath(d->uri); + if (!paths[0]) { + qWarning() << "Failed to get file path for uri:" << d->uri; + return {}; + } + d->fts = fts_open(paths, FTS_COMFOLLOW, nullptr); + free(paths[0]); + + if (!d->fts) { + qWarning() << "fts_open open error : " << QString::fromLocal8Bit(strerror(errno)); + d->error.setCode(DFMIOErrorCode::DFM_IO_ERROR_FTS_OPEN); + return {}; + } + } if (!d->fts) return {}; - QList> listFile; - QList> listDir; + // 收集所有文件信息 + QList> allFiles; QSet hideList; QUrl urlHidden = d->buildUrl(d->uri, ".hidden"); hideList = DLocalHelper::hideListFromUrl(urlHidden); @@ -819,7 +835,8 @@ QList> DEnumerator::sortFileInfoList() qWarning() << "Failed to get file path for uri:" << d->uri; return {}; } - while (1) { + + while (true) { FTSENT *ent = fts_read(d->fts); if (ent == nullptr) { @@ -834,20 +851,30 @@ QList> DEnumerator::sortFileInfoList() if (strcmp(ent->fts_path, dirPath) == 0 || flag == FTS_DP) continue; - d->insertSortFileInfoList(listFile, listDir, ent, d->fts, hideList); + auto sortInfo = DLocalHelper::createSortFileInfo(ent, hideList); + // 跳过子目录遍历 + if (sortInfo->isDir && !sortInfo->isSymLink) { + fts_set(d->fts, ent, FTS_SKIP); + } + allFiles.append(sortInfo); } fts_close(d->fts); d->fts = nullptr; - - // Clean up allocated memory free(dirPath); - if (d->isMixDirAndFile) - return listFile; - - listDir.append(listFile); - return listDir; + // 使用 DFileSorter 进行排序 + DFileSorter::SortConfig config; + // 映射排序角色:kSortRoleCompareFileName(1) -> Name(0), 以此类推 + // kSortRoleCompareDefault(0) 默认按名称排序 + config.role = (d->sortRoleFlag == SortRoleCompareFlag::kSortRoleCompareDefault) + ? DFileSorter::SortRole::Name + : static_cast(static_cast(d->sortRoleFlag) - 1); + config.order = d->sortOrder; + config.mixDirAndFile = d->isMixDirAndFile; + + DFileSorter sorter(config); + return sorter.sort(std::move(allFiles)); } DFMIOError DEnumerator::lastError() const diff --git a/src/dfm-io/dfm-io/sort/dfilesorter.cpp b/src/dfm-io/dfm-io/sort/dfilesorter.cpp new file mode 100644 index 00000000..bb1adb17 --- /dev/null +++ b/src/dfm-io/dfm-io/sort/dfilesorter.cpp @@ -0,0 +1,231 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "dfilesorter.h" +#include "dsortkeycache.h" + +#include +#include + +USING_IO_NAMESPACE + +namespace { + +// 获取符号链接指向文件的时间信息(用于按时间排序) +struct SymlinkTimeInfo { + qint64 lastModified = 0; + qint64 lastModifiedNs = 0; + qint64 lastRead = 0; + qint64 lastReadNs = 0; + bool valid = false; +}; + +SymlinkTimeInfo getSymlinkTargetTime(const QUrl &symlinkUrl) +{ + SymlinkTimeInfo info; + if (!symlinkUrl.isValid() || !symlinkUrl.isLocalFile()) { + return info; + } + + struct stat st; + if (stat(symlinkUrl.path().toUtf8().constData(), &st) == 0) { + info.lastModified = st.st_mtim.tv_sec; + info.lastModifiedNs = st.st_mtim.tv_nsec; + info.lastRead = st.st_atim.tv_sec; + info.lastReadNs = st.st_atim.tv_nsec; + info.valid = true; + } + return info; +} + +} // namespace + +DFileSorter::DFileSorter(const SortConfig &config) + : m_config(config) +{ +} + +QList> DFileSorter::sort( + QList> &&files) +{ + if (files.isEmpty()) { + return files; + } + + QList> result; + + if (m_config.mixDirAndFile) { + // 混排模式:所有文件一起排序 + std::stable_sort(files.begin(), files.end(), getCompareFunc()); + applySortOrder(files); + result = std::move(files); + } else { + // 分离模式:目录在前,文件在后,分别排序 + QList> dirs; + QList> regularFiles; + + separateDirAndFile(std::move(files), dirs, regularFiles); + + std::stable_sort(dirs.begin(), dirs.end(), getCompareFunc()); + applySortOrder(dirs); + + std::stable_sort(regularFiles.begin(), regularFiles.end(), getCompareFunc()); + applySortOrder(regularFiles); + + // 目录排在前面 + result = std::move(dirs); + result.append(std::move(regularFiles)); + } + + return result; +} + +QList> DFileSorter::sort( + const QList> &files) +{ + // 拷贝后排序 + QList> copy = files; + return sort(std::move(copy)); +} + +DFileSorter::CompareFunc DFileSorter::getCompareFunc() const +{ + switch (m_config.role) { + case SortRole::Name: + return [this](const auto &a, const auto &b) { return compareByName(a, b); }; + case SortRole::Size: + return [this](const auto &a, const auto &b) { return compareBySize(a, b); }; + case SortRole::LastModified: + return [this](const auto &a, const auto &b) { return compareByLastModified(a, b); }; + case SortRole::LastRead: + return [this](const auto &a, const auto &b) { return compareByLastRead(a, b); }; + default: + return [this](const auto &a, const auto &b) { return compareByName(a, b); }; + } +} + +bool DFileSorter::compareByName( + const QSharedPointer &a, + const QSharedPointer &b) const +{ + // 使用预缓存的排序键进行比较 + QString nameA = a->url.fileName(); + QString nameB = b->url.fileName(); + + QCollatorSortKey keyA = DSortKeyCache::instance().sortKey(nameA); + QCollatorSortKey keyB = DSortKeyCache::instance().sortKey(nameB); + + return keyA < keyB; +} + +bool DFileSorter::compareBySize( + const QSharedPointer &a, + const QSharedPointer &b) const +{ + // 获取有效大小:目录返回 -1,确保目录始终排在前面 + // 这与原 DLocalHelper::fileSizeByEnt 的行为一致 + auto getEffectiveSize = + [](const QSharedPointer &info) -> qint64 { + // 目录返回 -1 + if (info->isDir) { + return -1; + } + return info->filesize; + }; + + qint64 sizeA = getEffectiveSize(a); + qint64 sizeB = getEffectiveSize(b); + + // 先按有效大小比较,大小相同时按名称排序 + if (sizeA != sizeB) { + return sizeA < sizeB; + } + return compareByName(a, b); +} + +bool DFileSorter::compareByLastModified( + const QSharedPointer &a, + const QSharedPointer &b) const +{ + // 获取用于排序的修改时间:符号链接使用指向文件的时间 + auto getEffectiveModifiedTime = [](const QSharedPointer &info) + -> std::pair { + // 如果是符号链接且有目标 URL,使用目标文件的时间 + if (info->isSymLink && info->symlinkUrl.isValid()) { + auto targetTime = getSymlinkTargetTime(info->symlinkUrl); + if (targetTime.valid) { + return { targetTime.lastModified, targetTime.lastModifiedNs }; + } + } + return { info->lastModifed, info->lastModifedNs }; + }; + + auto [timeA, nsA] = getEffectiveModifiedTime(a); + auto [timeB, nsB] = getEffectiveModifiedTime(b); + + // 先比较秒,再比较纳秒 + if (timeA != timeB) { + return timeA < timeB; + } + if (nsA != nsB) { + return nsA < nsB; + } + // 时间相同,按名称排序 + return compareByName(a, b); +} + +bool DFileSorter::compareByLastRead( + const QSharedPointer &a, + const QSharedPointer &b) const +{ + // 获取用于排序的访问时间:符号链接使用指向文件的时间 + auto getEffectiveReadTime = [](const QSharedPointer &info) + -> std::pair { + // 如果是符号链接且有目标 URL,使用目标文件的时间 + if (info->isSymLink && info->symlinkUrl.isValid()) { + auto targetTime = getSymlinkTargetTime(info->symlinkUrl); + if (targetTime.valid) { + return { targetTime.lastRead, targetTime.lastReadNs }; + } + } + return { info->lastRead, info->lastReadNs }; + }; + + auto [timeA, nsA] = getEffectiveReadTime(a); + auto [timeB, nsB] = getEffectiveReadTime(b); + + // 先比较秒,再比较纳秒 + if (timeA != timeB) { + return timeA < timeB; + } + if (nsA != nsB) { + return nsA < nsB; + } + // 时间相同,按名称排序 + return compareByName(a, b); +} + +void DFileSorter::separateDirAndFile( + QList> &&files, + QList> &dirs, + QList> ®ularFiles) const +{ + dirs.reserve(files.size()); + regularFiles.reserve(files.size()); + + for (auto &file : files) { + if (file->isDir) { + dirs.append(std::move(file)); + } else { + regularFiles.append(std::move(file)); + } + } +} + +void DFileSorter::applySortOrder(QList> &list) const +{ + if (m_config.order == Qt::DescendingOrder) { + std::reverse(list.begin(), list.end()); + } +} diff --git a/src/dfm-io/dfm-io/sort/dfilesorter.h b/src/dfm-io/dfm-io/sort/dfilesorter.h new file mode 100644 index 00000000..09067af9 --- /dev/null +++ b/src/dfm-io/dfm-io/sort/dfilesorter.h @@ -0,0 +1,139 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef DFILESORTER_H +#define DFILESORTER_H + +#include +#include + +#include +#include + +#include + +BEGIN_IO_NAMESPACE + +/** + * @brief 文件排序器 + * + * 提供高性能的文件列表排序功能,支持多种排序角色和排序顺序。 + * 使用 QCollator::sortKey() 进行预缓存排序键,优化性能。 + * + * 用法示例: + * @code + * DFileSorter::SortConfig config; + * config.role = DFileSorter::SortRole::Name; + * config.order = Qt::AscendingOrder; + * config.mixDirAndFile = false; + * + * DFileSorter sorter(config); + * auto sortedList = sorter.sort(std::move(fileList)); + * @endcode + */ +class DFileSorter +{ +public: + /** + * @brief 排序角色 + */ + enum class SortRole : uint8_t { + Name = 0, // 按文件名排序 + Size = 1, // 按文件大小排序 + LastModified = 2, // 按最后修改时间排序 + LastRead = 3 // 按最后访问时间排序 + }; + + /** + * @brief 排序配置 + */ + struct SortConfig { + SortRole role { SortRole::Name }; // 排序角色 + Qt::SortOrder order { Qt::AscendingOrder }; // 排序顺序 + bool mixDirAndFile { false }; // 是否混排目录和文件 + }; + + /** + * @brief 构造函数 + * + * @param config 排序配置 + */ + explicit DFileSorter(const SortConfig &config); + + /** + * @brief 对文件列表进行排序 + * + * @param files 文件列表(使用移动语义避免拷贝) + * @return 排序后的文件列表 + */ + QList> sort( + QList> &&files); + + /** + * @brief 对文件列表进行排序(拷贝版本) + * + * @param files 文件列表 + * @return 排序后的文件列表 + */ + QList> sort( + const QList> &files); + +private: + using CompareFunc = std::function &, + const QSharedPointer &)>; + + /** + * @brief 获取排序比较函数 + */ + CompareFunc getCompareFunc() const; + + /** + * @brief 按名称比较 + */ + bool compareByName( + const QSharedPointer &a, + const QSharedPointer &b) const; + + /** + * @brief 按大小比较 + */ + bool compareBySize( + const QSharedPointer &a, + const QSharedPointer &b) const; + + /** + * @brief 按最后修改时间比较 + */ + bool compareByLastModified( + const QSharedPointer &a, + const QSharedPointer &b) const; + + /** + * @brief 按最后访问时间比较 + */ + bool compareByLastRead( + const QSharedPointer &a, + const QSharedPointer &b) const; + + /** + * @brief 分离目录和文件 + */ + void separateDirAndFile( + QList> &&files, + QList> &dirs, + QList> ®ularFiles) const; + + /** + * @brief 应用排序顺序 + */ + void applySortOrder(QList> &list) const; + +private: + SortConfig m_config; +}; + +END_IO_NAMESPACE + +#endif // DFILESORTER_H diff --git a/src/dfm-io/dfm-io/sort/dsortkeycache.cpp b/src/dfm-io/dfm-io/sort/dsortkeycache.cpp new file mode 100644 index 00000000..003ee411 --- /dev/null +++ b/src/dfm-io/dfm-io/sort/dsortkeycache.cpp @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "dsortkeycache.h" + +USING_IO_NAMESPACE + +DSortKeyCache &DSortKeyCache::instance() +{ + static DSortKeyCache instance; + return instance; +} + +QCollatorSortKey DSortKeyCache::sortKey(const QString &text) +{ + // 先尝试读锁查找缓存 + { + QReadLocker locker(&m_lock); + auto it = m_cache.find(text); + if (it != m_cache.end()) { + return *it; + } + } + + // 缓存未命中,需要创建 + QWriteLocker locker(&m_lock); + + // 双重检查,防止其他线程已经创建 + auto it = m_cache.find(text); + if (it != m_cache.end()) { + return *it; + } + + QCollatorSortKey key = collator().sortKey(text); + m_cache.insert(text, key); + return key; +} + +void DSortKeyCache::clear() +{ + QWriteLocker locker(&m_lock); + m_cache.clear(); +} + +QCollator &DSortKeyCache::collator() +{ + // 线程局部存储,每个线程有自己的 QCollator 实例 + // 避免多线程竞争问题 + thread_local static QCollator s_collator = []() { + QCollator c; + c.setNumericMode(true); // 支持数字自然排序,如 "file2" < "file10" + return c; + }(); + return s_collator; +} + +DSortKeyCache::DSortKeyCache() +{ +} diff --git a/src/dfm-io/dfm-io/sort/dsortkeycache.h b/src/dfm-io/dfm-io/sort/dsortkeycache.h new file mode 100644 index 00000000..48923278 --- /dev/null +++ b/src/dfm-io/dfm-io/sort/dsortkeycache.h @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef DSORTKEYCACHE_H +#define DSORTKEYCACHE_H + +#include + +#include +#include +#include +#include +#include + +BEGIN_IO_NAMESPACE + +/** + * @brief 排序键缓存管理器 + * + * 单例模式,提供线程安全的 QCollatorSortKey 缓存。 + * 使用 QCollator 配置 numericMode 进行国际化自然排序。 + * + * 性能优化: + * - 同一字符串只生成一次排序键 + * - 线程安全的 QCollator 实例(thread_local) + * - 读写锁保护缓存访问 + */ +class DSortKeyCache +{ +public: + /** + * @brief 获取单例实例 + */ + static DSortKeyCache &instance(); + + /** + * @brief 获取或创建字符串的排序键 + * + * 如果缓存中存在则直接返回,否则创建新排序键并缓存。 + * + * @param text 需要排序的文本 + * @return QCollatorSortKey 排序键 + */ + QCollatorSortKey sortKey(const QString &text); + + /** + * @brief 清理缓存 + * + * 在语言环境变化时调用。 + */ + void clear(); + + /** + * @brief 获取配置好的 QCollator 实例 + * + * 线程安全,每个线程有自己的实例。 + * + * @return QCollator& QCollator 引用 + */ + static QCollator &collator(); + +private: + DSortKeyCache(); + ~DSortKeyCache() = default; + DSortKeyCache(const DSortKeyCache &) = delete; + DSortKeyCache &operator=(const DSortKeyCache &) = delete; + + QHash m_cache; + QReadWriteLock m_lock; +}; + +END_IO_NAMESPACE + +#endif // DSORTKEYCACHE_H