Test: Add unit test for PSU plug out and in

Split the function into two, one for handling the sdbus::message, the
other for handling the changed properties, so that it's easier to write
unit test cases.

Added the test cases:
* On a system with a PSU present, remove the PSU;
* On a system without PSU, plug the PSU in;
* On a system with a PSU, remove the PSU and add it back, while the
  propertiesChanged callback is invoked with both Present and Version
  properties.
* On a system with two PSUs with same version, remove them one-by-one,
  and add back one-by-one, while PSU1 has a different version.

Tested: Verify ItemUpdater correctly handles the above cases:
        * Remove the activation and version object if PSU is removed;
        * Create activation and version object if PSU is added;
        * When there are two PSUs with same version, removing one only
          update the associations, removing the other shall result in
          the objects to be removed;
          Adding one back will create the objects, and adding the other
          one with different version will create new objects.

Signed-off-by: Lei YU <mine260309@gmail.com>
Change-Id: I14c7ae9f03ec91bb1c85bb5a18d69f20dc1efd53
diff --git a/src/item_updater.cpp b/src/item_updater.cpp
index 6c55062..89bd388 100644
--- a/src/item_updater.cpp
+++ b/src/item_updater.cpp
@@ -286,20 +286,22 @@
     return version;
 }
 
-void ItemUpdater::onPsuInventoryChanged(sdbusplus::message::message& msg)
+void ItemUpdater::onPsuInventoryChangedMsg(sdbusplus::message::message& msg)
 {
     using Interface = std::string;
-    using Property = std::string;
-    using Properties =
-        std::map<Property, sdbusplus::message::variant<bool, std::string>>;
-
     Interface interface;
     Properties properties;
-    std::optional<bool> present;
-    std::optional<std::string> version;
     std::string psuPath = msg.get_path();
 
     msg.read(interface, properties);
+    onPsuInventoryChanged(psuPath, properties);
+}
+
+void ItemUpdater::onPsuInventoryChanged(const std::string& psuPath,
+                                        const Properties& properties)
+{
+    std::optional<bool> present;
+    std::optional<std::string> version;
 
     // The code was expecting to get callback on mutliple properties changed.
     // But in practice, the callback is received one-by-one for each property.
@@ -370,7 +372,7 @@
             MatchRules::type::signal() + MatchRules::path(p) +
                 MatchRules::member("PropertiesChanged") +
                 MatchRules::interface("org.freedesktop.DBus.Properties"),
-            std::bind(&ItemUpdater::onPsuInventoryChanged, this,
+            std::bind(&ItemUpdater::onPsuInventoryChangedMsg, this,
                       std::placeholders::_1));
     }
 }
diff --git a/src/item_updater.hpp b/src/item_updater.hpp
index eeacdf8..f6fefff 100644
--- a/src/item_updater.hpp
+++ b/src/item_updater.hpp
@@ -91,12 +91,24 @@
      */
     void createActivation(sdbusplus::message::message& msg);
 
+    using Properties =
+        std::map<std::string, utils::UtilsInterface::PropertyType>;
+
     /** @brief Callback function for PSU inventory match.
      *  @details Update an Activation D-Bus object for PSU inventory.
      *
      * @param[in]  msg       - Data associated with subscribed signal
      */
