summaryrefslogtreecommitdiff
path: root/meta-openbmc-mods/meta-common/recipes-phosphor/interfaces/bmcweb/telemetry/0002-Add-POST-and-DELETE-in-MetricReportDefinitions.patch
diff options
context:
space:
mode:
Diffstat (limited to 'meta-openbmc-mods/meta-common/recipes-phosphor/interfaces/bmcweb/telemetry/0002-Add-POST-and-DELETE-in-MetricReportDefinitions.patch')
-rw-r--r--meta-openbmc-mods/meta-common/recipes-phosphor/interfaces/bmcweb/telemetry/0002-Add-POST-and-DELETE-in-MetricReportDefinitions.patch671
1 files changed, 0 insertions, 671 deletions
diff --git a/meta-openbmc-mods/meta-common/recipes-phosphor/interfaces/bmcweb/telemetry/0002-Add-POST-and-DELETE-in-MetricReportDefinitions.patch b/meta-openbmc-mods/meta-common/recipes-phosphor/interfaces/bmcweb/telemetry/0002-Add-POST-and-DELETE-in-MetricReportDefinitions.patch
deleted file mode 100644
index aaf3f17c7..000000000
--- a/meta-openbmc-mods/meta-common/recipes-phosphor/interfaces/bmcweb/telemetry/0002-Add-POST-and-DELETE-in-MetricReportDefinitions.patch
+++ /dev/null
@@ -1,671 +0,0 @@
-From 03c4ece83b58b954323111a1a7b2bf2b61402d7e Mon Sep 17 00:00:00 2001
-From: "Wludzik, Jozef" <jozef.wludzik@intel.com>
-Date: Mon, 18 May 2020 11:56:57 +0200
-Subject: [PATCH 2/4] Add POST and DELETE in MetricReportDefinitions
-
-Added POST action in MetricReportDefinitions node to allow user
-to add new MetricReportDefinition. Using minimal set of
-MetricReportDefinition parameters from user bmcweb converts it to
-DBus call "AddReport" to Telemetry that serves as a backend
-for Redfish TelemetryService.
-Added DELETE request in MetricReportDefinitions node to allow user
-to remove report from Telemetry.
-Added conversion from string that represents duration format into
-its numeric equivalent.
-Added unit tests for conversion from and to Duration format.
-
-Tested:
- - Tested using witherspoon image on QEMU
- - Verified POST action in different cases:
- - all parameters are provided, new report is added to collection
- - some parameters are missing or invalid, user gets response with
- description of the issue
- - Verified that reports are removed on DELETE request
- - Verified that on invalid DELETE request user receives response
- with error
- - Verified time_utils::fromDurationString()
- - Succesfully passed RedfishServiceValidator.py
-
-Signed-off-by: Wludzik, Jozef <jozef.wludzik@intel.com>
-Signed-off-by: Krzysztof Grobelny <krzysztof.grobelny@intel.com>
-Change-Id: I2fed96848594451e22fde686f8c066d7770cc65a
----
- meson.build | 1 +
- .../include/utils/telemetry_utils.hpp | 2 +
- redfish-core/include/utils/time_utils.hpp | 138 +++++++-
- redfish-core/lib/metric_report_definition.hpp | 328 ++++++++++++++++++
- redfish-core/ut/time_utils_test.cpp | 63 ++++
- 5 files changed, 530 insertions(+), 2 deletions(-)
- create mode 100644 redfish-core/ut/time_utils_test.cpp
-
-diff --git a/meson.build b/meson.build
-index 66a066b..22a8c4a 100644
---- a/meson.build
-+++ b/meson.build
-@@ -345,6 +345,7 @@ srcfiles_unittest = ['include/ut/dbus_utility_test.cpp',
- 'redfish-core/ut/privileges_test.cpp',
- 'redfish-core/ut/lock_test.cpp',
- 'redfish-core/ut/configfile_test.cpp',
-+ 'redfish-core/ut/time_utils_test.cpp',
- 'http/ut/utility_test.cpp']
-
- # Gather the Configuration data
-diff --git a/redfish-core/include/utils/telemetry_utils.hpp b/redfish-core/include/utils/telemetry_utils.hpp
-index a3a8156..0a3af5f 100644
---- a/redfish-core/include/utils/telemetry_utils.hpp
-+++ b/redfish-core/include/utils/telemetry_utils.hpp
-@@ -1,5 +1,7 @@
- #pragma once
-
-+#include "dbus_utility.hpp"
-+
- namespace redfish
- {
-
-diff --git a/redfish-core/include/utils/time_utils.hpp b/redfish-core/include/utils/time_utils.hpp
-index 4a87ba0..9965d4d 100644
---- a/redfish-core/include/utils/time_utils.hpp
-+++ b/redfish-core/include/utils/time_utils.hpp
-@@ -1,7 +1,13 @@
- #pragma once
-
-+#include "logging.hpp"
-+
-+#include <charconv>
- #include <chrono>
-+#include <cmath>
-+#include <optional>
- #include <string>
-+#include <system_error>
-
- namespace redfish
- {
-@@ -12,6 +18,8 @@ namespace time_utils
- namespace details
- {
-
-+using Days = std::chrono::duration<long long, std::ratio<24 * 60 * 60>>;
-+
- inline void leftZeroPadding(std::string& str, const std::size_t padding)
- {
- if (str.size() < padding)
-@@ -19,8 +27,135 @@ inline void leftZeroPadding(std::string& str, const std::size_t padding)
- str.insert(0, padding - str.size(), '0');
- }
- }
-+
-+template <typename FromTime>
-+bool fromDurationItem(std::string_view& fmt, const char postfix,
-+ std::chrono::milliseconds& out)
-+{
-+ const size_t pos = fmt.find(postfix);
-+ if (pos == std::string::npos)
-+ {
-+ return true;
-+ }
-+ if ((pos + 1U) > fmt.size())
-+ {
-+ return false;
-+ }
-+
-+ const char* end;
-+ std::chrono::milliseconds::rep ticks = 0;
-+ if constexpr (std::is_same_v<FromTime, std::chrono::milliseconds>)
-+ {
-+ end = fmt.data() + std::min<size_t>(pos, 3U);
-+ }
-+ else
-+ {
-+ end = fmt.data() + pos;
-+ }
-+
-+ auto [ptr, ec] = std::from_chars(fmt.data(), end, ticks);
-+ if (ptr != end || ec != std::errc())
-+ {
-+ BMCWEB_LOG_ERROR << "Failed to convert string to decimal with err: "
-+ << static_cast<int>(ec) << "("
-+ << std::make_error_code(ec).message() << "), ptr{"
-+ << static_cast<const void*>(ptr) << "} != end{"
-+ << static_cast<const void*>(end) << "})";
-+ return false;
-+ }
-+
-+ if constexpr (std::is_same_v<FromTime, std::chrono::milliseconds>)
-+ {
-+ ticks *= static_cast<std::chrono::milliseconds::rep>(
-+ std::pow(10, 3 - std::min<size_t>(pos, 3U)));
-+ }
-+ if (ticks < 0)
-+ {
-+ return false;
-+ }
-+
-+ out += FromTime(ticks);
-+ const auto maxConversionRange =
-+ std::chrono::duration_cast<FromTime>(std::chrono::milliseconds::max())
-+ .count();
-+ if (out < FromTime(ticks) || maxConversionRange < ticks)
-+ {
-+ return false;
-+ }
-+
-+ fmt.remove_prefix(pos + 1U);
-+ return true;
-+}
- } // namespace details
-
-+/**
-+ * @brief Convert string that represents value in Duration Format to its numeric
-+ * equivalent.
-+ */
-+std::optional<std::chrono::milliseconds>
-+ fromDurationString(const std::string& str)
-+{
-+ std::chrono::milliseconds out = std::chrono::milliseconds::zero();
-+ std::string_view v = str;
-+
-+ if (v.empty())
-+ {
-+ return out;
-+ }
-+ if (v.front() != 'P')
-+ {
-+ BMCWEB_LOG_ERROR << "Invalid duration format: " << str;
-+ return std::nullopt;
-+ }
-+
-+ v.remove_prefix(1);
-+ if (!details::fromDurationItem<details::Days>(v, 'D', out))
-+ {
-+ BMCWEB_LOG_ERROR << "Invalid duration format: " << str;
-+ return std::nullopt;
-+ }
-+
-+ if (v.empty())
-+ {
-+ return out;
-+ }
-+ if (v.front() != 'T')
-+ {
-+ BMCWEB_LOG_ERROR << "Invalid duration format: " << str;
-+ return std::nullopt;
-+ }
-+
-+ v.remove_prefix(1);
-+ if (!details::fromDurationItem<std::chrono::hours>(v, 'H', out) ||
-+ !details::fromDurationItem<std::chrono::minutes>(v, 'M', out))
-+ {
-+ BMCWEB_LOG_ERROR << "Invalid duration format: " << str;
-+ return std::nullopt;
-+ }
-+
-+ if (v.find('.') != std::string::npos && v.find('S') != std::string::npos)
-+ {
-+ if (!details::fromDurationItem<std::chrono::seconds>(v, '.', out) ||
-+ !details::fromDurationItem<std::chrono::milliseconds>(v, 'S', out))
-+ {
-+ BMCWEB_LOG_ERROR << "Invalid duration format: " << str;
-+ return std::nullopt;
-+ }
-+ }
-+ else if (!details::fromDurationItem<std::chrono::seconds>(v, 'S', out))
-+ {
-+ BMCWEB_LOG_ERROR << "Invalid duration format: " << str;
-+ return std::nullopt;
-+ }
-+
-+ if (!v.empty())
-+ {
-+ BMCWEB_LOG_ERROR << "Invalid duration format: " << str;
-+ return std::nullopt;
-+ }
-+ return out;
-+}
-+
- /**
- * @brief Convert time value into duration format that is based on ISO 8601.
- * Example output: "P12DT1M5.5S"
-@@ -36,8 +171,7 @@ inline std::string toDurationString(std::chrono::milliseconds ms)
- std::string fmt;
- fmt.reserve(sizeof("PxxxxxxxxxxxxDTxxHxxMxx.xxxxxxS"));
-
-- using Days = std::chrono::duration<long, std::ratio<24 * 60 * 60>>;
-- Days days = std::chrono::floor<Days>(ms);
-+ details::Days days = std::chrono::floor<details::Days>(ms);
- ms -= days;
-
- std::chrono::hours hours = std::chrono::floor<std::chrono::hours>(ms);
-diff --git a/redfish-core/lib/metric_report_definition.hpp b/redfish-core/lib/metric_report_definition.hpp
-index 59025d9..fcbc99c 100644
---- a/redfish-core/lib/metric_report_definition.hpp
-+++ b/redfish-core/lib/metric_report_definition.hpp
-@@ -1,9 +1,12 @@
- #pragma once
-
- #include "node.hpp"
-+#include "sensors.hpp"
- #include "utils/telemetry_utils.hpp"
- #include "utils/time_utils.hpp"
-
-+#include <boost/container/flat_map.hpp>
-+
- #include <tuple>
- #include <variant>
-
-@@ -95,6 +98,252 @@ inline void fillReportDefinition(
- asyncResp->res.jsonValue["Schedule"]["RecurrenceInterval"] =
- time_utils::toDurationString(std::chrono::milliseconds(*interval));
- }
-+
-+struct AddReportArgs
-+{
-+ std::string name;
-+ std::string reportingType;
-+ bool emitsReadingsUpdate = false;
-+ bool logToMetricReportsCollection = false;
-+ uint64_t interval = 0;
-+ std::vector<std::pair<std::string, std::vector<std::string>>> metrics;
-+};
-+
-+inline bool toDbusReportActions(crow::Response& res,
-+ std::vector<std::string>& actions,
-+ AddReportArgs& args)
-+{
-+ size_t index = 0;
-+ for (auto& action : actions)
-+ {
-+ if (action == "RedfishEvent")
-+ {
-+ args.emitsReadingsUpdate = true;
-+ }
-+ else if (action == "LogToMetricReportsCollection")
-+ {
-+ args.logToMetricReportsCollection = true;
-+ }
-+ else
-+ {
-+ messages::propertyValueNotInList(
-+ res, action, "ReportActions/" + std::to_string(index));
-+ return false;
-+ }
-+ index++;
-+ }
-+ return true;
-+}
-+
-+inline bool getUserParameters(crow::Response& res, const crow::Request& req,
-+ AddReportArgs& args)
-+{
-+ std::vector<nlohmann::json> metrics;
-+ std::vector<std::string> reportActions;
-+ std::optional<nlohmann::json> schedule;
-+ if (!json_util::readJson(req, res, "Id", args.name, "Metrics", metrics,
-+ "MetricReportDefinitionType", args.reportingType,
-+ "ReportActions", reportActions, "Schedule",
-+ schedule))
-+ {
-+ return false;
-+ }
-+
-+ constexpr const char* allowedCharactersInName =
-+ "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
-+ if (args.name.empty() || args.name.find_first_not_of(
-+ allowedCharactersInName) != std::string::npos)
-+ {
-+ BMCWEB_LOG_ERROR << "Failed to match " << args.name
-+ << " with allowed character "
-+ << allowedCharactersInName;
-+ messages::propertyValueIncorrect(res, "Id", args.name);
-+ return false;
-+ }
-+
-+ if (args.reportingType != "Periodic" && args.reportingType != "OnRequest")
-+ {
-+ messages::propertyValueNotInList(res, args.reportingType,
-+ "MetricReportDefinitionType");
-+ return false;
-+ }
-+
-+ if (!toDbusReportActions(res, reportActions, args))
-+ {
-+ return false;
-+ }
-+
-+ if (args.reportingType == "Periodic")
-+ {
-+ if (!schedule)
-+ {
-+ messages::createFailedMissingReqProperties(res, "Schedule");
-+ return false;
-+ }
-+
-+ std::string durationStr;
-+ if (!json_util::readJson(*schedule, res, "RecurrenceInterval",
-+ durationStr))
-+ {
-+ return false;
-+ }
-+
-+ std::optional<std::chrono::milliseconds> durationNum =
-+ time_utils::fromDurationString(durationStr);
-+ if (!durationNum)
-+ {
-+ messages::propertyValueIncorrect(res, "RecurrenceInterval",
-+ durationStr);
-+ return false;
-+ }
-+ args.interval = static_cast<uint64_t>(durationNum->count());
-+ }
-+
-+ args.metrics.reserve(metrics.size());
-+ for (auto& m : metrics)
-+ {
-+ std::string id;
-+ std::vector<std::string> uris;
-+ if (!json_util::readJson(m, res, "MetricId", id, "MetricProperties",
-+ uris))
-+ {
-+ return false;
-+ }
-+
-+ args.metrics.emplace_back(std::move(id), std::move(uris));
-+ }
-+
-+ return true;
-+}
-+
-+inline bool getChassisSensorNode(
-+ const std::shared_ptr<AsyncResp>& asyncResp,
-+ const std::vector<std::pair<std::string, std::vector<std::string>>>&
-+ metrics,
-+ boost::container::flat_set<std::pair<std::string, std::string>>& matched)
-+{
-+ for (const auto& [id, uris] : metrics)
-+ {
-+ for (size_t i = 0; i < uris.size(); i++)
-+ {
-+ const std::string& uri = uris[i];
-+ std::string chassis;
-+ std::string node;
-+
-+ if (!boost::starts_with(uri, "/redfish/v1/Chassis/") ||
-+ !dbus::utility::getNthStringFromPath(uri, 3, chassis) ||
-+ !dbus::utility::getNthStringFromPath(uri, 4, node))
-+ {
-+ BMCWEB_LOG_ERROR << "Failed to get chassis and sensor Node "
-+ "from "
-+ << uri;
-+ messages::propertyValueIncorrect(asyncResp->res, uri,
-+ "MetricProperties/" +
-+ std::to_string(i));
-+ return false;
-+ }
-+
-+ if (boost::ends_with(node, "#"))
-+ {
-+ node.pop_back();
-+ }
-+
-+ matched.emplace(std::move(chassis), std::move(node));
-+ }
-+ }
-+ return true;
-+}
-+
-+class AddReport
-+{
-+ public:
-+ AddReport(AddReportArgs argsIn, std::shared_ptr<AsyncResp> asyncResp) :
-+ asyncResp{std::move(asyncResp)}, args{std::move(argsIn)}
-+ {}
-+ ~AddReport()
-+ {
-+ if (asyncResp->res.result() != boost::beast::http::status::ok)
-+ {
-+ return;
-+ }
-+
-+ telemetry::ReadingParameters readingParams;
-+ readingParams.reserve(args.metrics.size());
-+
-+ for (const auto& [id, uris] : args.metrics)
-+ {
-+ for (size_t i = 0; i < uris.size(); i++)
-+ {
-+ const std::string& uri = uris[i];
-+ auto el = uriToDbus.find(uri);
-+ if (el == uriToDbus.end())
-+ {
-+ BMCWEB_LOG_ERROR << "Failed to find DBus sensor "
-+ "corresponding to URI "
-+ << uri;
-+ messages::propertyValueNotInList(asyncResp->res, uri,
-+ "MetricProperties/" +
-+ std::to_string(i));
-+ return;
-+ }
-+
-+ const std::string& dbusPath = el->second;
-+ readingParams.emplace_back(dbusPath, "SINGLE", id, uri);
-+ }
-+ }
-+
-+ crow::connections::systemBus->async_method_call(
-+ [asyncResp = std::move(asyncResp), name = args.name,
-+ uriToDbus = std::move(uriToDbus)](
-+ const boost::system::error_code ec, const std::string&) {
-+ if (ec == boost::system::errc::file_exists)
-+ {
-+ messages::resourceAlreadyExists(
-+ asyncResp->res, "MetricReportDefinition", "Id", name);
-+ return;
-+ }
-+ if (ec == boost::system::errc::too_many_files_open)
-+ {
-+ messages::createLimitReachedForResource(asyncResp->res);
-+ return;
-+ }
-+ if (ec == boost::system::errc::argument_list_too_long)
-+ {
-+ nlohmann::json metricProperties = nlohmann::json::array();
-+ for (const auto& [uri, _] : uriToDbus)
-+ {
-+ metricProperties.emplace_back(uri);
-+ }
-+ messages::propertyValueIncorrect(
-+ asyncResp->res, metricProperties, "MetricProperties");
-+ return;
-+ }
-+ if (ec)
-+ {
-+ messages::internalError(asyncResp->res);
-+ BMCWEB_LOG_ERROR << "respHandler DBus error " << ec;
-+ return;
-+ }
-+
-+ messages::created(asyncResp->res);
-+ },
-+ telemetry::service, "/xyz/openbmc_project/Telemetry/Reports",
-+ "xyz.openbmc_project.Telemetry.ReportManager", "AddReport",
-+ "TelemetryService/" + args.name, args.reportingType,
-+ args.emitsReadingsUpdate, args.logToMetricReportsCollection,
-+ args.interval, readingParams);
-+ }
-+
-+ void insert(const boost::container::flat_map<std::string, std::string>& el)
-+ {
-+ uriToDbus.insert(el.begin(), el.end());
-+ }
-+
-+ private:
-+ std::shared_ptr<AsyncResp> asyncResp;
-+ AddReportArgs args;
-+ boost::container::flat_map<std::string, std::string> uriToDbus{};
-+};
- } // namespace telemetry
-
- class MetricReportDefinitionCollection : public Node
-@@ -126,6 +375,46 @@ class MetricReportDefinitionCollection : public Node
- telemetry::getReportCollection(asyncResp,
- telemetry::metricReportDefinitionUri);
- }
-+
-+ void doPost(crow::Response& res, const crow::Request& req,
-+ const std::vector<std::string>&) override
-+ {
-+ auto asyncResp = std::make_shared<AsyncResp>(res);
-+ telemetry::AddReportArgs args;
-+ if (!telemetry::getUserParameters(res, req, args))
-+ {
-+ return;
-+ }
-+
-+ boost::container::flat_set<std::pair<std::string, std::string>>
-+ chassisSensors;
-+ if (!telemetry::getChassisSensorNode(asyncResp, args.metrics,
-+ chassisSensors))
-+ {
-+ return;
-+ }
-+
-+ auto addReportReq =
-+ std::make_shared<telemetry::AddReport>(std::move(args), asyncResp);
-+ for (const auto& [chassis, sensorType] : chassisSensors)
-+ {
-+ retrieveUriToDbusMap(
-+ chassis, sensorType,
-+ [asyncResp, addReportReq](
-+ const boost::beast::http::status status,
-+ const boost::container::flat_map<std::string, std::string>&
-+ uriToDbus) {
-+ if (status != boost::beast::http::status::ok)
-+ {
-+ BMCWEB_LOG_ERROR << "Failed to retrieve URI to dbus "
-+ "sensors map with err "
-+ << static_cast<unsigned>(status);
-+ return;
-+ }
-+ addReportReq->insert(uriToDbus);
-+ });
-+ }
-+ }
- };
-
- class MetricReportDefinition : public Node
-@@ -184,5 +473,44 @@ class MetricReportDefinition : public Node
- "org.freedesktop.DBus.Properties", "GetAll",
- telemetry::reportInterface);
- }
-+
-+ void doDelete(crow::Response& res, const crow::Request&,
-+ const std::vector<std::string>& params) override
-+ {
-+ auto asyncResp = std::make_shared<AsyncResp>(res);
-+ if (params.size() != 1)
-+ {
-+ messages::internalError(asyncResp->res);
-+ return;
-+ }
-+
-+ const std::string& id = params[0];
-+ const std::string reportPath = telemetry::getDbusReportPath(id);
-+
-+ crow::connections::systemBus->async_method_call(
-+ [asyncResp, id](const boost::system::error_code ec) {
-+ /*
-+ * boost::system::errc and std::errc are missing value for
-+ * EBADR error that is defined in Linux.
-+ */
-+ if (ec.value() == EBADR)
-+ {
-+ messages::resourceNotFound(asyncResp->res,
-+ "MetricReportDefinition", id);
-+ return;
-+ }
-+
-+ if (ec)
-+ {
-+ BMCWEB_LOG_ERROR << "respHandler DBus error " << ec;
-+ messages::internalError(asyncResp->res);
-+ return;
-+ }
-+
-+ asyncResp->res.result(boost::beast::http::status::no_content);
-+ },
-+ telemetry::service, reportPath, "xyz.openbmc_project.Object.Delete",
-+ "Delete");
-+ }
- };
- } // namespace redfish
-diff --git a/redfish-core/ut/time_utils_test.cpp b/redfish-core/ut/time_utils_test.cpp
-new file mode 100644
-index 0000000..70999ce
---- /dev/null
-+++ b/redfish-core/ut/time_utils_test.cpp
-@@ -0,0 +1,63 @@
-+#include "utils/time_utils.hpp"
-+
-+#include <gmock/gmock.h>
-+
-+using namespace testing;
-+
-+class FromDurationTest :
-+ public Test,
-+ public WithParamInterface<
-+ std::pair<std::string, std::optional<std::chrono::milliseconds>>>
-+{};
-+
-+INSTANTIATE_TEST_SUITE_P(
-+ _, FromDurationTest,
-+ Values(std::make_pair("PT12S", std::chrono::milliseconds(12000)),
-+ std::make_pair("PT0.204S", std::chrono::milliseconds(204)),
-+ std::make_pair("PT0.2S", std::chrono::milliseconds(200)),
-+ std::make_pair("PT50M", std::chrono::milliseconds(3000000)),
-+ std::make_pair("PT23H", std::chrono::milliseconds(82800000)),
-+ std::make_pair("P51D", std::chrono::milliseconds(4406400000)),
-+ std::make_pair("PT2H40M10.1S", std::chrono::milliseconds(9610100)),
-+ std::make_pair("P20DT2H40M10.1S",
-+ std::chrono::milliseconds(1737610100)),
-+ std::make_pair("", std::chrono::milliseconds(0)),
-+ std::make_pair("PTS", std::nullopt),
-+ std::make_pair("P1T", std::nullopt),
-+ std::make_pair("PT100M1000S100", std::nullopt),
-+ std::make_pair("PDTHMS", std::nullopt),
-+ std::make_pair("P99999999999999999DT", std::nullopt),
-+ std::make_pair("PD222T222H222M222.222S", std::nullopt),
-+ std::make_pair("PT99999H9999999999999999999999M99999999999S",
-+ std::nullopt),
-+ std::make_pair("PT-9H", std::nullopt)));
-+
-+TEST_P(FromDurationTest, convertToMilliseconds)
-+{
-+ const auto& [str, expected] = GetParam();
-+ EXPECT_THAT(redfish::time_utils::fromDurationString(str), Eq(expected));
-+}
-+
-+class ToDurationTest :
-+ public Test,
-+ public WithParamInterface<std::pair<std::chrono::milliseconds, std::string>>
-+{};
-+
-+INSTANTIATE_TEST_SUITE_P(
-+ _, ToDurationTest,
-+ Values(std::make_pair(std::chrono::milliseconds(12000), "PT12.000S"),
-+ std::make_pair(std::chrono::milliseconds(204), "PT0.204S"),
-+ std::make_pair(std::chrono::milliseconds(200), "PT0.200S"),
-+ std::make_pair(std::chrono::milliseconds(3000000), "PT50M"),
-+ std::make_pair(std::chrono::milliseconds(82800000), "PT23H"),
-+ std::make_pair(std::chrono::milliseconds(4406400000), "P51DT"),
-+ std::make_pair(std::chrono::milliseconds(9610100), "PT2H40M10.100S"),
-+ std::make_pair(std::chrono::milliseconds(1737610100),
-+ "P20DT2H40M10.100S"),
-+ std::make_pair(std::chrono::milliseconds(-250), "")));
-+
-+TEST_P(ToDurationTest, convertToDuration)
-+{
-+ const auto& [ms, expected] = GetParam();
-+ EXPECT_THAT(redfish::time_utils::toDurationString(ms), Eq(expected));
-+}
---
-2.17.1
-