added support for Collection Functions

new supported operations: min,max,sum,avg
new supported time scopes: interval,startup

added unit test to verify that each collection function returns correct
timestamp and value

Tested:
- POST/GET on telemetry features in bmcweb, no regression detected
- Using dbus API metric with collection function works as expected

Change-Id: Ib364c433915e07fd7a102f00109525362c40ab8a
Signed-off-by: Krzysztof Grobelny <krzysztof.grobelny@intel.com>
diff --git a/src/details/collection_function.cpp b/src/details/collection_function.cpp
new file mode 100644
index 0000000..d92fdf3
--- /dev/null
+++ b/src/details/collection_function.cpp
@@ -0,0 +1,172 @@
+#include "collection_function.hpp"
+
+#include <cmath>
+
+namespace details
+{
+
+class FunctionSingle : public CollectionFunction
+{
+  public:
+    ReadingItem calculate(const std::vector<ReadingItem>& readings,
+                          uint64_t) const override
+    {
+        return readings.back();
+    }
+
+    ReadingItem calculateForStartupInterval(std::vector<ReadingItem>& readings,
+                                            uint64_t timestamp) const override
+    {
+        readings.assign({readings.back()});
+        return readings.back();
+    }
+};
+
+class FunctionMinimum : public CollectionFunction
+{
+  public:
+    ReadingItem calculate(const std::vector<ReadingItem>& readings,
+                          uint64_t) const override
+    {
+        return *std::min_element(
+            readings.begin(), readings.end(),
+            [](const auto& left, const auto& right) {
+                return std::make_tuple(!std::isfinite(left.second),
+                                       left.second) <
+                       std::make_tuple(!std::isfinite(right.second),
+                                       right.second);
+            });
+    }
+
+    ReadingItem calculateForStartupInterval(std::vector<ReadingItem>& readings,
+                                            uint64_t timestamp) const override
+    {
+        readings.assign({ReadingItem(calculate(readings, timestamp))});
+        return readings.back();
+    }
+};
+
+class FunctionMaximum : public CollectionFunction
+{
+  public:
+    ReadingItem calculate(const std::vector<ReadingItem>& readings,
+                          uint64_t) const override
+    {
+        return *std::max_element(
+            readings.begin(), readings.end(),
+            [](const auto& left, const auto& right) {
+                return std::make_tuple(std::isfinite(left.second),
+                                       left.second) <
+                       std::make_tuple(std::isfinite(right.second),
+                                       right.second);
+            });
+    }
+
+    ReadingItem calculateForStartupInterval(std::vector<ReadingItem>& readings,
+                                            uint64_t timestamp) const override
+    {
+        readings.assign({ReadingItem(calculate(readings, timestamp))});
+        return readings.back();
+    }
+};
+
+class FunctionAverage : public CollectionFunction
+{
+  public:
+    ReadingItem calculate(const std::vector<ReadingItem>& readings,
+                          uint64_t timestamp) const override
+    {
+        auto valueSum = 0.0;
+        auto timeSum = uint64_t{0};
+        for (auto it = readings.begin(); it != std::prev(readings.end()); ++it)
+        {
+            if (std::isfinite(it->second))
+            {
+                const auto kt = std::next(it);
+                const auto duration = kt->first - it->first;
+                valueSum += it->second * duration;
+                timeSum += duration;
+            }
+        }
+
+        const auto duration = timestamp - readings.back().first;
+        valueSum += readings.back().second * duration;
+        timeSum += duration;
+
+        return ReadingItem{timestamp, valueSum / timeSum};
+    }
+
+    ReadingItem calculateForStartupInterval(std::vector<ReadingItem>& readings,
+                                            uint64_t timestamp) const override
+    {
+        auto result = calculate(readings, timestamp);
+        if (std::isfinite(result.second))
+        {
+            readings.assign({ReadingItem(readings.front().first, result.second),
+                             ReadingItem(timestamp, readings.back().second)});
+        }
+        return result;
+    }
+};
+
+class FunctionSummation : public CollectionFunction
+{
+  public:
+    ReadingItem calculate(const std::vector<ReadingItem>& readings,
+                          uint64_t timestamp) const override
+    {
+        auto valueSum = 0.0;
+        for (auto it = readings.begin(); it != std::prev(readings.end()); ++it)
+        {
+            if (std::isfinite(it->second))
+            {
+                const auto kt = std::next(it);
+                const auto duration = kt->first - it->first;
+                valueSum += it->second * duration;
+            }
+        }
+
+        const auto duration = timestamp - readings.back().first;
+        valueSum += readings.back().second * duration;
+
+        return ReadingItem{timestamp, valueSum};
+    }
+
+    ReadingItem calculateForStartupInterval(std::vector<ReadingItem>& readings,
+                                            uint64_t timestamp) const override
+    {
+        auto result = calculate(readings, timestamp);
+        if (std::isfinite(result.second) && timestamp > 0u)
+        {
+            readings.assign({ReadingItem(timestamp - 1u, result.second),
+                             ReadingItem(timestamp, readings.back().second)});
+        }
+        return result;
+    }
+};
+
+std::shared_ptr<CollectionFunction>
+    makeCollectionFunction(OperationType operationType)
+{
+    using namespace std::string_literals;
+
+    switch (operationType)
+    {
+        case OperationType::single:
+            return std::make_shared<FunctionSingle>();
+        case OperationType::min:
+            return std::make_shared<FunctionMinimum>();
+        case OperationType::max:
+            return std::make_shared<FunctionMaximum>();
+        case OperationType::avg:
+            return std::make_shared<FunctionAverage>();
+        case OperationType::sum:
+            return std::make_shared<FunctionSummation>();
+        default:
+            throw std::runtime_error("op: "s +
+                                     utils::enumToString(operationType) +
+                                     " is not supported"s);
+    }
+}
+
+} // namespace details
diff --git a/src/details/collection_function.hpp b/src/details/collection_function.hpp
new file mode 100644
index 0000000..a8708e7
--- /dev/null
+++ b/src/details/collection_function.hpp
@@ -0,0 +1,29 @@
+#pragma once
+
+#include "types/operation_type.hpp"
+
+#include <cstdint>
+#include <memory>
+#include <utility>
+#include <vector>
+
+namespace details
+{
+
+using ReadingItem = std::pair<uint64_t, double>;
+
+class CollectionFunction
+{
+  public:
+    virtual ~CollectionFunction() = default;
+
+    virtual ReadingItem calculate(const std::vector<ReadingItem>& readings,
+                                  uint64_t timestamp) const = 0;
+    virtual ReadingItem
+        calculateForStartupInterval(std::vector<ReadingItem>& readings,
+                                    uint64_t timestamp) const = 0;
+};
+
+std::shared_ptr<CollectionFunction> makeCollectionFunction(OperationType);
+
+} // namespace details
diff --git a/src/interfaces/clock.hpp b/src/interfaces/clock.hpp
new file mode 100644
index 0000000..0c355b7
--- /dev/null
+++ b/src/interfaces/clock.hpp
@@ -0,0 +1,22 @@
+#pragma once
+
+#include <chrono>
+
+namespace interfaces
+{
+
+class Clock
+{
+  public:
+    using duration = std::chrono::steady_clock::time_point::duration;
+    using rep = std::chrono::steady_clock::time_point::rep;
+    using period = std::chrono::steady_clock::time_point::period;
+    using time_point = std::chrono::steady_clock::time_point;
+
+    virtual ~Clock() = default;
+
+    virtual time_point now() const noexcept = 0;
+    virtual uint64_t timestamp() const noexcept = 0;
+};
+
+} // namespace interfaces
diff --git a/src/interfaces/metric.hpp b/src/interfaces/metric.hpp
index 3279fd7..62a8f44 100644
--- a/src/interfaces/metric.hpp
+++ b/src/interfaces/metric.hpp
@@ -16,7 +16,7 @@
     virtual ~Metric() = default;
 
     virtual void initialize() = 0;
