From 5ce0df0f03d738388d445773f766ffbf8e7fb7d4 Mon Sep 17 00:00:00 2001 From: Oliver Hamlet Date: Thu, 5 Aug 2021 17:26:07 +0100 Subject: [PATCH] Add support for masterlist prelude files This allows common metadata to be shared across masterlists in a 'prelude' YAML file that is then injected into a masterlist when it is loaded. Masterlist metadata can then reference anchors within the prelude to make use of those common metadata. The prelude file will be version controlled in a separate Git repository, so the masterlist update functions also apply to managing its versioning. --- docs/metadata/file_structure.rst | 18 +- include/loot/database_interface.h | 72 ++--- src/api/api_database.cpp | 20 +- src/api/api_database.h | 3 +- src/api/metadata_list.cpp | 133 ++++++++- src/api/metadata_list.h | 11 + .../api/interface/database_interface_test.h | 62 ++++- src/tests/api/internals/metadata_list_test.h | 262 +++++++++++++++++- 8 files changed, 520 insertions(+), 61 deletions(-) diff --git a/docs/metadata/file_structure.rst b/docs/metadata/file_structure.rst index 69fcd5dd..db9d9e2a 100644 --- a/docs/metadata/file_structure.rst +++ b/docs/metadata/file_structure.rst @@ -4,11 +4,17 @@ Metadata File Structure The root of a metadata file is a key-value map. LOOT will recognise the following keys, none of which are required. Other keys may also be present, but are not processed by LOOT. +.. describe:: prelude + + The prelude can have any value, but if a masterlist prelude path is provided when loading metadata, the masterlist's ``prelude`` value will be replaced by the parsed content of the masterlist prelude file. The prelude exists so that metadata that is common across different masterlists can be shared without duplication. + + Note that prelude replacement is only supported when using YAML's block style and an unquoted ``prelude`` key that is not preceded by a mapping key indicator and that is immediately followed by a colon separator, i.e. ``prelude:``. + .. describe:: bash_tags string list - A list of Bash Tags that are supported by the masterlist's game. These Bash Tags are used to provide autocomplete suggestions in LOOT's metadata editor. + A list of Bash Tags that are supported by the game. These Bash Tags are used to provide autocomplete suggestions in LOOT's metadata editor. .. describe:: globals @@ -35,14 +41,18 @@ Example .. code-block:: yaml + prelude: + - &thanksForUsing + type: say + content: 'Thanks for using LOOT!' + condition: 'file("LOOT")' + bash_tags: - 'C.Climate' - 'Relev' globals: - - type: say - content: 'You are using the latest version of LOOT.' - condition: 'version("LOOT", "0.5.0.0", ==)' + - *thanksForUsing groups: - name: 'Map Markers' diff --git a/include/loot/database_interface.h b/include/loot/database_interface.h index 560e8680..cdd9e195 100644 --- a/include/loot/database_interface.h +++ b/include/loot/database_interface.h @@ -46,7 +46,8 @@ public: */ /** - * @brief Loads the masterlist and userlist from the paths specified. + * @brief Loads the masterlist, userlist and masterlist prelude from the + * paths specified. * @details Can be called multiple times, each time replacing the * previously-loaded data. * @param masterlist_path @@ -56,9 +57,14 @@ public: * The relative or absolute path to the userlist file that should be * loaded, or an empty path. If an empty path, no userlist will be * loaded. + * @param masterlist_prelude_path + * The relative or absolute path to the masterlist prelude file that + * should be loaded. If an empty path, no masterlist prelude will be + * loaded. */ virtual void LoadLists(const std::filesystem::path& masterlist_path, - const std::filesystem::path& userlist_path = "") = 0; + const std::filesystem::path& userlist_path = "", + const std::filesystem::path& masterlist_prelude_path = "") = 0; /** * Writes a metadata file containing all loaded user-added metadata. @@ -91,47 +97,42 @@ public: */ /** - * @brief Update the given masterlist. - * @details Uses Git to update the given masterlist to a given remote. - * If the masterlist doesn't exist, this will create it. This + * @brief Update the given masterlist or masterlist prelude file. + * @details Uses Git to update the given file using a given remote. + * If the file doesn't exist, this will create it. This * function also initialises a Git repository in the given - * masterlist's parent folder. + * file's parent folder. * * If a Git repository is already present, it will be used to * perform a diff-only update, but if for any reason a * fast-forward merge update is not possible, the existing * repository will be deleted and a new repository cloned from * the given remote. - * @param masterlist_path - * The relative or absolute path to the masterlist file that should be - * updated. The filename must match the filename of the masterlist - * file in the given remote repository, otherwise it will not be - * updated correctly. Although LOOT itself expects this filename to be - * "masterlist.yaml", the API does not check for any specific - * filename. + * @param file_path + * The relative or absolute path to the file that should be + * updated. The filename must match the filename of the file in the + * given remote repository, otherwise it will not be updated + * correctly. The file must be present in the repository's root + * directory. * @param remote_url * The URL of the remote from which to fetch updates. This can also be * a relative or absolute path to a local repository. * @param remote_branch - * The branch of the remote from which to apply updates. LOOT's - * official masterlists are versioned using separate branches for each - * new version of the masterlist syntax, so if you're using them, - * check their repositories to see which is the latest release branch. - * @returns `true` if the masterlist was updated. `false` if no update was + * The branch of the remote from which to apply updates. + * @returns `true` if the file was updated. `false` if no update was * necessary, ie. it was already up-to-date. If `true`, the - * masterlist will need to be re-loaded and re-evaluated separately. + * file will need to be re-loaded and re-evaluated separately. */ - virtual bool UpdateMasterlist(const std::filesystem::path& masterlist_path, + virtual bool UpdateMasterlist(const std::filesystem::path& file_path, const std::string& remote_url, const std::string& remote_branch) = 0; /** - * @brief Get the given masterlist's revision. - * @details Getting a masterlist's revision is only possible if it is found - * inside a local Git repository. - * @param masterlist_path - * The relative or absolute path to the masterlist file that should be - * queried. + * @brief Get the given masterlist or masterlist prelude file's revision. + * @details Getting a file's revision is only possible if it is found + * in the root of a local Git repository. + * @param file_path + * The relative or absolute path to the file that should be queried. * @param get_short_id * If `true`, the shortest unique hexadecimal revision hash that is at * least 7 characters long will be outputted. Otherwise, the full 40 @@ -139,21 +140,22 @@ public: * @returns The revision data. */ virtual FileRevision GetMasterlistRevision( - const std::filesystem::path& masterlist_path, + const std::filesystem::path& file_path, const bool get_short_id) const = 0; /** - * Check if the given masterlist is the latest available for a given branch. - * @param masterlist_path - * The relative or absolute path to the masterlist file for which the - * latest revision should be obtained. It needs to be in a local Git - * repository. + * Check if the given masterlist or masterlist prelude file is the latest + * available for a given branch. + * @param file_path + * The relative or absolute path to the file for which the latest + * revision should be obtained. It needs to be in the root of a local + * Git repository. * @param branch * The branch to check against. - * @return True if the masterlist revision matches the latest masterlist - * revision for the given branch, and false otherwise. + * @return True if the file's current revision matches its latest revision + * for the given branch, and false otherwise. */ - virtual bool IsLatestMasterlist(const std::filesystem::path& masterlist_path, + virtual bool IsLatestMasterlist(const std::filesystem::path& file_path, const std::string& branch) const = 0; /** diff --git a/src/api/api_database.cpp b/src/api/api_database.cpp index 467a7d31..72537a4f 100644 --- a/src/api/api_database.cpp +++ b/src/api/api_database.cpp @@ -44,14 +44,26 @@ ApiDatabase::ApiDatabase( // Database Loading Functions /////////////////////////////////// -void ApiDatabase::LoadLists(const std::filesystem::path& masterlistPath, - const std::filesystem::path& userlistPath) { +void ApiDatabase::LoadLists( + const std::filesystem::path& masterlistPath, + const std::filesystem::path& userlistPath, + const std::filesystem::path& masterlistPreludePath) { MetadataList temp; MetadataList userTemp; if (!masterlistPath.empty()) { if (std::filesystem::exists(masterlistPath)) { - temp.Load(masterlistPath); + if (!masterlistPreludePath.empty()) { + if (std::filesystem::exists(masterlistPreludePath)) { + temp.LoadWithPrelude(masterlistPath, masterlistPreludePath); + } else { + throw FileAccessError( + "The given masterlist prelude path does not exist: " + + masterlistPreludePath.u8string()); + } + } else { + temp.Load(masterlistPath); + } } else { throw FileAccessError("The given masterlist path does not exist: " + masterlistPath.u8string()); @@ -91,7 +103,7 @@ 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 \"" + + throw std::invalid_argument("The path \"" + masterlistPath.u8string() + "\" does not have a valid parent directory."); diff --git a/src/api/api_database.h b/src/api/api_database.h index 40275af8..46118d92 100644 --- a/src/api/api_database.h +++ b/src/api/api_database.h @@ -43,7 +43,8 @@ struct ApiDatabase : public DatabaseInterface { explicit ApiDatabase(std::shared_ptr conditionEvaluator); void LoadLists(const std::filesystem::path& masterlist_path, - const std::filesystem::path& userlist_path = ""); + const std::filesystem::path& userlist_path = "", + const std::filesystem::path& masterlist_prelude_path = ""); void WriteUserMetadata(const std::filesystem::path& outputFile, const bool overwrite) const; diff --git a/src/api/metadata_list.cpp b/src/api/metadata_list.cpp index 6fff9bf0..6ae402cb 100644 --- a/src/api/metadata_list.cpp +++ b/src/api/metadata_list.cpp @@ -24,8 +24,10 @@ #include "api/metadata_list.h" +#include #include #include +#include #include "api/game/game.h" #include "api/helpers/logging.h" @@ -36,6 +38,105 @@ #include "loot/exception/file_access_error.h" namespace loot { +constexpr std::string_view PRELUDE_ON_FIRST_LINE = "prelude:"; +constexpr std::string_view PRELUDE_ON_NEW_LINE = "\nprelude:"; + +std::string read_to_string(const std::filesystem::path& filePath) { + std::ifstream in(filePath); + if (!in.good()) { + throw FileAccessError("Cannot open " + filePath.u8string()); + } + + auto content = std::string(std::istreambuf_iterator(in), + std::istreambuf_iterator()); + + in.close(); + + return content; +} + +std::optional> FindPreludeBounds(const std::string& masterlist) { + size_t startOfPrelude = std::string::npos; + size_t endOfPrelude = std::string::npos; + + // This assumes that the metadata file is using block style at + // the top level, that ? indicators and tags are not used, and + // that key strings are unquoted. + if (boost::starts_with(masterlist, PRELUDE_ON_FIRST_LINE)) { + startOfPrelude = PRELUDE_ON_FIRST_LINE.size(); + } else { + startOfPrelude = masterlist.find(PRELUDE_ON_NEW_LINE); + + if (startOfPrelude != std::string::npos) { + // Skip the leading line break. + startOfPrelude += PRELUDE_ON_NEW_LINE.size(); + } + } + + if (startOfPrelude == std::string::npos) { + // No prelude to replace. + return std::nullopt; + } + + // The end of the prelude is marked by a line break followed by a + // non-space, non-hash (#) character, as this means what follows is + // unindented content. + auto pos = startOfPrelude; + auto lastIndex = masterlist.size() - 1; + while (endOfPrelude == std::string::npos) { + auto nextLineBreakPos = masterlist.find("\n", pos); + if (nextLineBreakPos == std::string::npos || + nextLineBreakPos == lastIndex) { + break; + } + + pos = nextLineBreakPos + 1; + + auto nextChar = masterlist[pos]; + if (nextChar != ' ' && nextChar != '#' && nextChar != '\n') { + endOfPrelude = nextLineBreakPos; + break; + } + } + + return std::make_pair(startOfPrelude, endOfPrelude); +} + +// Indent all prelude content by two spaces to ensure it's parsed as part +// of the prelude. +std::string IndentPrelude(const std::string& prelude) { + auto newPrelude = "\n " + boost::replace_all_copy(prelude, "\n", "\n "); + + boost::replace_all(newPrelude, " \n", "\n"); + + if (boost::ends_with(newPrelude, "\n ")) { + return newPrelude.substr(0, newPrelude.size() - 2); + } + + return newPrelude; +} + +std::string ReplaceMetadataListPrelude(const std::string& prelude, + const std::string& masterlist) { + auto preludeBounds = FindPreludeBounds(masterlist); + + if (!preludeBounds.has_value()) { + // No prelude to replace. + return masterlist; + } + + auto newPrelude = IndentPrelude(prelude); + + auto [startOfPrelude, endOfPrelude] = preludeBounds.value(); + + if (endOfPrelude == std::string::npos) { + return masterlist.substr(0, startOfPrelude) + newPrelude; + } + + return masterlist.substr(0, startOfPrelude) + newPrelude + + masterlist.substr(endOfPrelude); +} + void MetadataList::Load(const std::filesystem::path& filepath) { Clear(); @@ -48,12 +149,36 @@ void MetadataList::Load(const std::filesystem::path& filepath) { if (!in.good()) throw FileAccessError("Cannot open " + filepath.u8string()); - YAML::Node metadataList = YAML::Load(in); + this->Load(in, filepath); + in.close(); +} + +void MetadataList::LoadWithPrelude(const std::filesystem::path& filePath, + const std::filesystem::path& preludePath) { + // Parsing YAML resolves references such that replacing the + // referenced keys entirely (rather than just replacing their values) + // does not cause aliases to be re-resolved, so the old values are + // retained. + // As such, replacing the prelude needs to happen before parsing, + // which means reading the files and performing string manipulation. + auto prelude_content = read_to_string(preludePath); + auto masterlist_content = read_to_string(filePath); + + masterlist_content = + ReplaceMetadataListPrelude(prelude_content, masterlist_content); + + auto stream = std::istringstream(masterlist_content); + this->Load(stream, filePath); +} + +void MetadataList::Load(std::istream& istream, + const std::filesystem::path& source_path) { + YAML::Node metadataList = YAML::Load(istream); if (!metadataList.IsMap()) throw FileAccessError("The root of the metadata file " + - filepath.u8string() + " is not a YAML map."); + source_path.u8string() + " is not a YAML map."); if (metadataList["plugins"]) { for (const auto& node : metadataList["plugins"]) { @@ -99,6 +224,7 @@ void MetadataList::Load(const std::filesystem::path& filepath) { groups_.insert(groups_.cbegin(), Group()); } + auto logger = getLogger(); if (logger) { logger->debug("File loaded successfully."); } @@ -256,7 +382,8 @@ void MetadataList::EvalAllConditions(ConditionEvaluator& conditionEvaluator) { plugins_.clear(); for (const auto& plugin : unevaluatedPlugins_) { - plugins_.emplace(plugin.first, conditionEvaluator.EvaluateAll(plugin.second)); + plugins_.emplace(plugin.first, + conditionEvaluator.EvaluateAll(plugin.second)); } if (unevaluatedRegexPlugins_.empty()) diff --git a/src/api/metadata_list.h b/src/api/metadata_list.h index 4947fc16..ae5798bd 100644 --- a/src/api/metadata_list.h +++ b/src/api/metadata_list.h @@ -46,9 +46,18 @@ struct hash { } namespace loot { + +// This assumes that the prelude and masterlist files both use +// YAML's block style (at least up to the end of the prelude in the +// latter). This is true for all official files. +std::string ReplaceMetadataListPrelude(const std::string& prelude, + const std::string& masterlist); + class MetadataList { public: void Load(const std::filesystem::path& filepath); + void LoadWithPrelude(const std::filesystem::path& filePath, + const std::filesystem::path& preludePath); void Save(const std::filesystem::path& filepath) const; void Clear(); @@ -82,6 +91,8 @@ protected: std::unordered_map unevaluatedPlugins_; std::vector unevaluatedRegexPlugins_; std::vector unevaluatedMessages_; + + void Load(std::istream& istream, const std::filesystem::path& source_path); }; } diff --git a/src/tests/api/interface/database_interface_test.h b/src/tests/api/interface/database_interface_test.h index 53996088..dc960e91 100644 --- a/src/tests/api/interface/database_interface_test.h +++ b/src/tests/api/interface/database_interface_test.h @@ -26,7 +26,6 @@ along with LOOT. If not, see #define LOOT_TESTS_API_INTERFACE_DATABASE_INTERFACE_TEST #include "loot/api.h" - #include "tests/api/interface/api_game_operations_test.h" namespace loot { @@ -124,13 +123,13 @@ protected: // Pass an empty first argument, as it's a prefix for the test instantation, // but we only have the one so no prefix is necessary. INSTANTIATE_TEST_SUITE_P(, - DatabaseInterfaceTest, - ::testing::Values(GameType::tes4, - GameType::tes5, - GameType::fo3, - GameType::fonv, - GameType::fo4, - GameType::tes5se)); + DatabaseInterfaceTest, + ::testing::Values(GameType::tes4, + GameType::tes5, + GameType::fo3, + GameType::fonv, + GameType::fo4, + GameType::tes5se)); TEST_P(DatabaseInterfaceTest, loadListsShouldSucceedEvenIfGameHandleIsDiscarded) { @@ -169,6 +168,47 @@ TEST_P(DatabaseInterfaceTest, EXPECT_NO_THROW(db_->LoadLists(masterlistPath, userlistPath_)); } +TEST_P( + DatabaseInterfaceTest, + loadListsShouldThrowIfAMasterlistIsPresentButAPreludeDoesNotExistAtTheGivenPath) { + ASSERT_NO_THROW(GenerateMasterlist()); + + auto preludePath = localPath / "prelude.yaml"; + + EXPECT_THROW(db_->LoadLists(masterlistPath, "", preludePath), + FileAccessError); +} + +TEST_P(DatabaseInterfaceTest, + loadListsShouldSucceedIfTheMasterlistAndPreludeAreBothPresent) { + using std::endl; + + std::ofstream out(masterlistPath); + out << "prelude:" << endl + << " - &ref" << endl + << " type: say" << endl + << " content: Loaded from same file" << endl + << "globals:" << endl + << " - *ref" << endl; + + out.close(); + + auto preludePath = localPath / "prelude.yaml"; + out.open(preludePath); + out << "common:" << endl + << " - &ref" << endl + << " type: say" << endl + << " content: Loaded from prelude" << endl; + + EXPECT_NO_THROW(db_->LoadLists(masterlistPath, "", preludePath)); + + auto messages = db_->GetGeneralMessages(); + ASSERT_EQ(1, messages.size()); + EXPECT_EQ(MessageType::say, messages[0].GetType()); + ASSERT_EQ(1, messages[0].GetContent().size()); + EXPECT_EQ("Loaded from prelude", messages[0].GetContent()[0].GetText()); +} + TEST_P( DatabaseInterfaceTest, writeUserMetadataShouldThrowIfTheFileAlreadyExistsAndTheOverwriteArgumentIsFalse) { @@ -424,8 +464,7 @@ TEST_P(DatabaseInterfaceTest, EXPECT_TRUE(groups[1].GetAfterGroups().empty()); EXPECT_EQ("group2", groups[2].GetName()); - EXPECT_EQ(std::vector({"group1"}), - groups[2].GetAfterGroups()); + EXPECT_EQ(std::vector({"group1"}), groups[2].GetAfterGroups()); } TEST_P( @@ -740,7 +779,8 @@ TEST_P(DatabaseInterfaceTest, std::vector expectedMessages({ Message(MessageType::say, - generalMasterlistMessage, "file(\"" + missingEsp + "\")"), + generalMasterlistMessage, + "file(\"" + missingEsp + "\")"), Message(MessageType::say, generalUserlistMessage), }); EXPECT_EQ(expectedMessages, messages); diff --git a/src/tests/api/internals/metadata_list_test.h b/src/tests/api/internals/metadata_list_test.h index 595a84a4..5d034dbb 100644 --- a/src/tests/api/internals/metadata_list_test.h +++ b/src/tests/api/internals/metadata_list_test.h @@ -26,7 +26,6 @@ along with LOOT. If not, see #define LOOT_TESTS_API_INTERNALS_METADATA_LIST_TEST #include "api/metadata_list.h" - #include "tests/common_game_test_fixture.h" namespace loot { @@ -205,6 +204,38 @@ TEST_P(MetadataListTest, EXPECT_TRUE(metadataList.BashTags().empty()); } +TEST_P( + MetadataListTest, + loadWithPreludeShouldReplaceThePreludeInTheFirstFileWithTheContentOfTheSecond) { + using std::endl; + + std::ofstream out(metadataPath); + out << "prelude:" << endl + << " - &ref" << endl + << " type: say" << endl + << " content: Loaded from same file" << endl + << "globals:" << endl + << " - *ref" << endl; + + out.close(); + + auto preludePath = metadataFilesPath / "prelude.yaml"; + out.open(preludePath); + out << "common:" << endl + << " - &ref" << endl + << " type: say" << endl + << " content: Loaded from prelude" << endl; + + MetadataList metadataList; + ASSERT_NO_THROW(metadataList.LoadWithPrelude(metadataPath, preludePath)); + + auto messages = metadataList.Messages(); + ASSERT_EQ(1, messages.size()); + EXPECT_EQ(MessageType::say, messages[0].GetType()); + ASSERT_EQ(1, messages[0].GetContent().size()); + EXPECT_EQ("Loaded from prelude", messages[0].GetContent()[0].GetText()); +} + TEST_P(MetadataListTest, saveShouldWriteTheLoadedMetadataToTheGivenFilePath) { MetadataList metadataList; ASSERT_NO_THROW(metadataList.Load(metadataPath)); @@ -219,8 +250,9 @@ TEST_P(MetadataListTest, saveShouldWriteTheLoadedMetadataToTheGivenFilePath) { EXPECT_EQ(std::vector({"C.Climate", "Relev"}), metadataList.BashTags()); - auto expectedGroups = - std::vector({Group("default"), Group("group1", {"group2"}), Group("group2", {"default"})}); + auto expectedGroups = std::vector({Group("default"), + Group("group1", {"group2"}), + Group("group2", {"default"})}); EXPECT_EQ(expectedGroups, metadataList.Groups()); EXPECT_EQ(std::vector({ @@ -391,6 +423,230 @@ TEST_P( EXPECT_EQ(blankEsp, plugin.GetName()); EXPECT_TRUE(plugin.GetDirtyInfo().empty()); } + +TEST(ReplaceMetadataListPrelude, shouldReturnAnEmptyStringIfGivenEmptyStrings) { + std::string prelude = ""; + std::string masterlist = ""; + + auto result = ReplaceMetadataListPrelude(prelude, masterlist); + + EXPECT_EQ(masterlist, result); +} + +TEST(ReplaceMetadataListPrelude, shouldNotChangeAMasterlistWithNoPrelude) { + std::string prelude = R"(globals: + - type: note + content: A message. +)"; + std::string masterlist = R"(plugins: + - name: a.esp +)"; + + auto result = ReplaceMetadataListPrelude(prelude, masterlist); + + EXPECT_EQ(masterlist, result); +} + +TEST(ReplaceMetadataListPrelude, + shouldReplaceAPreludeAtTheStartOfTheMasterlist) { + std::string prelude = R"(globals: + - type: note + content: A message. +)"; + std::string masterlist = R"(prelude: + a: b + +plugins: + - name: a.esp +)"; + + auto result = ReplaceMetadataListPrelude(prelude, masterlist); + + auto expectedResult = R"(prelude: + globals: + - type: note + content: A message. + +plugins: + - name: a.esp +)"; + + EXPECT_EQ(expectedResult, result); +} + +TEST(ReplaceMetadataListPrelude, shouldChangeAMasterlistThatEndsWithAPrelude) { + std::string prelude = R"(globals: + - type: note + content: A message. +)"; + std::string masterlist = R"(plugins: + - name: a.esp +prelude: + a: b + +)"; + + auto result = ReplaceMetadataListPrelude(prelude, masterlist); + + auto expectedResult = R"(plugins: + - name: a.esp +prelude: + globals: + - type: note + content: A message. +)"; + + EXPECT_EQ(expectedResult, result); +} + +TEST(ReplaceMetadataListPrelude, shouldReplaceOnlyThePreludeInTheMasterlist) { + std::string prelude = R"( + +globals: + - type: note + content: A message. + +)"; + std::string masterlist = R"( +common: + key: value +prelude: + a: b +plugins: + - name: a.esp +)"; + + auto result = ReplaceMetadataListPrelude(prelude, masterlist); + + auto expectedResult = R"( +common: + key: value +prelude: + + + globals: + - type: note + content: A message. + + +plugins: + - name: a.esp +)"; + + EXPECT_EQ(expectedResult, result); +} + +TEST(ReplaceMetadataListPrelude, + shouldSucceedIfGivenABlockStylePreludeAndABlockStyleMasterlist) { + std::string prelude = R"(globals: + - type: note + content: A message. +)"; + std::string masterlist = R"(prelude: + a: b + +plugins: + - name: a.esp +)"; + + auto result = ReplaceMetadataListPrelude(prelude, masterlist); + + auto expectedResult = R"(prelude: + globals: + - type: note + content: A message. + +plugins: + - name: a.esp +)"; + + EXPECT_EQ(expectedResult, result); +} + +TEST(ReplaceMetadataListPrelude, + shouldSucceedIfGivenAFlowStylePreludeAndABlockStyleMasterlist) { + std::string prelude = "globals: [{type: note, content: A message.}]"; + std::string masterlist = R"(prelude: + a: b + +plugins: + - name: a.esp +)"; + + auto result = ReplaceMetadataListPrelude(prelude, masterlist); + + auto expectedResult = R"(prelude: + globals: [{type: note, content: A message.}] +plugins: + - name: a.esp +)"; + + EXPECT_EQ(expectedResult, result); +} + +TEST(ReplaceMetadataListPrelude, doesNotChangeAFlowStyleMasterlist) { + std::string prelude = "globals: [{type: note, content: A message.}]"; + std::string masterlist = "{prelude: {}, plugins: [{name: a.esp}]}"; + + auto result = ReplaceMetadataListPrelude(prelude, masterlist); + + EXPECT_EQ(masterlist, result); +} + +TEST(ReplaceMetadataListPrelude, shouldNotStopAtComments) { + std::string prelude = R"(globals: + - type: note + content: A message. +)"; + std::string masterlist = R"(prelude: + a: b +# Comment line + c: d + +plugins: + - name: a.esp +)"; + + auto result = ReplaceMetadataListPrelude(prelude, masterlist); + + auto expectedResult = R"(prelude: + globals: + - type: note + content: A message. + +plugins: + - name: a.esp +)"; + + EXPECT_EQ(expectedResult, result); +} + +TEST(ReplaceMetadataListPrelude, shouldNotStopAtABlankLine) { + std::string prelude = R"(globals: + - type: note + content: A message. +)"; + std::string masterlist = R"(prelude: + a: b + + +plugins: + - name: a.esp +)"; + + auto result = ReplaceMetadataListPrelude(prelude, masterlist); + + auto expectedResult = R"(prelude: + globals: + - type: note + content: A message. + +plugins: + - name: a.esp +)"; + + EXPECT_EQ(expectedResult, result); +} } }