-    void onPsuInventoryChanged(sdbusplus::message::message& msg);
+    void onPsuInventoryChangedMsg(sdbusplus::message::message& msg);
+
+    /** @brief Callback function for PSU inventory match.
+     *  @details Update an Activation D-Bus object for PSU inventory.
+     *
+     * @param[in]  psuPath - The PSU inventory path
+     * @param[in]  properties - The updated properties
+     */
+    void onPsuInventoryChanged(const std::string& psuPath,
+                               const Properties& properties);
 
     /** @brief Create Activation object */
     std::unique_ptr<Activation> createActivationObject(
diff --git a/test/test_item_updater.cpp b/test/test_item_updater.cpp
index 2b9af43..b362803 100644
--- a/test/test_item_updater.cpp
+++ b/test/test_item_updater.cpp
@@ -13,11 +13,13 @@
 using ::testing::StrEq;
 
 using std::experimental::any;
-using PropertyType = utils::UtilsInterface::PropertyType;
 
 class TestItemUpdater : public ::testing::Test
 {
   public:
+    using Properties = ItemUpdater::Properties;
+    using PropertyType = utils::UtilsInterface::PropertyType;
+
     TestItemUpdater() :
         mockedUtils(
             reinterpret_cast<const utils::MockedUtils&>(utils::getUtils()))
@@ -39,6 +41,12 @@
         return std::string(dBusPath) + "/" + versionId;
     }
 
+    void onPsuInventoryChanged(const std::string& psuPath,
+                               const Properties& properties)
+    {
+        itemUpdater->onPsuInventoryChanged(psuPath, properties);
+    }
+
     static constexpr auto dBusPath = SOFTWARE_OBJPATH;
     sdbusplus::SdBusMock sdbusMock;
     sdbusplus::bus::bus mockedBus = sdbusplus::get_mocked_new(&sdbusMock);
@@ -206,3 +214,203 @@
     EXPECT_EQ(1u, assocs1.size());
     EXPECT_EQ(psu1, std::get<2>(assocs1[0]));
 }