-    virtual const std::vector<MetricValue>& getReadings() const = 0;
+    virtual std::vector<MetricValue> getReadings() const = 0;
     virtual LabeledMetricParameters dumpConfiguration() const = 0;
 };
 
diff --git a/src/metric.cpp b/src/metric.cpp
index 9dd4e67..04d3fe9 100644
--- a/src/metric.cpp
+++ b/src/metric.cpp
@@ -1,25 +1,138 @@
 #include "metric.hpp"
 
+#include "details/collection_function.hpp"
 #include "types/report_types.hpp"
 #include "utils/labeled_tuple.hpp"
 #include "utils/transform.hpp"
 
 #include <algorithm>
 
+class Metric::CollectionData
+{
+  public:
+    using ReadingItem = details::ReadingItem;
+
+    virtual ~CollectionData() = default;
+
+    virtual ReadingItem update(uint64_t timestamp) = 0;
+    virtual ReadingItem update(uint64_t timestamp, double value) = 0;
+};
+
+class Metric::DataPoint : public Metric::CollectionData
+{
+  public:
+    ReadingItem update(uint64_t timestamp) override
+    {
+        return ReadingItem{lastTimestamp, lastReading};
+    }
+
+    ReadingItem update(uint64_t timestamp, double reading) override
+    {
+        lastTimestamp = timestamp;
+        lastReading = reading;
+        return update(timestamp);
+    }
+
+  private:
+    uint64_t lastTimestamp = 0u;
+    double lastReading = 0.0;
+};
+
+class Metric::DataInterval : public Metric::CollectionData
+{
+  public:
+    DataInterval(std::shared_ptr<details::CollectionFunction> function,
+                 CollectionDuration duration) :
+        function(std::move(function)),
+        duration(duration)
+    {}
+
+    ReadingItem update(uint64_t timestamp) override
+    {
+        if (readings.size() > 0)
+        {
+            auto it = readings.begin();
+            for (auto kt = std::next(readings.rbegin()); kt != readings.rend();
+                 ++kt)
+            {
+                const auto& [nextItemTimestamp, nextItemReading] =
+                    *std::prev(kt);
+                if (timestamp >= nextItemTimestamp &&
+                    static_cast<uint64_t>(timestamp - nextItemTimestamp) >
+                        duration.t.count())
+                {
+                    it = kt.base();
+                    break;
+                }
+            }
+            readings.erase(readings.begin(), it);
+
+            if (timestamp > duration.t.count())
+            {
+                readings.front().first = std::max(
+                    readings.front().first, timestamp - duration.t.count());
+            }
+        }
+
+        return function->calculate(readings, timestamp);
+    }
+
+    ReadingItem update(uint64_t timestamp, double reading) override
+    {
+        readings.emplace_back(timestamp, reading);
+        return update(timestamp);
+    }
+
+  private:
+    std::shared_ptr<details::CollectionFunction> function;
+    std::vector<ReadingItem> readings;
+    CollectionDuration duration;
+};
+
+class Metric::DataStartup : public Metric::CollectionData
+{
+  public:
+    DataStartup(std::shared_ptr<details::CollectionFunction> function) :
+        function(std::move(function))
+    {}
+
+    ReadingItem update(uint64_t timestamp) override
+    {
+        return function->calculateForStartupInterval(readings, timestamp);
+    }
+
+    ReadingItem update(uint64_t timestamp, double reading) override
+    {
+        readings.emplace_back(timestamp, reading);
+        return function->calculateForStartupInterval(readings, timestamp);
+    }
+
+  private:
+    std::shared_ptr<details::CollectionFunction> function;
+    std::vector<ReadingItem> readings;
+};
+
 Metric::Metric(Sensors sensorsIn, OperationType operationTypeIn,
                std::string idIn, std::string metadataIn,
                CollectionTimeScope timeScopeIn,
-               CollectionDuration collectionDurationIn) :
+               CollectionDuration collectionDurationIn,
+               std::unique_ptr<interfaces::Clock> clockIn) :
     id(idIn),
     metadata(metadataIn),
     readings(sensorsIn.size(),
-             MetricValue{std::move(idIn), std::move(metadataIn), 0., 0u}),
+             MetricValue{std::move(idIn), std::move(metadataIn), 0.0, 0u}),
     sensors(std::move(sensorsIn)), operationType(operationTypeIn),
