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/meson.build b/meson.build
index 622043c..625f265 100644
--- a/meson.build
+++ b/meson.build
@@ -76,6 +76,7 @@
 executable(
     'telemetry',
     [
+        'src/details/collection_function.cpp',
         'src/discrete_threshold.cpp',
         'src/main.cpp',
         'src/metric.cpp',
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();
+    }
+};
diff --git a/tests/meson.build b/tests/meson.build
index a3a78ec..3059953 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -11,6 +11,7 @@
     executable(
         'telemetry-ut',
         [
+            '../src/details/collection_function.cpp',
             '../src/discrete_threshold.cpp',
             '../src/metric.cpp',
             '../src/numeric_threshold.cpp',
diff --git a/tests/src/dbus_environment.cpp b/tests/src/dbus_environment.cpp
index 9e025d7..07e0aff 100644
--- a/tests/src/dbus_environment.cpp
+++ b/tests/src/dbus_environment.cpp
@@ -66,14 +66,13 @@
     return [p = std::move(promise)]() { p->set_value(true); };
 }
 
-bool DbusEnvironment::waitForFuture(std::string_view name,
-                                    std::chrono::milliseconds timeout)
+bool DbusEnvironment::waitForFuture(std::string_view name, Milliseconds timeout)
 {
     return waitForFuture(getFuture(name), timeout);
 }
 
 bool DbusEnvironment::waitForFutures(std::string_view name,
-                                     std::chrono::milliseconds timeout)
+                                     Milliseconds timeout)
 {
     auto& data = futures[std::string(name)];
     auto ret = waitForFutures(
@@ -98,7 +97,7 @@
     return {};
 }
 
-void DbusEnvironment::sleepFor(std::chrono::milliseconds timeout)
+void DbusEnvironment::sleepFor(Milliseconds timeout)
 {
     auto end = std::chrono::high_resolution_clock::now() + timeout;
 
@@ -111,14 +110,13 @@
     synchronizeIoc();
 }
 
-std::chrono::milliseconds
-    DbusEnvironment::measureTime(std::function<void()> fun)
+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);
+    return std::chrono::duration_cast<Milliseconds>(end - begin);
 }
 
 boost::asio::io_context DbusEnvironment::ioc;
diff --git a/tests/src/dbus_environment.hpp b/tests/src/dbus_environment.hpp
index 8146bcb..0ddf241 100644
--- a/tests/src/dbus_environment.hpp
+++ b/tests/src/dbus_environment.hpp
@@ -1,5 +1,7 @@
 #pragma once
 
+#include "types/duration_type.hpp"
+
 #include <sdbusplus/asio/object_server.hpp>
 #include <sdbusplus/asio/property.hpp>
 
@@ -22,8 +24,8 @@
     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 sleepFor(Milliseconds);
+    static Milliseconds measureTime(std::function<void()>);
 
     static void synchronizeIoc()
     {
@@ -39,12 +41,12 @@
     }
 
     template <class T, class F>
-    static T waitForFutures(
-        std::vector<std::future<T>> futures, T init, F&& accumulator,
-        std::chrono::milliseconds timeout = std::chrono::seconds(10))
+    static T waitForFutures(std::vector<std::future<T>> futures, T init,
+                            F&& accumulator,
+                            Milliseconds timeout = std::chrono::seconds(10))
     {
-        constexpr auto precission = std::chrono::milliseconds(10);
-        auto elapsed = std::chrono::milliseconds(0);
+        constexpr auto precission = Milliseconds(10);
+        auto elapsed = Milliseconds(0);
 
         auto sum = init;
         for (auto& future : futures)
@@ -73,9 +75,8 @@
     }
 
     template <class T>
-    static T waitForFuture(
-        std::future<T> future,
-        std::chrono::milliseconds timeout = std::chrono::seconds(10))
+    static T waitForFuture(std::future<T> future,
+                           Milliseconds timeout = std::chrono::seconds(10))
     {
         std::vector<std::future<T>> futures;
         futures.emplace_back(std::move(future));
@@ -85,13 +86,11 @@
             [](auto, const auto& value) { return value; }, timeout);
     }
 
