HardwareState: HWMon data reader

Ported HWMon driver from
cla-sysrepo@19163e50ab062a8b5b75f754b30c22fdbd62b03a.

In cla-sysrepo, these drivers provided a property-based API, i.e., you
asked for a specific property (i.e., a filename) and the contents of the
file was returned.

In velia, we do not need this one-by-one property access. We provide all
attributes with a single call.

Change-Id: Ife783f21d4552e3b8bc69436d2f8fa53eb6745b7
diff --git a/tests/fs-helpers/FileInjector.cpp b/tests/fs-helpers/FileInjector.cpp
new file mode 100644
index 0000000..a3a80f2
--- /dev/null
+++ b/tests/fs-helpers/FileInjector.cpp
@@ -0,0 +1,26 @@
+#include <fstream>
+#include "FileInjector.h"
+
+/** @short Creates a file with specific permissions and content */
+FileInjector::FileInjector(const std::filesystem::path& path, const std::filesystem::perms permissions, const std::string& content)
+    : path(path)
+{
+    auto fileStream = std::ofstream(path, std::ios_base::out | std::ios_base::trunc);
+    if (!fileStream.is_open()) {
+        throw std::invalid_argument("FileInjector could not open file " + std::string(path) + " for writing");
+    }
+    fileStream << content;
+    std::filesystem::permissions(path, permissions);
+}
+
+/** @short Removes file associated with this FileInjector instance (if exists) */
+FileInjector::~FileInjector() noexcept(false)
+{
+    std::filesystem::remove(path);
+}
+
+/** @short Sets file permissions */
+void FileInjector::setPermissions(const std::filesystem::perms permissions)
+{
+    std::filesystem::permissions(path, permissions);
+}
diff --git a/tests/fs-helpers/FileInjector.h b/tests/fs-helpers/FileInjector.h
new file mode 100644
index 0000000..873d3b6
--- /dev/null
+++ b/tests/fs-helpers/FileInjector.h
@@ -0,0 +1,14 @@
+#pragma once
+#include <filesystem>
+#include <string>
+
+/** @short Represents a temporary file whose lifetime is bound by lifetime of the FileInjector instance */
+class FileInjector {
+private:
+    const std::string path;
+
+public:
+    FileInjector(const std::filesystem::path& path, const std::filesystem::perms permissions, const std::string& content);
+    ~FileInjector() noexcept(false);
+    void setPermissions(const std::filesystem::perms permissions);
+};
diff --git a/tests/fs-helpers/utils.cpp b/tests/fs-helpers/utils.cpp
new file mode 100644
index 0000000..1fc018a
--- /dev/null
+++ b/tests/fs-helpers/utils.cpp
@@ -0,0 +1,9 @@
+#include "utils.h"
+
+/** @short Remove directory tree at 'rootDir' path (if exists) */
+void removeDirectoryTreeIfExists(const std::filesystem::path& rootDir)
+{
+    if (std::filesystem::exists(rootDir)) {
+        std::filesystem::remove_all(rootDir);
+    }
+}
diff --git a/tests/fs-helpers/utils.h b/tests/fs-helpers/utils.h
new file mode 100644
index 0000000..93196f2
--- /dev/null
+++ b/tests/fs-helpers/utils.h
@@ -0,0 +1,4 @@
+#pragma once
+#include <filesystem>
+
+void removeDirectoryTreeIfExists(const std::filesystem::path& rootDir);
diff --git a/tests/hardware_emmc.cpp b/tests/hardware_emmc.cpp
index 91b06c0..4707686 100644
--- a/tests/hardware_emmc.cpp
+++ b/tests/hardware_emmc.cpp
@@ -7,6 +7,7 @@
 
 #include "trompeloeil_doctest.h"
 #include <filesystem>
+#include "fs-helpers/utils.h"
 #include "ietf-hardware/sysfs/EMMC.h"
 #include "pretty_printers.h"
 #include "test_log_setup.h"
@@ -14,17 +15,6 @@
 
 using namespace std::literals;
 