-    timeScope(timeScopeIn), collectionDuration(collectionDurationIn)
+    collectionTimeScope(timeScopeIn), collectionDuration(collectionDurationIn),
+    collectionAlgorithms(makeCollectionData(sensors.size(), operationType,
+                                            collectionTimeScope,
+                                            collectionDuration)),
+    clock(std::move(clockIn))
 {
-    tryUnpackJsonMetadata();
+    attemptUnpackJsonMetadata();
 }
 
+Metric::~Metric() = default;
+
 void Metric::initialize()
 {
     for (const auto& sensor : sensors)
@@ -28,32 +141,40 @@
     }
 }
 
-const std::vector<MetricValue>& Metric::getReadings() const
+std::vector<MetricValue> Metric::getReadings() const
 {
-    return readings;
+    const auto timestamp = clock->timestamp();
+
+    auto resultReadings = readings;
+
+    for (size_t i = 0; i < resultReadings.size(); ++i)
+    {
+        std::tie(resultReadings[i].timestamp, resultReadings[i].value) =
+            collectionAlgorithms[i]->update(timestamp);
+    }
+
+    return resultReadings;
 }
 
 void Metric::sensorUpdated(interfaces::Sensor& notifier, uint64_t timestamp)
 {
-    MetricValue& mv = findMetric(notifier);
-    mv.timestamp = timestamp;
+    findAssociatedData(notifier).update(timestamp);
 }
 
 void Metric::sensorUpdated(interfaces::Sensor& notifier, uint64_t timestamp,
                            double value)
 {
-    MetricValue& mv = findMetric(notifier);
-    mv.timestamp = timestamp;
-    mv.value = value;
+    findAssociatedData(notifier).update(timestamp, value);
 }
 
