From 0c75629fff2b10c8c3dd28c0feffbc659ef7bac7 Mon Sep 17 00:00:00 2001 From: Oliver Hamlet Date: Thu, 1 May 2025 18:37:01 +0100 Subject: [PATCH] Move many GameTest tests to interface tests So that the GameTest tests are those that use internal APIs. --- .../api/interface/create_game_handle_test.h | 68 +++ src/tests/api/interface/game_interface_test.h | 343 ++++++++++++ src/tests/api/internals/game/game_test.h | 487 ------------------ 3 files changed, 411 insertions(+), 487 deletions(-) diff --git a/src/tests/api/interface/create_game_handle_test.h b/src/tests/api/interface/create_game_handle_test.h index 1534df20..ab5d5c0b 100644 --- a/src/tests/api/interface/create_game_handle_test.h +++ b/src/tests/api/interface/create_game_handle_test.h @@ -141,6 +141,74 @@ TEST_P(CreateGameHandleTest, EXPECT_TRUE(handle_); } #endif + +#ifndef _WIN32 +TEST_P( + CreateGameHandleTest, + shouldThrowOnLinuxIfLocalPathIsNotGivenExceptForMorrowindOpenMWAndOblivionRemastered) { + if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw || + GetParam() == GameType::oblivionRemastered) { + EXPECT_NO_THROW(CreateGameHandle(GetParam(), gamePath)); + } else { + EXPECT_THROW(CreateGameHandle(GetParam(), gamePath), std::system_error); + } +} +#else +TEST_P(CreateGameHandleTest, + shouldNotThrowOnWindowsIfLocalPathIsNotGiven) { + EXPECT_NO_THROW(CreateGameHandle(GetParam(), gamePath)); +} +#endif + +TEST_P(CreateGameHandleTest, + shouldNotThrowIfGameAndLocalPathsAreNotEmpty) { + EXPECT_NO_THROW(CreateGameHandle(GetParam(), gamePath, localPath)); +} + +TEST_P( + CreateGameHandleTest, + shouldSetAdditionalDataPathsForFallout4FromMicrosoftStoreOrStarfield) { + if (GetParam() == GameType::fo4) { + // Create the file that indicates it's a Microsoft Store install. + touch(gamePath / "appxmanifest.xml"); + } else if (GetParam() == GameType::openmw) { + std::ofstream out(gamePath / "openmw.cfg"); + out << "data-local=\"" << (localPath / "data").u8string() << "\"" + << std::endl + << "config=\"" << localPath.u8string() << "\""; + } + + const auto game = CreateGameHandle(GetParam(), gamePath, localPath); + + if (GetParam() == GameType::fo4) { + const auto basePath = gamePath / ".." / ".."; + EXPECT_EQ(std::vector( + {basePath / "Fallout 4- Automatron (PC)" / "Content" / "Data", + basePath / "Fallout 4- Nuka-World (PC)" / "Content" / "Data", + basePath / "Fallout 4- Wasteland Workshop (PC)" / "Content" / + "Data", + basePath / "Fallout 4- High Resolution Texture Pack" / + "Content" / "Data", + basePath / "Fallout 4- Vault-Tec Workshop (PC)" / "Content" / + "Data", + basePath / "Fallout 4- Far Harbor (PC)" / "Content" / "Data", + basePath / "Fallout 4- Contraptions Workshop (PC)" / + "Content" / "Data"}), + game->GetAdditionalDataPaths()); + } else if (GetParam() == GameType::starfield) { + ASSERT_EQ(1, game->GetAdditionalDataPaths().size()); + + const auto expectedSuffix = std::filesystem::u8path("Documents") / + "My Games" / "Starfield" / "Data"; + EXPECT_TRUE(boost::ends_with(game->GetAdditionalDataPaths()[0].u8string(), + expectedSuffix.u8string())); + } else if (GetParam() == GameType::openmw) { + EXPECT_EQ(std::vector{localPath / "data"}, + game->GetAdditionalDataPaths()); + } else { + EXPECT_TRUE(game->GetAdditionalDataPaths().empty()); + } +} } } diff --git a/src/tests/api/interface/game_interface_test.h b/src/tests/api/interface/game_interface_test.h index 32ae1dc9..ab5d7888 100644 --- a/src/tests/api/interface/game_interface_test.h +++ b/src/tests/api/interface/game_interface_test.h @@ -30,6 +30,8 @@ along with LOOT. If not, see namespace loot { namespace test { +constexpr unsigned int ESP_ERROR_PLUGIN_METADATA_NOT_FOUND = 14; + class GameInterfaceTest : public ApiGameOperationsTest { protected: GameInterfaceTest() : @@ -85,6 +87,56 @@ TEST_P(GameInterfaceTest, setAdditionalDataPathsShouldDoThat) { EXPECT_EQ(paths, handle_->GetAdditionalDataPaths()); } +TEST_P(GameInterfaceTest, setAdditionalDataPathsShouldClearTheConditionCache) { + PluginMetadata metadata(blankEsm); + metadata.SetLoadAfterFiles({File("plugin.esp", "", "file(\"plugin.esp\")")}); + handle_->GetDatabase().SetPluginUserMetadata(metadata); + + auto evaluatedMetadata = + handle_->GetDatabase().GetPluginUserMetadata(blankEsm, true).value(); + EXPECT_TRUE(evaluatedMetadata.GetLoadAfterFiles().empty()); + + const auto dataFilePath = gamePath.parent_path() / "Data" / "plugin.esp"; + touch(dataFilePath); + handle_->SetAdditionalDataPaths({dataFilePath.parent_path()}); + + evaluatedMetadata = + handle_->GetDatabase().GetPluginUserMetadata(blankEsm, true).value(); + EXPECT_FALSE(evaluatedMetadata.GetLoadAfterFiles().empty()); +} + +TEST_P(GameInterfaceTest, + setAdditionalDataPathsShouldUpdateWhereLoadOrderPluginsAreFound) { + // Set no additional data paths to avoid picking up non-test plugins on PCs + // which have Starfield or Fallout 4 installed. + handle_->SetAdditionalDataPaths({}); + handle_->LoadCurrentLoadOrderState(); + auto loadOrder = handle_->GetLoadOrder(); + + const auto filename = "plugin.esp"; + const auto dataFilePath = gamePath.parent_path() / "Data" / filename; + std::filesystem::create_directories(dataFilePath.parent_path()); + std::filesystem::copy_file(getSourcePluginsPath() / blankEsp, dataFilePath); + ASSERT_TRUE(std::filesystem::exists(dataFilePath)); + + if (GetParam() == GameType::starfield) { + std::filesystem::copy_file(getSourcePluginsPath() / blankEsp, + dataPath / filename); + ASSERT_TRUE(std::filesystem::exists(dataPath / filename)); + } + + std::filesystem::last_write_time( + dataFilePath, + std::filesystem::file_time_type::clock().now() + std::chrono::hours(1)); + + handle_->SetAdditionalDataPaths({dataFilePath.parent_path()}); + handle_->LoadCurrentLoadOrderState(); + + loadOrder.push_back(filename); + + EXPECT_EQ(loadOrder, handle_->GetLoadOrder()); +} + TEST_P(GameInterfaceTest, isValidPluginShouldReturnTrueForAValidPlugin) { EXPECT_TRUE(handle_->IsValidPlugin(blankEsm)); } @@ -106,6 +158,35 @@ TEST_P(GameInterfaceTest, isValidPluginShouldReturnFalseForAnEmptyFile) { EXPECT_FALSE(handle_->IsValidPlugin(emptyFile)); } +TEST_P(GameInterfaceTest, + isValidPluginShouldResolveRelativePathsRelativeToDataPath) { + const auto path = ".." / dataPath.filename() / blankEsm; + + EXPECT_TRUE(handle_->IsValidPlugin(path)); +} + +TEST_P(GameInterfaceTest, isValidPluginShouldUseAbsolutePathsAsGiven) { + ASSERT_TRUE(dataPath.is_absolute()); + + const auto path = dataPath / std::filesystem::u8path(blankEsm); + + EXPECT_TRUE(handle_->IsValidPlugin(path)); +} + +TEST_P( + GameInterfaceTest, + isValidPluginShouldTryGhostedPathIfGivenPluginDoesNotExistExceptForOpenMW) { + if (GetParam() == GameType::openmw) { + // This wasn't done for OpenMW during common setup. + const auto pluginPath = dataPath / (blankMasterDependentEsm + ".ghost"); + std::filesystem::rename(dataPath / blankMasterDependentEsm, pluginPath); + + EXPECT_FALSE(handle_->IsValidPlugin(blankMasterDependentEsm)); + } else { + EXPECT_TRUE(handle_->IsValidPlugin(blankMasterDependentEsm)); + } +} + TEST_P(GameInterfaceTest, loadPluginsWithHeadersOnlyTrueShouldLoadTheHeadersOfAllGivenPlugins) { handle_->LoadPlugins(pluginsToLoad, true); @@ -143,9 +224,29 @@ TEST_P(GameInterfaceTest, loadPluginsShouldTrimDotGhostFileExtensions) { EXPECT_EQ(blankMasterDependentEsm, plugin->GetName()); } +TEST_P(GameInterfaceTest, + loadPluginsWithHeadersOnlyTrueShouldLoadTheHeadersOfGivenPlugins) { + 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, loadPluginsWithHeadersOnlyFalseShouldFullyLoadAllInstalledPlugins) { handle_->LoadPlugins(pluginsToLoad, false); + if (GetParam() == GameType::starfield) { EXPECT_EQ(6, handle_->GetLoadedPlugins().size()); } else { @@ -173,6 +274,212 @@ TEST_P(GameInterfaceTest, loadPluginsWithANonAsciiPluginShouldLoadIt) { EXPECT_EQ(blankEsmCrc, plugin->GetCRC().value()); } +TEST_P( + GameInterfaceTest, + loadPluginsShouldNotThrowIfAFilenameHasNonWindows1252EncodableCharacters) { + const auto pluginName = std::filesystem::u8path( + u8"\u2551\u00BB\u00C1\u2510\u2557\u00FE\u00C3\u00CE.esp"); + std::filesystem::copy(dataPath / blankEsp, dataPath / pluginName); + + EXPECT_NO_THROW(handle_->LoadPlugins({pluginName}, false)); +} + +TEST_P(GameInterfaceTest, + loadPluginsWithANonPluginShouldNotAddItToTheLoadedPlugins) { + ASSERT_THROW(handle_->LoadPlugins({nonPluginFile}, false), + std::invalid_argument); + + ASSERT_TRUE(handle_->GetLoadedPlugins().empty()); +} + +TEST_P(GameInterfaceTest, + loadPluginsWithAnInvalidPluginShouldNotAddItToTheLoadedPlugins) { + ASSERT_FALSE(std::filesystem::exists(dataPath / invalidPlugin)); + ASSERT_NO_THROW(std::filesystem::copy_file(dataPath / blankEsm, + dataPath / invalidPlugin)); + ASSERT_TRUE(std::filesystem::exists(dataPath / invalidPlugin)); + std::ofstream out(dataPath / invalidPlugin, std::fstream::app); + out << "GRUP0"; + out.close(); + + ASSERT_NO_THROW(handle_->LoadPlugins({invalidPlugin}, false)); + + ASSERT_TRUE(handle_->GetLoadedPlugins().empty()); +} + +TEST_P(GameInterfaceTest, loadPluginsShouldNotClearThePluginsCache) { + handle_->LoadPlugins({std::filesystem::u8path(blankEsm)}, true); + const auto pointer = handle_->GetPlugin(blankEsm); + ASSERT_NE(nullptr, pointer); + + handle_->LoadPlugins({std::filesystem::u8path(blankEsp)}, true); + + EXPECT_EQ(pointer, handle_->GetPlugin(blankEsm)); +} + +TEST_P(GameInterfaceTest, + loadPluginsShouldReplaceCacheEntriesForTheGivenPlugins) { + handle_->LoadPlugins({std::filesystem::u8path(blankEsm)}, true); + const auto pointer = handle_->GetPlugin(blankEsm); + ASSERT_NE(nullptr, pointer); + + handle_->LoadPlugins({std::filesystem::u8path(blankEsm)}, false); + + const auto newPointer = handle_->GetPlugin(blankEsm); + ASSERT_NE(nullptr, newPointer); + EXPECT_NE(pointer, newPointer); +} + +TEST_P(GameInterfaceTest, + loadPluginsShouldThrowIfGivenVectorElementsWithTheSameFilename) { + const auto dataPluginPath = dataPath / std::filesystem::u8path(blankEsm); + const auto sourcePluginPath = + getSourcePluginsPath() / std::filesystem::u8path(blankEsm); + + EXPECT_THROW(handle_->LoadPlugins(std::vector( + {dataPluginPath, sourcePluginPath}), + true), + std::invalid_argument); +} + +TEST_P(GameInterfaceTest, + loadPluginsShouldResolveRelativePathsRelativeToDataPath) { + const auto relativePath = ".." / dataPath.filename() / blankEsm; + + handle_->LoadPlugins(std::vector({relativePath}), + true); + + EXPECT_NE(nullptr, handle_->GetPlugin(blankEsm)); +} + +TEST_P(GameInterfaceTest, loadPluginsShouldUseAbsolutePathsAsGiven) { + const auto absolutePath = dataPath / std::filesystem::u8path(blankEsm); + + handle_->LoadPlugins(std::vector({absolutePath}), + true); + + EXPECT_NE(nullptr, handle_->GetPlugin(blankEsm)); +} + +TEST_P( + GameInterfaceTest, + loadPluginsShouldThrowIfFullyLoadingAPluginWithAMissingMasterIfGameIsMorrowindOrStarfield) { + const auto pluginName = + GetParam() == GameType::starfield ? blankFullEsm : blankEsm; + + std::filesystem::remove(dataPath / pluginName); + + if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw || + GetParam() == GameType::starfield) { + try { + handle_->LoadPlugins({blankMasterDependentEsm}, false); + FAIL(); + } catch (const std::system_error& e) { + EXPECT_EQ(ESP_ERROR_PLUGIN_METADATA_NOT_FOUND, e.code().value()); + EXPECT_EQ(esplugin_category(), e.code().category()); + } + } else { + handle_->LoadPlugins({blankMasterDependentEsm}, false); + + EXPECT_NE(nullptr, handle_->GetPlugin(blankMasterDependentEsm)); + } +} + +TEST_P( + GameInterfaceTest, + loadPluginsShouldThrowIfAPluginHasAMasterThatIsNotInTheInputAndIsNotAlreadyLoadedAndGameIsMorrowindOrStarfield) { + if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw || + GetParam() == GameType::starfield) { + try { + handle_->LoadPlugins({blankMasterDependentEsm}, false); + FAIL(); + } catch (const std::system_error& e) { + EXPECT_EQ(ESP_ERROR_PLUGIN_METADATA_NOT_FOUND, e.code().value()); + EXPECT_EQ(esplugin_category(), e.code().category()); + } + } else { + handle_->LoadPlugins({blankMasterDependentEsm}, false); + + EXPECT_NE(nullptr, handle_->GetPlugin(blankMasterDependentEsm)); + } +} + +TEST_P( + GameInterfaceTest, + loadPluginsShouldNotThrowIfAPluginHasAMasterThatIsNotInTheInputButIsAlreadyLoaded) { + const auto pluginName = + GetParam() == GameType::starfield ? blankFullEsm : blankEsm; + + handle_->LoadPlugins({pluginName}, true); + + handle_->LoadPlugins({blankMasterDependentEsm}, false); + + EXPECT_NE(nullptr, handle_->GetPlugin(blankMasterDependentEsm)); +} + +TEST_P(GameInterfaceTest, + sortPluginsWithNoLoadedPluginsShouldReturnAnEmptyList) { + const auto sorted = handle_->SortPlugins(handle_->GetLoadOrder()); + + EXPECT_TRUE(sorted.empty()); +} + +TEST_P(GameInterfaceTest, sortPluginsShouldOnlySortTheGivenPlugins) { + handle_->LoadPlugins(GetInstalledPlugins(), false); + + std::vector plugins{blankEsp, blankDifferentEsp}; + const auto sorted = handle_->SortPlugins(plugins); + + EXPECT_EQ(plugins, sorted); +} + +TEST_P(GameInterfaceTest, + sortingShouldNotMakeUnnecessaryChangesToAnExistingLoadOrder) { + std::filesystem::remove(dataPath / std::filesystem::u8path(nonAsciiEsm)); + + handle_->LoadCurrentLoadOrderState(); + + auto plugins = GetInstalledPlugins(); + handle_->LoadPlugins({plugins.front()}, true); + plugins.erase(plugins.begin()); + handle_->LoadPlugins(plugins, false); + + std::vector expectedSortedOrder; + if (GetParam() == GameType::openmw) { + // The existing load order for OpenMW doesn't have plugins loading after + // their masters, because the game doesn't enforce that, and the test + // setup cannot enforce the positions of inactive plugins. + expectedSortedOrder = { + blankDifferentEsm, + blankDifferentMasterDependentEsm, + blankDifferentEsp, + blankDifferentPluginDependentEsp, + blankEsm, + blankMasterDependentEsm, + blankMasterDependentEsp, + blankEsp, + blankPluginDependentEsp, + masterFile, + blankDifferentMasterDependentEsp, + }; + } else { + expectedSortedOrder = getLoadOrder(); + } + + // Check stability by running the sort 100 times. + for (int i = 0; i < 100; i++) { + auto input = handle_->GetLoadOrder(); + auto sorted = handle_->SortPlugins(handle_->GetLoadOrder()); + ASSERT_EQ(expectedSortedOrder, sorted) << " for sort " << i; + } +} + +TEST_P(GameInterfaceTest, sortPluginsShouldThrowIfAGivenPluginIsNotLoaded) { + std::vector plugins{blankEsp, blankDifferentEsp}; + + EXPECT_THROW(handle_->SortPlugins(plugins), std::invalid_argument); +} + TEST_P(GameInterfaceTest, clearLoadedPluginsShouldClearThePluginsCache) { handle_->LoadPlugins({std::filesystem::u8path(blankEsm)}, true); const auto pointer = handle_->GetPlugin(blankEsm); @@ -256,6 +563,42 @@ TEST_P(GameInterfaceTest, EXPECT_FALSE(handle_->IsPluginActive(blankEsp)); } +TEST_P(GameInterfaceTest, + isPluginActiveShouldActivePluginAsActiveWithHeaderLoaded) { + handle_->LoadCurrentLoadOrderState(); + + ASSERT_NO_THROW(handle_->LoadPlugins({blankEsm}, true)); + + EXPECT_TRUE(handle_->IsPluginActive(blankEsm)); +} + +TEST_P(GameInterfaceTest, + isPluginActiveShouldInactivePluginAsInactiveWithHeaderLoaded) { + handle_->LoadCurrentLoadOrderState(); + + ASSERT_NO_THROW(handle_->LoadPlugins({blankEsp}, true)); + + EXPECT_FALSE(handle_->IsPluginActive(blankEsp)); +} + +TEST_P(GameInterfaceTest, + isPluginActiveShouldActivePluginAsActiveWhenFullyLoaded) { + handle_->LoadCurrentLoadOrderState(); + + ASSERT_NO_THROW(handle_->LoadPlugins({blankEsm}, false)); + + EXPECT_TRUE(handle_->IsPluginActive(blankEsm)); +} + +TEST_P(GameInterfaceTest, + isPluginActiveShouldInactivePluginAsInactiveWhenFullyLoaded) { + handle_->LoadCurrentLoadOrderState(); + + ASSERT_NO_THROW(handle_->LoadPlugins({blankEsp}, false)); + + EXPECT_FALSE(handle_->IsPluginActive(blankEsp)); +} + TEST_P(GameInterfaceTest, getLoadOrderShouldReturnTheCurrentLoadOrder) { // Remove the non-ASCII duplicate plugin. std::filesystem::remove(dataPath / std::filesystem::u8path(nonAsciiEsm)); diff --git a/src/tests/api/internals/game/game_test.h b/src/tests/api/internals/game/game_test.h index 2f1946df..6a3671e6 100644 --- a/src/tests/api/internals/game/game_test.h +++ b/src/tests/api/internals/game/game_test.h @@ -59,259 +59,6 @@ TEST_P(GameTest, constructingShouldStoreTheGivenValues) { EXPECT_EQ(dataPath, game.DataPath()); } -#ifndef _WIN32 -TEST_P( - GameTest, - constructingShouldThrowOnLinuxIfLocalPathIsNotGivenExceptForMorrowindOpenMWAndOblivionRemastered) { - if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw || - GetParam() == GameType::oblivionRemastered) { - EXPECT_NO_THROW(Game(GetParam(), gamePath)); - } else { - EXPECT_THROW(Game(GetParam(), gamePath), std::system_error); - } -} -#else -TEST_P(GameTest, constructingShouldNotThrowOnWindowsIfLocalPathIsNotGiven) { - EXPECT_NO_THROW(Game(GetParam(), gamePath)); -} -#endif - -TEST_P(GameTest, constructingShouldNotThrowIfGameAndLocalPathsAreNotEmpty) { - EXPECT_NO_THROW(Game(GetParam(), gamePath, localPath)); -} - -TEST_P( - GameTest, - constructingForFallout4FromMicrosoftStoreOrStarfieldShouldSetAdditionalDataPaths) { - if (GetParam() == GameType::fo4) { - // Create the file that indicates it's a Microsoft Store install. - touch(gamePath / "appxmanifest.xml"); - } else if (GetParam() == GameType::openmw) { - std::ofstream out(gamePath / "openmw.cfg"); - out << "data-local=\"" << (localPath / "data").u8string() << "\"" - << std::endl - << "config=\"" << localPath.u8string() << "\""; - } - - Game game = Game(GetParam(), gamePath, localPath); - - if (GetParam() == GameType::fo4) { - const auto basePath = gamePath / ".." / ".."; - EXPECT_EQ(std::vector( - {basePath / "Fallout 4- Automatron (PC)" / "Content" / "Data", - basePath / "Fallout 4- Nuka-World (PC)" / "Content" / "Data", - basePath / "Fallout 4- Wasteland Workshop (PC)" / "Content" / - "Data", - basePath / "Fallout 4- High Resolution Texture Pack" / - "Content" / "Data", - basePath / "Fallout 4- Vault-Tec Workshop (PC)" / "Content" / - "Data", - basePath / "Fallout 4- Far Harbor (PC)" / "Content" / "Data", - basePath / "Fallout 4- Contraptions Workshop (PC)" / - "Content" / "Data"}), - game.GetAdditionalDataPaths()); - } else if (GetParam() == GameType::starfield) { - ASSERT_EQ(1, game.GetAdditionalDataPaths().size()); - - const auto expectedSuffix = std::filesystem::u8path("Documents") / - "My Games" / "Starfield" / "Data"; - EXPECT_TRUE(boost::ends_with(game.GetAdditionalDataPaths()[0].u8string(), - expectedSuffix.u8string())); - } else if (GetParam() == GameType::openmw) { - EXPECT_EQ(std::vector{localPath / "data"}, - game.GetAdditionalDataPaths()); - } else { - EXPECT_TRUE(game.GetAdditionalDataPaths().empty()); - } -} - -TEST_P(GameTest, setAdditionalDataPathsShouldClearTheConditionCache) { - Game game = Game(GetParam(), gamePath, localPath); - - PluginMetadata metadata(blankEsm); - metadata.SetLoadAfterFiles({File("plugin.esp", "", "file(\"plugin.esp\")")}); - game.GetDatabase().SetPluginUserMetadata(metadata); - - auto evaluatedMetadata = - game.GetDatabase().GetPluginUserMetadata(blankEsm, true).value(); - EXPECT_TRUE(evaluatedMetadata.GetLoadAfterFiles().empty()); - - const auto dataFilePath = gamePath.parent_path() / "Data" / "plugin.esp"; - touch(dataFilePath); - game.SetAdditionalDataPaths({dataFilePath.parent_path()}); - - evaluatedMetadata = - game.GetDatabase().GetPluginUserMetadata(blankEsm, true).value(); - EXPECT_FALSE(evaluatedMetadata.GetLoadAfterFiles().empty()); -} - -TEST_P(GameTest, - setAdditionalDataPathsShouldUpdateWhereLoadOrderPluginsAreFound) { - Game game = Game(GetParam(), gamePath, localPath); - - // Set no additional data paths to avoid picking up non-test plugins on PCs - // which have Starfield or Fallout 4 installed. - game.SetAdditionalDataPaths({}); - game.LoadCurrentLoadOrderState(); - auto loadOrder = game.GetLoadOrder(); - - const auto filename = "plugin.esp"; - const auto dataFilePath = gamePath.parent_path() / "Data" / filename; - std::filesystem::create_directories(dataFilePath.parent_path()); - std::filesystem::copy_file(getSourcePluginsPath() / blankEsp, dataFilePath); - ASSERT_TRUE(std::filesystem::exists(dataFilePath)); - - if (GetParam() == GameType::starfield) { - std::filesystem::copy_file(getSourcePluginsPath() / blankEsp, - dataPath / filename); - ASSERT_TRUE(std::filesystem::exists(dataPath / filename)); - } - - std::filesystem::last_write_time( - dataFilePath, - std::filesystem::file_time_type::clock().now() + std::chrono::hours(1)); - - game.SetAdditionalDataPaths({dataFilePath.parent_path()}); - game.LoadCurrentLoadOrderState(); - - loadOrder.push_back(filename); - - EXPECT_EQ(loadOrder, game.GetLoadOrder()); -} - -TEST_P(GameTest, isValidPluginShouldResolveRelativePathsRelativeToDataPath) { - const Game game(GetParam(), gamePath, localPath); - - const auto path = ".." / dataPath.filename() / blankEsm; - - EXPECT_TRUE(game.IsValidPlugin(path)); -} - -TEST_P(GameTest, isValidPluginShouldUseAbsolutePathsAsGiven) { - const Game game(GetParam(), gamePath, localPath); - - ASSERT_TRUE(dataPath.is_absolute()); - - const auto path = dataPath / std::filesystem::u8path(blankEsm); - - EXPECT_TRUE(game.IsValidPlugin(path)); -} - -TEST_P( - GameTest, - isValidPluginShouldTryGhostedPathIfGivenPluginDoesNotExistExceptForOpenMW) { - const Game game(GetParam(), gamePath, localPath); - - if (GetParam() == GameType::openmw) { - // This wasn't done for OpenMW during common setup. - const auto pluginPath = - game.DataPath() / (blankMasterDependentEsm + ".ghost"); - std::filesystem::rename(dataPath / blankMasterDependentEsm, pluginPath); - - EXPECT_FALSE(game.IsValidPlugin(blankMasterDependentEsm)); - } else { - EXPECT_TRUE(game.IsValidPlugin(blankMasterDependentEsm)); - } -} - -TEST_P(GameTest, - loadPluginsWithHeadersOnlyTrueShouldLoadTheHeadersOfGivenPlugins) { - Game game = Game(GetParam(), gamePath, localPath); - - EXPECT_NO_THROW(loadInstalledPlugins(game, true)); - if (GetParam() == GameType::starfield) { - EXPECT_EQ(10, game.GetCache().GetPlugins().size()); - } else if (supportsLightPlugins(GetParam())) { - EXPECT_EQ(12, game.GetCache().GetPlugins().size()); - } else { - EXPECT_EQ(11, game.GetCache().GetPlugins().size()); - } - - // Check that one plugin's header has been read. - ASSERT_NO_THROW(game.GetPlugin(masterFile)); - auto plugin = game.GetPlugin(masterFile); - EXPECT_EQ("5.0", plugin->GetVersion().value()); - - // Check that only the header has been read. - EXPECT_FALSE(plugin->GetCRC()); -} - -TEST_P(GameTest, loadPluginsWithANonPluginShouldNotAddItToTheLoadedPlugins) { - Game game = Game(GetParam(), gamePath, localPath); - - ASSERT_THROW(game.LoadPlugins( - std::vector({nonPluginFile}), false), - std::invalid_argument); - - ASSERT_TRUE(game.GetLoadedPlugins().empty()); -} - -TEST_P(GameTest, - loadPluginsWithAnInvalidPluginShouldNotAddItToTheLoadedPlugins) { - ASSERT_FALSE(std::filesystem::exists(dataPath / invalidPlugin)); - ASSERT_NO_THROW(std::filesystem::copy_file(dataPath / blankEsm, - dataPath / invalidPlugin)); - ASSERT_TRUE(std::filesystem::exists(dataPath / invalidPlugin)); - std::ofstream out(dataPath / invalidPlugin, std::fstream::app); - out << "GRUP0"; - out.close(); - - Game game = Game(GetParam(), gamePath, localPath); - - ASSERT_NO_THROW(game.LoadPlugins( - std::vector({invalidPlugin}), false)); - - ASSERT_TRUE(game.GetLoadedPlugins().empty()); -} - -TEST_P(GameTest, - loadPluginsWithHeadersOnlyFalseShouldFullyLoadAllGivenPlugins) { - Game game = Game(GetParam(), gamePath, localPath); - - EXPECT_NO_THROW(loadInstalledPlugins(game, false)); - if (GetParam() == GameType::starfield) { - EXPECT_EQ(10, game.GetCache().GetPlugins().size()); - } else if (supportsLightPlugins(GetParam())) { - EXPECT_EQ(12, game.GetCache().GetPlugins().size()); - } else { - EXPECT_EQ(11, game.GetCache().GetPlugins().size()); - } - - // Check that one plugin's header has been read. - ASSERT_NO_THROW(game.GetPlugin(blankEsm)); - auto plugin = game.GetPlugin(blankEsm); - EXPECT_EQ("5.0", plugin->GetVersion().value()); - - // Check that not only the header has been read. - EXPECT_EQ(blankEsmCrc, plugin->GetCRC().value()); -} - -TEST_P(GameTest, loadPluginsShouldNotClearThePluginsCache) { - Game game = Game(GetParam(), gamePath, localPath); - - game.LoadPlugins({std::filesystem::u8path(blankEsm)}, true); - const auto pointer = game.GetPlugin(blankEsm); - ASSERT_NE(nullptr, pointer); - - game.LoadPlugins({std::filesystem::u8path(blankEsp)}, true); - - EXPECT_EQ(pointer, game.GetPlugin(blankEsm)); -} - -TEST_P(GameTest, loadPluginsShouldReplaceCacheEntriesForTheGivenPlugins) { - Game game = Game(GetParam(), gamePath, localPath); - - game.LoadPlugins({std::filesystem::u8path(blankEsm)}, true); - const auto pointer = game.GetPlugin(blankEsm); - ASSERT_NE(nullptr, pointer); - - game.LoadPlugins({std::filesystem::u8path(blankEsm)}, false); - - const auto newPointer = game.GetPlugin(blankEsm); - ASSERT_NE(nullptr, newPointer); - EXPECT_NE(pointer, newPointer); -} - TEST_P( GameTest, loadPluginsShouldFindAndCacheArchivesForLoadDetectionWhenLoadingPlugins) { @@ -362,240 +109,6 @@ TEST_P(GameTest, loadPluginsShouldClearTheArchivesCacheBeforeFindingArchives) { EXPECT_NO_THROW(loadInstalledPlugins(game, false)); EXPECT_EQ(1, game.GetCache().GetArchivePaths().size()); } - -TEST_P( - GameTest, - loadPluginsShouldNotThrowIfAFilenameHasNonWindows1252EncodableCharacters) { - touch(dataPath / - std::filesystem::u8path( - u8"\u2551\u00BB\u00C1\u2510\u2557\u00FE\u00C3\u00CE.txt")); - - Game game = Game(GetParam(), gamePath, localPath); - - EXPECT_NO_THROW(loadInstalledPlugins(game, false)); -} - -TEST_P(GameTest, - loadPluginsShouldThrowIfGivenVectorElementsWithTheSameFilename) { - Game game = Game(GetParam(), gamePath, localPath); - - const auto dataPluginPath = dataPath / std::filesystem::u8path(blankEsm); - const auto sourcePluginPath = - getSourcePluginsPath() / std::filesystem::u8path(blankEsm); - - EXPECT_THROW(game.LoadPlugins(std::vector( - {dataPluginPath, sourcePluginPath}), - true), - std::invalid_argument); -} - -TEST_P(GameTest, loadPluginsShouldResolveRelativePathsRelativeToDataPath) { - Game game = Game(GetParam(), gamePath, localPath); - - const auto relativePath = ".." / dataPath.filename() / blankEsm; - - game.LoadPlugins(std::vector({relativePath}), true); - - EXPECT_NE(nullptr, game.GetPlugin(blankEsm)); -} - -TEST_P(GameTest, loadPluginsShouldUseAbsolutePathsAsGiven) { - Game game = Game(GetParam(), gamePath, localPath); - - const auto absolutePath = dataPath / std::filesystem::u8path(blankEsm); - - game.LoadPlugins(std::vector({absolutePath}), true); - - EXPECT_NE(nullptr, game.GetPlugin(blankEsm)); -} - -TEST_P( - GameTest, - loadPluginsShouldThrowIfFullyLoadingAPluginWithAMissingMasterIfGameIsMorrowindOrStarfield) { - Game game = Game(GetParam(), gamePath, localPath); - - const auto pluginName = - GetParam() == GameType::starfield ? blankFullEsm : blankEsm; - - std::filesystem::remove(dataPath / pluginName); - - if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw || - GetParam() == GameType::starfield) { - try { - game.LoadPlugins({blankMasterDependentEsm}, false); - FAIL(); - } catch (const std::system_error& e) { - EXPECT_EQ(ESP_ERROR_PLUGIN_METADATA_NOT_FOUND, e.code().value()); - EXPECT_EQ(esplugin_category(), e.code().category()); - } - } else { - game.LoadPlugins({blankMasterDependentEsm}, false); - - EXPECT_NE(nullptr, game.GetPlugin(blankMasterDependentEsm)); - } -} - -TEST_P( - GameTest, - loadPluginsShouldThrowIfAPluginHasAMasterThatIsNotInTheInputAndIsNotAlreadyLoadedAndGameIsMorrowindOrStarfield) { - Game game = Game(GetParam(), gamePath, localPath); - - if (GetParam() == GameType::tes3 || GetParam() == GameType::openmw || - GetParam() == GameType::starfield) { - try { - game.LoadPlugins({blankMasterDependentEsm}, false); - FAIL(); - } catch (const std::system_error& e) { - EXPECT_EQ(ESP_ERROR_PLUGIN_METADATA_NOT_FOUND, e.code().value()); - EXPECT_EQ(esplugin_category(), e.code().category()); - } - } else { - game.LoadPlugins({blankMasterDependentEsm}, false); - - EXPECT_NE(nullptr, game.GetPlugin(blankMasterDependentEsm)); - } -} - -TEST_P( - GameTest, - loadPluginsShouldNotThrowIfAPluginHasAMasterThatIsNotInTheInputButIsAlreadyLoaded) { - Game game = Game(GetParam(), gamePath, localPath); - - const auto pluginName = - GetParam() == GameType::starfield ? blankFullEsm : blankEsm; - - game.LoadPlugins({pluginName}, true); - - game.LoadPlugins({blankMasterDependentEsm}, false); - - EXPECT_NE(nullptr, game.GetPlugin(blankMasterDependentEsm)); -} - -TEST_P(GameTest, sortPluginsWithNoLoadedPluginsShouldReturnAnEmptyList) { - Game game = Game(GetParam(), gamePath, localPath); - - const auto sorted = game.SortPlugins(game.GetLoadOrder()); - - EXPECT_TRUE(sorted.empty()); -} - -TEST_P(GameTest, sortPluginsShouldOnlySortTheGivenPlugins) { - Game game = Game(GetParam(), gamePath, localPath); - loadInstalledPlugins(game, false); - - std::vector plugins{blankEsp, blankDifferentEsp}; - const auto sorted = game.SortPlugins(plugins); - - EXPECT_EQ(plugins, sorted); -} - -TEST_P(GameTest, sortingShouldNotMakeUnnecessaryChangesToAnExistingLoadOrder) { - Game game = Game(GetParam(), gamePath, localPath); - game.LoadCurrentLoadOrderState(); - - auto plugins = GetInstalledPlugins(); - game.LoadPlugins({plugins.front()}, true); - plugins.erase(plugins.begin()); - game.LoadPlugins(plugins, false); - - std::vector expectedSortedOrder; - if (GetParam() == GameType::openmw) { - // The existing load order for OpenMW doesn't have plugins loading after - // their masters, because the game doesn't enforce that, and the test - // setup cannot enforce the positions of inactive plugins. - expectedSortedOrder = { - blankDifferentEsm, - blankDifferentMasterDependentEsm, - blankDifferentEsp, - blankDifferentPluginDependentEsp, - blankEsm, - blankMasterDependentEsm, - blankMasterDependentEsp, - blankEsp, - blankPluginDependentEsp, - masterFile, - blankDifferentMasterDependentEsp, - }; - } else { - expectedSortedOrder = getLoadOrder(); - } - - // Check stability by running the sort 100 times. - for (int i = 0; i < 100; i++) { - auto sorted = game.SortPlugins(game.GetLoadOrder()); - ASSERT_EQ(expectedSortedOrder, sorted) << " for sort " << i; - } -} - -TEST_P(GameTest, sortPluginsShouldThrowIfAGivenPluginIsNotLoaded) { - Game game = Game(GetParam(), gamePath, localPath); - - std::vector plugins{blankEsp, blankDifferentEsp}; - - EXPECT_THROW(game.SortPlugins(plugins), std::invalid_argument); -} - -TEST_P(GameTest, clearLoadedPluginsShouldClearThePluginsCache) { - Game game = Game(GetParam(), gamePath, localPath); - - game.LoadPlugins({std::filesystem::u8path(blankEsm)}, true); - const auto pointer = game.GetPlugin(blankEsm); - ASSERT_NE(nullptr, pointer); - - game.ClearLoadedPlugins(); - - EXPECT_EQ(nullptr, game.GetPlugin(blankEsm)); -} - -TEST_P(GameTest, isPluginActiveShouldActivePluginAsActiveEvenIfNotLoaded) { - Game game = Game(GetParam(), gamePath, localPath); - game.LoadCurrentLoadOrderState(); - - EXPECT_TRUE(game.IsPluginActive(blankEsm)); -} - -TEST_P(GameTest, isPluginActiveShouldInactivePluginAsInactiveEvenIfNotLoaded) { - Game game = Game(GetParam(), gamePath, localPath); - game.LoadCurrentLoadOrderState(); - - EXPECT_FALSE(game.IsPluginActive(blankEsp)); -} - -TEST_P(GameTest, isPluginActiveShouldActivePluginAsActiveWithHeaderLoaded) { - Game game = Game(GetParam(), gamePath, localPath); - game.LoadCurrentLoadOrderState(); - - ASSERT_NO_THROW(loadInstalledPlugins(game, true)); - - EXPECT_TRUE(game.IsPluginActive(blankEsm)); -} - -TEST_P(GameTest, isPluginActiveShouldInactivePluginAsInactiveWithHeaderLoaded) { - Game game = Game(GetParam(), gamePath, localPath); - game.LoadCurrentLoadOrderState(); - - ASSERT_NO_THROW(loadInstalledPlugins(game, true)); - - EXPECT_FALSE(game.IsPluginActive(blankEsp)); -} - -TEST_P(GameTest, isPluginActiveShouldActivePluginAsActiveWhenFullyLoaded) { - Game game = Game(GetParam(), gamePath, localPath); - game.LoadCurrentLoadOrderState(); - - ASSERT_NO_THROW(loadInstalledPlugins(game, false)); - - EXPECT_TRUE(game.IsPluginActive(blankEsm)); -} - -TEST_P(GameTest, isPluginActiveShouldInactivePluginAsInactiveWhenFullyLoaded) { - Game game = Game(GetParam(), gamePath, localPath); - game.LoadCurrentLoadOrderState(); - - ASSERT_NO_THROW(loadInstalledPlugins(game, false)); - - EXPECT_FALSE(game.IsPluginActive(blankEsp)); -} } }