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/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]));
         }