chore(Storybook): Exclude DirectoryFilesWatcher from PagesModel, add tests

This commit is contained in:
Michał Cieślak 2023-10-09 22:26:50 +02:00 committed by Michał
parent a567910f3b
commit 5556c2a52f
6 changed files with 384 additions and 105 deletions

View File

@ -45,6 +45,7 @@ set(PROJECT_LIB "${PROJECT_NAME}Lib")
add_library(${PROJECT_LIB} add_library(${PROJECT_LIB}
cachecleaner.cpp cachecleaner.h cachecleaner.cpp cachecleaner.h
directorieswatcher.cpp directorieswatcher.h directorieswatcher.cpp directorieswatcher.h
directoryfileswatcher.cpp directoryfileswatcher.h
figmaio.cpp figmaio.h figmaio.cpp figmaio.h
figmalinks.cpp figmalinks.h figmalinks.cpp figmalinks.h
figmalinksmodel.cpp figmalinksmodel.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}) target_link_libraries(SectionsDecoratorModelTest PRIVATE Qt5::Test ${PROJECT_LIB})
add_test(NAME SectionsDecoratorModelTest COMMAND SectionsDecoratorModelTest) 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 add_executable(QmlTests
qmlTests/main.cpp qmlTests/main.cpp
qmlTests/src/TextUtils.cpp qmlTests/src/TextUtils.h qmlTests/src/TextUtils.cpp qmlTests/src/TextUtils.h

View File

@ -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);
}

View File

@ -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;
};

View File