-namespace {
-
-/** @short Remove directory tree at 'rootDir' path (if exists) */
-void removeDirectoryTreeIfExists(const std::string& rootDir)
-{
-    if (std::filesystem::exists(rootDir)) {
-        std::filesystem::remove_all(rootDir);
-    }
-}
-}
-
 TEST_CASE("EMMC driver")
 {
     TEST_INIT_LOGS;
diff --git a/tests/hardware_hwmon.cpp b/tests/hardware_hwmon.cpp
new file mode 100644
index 0000000..3f9fb63
--- /dev/null
+++ b/tests/hardware_hwmon.cpp
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2017-2018 CESNET, https://photonics.cesnet.cz/
+ *
+ * Written by Miroslav Mareš <mmares@cesnet.cz>
+ *
+*/
+
+#include "trompeloeil_doctest.h"
+#include <filesystem>
+#include <fstream>
+#include "fs-helpers/FileInjector.h"
+#include "fs-helpers/utils.h"
+#include "ietf-hardware/sysfs/HWMon.h"
+#include "pretty_printers.h"
+#include "test_log_setup.h"
+#include "tests/configure.cmake.h"
+
+using namespace std::literals;
+
+TEST_CASE("HWMon class")
+{
+    TEST_INIT_LOGS;
+
+    const auto fakeHwmonRoot = CMAKE_CURRENT_BINARY_DIR + "/tests/hwmon/"s;
+    removeDirectoryTreeIfExists(fakeHwmonRoot);
+    velia::ietf_hardware::sysfs::HWMon::Attributes expected;
+
+    SECTION("Test hwmon/device1")
+    {
+        std::filesystem::copy(CMAKE_CURRENT_SOURCE_DIR + "/tests/sysfs/hwmon/device1/hwmon"s, fakeHwmonRoot, std::filesystem::copy_options::recursive);
+        auto hwmon = velia::ietf_hardware::sysfs::HWMon(fakeHwmonRoot);
+        expected = {
+            {"temp1_crit", 105'000},
+            {"temp1_input", 66'600},
+            {"temp2_crit", 105'000},
+            {"temp2_input", 29'800},
+            {"temp10_crit", 666'777},
+            {"temp10_input", 66'600},
+            {"temp11_input", 111'222'333'444'555},
+        };
+
+        REQUIRE(hwmon.attributes() == expected);
+    }
+
+    SECTION("Test hwmon/device1 + one of the files unreadable")
+    {
+        std::filesystem::copy(CMAKE_CURRENT_SOURCE_DIR + "/tests/sysfs/hwmon/device1/hwmon"s, fakeHwmonRoot, std::filesystem::copy_options::recursive);
+
+        // Inject temporary file for "no read permission" test
+        auto injected_noread = std::make_unique<FileInjector>(fakeHwmonRoot + "/hwmon0/temp3_input", std::filesystem::perms::owner_write, "-42001");
+
+        auto hwmon = velia::ietf_hardware::sysfs::HWMon(fakeHwmonRoot);
+        expected = {
+            {"temp1_crit", 105'000},
+            {"temp1_input", 66'600},
+            {"temp2_crit", 105'000},
+            {"temp2_input", 29'800},
+            {"temp3_input", -42'001},
+            {"temp10_crit", 666'777},
+            {"temp10_input", 66'600},
+            {"temp11_input", 111'222'333'444'555},
+        };
+
+        // no read permission now
+        REQUIRE_THROWS_AS(hwmon.attributes(), std::invalid_argument);
+
+        // read permission granted
+        injected_noread->setPermissions(std::filesystem::perms::owner_all);
+        REQUIRE(hwmon.attributes() == expected);
+    }
+
+    SECTION("Test hwmon/device1 + one of the files disappears after construction")
+    {
+        std::filesystem::copy(CMAKE_CURRENT_SOURCE_DIR + "/tests/sysfs/hwmon/device1/hwmon"s, fakeHwmonRoot, std::filesystem::copy_options::recursive);
+
+        // Inject temporary file for "file does not exist" test
+        auto injected_notexist = std::make_unique<FileInjector>(fakeHwmonRoot + "/hwmon0/temp3_input", std::filesystem::perms::owner_read | std::filesystem::perms::owner_write, "-42001");
+
+        auto hwmon = velia::ietf_hardware::sysfs::HWMon(fakeHwmonRoot);
+
+        expected = {
+            {"temp1_crit", 105'000},
+            {"temp1_input", 66'600},
+            {"temp2_crit", 105'000},
+            {"temp2_input", 29'800},
+            {"temp3_input", -42'001},
+            {"temp10_crit", 666'777},
+            {"temp10_input", 66'600},
+            {"temp11_input", 111'222'333'444'555},
+        };
+
+        // file exists, should be OK
+        REQUIRE(hwmon.attributes() == expected);
+
+        // file deleted
+        injected_notexist.reset();
+        REQUIRE_THROWS_AS(hwmon.attributes(), std::invalid_argument);
+    }
+
+    SECTION("Test hwmon/device1 + invalid values")
+    {
+        std::filesystem::copy(CMAKE_CURRENT_SOURCE_DIR + "/tests/sysfs/hwmon/device1/hwmon"s, fakeHwmonRoot, std::filesystem::copy_options::recursive);
+
+        SECTION("Invalid content")
+        {
+            auto injected = std::make_unique<FileInjector>(fakeHwmonRoot + "/hwmon0/temp3_input", std::filesystem::perms::owner_read | std::filesystem::perms::owner_write, "cus bus");
+            auto hwmon = velia::ietf_hardware::sysfs::HWMon(fakeHwmonRoot);
+            REQUIRE_THROWS_AS(hwmon.attributes(), std::domain_error);
+        }
+
+        SECTION("Invalid value range")
+        {
+            auto injected = std::make_unique<FileInjector>(fakeHwmonRoot + "/hwmon0/temp3_input", std::filesystem::perms::owner_read | std::filesystem::perms::owner_write, "-99999999999999999999999999999999");
+            auto hwmon = velia::ietf_hardware::sysfs::HWMon(fakeHwmonRoot);
+            REQUIRE_THROWS_AS(hwmon.attributes(), std::domain_error);
+        }
+    }
+
+    SECTION("Test hwmon/device2")
+    {
+        std::filesystem::copy(CMAKE_CURRENT_SOURCE_DIR + "/tests/sysfs/hwmon/device2/hwmon"s, fakeHwmonRoot, std::filesystem::copy_options::recursive);
+
+        auto hwmon = velia::ietf_hardware::sysfs::HWMon(fakeHwmonRoot);
+        expected = {
+            {"temp1_crit", std::numeric_limits<int64_t>::max()},
+            {"temp1_input", -34'000},
+            {"temp1_max", 80'000},
+            {"temp2_crit", std::numeric_limits<int64_t>::min()}, // we can't write an integer literal for int64_t min value (see https://gcc.gnu.org/bugzilla/show_bug.cgi?id=52661)
+            {"temp2_input", -34'000},
+            {"temp2_max", 80'000},
+            {"temp3_crit", 100'000},
+            {"temp3_input", 30'000},
+            {"temp3_max", 80'000},
+            {"temp4_crit", 100'000},
+            {"temp4_input", 26'000},
+            {"temp4_max", 80'000},
+            {"temp5_crit", 100'000},
+            {"temp5_input", 29'000},
+            {"temp5_max", 80'000},
+        };
+
+        REQUIRE(hwmon.attributes() == expected);
+    }
+
+    SECTION("Test wrong directory structure")
+    {
+        std::string sourceDir;
+        SECTION("No hwmonX directory")
+        {
+            sourceDir = "tests/sysfs/hwmon/device4/hwmon"s;
+        }
+        SECTION("Multiple hwmonX directories")
+        {
+            sourceDir = "tests/sysfs/hwmon/device3/hwmon"s;
+        }
+
+        std::filesystem::copy(CMAKE_CURRENT_SOURCE_DIR + "/"s + sourceDir, fakeHwmonRoot, std::filesystem::copy_options::recursive);
+
+        REQUIRE_THROWS_AS(velia::ietf_hardware::sysfs::HWMon(fakeHwmonRoot), std::invalid_argument);
+    }
+}
diff --git a/tests/pretty_printers.h b/tests/pretty_printers.h
index 8fea584..eb1399c 100644
--- a/tests/pretty_printers.h
+++ b/tests/pretty_printers.h
@@ -12,7 +12,6 @@
 #include <sstream>
 #include <trompeloeil.hpp>
 
-
 namespace doctest {
 
 template <>
@@ -29,4 +28,18 @@
     }
 };
 
+template <>
+struct StringMaker<std::map<std::string, int64_t>> {
+    static String convert(const std::map<std::string, int64_t>& map)
+    {
+        std::ostringstream os;
+        os << "{" << std::endl;
+        for (const auto& [key, value] : map) {
+            os << "  \"" << key << "\": " << value << "," << std::endl;
+        }
+        os << "}";
+        return os.str().c_str();
+    }
+};
+
 }
diff --git a/tests/sysfs/hwmon/device2/hwmon/hwmon33/name b/tests/sysfs/hwmon/device2/hwmon/hwmon33/name
index 2ec2faf..5f10796 100755
--- a/tests/sysfs/hwmon/device2/hwmon/hwmon33/name
+++ b/tests/sysfs/hwmon/device2/hwmon/hwmon33/name
@@ -1 +1 @@
-satan temperatures
+some small or large numbers are to be read here
diff --git a/tests/sysfs/hwmon/device2/hwmon/hwmon33/temp1_crit b/tests/sysfs/hwmon/device2/hwmon/hwmon33/temp1_crit
index 363ca95..2045006 100755
--- a/tests/sysfs/hwmon/device2/hwmon/hwmon33/temp1_crit
+++ b/tests/sysfs/hwmon/device2/hwmon/hwmon33/temp1_crit
@@ -1 +1 @@
-1000000000000000000005666
+9223372036854775807
diff --git a/tests/sysfs/hwmon/device2/hwmon/hwmon33/temp2_crit b/tests/sysfs/hwmon/device2/hwmon/hwmon33/temp2_crit
index 0277850..7928ab8 100755
--- a/tests/sysfs/hwmon/device2/hwmon/hwmon33/temp2_crit
+++ b/tests/sysfs/hwmon/device2/hwmon/hwmon33/temp2_crit
@@ -1 +1 @@
-nazdar bazar carodej Merlin
+-9223372036854775808
diff --git a/tests/sysfs/hwmon/device2/hwmon/hwmon33/temp2_input b/tests/sysfs/hwmon/device2/hwmon/hwmon33/temp2_input
index 782a512..acab4b6 100755
--- a/tests/sysfs/hwmon/device2/hwmon/hwmon33/temp2_input
+++ b/tests/sysfs/hwmon/device2/hwmon/hwmon33/temp2_input
@@ -1 +1 @@
--1234567891234567896666
+-34000