Implemented sensor class

Sensor class was introduced, it monitors
xyz.openbmc_project.Sensor.Value, for change and notifies all
listeners.

Tested:
  - Unit tested with service stub that provides dbus interface
    xyz.openbmc_project.Sensor.Value
  - All changes are delivered to listeners
  - All other unit tests are passing

Change-Id: I8c9d58cc986c1fe2a4d2386815d559814016efa6
Signed-off-by: Krzysztof Grobelny <krzysztof.grobelny@intel.com>
diff --git a/tests/meson.build b/tests/meson.build
index 08a6e6c..89e5088 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -6,8 +6,7 @@
         gtest_dep = declare_dependency(
             dependencies: [
                 dependency('threads'),
-                gtest_proj.dependency('gtest'),
-                gtest_proj.dependency('gtest_main'),
+                gtest_proj.dependency('gtest')
             ]
         )
         gmock_dep = gtest_proj.dependency('gmock')
@@ -27,9 +26,16 @@
             '../src/persistent_json_storage.cpp',
             '../src/report.cpp',
             '../src/report_manager.cpp',
+            '../src/sensor.cpp',
             '../src/sensor_cache.cpp',
+            'src/dbus_environment.cpp',
+            'src/main.cpp',
+            'src/stubs/dbus_sensor_object.cpp',
+            'src/test_detached_timer.cpp',
             'src/test_persistent_json_storage.cpp',
+            'src/test_sensor.cpp',
             'src/test_sensor_cache.cpp',
