diff --git a/CMakeLists.txt b/CMakeLists.txt index 90694d36..c98ea1b2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -125,7 +125,7 @@ set(PSEUDOSEM_INCLUDE_DIRS "${SOURCE_DIR}/include") ExternalProject_Add(testing-metadata PREFIX "external" - URL "https://github.com/loot/testing-metadata/archive/1.2.0.tar.gz" + URL "https://github.com/loot/testing-metadata/archive/1.3.0.tar.gz" CONFIGURE_COMMAND "" BUILD_COMMAND "" INSTALL_COMMAND "") diff --git a/include/loot/database_interface.h b/include/loot/database_interface.h index b2e2b30c..91bfa615 100644 --- a/include/loot/database_interface.h +++ b/include/loot/database_interface.h @@ -27,6 +27,7 @@ #include #include +#include "loot/metadata/group.h" #include "loot/metadata/message.h" #include "loot/metadata/plugin_metadata.h" #include "loot/struct/masterlist_info.h" @@ -180,6 +181,33 @@ public: virtual std::vector GetGeneralMessages( bool evaluateConditions = false) const = 0; + /** + * @brief Gets the groups that are defined in the loaded metadata lists. + * @returns An unordered set of Group objects. + */ + virtual std::unordered_set GetGroups(bool includeUserMetadata = true) const = 0; + + /** + * @brief Gets the groups that are defined or extended in the loaded userlist. + * @param includeUserMetadata + * 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. + */ + virtual std::unordered_set GetUserGroups() const = 0; + + /** + * @brief Sets the group definitions to store in the userlist, overwriting any + * existing definitions there. + * @param groups + * The unordered set of Group objects to set. + */ + virtual void SetUserGroups(const std::unordered_set& groups) = 0; + + /** + * @brief Set the groups + /** * @} * @name Plugin Data Access diff --git a/src/api/api_database.cpp b/src/api/api_database.cpp index 27a22346..f5aebb53 100644 --- a/src/api/api_database.cpp +++ b/src/api/api_database.cpp @@ -33,6 +33,7 @@ #include "api/metadata/condition_evaluator.h" #include "api/metadata/yaml/plugin_metadata.h" #include "api/plugin/plugin_sorter.h" +#include "loot/metadata/group.h" #include "loot/exception/file_access_error.h" namespace loot { @@ -160,6 +161,41 @@ std::vector ApiDatabase::GetGeneralMessages( return masterlistMessages; } +std::unordered_set ApiDatabase::GetGroups(bool includeUserMetadata) const { + if (!includeUserMetadata) { + return masterlist_.Groups(); + } + + std::unordered_set mergedGroups; + + auto userlistGroups = userlist_.Groups(); + for (const auto& group : masterlist_.Groups()) { + auto userlistGroup = userlistGroups.find(group.GetName()); + 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); + } + } + mergedGroups.insert(userlistGroups.begin(), userlistGroups.end()); + + // Insert the default group if it's not already present. + mergedGroups.insert(Group()); + + return mergedGroups; +} + +std::unordered_set ApiDatabase::GetUserGroups() const { + return userlist_.Groups(); +} +void ApiDatabase::SetUserGroups(const std::unordered_set& groups) { + userlist_.SetGroups(groups); +} + PluginMetadata ApiDatabase::GetPluginMetadata(const std::string& plugin, bool includeUserMetadata, bool evaluateConditions) const { diff --git a/src/api/api_database.h b/src/api/api_database.h index c394381c..6a542851 100644 --- a/src/api/api_database.h +++ b/src/api/api_database.h @@ -68,6 +68,10 @@ struct ApiDatabase : public DatabaseInterface { std::vector GetGeneralMessages( bool evaluateConditions = false) const; + std::unordered_set GetGroups(bool includeUserMetadata = true) const; + std::unordered_set GetUserGroups() const; + void SetUserGroups(const std::unordered_set& groups); + PluginMetadata GetPluginMetadata(const std::string& plugin, bool includeUserMetadata = true, bool evaluateConditions = false) const; diff --git a/src/api/metadata_list.cpp b/src/api/metadata_list.cpp index 87fb1e60..f6486ebe 100644 --- a/src/api/metadata_list.cpp +++ b/src/api/metadata_list.cpp @@ -30,6 +30,7 @@ #include "api/game/game.h" #include "api/helpers/logging.h" #include "api/metadata/condition_evaluator.h" +#include "api/metadata/yaml/group.h" #include "api/metadata/yaml/plugin_metadata.h" #include "loot/exception/file_access_error.h" @@ -69,6 +70,11 @@ void MetadataList::Load(const boost::filesystem::path& filepath) { if (metadataList["bash_tags"]) bashTags_ = metadataList["bash_tags"].as>(); + if (metadataList["groups"]) + groups_ = metadataList["groups"].as>(); + + groups_.insert(Group()); + if (logger) { logger->debug("File loaded successfully."); } @@ -86,6 +92,9 @@ void MetadataList::Save(const boost::filesystem::path& filepath) const { if (!bashTags_.empty()) emitter << YAML::Key << "bash_tags" << YAML::Value << bashTags_; + if (!groups_.empty()) + emitter << YAML::Key << "groups" << YAML::Value << groups_; + if (!messages_.empty()) emitter << YAML::Key << "globals" << YAML::Value << messages_; @@ -127,6 +136,13 @@ std::vector MetadataList::Messages() const { return messages_; } std::set MetadataList::BashTags() const { return bashTags_; } +std::unordered_set MetadataList::Groups() const { return groups_; } + +void MetadataList::SetGroups(const std::unordered_set& groups) { + groups_ = groups; + groups_.insert(Group()); +} + // Merges multiple matching regex entries if any are found. PluginMetadata MetadataList::FindPlugin(const PluginMetadata& plugin) const { PluginMetadata match(plugin.GetName()); diff --git a/src/api/metadata_list.h b/src/api/metadata_list.h index e05f9a32..19da4464 100644 --- a/src/api/metadata_list.h +++ b/src/api/metadata_list.h @@ -33,6 +33,7 @@ #include "api/metadata/condition_evaluator.h" #include "loot/metadata/plugin_metadata.h" +#include "loot/metadata/group.h" namespace loot { class MetadataList { @@ -44,6 +45,9 @@ public: std::list Plugins() const; std::vector Messages() const; std::set BashTags() const; + std::unordered_set Groups() const; + + void SetGroups(const std::unordered_set& groups); // Merges multiple matching regex entries if any are found. PluginMetadata FindPlugin(const PluginMetadata& plugin) const; @@ -59,6 +63,7 @@ public: void EvalAllConditions(const ConditionEvaluator& conditionEvaluator); protected: + std::unordered_set groups_; std::set bashTags_; std::unordered_set plugins_; std::list regexPlugins_; diff --git a/src/tests/api/interface/api_game_operations_test.h b/src/tests/api/interface/api_game_operations_test.h index 342b22a2..2de9a385 100644 --- a/src/tests/api/interface/api_game_operations_test.h +++ b/src/tests/api/interface/api_game_operations_test.h @@ -72,6 +72,11 @@ protected: << " - type: say" << endl << " content: '" << generalMasterlistMessage << "'" << endl << " condition: 'file(\"" << missingEsp << "\")'" << endl + << "groups:" << endl + << " - name: group1" << endl + << " - name: group2" << endl + << " after:" << endl + << " - group1" << endl << "plugins:" << endl << " - name: " << blankEsm << endl << " after:" << endl diff --git a/src/tests/api/interface/database_interface_test.h b/src/tests/api/interface/database_interface_test.h index de1b550a..06e998a0 100644 --- a/src/tests/api/interface/database_interface_test.h +++ b/src/tests/api/interface/database_interface_test.h @@ -103,6 +103,13 @@ protected: userlist << "bash_tags:" << endl << " - RaceRelations" << endl << " - C.Lighting" << endl + << "groups:" << endl + << " - name: group2" << endl + << " after:" << endl + << " - default" << endl + << " - name: group3" << endl + << " after:" << endl + << " - group1" << endl << "globals:" << endl << " - type: say" << endl << " content: '" << generalUserlistMessage << "'" << endl @@ -396,6 +403,108 @@ TEST_P( EXPECT_TRUE(db_->IsLatestMasterlist(masterlistPath.string(), branch_)); } +TEST_P(DatabaseInterfaceTest, + getGroupsShouldReturnAllGroupsListedInTheLoadedMetadata) { + ASSERT_NO_THROW(GenerateMasterlist()); + ASSERT_NO_THROW(GenerateUserlist()); + + ASSERT_NO_THROW( + db_->LoadLists(masterlistPath.string(), userlistPath_.string())); + + auto groups = db_->GetGroups(); + + EXPECT_EQ(4, groups.size()); + + EXPECT_EQ(1, groups.count(Group("default"))); + EXPECT_TRUE(groups.find(Group("default"))->GetAfterGroups().empty()); + + EXPECT_EQ(1, groups.count(Group("group1"))); + EXPECT_TRUE(groups.find(Group("group1"))->GetAfterGroups().empty()); + + EXPECT_EQ(1, groups.count(Group("group2"))); + EXPECT_EQ(std::unordered_set({ "group1", "default" }), groups.find(Group("group2"))->GetAfterGroups()); + + EXPECT_EQ(1, groups.count(Group("group3"))); + EXPECT_EQ(std::unordered_set({ "group1" }), groups.find(Group("group3"))->GetAfterGroups()); +} + +TEST_P(DatabaseInterfaceTest, + getGroupsShouldReturnDefaultGroupEvenIfNoMetadataIsLoaded) { + auto groups = db_->GetGroups(); + + EXPECT_EQ(1, groups.size()); + + EXPECT_EQ("default", groups.begin()->GetName()); + EXPECT_TRUE(groups.begin()->GetAfterGroups().empty()); +} + +TEST_P(DatabaseInterfaceTest, + getGroupsShouldNotIncludeUserlistMetadataIfParameterIsFalse) { + ASSERT_NO_THROW(GenerateMasterlist()); + ASSERT_NO_THROW(GenerateUserlist()); + + ASSERT_NO_THROW( + db_->LoadLists(masterlistPath.string(), userlistPath_.string())); + + auto groups = db_->GetGroups(false); + + EXPECT_EQ(3, groups.size()); + + EXPECT_EQ(1, groups.count(Group("default"))); + EXPECT_TRUE(groups.find(Group("default"))->GetAfterGroups().empty()); + + EXPECT_EQ(1, groups.count(Group("group1"))); + EXPECT_TRUE(groups.find(Group("group1"))->GetAfterGroups().empty()); + + EXPECT_EQ(1, groups.count(Group("group2"))); + EXPECT_EQ(std::unordered_set({ "group1" }), groups.find(Group("group2"))->GetAfterGroups()); +} + +TEST_P(DatabaseInterfaceTest, + getUserGroupsShouldReturnOnlyGroupMetadataFromTheUserlist) { + ASSERT_NO_THROW(GenerateMasterlist()); + ASSERT_NO_THROW(GenerateUserlist()); + + ASSERT_NO_THROW( + db_->LoadLists(masterlistPath.string(), userlistPath_.string())); + + auto groups = db_->GetUserGroups(); + + EXPECT_EQ(3, groups.size()); + + EXPECT_EQ(1, groups.count(Group("default"))); + EXPECT_TRUE(groups.find(Group("default"))->GetAfterGroups().empty()); + + EXPECT_EQ(1, groups.count(Group("group2"))); + EXPECT_EQ(std::unordered_set({ "default" }), groups.find(Group("group2"))->GetAfterGroups()); + + EXPECT_EQ(1, groups.count(Group("group3"))); + EXPECT_EQ(std::unordered_set({ "group1" }), groups.find(Group("group3"))->GetAfterGroups()); +} + +TEST_P(DatabaseInterfaceTest, + setUserGroupsShouldReplaceExistingUserGroupMetadataWithTheGivenMetadata) { + ASSERT_NO_THROW(GenerateMasterlist()); + ASSERT_NO_THROW(GenerateUserlist()); + + ASSERT_NO_THROW( + db_->LoadLists(masterlistPath.string(), userlistPath_.string())); + + db_->SetUserGroups(std::unordered_set({ + Group("group4"), + })); + + auto groups = db_->GetUserGroups(); + + EXPECT_EQ(2, groups.size()); + + EXPECT_EQ(1, groups.count(Group("default"))); + EXPECT_TRUE(groups.find(Group("default"))->GetAfterGroups().empty()); + + EXPECT_EQ(1, groups.count(Group("group4"))); + EXPECT_TRUE(groups.find(Group("group4"))->GetAfterGroups().empty()); +} + TEST_P(DatabaseInterfaceTest, getKnownBashTagsShouldReturnAllBashTagsListedInLoadedMetadata) { ASSERT_NO_THROW(GenerateMasterlist()); diff --git a/src/tests/api/internals/metadata_list_test.h b/src/tests/api/internals/metadata_list_test.h index 6fd6900d..f268b270 100644 --- a/src/tests/api/internals/metadata_list_test.h +++ b/src/tests/api/internals/metadata_list_test.h @@ -68,6 +68,7 @@ protected: const boost::filesystem::path metadataPath; const boost::filesystem::path savedMetadataPath; + const boost::filesystem::path groupMetadataPath; const boost::filesystem::path missingMetadataPath; const std::vector invalidMetadataPaths; }; @@ -118,6 +119,24 @@ TEST_P(MetadataListTest, loadShouldLoadBashTags) { metadataList.BashTags()); } +TEST_P(MetadataListTest, loadShouldLoadGroups) { + MetadataList metadataList; + ASSERT_NO_THROW(metadataList.Load(metadataPath)); + + auto groups = metadataList.Groups(); + + EXPECT_EQ(3, groups.size()); + + EXPECT_EQ(1, groups.count(Group("default"))); + EXPECT_TRUE(groups.find(Group("default"))->GetAfterGroups().empty()); + + EXPECT_EQ(1, groups.count(Group("group1"))); + EXPECT_EQ(std::unordered_set({ "group2" }), groups.find(Group("group1"))->GetAfterGroups()); + + EXPECT_EQ(1, groups.count(Group("group2"))); + EXPECT_EQ(std::unordered_set({ "default" }), groups.find(Group("group2"))->GetAfterGroups()); +} + TEST_P(MetadataListTest, loadShouldThrowIfAnInvalidMetadataFileIsGiven) { MetadataList ml; for (const auto& path : invalidMetadataPaths) { @@ -169,6 +188,9 @@ TEST_P(MetadataListTest, saveShouldWriteTheLoadedMetadataToTheGivenFilePath) { EXPECT_EQ(std::set({"C.Climate", "Relev"}), metadataList.BashTags()); + EXPECT_EQ(std::unordered_set({ Group("default"), Group("group1"), Group("group2") }), + metadataList.Groups()); + EXPECT_EQ(std::vector({ Message(MessageType::say, "A global message."), }), @@ -206,6 +228,25 @@ TEST_P(MetadataListTest, clearShouldClearLoadedData) { EXPECT_TRUE(metadataList.BashTags().empty()); } +TEST_P(MetadataListTest, setGroupsShouldReplaceExistingGroups) { + MetadataList metadataList; + ASSERT_NO_THROW(metadataList.Load(metadataPath)); + + metadataList.SetGroups({ + Group("group4") + }); + + auto groups = metadataList.Groups(); + + EXPECT_EQ(2, groups.size()); + + EXPECT_EQ(1, groups.count(Group("default"))); + EXPECT_TRUE(groups.find(Group("default"))->GetAfterGroups().empty()); + + EXPECT_EQ(1, groups.count(Group("group4"))); + EXPECT_TRUE(groups.find(Group("group4"))->GetAfterGroups().empty()); +} + TEST_P( MetadataListTest, findPluginShouldReturnAnEmptyPluginObjectIfTheGivenPluginIsNotInTheMetadataList) { @@ -329,7 +370,7 @@ TEST_P( plugin = metadataList.FindPlugin(PluginMetadata(blankEsp)); EXPECT_EQ(blankEsp, plugin.GetName()); - EXPECT_TRUE(plugin.HasNameOnly()); + EXPECT_TRUE(plugin.GetDirtyInfo().empty()); } } }