-    static bool waitForFuture(
-        std::string_view name,
-        std::chrono::milliseconds timeout = std::chrono::seconds(10));
+    static bool waitForFuture(std::string_view name,
+                              Milliseconds timeout = std::chrono::seconds(10));
 
-    static bool waitForFutures(
-        std::string_view name,
-        std::chrono::milliseconds timeout = std::chrono::seconds(10));
+    static bool waitForFutures(std::string_view name,
+                               Milliseconds timeout = std::chrono::seconds(10));
 
   private:
     static std::future<bool> getFuture(std::string_view name);
diff --git a/tests/src/fakes/clock_fake.hpp b/tests/src/fakes/clock_fake.hpp
new file mode 100644
index 0000000..28c2940
--- /dev/null
+++ b/tests/src/fakes/clock_fake.hpp
@@ -0,0 +1,44 @@
+#pragma once
+
+#include "interfaces/clock.hpp"
+#include "types/duration_type.hpp"
+
+class ClockFake : public interfaces::Clock
+{
+  public:
+    time_point now() const noexcept override
+    {
+        return timePoint;
+    }
+
+    uint64_t timestamp() const noexcept override
+    {
+        return toTimestamp(now());
+    }
+
+    uint64_t advance(Milliseconds delta) noexcept
+    {
+        timePoint += delta;
+        return timestamp();
+    }
+
+    void set(Milliseconds timeSinceEpoch) noexcept
+    {
+        timePoint = time_point{timeSinceEpoch};
+    }
+
+    static uint64_t toTimestamp(Milliseconds time)
+    {
+        return time.count();
+    }
+
+    static uint64_t toTimestamp(time_point tp)
+    {
+        return std::chrono::time_point_cast<Milliseconds>(tp)
+            .time_since_epoch()
+            .count();
+    }
+
+  private:
+    time_point timePoint = std::chrono::steady_clock::now();
+};
diff --git a/tests/src/mocks/metric_mock.hpp b/tests/src/mocks/metric_mock.hpp
index 98bad87..f66f38d 100644
--- a/tests/src/mocks/metric_mock.hpp
+++ b/tests/src/mocks/metric_mock.hpp
@@ -12,12 +12,11 @@
         using namespace testing;
 
         ON_CALL(*this, getReadings())
-            .WillByDefault(ReturnRefOfCopy(std::vector<MetricValue>()));
+            .WillByDefault(Return(std::vector<MetricValue>()));
     }
 
     MOCK_METHOD(void, initialize, (), (override));
-    MOCK_METHOD(const std::vector<MetricValue>&, getReadings, (),
-                (const, override));
+    MOCK_METHOD(std::vector<MetricValue>, getReadings, (), (const, override));
     MOCK_METHOD(LabeledMetricParameters, dumpConfiguration, (),
                 (const, override));
 };
diff --git a/tests/src/params/metric_params.hpp b/tests/src/params/metric_params.hpp
index f099472..4654060 100644
--- a/tests/src/params/metric_params.hpp
+++ b/tests/src/params/metric_params.hpp
@@ -4,7 +4,11 @@
 #include "types/collection_time_scope.hpp"
 #include "types/operation_type.hpp"
 
+#include <chrono>
+#include <cstdint>
+#include <ostream>
 #include <string>
+#include <vector>
 
 class MetricParams final
 {
@@ -64,6 +68,28 @@
         return collectionDurationProperty;
     }
 
+    MetricParams& readings(std::vector<std::pair<Milliseconds, double>> value)
+    {
+        readingsProperty = std::move(value);
+        return *this;
+    }
+
+    const std::vector<std::pair<Milliseconds, double>>& readings() const
+    {
+        return readingsProperty;
+    }
+
+    MetricParams& expectedReading(Milliseconds delta, double reading)
+    {
+        expectedReadingProperty = std::make_pair(delta, reading);
+        return *this;
+    }
+
+    const std::pair<Milliseconds, double>& expectedReading() const
+    {
+        return expectedReadingProperty;
+    }
+
   private:
     OperationType operationTypeProperty = {};
     std::string idProperty = "MetricId";
