diff --git a/components/libComponents/include/components/FileChangeListenerImpl.h b/components/libComponents/include/components/FileChangeListenerImpl.h index f37be693..eca632ba 100644 --- a/components/libComponents/include/components/FileChangeListenerImpl.h +++ b/components/libComponents/include/components/FileChangeListenerImpl.h @@ -45,7 +45,7 @@ class FileChangeListenerImpl { struct Node { Node( utils::u8path path = {}, Node *parent = nullptr, bool isDirectory = true, bool didFileExistOnLastWatch = false) - : parent_(parent), isDirectory_(isDirectory), didFileExistOnLastWatch_(didFileExistOnLastWatch) { + : path_(path), parent_(parent), isDirectory_(isDirectory), didFileExistOnLastWatch_(didFileExistOnLastWatch) { } utils::u8path path_; @@ -78,6 +78,9 @@ class FileChangeListenerImpl { void onDirectoryChanged(const QString &dirPath); Node *createDirectoryWatches(const raco::utils::u8path &path); void removeDirectoryWatches(Node* node); + Node *getNode(const raco::utils::u8path &path); + void updateDirectoryWatches(Node *node); + static bool fileCanBeAccessed(const raco::utils::u8path &path); }; diff --git a/components/libComponents/src/FileChangeListenerImpl.cpp b/components/libComponents/src/FileChangeListenerImpl.cpp index 1dcae06c..d68e393a 100644 --- a/components/libComponents/src/FileChangeListenerImpl.cpp +++ b/components/libComponents/src/FileChangeListenerImpl.cpp @@ -91,6 +91,27 @@ FileChangeListenerImpl::Node* FileChangeListenerImpl::createDirectoryWatches(con return &rootNode_; } +FileChangeListenerImpl::Node* FileChangeListenerImpl::getNode(const raco::utils::u8path& path) { + if (path != path.root_path()) { + auto node = getNode(path.parent_path()); + + auto it = node->children_.find(path); + assert(it != node->children_.end()); + return it->second.get(); + } + return &rootNode_; +} + + +void FileChangeListenerImpl::updateDirectoryWatches(Node* node) { + if (node->isDirectory_ && node->path_.existsDirectory()) { + addPathToWatch(QString::fromStdString(node->path_.string())); + for (const auto& [dummy, childNode] : node->children_) { + updateDirectoryWatches(childNode.get()); + } + } +} + void FileChangeListenerImpl::remove(const std::string& absPath) { const raco::utils::u8path& path(absPath); if (fileWatcher_.removePath(QString::fromStdString(path.string()))) { @@ -147,7 +168,9 @@ void FileChangeListenerImpl::onDelayedLoad() { void FileChangeListenerImpl::onDirectoryChanged(const QString& dirPathString) { utils::u8path dirPath(dirPathString.toStdString()); - std::vector updated; + if (auto node = getNode(dirPath)) { + updateDirectoryWatches(node); + } for (const auto& [path, entry] : watchedFiles_) { if (dirPath.contains(path)) { if (path.exists() != entry->didFileExistOnLastWatch_) { diff --git a/components/libComponents/tests/FileChangeMonitor_test.cpp b/components/libComponents/tests/FileChangeMonitor_test.cpp index 927c7959..e5f60d24 100644 --- a/components/libComponents/tests/FileChangeMonitor_test.cpp +++ b/components/libComponents/tests/FileChangeMonitor_test.cpp @@ -14,12 +14,48 @@ #include "components/FileChangeMonitorImpl.h" #include "testing/TestEnvironmentCore.h" #include "utils/u8path.h" +#include "utils/FileUtils.h" #include using namespace raco::core; -class FileChangeMonitorTest : public TestEnvironmentCore { +class BasicFileChangeMonitorTest : public TestEnvironmentCore { +protected: + bool waitForFileChangeCounterGEq(int count, int timeOutInMS = 2000) { + auto start = std::chrono::steady_clock::now(); + do { + // Programmatic file/directory modifications need to be explicitly processed in a Qt event loop (QCoreApplication). + + // Briefly yield, so the QFileSystemWatcher has time to react to our file changes and + // add its event to the event loop. + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + // Process the event the QFileSystemWatcher queued. This will cause the FileChangeListener + // to create a timer. + QCoreApplication::processEvents(); + + // Wait for the timer to queue its event, and then process the timer event, which eventually + // leads to the callbacks registered with FileMonitor::registerFileChangedHandler to be called. + std::this_thread::sleep_for(std::chrono::milliseconds(raco::components::FileChangeListenerImpl::DELAYED_FILE_LOAD_TIME_MSEC + 100)); + QCoreApplication::processEvents(); + } + while (fileChangeCounter_ < count && std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count() <= timeOutInMS); + + return fileChangeCounter_ == count; + } + + int argc = 0; + // Apparently QCoreApplication needs to be initialized before the ProjectFileChangeMonitor is created, since that + // will create signal connections which don't work on Linux otherwise (but they do work on Windows). + QCoreApplication eventLoop_{argc, nullptr}; + + int fileChangeCounter_{0}; + std::function testCallback_ = [this]() { ++fileChangeCounter_; }; + std::unique_ptr testFileChangeMonitor_ = std::make_unique(); + std::vector createdFileListeners_; +}; + +class FileChangeMonitorTest : public BasicFileChangeMonitorTest { protected: static constexpr const char* TEST_RESOURCES_FOLDER_NAME = "testresources"; static constexpr const char* TEST_FILE_NAME = "test.txt"; @@ -28,8 +64,7 @@ class FileChangeMonitorTest : public TestEnvironmentCore { TestEnvironmentCore::SetUp(); std::filesystem::create_directory(testFolderPath_); - testFileOutputStream_ = std::ofstream(testFilePath_.string(), std::ios_base::out); - testFileOutputStream_.close(); + raco::utils::file::write(testFilePath_, {}); createdFileListeners_.emplace_back(testFileChangeMonitor_->registerFileChangedHandler(testFilePath_.string(), testCallback_)); } @@ -47,28 +82,6 @@ class FileChangeMonitorTest : public TestEnvironmentCore { TestEnvironmentCore::TearDown(); } - int waitForFileChangeCounterGEq(int count, int timeOutInMS = 2000) { - auto start = std::chrono::steady_clock::now(); - do { - // Programmatic file/directory modifications need to be explicitly processed in a Qt event loop (QCoreApplication). - - // Briefly yield, so the QFileSystemWatcher has time to react to our file changes and - // add its event to the event loop. - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - // Process the event the QFileSystemWatcher queued. This will cause the FileChangeListener - // to create a timer. - QCoreApplication::processEvents(); - - // Wait for the timer to queue its event, and then process the timer event, which eventually - // leads to the callbacks registered with FileMonitor::registerFileChangedHandler to be called. - std::this_thread::sleep_for(std::chrono::milliseconds(raco::components::FileChangeListenerImpl::DELAYED_FILE_LOAD_TIME_MSEC + 100)); - QCoreApplication::processEvents(); - } - while (fileChangeCounter_ < count && std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count() <= timeOutInMS); - - return fileChangeCounter_; - } - void runRenamingRoutine(raco::utils::u8path& originPath, const char* firstRename, const char* secondRename) { auto newFilePath = originPath; newFilePath.replace_filename(firstRename); @@ -77,32 +90,22 @@ class FileChangeMonitorTest : public TestEnvironmentCore { evenNewerFilePath.replace_filename(secondRename); std::filesystem::rename(testFilePath_, newFilePath); - ASSERT_EQ(waitForFileChangeCounterGEq(1), 1); + ASSERT_TRUE(waitForFileChangeCounterGEq(1)); std::filesystem::rename(newFilePath, evenNewerFilePath); - ASSERT_EQ(waitForFileChangeCounterGEq(1), 1); + ASSERT_TRUE(waitForFileChangeCounterGEq(1)); std::filesystem::rename(evenNewerFilePath, testFilePath_); - ASSERT_EQ(waitForFileChangeCounterGEq(2), 2); + ASSERT_TRUE(waitForFileChangeCounterGEq(2)); std::filesystem::rename(testFilePath_, newFilePath); - ASSERT_EQ(waitForFileChangeCounterGEq(3), 3); + ASSERT_TRUE(waitForFileChangeCounterGEq(3)); } - int argc = 0; - // Apparently QCoreApplication needs to be initialized before the ProjectFileChangeMonitor is created, since that - // will create signal connections which don't work on Linux otherwise (but they do work on Windows). - QCoreApplication eventLoop_{argc, nullptr}; - std::ofstream testFileOutputStream_; - int fileChangeCounter_{0}; raco::utils::u8path testFolderPath_{test_path().append(TEST_RESOURCES_FOLDER_NAME)}; raco::utils::u8path testFilePath_{raco::utils::u8path(testFolderPath_).append(TEST_FILE_NAME)}; - std::function testCallback_ = [this]() { ++fileChangeCounter_; }; - std::unique_ptr testFileChangeMonitor_ = std::make_unique(); - std::vector createdFileListeners_; }; - TEST_F(FileChangeMonitorTest, InstantiationNoFileChange) { ASSERT_EQ(fileChangeCounter_, 0); } @@ -110,29 +113,24 @@ TEST_F(FileChangeMonitorTest, InstantiationNoFileChange) { TEST_F(FileChangeMonitorTest, FileModificationCreation) { testFilePath_ = raco::utils::u8path(testFolderPath_).append("differentFile.txt"); createdFileListeners_.emplace_back(testFileChangeMonitor_->registerFileChangedHandler(testFilePath_.string(), testCallback_)); - ASSERT_EQ(waitForFileChangeCounterGEq(0), 0); + ASSERT_TRUE(waitForFileChangeCounterGEq(0)); - testFileOutputStream_ = std::ofstream(testFilePath_.string(), std::ios_base::out); - testFileOutputStream_.close(); + raco::utils::file::write(testFilePath_, {}); - ASSERT_EQ(waitForFileChangeCounterGEq(1), 1); + ASSERT_TRUE(waitForFileChangeCounterGEq(1)); } TEST_F(FileChangeMonitorTest, FileModificationEditing) { - testFileOutputStream_.open(testFilePath_.string(), std::ios_base::out); - - testFileOutputStream_ << "Test"; - testFileOutputStream_.flush(); - testFileOutputStream_.close(); + raco::utils::file::write(testFilePath_, "Test"); - ASSERT_EQ(waitForFileChangeCounterGEq(1), 1); + ASSERT_TRUE(waitForFileChangeCounterGEq(1)); } TEST_F(FileChangeMonitorTest, FileModificationDeletion) { std::filesystem::remove(testFilePath_); - ASSERT_EQ(waitForFileChangeCounterGEq(1), 1); + ASSERT_TRUE(waitForFileChangeCounterGEq(1)); } @@ -142,16 +140,17 @@ TEST_F(FileChangeMonitorTest, FileModificationRenaming) { TEST_F(FileChangeMonitorTest, FileModificationMultipleModificationsAtTheSameTime) { - testFileOutputStream_.open(testFilePath_.string(), std::ios_base::out); - testFileOutputStream_ << "Test"; + std::ofstream testFileOutputStream(testFilePath_.string(), std::ios_base::out); + testFileOutputStream << "Test"; std::ofstream otherOutputStream = std::ofstream(testFilePath_.string(), std::ios_base::app); otherOutputStream << "Other"; otherOutputStream.close(); - testFileOutputStream_.close(); + testFileOutputStream.close(); - ASSERT_GE(waitForFileChangeCounterGEq(1), 1); // Linux gives sometimes one, sometimes two events. + // Linux gives sometimes one, sometimes two events. + ASSERT_TRUE(waitForFileChangeCounterGEq(1)); } #if (!defined(__linux)) @@ -163,22 +162,128 @@ TEST_F(FileChangeMonitorTest, FileModificationSetAndTryToEditReadOnly) { ASSERT_TRUE(!ec) << "Failed to set permissons. Error code: " << ec.value() << " Error message: '" << ec.message() << "'"; // fileChangeCounter_ will be 0 in WSL, but the proper value in Linux container - ASSERT_EQ(waitForFileChangeCounterGEq(1), 1); + ASSERT_TRUE(waitForFileChangeCounterGEq(1)); - testFileOutputStream_.open(testFilePath_.string(), std::ios_base::out); - ASSERT_FALSE(testFileOutputStream_.is_open()); + std::ofstream testFileOutputStream(testFilePath_.string(), std::ios_base::out); + ASSERT_FALSE(testFileOutputStream.is_open()); - ASSERT_EQ(waitForFileChangeCounterGEq(1), 1); + ASSERT_TRUE(waitForFileChangeCounterGEq(1)); } #endif TEST_F(FileChangeMonitorTest, FolderModificationDeletion) { std::filesystem::remove_all(test_path().append(TEST_RESOURCES_FOLDER_NAME)); - ASSERT_EQ(waitForFileChangeCounterGEq(1), 1); + ASSERT_TRUE(waitForFileChangeCounterGEq(1)); } TEST_F(FileChangeMonitorTest, FolderModificationRenaming) { runRenamingRoutine(testFolderPath_, "newFolder", "evenNewerFolder"); } + +TEST_F(BasicFileChangeMonitorTest, create_file_existing_directory) { + auto testFolderPath = test_path() / "test-folder"; + std::string test_file_name = "test.txt"; + + std::filesystem::create_directory(testFolderPath); + + createdFileListeners_.emplace_back(testFileChangeMonitor_->registerFileChangedHandler((testFolderPath / test_file_name).string(), testCallback_)); + EXPECT_EQ(fileChangeCounter_, 0); + + raco::utils::file::write(testFolderPath / test_file_name, {}); + ASSERT_TRUE(waitForFileChangeCounterGEq(1)); +} + +TEST_F(BasicFileChangeMonitorTest, create_file_no_directory) { + auto testFolderPath = test_path() / "test-folder"; + std::string test_file_name = "test.txt"; + + createdFileListeners_.emplace_back(testFileChangeMonitor_->registerFileChangedHandler((testFolderPath / test_file_name).string(), testCallback_)); + EXPECT_EQ(fileChangeCounter_, 0); + + std::filesystem::create_directory(testFolderPath); + ASSERT_TRUE(waitForFileChangeCounterGEq(0)); + + raco::utils::file::write(testFolderPath / test_file_name, {}); + ASSERT_TRUE(waitForFileChangeCounterGEq(1)); +} + +TEST_F(FileChangeMonitorTest, directory_rename_disappearance) { + auto movedFolderPath = test_path() / "moved-directory"; + std::filesystem::rename(testFolderPath_, movedFolderPath); + ASSERT_TRUE(waitForFileChangeCounterGEq(1)); + + std::filesystem::rename(movedFolderPath, testFolderPath_); + ASSERT_TRUE(waitForFileChangeCounterGEq(2)); +} + +TEST_F(BasicFileChangeMonitorTest, directory_rename_appearance) { + auto initialFolderPath = test_path() / "initial-folder"; + auto testFolderPath = test_path() / "test-folder"; + std::string test_file_name = "test.txt"; + + std::filesystem::create_directory(initialFolderPath); + raco::utils::file::write(initialFolderPath / test_file_name, {}); + + createdFileListeners_.emplace_back(testFileChangeMonitor_->registerFileChangedHandler((testFolderPath / test_file_name).string(), testCallback_)); + EXPECT_EQ(fileChangeCounter_, 0); + + std::filesystem::rename(initialFolderPath, testFolderPath); + ASSERT_TRUE(waitForFileChangeCounterGEq(1)); +} + +TEST_F(BasicFileChangeMonitorTest, directory_appear_file_change) { + auto initialFolderPath = test_path() / "initial-folder"; + auto testFolderPath = test_path() / "test-folder"; + std::string test_file_name = "test.txt"; + + std::filesystem::create_directory(initialFolderPath); + raco::utils::file::write(initialFolderPath / test_file_name, {}); + + createdFileListeners_.emplace_back(testFileChangeMonitor_->registerFileChangedHandler((testFolderPath / test_file_name).string(), testCallback_)); + EXPECT_EQ(fileChangeCounter_, 0); + + std::filesystem::rename(initialFolderPath, testFolderPath); + ASSERT_TRUE(waitForFileChangeCounterGEq(1)); + + raco::utils::file::write(testFolderPath / test_file_name, "Test"); + ASSERT_TRUE(waitForFileChangeCounterGEq(2)); +} + +TEST_F(BasicFileChangeMonitorTest, directory_appear_file_disappear) { + auto initialFolderPath = test_path() / "initial-folder"; + auto testFolderPath = test_path() / "test-folder"; + std::string test_file_name = "test.txt"; + + std::filesystem::create_directory(initialFolderPath); + raco::utils::file::write(initialFolderPath / test_file_name, {}); + + createdFileListeners_.emplace_back(testFileChangeMonitor_->registerFileChangedHandler((testFolderPath / test_file_name).string(), testCallback_)); + EXPECT_EQ(fileChangeCounter_, 0); + + std::filesystem::rename(initialFolderPath, testFolderPath); + ASSERT_TRUE(waitForFileChangeCounterGEq(1)); + + std::filesystem::rename(testFolderPath / test_file_name, testFolderPath / "new-file-name.txt"); + ASSERT_TRUE(waitForFileChangeCounterGEq(2)); +} + +TEST_F(BasicFileChangeMonitorTest, directory_appear_file_appear) { + auto initialFolderPath = test_path() / "initial-folder"; + auto testFolderPath = test_path() / "test-folder"; + std::string initial_test_file_name = "initial.txt"; + std::string test_file_name = "test.txt"; + + std::filesystem::create_directory(initialFolderPath); + raco::utils::file::write(initialFolderPath / initial_test_file_name, {}); + + createdFileListeners_.emplace_back(testFileChangeMonitor_->registerFileChangedHandler((testFolderPath / test_file_name).string(), testCallback_)); + EXPECT_EQ(fileChangeCounter_, 0); + + std::filesystem::rename(initialFolderPath, testFolderPath); + ASSERT_TRUE(waitForFileChangeCounterGEq(0)); + + std::filesystem::rename(testFolderPath / initial_test_file_name, testFolderPath / test_file_name); + ASSERT_TRUE(waitForFileChangeCounterGEq(1)); +} diff --git a/components/libRamsesBase/include/ramses_adaptor/SceneBackend.h b/components/libRamsesBase/include/ramses_adaptor/SceneBackend.h index 1503aa91..0893e753 100644 --- a/components/libRamsesBase/include/ramses_adaptor/SceneBackend.h +++ b/components/libRamsesBase/include/ramses_adaptor/SceneBackend.h @@ -54,6 +54,8 @@ class SceneBackend : public raco::core::SceneBackendInterface { std::vector getSceneItemDescriptions() const override; std::string getExportedObjectNames(SEditorObject editorObject) const override; + static bool discardLogicEngineMessage(std::string_view message); + private: /** * @brief call LogicEngine validate() and filter out warnings that RamsesComposer is diff --git a/components/libRamsesBase/src/ramses_adaptor/SceneBackend.cpp b/components/libRamsesBase/src/ramses_adaptor/SceneBackend.cpp index 73ef4eb5..6845ea55 100644 --- a/components/libRamsesBase/src/ramses_adaptor/SceneBackend.cpp +++ b/components/libRamsesBase/src/ramses_adaptor/SceneBackend.cpp @@ -79,21 +79,28 @@ void SceneBackend::readDataFromEngine(core::DataChangeRecorder& recorder) { } } +bool SceneBackend::discardLogicEngineMessage(std::string_view message) { + if (message.find("has unlinked output") != std::string::npos) { + return true; + } + if (message.find("has no ingoing links! Node should be deleted or properly linked!") != std::string::npos) { + return true; + } + if (message.find("has no outgoing links! Node should be deleted or properly linked!") != std::string::npos) { + return true; + } + return false; +} + + std::vector SceneBackend::logicEngineFilteredValidation() const { std::vector filteredWarnings; std::vector warnings = logicEngine()->validate(); for (auto& warning : warnings) { - if (warning.message.find("has unlinked output") != std::string::npos) { - continue; - } - if (warning.message.find("has no ingoing links! Node should be deleted or properly linked!") != std::string::npos) { - continue; - } - if (warning.message.find("has no outgoing links! Node should be deleted or properly linked!") != std::string::npos) { - continue; + if (!SceneBackend::discardLogicEngineMessage(warning.message)) { + filteredWarnings.emplace_back(warning); } - filteredWarnings.emplace_back(warning); } return filteredWarnings; } diff --git a/components/libRamsesBase/src/ramses_base/Utils.cpp b/components/libRamsesBase/src/ramses_base/Utils.cpp index f3a028e2..492182f7 100644 --- a/components/libRamsesBase/src/ramses_base/Utils.cpp +++ b/components/libRamsesBase/src/ramses_base/Utils.cpp @@ -11,6 +11,7 @@ #include "data_storage/Table.h" #include "lodepng.h" +#include "ramses_adaptor/SceneBackend.h" #include "ramses_base/LogicEngine.h" #include "ramses_base/RamsesHandles.h" #include "ramses_base/EnumerationTranslations.h" @@ -500,7 +501,12 @@ void installRamsesLogHandler(bool enableTrace) { void installLogicLogHandler() { rlogic::Logger::SetDefaultLogging(false); - rlogic::Logger::SetLogHandler([](rlogic::ELogMessageType level, std::string_view message) { SPDLOG_LOGGER_CALL(raco::log_system::get(raco::log_system::RAMSES_LOGIC), toSpdLogLevel(level), message); }); + rlogic::Logger::SetLogHandler( + [](rlogic::ELogMessageType level, std::string_view message) { + if (!ramses_adaptor::SceneBackend::discardLogicEngineMessage(message)) { + SPDLOG_LOGGER_CALL(raco::log_system::get(raco::log_system::RAMSES_LOGIC), toSpdLogLevel(level), message); + } + }); } void setRamsesLogLevel(spdlog::level::level_enum level) {