topology: add initial module

To use this module, add information about boards and their exposes
records and then request a map of what resulting associations should be
exposed on D-Bus.

Tested: Unit tests pass

Signed-off-by: Benjamin Fair <benjaminfair@google.com>
Change-Id: I42cd79407ab476599f5010c11cc146a569694b24
diff --git a/include/topology.hpp b/include/topology.hpp
new file mode 100644
index 0000000..18c9244
--- /dev/null
+++ b/include/topology.hpp
@@ -0,0 +1,27 @@
+#pragma once
+
+#include <nlohmann/json.hpp>
+
+#include <set>
+#include <unordered_map>
+
+using Association = std::tuple<std::string, std::string, std::string>;
+
+class Topology
+{
+  public:
+    explicit Topology() = default;
+
+    void addBoard(const std::string& path, const std::string& boardType,
+                  const nlohmann::json& exposesItem);
+    std::unordered_map<std::string, std::vector<Association>> getAssocs();
+
+  private:
+    using Path = std::string;
+    using BoardType = std::string;
+    using PortType = std::string;
+
+    std::unordered_map<PortType, std::vector<Path>> upstreamPorts;
+    std::unordered_map<PortType, std::vector<Path>> downstreamPorts;
+    std::unordered_map<Path, BoardType> boardTypes;
+};
diff --git a/meson.build b/meson.build
index 105606f..05ade13 100644
--- a/meson.build
+++ b/meson.build
@@ -199,12 +199,15 @@
 if not build_tests.disabled()
     test_boost_args = boost_args + ['-DBOOST_ASIO_DISABLE_THREADS']
     gtest = dependency('gtest', main: true, disabler: true, required: false)
-    if not gtest.found() and build_tests.enabled()
+    gmock = dependency('gmock', disabler: true, required: false)
+    if not (gtest.found() and gmock.found()) and build_tests.enabled()
         cmake = import('cmake')
         gtest_subproject = cmake.subproject('gtest')
         cm_gtest = gtest_subproject.dependency('gtest')
         cm_gtest_main = gtest_subproject.dependency('gtest_main')
         gtest = declare_dependency(dependencies: [cm_gtest, cm_gtest_main, threads])
+        gmock = gtest_subproject.dependency('gmock')
+
     endif
 
     test(
@@ -243,4 +246,20 @@
             include_directories: 'include',
         )
     )
+
+    test(
+        'test_topology',
+        executable(
+            'test_topology',
+            'test/test_topology.cpp',
+            'src/topology.cpp',
+            cpp_args: test_boost_args,
+            dependencies: [
+                gtest,
+                gmock,
+            ],
+            implicit_include_directories: false,
+            include_directories: 'include',
+        )
+    )
 endif
