test: topology: new configurations

Add tests for the new topology schema.

New testcases:
- 2 powersupplies powering a board, PSUs and board contained by a
  chassis. Similar to a Tyan S8030.
- 4 Blades, each contained by a SubChassis, all 4 SubChassis contained
  by a SuperChassis. (modeled after Yv3 [1])

Tested: Topology Unit Tests Pass

References:
[1] https://www.opencompute.org/products/237/wiwynn-yosemite-v3-server

Change-Id: I312c93e669915145bfa6d9738fafc35b4506b728
Signed-off-by: Alexander Hansen <alexander.hansen@9elements.com>
diff --git a/test/test_topology.cpp b/test/test_topology.cpp
index f04ce4f..b8b9c4d 100644
--- a/test/test_topology.cpp
+++ b/test/test_topology.cpp
@@ -319,3 +319,236 @@
         EXPECT_EQ(assocs.size(), 0U);
     }
 }
+
+TEST(Topology, SimilarToTyanS8030)
+{
+    const std::string chassisPath =
+        "/xyz/openbmc_project/inventory/system/chassis/ChassisA";
+    const std::string boardPath =
+        "/xyz/openbmc_project/inventory/system/board/BoardA";
+    const std::string psu0Path =
+        "/xyz/openbmc_project/inventory/system/powersupply/PSU0";
+    const std::string psu1Path =
+        "/xyz/openbmc_project/inventory/system/powersupply/PSU1";
+
+    const nlohmann::json chassisContainExposesItem = nlohmann::json::parse(R"(
+    {
+        "Name": "GenericContainPort",
+        "PortType": "containing",
+        "Type": "Port"
+    }
+)");
+    const nlohmann::json containedByExposesItem = nlohmann::json::parse(R"(
+    {
+        "Name": "GenericContainPort",
+        "PortType": "contained_by",
+        "Type": "Port"
+    }
+)");
+    const nlohmann::json boardPowerExposesItem = nlohmann::json::parse(R"(
+    {
+        "Name": "GenericPowerPort",
+        "PortType": "powered_by",
+        "Type": "Port"
+    }
+)");
+    const nlohmann::json psuPowerExposesItem = nlohmann::json::parse(R"(
+    {
+        "Name": "GenericPowerPort",
+        "PortType": "powering",
+        "Type": "Port"
+    }
+)");
+    Topology topo;
+    BoardMap boards{
+        {chassisPath, "ChassisA"},
+        {boardPath, "BoardA"},
+        {psu0Path, "PSU0"},
+        {psu1Path, "PSU1"},
+    };
+
+    // configure the chassis to be containing something
+    topo.addBoard(chassisPath, "Chassis", "ChassisA",
+                  chassisContainExposesItem);
+
+    // configure board to be contained by something
+    topo.addBoard(boardPath, "Board", "BoardA", containedByExposesItem);
+
+    // configure the board to be powered by something
+    topo.addBoard(boardPath, "Board", "BoardA", boardPowerExposesItem);
+
+    // configure the PSUs to be powering something
+    topo.addBoard(psu0Path, "PowerSupply", "PSU0", psuPowerExposesItem);
+    topo.addBoard(psu1Path, "PowerSupply", "PSU1", psuPowerExposesItem);
+
+    // configured PSUs to be contained by something
+    topo.addBoard(psu0Path, "PowerSupply", "PSU0", containedByExposesItem);
+    topo.addBoard(psu1Path, "PowerSupply", "PSU1", containedByExposesItem);
+
+    auto assocs = topo.getAssocs(std::views::keys(boards));
+
+    EXPECT_TRUE(assocs.contains(boardPath));
+    EXPECT_TRUE(assocs.contains(psu0Path));
+    EXPECT_TRUE(assocs.contains(psu0Path));
+
+    // expect chassis to contain board
+    EXPECT_EQ(assocs[boardPath].size(), 1);
+    EXPECT_TRUE(assocs[boardPath].contains(
+        {"contained_by", "containing", chassisPath}));
+
+    // expect powering association from each PSU to the board
+    // and expect each PSU to be contained by the chassis
+    EXPECT_EQ(assocs[psu0Path].size(), 2);
+    EXPECT_TRUE(
+        assocs[psu0Path].contains({"powering", "powered_by", boardPath}));
+    EXPECT_TRUE(
+        assocs[psu0Path].contains({"contained_by", "containing", chassisPath}));
+
+    EXPECT_EQ(assocs[psu1Path].size(), 2);
+    EXPECT_TRUE(
+        assocs[psu1Path].contains({"powering", "powered_by", boardPath}));
+    EXPECT_TRUE(
+        assocs[psu1Path].contains({"contained_by", "containing", chassisPath}));
+}
+
+static nlohmann::json makeExposesItem(const std::string& name,
+                                      const std::string& assocName)
+{
+    nlohmann::json exposesItem = nlohmann::json::parse(R"(
+    {
+        "Name": "REPLACE",
+        "PortType": "REPLACE",
+        "Type": "Port"
+    }
+)");
+
+    exposesItem["Name"] = name;
+    exposesItem["PortType"] = assocName;
+
+    return exposesItem;
+}
+
+static nlohmann::json makeContainedPortExposesItem(const std::string& name)
+{
+    return makeExposesItem(name, "contained_by");
+}
+
+static nlohmann::json makeContainingPortExposesItem(const std::string& name)
+{
+    return makeExposesItem(name, "containing");
+}
+
+TEST(Topology, SimilarToYosemiteV3)
+{
+    const std::string blade1Path =
+        "/xyz/openbmc_project/inventory/system/board/Blade1";
+    const std::string blade2Path =
+        "/xyz/openbmc_project/inventory/system/board/Blade2";
+    const std::string blade3Path =
+        "/xyz/openbmc_project/inventory/system/board/Blade3";
+    const std::string blade4Path =
+        "/xyz/openbmc_project/inventory/system/board/Blade4";
+    const std::string chassis1Path =
+        "/xyz/openbmc_project/inventory/system/chassis/Blade1Chassis";
+    const std::string chassis2Path =
+        "/xyz/openbmc_project/inventory/system/chassis/Blade2Chassis";
+    const std::string chassis3Path =
+        "/xyz/openbmc_project/inventory/system/chassis/Blade3Chassis";
+    const std::string chassis4Path =
+        "/xyz/openbmc_project/inventory/system/chassis/Blade4Chassis";
+    const std::string superChassisPath =
+        "/xyz/openbmc_project/inventory/system/chassis/SuperChassis";
+
+    const nlohmann::json blade1ExposesItem =
+        makeContainedPortExposesItem("Blade1Port");
+    const nlohmann::json blade2ExposesItem =
+        makeContainedPortExposesItem("Blade2Port");
+    const nlohmann::json blade3ExposesItem =
+        makeContainedPortExposesItem("Blade3Port");
+    const nlohmann::json blade4ExposesItem =
+        makeContainedPortExposesItem("Blade4Port");
+
+    const nlohmann::json chassis1ExposesItem =
+        makeContainingPortExposesItem("Blade1Port");
+    const nlohmann::json chassis2ExposesItem =
+        makeContainingPortExposesItem("Blade2Port");
+    const nlohmann::json chassis3ExposesItem =
+        makeContainingPortExposesItem("Blade3Port");
+    const nlohmann::json chassis4ExposesItem =
+        makeContainingPortExposesItem("Blade4Port");
+
+    const nlohmann::json chassis1ExposesItem2 =
+        makeContainedPortExposesItem("SuperChassisPort");
+    const nlohmann::json chassis2ExposesItem2 =
+        makeContainedPortExposesItem("SuperChassisPort");
+    const nlohmann::json chassis3ExposesItem2 =
+        makeContainedPortExposesItem("SuperChassisPort");
+    const nlohmann::json chassis4ExposesItem2 =
+        makeContainedPortExposesItem("SuperChassisPort");
+
+    const nlohmann::json superChassisExposesItem =
+        makeContainingPortExposesItem("SuperChassisPort");
+
+    Topology topo;
+    BoardMap boards{
+        {blade1Path, "Blade1"},
+        {blade2Path, "Blade2"},
+        {blade3Path, "Blade3"},
+        {blade4Path, "Blade4"},
+        {chassis1Path, "Chassis1"},
+        {chassis2Path, "Chassis2"},
+        {chassis3Path, "Chassis3"},
+        {chassis4Path, "Chassis4"},
+        {superChassisPath, "SuperChassis"},
+    };
+
+    topo.addBoard(blade1Path, "Board", "Blade1", blade1ExposesItem);
+    topo.addBoard(blade2Path, "Board", "Blade2", blade2ExposesItem);
+    topo.addBoard(blade3Path, "Board", "Blade3", blade3ExposesItem);
+    topo.addBoard(blade4Path, "Board", "Blade4", blade4ExposesItem);
+
+    topo.addBoard(chassis1Path, "Chassis", "Chassis1", chassis1ExposesItem);
+    topo.addBoard(chassis2Path, "Chassis", "Chassis2", chassis2ExposesItem);
+    topo.addBoard(chassis3Path, "Chassis", "Chassis3", chassis3ExposesItem);
+    topo.addBoard(chassis4Path, "Chassis", "Chassis4", chassis4ExposesItem);
+
+    topo.addBoard(chassis1Path, "Chassis", "Chassis1", chassis1ExposesItem2);
+    topo.addBoard(chassis2Path, "Chassis", "Chassis2", chassis2ExposesItem2);
+    topo.addBoard(chassis3Path, "Chassis", "Chassis3", chassis3ExposesItem2);
+    topo.addBoard(chassis4Path, "Chassis", "Chassis4", chassis4ExposesItem2);
+
+    topo.addBoard(superChassisPath, "Chassis", "SuperChassis",
+                  superChassisExposesItem);
+
+    auto assocs = topo.getAssocs(std::views::keys(boards));
+
+    // all blades are contained by their respective chassis
+    EXPECT_EQ(assocs[blade1Path].size(), 1);
+    EXPECT_TRUE(assocs[blade1Path].contains(
+        {"contained_by", "containing", chassis1Path}));
+
+    EXPECT_EQ(assocs[blade2Path].size(), 1);
+    EXPECT_TRUE(assocs[blade2Path].contains(
+        {"contained_by", "containing", chassis2Path}));
+
+    EXPECT_EQ(assocs[blade3Path].size(), 1);
+    EXPECT_TRUE(assocs[blade3Path].contains(
+        {"contained_by", "containing", chassis3Path}));
+
+    EXPECT_EQ(assocs[blade4Path].size(), 1);
+    EXPECT_TRUE(assocs[blade4Path].contains(
+        {"contained_by", "containing", chassis4Path}));
+
+    // all chassis are contained by the superchassis
+    EXPECT_TRUE(assocs[chassis1Path].contains(
+        {"contained_by", "containing", superChassisPath}));
+
+    EXPECT_TRUE(assocs[chassis2Path].contains(
+        {"contained_by", "containing", superChassisPath}));
+
+    EXPECT_TRUE(assocs[chassis3Path].contains(
+        {"contained_by", "containing", superChassisPath}));
+
+    EXPECT_TRUE(assocs[chassis4Path].contains(
+        {"contained_by", "containing", superChassisPath}));
+}