From 6ea0c47f37d3fd4c092deeb32b56960b5659db12 Mon Sep 17 00:00:00 2001 From: Oliver Hamlet Date: Wed, 12 Mar 2025 17:25:33 +0000 Subject: [PATCH] Add libloot v0.25.3 public API tests --- .../api/interface/api_game_operations_test.h | 132 +++ .../api/interface/create_game_handle_test.h | 157 ++++ .../api/interface/database_interface_test.h | 818 ++++++++++++++++++ .../tests/api/interface/game_interface_test.h | 383 ++++++++ .../tests/api/interface/is_compatible_test.h | 61 ++ cxx/src/tests/api/interface/main.cpp | 126 +++ cxx/src/tests/common_game_test_fixture.h | 507 +++++++++++ cxx/src/tests/printers.h | 102 +++ cxx/src/tests/test_helpers.h | 84 ++ 9 files changed, 2370 insertions(+) create mode 100644 cxx/src/tests/api/interface/api_game_operations_test.h create mode 100644 cxx/src/tests/api/interface/create_game_handle_test.h create mode 100644 cxx/src/tests/api/interface/database_interface_test.h create mode 100644 cxx/src/tests/api/interface/game_interface_test.h create mode 100644 cxx/src/tests/api/interface/is_compatible_test.h create mode 100644 cxx/src/tests/api/interface/main.cpp create mode 100644 cxx/src/tests/common_game_test_fixture.h create mode 100644 cxx/src/tests/printers.h create mode 100644 cxx/src/tests/test_helpers.h 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 +#include + +#include "loot/enum/game_type.h" +#include "tests/test_helpers.h" + +namespace loot { +namespace test { +static const std::array ALL_GAME_TYPES = { + GameType::tes3, + GameType::tes4, + GameType::tes5, + GameType::tes5se, + GameType::tes5vr, + GameType::fo3, + GameType::fonv, + GameType::fo4, + GameType::fo4vr, + GameType::starfield, + GameType::openmw, +}; + +class CommonGameTestFixture : public ::testing::Test { +protected: + CommonGameTestFixture(GameType gameType) : + gameType_(gameType), + rootTestPath(getRootTestPath()), + french("fr"), + german("de"), + missingPath(rootTestPath / "missing"), + gamePath(rootTestPath / "games" / "game"), + dataPath(gamePath / getPluginsFolder()), + localPath(rootTestPath / "local" / "game"), + metadataFilesPath(rootTestPath / "metadata"), + masterFile(getMasterFile()), + missingEsp("Blank.missing.esp"), + nonPluginFile("NotAPlugin.esm"), + invalidPlugin("Invalid.esm"), + blankEsm("Blank.esm"), + blankFullEsm("Blank.full.esm"), + blankMediumEsm("Blank.medium.esm"), + blankDifferentEsm("Blank - Different.esm"), + blankMasterDependentEsm("Blank - Master Dependent.esm"), + blankDifferentMasterDependentEsm( + "Blank - Different Master Dependent.esm"), + blankEsl("Blank.esl"), + blankEsp("Blank.esp"), + blankDifferentEsp("Blank - Different.esp"), + blankMasterDependentEsp("Blank - Master Dependent.esp"), + blankDifferentMasterDependentEsp( + "Blank - Different Master Dependent.esp"), + blankPluginDependentEsp("Blank - Plugin Dependent.esp"), + blankDifferentPluginDependentEsp( + "Blank - Different Plugin Dependent.esp"), + blankEsmCrc(getBlankEsmCrc()) { + assertInitialState(); + } + + void assertInitialState() { + using std::filesystem::create_directories; + using std::filesystem::exists; + + create_directories(dataPath); + ASSERT_TRUE(exists(dataPath)); + + create_directories(localPath); + ASSERT_TRUE(exists(localPath)); + + create_directories(metadataFilesPath); + ASSERT_TRUE(exists(metadataFilesPath)); + + auto sourcePluginsPath = getSourcePluginsPath(); + + if (gameType_ == GameType::starfield) { + copyPlugin(sourcePluginsPath, blankFullEsm); + copyPlugin(sourcePluginsPath, blankMediumEsm); + + std::filesystem::copy_file(sourcePluginsPath / blankFullEsm, + dataPath / blankEsm); + ASSERT_TRUE(exists(dataPath / blankEsm)); + + std::filesystem::copy_file(sourcePluginsPath / blankFullEsm, + dataPath / blankDifferentEsm); + ASSERT_TRUE(exists(dataPath / blankDifferentEsm)); + + std::filesystem::copy_file( + sourcePluginsPath / "Blank - Override.full.esm", + dataPath / blankMasterDependentEsm); + ASSERT_TRUE(exists(dataPath / blankMasterDependentEsm)); + + std::filesystem::copy_file(sourcePluginsPath / "Blank.esp", + dataPath / blankEsp); + ASSERT_TRUE(exists(dataPath / blankEsp)); + + std::filesystem::copy_file(sourcePluginsPath / blankEsp, + dataPath / blankDifferentEsp); + ASSERT_TRUE(exists(dataPath / blankDifferentEsp)); + + std::filesystem::copy_file(sourcePluginsPath / "Blank - Override.esp", + dataPath / blankMasterDependentEsp); + ASSERT_TRUE(exists(dataPath / blankMasterDependentEsp)); + } else { + copyPlugin(sourcePluginsPath, blankEsm); + copyPlugin(sourcePluginsPath, blankDifferentEsm); + copyPlugin(sourcePluginsPath, blankMasterDependentEsm); + copyPlugin(sourcePluginsPath, blankDifferentMasterDependentEsm); + copyPlugin(sourcePluginsPath, blankEsp); + copyPlugin(sourcePluginsPath, blankDifferentEsp); + copyPlugin(sourcePluginsPath, blankMasterDependentEsp); + copyPlugin(sourcePluginsPath, blankDifferentMasterDependentEsp); + copyPlugin(sourcePluginsPath, blankPluginDependentEsp); + copyPlugin(sourcePluginsPath, blankDifferentPluginDependentEsp); + } + + if (supportsLightPlugins(gameType_)) { + if (gameType_ == GameType::starfield) { + std::filesystem::copy_file(sourcePluginsPath / "Blank.small.esm", + dataPath / blankEsl); + ASSERT_TRUE(exists(dataPath / blankEsl)); + } else { + copyPlugin(sourcePluginsPath, blankEsl); + } + } + + // Make sure the game master file exists. + ASSERT_NO_THROW( + std::filesystem::copy_file(dataPath / blankEsm, dataPath / masterFile)); + ASSERT_TRUE(exists(dataPath / masterFile)); + + // Set initial load order and active plugins. + setLoadOrder(getInitialLoadOrder()); + + // Ghost a plugin, except for OpenMW. + if (gameType_ != GameType::openmw) { + ASSERT_NO_THROW(std::filesystem::rename( + dataPath / blankMasterDependentEsm, + dataPath / (blankMasterDependentEsm + ".ghost"))); + ASSERT_FALSE(exists(dataPath / blankMasterDependentEsm)); + ASSERT_TRUE(exists(dataPath / (blankMasterDependentEsm + ".ghost"))); + } else { + touch(gamePath / "openmw.cfg"); + } + + // Write out an non-empty, non-plugin file. + std::ofstream out(dataPath / nonPluginFile); + out << "This isn't a valid plugin file."; + out.close(); + ASSERT_TRUE(exists(dataPath / nonPluginFile)); + + ASSERT_FALSE(exists(missingPath)); + ASSERT_FALSE(exists(dataPath / missingEsp)); + } + + void copyPlugin(const std::filesystem::path& sourceParentPath, + const std::string& filename) { + std::filesystem::copy_file(sourceParentPath / filename, + dataPath / filename); + ASSERT_TRUE(std::filesystem::exists(dataPath / filename)); + } + + void TearDown() override { + // Grant write permissions to everything in rootTestPath + // in case the test made anything read only. + for (const auto& path : + std::filesystem::recursive_directory_iterator(rootTestPath)) { + std::filesystem::permissions(path, std::filesystem::perms::all); + } + std::filesystem::remove_all(rootTestPath); + } + + std::vector readFileLines(const std::filesystem::path& file) { + std::ifstream in(file); + + std::vector lines; + while (in) { + std::string line; + std::getline(in, line); + + if (!line.empty()) { + lines.push_back(line); + } + } + + return lines; + } + + std::vector getLoadOrder() { + std::vector actual; + if (isLoadOrderTimestampBased(gameType_)) { + std::map loadOrder; + for (std::filesystem::directory_iterator it(dataPath); + it != std::filesystem::directory_iterator(); + ++it) { + if (std::filesystem::is_regular_file(it->status())) { + std::string filename = it->path().filename().u8string(); + if (filename == nonPluginFile) + continue; + if (boost::ends_with(filename, ".ghost")) + filename = it->path().stem().u8string(); + if (boost::ends_with(filename, ".esp") || + boost::ends_with(filename, ".esm")) + loadOrder.emplace(std::filesystem::last_write_time(it->path()), + filename); + } + } + for (const auto& plugin : loadOrder) actual.push_back(plugin.second); + } else if (gameType_ == GameType::tes5) { + std::ifstream in(localPath / "loadorder.txt"); + while (in) { + std::string line; + std::getline(in, line); + + if (!line.empty()) + actual.push_back(line); + } + } else if (gameType_ == GameType::openmw) { + throw std::runtime_error( + "OpenMW's load order derivation is too complicated to replicate " + "accurately just for a test."); + } else { + actual = readFileLines(localPath / "Plugins.txt"); + for (auto& line : actual) { + if (line[0] == '*') + line = line.substr(1); + } + } + + return actual; + } + + std::vector> getInitialLoadOrder() const { + std::vector> loadOrder; + + if (gameType_ == GameType::starfield) { + loadOrder = { + {masterFile, true}, + {blankEsm, true}, + {blankDifferentEsm, false}, + {blankFullEsm, false}, + {blankMasterDependentEsm, false}, + {blankMediumEsm, false}, + {blankEsl, false}, + {blankEsp, false}, + {blankDifferentEsp, false}, + {blankMasterDependentEsp, false}, + }; + } else { + loadOrder = { + {masterFile, true}, + {blankEsm, true}, + {blankDifferentEsm, false}, + {blankMasterDependentEsm, false}, + {blankDifferentMasterDependentEsm, false}, + {blankEsp, false}, + {blankDifferentEsp, false}, + {blankMasterDependentEsp, false}, + {blankDifferentMasterDependentEsp, true}, + {blankPluginDependentEsp, false}, + {blankDifferentPluginDependentEsp, false}, + }; + + if (supportsLightPlugins(gameType_)) { + loadOrder.insert(loadOrder.begin() + 5, + std::make_pair(blankEsl, false)); + } + } + + return loadOrder; + } + + std::filesystem::path getSourcePluginsPath() const { + return loot::test::getSourcePluginsPath(gameType_); + } + + void touch(const std::filesystem::path& path) { + std::filesystem::create_directories(path.parent_path()); + std::ofstream out(path); + out.close(); + } + + std::vector ReadFile(const std::filesystem::path& path) { + std::vector bytes; + std::ifstream in(path, std::ios::binary); + + std::copy(std::istreambuf_iterator(in), + std::istreambuf_iterator(), + std::back_inserter(bytes)); + + return bytes; + } + + void WriteFile(const std::filesystem::path& path, + const std::vector& content) { + std::ofstream out(path, std::ios::binary); + + out.write(content.data(), content.size()); + } + + std::vector GetInstalledPlugins() { + if (gameType_ == GameType::starfield) { + return { + masterFile, + blankEsm, + blankDifferentEsm, + blankFullEsm, + blankMasterDependentEsm, + blankMediumEsm, + blankEsl, + blankEsp, + blankDifferentEsp, + blankMasterDependentEsp, + }; + } else { + return { + masterFile, + blankEsm, + blankDifferentEsm, + blankMasterDependentEsm, + blankDifferentMasterDependentEsm, + blankEsp, + blankDifferentEsp, + blankMasterDependentEsp, + blankDifferentMasterDependentEsp, + blankPluginDependentEsp, + blankDifferentPluginDependentEsp, + }; + } + } + + void SetBlueprintFlag(const std::filesystem::path& path) { + auto bytes = ReadFile(path); + bytes[9] = 0x8; + WriteFile(path, bytes); + } + +private: + GameType gameType_; + const std::filesystem::path rootTestPath; + +protected: + const std::string french; + const std::string german; + + const std::filesystem::path missingPath; + const std::filesystem::path gamePath; + const std::filesystem::path dataPath; + const std::filesystem::path localPath; + const std::filesystem::path metadataFilesPath; + + const std::string masterFile; + const std::string missingEsp; + const std::string nonPluginFile; + const std::string invalidPlugin; + const std::string blankEsm; + const std::string blankFullEsm; + const std::string blankMediumEsm; + const std::string blankDifferentEsm; + const std::string blankMasterDependentEsm; + const std::string blankDifferentMasterDependentEsm; + const std::string blankEsl; + const std::string blankEsp; + const std::string blankDifferentEsp; + const std::string blankMasterDependentEsp; + const std::string blankDifferentMasterDependentEsp; + const std::string blankPluginDependentEsp; + const std::string blankDifferentPluginDependentEsp; + + const uint32_t blankEsmCrc; + +private: + std::string getMasterFile() const { + if (gameType_ == GameType::tes3 || gameType_ == GameType::openmw) + return "Morrowind.esm"; + else if (gameType_ == GameType::tes4) + return "Oblivion.esm"; + else if (gameType_ == GameType::tes5 || gameType_ == GameType::tes5se || + gameType_ == GameType::tes5vr) + return "Skyrim.esm"; + else if (gameType_ == GameType::fo3) + return "Fallout3.esm"; + else if (gameType_ == GameType::fonv) + return "FalloutNV.esm"; + else if (gameType_ == GameType::fo4 || gameType_ == GameType::fo4vr) + return "Fallout4.esm"; + else if (gameType_ == GameType::starfield) + return "Starfield.esm"; + else + throw std::logic_error("Unrecognised game type"); + } + + std::string getPluginsFolder() const { + if (gameType_ == GameType::openmw) { + return "resources/vfs"; + } else if (gameType_ == GameType::tes3) { + return "Data Files"; + } else { + return "Data"; + } + } + + uint32_t getBlankEsmCrc() const { + switch (gameType_) { + case GameType::tes3: + case GameType::openmw: + return 0x790DC6FB; + case GameType::tes4: + return 0x374E2A6F; + case GameType::starfield: + return 0xDE586309; + default: + return 0x6A1273DC; + } + } + + void setLoadOrder( + const std::vector>& loadOrder) const { + if (gameType_ == GameType::tes3) { + std::ofstream out(gamePath / "Morrowind.ini"); + for (const auto& plugin : loadOrder) { + if (plugin.second) { + out << "GameFile0=" << plugin.first << std::endl; + } + } + } else if (gameType_ == GameType::openmw) { + std::ofstream out(localPath / "openmw.cfg"); + + for (const auto& plugin : loadOrder) { + if (plugin.second) { + out << "content=" << plugin.first << std::endl; + } + } + } else { + std::ofstream out(localPath / "Plugins.txt"); + for (const auto& plugin : loadOrder) { + if (supportsLightPlugins(gameType_)) { + if (plugin.second) + out << '*'; + } else if (!plugin.second) + continue; + + out << plugin.first << std::endl; + } + } + + if (isLoadOrderTimestampBased(gameType_)) { + std::filesystem::file_time_type modificationTime = + std::filesystem::file_time_type::clock::now(); + for (const auto& plugin : loadOrder) { + if (std::filesystem::exists( + dataPath / std::filesystem::path(plugin.first + ".ghost"))) { + std::filesystem::last_write_time( + dataPath / std::filesystem::path(plugin.first + ".ghost"), + modificationTime); + } else { + std::filesystem::last_write_time(dataPath / plugin.first, + modificationTime); + } + modificationTime += std::chrono::seconds(60); + } + } else if (gameType_ == GameType::tes5) { + std::ofstream out(localPath / "loadorder.txt"); + for (const auto& plugin : loadOrder) out << plugin.first << std::endl; + } + } + + static bool isLoadOrderTimestampBased(GameType gameType) { + return gameType == GameType::tes3 || gameType == GameType::tes4 || + gameType == GameType::fo3 || gameType == GameType::fonv; + } +}; +} +} +#endif diff --git a/cxx/src/tests/printers.h b/cxx/src/tests/printers.h new file mode 100644 index 00000000..c55222e6 --- /dev/null +++ b/cxx/src/tests/printers.h @@ -0,0 +1,102 @@ +/* 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_PRINTERS +#define LOOT_TESTS_PRINTERS + +#include + +#include + +#include "api/metadata/message_content.h" +#include "api/plugin.h" +#include "loot/metadata/file.h" +#include "loot/metadata/location.h" +#include "loot/metadata/message.h" +#include "loot/metadata/plugin_cleaning_data.h" +#include "loot/metadata/plugin_metadata.h" +#include "loot/metadata/tag.h" + +namespace loot { +namespace test { +void PrintTo(const File& value, ::std::ostream* os) { + *os << "File(\"" << std::string(value.GetName()) << "\", " + << "\"" << value.GetDisplayName() << "\", " + << "\"" << value.GetCondition() << "\"" + << ")"; +} + +void PrintTo(const Location& value, ::std::ostream* os) { + *os << "Location(\"" << value.GetURL() << "\", " + << "\"" << value.GetName() << "\", " + << ")"; +} + +void PrintTo(const Message& value, ::std::ostream* os) { + std::string type; + if (value.GetType() == MessageType::warn) + type = "warn"; + else if (value.GetType() == MessageType::error) + type = "error"; + else + type = "say"; + + *os << "Message(\"" << type << "\", " + << ::testing::PrintToString(value.GetContent()) << ", " + << "\"" << value.GetCondition() << "\"" + << ")"; +} + +void PrintTo(const MessageContent& value, ::std::ostream* os) { + *os << "MessageContent(\"" << value.GetText() << "\", " + << "\"" << Language(value.GetLanguage()).GetName() << "\"" + << ")"; +} + +void PrintTo(const PluginCleaningData& value, ::std::ostream* os) { + *os << "PluginCleaningData(0x" << std::hex << std::uppercase << value.GetCRC() + << std::nouppercase << std::dec << ", " << value.GetITMCount() << ", " + << value.GetDeletedReferenceCount() << ", " + << value.GetDeletedNavmeshCount() << ", " + << "\"" << value.GetCleaningUtility() << "\"" + << ")"; +} + +void PrintTo(const PluginMetadata& value, ::std::ostream* os) { + *os << "PluginMetadata(\"" << value.GetName() << "\")"; +} + +void PrintTo(const Tag& value, ::std::ostream* os) { + *os << "Tag(\"" << value.GetName() << "\", " << value.IsAddition() << ", " + << "\"" << value.GetCondition() << "\"" + << ")"; +} + +void PrintTo(const Plugin& value, ::std::ostream* os) { + *os << "Plugin(\"" << value.GetName() << "\")"; +} +} +} + +#endif diff --git a/cxx/src/tests/test_helpers.h b/cxx/src/tests/test_helpers.h new file mode 100644 index 00000000..a40d23c0 --- /dev/null +++ b/cxx/src/tests/test_helpers.h @@ -0,0 +1,84 @@ +/* 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_TEST_HELPERS +#define LOOT_TESTS_TEST_HELPERS + +#include +#include +#include + +#include "loot/enum/game_type.h" + +namespace loot::test { +bool supportsLightPlugins(GameType gameType) { + return gameType == GameType::tes5se || gameType == GameType::tes5vr || + gameType == GameType::fo4 || gameType == GameType::fo4vr || + gameType == GameType::starfield; +} + +std::filesystem::path getSourcePluginsPath(GameType gameType) { + using std::filesystem::absolute; + if (gameType == GameType::tes3 || gameType == GameType::openmw) { + return absolute("./testing-plugins/Morrowind/Data Files"); + } else if (gameType == GameType::tes4) { + return absolute("./testing-plugins/Oblivion/Data"); + } else if (gameType == GameType::starfield) { + return absolute("./testing-plugins/Starfield/Data"); + } else if (supportsLightPlugins(gameType)) { + return absolute("./testing-plugins/SkyrimSE/Data"); + } else { + return absolute("./testing-plugins/Skyrim/Data"); + } +} + +std::filesystem::path getSourceArchivesPath(GameType gameType) { + if (gameType == GameType::fo4 || gameType == GameType::fo4vr || + gameType == GameType::starfield) { + return "./testing-plugins/Fallout 4/Data"; + } else { + return getSourcePluginsPath(gameType); + } +} + +std::filesystem::path getRootTestPath() { + std::random_device randomDevice; + std::default_random_engine prng(randomDevice()); + std::uniform_int_distribution dist(0x61, + 0x7A); // values of a-z in ASCII/UTF-8. + + // The non-ASCII character is there to ensure test coverage of non-ASCII path + // handling. + std::string directoryName = u8"libloot-t\u00E9st-"; + + for (int i = 0; i < 16; i += 1) { + directoryName.push_back(static_cast(dist(prng))); + } + + return std::filesystem::absolute(std::filesystem::temp_directory_path() / + std::filesystem::u8path(directoryName)); +} +} + +#endif