From 3c8984eb13816a96c5c2a1d72060a53ddeae4071 Mon Sep 17 00:00:00 2001 From: Andrei BENCSIK Date: Sat, 27 Jan 2024 23:28:44 +0200 Subject: [PATCH] Initial idea for unit tests for ModLibrary add a fileshystem stub for the filesystem functions used in ModLibrary::rescan ideas: make a struct with function pointers, why? * simpler than virtual (yes, it is) * allows us to easily use std::filesystem::functions, no need for wrappers * we don't use gmock anyways, so mocking isn't as easy * simpler than virtual (yes, I really don't like 'em, that's why it's twice) very much WIP, didn't test much, it's really late, this commit message will change :) --- src/data/mod_library.cpp | 81 +++++++++++++++++++++++++++++++-------- src/data/mod_library.hpp | 23 ++++++++++- test/CMakeLists.txt | 26 +++++++------ test/test_mod_library.cpp | 30 +++++++++++++++ 4 files changed, 130 insertions(+), 30 deletions(-) create mode 100644 test/test_mod_library.cpp diff --git a/src/data/mod_library.cpp b/src/data/mod_library.cpp index cf3772a9b..2260357e7 100644 --- a/src/data/mod_library.cpp +++ b/src/data/mod_library.cpp @@ -27,32 +27,70 @@ #include +static void nativeFilesystemIterate( + const std::filesystem::path& p, + std::filesystem::directory_options options, + std::error_code& ec, + Filesystem::DirectoryIterateCallbackFuncT callback, + void* userData) noexcept +{ + namespace fs = std::filesystem; + auto dir_it = fs::directory_iterator(p, options, ec); + if (ec) + { + return; + } + + for (const auto& dir_entry : dir_it) + { + if (!callback(dir_entry.path(), userData)) + { + return; + } + } +} + +const Filesystem gNativeFilesystemHandle{ + &std::filesystem::is_directory, + &std::filesystem::exists, + &nativeFilesystemIterate}; + namespace rigel::data { namespace { -bool isNonEmptyDirectory(const std::filesystem::directory_entry& entry) +bool isNonEmptyDirectory( + const std::filesystem::path& entry, + const Filesystem& fsHandle) { namespace fs = std::filesystem; - std::error_code err; - if (!entry.is_directory(err) || err) + if (!fsHandle.f_is_directory(entry, err) || err) { return false; } - if (!entry.exists(err) || err) + if (!fsHandle.f_exists(entry, err) || err) { return false; } - using std::begin; - using std::end; + bool foundOne = false; + fsHandle.f_directory_iterate( + entry, + fs::directory_options::none, + err, + [](const std::filesystem::path& p, void* userData) noexcept -> bool + { + *static_cast(userData) = true; + // once we've found one, we can just return, it's surely non empty + return false; + }, + &foundOne); - const auto iContents = fs::directory_iterator{entry.path(), err}; - return !err && begin(iContents) != end(iContents); + return !err && !foundOne; } } // namespace @@ -82,7 +120,7 @@ void ModLibrary::updateGamePath(std::filesystem::path gamePath) } -void ModLibrary::rescan() +void ModLibrary::rescan(const Filesystem& fsHandle) { namespace fs = std::filesystem; @@ -94,18 +132,27 @@ void ModLibrary::rescan() auto newAvailableMods = std::vector{}; std::error_code err; - auto iModsDir = fs::directory_iterator{ - mGamePath / MODS_PATH, fs::directory_options::skip_permission_denied, err}; - if (!err) + struct Context { - for (const auto& entry : iModsDir) + std::vector* pNewAvailableMods = nullptr; + const Filesystem* pFsHandle = nullptr; + } ctx{&newAvailableMods, &fsHandle}; + + fsHandle.f_directory_iterate( + mGamePath / MODS_PATH, + fs::directory_options::skip_permission_denied, + err, + [](const std::filesystem::path& p, void* userData) noexcept -> bool { - if (isNonEmptyDirectory(entry)) + auto ctx = + static_cast(userData); + if (isNonEmptyDirectory(p, *(ctx->pFsHandle))) { - newAvailableMods.push_back(entry.path().filename().u8string()); + ctx->pNewAvailableMods->push_back(p.filename().u8string()); } - } - } + return true; + }, + &ctx); LOG_F(INFO, "Found %d mods", int(newAvailableMods.size())); diff --git a/src/data/mod_library.hpp b/src/data/mod_library.hpp index 722170bd2..cc899abb2 100644 --- a/src/data/mod_library.hpp +++ b/src/data/mod_library.hpp @@ -21,6 +21,27 @@ #include #include +struct Filesystem +{ + using IsDirectoryFuncT = + bool (*)(const std::filesystem::path& p, std::error_code& ec); + using ExistsFuncT = + bool (*)(const std::filesystem::path& p, std::error_code& ec) noexcept; + using DirectoryIterateCallbackFuncT = + bool (*)(const std::filesystem::path& p, void* user_data) noexcept; + using DirectoryIterateFuncT = void (*)( + const std::filesystem::path& p, + std::filesystem::directory_options options, + std::error_code& ec, + DirectoryIterateCallbackFuncT callback, + void* user_data) noexcept; + + IsDirectoryFuncT f_is_directory; + ExistsFuncT f_exists; + DirectoryIterateFuncT f_directory_iterate; +}; + +extern const Filesystem gNativeFilesystemHandle; namespace rigel::data { @@ -57,7 +78,7 @@ class ModLibrary std::vector initialSelection); void updateGamePath(std::filesystem::path gamePath); - void rescan(); + void rescan(const Filesystem& fsHandle = gNativeFilesystemHandle); [[nodiscard]] std::vector enabledModPaths() const; [[nodiscard]] const std::string& modDirName(int index) const; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a2648f88b..c4173b38c 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,17 +1,19 @@ add_executable(tests test_main.cpp - test_array_view.cpp - test_duke_script_loader.cpp - test_elevator.cpp - test_high_score_list.cpp - test_json_utils.cpp - test_letter_collection.cpp - test_physics_system.cpp - test_player.cpp - test_rng.cpp - test_spike_ball.cpp - test_string_utils.cpp - test_timing.cpp + # test_array_view.cpp + # test_duke_script_loader.cpp + # test_elevator.cpp + # test_high_score_list.cpp + # test_json_utils.cpp + # test_letter_collection.cpp + # test_physics_system.cpp + # test_player.cpp + # test_rng.cpp + # test_spike_ball.cpp + # test_string_utils.cpp + # test_timing.cpp + + test_mod_library.cpp ) target_link_libraries(tests diff --git a/test/test_mod_library.cpp b/test/test_mod_library.cpp new file mode 100644 index 000000000..e97550299 --- /dev/null +++ b/test/test_mod_library.cpp @@ -0,0 +1,30 @@ +#include +#include + +RIGEL_DISABLE_WARNINGS +#include +RIGEL_RESTORE_WARNINGS + +#include + +TEST_CASE("Mod library rescan") +{ + namespace fs = std::filesystem; + + rigel::data::ModLibrary ml{"/tmp", {}, {}}; + + Filesystem mockFsHandle{ + [](const std::filesystem::path& p, std::error_code& ec) noexcept -> bool + { return true; }, + [](const std::filesystem::path& p, std::error_code& ec) noexcept -> bool + { return true; }, + []( + const std::filesystem::path& p, + std::filesystem::directory_options options, + std::error_code& ec, + Filesystem::DirectoryIterateCallbackFuncT callback, + void* user_data) noexcept { + }}; + + ml.rescan(mockFsHandle); +}