+            'src/test_unique_call.cpp',
             'src/utils/generate_unique_mock_id.cpp',
         ],
         dependencies: [
diff --git a/tests/src/dbus_environment.cpp b/tests/src/dbus_environment.cpp
new file mode 100644
index 0000000..ab749db
--- /dev/null
+++ b/tests/src/dbus_environment.cpp
@@ -0,0 +1,121 @@
+#include "dbus_environment.hpp"
+
+#include <future>
+#include <thread>
+
+DbusEnvironment::~DbusEnvironment()
+{
+    teardown();
+}
+
+void DbusEnvironment::SetUp()
+{
+    if (setUp == false)
+    {
+        setUp = true;
+
+        bus = std::make_shared<sdbusplus::asio::connection>(ioc);
+        bus->request_name(serviceName());
+
+        objServer = std::make_unique<sdbusplus::asio::object_server>(bus);
+    }
+}
+
+void DbusEnvironment::TearDown()
+{
+    ioc.poll();
+
+    futures.clear();
+}
+
+void DbusEnvironment::teardown()
+{
+    if (setUp == true)
+    {
+        setUp = false;
+
+        objServer = nullptr;
+        bus = nullptr;
+    }
+}
+
+boost::asio::io_context& DbusEnvironment::getIoc()
+{
+    return ioc;
+}
+
+std::shared_ptr<sdbusplus::asio::connection> DbusEnvironment::getBus()
+{
+    return bus;
+}
+
+std::shared_ptr<sdbusplus::asio::object_server> DbusEnvironment::getObjServer()
+{
+    return objServer;
+}
+
+const char* DbusEnvironment::serviceName()
+{
+    return "telemetry.ut";
+}
+
+std::function<void()> DbusEnvironment::setPromise(std::string_view name)
+{
+    auto promise = std::make_shared<std::promise<bool>>();
+
+    {
+        futures[std::string(name)].emplace_back(promise->get_future());
+    }
+
+    return [p = std::move(promise)]() { p->set_value(true); };
+}
+
+bool DbusEnvironment::waitForFuture(std::string_view name,
+                                    std::chrono::milliseconds timeout)
+{
+    return waitForFuture(getFuture(name), timeout).value_or(false);
+}
+
+std::future<bool> DbusEnvironment::getFuture(std::string_view name)
+{
+    auto& data = futures[std::string(name)];
+    auto it = data.begin();
+
+    if (it != data.end())
+    {
+        auto result = std::move(*it);
+        data.erase(it);
+        return result;
+    }
+
+    return {};
+}
+
+void DbusEnvironment::sleepFor(std::chrono::milliseconds timeout)
+{
+    auto end = std::chrono::high_resolution_clock::now() + timeout;
+
+    while (std::chrono::high_resolution_clock::now() < end)
+    {
+        synchronizeIoc();
+        std::this_thread::yield();
+    }
+
+    synchronizeIoc();
+}
+
+std::chrono::milliseconds
+    DbusEnvironment::measureTime(std::function<void()> fun)
+{
+    auto begin = std::chrono::high_resolution_clock::now();
+    fun();
+    auto end = std::chrono::high_resolution_clock::now();
+
+    return std::chrono::duration_cast<std::chrono::milliseconds>(end - begin);
+}
+
+boost::asio::io_context DbusEnvironment::ioc;
+std::shared_ptr<sdbusplus::asio::connection> DbusEnvironment::bus;
+std::shared_ptr<sdbusplus::asio::object_server> DbusEnvironment::objServer;
+std::map<std::string, std::vector<std::future<bool>>> DbusEnvironment::futures;
+bool DbusEnvironment::setUp = false;
diff --git a/tests/src/dbus_environment.hpp b/tests/src/dbus_environment.hpp
new file mode 100644
index 0000000..d039922
--- /dev/null
+++ b/tests/src/dbus_environment.hpp
@@ -0,0 +1,84 @@
+#include <sdbusplus/asio/object_server.hpp>
+
+#include <future>
+#include <thread>
+
+#include <gmock/gmock.h>
+
+class DbusEnvironment : public ::testing::Environment
+{
+  public:
+    ~DbusEnvironment();
+
+    void SetUp() override;
+    void TearDown() override;
+    void teardown();
+
+    static boost::asio::io_context& getIoc();
+    static std::shared_ptr<sdbusplus::asio::connection> getBus();
+    static std::shared_ptr<sdbusplus::asio::object_server> getObjServer();
+    static const char* serviceName();
+    static std::function<void()> setPromise(std::string_view name);
+    static void sleepFor(std::chrono::milliseconds);
+    static std::chrono::milliseconds measureTime(std::function<void()>);
+
+    static void synchronizeIoc()
+    {
+        while (ioc.poll() > 0)
+        {
+        }
+    }
+
+    template <class Functor>
+    static void synchronizedPost(Functor&& functor)
+    {
+        boost::asio::post(ioc, std::forward<Functor>(functor));
+        synchronizeIoc();
+    }
+
+    template <class T>
+    static std::optional<T> waitForFuture(
+        std::future<T> future,
+        std::chrono::milliseconds timeout = std::chrono::seconds(10))
+    {
+        constexpr auto precission = std::chrono::milliseconds(10);
+        auto elapsed = std::chrono::milliseconds(0);
+
+        while (future.valid() && elapsed < timeout)
+        {
+            synchronizeIoc();
+
+            try
+            {
+                if (future.wait_for(precission) == std::future_status::ready)
+                {
+                    return future.get();
+                }
+                else
+                {
+                    elapsed += precission;
+                }
+            }
+            catch (const std::future_error& e)
+            {
+                std::cerr << e.what() << "\n";
+                return {};
+            }
+        }
+
+        return {};
+    }
+
+    static bool waitForFuture(
+        std::string_view name,
+        std::chrono::milliseconds timeout = std::chrono::seconds(10));
+
+  private:
+    static std::future<bool> getFuture(std::string_view name);
+
+    static boost::asio::io_context ioc;
+    static std::shared_ptr<sdbusplus::asio::connection> bus;
+    static std::shared_ptr<sdbusplus::asio::object_server> objServer;
+    static std::map<std::string, std::vector<std::future<bool>>> futures;
+    static bool setUp;
+};
diff --git a/tests/src/main.cpp b/tests/src/main.cpp
new file mode 100644
index 0000000..6e29319
--- /dev/null
+++ b/tests/src/main.cpp
@@ -0,0 +1,16 @@
+#include "dbus_environment.hpp"
+
+#include <gmock/gmock.h>
+
+int main(int argc, char** argv)
+{
+    auto env = new DbusEnvironment;
+
+    testing::InitGoogleTest(&argc, argv);
+    testing::AddGlobalTestEnvironment(env);
+    auto ret = RUN_ALL_TESTS();
+
+    env->teardown();
+
+    return ret;
+}
diff --git a/tests/src/mocks/json_storage_mock.hpp b/tests/src/mocks/json_storage_mock.hpp
index 295bc94..99ecd41 100644
--- a/tests/src/mocks/json_storage_mock.hpp
+++ b/tests/src/mocks/json_storage_mock.hpp
@@ -7,8 +7,11 @@
 class StorageMock : public interfaces::JsonStorage
 {
   public:
-    MOCK_METHOD2(store, void(const FilePath&, const nlohmann::json&));
-    MOCK_METHOD1(remove, bool(const FilePath&));
-    MOCK_CONST_METHOD1(load, std::optional<nlohmann::json>(const FilePath&));
-    MOCK_CONST_METHOD1(list, std::vector<FilePath>(const DirectoryPath&));
+    MOCK_METHOD(void, store, (const FilePath&, const nlohmann::json&),
+                (override));
+    MOCK_METHOD(bool, remove, (const FilePath&), (override));
+    MOCK_METHOD(std::optional<nlohmann::json>, load, (const FilePath&),
+                (const, override));
+    MOCK_METHOD(std::vector<FilePath>, list, (const DirectoryPath&),
+                (const, override));
 };
diff --git a/tests/src/mocks/sensor_listener_mock.hpp b/tests/src/mocks/sensor_listener_mock.hpp
new file mode 100644
index 0000000..b8f1ef5
--- /dev/null
+++ b/tests/src/mocks/sensor_listener_mock.hpp
@@ -0,0 +1,29 @@
+#pragma once
+
+#include "interfaces/sensor_listener.hpp"
+
+#include <gmock/gmock.h>
+
+class SensorListenerMock : public interfaces::SensorListener
+{
+  public:
+    void delegateIgnoringArgs()
+    {
+        using namespace testing;
+
+        ON_CALL(*this, sensorUpdated(_, _)).WillByDefault(Invoke([this] {
+            sensorUpdated();
+        }));
+
+        ON_CALL(*this, sensorUpdated(_, _, _)).WillByDefault(Invoke([this] {
+            sensorUpdated();
+        }));
+    }
+
+    MOCK_METHOD(void, sensorUpdated, (interfaces::Sensor&, uint64_t),
+                (override));
+    MOCK_METHOD(void, sensorUpdated, (interfaces::Sensor&, uint64_t, double),
+                (override));
+
+    MOCK_METHOD(void, sensorUpdated, (), ());
+};
diff --git a/tests/src/mocks/sensor_mock.hpp b/tests/src/mocks/sensor_mock.hpp
index 9e8d7f3..0dcb2b5 100644
--- a/tests/src/mocks/sensor_mock.hpp
+++ b/tests/src/mocks/sensor_mock.hpp
@@ -20,7 +20,9 @@
         return Id("SensorMock", service, path);
     }
 
