diff options
| author | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-11 02:37:39 -0600 |
|---|---|---|
| committer | SulfurNitride <SulfurNitride@users.noreply.github.com> | 2026-02-11 02:37:39 -0600 |
| commit | 7ee008e150bc5bcf76082d726f719ee0fdfda982 (patch) | |
| tree | 27fb39be241fdb5ac2734c574de678977d1856d0 /libs/uibase/tests | |
Fluorine Manager: full Linux port of Mod Organizer 2
Complete native Linux port with FUSE-based virtual filesystem,
Proton/umu-run integration, and Flatpak packaging.
Key features:
- FUSE VFS replacing Windows USVFS (in-process + standalone helper for Flatpak)
- Proton/GE-Proton/umu-run launcher with env var forwarding
- Flatpak support (sandbox-aware VFS, NXM handler, umu-run)
- Wine prefix management UI
- Case-insensitive path resolution for Linux filesystems
- QSettings-safe INI handling (avoids Bethesda INI corruption)
- Portable instance support with auto-generated launcher scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'libs/uibase/tests')
| -rw-r--r-- | libs/uibase/tests/CMakeLists.txt | 13 | ||||
| -rw-r--r-- | libs/uibase/tests/cmake/CMakeLists.txt | 10 | ||||
| -rw-r--r-- | libs/uibase/tests/cmake/plugin.cpp | 4 | ||||
| -rw-r--r-- | libs/uibase/tests/test_formatters.cpp | 48 | ||||
| -rw-r--r-- | libs/uibase/tests/test_ifiletree.cpp | 1172 | ||||
| -rw-r--r-- | libs/uibase/tests/test_main.cpp | 15 | ||||
| -rw-r--r-- | libs/uibase/tests/test_strings.cpp | 47 | ||||
| -rw-r--r-- | libs/uibase/tests/test_versioning.cpp | 90 | ||||
| -rw-r--r-- | libs/uibase/tests/translations/tests_en.qm | bin | 0 -> 141 bytes | |||
| -rw-r--r-- | libs/uibase/tests/translations/tests_en.ts | 11 | ||||
| -rw-r--r-- | libs/uibase/tests/translations/tests_fr.qm | bin | 0 -> 147 bytes | |||
| -rw-r--r-- | libs/uibase/tests/translations/tests_fr.ts | 11 |
12 files changed, 1421 insertions, 0 deletions
diff --git a/libs/uibase/tests/CMakeLists.txt b/libs/uibase/tests/CMakeLists.txt new file mode 100644 index 0000000..4483ff0 --- /dev/null +++ b/libs/uibase/tests/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.16) + +add_executable(uibase-tests EXCLUDE_FROM_ALL) +target_sources(uibase-tests + PRIVATE + test_main.cpp + test_formatters.cpp + test_ifiletree.cpp + test_strings.cpp + test_versioning.cpp +) +mo2_configure_tests(uibase-tests NO_SOURCES NO_MAIN NO_MOCK WARNINGS 4 AUTOMOC OFF) +target_link_libraries(uibase-tests PRIVATE uibase) diff --git a/libs/uibase/tests/cmake/CMakeLists.txt b/libs/uibase/tests/cmake/CMakeLists.txt new file mode 100644 index 0000000..e1d636f --- /dev/null +++ b/libs/uibase/tests/cmake/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.16) + +project(uibase-tests-cmake) + +find_package(mo2-uibase CONFIG REQUIRED) + +add_library(plugin SHARED) +target_sources(plugin PRIVATE plugin.cpp) +target_link_libraries(plugin PRIVATE mo2::uibase) +set_target_properties(plugin PROPERTIES CXX_STANDARD 20) diff --git a/libs/uibase/tests/cmake/plugin.cpp b/libs/uibase/tests/cmake/plugin.cpp new file mode 100644 index 0000000..8356dcd --- /dev/null +++ b/libs/uibase/tests/cmake/plugin.cpp @@ -0,0 +1,4 @@ +#include <uibase/iplugin.h> + +class Plugin : public MOBase::IPlugin +{}; diff --git a/libs/uibase/tests/test_formatters.cpp b/libs/uibase/tests/test_formatters.cpp new file mode 100644 index 0000000..4093112 --- /dev/null +++ b/libs/uibase/tests/test_formatters.cpp @@ -0,0 +1,48 @@ +#pragma warning(push) +#pragma warning(disable : 4668) +#include <gtest/gtest.h> +#pragma warning(pop) + +#include <QString> +#include <vector> + +#include <uibase/formatters.h> + +#include <format> + +TEST(FormatterTest, Enum) +{ + enum class E + { + a = 1, + b = 2 + }; + ASSERT_EQ("1", std::format("{}", E::a)); + ASSERT_EQ("2", std::format("{}", E::b)); +} + +// simply test that we can format between string types +TEST(FormatterTest, String) +{ + using namespace std::string_literals; + ASSERT_EQ("Hello World!", std::format("{}", L"Hello World!"s)); + ASSERT_EQ("Hello World!", std::format("{}", QString("Hello World!"))); + ASSERT_EQ(L"Hello World!", std::format(L"{}", QString("Hello World!"))); +} + +TEST(FormatterTest, RandomAccessContainer) +{ + // note: this is standard since C++23, so this does not test much, and unfortunately + // there is no way to customize the output format of the random access container + ASSERT_EQ("[]", std::format("{}", std::vector<int>{})); + ASSERT_EQ("[1, 2, 3]", std::format("{}", std::vector{1, 2, 3})); + ASSERT_EQ("[1, 2, 3, 4, 5, 6, 7]", + std::format("{}", std::vector{1, 2, 3, 4, 5, 6, 7})); + + ASSERT_EQ("[\"AL\", \"Holt59\", \"Silarn\"]", + std::format("{}", QStringList{"AL", "Holt59", "Silarn"})); + + ASSERT_EQ("[QVariant(type=QString, value=MO2), QVariant(type=bool, value=true), " + "QVariant(type=int, value=45)]", + std::format("{}", QVariantList{"MO2", true, 45})); +} diff --git a/libs/uibase/tests/test_ifiletree.cpp b/libs/uibase/tests/test_ifiletree.cpp new file mode 100644 index 0000000..74fe3d7 --- /dev/null +++ b/libs/uibase/tests/test_ifiletree.cpp @@ -0,0 +1,1172 @@ +#pragma warning(push) +#pragma warning(disable : 4668) +#include <gtest/gtest.h> +#pragma warning(pop) + +#include <algorithm> +#include <ranges> +#include <string> +#include <unordered_set> +#include <variant> + +#include <uibase/ifiletree.h> + +std::ostream& operator<<(std::ostream& os, const QString& str) +{ + return os << str.toStdString(); +} + +using namespace MOBase; + +namespace std +{ +// If you can't declare the function in the class it's important that PrintTo() +// is defined in the SAME namespace that defines Point. C++'s look-up rules +// rely on that. +void PrintTo(std::shared_ptr<const FileTreeEntry> entry, std::ostream* os) +{ + *os << entry->pathFrom(nullptr, "/"); +} +} // namespace std + +/** + * + */ +struct FileListTree : public IFileTree +{ + using File = std::pair<QStringList, bool>; + + std::shared_ptr<IFileTree> makeDirectory(std::shared_ptr<const IFileTree> parent, + QString name, + std::vector<File>&& files) const + { + return std::shared_ptr<FileListTree>( + new FileListTree(parent, name, std::move(files))); + } + + std::shared_ptr<IFileTree> makeDirectory(std::shared_ptr<const IFileTree> parent, + QString name) const + { + return std::shared_ptr<FileListTree>(new FileListTree(parent, name)); + } + + bool populated() const { return m_Populated; } + + virtual bool doPopulate(std::shared_ptr<const IFileTree> parent, + std::vector<std::shared_ptr<FileTreeEntry>>& entries) const + { + // We know that the files are sorted: + QString currentName = ""; + std::vector<File> currentFiles; + for (auto& p : m_Files) { + if (currentName == "") { + currentName = p.first[0]; + } + + if (currentName != p.first[0]) { + entries.push_back(makeDirectory(parent, currentName, std::move(currentFiles))); + currentFiles.clear(); + } + + currentName = p.first[0]; + + if (p.first.size() == 1) { + if (!p.second) { + entries.push_back(makeFile(parent, currentName)); + currentName = ""; + } + } else { + currentFiles.push_back( + {QStringList(p.first.begin() + 1, p.first.end()), p.second}); + } + } + + if (currentName != "") { + entries.push_back(makeDirectory(parent, currentName, std::move(currentFiles))); + } + + m_Populated = true; + + return false; + } + + virtual std::shared_ptr<IFileTree> doClone() const override + { + return std::shared_ptr<FileListTree>(new FileListTree(nullptr, name(), m_Files)); + } + +public: + static std::shared_ptr<IFileTree> + makeTree(std::vector<std::pair<QString, bool>>&& files) + { + std::sort(std::begin(files), std::end(files), + [](const std::pair<QString, bool>& a, const std::pair<QString, bool>& b) { + return FileNameComparator::compare(a.first, b.first) < 0; + }); + + std::vector<File> pFiles; + + for (auto p : files) { + pFiles.push_back({p.first.split("/", Qt::SkipEmptyParts), p.second}); + } + + return std::shared_ptr<FileListTree>( + new FileListTree(nullptr, "", std::move(pFiles))); + } + +protected: + FileListTree(std::shared_ptr<const IFileTree> parent, QString name) + : FileTreeEntry(parent, name), IFileTree() + {} + FileListTree(std::shared_ptr<const IFileTree> parent, QString name, + std::vector<File> const& files) + : FileTreeEntry(parent, name), IFileTree(), m_Files(files) + {} + FileListTree(std::shared_ptr<const IFileTree> parent, QString name, + std::vector<File>&& files) + : FileTreeEntry(parent, name), IFileTree(), m_Files(std::move(files)) + {} + + mutable bool m_Populated = false; + std::vector<File> m_Files; +}; + +/** + * @brief Check if the given tree has been populated. + * + * Since IFileTree does not expose the "populated" flag, this is a convenient + * method that simply downcast to `FileListTree` and check `populated()` on it. + * + * @param tree The tree to check. + * + * @return true if the tree has been populated, false otherwize. + */ +bool populated(std::shared_ptr<const IFileTree> tree) +{ + return std::dynamic_pointer_cast<const FileListTree>(tree)->populated(); +} + +/** + * @brief Retrieve all the entry in the given tree. + * + * @param fileTree The tree to get the entries from. + * + * @return a vector containing all the entries in the tree. + */ +std::vector<std::shared_ptr<const FileTreeEntry>> +getAllEntries(std::shared_ptr<const IFileTree> fileTree) +{ + std::vector<std::shared_ptr<const FileTreeEntry>> entries; + for (auto entry : *fileTree) { + entries.push_back(entry); + if (entry->isDir()) { + auto childEntries = getAllEntries(entry->astree()); + entries.insert(entries.end(), childEntries.begin(), childEntries.end()); + } + } + return entries; +} + +/** + * @brief Check that the given file tree match the given entries. + * + * This is probably pretty slow but it is only for unit testing. This will check + * both way: all entries in the vector must be in the tree at the right place, and + * all entries in the tree must be in the vector. + * + * @param fileTree The tree to check. + * @param entries The entries to check. Filenames must be separated by /. Must contain + * all the entry, including intermediate directories, except the root. + * + */ +void assertTreeEquals(std::shared_ptr<const IFileTree> fileTree, + std::vector<std::pair<QString, bool>> const& entries) +{ + // Check that all entries are in the tree: + for (auto& entry : entries) { + auto treeEntry = fileTree->find(entry.first); + ASSERT_NE(treeEntry, nullptr) + << "Entry " << entry.first << " not found in the tree."; + ASSERT_EQ(entry.second, treeEntry->isDir()) + << "Entry " << entry.first << " is not of the right type."; + } + + // Check that all entries in the tree are in the vector: + auto treeEntries = getAllEntries(fileTree); + for (auto& entry : treeEntries) { + auto path = entry->pathFrom(fileTree, "/"); + auto it = std::find_if(entries.begin(), entries.end(), [&path](auto const& p) { + return p.first.compare(path, Qt::CaseInsensitive) == 0; + }); + ASSERT_NE(it, entries.end()) << "Entry '" << path << "' not expected in the tree."; + ASSERT_EQ(it->second, entry->isDir()) + << "Entry '" << path << "' is not of the right type."; + } +} + +/** + * @brief Create a mapping from path to file entry for the given tree. + * + * @param fileTree The tree to create the mapping from. + * + * @return a mapping from path (separated by /) to file entry. + */ +std::map<QString, std::shared_ptr<const FileTreeEntry>> +createMapping(std::shared_ptr<const IFileTree> fileTree) +{ + std::map<QString, std::shared_ptr<const FileTreeEntry>> mapping; + for (auto entry : *fileTree) { + mapping[entry->path("/")] = entry; + if (entry->isDir()) { + auto tmp = createMapping(entry->astree()); + mapping.insert(std::begin(tmp), std::end(tmp)); + } + } + return mapping; +} + +TEST(IFileTreeTest, ExtensionComputedCorrectly) +{ + // Fake tree to create entry: + std::shared_ptr<IFileTree> fileTree = FileListTree::makeTree({}); + + auto a = fileTree->addFile("a.txt"); + EXPECT_EQ(a->name(), "a.txt"); + EXPECT_EQ(a->suffix(), "txt"); + + fileTree->move(a, "a.c.b"); + EXPECT_EQ(a->name(), "a.c.b"); + EXPECT_EQ(a->suffix(), "b"); +} + +TEST(IFileTreeTest, TreeIsPopulatedCorrectly) +{ + std::vector<std::pair<QString, bool>> strTree{{"a/", true}, {"b", true}, + {"c.x", false}, {"d.y", false}, + {"e/q/c.t", false}, {"e/q/p", true}}; + + std::shared_ptr<IFileTree> fileTree = FileListTree::makeTree(std::move(strTree)); + + ASSERT_NE(fileTree, nullptr); + + ASSERT_TRUE(fileTree->exists("a")); + ASSERT_TRUE(fileTree->exists("b")); + ASSERT_TRUE(fileTree->exists("c.x")); + ASSERT_TRUE(fileTree->exists("d.y")); + ASSERT_TRUE(fileTree->exists("e")); + ASSERT_TRUE(fileTree->exists("e/q")); + ASSERT_TRUE(fileTree->exists("e/q/c.t")); + ASSERT_TRUE(fileTree->exists("e/q/p")); + + assertTreeEquals(fileTree, {{"a", true}, + {"b", true}, + {"c.x", false}, + {"d.y", false}, + {"e", true}, + {"e/q", true}, + {"e/q/c.t", false}, + {"e/q/p", true}}); + + // Retrieve the entry: + { + std::shared_ptr<FileTreeEntry> a = fileTree->find("a"), b = fileTree->find("b"), + cx = fileTree->find("c.x"), + dy = fileTree->find("d.y"), e = fileTree->find("e"), + e_q = fileTree->find("e/q"), + e_q_ct = fileTree->find("e/q/c.t"), + e_q_p = fileTree->find("e/q/p"); + + EXPECT_NE(a, nullptr); + EXPECT_TRUE(a->isDir()); + EXPECT_EQ(a->astree(), a); + EXPECT_EQ(a->name(), "a"); + EXPECT_EQ(a->path("/"), "a"); + EXPECT_NE(b, nullptr); + EXPECT_TRUE(b->isDir()); + EXPECT_EQ(b->astree(), b); + EXPECT_EQ(b->name(), "b"); + EXPECT_EQ(b->path("/"), "b"); + EXPECT_NE(cx, nullptr); + EXPECT_TRUE(cx->isFile()); + EXPECT_EQ(cx->astree(), nullptr); + EXPECT_EQ(cx->name(), "c.x"); + EXPECT_EQ(cx->path("/"), "c.x"); + EXPECT_NE(dy, nullptr); + EXPECT_TRUE(dy->isFile()); + EXPECT_EQ(dy->astree(), nullptr); + EXPECT_EQ(dy->name(), "d.y"); + EXPECT_EQ(dy->path("/"), "d.y"); + EXPECT_NE(e, nullptr); + EXPECT_TRUE(e->isDir()); + EXPECT_EQ(e->astree(), e); + EXPECT_EQ(e->name(), "e"); + EXPECT_EQ(e->path("/"), "e"); + EXPECT_NE(e_q, nullptr); + EXPECT_TRUE(e_q->isDir()); + EXPECT_EQ(e_q->astree(), e_q); + EXPECT_EQ(e_q->name(), "q"); + EXPECT_EQ(e_q->path("/"), "e/q"); + EXPECT_NE(e_q_ct, nullptr); + EXPECT_TRUE(e_q_ct->isFile()); + EXPECT_EQ(e_q_ct->astree(), nullptr); + EXPECT_EQ(e_q_ct->name(), "c.t"); + EXPECT_EQ(e_q_ct->path("/"), "e/q/c.t"); + EXPECT_NE(e_q_p, nullptr); + EXPECT_TRUE(e_q_p->isDir()); + EXPECT_EQ(e_q_p->astree(), e_q_p); + EXPECT_EQ(e_q_p->name(), "p"); + EXPECT_EQ(e_q_p->path("/"), "e/q/p"); + + // Some relation check: + EXPECT_EQ(a->parent(), fileTree); + EXPECT_EQ(b->parent(), fileTree); + EXPECT_EQ(cx->parent(), fileTree); + EXPECT_EQ(dy->parent(), fileTree); + EXPECT_EQ(e->parent(), fileTree); + EXPECT_EQ(e_q->parent(), e->astree()); + EXPECT_EQ(e_q_ct->parent(), e_q->astree()); + EXPECT_EQ(e_q_p->parent(), e_q->astree()); + + // Check that we can reach the children: + EXPECT_EQ(e->astree()->find("q"), e_q); + EXPECT_EQ(e->astree()->find("q/c.t"), e_q_ct); + EXPECT_EQ(e->astree()->find("q/p"), e_q_p); + + // Check the content: + EXPECT_EQ(a->astree()->size(), std::size_t{0}); + EXPECT_TRUE(a->astree()->empty()); + EXPECT_EQ(a->astree()->begin(), a->astree()->end()); + EXPECT_EQ(b->astree()->size(), std::size_t{0}); + EXPECT_TRUE(b->astree()->empty()); + EXPECT_EQ(b->astree()->begin(), b->astree()->end()); + EXPECT_EQ(cx->astree(), nullptr); + EXPECT_EQ(dy->astree(), nullptr); + EXPECT_EQ(e->astree()->size(), std::size_t{1}); + EXPECT_EQ(e->astree()->at(0), e_q); + EXPECT_EQ(e_q->astree()->size(), std::size_t{2}); + EXPECT_NE(std::find(e_q->astree()->begin(), e_q->astree()->end(), e_q_ct), + e_q->astree()->end()); + EXPECT_NE(std::find(e_q->astree()->begin(), e_q->astree()->end(), e_q_p), + e_q->astree()->end()); + + EXPECT_EQ(a->pathFrom(fileTree), "a"); + EXPECT_EQ(a->path(), "a"); + EXPECT_EQ(b->pathFrom(fileTree), "b"); + EXPECT_EQ(b->path(), "b"); + EXPECT_EQ(cx->path(), "c.x"); + EXPECT_EQ(dy->path(), "d.y"); + EXPECT_EQ(e->path(), "e"); + EXPECT_EQ(e_q->path(), "e\\q"); + EXPECT_EQ(e_q->pathFrom(e->astree()), "q"); + EXPECT_EQ(e_q_ct->path("/"), "e/q/c.t"); + EXPECT_EQ(e_q_ct->pathFrom(e->astree()), "q\\c.t"); + EXPECT_EQ(e_q_ct->pathFrom(e_q->astree(), "/"), "c.t"); + EXPECT_EQ(e_q_p->path(), "e\\q\\p"); + EXPECT_EQ(e_q_p->path("/"), "e/q/p"); + EXPECT_EQ(e_q_p->pathFrom(e->astree()), "q\\p"); + EXPECT_EQ(e_q_p->pathFrom(e_q->astree()), "p"); + + EXPECT_EQ(a->pathFrom(b->astree()), ""); + EXPECT_EQ(b->pathFrom(a->astree()), ""); + EXPECT_EQ(e->pathFrom(e_q->astree()), ""); + } + + { + std::shared_ptr<FileTreeEntry> a = fileTree->find("a", FileTreeEntry::DIRECTORY), + b = fileTree->find("b", FileTreeEntry::DIRECTORY), + cx = fileTree->find("c.x", FileTreeEntry::FILE), + dy = fileTree->find("d.y", FileTreeEntry::FILE), + e = fileTree->find("e", FileTreeEntry::DIRECTORY), + e_q = + fileTree->find("e/q", FileTreeEntry::DIRECTORY), + e_q_ct = + fileTree->find("e/q/c.t", FileTreeEntry::FILE), + e_q_p = fileTree->find("e/q/p", + FileTreeEntry::DIRECTORY); + + EXPECT_TRUE((a != nullptr && a->isDir() && a->name() == "a")); + EXPECT_TRUE((b != nullptr && b->isDir() && b->name() == "b")); + EXPECT_TRUE((cx != nullptr && cx->isFile() && cx->name() == "c.x")); + EXPECT_TRUE((dy != nullptr && dy->isFile() && dy->name() == "d.y")); + EXPECT_TRUE((e != nullptr && e->isDir() && e->name() == "e")); + EXPECT_TRUE((e_q != nullptr && e_q->isDir() && e_q->name() == "q")); + EXPECT_TRUE((e_q_ct != nullptr && e_q_ct->isFile() && e_q_ct->name() == "c.t")); + EXPECT_TRUE((e_q_p != nullptr && e_q_p->isDir() && e_q_p->name() == "p")); + + EXPECT_EQ(fileTree->find("a", FileTreeEntry::FILE), nullptr); + EXPECT_EQ(fileTree->find("b", FileTreeEntry::FILE), nullptr); + EXPECT_EQ(fileTree->find("c.x", FileTreeEntry::DIRECTORY), nullptr); + EXPECT_EQ(fileTree->find("d.y", FileTreeEntry::DIRECTORY), nullptr); + EXPECT_EQ(fileTree->find("e", FileTreeEntry::FILE), nullptr); + EXPECT_EQ(fileTree->find("e/q", FileTreeEntry::FILE), nullptr); + EXPECT_EQ(fileTree->find("e/q/c.t", FileTreeEntry::DIRECTORY), nullptr); + EXPECT_EQ(fileTree->find("e/q/p", FileTreeEntry::FILE), nullptr); + } +} + +TEST(IFileTreeTest, TreeIsDestructedCorrectly) +{ + std::vector<std::pair<QString, bool>> strTree{{"a/", true}, {"b", true}, + {"c.x", false}, {"d.y", false}, + {"e/q/c.t", false}, {"e/q/p", true}}; + + std::shared_ptr<IFileTree> fileTree = FileListTree::makeTree(std::move(strTree)); + + EXPECT_NE(fileTree, nullptr); + + // Retrieve weak ptr for the entry: + std::weak_ptr<FileTreeEntry> a = fileTree->find("a"), b = fileTree->find("b"), + cx = fileTree->find("c.x"), dy = fileTree->find("d.y"), + e = fileTree->find("e"), e_q = fileTree->find("e/q"), + e_q_ct = fileTree->find("e/q/c.t"), + e_q_p = fileTree->find("e/q/p"); + + // And for the trees: + std::weak_ptr<IFileTree> r_t = fileTree, a_t = a.lock()->astree(), + b_t = b.lock()->astree(), e_t = e.lock()->astree(), + e_q_t = e_q.lock()->astree(), + e_q_p_t = e_q_p.lock()->astree(); + + // Release the base tree: + fileTree.reset(); + + EXPECT_TRUE(a.expired()); + EXPECT_TRUE(b.expired()); + EXPECT_TRUE(cx.expired()); + EXPECT_TRUE(dy.expired()); + EXPECT_TRUE(e.expired()); + EXPECT_TRUE(e_q.expired()); + EXPECT_TRUE(e_q_ct.expired()); + EXPECT_TRUE(e_q_p.expired()); + + EXPECT_TRUE(a_t.expired()); + EXPECT_TRUE(b_t.expired()); + EXPECT_TRUE(e_t.expired()); + EXPECT_TRUE(e_q_t.expired()); + EXPECT_TRUE(e_q_p_t.expired()); +} + +TEST(IFileTreeTest, BasicTreeManipulation) +{ + std::vector<std::pair<QString, bool>> strTree{{"a/", true}, {"b", true}, + {"c.x", false}, {"d.y", false}, + {"e/q/c.t", false}, {"e/q/p", true}}; + + std::shared_ptr<IFileTree> fileTree = FileListTree::makeTree(std::move(strTree)); + + EXPECT_NE(fileTree, nullptr); + + // Retrieve the entry: + std::shared_ptr<FileTreeEntry> a = fileTree->find("a"), b = fileTree->find("b"), + cx = fileTree->find("c.x"), dy = fileTree->find("d.y"), + e = fileTree->find("e"), e_q = fileTree->find("e/q"), + e_q_ct = fileTree->find("e/q/c.t"), + e_q_p = fileTree->find("e/q/p"); + + EXPECT_TRUE(b->moveTo(a->astree())); + EXPECT_FALSE(fileTree->exists("b")); + EXPECT_EQ(fileTree->find("a/b"), b); + EXPECT_TRUE(a->astree()->exists("b")); + EXPECT_EQ(a->astree()->find("b"), b); + EXPECT_EQ(a->astree()->size(), std::size_t{1}); + EXPECT_EQ(a->astree()->at(0), b); +} + +TEST(IFileTreeTest, IterOperations) +{ + auto tree = + FileListTree::makeTree({{"a", true}, {"c", true}, {"b", false}, {"d", false}}); + + // Order should be a -> c -> b -> d + std::vector expected{tree->find("a"), tree->find("c"), tree->find("b"), + tree->find("d")}; + std::vector entries(std::begin(*tree), std::end(*tree)); + EXPECT_EQ(entries, expected); + + // Order should be reversed: + expected = std::vector(expected.rbegin(), expected.rend()); + entries = std::vector(std::rbegin(*tree), std::rend(*tree)); + EXPECT_EQ(entries, expected); + + // We can erasae in the middle: + for (auto it = tree->begin(); it != tree->end();) { + if ((*it)->name() == "b") { + it = tree->erase(*it); + // Check that the returned iterator is valid (it should be the iterator + // to d): + EXPECT_EQ(it, tree->end() - 1); + EXPECT_EQ(*it, tree->find("d")); + } else { + ++it; + } + } + assertTreeEquals(tree, {{"a", true}, {"c", true}, {"d", false}}); +} + +TEST(IFileTreeTest, AddOperations) +{ + { + auto fileTree = FileListTree::makeTree( + {{"a", true}, {"c.x", false}, {"e/q/c.t", false}, {"e/q/p", true}}); + auto map = createMapping(fileTree); + + EXPECT_EQ(fileTree->addFile("a"), nullptr); + EXPECT_EQ(fileTree->addFile("c.x"), nullptr); + EXPECT_EQ(fileTree->addFile("e"), nullptr); + EXPECT_EQ(fileTree->addFile("e/q"), nullptr); + EXPECT_EQ(fileTree->addFile("e/q/c.t"), nullptr); + EXPECT_EQ(fileTree->addFile("e/q/p"), nullptr); + + auto a_p = fileTree->addFile("a/p"); + EXPECT_NE(a_p, nullptr); + EXPECT_EQ(a_p->parent(), map["a"]); + + auto e_q_ct = fileTree->addFile("e/q/c.t", true); + EXPECT_NE(e_q_ct, nullptr); + EXPECT_EQ(e_q_ct->parent(), map["e/q"]); + EXPECT_EQ(map["e/q/c.t"]->parent(), nullptr); + EXPECT_EQ(map["e/q"]->astree()->size(), std::size_t{2}); + + // Directory are replaced with addFile(): + auto e_q = fileTree->addFile("e/q", true); + EXPECT_NE(e_q, nullptr); + EXPECT_EQ(e_q->parent(), map["e"]); + EXPECT_EQ(map["e/q"]->parent(), nullptr); + EXPECT_EQ(map["e"]->astree()->size(), std::size_t{1}); + } +} + +TEST(IFileTreeTest, TreeInsertOperations) +{ + + // Test failure: + { + auto fileTree = FileListTree::makeTree({{"a/", true}, + {"b", true}, + {"c.x", false}, + {"d.y", false}, + {"e/q/c.t", false}, + {"e/q/p", true}, + {"e/q/z/", true}, + {"e/q/z/a.t", false}, + {"e/q/z/b", true}, + {"f/q/c.t", false}, + {"f/q/o", true}, + {"f/q/z/b", false}, + {"f/q/z/c.t", false}}); + + EXPECT_NE(fileTree, nullptr); + + // Retrieve the entry: + auto map = createMapping(fileTree); + auto e = fileTree->findDirectory("e"); + auto f_q = fileTree->findDirectory("f/q"); + + auto it = e->insert(f_q, IFileTree::InsertPolicy::FAIL_IF_EXISTS); + EXPECT_EQ(it, e->end()); + EXPECT_EQ(f_q->parent(), fileTree->find("f")); + } + + // Test replace: + { + auto fileTree = FileListTree::makeTree({{"a/", true}, + {"b", true}, + {"c.x", false}, + {"d.y", false}, + {"e/q/c.t", false}, + {"e/q/p", true}, + {"e/q/z/", true}, + {"e/q/z/a.t", false}, + {"e/q/z/b", true}, + {"f/q/c.t", false}, + {"f/q/o", true}, + {"f/q/z/b", false}, + {"f/q/z/c.t", false}}); + + EXPECT_NE(fileTree, nullptr); + + // Retrieve the entry: + auto map = createMapping(fileTree); + auto e = fileTree->findDirectory("e"); + auto f_q = fileTree->findDirectory("f/q"); + + auto it = e->insert(f_q, IFileTree::InsertPolicy::REPLACE); + EXPECT_NE(it, e->end()); + EXPECT_EQ(f_q->parent(), e); + EXPECT_EQ(map["e/q"]->parent(), nullptr); + EXPECT_EQ(e->find("q"), map["f/q"]); + EXPECT_TRUE(fileTree->findDirectory("f")->empty()); + EXPECT_EQ(e->find("q/c.t"), map["f/q/c.t"]); + EXPECT_EQ(e->find("q/o"), map["f/q/o"]); + EXPECT_EQ(e->find("q/z"), map["f/q/z"]); + EXPECT_EQ(e->find("q/z/b"), map["f/q/z/b"]); + EXPECT_EQ(e->find("q/z/c.t"), map["f/q/z/c.t"]); + } + + // Test merge: + { + auto fileTree = FileListTree::makeTree({{"a/", true}, + {"b", true}, + {"c.x", false}, + {"d.y", false}, + {"e/q/c.t", false}, + {"e/q/p", true}, + {"e/q/z", true}, + {"e/q/z/a.t", false}, + {"e/q/z/b", true}, + {"f/q/c.t", false}, + {"f/q/o", true}, + {"f/q/z", true}, + {"f/q/z/b", false}, + {"f/q/z/c.t", false}}); + + EXPECT_NE(fileTree, nullptr); + + // Retrieve the entry: + auto map = createMapping(fileTree); + auto e = fileTree->findDirectory("e"); + auto f_q = fileTree->findDirectory("f/q"); + + auto it = e->insert(f_q, IFileTree::InsertPolicy::MERGE); + assertTreeEquals(e, {{"q", true}, + {"q/o", true}, + {"q/p", true}, + {"q/z", true}, + {"q/c.t", false}, + {"q/z/a.t", false}, + {"q/z/c.t", false}, + {"q/z/b", false}}); + EXPECT_EQ(e->find("q/z/b"), map["f/q/z/b"]); + EXPECT_EQ(fileTree->findDirectory("f")->size(), std::size_t{0}); + EXPECT_EQ(map["f/q"]->parent(), nullptr); + EXPECT_EQ(map["f/q/z"]->parent(), nullptr); + } +} + +TEST(IFileTreeTest, TreeMoveAndCopyOperations) +{ + { + auto tree1 = FileListTree::makeTree( + {{"a/b/m.y", false}, {"a/b/c", true}, {"b/", true}, {"c", false}}); + auto a = tree1->findDirectory("a"); + EXPECT_FALSE(populated(a)); + + tree1->move(tree1->find("a"), "a1"); + + // Moving the tree should not have populated it: + EXPECT_EQ(tree1->find("a"), nullptr); + EXPECT_EQ(tree1->find("a1"), a); + EXPECT_FALSE(populated(a)); + + tree1->copy(tree1->find("a1"), "a2"); + + // Copying the tree should not have populated it: + EXPECT_FALSE(populated(a)); + EXPECT_FALSE(populated(tree1->findDirectory("a2"))); + EXPECT_EQ(tree1->find("a1"), a); + EXPECT_NE(tree1->find("a1"), tree1->find("a2")); + + assertTreeEquals(tree1, { + {"a1", true}, + {"a1/b", true}, + {"a1/b/c", true}, + {"a1/b/m.y", false}, + {"a2", true}, + {"a2/b", true}, + {"a2/b/c", true}, + {"a2/b/m.y", false}, + {"b", true}, + {"c", false}, + }); + + // Everything should be populated now: + EXPECT_TRUE(populated(tree1->findDirectory("a1"))); + EXPECT_TRUE(populated(tree1->findDirectory("a2"))); + + QString a1("a1/"), a2("a2/"); + for (auto p : {"b", "b/c", "b/m.y"}) { + EXPECT_NE(tree1->find(a1 + p), tree1->find(a2 + p)) + << "Entry '" << (a1 + p) << "' and '" << (a2 + p) << "' should be different."; + } + } +} + +TEST(IFileTreeTest, TreeMergeOperations) +{ + + { + auto fileTree = FileListTree::makeTree({{"a/", true}, + {"b", true}, + {"c.x", false}, + {"d.y", false}, + {"e/q/c.t", false}, + {"e/q/p", true}}); + + EXPECT_NE(fileTree, nullptr); + + // Retrieve the entry: + auto map = createMapping(fileTree); + auto e = fileTree->findDirectory("e"); + auto e_q = fileTree->findDirectory("e/q"); + + // Merge e in the root: + IFileTree::OverwritesType overwrites; + auto noverwrites = fileTree->merge(e, &overwrites); + + EXPECT_EQ(noverwrites, std::size_t{0}); + EXPECT_TRUE(overwrites.empty()); + EXPECT_EQ(e->size(), std::size_t{0}); + assertTreeEquals(fileTree, {{"a", true}, + {"b", true}, + {"c.x", false}, + {"d.y", false}, + {"e", true}, + {"q", true}, + {"q/c.t", false}, + {"q/p", true}}); + + auto p = fileTree->addFile("p"); + EXPECT_NE(p, nullptr); + + // Not: e/q is not q + overwrites.clear(); + noverwrites = fileTree->merge(e_q, &overwrites); + EXPECT_EQ(noverwrites, std::size_t{1}); + EXPECT_EQ(overwrites.size(), std::size_t{1}); + EXPECT_EQ(overwrites[p], map["e/q/p"]); + assertTreeEquals(fileTree, {{"a", true}, + {"b", true}, + {"c.x", false}, + {"d.y", false}, + {"e", true}, + {"q", true}, + {"c.t", false}, + {"p", true}}); + // Note: the "p" at the root should be the one under q initially. + EXPECT_EQ(fileTree->find("p"), map["e/q/p"]); + } + + // Merge failure: + { + auto tree1 = FileListTree::makeTree({{"a/", true}, + {"b", true}, + {"c.x", false}, + {"d.y", false}, + {"e/q/c.t", false}, + {"e/q/p", true}}); + + std::size_t noverwrites = tree1->findDirectory("e")->merge(tree1); + EXPECT_EQ(noverwrites, IFileTree::MERGE_FAILED); + + noverwrites = tree1->findDirectory("e/q")->merge(tree1); + EXPECT_EQ(noverwrites, IFileTree::MERGE_FAILED); + + noverwrites = tree1->merge(tree1); + EXPECT_EQ(noverwrites, IFileTree::MERGE_FAILED); + } + + // + { + auto tree1 = FileListTree::makeTree({{"a/b/c/m.y", false}, + {"a/b/c/n", true}, + {"a/b/x.t", false}, + {"a/b/y.t", false}, + {"b/", true}, + {"c", false}}); + auto map1 = createMapping(tree1); + + auto tree2 = FileListTree::makeTree({{"a/b/c/m.y", false}, + {"a/b/c/n", false}, // n is a file here + {"a/b/y.t", false}, + {"b/v", false}, + {"b/e", true}}); + auto map2 = createMapping(tree2); + + IFileTree::OverwritesType overwrites; + std::size_t noverwrites = tree1->merge(tree2, &overwrites); + + EXPECT_EQ(noverwrites, std::size_t{3}); + EXPECT_EQ(noverwrites, overwrites.size()); + EXPECT_EQ(overwrites[map1["a/b/c/m.y"]], map2["a/b/c/m.y"]); + EXPECT_EQ(overwrites[map1["a/b/c/n"]], map2["a/b/c/n"]); + EXPECT_EQ(overwrites[map1["a/b/y.t"]], map2["a/b/y.t"]); + + assertTreeEquals(tree1, {{"a", true}, + {"b", true}, + {"c", false}, + {"a/b", true}, + {"a/b/c", true}, + {"a/b/c/m.y", false}, + {"a/b/c/n", false}, + {"a/b/x.t", false}, + {"a/b/y.t", false}, + {"b/v", false}, + {"b/e", true}}); + + // Merged directories should be the one from the original tree: + EXPECT_EQ(tree1->find("a"), map1["a"]); + EXPECT_EQ(tree1->find("a/b"), map1["a/b"]); + EXPECT_EQ(tree1->find("a/b/c"), map1["a/b/c"]); + EXPECT_EQ(tree1->find("b"), map1["b"]); + + // Overriden: + EXPECT_EQ(tree1->find("a/b/c/m.y"), map2["a/b/c/m.y"]); + EXPECT_EQ(tree1->find("a/b/c/n"), map2["a/b/c/n"]); + EXPECT_EQ(tree1->find("a/b/y.t"), map2["a/b/y.t"]); + } +} + +TEST(IFileTreeTest, TreeWalkOperations) +{ + + auto fileTree = FileListTree::makeTree({{"a/", true}, + {"b", true}, + {"b/u", false}, + {"b/v", false}, + {"c.x", false}, + {"d.y", false}, + {"e/q/c.t", false}, + {"e/q/p", true}}); + + auto map = createMapping(fileTree); + + // Note: Testing specific order here, while in reality user should not rely + // on it (and it is not specified, on purpose). Only guarantee is that a folder + // is visited before its children. + { + // Populate the vector: + std::vector<std::pair<QString, std::shared_ptr<const FileTreeEntry>>> entries; + fileTree->walk( + [&entries](auto path, auto entry) { + entries.push_back({path, entry}); + return IFileTree::WalkReturn::CONTINUE; + }, + "/"); + + decltype(entries) expected{{"", map["a"]}, {"", map["b"]}, + {"b/", map["b/u"]}, {"b/", map["b/v"]}, + {"", map["e"]}, {"e/", map["e/q"]}, + {"e/q/", map["e/q/p"]}, {"e/q/", map["e/q/c.t"]}, + {"", map["c.x"]}, {"", map["d.y"]}}; + EXPECT_EQ(entries, expected); + + entries.clear(); + fileTree->walk( + [&entries](auto path, auto entry) { + if (entry->name() == "e") { + return IFileTree::WalkReturn::STOP; + } + entries.push_back({path, entry}); + return IFileTree::WalkReturn::CONTINUE; + }, + "/"); + + // Note: This assumes a given order, while in reality it is not specified. + expected = { + {"", map["a"]}, + {"", map["b"]}, + {"b/", map["b/u"]}, + {"b/", map["b/v"]}, + }; + EXPECT_EQ(entries, expected); + + entries.clear(); + fileTree->walk( + [&entries](auto path, auto entry) { + if (entry->name() == "e") { + return IFileTree::WalkReturn::SKIP; + } + entries.push_back({path, entry}); + return IFileTree::WalkReturn::CONTINUE; + }, + "/"); + + // Note: This assumes a given order, while in reality it is not specified. + expected = {{"", map["a"]}, {"", map["b"]}, {"b/", map["b/u"]}, + {"b/", map["b/v"]}, {"", map["c.x"]}, {"", map["d.y"]}}; + EXPECT_EQ(entries, expected); + } + + // same as above but with generator version + { + // Populate the vector: + auto entries = walk(fileTree) | std::ranges::to<std::vector>(); + decltype(entries) expected{map["a"], map["b"], map["b/u"], map["b/v"], + map["e"], map["e/q"], map["e/q/p"], map["e/q/c.t"], + map["c.x"], map["d.y"]}; + EXPECT_EQ(entries, expected); + + entries.clear(); + for (const auto entry : walk(fileTree)) { + if (entry->name() == "e") { + break; // Stop on e + } + entries.push_back(entry); + } + + // Note: This assumes a given order, while in reality it is not specified. + expected = { + map["a"], + map["b"], + map["b/u"], + map["b/v"], + }; + EXPECT_EQ(entries, expected); + + // note: third test with SKIP is not possible with generator version + } +} + +TEST(IFileTreeTest, TreeGlobOperations) +{ + using entrySet = std::unordered_set<std::shared_ptr<const FileTreeEntry>>; + + const auto REGEX = GlobPatternType::REGEX; + + { + auto fileTree = FileListTree::makeTree({{"a/", true}, + {"a/g.t", false}, + {"b", true}, + {"b/u", false}, + {"b/v", false}, + {"c.x", false}, + {"d.y", false}, + {"e/q/c.t", false}, + {"e/q/m.x", false}, + {"e/q/p", true}}); + + auto map = createMapping(fileTree); + + entrySet entries, expected; + + entries = glob(fileTree, "*") | std::ranges::to<std::unordered_set>(); + expected = {map["a"], map["b"], map["c.x"], map["d.y"], map["e"]}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, ".*", REGEX) | std::ranges::to<std::unordered_set>(); + expected = {map["a"], map["b"], map["c.x"], map["d.y"], map["e"]}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "**") | std::ranges::to<std::unordered_set>(); + expected = {fileTree, map["a"], map["b"], map["e"], map["e/q"], map["e/q/p"]}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "**", REGEX) | std::ranges::to<std::unordered_set>(); + expected = {fileTree, map["a"], map["b"], map["e"], map["e/q"], map["e/q/p"]}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "*.x") | std::ranges::to<std::unordered_set>(); + expected = {map["c.x"]}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, ".*[.]x", REGEX) | std::ranges::to<std::unordered_set>(); + expected = {map["c.x"]}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "**/*.x") | std::ranges::to<std::unordered_set>(); + expected = {map["c.x"], map["e/q/m.x"]}; + EXPECT_EQ(entries, expected); + + entries = + glob(fileTree, "**/.*[.]x", REGEX) | std::ranges::to<std::unordered_set>(); + expected = {map["c.x"], map["e/q/m.x"]}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "*.t") | std::ranges::to<std::unordered_set>(); + expected = {}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "**/*.t") | std::ranges::to<std::unordered_set>(); + expected = {map["a/g.t"], map["e/q/c.t"]}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "a/*") | std::ranges::to<std::unordered_set>(); + expected = {map["a/g.t"]}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "a/.*", REGEX) | std::ranges::to<std::unordered_set>(); + expected = {map["a/g.t"]}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "**/*.[xt]") | std::ranges::to<std::unordered_set>(); + expected = {map["c.x"], map["e/q/m.x"], map["a/g.t"], map["e/q/c.t"]}; + EXPECT_EQ(entries, expected); + + entries = + glob(fileTree, "**/.*[.][xt]", REGEX) | std::ranges::to<std::unordered_set>(); + expected = {map["c.x"], map["e/q/m.x"], map["a/g.t"], map["e/q/c.t"]}; + EXPECT_EQ(entries, expected); + } + + { + auto fileTree = FileListTree::makeTree({{"aq.js", false}, + {"bb", true}, + {"cm.tx", false}, + {"dp.js", false}, + {"ev", false}, + {"go.ya", false}, + {"gw.md", false}, + {"hh", false}, + {"hl", true}, + {"in", true}, + {"mz", true}, + {"sc", true}, + {"bb/ce.cp", false}, + {"bb/cm.tx", false}, + {"bb/gw", true}, + {"bb/iw.cp", false}, + {"bb/js", true}, + {"bb/px.cp", false}, + {"hl/ds.in", false}, + {"in/nu", true}, + {"mz/tu.js", false}, + {"sc/cm.tx", false}, + {"sc/cw.ts", false}, + {"sc/cz.rc", false}, + {"sc/dr.cp", false}, + {"sc/hh.cp", false}, + {"sc/kn.ui", false}, + {"sc/lr.cp", false}, + {"sc/nd.o", false}, + {"sc/nv.o", false}, + {"sc/rv.ui", false}, + {"sc/tv.h", false}, + {"bb/gw/cp.qm", false}, + {"bb/gw/hq.qm", false}, + {"bb/gw/pu.ts", false}, + {"bb/gw/tu.ts", false}, + {"bb/js/cm.tx", false}, + {"bb/js/co.cp", false}, + {"in/nu/el.h", false}, + {"in/nu/fj.h", false}, + {"in/nu/lw", true}, + {"in/nu/xx", true}, + {"in/nu/lw/cp.h", false}, + {"in/nu/lw/go.h", false}, + {"in/nu/xx/ap.h", false}, + {"in/nu/xx/qz.h", false}}); + + auto map = createMapping(fileTree); + + entrySet entries, expected; + + entries = glob(fileTree, "*.h") | std::ranges::to<std::unordered_set>(); + expected = {}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "*") | std::ranges::to<std::unordered_set>(); + expected = {map.at("aq.js"), map.at("bb"), map.at("cm.tx"), map.at("dp.js"), + map.at("ev"), map.at("go.ya"), map.at("gw.md"), map.at("hh"), + map.at("hl"), map.at("in"), map.at("mz"), map.at("sc")}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "*/*") | std::ranges::to<std::unordered_set>(); + expected = { + map.at("bb/ce.cp"), map.at("bb/cm.tx"), map.at("bb/gw"), map.at("bb/iw.cp"), + map.at("bb/js"), map.at("bb/px.cp"), map.at("hl/ds.in"), map.at("in/nu"), + map.at("mz/tu.js"), map.at("sc/cm.tx"), map.at("sc/cw.ts"), map.at("sc/cz.rc"), + map.at("sc/dr.cp"), map.at("sc/hh.cp"), map.at("sc/kn.ui"), map.at("sc/lr.cp"), + map.at("sc/nd.o"), map.at("sc/nv.o"), map.at("sc/rv.ui"), map.at("sc/tv.h")}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "*/*/*") | std::ranges::to<std::unordered_set>(); + expected = {map.at("bb/gw/cp.qm"), map.at("bb/gw/hq.qm"), map.at("bb/gw/pu.ts"), + map.at("bb/gw/tu.ts"), map.at("bb/js/cm.tx"), map.at("bb/js/co.cp"), + map.at("in/nu/el.h"), map.at("in/nu/fj.h"), map.at("in/nu/lw"), + map.at("in/nu/xx")}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "**") | std::ranges::to<std::unordered_set>(); + expected = {fileTree, map.at("bb"), map.at("bb/gw"), map.at("bb/js"), + map.at("hl"), map.at("in"), map.at("in/nu"), map.at("in/nu/lw"), + map.at("in/nu/xx"), map.at("mz"), map.at("sc")}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "**/*") | std::ranges::to<std::unordered_set>(); + expected = {map.at("aq.js"), + map.at("bb"), + map.at("cm.tx"), + map.at("dp.js"), + map.at("ev"), + map.at("go.ya"), + map.at("gw.md"), + map.at("hh"), + map.at("hl"), + map.at("in"), + map.at("mz"), + map.at("sc"), + map.at("bb/ce.cp"), + map.at("bb/cm.tx"), + map.at("bb/gw"), + map.at("bb/iw.cp"), + map.at("bb/js"), + map.at("bb/px.cp"), + map.at("bb/gw/cp.qm"), + map.at("bb/gw/hq.qm"), + map.at("bb/gw/pu.ts"), + map.at("bb/gw/tu.ts"), + map.at("bb/js/cm.tx"), + map.at("bb/js/co.cp"), + map.at("hl/ds.in"), + map.at("in/nu"), + map.at("in/nu/el.h"), + map.at("in/nu/fj.h"), + map.at("in/nu/lw"), + map.at("in/nu/xx"), + map.at("in/nu/lw/cp.h"), + map.at("in/nu/lw/go.h"), + map.at("in/nu/xx/ap.h"), + map.at("in/nu/xx/qz.h"), + map.at("mz/tu.js"), + map.at("sc/cm.tx"), + map.at("sc/cw.ts"), + map.at("sc/cz.rc"), + map.at("sc/dr.cp"), + map.at("sc/hh.cp"), + map.at("sc/kn.ui"), + map.at("sc/lr.cp"), + map.at("sc/nd.o"), + map.at("sc/nv.o"), + map.at("sc/rv.ui"), + map.at("sc/tv.h")}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "**/cm.tx") | std::ranges::to<std::unordered_set>(); + expected = {map.at("cm.tx"), map.at("bb/cm.tx"), map.at("bb/js/cm.tx"), + map.at("sc/cm.tx")}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "**/sc/**/cm.tx") | std::ranges::to<std::unordered_set>(); + expected = {map.at("sc/cm.tx")}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "**/sc") | std::ranges::to<std::unordered_set>(); + expected = {map.at("sc")}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "in/**") | std::ranges::to<std::unordered_set>(); + expected = {map.at("in"), map.at("in/nu"), map.at("in/nu/lw"), map.at("in/nu/xx")}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "in/**/**") | std::ranges::to<std::unordered_set>(); + expected = {map.at("in"), map.at("in/nu"), map.at("in/nu/lw"), map.at("in/nu/xx")}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "in/*/*") | std::ranges::to<std::unordered_set>(); + expected = {map.at("in/nu/el.h"), map.at("in/nu/fj.h"), map.at("in/nu/lw"), + map.at("in/nu/xx")}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "in/*/*.h") | std::ranges::to<std::unordered_set>(); + expected = {map.at("in/nu/el.h"), map.at("in/nu/fj.h")}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "sc/**/*.cp") | std::ranges::to<std::unordered_set>(); + expected = {map.at("sc/dr.cp"), map.at("sc/hh.cp"), map.at("sc/lr.cp")}; + EXPECT_EQ(entries, expected); + + entries = glob(fileTree, "sc/**/n*.o") | std::ranges::to<std::unordered_set>(); + expected = {map.at("sc/nd.o"), map.at("sc/nv.o")}; + EXPECT_EQ(entries, expected); + } +} diff --git a/libs/uibase/tests/test_main.cpp b/libs/uibase/tests/test_main.cpp new file mode 100644 index 0000000..0399b43 --- /dev/null +++ b/libs/uibase/tests/test_main.cpp @@ -0,0 +1,15 @@ +#include <gtest/gtest.h> + +#include <QCoreApplication> +#include <QTranslator> + +int main(int argc, char** argv) +{ + QCoreApplication app(argc, argv); + QTranslator translator; + if (translator.load("tests_fr", "tests/translations")) { + app.installTranslator(&translator); + } + testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/libs/uibase/tests/test_strings.cpp b/libs/uibase/tests/test_strings.cpp new file mode 100644 index 0000000..0185f83 --- /dev/null +++ b/libs/uibase/tests/test_strings.cpp @@ -0,0 +1,47 @@ +#pragma warning(push) +#pragma warning(disable : 4668) +#include <gtest/gtest.h> +#pragma warning(pop) + +#include <QCoreApplication> + +#include <uibase/strings.h> + +#include <format> + +using namespace MOBase; + +TEST(StringsTest, IEquals) +{ + ASSERT_TRUE(iequals("hello world", "HelLO WOrlD")); +} + +TEST(StringsTest, IReplaceAll) +{ + auto ireplace_all = [](std::string_view input, std::string_view search, + std::string_view replace) { + std::string s_input{input}; + MOBase::ireplace_all(s_input, search, replace); + return s_input; + }; + + ASSERT_EQ("", ireplace_all("", "world", "MO2")); + ASSERT_EQ("Hello World!", ireplace_all("Hello World!", "Test", "MO2")); + ASSERT_EQ("replace a stuff with a stuff a", + ireplace_all("replace some stuff with some stuff some", "some", "a")); + ASSERT_EQ("replace a stuff with a stuff som", + ireplace_all("replace some stuff with some stuff som", "some", "a")); + ASSERT_EQ("1YYY3YYY2", ireplace_all("1aBc3AbC2", "abC", "YYY")); + + ASSERT_EQ( + "data path: C:/Users/USERNAME/AppData/Local/ModOrganizer/Starfield", + ireplace_all("data path: C:/Users/lords/AppData/Local/ModOrganizer/Starfield", + "/lords", "/USERNAME")); +} + +// this is more a tests of the tests +TEST(StringsTest, Translation) +{ + ASSERT_EQ("Traduction en Français", + QCoreApplication::translate("uibase-tests", "Translate to French")); +} diff --git a/libs/uibase/tests/test_versioning.cpp b/libs/uibase/tests/test_versioning.cpp new file mode 100644 index 0000000..08a346f --- /dev/null +++ b/libs/uibase/tests/test_versioning.cpp @@ -0,0 +1,90 @@ +#pragma warning(push) +#pragma warning(disable : 4668) +#include <gtest/gtest.h> +#pragma warning(pop) + +#include <QString> +#include <vector> + +#include <uibase/versioning.h> + +#include <format> + +using namespace MOBase; + +using enum Version::ReleaseType; +using ParseMode = Version::ParseMode; + +TEST(VersioningTest, VersionParse) +{ + // TODO: add exceptions test + + // semver + ASSERT_EQ(Version(1, 0, 0), Version::parse("1.0.0")); + ASSERT_EQ(Version(1, 0, 0, Development, 1), Version::parse("1.0.0-dev.1")); + ASSERT_EQ(Version(1, 0, 0, Development, 2), Version::parse("1.0.0-dev.2")); + ASSERT_EQ(Version(1, 0, 0, Alpha), Version::parse("1.0.0-a")); + ASSERT_EQ(Version(1, 0, 0, Alpha), Version::parse("1.0.0-alpha")); + ASSERT_EQ(Version(1, 0, 0, 0, {Alpha, 1, Beta}), Version::parse("1.0.0-alpha.1.b")); + ASSERT_EQ(Version(1, 0, 0, Beta, 2), Version::parse("1.0.0-beta.2")); + ASSERT_EQ(Version(2, 5, 2, ReleaseCandidate, 1), Version::parse("2.5.2-rc.1")); + + // mo2 + ASSERT_EQ(Version(1, 0, 0), Version::parse("1.0.0", ParseMode::MO2)); + ASSERT_EQ(Version(1, 0, 0, Development, 1), + Version::parse("1.0.0dev1", ParseMode::MO2)); + ASSERT_EQ(Version(1, 0, 0, Development, 2), + Version::parse("1.0.0dev2", ParseMode::MO2)); + ASSERT_EQ(Version(1, 0, 0, Alpha, 1), Version::parse("1.0.0a1", ParseMode::MO2)); + ASSERT_EQ(Version(1, 0, 0, Alpha, 1), Version::parse("1.0.0alpha1", ParseMode::MO2)); + ASSERT_EQ(Version(1, 0, 0, Beta, 2), Version::parse("1.0.0beta2", ParseMode::MO2)); + ASSERT_EQ(Version(1, 0, 0, Beta, 2), Version::parse("1.0.0beta2", ParseMode::MO2)); + ASSERT_EQ(Version(2, 4, 1, 0, {ReleaseCandidate, 1, 1}), + Version::parse("2.4.1rc1.1", ParseMode::MO2)); + ASSERT_EQ(Version(2, 2, 2, 1, Beta, 2), + Version::parse("2.2.2.1beta2", ParseMode::MO2)); + ASSERT_EQ(Version(2, 5, 2, ReleaseCandidate, 1), + Version::parse("v2.5.2rc1", ParseMode::MO2)); + ASSERT_EQ(Version(2, 5, 2, ReleaseCandidate, 2), + Version::parse("2.5.2rc2", ParseMode::MO2)); +} + +TEST(VersioningTest, VersionString) +{ + ASSERT_EQ("1.0.0", Version(1, 0, 0).string()); + ASSERT_EQ("1.0.0-dev.1", Version(1, 0, 0, Development, 1).string()); + ASSERT_EQ("1.0.0-dev.2", Version(1, 0, 0, Development, 2).string()); + ASSERT_EQ("1.0.0-alpha", Version(1, 0, 0, Alpha).string()); + ASSERT_EQ("1.0.0-alpha.1.beta", Version(1, 0, 0, 0, {Alpha, 1, Beta}).string()); + ASSERT_EQ("1.0.0-beta.2", Version(1, 0, 0, Beta, 2).string()); + ASSERT_EQ("2.5.2-rc.1", Version(2, 5, 2, ReleaseCandidate, 1).string()); + ASSERT_EQ("2.5.2rc1", + Version(2, 5, 2, ReleaseCandidate, 1).string(Version::FormatCondensed)); +} + +TEST(VersioningTest, VersionCompare) +{ + // shortcut + using v = Version; + + // test from https://semver.org/ + ASSERT_TRUE(v(1, 0, 0) < v(2, 0, 0)); + ASSERT_TRUE(v(2, 0, 0) < v(2, 1, 0)); + ASSERT_TRUE(v(2, 1, 0) < v(2, 1, 1)); + + ASSERT_TRUE(v(1, 0, 0, Alpha) < v(1, 0, 0, Alpha, 1)); + ASSERT_TRUE(v(1, 0, 0, Alpha, 1) < v(1, 0, 0, 0, {Alpha, Beta})); + ASSERT_TRUE(v(1, 0, 0, 0, {Alpha, Beta}) < v(1, 0, 0, 1)); + ASSERT_TRUE(v(1, 0, 0, Beta) < v(1, 0, 0, Beta, 2)); + ASSERT_TRUE(v(1, 0, 0, Beta, 2) < v(1, 0, 0, Beta, 11)); + ASSERT_TRUE(v(1, 0, 0, Beta, 11) < v(1, 0, 0, ReleaseCandidate, 1)); + ASSERT_TRUE(v(1, 0, 0, ReleaseCandidate, 0) < v(1, 0, 0)); + + ASSERT_TRUE(v(2, 4, 1, 0, {ReleaseCandidate, 1, 0}) == + v(2, 4, 1, ReleaseCandidate, 1)); + ASSERT_TRUE(v(2, 4, 1, 0, {ReleaseCandidate, 1, 0}) < + v(2, 4, 1, 0, {ReleaseCandidate, 1, 1})); + ASSERT_TRUE(v(2, 4, 1, ReleaseCandidate, 1) < + v(2, 4, 1, 0, {ReleaseCandidate, 1, 1})); + ASSERT_TRUE(v(1, 0, 0) < v(2, 0, 0, Alpha)); +} diff --git a/libs/uibase/tests/translations/tests_en.qm b/libs/uibase/tests/translations/tests_en.qm Binary files differnew file mode 100644 index 0000000..d5ca501 --- /dev/null +++ b/libs/uibase/tests/translations/tests_en.qm diff --git a/libs/uibase/tests/translations/tests_en.ts b/libs/uibase/tests/translations/tests_en.ts new file mode 100644 index 0000000..1f54d86 --- /dev/null +++ b/libs/uibase/tests/translations/tests_en.ts @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<!DOCTYPE TS> +<TS version="2.1" language="en_US"> +<context> + <name>uibase-tests</name> + <message> + <source>Translate to French</source> + <translation>Translate to French</translation> + </message> +</context> +</TS> diff --git a/libs/uibase/tests/translations/tests_fr.qm b/libs/uibase/tests/translations/tests_fr.qm Binary files differnew file mode 100644 index 0000000..80958d6 --- /dev/null +++ b/libs/uibase/tests/translations/tests_fr.qm diff --git a/libs/uibase/tests/translations/tests_fr.ts b/libs/uibase/tests/translations/tests_fr.ts new file mode 100644 index 0000000..eed02a0 --- /dev/null +++ b/libs/uibase/tests/translations/tests_fr.ts @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<!DOCTYPE TS> +<TS version="2.1" language="fr_FR"> +<context> + <name>uibase-tests</name> + <message> + <source>Translate to French</source> + <translation>Traduction en Français</translation> + </message> +</context> +</TS> |