@ -1,10 +1,10 @@
#include "pagesmodel.h" #include "pagesmodel.h"
#include <QDir> #include <QFileInfo>
#include <QFileSystemWatcher>
#include <QRegularExpression> #include <QRegularExpression>
#include <unordered_map> #include "directoryfileswatcher.h"
namespace { namespace {
const auto categoryUncategorized QStringLiteral("Uncategorized"); const auto categoryUncategorized QStringLiteral("Uncategorized");
@ -12,62 +12,37 @@ const auto categoryUncategorized QStringLiteral("Uncategorized");
PagesModel::PagesModel(const QString &path, QObject *parent) PagesModel::PagesModel(const QString &path, QObject *parent)
: QAbstractListModel{parent}, m_path{path}, : QAbstractListModel{parent}, m_path{path},
fsWatcher(new QFileSystemWatcher(this)) m_pagesWatcher(new DirectoryFilesWatcher(
path, QStringLiteral("*Page.qml"), this))
{ {
m_items = load(); m_items = readMetadata(m_pagesWatcher->files());
readMetadata(m_items);
for (const auto& item : qAsConst(m_items)) { for (const auto& item : qAsConst(m_items))
setFigmaLinks(item.title, item.figmaLinks); setFigmaLinks(item.title, item.figmaLinks);
connect(m_pagesWatcher, &DirectoryFilesWatcher::filesChanged,
this, &PagesModel::onPagesChanged);
} }
fsWatcher->addPath(path); PagesModelItem PagesModel::readMetadata(const QString& path)
{
connect(fsWatcher, &QFileSystemWatcher::directoryChanged,
this, &PagesModel::reload);
}
QList<PagesModelItem> PagesModel::load() const {
static QRegularExpression fileNameRegex(
QRegularExpression::anchoredPattern("(.*)Page\\.qml"));
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; PagesModelItem item;
item.path = fileInfo.filePath(); item.path = path;
item.title = fileNameMatch.captured(1); item.title = QFileInfo(path).fileName().chopped(
item.lastModified = fileInfo.lastModified(); (QStringLiteral("Page.qml").size()));
items << item; QFile file(path);
});
return items;
}
void PagesModel::readMetadata(PagesModelItem& item) {
static QRegularExpression categoryRegex(
"^//(\\s)*category:(.+)$", QRegularExpression::MultilineOption);
QFile file(item.path);
file.open(QIODevice::ReadOnly); file.open(QIODevice::ReadOnly);
QByteArray content = file.readAll(); QByteArray content = file.readAll();
static QRegularExpression categoryRegex(
"^//(\\s)*category:(.+)$", QRegularExpression::MultilineOption);
QRegularExpressionMatch categoryMatch = categoryRegex.match(content); QRegularExpressionMatch categoryMatch = categoryRegex.match(content);
QString category = categoryMatch.hasMatch() QString category = categoryMatch.hasMatch()
? categoryMatch.captured(2).trimmed() : categoryUncategorized; ? categoryMatch.captured(2).trimmed() : categoryUncategorized;
item.category = category; item.category = category.size() ? category : categoryUncategorized;
static QRegularExpression figmaRegex( static QRegularExpression figmaRegex(
"^(\\/\\/\\s*)((?:https:\\/\\/)?(?:www\\.)?figma\\.com\\/.*)$", "^(\\/\\/\\s*)((?:https:\\/\\/)?(?:www\\.)?figma\\.com\\/.*)$",
@ -83,90 +58,78 @@ void PagesModel::readMetadata(PagesModelItem& item) {
} }
item.figmaLinks = links; item.figmaLinks = links;
return item;
} }
void PagesModel::readMetadata(QList<PagesModelItem> &items) { QList<PagesModelItem> PagesModel::readMetadata(const QStringList &paths)
std::for_each(items.begin(), items.end(), [](auto&item) { {
readMetadata(item); QList<PagesModelItem> metadata;
}); metadata.reserve(paths.size());
}
void PagesModel::reload() { std::transform(paths.begin(), paths.end(), std::back_inserter(metadata),
const QList<PagesModelItem> currentItems = load(); [](auto& path) {
std::unordered_map<QString, PagesModelItem> mapping; return readMetadata(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); return metadata;
}
beginRemoveRows(QModelIndex{}, index, index); void PagesModel::onPagesChanged(const QStringList& added,
const QStringList& removed,
const QStringList& changed)
{
for (auto& path : removed) {
auto index = getIndexByPath(path);
beginRemoveRows({}, index, index);
m_items.removeAt(index); m_items.removeAt(index);
endRemoveRows(); endRemoveRows();
} }
if (newItems.size()) { if (added.size()) {
beginInsertRows(QModelIndex{}, rowCount(), rowCount() + newItems.size() - 1); beginInsertRows({}, rowCount(), rowCount() + added.size() - 1);
for (auto& item : newItems) { auto metadata = readMetadata(added);
readMetadata(item);
m_items << item;
}
for (const auto& item : qAsConst(metadata))
setFigmaLinks(item.title, item.figmaLinks);
m_items << metadata;
endInsertRows(); endInsertRows();
} }
for (auto& item : changedItems) { for (auto& path : changed) {
auto it = std::find_if(m_items.begin(), m_items.end(), [&item](auto& it){ auto index = getIndexByPath(path);
return it.title == item.title; const auto& previous = m_items.at(index);
});
auto index = std::distance(m_items.begin(), it); PagesModelItem metadata = readMetadata(path);
const auto& previous = *it; setFigmaLinks(metadata.title, metadata.figmaLinks);
readMetadata(item);
setFigmaLinks(item.title, item.figmaLinks);
if (previous.category != item.category) { if (previous.category != metadata.category) {
// For simplicity category change is handled by removing and // For simplicity category change is handled by removing and
// adding item. In the future it can be changed to regular dataChanged // adding item. In the future it can be changed to regular dataChanged
// event and handled properly in upstream models like SectionSDecoratorModel. // event and handled properly in upstream models like SectionSDecoratorModel.
beginRemoveRows(QModelIndex{}, index, index); beginRemoveRows({}, index, index);
m_items.removeAt(index); m_items.removeAt(index);
endRemoveRows(); endRemoveRows();
beginInsertRows(QModelIndex{}, rowCount(), rowCount()); beginInsertRows({}, rowCount(), rowCount());
m_items << item; m_items << metadata;
endInsertRows(); 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 QHash<int, QByteArray> PagesModel::roleNames() const
{ {
static const QHash<int,QByteArray> roles { static const QHash<int,QByteArray> roles {

View File

@ -5,7 +5,7 @@
#include "figmalinksmodel.h" #include "figmalinksmodel.h"
class QFileSystemWatcher; class DirectoryFilesWatcher;
struct PagesModelItem { struct PagesModelItem {
QString path; QString path;
@ -31,9 +31,15 @@ public:
int rowCount(const QModelIndex &parent = QModelIndex()) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role) const override; QVariant data(const QModelIndex &index, int role) const override;
void reload();
private: 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(PagesModelItem &item);
static void readMetadata(QList<PagesModelItem> &items); static void readMetadata(QList<PagesModelItem> &items);
@ -43,5 +49,5 @@ private:
QString m_path; QString m_path;
QList<PagesModelItem> m_items; QList<PagesModelItem> m_items;
QMap<QString, FigmaLinksModel*> m_figmaSubmodels; QMap<QString, FigmaLinksModel*> m_figmaSubmodels;
QFileSystemWatcher* fsWatcher; DirectoryFilesWatcher* m_pagesWatcher;
}; };

View File

@ -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"