-    MOCK_CONST_METHOD0(id, Id());
+    MOCK_METHOD(Id, id, (), (const, override));
+    MOCK_METHOD(void, registerForUpdates,
+                (const std::weak_ptr<interfaces::SensorListener>&), (override));
 
     const uint64_t mockId = generateUniqueMockId();
 
diff --git a/tests/src/stubs/dbus_sensor_object.cpp b/tests/src/stubs/dbus_sensor_object.cpp
new file mode 100644
index 0000000..c3d4170
--- /dev/null
+++ b/tests/src/stubs/dbus_sensor_object.cpp
@@ -0,0 +1,59 @@
+#include "dbus_sensor_object.hpp"
+
+#include <boost/asio.hpp>
+#include <sdbusplus/asio/connection.hpp>
+#include <sdbusplus/asio/object_server.hpp>
+#include <sdbusplus/bus.hpp>
+
+namespace stubs
+{
+
+DbusSensorObject::DbusSensorObject(
+    boost::asio::io_context& ioc,
+    const std::shared_ptr<sdbusplus::asio::connection>& bus,
+    const std::shared_ptr<sdbusplus::asio::object_server>& objServer) :
+    ioc(ioc),
+    bus(bus), objServer(objServer)
+{
+    sensorIface = objServer->add_interface(path(), interface());
+
+    sensorIface->register_property_r(property.value(), double{},
+                                     sdbusplus::vtable::property_::emits_change,
+                                     [this](const auto&) { return value; });
+
+    sensorIface->initialize();
+}
+
+DbusSensorObject::~DbusSensorObject()
+{
+    objServer->remove_interface(sensorIface);
+}
+
+void DbusSensorObject::setValue(double v)
+{
+    value = v;
+
+    sensorIface->signal_property(property.value());
+}
+
+double DbusSensorObject::getValue() const
+{
+    return value;
+}
+
+const char* DbusSensorObject::path()
+{
+    return "/telemetry/ut/DbusSensorObject";
+}
+
+const char* DbusSensorObject::interface()
+{
+    return "xyz.openbmc_project.Sensor.Value";
+}
+
+const char* DbusSensorObject::Properties::value()
+{
+    return "Value";
+}
+
+} // namespace stubs
diff --git a/tests/src/stubs/dbus_sensor_object.hpp b/tests/src/stubs/dbus_sensor_object.hpp
new file mode 100644
index 0000000..99271fa
--- /dev/null
+++ b/tests/src/stubs/dbus_sensor_object.hpp
@@ -0,0 +1,43 @@
+#pragma once
+
+#include <boost/asio.hpp>
+#include <sdbusplus/asio/connection.hpp>
+#include <sdbusplus/asio/object_server.hpp>
+#include <sdbusplus/bus.hpp>
+
+namespace stubs
+{
+
+class DbusSensorObject
+{
+  public:
+    DbusSensorObject(
+        boost::asio::io_context& ioc,
+        const std::shared_ptr<sdbusplus::asio::connection>& bus,
+        const std::shared_ptr<sdbusplus::asio::object_server>& objServer);
+    ~DbusSensorObject();
+
+    static const char* path();
+    static const char* interface();
+
+    void setValue(double);
+    double getValue() const;
+
+    struct Properties
+    {
+        static const char* value();
+    };
+
+    static constexpr Properties property = {};
+
+  private:
+    boost::asio::io_context& ioc;
+    std::shared_ptr<sdbusplus::asio::connection> bus;
+    std::shared_ptr<sdbusplus::asio::object_server> objServer;
+
+    std::shared_ptr<sdbusplus::asio::dbus_interface> sensorIface;
+
+    double value = 0.0;
+};
+
+} // namespace stubs
diff --git a/tests/src/test_detached_timer.cpp b/tests/src/test_detached_timer.cpp
new file mode 100644
index 0000000..a5ff705
--- /dev/null
+++ b/tests/src/test_detached_timer.cpp
@@ -0,0 +1,34 @@
+#include "dbus_environment.hpp"
+#include "utils/detached_timer.hpp"
+
+#include <gmock/gmock.h>
+
+namespace utils
+{
+
+using namespace testing;
+using namespace std::chrono_literals;
+
+class TestDetachedTimer : public Test
+{
+  public:
+    uint32_t value = 0;
+};
+
+TEST_F(TestDetachedTimer, executesLambdaAfterTimeout)
+{
+    auto setPromise = DbusEnvironment::setPromise("timer");
+
+    makeDetachedTimer(DbusEnvironment::getIoc(), 100ms, [this, &setPromise] {
+        ++value;
+        setPromise();
+    });
+
+    auto elapsed = DbusEnvironment::measureTime(
+        [] { DbusEnvironment::waitForFuture("timer"); });
+
+    EXPECT_THAT(elapsed, AllOf(Ge(100ms), Lt(200ms)));
+    EXPECT_THAT(value, Eq(1u));
+}
+
+} // namespace utils
diff --git a/tests/src/test_sensor.cpp b/tests/src/test_sensor.cpp
new file mode 100644
index 0000000..c650831
--- /dev/null
+++ b/tests/src/test_sensor.cpp
@@ -0,0 +1,153 @@
+#include "dbus_environment.hpp"
+#include "mocks/sensor_listener_mock.hpp"
+#include "sensor.hpp"
+#include "sensor_cache.hpp"
+#include "stubs/dbus_sensor_object.hpp"
+#include "utils/sensor_id_eq.hpp"
+
+#include <sdbusplus/asio/property.hpp>
+
+#include <thread>
+
+#include <gmock/gmock.h>
+
+using namespace testing;
+using namespace std::chrono_literals;
+
+class TestSensor : public Test
+{
+  public:
+    void SetUp() override
+    {
+        sensorObject.setValue(42.7);
+    }
+
+    void TearDown() override
+    {
+        DbusEnvironment::synchronizeIoc();
+    }
+
+    void
+        registerForUpdates(std::shared_ptr<interfaces::SensorListener> listener)
+    {
+        DbusEnvironment::synchronizedPost(
+            [this, listener] { sut->registerForUpdates(listener); });
+    }
+
+    std::chrono::milliseconds notifiesInGivenIntervalAfterSchedule(
+        std::chrono::milliseconds interval);
+
+    stubs::DbusSensorObject sensorObject{DbusEnvironment::getIoc(),
+                                         DbusEnvironment::getBus(),
+                                         DbusEnvironment::getObjServer()};
+
+    SensorCache sensorCache;
+    uint64_t timestamp = std::time(0);
+    std::shared_ptr<Sensor> sut = sensorCache.makeSensor<Sensor>(
+        DbusEnvironment::serviceName(), sensorObject.path(),
+        DbusEnvironment::getIoc(), DbusEnvironment::getBus());
+    std::shared_ptr<SensorListenerMock> listenerMock =
+        std::make_shared<StrictMock<SensorListenerMock>>();
+    std::shared_ptr<SensorListenerMock> listenerMock2 =
+        std::make_shared<StrictMock<SensorListenerMock>>();
+};
+
+TEST_F(TestSensor, createsCorretlyViaSensorCache)
+{
+    ASSERT_THAT(sut->id(),
+                sensorIdEq(Sensor::Id("Sensor", DbusEnvironment::serviceName(),
+                                      sensorObject.path())));
+}
+
+TEST_F(TestSensor, notifiesWithValueAfterRegister)
+{
+    EXPECT_CALL(*listenerMock, sensorUpdated(Ref(*sut), Ge(timestamp), 42.7))
+        .WillOnce(Invoke(DbusEnvironment::setPromise("async_read")));
+
+    registerForUpdates(listenerMock);
+
+    ASSERT_TRUE(DbusEnvironment::waitForFuture("async_read"));
+}
+
+TEST_F(TestSensor, notifiesOnceWithValueAfterRegister)
+{
+    EXPECT_CALL(*listenerMock, sensorUpdated(Ref(*sut), Ge(timestamp), 42.7))
+        .WillOnce(Invoke(DbusEnvironment::setPromise("async_read")));
+    EXPECT_CALL(*listenerMock2, sensorUpdated(Ref(*sut), Ge(timestamp), 42.7))
+        .WillOnce(Invoke(DbusEnvironment::setPromise("async_read2")));
+
+    DbusEnvironment::synchronizedPost([this] {
+        sut->registerForUpdates(listenerMock);
+        sut->registerForUpdates(listenerMock2);
+    });
+
+    ASSERT_TRUE(DbusEnvironment::waitForFuture("async_read"));
+    ASSERT_TRUE(DbusEnvironment::waitForFuture("async_read2"));
+}
+
+class TestSensorNotification : public TestSensor
+{
+  public:
+    void SetUp() override
+    {
+        EXPECT_CALL(*listenerMock, sensorUpdated(Ref(*sut), Ge(timestamp), 0.))
+            .WillOnce(Invoke(DbusEnvironment::setPromise("async_read")));
+
+        registerForUpdates(listenerMock);
+
+        ASSERT_TRUE(DbusEnvironment::waitForFuture("async_read"));
+    }
+
+    std::shared_ptr<SensorListenerMock> listenerMock2 =
+        std::make_shared<StrictMock<SensorListenerMock>>();
+};
+
+TEST_F(TestSensorNotification, notifiesListenerWithValueWhenChangeOccurs)
+{
+    EXPECT_CALL(*listenerMock, sensorUpdated(Ref(*sut), Ge(timestamp), 42.7))
+        .WillOnce(Invoke(DbusEnvironment::setPromise("notify")));
+
+    sensorObject.setValue(42.7);
+
+    ASSERT_TRUE(DbusEnvironment::waitForFuture("notify"));
+}
+
+TEST_F(TestSensorNotification, notifiesListenerWithValueWhenNoChangeOccurs)
+{
+    Sequence seq;
+
+    EXPECT_CALL(*listenerMock, sensorUpdated(Ref(*sut), Ge(timestamp), 42.7))
+        .InSequence(seq);
+    EXPECT_CALL(*listenerMock, sensorUpdated(Ref(*sut), Ge(timestamp)))
+        .InSequence(seq)
+        .WillOnce(Invoke(DbusEnvironment::setPromise("notify")));
+
+    sensorObject.setValue(42.7);
+    sensorObject.setValue(42.7);
+
+    ASSERT_TRUE(DbusEnvironment::waitForFuture("notify"));
+}
+
+TEST_F(TestSensorNotification, doesntNotifyExpiredListener)
+{
+    Sequence seq;
+    EXPECT_CALL(*listenerMock2, sensorUpdated(Ref(*sut), Ge(timestamp), 0.))
+        .InSequence(seq);
+    EXPECT_CALL(*listenerMock2, sensorUpdated(Ref(*sut), Ge(timestamp), 42.7))
+        .InSequence(seq)
+        .WillOnce(Invoke(DbusEnvironment::setPromise("notify")));
+
+    registerForUpdates(listenerMock2);
+    listenerMock = nullptr;
+
+    sensorObject.setValue(42.7);
+
+    ASSERT_TRUE(DbusEnvironment::waitForFuture("notify"));
+}
+
+TEST_F(TestSensorNotification, notifiesWithValueDuringRegister)
+{
+    EXPECT_CALL(*listenerMock2, sensorUpdated(Ref(*sut), Ge(timestamp), 0.));
+
+    registerForUpdates(listenerMock2);
+}
diff --git a/tests/src/test_sensor_cache.cpp b/tests/src/test_sensor_cache.cpp
index 5be9a0d..a3831dc 100644
--- a/tests/src/test_sensor_cache.cpp
+++ b/tests/src/test_sensor_cache.cpp
@@ -1,5 +1,6 @@
 #include "mocks/sensor_mock.hpp"
 #include "sensor_cache.hpp"
