Added Periodic reportingType support to Report

When report interval expires report will collect readings from
metrics and update timestamp.

Tested:
  - Added new units tests covering added code
  - All existing unit tests are passing

Change-Id: I7f23ca05d77efb0f18d2c0d0f138c524ffb4f6af
Signed-off-by: Krzysztof Grobelny <krzysztof.grobelny@intel.com>
diff --git a/meson.build b/meson.build
index 165eb2d..9dcbcb2 100644
--- a/meson.build
+++ b/meson.build
@@ -72,6 +72,7 @@
     'telemetry',
     [
         'src/main.cpp',
+        'src/metric.cpp',
         'src/persistent_json_storage.cpp',
         'src/report.cpp',
         'src/report_factory.cpp',
diff --git a/src/interfaces/metric.hpp b/src/interfaces/metric.hpp
new file mode 100644
index 0000000..51fc8fa
--- /dev/null
+++ b/src/interfaces/metric.hpp
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "metric_value.hpp"
+
+#include <vector>
+
+namespace interfaces
+{
+
+class Metric
+{
+  public:
+    virtual ~Metric() = default;
+
+    virtual const std::vector<MetricValue>& getReadings() const = 0;
+};
+
+} // namespace interfaces
diff --git a/src/interfaces/types.hpp b/src/interfaces/types.hpp
index 09a75cd..0d5eeb2 100644
--- a/src/interfaces/types.hpp
+++ b/src/interfaces/types.hpp
@@ -9,3 +9,7 @@
 using ReadingParameters =
     std::vector<std::tuple<std::vector<sdbusplus::message::object_path>,
                            std::string, std::string, std::string>>;
+
+using Readings = std::tuple<
+    uint64_t,
+    std::vector<std::tuple<std::string, std::string, double, uint64_t>>>;
diff --git a/src/metric.cpp b/src/metric.cpp
new file mode 100644
index 0000000..b40da5a
--- /dev/null
+++ b/src/metric.cpp
@@ -0,0 +1 @@
+#include "metric.hpp"
diff --git a/src/metric.hpp b/src/metric.hpp
new file mode 100644
index 0000000..a676e25
--- /dev/null
+++ b/src/metric.hpp
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "interfaces/metric.hpp"
+#include "interfaces/sensor_listener.hpp"
+
+class Metric : public interfaces::Metric, public interfaces::SensorListener
+{
+  public:
+    const std::vector<MetricValue>& getReadings() const override
+    {
+        return readings;
+    }
+
+    void sensorUpdated(interfaces::Sensor&, uint64_t) override
+    {}
+
+    void sensorUpdated(interfaces::Sensor&, uint64_t, double value) override
+    {}
+
+  private:
+    std::vector<MetricValue> readings;
+};
diff --git a/src/metric_value.hpp b/src/metric_value.hpp
new file mode 100644
index 0000000..d01ac4e
--- /dev/null
+++ b/src/metric_value.hpp
@@ -0,0 +1,12 @@
+#pragma once
+
+#include <cstdint>
+#include <string>
+
+struct MetricValue
+{
+    std::string id;
+    std::string metadata;
+    double value;
+    uint64_t timestamp;
+};
diff --git a/src/report.cpp b/src/report.cpp
index 81e3b05..2e68d45 100644
--- a/src/report.cpp
+++ b/src/report.cpp
@@ -2,9 +2,7 @@
 
 #include "report_manager.hpp"
 
-using Readings = std::tuple<
-    uint64_t,
-    std::vector<std::tuple<std::string, std::string, double, uint64_t>>>;
+#include <numeric>
 
 Report::Report(boost::asio::io_context& ioc,
                const std::shared_ptr<sdbusplus::asio::object_server>& objServer,
@@ -13,9 +11,11 @@
                const bool logToMetricReportsCollection,
                const std::chrono::milliseconds period,
                const ReadingParameters& metricParams,
-               interfaces::ReportManager& reportManager) :
-    name{reportName},
-    path{reportDir + name}, interval{period}, objServer(objServer)
+               interfaces::ReportManager& reportManager,
+               std::vector<std::shared_ptr<interfaces::Metric>> metrics) :
+    name(reportName),
+    path(reportDir + name), interval(period), objServer(objServer),
+    metrics(std::move(metrics)), timer(ioc)
 {
     reportIface = objServer->add_unique_interface(
         path, reportIfaceName,
@@ -34,7 +34,10 @@
                     return true;
                 });
             dbusIface.register_property("Persistency", bool{false});