-MetricValue& Metric::findMetric(interfaces::Sensor& notifier)
+Metric::CollectionData&
+    Metric::findAssociatedData(const interfaces::Sensor& notifier)
 {
     auto it = std::find_if(
         sensors.begin(), sensors.end(),
         [&notifier](const auto& sensor) { return sensor.get() == &notifier; });
     auto index = std::distance(sensors.begin(), it);
-    return readings.at(index);
+    return *collectionAlgorithms.at(index);
 }
 
 LabeledMetricParameters Metric::dumpConfiguration() const
@@ -63,10 +184,50 @@
     });
 
     return LabeledMetricParameters(std::move(sensorPath), operationType, id,
-                                   metadata, timeScope, collectionDuration);
+                                   metadata, collectionTimeScope,
+                                   collectionDuration);
 }
 
-void Metric::tryUnpackJsonMetadata()
+std::vector<std::unique_ptr<Metric::CollectionData>>
+    Metric::makeCollectionData(size_t size, OperationType op,
+                               CollectionTimeScope timeScope,
+                               CollectionDuration duration)
+{
+    using namespace std::string_literals;
+
+    std::vector<std::unique_ptr<Metric::CollectionData>> result;
+
+    result.reserve(size);
+
+    switch (timeScope)
+    {
+        case CollectionTimeScope::interval:
+            std::generate_n(
+                std::back_inserter(result), size,
+                [cf = details::makeCollectionFunction(op), duration] {
+                    return std::make_unique<DataInterval>(cf, duration);
+                });
+            break;
+        case CollectionTimeScope::point:
+            std::generate_n(std::back_inserter(result), size,
+                            [] { return std::make_unique<DataPoint>(); });
+            break;
+        case CollectionTimeScope::startup:
+            std::generate_n(std::back_inserter(result), size,
+                            [cf = details::makeCollectionFunction(op)] {
+                                return std::make_unique<DataStartup>(cf);
+                            });
+            break;
+        default:
+            throw std::runtime_error("timeScope: "s +
+                                     utils::enumToString(timeScope) +
+                                     " is not supported"s);
+    }
+
+    return result;
+}
+
+void Metric::attemptUnpackJsonMetadata()
 {
     using MetricMetadata =
         utils::LabeledTuple<std::tuple<std::vector<std::string>>,
@@ -91,6 +252,6 @@
             }
         }
     }
-    catch (const nlohmann::json::parse_error&)
+    catch (const nlohmann::json::exception&)
     {}
 }
diff --git a/src/metric.hpp b/src/metric.hpp
index 08684da..dfed383 100644
--- a/src/metric.hpp
+++ b/src/metric.hpp
@@ -1,5 +1,6 @@
 #pragma once
 
+#include "interfaces/clock.hpp"
 #include "interfaces/metric.hpp"
 #include "interfaces/sensor.hpp"
 #include "interfaces/sensor_listener.hpp"
