diff --git a/src/api/sorting/plugin_sort.cpp b/src/api/sorting/plugin_sort.cpp index be5c98df..f19b85a6 100644 --- a/src/api/sorting/plugin_sort.cpp +++ b/src/api/sorting/plugin_sort.cpp @@ -100,6 +100,78 @@ std::unordered_map GetGroupsMap( return groupsMap; } +void ValidateSpecificAndHardcodedEdges( + const std::vector::const_iterator& begin, + const std::vector::const_iterator& firstNonMaster, + const std::vector::const_iterator& end, + const std::vector& hardcodedPlugins) { + const auto isNonMaster = [&](const std::string& name) { + return std::any_of( + firstNonMaster, end, [&](const PluginSortingData& plugin) { + return CompareFilenames(plugin.GetName(), name) == 0; + }); + }; + + for (auto it = begin; it != firstNonMaster; ++it) { + for (const auto& master : it->GetMasters()) { + if (isNonMaster(master)) { + throw CyclicInteractionError( + std::vector{Vertex(master, EdgeType::master), + Vertex(it->GetName(), EdgeType::masterFlag)}); + } + } + + for (const auto& file : it->GetMasterlistRequirements()) { + const auto name = std::string(file.GetName()); + if (isNonMaster(name)) { + throw CyclicInteractionError( + std::vector{Vertex(name, EdgeType::masterlistRequirement), + Vertex(it->GetName(), EdgeType::masterFlag)}); + } + } + + for (const auto& file : it->GetUserRequirements()) { + const auto name = std::string(file.GetName()); + if (isNonMaster(name)) { + throw CyclicInteractionError( + std::vector{Vertex(name, EdgeType::userRequirement), + Vertex(it->GetName(), EdgeType::masterFlag)}); + } + } + + for (const auto& file : it->GetMasterlistLoadAfterFiles()) { + const auto name = std::string(file.GetName()); + if (isNonMaster(name)) { + throw CyclicInteractionError( + std::vector{Vertex(name, EdgeType::masterlistLoadAfter), + Vertex(it->GetName(), EdgeType::masterFlag)}); + } + } + + for (const auto& file : it->GetUserLoadAfterFiles()) { + const auto name = std::string(file.GetName()); + if (isNonMaster(name)) { + throw CyclicInteractionError( + std::vector{Vertex(name, EdgeType::userLoadAfter), + Vertex(it->GetName(), EdgeType::masterFlag)}); + } + } + } + + if (begin != firstNonMaster) { + // There's at least one master, check that there are no hardcoded + // non-masters. + for (const auto& plugin : hardcodedPlugins) { + if (isNonMaster(plugin)) { + // Just report the cycle to the first master. + throw CyclicInteractionError(std::vector{ + Vertex(plugin, EdgeType::hardcoded), + Vertex(begin->GetName(), EdgeType::masterFlag)}); + } + } + } +} + std::vector SortPlugins( const std::vector::const_iterator& begin, const std::vector::const_iterator& end, @@ -195,6 +267,11 @@ std::vector SortPlugins( pluginsSortingData.end(), [](const PluginSortingData& plugin) { return plugin.IsMaster(); }); + ValidateSpecificAndHardcodedEdges(pluginsSortingData.begin(), + firstNonMasterIt, + pluginsSortingData.end(), + hardcodedPlugins); + auto newLoadOrder = SortPlugins(pluginsSortingData.begin(), firstNonMasterIt, hardcodedPlugins, diff --git a/src/tests/api/internals/sorting/plugin_graph_test.h b/src/tests/api/internals/sorting/plugin_graph_test.h index 4d3cd44f..e275a743 100644 --- a/src/tests/api/internals/sorting/plugin_graph_test.h +++ b/src/tests/api/internals/sorting/plugin_graph_test.h @@ -56,7 +56,7 @@ public: return std::optional(); } - bool IsMaster() const override { return false; } + bool IsMaster() const override { return isMaster_; } bool IsLightPlugin() const override { return false; } @@ -93,6 +93,8 @@ public: void AddMaster(const std::string& master) { masters_.push_back(master); } + void SetIsMaster(bool isMaster) { isMaster_ = isMaster; } + void AddOverlappingRecords(const PluginInterface& plugin) { recordsOverlapWith.insert(&plugin); } @@ -114,6 +116,7 @@ private: std::set assetsOverlapWith; size_t overrideRecordCount_{0}; size_t assetCount_{0}; + bool isMaster_{false}; }; } diff --git a/src/tests/api/internals/sorting/plugin_sort_test.h b/src/tests/api/internals/sorting/plugin_sort_test.h index 65831805..8b5b7687 100644 --- a/src/tests/api/internals/sorting/plugin_sort_test.h +++ b/src/tests/api/internals/sorting/plugin_sort_test.h @@ -92,7 +92,7 @@ protected: PluginSortingData CreatePluginSortingData( const std::string& name, - const std::vector& loadOrder) { + const std::vector& loadOrder = {}) { const auto plugin = GetPlugin(name); return PluginSortingData(plugin, @@ -407,6 +407,177 @@ TEST_P(PluginSortTest, sortingShouldThrowIfACyclicInteractionIsEncountered) { EXPECT_THROW(SortPlugins(game_, game_.GetLoadOrder()), CyclicInteractionError); } + +TEST_P(PluginSortTest, + sortingShouldThrowIfMasterEdgeWouldContradictMasterFlags) { + // Can't test with the test plugin files, so use the other SortPlugins() + // overload to provide stubs. + const auto esm = GetPlugin(blankEsm); + const auto esp = GetPlugin(blankEsp); + + esm->SetIsMaster(true); + esm->AddMaster(esp->GetName()); + + std::vector pluginsSortingData{ + CreatePluginSortingData(esm->GetName()), + CreatePluginSortingData(esp->GetName())}; + + try { + SortPlugins(std::move(pluginsSortingData), GetParam(), {Group()}, {}, {}); + FAIL(); + } catch (const CyclicInteractionError& e) { + ASSERT_EQ(2, e.GetCycle().size()); + EXPECT_EQ(esp->GetName(), e.GetCycle()[0].GetName()); + EXPECT_EQ(EdgeType::master, e.GetCycle()[0].GetTypeOfEdgeToNextVertex()); + EXPECT_EQ(esm->GetName(), e.GetCycle()[1].GetName()); + EXPECT_EQ(EdgeType::masterFlag, + e.GetCycle()[1].GetTypeOfEdgeToNextVertex()); + } +} + +TEST_P( + PluginSortTest, + sortingShouldThrowIfMasterlistRequirementEdgeWouldContradictMasterFlags) { + using std::endl; + + ASSERT_NO_THROW(loadInstalledPlugins(game_, false)); + + const auto masterlistPath = metadataFilesPath / "masterlist.yaml"; + std::ofstream masterlist(masterlistPath); + masterlist << "plugins:" << endl + << " - name: " << blankEsm << endl + << " req:" << endl + << " - " << blankEsp << endl; + masterlist.close(); + + game_.GetDatabase().LoadLists(masterlistPath); + + try { + SortPlugins(game_, game_.GetLoadOrder()); + FAIL(); + } catch (const CyclicInteractionError& e) { + ASSERT_EQ(2, e.GetCycle().size()); + EXPECT_EQ(blankEsp, e.GetCycle()[0].GetName()); + EXPECT_EQ(EdgeType::masterlistRequirement, + e.GetCycle()[0].GetTypeOfEdgeToNextVertex()); + EXPECT_EQ(blankEsm, e.GetCycle()[1].GetName()); + EXPECT_EQ(EdgeType::masterFlag, + e.GetCycle()[1].GetTypeOfEdgeToNextVertex()); + } +} + +TEST_P(PluginSortTest, + sortingShouldThrowIfUserRequirementEdgeWouldContradictMasterFlags) { + ASSERT_NO_THROW(loadInstalledPlugins(game_, false)); + + PluginMetadata plugin(blankEsm); + plugin.SetRequirements({File(blankEsp)}); + + game_.GetDatabase().SetPluginUserMetadata(plugin); + + try { + SortPlugins(game_, game_.GetLoadOrder()); + FAIL(); + } catch (const CyclicInteractionError& e) { + ASSERT_EQ(2, e.GetCycle().size()); + EXPECT_EQ(blankEsp, e.GetCycle()[0].GetName()); + EXPECT_EQ(EdgeType::userRequirement, + e.GetCycle()[0].GetTypeOfEdgeToNextVertex()); + EXPECT_EQ(blankEsm, e.GetCycle()[1].GetName()); + EXPECT_EQ(EdgeType::masterFlag, + e.GetCycle()[1].GetTypeOfEdgeToNextVertex()); + } +} + +TEST_P(PluginSortTest, + sortingShouldThrowIfMasterlistLoadAfterEdgeWouldContradictMasterFlags) { + using std::endl; + + ASSERT_NO_THROW(loadInstalledPlugins(game_, false)); + + const auto masterlistPath = metadataFilesPath / "masterlist.yaml"; + std::ofstream masterlist(masterlistPath); + masterlist << "plugins:" << endl + << " - name: " << blankEsm << endl + << " after:" << endl + << " - " << blankEsp << endl; + masterlist.close(); + + game_.GetDatabase().LoadLists(masterlistPath); + + try { + SortPlugins(game_, game_.GetLoadOrder()); + FAIL(); + } catch (const CyclicInteractionError& e) { + ASSERT_EQ(2, e.GetCycle().size()); + EXPECT_EQ(blankEsp, e.GetCycle()[0].GetName()); + EXPECT_EQ(EdgeType::masterlistLoadAfter, + e.GetCycle()[0].GetTypeOfEdgeToNextVertex()); + EXPECT_EQ(blankEsm, e.GetCycle()[1].GetName()); + EXPECT_EQ(EdgeType::masterFlag, + e.GetCycle()[1].GetTypeOfEdgeToNextVertex()); + } +} + +TEST_P(PluginSortTest, + sortingShouldThrowIfUserLoadAfterEdgeWouldContradictMasterFlags) { + ASSERT_NO_THROW(loadInstalledPlugins(game_, false)); + + PluginMetadata plugin(blankEsm); + plugin.SetLoadAfterFiles({File(blankEsp)}); + + game_.GetDatabase().SetPluginUserMetadata(plugin); + + try { + SortPlugins(game_, game_.GetLoadOrder()); + FAIL(); + } catch (const CyclicInteractionError& e) { + ASSERT_EQ(2, e.GetCycle().size()); + EXPECT_EQ(blankEsp, e.GetCycle()[0].GetName()); + EXPECT_EQ(EdgeType::userLoadAfter, + e.GetCycle()[0].GetTypeOfEdgeToNextVertex()); + EXPECT_EQ(blankEsm, e.GetCycle()[1].GetName()); + EXPECT_EQ(EdgeType::masterFlag, + e.GetCycle()[1].GetTypeOfEdgeToNextVertex()); + } +} + +TEST_P(PluginSortTest, + sortingShouldThrowIfHardcodedEdgeWouldContradictMasterFlags) { + // Can't test with the test plugin files, so use the other SortPlugins() + // overload to provide stubs. + const auto esm = GetPlugin(blankEsm); + const auto esp = GetPlugin(blankEsp); + + esm->SetIsMaster(true); + + std::vector pluginsSortingData{ + CreatePluginSortingData(esm->GetName()), + CreatePluginSortingData(esp->GetName())}; + + EXPECT_THROW(SortPlugins(std::move(pluginsSortingData), + GetParam(), + {Group()}, + {}, + {esp->GetName()}), + CyclicInteractionError); + + try { + SortPlugins(std::move(pluginsSortingData), + GetParam(), + {Group()}, + {}, + {esp->GetName()}); + FAIL(); + } catch (const CyclicInteractionError& e) { + ASSERT_EQ(2, e.GetCycle().size()); + EXPECT_EQ(blankEsp, e.GetCycle()[0].GetName()); + EXPECT_EQ(EdgeType::hardcoded, e.GetCycle()[0].GetTypeOfEdgeToNextVertex()); + EXPECT_EQ(blankEsm, e.GetCycle()[1].GetName()); + EXPECT_EQ(EdgeType::masterFlag, + e.GetCycle()[1].GetTypeOfEdgeToNextVertex()); + } +} } }