Overhaul Group APIs

- Group equality now compares all fields
- All other comparison operators are now overridden for Group
- Group "after" metadata is now stored as a vector of strings instead of
  an unordered set, to preserve source metadata ordering and simplify
  implementing the comparison operator  overloads
- std::hash is no longer specialised for Group
- DatabaseInterface::GetGroups() and DatabaseInterface::GetUserGroups()
  now return vectors of Groups instead of unordered sets.
- DatabaseInterface::SetUserGroups() now takes a vector of Groups
  instead of an unordered set.
This commit is contained in:
Oliver Hamlet
2020-07-11 19:13:13 +01:00
parent 83ba085a6d
commit e85ec7b1ce
17 changed files with 590 additions and 205 deletions
+6 -4
View File
@@ -190,16 +190,18 @@ public:
* If true, any group metadata present in the userlist is included in
* the returned metadata, otherwise the metadata returned only includes
* metadata from the masterlist.
* @returns An unordered set of Group objects.
* @returns An vector of Group objects. Each Group's name is unique, if a
* group has masterlist and user metadata the two are merged into a
* single group object.
*/
virtual std::unordered_set<Group> GetGroups(
virtual std::vector<Group> GetGroups(
bool includeUserMetadata = true) const = 0;
/**
* @brief Gets the groups that are defined or extended in the loaded userlist.
* @returns An unordered set of Group objects.
*/
virtual std::unordered_set<Group> GetUserGroups() const = 0;
virtual std::vector<Group> GetUserGroups() const = 0;
/**
* @brief Sets the group definitions to store in the userlist, overwriting any
@@ -207,7 +209,7 @@ public:
* @param groups
* The unordered set of Group objects to set.
*/
virtual void SetUserGroups(const std::unordered_set<Group>& groups) = 0;
virtual void SetUserGroups(const std::vector<Group>& groups) = 0;
/**
* @brief Get the "shortest" path between the two given groups according to
+39 -17
View File
@@ -24,8 +24,8 @@
#ifndef LOOT_METADATA_GROUP
#define LOOT_METADATA_GROUP
#include <unordered_set>
#include <string>
#include <vector>
#include "loot/api_decorator.h"
@@ -54,7 +54,7 @@ public:
* @return A Group object.
*/
LOOT_API explicit Group(const std::string& name,
const std::unordered_set<std::string>& afterGroups = {},
const std::vector<std::string>& afterGroups = {},
const std::string& description = "");
/**
@@ -63,6 +63,14 @@ public:
*/
LOOT_API bool operator==(const Group& rhs) const;
/**
* A less-than operator implemented with no semantics so that Group objects
* can be stored in sets.
* @returns True if this Group is less than the given Group, false
* otherwise.
*/
LOOT_API bool operator<(const Group& rhs) const;
/**
* Get the name of the group.
* @return The group's name.
@@ -79,29 +87,43 @@ public:
* Get the set of groups this group loads after.
* @return A set of group names.
*/
LOOT_API std::unordered_set<std::string> GetAfterGroups() const;
LOOT_API std::vector<std::string> GetAfterGroups() const;
private:
std::string name_;
std::string description_;
std::unordered_set<std::string> afterGroups_;
std::vector<std::string> afterGroups_;
};
}
namespace std {
/**
* A specialisation of std::hash for loot::Group.
* Check if two Group objects are not equal.
* @returns True if the Group objects are not equal, false otherwise.
*/
template<>
struct hash<loot::Group> {
/**
* Calculate a hash value for a loot::Group object.
* @return The hash generated from the group's name.
*/
size_t operator()(const loot::Group& group) const {
return hash<string>()(group.GetName());
}
};
LOOT_API bool operator!=(const Group& lhs, const Group& rhs);
/**
* Check if the first Group object is greater than the second Group
* object.
* @returns True if the second Group object is less than the first Group
* object, false otherwise.
*/
LOOT_API bool operator>(const Group& lhs, const Group& rhs);
/**
* Check if the first Group object is less than or equal to the second
* Group object.
* @returns True if the first Group object is not greater than the second
* Group object, false otherwise.
*/
LOOT_API bool operator<=(const Group& lhs, const Group& rhs);
/**
* Check if the first Group object is greater than or equal to the second
* Group object.
* @returns True if the first Group object is not less than the second
* Group object, false otherwise.
*/
LOOT_API bool operator>=(const Group& lhs, const Group& rhs);
}
#endif
+44 -39
View File
@@ -30,14 +30,15 @@
#include "api/game/game.h"
#include "api/metadata/condition_evaluator.h"
#include "api/metadata/yaml/plugin_metadata.h"
#include "api/sorting/plugin_sort.h"
#include "api/sorting/group_sort.h"
#include "loot/metadata/group.h"
#include "api/sorting/plugin_sort.h"
#include "loot/exception/file_access_error.h"
#include "loot/metadata/group.h"
namespace loot {
ApiDatabase::ApiDatabase(std::shared_ptr<ConditionEvaluator> conditionEvaluator) :
conditionEvaluator_(conditionEvaluator) {}
ApiDatabase::ApiDatabase(
std::shared_ptr<ConditionEvaluator> conditionEvaluator) :
conditionEvaluator_(conditionEvaluator) {}
///////////////////////////////////
// Database Loading Functions
@@ -90,7 +91,8 @@ bool ApiDatabase::UpdateMasterlist(const std::filesystem::path& masterlistPath,
const std::string& remoteURL,
const std::string& remoteBranch) {
if (!std::filesystem::is_directory(masterlistPath.parent_path()))
throw std::invalid_argument("Given masterlist path \"" + masterlistPath.u8string() +
throw std::invalid_argument("Given masterlist path \"" +
masterlistPath.u8string() +
"\" does not have a valid parent directory.");
Masterlist masterlist;
@@ -110,7 +112,7 @@ MasterlistInfo ApiDatabase::GetMasterlistRevision(
bool ApiDatabase::IsLatestMasterlist(
const std::filesystem::path& masterlist_path,
const std::string& branch) const {
const std::string& branch) const {
return Masterlist::IsLatest(masterlist_path, branch);
}
@@ -155,59 +157,62 @@ std::vector<Message> ApiDatabase::GetGeneralMessages(
return masterlistMessages;
}
std::unordered_set<Group> ApiDatabase::GetGroups(bool includeUserMetadata) const {
if (!includeUserMetadata) {
auto groups = masterlist_.Groups();
std::vector<Group> ApiDatabase::GetGroups(
bool includeUserMetadata) const {
auto groups = masterlist_.Groups();
//Insert the default group in case the masterlist hasn't been loaded.
groups.insert(Group());
if (includeUserMetadata) {
std::vector<Group> newGroups;
for (const auto& userlistGroup : userlist_.Groups()) {
auto groupIt = std::find_if(groups.begin(), groups.end(), [&](const Group& existingGroup) {
return existingGroup.GetName() == userlistGroup.GetName();
});
return groups;
}
if (groupIt == groups.end()) {
newGroups.push_back(userlistGroup);
} else {
// Replace the masterlist group description with the userlist group
// description if the latter is not empty.
auto description = userlistGroup.GetDescription().empty()
? groupIt->GetDescription()
: userlistGroup.GetDescription();
std::unordered_set<Group> mergedGroups;
auto afterGroups = groupIt->GetAfterGroups();
auto userAfterGroups = userlistGroup.GetAfterGroups();
afterGroups.insert(afterGroups.end(), userAfterGroups.begin(), userAfterGroups.end());
auto userlistGroups = userlist_.Groups();
for (const auto& group : masterlist_.Groups()) {
auto userlistGroup = userlistGroups.find(group);
if (userlistGroup != userlistGroups.end()) {
auto afterGroups = group.GetAfterGroups();
auto userlistAfterGroups = userlistGroup->GetAfterGroups();
afterGroups.insert(userlistAfterGroups.begin(), userlistAfterGroups.end());
mergedGroups.insert(Group(group.GetName(), afterGroups));
} else {
mergedGroups.insert(group);
*groupIt = Group(userlistGroup.GetName(), afterGroups, description);
}
}
groups.insert(groups.end(), newGroups.cbegin(), newGroups.cend());
}
mergedGroups.insert(userlistGroups.begin(), userlistGroups.end());
// Insert the default group if it's not already present.
mergedGroups.insert(Group());
return mergedGroups;
return groups;
}
std::unordered_set<Group> ApiDatabase::GetUserGroups() const {
std::vector<Group> ApiDatabase::GetUserGroups() const {
return userlist_.Groups();
}
void ApiDatabase::SetUserGroups(const std::unordered_set<Group>& groups) {
void ApiDatabase::SetUserGroups(const std::vector<Group>& groups) {
userlist_.SetGroups(groups);
}
std::vector<Vertex> ApiDatabase::GetGroupsPath(const std::string& fromGroupName,
const std::string& toGroupName) const {
std::vector<Vertex> ApiDatabase::GetGroupsPath(
const std::string& fromGroupName,
const std::string& toGroupName) const {
auto masterlistGroups = GetGroups(false);
auto userGroups = GetUserGroups();
return loot::GetGroupsPath(masterlistGroups, userGroups, fromGroupName, toGroupName);
return loot::GetGroupsPath(
masterlistGroups, userGroups, fromGroupName, toGroupName);
}
std::optional<PluginMetadata> ApiDatabase::GetPluginMetadata(const std::string& plugin,
bool includeUserMetadata,
bool evaluateConditions) const {
std::optional<PluginMetadata> ApiDatabase::GetPluginMetadata(
const std::string& plugin,
bool includeUserMetadata,
bool evaluateConditions) const {
auto metadata = masterlist_.FindPlugin(plugin);
if (includeUserMetadata) {
+3 -3
View File
@@ -67,9 +67,9 @@ struct ApiDatabase : public DatabaseInterface {
std::vector<Message> GetGeneralMessages(
bool evaluateConditions = false) const;
std::unordered_set<Group> GetGroups(bool includeUserMetadata = true) const;
std::unordered_set<Group> GetUserGroups() const;
void SetUserGroups(const std::unordered_set<Group>& groups);
std::vector<Group> GetGroups(bool includeUserMetadata = true) const;
std::vector<Group> GetUserGroups() const;
void SetUserGroups(const std::vector<Group>& groups);
std::vector<Vertex> GetGroupsPath(const std::string& fromGroupName,
const std::string& toGroupName) const;
+34 -3
View File
@@ -30,21 +30,52 @@ namespace loot {
Group::Group() : name_("default") {}
Group::Group(const std::string& name,
const std::unordered_set<std::string>& afterGroups,
const std::vector<std::string>& afterGroups,
const std::string& description) :
name_(name),
afterGroups_(afterGroups),
description_(description) {}
bool Group::operator==(const Group& rhs) const {
return name_ == rhs.name_;
return name_ == rhs.name_ && description_ == rhs.description_ &&
afterGroups_ == rhs.afterGroups_;
}
bool Group::operator<(const Group& rhs) const {
if (name_ < rhs.name_) {
return true;
}
if (rhs.name_ < name_) {
return false;
}
if (description_ < rhs.description_) {
return true;
}
if (rhs.description_ < description_) {
return false;
}
return afterGroups_ < rhs.afterGroups_;
}
std::string Group::GetName() const { return name_; }
std::string Group::GetDescription() const { return description_; }
std::unordered_set<std::string> Group::GetAfterGroups() const {
std::vector<std::string> Group::GetAfterGroups() const {
return afterGroups_;
}
bool operator!=(const Group& lhs, const Group& rhs) {
return !(lhs == rhs);
}
bool operator>(const Group& lhs, const Group& rhs) { return rhs < lhs; }
bool operator<=(const Group& lhs, const Group& rhs) { return !(lhs > rhs); }
bool operator>=(const Group& lhs, const Group& rhs) { return !(lhs < rhs); }
}
+2 -2
View File
@@ -64,14 +64,14 @@ struct convert<loot::Group> {
std::string name = node["name"].as<std::string>();
std::string description;
std::unordered_set<std::string> afterGroups;
std::vector<std::string> afterGroups;
if (node["description"]) {
description = node["description"].as<std::string>();
}
if (node["after"]) {
afterGroups = node["after"].as<std::unordered_set<std::string>>();
afterGroups = node["after"].as<std::vector<std::string>>();
}
rhs = loot::Group(name, afterGroups, description);
+54 -16
View File
@@ -52,8 +52,8 @@ void MetadataList::Load(const std::filesystem::path& filepath) {
in.close();
if (!metadataList.IsMap())
throw FileAccessError("The root of the metadata file " + filepath.u8string() +
" is not a YAML map.");
throw FileAccessError("The root of the metadata file " +
filepath.u8string() + " is not a YAML map.");
if (metadataList["plugins"]) {
for (const auto& node : metadataList["plugins"]) {
@@ -61,7 +61,7 @@ void MetadataList::Load(const std::filesystem::path& filepath) {
if (plugin.IsRegexPlugin())
regexPlugins_.push_back(plugin);
else if (!plugins_.insert(plugin).second)
throw FileAccessError("More than one entry exists for \"" +
throw FileAccessError("More than one entry exists for plugin \"" +
plugin.GetName() + "\"");
}
}
@@ -71,10 +71,23 @@ void MetadataList::Load(const std::filesystem::path& filepath) {
if (metadataList["bash_tags"])
bashTags_ = metadataList["bash_tags"].as<std::set<std::string>>();
if (metadataList["groups"])
groups_ = metadataList["groups"].as<std::unordered_set<Group>>();
std::unordered_set<std::string> groupNames;
if (metadataList["groups"]) {
for (const auto& node : metadataList["groups"]) {
auto group = node.as<Group>();
if (groupNames.count(group.GetName()) != 0) {
throw FileAccessError("More than one entry exists for group \"" +
group.GetName() + "\"");
}
groups_.push_back(group);
groupNames.insert(group.GetName());
}
}
groups_.insert(Group());
auto defaultGroup = Group();
if (groupNames.count(defaultGroup.GetName()) == 0) {
groups_.insert(groups_.cbegin(), Group());
}
if (logger) {
logger->debug("File loaded successfully.");
@@ -93,16 +106,19 @@ void MetadataList::Save(const std::filesystem::path& filepath) const {
if (!bashTags_.empty())
emitter << YAML::Key << "bash_tags" << YAML::Value << bashTags_;
if (!groups_.empty())
if (!groups_.empty()) {
emitter << YAML::Key << "groups" << YAML::Value << groups_;
}
if (!messages_.empty())
emitter << YAML::Key << "globals" << YAML::Value << messages_;
auto plugins = Plugins();
std::sort(plugins.begin(), plugins.end(), [](const PluginMetadata& p1, const PluginMetadata& p2) {
return CompareFilenames(p1.GetName(), p2.GetName()) < 0;
});
std::sort(plugins.begin(),
plugins.end(),
[](const PluginMetadata& p1, const PluginMetadata& p2) {
return CompareFilenames(p1.GetName(), p2.GetName()) < 0;
});
if (!plugins.empty())
emitter << YAML::Key << "plugins" << YAML::Value << plugins;
@@ -118,10 +134,15 @@ void MetadataList::Save(const std::filesystem::path& filepath) const {
}
void MetadataList::Clear() {
groups_.clear();
bashTags_.clear();
plugins_.clear();
regexPlugins_.clear();
messages_.clear();
unevaluatedPlugins_.clear();
unevaluatedRegexPlugins_.clear();
unevaluatedMessages_.clear();
}
std::vector<PluginMetadata> MetadataList::Plugins() const {
@@ -137,11 +158,29 @@ std::vector<Message> MetadataList::Messages() const { return messages_; }
std::set<std::string> MetadataList::BashTags() const { return bashTags_; }
std::unordered_set<Group> MetadataList::Groups() const { return groups_; }
std::vector<Group> MetadataList::Groups() const {
if (groups_.empty()) {
return {Group()};
}
void MetadataList::SetGroups(const std::unordered_set<Group>& groups) {
groups_ = groups;
groups_.insert(Group());
return groups_;
}
void MetadataList::SetGroups(const std::vector<Group>& groups) {
// In case the default group is missing.
auto defaultGroupName = Group().GetName();
auto defaultGroupsExists =
std::any_of(groups.cbegin(), groups.cend(), [&](const Group& group) {
return group.GetName() == defaultGroupName;
});
if (!defaultGroupsExists) {
groups_.clear();
groups_.push_back(Group());
groups_.insert(groups_.end(), groups.begin(), groups.end());
} else {
groups_ = groups;
}
}
// Merges multiple matching regex entries if any are found.
@@ -195,8 +234,7 @@ void MetadataList::AppendMessage(const Message& message) {
messages_.push_back(message);
}
void MetadataList::EvalAllConditions(
ConditionEvaluator& conditionEvaluator) {
void MetadataList::EvalAllConditions(ConditionEvaluator& conditionEvaluator) {
if (unevaluatedPlugins_.empty())
unevaluatedPlugins_.swap(plugins_);
else
+3 -3
View File
@@ -63,9 +63,9 @@ public:
std::vector<PluginMetadata> Plugins() const;
std::vector<Message> Messages() const;
std::set<std::string> BashTags() const;
std::unordered_set<Group> Groups() const;
std::vector<Group> Groups() const;
void SetGroups(const std::unordered_set<Group>& groups);
void SetGroups(const std::vector<Group>& groups);
// Merges multiple matching regex entries if any are found.
std::optional<PluginMetadata> FindPlugin(const std::string& pluginName) const;
@@ -81,7 +81,7 @@ public:
void EvalAllConditions(ConditionEvaluator& conditionEvaluator);
protected:
std::unordered_set<Group> groups_;
std::vector<Group> groups_;
std::set<std::string> bashTags_;
std::unordered_set<PluginMetadata> plugins_;
std::vector<PluginMetadata> regexPlugins_;
+19 -10
View File
@@ -102,7 +102,7 @@ private:
std::vector<Vertex> trail;
};
std::string join(const std::unordered_set<std::string>& set) {
std::string joinVector(const std::vector<std::string>& set) {
std::string output;
for (const auto& element : set) {
output += element + ", ";
@@ -111,8 +111,17 @@ std::string join(const std::unordered_set<std::string>& set) {
return output.substr(0, output.length() - 2);
}
GroupGraph BuildGraph(const std::unordered_set<Group>& masterlistGroups,
const std::unordered_set<Group>& userGroups) {
std::string joinUnorderedSet(const std::unordered_set<std::string>& set) {
std::string output;
for (const auto& element : set) {
output += element + ", ";
}
return output.substr(0, output.length() - 2);
}
GroupGraph BuildGraph(const std::vector<Group>& masterlistGroups,
const std::vector<Group>& userGroups) {
GroupGraph graph;
std::unordered_map<std::string, vertex_t> groupVertices;
@@ -127,7 +136,7 @@ GroupGraph BuildGraph(const std::unordered_set<Group>& masterlistGroups,
logger->trace(
"Masterlist group \"{}\" directly loads after groups \"{}\"",
group.GetName(),
join(group.GetAfterGroups()));
joinVector(group.GetAfterGroups()));
}
auto vertex = groupVertices.at(group.GetName());
@@ -153,7 +162,7 @@ GroupGraph BuildGraph(const std::unordered_set<Group>& masterlistGroups,
if (logger) {
logger->trace("Userlist group \"{}\" directly loads after groups \"{}\"",
group.GetName(),
join(group.GetAfterGroups()));
joinVector(group.GetAfterGroups()));
}
auto vertex = groupVertices.at(group.GetName());
@@ -172,8 +181,8 @@ GroupGraph BuildGraph(const std::unordered_set<Group>& masterlistGroups,
}
std::unordered_map<std::string, std::unordered_set<std::string>>
GetTransitiveAfterGroups(const std::unordered_set<Group>& masterlistGroups,
const std::unordered_set<Group>& userGroups) {
GetTransitiveAfterGroups(const std::vector<Group>& masterlistGroups,
const std::vector<Group>& userGroups) {
GroupGraph graph = BuildGraph(masterlistGroups, userGroups);
auto logger = getLogger();
@@ -205,7 +214,7 @@ GetTransitiveAfterGroups(const std::unordered_set<Group>& masterlistGroups,
if (logger) {
logger->trace("Group \"{}\" transitively loads after groups \"{}\"",
graph[vertex],
join(visitedGroups));
joinUnorderedSet(visitedGroups));
}
}
@@ -229,8 +238,8 @@ vertex_t GetVertexByName(const GroupGraph& graph, const std::string& name) {
}
std::vector<Vertex> GetGroupsPath(
const std::unordered_set<Group>& masterlistGroups,
const std::unordered_set<Group>& userGroups,
const std::vector<Group>& masterlistGroups,
const std::vector<Group>& userGroups,
const std::string& fromGroupName,
const std::string& toGroupName) {
GroupGraph graph = BuildGraph(masterlistGroups, userGroups);
+4 -4
View File
@@ -36,12 +36,12 @@
namespace loot {
// Map entries are a group name and names of transitive load after groups.
std::unordered_map<std::string, std::unordered_set<std::string>>
GetTransitiveAfterGroups(const std::unordered_set<Group>& masterlistGroups,
const std::unordered_set<Group>& userGroups);
GetTransitiveAfterGroups(const std::vector<Group>& masterlistGroups,
const std::vector<Group>& userGroups);
std::vector<Vertex> GetGroupsPath(
const std::unordered_set<Group>& masterlistGroups,
const std::unordered_set<Group>& userGroups,
const std::vector<Group>& masterlistGroups,
const std::vector<Group>& userGroups,
const std::string& fromGroupName,
const std::string& toGroupName);
}
+15 -7
View File
@@ -534,7 +534,7 @@ void ignorePlugin(const std::string& pluginName,
std::unordered_set<std::string> pathfinder(
const Group& group,
const std::string& targetGroupName,
const std::unordered_set<Group>& groups,
const std::unordered_map<std::string, Group>& groups,
std::unordered_set<std::string> visitedGroups) {
// If the current group is the target group, return the set of groups in the
// path leading to it.
@@ -552,10 +552,14 @@ std::unordered_set<std::string> pathfinder(
// all return values.
std::unordered_set<std::string> mergedVisitedGroups;
for (const auto& afterGroupName : group.GetAfterGroups()) {
auto afterGroup = *groups.find(Group(afterGroupName));
auto groupIt = groups.find(afterGroupName);
if (groupIt == groups.end()) {
throw std::runtime_error("Cannot find group \"" + afterGroupName +
"\" during sorting.");
}
auto recursedVisitedGroups =
pathfinder(afterGroup, targetGroupName, groups, visitedGroups);
pathfinder(groupIt->second, targetGroupName, groups, visitedGroups);
mergedVisitedGroups.insert(recursedVisitedGroups.begin(),
recursedVisitedGroups.end());
@@ -576,22 +580,26 @@ std::unordered_set<std::string> pathfinder(
}
std::unordered_set<std::string> getGroupsInPaths(
const std::unordered_set<Group>& groups,
const std::unordered_map<std::string, Group>& groups,
const std::string& firstGroupName,
const std::string& lastGroupName) {
// Groups are linked in reverse order, i.e. firstGroup can be found from
// lastGroup, but not the other way around.
auto lastGroup = *groups.find(Group(lastGroupName));
auto groupIt = groups.find(lastGroupName);
if (groupIt == groups.end()) {
throw std::runtime_error("Cannot find group \"" + lastGroupName +
"\" during sorting.");
}
auto groupsInPaths = pathfinder(
lastGroup, firstGroupName, groups, std::unordered_set<std::string>());
groupIt->second, firstGroupName, groups, std::unordered_set<std::string>());
groupsInPaths.erase(lastGroupName);
return groupsInPaths;
}
void PluginGraph::AddGroupEdges(const std::unordered_set<Group>& groups) {
void PluginGraph::AddGroupEdges(const std::unordered_map<std::string, Group>& groups) {
std::vector<std::pair<vertex_t, vertex_t>> acyclicEdgePairs;
std::map<std::string, std::unordered_set<std::string>> groupPluginsToIgnore;
+1 -1
View File
@@ -86,7 +86,7 @@ public:
void AddPluginVertices(Game& game, const std::vector<std::string>& loadOrder);
void AddSpecificEdges();
void AddHardcodedPluginEdges(Game& game);
void AddGroupEdges(const std::unordered_set<Group>& groups);
void AddGroupEdges(const std::unordered_map<std::string, Group>& groups);
void AddOverlapEdges();
void AddTieBreakEdges();
+6 -1
View File
@@ -51,7 +51,12 @@ std::vector<std::string> SortPlugins(
// Now add the interactions between plugins to the graph as edges.
graph.AddSpecificEdges();
graph.AddHardcodedPluginEdges(game);
graph.AddGroupEdges(game.GetDatabase()->GetGroups());
std::unordered_map<std::string, Group> groups;
for (const auto& group : game.GetDatabase()->GetGroups()) {
groups.emplace(group.GetName(), group);
}
graph.AddGroupEdges(groups);
graph.AddOverlapEdges();
graph.AddTieBreakEdges();
@@ -380,31 +380,30 @@ TEST_P(DatabaseInterfaceTest,
auto groups = db_->GetGroups();
EXPECT_EQ(4, groups.size());
ASSERT_EQ(4, groups.size());
EXPECT_EQ(1, groups.count(Group("default")));
EXPECT_TRUE(groups.find(Group("default"))->GetAfterGroups().empty());
EXPECT_EQ("default", groups[0].GetName());
EXPECT_TRUE(groups[0].GetAfterGroups().empty());
EXPECT_EQ(1, groups.count(Group("group1")));
EXPECT_TRUE(groups.find(Group("group1"))->GetAfterGroups().empty());
EXPECT_EQ("group1", groups[1].GetName());
EXPECT_TRUE(groups[1].GetAfterGroups().empty());
EXPECT_EQ(1, groups.count(Group("group2")));
EXPECT_EQ(std::unordered_set<std::string>({"group1", "default"}),
groups.find(Group("group2"))->GetAfterGroups());
EXPECT_EQ("group2", groups[2].GetName());
EXPECT_EQ(std::vector<std::string>({"group1", "default"}),
groups[2].GetAfterGroups());
EXPECT_EQ(1, groups.count(Group("group3")));
EXPECT_EQ(std::unordered_set<std::string>({"group1"}),
groups.find(Group("group3"))->GetAfterGroups());
EXPECT_EQ("group3", groups[3].GetName());
EXPECT_EQ(std::vector<std::string>({"group1"}), groups[3].GetAfterGroups());
}
TEST_P(DatabaseInterfaceTest,
getGroupsShouldReturnDefaultGroupEvenIfNoMetadataIsLoaded) {
auto groups = db_->GetGroups();
EXPECT_EQ(1, groups.size());
ASSERT_EQ(1, groups.size());
EXPECT_EQ("default", groups.begin()->GetName());
EXPECT_TRUE(groups.begin()->GetAfterGroups().empty());
EXPECT_EQ("default", groups[0].GetName());
EXPECT_TRUE(groups[0].GetAfterGroups().empty());
}
TEST_P(DatabaseInterfaceTest,
@@ -416,17 +415,17 @@ TEST_P(DatabaseInterfaceTest,
auto groups = db_->GetGroups(false);
EXPECT_EQ(3, groups.size());
ASSERT_EQ(3, groups.size());
EXPECT_EQ(1, groups.count(Group("default")));
EXPECT_TRUE(groups.find(Group("default"))->GetAfterGroups().empty());
EXPECT_EQ("default", groups[0].GetName());
EXPECT_TRUE(groups[0].GetAfterGroups().empty());
EXPECT_EQ(1, groups.count(Group("group1")));
EXPECT_TRUE(groups.find(Group("group1"))->GetAfterGroups().empty());
EXPECT_EQ("group1", groups[1].GetName());
EXPECT_TRUE(groups[1].GetAfterGroups().empty());
EXPECT_EQ(1, groups.count(Group("group2")));
EXPECT_EQ(std::unordered_set<std::string>({"group1"}),
groups.find(Group("group2"))->GetAfterGroups());
EXPECT_EQ("group2", groups[2].GetName());
EXPECT_EQ(std::vector<std::string>({"group1"}),
groups[2].GetAfterGroups());
}
TEST_P(
@@ -449,18 +448,16 @@ TEST_P(DatabaseInterfaceTest,
auto groups = db_->GetUserGroups();
EXPECT_EQ(3, groups.size());
ASSERT_EQ(3, groups.size());
EXPECT_EQ(1, groups.count(Group("default")));
EXPECT_TRUE(groups.find(Group("default"))->GetAfterGroups().empty());
EXPECT_EQ("default", groups[0].GetName());
EXPECT_TRUE(groups[0].GetAfterGroups().empty());
EXPECT_EQ(1, groups.count(Group("group2")));
EXPECT_EQ(std::unordered_set<std::string>({"default"}),
groups.find(Group("group2"))->GetAfterGroups());
EXPECT_EQ("group2", groups[1].GetName());
EXPECT_EQ(std::vector<std::string>({"default"}), groups[1].GetAfterGroups());
EXPECT_EQ(1, groups.count(Group("group3")));
EXPECT_EQ(std::unordered_set<std::string>({"group1"}),
groups.find(Group("group3"))->GetAfterGroups());
EXPECT_EQ("group3", groups[2].GetName());
EXPECT_EQ(std::vector<std::string>({"group1"}), groups[2].GetAfterGroups());
}
TEST_P(
@@ -471,19 +468,19 @@ TEST_P(
ASSERT_NO_THROW(db_->LoadLists(masterlistPath, userlistPath_));
db_->SetUserGroups(std::unordered_set<Group>({
db_->SetUserGroups(std::vector<Group>({
Group("group4"),
}));
auto groups = db_->GetUserGroups();
EXPECT_EQ(2, groups.size());
ASSERT_EQ(2, groups.size());
EXPECT_EQ(1, groups.count(Group("default")));
EXPECT_TRUE(groups.find(Group("default"))->GetAfterGroups().empty());
EXPECT_EQ("default", groups[0].GetName());
EXPECT_TRUE(groups[0].GetAfterGroups().empty());
EXPECT_EQ(1, groups.count(Group("group4")));
EXPECT_TRUE(groups.find(Group("group4"))->GetAfterGroups().empty());
EXPECT_EQ("group4", groups[1].GetName());
EXPECT_TRUE(groups[1].GetAfterGroups().empty());
}
TEST_P(DatabaseInterfaceTest,
+294 -23
View File
@@ -50,34 +50,306 @@ TEST(Group,
}
TEST(Group, allArgsConstructorShouldStoreGivenValues) {
Group group(
"group1", std::unordered_set<std::string>({"other_group"}), "test");
Group group("group1", {"other_group"}, "test");
EXPECT_EQ("group1", group.GetName());
EXPECT_EQ("test", group.GetDescription());
EXPECT_EQ(std::unordered_set<std::string>({"other_group"}),
EXPECT_EQ(std::vector<std::string>({"other_group"}),
group.GetAfterGroups());
}
TEST(Group, groupsWithCaseInsensitiveEqualNameStringsShouldNotBeEqual) {
Group group1("name");
Group group2("Name");
EXPECT_FALSE(group1 == group2);
}
TEST(Group, groupsWithCaseSensitiveEqualNameStringsShouldBeEqual) {
Group group1("name");
Group group2("name");
TEST(Group, equalityShouldBeCaseSensitiveOnNameAndDescription) {
Group group1("name", {}, "description");
Group group2("name", {}, "description");
EXPECT_TRUE(group1 == group2);
}
TEST(Group, groupsWithDifferentNamesShouldBeUnequal) {
Group group1("name1");
Group group2("name2");
group1 = Group("name");
group2 = Group("Name");
EXPECT_FALSE(group1 == group2);
group1 = Group("name", {}, "description");
group2 = Group("name", {}, "Description");
EXPECT_FALSE(group1 == group2);
group1 = Group("name1");
group2 = Group("name2");
EXPECT_FALSE(group1 == group2);
group1 = Group("name", {}, "description1");
group2 = Group("name", {}, "description2");
EXPECT_FALSE(group1 == group2);
}
TEST(Group, equalityShouldRequireEqualAfterGroups) {
Group group1("name", {}, "description");
Group group2("name", {}, "description");
EXPECT_TRUE(group1 == group2);
group1 = Group("name", {}, "description");
group2 = Group("name", {"after1"}, "Description");
EXPECT_FALSE(group1 == group2);
}
TEST(Group, inequalityShouldBeTheInverseOfEquality) {
Group group1("name", {}, "description");
Group group2("name", {}, "description");
EXPECT_FALSE(group1 != group2);
group1 = Group("name");
group2 = Group("Name");
EXPECT_TRUE(group1 != group2);
group1 = Group("name", {}, "description");
group2 = Group("name", {}, "Description");
EXPECT_TRUE(group1 != group2);
group1 = Group("name1");
group2 = Group("name2");
EXPECT_TRUE(group1 != group2);
group1 = Group("name", {}, "description1");
group2 = Group("name", {}, "description2");
EXPECT_TRUE(group1 != group2);
group1 = Group("name", {}, "description");
group2 = Group("name", {"after1"}, "Description");
EXPECT_TRUE(group1 != group2);
}
TEST(Group,
lessThanOperatorShouldUseCaseSensitiveLexicographicalComparisonForNames) {
Group group1("name", {}, "description");
Group group2("name", {}, "description");
EXPECT_FALSE(group1 < group2);
EXPECT_FALSE(group2 < group1);
group1 = Group("Name", {}, "description");
group2 = Group("name", {}, "description");
EXPECT_TRUE(group1 < group2);
EXPECT_FALSE(group2 < group1);
group1 = Group("name1", {}, "description");
group2 = Group("name2", {}, "description");
EXPECT_TRUE(group1 < group2);
EXPECT_FALSE(group2 < group1);
}
TEST(
Group,
lessThanOperatorShouldUseCaseSensitiveLexicographicalComparisonForDescriptions) {
Group group1("name", {}, "description");
Group group2("name", {}, "description");
EXPECT_FALSE(group1 < group2);
EXPECT_FALSE(group2 < group1);
group1 = Group("name", {}, "Description");
group2 = Group("name", {}, "description");
EXPECT_TRUE(group1 < group2);
EXPECT_FALSE(group2 < group1);
group1 = Group("name", {}, "description1");
group2 = Group("name", {}, "description2");
EXPECT_TRUE(group1 < group2);
EXPECT_FALSE(group2 < group1);
}
TEST(Group, lessThanOperatorShouldCompareAfterGroups) {
Group group1("name", {}, "description");
Group group2("name", {}, "description");
EXPECT_FALSE(group1 < group2);
EXPECT_FALSE(group2 < group1);
group1 = Group("name", {}, "description");
group2 = Group("name", {"group"}, "description");
EXPECT_TRUE(group1 < group2);
EXPECT_FALSE(group2 < group1);
group1 = Group("name", {"Group"}, "description");
group2 = Group("name", {"group"}, "description");
EXPECT_TRUE(group1 < group2);
EXPECT_FALSE(group2 < group1);
group1 = Group("name", {"group1"}, "description");
group2 = Group("name", {"group2"}, "description");
EXPECT_TRUE(group1 < group2);
EXPECT_FALSE(group2 < group1);
}
TEST(
Group,
greaterThanOperatorShouldReturnTrueIfTheSecondGroupIsLessThanTheFirst) {
Group group1("name", {}, "description");
Group group2("name", {}, "description");
EXPECT_FALSE(group1 > group2);
EXPECT_FALSE(group2 > group1);
group1 = Group("Name", {}, "description");
group2 = Group("name", {}, "description");
EXPECT_FALSE(group1 > group2);
EXPECT_TRUE(group2 > group1);
group1 = Group("name1", {}, "description");
group2 = Group("name2", {}, "description");
EXPECT_FALSE(group1 > group2);
EXPECT_TRUE(group2 > group1);
group1 = Group("name", {}, "Description");
group2 = Group("name", {}, "description");
EXPECT_FALSE(group1 > group2);
EXPECT_TRUE(group2 > group1);
group1 = Group("name", {}, "description1");
group2 = Group("name", {}, "description2");
EXPECT_FALSE(group1 > group2);
EXPECT_TRUE(group2 > group1);
group1 = Group("name", {}, "description");
group2 = Group("name", {"group"}, "description");
EXPECT_FALSE(group1 > group2);
EXPECT_TRUE(group2 > group1);
group1 = Group("name", {"Group"}, "description");
group2 = Group("name", {"group"}, "description");
EXPECT_FALSE(group1 > group2);
EXPECT_TRUE(group2 > group1);
group1 = Group("name", {"group1"}, "description");
group2 = Group("name", {"group2"}, "description");
EXPECT_FALSE(group1 > group2);
EXPECT_TRUE(group2 > group1);
}
TEST(Group,
lessThanOrEqualToOperatorShouldReturnTrueIfTheFirstGroupIsNotGreaterThanTheSecond) {
Group group1("name", {}, "description");
Group group2("name", {}, "description");
EXPECT_TRUE(group1 <= group2);
EXPECT_TRUE(group2 <= group1);
group1 = Group("Name", {}, "description");
group2 = Group("name", {}, "description");
EXPECT_TRUE(group1 <= group2);
EXPECT_FALSE(group2 <= group1);
group1 = Group("name1", {}, "description");
group2 = Group("name2", {}, "description");
EXPECT_TRUE(group1 <= group2);
EXPECT_FALSE(group2 <= group1);
group1 = Group("name", {}, "Description");
group2 = Group("name", {}, "description");
EXPECT_TRUE(group1 <= group2);
EXPECT_FALSE(group2 <= group1);
group1 = Group("name", {}, "description1");
group2 = Group("name", {}, "description2");
EXPECT_TRUE(group1 <= group2);
EXPECT_FALSE(group2 <= group1);
group1 = Group("name", {}, "description");
group2 = Group("name", {"group"}, "description");
EXPECT_TRUE(group1 <= group2);
EXPECT_FALSE(group2 <= group1);
group1 = Group("name", {"Group"}, "description");
group2 = Group("name", {"group"}, "description");
EXPECT_TRUE(group1 <= group2);
EXPECT_FALSE(group2 <= group1);
group1 = Group("name", {"group1"}, "description");
group2 = Group("name", {"group2"}, "description");
EXPECT_TRUE(group1 <= group2);
EXPECT_FALSE(group2 <= group1);
}
TEST(Group,
greaterThanOrEqualToOperatorShouldReturnTrueIfTheFirstGroupIsNotLessThanTheSecond) {
Group group1("name", {}, "description");
Group group2("name", {}, "description");
EXPECT_TRUE(group1 >= group2);
EXPECT_TRUE(group2 >= group1);
group1 = Group("Name", {}, "description");
group2 = Group("name", {}, "description");
EXPECT_FALSE(group1 >= group2);
EXPECT_TRUE(group2 >= group1);
group1 = Group("name1", {}, "description");
group2 = Group("name2", {}, "description");
EXPECT_FALSE(group1 >= group2);
EXPECT_TRUE(group2 >= group1);
group1 = Group("name", {}, "Description");
group2 = Group("name", {}, "description");
EXPECT_FALSE(group1 >= group2);
EXPECT_TRUE(group2 >= group1);
group1 = Group("name", {}, "description1");
group2 = Group("name", {}, "description2");
EXPECT_FALSE(group1 >= group2);
EXPECT_TRUE(group2 >= group1);
group1 = Group("name", {}, "description");
group2 = Group("name", {"group"}, "description");
EXPECT_FALSE(group1 >= group2);
EXPECT_TRUE(group2 >= group1);
group1 = Group("name", {"Group"}, "description");
group2 = Group("name", {"group"}, "description");
EXPECT_FALSE(group1 >= group2);
EXPECT_TRUE(group2 >= group1);
group1 = Group("name", {"group1"}, "description");
group2 = Group("name", {"group2"}, "description");
EXPECT_FALSE(group1 >= group2);
EXPECT_TRUE(group2 >= group1);
}
TEST(Group, emittingAsYamlShouldOmitAfterKeyIfAfterGroupsIsEmpty) {
@@ -102,7 +374,7 @@ TEST(Group, emittingAsYamlShouldIncludeDescriptionKeyIfDescriptionIsNotEmpty) {
}
TEST(Group, emittingAsYamlShouldIncludeAfterKeyIfAfterGroupsIsNotEmpty) {
Group group("group1", std::unordered_set<std::string>({"other_group"}));
Group group("group1", {"other_group"});
YAML::Emitter emitter;
emitter << group;
@@ -142,14 +414,13 @@ TEST(Group, encodingAsYamlShouldOmitAfterKeyIfAfterGroupsIsEmpty) {
}
TEST(Group, encodingAsYamlShouldIncludeAfterKeyIfAfterGroupsIsNotEmpty) {
Group group("group1", std::unordered_set<std::string>({"other_group"}));
Group group("group1", {"other_group"});
YAML::Node node;
node = group;
std::unordered_set<std::string> expectedAfterGroups = {"other_group"};
std::vector<std::string> expectedAfterGroups = {"other_group"};
EXPECT_EQ("group1", node["name"].as<std::string>());
EXPECT_EQ(expectedAfterGroups,
node["after"].as<std::unordered_set<std::string>>());
EXPECT_EQ(expectedAfterGroups, node["after"].as<std::vector<std::string>>());
}
TEST(Group, decodingFromYamlShouldSetGivenName) {
@@ -172,7 +443,7 @@ TEST(Group, decodingFromYamlShouldSetAfterGroupsIfAnyAreGiven) {
YAML::Node node = YAML::Load("{name: group1, after: [ other_group ]}");
Group group = node.as<Group>();
std::unordered_set<std::string> expectedAfterGroups = {"other_group"};
std::vector<std::string> expectedAfterGroups = {"other_group"};
EXPECT_EQ("group1", group.GetName());
EXPECT_EQ(expectedAfterGroups, group.GetAfterGroups());
}
+18 -21
View File
@@ -130,18 +130,16 @@ TEST_P(MetadataListTest, loadShouldLoadGroups) {
auto groups = metadataList.Groups();
EXPECT_EQ(3, groups.size());
ASSERT_EQ(3, groups.size());
EXPECT_EQ(1, groups.count(Group("default")));
EXPECT_TRUE(groups.find(Group("default"))->GetAfterGroups().empty());
EXPECT_EQ("default", groups[0].GetName());
EXPECT_TRUE(groups[0].GetAfterGroups().empty());
EXPECT_EQ(1, groups.count(Group("group1")));
EXPECT_EQ(std::unordered_set<std::string>({"group2"}),
groups.find(Group("group1"))->GetAfterGroups());
EXPECT_EQ("group1", groups[1].GetName());
EXPECT_EQ(std::vector<std::string>({"group2"}), groups[1].GetAfterGroups());
EXPECT_EQ(1, groups.count(Group("group2")));
EXPECT_EQ(std::unordered_set<std::string>({"default"}),
groups.find(Group("group2"))->GetAfterGroups());
EXPECT_EQ("group2", groups[2].GetName());
EXPECT_EQ(std::vector<std::string>({"default"}), groups[2].GetAfterGroups());
}
TEST_P(MetadataListTest, loadYamlParsingShouldSupportMergeKeys) {
@@ -164,11 +162,10 @@ TEST_P(MetadataListTest, loadYamlParsingShouldSupportMergeKeys) {
auto groups = metadataList.Groups();
EXPECT_EQ(1, groups.size());
ASSERT_EQ(1, groups.size());
EXPECT_EQ(1, groups.count(Group("default")));
EXPECT_EQ(std::unordered_set<std::string>({"earliest"}),
groups.find(Group("default"))->GetAfterGroups());
EXPECT_EQ("default", groups[0].GetName());
EXPECT_EQ(std::vector<std::string>({"earliest"}), groups[0].GetAfterGroups());
}
TEST_P(MetadataListTest, loadShouldThrowIfAnInvalidMetadataFileIsGiven) {
@@ -222,9 +219,9 @@ TEST_P(MetadataListTest, saveShouldWriteTheLoadedMetadataToTheGivenFilePath) {
EXPECT_EQ(std::set<std::string>({"C.Climate", "Relev"}),
metadataList.BashTags());
EXPECT_EQ(std::unordered_set<Group>(
{Group("default"), Group("group1"), Group("group2")}),
metadataList.Groups());
auto expectedGroups =
std::vector<Group>({Group("default"), Group("group1", {"group2"}), Group("group2", {"default"})});
EXPECT_EQ(expectedGroups, metadataList.Groups());
EXPECT_EQ(std::vector<Message>({
Message(MessageType::say, "A global message."),
@@ -271,13 +268,13 @@ TEST_P(MetadataListTest, setGroupsShouldReplaceExistingGroups) {
auto groups = metadataList.Groups();
EXPECT_EQ(2, groups.size());
ASSERT_EQ(2, groups.size());
EXPECT_EQ(1, groups.count(Group("default")));
EXPECT_TRUE(groups.find(Group("default"))->GetAfterGroups().empty());
EXPECT_EQ("default", groups[0].GetName());
EXPECT_TRUE(groups[0].GetAfterGroups().empty());
EXPECT_EQ(1, groups.count(Group("group4")));
EXPECT_TRUE(groups.find(Group("group4"))->GetAfterGroups().empty());
EXPECT_EQ("group4", groups[1].GetName());
EXPECT_TRUE(groups[1].GetAfterGroups().empty());
}
TEST_P(
@@ -35,7 +35,7 @@ along with LOOT. If not, see
namespace loot {
namespace test {
TEST(GetTransitiveAfterGroups, shouldMapGroupsToTheirTransitiveAfterGroups) {
std::unordered_set<Group> groups(
std::vector<Group> groups(
{Group("a"), Group("b", {"a"}), Group("c", {"b"})});
auto mapped = GetTransitiveAfterGroups(groups, {});
@@ -46,14 +46,14 @@ TEST(GetTransitiveAfterGroups, shouldMapGroupsToTheirTransitiveAfterGroups) {
}
TEST(GetTransitiveAfterGroups, shouldThrowIfAnAfterGroupDoesNotExist) {
std::unordered_set<Group> groups({Group("b", {"a"})});
std::vector<Group> groups({Group("b", {"a"})});
EXPECT_THROW(GetTransitiveAfterGroups(groups, {}), UndefinedGroupError);
}
TEST(GetTransitiveAfterGroups, shouldThrowIfAfterGroupsAreCyclic) {
std::unordered_set<Group> groups({Group("a"), Group("b", {"a"})});
std::unordered_set<Group> userGroups({Group("a", {"c"}), Group("c", {"b"})});
std::vector<Group> groups({Group("a"), Group("b", {"a"})});
std::vector<Group> userGroups({Group("a", {"c"}), Group("c", {"b"})});
try {
GetTransitiveAfterGroups(groups, userGroups);
@@ -101,16 +101,16 @@ TEST(GetTransitiveAfterGroups, shouldThrowIfAfterGroupsAreCyclic) {
}
TEST(GetGroupsPath, shouldThrowIfTheFromGroupDoesNotExist) {
std::unordered_set<Group> groups({Group("a"), Group("b", {"a"})});
std::unordered_set<Group> userGroups({Group("a", {"c"}), Group("c", {"b"})});
std::vector<Group> groups({Group("a"), Group("b", {"a"})});
std::vector<Group> userGroups({Group("a", {"c"}), Group("c", {"b"})});
EXPECT_THROW(GetGroupsPath(groups, userGroups, "d", "a"),
std::invalid_argument);
}
TEST(GetGroupsPath, shouldThrowIfTheToGroupDoesNotExist) {
std::unordered_set<Group> groups({Group("a"), Group("b", {"a"})});
std::unordered_set<Group> userGroups({Group("a", {"c"}), Group("c", {"b"})});
std::vector<Group> groups({Group("a"), Group("b", {"a"})});
std::vector<Group> userGroups({Group("a", {"c"}), Group("c", {"b"})});
EXPECT_THROW(GetGroupsPath(groups, userGroups, "a", "d"),
std::invalid_argument);
@@ -118,7 +118,7 @@ TEST(GetGroupsPath, shouldThrowIfTheToGroupDoesNotExist) {
TEST(GetGroupsPath,
shouldReturnAnEmptyVectorIfThereIsNoPathBetweenTheTwoGroups) {
std::unordered_set<Group> groups({Group("a", {}),
std::vector<Group> groups({Group("a", {}),
Group("b", {"a"}),
Group("c", {"a"}),
Group("d", {"c"}),
@@ -131,7 +131,7 @@ TEST(GetGroupsPath,
TEST(GetGroupsPath,
shouldFindThePathWithTheLeastNumberOfEdgesInAMasterlistOnlyGraph) {
std::unordered_set<Group> groups({Group("a", {}),
std::vector<Group> groups({Group("a", {}),
Group("b", {"a"}),
Group("c", {"a"}),
Group("d", {"c"}),
@@ -152,11 +152,11 @@ TEST(GetGroupsPath,
TEST(GetGroupsPath,
shouldFindThePathWithTheLeastNumberOfEdgesThatContainsUserMetadata) {
std::unordered_set<Group> groups({Group("a", {}),
std::vector<Group> groups({Group("a", {}),
Group("b", {"a"}),
Group("c", {"a"}),
Group("e", {"b"})});
std::unordered_set<Group> userGroups({Group("d", {"c"}), Group("e", {"d"})});
std::vector<Group> userGroups({Group("d", {"c"}), Group("e", {"d"})});
auto path = GetGroupsPath(groups, userGroups, "a", "e");
@@ -175,11 +175,11 @@ TEST(GetGroupsPath,
}
TEST(GetGroupsPath, shouldThrowIfMasterlistGroupLoadsAfterAUserlistGroup) {
std::unordered_set<Group> groups({Group("a", {}),
std::vector<Group> groups({Group("a", {}),
Group("b", {"a"}),
Group("c", {"a"}),
Group("e", {"b", "d"})});
std::unordered_set<Group> userGroups({Group("d", {"c"})});
std::vector<Group> userGroups({Group("d", {"c"})});
EXPECT_THROW(GetGroupsPath(groups, userGroups, "a", "e"),
UndefinedGroupError);