hwmonio: Add injection point to test internal behavior

With the complexities of the various behaviors within the read() method
of HwmonIO, introduce an injection point for testing.  There is a
default available, and therefore this is a surgical change that only
impacts future tests.

Signed-off-by: Patrick Venture <venture@google.com>
Change-Id: I1ead56c7fe1a2f87ebf316488e68f435a41c9d19
diff --git a/hwmonio.cpp b/hwmonio.cpp
index 9142c18..82d1a9d 100644
--- a/hwmonio.cpp
+++ b/hwmonio.cpp
@@ -25,6 +25,40 @@
 namespace hwmonio
 {
 
+int64_t FileSystem::read(const std::string& path) const
+{
+    int64_t val;
+    std::ifstream ifs;
+    ifs.exceptions(std::ifstream::failbit | std::ifstream::badbit |
+                   std::ifstream::eofbit);
+
+    errno = 0;
+    if (!ifs.is_open())
+        ifs.open(path);
+    ifs.clear();
+    ifs.seekg(0);
+    ifs >> val;
+
+    return val;
+}
+
+void FileSystem::write(const std::string& path, uint32_t value) const
+{
+    std::ofstream ofs;
+    ofs.exceptions(std::ofstream::failbit | std::ofstream::badbit |
+                   std::ofstream::eofbit);
+
+    errno = 0;
+    if (!ofs.is_open())
+        ofs.open(path);
+    ofs.clear();
+    ofs.seekp(0);
+    ofs << value;
+    ofs.flush();
+}
+
+FileSystem fileSystemImpl;
+
 static constexpr auto retryableErrors = {
     /*
      * Retry on bus or device errors or timeouts in case
@@ -71,7 +105,8 @@
     EMSGSIZE,
 };
 
-HwmonIO::HwmonIO(const std::string& path) : _p(path)
+HwmonIO::HwmonIO(const std::string& path, const FileSystemInterface* intf) :
+    _p(path), _intf(intf)
 {
 }
 
@@ -80,22 +115,13 @@
                       std::chrono::milliseconds delay) const
 {
     int64_t val;
-    std::ifstream ifs;
     auto fullPath = sysfs::make_sysfs_path(_p, type, id, sensor);
 
-    ifs.exceptions(std::ifstream::failbit | std::ifstream::badbit |
-                   std::ifstream::eofbit);
-
     while (true)
     {
         try
         {
-            errno = 0;
-            if (!ifs.is_open())
-                ifs.open(fullPath);
-            ifs.clear();
-            ifs.seekg(0);
-            ifs >> val;
+            val = _intf->read(fullPath);
         }
         catch (const std::exception& e)
         {
@@ -148,12 +174,8 @@
                     size_t retries, std::chrono::milliseconds delay) const
 
 {
-    std::ofstream ofs;
     auto fullPath = sysfs::make_sysfs_path(_p, type, id, sensor);
 
-    ofs.exceptions(std::ofstream::failbit | std::ofstream::badbit |
-                   std::ofstream::eofbit);
-
     // See comments in the read method for an explanation of the odd exception
     // handling behavior here.
 
@@ -161,13 +183,7 @@
     {
         try
         {
-            errno = 0;
-            if (!ofs.is_open())
-                ofs.open(fullPath);
-            ofs.clear();
-            ofs.seekp(0);
-            ofs << val;
-            ofs.flush();
+            _intf->write(fullPath, val);
         }
         catch (const std::exception& e)
         {
diff --git a/hwmonio.hpp b/hwmonio.hpp
index b0b8617..ff18204 100644
--- a/hwmonio.hpp
+++ b/hwmonio.hpp
@@ -9,6 +9,28 @@
 static constexpr auto retries = 10;
 static constexpr auto delay = std::chrono::milliseconds{100};
 
+/** @class FileSystemInterface
+ *  @brief Abstract base class allowing testing of HwmonIO.
+ *
+ *  This is used to provide testing of behaviors within HwmonIO.
+ */
+class FileSystemInterface
+{
+  public:
+    virtual ~FileSystemInterface() = default;
+    virtual int64_t read(const std::string& path) const = 0;
+    virtual void write(const std::string& path, uint32_t value) const = 0;
+};
+
+class FileSystem : public FileSystemInterface
+{
+  public:
+    int64_t read(const std::string& path) const override;
+    void write(const std::string& path, uint32_t value) const override;
+};
+
+extern FileSystem fileSystemImpl;
+
 /** @class HwmonIOInterface
  *  @brief Abstract base class defining a HwmonIOInterface.
  *
@@ -56,7 +78,8 @@
      *  @param[in] path - hwmon instance root - eg:
      *      /sys/class/hwmon/hwmon<N>
      */
-    explicit HwmonIO(const std::string& path);
+    explicit HwmonIO(const std::string& path,
+                     const FileSystemInterface* intf = &fileSystemImpl);
 
     /** @brief Perform formatted hwmon sysfs read.
      *
@@ -108,6 +131,7 @@
 
   private:
     std::string _p;
+    const FileSystemInterface* _intf;
 };
 
 } // namespace hwmonio
diff --git a/test/Makefile.am b/test/Makefile.am
index 37aa977..016409f 100644
--- a/test/Makefile.am
+++ b/test/Makefile.am
@@ -21,7 +21,7 @@
 	$(CODE_COVERAGE_LIBS)
 
 # Run all 'check' test programs
-check_PROGRAMS = hwmon_unittest fanpwm_unittest sensor_unittest
+check_PROGRAMS = hwmon_unittest fanpwm_unittest sensor_unittest hwmonio_default_unittest
 TESTS = $(check_PROGRAMS)
 
 hwmon_unittest_SOURCES = hwmon_unittest.cpp
@@ -42,3 +42,6 @@
 	$(top_builddir)/hwmon.o \
 	env.o \
 	gpio.o
+
+hwmonio_default_unittest_SOURCES = hwmonio_default_unittest.cpp
+hwmonio_default_unittest_LDADD = $(top_builddir)/hwmonio.o
diff --git a/test/filesystem_mock.hpp b/test/filesystem_mock.hpp
new file mode 100644
index 0000000..9d24d3c
--- /dev/null
+++ b/test/filesystem_mock.hpp
@@ -0,0 +1,19 @@
+#pragma once
+
+#include "hwmonio.hpp"
+
+#include <string>
+
+#include <gmock/gmock.h>
+
+namespace hwmonio
+{
+
+class FileSystemMock : public FileSystemInterface
+{
+  public:
+    MOCK_CONST_METHOD1(read, int64_t(const std::string&));
+    MOCK_CONST_METHOD2(write, void(const std::string&, uint32_t));
+};
+
+} // namespace hwmonio
diff --git a/test/hwmonio_default_unittest.cpp b/test/hwmonio_default_unittest.cpp
new file mode 100644
index 0000000..233bf56
--- /dev/null
+++ b/test/hwmonio_default_unittest.cpp
@@ -0,0 +1,58 @@
+#include "filesystem_mock.hpp"
+#include "hwmonio.hpp"
+
+#include <chrono>
+#include <string>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+namespace hwmonio
+{
+namespace
+{
+
+using ::testing::_;
+using ::testing::Return;
+
+class HwmonIOTest : public ::testing::Test
+{
+  protected:
+    HwmonIOTest() : _hwmonio(_path, &_mock)
+    {
+    }
+
+    const int64_t _value = 12;
+    const std::string _path = "abcd";
+    const std::string _type = "fan";
+    const std::string _id = "a";
+    const std::string _sensor = "1";
+    const size_t _retries = 1;
+    const std::chrono::milliseconds _delay = std::chrono::milliseconds{10};
+
+    FileSystemMock _mock;
+    HwmonIO _hwmonio;
+};
+
+TEST_F(HwmonIOTest, ReadReturnsValue)
+{
+    EXPECT_CALL(_mock, read(_)).WillOnce(Return(_value));
+    EXPECT_THAT(_hwmonio.read(_type, _id, _sensor, _retries, _delay), _value);
+}
+
+int64_t SetErrnoExcept(const std::string&)
+{
+    errno = ETIMEDOUT;
+    throw std::runtime_error("bad times");
+}
+
+TEST_F(HwmonIOTest, ReadExceptsRetryable)
+{
+    EXPECT_CALL(_mock, read(_))
+        .WillOnce(&SetErrnoExcept)
+        .WillOnce(Return(_value));
+    EXPECT_THAT(_hwmonio.read(_type, _id, _sensor, _retries, _delay), _value);
+}
+
+} // namespace
+} // namespace hwmonio