Files

1173 lines
44 KiB
C++

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