diff --git a/cxx/src/tests/api/interface/api_game_operations_test.h b/cxx/src/tests/api/interface/api_game_operations_test.h
new file mode 100644
index 00000000..98cab2ae
--- /dev/null
+++ b/cxx/src/tests/api/interface/api_game_operations_test.h
@@ -0,0 +1,132 @@
+/* LOOT
+
+A load order optimisation tool for Oblivion, Skyrim, Fallout 3 and
+Fallout: New Vegas.
+
+Copyright (C) 2013-2016 WrinklyNinja
+
+This file is part of LOOT.
+
+LOOT is free software: you can redistribute
+it and/or modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, either version 3 of
+the License, or (at your option) any later version.
+
+LOOT is distributed in the hope that it will
+be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with LOOT. If not, see
+.
+*/
+
+#ifndef LOOT_TESTS_API_INTERFACE_API_GAME_OPERATIONS_TEST
+#define LOOT_TESTS_API_INTERFACE_API_GAME_OPERATIONS_TEST
+
+#include
+
+#include "loot/api.h"
+#include "tests/common_game_test_fixture.h"
+
+namespace loot {
+namespace test {
+class ApiGameOperationsTest : public CommonGameTestFixture,
+ public testing::WithParamInterface {
+protected:
+ ApiGameOperationsTest() :
+ CommonGameTestFixture(GetParam()),
+ handle_(nullptr),
+ masterlistPath(localPath / "masterlist.yaml"),
+ noteMessage(
+ "Do not clean ITM records, they are intentional and required for "
+ "the mod to function."),
+ warningMessage(
+ "Check you are using v2+. If not, Update. v1 has a severe bug with "
+ "the Mystic Emporium disappearing."),
+ errorMessage("Obsolete. Remove this and install Enhanced Weather."),
+ generalMasterlistMessage("A general masterlist message.") {}
+
+ void SetUp() override {
+ CommonGameTestFixture::SetUp();
+
+ ASSERT_FALSE(std::filesystem::exists(masterlistPath));
+
+ handle_ = CreateGameHandle(GetParam(), gamePath, localPath);
+ }
+
+ void GenerateMasterlist() {
+ using std::endl;
+
+ std::ofstream masterlist(masterlistPath);
+ masterlist << "bash_tags:" << endl
+ << " - Actors.ACBS" << endl
+ << " - C.Climate" << endl
+ << "globals:" << endl
+ << " - 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
+ << " - " << masterFile << endl
+ << " msg:" << endl
+ << " - type: say" << endl
+ << " content: '" << noteMessage << "'" << endl
+ << " condition: 'file(\"" << missingEsp << "\")'" << endl
+ << " tag:" << endl
+ << " - Actors.ACBS" << endl
+ << " - Actors.AIData" << endl
+ << " - '-C.Water'" << endl
+ << " - name: " << blankDifferentEsm << endl
+ << " after:" << endl
+ << " - " << blankMasterDependentEsm << endl
+ << " msg:" << endl
+ << " - type: warn" << endl
+ << " content: '" << warningMessage << "'" << endl
+ << " dirty:" << endl
+ << " - crc: 0x7d22f9df" << endl
+ << " util: TES4Edit" << endl
+ << " udr: 4" << endl
+ << " - name: " << blankDifferentEsp << endl
+ << " after:" << endl
+ << " - " << blankPluginDependentEsp << endl
+ << " msg:" << endl
+ << " - type: error" << endl
+ << " content: '" << errorMessage << "'" << endl
+ << " - name: " << blankEsp << endl
+ << " after:" << endl
+ << " - " << blankDifferentMasterDependentEsp << endl
+ << " - name: " << blankDifferentMasterDependentEsp << endl
+ << " after:" << endl
+ << " - " << blankMasterDependentEsp << endl
+ << " msg:" << endl
+ << " - type: say" << endl
+ << " content: '" << noteMessage << "'" << endl
+ << " - type: warn" << endl
+ << " content: '" << warningMessage << "'" << endl
+ << " - type: error" << endl
+ << " content: '" << errorMessage << "'" << endl;
+
+ masterlist.close();
+ }
+
+ std::unique_ptr handle_;
+
+ const std::filesystem::path masterlistPath;
+
+ const std::string noteMessage;
+ const std::string warningMessage;
+ const std::string errorMessage;
+ const std::string generalMasterlistMessage;
+};
+}
+}
+
+#endif
diff --git a/cxx/src/tests/api/interface/create_game_handle_test.h b/cxx/src/tests/api/interface/create_game_handle_test.h
new file mode 100644
index 00000000..07b7a01b
--- /dev/null
+++ b/cxx/src/tests/api/interface/create_game_handle_test.h
@@ -0,0 +1,157 @@
+/* LOOT
+
+A load order optimisation tool for Oblivion, Skyrim, Fallout 3 and
+Fallout: New Vegas.
+
+Copyright (C) 2014-2016 WrinklyNinja
+
+This file is part of LOOT.
+
+LOOT is free software: you can redistribute
+it and/or modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, either version 3 of
+the License, or (at your option) any later version.
+
+LOOT is distributed in the hope that it will
+be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with LOOT. If not, see
+.
+*/
+
+#ifndef LOOT_TESTS_API_INTERFACE_CREATE_GAME_HANDLE_TEST
+#define LOOT_TESTS_API_INTERFACE_CREATE_GAME_HANDLE_TEST
+
+#include
+
+#include "loot/api.h"
+#include "tests/common_game_test_fixture.h"
+
+namespace loot {
+namespace test {
+class CreateGameHandleTest : public CommonGameTestFixture,
+ public testing::WithParamInterface {
+protected:
+ CreateGameHandleTest() :
+ CommonGameTestFixture(GetParam()),
+ handle_(nullptr),
+ gamePathSymlink(gamePath.string() + ".symlink"),
+ localPathSymlink(localPath.string() + ".symlink"),
+ gamePathJunctionLink(gamePath.string() + ".junction"),
+ localPathJunctionLink(localPath.string() + ".junction"),
+ originalWorkingDirectory(std::filesystem::current_path()) {}
+
+ void SetUp() override {
+ using std::filesystem::file_type;
+ using std::filesystem::status;
+ CommonGameTestFixture::SetUp();
+
+ std::filesystem::create_directory_symlink(gamePath, gamePathSymlink);
+ ASSERT_EQ(file_type::directory, status(gamePathSymlink).type());
+
+ std::filesystem::create_directory_symlink(localPath, localPathSymlink);
+ ASSERT_EQ(file_type::directory, status(localPathSymlink).type());
+
+#ifdef _WIN32
+ system(("mklink /J \"" +
+ std::filesystem::absolute(gamePathJunctionLink).string() + "\" \"" +
+ std::filesystem::absolute(dataPath).parent_path().string() + "\"")
+ .c_str());
+ system(("mklink /J \"" +
+ std::filesystem::absolute(localPathJunctionLink).string() +
+ "\" \"" + std::filesystem::absolute(localPath).string() + "\"")
+ .c_str());
+#endif
+
+ // relative() doesn't work when the current working directory and the given
+ // path are on separate drives, so ensure that's not the case.
+ std::filesystem::current_path(gamePath.parent_path());
+ }
+
+ void TearDown() override {
+ std::filesystem::current_path(originalWorkingDirectory);
+ }
+
+ std::unique_ptr handle_;
+
+ const std::filesystem::path gamePathSymlink;
+ const std::filesystem::path localPathSymlink;
+ const std::filesystem::path gamePathJunctionLink;
+ const std::filesystem::path localPathJunctionLink;
+ const std::filesystem::path originalWorkingDirectory;
+};
+
+// 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(,
+ CreateGameHandleTest,
+ ::testing::Values(GameType::tes4,
+ GameType::tes5,
+ GameType::fo3,
+ GameType::fonv,
+ GameType::fo4,
+ GameType::tes5se,
+ GameType::fo4vr,
+ GameType::tes5vr,
+ GameType::tes3,
+ GameType::starfield,
+ GameType::openmw));
+
+TEST_P(CreateGameHandleTest,
+ shouldSucceedIfPassedValidParametersWithRelativePaths) {
+ using std::filesystem::relative;
+ EXPECT_NO_THROW(handle_ = CreateGameHandle(
+ GetParam(), relative(gamePath), relative(localPath)));
+ EXPECT_TRUE(handle_);
+}
+
+TEST_P(CreateGameHandleTest,
+ shouldSucceedIfPassedValidParametersWithAbsolutePaths) {
+ EXPECT_NO_THROW(handle_ = CreateGameHandle(GetParam(), gamePath, localPath));
+ EXPECT_TRUE(handle_);
+}
+
+TEST_P(CreateGameHandleTest, shouldThrowIfPassedAGamePathThatDoesNotExist) {
+ EXPECT_THROW(CreateGameHandle(GetParam(), missingPath, localPath),
+ std::invalid_argument);
+}
+
+TEST_P(CreateGameHandleTest, shouldSucceedIfPassedALocalPathThatDoesNotExist) {
+ EXPECT_NO_THROW(handle_ =
+ CreateGameHandle(GetParam(), gamePath, missingPath));
+ EXPECT_TRUE(handle_);
+}
+
+TEST_P(CreateGameHandleTest, shouldThrowIfPassedALocalPathThatIsNotADirectory) {
+ EXPECT_THROW(CreateGameHandle(GetParam(), gamePath, dataPath / blankEsm),
+ std::invalid_argument);
+}
+
+#ifdef _WIN32
+TEST_P(CreateGameHandleTest, shouldReturnOkIfPassedAnEmptyLocalPathString) {
+ EXPECT_NO_THROW(handle_ = CreateGameHandle(GetParam(), gamePath, ""));
+ EXPECT_TRUE(handle_);
+}
+#endif
+
+TEST_P(CreateGameHandleTest, shouldReturnOkIfPassedGameAndLocalPathSymlinks) {
+ EXPECT_NO_THROW(handle_ = CreateGameHandle(
+ GetParam(), gamePathSymlink, localPathSymlink));
+ EXPECT_TRUE(handle_);
+}
+
+#ifdef _WIN32
+TEST_P(CreateGameHandleTest,
+ shouldReturnOkIfPassedGameAndLocalPathJunctionLinks) {
+ EXPECT_NO_THROW(handle_ = CreateGameHandle(
+ GetParam(), gamePathJunctionLink, localPathJunctionLink));
+ EXPECT_TRUE(handle_);
+}
+#endif
+}
+}
+
+#endif
diff --git a/cxx/src/tests/api/interface/database_interface_test.h b/cxx/src/tests/api/interface/database_interface_test.h
new file mode 100644
index 00000000..2a4511b2
--- /dev/null
+++ b/cxx/src/tests/api/interface/database_interface_test.h
@@ -0,0 +1,818 @@
+/* LOOT
+
+A load order optimisation tool for Oblivion, Skyrim, Fallout 3 and
+Fallout: New Vegas.
+
+Copyright (C) 2014-2016 WrinklyNinja
+
+This file is part of LOOT.
+
+LOOT is free software: you can redistribute
+it and/or modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, either version 3 of
+the License, or (at your option) any later version.
+
+LOOT is distributed in the hope that it will
+be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with LOOT. If not, see
+.
+*/
+
+#ifndef LOOT_TESTS_API_INTERFACE_DATABASE_INTERFACE_TEST
+#define LOOT_TESTS_API_INTERFACE_DATABASE_INTERFACE_TEST
+
+#include "loot/api.h"
+#include "tests/api/interface/api_game_operations_test.h"
+
+namespace loot {
+namespace test {
+class DatabaseInterfaceTest : public ApiGameOperationsTest {
+protected:
+ DatabaseInterfaceTest() :
+ userlistPath_(localPath / "userlist.yaml"),
+ minimalOutputPath_(localPath / "minimal.yml"),
+ generalUserlistMessage("A general userlist message.") {}
+
+ void SetUp() override {
+ ApiGameOperationsTest::SetUp();
+
+ ASSERT_FALSE(std::filesystem::exists(minimalOutputPath_));
+ }
+
+ std::string GetFileContent(const std::filesystem::path& file) {
+ std::ifstream stream(file);
+ std::stringstream content;
+ content << stream.rdbuf();
+
+ return content.str();
+ }
+
+ void GenerateUserlist() {
+ using std::endl;
+
+ std::ofstream userlist(userlistPath_);
+ 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
+ << "plugins:" << endl
+ << " - name: " << blankEsm << endl
+ << " after:" << endl
+ << " - " << blankDifferentEsm << endl
+ << " tag:" << endl
+ << " - name: Actors.ACBS" << endl
+ << " condition: 'file(\"" << missingEsp << "\")'" << endl
+ << " - name: " << blankDifferentEsp << endl
+ << " inc:" << endl
+ << " - " << blankEsp << endl
+ << " tag:" << endl
+ << " - name: C.Climate" << endl
+ << " condition: 'file(\"" << missingEsp << "\")'" << endl;
+
+ userlist.close();
+ }
+
+ const std::filesystem::path userlistPath_;
+ const std::filesystem::path minimalOutputPath_;
+ const std::string generalUserlistMessage;
+};
+
+// 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));
+
+TEST_P(DatabaseInterfaceTest,
+ loadListsShouldSucceedEvenIfGameHandleIsDiscarded) {
+ handle_ = CreateGameHandle(GetParam(), gamePath, localPath);
+
+ ASSERT_NO_THROW(GenerateMasterlist());
+
+ EXPECT_NO_THROW(handle_->GetDatabase().LoadLists(masterlistPath, ""));
+}
+
+TEST_P(DatabaseInterfaceTest, loadListsShouldThrowIfNoMasterlistIsPresent) {
+ EXPECT_THROW(handle_->GetDatabase().LoadLists(masterlistPath, ""),
+ FileAccessError);
+}
+
+TEST_P(
+ DatabaseInterfaceTest,
+ loadListsShouldThrowIfAMasterlistIsPresentButAUserlistDoesNotExistAtTheGivenPath) {
+ ASSERT_NO_THROW(GenerateMasterlist());
+ EXPECT_THROW(handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_),
+ FileAccessError);
+}
+
+TEST_P(
+ DatabaseInterfaceTest,
+ loadListsShouldSucceedIfTheMasterlistIsPresentAndTheUserlistPathIsAnEmptyString) {
+ ASSERT_NO_THROW(GenerateMasterlist());
+
+ EXPECT_NO_THROW(handle_->GetDatabase().LoadLists(masterlistPath, ""));
+}
+
+TEST_P(DatabaseInterfaceTest,
+ loadListsShouldSucceedIfTheMasterlistAndUserlistAreBothPresent) {
+ ASSERT_NO_THROW(GenerateMasterlist());
+ ASSERT_NO_THROW(std::filesystem::copy(masterlistPath, userlistPath_));
+
+ EXPECT_NO_THROW(
+ handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_));
+}
+
+TEST_P(
+ DatabaseInterfaceTest,
+ loadListsShouldThrowIfAMasterlistIsPresentButAPreludeDoesNotExistAtTheGivenPath) {
+ ASSERT_NO_THROW(GenerateMasterlist());
+
+ auto preludePath = localPath / "prelude.yaml";
+
+ EXPECT_THROW(
+ handle_->GetDatabase().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(
+ handle_->GetDatabase().LoadLists(masterlistPath, "", preludePath));
+
+ auto messages = handle_->GetDatabase().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) {
+ ASSERT_NO_THROW(
+ handle_->GetDatabase().WriteUserMetadata(minimalOutputPath_, false));
+ ASSERT_TRUE(std::filesystem::exists(minimalOutputPath_));
+
+ EXPECT_THROW(
+ handle_->GetDatabase().WriteUserMetadata(minimalOutputPath_, false),
+ FileAccessError);
+}
+
+TEST_P(
+ DatabaseInterfaceTest,
+ writeUserMetadataShouldReturnOkAndWriteToFileIfTheArgumentsAreValidAndTheOverwriteArgumentIsTrue) {
+ EXPECT_NO_THROW(
+ handle_->GetDatabase().WriteUserMetadata(minimalOutputPath_, true));
+ EXPECT_TRUE(std::filesystem::exists(minimalOutputPath_));
+}
+
+TEST_P(
+ DatabaseInterfaceTest,
+ writeUserMetadataShouldReturnOkIfTheFileAlreadyExistsAndTheOverwriteArgumentIsTrue) {
+ ASSERT_NO_THROW(
+ handle_->GetDatabase().WriteUserMetadata(minimalOutputPath_, false));
+ ASSERT_TRUE(std::filesystem::exists(minimalOutputPath_));
+
+ EXPECT_NO_THROW(
+ handle_->GetDatabase().WriteUserMetadata(minimalOutputPath_, true));
+}
+
+TEST_P(DatabaseInterfaceTest,
+ writeUserMetadataShouldThrowIfPathGivenExistsAndIsReadOnly) {
+ ASSERT_NO_THROW(
+ handle_->GetDatabase().WriteUserMetadata(minimalOutputPath_, false));
+ ASSERT_TRUE(std::filesystem::exists(minimalOutputPath_));
+
+ std::filesystem::permissions(minimalOutputPath_,
+ std::filesystem::perms::owner_read,
+ std::filesystem::perm_options::replace);
+
+ EXPECT_THROW(
+ handle_->GetDatabase().WriteUserMetadata(minimalOutputPath_, true),
+ FileAccessError);
+}
+
+TEST_P(DatabaseInterfaceTest,
+ writeUserMetadataShouldShouldNotWriteMasterlistMetadata) {
+ ASSERT_NO_THROW(GenerateMasterlist());
+ ASSERT_NO_THROW(handle_->GetDatabase().LoadLists(masterlistPath, ""));
+
+ EXPECT_NO_THROW(
+ handle_->GetDatabase().WriteUserMetadata(minimalOutputPath_, true));
+
+ EXPECT_EQ("{}", GetFileContent(minimalOutputPath_));
+}
+
+TEST_P(DatabaseInterfaceTest, writeUserMetadataShouldShouldWriteUserMetadata) {
+ ASSERT_NO_THROW(GenerateMasterlist());
+ ASSERT_NO_THROW(std::filesystem::copy(masterlistPath, userlistPath_));
+
+ std::ofstream masterlist(masterlistPath);
+ masterlist << "bash_tags:\n []\nglobals:\n []\nplugins:\n []";
+ masterlist.close();
+
+ ASSERT_NO_THROW(
+ handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_));
+
+ EXPECT_NO_THROW(
+ handle_->GetDatabase().WriteUserMetadata(minimalOutputPath_, true));
+
+ EXPECT_FALSE(GetFileContent(minimalOutputPath_).empty());
+}
+
+TEST_P(DatabaseInterfaceTest,
+ getGroupsShouldReturnAllGroupsListedInTheLoadedMetadata) {
+ ASSERT_NO_THROW(GenerateMasterlist());
+ ASSERT_NO_THROW(GenerateUserlist());
+
+ ASSERT_NO_THROW(
+ handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_));
+
+ auto groups = handle_->GetDatabase().GetGroups();
+
+ ASSERT_EQ(4, groups.size());
+
+ EXPECT_EQ("default", groups[0].GetName());
+ EXPECT_TRUE(groups[0].GetAfterGroups().empty());
+
+ EXPECT_EQ("group1", groups[1].GetName());
+ EXPECT_TRUE(groups[1].GetAfterGroups().empty());
+
+ EXPECT_EQ("group2", groups[2].GetName());
+ EXPECT_EQ(std::vector({"group1", "default"}),
+ groups[2].GetAfterGroups());
+
+ EXPECT_EQ("group3", groups[3].GetName());
+ EXPECT_EQ(std::vector({"group1"}), groups[3].GetAfterGroups());
+}
+
+TEST_P(DatabaseInterfaceTest,
+ getGroupsShouldReturnDefaultGroupEvenIfNoMetadataIsLoaded) {
+ auto groups = handle_->GetDatabase().GetGroups();
+
+ ASSERT_EQ(1, groups.size());
+
+ EXPECT_EQ("default", groups[0].GetName());
+ EXPECT_TRUE(groups[0].GetAfterGroups().empty());
+}
+
+TEST_P(DatabaseInterfaceTest,
+ getGroupsShouldNotIncludeUserlistMetadataIfParameterIsFalse) {
+ ASSERT_NO_THROW(GenerateMasterlist());
+ ASSERT_NO_THROW(GenerateUserlist());
+
+ ASSERT_NO_THROW(
+ handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_));
+
+ auto groups = handle_->GetDatabase().GetGroups(false);
+
+ ASSERT_EQ(3, groups.size());
+
+ EXPECT_EQ("default", groups[0].GetName());
+ EXPECT_TRUE(groups[0].GetAfterGroups().empty());
+
+ EXPECT_EQ("group1", groups[1].GetName());
+ EXPECT_TRUE(groups[1].GetAfterGroups().empty());
+
+ EXPECT_EQ("group2", groups[2].GetName());
+ EXPECT_EQ(std::vector({"group1"}), groups[2].GetAfterGroups());
+}
+
+TEST_P(
+ DatabaseInterfaceTest,
+ getGroupsShouldReturnDefaultGroupIfNoMasterlistIsLoadedAndUserlistMetadataIsNotIncluded) {
+ auto groups = handle_->GetDatabase().GetGroups(false);
+
+ EXPECT_EQ(1, groups.size());
+
+ EXPECT_EQ("default", groups.begin()->GetName());
+ EXPECT_TRUE(groups.begin()->GetAfterGroups().empty());
+}
+
+TEST_P(DatabaseInterfaceTest,
+ getUserGroupsShouldReturnOnlyGroupMetadataFromTheUserlist) {
+ ASSERT_NO_THROW(GenerateMasterlist());
+ ASSERT_NO_THROW(GenerateUserlist());
+
+ ASSERT_NO_THROW(
+ handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_));
+
+ auto groups = handle_->GetDatabase().GetUserGroups();
+
+ ASSERT_EQ(3, groups.size());
+
+ EXPECT_EQ("default", groups[0].GetName());
+ EXPECT_TRUE(groups[0].GetAfterGroups().empty());
+
+ EXPECT_EQ("group2", groups[1].GetName());
+ EXPECT_EQ(std::vector({"default"}), groups[1].GetAfterGroups());
+
+ EXPECT_EQ("group3", groups[2].GetName());
+ EXPECT_EQ(std::vector({"group1"}), groups[2].GetAfterGroups());
+}
+
+TEST_P(
+ DatabaseInterfaceTest,
+ setUserGroupsShouldReplaceExistingUserGroupMetadataWithTheGivenMetadata) {
+ ASSERT_NO_THROW(GenerateMasterlist());
+ ASSERT_NO_THROW(GenerateUserlist());
+
+ ASSERT_NO_THROW(
+ handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_));
+
+ handle_->GetDatabase().SetUserGroups(std::vector({
+ Group("group4"),
+ }));
+
+ auto groups = handle_->GetDatabase().GetUserGroups();
+
+ ASSERT_EQ(2, groups.size());
+
+ EXPECT_EQ("default", groups[0].GetName());
+ EXPECT_TRUE(groups[0].GetAfterGroups().empty());
+
+ EXPECT_EQ("group4", groups[1].GetName());
+ EXPECT_TRUE(groups[1].GetAfterGroups().empty());
+}
+
+TEST_P(DatabaseInterfaceTest,
+ getGroupsPathShouldReturnTheShortestPathBetweenTheGivenGroups) {
+ ASSERT_NO_THROW(GenerateMasterlist());
+ ASSERT_NO_THROW(GenerateUserlist());
+
+ ASSERT_NO_THROW(
+ handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_));
+
+ auto path = handle_->GetDatabase().GetGroupsPath("group1", "group3");
+
+ ASSERT_EQ(2, path.size());
+ EXPECT_EQ("group1", path[0].GetName());
+ EXPECT_EQ(EdgeType::userLoadAfter, path[0].GetTypeOfEdgeToNextVertex());
+ EXPECT_EQ("group3", path[1].GetName());
+ EXPECT_FALSE(path[1].GetTypeOfEdgeToNextVertex().has_value());
+}
+
+TEST_P(DatabaseInterfaceTest,
+ getKnownBashTagsShouldReturnAllBashTagsListedInLoadedMetadata) {
+ ASSERT_NO_THROW(GenerateMasterlist());
+ ASSERT_NO_THROW(GenerateUserlist());
+
+ ASSERT_NO_THROW(
+ handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_));
+
+ auto tags = handle_->GetDatabase().GetKnownBashTags();
+
+ std::vector expectedTags({
+ "Actors.ACBS",
+ "C.Climate",
+ "RaceRelations",
+ "C.Lighting",
+ });
+ EXPECT_EQ(expectedTags, tags);
+}
+
+TEST_P(DatabaseInterfaceTest,
+ getGeneralMessagesShouldGetGeneralMessagesFromTheMasterlistAndUserlist) {
+ ASSERT_NO_THROW(GenerateMasterlist());
+ ASSERT_NO_THROW(GenerateUserlist());
+ ASSERT_NO_THROW(
+ handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_));
+
+ auto messages = handle_->GetDatabase().GetGeneralMessages();
+
+ std::vector expectedMessages({
+ Message(MessageType::say,
+ generalMasterlistMessage,
+ "file(\"" + missingEsp + "\")"),
+ Message(MessageType::say, generalUserlistMessage),
+ });
+ EXPECT_EQ(expectedMessages, messages);
+}
+
+TEST_P(
+ DatabaseInterfaceTest,
+ getGeneralMessagesShouldReturnOnlyValidMessagesIfConditionsAreEvaluated) {
+ ASSERT_NO_THROW(GenerateMasterlist());
+ ASSERT_NO_THROW(handle_->GetDatabase().LoadLists(masterlistPath, ""));
+
+ auto messages = handle_->GetDatabase().GetGeneralMessages(true);
+
+ EXPECT_TRUE(messages.empty());
+}
+
+TEST_P(DatabaseInterfaceTest,
+ getPluginMetadataShouldReturnAnEmptyOptionalIfThePluginHasNoMetadata) {
+ EXPECT_FALSE(handle_->GetDatabase().GetPluginMetadata(blankEsm));
+}
+
+TEST_P(
+ DatabaseInterfaceTest,
+ getPluginMetadataShouldReturnMergedMasterAndUserMetadataForTheGivenPluginIfIncludeUserMetadataIsTrue) {
+ ASSERT_NO_THROW(GenerateMasterlist());
+ ASSERT_NO_THROW(GenerateUserlist());
+ ASSERT_NO_THROW(
+ handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_));
+
+ auto metadata =
+ handle_->GetDatabase().GetPluginMetadata(blankEsm, true).value();
+
+ std::vector expectedLoadAfter({
+ File(blankDifferentEsm),
+ File(masterFile),
+ });
+ EXPECT_EQ(expectedLoadAfter, metadata.GetLoadAfterFiles());
+}
+
+TEST_P(DatabaseInterfaceTest,
+ getPluginMetadataShouldPreferUserMetadataWhenMergingMetadata) {
+ ASSERT_NO_THROW(GenerateMasterlist());
+ ASSERT_NO_THROW(GenerateUserlist());
+ ASSERT_NO_THROW(
+ handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_));
+
+ auto metadata =
+ handle_->GetDatabase().GetPluginMetadata(blankEsm, true).value();
+
+ std::vector expectedTags({
+ Tag("Actors.ACBS", true, "file(\"" + missingEsp + "\")"),
+ Tag("Actors.ACBS"),
+ Tag("Actors.AIData"),
+ Tag("C.Water", false),
+ });
+ EXPECT_EQ(expectedTags, metadata.GetTags());
+ EXPECT_EQ("file(\"" + missingEsp + "\")",
+ metadata.GetTags()[0].GetCondition());
+}
+
+TEST_P(
+ DatabaseInterfaceTest,
+ getPluginMetadataShouldReturnOnlyMasterlistMetadataForTheGivenPluginIfIncludeUserMetadataIsFalse) {
+ ASSERT_NO_THROW(GenerateMasterlist());
+ ASSERT_NO_THROW(GenerateUserlist());
+ ASSERT_NO_THROW(
+ handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_));
+
+ auto metadata =
+ handle_->GetDatabase().GetPluginMetadata(blankEsm, false).value();
+
+ std::vector expectedLoadAfter({
+ File(masterFile),
+ });
+ EXPECT_EQ(expectedLoadAfter, metadata.GetLoadAfterFiles());
+}
+
+TEST_P(
+ DatabaseInterfaceTest,
+ getPluginMetadataShouldReturnOnlyValidMetadataForTheGivenPluginIfConditionsAreEvaluated) {
+ ASSERT_NO_THROW(GenerateMasterlist());
+ ASSERT_NO_THROW(handle_->GetDatabase().LoadLists(masterlistPath, ""));
+
+ auto metadata =
+ handle_->GetDatabase().GetPluginMetadata(blankEsm, false, true).value();
+
+ EXPECT_TRUE(metadata.GetMessages().empty());
+}
+
+TEST_P(
+ DatabaseInterfaceTest,
+ getPluginUserMetadataShouldReturnAnEmptyPluginMetadataObjectIfThePluginHasNoUserMetadata) {
+ ASSERT_NO_THROW(GenerateMasterlist());
+ ASSERT_NO_THROW(GenerateUserlist());
+ ASSERT_NO_THROW(
+ handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_));
+
+ EXPECT_FALSE(handle_->GetDatabase().GetPluginUserMetadata(blankDifferentEsm));
+}
+
+TEST_P(DatabaseInterfaceTest,
+ getPluginUserMetadataShouldReturnOnlyUserMetadataForTheGivenPlugin) {
+ ASSERT_NO_THROW(GenerateMasterlist());
+ ASSERT_NO_THROW(GenerateUserlist());
+ ASSERT_NO_THROW(
+ handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_));
+
+ auto metadata =
+ handle_->GetDatabase().GetPluginUserMetadata(blankEsm).value();
+
+ std::vector expectedLoadAfter({
+ File(blankDifferentEsm),
+ });
+ EXPECT_EQ(expectedLoadAfter, metadata.GetLoadAfterFiles());
+}
+
+TEST_P(
+ DatabaseInterfaceTest,
+ getPluginUserMetadataShouldReturnOnlyValidMetadataForTheGivenPluginIfConditionsAreEvaluated) {
+ ASSERT_NO_THROW(GenerateMasterlist());
+ ASSERT_NO_THROW(GenerateUserlist());
+ ASSERT_NO_THROW(
+ handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_));
+
+ auto metadata =
+ handle_->GetDatabase().GetPluginMetadata(blankEsm, false, true).value();
+
+ EXPECT_TRUE(metadata.GetMessages().empty());
+}
+
+TEST_P(
+ DatabaseInterfaceTest,
+ setPluginUserMetadataShouldReplaceExistingUserMetadataWithTheGivenMetadata) {
+ ASSERT_NO_THROW(GenerateMasterlist());
+ ASSERT_NO_THROW(GenerateUserlist());
+ ASSERT_NO_THROW(
+ handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_));
+
+ PluginMetadata newMetadata(blankDifferentEsp);
+ newMetadata.SetRequirements(std::vector({File(masterFile)}));
+
+ handle_->GetDatabase().SetPluginUserMetadata(newMetadata);
+
+ auto metadata =
+ handle_->GetDatabase().GetPluginUserMetadata(blankDifferentEsp).value();
+
+ std::set expectedLoadAfter({
+ File(blankDifferentEsm),
+ });
+ EXPECT_TRUE(metadata.GetIncompatibilities().empty());
+ EXPECT_EQ(newMetadata.GetRequirements(), metadata.GetRequirements());
+}
+
+TEST_P(DatabaseInterfaceTest,
+ setPluginUserMetadataShouldNotAffectExistingMasterlistMetadata) {
+ ASSERT_NO_THROW(GenerateMasterlist());
+ ASSERT_NO_THROW(GenerateUserlist());
+ ASSERT_NO_THROW(
+ handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_));
+
+ PluginMetadata newMetadata(blankEsm);
+ newMetadata.SetRequirements(std::vector({File(masterFile)}));
+
+ handle_->GetDatabase().SetPluginUserMetadata(newMetadata);
+
+ auto metadata = handle_->GetDatabase().GetPluginMetadata(blankEsm).value();
+
+ std::vector expectedLoadAfter({
+ File(masterFile),
+ });
+ EXPECT_EQ(expectedLoadAfter, metadata.GetLoadAfterFiles());
+}
+
+TEST_P(DatabaseInterfaceTest,
+ discardPluginUserMetadataShouldDiscardAllUserMetadataForTheGivenPlugin) {
+ ASSERT_NO_THROW(GenerateMasterlist());
+ ASSERT_NO_THROW(GenerateUserlist());
+ ASSERT_NO_THROW(
+ handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_));
+
+ handle_->GetDatabase().DiscardPluginUserMetadata(blankEsm);
+
+ EXPECT_FALSE(handle_->GetDatabase().GetPluginUserMetadata(blankEsm));
+}
+
+TEST_P(
+ DatabaseInterfaceTest,
+ discardPluginUserMetadataShouldNotDiscardMasterlistMetadataForTheGivenPlugin) {
+ ASSERT_NO_THROW(GenerateMasterlist());
+ ASSERT_NO_THROW(GenerateUserlist());
+ ASSERT_NO_THROW(
+ handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_));
+
+ handle_->GetDatabase().DiscardPluginUserMetadata(blankEsm);
+
+ auto metadata = handle_->GetDatabase().GetPluginMetadata(blankEsm).value();
+
+ std::vector expectedLoadAfter({
+ File(masterFile),
+ });
+ EXPECT_EQ(expectedLoadAfter, metadata.GetLoadAfterFiles());
+}
+
+TEST_P(DatabaseInterfaceTest,
+ discardPluginUserMetadataShouldNotDiscardUserMetadataForOtherPlugins) {
+ ASSERT_NO_THROW(GenerateMasterlist());
+ ASSERT_NO_THROW(GenerateUserlist());
+ ASSERT_NO_THROW(
+ handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_));
+
+ handle_->GetDatabase().DiscardPluginUserMetadata(blankEsm);
+
+ auto metadata =
+ handle_->GetDatabase().GetPluginUserMetadata(blankDifferentEsp);
+
+ EXPECT_TRUE(metadata);
+}
+
+TEST_P(DatabaseInterfaceTest,
+ discardPluginUserMetadataShouldNotDiscardGeneralMessages) {
+ ASSERT_NO_THROW(GenerateMasterlist());
+ ASSERT_NO_THROW(GenerateUserlist());
+ ASSERT_NO_THROW(
+ handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_));
+
+ handle_->GetDatabase().DiscardPluginUserMetadata(blankEsm);
+
+ auto messages = handle_->GetDatabase().GetGeneralMessages();
+
+ std::vector expectedMessages({
+ Message(MessageType::say,
+ generalMasterlistMessage,
+ "file(\"" + missingEsp + "\")"),
+ Message(MessageType::say, generalUserlistMessage),
+ });
+ EXPECT_EQ(expectedMessages, messages);
+}
+
+TEST_P(DatabaseInterfaceTest,
+ discardPluginUserMetadataShouldNotDiscardKnownBashTags) {
+ ASSERT_NO_THROW(GenerateMasterlist());
+ ASSERT_NO_THROW(GenerateUserlist());
+ ASSERT_NO_THROW(
+ handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_));
+
+ handle_->GetDatabase().DiscardPluginUserMetadata(blankEsm);
+
+ auto tags = handle_->GetDatabase().GetKnownBashTags();
+
+ std::vector expectedTags({
+ "Actors.ACBS",
+ "C.Climate",
+ "RaceRelations",
+ "C.Lighting",
+ });
+ EXPECT_EQ(expectedTags, tags);
+}
+
+TEST_P(
+ DatabaseInterfaceTest,
+ discardAllUserMetadataShouldDiscardAllUserMetadataAndNoMasterlistMetadata) {
+ ASSERT_NO_THROW(GenerateMasterlist());
+ ASSERT_NO_THROW(GenerateUserlist());
+ ASSERT_NO_THROW(
+ handle_->GetDatabase().LoadLists(masterlistPath, userlistPath_));
+
+ handle_->GetDatabase().DiscardAllUserMetadata();
+
+ EXPECT_FALSE(handle_->GetDatabase().GetPluginUserMetadata(blankEsm));
+ EXPECT_FALSE(handle_->GetDatabase().GetPluginUserMetadata(blankDifferentEsp));
+
+ auto metadata = handle_->GetDatabase().GetPluginMetadata(blankEsm).value();
+
+ std::vector expectedLoadAfter({
+ File(masterFile),
+ });
+ EXPECT_EQ(expectedLoadAfter, metadata.GetLoadAfterFiles());
+
+ auto messages = handle_->GetDatabase().GetGeneralMessages();
+
+ std::vector expectedMessages({
+ Message(MessageType::say,
+ generalMasterlistMessage,
+ "file(\"" + missingEsp + "\")"),
+ });
+ EXPECT_EQ(expectedMessages, messages);
+
+ auto tags = handle_->GetDatabase().GetKnownBashTags();
+
+ std::vector expectedTags({
+ "Actors.ACBS",
+ "C.Climate",
+ });
+ EXPECT_EQ(expectedTags, tags);
+}
+
+TEST_P(DatabaseInterfaceTest,
+ writeMinimalListShouldReturnOkAndWriteToFileIfArgumentsGivenAreValid) {
+ EXPECT_NO_THROW(
+ handle_->GetDatabase().WriteMinimalList(minimalOutputPath_, false));
+ EXPECT_TRUE(std::filesystem::exists(minimalOutputPath_));
+}
+
+TEST_P(
+ DatabaseInterfaceTest,
+ writeMinimalListShouldThrowIfTheFileAlreadyExistsAndTheOverwriteArgumentIsFalse) {
+ ASSERT_NO_THROW(
+ handle_->GetDatabase().WriteMinimalList(minimalOutputPath_, false));
+ ASSERT_TRUE(std::filesystem::exists(minimalOutputPath_));
+
+ EXPECT_THROW(
+ handle_->GetDatabase().WriteMinimalList(minimalOutputPath_, false),
+ FileAccessError);
+}
+
+TEST_P(
+ DatabaseInterfaceTest,
+ writeMinimalListShouldReturnOkAndWriteToFileIfTheArgumentsAreValidAndTheOverwriteArgumentIsTrue) {
+ EXPECT_NO_THROW(
+ handle_->GetDatabase().WriteMinimalList(minimalOutputPath_, true));
+ EXPECT_TRUE(std::filesystem::exists(minimalOutputPath_));
+}
+
+TEST_P(
+ DatabaseInterfaceTest,
+ writeMinimalListShouldReturnOkIfTheFileAlreadyExistsAndTheOverwriteArgumentIsTrue) {
+ ASSERT_NO_THROW(
+ handle_->GetDatabase().WriteMinimalList(minimalOutputPath_, false));
+ ASSERT_TRUE(std::filesystem::exists(minimalOutputPath_));
+
+ EXPECT_NO_THROW(
+ handle_->GetDatabase().WriteMinimalList(minimalOutputPath_, true));
+}
+
+TEST_P(DatabaseInterfaceTest,
+ writeMinimalListShouldThrowIfPathGivenExistsAndIsReadOnly) {
+ ASSERT_NO_THROW(
+ handle_->GetDatabase().WriteMinimalList(minimalOutputPath_, false));
+ ASSERT_TRUE(std::filesystem::exists(minimalOutputPath_));
+
+ std::filesystem::permissions(minimalOutputPath_,
+ std::filesystem::perms::owner_read,
+ std::filesystem::perm_options::replace);
+
+ EXPECT_THROW(
+ handle_->GetDatabase().WriteMinimalList(minimalOutputPath_, true),
+ FileAccessError);
+}
+
+TEST_P(DatabaseInterfaceTest,
+ writeMinimalListShouldWriteOnlyBashTagsAndDirtyInfo) {
+ using std::endl;
+
+ ASSERT_NO_THROW(GenerateMasterlist());
+ ASSERT_NO_THROW(handle_->GetDatabase().LoadLists(masterlistPath, ""));
+
+ EXPECT_NO_THROW(
+ handle_->GetDatabase().WriteMinimalList(minimalOutputPath_, true));
+
+ const auto content = GetFileContent(minimalOutputPath_);
+
+ // Plugin entries are unordered.
+ std::stringstream expectedContent;
+ if (content.find(blankDifferentEsm) < content.find(blankEsm)) {
+ expectedContent << "plugins:" << endl
+ << " - name: '" << blankDifferentEsm << "'" << endl
+ << " dirty:" << endl
+ << " - crc: 0x7D22F9DF" << endl
+ << " util: 'TES4Edit'" << endl
+ << " udr: 4" << endl
+ << " - name: '" << blankEsm << "'" << endl
+ << " tag:" << endl
+ << " - Actors.ACBS" << endl
+ << " - Actors.AIData" << endl
+ << " - -C.Water";
+ } else {
+ expectedContent << "plugins:" << endl
+ << " - name: '" << blankEsm << "'" << endl
+ << " tag:" << endl
+ << " - Actors.ACBS" << endl
+ << " - Actors.AIData" << endl
+ << " - -C.Water" << endl
+ << " - name: '" << blankDifferentEsm << "'" << endl
+ << " dirty:" << endl
+ << " - crc: 0x7D22F9DF" << endl
+ << " util: 'TES4Edit'" << endl
+ << " udr: 4";
+ }
+
+ EXPECT_EQ(expectedContent.str(), content);
+}
+}
+}
+
+#endif
diff --git a/cxx/src/tests/api/interface/game_interface_test.h b/cxx/src/tests/api/interface/game_interface_test.h
new file mode 100644
index 00000000..4545d3dd
--- /dev/null
+++ b/cxx/src/tests/api/interface/game_interface_test.h
@@ -0,0 +1,383 @@
+/* LOOT
+
+A load order optimisation tool for Oblivion, Skyrim, Fallout 3 and
+Fallout: New Vegas.
+
+Copyright (C) 2014-2016 WrinklyNinja
+
+This file is part of LOOT.
+
+LOOT is free software: you can redistribute
+it and/or modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, either version 3 of
+the License, or (at your option) any later version.
+
+LOOT is distributed in the hope that it will
+be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with LOOT. If not, see
+.
+*/
+
+#ifndef LOOT_TESTS_API_INTERFACE_GAME_INTERFACE_TEST
+#define LOOT_TESTS_API_INTERFACE_GAME_INTERFACE_TEST
+
+#include "loot/api.h"
+#include "tests/api/interface/api_game_operations_test.h"
+
+namespace loot {
+namespace test {
+class GameInterfaceTest : public ApiGameOperationsTest {
+protected:
+ GameInterfaceTest() :
+ emptyFile("EmptyFile.esm"), nonAsciiEsm(u8"non\u00C1scii.esm") {
+ // Make sure the plugin with a non-ASCII filename exists.
+ std::filesystem::copy_file(dataPath / blankEsm,
+ dataPath / std::filesystem::u8path(nonAsciiEsm));
+
+ if (GetParam() == GameType::starfield) {
+ pluginsToLoad = {
+ masterFile,
+ blankEsm,
+ blankFullEsm,
+ blankMasterDependentEsm,
+ blankEsp,
+ blankMasterDependentEsp,
+ };
+ } else {
+ pluginsToLoad = {
+ // These are all ASCII filenames.
+ masterFile,
+ blankEsm,
+ blankDifferentEsm,
+ blankMasterDependentEsm,
+ blankDifferentMasterDependentEsm,
+ blankEsp,
+ blankDifferentEsp,
+ blankMasterDependentEsp,
+ blankDifferentMasterDependentEsp,
+ blankPluginDependentEsp,
+ blankDifferentPluginDependentEsp,
+ };
+ }
+ }
+
+ const std::string emptyFile;
+ const std::string nonAsciiEsm;
+ std::vector pluginsToLoad;
+};
+
+// 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(,
+ GameInterfaceTest,
+ ::testing::Values(GameType::tes4,
+ GameType::tes5,
+ GameType::fo3,
+ GameType::fonv,
+ GameType::fo4,
+ GameType::tes5se,
+ GameType::fo4vr,
+ GameType::tes5vr,
+ GameType::tes3,
+ GameType::starfield,
+ GameType::openmw));
+
+TEST_P(GameInterfaceTest, setAdditionalDataPathsShouldDoThat) {
+ const auto paths = std::vector{
+ localPath, localPath.parent_path() / "other"};
+
+ handle_->SetAdditionalDataPaths(paths);
+
+ EXPECT_EQ(paths, handle_->GetAdditionalDataPaths());
+}
+
+TEST_P(GameInterfaceTest, isValidPluginShouldReturnTrueForAValidPlugin) {
+ EXPECT_TRUE(handle_->IsValidPlugin(blankEsm));
+}
+
+TEST_P(GameInterfaceTest,
+ isValidPluginShouldReturnTrueForAValidNonAsciiPlugin) {
+ EXPECT_TRUE(handle_->IsValidPlugin(std::filesystem::u8path(nonAsciiEsm)));
+}
+
+TEST_P(GameInterfaceTest, isValidPluginShouldReturnFalseForANonPluginFile) {
+ EXPECT_FALSE(handle_->IsValidPlugin(nonPluginFile));
+}
+
+TEST_P(GameInterfaceTest, isValidPluginShouldReturnFalseForAnEmptyFile) {
+ // Write out an empty file.
+ touch(dataPath / emptyFile);
+ ASSERT_TRUE(std::filesystem::exists(dataPath / emptyFile));
+
+ EXPECT_FALSE(handle_->IsValidPlugin(emptyFile));
+}
+
+TEST_P(
+ GameInterfaceTest,
+ loadPluginsWithHeadersOnlyTrueShouldLoadTheHeadersOfAllGivenPlugins) {
+ handle_->LoadPlugins(pluginsToLoad, true);
+ if (GetParam() == GameType::starfield) {
+ EXPECT_EQ(6, handle_->GetLoadedPlugins().size());
+ } else {
+ EXPECT_EQ(11, handle_->GetLoadedPlugins().size());
+ }
+
+ // Check that one plugin's header has been read.
+ ASSERT_NO_THROW(handle_->GetPlugin(masterFile));
+ auto plugin = handle_->GetPlugin(masterFile);
+ EXPECT_EQ("5.0", plugin->GetVersion().value());
+
+ // Check that only the header has been read.
+ EXPECT_FALSE(plugin->GetCRC());
+}
+
+TEST_P(GameInterfaceTest, loadPluginsShouldTrimDotGhostFileExtensions) {
+ if (GetParam() == GameType::openmw) {
+ // Ghosting is not supported for OpenMW.
+ EXPECT_THROW(
+ handle_->LoadPlugins({blankMasterDependentEsm + ".ghost"}, true),
+ std::invalid_argument);
+ return;
+ } else {
+ handle_->LoadPlugins({blankMasterDependentEsm + ".ghost"}, true);
+ }
+
+ EXPECT_EQ(1, handle_->GetLoadedPlugins().size());
+
+ ASSERT_NO_THROW(handle_->GetPlugin(blankMasterDependentEsm));
+ const auto plugin = handle_->GetPlugin(blankMasterDependentEsm);
+ ASSERT_NE(nullptr, plugin);
+ EXPECT_EQ(blankMasterDependentEsm, plugin->GetName());
+}
+
+TEST_P(GameInterfaceTest,
+ loadPluginsWithHeadersOnlyFalseShouldFullyLoadAllInstalledPlugins) {
+ handle_->LoadPlugins(pluginsToLoad, false);
+ if (GetParam() == GameType::starfield) {
+ EXPECT_EQ(6, handle_->GetLoadedPlugins().size());
+ } else {
+ EXPECT_EQ(11, handle_->GetLoadedPlugins().size());
+ }
+
+ // Check that one plugin's header has been read.
+ ASSERT_NO_THROW(handle_->GetPlugin(masterFile));
+ auto plugin = handle_->GetPlugin(masterFile);
+ EXPECT_EQ("5.0", plugin->GetVersion().value());
+
+ // Check that not only the header has been read.
+ EXPECT_EQ(blankEsmCrc, plugin->GetCRC().value());
+}
+
+TEST_P(GameInterfaceTest, loadPluginsWithANonAsciiPluginShouldLoadIt) {
+ handle_->LoadPlugins({std::filesystem::u8path(nonAsciiEsm)}, false);
+ EXPECT_EQ(1, handle_->GetLoadedPlugins().size());
+
+ // Check that one plugin's header has been read.
+ auto plugin = handle_->GetPlugin(nonAsciiEsm);
+ EXPECT_EQ("5.0", plugin->GetVersion().value());
+
+ // Check that not only the header has been read.
+ EXPECT_EQ(blankEsmCrc, plugin->GetCRC().value());
+}
+
+TEST_P(GameInterfaceTest, clearLoadedPluginsShouldClearThePluginsCache) {
+ handle_->LoadPlugins({std::filesystem::u8path(blankEsm)}, true);
+ const auto pointer = handle_->GetPlugin(blankEsm);
+ ASSERT_NE(nullptr, pointer);
+
+ handle_->ClearLoadedPlugins();
+
+ EXPECT_EQ(nullptr, handle_->GetPlugin(blankEsm));
+}
+
+TEST_P(GameInterfaceTest, getPluginThatIsNotCachedShouldReturnANullPointer) {
+ EXPECT_FALSE(handle_->GetPlugin(blankEsm));
+}
+
+TEST_P(GameInterfaceTest,
+ gettingPluginsShouldReturnAnEmptySetIfNoneHaveBeenLoaded) {
+ EXPECT_TRUE(handle_->GetLoadedPlugins().empty());
+}
+
+TEST_P(GameInterfaceTest, sortPluginsShouldSucceedIfPassedValidArguments) {
+ std::vector expectedOrder;
+ if (GetParam() == GameType::starfield) {
+ expectedOrder = {
+ masterFile,
+ blankEsm,
+ blankFullEsm,
+ blankMasterDependentEsm,
+ blankEsp,
+ blankMasterDependentEsp,
+ };
+ } else {
+ expectedOrder = {
+ masterFile,
+ blankEsm,
+ blankMasterDependentEsm,
+ blankDifferentEsm,
+ blankDifferentMasterDependentEsm,
+ blankMasterDependentEsp,
+ blankDifferentMasterDependentEsp,
+ blankEsp,
+ blankPluginDependentEsp,
+ blankDifferentEsp,
+ blankDifferentPluginDependentEsp,
+ };
+ }
+
+ if (GetParam() == GameType::fo4 || GetParam() == GameType::tes5se) {
+ expectedOrder.insert(expectedOrder.begin() + 5, blankEsl);
+ }
+
+ ASSERT_NO_THROW(GenerateMasterlist());
+ ASSERT_NO_THROW(handle_->GetDatabase().LoadLists(masterlistPath, ""));
+
+ if (GetParam() == GameType::fo4 || GetParam() == GameType::tes5se) {
+ pluginsToLoad.push_back(blankEsl);
+ }
+
+ handle_->LoadCurrentLoadOrderState();
+ handle_->LoadPlugins(pluginsToLoad, false);
+
+ std::vector pluginsToSort;
+ for (const auto& plugin : pluginsToLoad) {
+ pluginsToSort.push_back(plugin.filename().u8string());
+ }
+
+ std::vector actualOrder = handle_->SortPlugins(pluginsToSort);
+
+ EXPECT_EQ(expectedOrder, actualOrder);
+}
+
+TEST_P(GameInterfaceTest,
+ isPluginActiveShouldReturnFalseIfTheGivenPluginIsNotActive) {
+ handle_->LoadCurrentLoadOrderState();
+
+ EXPECT_TRUE(handle_->IsPluginActive(blankEsm));
+}
+
+TEST_P(GameInterfaceTest,
+ isPluginActiveShouldReturnTrueIfTheGivenPluginIsActive) {
+ handle_->LoadCurrentLoadOrderState();
+ EXPECT_FALSE(handle_->IsPluginActive(blankEsp));
+}
+
+TEST_P(GameInterfaceTest, getLoadOrderShouldReturnTheCurrentLoadOrder) {
+ // Remove the non-ASCII duplicate plugin.
+ std::filesystem::remove(dataPath / std::filesystem::u8path(nonAsciiEsm));
+
+ // Set no additional data paths to avoid picking up non-test plugins on PCs
+ // which have Starfield or Fallout 4 installed. Don't clear the additional
+ // data paths for OpenMW because they come from test config.
+ if (GetParam() != GameType::openmw) {
+ handle_->SetAdditionalDataPaths({});
+ }
+
+ handle_->LoadCurrentLoadOrderState();
+
+ if (GetParam() == GameType::openmw) {
+ ASSERT_EQ(std::vector({
+ blankDifferentEsm,
+ blankDifferentMasterDependentEsm,
+ blankDifferentEsp,
+ blankDifferentPluginDependentEsp,
+ blankMasterDependentEsm,
+ blankMasterDependentEsp,
+ blankEsp,
+ blankPluginDependentEsp,
+ masterFile,
+ blankEsm,
+ blankDifferentMasterDependentEsp,
+ }),
+ handle_->GetLoadOrder());
+ } else {
+ ASSERT_EQ(getLoadOrder(), handle_->GetLoadOrder());
+ }
+}
+
+TEST_P(GameInterfaceTest, setLoadOrderShouldSetTheLoadOrder) {
+ // Remove the non-ASCII duplicate plugin.
+ std::filesystem::remove(dataPath / std::filesystem::u8path(nonAsciiEsm));
+
+ // Set no additional data paths to avoid picking up non-test plugins on PCs
+ // which have Starfield or Fallout 4 installed. Don't clear the additional
+ // data paths for OpenMW because they come from test config.
+ if (GetParam() != GameType::openmw) {
+ handle_->SetAdditionalDataPaths({});
+ }
+
+ handle_->LoadCurrentLoadOrderState();
+
+ const auto gameSupportsEsl =
+ GetParam() == GameType::fo4 || GetParam() == GameType::fo4vr ||
+ GetParam() == GameType::tes5se || GetParam() == GameType::tes5vr ||
+ GetParam() == GameType::starfield;
+
+ std::vector loadOrder;
+ if (GetParam() == GameType::starfield) {
+ loadOrder = {
+ masterFile,
+ blankEsm,
+ blankMasterDependentEsm,
+ blankDifferentEsm,
+ blankDifferentEsp,
+ blankEsp,
+ blankMasterDependentEsp,
+ };
+ } else if (GetParam() == GameType::openmw) {
+ loadOrder = {
+ blankDifferentMasterDependentEsm,
+ blankDifferentPluginDependentEsp,
+ blankDifferentEsm,
+ blankDifferentEsp,
+ blankMasterDependentEsm,
+ blankMasterDependentEsp,
+ blankPluginDependentEsp,
+ blankEsp,
+ masterFile,
+ blankDifferentMasterDependentEsp,
+ blankEsm,
+ };
+ } else {
+ loadOrder = {
+ masterFile,
+ blankEsm,
+ blankMasterDependentEsm,
+ blankDifferentEsm,
+ blankDifferentMasterDependentEsm,
+ blankDifferentEsp,
+ blankDifferentPluginDependentEsp,
+ blankEsp,
+ blankMasterDependentEsp,
+ blankDifferentMasterDependentEsp,
+ blankPluginDependentEsp,
+ };
+
+ if (gameSupportsEsl) {
+ loadOrder.insert(loadOrder.begin() + 5, blankEsl);
+ }
+ }
+
+ EXPECT_NO_THROW(handle_->SetLoadOrder(loadOrder));
+
+ EXPECT_EQ(loadOrder, handle_->GetLoadOrder());
+
+ // It's not possible to persist the load order of inactive plugins for
+ // OpenMW.
+ if (GetParam() != GameType::openmw) {
+ if (gameSupportsEsl) {
+ loadOrder.erase(std::begin(loadOrder));
+ }
+
+ EXPECT_EQ(loadOrder, getLoadOrder());
+ }
+}
+}
+}
+
+#endif
diff --git a/cxx/src/tests/api/interface/is_compatible_test.h b/cxx/src/tests/api/interface/is_compatible_test.h
new file mode 100644
index 00000000..30a48411
--- /dev/null
+++ b/cxx/src/tests/api/interface/is_compatible_test.h
@@ -0,0 +1,61 @@
+/* LOOT
+
+A load order optimisation tool for Oblivion, Skyrim, Fallout 3 and
+Fallout: New Vegas.
+
+Copyright (C) 2014-2016 WrinklyNinja
+
+This file is part of LOOT.
+
+LOOT is free software: you can redistribute
+it and/or modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, either version 3 of
+the License, or (at your option) any later version.
+
+LOOT is distributed in the hope that it will
+be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with LOOT. If not, see
+.
+*/
+
+#ifndef LOOT_TESTS_API_INTERFACE_IS_COMPATIBLE_TEST
+#define LOOT_TESTS_API_INTERFACE_IS_COMPATIBLE_TEST
+
+#include
+
+#include "loot/api.h"
+
+namespace loot {
+namespace test {
+TEST(IsCompatible,
+ shouldReturnTrueWithEqualMajorAndMinorVersionsAndUnequalPatchVersion) {
+ EXPECT_TRUE(IsCompatible(
+ LIBLOOT_VERSION_MAJOR, LIBLOOT_VERSION_MINOR, LIBLOOT_VERSION_PATCH));
+}
+
+TEST(IsCompatible,
+ shouldReturnFalseWithEqualMajorVersionAndUnequalMinorAndPatchVersions) {
+ EXPECT_FALSE(IsCompatible(LIBLOOT_VERSION_MAJOR,
+ LIBLOOT_VERSION_MINOR + 1,
+ LIBLOOT_VERSION_PATCH + 1));
+}
+
+TEST(GetLiblootRevision, shouldReturnANonEmptyString) {
+ EXPECT_FALSE(GetLiblootRevision().empty());
+}
+
+TEST(GetVersion, shouldConcatenateMajorMinorAndPatchVersionNumbersWithPeriods) {
+ auto expectedVersion = std::to_string(LIBLOOT_VERSION_MAJOR) + "." +
+ std::to_string(LIBLOOT_VERSION_MINOR) + "." +
+ std::to_string(LIBLOOT_VERSION_PATCH);
+
+ EXPECT_EQ(expectedVersion, GetLiblootVersion());
+}
+}
+}
+
+#endif
diff --git a/cxx/src/tests/api/interface/main.cpp b/cxx/src/tests/api/interface/main.cpp
new file mode 100644
index 00000000..405173f4
--- /dev/null
+++ b/cxx/src/tests/api/interface/main.cpp
@@ -0,0 +1,126 @@
+/* LOOT
+
+ A load order optimisation tool for Oblivion, Skyrim, Fallout 3 and
+ Fallout: New Vegas.
+
+ Copyright (C) 2014-2016 WrinklyNinja
+
+ This file is part of LOOT.
+
+ LOOT is free software: you can redistribute
+ it and/or modify it under the terms of the GNU General Public License
+ as published by the Free Software Foundation, either version 3 of
+ the License, or (at your option) any later version.
+
+ LOOT is distributed in the hope that it will
+ be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with LOOT. If not, see
+ .
+ */
+
+#include
+
+#include "loot/api.h"
+#include "tests/api/interface/create_game_handle_test.h"
+#include "tests/api/interface/database_interface_test.h"
+#include "tests/api/interface/game_interface_test.h"
+#include "tests/api/interface/is_compatible_test.h"
+
+int main(int argc, char **argv) {
+ ::testing::InitGoogleTest(&argc, argv);
+ return RUN_ALL_TESTS();
+}
+
+namespace loot {
+namespace test {
+void testLoggingCallback(LogLevel, const char *) {
+ // Do nothing.
+}
+
+struct TestLogger {
+ void callback(LogLevel, const char *message) {
+ loggedMessages += std::string(message);
+ }
+
+ std::string loggedMessages;
+};
+
+TEST(SetLoggingCallback, shouldAcceptAFreeFunction) {
+ SetLoggingCallback(testLoggingCallback);
+
+ try {
+ CreateGameHandle(GameType::tes4, "dummy");
+ FAIL();
+ } catch (...) {
+ SetLoggingCallback([](LogLevel, const char *) {});
+ }
+}
+
+TEST(SetLoggingCallback, shouldAcceptAMemberFunction) {
+ TestLogger testLogger;
+ auto boundCallback = std::bind(&TestLogger::callback,
+ &testLogger,
+ std::placeholders::_1,
+ std::placeholders::_2);
+ SetLoggingCallback(boundCallback);
+
+ try {
+ CreateGameHandle(GameType::tes4, "dummy");
+ FAIL();
+ } catch (...) {
+ EXPECT_EQ(
+ "Attempting to create a game handle for game type \"The Elder Scrolls "
+ "IV: Oblivion\" with game path \"dummy\" and game local path \"\"",
+ testLogger.loggedMessages);
+
+ SetLoggingCallback([](LogLevel, const char *) {});
+ }
+}
+
+TEST(SetLoggingCallback, shouldAcceptALambdaFunction) {
+ std::string loggedMessages;
+ auto callback = [&](LogLevel, const char *string) {
+ loggedMessages += std::string(string);
+ };
+ SetLoggingCallback(callback);
+
+ try {
+ CreateGameHandle(GameType::tes4, "dummy");
+ FAIL();
+ } catch (...) {
+ EXPECT_EQ(
+ "Attempting to create a game handle for game type \"The Elder Scrolls "
+ "IV: Oblivion\" with game path \"dummy\" and game local path \"\"",
+ loggedMessages);
+
+ SetLoggingCallback([](LogLevel, const char *) {});
+ }
+}
+
+TEST(SetLoggingCallback,
+ shouldNotBreakLoggingIfPassedLambdaFunctionGoesOutOfScope) {
+ std::string loggedMessages;
+ {
+ SetLoggingCallback([&](LogLevel, const char *string) {
+ loggedMessages += std::string(string);
+ });
+ }
+
+ try {
+ CreateGameHandle(GameType::tes4, "dummy");
+ FAIL();
+ } catch (...) {
+ EXPECT_EQ(
+ "Attempting to create a game handle for game type \"The Elder Scrolls "
+ "IV: Oblivion\" with game path \"dummy\" and game local path \"\"",
+ loggedMessages);
+
+ SetLoggingCallback([](LogLevel, const char *) {});
+ }
+}
+}
+}
diff --git a/cxx/src/tests/common_game_test_fixture.h b/cxx/src/tests/common_game_test_fixture.h
new file mode 100644
index 00000000..8db9c037
--- /dev/null
+++ b/cxx/src/tests/common_game_test_fixture.h
@@ -0,0 +1,507 @@
+/* LOOT
+
+A load order optimisation tool for Oblivion, Skyrim, Fallout 3 and
+Fallout: New Vegas.
+
+Copyright (C) 2014-2016 WrinklyNinja
+
+This file is part of LOOT.
+
+LOOT is free software: you can redistribute
+it and/or modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation, either version 3 of
+the License, or (at your option) any later version.
+
+LOOT is distributed in the hope that it will
+be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with LOOT. If not, see
+.
+*/
+
+#ifndef LOOT_TESTS_COMMON_GAME_TEST_FIXTURE
+#define LOOT_TESTS_COMMON_GAME_TEST_FIXTURE
+
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include