+#include "utils/sensor_id_eq.hpp"
 
 #include <initializer_list>
 
@@ -13,13 +14,6 @@
     SensorCache sut;
 };
 
-auto sensorIdEq(interfaces::Sensor::Id id)
-{
-    return AllOf(Field(&interfaces::Sensor::Id::type, Eq(id.type)),
-                 Field(&interfaces::Sensor::Id::service, Eq(id.service)),
-                 Field(&interfaces::Sensor::Id::path, Eq(id.path)));
-}
-
 struct IdParam
 {
     IdParam() = default;
diff --git a/tests/src/test_unique_call.cpp b/tests/src/test_unique_call.cpp
new file mode 100644
index 0000000..876edd1
--- /dev/null
+++ b/tests/src/test_unique_call.cpp
@@ -0,0 +1,51 @@
+#include "utils/unique_call.hpp"
+
+#include <gmock/gmock.h>
+
+namespace utils
+{
+
+using namespace testing;
+
+class TestUniqueCall : public Test
+{
+  public:
+    void uniqueCallIncrementCounter()
+    {
+        uniqueCall1([this](auto context) { ++counter; });
+    }
+
+    void uniqueCallWhileUniqueCallIsActiveIncrementCounter()
+    {
+        uniqueCall2([this](auto context) {
+            ++counter;
+            uniqueCallWhileUniqueCallIsActiveIncrementCounter();
+        });
+    }
+
+    UniqueCall uniqueCall1;
+    UniqueCall uniqueCall2;
+    uint32_t counter = 0u;
+};
+
+TEST_F(TestUniqueCall, shouldExecuteNormally)
+{
+    for (size_t i = 0; i < 3u; ++i)
+    {
+        uniqueCallIncrementCounter();
+    }
+
+    ASSERT_THAT(counter, Eq(3u));
+}
+
+TEST_F(TestUniqueCall, shouldNotExecuteWhenPreviousExecutionIsStillActive)
+{
+    for (size_t i = 0; i < 3u; ++i)
+    {
+        uniqueCallWhileUniqueCallIsActiveIncrementCounter();
+    }
+
+    ASSERT_THAT(counter, Eq(3u));
+}
+
+} // namespace utils
diff --git a/tests/src/utils/sensor_id_eq.hpp b/tests/src/utils/sensor_id_eq.hpp
new file mode 100644
index 0000000..f952504
--- /dev/null
+++ b/tests/src/utils/sensor_id_eq.hpp
@@ -0,0 +1,14 @@
+#pragma once
+
+#include "interfaces/sensor.hpp"
+
+#include <gmock/gmock.h>
+
+inline auto sensorIdEq(interfaces::Sensor::Id id)
+{
+    using namespace testing;
+
+    return AllOf(Field(&interfaces::Sensor::Id::type, Eq(id.type)),
+                 Field(&interfaces::Sensor::Id::service, Eq(id.service)),
+                 Field(&interfaces::Sensor::Id::path, Eq(id.path)));
+}