-            dbusIface.register_property("Readings", Readings{});
+            dbusIface.register_property_r(
+                "Readings", readings,
+                sdbusplus::vtable::property_::emits_change,
+                [this](const auto&) { return readings; });
             dbusIface.register_property("ReportingType", reportingType);
             dbusIface.register_property("ReadingParameters", metricParams);
             dbusIface.register_property("EmitsReadingsUpdate",
@@ -51,4 +54,52 @@
                 });
             });
         });
+
+    if (reportingType == "Periodic")
+    {
+        scheduleTimer(interval);
+    }
+}
+
+void Report::timerProc(boost::system::error_code ec, Report& self)
+{
+    if (ec)
+    {
+        return;
+    }
+
+    self.updateReadings();
+    self.scheduleTimer(self.interval);
+}
+
+void Report::scheduleTimer(std::chrono::milliseconds timerInterval)
+{
+    timer.expires_after(timerInterval);
+    timer.async_wait(
+        [this](boost::system::error_code ec) { timerProc(ec, *this); });
+}
+
+void Report::updateReadings()
+{
+    auto numElements = std::accumulate(
+        metrics.begin(), metrics.end(), 0u, [](auto sum, const auto& metric) {
+            return sum + metric->getReadings().size();
+        });
+
+    readingsCache.resize(numElements);
+
+    auto it = readingsCache.begin();
+
+    for (const auto& metric : metrics)
+    {
+        for (const auto& reading : metric->getReadings())
+        {
+            *(it++) = std::make_tuple(reading.id, reading.metadata,
+                                      reading.value, reading.timestamp);
+        }
+    }
+
+    std::get<0>(readings) = std::time(0);
+    std::get<1>(readings) = readingsCache;
+    reportIface->signal_property("Readings");
 }
diff --git a/src/report.hpp b/src/report.hpp
index fb40614..8cef26a 100644
--- a/src/report.hpp
+++ b/src/report.hpp
@@ -1,10 +1,12 @@
 #pragma once
 
+#include "interfaces/metric.hpp"
 #include "interfaces/report.hpp"
 #include "interfaces/report_manager.hpp"
 #include "interfaces/types.hpp"
 
 #include <boost/asio/io_context.hpp>
+#include <boost/asio/steady_timer.hpp>
 #include <sdbusplus/asio/object_server.hpp>
 
 #include <chrono>
@@ -20,12 +22,13 @@
            const bool logToMetricReportsCollection,
            const std::chrono::milliseconds period,
            const ReadingParameters& metricParams,
-           interfaces::ReportManager& reportManager);
+           interfaces::ReportManager& reportManager,
+           std::vector<std::shared_ptr<interfaces::Metric>> metrics);
     ~Report() = default;
 
-    Report(Report&) = delete;
+    Report(const Report&) = delete;
     Report(Report&&) = delete;
-    Report& operator=(Report&) = delete;
+    Report& operator=(const Report&) = delete;
     Report& operator=(Report&&) = delete;
 
     std::string getName() const override
@@ -39,12 +42,20 @@
     }
 
   private:
+    static void timerProc(boost::system::error_code, Report& self);
+    void scheduleTimer(std::chrono::milliseconds interval);
+    void updateReadings();
+
     const std::string name;
     const std::string path;
     std::chrono::milliseconds interval;
+    Readings readings = {};
+    std::tuple_element_t<1, Readings> readingsCache = {};
     std::shared_ptr<sdbusplus::asio::object_server> objServer;
     std::unique_ptr<sdbusplus::asio::dbus_interface> reportIface;
     std::unique_ptr<sdbusplus::asio::dbus_interface> deleteIface;
+    std::vector<std::shared_ptr<interfaces::Metric>> metrics;
+    boost::asio::steady_timer timer;
 
   public:
     static constexpr const char* reportIfaceName =
