test: dbus: passive read interface

Added basic unit-tests and added a factory for creating the
DbusPassive read interface so that it can be nicely error checked.  This
is handled via a valid type check where the only valid types are 'fan'
and 'temp'.

Change-Id: I558ed09bf509d26f20c6e431bb0789074e9aa841
Signed-off-by: Patrick Venture <venture@google.com>
diff --git a/dbus/dbuspassive.cpp b/dbus/dbuspassive.cpp
index 6b4ea5f..daeef62 100644
--- a/dbus/dbuspassive.cpp
+++ b/dbus/dbuspassive.cpp
@@ -13,12 +13,31 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 #include <chrono>
 #include <cmath>
+#include <memory>
 #include <mutex>
+#include <sdbusplus/bus.hpp>
+#include <string>
 
 #include "dbuspassive.hpp"
+#include "dbus/util.hpp"
+
+std::unique_ptr<ReadInterface> DbusPassive::CreateDbusPassive(
+    sdbusplus::bus::bus& bus, const std::string& type,
+    const std::string& id, DbusHelperInterface *helper)
+{
+    if (helper == nullptr)
+    {
+        return nullptr;
+    }
+    if (!ValidType(type))
+    {
+        return nullptr;
+    }
+
+    return std::make_unique<DbusPassive>(bus, type, id, helper);
+}
 
 DbusPassive::DbusPassive(
     sdbusplus::bus::bus& bus,
diff --git a/dbus/dbuspassive.hpp b/dbus/dbuspassive.hpp
index 8ecfe71..545b928 100644
--- a/dbus/dbuspassive.hpp
+++ b/dbus/dbuspassive.hpp
@@ -4,6 +4,7 @@
 #include <cmath>
 #include <iostream>
 #include <map>
+#include <memory>
 #include <mutex>
 #include <set>
 #include <string>
@@ -33,6 +34,10 @@
 class DbusPassive : public ReadInterface
 {
     public:
+        static std::unique_ptr<ReadInterface> CreateDbusPassive(
+            sdbusplus::bus::bus& bus, const std::string& type,
+            const std::string& id, DbusHelperInterface *helper);
+
         DbusPassive(sdbusplus::bus::bus& bus,
                     const std::string& type,
                     const std::string& id,
diff --git a/dbus/util.cpp b/dbus/util.cpp
index f0ffffb..91cc840 100644
--- a/dbus/util.cpp
+++ b/dbus/util.cpp
@@ -1,4 +1,5 @@
 #include <iostream>
+#include <set>
 
 #include "dbus/util.hpp"
 
@@ -105,3 +106,8 @@
                        "path='" + GetSensorPath(type, id) + "'");
 }
 
+bool ValidType(const std::string& type)
+{
+    static std::set<std::string> valid = {"fan", "temp"};
+    return (valid.find(type) != valid.end());
+}
diff --git a/dbus/util.hpp b/dbus/util.hpp
index e858c0c..44e137c 100644
--- a/dbus/util.hpp
+++ b/dbus/util.hpp
@@ -58,3 +58,4 @@
 
 std::string GetSensorPath(const std::string& type, const std::string& id);
 std::string GetMatch(const std::string& type, const std::string& id);
+bool ValidType(const std::string& type);
diff --git a/sensors/builder.cpp b/sensors/builder.cpp
index 9ab6a8c..01b0a64 100644
--- a/sensors/builder.cpp
+++ b/sensors/builder.cpp
@@ -66,11 +66,11 @@
         switch (rtype)
         {
             case IOInterfaceType::DBUSPASSIVE:
-                ri = std::make_unique<DbusPassive>(
-                         PassiveListeningBus,
-                         info->type,
-                         name,
-                         &helper);
+                ri = DbusPassive::CreateDbusPassive(PassiveListeningBus,
+                                                    info->type,
+                                                    name,
+                                                    &helper);
+                /* TODO(venture): if this returns nullptr */
                 break;
             case IOInterfaceType::EXTERNAL:
                 // These are a special case for read-only.
diff --git a/test/Makefile.am b/test/Makefile.am
index 4d78bc6..d8056e2 100644
--- a/test/Makefile.am
+++ b/test/Makefile.am
@@ -13,7 +13,8 @@
 # Run all 'check' test programs
 check_PROGRAMS = sensor_manager_unittest sensor_pluggable_unittest \
  sensor_host_unittest util_unittest pid_zone_unittest \
- pid_thermalcontroller_unittest pid_fancontroller_unittest
+ pid_thermalcontroller_unittest pid_fancontroller_unittest \
+ dbus_passive_unittest
 TESTS = $(check_PROGRAMS)
 
 # Until libconfig is mocked out or replaced, include it.
@@ -43,3 +44,7 @@
 pid_fancontroller_unittest_LDADD = $(top_builddir)/pid/ec/pid.o \
  $(top_builddir)/pid/util.o $(top_builddir)/pid/controller.o \
  $(top_builddir)/pid/fancontroller.o
+
+dbus_passive_unittest_SOURCES = dbus_passive_unittest.cpp
+dbus_passive_unittest_LDADD = $(top_builddir)/dbus/util.o \
+ $(top_builddir)/dbus/dbuspassive.o
diff --git a/test/dbus_passive_unittest.cpp b/test/dbus_passive_unittest.cpp
new file mode 100644
index 0000000..e84de89
--- /dev/null
+++ b/test/dbus_passive_unittest.cpp
@@ -0,0 +1,275 @@
+#include "dbus/dbuspassive.hpp"
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+#include <sdbusplus/test/sdbus_mock.hpp>
+#include <string>
+
+#include "test/dbushelper_mock.hpp"
+
+using ::testing::InSequence;
+using ::testing::Invoke;
+using ::testing::IsNull;
+using ::testing::NotNull;
+using ::testing::Return;
+using ::testing::StrEq;
+using ::testing::_;
+
+std::string SensorIntf = "xyz.openbmc_project.Sensor.Value";
+
+TEST(DbusPassiveTest, FactoryFailsWithInvalidType) {
+    // Verify the type is checked by the factory.
+
+    sdbusplus::SdBusMock sdbus_mock;
+    auto bus_mock = sdbusplus::get_mocked_new(&sdbus_mock);
+    std::string type = "invalid";
+    std::string id = "id";
+
+    DbusHelperMock helper;
+
+    std::unique_ptr<ReadInterface> ri =
+        DbusPassive::CreateDbusPassive(bus_mock, type, id, &helper);
+
+    EXPECT_EQ(ri, nullptr);
+}
+
+TEST(DbusPassiveTest, BoringConstructorTest) {
+    // Just build the object, which should be avoided as this does no error
+    // checking at present.
+
+    sdbusplus::SdBusMock sdbus_mock;
+    auto bus_mock = sdbusplus::get_mocked_new(&sdbus_mock);
+    std::string type = "invalid";
+    std::string id = "id";
+    std::string path = "/xyz/openbmc_project/sensors/unknown/id";
+
+    DbusHelperMock helper;
+    EXPECT_CALL(helper, GetService(_, StrEq(SensorIntf), StrEq(path)))
+        .WillOnce(Return("asdf"));
+
+    EXPECT_CALL(helper, GetProperties(_, StrEq("asdf"), StrEq(path),
+                                      NotNull()))
+        .WillOnce(Invoke([&](sdbusplus::bus::bus& bus,
+                             const std::string& service,
+                             const std::string& path,
+                             struct SensorProperties* prop)  {
+            prop->scale = -3;
+            prop->value = 10;
+            prop->unit = "x";
+        }));
+
+    DbusPassive(bus_mock, type, id, &helper);
+    // Success
+}
+
+class DbusPassiveTestObj : public ::testing::Test {
+    protected:
+        DbusPassiveTestObj()
+        : sdbus_mock(),
+          bus_mock(std::move(sdbusplus::get_mocked_new(&sdbus_mock))),
+          helper()
+        {
+            EXPECT_CALL(helper, GetService(_, StrEq(SensorIntf), StrEq(path)))
+                .WillOnce(Return("asdf"));
+
+            EXPECT_CALL(helper, GetProperties(_, StrEq("asdf"), StrEq(path),
+                                             NotNull()))
+                .WillOnce(Invoke([&](sdbusplus::bus::bus& bus,
+                                     const std::string& service,
+                                     const std::string& path,
+                                     struct SensorProperties* prop)  {
+                    prop->scale = _scale;
+                    prop->value = _value;
+                    prop->unit = "x";
+                }));
+
+            ri = DbusPassive::CreateDbusPassive(bus_mock, type, id, &helper);
+            passive = reinterpret_cast<DbusPassive*>(ri.get());
+            EXPECT_FALSE(passive == nullptr);
+        }
+
+        sdbusplus::SdBusMock sdbus_mock;
+        sdbusplus::bus::bus bus_mock;
+        DbusHelperMock helper;
+        std::string type = "temp";
+        std::string id = "id";
+        std::string path = "/xyz/openbmc_project/sensors/temperature/id";
+        int64_t _scale = -3;
+        int64_t _value = 10;
+
+        std::unique_ptr<ReadInterface> ri;
+        DbusPassive *passive;
+};
+
+TEST_F(DbusPassiveTestObj, ReadReturnsExpectedValues) {
+    // Verify read is returning the values.
+    ReadReturn v;
+    v.value = 0.01;
+    // TODO: updated is set when the value is created, so we can range check
+    // it.
+    ReadReturn r = passive->read();
+    EXPECT_EQ(v.value, r.value);
+}
+
+TEST_F(DbusPassiveTestObj, SetValueUpdatesValue) {
+    // Verify setvalue does as advertised.
+
+    double value = 0.01;
+    passive->setValue(value);
+
+    // TODO: updated is set when the value is set, so we can range check it.
+    ReadReturn r = passive->read();
+    EXPECT_EQ(value, r.value);
+}
+
+TEST_F(DbusPassiveTestObj, GetScaleReturnsExpectedValue) {
+    // Verify the scale is returned as expected.
+    EXPECT_EQ(_scale, passive->getScale());
+}
+
+TEST_F(DbusPassiveTestObj, GetIdReturnsExpectedValue) {
+    // Verify getId returns the expected value.
+    EXPECT_EQ(id, passive->getId());
+}
+
+TEST_F(DbusPassiveTestObj, VerifyHandlesDbusSignal) {
+    // The dbus passive sensor listens for updates and if it's the Value
+    // property, it needs to handle it.
+
+    EXPECT_CALL(sdbus_mock, sd_bus_message_ref(IsNull()))
+        .WillOnce(Return(nullptr));
+    sdbusplus::message::message msg(nullptr, &sdbus_mock);
+
+    const char *Value = "Value";
+    int64_t xValue = 10000;
+    const char *intf = "xyz.openbmc_project.Sensor.Value";
+    // string, std::map<std::string, sdbusplus::message::variant<int64_t>>
+    // msg.read(msgSensor, msgData);
+
+    EXPECT_CALL(sdbus_mock,
+                sd_bus_message_read_basic(IsNull(), 's', NotNull()))
+        .WillOnce(Invoke([&](sd_bus_message *m, char type, void *p) {
+            const char **s = static_cast<const char **>(p);
+            // Read the first parameter, the string.
+            *s = intf;
+            return 0;
+        }))
+        .WillOnce(Invoke([&](sd_bus_message *m, char type, void *p) {
+            const char **s = static_cast<const char **>(p);
+            *s = Value;
+            // Read the string in the pair (dictionary).
+            return 0;
+        }));
+
+    // std::map
+    EXPECT_CALL(sdbus_mock,
+                sd_bus_message_enter_container(IsNull(), 'a', StrEq("{sv}")))
+        .WillOnce(Return(0));
+
+    // while !at_end()
+    EXPECT_CALL(sdbus_mock, sd_bus_message_at_end(IsNull(), 0))
+        .WillOnce(Return(0))
+        .WillOnce(Return(1));  // So it exits the loop after reading one pair.
+
+    // std::pair
+    EXPECT_CALL(sdbus_mock,
+                sd_bus_message_enter_container(IsNull(), 'e', StrEq("sv")))
+        .WillOnce(Return(0));
+
+    EXPECT_CALL(sdbus_mock,
+                sd_bus_message_verify_type(IsNull(), 'v', StrEq("x")))
+        .WillOnce(Return(1));
+    EXPECT_CALL(sdbus_mock,
+                sd_bus_message_enter_container(IsNull(), 'v', StrEq("x")))
+        .WillOnce(Return(0));
+
+    EXPECT_CALL(sdbus_mock,
+                sd_bus_message_read_basic(IsNull(), 'x', NotNull()))
+        .WillOnce(Invoke([&](sd_bus_message *m, char type, void *p) {
+            int64_t *s = static_cast<int64_t *>(p);
+            *s = xValue;
+            return 0;
+        }));
+
+    EXPECT_CALL(sdbus_mock, sd_bus_message_exit_container(IsNull()))
+        .WillOnce(Return(0))  /* variant. */
+        .WillOnce(Return(0))  /* std::pair */
+        .WillOnce(Return(0)); /* std::map */
+
+    int rv = HandleSensorValue(msg, passive);
+    EXPECT_EQ(rv, 0); // It's always 0.
+
+    ReadReturn r = passive->read();
+    EXPECT_EQ(10, r.value);
+}
+
+TEST_F(DbusPassiveTestObj, VerifyIgnoresOtherPropertySignal) {
+    // The dbus passive sensor listens for updates and if it's the Value
+    // property, it needs to handle it.  In this case, it won't be.
+
+    EXPECT_CALL(sdbus_mock, sd_bus_message_ref(IsNull()))
+        .WillOnce(Return(nullptr));
+    sdbusplus::message::message msg(nullptr, &sdbus_mock);
+
+    const char *Scale = "Scale";
+    int64_t xScale = -6;
+    const char *intf = "xyz.openbmc_project.Sensor.Value";
+    // string, std::map<std::string, sdbusplus::message::variant<int64_t>>
+    // msg.read(msgSensor, msgData);
+
+    EXPECT_CALL(sdbus_mock,
+                sd_bus_message_read_basic(IsNull(), 's', NotNull()))
+        .WillOnce(Invoke([&](sd_bus_message *m, char type, void *p) {
+            const char **s = static_cast<const char **>(p);
+            // Read the first parameter, the string.
+            *s = intf;
+            return 0;
+        }))
+        .WillOnce(Invoke([&](sd_bus_message *m, char type, void *p) {
+            const char **s = static_cast<const char **>(p);
+            *s = Scale;
+            // Read the string in the pair (dictionary).
+            return 0;
+        }));
+
+    // std::map
+    EXPECT_CALL(sdbus_mock,
+                sd_bus_message_enter_container(IsNull(), 'a', StrEq("{sv}")))
+        .WillOnce(Return(0));
+
+    // while !at_end()
+    EXPECT_CALL(sdbus_mock, sd_bus_message_at_end(IsNull(), 0))
+        .WillOnce(Return(0))
+        .WillOnce(Return(1));  // So it exits the loop after reading one pair.
+
+    // std::pair
+    EXPECT_CALL(sdbus_mock,
+                sd_bus_message_enter_container(IsNull(), 'e', StrEq("sv")))
+        .WillOnce(Return(0));
+
+    EXPECT_CALL(sdbus_mock,
+                sd_bus_message_verify_type(IsNull(), 'v', StrEq("x")))
+        .WillOnce(Return(1));
+    EXPECT_CALL(sdbus_mock,
+                sd_bus_message_enter_container(IsNull(), 'v', StrEq("x")))
+        .WillOnce(Return(0));
+
+    EXPECT_CALL(sdbus_mock,
+                sd_bus_message_read_basic(IsNull(), 'x', NotNull()))
+        .WillOnce(Invoke([&](sd_bus_message *m, char type, void *p) {
+            int64_t *s = static_cast<int64_t *>(p);
+            *s = xScale;
+            return 0;
+        }));
+
+    EXPECT_CALL(sdbus_mock, sd_bus_message_exit_container(IsNull()))
+        .WillOnce(Return(0))  /* variant. */
+        .WillOnce(Return(0))  /* std::pair */
+        .WillOnce(Return(0)); /* std::map */
+
+    int rv = HandleSensorValue(msg, passive);
+    EXPECT_EQ(rv, 0); // It's always 0.
+
+    ReadReturn r = passive->read();
+    EXPECT_EQ(0.01, r.value);
+}