diff --git a/src/plugins/qmldesigner/designercore/include/projectstorageids.h b/src/plugins/qmldesigner/designercore/include/projectstorageids.h index 5b25f128d2e..5e39fcdd864 100644 --- a/src/plugins/qmldesigner/designercore/include/projectstorageids.h +++ b/src/plugins/qmldesigner/designercore/include/projectstorageids.h @@ -84,7 +84,8 @@ enum class BasicIdType { SignalDeclaration, EnumerationDeclaration, Import, - TypeName + TypeName, + ProjectPartId }; using TypeId = BasicId; @@ -114,4 +115,7 @@ using ImportIds = std::vector; using TypeNameId = BasicId; using TypeNameIds = std::vector; +using ProjectPartId = BasicId; +using ProjectPartIds = std::vector; + } // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/designercore/projectstorage/directorypathcompressor.h b/src/plugins/qmldesigner/designercore/projectstorage/directorypathcompressor.h new file mode 100644 index 00000000000..5b200d4d4fc --- /dev/null +++ b/src/plugins/qmldesigner/designercore/projectstorage/directorypathcompressor.h @@ -0,0 +1,83 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "projectstorageids.h" + +#include +#include + +#include + +#include + +namespace QmlDesigner { + +template +class DirectoryPathCompressor +{ +public: + DirectoryPathCompressor() { m_timer.setSingleShot(true); } + + virtual ~DirectoryPathCompressor() = default; + + void addSourceContextId(SourceContextId sourceContextId) + { + auto found = std::lower_bound(m_sourceContextIds.begin(), + m_sourceContextIds.end(), + sourceContextId); + + if (found == m_sourceContextIds.end() || *found != sourceContextId) + m_sourceContextIds.insert(found, sourceContextId); + + restartTimer(); + } + + SourceContextIds takeSourceContextIds() { return std::move(m_sourceContextIds); } + + virtual void setCallback(std::function &&callback) + { + QObject::connect(&m_timer, &Timer::timeout, [this, callback = std::move(callback)] { + callback(takeSourceContextIds()); + }); + } + + virtual void restartTimer() + { + m_timer.start(20); + } + + Timer &timer() + { + return m_timer; + } + +private: + SourceContextIds m_sourceContextIds; + Timer m_timer; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/designercore/projectstorage/filestatus.h b/src/plugins/qmldesigner/designercore/projectstorage/filestatus.h new file mode 100644 index 00000000000..ccc3264bdd3 --- /dev/null +++ b/src/plugins/qmldesigner/designercore/projectstorage/filestatus.h @@ -0,0 +1,65 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "projectstorageids.h" + +#include +#include +#include + +namespace QmlDesigner { + +class FileStatus +{ +public: + FileStatus(SourceId sourceId, off_t size, std::time_t lastModified) + : sourceId(sourceId) + , size(size) + , lastModified(lastModified) + {} + + friend + bool operator==(const FileStatus &first, const FileStatus &second) + { + return first.sourceId == second.sourceId && first.size == second.size + && first.lastModified == second.lastModified; + } + + friend + bool operator<(const FileStatus &first, const FileStatus &second) + { + return first.sourceId < second.sourceId; + } + +public: + SourceId sourceId; + off_t size; + std::time_t lastModified; +}; + +using FileStatuses = std::vector; +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/designercore/projectstorage/filestatuscache.cpp b/src/plugins/qmldesigner/designercore/projectstorage/filestatuscache.cpp new file mode 100644 index 00000000000..57e1d4cee18 --- /dev/null +++ b/src/plugins/qmldesigner/designercore/projectstorage/filestatuscache.cpp @@ -0,0 +1,136 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "filestatuscache.h" +#include "filesystem.h" + +#include +#include + +#include +#include + +namespace QmlDesigner { + +long long FileStatusCache::lastModifiedTime(SourceId sourceId) const +{ + return findEntry(sourceId).lastModified; +} + +void FileStatusCache::update(SourceId sourceId) +{ + auto found = std::lower_bound(m_cacheEntries.begin(), + m_cacheEntries.end(), + Internal::FileStatusCacheEntry{sourceId}, + [](const auto &first, const auto &second) { + return first.sourceId < second.sourceId; + }); + + if (found != m_cacheEntries.end() && found->sourceId == sourceId) + found->lastModified = m_fileSystem.lastModified(sourceId); +} + +void FileStatusCache::update(SourceIds sourceIds) +{ + std::set_intersection(m_cacheEntries.begin(), + m_cacheEntries.end(), + sourceIds.begin(), + sourceIds.end(), + Utils::make_iterator([&](auto &entry) { + entry.lastModified = m_fileSystem.lastModified(entry.sourceId); + })); +} + +SourceIds FileStatusCache::modified(SourceIds sourceIds) const +{ + SourceIds modifiedSourceIds; + modifiedSourceIds.reserve(sourceIds.size()); + + std::set_intersection(m_cacheEntries.begin(), + m_cacheEntries.end(), + sourceIds.begin(), + sourceIds.end(), + Utils::make_iterator([&](auto &entry) { + auto newLastModified = m_fileSystem.lastModified(entry.sourceId); + if (newLastModified > entry.lastModified) { + modifiedSourceIds.push_back(entry.sourceId); + entry.lastModified = newLastModified; + } + })); + + Internal::FileStatusCacheEntries newEntries; + newEntries.reserve(sourceIds.size()); + + std::set_difference(sourceIds.begin(), + sourceIds.end(), + m_cacheEntries.begin(), + m_cacheEntries.end(), + Utils::make_iterator([&](SourceId newSourceId) { + newEntries.emplace_back(newSourceId, + m_fileSystem.lastModified(newSourceId)); + modifiedSourceIds.push_back(newSourceId); + })); + + if (newEntries.size()) { + Internal::FileStatusCacheEntries mergedEntries; + mergedEntries.reserve(m_cacheEntries.size() + newEntries.size()); + + std::set_union(newEntries.begin(), + newEntries.end(), + m_cacheEntries.begin(), + m_cacheEntries.end(), + std::back_inserter(mergedEntries)); + + m_cacheEntries = std::move(mergedEntries); + } + + std::sort(modifiedSourceIds.begin(), modifiedSourceIds.end()); + + return modifiedSourceIds; +} + +FileStatusCache::size_type FileStatusCache::size() const +{ + return m_cacheEntries.size(); +} + +Internal::FileStatusCacheEntry FileStatusCache::findEntry(SourceId sourceId) const +{ + auto found = std::lower_bound(m_cacheEntries.begin(), + m_cacheEntries.end(), + Internal::FileStatusCacheEntry{sourceId}, + [](const auto &first, const auto &second) { + return first.sourceId < second.sourceId; + }); + + if (found != m_cacheEntries.end() && found->sourceId == sourceId) + return *found; + + auto inserted = m_cacheEntries.emplace(found, sourceId, m_fileSystem.lastModified(sourceId)); + + return *inserted; +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/designercore/projectstorage/filestatuscache.h b/src/plugins/qmldesigner/designercore/projectstorage/filestatuscache.h new file mode 100644 index 00000000000..55e92903878 --- /dev/null +++ b/src/plugins/qmldesigner/designercore/projectstorage/filestatuscache.h @@ -0,0 +1,95 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "filesysteminterface.h" +#include "vector" + +#include + +QT_FORWARD_DECLARE_CLASS(QFileInfo) + +namespace QmlDesigner { +namespace Internal { +class FileStatusCacheEntry +{ +public: + FileStatusCacheEntry(QmlDesigner::SourceId sourceId, long long lastModified = 0) + : sourceId(sourceId) + , lastModified(lastModified) + {} + + friend bool operator<(FileStatusCacheEntry first, FileStatusCacheEntry second) + { + return first.sourceId < second.sourceId; + } + + friend bool operator<(FileStatusCacheEntry first, SourceId second) + { + return first.sourceId < second; + } + + friend bool operator<(SourceId first, FileStatusCacheEntry second) + { + return first < second.sourceId; + } + +public: + SourceId sourceId; + long long lastModified; +}; + +using FileStatusCacheEntries = std::vector; + +} + +class FileStatusCache +{ +public: + using size_type = Internal::FileStatusCacheEntries::size_type; + + FileStatusCache(FileSystemInterface &fileSystem) + : m_fileSystem(fileSystem) + {} + FileStatusCache &operator=(const FileStatusCache &) = delete; + FileStatusCache(const FileStatusCache &) = delete; + + long long lastModifiedTime(SourceId sourceId) const; + void update(SourceId sourceId); + void update(SourceIds sourceIds); + SourceIds modified(SourceIds sourceIds) const; + + size_type size() const; + +private: + Internal::FileStatusCacheEntry findEntry(SourceId sourceId) const; + +private: + mutable Internal::FileStatusCacheEntries m_cacheEntries; + FileSystemInterface &m_fileSystem; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/designercore/projectstorage/filesystem.cpp b/src/plugins/qmldesigner/designercore/projectstorage/filesystem.cpp new file mode 100644 index 00000000000..08ec31c707a --- /dev/null +++ b/src/plugins/qmldesigner/designercore/projectstorage/filesystem.cpp @@ -0,0 +1,73 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "filesystem.h" +#include "projectstorage.h" +#include "projectstorageids.h" +#include "sourcepathcache.h" +#include "sqlitedatabase.h" + +#include + +#include +#include +#include + +namespace QmlDesigner { + +SourceIds FileSystem::directoryEntries(const QString &directoryPath) const +{ + QDir directory{directoryPath}; + + QFileInfoList fileInfos = directory.entryInfoList(); + + SourceIds sourceIds = Utils::transform(fileInfos, [&](const QFileInfo &fileInfo) { + return m_sourcePathCache.sourceId(SourcePath{fileInfo.path()}); + }); + + std::sort(sourceIds.begin(), sourceIds.end()); + + return sourceIds; +} + +long long FileSystem::lastModified(SourceId sourceId) const +{ + QFileInfo fileInfo(QString(m_sourcePathCache.sourcePath(sourceId))); + + fileInfo.refresh(); + + if (fileInfo.exists()) + return fileInfo.lastModified().toMSecsSinceEpoch() / 1000; + + return 0; +} + +void FileSystem::remove(const SourceIds &sourceIds) +{ + for (SourceId sourceId : sourceIds) + QFile::remove(QString{m_sourcePathCache.sourcePath(sourceId)}); +} + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/designercore/projectstorage/filesystem.h b/src/plugins/qmldesigner/designercore/projectstorage/filesystem.h new file mode 100644 index 00000000000..43b57ff38d9 --- /dev/null +++ b/src/plugins/qmldesigner/designercore/projectstorage/filesystem.h @@ -0,0 +1,62 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "filestatuscache.h" +#include "filesysteminterface.h" +#include "nonlockingmutex.h" + +namespace Sqlite { +class Database; +} + +namespace QmlDesigner { + +template +class SourcePathCache; + +template +class ProjectStorage; + +using PathCache = SourcePathCache, NonLockingMutex>; + +class FileSystem final : public FileSystemInterface +{ +public: + FileSystem(PathCache &sourcePathCache) + : m_sourcePathCache(sourcePathCache) + {} + + SourceIds directoryEntries(const QString &directoryPath) const override; + long long lastModified(SourceId sourceId) const override; + + void remove(const SourceIds &sourceIds) override; + +private: + PathCache &m_sourcePathCache; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/designercore/projectstorage/filesysteminterface.h b/src/plugins/qmldesigner/designercore/projectstorage/filesysteminterface.h new file mode 100644 index 00000000000..2f1809982b3 --- /dev/null +++ b/src/plugins/qmldesigner/designercore/projectstorage/filesysteminterface.h @@ -0,0 +1,44 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "projectstorageids.h" + +#include + +namespace QmlDesigner { + +class FileSystemInterface +{ +public: + virtual SourceIds directoryEntries(const QString &directoryPath) const = 0; + virtual long long lastModified(SourceId sourceId) const = 0; + virtual void remove(const SourceIds &sourceIds) = 0; + +protected: + ~FileSystemInterface() = default; +}; +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/designercore/projectstorage/nonlockingmutex.h b/src/plugins/qmldesigner/designercore/projectstorage/nonlockingmutex.h new file mode 100644 index 00000000000..b5e5257d416 --- /dev/null +++ b/src/plugins/qmldesigner/designercore/projectstorage/nonlockingmutex.h @@ -0,0 +1,42 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +namespace QmlDesigner { + +class NonLockingMutex +{ +public: + constexpr NonLockingMutex() noexcept {} + NonLockingMutex(const NonLockingMutex &) = delete; + NonLockingMutex &operator=(const NonLockingMutex &) = delete; + void lock() {} + void unlock() {} + void lock_shared() {} + void unlock_shared() {} +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/designercore/projectstorage/projectstoragepathwatcher.h b/src/plugins/qmldesigner/designercore/projectstorage/projectstoragepathwatcher.h new file mode 100644 index 00000000000..30cbfe65a07 --- /dev/null +++ b/src/plugins/qmldesigner/designercore/projectstorage/projectstoragepathwatcher.h @@ -0,0 +1,407 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "directorypathcompressor.h" +#include "filesystem.h" +#include "projectstoragepathwatcherinterface.h" +#include "projectstoragepathwatchernotifierinterface.h" +#include "projectstoragepathwatchertypes.h" +#include "storagecache.h" + +#include + +#include + +namespace QmlDesigner { + +template +void set_greedy_intersection_call( + InputIt1 first1, InputIt1 last1, InputIt2 first2, InputIt2 last2, Callable callable) +{ + while (first1 != last1 && first2 != last2) { + if (*first1 < *first2) { + ++first1; + } else { + if (*first2 < *first1) + ++first2; + else + callable(*first1++); + } + } +} + + +template +class ProjectStoragePathWatcher : public ProjectStoragePathWatcherInterface +{ +public: + ProjectStoragePathWatcher(SourcePathCache &pathCache, + FileSystemInterface &fileSystem, + ProjectStoragePathWatcherNotifierInterface *notifier = nullptr) + : m_fileStatusCache(fileSystem) + , m_fileSystem(fileSystem) + , m_pathCache(pathCache) + , m_notifier(notifier) + { + QObject::connect(&m_fileSystemWatcher, + &FileSystemWatcher::directoryChanged, + [&](const QString &path) { compressChangedDirectoryPath(path); }); + + m_directoryPathCompressor.setCallback([&](QmlDesigner::SourceContextIds &&sourceContextIds) { + addChangedPathForFilePath(std::move(sourceContextIds)); + }); + } + + ~ProjectStoragePathWatcher() + { + m_directoryPathCompressor.setCallback([&](SourceContextIds &&) {}); + } + + void updateIdPaths(const std::vector &idPaths) override + { + auto entriesAndIds = convertIdPathsToWatcherEntriesAndIds(idPaths); + + addEntries(entriesAndIds.first); + removeUnusedEntries(entriesAndIds.first, entriesAndIds.second); + } + + void removeIds(const ProjectPartIds &ids) override + { + auto removedEntries = removeIdsFromWatchedEntries(ids); + + auto filteredPaths = filterNotWatchedPaths(removedEntries); + + if (!filteredPaths.empty()) + m_fileSystemWatcher.removePaths(convertWatcherEntriesToDirectoryPathList(filteredPaths)); + } + + void setNotifier(ProjectStoragePathWatcherNotifierInterface *notifier) override + { + m_notifier = notifier; + } + + std::size_t sizeOfIdPaths(const std::vector &idPaths) + { + auto sumSize = [](std::size_t size, const IdPaths &idPath) { + return size + idPath.sourceIds.size(); + }; + + return std::accumulate(idPaths.begin(), idPaths.end(), std::size_t(0), sumSize); + } + + std::pair convertIdPathsToWatcherEntriesAndIds( + const std::vector &idPaths) + { + WatcherEntries entries; + entries.reserve(sizeOfIdPaths(idPaths)); + ProjectChunkIds ids; + ids.reserve(ids.size()); + + auto outputIterator = std::back_inserter(entries); + + for (const IdPaths &idPath : idPaths) + { + ProjectChunkId id = idPath.id; + + ids.push_back(id); + + outputIterator = std::transform( + idPath.sourceIds.begin(), idPath.sourceIds.end(), outputIterator, [&](SourceId sourceId) { + return WatcherEntry{id, + m_pathCache.sourceContextId(sourceId), + sourceId, + m_fileStatusCache.lastModifiedTime(sourceId)}; + }); + } + + std::sort(entries.begin(), entries.end()); + std::sort(ids.begin(), ids.end()); + + return {entries, ids}; + } + + void addEntries(const WatcherEntries &entries) + { + auto newEntries = notWatchedEntries(entries); + + auto filteredPaths = filterNotWatchedPaths(newEntries); + + mergeToWatchedEntries(newEntries); + + if (!filteredPaths.empty()) + m_fileSystemWatcher.addPaths(convertWatcherEntriesToDirectoryPathList(filteredPaths)); + } + + void removeUnusedEntries(const WatcherEntries &entries, const ProjectChunkIds &ids) + { + auto oldEntries = notAnymoreWatchedEntriesWithIds(entries, ids); + + removeFromWatchedEntries(oldEntries); + + auto filteredPaths = filterNotWatchedPaths(oldEntries); + + if (!filteredPaths.empty()) + m_fileSystemWatcher.removePaths(convertWatcherEntriesToDirectoryPathList(filteredPaths)); + } + + FileSystemWatcher &fileSystemWatcher() { return m_fileSystemWatcher; } + + QStringList convertWatcherEntriesToDirectoryPathList(const SourceContextIds &sourceContextIds) const + { + return Utils::transform(sourceContextIds, [&](SourceContextId id) { + return QString(m_pathCache.sourceContextPath(id)); + }); + } + + QStringList convertWatcherEntriesToDirectoryPathList(const WatcherEntries &watcherEntries) const + { + SourceContextIds sourceContextIds = Utils::transform( + watcherEntries, [&](WatcherEntry entry) { return entry.sourceContextId; }); + + std::sort(sourceContextIds.begin(), sourceContextIds.end()); + sourceContextIds.erase(std::unique(sourceContextIds.begin(), sourceContextIds.end()), + sourceContextIds.end()); + + return convertWatcherEntriesToDirectoryPathList(sourceContextIds); + } + + WatcherEntries notWatchedEntries(const WatcherEntries &entries) const + { + WatcherEntries notWatchedEntries; + notWatchedEntries.reserve(entries.size()); + + std::set_difference(entries.begin(), + entries.end(), + m_watchedEntries.cbegin(), + m_watchedEntries.cend(), + std::back_inserter(notWatchedEntries)); + + return notWatchedEntries; + } + + SourceContextIds notWatchedPaths(const SourceContextIds &ids) const + { + SourceContextIds notWatchedDirectoryIds; + notWatchedDirectoryIds.reserve(ids.size()); + + std::set_difference(ids.begin(), + ids.end(), + m_watchedEntries.cbegin(), + m_watchedEntries.cend(), + std::back_inserter(notWatchedDirectoryIds)); + + return notWatchedDirectoryIds; + } + + template + WatcherEntries notAnymoreWatchedEntries( + const WatcherEntries &newEntries, + Compare compare) const + { + WatcherEntries notAnymoreWatchedEntries; + notAnymoreWatchedEntries.reserve(m_watchedEntries.size()); + + std::set_difference(m_watchedEntries.cbegin(), + m_watchedEntries.cend(), + newEntries.begin(), + newEntries.end(), + std::back_inserter(notAnymoreWatchedEntries), + compare); + + return notAnymoreWatchedEntries; + } + + WatcherEntries notAnymoreWatchedEntriesWithIds(const WatcherEntries &newEntries, + const ProjectChunkIds &ids) const + { + auto oldEntries = notAnymoreWatchedEntries(newEntries, std::less()); + + auto newEnd = std::remove_if(oldEntries.begin(), + oldEntries.end(), + [&] (WatcherEntry entry) { + return !std::binary_search(ids.begin(), ids.end(), entry.id); + }); + + oldEntries.erase(newEnd, oldEntries.end()); + + return oldEntries; + } + + void mergeToWatchedEntries(const WatcherEntries &newEntries) + { + WatcherEntries newWatchedEntries; + newWatchedEntries.reserve(m_watchedEntries.size() + newEntries.size()); + + std::merge(m_watchedEntries.cbegin(), + m_watchedEntries.cend(), + newEntries.begin(), + newEntries.end(), + std::back_inserter(newWatchedEntries)); + + m_watchedEntries = std::move(newWatchedEntries); + } + + static SourceContextIds uniquePaths(const WatcherEntries &pathEntries) + { + SourceContextIds uniqueDirectoryIds; + uniqueDirectoryIds.reserve(pathEntries.size()); + + auto compare = [](WatcherEntry first, WatcherEntry second) { + return first.sourceContextId == second.sourceContextId; + }; + + std::unique_copy(pathEntries.begin(), + pathEntries.end(), + std::back_inserter(uniqueDirectoryIds), + compare); + + return uniqueDirectoryIds; + } + + SourceContextIds filterNotWatchedPaths(const WatcherEntries &entries) const + { + return notWatchedPaths(uniquePaths(entries)); + } + + const WatcherEntries &watchedEntries() const + { + return m_watchedEntries; + } + + WatcherEntries removeIdsFromWatchedEntries(const ProjectPartIds &ids) + { + auto keep = [&](WatcherEntry entry) { + return !std::binary_search(ids.begin(), ids.end(), entry.id); + }; + + auto found = std::stable_partition(m_watchedEntries.begin(), m_watchedEntries.end(), keep); + + WatcherEntries removedEntries(found, m_watchedEntries.end()); + + m_watchedEntries.erase(found, m_watchedEntries.end()); + + return removedEntries; + } + + void removeFromWatchedEntries(const WatcherEntries &oldEntries) + { + WatcherEntries newWatchedEntries; + newWatchedEntries.reserve(m_watchedEntries.size() - oldEntries.size()); + + std::set_difference(m_watchedEntries.cbegin(), + m_watchedEntries.cend(), + oldEntries.begin(), + oldEntries.end(), + std::back_inserter(newWatchedEntries)); + + m_watchedEntries = std::move(newWatchedEntries); + } + + void compressChangedDirectoryPath(const QString &path) + { + m_directoryPathCompressor.addSourceContextId( + m_pathCache.sourceContextId(Utils::PathString{path})); + } + + WatcherEntries watchedEntriesForPaths(QmlDesigner::SourceContextIds &&sourceContextIds) + { + WatcherEntries foundEntries; + foundEntries.reserve(m_watchedEntries.size()); + + set_greedy_intersection_call(m_watchedEntries.begin(), + m_watchedEntries.end(), + sourceContextIds.begin(), + sourceContextIds.end(), + [&](WatcherEntry &entry) { + m_fileStatusCache.update(entry.sourceId); + auto currentLastModified = m_fileStatusCache.lastModifiedTime( + entry.sourceId); + if (entry.lastModified < currentLastModified) { + foundEntries.push_back(entry); + entry.lastModified = currentLastModified; + } + }); + + return foundEntries; + } + + SourceIds watchedPaths(const WatcherEntries &entries) const + { + auto sourceIds = Utils::transform(entries, &WatcherEntry::sourceId); + + std::sort(sourceIds.begin(), sourceIds.end()); + + sourceIds.erase(std::unique(sourceIds.begin(), sourceIds.end()), sourceIds.end()); + + return sourceIds; + } + + std::vector idPathsForWatcherEntries(WatcherEntries &&foundEntries) + { + std::sort(foundEntries.begin(), foundEntries.end(), [](WatcherEntry first, WatcherEntry second) { + return std::tie(first.id, first.sourceId) < std::tie(second.id, second.sourceId); + }); + + std::vector idPaths; + idPaths.reserve(foundEntries.size()); + + for (WatcherEntry entry : foundEntries) { + if (idPaths.empty() || idPaths.back().id != entry.id) + idPaths.emplace_back(entry.id, SourceIds{}); + idPaths.back().sourceIds.push_back(entry.sourceId); + } + + return idPaths; + } + + void addChangedPathForFilePath(SourceContextIds &&sourceContextIds) + { + if (m_notifier) { + WatcherEntries foundEntries = watchedEntriesForPaths(std::move(sourceContextIds)); + + SourceIds watchedSourceIds = watchedPaths(foundEntries); + + std::vector changedIdPaths = idPathsForWatcherEntries(std::move(foundEntries)); + + m_notifier->pathsChanged(watchedSourceIds); + m_notifier->pathsWithIdsChanged(changedIdPaths); + } + } + + SourcePathCache &pathCache() { return m_pathCache; } + +private: + WatcherEntries m_watchedEntries; + FileSystemWatcher m_fileSystemWatcher; + FileStatusCache m_fileStatusCache; + FileSystemInterface &m_fileSystem; + SourcePathCache &m_pathCache; + ProjectStoragePathWatcherNotifierInterface *m_notifier; + DirectoryPathCompressor m_directoryPathCompressor; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/designercore/projectstorage/projectstoragepathwatcherinterface.h b/src/plugins/qmldesigner/designercore/projectstorage/projectstoragepathwatcherinterface.h new file mode 100644 index 00000000000..fed78ffde57 --- /dev/null +++ b/src/plugins/qmldesigner/designercore/projectstorage/projectstoragepathwatcherinterface.h @@ -0,0 +1,52 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "projectstoragepathwatchertypes.h" + +#include + +namespace QmlDesigner { + +class ProjectStoragePathWatcherNotifierInterface; + +class ProjectStoragePathWatcherInterface +{ +public: + ProjectStoragePathWatcherInterface() = default; + ProjectStoragePathWatcherInterface(const ProjectStoragePathWatcherInterface &) = delete; + ProjectStoragePathWatcherInterface &operator=(const ProjectStoragePathWatcherInterface &) = delete; + + virtual void updateIdPaths(const std::vector &idPaths) = 0; + virtual void removeIds(const ProjectPartIds &ids) = 0; + + virtual void setNotifier(ProjectStoragePathWatcherNotifierInterface *notifier) = 0; + +protected: + ~ProjectStoragePathWatcherInterface() = default; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/designercore/projectstorage/projectstoragepathwatchernotifierinterface.h b/src/plugins/qmldesigner/designercore/projectstorage/projectstoragepathwatchernotifierinterface.h new file mode 100644 index 00000000000..c7ea4a17098 --- /dev/null +++ b/src/plugins/qmldesigner/designercore/projectstorage/projectstoragepathwatchernotifierinterface.h @@ -0,0 +1,48 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "projectstoragepathwatchertypes.h" + +#include + +namespace QmlDesigner { + +class ProjectStoragePathWatcherNotifierInterface +{ +public: + ProjectStoragePathWatcherNotifierInterface() = default; + ProjectStoragePathWatcherNotifierInterface(const ProjectStoragePathWatcherNotifierInterface &) = delete; + ProjectStoragePathWatcherNotifierInterface &operator=(const ProjectStoragePathWatcherNotifierInterface &) = delete; + + virtual void pathsWithIdsChanged(const std::vector &idPaths) = 0; + virtual void pathsChanged(const SourceIds &filePathIds) = 0; + +protected: + ~ProjectStoragePathWatcherNotifierInterface() = default; +}; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/designercore/projectstorage/projectstoragepathwatchertypes.h b/src/plugins/qmldesigner/designercore/projectstorage/projectstoragepathwatchertypes.h new file mode 100644 index 00000000000..94470ebbb8f --- /dev/null +++ b/src/plugins/qmldesigner/designercore/projectstorage/projectstoragepathwatchertypes.h @@ -0,0 +1,133 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "projectstorageids.h" + +#include + +namespace QmlDesigner { + +enum class SourceType : int { Qml, QmlUi, QmlTypes, QmlDir }; + +class ProjectChunkId +{ +public: + ProjectPartId id; + SourceType sourceType; + + friend bool operator==(ProjectChunkId first, ProjectChunkId second) + { + return first.id == second.id && first.sourceType == second.sourceType; + } + + friend bool operator==(ProjectChunkId first, ProjectPartId second) + { + return first.id == second; + } + + friend bool operator==(ProjectPartId first, ProjectChunkId second) + { + return first == second.id; + } + + friend bool operator!=(ProjectChunkId first, ProjectChunkId second) + { + return !(first == second); + } + + friend bool operator<(ProjectChunkId first, ProjectChunkId second) + { + return std::tie(first.id, first.sourceType) < std::tie(second.id, second.sourceType); + } + + friend bool operator<(ProjectChunkId first, ProjectPartId second) { return first.id < second; } + + friend bool operator<(ProjectPartId first, ProjectChunkId second) { return first < second.id; } +}; + +using ProjectChunkIds = std::vector; + +class IdPaths +{ +public: + IdPaths(ProjectPartId projectPartId, SourceType sourceType, SourceIds &&sourceIds) + : id{projectPartId, sourceType} + , sourceIds(std::move(sourceIds)) + {} + IdPaths(ProjectChunkId projectChunkId, SourceIds &&sourceIds) + : id(projectChunkId) + , sourceIds(std::move(sourceIds)) + {} + + friend bool operator==(IdPaths first, IdPaths second) + { + return first.id == second.id && first.sourceIds == second.sourceIds; + } + +public: + ProjectChunkId id; + SourceIds sourceIds; +}; + +class WatcherEntry +{ +public: + ProjectChunkId id; + SourceContextId sourceContextId; + SourceId sourceId; + long long lastModified = -1; + + friend bool operator==(WatcherEntry first, WatcherEntry second) + { + return first.id == second.id && first.sourceContextId == second.sourceContextId + && first.sourceId == second.sourceId; + } + + friend bool operator<(WatcherEntry first, WatcherEntry second) + { + return std::tie(first.sourceContextId, first.sourceId, first.id) + < std::tie(second.sourceContextId, second.sourceId, second.id); + } + + friend bool operator<(SourceContextId sourceContextId, WatcherEntry entry) + { + return sourceContextId < entry.sourceContextId; + } + + friend bool operator<(WatcherEntry entry, SourceContextId sourceContextId) + { + return entry.sourceContextId < sourceContextId; + } + + operator SourceId() const { return sourceId; } + + operator SourceContextId() const { return sourceContextId; } +}; + +using WatcherEntries = std::vector; + +} // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/designercore/projectstorage/storagecache.h b/src/plugins/qmldesigner/designercore/projectstorage/storagecache.h index a768d513a80..747c3d9a07d 100644 --- a/src/plugins/qmldesigner/designercore/projectstorage/storagecache.h +++ b/src/plugins/qmldesigner/designercore/projectstorage/storagecache.h @@ -25,6 +25,7 @@ #pragma once +#include "nonlockingmutex.h" #include "projectstorageids.h" #include "storagecacheentry.h" #include "storagecachefwd.h" @@ -48,18 +49,6 @@ class StorageCacheException : public std::exception } }; -class NonLockingMutex -{ -public: - constexpr NonLockingMutex() noexcept {} - NonLockingMutex(const NonLockingMutex &) = delete; - NonLockingMutex &operator=(const NonLockingMutex &) = delete; - void lock() {} - void unlock() {} - void lock_shared() {} - void unlock_shared() {} -}; - template + +namespace { + +using QmlDesigner::SourceContextId; +using QmlDesigner::SourceContextIds; + +class DirectoryPathCompressor : public testing::Test +{ +protected: + void SetUp() + { + compressor.setCallback(mockCompressorCallback.AsStdFunction()); + } + +protected: + NiceMock> mockCompressorCallback; + QmlDesigner::DirectoryPathCompressor> compressor; + NiceMock &mockTimer = compressor.timer(); + SourceContextId sourceContextId1{1}; + SourceContextId sourceContextId2{2}; +}; + +TEST_F(DirectoryPathCompressor, AddFilePath) +{ + compressor.addSourceContextId(sourceContextId1); + + ASSERT_THAT(compressor.takeSourceContextIds(), ElementsAre(sourceContextId1)); +} + +TEST_F(DirectoryPathCompressor, NoFilePathsAferTakenThem) +{ + compressor.addSourceContextId(sourceContextId1); + + compressor.takeSourceContextIds(); + + ASSERT_THAT(compressor.takeSourceContextIds(), IsEmpty()); +} + +TEST_F(DirectoryPathCompressor, CallRestartTimerAfterAddingPath) +{ + EXPECT_CALL(mockTimer, start(20)); + + compressor.addSourceContextId(sourceContextId1); +} + +TEST_F(DirectoryPathCompressor, CallTimeOutAfterAddingPath) +{ + EXPECT_CALL(mockCompressorCallback, Call(ElementsAre(sourceContextId1, sourceContextId2))); + + compressor.addSourceContextId(sourceContextId1); + compressor.addSourceContextId(sourceContextId2); +} + +TEST_F(DirectoryPathCompressor, RemoveDuplicates) +{ + EXPECT_CALL(mockCompressorCallback, Call(ElementsAre(sourceContextId1, sourceContextId2))); + + compressor.addSourceContextId(sourceContextId1); + compressor.addSourceContextId(sourceContextId2); + compressor.addSourceContextId(sourceContextId1); +} + +} diff --git a/tests/unit/unittest/filestatuscache-test.cpp b/tests/unit/unittest/filestatuscache-test.cpp new file mode 100644 index 00000000000..c28faf4ffa6 --- /dev/null +++ b/tests/unit/unittest/filestatuscache-test.cpp @@ -0,0 +1,296 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "filesystemmock.h" +#include "googletest.h" + +#include + +#include +#include + +#include + +namespace { + +using QmlDesigner::SourceId; +using QmlDesigner::SourceIds; + +class FileStatusCache : public testing::Test +{ +protected: + FileStatusCache() + { + ON_CALL(fileSystem, lastModified(Eq(header))).WillByDefault(Return(headerLastModifiedTime)); + ON_CALL(fileSystem, lastModified(Eq(source))).WillByDefault(Return(sourceLastModifiedTime)); + ON_CALL(fileSystem, lastModified(Eq(header2))).WillByDefault(Return(header2LastModifiedTime)); + ON_CALL(fileSystem, lastModified(Eq(source2))).WillByDefault(Return(source2LastModifiedTime)); + } + +protected: + NiceMock fileSystem; + QmlDesigner::FileStatusCache cache{fileSystem}; + SourceId header{1}; + SourceId source{2}; + SourceId header2{3}; + SourceId source2{4}; + SourceIds entries{header, source, header2, source2}; + long long headerLastModifiedTime = 100; + long long headerLastModifiedTime2 = 110; + long long header2LastModifiedTime = 300; + long long header2LastModifiedTime2 = 310; + long long sourceLastModifiedTime = 200; + long long source2LastModifiedTime = 400; +}; + +TEST_F(FileStatusCache, CreateEntry) +{ + cache.lastModifiedTime(header); + + ASSERT_THAT(cache, SizeIs(1)); +} + +TEST_F(FileStatusCache, AskCreatedEntryForLastModifiedTime) +{ + auto lastModified = cache.lastModifiedTime(header); + + ASSERT_THAT(lastModified, headerLastModifiedTime); +} + +TEST_F(FileStatusCache, AskCachedEntryForLastModifiedTime) +{ + cache.lastModifiedTime(header); + + auto lastModified = cache.lastModifiedTime(header); + + ASSERT_THAT(lastModified, headerLastModifiedTime); +} + +TEST_F(FileStatusCache, DontAddEntryTwice) +{ + cache.lastModifiedTime(header); + + cache.lastModifiedTime(header); + + ASSERT_THAT(cache, SizeIs(1)); +} + +TEST_F(FileStatusCache, AddNewEntry) +{ + cache.lastModifiedTime(header); + + cache.lastModifiedTime(source); + + ASSERT_THAT(cache, SizeIs(2)); +} + +TEST_F(FileStatusCache, AskNewEntryForLastModifiedTime) +{ + cache.lastModifiedTime(header); + + auto lastModified = cache.lastModifiedTime(source); + + ASSERT_THAT(lastModified, sourceLastModifiedTime); +} + +TEST_F(FileStatusCache, AddNewEntryReverseOrder) +{ + cache.lastModifiedTime(source); + + cache.lastModifiedTime(header); + + ASSERT_THAT(cache, SizeIs(2)); +} + +TEST_F(FileStatusCache, AskNewEntryReverseOrderAddedForLastModifiedTime) +{ + cache.lastModifiedTime(source); + + auto lastModified = cache.lastModifiedTime(header); + + ASSERT_THAT(lastModified, headerLastModifiedTime); +} + +TEST_F(FileStatusCache, UpdateFile) +{ + EXPECT_CALL(fileSystem, lastModified(Eq(header))) + .Times(2) + .WillOnce(Return(headerLastModifiedTime)) + .WillOnce(Return(headerLastModifiedTime2)); + cache.lastModifiedTime(header); + + cache.update(header); + + ASSERT_THAT(cache.lastModifiedTime(header), headerLastModifiedTime2); +} + +TEST_F(FileStatusCache, UpdateFileDoesNotChangeEntryCount) +{ + EXPECT_CALL(fileSystem, lastModified(Eq(header))) + .Times(2) + .WillOnce(Return(headerLastModifiedTime)) + .WillOnce(Return(headerLastModifiedTime2)); + cache.lastModifiedTime(header); + + cache.update(header); + + ASSERT_THAT(cache, SizeIs(1)); +} + +TEST_F(FileStatusCache, UpdateFileForNonExistingEntry) +{ + cache.update(header); + + ASSERT_THAT(cache, SizeIs(0)); +} + +TEST_F(FileStatusCache, UpdateFiles) +{ + EXPECT_CALL(fileSystem, lastModified(Eq(header))) + .Times(2) + .WillOnce(Return(headerLastModifiedTime)) + .WillOnce(Return(headerLastModifiedTime2)); + EXPECT_CALL(fileSystem, lastModified(Eq(header2))) + .Times(2) + .WillOnce(Return(header2LastModifiedTime)) + .WillOnce(Return(header2LastModifiedTime2)); + cache.lastModifiedTime(header); + cache.lastModifiedTime(header2); + + cache.update(entries); + + ASSERT_THAT(cache.lastModifiedTime(header), headerLastModifiedTime2); + ASSERT_THAT(cache.lastModifiedTime(header2), header2LastModifiedTime2); +} + +TEST_F(FileStatusCache, UpdateFilesDoesNotChangeEntryCount) +{ + EXPECT_CALL(fileSystem, lastModified(Eq(header))) + .Times(2) + .WillOnce(Return(headerLastModifiedTime)) + .WillOnce(Return(headerLastModifiedTime2)); + EXPECT_CALL(fileSystem, lastModified(Eq(header2))) + .Times(2) + .WillOnce(Return(header2LastModifiedTime)) + .WillOnce(Return(header2LastModifiedTime2)); + cache.lastModifiedTime(header); + cache.lastModifiedTime(header2); + + cache.update(entries); + + ASSERT_THAT(cache, SizeIs(2)); +} + +TEST_F(FileStatusCache, UpdateFilesForNonExistingEntry) +{ + cache.update(entries); + + ASSERT_THAT(cache, SizeIs(0)); +} + +TEST_F(FileStatusCache, NewModifiedEntries) +{ + auto modifiedIds = cache.modified(entries); + + ASSERT_THAT(modifiedIds, entries); +} + +TEST_F(FileStatusCache, NoNewModifiedEntries) +{ + cache.modified(entries); + + auto modifiedIds = cache.modified(entries); + + ASSERT_THAT(modifiedIds, IsEmpty()); +} + +TEST_F(FileStatusCache, SomeNewModifiedEntries) +{ + cache.modified({source, header2}); + + auto modifiedIds = cache.modified(entries); + + ASSERT_THAT(modifiedIds, ElementsAre(header, source2)); +} + +TEST_F(FileStatusCache, SomeAlreadyExistingModifiedEntries) +{ + EXPECT_CALL(fileSystem, lastModified(Eq(header))) + .Times(2) + .WillOnce(Return(headerLastModifiedTime)) + .WillOnce(Return(headerLastModifiedTime2)); + EXPECT_CALL(fileSystem, lastModified(Eq(header2))) + .Times(2) + .WillOnce(Return(header2LastModifiedTime)) + .WillOnce(Return(header2LastModifiedTime2)); + EXPECT_CALL(fileSystem, lastModified(Eq(source))).Times(2).WillRepeatedly(Return(sourceLastModifiedTime)); + EXPECT_CALL(fileSystem, lastModified(Eq(source2))) + .Times(2) + .WillRepeatedly(Return(source2LastModifiedTime)); + cache.modified(entries); + + auto modifiedIds = cache.modified(entries); + + ASSERT_THAT(modifiedIds, ElementsAre(header, header2)); +} + +TEST_F(FileStatusCache, SomeAlreadyExistingAndSomeNewModifiedEntries) +{ + EXPECT_CALL(fileSystem, lastModified(Eq(header))).WillRepeatedly(Return(headerLastModifiedTime)); + EXPECT_CALL(fileSystem, lastModified(Eq(header2))) + .Times(2) + .WillOnce(Return(header2LastModifiedTime)) + .WillOnce(Return(header2LastModifiedTime2)); + EXPECT_CALL(fileSystem, lastModified(Eq(source))).Times(2).WillRepeatedly(Return(sourceLastModifiedTime)); + EXPECT_CALL(fileSystem, lastModified(Eq(source2))).WillRepeatedly(Return(source2LastModifiedTime)); + cache.modified({source, header2}); + + auto modifiedIds = cache.modified(entries); + + ASSERT_THAT(modifiedIds, ElementsAre(header, header2, source2)); +} + +TEST_F(FileStatusCache, TimeIsUpdatedForSomeAlreadyExistingModifiedEntries) +{ + EXPECT_CALL(fileSystem, lastModified(Eq(header))) + .Times(2) + .WillOnce(Return(headerLastModifiedTime)) + .WillOnce(Return(headerLastModifiedTime2)); + EXPECT_CALL(fileSystem, lastModified(Eq(header2))) + .Times(2) + .WillOnce(Return(header2LastModifiedTime)) + .WillOnce(Return(header2LastModifiedTime2)); + EXPECT_CALL(fileSystem, lastModified(Eq(source))).Times(2).WillRepeatedly(Return(sourceLastModifiedTime)); + EXPECT_CALL(fileSystem, lastModified(Eq(source2))) + .Times(2) + .WillRepeatedly(Return(source2LastModifiedTime)); + cache.modified(entries); + + cache.modified(entries); + + ASSERT_THAT(cache.lastModifiedTime(header), headerLastModifiedTime2); +} + +} // namespace diff --git a/tests/unit/unittest/filesystemmock.h b/tests/unit/unittest/filesystemmock.h new file mode 100644 index 00000000000..ffcf5e33a09 --- /dev/null +++ b/tests/unit/unittest/filesystemmock.h @@ -0,0 +1,41 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "googletest.h" + +#include + +class FileSystemMock : public QmlDesigner::FileSystemInterface +{ +public: + MOCK_METHOD(QmlDesigner::SourceIds, + directoryEntries, + (const QString &directoryPath), + (const, override)); + MOCK_METHOD(long long, lastModified, (QmlDesigner::SourceId filePathId), (const, override)); + MOCK_METHOD(void, remove, (const QmlDesigner::SourceIds &filePathIds), (override)); +}; diff --git a/tests/unit/unittest/gtest-creator-printing.cpp b/tests/unit/unittest/gtest-creator-printing.cpp index c53a8f7026f..24302ff88c9 100644 --- a/tests/unit/unittest/gtest-creator-printing.cpp +++ b/tests/unit/unittest/gtest-creator-printing.cpp @@ -36,8 +36,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -46,8 +48,6 @@ #include #include -#include - void PrintTo(const Utf8String &text, ::std::ostream *os) { *os << text; @@ -932,6 +932,43 @@ std::ostream &operator<<(std::ostream &out, const Diagnostic &diag) { namespace QmlDesigner { +const char *sourceTypeToText(SourceType sourceType) +{ + switch (sourceType) { + case SourceType::Qml: + return "Qml"; + case SourceType::QmlUi: + return "QmlUi"; + case SourceType::QmlDir: + return "QmlDir"; + case SourceType::QmlTypes: + return "QmlTypes"; + } + + return ""; +} + +std::ostream &operator<<(std::ostream &out, SourceType sourceType) +{ + return out << sourceTypeToText(sourceType); +} + +std::ostream &operator<<(std::ostream &out, const ProjectChunkId &id) +{ + return out << "(" << id.id << ", " << id.sourceType << ")"; +} + +std::ostream &operator<<(std::ostream &out, const IdPaths &idPaths) +{ + return out << idPaths.id << ", " << idPaths.sourceIds << ")"; +} + +std::ostream &operator<<(std::ostream &out, const WatcherEntry &entry) +{ + return out << "(" << entry.sourceId << ", " << entry.sourceContextId << ", " << entry.id << ", " + << entry.lastModified << ")"; +} + std::ostream &operator<<(std::ostream &out, const ModelNode &node) { if (!node.isValid()) diff --git a/tests/unit/unittest/gtest-creator-printing.h b/tests/unit/unittest/gtest-creator-printing.h index e1966822578..4148a2927d7 100644 --- a/tests/unit/unittest/gtest-creator-printing.h +++ b/tests/unit/unittest/gtest-creator-printing.h @@ -216,6 +216,10 @@ class ModelNode; class VariantProperty; template class BasicId; +class WatcherEntry; +class IdPaths; +class ProjectChunkId; +enum class SourceType : int; std::ostream &operator<<(std::ostream &out, const ModelNode &node); std::ostream &operator<<(std::ostream &out, const VariantProperty &property); @@ -226,6 +230,11 @@ std::ostream &operator<<(std::ostream &out, const BasicId +#include + +#include + +namespace { + +using Watcher = QmlDesigner::ProjectStoragePathWatcher, + NiceMock, + NiceMock>; +using QmlDesigner::IdPaths; +using QmlDesigner::ProjectChunkId; +using QmlDesigner::ProjectChunkIds; +using QmlDesigner::ProjectPartId; +using QmlDesigner::ProjectPartIds; +using QmlDesigner::SourceContextId; +using QmlDesigner::SourceContextIds; +using QmlDesigner::SourceId; +using QmlDesigner::SourceIds; +using QmlDesigner::SourcePath; +using QmlDesigner::SourcePathView; +using QmlDesigner::SourceType; +using QmlDesigner::WatcherEntries; +using QmlDesigner::WatcherEntry; + +class ProjectStoragePathWatcher : public testing::Test +{ +protected: + ProjectStoragePathWatcher() + { + ON_CALL(sourcePathCacheMock, sourceId(Eq(path1))).WillByDefault(Return(pathIds[0])); + ON_CALL(sourcePathCacheMock, sourceId(Eq(path2))).WillByDefault(Return(pathIds[1])); + ON_CALL(sourcePathCacheMock, sourceId(Eq(path3))).WillByDefault(Return(pathIds[2])); + ON_CALL(sourcePathCacheMock, sourceId(Eq(path4))).WillByDefault(Return(pathIds[3])); + ON_CALL(sourcePathCacheMock, sourceId(Eq(path5))).WillByDefault(Return(pathIds[4])); + ON_CALL(sourcePathCacheMock, sourcePath(Eq(pathIds[0]))).WillByDefault(Return(SourcePath{path1})); + ON_CALL(sourcePathCacheMock, sourcePath(Eq(pathIds[1]))).WillByDefault(Return(SourcePath{path2})); + ON_CALL(sourcePathCacheMock, sourcePath(Eq(pathIds[2]))).WillByDefault(Return(SourcePath{path3})); + ON_CALL(sourcePathCacheMock, sourcePath(Eq(pathIds[3]))).WillByDefault(Return(SourcePath{path4})); + ON_CALL(sourcePathCacheMock, sourcePath(Eq(pathIds[4]))).WillByDefault(Return(SourcePath{path5})); + ON_CALL(sourcePathCacheMock, sourceContextId(TypedEq(pathIds[0]))) + .WillByDefault(Return(sourceContextIds[0])); + ON_CALL(sourcePathCacheMock, sourceContextId(TypedEq(pathIds[1]))) + .WillByDefault(Return(sourceContextIds[0])); + ON_CALL(sourcePathCacheMock, sourceContextId(TypedEq(pathIds[2]))) + .WillByDefault(Return(sourceContextIds[1])); + ON_CALL(sourcePathCacheMock, sourceContextId(TypedEq(pathIds[3]))) + .WillByDefault(Return(sourceContextIds[1])); + ON_CALL(sourcePathCacheMock, sourceContextId(TypedEq(pathIds[4]))) + .WillByDefault(Return(sourceContextIds[2])); + ON_CALL(mockFileSystem, lastModified(_)).WillByDefault(Return(1)); + ON_CALL(sourcePathCacheMock, + sourceContextId(TypedEq(sourceContextPathString))) + .WillByDefault(Return(sourceContextIds[0])); + ON_CALL(sourcePathCacheMock, + sourceContextId(TypedEq(sourceContextPathString2))) + .WillByDefault(Return(sourceContextIds[1])); + ON_CALL(sourcePathCacheMock, sourceContextPath(Eq(sourceContextIds[0]))) + .WillByDefault(Return(sourceContextPath)); + ON_CALL(sourcePathCacheMock, sourceContextPath(Eq(sourceContextIds[1]))) + .WillByDefault(Return(sourceContextPath2)); + ON_CALL(sourcePathCacheMock, sourceContextPath(Eq(sourceContextIds[2]))) + .WillByDefault(Return(sourceContextPath3)); + ON_CALL(mockFileSystem, directoryEntries(Eq(sourceContextPath))) + .WillByDefault(Return(SourceIds{pathIds[0], pathIds[1]})); + ON_CALL(mockFileSystem, directoryEntries(Eq(sourceContextPath2))) + .WillByDefault(Return(SourceIds{pathIds[2], pathIds[3]})); + ON_CALL(mockFileSystem, directoryEntries(Eq(sourceContextPath3))) + .WillByDefault(Return(SourceIds{pathIds[4]})); + } + static WatcherEntries sorted(WatcherEntries &&entries) + { + std::stable_sort(entries.begin(), entries.end()); + + return std::move(entries); + } + +protected: + NiceMock sourcePathCacheMock; + NiceMock notifier; + NiceMock mockFileSystem; + Watcher watcher{sourcePathCacheMock, mockFileSystem, ¬ifier}; + NiceMock &mockQFileSytemWatcher = watcher.fileSystemWatcher(); + ProjectChunkId id1{ProjectPartId{2}, SourceType::Qml}; + ProjectChunkId id2{ProjectPartId{2}, SourceType::QmlUi}; + ProjectChunkId id3{ProjectPartId{4}, SourceType::QmlTypes}; + SourcePathView path1{"/path/path1"}; + SourcePathView path2{"/path/path2"}; + SourcePathView path3{"/path2/path1"}; + SourcePathView path4{"/path2/path2"}; + SourcePathView path5{"/path3/path"}; + QString path1QString = QString(path1.toStringView()); + QString path2QString = QString(path2.toStringView()); + QString sourceContextPath = "/path"; + QString sourceContextPath2 = "/path2"; + QString sourceContextPath3 = "/path3"; + Utils::PathString sourceContextPathString = sourceContextPath; + Utils::PathString sourceContextPathString2 = sourceContextPath2; + SourceIds pathIds = {SourceId{1}, SourceId{2}, SourceId{3}, SourceId{4}, SourceId{5}}; + SourceContextIds sourceContextIds = {SourceContextId{1}, SourceContextId{2}, SourceContextId{3}}; + ProjectChunkIds ids{id1, id2, id3}; + WatcherEntry watcherEntry1{id1, sourceContextIds[0], pathIds[0]}; + WatcherEntry watcherEntry2{id2, sourceContextIds[0], pathIds[0]}; + WatcherEntry watcherEntry3{id1, sourceContextIds[0], pathIds[1]}; + WatcherEntry watcherEntry4{id2, sourceContextIds[0], pathIds[1]}; + WatcherEntry watcherEntry5{id3, sourceContextIds[0], pathIds[1]}; + WatcherEntry watcherEntry6{id1, sourceContextIds[1], pathIds[2]}; + WatcherEntry watcherEntry7{id2, sourceContextIds[1], pathIds[3]}; + WatcherEntry watcherEntry8{id3, sourceContextIds[1], pathIds[3]}; +}; + +TEST_F(ProjectStoragePathWatcher, AddIdPaths) +{ + EXPECT_CALL(mockQFileSytemWatcher, + addPaths( + UnorderedElementsAre(QString(sourceContextPath), QString(sourceContextPath2)))); + + watcher.updateIdPaths( + {{id1, {pathIds[0], pathIds[1], pathIds[2]}}, {id2, {pathIds[0], pathIds[1], pathIds[3]}}}); +} + +TEST_F(ProjectStoragePathWatcher, UpdateIdPathsCallsAddPathInFileWatcher) +{ + watcher.updateIdPaths({{id1, {pathIds[0], pathIds[1]}}, {id2, {pathIds[0], pathIds[1]}}}); + + EXPECT_CALL(mockQFileSytemWatcher, addPaths(UnorderedElementsAre(QString(sourceContextPath2)))); + + watcher.updateIdPaths( + {{id1, {pathIds[0], pathIds[1], pathIds[2]}}, {id2, {pathIds[0], pathIds[1], pathIds[3]}}}); +} + +TEST_F(ProjectStoragePathWatcher, UpdateIdPathsAndRemoveUnusedPathsCallsRemovePathInFileWatcher) +{ + watcher.updateIdPaths( + {{id1, {pathIds[0], pathIds[1], pathIds[2]}}, {id2, {pathIds[0], pathIds[1], pathIds[3]}}}); + + EXPECT_CALL(mockQFileSytemWatcher, removePaths(UnorderedElementsAre(QString(sourceContextPath2)))); + + watcher.updateIdPaths({{id1, {pathIds[0], pathIds[1]}}, {id2, {pathIds[0], pathIds[1]}}}); +} + +TEST_F(ProjectStoragePathWatcher, UpdateIdPathsAndRemoveUnusedPathsDoNotCallsRemovePathInFileWatcher) +{ + watcher.updateIdPaths({{id1, {pathIds[0], pathIds[1], pathIds[2]}}, + {id2, {pathIds[0], pathIds[1], pathIds[3]}}, + {id3, {pathIds[0]}}}); + + EXPECT_CALL(mockQFileSytemWatcher, removePaths(_)).Times(0); + + watcher.updateIdPaths({{id1, {pathIds[1]}}, {id2, {pathIds[3]}}}); +} + +TEST_F(ProjectStoragePathWatcher, UpdateIdPathsAndRemoveUnusedPaths) +{ + watcher.updateIdPaths({{id1, {pathIds[0], pathIds[1]}}, {id2, {pathIds[0], pathIds[1]}}, {id3, {pathIds[1]}}}); + + watcher.updateIdPaths({{id1, {pathIds[0]}}, {id2, {pathIds[1]}}}); + + ASSERT_THAT(watcher.watchedEntries(), ElementsAre(watcherEntry1, watcherEntry4, watcherEntry5)); +} + +TEST_F(ProjectStoragePathWatcher, ExtractSortedEntriesFromConvertIdPaths) +{ + auto entriesAndIds = watcher.convertIdPathsToWatcherEntriesAndIds({{id2, {pathIds[0], pathIds[1]}}, {id1, {pathIds[0], pathIds[1]}}}); + + ASSERT_THAT(entriesAndIds.first, + ElementsAre(watcherEntry1, watcherEntry2, watcherEntry3, watcherEntry4)); +} + +TEST_F(ProjectStoragePathWatcher, ExtractSortedIdsFromConvertIdPaths) +{ + auto entriesAndIds = watcher.convertIdPathsToWatcherEntriesAndIds({{id2, {}}, {id1, {}}, {id3, {}}}); + + ASSERT_THAT(entriesAndIds.second, ElementsAre(ids[0], ids[1], ids[2])); +} + +TEST_F(ProjectStoragePathWatcher, MergeEntries) +{ + watcher.updateIdPaths({{id1, {pathIds[0]}}, {id2, {pathIds[1]}}}); + + ASSERT_THAT(watcher.watchedEntries(), ElementsAre(watcherEntry1, watcherEntry4)); +} + +TEST_F(ProjectStoragePathWatcher, MergeMoreEntries) +{ + watcher.updateIdPaths({{id2, {pathIds[0], pathIds[1]}}}); + + watcher.updateIdPaths({{id1, {pathIds[0], pathIds[1]}}}); + + ASSERT_THAT(watcher.watchedEntries(), ElementsAre(watcherEntry1, watcherEntry2, watcherEntry3, watcherEntry4)); +} + +TEST_F(ProjectStoragePathWatcher, AddEmptyEntries) +{ + EXPECT_CALL(mockQFileSytemWatcher, addPaths(_)) + .Times(0); + + watcher.updateIdPaths({}); +} + +TEST_F(ProjectStoragePathWatcher, AddEntriesWithSameIdAndDifferentPaths) +{ + EXPECT_CALL(mockQFileSytemWatcher, + addPaths(ElementsAre(sourceContextPath, sourceContextPath2, sourceContextPath3))); + + watcher.updateIdPaths({{id1, {pathIds[0], pathIds[1], pathIds[2], pathIds[4]}}}); +} + +TEST_F(ProjectStoragePathWatcher, AddEntriesWithDifferentIdAndSamePaths) +{ + EXPECT_CALL(mockQFileSytemWatcher, addPaths(ElementsAre(sourceContextPath))); + + watcher.updateIdPaths({{id1, {pathIds[0], pathIds[1]}}}); +} + +TEST_F(ProjectStoragePathWatcher, DontAddNewEntriesWithSameIdAndSamePaths) +{ + watcher.updateIdPaths({{id1, {pathIds[0], pathIds[1], pathIds[2], pathIds[3], pathIds[4]}}}); + + EXPECT_CALL(mockQFileSytemWatcher, addPaths(_)).Times(0); + + watcher.updateIdPaths({{id1, {pathIds[0], pathIds[1], pathIds[2], pathIds[3], pathIds[4]}}}); +} + +TEST_F(ProjectStoragePathWatcher, DontAddNewEntriesWithDifferentIdAndSamePaths) +{ + watcher.updateIdPaths({{id1, {pathIds[0], pathIds[1], pathIds[2], pathIds[3], pathIds[4]}}}); + + EXPECT_CALL(mockQFileSytemWatcher, addPaths(_)).Times(0); + + watcher.updateIdPaths({{id2, {pathIds[0], pathIds[1], pathIds[2], pathIds[3], pathIds[4]}}}); +} + +TEST_F(ProjectStoragePathWatcher, RemoveEntriesWithId) +{ + watcher.updateIdPaths({{id1, {pathIds[0], pathIds[1]}}, + {id2, {pathIds[0], pathIds[1]}}, + {id3, {pathIds[1], pathIds[3]}}}); + + watcher.removeIds({ProjectPartId{2}}); + + ASSERT_THAT(watcher.watchedEntries(), ElementsAre(watcherEntry5, watcherEntry8)); +} + +TEST_F(ProjectStoragePathWatcher, RemoveNoPathsForEmptyIds) +{ + EXPECT_CALL(mockQFileSytemWatcher, removePaths(_)) + .Times(0); + + watcher.removeIds({}); +} + +TEST_F(ProjectStoragePathWatcher, RemoveNoPathsForOneId) +{ + watcher.updateIdPaths( + {{id1, {pathIds[0], pathIds[1]}}, {id2, {pathIds[0], pathIds[1], pathIds[3]}}}); + + EXPECT_CALL(mockQFileSytemWatcher, removePaths(_)) + .Times(0); + + watcher.removeIds({id3.id}); +} + +TEST_F(ProjectStoragePathWatcher, RemovePathForOneId) +{ + watcher.updateIdPaths( + {{id1, {pathIds[0], pathIds[1]}}, {id3, {pathIds[0], pathIds[1], pathIds[3]}}}); + + EXPECT_CALL(mockQFileSytemWatcher, removePaths(ElementsAre(sourceContextPath2))); + + watcher.removeIds({id3.id}); +} + +TEST_F(ProjectStoragePathWatcher, RemoveNoPathSecondTime) +{ + watcher.updateIdPaths( + {{id1, {pathIds[0], pathIds[1]}}, {id2, {pathIds[0], pathIds[1], pathIds[3]}}}); + watcher.removeIds({id2.id}); + + EXPECT_CALL(mockQFileSytemWatcher, removePaths(_)).Times(0); + + watcher.removeIds({id2.id}); +} + +TEST_F(ProjectStoragePathWatcher, RemoveAllPathsForThreeId) +{ + watcher.updateIdPaths( + {{id1, {pathIds[0], pathIds[1], pathIds[2]}}, {id2, {pathIds[0], pathIds[1], pathIds[3]}}}); + + EXPECT_CALL(mockQFileSytemWatcher, + removePaths(ElementsAre(sourceContextPath, sourceContextPath2))); + + watcher.removeIds({id1.id, id2.id, id3.id}); +} + +TEST_F(ProjectStoragePathWatcher, RemoveOnePathForTwoId) +{ + watcher.updateIdPaths( + {{id1, {pathIds[0], pathIds[1]}}, {id2, {pathIds[0], pathIds[1]}}, {id3, {pathIds[3]}}}); + + EXPECT_CALL(mockQFileSytemWatcher, removePaths(ElementsAre(sourceContextPath))); + + watcher.removeIds({id1.id, id2.id}); +} + +TEST_F(ProjectStoragePathWatcher, NotAnymoreWatchedEntriesWithId) +{ + watcher.addEntries(sorted({watcherEntry1, watcherEntry2, watcherEntry3, watcherEntry4, watcherEntry5})); + + auto oldEntries = watcher.notAnymoreWatchedEntriesWithIds({watcherEntry1, watcherEntry4}, {ids[0], ids[1]}); + + ASSERT_THAT(oldEntries, ElementsAre(watcherEntry2, watcherEntry3)); +} + +TEST_F(ProjectStoragePathWatcher, RemoveUnusedEntries) +{ + watcher.addEntries(sorted({watcherEntry1, watcherEntry2, watcherEntry3, watcherEntry4, watcherEntry5})); + + watcher.removeFromWatchedEntries({watcherEntry2, watcherEntry3}); + + ASSERT_THAT(watcher.watchedEntries(), ElementsAre(watcherEntry1, watcherEntry4, watcherEntry5)); +} + +TEST_F(ProjectStoragePathWatcher, TwoNotifyFileChanges) +{ + watcher.updateIdPaths({{id1, {pathIds[0], pathIds[1], pathIds[2]}}, + {id2, {pathIds[0], pathIds[1], pathIds[2], pathIds[3], pathIds[4]}}, + {id3, {pathIds[4]}}}); + ON_CALL(mockFileSystem, lastModified(Eq(pathIds[0]))).WillByDefault(Return(2)); + ON_CALL(mockFileSystem, lastModified(Eq(pathIds[1]))).WillByDefault(Return(2)); + ON_CALL(mockFileSystem, lastModified(Eq(pathIds[3]))).WillByDefault(Return(2)); + + EXPECT_CALL(notifier, + pathsWithIdsChanged( + ElementsAre(IdPaths{id1, {SourceId{1}, SourceId{2}}}, + IdPaths{id2, {SourceId{1}, SourceId{2}, SourceId{4}}}))); + + mockQFileSytemWatcher.directoryChanged(sourceContextPath); + mockQFileSytemWatcher.directoryChanged(sourceContextPath2); +} + +TEST_F(ProjectStoragePathWatcher, NotifyForPathChanges) +{ + watcher.updateIdPaths( + {{id1, {pathIds[0], pathIds[1], pathIds[2]}}, {id2, {pathIds[0], pathIds[1], pathIds[3]}}}); + ON_CALL(mockFileSystem, lastModified(Eq(pathIds[0]))).WillByDefault(Return(2)); + ON_CALL(mockFileSystem, lastModified(Eq(pathIds[3]))).WillByDefault(Return(2)); + + EXPECT_CALL(notifier, pathsChanged(ElementsAre(pathIds[0]))); + + mockQFileSytemWatcher.directoryChanged(sourceContextPath); +} + +TEST_F(ProjectStoragePathWatcher, NoNotifyForUnwatchedPathChanges) +{ + watcher.updateIdPaths({{id1, {pathIds[3]}}, {id2, {pathIds[3]}}}); + + EXPECT_CALL(notifier, pathsChanged(IsEmpty())); + + mockQFileSytemWatcher.directoryChanged(sourceContextPath); +} + +TEST_F(ProjectStoragePathWatcher, NoDuplicatePathChanges) +{ + watcher.updateIdPaths( + {{id1, {pathIds[0], pathIds[1], pathIds[2]}}, {id2, {pathIds[0], pathIds[1], pathIds[3]}}}); + ON_CALL(mockFileSystem, lastModified(Eq(pathIds[0]))).WillByDefault(Return(2)); + + EXPECT_CALL(notifier, pathsChanged(ElementsAre(pathIds[0]))); + + mockQFileSytemWatcher.directoryChanged(sourceContextPath); + mockQFileSytemWatcher.directoryChanged(sourceContextPath); +} +} // namespace diff --git a/tests/unit/unittest/projectstoragepathwatchermock.h b/tests/unit/unittest/projectstoragepathwatchermock.h new file mode 100644 index 00000000000..509f9868313 --- /dev/null +++ b/tests/unit/unittest/projectstoragepathwatchermock.h @@ -0,0 +1,41 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "googletest.h" + +#include "projectstorage/projectstoragepathwatcherinterface.h" + +class MockProjectStoragePathWatcher : public QmlDesigner::ProjectStoragePathWatcherInterface +{ +public: + MOCK_METHOD(void, updateIdPaths, (const std::vector &idPaths), ()); + MOCK_METHOD(void, removeIds, (const QmlDesigner::ProjectPartIds &ids), ()); + MOCK_METHOD(void, + setNotifier, + (QmlDesigner::ProjectStoragePathWatcherNotifierInterface * notifier), + ()); +}; diff --git a/tests/unit/unittest/projectstoragepathwatchernotifiermock.h b/tests/unit/unittest/projectstoragepathwatchernotifiermock.h new file mode 100644 index 00000000000..881a261b9ec --- /dev/null +++ b/tests/unit/unittest/projectstoragepathwatchernotifiermock.h @@ -0,0 +1,42 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "googletest.h" + +#include + +class ProjectStoragePathWatcherNotifierMock + : public QmlDesigner::ProjectStoragePathWatcherNotifierInterface +{ +public: + MOCK_METHOD(void, + pathsWithIdsChanged, + (const std::vector &idPaths), + (override)); + MOCK_METHOD(void, pathsChanged, (const QmlDesigner::SourceIds &sourceIds), (override)); +}; + diff --git a/tests/unit/unittest/sourcepathcachemock.h b/tests/unit/unittest/sourcepathcachemock.h new file mode 100644 index 00000000000..17e820342c5 --- /dev/null +++ b/tests/unit/unittest/sourcepathcachemock.h @@ -0,0 +1,48 @@ +/**************************************************************************** +** +** Copyright (C) 2021 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "googletest.h" + +#include +#include + +class SourcePathCacheMock +{ +public: + MOCK_METHOD(QmlDesigner::SourceId, sourceId, (QmlDesigner::SourcePathView sourcePath), (const)); + MOCK_METHOD(QmlDesigner::SourcePath, sourcePath, (QmlDesigner::SourceId sourceId), (const)); + MOCK_METHOD(QmlDesigner::SourceContextId, + sourceContextId, + (Utils::SmallStringView directoryPath), + (const)); + MOCK_METHOD(Utils::PathString, + sourceContextPath, + (QmlDesigner::SourceContextId directoryPathId), + (const)); + MOCK_METHOD(QmlDesigner::SourceContextId, sourceContextId, (QmlDesigner::SourceId sourceId), (const)); + MOCK_METHOD(void, populateIfEmpty, ()); +};