diff --git a/src/report_factory.cpp b/src/report_factory.cpp
index 9f51db5..27e4ee7 100644
--- a/src/report_factory.cpp
+++ b/src/report_factory.cpp
@@ -15,7 +15,10 @@
     std::chrono::milliseconds period, const ReadingParameters& metricParams,
     interfaces::ReportManager& reportManager) const
 {
+    std::vector<std::shared_ptr<interfaces::Metric>> metrics;
+
     return std::make_unique<Report>(
         ioc, objServer, name, reportingType, emitsReadingsSignal,
-        logToMetricReportsCollection, period, metricParams, reportManager);
+        logToMetricReportsCollection, period, metricParams, reportManager,
+        std::move(metrics));
 }
diff --git a/tests/meson.build b/tests/meson.build
index fbee253..73846f6 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -23,6 +23,7 @@
     executable(
         'telemetry-ut',
         [
+            '../src/metric.cpp',
             '../src/persistent_json_storage.cpp',
             '../src/report.cpp',
             '../src/report_factory.cpp',
@@ -33,6 +34,7 @@
             'src/main.cpp',
             'src/stubs/dbus_sensor_object.cpp',
             'src/test_detached_timer.cpp',
+            'src/test_metric.cpp',
             'src/test_persistent_json_storage.cpp',
             'src/test_report.cpp',
             'src/test_report_manager.cpp',
diff --git a/tests/src/mocks/metric_mock.hpp b/tests/src/mocks/metric_mock.hpp
new file mode 100644
index 0000000..6150455
--- /dev/null
+++ b/tests/src/mocks/metric_mock.hpp
@@ -0,0 +1,20 @@
+#pragma once
+
+#include "interfaces/metric.hpp"
+
+#include <gmock/gmock.h>
+
+class MetricMock : public interfaces::Metric
+{
+  public:
+    MetricMock()
+    {
+        using namespace testing;
+
+        ON_CALL(*this, getReadings())
+            .WillByDefault(ReturnRefOfCopy(std::vector<MetricValue>()));
+    }
+
+    MOCK_METHOD(const std::vector<MetricValue>&, getReadings, (),
+                (const, override));
+};
diff --git a/tests/src/params/report_params.hpp b/tests/src/params/report_params.hpp
new file mode 100644
index 0000000..908fb5d
--- /dev/null
+++ b/tests/src/params/report_params.hpp
@@ -0,0 +1,34 @@
+#pragma once
+
+#include <chrono>
+#include <string>
+
+class ReportParams final
+{
+  public:
+    ReportParams& reportName(std::string val)
+    {
+        reportNameProperty = std::move(val);
+        return *this;
+    }
+
+    const std::string& reportName() const
+    {
+        return reportNameProperty;
+    }
+
+    ReportParams& reportingType(std::string val)
+    {
+        reportingTypeProperty = std::move(val);
+        return *this;
+    }
+
+    const std::string& reportingType() const
+    {
+        return reportingTypeProperty;
+    }
+
+  protected:
+    std::string reportNameProperty = "TestReport";
+    std::string reportingTypeProperty = "OnRequest";
+};
\ No newline at end of file
diff --git a/tests/src/test_metric.cpp b/tests/src/test_metric.cpp
new file mode 100644
index 0000000..32ed2a6
--- /dev/null
+++ b/tests/src/test_metric.cpp
@@ -0,0 +1,11 @@
+#include "metric.hpp"
+
+#include <gmock/gmock.h>
+
+using namespace testing;
+
+class TestMetric : public Test
+{
+  public:
+    Metric sut = {};
+};
\ No newline at end of file
diff --git a/tests/src/test_report.cpp b/tests/src/test_report.cpp
index dea453d..bb0f4c5 100644
--- a/tests/src/test_report.cpp
+++ b/tests/src/test_report.cpp
@@ -1,18 +1,20 @@
 #include "dbus_environment.hpp"
+#include "mocks/metric_mock.hpp"
 #include "mocks/report_manager_mock.hpp"
+#include "params/report_params.hpp"
 #include "report.hpp"
 #include "report_manager.hpp"
+#include "utils/conv_container.hpp"
 
 #include <sdbusplus/exception.hpp>
 
 using namespace testing;
 using namespace std::literals::string_literals;
+using namespace std::chrono_literals;
 
 class TestReport : public Test
 {
   public:
-    std::string defaultReportName = "TestReport";
-    std::string defaultReportType = "Periodic";
     bool defaultEmitReadingSignal = true;
     bool defaultLogToMetricReportCollection = true;
     uint64_t defaultInterval = ReportManager::minInterval.count();
@@ -20,12 +22,28 @@
 
     std::unique_ptr<ReportManagerMock> reportManagerMock =
         std::make_unique<StrictMock<ReportManagerMock>>();
-    Report sut =
-        Report(DbusEnvironment::getIoc(), DbusEnvironment::getObjServer(),
-               defaultReportName, defaultReportType, defaultEmitReadingSignal,
-               defaultLogToMetricReportCollection,
-               std::chrono::milliseconds{defaultInterval}, defaultReadingParams,
-               *reportManagerMock);
+    std::vector<std::shared_ptr<MetricMock>> metricMocks = {
+        std::make_shared<NiceMock<MetricMock>>(),
+        std::make_shared<NiceMock<MetricMock>>(),
+        std::make_shared<NiceMock<MetricMock>>()};
+    std::unique_ptr<Report> sut;
+
+    void SetUp() override
+    {
+        sut = makeReport(ReportParams());
+    }
+
+    std::unique_ptr<Report> makeReport(const ReportParams& params)
+    {
+        return std::make_unique<Report>(
+            DbusEnvironment::getIoc(), DbusEnvironment::getObjServer(),
+            params.reportName(), params.reportingType(),
+            defaultEmitReadingSignal, defaultLogToMetricReportCollection,
+            std::chrono::milliseconds(defaultInterval), defaultReadingParams,
+            *reportManagerMock,
+            utils::convContainer<std::shared_ptr<interfaces::Metric>>(
+                metricMocks));
+    }
 
     template <class T>
     static T getProperty(const std::string& path, const std::string& property)
@@ -78,42 +96,47 @@
 
 TEST_F(TestReport, verifyIfPropertiesHaveValidValue)
 {
-    EXPECT_THAT(getProperty<uint64_t>(sut.getPath(), "Interval"),
+    EXPECT_THAT(getProperty<uint64_t>(sut->getPath(), "Interval"),
                 Eq(defaultInterval));
-    EXPECT_THAT(getProperty<bool>(sut.getPath(), "Persistency"), Eq(false));
-    EXPECT_THAT(getProperty<std::string>(sut.getPath(), "ReportingType"),
-                Eq(defaultReportType));
-    EXPECT_THAT(getProperty<bool>(sut.getPath(), "EmitsReadingsUpdate"),
+    EXPECT_THAT(getProperty<bool>(sut->getPath(), "Persistency"), Eq(false));
+    EXPECT_THAT(getProperty<bool>(sut->getPath(), "EmitsReadingsUpdate"),
                 Eq(defaultEmitReadingSignal));
     EXPECT_THAT(
-        getProperty<bool>(sut.getPath(), "LogToMetricReportsCollection"),
+        getProperty<bool>(sut->getPath(), "LogToMetricReportsCollection"),
         Eq(defaultLogToMetricReportCollection));
     EXPECT_THAT(
-        getProperty<ReadingParameters>(sut.getPath(), "ReadingParameters"),
+        getProperty<ReadingParameters>(sut->getPath(), "ReadingParameters"),
         Eq(defaultReadingParams));
 }
 
+TEST_F(TestReport, readingsAreInitialyEmpty)
+{
+    EXPECT_THAT(getProperty<Readings>(sut->getPath(), "Readings"),
+                Eq(Readings{}));
+}
+
 TEST_F(TestReport, setIntervalWithValidValue)
 {
     uint64_t newValue = defaultInterval + 1;
-    EXPECT_THAT(setProperty(sut.getPath(), "Interval", newValue).value(),
+    EXPECT_THAT(setProperty(sut->getPath(), "Interval", newValue).value(),
                 Eq(boost::system::errc::success));
-    EXPECT_THAT(getProperty<uint64_t>(sut.getPath(), "Interval"), Eq(newValue));
+    EXPECT_THAT(getProperty<uint64_t>(sut->getPath(), "Interval"),
+                Eq(newValue));
 }
 
 TEST_F(TestReport, settingIntervalWithInvalidValueDoesNotChangeProperty)
 {
     uint64_t newValue = defaultInterval - 1;
-    EXPECT_THAT(setProperty(sut.getPath(), "Interval", newValue).value(),
+    EXPECT_THAT(setProperty(sut->getPath(), "Interval", newValue).value(),
                 Eq(boost::system::errc::success));
-    EXPECT_THAT(getProperty<uint64_t>(sut.getPath(), "Interval"),
+    EXPECT_THAT(getProperty<uint64_t>(sut->getPath(), "Interval"),
                 Eq(defaultInterval));
 }
 
 TEST_F(TestReport, deleteReport)
 {
-    EXPECT_CALL(*reportManagerMock, removeReport(&sut));
-    auto ec = deleteReport(sut.getPath());
+    EXPECT_CALL(*reportManagerMock, removeReport(sut.get()));
+    auto ec = deleteReport(sut->getPath());
     EXPECT_THAT(ec, Eq(boost::system::errc::success));
 }
 
@@ -123,47 +146,127 @@
     EXPECT_THAT(ec.value(), Eq(EBADR));
 }
 
-class TestReportCreation : public Test
+class TestReportValidNames :
+    public TestReport,
+    public WithParamInterface<ReportParams>
 {
   public:
-    std::unique_ptr<ReportManagerMock> reportManagerMock =
-        std::make_unique<StrictMock<ReportManagerMock>>();
-
-    std::unique_ptr<Report> createReportWithName(std::string name)
-    {
-        return std::make_unique<Report>(
-            DbusEnvironment::getIoc(), DbusEnvironment::getObjServer(), name,
-            "", true, true,
-            std::chrono::milliseconds{ReportManager::minInterval.count()},
-            ReadingParameters{}, *reportManagerMock);
-    }
+    void SetUp() override
+    {}
 };
 
-class TestReportValidNames :
-    public TestReportCreation,
-    public WithParamInterface<const char*>
-{};
-
-INSTANTIATE_TEST_SUITE_P(ValidNames, TestReportValidNames,
-                         ValuesIn({"Valid_1", "Valid_1/Valid_2",
-                                   "Valid_1/Valid_2/Valid_3"}));
+INSTANTIATE_TEST_SUITE_P(
+    ValidNames, TestReportValidNames,
+    Values(ReportParams().reportName("Valid_1"),
+           ReportParams().reportName("Valid_1/Valid_2"),
+           ReportParams().reportName("Valid_1/Valid_2/Valid_3")));
 
 TEST_P(TestReportValidNames, reportCtorDoesNotThrowOnValidName)
 {
-    EXPECT_NO_THROW(createReportWithName(GetParam()));
+    EXPECT_NO_THROW(makeReport(GetParam()));
 }
 
 class TestReportInvalidNames :
-    public TestReportCreation,
-    public WithParamInterface<const char*>
-{};
+    public TestReport,
+    public WithParamInterface<ReportParams>
+{
+  public:
+    void SetUp() override
+    {}
+};
 
 INSTANTIATE_TEST_SUITE_P(InvalidNames, TestReportInvalidNames,
-                         ValuesIn({"/", "/Invalid", "Invalid/",
-                                   "Invalid/Invalid/", "Invalid?"}));
+                         Values(ReportParams().reportName("/"),
+                                ReportParams().reportName("/Invalid"),
+                                ReportParams().reportName("Invalid/"),
+                                ReportParams().reportName("Invalid/Invalid/"),
+                                ReportParams().reportName("Invalid?")));
 
 TEST_P(TestReportInvalidNames, reportCtorThrowOnInvalidName)
 {
-    EXPECT_THROW(createReportWithName(GetParam()),
-                 sdbusplus::exception::SdBusError);
+    EXPECT_THROW(makeReport(GetParam()), sdbusplus::exception::SdBusError);
+}
+
+class TestReportAllReportTypes :
+    public TestReport,
+    public WithParamInterface<ReportParams>
+{
+    void SetUp() override
+    {
+        sut = makeReport(GetParam());
+    }
+};
+
+INSTANTIATE_TEST_SUITE_P(_, TestReportAllReportTypes,
+                         Values(ReportParams().reportingType("OnRequest"),
+                                ReportParams().reportingType("OnChange"),
+                                ReportParams().reportingType("Periodic")));
+
+TEST_P(TestReportAllReportTypes, returnPropertValueOfReportType)
+{
+    EXPECT_THAT(getProperty<std::string>(sut->getPath(), "ReportingType"),
+                Eq(GetParam().reportingType()));
+}
+
+class TestReportNonPeriodicReport :
+    public TestReport,
+    public WithParamInterface<ReportParams>
+{
+    void SetUp() override
+    {
+        sut = makeReport(GetParam());
+    }
+};
+
+INSTANTIATE_TEST_SUITE_P(_, TestReportNonPeriodicReport,
+                         Values(ReportParams().reportingType("OnRequest"),
+                                ReportParams().reportingType("OnChange")));
+
+TEST_P(TestReportNonPeriodicReport, readingsAreNotUpdatedAfterIntervalExpires)
+{
+    DbusEnvironment::sleepFor(ReportManager::minInterval + 1ms);
+
+    EXPECT_THAT(getProperty<Readings>(sut->getPath(), "Readings"),
+                Eq(Readings{}));
+}
+
+class TestReportPeriodicReport : public TestReport
+{
+    void SetUp() override
+    {
+        sut = makeReport(ReportParams().reportingType("Periodic"));
+
+        ASSERT_THAT(metricMocks, SizeIs(Ge(2)));
+        ON_CALL(*metricMocks[0], getReadings())
+            .WillByDefault(ReturnRefOfCopy(std::vector<MetricValue>(
+                {MetricValue{"a", "b", 17.1, 114},
+                 MetricValue{"aaa", "bbb", 21.7, 100}})));
+        ON_CALL(*metricMocks[1], getReadings())
+            .WillByDefault(ReturnRefOfCopy(
+                std::vector<MetricValue>({MetricValue{"aa", "bb", 42.0, 74}})));
+    }
+};
+
+TEST_F(TestReportPeriodicReport, readingTimestampIsUpdatedAfterIntervalExpires)
+{
+    const uint64_t expectedTime = std::time(0);
+    DbusEnvironment::sleepFor(ReportManager::minInterval + 1ms);
+
+    const auto [timestamp, readings] =
+        getProperty<Readings>(sut->getPath(), "Readings");
+
+    EXPECT_THAT(timestamp, Ge(expectedTime));
+}
+
+TEST_F(TestReportPeriodicReport, readingsAreUpdatedAfterIntervalExpires)
+{
+    DbusEnvironment::sleepFor(ReportManager::minInterval + 1ms);
+
+    const auto [timestamp, readings] =
+        getProperty<Readings>(sut->getPath(), "Readings");
+
+    EXPECT_THAT(readings,
+                ElementsAre(std::make_tuple("a"s, "b"s, 17.1, 114u),
+                            std::make_tuple("aaa"s, "bbb"s, 21.7, 100u),
+                            std::make_tuple("aa"s, "bb"s, 42.0, 74u)));
 }
diff --git a/tests/src/utils/conv_container.hpp b/tests/src/utils/conv_container.hpp
new file mode 100644
index 0000000..3806a2a
--- /dev/null
+++ b/tests/src/utils/conv_container.hpp
@@ -0,0 +1,19 @@
+#pragma once
+
+#include <algorithm>
+
+namespace utils
+{
+
+template <class R, class T, class... Args,
+          template <class, class...> class Container>
+auto convContainer(const Container<T, Args...>& container)
+{
+    Container<R> result;
+    std::transform(container.begin(), container.end(),
+                   std::back_inserter(result),
+                   [](const auto& item) -> R { return item; });
+    return result;
+}
+
+} // namespace utils