+
+TEST_F(TestItemUpdater, OnOnePSURemoved)
+{
+    constexpr auto psuPath = "/com/example/inventory/psu0";
+    constexpr auto service = "com.example.Software.Psu";
+    constexpr auto version = "version0";
+    std::string objPath = getObjPath(version);
+    EXPECT_CALL(mockedUtils, getPSUInventoryPath(_))
+        .WillOnce(Return(std::vector<std::string>({psuPath})));
+    EXPECT_CALL(mockedUtils, getService(_, StrEq(psuPath), _))
+        .WillOnce(Return(service));
+    EXPECT_CALL(mockedUtils, getPropertyImpl(_, StrEq(service), StrEq(psuPath),
+                                             _, StrEq(VERSION)))
+        .WillOnce(Return(any(PropertyType(std::string(version)))));
+    EXPECT_CALL(mockedUtils, getPropertyImpl(_, StrEq(service), StrEq(psuPath),
+                                             _, StrEq(PRESENT)))
+        .WillOnce(Return(any(PropertyType(true)))); // present
+
+    // The item updater itself
+    EXPECT_CALL(sdbusMock, sd_bus_emit_object_added(_, StrEq(dBusPath)))
+        .Times(1);
+
+    // activation and version object will be added
+    EXPECT_CALL(sdbusMock, sd_bus_emit_object_added(_, StrEq(objPath)))
+        .Times(2);
+    itemUpdater = std::make_unique<ItemUpdater>(mockedBus, dBusPath);
+
+    // the activation and version object will be removed
+    Properties p{{PRESENT, PropertyType(false)}};
+    EXPECT_CALL(sdbusMock, sd_bus_emit_object_removed(_, StrEq(objPath)))
+        .Times(2);
+    onPsuInventoryChanged(psuPath, p);
+
+    // on exit, item updater is removed
+    EXPECT_CALL(sdbusMock, sd_bus_emit_object_removed(_, StrEq(dBusPath)))
+        .Times(1);
+}
+
+TEST_F(TestItemUpdater, OnOnePSUAdded)
+{
+    constexpr auto psuPath = "/com/example/inventory/psu0";
+    constexpr auto service = "com.example.Software.Psu";
+    constexpr auto version = "version0";
+    std::string objPath = getObjPath(version);
+    EXPECT_CALL(mockedUtils, getPSUInventoryPath(_))
+        .WillOnce(Return(std::vector<std::string>({psuPath})));
+    EXPECT_CALL(mockedUtils, getService(_, StrEq(psuPath), _))
+        .WillOnce(Return(service));
+    EXPECT_CALL(mockedUtils, getPropertyImpl(_, StrEq(service), StrEq(psuPath),
+                                             _, StrEq(VERSION)))
+        .WillOnce(Return(any(PropertyType(std::string(version)))));
+    EXPECT_CALL(mockedUtils, getPropertyImpl(_, StrEq(service), StrEq(psuPath),
+                                             _, StrEq(PRESENT)))
+        .WillOnce(Return(any(PropertyType(false)))); // not present
+
+    // The item updater itself
+    EXPECT_CALL(sdbusMock, sd_bus_emit_object_added(_, StrEq(dBusPath)))
+        .Times(1);
+
+    // No activation/version objects are created
+    EXPECT_CALL(sdbusMock, sd_bus_emit_object_added(_, StrEq(objPath)))
+        .Times(0);
+    itemUpdater = std::make_unique<ItemUpdater>(mockedBus, dBusPath);
+
+    // The PSU is present and version is added in a single call
+    Properties propAdded{{PRESENT, PropertyType(true)},
+                         {VERSION, PropertyType(std::string(version))}};
+    EXPECT_CALL(sdbusMock, sd_bus_emit_object_added(_, StrEq(objPath)))
+        .Times(2);
+    onPsuInventoryChanged(psuPath, propAdded);
+}
+
+TEST_F(TestItemUpdater, OnOnePSURemovedAndAdded)
+{
+    constexpr auto psuPath = "/com/example/inventory/psu0";
+    constexpr auto service = "com.example.Software.Psu";
+    constexpr auto version = "version0";
+    std::string objPath = getObjPath(version);
+    EXPECT_CALL(mockedUtils, getPSUInventoryPath(_))
+        .WillOnce(Return(std::vector<std::string>({psuPath})));
+    EXPECT_CALL(mockedUtils, getService(_, StrEq(psuPath), _))
+        .WillOnce(Return(service));
+    EXPECT_CALL(mockedUtils, getPropertyImpl(_, StrEq(service), StrEq(psuPath),
+                                             _, StrEq(VERSION)))
+        .WillOnce(Return(any(PropertyType(std::string(version)))));
+    EXPECT_CALL(mockedUtils, getPropertyImpl(_, StrEq(service), StrEq(psuPath),
+                                             _, StrEq(PRESENT)))
+        .WillOnce(Return(any(PropertyType(true)))); // present
+
+    // The item updater itself
+    EXPECT_CALL(sdbusMock, sd_bus_emit_object_added(_, StrEq(dBusPath)))
+        .Times(1);
+
+    // activation and version object will be added
+    EXPECT_CALL(sdbusMock, sd_bus_emit_object_added(_, StrEq(objPath)))
+        .Times(2);
+    itemUpdater = std::make_unique<ItemUpdater>(mockedBus, dBusPath);
+
+    // the activation and version object will be removed
+    Properties propRemoved{{PRESENT, PropertyType(false)}};
+    EXPECT_CALL(sdbusMock, sd_bus_emit_object_removed(_, StrEq(objPath)))
+        .Times(2);
+    onPsuInventoryChanged(psuPath, propRemoved);
+
+    Properties propAdded{{PRESENT, PropertyType(true)}};
+    Properties propVersion{{VERSION, PropertyType(std::string(version))}};
+    EXPECT_CALL(sdbusMock, sd_bus_emit_object_added(_, StrEq(objPath)))
+        .Times(2);
+    onPsuInventoryChanged(psuPath, propAdded);
+    onPsuInventoryChanged(psuPath, propVersion);
+
+    // on exit, objects are removed
+    EXPECT_CALL(sdbusMock, sd_bus_emit_object_removed(_, StrEq(objPath)))
+        .Times(2);
+    EXPECT_CALL(sdbusMock, sd_bus_emit_object_removed(_, StrEq(dBusPath)))
+        .Times(1);
+}
+
+TEST_F(TestItemUpdater,
+       TwoPSUsWithSameVersionRemovedAndAddedWithDifferntVersion)
+{
+    constexpr auto psu0 = "/com/example/inventory/psu0";
+    constexpr auto psu1 = "/com/example/inventory/psu1";
+    constexpr auto service = "com.example.Software.Psu";
+    auto version0 = std::string("version0");
+    auto version1 = std::string("version0");
+    auto objPath0 = getObjPath(version0);
+    auto objPath1 = getObjPath(version1);
+
+    EXPECT_CALL(mockedUtils, getPSUInventoryPath(_))
+        .WillOnce(Return(std::vector<std::string>({psu0, psu1})));
+    EXPECT_CALL(mockedUtils, getService(_, StrEq(psu0), _))
+        .WillOnce(Return(service));
+    EXPECT_CALL(mockedUtils, getService(_, StrEq(psu1), _))
+        .WillOnce(Return(service));
+    EXPECT_CALL(mockedUtils, getPropertyImpl(_, StrEq(service), StrEq(psu0), _,
+                                             StrEq(VERSION)))
+        .WillOnce(Return(any(PropertyType(version0))));
+    EXPECT_CALL(mockedUtils, getPropertyImpl(_, StrEq(service), StrEq(psu0), _,
+                                             StrEq(PRESENT)))
+        .WillOnce(Return(any(PropertyType(true)))); // present
+    EXPECT_CALL(mockedUtils, getPropertyImpl(_, StrEq(service), StrEq(psu1), _,
+                                             StrEq(VERSION)))
+        .WillOnce(Return(any(PropertyType(version1))));
+    EXPECT_CALL(mockedUtils, getPropertyImpl(_, StrEq(service), StrEq(psu1), _,
+                                             StrEq(PRESENT)))
+        .WillOnce(Return(any(PropertyType(true)))); // present
+
+    // The item updater itself
+    EXPECT_CALL(sdbusMock, sd_bus_emit_object_added(_, StrEq(dBusPath)))
+        .Times(1);
+
+    // activation and version object will be added
+    EXPECT_CALL(sdbusMock, sd_bus_emit_object_added(_, StrEq(objPath0)))
+        .Times(2);
+    itemUpdater = std::make_unique<ItemUpdater>(mockedBus, dBusPath);
+
+    // Verify there is only one activation and it has two associations
+    const auto& activations = GetActivations();
+    EXPECT_EQ(1u, activations.size());
+    const auto& activation = activations.find(version0)->second;
+    auto assocs = activation->associations();
+    EXPECT_EQ(2u, assocs.size());
+    EXPECT_EQ(psu0, std::get<2>(assocs[0]));
+    EXPECT_EQ(psu1, std::get<2>(assocs[1]));
+
+    // PSU0 is removed, only associations shall be updated
+    Properties propRemoved{{PRESENT, PropertyType(false)}};
+    onPsuInventoryChanged(psu0, propRemoved);
+    assocs = activation->associations();
+    EXPECT_EQ(1u, assocs.size());
+    EXPECT_EQ(psu1, std::get<2>(assocs[0]));
+
+    // PSU1 is removed, the activation and version object shall be removed
+    EXPECT_CALL(sdbusMock, sd_bus_emit_object_removed(_, StrEq(objPath0)))
+        .Times(2);
+    onPsuInventoryChanged(psu1, propRemoved);
+
+    // Add PSU0 and PSU1 back, but PSU1 with a different version
+    version1 = "version1";
+    objPath1 = getObjPath(version1);
+    Properties propAdded0{{PRESENT, PropertyType(true)},
+                          {VERSION, PropertyType(std::string(version0))}};
+    Properties propAdded1{{PRESENT, PropertyType(true)},
+                          {VERSION, PropertyType(std::string(version1))}};
+    EXPECT_CALL(sdbusMock, sd_bus_emit_object_added(_, StrEq(objPath0)))
+        .Times(2);
+    EXPECT_CALL(sdbusMock, sd_bus_emit_object_added(_, StrEq(objPath1)))
+        .Times(2);
+    onPsuInventoryChanged(psu0, propAdded0);
+    onPsuInventoryChanged(psu1, propAdded1);
+
+    // on exit, objects are removed
+    EXPECT_CALL(sdbusMock, sd_bus_emit_object_removed(_, StrEq(objPath0)))
+        .Times(2);
+    EXPECT_CALL(sdbusMock, sd_bus_emit_object_removed(_, StrEq(objPath1)))
+        .Times(2);
+    EXPECT_CALL(sdbusMock, sd_bus_emit_object_removed(_, StrEq(dBusPath)))
+        .Times(1);
+}