@@ -71,4 +97,24 @@
     CollectionTimeScope collectionTimeScopeProperty = {};
     CollectionDuration collectionDurationProperty =
         CollectionDuration(Milliseconds(0u));
+    std::vector<std::pair<Milliseconds, double>> readingsProperty = {};
+    std::pair<Milliseconds, double> expectedReadingProperty = {};
 };
+
+inline std::ostream& operator<<(std::ostream& os, const MetricParams& mp)
+{
+    using utils::enumToString;
+
+    os << "{ op: " << enumToString(mp.operationType())
+       << ", timeScope: " << enumToString(mp.collectionTimeScope())
+       << ", duration: " << mp.collectionDuration().t.count()
+       << ", readings: { ";
+    for (auto [timestamp, reading] : mp.readings())
+    {
+        os << reading << "(" << timestamp.count() << "ms), ";
+    }
+
+    auto [timestamp, reading] = mp.expectedReading();
+    os << " }, expected: " << reading << "(" << timestamp.count() << "ms) }";
+    return os;
+}
diff --git a/tests/src/params/trigger_params.hpp b/tests/src/params/trigger_params.hpp
index ac8656a..753cbb1 100644
--- a/tests/src/params/trigger_params.hpp
+++ b/tests/src/params/trigger_params.hpp
@@ -84,9 +84,7 @@
     std::vector<LabeledSensorInfo> labeledSensorsProperty = {
         {"service1", "/xyz/openbmc_project/sensors/temperature/BMC_Temp",
          "metadata1"}};
-
     std::vector<std::string> reportNamesProperty = {"Report1"};
-
     LabeledTriggerThresholdParams labeledThresholdsProperty =
         std::vector<numeric::LabeledThresholdParam>{
             numeric::LabeledThresholdParam{numeric::Type::lowerCritical,
diff --git a/tests/src/test_metric.cpp b/tests/src/test_metric.cpp
index 6c68513..87e32f9 100644
--- a/tests/src/test_metric.cpp
+++ b/tests/src/test_metric.cpp
@@ -1,3 +1,4 @@
+#include "fakes/clock_fake.hpp"
 #include "helpers.hpp"
 #include "metric.hpp"
 #include "mocks/sensor_mock.hpp"
@@ -35,17 +36,18 @@
             utils::convContainer<std::shared_ptr<interfaces::Sensor>>(
                 sensorMocks),
             p.operationType(), p.id(), p.metadata(), p.collectionTimeScope(),
-            p.collectionDuration());
+            p.collectionDuration(), std::move(clockFakePtr));
     }
 
-    MetricParams params =
-        MetricParams()
-            .id("id")
-            .metadata("metadata")
-            .operationType(OperationType::avg)
-            .collectionTimeScope(CollectionTimeScope::interval)
-            .collectionDuration(CollectionDuration(42ms));
+    MetricParams params = MetricParams()
+                              .id("id")
+                              .metadata("metadata")
+                              .operationType(OperationType::avg)
+                              .collectionTimeScope(CollectionTimeScope::point)
+                              .collectionDuration(CollectionDuration(0ms));
     std::vector<std::shared_ptr<SensorMock>> sensorMocks = makeSensorMocks(1u);
+    std::unique_ptr<ClockFake> clockFakePtr = std::make_unique<ClockFake>();
+    ClockFake& clockFake = *clockFakePtr;
     std::shared_ptr<Metric> sut;
 };
 
@@ -157,14 +159,196 @@
     const auto conf = sut->dumpConfiguration();
 
     LabeledMetricParameters expected = {};
-    expected.at_label<ts::Id>() = "id";
-    expected.at_label<ts::MetricMetadata>() = "metadata";
-    expected.at_label<ts::OperationType>() = OperationType::avg;
-    expected.at_label<ts::CollectionTimeScope>() =
-        CollectionTimeScope::interval;
-    expected.at_label<ts::CollectionDuration>() = CollectionDuration(42ms);
+    expected.at_label<ts::Id>() = params.id();
+    expected.at_label<ts::MetricMetadata>() = params.metadata();
+    expected.at_label<ts::OperationType>() = params.operationType();
+    expected.at_label<ts::CollectionTimeScope>() = params.collectionTimeScope();
+    expected.at_label<ts::CollectionDuration>() = params.collectionDuration();
     expected.at_label<ts::SensorPath>() = {
         LabeledSensorParameters("service1", "path1")};
 
     EXPECT_THAT(conf, Eq(expected));
 }
