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/meson.build b/meson.build
index 8d0b018..d3472da 100644
--- a/meson.build
+++ b/meson.build
@@ -29,7 +29,12 @@
language: 'cpp'
)
-boost = dependency('boost', version: '>=1.74.0', required: false)
+boost = dependency(
+ 'boost',
+ version: '>=1.74.0',
+ required: false,
+ modules: ['coroutine'])
+
if not boost.found()
subproject('boost', required: false)
boost = declare_dependency(include_directories: 'subprojects/boost_1_74_0')
@@ -70,6 +75,7 @@
'src/persistent_json_storage.cpp',
'src/report.cpp',
'src/report_manager.cpp',
+ 'src/sensor.cpp',
'src/sensor_cache.cpp',
],
dependencies: [
diff --git a/src/interfaces/sensor.hpp b/src/interfaces/sensor.hpp
index dbeb158..07c7897 100644
--- a/src/interfaces/sensor.hpp
+++ b/src/interfaces/sensor.hpp
@@ -1,5 +1,7 @@
#pragma once
+#include <chrono>
+#include <memory>
#include <ostream>
#include <string>
#include <string_view>
@@ -8,6 +10,8 @@
namespace interfaces
{
+class SensorListener;
+
class Sensor
{
public:
@@ -28,11 +32,17 @@
return std::tie(type, service, path) <
std::tie(other.type, other.service, other.path);
}
+
+ inline std::string str() const
+ {
+ return type + ":" + service + ":" + path;
+ }
};
virtual ~Sensor() = default;
virtual Id id() const = 0;
+ virtual void registerForUpdates(const std::weak_ptr<SensorListener>&) = 0;
};
} // namespace interfaces
diff --git a/src/interfaces/sensor_listener.hpp b/src/interfaces/sensor_listener.hpp
new file mode 100644
index 0000000..9a2f0a2
--- /dev/null
+++ b/src/interfaces/sensor_listener.hpp
@@ -0,0 +1,19 @@
+#pragma once
+
+#include <optional>
+
+namespace interfaces
+{
+
+class Sensor;
+
+class SensorListener
+{
+ public:
+ virtual ~SensorListener() = default;
+
+ virtual void sensorUpdated(interfaces::Sensor&, uint64_t) = 0;
+ virtual void sensorUpdated(interfaces::Sensor&, uint64_t, double) = 0;
+};
+
+} // namespace interfaces
diff --git a/src/sensor.cpp b/src/sensor.cpp
new file mode 100644
index 0000000..d38fbd9
--- /dev/null
+++ b/src/sensor.cpp
@@ -0,0 +1,168 @@
+#include "sensor.hpp"
+
+#include "utils/detached_timer.hpp"
+
+#include <boost/container/flat_map.hpp>
+#include <phosphor-logging/log.hpp>
+#include <sdbusplus/asio/property.hpp>
+#include <sdbusplus/bus/match.hpp>
+
+#include <functional>
+#include <iostream>
+
+Sensor::Sensor(interfaces::Sensor::Id sensorId, boost::asio::io_context& ioc,
+ const std::shared_ptr<sdbusplus::asio::connection>& bus) :
+ sensorId(std::move(sensorId)),
+ ioc(ioc), bus(bus)
+{}
+
+Sensor::Id Sensor::makeId(std::string_view service, std::string_view path)
+{
+ return Id("Sensor", service, path);
+}
+
+Sensor::Id Sensor::id() const
+{
+ return sensorId;
+}
+
+void Sensor::async_read()
+{
+ uniqueCall([this](auto lock) { async_read(std::move(lock)); });
+}
+
+void Sensor::async_read(std::shared_ptr<utils::UniqueCall::Lock> lock)
+{
+ makeSignalMonitor();
+
+ sdbusplus::asio::getProperty<double>(
+ *bus, sensorId.service, sensorId.path,
+ "xyz.openbmc_project.Sensor.Value", "Value",
+ [lock, id = sensorId,
+ weakSelf = weak_from_this()](boost::system::error_code ec) {
+ phosphor::logging::log<phosphor::logging::level::ERR>(
+ "DBus 'GetProperty' call failed on Sensor Value",
+ phosphor::logging::entry("sensor=%s, ec=%lu", id.str().c_str(),
+ ec.value()));
+
+ if (auto self = weakSelf.lock())
+ {
+
+ constexpr auto retryIntervalAfterFailedRead =
+ std::chrono::seconds(30);
+ utils::makeDetachedTimer(
+ self->ioc, retryIntervalAfterFailedRead, [weakSelf] {
+ if (auto self = weakSelf.lock())
+ {
+ self->async_read();
+ }
+ });
+ }
+ },
+ [lock, weakSelf = weak_from_this()](double newValue) {
+ if (auto self = weakSelf.lock())
+ {
+ self->updateValue(newValue);
+ }
+ });
+}
+
+void Sensor::registerForUpdates(
+ const std::weak_ptr<interfaces::SensorListener>& weakListener)
+{
+ if (auto listener = weakListener.lock())
+ {
+ listeners.emplace_back(weakListener);
+
+ if (value)
+ {
+ listener->sensorUpdated(*this, timestamp, *value);
+ }
+ else
+ {
+ async_read();
+ }
+ }
+}
+
+void Sensor::updateValue(double newValue)
+{
+ timestamp = std::time(0);
+
+ if (value == newValue)
+ {
+ for (const auto& weakListener : listeners)
+ {
+ if (auto listener = weakListener.lock())
+ {
+ listener->sensorUpdated(*this, timestamp);
+ }
+ }
+ }
+ else
+ {
+ value = newValue;
+
+ for (const auto& weakListener : listeners)
+ {
+ if (auto listener = weakListener.lock())
+ {
+ listener->sensorUpdated(*this, timestamp, *value);
+ }
+ }
+ }
+}
+
+void Sensor::makeSignalMonitor()
+{
+ if (signalMonitor)
+ {
+ return;
+ }
+
+ using namespace std::string_literals;
+
+ const auto param = "type='signal',member='PropertiesChanged',path='"s +
+ sensorId.path +
+ "',arg0='xyz.openbmc_project.Sensor.Value'"s;
+
+ signalMonitor = std::make_unique<sdbusplus::bus::match::match>(
+ *bus, param,
+ [weakSelf = weak_from_this()](sdbusplus::message::message& message) {
+ signalProc(weakSelf, message);
+ });
+}
+
+void Sensor::signalProc(const std::weak_ptr<Sensor>& weakSelf,
+ sdbusplus::message::message& message)
+{
+ if (auto self = weakSelf.lock())
+ {
+ std::string iface;
+ boost::container::flat_map<std::string, ValueVariant>
+ changed_properties;
+ std::vector<std::string> invalidated_properties;
+
+ message.read(iface, changed_properties, invalidated_properties);
+
+ if (iface == "xyz.openbmc_project.Sensor.Value")
+ {
+ const auto it = changed_properties.find("Value");
+ if (it != changed_properties.end())
+ {
+ if (auto val = std::get_if<double>(&it->second))
+ {
+ self->updateValue(*val);
+ }
+ else
+ {
+ phosphor::logging::log<phosphor::logging::level::ERR>(
+ "Failed to receive Value from Sensor "
+ "PropertiesChanged signal",
+ phosphor::logging::entry("sensor=%s",
+ self->sensorId.path.c_str()));
+ }
+ }
+ }
+ }
+}
diff --git a/src/sensor.hpp b/src/sensor.hpp
new file mode 100644
index 0000000..1374c54
--- /dev/null
+++ b/src/sensor.hpp
@@ -0,0 +1,52 @@
+#pragma once
+
+#include "interfaces/sensor.hpp"
+#include "interfaces/sensor_listener.hpp"
+#include "utils/unique_call.hpp"
+
+#include <boost/asio/high_resolution_timer.hpp>
+#include <sdbusplus/asio/connection.hpp>
+
+#include <memory>
+
+class Sensor final :
+ public interfaces::Sensor,
+ public std::enable_shared_from_this<Sensor>
+{
+ using ValueVariant = std::variant<std::monostate, double>;
+
+ public:
+ Sensor(interfaces::Sensor::Id sensorId, boost::asio::io_context& ioc,
+ const std::shared_ptr<sdbusplus::asio::connection>& bus);
+
+ Sensor(const Sensor&) = delete;
+ Sensor& operator=(const Sensor&) = delete;
+
+ static Id makeId(std::string_view service, std::string_view path);
+
+ Id id() const override;
+ void registerForUpdates(
+ const std::weak_ptr<interfaces::SensorListener>& weakListener) override;
+
+ private:
+ static std::optional<double> readValue(const ValueVariant& v);
+ static void signalProc(const std::weak_ptr<Sensor>& weakSelf,
+ sdbusplus::message::message&);
+
+ void async_read();
+ void async_read(std::shared_ptr<utils::UniqueCall::Lock>);
+ void makeSignalMonitor();
+ void updateValue(double);
+
+ interfaces::Sensor::Id sensorId;
+ boost::asio::io_context& ioc;
+ std::shared_ptr<sdbusplus::asio::connection> bus;
+ std::chrono::milliseconds timerInterval = std::chrono::milliseconds(0);
+ std::optional<boost::asio::high_resolution_timer> timer;
+
+ utils::UniqueCall uniqueCall;
+ std::vector<std::weak_ptr<interfaces::SensorListener>> listeners;
+ uint64_t timestamp = 0;
+ std::optional<double> value;
+ std::unique_ptr<sdbusplus::bus::match::match> signalMonitor;
+};
diff --git a/src/utils/detached_timer.hpp b/src/utils/detached_timer.hpp
new file mode 100644
index 0000000..21343f9
--- /dev/null
+++ b/src/utils/detached_timer.hpp
@@ -0,0 +1,28 @@
+#pragma once
+
+#include <boost/asio/io_context.hpp>
+#include <boost/asio/spawn.hpp>
+#include <boost/asio/steady_timer.hpp>
+
+#include <chrono>
+
+namespace utils
+{
+
+template <class F>
+void makeDetachedTimer(boost::asio::io_context& ioc,
+ std::chrono::milliseconds delay, F&& fun)
+{
+ auto timer = std::make_unique<boost::asio::steady_timer>(ioc);
+ timer->expires_after(delay);
+ timer->async_wait([timer = std::move(timer),
+ fun = std::move(fun)](boost::system::error_code ec) {
+ if (ec)
+ {
+ return;
+ }
+ fun();
+ });
+}
+
+} // namespace utils
diff --git a/src/utils/unique_call.hpp b/src/utils/unique_call.hpp
new file mode 100644
index 0000000..db10786
--- /dev/null
+++ b/src/utils/unique_call.hpp
@@ -0,0 +1,30 @@
+#pragma once
+
+#include <memory>
+#include <utility>
+
+namespace utils
+{
+
+class UniqueCall
+{
+ public:
+ struct Lock
+ {};
+
+ template <class Functor, class... Args>
+ void operator()(Functor&& functor, Args&&... args)
+ {
+ if (lock.expired())
+ {
+ auto l = std::make_shared<Lock>();
+ lock = l;
+ functor(std::move(l), std::forward<Args>(args)...);
+ }
+ }
+
+ private:
+ std::weak_ptr<Lock> lock;
+};
+
+} // namespace utils
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)));
+}