Get psu inventory path

Invoke GetSubTreePaths on configurable psu inventory base path to get
all the PSU paths.
Add unit test case and meson.build to build the unit test case.

In test/meson.build, manually adding dynamic_linker related options for
OE SDK environment to link_args, so that the built test binary could be
executed in OE SDK environment. Otherwise, the ld will not find the
correct shared libraries in OE SDK.

And you have to execute the test binaries manually in OE SDK, because
meson skips running the tests due to it thinks it's cross compiling.
This is described in README as well.

Tested: Verify the build and unit test run OK in OE SDK
        environment and in OpenBMC CI.
        Verify it gets the PSU inventory paths correctly on Witherspoon.

Signed-off-by: Lei YU <mine260309@gmail.com>
Change-Id: I2e2003c5897d7a623fad7f87d263b71c926fc46d
diff --git a/README b/README
index 14ace83..8f3921a 100644
--- a/README
+++ b/README
@@ -13,3 +13,15 @@
 meson build/ && ninja -C build
 ```
 
+## Unit test
+
+* Run it in OpenBMC CI, refer to [local-ci-build.md][1]
+* Run it in [OE SDK][2], run below commands in a x86-64 SDK env:
+   ```
+   meson -Doe-sdk=enabled -Dtests=enabled build/
+   ninja -C build/ test  # Meson skips running the case due to it thinks it's cross compiling
+   ./build/test/utest  # Manually run the test
+   ```
+
+[1]: https://github.com/openbmc/docs/blob/master/local-ci-build.md
+[2]: https://github.com/openbmc/docs/blob/master/cheatsheet.md#building-the-openbmc-sdk
diff --git a/meson.build b/meson.build
index bc5e1b8..569eab9 100644
--- a/meson.build
+++ b/meson.build
@@ -15,4 +15,33 @@
                copy: true,
                install_dir: servicedir)
 
+# Common configurations for src and test
+cdata = configuration_data()
+cdata.set_quoted('VERSION_IFACE', 'xyz.openbmc_project.Software.Version')
+cdata.set_quoted('FILEPATH_IFACE', 'xyz.openbmc_project.Common.FilePath')
+cdata.set_quoted('BUSNAME_UPDATER', 'xyz.openbmc_project.Software.Psu.Updater')
+cdata.set_quoted('PSU_INVENTORY_IFACE', 'xyz.openbmc_project.Inventory.Item.PowerSupply')
+cdata.set_quoted('ACTIVATION_FWD_ASSOCIATION', 'inventory')
+cdata.set_quoted('ACTIVATION_REV_ASSOCIATION', 'activation')
+cdata.set_quoted('ACTIVE_FWD_ASSOCIATION', 'active')
+cdata.set_quoted('ACTIVE_REV_ASSOCIATION', 'software_version')
+cdata.set_quoted('FUNCTIONAL_FWD_ASSOCIATION', 'functional')
+cdata.set_quoted('FUNCTIONAL_REV_ASSOCIATION', 'software_version')
+
+cdata.set_quoted('SOFTWARE_OBJPATH', get_option('SOFTWARE_OBJPATH'))
+cdata.set_quoted('MANIFEST_FILE', get_option('MANIFEST_FILE'))
+cdata.set_quoted('PSU_INVENTORY_PATH_BASE', get_option('PSU_INVENTORY_PATH_BASE'))
+
+phosphor_dbus_interfaces = dependency('phosphor-dbus-interfaces')
+phosphor_logging = dependency('phosphor-logging')
+sdbusplus = dependency('sdbusplus')
+
+add_project_link_arguments(['-lstdc++fs'], language: 'cpp')
+
 subdir('src')
+
+build_tests = get_option('tests')
+
+if not build_tests.disabled()
+  subdir('test')
+endif
diff --git a/meson_options.txt b/meson_options.txt
index b878b69..e5297cf 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -1,3 +1,6 @@
+option('tests', type: 'feature', description: 'Build tests')
+option('oe-sdk', type: 'feature', description: 'Enable OE SDK')
+
 option('MANIFEST_FILE',
        type: 'string',
        value: 'MANIFEST',
@@ -8,7 +11,7 @@
        value: '/xyz/openbmc_project/software',
        description: 'The software manager Dbus root')
 
-option('PSU_INVENTORY_PATH',
+option('PSU_INVENTORY_PATH_BASE',
        type: 'string',
-       value: '/xyz/openbmc_project/inventory/system/chassis',
-       description: 'The PSU inventory path')
+       value: '/xyz/openbmc_project/inventory/system',
+       description: 'The base path for PSU inventory')
diff --git a/src/item_updater.cpp b/src/item_updater.cpp
index cfbadb0..b670735 100644
--- a/src/item_updater.cpp
+++ b/src/item_updater.cpp
@@ -92,7 +92,7 @@
 
         associations.emplace_back(std::make_tuple(ACTIVATION_FWD_ASSOCIATION,
                                                   ACTIVATION_REV_ASSOCIATION,
-                                                  PSU_INVENTORY_PATH));
+                                                  PSU_INVENTORY_PATH_BASE));
 
         fs::path manifestPath(filePath);
         manifestPath /= MANIFEST_FILE;
diff --git a/src/item_updater.hpp b/src/item_updater.hpp
index 96006f4..aacd77f 100644
--- a/src/item_updater.hpp
+++ b/src/item_updater.hpp
@@ -4,8 +4,10 @@
 
 #include "activation.hpp"
 #include "types.hpp"
+#include "utils.hpp"
 #include "version.hpp"
 
+#include <phosphor-logging/log.hpp>
 #include <sdbusplus/server.hpp>
 #include <xyz/openbmc_project/Association/Definitions/server.hpp>
 #include <xyz/openbmc_project/Collection/DeleteAll/server.hpp>
@@ -43,6 +45,13 @@
                      std::bind(std::mem_fn(&ItemUpdater::createActivation),
                                this, std::placeholders::_1))
     {
+        // TODO: create psu inventory objects based on the paths
+        using namespace phosphor::logging;
+        auto paths = utils::getPSUInventoryPath(bus);
+        for (const auto& p : paths)
+        {
+            log<level::INFO>("PSU path", entry("PATH=%s", p.c_str()));
+        }
     }
 
     /** @brief Deletes version
diff --git a/src/meson.build b/src/meson.build
index 95b1a5a..4e2e126 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -1,28 +1,7 @@
-phosphor_dbus_interfaces = dependency('phosphor-dbus-interfaces')
-phosphor_logging = dependency('phosphor-logging')
-sdbusplus = dependency('sdbusplus')
-
-add_project_link_arguments(['-lstdc++fs'], language: 'cpp')
-
-cdata = configuration_data()
-cdata.set_quoted('VERSION_IFACE', 'xyz.openbmc_project.Software.Version')
-cdata.set_quoted('FILEPATH_IFACE', 'xyz.openbmc_project.Common.FilePath')
-cdata.set_quoted('BUSNAME_UPDATER', 'xyz.openbmc_project.Software.Psu.Updater')
-cdata.set_quoted('ACTIVATION_FWD_ASSOCIATION', 'inventory')
-cdata.set_quoted('ACTIVATION_REV_ASSOCIATION', 'activation')
-cdata.set_quoted('ACTIVE_FWD_ASSOCIATION', 'active')
-cdata.set_quoted('ACTIVE_REV_ASSOCIATION', 'software_version')
-cdata.set_quoted('FUNCTIONAL_FWD_ASSOCIATION', 'functional')
-cdata.set_quoted('FUNCTIONAL_REV_ASSOCIATION', 'software_version')
-
-cdata.set_quoted('SOFTWARE_OBJPATH', get_option('SOFTWARE_OBJPATH'))
-cdata.set_quoted('MANIFEST_FILE', get_option('MANIFEST_FILE'))
-cdata.set_quoted('PSU_INVENTORY_PATH', get_option('PSU_INVENTORY_PATH'))
-
 configure_file(output: 'config.h',
   configuration: cdata,
 )
-configuration_inc = include_directories('.')
+psu_inc = include_directories('.')
 
 executable(
   'phosphor-psu-code-manager',
@@ -30,7 +9,8 @@
   'item_updater.cpp',
   'main.cpp',
   'version.cpp',
-  include_directories: configuration_inc,
+  'utils.cpp',
+  include_directories: psu_inc,
   dependencies: [
     phosphor_logging,
     phosphor_dbus_interfaces,
diff --git a/src/utils.cpp b/src/utils.cpp
new file mode 100644
index 0000000..deb5eee
--- /dev/null
+++ b/src/utils.cpp
@@ -0,0 +1,31 @@
+#include "config.h"
+
+#include "utils.hpp"
+
+#include <fstream>
+
+namespace utils
+{
+
+namespace // anonymous
+{
+constexpr auto MAPPER_BUSNAME = "xyz.openbmc_project.ObjectMapper";
+constexpr auto MAPPER_PATH = "/xyz/openbmc_project/object_mapper";
+constexpr auto MAPPER_INTERFACE = "xyz.openbmc_project.ObjectMapper";
+} // namespace
+
+std::vector<std::string> getPSUInventoryPath(sdbusplus::bus::bus& bus)
+{
+    std::vector<std::string> paths;
+    auto method = bus.new_method_call(MAPPER_BUSNAME, MAPPER_PATH,
+                                      MAPPER_INTERFACE, "GetSubTreePaths");
+    method.append(PSU_INVENTORY_PATH_BASE);
+    method.append(0); // Depth 0 to search all
+    method.append(std::vector<std::string>({PSU_INVENTORY_IFACE}));
+    auto reply = bus.call(method);
+
+    reply.read(paths);
+    return paths;
+}
+
+} // namespace utils
diff --git a/src/utils.hpp b/src/utils.hpp
new file mode 100644
index 0000000..5203509
--- /dev/null
+++ b/src/utils.hpp
@@ -0,0 +1,15 @@
+#pragma once
+
+#include <sdbusplus/bus.hpp>
+#include <string>
+#include <vector>
+
+namespace utils
+{
+
+/**
+ * @brief Get PSU inventory object path from DBus
+ */
+std::vector<std::string> getPSUInventoryPath(sdbusplus::bus::bus& bus);
+
+} // namespace utils
diff --git a/test/meson.build b/test/meson.build
new file mode 100644
index 0000000..4d644cf
--- /dev/null
+++ b/test/meson.build
@@ -0,0 +1,39 @@
+oe_sdk = get_option('oe-sdk')
+if oe_sdk.enabled()
+  # Setup OE SYSROOT
+  OECORE_TARGET_SYSROOT = run_command('sh', '-c', 'echo $OECORE_TARGET_SYSROOT').stdout().strip()
+  if OECORE_TARGET_SYSROOT == ''
+      error('Unable to get $OECORE_TARGET_SYSROOT, check your environment.')
+  endif
+  message('OE_SYSROOT: ' + OECORE_TARGET_SYSROOT)
+  rpath = ':'.join([OECORE_TARGET_SYSROOT + '/lib', OECORE_TARGET_SYSROOT + '/usr/lib'])
+  ld_so = run_command('sh', '-c', 'find ' + OECORE_TARGET_SYSROOT + '/lib/ld-*.so | sort -r -n | head -n1').stdout().strip()
+  dynamic_linker = ['-Wl,-dynamic-linker,' + ld_so]
+else
+  dynamic_linker = []
+endif
+
+gtest = dependency('gtest', main: true, disabler: true, required: build_tests)
+gmock = dependency('gmock', disabler: true, required: build_tests)
+
+configure_file(output: 'config.h',
+  configuration: cdata,
+)
+test_inc = include_directories('.')
+
+utest = executable(
+  'utest',
+  '../src/utils.cpp',
+  'test_utils.cpp',
+  include_directories: [psu_inc, test_inc],
+  link_args: dynamic_linker,
+  build_rpath: oe_sdk.enabled() ? rpath : '',
+  dependencies: [
+    gtest,
+    gmock,
+    phosphor_logging,
+    phosphor_dbus_interfaces,
+    sdbusplus,
+  ])
+
+test('all', utest)
diff --git a/test/test_utils.cpp b/test/test_utils.cpp
new file mode 100644
index 0000000..08eecf7
--- /dev/null
+++ b/test/test_utils.cpp
@@ -0,0 +1,62 @@
+#include "utils.hpp"
+
+#include <sdbusplus/test/sdbus_mock.hpp>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+using ::testing::_;
+using ::testing::Invoke;
+using ::testing::IsNull;
+using ::testing::Return;
+using ::testing::StrEq;
+
+TEST(Utils, GetPSUInventoryPath)
+{
+    sdbusplus::SdBusMock sdbusMock;
+    auto bus = sdbusplus::get_mocked_new(&sdbusMock);
+
+    EXPECT_CALL(sdbusMock, sd_bus_message_new_method_call(
+                               _, _, _, _, _, StrEq("GetSubTreePaths")));
+
+    EXPECT_CALL(sdbusMock, sd_bus_message_ref(IsNull()))
+        .WillOnce(Return(nullptr));
+    sdbusplus::message::message msg(nullptr, &sdbusMock);
+
+    const char* path0 = "/com/example/chassis/powersupply0";
+    const char* path1 = "/com/example/chassis/powersupply1";
+
+    // std::vector
+    EXPECT_CALL(sdbusMock,
+                sd_bus_message_enter_container(IsNull(), 'a', StrEq("s")))
+        .WillOnce(Return(1));
+
+    // while !at_end()
+    EXPECT_CALL(sdbusMock, sd_bus_message_at_end(IsNull(), 0))
+        .WillOnce(Return(0))
+        .WillOnce(Return(0))
+        .WillOnce(Return(1)); // So it exits the loop after reading two strings.
+
+    // std::string
+    EXPECT_CALL(sdbusMock, sd_bus_message_read_basic(IsNull(), 's', _))
+        .WillOnce(Invoke([&](sd_bus_message*, char, void* p) {
+            const char** s = static_cast<const char**>(p);
+            // Read the first parameter, the string.
+            *s = path0;
+            return 0;
+        }))
+        .WillOnce(Invoke([&](sd_bus_message*, char, void* p) {
+            const char** s = static_cast<const char**>(p);
+            // Read the first parameter, the string.
+            *s = path1;
+            return 0;
+        }));
+
+    EXPECT_CALL(sdbusMock, sd_bus_message_exit_container(IsNull()))
+        .WillOnce(Return(0)); /* end of std::vector */
+
+    auto ret = utils::getPSUInventoryPath(bus);
+    EXPECT_EQ(2u, ret.size());
+    EXPECT_EQ(path0, ret[0]);
+    EXPECT_EQ(path1, ret[1]);
+}