chore(Storybook): Exclude DirectoryFilesWatcher from PagesModel, add tests
This commit is contained in:
parent
a567910f3b
commit
5556c2a52f
|
@ -45,6 +45,7 @@ set(PROJECT_LIB "${PROJECT_NAME}Lib")
|
|||
add_library(${PROJECT_LIB}
|
||||
cachecleaner.cpp cachecleaner.h
|
||||
directorieswatcher.cpp directorieswatcher.h
|
||||
directoryfileswatcher.cpp directoryfileswatcher.h
|
||||
figmaio.cpp figmaio.h
|
||||
figmalinks.cpp figmalinks.h
|
||||
figmalinksmodel.cpp figmalinksmodel.h
|
||||
|
@ -96,6 +97,10 @@ add_executable(SectionsDecoratorModelTest tests/tst_SectionsDecoratorModel.cpp)
|
|||
target_link_libraries(SectionsDecoratorModelTest PRIVATE Qt5::Test ${PROJECT_LIB})
|
||||
add_test(NAME SectionsDecoratorModelTest COMMAND SectionsDecoratorModelTest)
|
||||
|
||||
add_executable(DirectoryFilesWatcherTest tests/tst_DirectoryFilesWatcher.cpp)
|
||||
target_link_libraries(DirectoryFilesWatcherTest PRIVATE Qt5::Test ${PROJECT_LIB})
|
||||
add_test(NAME DirectoryFilesWatcherTest COMMAND DirectoryFilesWatcherTest)
|
||||
|
||||
add_executable(QmlTests
|
||||
qmlTests/main.cpp
|
||||
qmlTests/src/TextUtils.cpp qmlTests/src/TextUtils.h
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
#include "directoryfileswatcher.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFileSystemWatcher>
|
||||
|
||||
DirectoryFilesWatcher::DirectoryFilesWatcher(
|
||||
const QString &path, const QString &pattern, QObject *parent)
|
||||
: QObject{parent}, m_path{path}, m_pattern{pattern},
|
||||
m_fsWatcher{new QFileSystemWatcher(this)}
|
||||
{
|
||||
auto files = readDirectory();
|
||||
m_files.reserve(files.size());
|
||||
m_files.insert(std::move_iterator(files.begin()),
|
||||
std::move_iterator(files.end()));
|
||||
|
||||
m_fsWatcher->addPath(path);
|
||||
|
||||
connect(m_fsWatcher, &QFileSystemWatcher::directoryChanged,
|
||||
this, &DirectoryFilesWatcher::onDirectoryChanged);
|
||||
}
|
||||
|
||||
QStringList DirectoryFilesWatcher::files() const
|
||||
{
|
||||
QStringList list;
|
||||
list.reserve(m_files.size());
|
||||
|
||||
for (auto& [file, _] : m_files)
|
||||
list << file;
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
std::vector<std::pair<QString, QDateTime>> DirectoryFilesWatcher::readDirectory() const
|
||||
{
|
||||
QDir dir(m_path);
|
||||
dir.setFilter(QDir::Files);
|
||||
dir.setNameFilters({m_pattern});
|
||||
|
||||
const QFileInfoList filesInfo = dir.entryInfoList();
|
||||
std::vector<std::pair<QString, QDateTime>> files;
|
||||
files.reserve(filesInfo.size());
|
||||
|
||||
std::transform(filesInfo.begin(), filesInfo.end(),
|
||||
std::back_inserter(files),
|
||||
[] (auto &info) {
|
||||
return std::make_pair(info.filePath(), info.lastModified());
|
||||
});
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
void DirectoryFilesWatcher::onDirectoryChanged() {
|
||||
auto files = readDirectory();
|
||||
|
||||
QStringList added;
|
||||
QStringList removed;
|
||||
QStringList changed;
|
||||
|
||||
for (auto& [file, date] : files) {
|
||||
auto it = m_files.find(file);
|
||||
|
||||
if (it == m_files.end()) {
|
||||
added.push_back(file);
|
||||
} else {
|
||||
if (date != it->second)
|
||||
changed.push_back(file);
|
||||
|
||||
m_files.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& [file, date] : m_files)
|
||||
removed.push_back(file);
|
||||
|
||||
m_files.clear();
|
||||
m_files.reserve(files.size());
|
||||
m_files.insert(std::move_iterator(files.begin()),
|
||||
std::move_iterator(files.end()));
|
||||
|
||||
emit filesChanged(added, removed, changed);
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
#pragma once
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QObject>
|
||||
#include <QStringList>
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
class QFileSystemWatcher;
|
||||
|
||||
class DirectoryFilesWatcher : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit DirectoryFilesWatcher(const QString &path, const QString &pattern,
|
||||
QObject *parent = nullptr);
|
||||
|
||||
QStringList files() const;
|
||||
|
||||
signals:
|
||||
void filesChanged(const QStringList& added, const QStringList& removed,
|
||||
const QStringList& changed);
|
||||
|
||||
private:
|
||||
std::vector<std::pair<QString, QDateTime>> readDirectory() const;
|
||||
void onDirectoryChanged();
|
||||
|
||||
QString m_path;
|
||||
QString m_pattern;
|
||||
std::unordered_map<QString, QDateTime> m_files;
|
||||
QFileSystemWatcher* m_fsWatcher;
|
||||
};
|
|
@ -1,10 +1,10 @@
|
|||
#include "pagesmodel.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFileSystemWatcher>
|
||||
#include <QFileInfo>
|
||||
#include <QRegularExpression>
|
||||
|
||||
#include <unordered_map>
|
||||
#include "directoryfileswatcher.h"
|
||||
|
||||
|
||||
namespace {
|
||||
const auto categoryUncategorized QStringLiteral("Uncategorized");
|
||||
|
@ -12,62 +12,37 @@ const auto categoryUncategorized QStringLiteral("Uncategorized");
|
|||
|
||||
PagesModel::PagesModel(const QString &path, QObject *parent)
|
||||
: QAbstractListModel{parent}, m_path{path},
|
||||
fsWatcher(new QFileSystemWatcher(this))
|
||||
m_pagesWatcher(new DirectoryFilesWatcher(
|
||||
path, QStringLiteral("*Page.qml"), this))
|
||||
{
|
||||
m_items = load();
|
||||
readMetadata(m_items);
|
||||
m_items = readMetadata(m_pagesWatcher->files());
|
||||
|
||||
for (const auto& item : qAsConst(m_items)) {
|
||||
for (const auto& item : qAsConst(m_items))
|
||||
setFigmaLinks(item.title, item.figmaLinks);
|
||||
}
|
||||
|
||||
fsWatcher->addPath(path);
|
||||
|
||||
connect(fsWatcher, &QFileSystemWatcher::directoryChanged,
|
||||
this, &PagesModel::reload);
|
||||
connect(m_pagesWatcher, &DirectoryFilesWatcher::filesChanged,
|
||||
this, &PagesModel::onPagesChanged);
|
||||
}
|
||||
|
||||
QList<PagesModelItem> PagesModel::load() const {
|
||||
static QRegularExpression fileNameRegex(
|
||||
QRegularExpression::anchoredPattern("(.*)Page\\.qml"));
|
||||
PagesModelItem PagesModel::readMetadata(const QString& path)
|
||||
{
|
||||
PagesModelItem item;
|
||||
item.path = path;
|
||||
item.title = QFileInfo(path).fileName().chopped(
|
||||
(QStringLiteral("Page.qml").size()));
|
||||
|
||||
QDir dir(m_path);
|
||||
dir.setFilter(QDir::Files);
|
||||
|
||||
const QFileInfoList files = dir.entryInfoList();
|
||||
QList<PagesModelItem> items;
|
||||
|
||||
std::for_each(files.begin(), files.end(), [this, &items] (auto &fileInfo) {
|
||||
QString fileName = fileInfo.fileName();
|
||||
QRegularExpressionMatch fileNameMatch = fileNameRegex.match(fileName);
|
||||
|
||||
if (!fileNameMatch.hasMatch())
|
||||
return;
|
||||
|
||||
PagesModelItem item;
|
||||
item.path = fileInfo.filePath();
|
||||
item.title = fileNameMatch.captured(1);
|
||||
item.lastModified = fileInfo.lastModified();
|
||||
|
||||
items << item;
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
void PagesModel::readMetadata(PagesModelItem& item) {
|
||||
static QRegularExpression categoryRegex(
|
||||
"^//(\\s)*category:(.+)$", QRegularExpression::MultilineOption);
|
||||
|
||||
QFile file(item.path);
|
||||
QFile file(path);
|
||||
file.open(QIODevice::ReadOnly);
|
||||
QByteArray content = file.readAll();
|
||||
|
||||
static QRegularExpression categoryRegex(
|
||||
"^//(\\s)*category:(.+)$", QRegularExpression::MultilineOption);
|
||||
|
||||
QRegularExpressionMatch categoryMatch = categoryRegex.match(content);
|
||||
QString category = categoryMatch.hasMatch()
|
||||
? categoryMatch.captured(2).trimmed() : categoryUncategorized;
|
||||
|
||||
item.category = category;
|
||||
item.category = category.size() ? category : categoryUncategorized;
|
||||
|
||||
static QRegularExpression figmaRegex(
|
||||
"^(\\/\\/\\s*)((?:https:\\/\\/)?(?:www\\.)?figma\\.com\\/.*)$",
|
||||
|
@ -83,90 +58,78 @@ void PagesModel::readMetadata(PagesModelItem& item) {
|
|||
}
|
||||
|
||||
item.figmaLinks = links;
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
void PagesModel::readMetadata(QList<PagesModelItem> &items) {
|
||||
std::for_each(items.begin(), items.end(), [](auto&item) {
|
||||
readMetadata(item);
|
||||
QList<PagesModelItem> PagesModel::readMetadata(const QStringList &paths)
|
||||
{
|
||||
QList<PagesModelItem> metadata;
|
||||
metadata.reserve(paths.size());
|
||||
|
||||
std::transform(paths.begin(), paths.end(), std::back_inserter(metadata),
|
||||
[](auto& path) {
|
||||
return readMetadata(path);
|
||||
});
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
void PagesModel::reload() {
|
||||
const QList<PagesModelItem> currentItems = load();
|
||||
std::unordered_map<QString, PagesModelItem> mapping;
|
||||
void PagesModel::onPagesChanged(const QStringList& added,
|
||||
const QStringList& removed,
|
||||
const QStringList& changed)
|
||||
{
|
||||
for (auto& path : removed) {
|
||||
auto index = getIndexByPath(path);
|
||||
|
||||
for (const PagesModelItem &item : qAsConst(m_items))
|
||||
mapping[item.title] = item;
|
||||
|
||||
std::vector<PagesModelItem> newItems;
|
||||
std::vector<PagesModelItem> changedItems;
|
||||
std::vector<PagesModelItem> removedItems;
|
||||
|
||||
for (const auto &item : currentItems) {
|
||||
auto it = mapping.find(item.title);
|
||||
|
||||
if (it == mapping.end()) {
|
||||
newItems.push_back(item);
|
||||
} else {
|
||||
if (item.lastModified != it->second.lastModified)
|
||||
changedItems.push_back(item);
|
||||
|
||||
mapping.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& [key, value] : mapping)
|
||||
removedItems.push_back(value);
|
||||
|
||||
for (auto& item : removedItems) {
|
||||
|
||||
auto it = std::find_if(m_items.begin(), m_items.end(), [&item](auto& it){
|
||||
return it.title == item.title;
|
||||
});
|
||||
|
||||
auto index = std::distance(m_items.begin(), it);
|
||||
|
||||
beginRemoveRows(QModelIndex{}, index, index);
|
||||
beginRemoveRows({}, index, index);
|
||||
m_items.removeAt(index);
|
||||
endRemoveRows();
|
||||
}
|
||||
|
||||
if (newItems.size()) {
|
||||
beginInsertRows(QModelIndex{}, rowCount(), rowCount() + newItems.size() - 1);
|
||||
if (added.size()) {
|
||||
beginInsertRows({}, rowCount(), rowCount() + added.size() - 1);
|
||||
|
||||
for (auto& item : newItems) {
|
||||
readMetadata(item);
|
||||
m_items << item;
|
||||
}
|
||||
auto metadata = readMetadata(added);
|
||||
|
||||
for (const auto& item : qAsConst(metadata))
|
||||
setFigmaLinks(item.title, item.figmaLinks);
|
||||
|
||||
m_items << metadata;
|
||||
endInsertRows();
|
||||
}
|
||||
|
||||
for (auto& item : changedItems) {
|
||||
auto it = std::find_if(m_items.begin(), m_items.end(), [&item](auto& it){
|
||||
return it.title == item.title;
|
||||
});
|
||||
for (auto& path : changed) {
|
||||
auto index = getIndexByPath(path);
|
||||
const auto& previous = m_items.at(index);
|
||||
|
||||
auto index = std::distance(m_items.begin(), it);
|
||||
const auto& previous = *it;
|
||||
readMetadata(item);
|
||||
setFigmaLinks(item.title, item.figmaLinks);
|
||||
PagesModelItem metadata = readMetadata(path);
|
||||
setFigmaLinks(metadata.title, metadata.figmaLinks);
|
||||
|
||||
if (previous.category != item.category) {
|
||||
if (previous.category != metadata.category) {
|
||||
// For simplicity category change is handled by removing and
|
||||
// adding item. In the future it can be changed to regular dataChanged
|
||||
// event and handled properly in upstream models like SectionSDecoratorModel.
|
||||
beginRemoveRows(QModelIndex{}, index, index);
|
||||
beginRemoveRows({}, index, index);
|
||||
m_items.removeAt(index);
|
||||
endRemoveRows();
|
||||
|
||||
beginInsertRows(QModelIndex{}, rowCount(), rowCount());
|
||||
m_items << item;
|
||||
beginInsertRows({}, rowCount(), rowCount());
|
||||
m_items << metadata;
|
||||
endInsertRows();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int PagesModel::getIndexByPath(const QString& path) const
|
||||
{
|
||||
auto it = std::find_if(m_items.begin(), m_items.end(), [&path](auto& it) {
|
||||
return it.path == path;
|
||||
});
|
||||
assert(it != m_items.end());
|
||||
return std::distance(m_items.begin(), it);
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> PagesModel::roleNames() const
|
||||
{
|
||||
static const QHash<int,QByteArray> roles {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
#include "figmalinksmodel.h"
|
||||
|
||||
class QFileSystemWatcher;
|
||||
class DirectoryFilesWatcher;
|
||||
|
||||
struct PagesModelItem {
|
||||
QString path;
|
||||
|
@ -31,9 +31,15 @@ public:
|
|||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role) const override;
|
||||
|
||||
void reload();
|
||||
|
||||
private:
|
||||
QList<PagesModelItem> load() const;
|
||||
void onPagesChanged(const QStringList& added, const QStringList& removed,
|
||||
const QStringList& changed);
|
||||
|
||||
int getIndexByPath(const QString& path) const;
|
||||
|
||||
static PagesModelItem readMetadata(const QString& path);
|
||||
static QList<PagesModelItem> readMetadata(const QStringList& paths);
|
||||
|
||||
static void readMetadata(PagesModelItem &item);
|
||||
static void readMetadata(QList<PagesModelItem> &items);
|
||||
|
@ -43,5 +49,5 @@ private:
|
|||
QString m_path;
|
||||
QList<PagesModelItem> m_items;
|
||||
QMap<QString, FigmaLinksModel*> m_figmaSubmodels;
|
||||
QFileSystemWatcher* fsWatcher;
|
||||
DirectoryFilesWatcher* m_pagesWatcher;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,192 @@
|
|||
#include <QSignalSpy>
|
||||
#include <QTest>
|
||||
|
||||
#include <QTemporaryDir>
|
||||
#include <directoryfileswatcher.h>
|
||||
|
||||
|
||||
class TestDirectoryFilesWatcher: public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void emptyDirTest()
|
||||
{
|
||||
QTemporaryDir dir;
|
||||
QVERIFY(dir.isValid());
|
||||
|
||||
DirectoryFilesWatcher watcher(dir.path(), "*");
|
||||
QVERIFY(watcher.files().empty());
|
||||
}
|
||||
|
||||
void nonEmptyDirTest()
|
||||
{
|
||||
QTemporaryDir dir;
|
||||
|
||||
QString filename = dir.path() + "/Data.txt";
|
||||
{
|
||||
QFile file(filename);
|
||||
if (file.open(QIODevice::ReadWrite)) {
|
||||
QTextStream stream(&file);
|
||||
stream << "something";
|
||||
}
|
||||
}
|
||||
|
||||
QVERIFY(dir.isValid());
|
||||
|
||||
DirectoryFilesWatcher watcher(dir.path(), "*");
|
||||
QCOMPARE(watcher.files().size(), 1);
|
||||
QCOMPARE(watcher.files().at(0), filename);
|
||||
}
|
||||
|
||||
void notifyAddTest()
|
||||
{
|
||||
QTemporaryDir dir;
|
||||
QVERIFY(dir.isValid());
|
||||
|
||||
DirectoryFilesWatcher watcher(dir.path(), "*");
|
||||
|
||||
QSignalSpy changeSpy(&watcher, &DirectoryFilesWatcher::filesChanged);
|
||||
|
||||
|
||||
QString filename = dir.path() + "/Data.txt";
|
||||
{
|
||||
QFile file(filename);
|
||||
if (file.open(QIODevice::ReadWrite)) {
|
||||
QTextStream stream(&file);
|
||||
stream << "something";
|
||||
}
|
||||
}
|
||||
|
||||
changeSpy.wait();
|
||||
QCOMPARE(changeSpy.count(), 1);
|
||||
QCOMPARE(changeSpy.at(0).at(0).toStringList(), { filename });
|
||||
QCOMPARE(changeSpy.at(0).at(1).toStringList(), { });
|
||||
QCOMPARE(changeSpy.at(0).at(2).toStringList(), { });
|
||||
}
|
||||
|
||||
void notifyMultipleAddTest()
|
||||
{
|
||||
QTemporaryDir dir;
|
||||
QVERIFY(dir.isValid());
|
||||
|
||||
DirectoryFilesWatcher watcher(dir.path(), "*");
|
||||
|
||||
QSignalSpy changeSpy(&watcher, &DirectoryFilesWatcher::filesChanged);
|
||||
|
||||
|
||||
QString filename = dir.path() + "/Data.txt";
|
||||
{
|
||||
QFile file(filename);
|
||||
if (file.open(QIODevice::ReadWrite)) {
|
||||
QTextStream stream(&file);
|
||||
stream << "something";
|
||||
}
|
||||
}
|
||||
|
||||
QString filename2 = dir.path() + "/Data2.txt";
|
||||
{
|
||||
QFile file(filename2);
|
||||
if (file.open(QIODevice::ReadWrite)) {
|
||||
QTextStream stream(&file);
|
||||
stream << "something";
|
||||
}
|
||||
}
|
||||
|
||||
changeSpy.wait();
|
||||
QCOMPARE(changeSpy.count(), 1);
|
||||
QCOMPARE(changeSpy.at(0).at(0).toStringList(),
|
||||
QStringList({ filename, filename2 }));
|
||||
QCOMPARE(changeSpy.at(0).at(1).toStringList(), { });
|
||||
QCOMPARE(changeSpy.at(0).at(2).toStringList(), { });
|
||||
}
|
||||
|
||||
void notifyRemoveTest()
|
||||
{
|
||||
QTemporaryDir dir;
|
||||
QVERIFY(dir.isValid());
|
||||
|
||||
QString filename = dir.path() + "/Data.txt";
|
||||
{
|
||||
QFile file(filename);
|
||||
if (file.open(QIODevice::ReadWrite)) {
|
||||
QTextStream stream(&file);
|
||||
stream << "something";
|
||||
}
|
||||
}
|
||||
|
||||
QString filename2 = dir.path() + "/Data2.txt";
|
||||
{
|
||||
QFile file(filename2);
|
||||
if (file.open(QIODevice::ReadWrite)) {
|
||||
QTextStream stream(&file);
|
||||
stream << "something";
|
||||
}
|
||||
}
|
||||
|
||||
DirectoryFilesWatcher watcher(dir.path(), "*");
|
||||
|
||||
QSignalSpy changeSpy(&watcher, &DirectoryFilesWatcher::filesChanged);
|
||||
|
||||
QVERIFY(QFile::remove(filename));
|
||||
|
||||
|
||||
changeSpy.wait();
|
||||
QCOMPARE(changeSpy.count(), 1);
|
||||
QCOMPARE(changeSpy.at(0).at(0).toStringList(), { });
|
||||
QCOMPARE(changeSpy.at(0).at(1).toStringList(), { filename });
|
||||
QCOMPARE(changeSpy.at(0).at(2).toStringList(), { });
|
||||
}
|
||||
|
||||
void notifyChangeTest()
|
||||
{
|
||||
QTemporaryDir dir;
|
||||
QVERIFY(dir.isValid());
|
||||
|
||||
QString filename = dir.path() + "/Data.txt";
|
||||
{
|
||||
QFile file(filename);
|
||||
if (file.open(QIODevice::ReadWrite)) {
|
||||
QTextStream stream(&file);
|
||||
stream << "something";
|
||||
}
|
||||
}
|
||||
|
||||
QString filename2 = dir.path() + "/Data2.txt";
|
||||
{
|
||||
QFile file(filename2);
|
||||
if (file.open(QIODevice::ReadWrite)) {
|
||||
QTextStream stream(&file);
|
||||
stream << "something";
|
||||
}
|
||||
}
|
||||
|
||||
DirectoryFilesWatcher watcher(dir.path(), "*");
|
||||
|
||||
QSignalSpy changeSpy(&watcher, &DirectoryFilesWatcher::filesChanged);
|
||||
|
||||
// wait a bit to have a different timestamp for tmp file
|
||||
QTest::qSleep(5);
|
||||
|
||||
QString tempraryFilename = dir.path() + "/Tmp.txt";
|
||||
{
|
||||
QFile file(tempraryFilename);
|
||||
if (file.open(QIODevice::ReadWrite)) {
|
||||
QTextStream stream(&file);
|
||||
stream << "something else";
|
||||
}
|
||||
}
|
||||
|
||||
QFile::remove(filename);
|
||||
QFile::rename(tempraryFilename, filename);
|
||||
|
||||
changeSpy.wait();
|
||||
QCOMPARE(changeSpy.count(), 1);
|
||||
QCOMPARE(changeSpy.at(0).at(0).toStringList(), { });
|
||||
QCOMPARE(changeSpy.at(0).at(1).toStringList(), { });
|
||||
QCOMPARE(changeSpy.at(0).at(2).toStringList(), { filename });
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_MAIN(TestDirectoryFilesWatcher)
|
||||
#include "tst_DirectoryFilesWatcher.moc"
|
Loading…
Reference in New Issue