@@ -11,24 +12,36 @@
 {
   public:
     Metric(Sensors sensors, OperationType operationType, std::string id,
-           std::string metadata, CollectionTimeScope, CollectionDuration);
+           std::string metadata, CollectionTimeScope, CollectionDuration,
+           std::unique_ptr<interfaces::Clock>);
+    ~Metric();
 
     void initialize() override;
-    const std::vector<MetricValue>& getReadings() const override;
+    std::vector<MetricValue> getReadings() const override;
     void sensorUpdated(interfaces::Sensor&, uint64_t) override;
     void sensorUpdated(interfaces::Sensor&, uint64_t, double value) override;
     LabeledMetricParameters dumpConfiguration() const override;
 
   private:
-    void tryUnpackJsonMetadata();
+    class CollectionData;
+    class DataPoint;
+    class DataInterval;
+    class DataStartup;
 
-    MetricValue& findMetric(interfaces::Sensor&);
+    static std::vector<std::unique_ptr<CollectionData>>
+        makeCollectionData(size_t size, OperationType, CollectionTimeScope,
+                           CollectionDuration);
+
+    void attemptUnpackJsonMetadata();
+    CollectionData& findAssociatedData(const interfaces::Sensor& notifier);
 
     std::string id;
     std::string metadata;
     std::vector<MetricValue> readings;
     Sensors sensors;
     OperationType operationType;
-    CollectionTimeScope timeScope;
+    CollectionTimeScope collectionTimeScope;
     CollectionDuration collectionDuration;
+    std::vector<std::unique_ptr<CollectionData>> collectionAlgorithms;
+    std::unique_ptr<interfaces::Clock> clock;
 };
diff --git a/src/report_factory.cpp b/src/report_factory.cpp
index fb6edf9..091739c 100644
--- a/src/report_factory.cpp
+++ b/src/report_factory.cpp
@@ -3,6 +3,7 @@
 #include "metric.hpp"
 #include "report.hpp"
 #include "sensor.hpp"
+#include "utils/clock.hpp"
 #include "utils/conversion.hpp"
 #include "utils/dbus_mapper.hpp"
 #include "utils/transform.hpp"
@@ -33,7 +34,8 @@
                 param.at_label<ts::OperationType>(), param.at_label<ts::Id>(),
                 param.at_label<ts::MetricMetadata>(),
                 param.at_label<ts::CollectionTimeScope>(),
-                param.at_label<ts::CollectionDuration>());
+                param.at_label<ts::CollectionDuration>(),
+                std::make_unique<Clock>());
         });
 
     return std::make_unique<Report>(
diff --git a/src/types/collection_duration.hpp b/src/types/collection_duration.hpp
index ddeb9cc..2db8278 100644
--- a/src/types/collection_duration.hpp
+++ b/src/types/collection_duration.hpp
@@ -6,6 +6,7 @@
 #include <nlohmann/json.hpp>
 
 #include <chrono>
+#include <cstdint>
 
 BOOST_STRONG_TYPEDEF(Milliseconds, CollectionDuration)
 
diff --git a/src/types/duration_type.hpp b/src/types/duration_type.hpp
new file mode 100644
index 0000000..69d3862
--- /dev/null
+++ b/src/types/duration_type.hpp
@@ -0,0 +1,6 @@
+#pragma once
+
+#include <chrono>
+#include <cstdint>
+
+using Milliseconds = std::chrono::duration<uint64_t, std::milli>;
diff --git a/src/utils/clock.hpp b/src/utils/clock.hpp
new file mode 100644
index 0000000..00fd8d0
--- /dev/null
+++ b/src/utils/clock.hpp
@@ -0,0 +1,22 @@
+#pragma once
+
+#include "interfaces/clock.hpp"
+#include "types/duration_type.hpp"
+
+#include <chrono>
+
+class Clock : public interfaces::Clock
+{
+  public:
+    time_point now() const noexcept override
+    {
+        return std::chrono::steady_clock::now();
+    }
+
+    uint64_t timestamp() const noexcept override
+    {
+        return std::chrono::time_point_cast<Milliseconds>(now())
+            .time_since_epoch()
+            .count();
+    }
+};