diff --git a/syncthingconnector/syncthingconnectionmockhelpers.cpp b/syncthingconnector/syncthingconnectionmockhelpers.cpp index 880f4c3..0af2aa6 100644 --- a/syncthingconnector/syncthingconnectionmockhelpers.cpp +++ b/syncthingconnector/syncthingconnectionmockhelpers.cpp @@ -24,7 +24,7 @@ namespace Data { */ namespace TestData { static bool initialized = false; -static std::string config, status, folderStats, deviceStats, errors, folderStatus, folderStatus2, folderStatus3, pullErrors, connections, version, empty; +static std::string config, status, folderStats, deviceStats, errors, folderStatus, folderStatus2, folderStatus3, pullErrors, connections, version, empty, browse; static std::string events[7]; } // namespace TestData @@ -62,10 +62,10 @@ void setupTestData() // read mock files for REST-API const char *const fileNames[] = { "config", "status", "folderstats", "devicestats", "errors", "folderstatus-01", "folderstatus-02", - "folderstatus-03", "pullerrors-01", "connections", "version", "empty" }; + "folderstatus-03", "pullerrors-01", "connections", "version", "empty", "browse" }; const char *const *fileName = fileNames; for (auto *const testDataVariable : { &config, &status, &folderStats, &deviceStats, &errors, &folderStatus, &folderStatus2, &folderStatus3, - &pullErrors, &connections, &version, &empty }) { + &pullErrors, &connections, &version, &empty, &browse }) { *testDataVariable = readMockFile(testApp.testFilePath(argsToString("mocks/", *fileName, ".json"))); ++fileName; } @@ -171,6 +171,11 @@ MockedReply *MockedReply::forRequest(const QString &method, const QString &path, } else if (folder == QLatin1String("forever-alone")) { buffer = &folderStatus3; } + } else if (path == QLatin1String("db/browse") && !query.hasQueryItem(QStringLiteral("prefix"))) { + const auto folder = query.queryItemValue(QStringLiteral("folder")); + if (folder == QLatin1String("GXWxf-3zgnU")) { + buffer = &browse; + } } else if (path == QLatin1String("folder/pullerrors")) { const QString folder(query.queryItemValue(QStringLiteral("folder"))); if (folder == QLatin1String("GXWxf-3zgnU") && s_eventIndex >= 6) { diff --git a/syncthingconnector/testfiles/mocks/browse.json b/syncthingconnector/testfiles/mocks/browse.json new file mode 100644 index 0000000..63aebdd --- /dev/null +++ b/syncthingconnector/testfiles/mocks/browse.json @@ -0,0 +1,46 @@ +[ + { + "modTime" : "2020-10-02T23:48:52.076996974+02:00", + "name" : "100ANDRO", + "size" : 128, + "type" : "FILE_INFO_TYPE_DIRECTORY" + }, + { + "modTime" : "2020-10-09T13:04:42.4410738+02:00", + "name" : "Camera", + "size" : 128, + "type" : "FILE_INFO_TYPE_DIRECTORY", + "children" : [ + { + "modTime" : "2020-12-16T23:31:34.5009668+01:00", + "name" : "IMG_20201114_124821.jpg", + "size" : 10682189, + "type" : "FILE_INFO_TYPE_FILE" + }, + { + "modTime" : "2020-12-16T23:31:35.0106367+01:00", + "name" : "IMG_20201213_122451.jpg", + "size" : 7936351, + "type" : "FILE_INFO_TYPE_FILE" + }, + { + "modTime" : "2020-12-13T12:25:05.017097469+01:00", + "name" : "IMG_20201213_122504.jpg", + "size" : 8406507, + "type" : "FILE_INFO_TYPE_FILE" + }, + { + "modTime" : "2020-12-13T12:25:06.127097469+01:00", + "name" : "IMG_20201213_122505.jpg", + "size" : 8381931, + "type" : "FILE_INFO_TYPE_FILE" + }, + { + "modTime" : "2020-12-13T12:53:29.707298401+01:00", + "name" : "IMG_20201213_125329.jpg", + "size" : 4388331, + "type" : "FILE_INFO_TYPE_FILE" + } + ] + } +] diff --git a/syncthingmodel/CMakeLists.txt b/syncthingmodel/CMakeLists.txt index 4fdbe52..186712c 100644 --- a/syncthingmodel/CMakeLists.txt +++ b/syncthingmodel/CMakeLists.txt @@ -39,7 +39,8 @@ set(TS_FILES translations/${META_PROJECT_NAME}_zh_CN.ts translations/${META_PROJ translations/${META_PROJECT_NAME}_de_DE.ts translations/${META_PROJECT_NAME}_en_US.ts) set(QT_TESTS models) -set(QT_TEST_SRC_FILES_models syncthingicons.cpp syncthingmodel.cpp syncthingdirectorymodel.cpp syncthingdevicemodel.cpp) +set(QT_TEST_SRC_FILES_models syncthingicons.cpp syncthingmodel.cpp syncthingdirectorymodel.cpp syncthingdevicemodel.cpp + syncthingfilemodel.cpp) # find c++utilities find_package(${PACKAGE_NAMESPACE_PREFIX}c++utilities${CONFIGURATION_PACKAGE_SUFFIX} 5.0.0 REQUIRED) diff --git a/syncthingmodel/syncthingfilemodel.cpp b/syncthingmodel/syncthingfilemodel.cpp index 1dd3008..e9e6a26 100644 --- a/syncthingmodel/syncthingfilemodel.cpp +++ b/syncthingmodel/syncthingfilemodel.cpp @@ -8,8 +8,6 @@ #include -#include - using namespace std; using namespace CppUtilities; @@ -25,8 +23,11 @@ SyncthingFileModel::SyncthingFileModel(SyncthingConnection &connection, const Sy m_root->modificationTime = dir.lastFileTime; m_root->size = dir.globalStats.bytes; m_root->type = SyncthingItemType::Directory; + m_fetchQueue.append(QString()); m_connection.browse(m_dirId, QString(), 1, [this](std::vector> &&items, QString &&errorMessage) { Q_UNUSED(errorMessage) + + m_fetchQueue.removeAll(QString()); if (items.empty()) { return; } @@ -99,7 +100,6 @@ QModelIndex SyncthingFileModel::index(const QString &path) const return res; } } - std::cerr << "index for path " << path.toStdString() << ": " << this->path(res).toStdString() << '\n'; return res; } @@ -137,7 +137,7 @@ QModelIndex SyncthingFileModel::parent(const QModelIndex &child) const if (!childItem) { return QModelIndex(); } - return !childItem->parent ? QModelIndex() : createIndex(static_cast(childItem->index), 0, childItem->parent); + return !childItem->parent ? QModelIndex() : createIndex(static_cast(childItem->parent->index), 0, childItem->parent); } QVariant SyncthingFileModel::headerData(int section, Qt::Orientation orientation, int role) const @@ -263,14 +263,6 @@ static void addLevel(std::vector> &items, int lev addLevel(item->children, level); } } - -static void considerFetched(std::vector> &items) -{ - for (auto &item : items) { - item->childrenPopulated = true; - considerFetched(item->children); - } -} /// \endcond void SyncthingFileModel::fetchMore(const QModelIndex &parent) @@ -314,60 +306,30 @@ void SyncthingFileModel::processFetchQueue() m_dirId, path, 1, [this, p = path](std::vector> &&items, QString &&errorMessage) mutable { Q_UNUSED(errorMessage) - { - const auto refreshedIndex = index(p); - if (!refreshedIndex.isValid()) { - m_fetchQueue.removeAll(p); - processFetchQueue(); - return; - } - auto *const refreshedItem = reinterpret_cast(refreshedIndex.internalPointer()); - if (!refreshedItem->children.empty()) { - if (false && refreshedItem == m_root.get()) { - beginResetModel(); - } else { - considerFetched(refreshedItem->children); - std::cout << "begin remove rows at: " << this->path(refreshedIndex).toStdString() << std::endl; - std::cout << " - from 0 to " << static_cast(refreshedItem->children.size() - 1) << std::endl; - for (int row = 0; row < static_cast(refreshedItem->children.size()); ++row) { - std::cout << " - " << row << " - " << index(row, 0, refreshedIndex).data().toString().toStdString() << std::endl; - } - beginRemoveRows(refreshedIndex, 0, static_cast(refreshedItem->children.size() - 1)); - } - std::cout << "old row count: " << rowCount(refreshedIndex) << std::endl; - refreshedItem->children.clear(); - if (false && refreshedItem == m_root.get()) { - endResetModel(); - } else { - endRemoveRows(); - } - std::cout << "new row count: " << rowCount(refreshedIndex) << std::endl; - } + m_fetchQueue.removeAll(p); + const auto refreshedIndex = index(p); + if (!refreshedIndex.isValid()) { + processFetchQueue(); + return; + } + auto *const refreshedItem = reinterpret_cast(refreshedIndex.internalPointer()); + if (!refreshedItem->children.empty()) { + beginRemoveRows(refreshedIndex, 0, static_cast(refreshedItem->children.size() - 1)); + refreshedItem->children.clear(); + endRemoveRows(); } if (!items.empty()) { - QTimer::singleShot(400, this, [this, p = std::move(p), items = std::move(items)]() mutable { - const auto refreshedIndex = index(p); - if (!refreshedIndex.isValid()) { - m_fetchQueue.removeAll(p); - processFetchQueue(); - return; - } - auto *const refreshedItem = reinterpret_cast(refreshedIndex.internalPointer()); - const auto last = items.size() - 1; - addLevel(items, refreshedItem->level); - for (auto &item : items) { - item->parent = refreshedItem; - } - beginInsertRows( - refreshedIndex, 0, last < std::numeric_limits::max() ? static_cast(last) : std::numeric_limits::max()); - refreshedItem->children = std::move(items); - refreshedItem->childrenPopulated = true; - endInsertRows(); - - m_fetchQueue.removeAll(p); - processFetchQueue(); - }); + const auto last = items.size() - 1; + addLevel(items, refreshedItem->level); + for (auto &item : items) { + item->parent = refreshedItem; + } + beginInsertRows(refreshedIndex, 0, last < std::numeric_limits::max() ? static_cast(last) : std::numeric_limits::max()); + refreshedItem->children = std::move(items); + refreshedItem->childrenPopulated = true; + endInsertRows(); } + processFetchQueue(); }); } diff --git a/syncthingmodel/tests/models.cpp b/syncthingmodel/tests/models.cpp index 55bcb53..8b74287 100644 --- a/syncthingmodel/tests/models.cpp +++ b/syncthingmodel/tests/models.cpp @@ -1,13 +1,14 @@ -#include "../syncthingdirectorymodel.h" #include "../syncthingdevicemodel.h" +#include "../syncthingdirectorymodel.h" +#include "../syncthingfilemodel.h" #include #include #include -#include #include +#include #include @@ -20,6 +21,7 @@ private Q_SLOTS: void testDirectoryModel(); void testDevicesModel(); + void testFileModel(); private: QTimer m_timeout; @@ -38,7 +40,7 @@ void ModelTests::initTestCase() m_timeout.start(); connect(&m_timeout, &QTimer::timeout, this, [this] { m_loop.quit(); - QFAIL("Timeout exceeded when loading mocked config/status for test"); + QFAIL("Timeout exceeded"); }); // request config and status and wait until available @@ -97,5 +99,91 @@ void ModelTests::testDevicesModel() QCOMPARE(model.index(1, 1, dev2Idx).data(), QStringLiteral("dynamic, tcp://192.168.1.3:22000")); } +void ModelTests::testFileModel() +{ + auto row = 0; + const auto *dirInfo = m_connection.findDirInfo(QStringLiteral("GXWxf-3zgnU"), row); + QVERIFY(dirInfo); + + // test behavior of empty/unpopulated model + auto model = Data::SyncthingFileModel(m_connection, *dirInfo); + QCOMPARE(model.rowCount(QModelIndex()), 1); + const auto rootIdx = QPersistentModelIndex(model.index(0, 0)); + QVERIFY(rootIdx.isValid()); + QVERIFY(!model.index(1, 0).isValid()); + QCOMPARE(model.rowCount(rootIdx), 1); + QCOMPARE(model.index(0, 0, rootIdx).data(), QVariant()); + QCOMPARE(model.index(1, 0, rootIdx).data(), QVariant()); + QVERIFY(model.canFetchMore(rootIdx)); + + // wait until the root has been updated + connect(&model, &QAbstractItemModel::rowsInserted, this, [this](const QModelIndex &parent, int first, int last) { + Q_UNUSED(first) + Q_UNUSED(last) + if (!parent.parent().isValid() && parent.row() == 0 && parent.column() == 0) { + m_timeout.stop(); + m_loop.quit(); + } + }); + m_timeout.start(); + m_loop.exec(); + + QVERIFY(rootIdx.isValid()); + QCOMPARE(model.rowCount(rootIdx), 2); + + // test access to nested folders + const auto androidIdx = QPersistentModelIndex(model.index(0, 0, rootIdx)); + const auto cameraIdx = QPersistentModelIndex(model.index(1, 0, rootIdx)); + const auto nestedIdx = QPersistentModelIndex(model.index(0, 0, cameraIdx)); + const auto initialAndroidPtr = androidIdx.constInternalPointer(); + const auto initialCameraPtr = cameraIdx.constInternalPointer(); + QVERIFY(androidIdx.isValid()); + QVERIFY(cameraIdx.isValid()); + QCOMPARE(androidIdx.parent(), rootIdx); + QCOMPARE(cameraIdx.parent(), rootIdx); + QCOMPARE(nestedIdx.parent(), cameraIdx); + QCOMPARE(model.rowCount(androidIdx), 0); + QCOMPARE(model.rowCount(cameraIdx), 5); + QCOMPARE(androidIdx.data(), QStringLiteral("100ANDRO")); + QCOMPARE(cameraIdx.data(), QStringLiteral("Camera")); + QCOMPARE(model.index(0, 0, cameraIdx).data(), QStringLiteral("IMG_20201114_124821.jpg")); + QCOMPARE(model.index(0, 1, cameraIdx).data(), QStringLiteral("10.19 MiB")); + QCOMPARE(model.index(0, 2, cameraIdx).data(), QStringLiteral("2020-12-16 22:31:34.500")); + QCOMPARE(model.index(1, 0, cameraIdx).data(), QStringLiteral("IMG_20201213_122451.jpg")); + QCOMPARE(model.index(2, 0, cameraIdx).data(), QStringLiteral("IMG_20201213_122504.jpg")); + QCOMPARE(model.index(3, 0, cameraIdx).data(), QStringLiteral("IMG_20201213_122505.jpg")); + QCOMPARE(model.index(4, 0, cameraIdx).data(), QStringLiteral("IMG_20201213_125329.jpg")); + QCOMPARE(model.index(5, 0, cameraIdx).data(), QVariant()); + QCOMPARE(model.index(5, 1, cameraIdx).data(), QVariant()); + QCOMPARE(model.index(5, 2, cameraIdx).data(), QVariant()); + QCOMPARE(model.index(5, 3, cameraIdx).data(), QVariant()); + + // test conversion of indexes to/from paths + const auto testPath = QStringLiteral("Camera/IMG_20201213_122504.jpg/"); + const auto testPathIdx = model.index(2, 0, cameraIdx); + QCOMPARE(model.path(testPathIdx), testPath); + QCOMPARE(model.index(testPath), testPathIdx); + + // re-load the data again and wait for the update + model.fetchMore(rootIdx); + m_timeout.start(); + m_loop.exec(); + + // verify that only the root index is still valid (all other indexes have been invalidated) + QVERIFY(rootIdx.isValid()); + QCOMPARE(model.rowCount(rootIdx), 2); + QVERIFY(androidIdx.constInternalPointer() != initialAndroidPtr); + QVERIFY(!androidIdx.isValid()); + QVERIFY(cameraIdx.constInternalPointer() != initialCameraPtr); + QVERIFY(!cameraIdx.isValid()); + QVERIFY(!nestedIdx.isValid()); + + // verify that data was re-loaded + const auto androidIdx2 = QPersistentModelIndex(model.index(0, 0, rootIdx)); + const auto cameraIdx2 = QPersistentModelIndex(model.index(1, 0, rootIdx)); + QCOMPARE(androidIdx2.data(), QStringLiteral("100ANDRO")); + QCOMPARE(cameraIdx2.data(), QStringLiteral("Camera")); +} + QTEST_MAIN(ModelTests) #include "models.moc"