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.
This commit is contained in:
Oliver Hamlet
2021-09-23 13:31:34 +01:00
parent f1d219ac6e
commit 5ce0df0f03
8 changed files with 520 additions and 61 deletions
+14 -4
View File
@@ -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'
+37 -35
View File
@@ -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;
/**
+16 -4
View File
@@ -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.");
+2 -1
View File
@@ -43,7 +43,8 @@ struct ApiDatabase : public DatabaseInterface {
explicit ApiDatabase(std::shared_ptr<ConditionEvaluator> 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;
+130 -3
View File
@@ -24,8 +24,10 @@
#include "api/metadata_list.h"
#include <boost/algorithm/string.hpp>
#include <filesystem>
#include <fstream>
#include <sstream>
#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<char>(in),
std::istreambuf_iterator<char>());
in.close();
return content;
}
std::optional<std::pair<size_t, size_t>> 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())
+11
View File
@@ -46,9 +46,18 @@ struct hash<loot::Filename> {
}
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<Filename, PluginMetadata> unevaluatedPlugins_;
std::vector<PluginMetadata> unevaluatedRegexPlugins_;
std::vector<Message> unevaluatedMessages_;
void Load(std::istream& istream, const std::filesystem::path& source_path);
};
}
@@ -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<std::string>({"group1"}),
groups[2].GetAfterGroups());
EXPECT_EQ(std::vector<std::string>({"group1"}), groups[2].GetAfterGroups());
}
TEST_P(
@@ -740,7 +779,8 @@ TEST_P(DatabaseInterfaceTest,
std::vector<Message> expectedMessages({
Message(MessageType::say,
generalMasterlistMessage, "file(\"" + missingEsp + "\")"),
generalMasterlistMessage,
"file(\"" + missingEsp + "\")"),
Message(MessageType::say, generalUserlistMessage),
});
EXPECT_EQ(expectedMessages, messages);
+259 -3
View File
@@ -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<std::string>({"C.Climate", "Relev"}),
metadataList.BashTags());
auto expectedGroups =
std::vector<Group>({Group("default"), Group("group1", {"group2"}), Group("group2", {"default"})});
auto expectedGroups = std::vector<Group>({Group("default"),
Group("group1", {"group2"}),
Group("group2", {"default"})});
EXPECT_EQ(expectedGroups, metadataList.Groups());
EXPECT_EQ(std::vector<Message>({
@@ -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);
}
}
}