From 1b0ed35a7ff0c1cc2593f2ce7122c1fc10a38044 Mon Sep 17 00:00:00 2001 From: Oliver Hamlet Date: Tue, 10 Jan 2023 20:35:11 +0000 Subject: [PATCH] Rewrite PluginSortTest group tests and add many more Most of the existing tests ave been rewritten as PluginGraphTest tests, because the tests can be simpler, faster, clearer and more tightly scoped to the functionality that's actually being tested. --- .../api/internals/sorting/plugin_graph_test.h | 1221 ++++++++++++++++- .../api/internals/sorting/plugin_sort_test.h | 268 +--- 2 files changed, 1225 insertions(+), 264 deletions(-) diff --git a/src/tests/api/internals/sorting/plugin_graph_test.h b/src/tests/api/internals/sorting/plugin_graph_test.h index 7179ab59..4d3cd44f 100644 --- a/src/tests/api/internals/sorting/plugin_graph_test.h +++ b/src/tests/api/internals/sorting/plugin_graph_test.h @@ -27,7 +27,9 @@ along with LOOT. If not, see #include +#include "api/sorting/group_sort.h" #include "api/sorting/plugin_graph.h" +#include "loot/exception/cyclic_interaction_error.h" namespace loot { namespace test { @@ -116,7 +118,34 @@ private: } class PluginGraphTest : public ::testing::Test { -public: +protected: + PluginGraphTest() { + std::vector masterlistGroups{Group("A"), + Group("B", {"A"}), + Group("C"), + Group("default", {"C"}), + Group("E", {"default"}), + Group("F", {"E"})}; + std::vector userlistGroups{Group("C", {"B"})}; + + groupsMap = GetGroupsMap(masterlistGroups, userlistGroups); + predecessorGroupsMap = + GetPredecessorGroups(masterlistGroups, userlistGroups); + } + + static std::unordered_map GetGroupsMap( + const std::vector masterlistGroups, + const std::vector userGroups) { + const auto mergedGroups = MergeGroups(masterlistGroups, userGroups); + + std::unordered_map groupsMap; + for (const auto& group : mergedGroups) { + groupsMap.emplace(group.GetName(), group); + } + + return groupsMap; + } + PluginSortingData CreatePluginSortingData(const std::string& name) { const auto plugin = GetPlugin(name); @@ -124,6 +153,24 @@ public: plugin, PluginMetadata(), PluginMetadata(), {}, GameType::tes4, {}); } + PluginSortingData CreatePluginSortingData(const std::string& name, + const std::string& group, + bool isGroupUserMetadata = false) { + const auto plugin = GetPlugin(name); + + PluginMetadata masterlistMetadata; + PluginMetadata userMetadata; + + if (isGroupUserMetadata) { + userMetadata.SetGroup(group); + } else { + masterlistMetadata.SetGroup(group); + } + + return PluginSortingData( + plugin, masterlistMetadata, userMetadata, {}, GameType::tes4, {}); + } + plugingraph::TestPlugin* GetPlugin(const std::string& name) { auto it = plugins.find(name); @@ -136,6 +183,10 @@ public: return plugins.insert_or_assign(name, plugin).first->second.get(); } + std::unordered_map groupsMap; + std::unordered_map> + predecessorGroupsMap; + private: std::map> plugins; }; @@ -148,6 +199,1174 @@ TEST_F(PluginGraphTest, EXPECT_TRUE(sorted.empty()); } +TEST_F( + PluginGraphTest, + addGroupEdgesShouldAddUserGroupEdgeIfSourcePluginIsInGroupDueToUserMetadata) { + PluginGraph graph; + + const auto a = graph.AddVertex(CreatePluginSortingData("A.esp", "A", true)); + const auto b = graph.AddVertex(CreatePluginSortingData("B.esp", "B")); + + graph.AddGroupEdges(groupsMap, predecessorGroupsMap); + + // Cause a cycle to see the edge types. + graph.AddEdge(b, a, EdgeType::master); + + try { + graph.CheckForCycles(); + FAIL(); + } catch (const CyclicInteractionError& e) { + ASSERT_EQ(2, e.GetCycle().size()); + EXPECT_EQ("A.esp", e.GetCycle()[0].GetName()); + EXPECT_EQ(EdgeType::userGroup, e.GetCycle()[0].GetTypeOfEdgeToNextVertex()); + EXPECT_EQ("B.esp", e.GetCycle()[1].GetName()); + EXPECT_EQ(EdgeType::master, e.GetCycle()[1].GetTypeOfEdgeToNextVertex()); + } +} + +TEST_F( + PluginGraphTest, + addGroupEdgesShouldAddUserGroupEdgeIfTargetPluginIsInGroupDueToUserMetadata) { + PluginGraph graph; + + const auto a = graph.AddVertex(CreatePluginSortingData("A.esp", "A")); + const auto b = graph.AddVertex(CreatePluginSortingData("B.esp", "B", true)); + + graph.AddGroupEdges(groupsMap, predecessorGroupsMap); + + // Cause a cycle to see the edge types. + graph.AddEdge(b, a, EdgeType::master); + + try { + graph.CheckForCycles(); + FAIL(); + } catch (const CyclicInteractionError& e) { + ASSERT_EQ(2, e.GetCycle().size()); + EXPECT_EQ("A.esp", e.GetCycle()[0].GetName()); + EXPECT_EQ(EdgeType::userGroup, e.GetCycle()[0].GetTypeOfEdgeToNextVertex()); + EXPECT_EQ("B.esp", e.GetCycle()[1].GetName()); + EXPECT_EQ(EdgeType::master, e.GetCycle()[1].GetTypeOfEdgeToNextVertex()); + } +} + +TEST_F(PluginGraphTest, + addGroupEdgesShouldAddUserGroupEdgeIfGroupPathStartsWithUserMetadata) { + PluginGraph graph; + + const auto b = graph.AddVertex(CreatePluginSortingData("B.esp", "B")); + const auto d = graph.AddVertex(CreatePluginSortingData("D.esp")); + + graph.AddGroupEdges(groupsMap, predecessorGroupsMap); + + // Cause a cycle to see the edge types. + graph.AddEdge(d, b, EdgeType::master); + + try { + graph.CheckForCycles(); + FAIL(); + } catch (const CyclicInteractionError& e) { + ASSERT_EQ(2, e.GetCycle().size()); + EXPECT_EQ("B.esp", e.GetCycle()[0].GetName()); + EXPECT_EQ(EdgeType::userGroup, e.GetCycle()[0].GetTypeOfEdgeToNextVertex()); + EXPECT_EQ("D.esp", e.GetCycle()[1].GetName()); + EXPECT_EQ(EdgeType::master, e.GetCycle()[1].GetTypeOfEdgeToNextVertex()); + } +} + +TEST_F(PluginGraphTest, + addGroupEdgesShouldAddUserGroupEdgeIfGroupPathEndsWithUserMetadata) { + PluginGraph graph; + + const auto a = graph.AddVertex(CreatePluginSortingData("A.esp", "A")); + const auto c = graph.AddVertex(CreatePluginSortingData("C.esp", "C")); + + graph.AddGroupEdges(groupsMap, predecessorGroupsMap); + + // Cause a cycle to see the edge types. + graph.AddEdge(c, a, EdgeType::master); + + try { + graph.CheckForCycles(); + FAIL(); + } catch (const CyclicInteractionError& e) { + ASSERT_EQ(2, e.GetCycle().size()); + EXPECT_EQ("A.esp", e.GetCycle()[0].GetName()); + EXPECT_EQ(EdgeType::userGroup, e.GetCycle()[0].GetTypeOfEdgeToNextVertex()); + EXPECT_EQ("C.esp", e.GetCycle()[1].GetName()); + EXPECT_EQ(EdgeType::master, e.GetCycle()[1].GetTypeOfEdgeToNextVertex()); + } +} + +TEST_F(PluginGraphTest, + addGroupEdgesShouldAddUserGroupEdgeIfGroupPathInvolvesUserMetadata) { + PluginGraph graph; + + const auto a = graph.AddVertex(CreatePluginSortingData("A.esp", "A")); + const auto d = graph.AddVertex(CreatePluginSortingData("D.esp")); + + graph.AddGroupEdges(groupsMap, predecessorGroupsMap); + + // Cause a cycle to see the edge types. + graph.AddEdge(d, a, EdgeType::master); + + try { + graph.CheckForCycles(); + FAIL(); + } catch (const CyclicInteractionError& e) { + ASSERT_EQ(2, e.GetCycle().size()); + EXPECT_EQ("A.esp", e.GetCycle()[0].GetName()); + EXPECT_EQ(EdgeType::userGroup, e.GetCycle()[0].GetTypeOfEdgeToNextVertex()); + EXPECT_EQ("D.esp", e.GetCycle()[1].GetName()); + EXPECT_EQ(EdgeType::master, e.GetCycle()[1].GetTypeOfEdgeToNextVertex()); + } +} + +TEST_F(PluginGraphTest, + addGroupEdgesShouldAddMasterlistGroupEdgeIfNoUserMetadataIsInvolved) { + PluginGraph graph; + + const auto a = graph.AddVertex(CreatePluginSortingData("A.esp", "A")); + const auto b = graph.AddVertex(CreatePluginSortingData("B.esp", "B")); + + graph.AddGroupEdges(groupsMap, predecessorGroupsMap); + + // Cause a cycle to see the edge types. + graph.AddEdge(b, a, EdgeType::master); + + try { + graph.CheckForCycles(); + FAIL(); + } catch (const CyclicInteractionError& e) { + ASSERT_EQ(2, e.GetCycle().size()); + EXPECT_EQ("A.esp", e.GetCycle()[0].GetName()); + EXPECT_EQ(EdgeType::masterlistGroup, + e.GetCycle()[0].GetTypeOfEdgeToNextVertex()); + EXPECT_EQ("B.esp", e.GetCycle()[1].GetName()); + EXPECT_EQ(EdgeType::master, e.GetCycle()[1].GetTypeOfEdgeToNextVertex()); + } +} + +TEST_F( + PluginGraphTest, + addGroupEdgesShouldAddEdgesBetweenPluginsInIndirectlyConnectedGroups_whenAnIntermediatePluginEdgeIsSkipped) { + PluginGraph graph; + + const auto a1 = graph.AddVertex(CreatePluginSortingData("A1.esp", "A")); + const auto a2 = graph.AddVertex(CreatePluginSortingData("A2.esp", "A")); + const auto b1 = graph.AddVertex(CreatePluginSortingData("B1.esp", "B")); + const auto b2 = graph.AddVertex(CreatePluginSortingData("B2.esp", "B")); + const auto c1 = graph.AddVertex(CreatePluginSortingData("C1.esp", "C")); + const auto c2 = graph.AddVertex(CreatePluginSortingData("C2.esp", "C")); + + graph.AddEdge(b1, a1, EdgeType::master); + + graph.AddGroupEdges(groupsMap, predecessorGroupsMap); + + // Should be A2.esp -> B1.esp -> A1.esp -> B2.esp -> C1.esp + // -> C2.esp + EXPECT_TRUE(graph.EdgeExists(b1, a1)); + EXPECT_TRUE(graph.EdgeExists(a1, b2)); + EXPECT_TRUE(graph.EdgeExists(a2, b1)); + EXPECT_TRUE(graph.EdgeExists(a2, b2)); + EXPECT_TRUE(graph.EdgeExists(b1, c1)); + EXPECT_TRUE(graph.EdgeExists(b1, c2)); + EXPECT_TRUE(graph.EdgeExists(b2, c1)); + EXPECT_TRUE(graph.EdgeExists(b2, c2)); + EXPECT_TRUE(graph.EdgeExists(a1, c1)); + EXPECT_TRUE(graph.EdgeExists(a1, c2)); + EXPECT_FALSE(graph.EdgeExists(c1, c2)); + EXPECT_FALSE(graph.EdgeExists(c2, c1)); + + EXPECT_NO_THROW(graph.CheckForCycles()); +} + +TEST_F(PluginGraphTest, addGroupEdgesShouldAddEdgesAcrossEmptyGroups) { + PluginGraph graph; + + const auto a = graph.AddVertex(CreatePluginSortingData("A.esp", "A")); + const auto c = graph.AddVertex(CreatePluginSortingData("C.esp", "C")); + + graph.AddGroupEdges(groupsMap, predecessorGroupsMap); + + // Should be A.esp -> C.esp + EXPECT_TRUE(graph.EdgeExists(a, c)); + + EXPECT_NO_THROW(graph.CheckForCycles()); +} + +TEST_F(PluginGraphTest, + addGroupEdgesShouldAddEdgesAcrossTheNonEmptyDefaultGroup) { + PluginGraph graph; + + const auto a = graph.AddVertex(CreatePluginSortingData("A.esp", "A")); + const auto d = graph.AddVertex(CreatePluginSortingData("D.esp")); + const auto e = graph.AddVertex(CreatePluginSortingData("E.esp", "E")); + + graph.AddGroupEdges(groupsMap, predecessorGroupsMap); + + // Should be A.esp -> D.esp -> E.esp + // ----------> + EXPECT_TRUE(graph.EdgeExists(a, d)); + EXPECT_TRUE(graph.EdgeExists(d, e)); + EXPECT_TRUE(graph.EdgeExists(a, e)); + + EXPECT_NO_THROW(graph.CheckForCycles()); +} + +TEST_F(PluginGraphTest, addGroupEdgesShouldSkipAnEdgeThatWouldCauseACycle) { + PluginGraph graph; + + const auto a = graph.AddVertex(CreatePluginSortingData("A.esp", "A")); + const auto c = graph.AddVertex(CreatePluginSortingData("C.esp", "C")); + + graph.AddEdge(c, a, EdgeType::master); + + graph.AddGroupEdges(groupsMap, predecessorGroupsMap); + + // Should be C.esp -> A.esp + EXPECT_TRUE(graph.EdgeExists(c, a)); + + EXPECT_NO_THROW(graph.CheckForCycles()); +} + +TEST_F( + PluginGraphTest, + addGroupEdgesDoesNotSkipAnEdgeThatCausesACycleInvolvingOtherNonDefaultGroups) { + PluginGraph graph; + + const auto a = graph.AddVertex(CreatePluginSortingData("A.esp", "A")); + const auto b = graph.AddVertex(CreatePluginSortingData("B.esp", "B")); + const auto c = graph.AddVertex(CreatePluginSortingData("C.esp", "C")); + + graph.AddEdge(c, a, EdgeType::master); + + graph.AddGroupEdges(groupsMap, predecessorGroupsMap); + + // Should be C.esp -> A.esp -> B.esp + EXPECT_TRUE(graph.EdgeExists(c, a)); + EXPECT_TRUE(graph.EdgeExists(a, b)); + + // FIXME: This should not cause a cycle. + try { + graph.CheckForCycles(); + FAIL(); + } catch (CyclicInteractionError& e) { + ASSERT_EQ(3, e.GetCycle().size()); + EXPECT_EQ(graph.GetPlugin(a).GetName(), e.GetCycle()[0].GetName()); + EXPECT_EQ(EdgeType::masterlistGroup, + e.GetCycle()[0].GetTypeOfEdgeToNextVertex()); + EXPECT_EQ(graph.GetPlugin(b).GetName(), e.GetCycle()[1].GetName()); + EXPECT_EQ(EdgeType::userGroup, e.GetCycle()[1].GetTypeOfEdgeToNextVertex()); + EXPECT_EQ(graph.GetPlugin(c).GetName(), e.GetCycle()[2].GetName()); + EXPECT_EQ(EdgeType::master, e.GetCycle()[2].GetTypeOfEdgeToNextVertex()); + } +} + +TEST_F( + PluginGraphTest, + addGroupEdgesShouldSkipOnlyEdgesToTheTargetGroupPluginsThatWouldCauseACycle) { + PluginGraph graph; + + const auto a = graph.AddVertex(CreatePluginSortingData("A.esp", "A")); + const auto c1 = graph.AddVertex(CreatePluginSortingData("C1.esp", "C")); + const auto c2 = graph.AddVertex(CreatePluginSortingData("C2.esp", "C")); + + graph.AddEdge(c1, a, EdgeType::master); + + graph.AddGroupEdges(groupsMap, predecessorGroupsMap); + + // Should be C1.esp -> A.esp -> C2.esp + EXPECT_TRUE(graph.EdgeExists(c1, a)); + EXPECT_TRUE(graph.EdgeExists(a, c2)); + + EXPECT_NO_THROW(graph.CheckForCycles()); +} + +TEST_F( + PluginGraphTest, + addGroupEdgesShouldSkipOnlyEdgesFromAncestorsToTheTargetGroupPluginsThatWouldCauseACycle) { + PluginGraph graph; + + const auto b = graph.AddVertex(CreatePluginSortingData("B.esp", "B")); + const auto c = graph.AddVertex(CreatePluginSortingData("C.esp", "C")); + const auto d1 = graph.AddVertex(CreatePluginSortingData("D1.esp")); + const auto d2 = graph.AddVertex(CreatePluginSortingData("D2.esp")); + const auto d3 = graph.AddVertex(CreatePluginSortingData("D3.esp")); + + graph.AddEdge(d1, b, EdgeType::masterFlag); + graph.AddEdge(d2, b, EdgeType::masterFlag); + graph.AddEdge(c, b, EdgeType::masterFlag); + graph.AddEdge(c, d2, EdgeType::master); + graph.AddEdge(c, d3, EdgeType::masterFlag); + + graph.AddGroupEdges(groupsMap, predecessorGroupsMap); + + // Should be: C.esp -> D2.esp -> B.esp -> D3.esp + // -> D1.esp -> + // --------------------> + // -----------> + EXPECT_TRUE(graph.EdgeExists(d1, b)); + EXPECT_TRUE(graph.EdgeExists(d2, b)); + EXPECT_TRUE(graph.EdgeExists(c, b)); + EXPECT_TRUE(graph.EdgeExists(c, d2)); + EXPECT_TRUE(graph.EdgeExists(c, d3)); + + EXPECT_TRUE(graph.EdgeExists(b, d3)); + + // FIXME: This edge should be added but isn't, it's a limitation of the + // current implementation. + EXPECT_FALSE(graph.EdgeExists(c, d1)); + + EXPECT_NO_THROW(graph.CheckForCycles()); +} + +TEST_F( + PluginGraphTest, + addGroupEdgesShouldAddAPluginsEdgesAcrossASuccessorIfAtLeastOneEdgeToTheSuccessorGroupWasSkipped_successiveDepths) { + PluginGraph graph; + + const auto a1 = graph.AddVertex(CreatePluginSortingData("A1.esp", "A")); + const auto a2 = graph.AddVertex(CreatePluginSortingData("A2.esp", "A")); + const auto b1 = graph.AddVertex(CreatePluginSortingData("B1.esp", "B")); + const auto b2 = graph.AddVertex(CreatePluginSortingData("B2.esp", "B")); + const auto c1 = graph.AddVertex(CreatePluginSortingData("C1.esp", "C")); + const auto c2 = graph.AddVertex(CreatePluginSortingData("C2.esp", "C")); + + graph.AddEdge(b1, a1, EdgeType::master); + graph.AddEdge(c1, b2, EdgeType::master); + + graph.AddGroupEdges(groupsMap, predecessorGroupsMap); + + // Should be A2.esp -> B1.esp -> A1.esp -> C1.esp -> B2.esp -> C2.esp + EXPECT_TRUE(graph.EdgeExists(b1, a1)); + EXPECT_TRUE(graph.EdgeExists(c1, b2)); + EXPECT_TRUE(graph.EdgeExists(a1, b2)); + EXPECT_TRUE(graph.EdgeExists(a1, c1)); + EXPECT_TRUE(graph.EdgeExists(a1, c2)); + EXPECT_TRUE(graph.EdgeExists(a2, b1)); + EXPECT_TRUE(graph.EdgeExists(a2, b2)); + EXPECT_TRUE(graph.EdgeExists(b1, c1)); + EXPECT_TRUE(graph.EdgeExists(b1, c2)); + EXPECT_TRUE(graph.EdgeExists(b2, c2)); + + EXPECT_NO_THROW(graph.CheckForCycles()); +} + +TEST_F( + PluginGraphTest, + addGroupEdgesShouldAddAPluginsEdgesAcrossASuccessorIfAtLeastOneEdgeToTheSuccessorGroupWasSkipped_successiveDepthsDifferentOrder) { + PluginGraph graph; + + const auto a1 = graph.AddVertex(CreatePluginSortingData("A1.esp", "A")); + const auto a2 = graph.AddVertex(CreatePluginSortingData("A2.esp", "A")); + const auto b1 = graph.AddVertex(CreatePluginSortingData("B1.esp", "B")); + const auto b2 = graph.AddVertex(CreatePluginSortingData("B2.esp", "B")); + const auto c1 = graph.AddVertex(CreatePluginSortingData("C1.esp", "C")); + const auto c2 = graph.AddVertex(CreatePluginSortingData("C2.esp", "C")); + + graph.AddEdge(b1, a1, EdgeType::master); + graph.AddEdge(c1, b1, EdgeType::master); + + graph.AddGroupEdges(groupsMap, predecessorGroupsMap); + + // Should be A2.esp -> C1.esp -> B1.esp -> A1.esp -> B2.esp -> C2.esp + EXPECT_TRUE(graph.EdgeExists(b1, a1)); + EXPECT_TRUE(graph.EdgeExists(c1, b1)); + EXPECT_TRUE(graph.EdgeExists(a1, b2)); + EXPECT_TRUE(graph.EdgeExists(a2, b1)); + EXPECT_TRUE(graph.EdgeExists(a2, b2)); + EXPECT_TRUE(graph.EdgeExists(a1, c2)); + EXPECT_TRUE(graph.EdgeExists(b1, c2)); + EXPECT_TRUE(graph.EdgeExists(b2, c2)); + EXPECT_TRUE(graph.EdgeExists(a2, c1)); + + // FIXME: This edge is unwanted and causes a cycle. + EXPECT_TRUE(graph.EdgeExists(b2, c1)); + + EXPECT_THROW(graph.CheckForCycles(), CyclicInteractionError); +} + +TEST_F( + PluginGraphTest, + addGroupEdgesShouldAddEdgeFromAncestorToSuccessorIfNoneOfAGroupsPluginsCan_simple) { + PluginGraph graph; + + const auto a = graph.AddVertex(CreatePluginSortingData("A.esp", "A")); + const auto b1 = graph.AddVertex(CreatePluginSortingData("B1.esp", "B")); + const auto b2 = graph.AddVertex(CreatePluginSortingData("B2.esp", "B")); + const auto c = graph.AddVertex(CreatePluginSortingData("C.esp", "C")); + + graph.AddEdge(c, b1, EdgeType::master); + graph.AddEdge(c, b2, EdgeType::master); + + graph.AddGroupEdges(groupsMap, predecessorGroupsMap); + + // Should be A.esp -> C1.esp -> B1.esp + // -> B2.esp + EXPECT_TRUE(graph.EdgeExists(a, b1)); + EXPECT_TRUE(graph.EdgeExists(a, b2)); + EXPECT_TRUE(graph.EdgeExists(c, b1)); + EXPECT_TRUE(graph.EdgeExists(c, b2)); + EXPECT_TRUE(graph.EdgeExists(a, c)); + EXPECT_FALSE(graph.EdgeExists(b1, b2)); + EXPECT_FALSE(graph.EdgeExists(b2, b1)); + + EXPECT_NO_THROW(graph.CheckForCycles()); +} + +TEST_F( + PluginGraphTest, + addGroupEdgesShouldAddEdgeFromAncestorToSuccessorIfNoneOfAGroupsPluginsCan_withEdgesAcrossedTheSkippedGroup) { + PluginGraph graph; + + const auto a1 = graph.AddVertex(CreatePluginSortingData("A1.esp", "A")); + const auto a2 = graph.AddVertex(CreatePluginSortingData("A2.esp", "A")); + const auto b1 = graph.AddVertex(CreatePluginSortingData("B1.esp", "B")); + const auto b2 = graph.AddVertex(CreatePluginSortingData("B2.esp", "B")); + const auto c1 = graph.AddVertex(CreatePluginSortingData("C1.esp", "C")); + const auto c2 = graph.AddVertex(CreatePluginSortingData("C2.esp", "C")); + const auto d1 = graph.AddVertex(CreatePluginSortingData("D1.esp")); + const auto d2 = graph.AddVertex(CreatePluginSortingData("D2.esp")); + + graph.AddEdge(b1, a1, EdgeType::master); + graph.AddEdge(c1, b1, EdgeType::master); + graph.AddEdge(d1, c1, EdgeType::master); + graph.AddEdge(d2, c1, EdgeType::master); + + graph.AddGroupEdges(groupsMap, predecessorGroupsMap); + + // Should be: + // A2.esp -> D1.esp -> C1.esp -> B1.esp -> A1.esp -> B2.esp -> C2.esp + // -> D2.esp -> + EXPECT_TRUE(graph.EdgeExists(b1, a1)); + EXPECT_TRUE(graph.EdgeExists(c1, b1)); + EXPECT_TRUE(graph.EdgeExists(d1, c1)); + EXPECT_TRUE(graph.EdgeExists(d2, c1)); + EXPECT_TRUE(graph.EdgeExists(a1, b2)); + EXPECT_TRUE(graph.EdgeExists(a2, b1)); + EXPECT_TRUE(graph.EdgeExists(a2, b2)); + EXPECT_TRUE(graph.EdgeExists(a1, c2)); + EXPECT_TRUE(graph.EdgeExists(b1, c2)); + EXPECT_TRUE(graph.EdgeExists(a2, c1)); + EXPECT_TRUE(graph.EdgeExists(a2, d1)); + EXPECT_TRUE(graph.EdgeExists(a2, d2)); + EXPECT_FALSE(graph.EdgeExists(d1, d2)); + EXPECT_FALSE(graph.EdgeExists(d2, d1)); + + // FIXME: This edge is unwanted and causes a cycle. + EXPECT_TRUE(graph.EdgeExists(b2, c1)); + + EXPECT_THROW(graph.CheckForCycles(), CyclicInteractionError); +} + +TEST_F( + PluginGraphTest, + addGroupEdgesShouldDeprioritiseEdgesFromDefaultGroupPlugins_defaultLast) { + PluginGraph graph; + + const auto b = graph.AddVertex(CreatePluginSortingData("B.esp", "B")); + const auto c = graph.AddVertex(CreatePluginSortingData("C.esp", "C")); + const auto d = graph.AddVertex(CreatePluginSortingData("D.esp")); + + graph.AddEdge(d, b, EdgeType::master); + + graph.AddGroupEdges(groupsMap, predecessorGroupsMap); + + // Should be D.esp -> B.esp -> C.esp + EXPECT_TRUE(graph.EdgeExists(b, c)); + EXPECT_TRUE(graph.EdgeExists(d, b)); + + EXPECT_NO_THROW(graph.CheckForCycles()); +} + +TEST_F( + PluginGraphTest, + addGroupEdgesShouldDeprioritiseEdgesFromDefaultGroupPlugins_defaultFirst) { + PluginGraph graph; + + const auto d = graph.AddVertex(CreatePluginSortingData("D.esp")); + const auto e = graph.AddVertex(CreatePluginSortingData("E.esp", "E")); + const auto f = graph.AddVertex(CreatePluginSortingData("F.esp", "F")); + + graph.AddEdge(f, d, EdgeType::master); + + graph.AddGroupEdges(groupsMap, predecessorGroupsMap); + + // Should be E.esp -> F.esp -> D.esp + EXPECT_TRUE(graph.EdgeExists(e, f)); + EXPECT_TRUE(graph.EdgeExists(f, d)); + EXPECT_FALSE(graph.EdgeExists(d, e)); + + EXPECT_NO_THROW(graph.CheckForCycles()); +} + +TEST_F( + PluginGraphTest, + addGroupEdgesShouldDeprioritiseEdgesFromDefaultGroupPlugins_acrossSkippedIntermediateGroups) { + PluginGraph graph; + + const auto d = graph.AddVertex(CreatePluginSortingData("D.esp")); + const auto e = graph.AddVertex(CreatePluginSortingData("E.esp", "E")); + const auto f = graph.AddVertex(CreatePluginSortingData("F.esp", "F")); + + graph.AddEdge(e, d, EdgeType::master); + + graph.AddGroupEdges(groupsMap, predecessorGroupsMap); + + // Should be E.esp -> D.esp -> F.esp + EXPECT_TRUE(graph.EdgeExists(e, d)); + EXPECT_TRUE(graph.EdgeExists(d, f)); + EXPECT_FALSE(graph.EdgeExists(f, e)); + + EXPECT_NO_THROW(graph.CheckForCycles()); +} + +TEST_F( + PluginGraphTest, + addGroupEdgesShouldDeprioritiseEdgesFromDefaultGroupPlugins_d1Firstd2Last) { + PluginGraph graph; + + const auto d1 = graph.AddVertex(CreatePluginSortingData("D1.esp")); + const auto d2 = graph.AddVertex(CreatePluginSortingData("D2.esp")); + const auto e = graph.AddVertex(CreatePluginSortingData("E.esp", "E")); + const auto f = graph.AddVertex(CreatePluginSortingData("F.esp", "F")); + + graph.AddEdge(f, d2, EdgeType::master); + + graph.AddGroupEdges(groupsMap, predecessorGroupsMap); + + // Should be D1.esp -> E.esp -> F.esp -> D2.esp + EXPECT_TRUE(graph.EdgeExists(e, f)); + EXPECT_TRUE(graph.EdgeExists(f, d2)); + EXPECT_TRUE(graph.EdgeExists(d1, e)); + EXPECT_FALSE(graph.EdgeExists(d2, d1)); + + EXPECT_NO_THROW(graph.CheckForCycles()); +} + +TEST_F( + PluginGraphTest, + addGroupEdgesShouldDeprioritiseEdgesFromDefaultGroupPlugins_noIdealResult) { + PluginGraph graph; + + const auto b = graph.AddVertex(CreatePluginSortingData("B.esp", "B")); + const auto c = graph.AddVertex(CreatePluginSortingData("C.esp", "C")); + const auto d = graph.AddVertex(CreatePluginSortingData("D.esp")); + const auto e = graph.AddVertex(CreatePluginSortingData("E.esp", "E")); + const auto f = graph.AddVertex(CreatePluginSortingData("F.esp", "F")); + + graph.AddEdge(d, b, EdgeType::master); + graph.AddEdge(f, d, EdgeType::master); + + graph.AddGroupEdges(groupsMap, predecessorGroupsMap); + + // No ideal result, expected is F.esp -> D.esp -> B.esp -> C.esp -> E.esp + EXPECT_TRUE(graph.EdgeExists(f, d)); + EXPECT_TRUE(graph.EdgeExists(d, b)); + EXPECT_TRUE(graph.EdgeExists(b, c)); + EXPECT_TRUE(graph.EdgeExists(c, e)); + + // FIXME: This edge is unwanted and causes a cycle. + EXPECT_TRUE(graph.EdgeExists(e, f)); + + EXPECT_THROW(graph.CheckForCycles(), CyclicInteractionError); +} + +TEST_F( + PluginGraphTest, + addGroupEdgesShouldDeprioritiseEdgesFromDefaultGroupPlugins_defaultInMiddleDBookends) { + PluginGraph graph; + + const auto b = graph.AddVertex(CreatePluginSortingData("B.esp", "B")); + const auto c = graph.AddVertex(CreatePluginSortingData("C.esp", "C")); + const auto d1 = graph.AddVertex(CreatePluginSortingData("D1.esp")); + const auto d2 = graph.AddVertex(CreatePluginSortingData("D2.esp")); + const auto e = graph.AddVertex(CreatePluginSortingData("E.esp", "E")); + const auto f = graph.AddVertex(CreatePluginSortingData("F.esp", "F")); + + graph.AddEdge(d2, b, EdgeType::master); + graph.AddEdge(f, d1, EdgeType::master); + + graph.AddGroupEdges(groupsMap, predecessorGroupsMap); + + // Should be D2.esp -> B.esp -> C.esp -> E.esp -> F.esp -> D1.esp + EXPECT_TRUE(graph.EdgeExists(d2, b)); + EXPECT_TRUE(graph.EdgeExists(b, c)); + EXPECT_TRUE(graph.EdgeExists(c, e)); + EXPECT_TRUE(graph.EdgeExists(e, f)); + EXPECT_TRUE(graph.EdgeExists(f, d1)); + EXPECT_FALSE(graph.EdgeExists(d1, d2)); + + EXPECT_NO_THROW(graph.CheckForCycles()); +} + +TEST_F( + PluginGraphTest, + addGroupEdgesShouldDeprioritiseEdgesFromDefaultGroupPlugins_defaultInMiddleDThroughout) { + PluginGraph graph; + + const auto b = graph.AddVertex(CreatePluginSortingData("B.esp", "B")); + const auto c = graph.AddVertex(CreatePluginSortingData("C.esp", "C")); + const auto d1 = graph.AddVertex(CreatePluginSortingData("D1.esp")); + const auto d2 = graph.AddVertex(CreatePluginSortingData("D2.esp")); + const auto d3 = graph.AddVertex(CreatePluginSortingData("D3.esp")); + const auto d4 = graph.AddVertex(CreatePluginSortingData("D4.esp")); + const auto e = graph.AddVertex(CreatePluginSortingData("E.esp", "E")); + const auto f = graph.AddVertex(CreatePluginSortingData("F.esp", "F")); + + graph.AddEdge(d2, b, EdgeType::master); + graph.AddEdge(d4, c, EdgeType::master); + graph.AddEdge(f, d1, EdgeType::master); + + graph.AddGroupEdges(groupsMap, predecessorGroupsMap); + + // Should be: + // D2.esp -> B.esp -> D4.esp -> C.esp -> D3.esp -> E.esp -> F.esp -> D1.esp + EXPECT_TRUE(graph.EdgeExists(d2, b)); + EXPECT_TRUE(graph.EdgeExists(b, c)); + EXPECT_TRUE(graph.EdgeExists(c, d3)); + EXPECT_TRUE(graph.EdgeExists(c, e)); + EXPECT_TRUE(graph.EdgeExists(d3, e)); + EXPECT_TRUE(graph.EdgeExists(e, f)); + EXPECT_TRUE(graph.EdgeExists(f, d1)); + EXPECT_TRUE(graph.EdgeExists(d4, c)); + EXPECT_TRUE(graph.EdgeExists(b, d4)); + EXPECT_FALSE(graph.EdgeExists(d1, d2)); + EXPECT_FALSE(graph.EdgeExists(d1, d3)); + EXPECT_FALSE(graph.EdgeExists(d1, d4)); + + EXPECT_NO_THROW(graph.CheckForCycles()); +} + +TEST_F(PluginGraphTest, + addGroupEdgesShouldHandleAsymmetricBranchesInTheGroupsGraph) { + std::vector masterlistGroups{Group("A"), + Group("B", {"A"}), + Group("C", {"B"}), + Group("D", {"A"}), + Group()}; + + groupsMap = GetGroupsMap(masterlistGroups, {}); + predecessorGroupsMap = GetPredecessorGroups(masterlistGroups, {}); + + PluginGraph graph; + + const auto a = graph.AddVertex(CreatePluginSortingData("A.esp", "A")); + const auto b = graph.AddVertex(CreatePluginSortingData("B.esp", "B")); + const auto c = graph.AddVertex(CreatePluginSortingData("C.esp", "C")); + const auto d = graph.AddVertex(CreatePluginSortingData("D.esp", "D")); + + graph.AddGroupEdges(groupsMap, predecessorGroupsMap); + + // Should be A.esp -> B.esp -> C.esp + // -> D.esp + EXPECT_TRUE(graph.EdgeExists(a, b)); + EXPECT_TRUE(graph.EdgeExists(b, c)); + EXPECT_TRUE(graph.EdgeExists(a, d)); + EXPECT_FALSE(graph.EdgeExists(d, b)); + EXPECT_FALSE(graph.EdgeExists(d, c)); + EXPECT_FALSE(graph.EdgeExists(b, d)); + EXPECT_FALSE(graph.EdgeExists(c, d)); + + EXPECT_NO_THROW(graph.CheckForCycles()); +} + +TEST_F(PluginGraphTest, + addGroupEdgesShouldHandleAsymmetricBranchesInTheGroupsGraphThatMerge) { + std::vector masterlistGroups{Group("A"), + Group("B", {"A"}), + Group("C", {"B"}), + Group("D", {"A"}), + Group("E", {"C", "D"}), + Group()}; + + groupsMap = GetGroupsMap(masterlistGroups, {}); + predecessorGroupsMap = GetPredecessorGroups(masterlistGroups, {}); + + PluginGraph graph; + + const auto a = graph.AddVertex(CreatePluginSortingData("A.esp", "A")); + const auto b = graph.AddVertex(CreatePluginSortingData("B.esp", "B")); + const auto c = graph.AddVertex(CreatePluginSortingData("C.esp", "C")); + const auto d = graph.AddVertex(CreatePluginSortingData("D.esp", "D")); + const auto e = graph.AddVertex(CreatePluginSortingData("E.esp", "E")); + + graph.AddGroupEdges(groupsMap, predecessorGroupsMap); + + // Should be A.esp -> B.esp -> C.esp -> E.esp + // -> D.esp ----------> + EXPECT_TRUE(graph.EdgeExists(a, b)); + EXPECT_TRUE(graph.EdgeExists(b, c)); + EXPECT_TRUE(graph.EdgeExists(c, e)); + EXPECT_TRUE(graph.EdgeExists(a, d)); + EXPECT_TRUE(graph.EdgeExists(d, e)); + EXPECT_FALSE(graph.EdgeExists(d, b)); + EXPECT_FALSE(graph.EdgeExists(d, c)); + EXPECT_FALSE(graph.EdgeExists(b, d)); + EXPECT_FALSE(graph.EdgeExists(c, d)); + + EXPECT_NO_THROW(graph.CheckForCycles()); +} + +TEST_F( + PluginGraphTest, + addGroupEdgesShouldHandleBranchesInTheGroupsGraphThatFormADiamondPattern) { + std::vector masterlistGroups{Group("A"), + Group("B", {"A"}), + Group("C", {"A"}), + Group("D", {"B", "C"}), + Group()}; + + groupsMap = GetGroupsMap(masterlistGroups, {}); + predecessorGroupsMap = GetPredecessorGroups(masterlistGroups, {}); + + PluginGraph graph; + + const auto a = graph.AddVertex(CreatePluginSortingData("A.esp", "A")); + const auto b = graph.AddVertex(CreatePluginSortingData("B.esp", "B")); + const auto c = graph.AddVertex(CreatePluginSortingData("C.esp", "C")); + const auto d = graph.AddVertex(CreatePluginSortingData("D.esp", "D")); + + graph.AddGroupEdges(groupsMap, predecessorGroupsMap); + + // Should be A.esp -> B.esp -> D.esp + // -> C.esp -> + EXPECT_TRUE(graph.EdgeExists(a, b)); + EXPECT_TRUE(graph.EdgeExists(b, d)); + EXPECT_TRUE(graph.EdgeExists(a, c)); + EXPECT_TRUE(graph.EdgeExists(c, d)); + EXPECT_FALSE(graph.EdgeExists(b, c)); + EXPECT_FALSE(graph.EdgeExists(c, b)); + + EXPECT_NO_THROW(graph.CheckForCycles()); +} + +TEST_F( + PluginGraphTest, + addGroupEdgesShouldAddEdgesAcrossTheMergePointOfBranchesInTheGroupsGraph) { + std::vector masterlistGroups{Group("A"), + Group("B", {"A"}), + Group("C", {"A"}), + Group("D", {"B", "C"}), + Group("E", {"D"}), + Group()}; + + groupsMap = GetGroupsMap(masterlistGroups, {}); + predecessorGroupsMap = GetPredecessorGroups(masterlistGroups, {}); + + PluginGraph graph; + + const auto a = graph.AddVertex(CreatePluginSortingData("A.esp", "A")); + const auto b = graph.AddVertex(CreatePluginSortingData("B.esp", "B")); + const auto c = graph.AddVertex(CreatePluginSortingData("C.esp", "C")); + const auto d = graph.AddVertex(CreatePluginSortingData("D.esp", "D")); + const auto e = graph.AddVertex(CreatePluginSortingData("E.esp", "E")); + + graph.AddEdge(d, c, EdgeType::master); + + graph.AddGroupEdges(groupsMap, predecessorGroupsMap); + + // Should be A.esp -> B.esp -> D.esp -> C.esp -> E.esp + EXPECT_TRUE(graph.EdgeExists(d, c)); + EXPECT_TRUE(graph.EdgeExists(a, b)); + EXPECT_TRUE(graph.EdgeExists(b, d)); + EXPECT_TRUE(graph.EdgeExists(d, e)); + EXPECT_TRUE(graph.EdgeExists(a, c)); + EXPECT_TRUE(graph.EdgeExists(c, e)); + EXPECT_FALSE(graph.EdgeExists(b, c)); + EXPECT_FALSE(graph.EdgeExists(c, b)); + EXPECT_FALSE(graph.EdgeExists(c, d)); + + EXPECT_NO_THROW(graph.CheckForCycles()); +} + +TEST_F(PluginGraphTest, + addGroupEdgesShouldHandleAGroupGraphWithMultipleSuccessiveBranches) { + PluginGraph graph; + + std::vector masterlistGroups{Group("A"), + Group("B", {"A"}), + Group("C", {"A"}), + Group("D", {"B", "C"}), + Group("E", {"D"}), + Group("F", {"D"}), + Group("G", {"E", "F"}), + Group()}; + + groupsMap = GetGroupsMap(masterlistGroups, {}); + predecessorGroupsMap = GetPredecessorGroups(masterlistGroups, {}); + + const auto a = graph.AddVertex(CreatePluginSortingData("A.esp", "A")); + const auto b = graph.AddVertex(CreatePluginSortingData("B.esp", "B")); + const auto c = graph.AddVertex(CreatePluginSortingData("C.esp", "C")); + const auto d = graph.AddVertex(CreatePluginSortingData("D.esp", "D")); + const auto e = graph.AddVertex(CreatePluginSortingData("E.esp", "E")); + const auto f = graph.AddVertex(CreatePluginSortingData("F.esp", "F")); + const auto g = graph.AddVertex(CreatePluginSortingData("G.esp", "G")); + + graph.AddGroupEdges(groupsMap, predecessorGroupsMap); + + // Should be: + // A.esp -> B.esp -> D.esp -> E.esp -> G.esp + // -> C.esp -> -> F.esp -> + EXPECT_TRUE(graph.EdgeExists(a, b)); + EXPECT_TRUE(graph.EdgeExists(a, c)); + EXPECT_TRUE(graph.EdgeExists(b, d)); + EXPECT_TRUE(graph.EdgeExists(c, d)); + EXPECT_TRUE(graph.EdgeExists(d, e)); + EXPECT_TRUE(graph.EdgeExists(d, f)); + EXPECT_TRUE(graph.EdgeExists(e, g)); + EXPECT_TRUE(graph.EdgeExists(f, g)); + + EXPECT_FALSE(graph.EdgeExists(b, c)); + EXPECT_FALSE(graph.EdgeExists(c, b)); + EXPECT_FALSE(graph.EdgeExists(e, f)); + EXPECT_FALSE(graph.EdgeExists(f, e)); + + EXPECT_NO_THROW(graph.CheckForCycles()); +} + +TEST_F(PluginGraphTest, addGroupEdgesShouldHandleIsolatedGroups) { + std::vector masterlistGroups{ + Group("A"), Group("B", {"A"}), Group("C"), Group()}; + + groupsMap = GetGroupsMap(masterlistGroups, {}); + predecessorGroupsMap = GetPredecessorGroups(masterlistGroups, {}); + + PluginGraph graph; + + const auto a = graph.AddVertex(CreatePluginSortingData("A.esp", "A")); + const auto b = graph.AddVertex(CreatePluginSortingData("B.esp", "B")); + const auto c = graph.AddVertex(CreatePluginSortingData("C.esp", "C")); + + graph.AddGroupEdges(groupsMap, predecessorGroupsMap); + + // Should be A.esp -> B.esp + // C.esp + EXPECT_TRUE(graph.EdgeExists(a, b)); + EXPECT_FALSE(graph.EdgeExists(a, c)); + EXPECT_FALSE(graph.EdgeExists(c, a)); + EXPECT_FALSE(graph.EdgeExists(b, c)); + EXPECT_FALSE(graph.EdgeExists(c, b)); + + EXPECT_NO_THROW(graph.CheckForCycles()); +} + +TEST_F(PluginGraphTest, addGroupEdgesShouldHandleDisconnectedGroupGraphs) { + std::vector masterlistGroups{ + Group("A"), Group("B", {"A"}), Group("C"), Group("D", {"C"}), Group()}; + + groupsMap = GetGroupsMap(masterlistGroups, {}); + predecessorGroupsMap = GetPredecessorGroups(masterlistGroups, {}); + + PluginGraph graph; + + const auto a = graph.AddVertex(CreatePluginSortingData("A.esp", "A")); + const auto b = graph.AddVertex(CreatePluginSortingData("B.esp", "B")); + const auto c = graph.AddVertex(CreatePluginSortingData("C.esp", "C")); + const auto d = graph.AddVertex(CreatePluginSortingData("D.esp", "D")); + + graph.AddGroupEdges(groupsMap, predecessorGroupsMap); + + // Should be A.esp -> B.esp + // C.esp -> D.esp + EXPECT_TRUE(graph.EdgeExists(a, b)); + EXPECT_TRUE(graph.EdgeExists(c, d)); + EXPECT_FALSE(graph.EdgeExists(a, c)); + EXPECT_FALSE(graph.EdgeExists(a, d)); + EXPECT_FALSE(graph.EdgeExists(b, c)); + EXPECT_FALSE(graph.EdgeExists(b, d)); + EXPECT_FALSE(graph.EdgeExists(c, a)); + EXPECT_FALSE(graph.EdgeExists(c, b)); + EXPECT_FALSE(graph.EdgeExists(d, a)); + EXPECT_FALSE(graph.EdgeExists(d, b)); + + EXPECT_NO_THROW(graph.CheckForCycles()); +} + +TEST_F(PluginGraphTest, + addGroupEdgesShouldAddEdgesAcrossTheMergePointOfTwoRootVertexPaths) { + std::vector masterlistGroups{Group("A"), + Group("B"), + Group("C", {"A", "B"}), + Group("D", {"C"}), + Group()}; + + groupsMap = GetGroupsMap(masterlistGroups, {}); + predecessorGroupsMap = GetPredecessorGroups(masterlistGroups, {}); + + PluginGraph graph; + + const auto a = graph.AddVertex(CreatePluginSortingData("A.esp", "A")); + const auto b = graph.AddVertex(CreatePluginSortingData("B.esp", "B")); + const auto c = graph.AddVertex(CreatePluginSortingData("C.esp", "C")); + const auto d = graph.AddVertex(CreatePluginSortingData("D.esp", "D")); + + graph.AddEdge(c, b, EdgeType::master); + + graph.AddGroupEdges(groupsMap, predecessorGroupsMap); + + // Should be A.esp -> C.esp -> D.esp + // B.esp ----------> + EXPECT_TRUE(graph.EdgeExists(c, b)); + EXPECT_TRUE(graph.EdgeExists(a, c)); + EXPECT_TRUE(graph.EdgeExists(c, d)); + EXPECT_TRUE(graph.EdgeExists(b, d)); + EXPECT_FALSE(graph.EdgeExists(a, b)); + EXPECT_FALSE(graph.EdgeExists(b, a)); + + EXPECT_NO_THROW(graph.CheckForCycles()); +} + +TEST_F( + PluginGraphTest, + addGroupEdgesDoesNotDependOnGroupDefinitionOrderIfThereIsASingleLinearPath) { + std::vector> masterlistsGroups{ + {Group("B"), Group("C", {"B"}), Group("default", {"C"})}, + {Group("C", {"B"}), Group("B"), Group("default", {"C"})}}; + + for (const auto& masterlistGroups : masterlistsGroups) { + groupsMap = GetGroupsMap(masterlistGroups, {}); + predecessorGroupsMap = GetPredecessorGroups(masterlistGroups, {}); + + PluginGraph graph; + + const auto b = graph.AddVertex(CreatePluginSortingData("B.esp", "B")); + const auto c = graph.AddVertex(CreatePluginSortingData("C.esp", "C")); + const auto d = graph.AddVertex(CreatePluginSortingData("D.esp")); + + graph.AddEdge(d, b, EdgeType::master); + + graph.AddGroupEdges(groupsMap, predecessorGroupsMap); + + // Should be D.esp -> B.esp -> C.esp + EXPECT_TRUE(graph.EdgeExists(b, c)); + EXPECT_TRUE(graph.EdgeExists(d, b)); + EXPECT_FALSE(graph.EdgeExists(c, d)); + + EXPECT_NO_THROW(graph.CheckForCycles()); + } +} + +TEST_F(PluginGraphTest, + addGroupEdgesDependsOnGroupDefinitionOrderIfThereAreMultipleRoots) { + // Create a graph with groups in one order. + { + std::vector masterlistGroups{Group("A"), + Group("B"), + Group("C", {"A", "B"}), + Group("D", {"C"}), + Group()}; + + groupsMap = GetGroupsMap(masterlistGroups, {}); + predecessorGroupsMap = GetPredecessorGroups(masterlistGroups, {}); + + PluginGraph graph; + + const auto a = graph.AddVertex(CreatePluginSortingData("A.esp", "A")); + const auto b = graph.AddVertex(CreatePluginSortingData("B.esp", "B")); + const auto c = graph.AddVertex(CreatePluginSortingData("C.esp", "C")); + const auto d = graph.AddVertex(CreatePluginSortingData("D.esp", "D")); + + graph.AddEdge(d, a, EdgeType::master); + + graph.AddGroupEdges(groupsMap, predecessorGroupsMap); + + // Should be B.esp -> D.esp -> A.esp -> C.esp + // B.esp -------------------> + EXPECT_TRUE(graph.EdgeExists(d, a)); + EXPECT_TRUE(graph.EdgeExists(a, c)); + EXPECT_TRUE(graph.EdgeExists(b, c)); + EXPECT_TRUE(graph.EdgeExists(b, d)); + EXPECT_FALSE(graph.EdgeExists(a, b)); + EXPECT_FALSE(graph.EdgeExists(b, a)); + + // FIXME: This edge is unwanted and causes a cycle. + EXPECT_TRUE(graph.EdgeExists(c, d)); + + EXPECT_THROW(graph.CheckForCycles(), CyclicInteractionError); + } + + // Now do the same again, but with a different group order for A and B. + { + std::vector masterlistGroups{Group("B"), + Group("A"), + Group("C", {"A", "B"}), + Group("D", {"C"}), + Group()}; + + groupsMap = GetGroupsMap(masterlistGroups, {}); + predecessorGroupsMap = GetPredecessorGroups(masterlistGroups, {}); + + PluginGraph graph; + + const auto a = graph.AddVertex(CreatePluginSortingData("A.esp", "A")); + const auto b = graph.AddVertex(CreatePluginSortingData("B.esp", "B")); + const auto c = graph.AddVertex(CreatePluginSortingData("C.esp", "C")); + const auto d = graph.AddVertex(CreatePluginSortingData("D.esp", "D")); + + graph.AddEdge(d, a, EdgeType::master); + + graph.AddGroupEdges(groupsMap, predecessorGroupsMap); + + // Should be B.esp -> C.esp -> D.esp -> A.esp + EXPECT_TRUE(graph.EdgeExists(d, a)); + EXPECT_TRUE(graph.EdgeExists(b, c)); + EXPECT_TRUE(graph.EdgeExists(c, d)); + EXPECT_FALSE(graph.EdgeExists(a, b)); + EXPECT_FALSE(graph.EdgeExists(b, a)); + + // FIXME: This edge is unwanted and causes a cycle. + EXPECT_TRUE(graph.EdgeExists(a, c)); + + EXPECT_THROW(graph.CheckForCycles(), CyclicInteractionError); + } +} + +TEST_F(PluginGraphTest, addGroupEdgesDependsOnBranchingGroupDefinitionOrder) { + // Create a graph with groups in one order. + { + std::vector masterlistGroups{Group("A"), + Group("B", {"A"}), + Group("C", {"A"}), + Group("D", {"B", "C"}), + Group("E", {"D"}), + Group()}; + + groupsMap = GetGroupsMap(masterlistGroups, {}); + predecessorGroupsMap = GetPredecessorGroups(masterlistGroups, {}); + + PluginGraph graph; + + const auto a = graph.AddVertex(CreatePluginSortingData("A.esp", "A")); + const auto b = graph.AddVertex(CreatePluginSortingData("B.esp", "B")); + const auto c = graph.AddVertex(CreatePluginSortingData("C.esp", "C")); + const auto d = graph.AddVertex(CreatePluginSortingData("D.esp", "D")); + const auto e = graph.AddVertex(CreatePluginSortingData("E.esp", "E")); + + graph.AddEdge(e, c, EdgeType::master); + + graph.AddGroupEdges(groupsMap, predecessorGroupsMap); + + // Should be A.esp -> B.esp -> D.esp -> E.esp -> C.esp + EXPECT_TRUE(graph.EdgeExists(e, c)); + EXPECT_TRUE(graph.EdgeExists(a, b)); + EXPECT_TRUE(graph.EdgeExists(b, d)); + EXPECT_TRUE(graph.EdgeExists(d, e)); + EXPECT_TRUE(graph.EdgeExists(a, c)); + EXPECT_FALSE(graph.EdgeExists(b, c)); + EXPECT_FALSE(graph.EdgeExists(c, b)); + + // FIXME: This edge is unwanted and causes a cycle. + EXPECT_TRUE(graph.EdgeExists(c, d)); + + EXPECT_THROW(graph.CheckForCycles(), CyclicInteractionError); + } + + // Now do the same again, but with a different group order for B and C. + { + std::vector masterlistGroups{Group("A"), + Group("C", {"A"}), + Group("B", {"A"}), + Group("D", {"B", "C"}), + Group("E", {"D"}), + Group()}; + + groupsMap = GetGroupsMap(masterlistGroups, {}); + predecessorGroupsMap = GetPredecessorGroups(masterlistGroups, {}); + + PluginGraph graph; + + const auto a = graph.AddVertex(CreatePluginSortingData("A.esp", "A")); + const auto b = graph.AddVertex(CreatePluginSortingData("B.esp", "B")); + const auto c = graph.AddVertex(CreatePluginSortingData("C.esp", "C")); + const auto d = graph.AddVertex(CreatePluginSortingData("D.esp", "D")); + const auto e = graph.AddVertex(CreatePluginSortingData("E.esp", "E")); + + graph.AddEdge(e, c, EdgeType::master); + + graph.AddGroupEdges(groupsMap, predecessorGroupsMap); + + // Should be A.esp -> B.esp -> E.esp -> C.esp -> D.esp + EXPECT_TRUE(graph.EdgeExists(e, c)); + EXPECT_TRUE(graph.EdgeExists(a, b)); + EXPECT_TRUE(graph.EdgeExists(b, d)); + EXPECT_TRUE(graph.EdgeExists(b, e)); + EXPECT_TRUE(graph.EdgeExists(a, e)); + EXPECT_TRUE(graph.EdgeExists(a, c)); + EXPECT_TRUE(graph.EdgeExists(c, d)); + EXPECT_FALSE(graph.EdgeExists(b, c)); + EXPECT_FALSE(graph.EdgeExists(c, b)); + + // FIXME: This edge is unwanted and causes a cycle. + EXPECT_TRUE(graph.EdgeExists(d, e)); + + EXPECT_THROW(graph.CheckForCycles(), CyclicInteractionError); + } +} + +TEST_F(PluginGraphTest, addGroupEdgesDoesNotDependOnPluginGraphVertexOrder) { + std::vector masterlistGroups{ + Group("A"), Group("B", {"A"}), Group("C", {"B"}), Group()}; + + groupsMap = GetGroupsMap(masterlistGroups, {}); + predecessorGroupsMap = GetPredecessorGroups(masterlistGroups, {}); + + const auto a1Data = CreatePluginSortingData("A1.esp", "A"); + const auto a2Data = CreatePluginSortingData("A2.esp", "A"); + const auto bData = CreatePluginSortingData("B.esp", "B"); + const auto cData = CreatePluginSortingData("C.esp", "C"); + + const std::vector> variations{ + {a1Data, a2Data, bData, cData}, {a1Data, a2Data, cData, bData}, + {a1Data, bData, cData, a2Data}, {a1Data, bData, a2Data, cData}, + {a1Data, cData, a2Data, bData}, {a1Data, cData, bData, a2Data}, + + {a2Data, a1Data, bData, cData}, {a2Data, a1Data, cData, bData}, + {a2Data, bData, cData, a1Data}, {a2Data, bData, a1Data, cData}, + {a2Data, cData, a1Data, bData}, {a2Data, cData, bData, a1Data}, + + {bData, a2Data, a1Data, cData}, {bData, a2Data, cData, a1Data}, + {bData, a1Data, cData, a2Data}, {bData, a1Data, a2Data, cData}, + {bData, cData, a2Data, a1Data}, {bData, cData, a1Data, a2Data}, + + {cData, a2Data, bData, a1Data}, {cData, a2Data, a1Data, bData}, + {cData, bData, a1Data, a2Data}, {cData, bData, a2Data, a1Data}, + {cData, a1Data, a2Data, bData}, {cData, a1Data, bData, a2Data}, + }; + + for (const auto& pluginsSortingData : variations) { + PluginGraph graph; + + for (const auto& plugin : pluginsSortingData) { + graph.AddVertex(plugin); + } + + const auto a1 = graph.GetVertexByName("A1.esp").value(); + const auto a2 = graph.GetVertexByName("A2.esp").value(); + const auto b = graph.GetVertexByName("B.esp").value(); + const auto c = graph.GetVertexByName("C.esp").value(); + + graph.AddEdge(c, a1, EdgeType::master); + + graph.AddGroupEdges(groupsMap, predecessorGroupsMap); + + // Should be A2.esp -> C.esp -> A1.esp -> B.esp + // A2.esp --------------------> + ASSERT_TRUE(graph.EdgeExists(c, a1)); + ASSERT_TRUE(graph.EdgeExists(a1, b)); + ASSERT_TRUE(graph.EdgeExists(a2, b)); + ASSERT_TRUE(graph.EdgeExists(a2, c)); + ASSERT_FALSE(graph.EdgeExists(a1, a2)); + ASSERT_FALSE(graph.EdgeExists(a2, a1)); + + // FIXME: This edge is unwanted and causes a cycle. + ASSERT_TRUE(graph.EdgeExists(b, c)); + + EXPECT_THROW(graph.CheckForCycles(), CyclicInteractionError); + } +} + TEST_F(PluginGraphTest, addOverlapEdgesShouldNotAddEdgesBetweenNonOverlappingPlugins) { PluginGraph graph; diff --git a/src/tests/api/internals/sorting/plugin_sort_test.h b/src/tests/api/internals/sorting/plugin_sort_test.h index 5e5aef0f..65831805 100644 --- a/src/tests/api/internals/sorting/plugin_sort_test.h +++ b/src/tests/api/internals/sorting/plugin_sort_test.h @@ -38,7 +38,6 @@ protected: PluginSortTest() : game_(GetParam(), dataPath.parent_path(), localPath), blankEslEsp("Blank.esl.esp"), - masterlistPath_(metadataFilesPath / "userlist.yaml"), cccPath_(dataPath.parent_path() / getCCCFilename()) {} void loadInstalledPlugins(Game& game, bool headersOnly) { @@ -69,32 +68,6 @@ protected: game.LoadPlugins(plugins, headersOnly); } - void GenerateMasterlist() { - using std::endl; - - std::ofstream masterlist(masterlistPath_); - masterlist << "groups:" << endl - << " - name: earliest" << endl - << " - name: earlier" << endl - << " after:" << endl - << " - earliest" << endl - << " - name: default" << endl - << " after:" << endl - << " - earlier" << endl - << " - name: group1" << endl - << " - name: group2" << endl - << " after:" << endl - << " - group1" << endl - << " - name: group3" << endl - << " after:" << endl - << " - group2" << endl - << " - name: group4" << endl - << " after:" << endl - << " - default" << endl; - - masterlist.close(); - } - std::string getCCCFilename() { if (GetParam() == GameType::fo4) { return "Fallout4.ccc"; @@ -144,7 +117,6 @@ protected: Game game_; const std::string blankEslEsp; - const std::filesystem::path masterlistPath_; const std::filesystem::path cccPath_; private: @@ -230,18 +202,18 @@ TEST_P(PluginSortTest, } } -TEST_P(PluginSortTest, sortingShouldResolveGroupsAsTransitiveLoadAfterSets) { +TEST_P(PluginSortTest, + sortingShouldUseGroupMetadataWhenDecidingRelativePluginPositions) { ASSERT_NO_THROW(loadInstalledPlugins(game_, false)); - GenerateMasterlist(); - game_.GetDatabase().LoadLists(masterlistPath_); + game_.GetDatabase().SetUserGroups({Group("A"), Group("B", {"A"})}); PluginMetadata plugin(blankDifferentEsm); - plugin.SetGroup("group1"); + plugin.SetGroup("A"); game_.GetDatabase().SetPluginUserMetadata(plugin); plugin = PluginMetadata(blankEsm); - plugin.SetGroup("group3"); + plugin.SetGroup("B"); game_.GetDatabase().SetPluginUserMetadata(plugin); std::vector expectedSortedOrder({ @@ -330,236 +302,6 @@ TEST_P(PluginSortTest, sortingShouldThrowIfAPluginHasAGroupThatDoesNotExist) { EXPECT_THROW(SortPlugins(game_, game_.GetLoadOrder()), UndefinedGroupError); } -TEST_P(PluginSortTest, - sortingShouldIgnoreAGroupEdgeIfItWouldCauseACycleInIsolation) { - ASSERT_NO_THROW(loadInstalledPlugins(game_, false)); - - GenerateMasterlist(); - game_.GetDatabase().LoadLists(masterlistPath_); - - PluginMetadata plugin(blankEsm); - plugin.SetGroup("group4"); - game_.GetDatabase().SetPluginUserMetadata(plugin); - - std::vector expectedSortedOrder({ - masterFile, - blankDifferentEsm, - blankDifferentMasterDependentEsm, - blankEsm, - blankMasterDependentEsm, - blankEsp, - blankDifferentEsp, - blankMasterDependentEsp, - blankDifferentMasterDependentEsp, - blankPluginDependentEsp, - blankDifferentPluginDependentEsp, - }); - - if (GetParam() == GameType::fo4 || GetParam() == GameType::tes5se) { - expectedSortedOrder.insert(expectedSortedOrder.begin() + 3, blankEsl); - } - - std::vector sorted = SortPlugins(game_, game_.GetLoadOrder()); - EXPECT_EQ(expectedSortedOrder, sorted); -} - -TEST_P( - PluginSortTest, - sortingShouldIgnoreGroupEdgesInvolvedInABackCycleOfAGroupEdgeFromADefaultGroupPlugin) { - ASSERT_NO_THROW(loadInstalledPlugins(game_, false)); - - GenerateMasterlist(); - game_.GetDatabase().LoadLists(masterlistPath_); - - PluginMetadata plugin(blankEsp); - - plugin = PluginMetadata(blankDifferentMasterDependentEsp); - plugin.SetLoadAfterFiles({File(blankMasterDependentEsp)}); - game_.GetDatabase().SetPluginUserMetadata(plugin); - - plugin = PluginMetadata(blankDifferentEsp); - plugin.SetGroup("group1"); - game_.GetDatabase().SetPluginUserMetadata(plugin); - - plugin = PluginMetadata(blankMasterDependentEsp); - plugin.SetGroup("group2"); - game_.GetDatabase().SetPluginUserMetadata(plugin); - - std::vector expectedSortedOrder({ - masterFile, - blankEsm, - blankDifferentEsm, - blankMasterDependentEsm, - blankDifferentMasterDependentEsm, - blankEsp, - blankDifferentEsp, - blankMasterDependentEsp, - blankDifferentMasterDependentEsp, - blankPluginDependentEsp, - blankDifferentPluginDependentEsp, - }); - - if (GetParam() == GameType::fo4 || GetParam() == GameType::tes5se) { - expectedSortedOrder.insert(expectedSortedOrder.begin() + 5, blankEsl); - } - - std::vector sorted = SortPlugins(game_, game_.GetLoadOrder()); - EXPECT_EQ(expectedSortedOrder, sorted); -} - -TEST_P( - PluginSortTest, - sortingShouldIgnoreGroupEdgesInvolvedInABackCycleOfAGroupEdgeToADefaultGroupPlugin) { - ASSERT_NO_THROW(loadInstalledPlugins(game_, false)); - - GenerateMasterlist(); - game_.GetDatabase().LoadLists(masterlistPath_); - - PluginMetadata plugin(blankMasterDependentEsm); - plugin.SetGroup("earliest"); - game_.GetDatabase().SetPluginUserMetadata(plugin); - - plugin = PluginMetadata(blankDifferentEsm); - plugin.SetGroup("earlier"); - game_.GetDatabase().SetPluginUserMetadata(plugin); - - std::vector expectedSortedOrder({ - blankEsm, - blankMasterDependentEsm, - blankDifferentEsm, - blankDifferentMasterDependentEsm, - blankEsp, - blankDifferentEsp, - blankMasterDependentEsp, - blankDifferentMasterDependentEsp, - blankPluginDependentEsp, - blankDifferentPluginDependentEsp, - }); - - if (GetParam() == GameType::fo4 || GetParam() == GameType::tes5se) { - expectedSortedOrder.insert(expectedSortedOrder.begin(), masterFile); - expectedSortedOrder.insert(expectedSortedOrder.begin() + 5, blankEsl); - } else { - expectedSortedOrder.insert(expectedSortedOrder.begin() + 3, masterFile); - } - - std::vector sorted = SortPlugins(game_, game_.GetLoadOrder()); - EXPECT_EQ(expectedSortedOrder, sorted); -} - -TEST_P( - PluginSortTest, - sortingShouldThrowForAGroupEdgeThatCausesAMultiGroupCycleBetweenTwoNonDefaultGroups) { - ASSERT_NO_THROW(loadInstalledPlugins(game_, false)); - - GenerateMasterlist(); - game_.GetDatabase().LoadLists(masterlistPath_); - - PluginMetadata plugin(blankMasterDependentEsm); - plugin.SetGroup("earliest"); - game_.GetDatabase().SetPluginUserMetadata(plugin); - - plugin = PluginMetadata(blankDifferentEsm); - plugin.SetGroup("earlier"); - game_.GetDatabase().SetPluginUserMetadata(plugin); - - plugin = PluginMetadata(blankEsm); - plugin.SetGroup("group4"); - game_.GetDatabase().SetPluginUserMetadata(plugin); - - try { - SortPlugins(game_, game_.GetLoadOrder()); - FAIL(); - } catch (CyclicInteractionError &e) { - ASSERT_EQ(3, e.GetCycle().size()); - EXPECT_EQ("Blank - Different Master Dependent.esm", - e.GetCycle()[0].GetName()); - EXPECT_EQ(EdgeType::userGroup, e.GetCycle()[0].GetTypeOfEdgeToNextVertex()); - EXPECT_EQ("Blank.esm", e.GetCycle()[1].GetName()); - EXPECT_EQ(EdgeType::master, e.GetCycle()[1].GetTypeOfEdgeToNextVertex()); - EXPECT_EQ("Blank - Master Dependent.esm", e.GetCycle()[2].GetName()); - EXPECT_EQ(EdgeType::userGroup, e.GetCycle()[2].GetTypeOfEdgeToNextVertex()); - } -} - -TEST_P( - PluginSortTest, - sortingShouldNotIgnoreIntermediatePluginsInAMultiGroupCycleIfTheEarlierPluginIsNotAMasterAndTheLaterIs) { - ASSERT_NO_THROW(loadInstalledPlugins(game_, false)); - - GenerateMasterlist(); - game_.GetDatabase().LoadLists(masterlistPath_); - - PluginMetadata plugin(blankMasterDependentEsp); - plugin.SetGroup("earliest"); - game_.GetDatabase().SetPluginUserMetadata(plugin); - - plugin = PluginMetadata(blankDifferentEsm); - plugin.SetGroup("earlier"); - game_.GetDatabase().SetPluginUserMetadata(plugin); - - std::vector expectedSortedOrder({ - blankDifferentEsm, - blankEsm, - blankMasterDependentEsm, - blankDifferentMasterDependentEsm, - blankMasterDependentEsp, - blankEsp, - blankDifferentEsp, - blankDifferentMasterDependentEsp, - blankPluginDependentEsp, - blankDifferentPluginDependentEsp, - }); - - if (GetParam() == GameType::fo4 || GetParam() == GameType::tes5se) { - expectedSortedOrder.insert(expectedSortedOrder.begin(), masterFile); - expectedSortedOrder.insert(expectedSortedOrder.begin() + 5, blankEsl); - } else { - expectedSortedOrder.insert(expectedSortedOrder.begin() + 1, masterFile); - } - - std::vector sorted = SortPlugins(game_, game_.GetLoadOrder()); - EXPECT_EQ(expectedSortedOrder, sorted); -} - -TEST_P( - PluginSortTest, - sortingShouldNotIgnorePluginsInTheSameGroupAsTheTargetPluginOfAGroupEdgeThatCausesACycleInIsolation) { - ASSERT_NO_THROW(loadInstalledPlugins(game_, false)); - - GenerateMasterlist(); - game_.GetDatabase().LoadLists(masterlistPath_); - - PluginMetadata plugin(blankEsm); - plugin.SetGroup("group4"); - game_.GetDatabase().SetPluginUserMetadata(plugin); - - plugin = PluginMetadata(blankDifferentMasterDependentEsm); - plugin.SetGroup("group4"); - game_.GetDatabase().SetPluginUserMetadata(plugin); - - std::vector expectedSortedOrder({ - masterFile, - blankDifferentEsm, - blankEsm, - blankMasterDependentEsm, - blankDifferentMasterDependentEsm, - blankEsp, - blankDifferentEsp, - blankMasterDependentEsp, - blankDifferentMasterDependentEsp, - blankPluginDependentEsp, - blankDifferentPluginDependentEsp, - }); - - if (GetParam() == GameType::fo4 || GetParam() == GameType::tes5se) { - expectedSortedOrder.insert(expectedSortedOrder.begin() + 2, blankEsl); - } - - std::vector sorted = SortPlugins(game_, game_.GetLoadOrder()); - EXPECT_EQ(expectedSortedOrder, sorted); -} - TEST_P(PluginSortTest, sortingShouldUseLoadAfterMetadataWhenDecidingRelativePluginPositions) { ASSERT_NO_THROW(loadInstalledPlugins(game_, false));