diff --git a/src/topology.cpp b/src/topology.cpp
new file mode 100644
index 0000000..02e7458
--- /dev/null
+++ b/src/topology.cpp
@@ -0,0 +1,66 @@
+#include "topology.hpp"
+
+#include <iostream>
+
+void Topology::addBoard(const std::string& path, const std::string& boardType,
+                        const nlohmann::json& exposesItem)
+{
+    auto findType = exposesItem.find("Type");
+    if (findType == exposesItem.end())
+    {
+        return;
+    }
+    PortType exposesType = findType->get<std::string>();
+
+    if (exposesType == "DownstreamPort")
+    {
+        auto findConnectsTo = exposesItem.find("ConnectsToType");
+        if (findConnectsTo == exposesItem.end())
+        {
+            std::cerr << "Board at path " << path
+                      << " is missing ConnectsToType" << std::endl;
+            return;
+        }
+        PortType connectsTo = findConnectsTo->get<std::string>();
+
+        downstreamPorts[connectsTo].emplace_back(path);
+        boardTypes[path] = boardType;
+    }
+    else if (exposesType.ends_with("Port"))
+    {
+        upstreamPorts[exposesType].emplace_back(path);
+        boardTypes[path] = boardType;
+    }
+}
+
+std::unordered_map<std::string, std::vector<Association>> Topology::getAssocs()
+{
+    std::unordered_map<std::string, std::vector<Association>> result;
+
+    // look at each upstream port type
+    for (const auto& upstreamPortPair : upstreamPorts)
+    {
+        auto downstreamMatch = downstreamPorts.find(upstreamPortPair.first);
+
+        if (downstreamMatch == downstreamPorts.end())
+        {
+            // no match
+            continue;
+        }
+
+        for (const Path& upstream : upstreamPortPair.second)
+        {
+            if (boardTypes[upstream] == "Chassis" ||
+                boardTypes[upstream] == "Board")
+            {
+                for (const Path& downstream : downstreamMatch->second)
+                {
+                    result[downstream].emplace_back("contained_by",
+                                                    "containing", upstream);
+                }
+            }
+        }
+    }
+
+    return result;
+}
diff --git a/test/test_topology.cpp b/test/test_topology.cpp
new file mode 100644
index 0000000..76ab54a
--- /dev/null
+++ b/test/test_topology.cpp
@@ -0,0 +1,187 @@
+#include "topology.hpp"
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+using ::testing::UnorderedElementsAre;
+
+const std::string subchassisPath =
+    "/xyz/openbmc_project/inventory/system/chassis/Subchassis";
+const std::string superchassisPath =
+    "/xyz/openbmc_project/inventory/system/chassis/Superchassis";
+
+const Association subchassisAssoc =
+    std::make_tuple("contained_by", "containing", superchassisPath);
+
+const nlohmann::json subchassisExposesItem = nlohmann::json::parse(R"(
+    {
+        "ConnectsToType": "BackplanePort",
+        "Name": "MyDownstreamPort",
+        "Type": "DownstreamPort"
+    }
+)");
+
+const nlohmann::json superchassisExposesItem = nlohmann::json::parse(R"(
+    {
+        "Name": "MyBackplanePort",
+        "Type": "BackplanePort"
+    }
+)");
+
+const nlohmann::json otherExposesItem = nlohmann::json::parse(R"(
+    {
+        "Name": "MyExposes",
+        "Type": "OtherType"
+    }
+)");
+
+TEST(Topology, Empty)
+{
+    Topology topo;
+
+    auto assocs = topo.getAssocs();
+
+    EXPECT_EQ(assocs.size(), 0);
+}
+
+TEST(Topology, EmptyExposes)
+{
+    Topology topo;
+
+    topo.addBoard(subchassisPath, "Chassis", nlohmann::json());
+    topo.addBoard(superchassisPath, "Chassis", nlohmann::json());
+
+    auto assocs = topo.getAssocs();
+
+    EXPECT_EQ(assocs.size(), 0);
+}
+
+TEST(Topology, MissingConnectsTo)
+{
+    const nlohmann::json subchassisMissingConnectsTo = nlohmann::json::parse(R"(
+        {
+            "Name": "MyDownstreamPort",
+            "Type": "DownstreamPort"
+        }
+    )");
+
+    Topology topo;
+
+    topo.addBoard(subchassisPath, "Chassis", subchassisMissingConnectsTo);
+    topo.addBoard(superchassisPath, "Chassis", superchassisExposesItem);
+
+    auto assocs = topo.getAssocs();
+
+    EXPECT_EQ(assocs.size(), 0);
+}
+
+TEST(Topology, OtherExposes)
+{
+    Topology topo;
+
+    topo.addBoard(subchassisPath, "Chassis", otherExposesItem);
+    topo.addBoard(superchassisPath, "Chassis", otherExposesItem);
+
+    auto assocs = topo.getAssocs();
+
+    EXPECT_EQ(assocs.size(), 0);
+}
+
+TEST(Topology, NoMatchSubchassis)
+{
+    Topology topo;
+
+    topo.addBoard(subchassisPath, "Chassis", otherExposesItem);
+    topo.addBoard(superchassisPath, "Chassis", superchassisExposesItem);
+
+    auto assocs = topo.getAssocs();
+
+    EXPECT_EQ(assocs.size(), 0);
+}
+
+TEST(Topology, NoMatchSuperchassis)
+{
+    Topology topo;
+
+    topo.addBoard(subchassisPath, "Chassis", subchassisExposesItem);
+    topo.addBoard(superchassisPath, "Chassis", otherExposesItem);
+
+    auto assocs = topo.getAssocs();
+
+    EXPECT_EQ(assocs.size(), 0);
+}
+
+TEST(Topology, Basic)
+{
+    Topology topo;
+
+    topo.addBoard(subchassisPath, "Chassis", subchassisExposesItem);
+    topo.addBoard(superchassisPath, "Chassis", superchassisExposesItem);
+
+    auto assocs = topo.getAssocs();
+
+    EXPECT_EQ(assocs.size(), 1);
+    EXPECT_EQ(assocs[subchassisPath].size(), 1);
+    EXPECT_EQ(assocs[subchassisPath][0], subchassisAssoc);
+}
+
+TEST(Topology, 2Subchassis)
+{
+    Topology topo;
+
+    topo.addBoard(subchassisPath, "Chassis", subchassisExposesItem);
+    topo.addBoard(subchassisPath + "2", "Chassis", subchassisExposesItem);
+    topo.addBoard(superchassisPath, "Chassis", superchassisExposesItem);
+
+    auto assocs = topo.getAssocs();
+
+    EXPECT_EQ(assocs.size(), 2);
+    EXPECT_EQ(assocs[subchassisPath].size(), 1);
+    EXPECT_EQ(assocs[subchassisPath][0], subchassisAssoc);
+    EXPECT_EQ(assocs[subchassisPath + "2"].size(), 1);
+    EXPECT_EQ(assocs[subchassisPath + "2"][0], subchassisAssoc);
+}
+
+TEST(Topology, 2Superchassis)
+{
+    const Association subchassisAssoc2 =
+        std::make_tuple("contained_by", "containing", superchassisPath + "2");
+
+    Topology topo;
+
+    topo.addBoard(subchassisPath, "Chassis", subchassisExposesItem);
+    topo.addBoard(superchassisPath, "Chassis", superchassisExposesItem);
+    topo.addBoard(superchassisPath + "2", "Chassis", superchassisExposesItem);
+
+    auto assocs = topo.getAssocs();
+
+    EXPECT_EQ(assocs.size(), 1);
+    EXPECT_EQ(assocs[subchassisPath].size(), 2);
+
+    EXPECT_THAT(assocs[subchassisPath],
+                UnorderedElementsAre(subchassisAssoc, subchassisAssoc2));
+}
+
+TEST(Topology, 2SuperchassisAnd2Subchassis)
+{
+    const Association subchassisAssoc2 =
+        std::make_tuple("contained_by", "containing", superchassisPath + "2");
+
+    Topology topo;
+
+    topo.addBoard(subchassisPath, "Chassis", subchassisExposesItem);
+    topo.addBoard(subchassisPath + "2", "Chassis", subchassisExposesItem);
+    topo.addBoard(superchassisPath, "Chassis", superchassisExposesItem);
+    topo.addBoard(superchassisPath + "2", "Chassis", superchassisExposesItem);
+
+    auto assocs = topo.getAssocs();
+
+    EXPECT_EQ(assocs.size(), 2);
+    EXPECT_EQ(assocs[subchassisPath].size(), 2);
+    EXPECT_EQ(assocs[subchassisPath + "2"].size(), 2);
+
+    EXPECT_THAT(assocs[subchassisPath],
+                UnorderedElementsAre(subchassisAssoc, subchassisAssoc2));
+    EXPECT_THAT(assocs[subchassisPath + "2"],
+                UnorderedElementsAre(subchassisAssoc, subchassisAssoc2));
+}