+
+class TestMetricCalculationFunctions :
+    public TestMetric,
+    public WithParamInterface<MetricParams>
+{
+  public:
+    void SetUp() override
+    {
+        clockFakePtr->set(0ms);
+
+        sut = makeSut(params.operationType(GetParam().operationType())
+                          .collectionTimeScope(GetParam().collectionTimeScope())
+                          .collectionDuration(GetParam().collectionDuration()));
+    }
+
+    static std::vector<std::pair<Milliseconds, double>> defaultReadings()
+    {
+        std::vector<std::pair<Milliseconds, double>> ret;
+        ret.emplace_back(0ms, std::numeric_limits<double>::quiet_NaN());
+        ret.emplace_back(10ms, 14.);
+        ret.emplace_back(1ms, 3.);
+        ret.emplace_back(5ms, 7.);
+        return ret;
+    }
+};
+
+MetricParams defaultSingleParams()
+{
+    return MetricParams()
+        .operationType(OperationType::single)
+        .readings(TestMetricCalculationFunctions::defaultReadings())
+        .expectedReading(11ms, 7.0);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    OperationSingleReturnsLastReading, TestMetricCalculationFunctions,
+    Values(
+        defaultSingleParams().collectionTimeScope(CollectionTimeScope::point),
+        defaultSingleParams()
+            .collectionTimeScope(CollectionTimeScope::interval)
+            .collectionDuration(CollectionDuration(100ms)),
+        defaultSingleParams().collectionTimeScope(
+            CollectionTimeScope::startup)));
+
+MetricParams defaultPointParams()
+{
+    return defaultSingleParams().collectionTimeScope(
+        CollectionTimeScope::point);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    TimeScopePointReturnsLastReading, TestMetricCalculationFunctions,
+    Values(defaultPointParams().operationType(OperationType::single),
+           defaultPointParams().operationType(OperationType::min),
+           defaultPointParams().operationType(OperationType::max),
+           defaultPointParams().operationType(OperationType::sum),
+           defaultPointParams().operationType(OperationType::avg)));
+
+MetricParams defaultMinParams()
+{
+    return defaultSingleParams().operationType(OperationType::min);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    ReturnsMinForGivenTimeScope, TestMetricCalculationFunctions,
+    Values(defaultMinParams()
+               .collectionTimeScope(CollectionTimeScope::interval)
+               .collectionDuration(CollectionDuration(100ms))
+               .expectedReading(10ms, 3.0),
+           defaultMinParams()
+               .collectionTimeScope(CollectionTimeScope::interval)
+               .collectionDuration(CollectionDuration(3ms))
+               .expectedReading(13ms, 7.0),
+           defaultMinParams()
+               .collectionTimeScope(CollectionTimeScope::startup)
+               .expectedReading(10ms, 3.0)));
+
+MetricParams defaultMaxParams()
+{
+    return defaultSingleParams().operationType(OperationType::max);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    ReturnsMaxForGivenTimeScope, TestMetricCalculationFunctions,
+    Values(defaultMaxParams()
+               .collectionTimeScope(CollectionTimeScope::interval)
+               .collectionDuration(CollectionDuration(100ms))
+               .expectedReading(0ms, 14.0),
+           defaultMaxParams()
+               .collectionTimeScope(CollectionTimeScope::interval)
+               .collectionDuration(CollectionDuration(6ms))
+               .expectedReading(10ms, 14.0),
+           defaultMaxParams()
+               .collectionTimeScope(CollectionTimeScope::interval)
+               .collectionDuration(CollectionDuration(5ms))
+               .expectedReading(11ms, 7.0),
+           defaultMaxParams()
+               .collectionTimeScope(CollectionTimeScope::startup)
+               .expectedReading(0ms, 14.0)));
+
+MetricParams defaultSumParams()
+{
+    return defaultSingleParams().operationType(OperationType::sum);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    ReturnsSumForGivenTimeScope, TestMetricCalculationFunctions,
+    Values(defaultSumParams()
+               .collectionTimeScope(CollectionTimeScope::interval)
+               .collectionDuration(CollectionDuration(100ms))
+               .expectedReading(16ms, 14. * 10 + 3. * 1 + 7 * 5),
+           defaultSumParams()
+               .collectionTimeScope(CollectionTimeScope::interval)
+               .collectionDuration(CollectionDuration(8ms))
+               .expectedReading(16ms, 14. * 2 + 3. * 1 + 7 * 5),
+           defaultSumParams()
+               .collectionTimeScope(CollectionTimeScope::interval)
+               .collectionDuration(CollectionDuration(6ms))
+               .expectedReading(16ms, 3. * 1 + 7 * 5),
+           defaultSumParams()
+               .collectionTimeScope(CollectionTimeScope::startup)
+               .expectedReading(16ms, 14. * 10 + 3. * 1 + 7 * 5)));
+
+MetricParams defaultAvgParams()
+{
+    return defaultSingleParams().operationType(OperationType::avg);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    ReturnsAvgForGivenTimeScope, TestMetricCalculationFunctions,
+    Values(defaultAvgParams()
+               .collectionTimeScope(CollectionTimeScope::interval)
+               .collectionDuration(CollectionDuration(100ms))
+               .expectedReading(16ms, (14. * 10 + 3. * 1 + 7 * 5) / 16.),
+           defaultAvgParams()
+               .collectionTimeScope(CollectionTimeScope::interval)
+               .collectionDuration(CollectionDuration(8ms))
+               .expectedReading(16ms, (14. * 2 + 3. * 1 + 7 * 5) / 8.),
+           defaultAvgParams()
+               .collectionTimeScope(CollectionTimeScope::interval)
+               .collectionDuration(CollectionDuration(6ms))
+               .expectedReading(16ms, (3. * 1 + 7 * 5) / 6.),
+           defaultAvgParams()
+               .collectionTimeScope(CollectionTimeScope::startup)
+               .expectedReading(16ms, (14. * 10 + 3. * 1 + 7 * 5) / 16.)));
+
+TEST_P(TestMetricCalculationFunctions, calculatesReadingValue)
+{
+    for (auto [timestamp, reading] : GetParam().readings())
+    {
+        sut->sensorUpdated(*sensorMocks.front(), clockFake.timestamp(),
+                           reading);
+        clockFake.advance(timestamp);
+    }
+
+    const auto [expectedTimestamp, expectedReading] =
+        GetParam().expectedReading();
+    const auto readings = sut->getReadings();
+
+    EXPECT_THAT(readings, ElementsAre(MetricValue{
+                              "id", "metadata", expectedReading,
+                              ClockFake::toTimestamp(expectedTimestamp)}));
+}
+
+TEST_P(TestMetricCalculationFunctions,
+       calculatedReadingValueWithIntermediateCalculations)
+{
+    for (auto [timestamp, reading] : GetParam().readings())
+    {
+        sut->sensorUpdated(*sensorMocks.front(), clockFake.timestamp(),
+                           reading);
+        clockFake.advance(timestamp);
+        sut->getReadings();
+    }
+
+    const auto [expectedTimestamp, expectedReading] =
+        GetParam().expectedReading();
+    const auto readings = sut->getReadings();
+
+    EXPECT_THAT(readings, ElementsAre(MetricValue{
+                              "id", "metadata", expectedReading,
+                              ClockFake::toTimestamp(expectedTimestamp)}));
+}
diff --git a/tests/src/test_report.cpp b/tests/src/test_report.cpp
index 96f74f0..441ebd9 100644
--- a/tests/src/test_report.cpp
+++ b/tests/src/test_report.cpp
@@ -46,7 +46,7 @@
         for (size_t i = 0; i < metricParameters.size(); ++i)
         {
             ON_CALL(*metricMocks[i], getReadings())
-                .WillByDefault(ReturnRefOfCopy(std::vector({readings[i]})));
+                .WillByDefault(Return(std::vector({readings[i]})));
             ON_CALL(*metricMocks[i], dumpConfiguration())
                 .WillByDefault(Return(metricParameters[i]));
         }