regulators: Create FFDCFile class

Create C++ class for a file that contains FFDC (first failure data
capture) data.

This class is used to store FFDC data in an error log.  The FFDC data is
passed to the error logging system using a file descriptor.

The constructor creates the file and opens it for both reading and
writing.  The getFileDescriptor() method returns the file descriptor for
reading/writing the file.  The destructor closes and deletes the file.

Also moved the test utility functions makeFileUnRemovable() and
makeFileRemovable() to test_utils.hpp so they can be used by multiple
testcases.

Change-Id: Iddef488a28e83a0df7e7f6955c3217ecb3ec2d51
diff --git a/phosphor-regulators/test/ffdc_file_tests.cpp b/phosphor-regulators/test/ffdc_file_tests.cpp
new file mode 100644
index 0000000..0067f1a
--- /dev/null
+++ b/phosphor-regulators/test/ffdc_file_tests.cpp
@@ -0,0 +1,225 @@
+/**
+ * Copyright © 2020 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include "ffdc_file.hpp"
+#include "test_utils.hpp"
+
+#include <errno.h>     // for errno
+#include <fcntl.h>     // for fcntl()
+#include <string.h>    // for memset(), size_t
+#include <sys/types.h> // for lseek()
+#include <unistd.h>    // for read(), write(), lseek(), fcntl(), close()
+
+#include <exception>
+#include <filesystem>
+
+#include <gtest/gtest.h>
+
+using namespace phosphor::power::regulators;
+using namespace phosphor::power::regulators::test_utils;
+namespace fs = std::filesystem;
+
+/**
+ * Returns whether the specified file descriptor is valid/open.
+ *
+ * @param[in] fd - File descriptor
+ * @return true if descriptor is valid/open, false otherwise
+ */
+bool isValid(int fd)
+{
+    return (fcntl(fd, F_GETFL) != -1) || (errno != EBADF);
+}
+
+TEST(FFDCFileTests, Constructor)
+{
+    // Test where only the FFDCFormat parameter is specified
+    {
+        FFDCFile file{FFDCFormat::JSON};
+        EXPECT_NE(file.getFileDescriptor(), -1);
+        EXPECT_TRUE(isValid(file.getFileDescriptor()));
+        EXPECT_EQ(file.getFormat(), FFDCFormat::JSON);
+        EXPECT_FALSE(file.getPath().empty());
+        EXPECT_TRUE(fs::exists(file.getPath()));
+        EXPECT_EQ(file.getSubType(), 0);
+        EXPECT_EQ(file.getVersion(), 0);
+    }
+
+    // Test where all constructor parameters are specified
+    {
+        FFDCFile file{FFDCFormat::Custom, 2, 3};
+        EXPECT_NE(file.getFileDescriptor(), -1);
+        EXPECT_TRUE(isValid(file.getFileDescriptor()));
+        EXPECT_EQ(file.getFormat(), FFDCFormat::Custom);
+        EXPECT_FALSE(file.getPath().empty());
+        EXPECT_TRUE(fs::exists(file.getPath()));
+        EXPECT_EQ(file.getSubType(), 2);
+        EXPECT_EQ(file.getVersion(), 3);
+    }
+
+    // Note: The case where open() fails currently needs to be tested manually
+}
+
+TEST(FFDCFileTests, GetFileDescriptor)
+{
+    FFDCFile file{FFDCFormat::JSON};
+    int fd = file.getFileDescriptor();
+    EXPECT_NE(fd, -1);
+    EXPECT_TRUE(isValid(fd));
+
+    // Write some data to the file
+    char buffer[] = "This is some sample data";
+    size_t count = sizeof(buffer);
+    EXPECT_EQ(write(fd, buffer, count), count);
+
+    // Seek back to the beginning of the file
+    EXPECT_EQ(lseek(fd, 0, SEEK_SET), 0);
+
+    // Clear buffer
+    memset(buffer, '\0', count);
+    EXPECT_STREQ(buffer, "");
+
+    // Read and verify file contents
+    EXPECT_EQ(read(fd, buffer, count), count);
+    EXPECT_STREQ(buffer, "This is some sample data");
+}
+
+TEST(FFDCFileTests, GetFormat)
+{
+    // Test where 'Text' was specified
+    {
+        FFDCFile file{FFDCFormat::Text};
+        EXPECT_EQ(file.getFormat(), FFDCFormat::Text);
+    }
+
+    // Test where 'Custom' was specified
+    {
+        FFDCFile file{FFDCFormat::Custom, 2, 3};
+        EXPECT_EQ(file.getFormat(), FFDCFormat::Custom);
+    }
+}
+
+TEST(FFDCFileTests, GetPath)
+{
+    FFDCFile file{FFDCFormat::JSON};
+    EXPECT_FALSE(file.getPath().empty());
+    EXPECT_TRUE(fs::exists(file.getPath()));
+}
+
+TEST(FFDCFileTests, GetSubType)
+{
+    // Test where subType was not specified
+    {
+        FFDCFile file{FFDCFormat::JSON};
+        EXPECT_EQ(file.getSubType(), 0);
+    }
+
+    // Test where subType was specified
+    {
+        FFDCFile file{FFDCFormat::Custom, 3, 2};
+        EXPECT_EQ(file.getSubType(), 3);
+    }
+}
+
+TEST(FFDCFileTests, GetVersion)
+{
+    // Test where version was not specified
+    {
+        FFDCFile file{FFDCFormat::JSON};
+        EXPECT_EQ(file.getVersion(), 0);
+    }
+
+    // Test where version was specified
+    {
+        FFDCFile file{FFDCFormat::Custom, 2, 5};
+        EXPECT_EQ(file.getVersion(), 5);
+    }
+}
+
+TEST(FFDCFileTests, Remove)
+{
+    // Test where works
+    {
+        FFDCFile file{FFDCFormat::JSON};
+        EXPECT_NE(file.getFileDescriptor(), -1);
+        EXPECT_TRUE(isValid(file.getFileDescriptor()));
+        EXPECT_FALSE(file.getPath().empty());
+        EXPECT_TRUE(fs::exists(file.getPath()));
+
+        int fd = file.getFileDescriptor();
+        fs::path path = file.getPath();
+
+        file.remove();
+        EXPECT_EQ(file.getFileDescriptor(), -1);
+        EXPECT_TRUE(file.getPath().empty());
+
+        EXPECT_FALSE(isValid(fd));
+        EXPECT_FALSE(fs::exists(path));
+    }
+
+    // Test where file was already removed
+    {
+        FFDCFile file{FFDCFormat::JSON};
+        EXPECT_NE(file.getFileDescriptor(), -1);
+        EXPECT_FALSE(file.getPath().empty());
+
+        file.remove();
+        EXPECT_EQ(file.getFileDescriptor(), -1);
+        EXPECT_TRUE(file.getPath().empty());
+
+        file.remove();
+        EXPECT_EQ(file.getFileDescriptor(), -1);
+        EXPECT_TRUE(file.getPath().empty());
+    }
+
+    // Test where closing the file fails
+    {
+        FFDCFile file{FFDCFormat::JSON};
+        int fd = file.getFileDescriptor();
+        EXPECT_TRUE(isValid(fd));
+
+        EXPECT_EQ(close(fd), 0);
+        EXPECT_FALSE(isValid(fd));
+
+        try
+        {
+            file.remove();
+            ADD_FAILURE() << "Should not have reached this line.";
+        }
+        catch (const std::exception& e)
+        {
+            EXPECT_NE(std::string{e.what()}.find("Unable to close FFDC file: "),
+                      std::string::npos);
+        }
+    }
+
+    // Test where deleting the file fails
+    {
+        FFDCFile file{FFDCFormat::JSON};
+        fs::path path = file.getPath();
+        EXPECT_TRUE(fs::exists(path));
+
+        makeFileUnRemovable(path);
+        try
+        {
+            file.remove();
+            ADD_FAILURE() << "Should not have reached this line.";
+        }
+        catch (const std::exception& e)
+        {
+            // This is expected.  Exception message will vary.
+        }
+        makeFileRemovable(path);
+    }
+}
diff --git a/phosphor-regulators/test/meson.build b/phosphor-regulators/test/meson.build
index 511695a..74681f6 100644
--- a/phosphor-regulators/test/meson.build
+++ b/phosphor-regulators/test/meson.build
@@ -11,6 +11,7 @@
     'device_tests.cpp',
     'error_history_tests.cpp',
     'exception_utils_tests.cpp',
+    'ffdc_file_tests.cpp',
     'id_map_tests.cpp',
     'mock_journal.cpp',
     'pmbus_error_tests.cpp',
diff --git a/phosphor-regulators/test/temporary_file_tests.cpp b/phosphor-regulators/test/temporary_file_tests.cpp
index dafdcf3..7fe0a4d 100644
--- a/phosphor-regulators/test/temporary_file_tests.cpp
+++ b/phosphor-regulators/test/temporary_file_tests.cpp
@@ -14,55 +14,18 @@
  * limitations under the License.
  */
 #include "temporary_file.hpp"
+#include "test_utils.hpp"
 
 #include <filesystem>
-#include <fstream>
 #include <string>
 #include <utility>
 
 #include <gtest/gtest.h>
 
 using namespace phosphor::power::regulators;
+using namespace phosphor::power::regulators::test_utils;
 namespace fs = std::filesystem;
 
-/**
- * Modify the specified temporary file so that fs::remove() fails with an
- * exception.
- *
- * @param path path to the temporary file
- */
-void makeFileUnRemovable(const fs::path& path)
-{
-    // Delete temporary file.  Note that this is not sufficient to cause
-    // fs::remove() to throw an exception.
-    fs::remove(path);
-
-    // Create a directory at the temporary file path
-    fs::create_directory(path);
-
-    // Create a file within the directory.  fs::remove() will throw an exception
-    // if the path is a non-empty directory.
-    std::ofstream childFile{path / "childFile"};
-}
-
-/**
- * Modify the specified temporary file so that fs::remove() can successfully
- * delete it.
- *
- * Undo the modifications from an earlier call to makeFileUnRemovable().
- *
- * @param path path to the temporary file
- */
-void makeFileRemovable(const fs::path& path)
-{
-    // makeFileUnRemovable() creates a directory at the temporary file path.
-    // Remove the directory and all of its contents.
-    fs::remove_all(path);
-
-    // Re-create the temporary file
-    std::ofstream file{path};
-}
-
 TEST(TemporaryFileTests, DefaultConstructor)
 {
     TemporaryFile file{};
diff --git a/phosphor-regulators/test/test_utils.hpp b/phosphor-regulators/test/test_utils.hpp
index 313d9f7..f56dc18 100644
--- a/phosphor-regulators/test/test_utils.hpp
+++ b/phosphor-regulators/test/test_utils.hpp
@@ -23,6 +23,8 @@
 #include "rail.hpp"
 #include "rule.hpp"
 
+#include <filesystem>
+#include <fstream>
 #include <memory>
 #include <string>
 #include <utility>
@@ -31,6 +33,8 @@
 namespace phosphor::power::regulators::test_utils
 {
 
+namespace fs = std::filesystem;
+
 /**
  * Create an I2CInterface object with hard-coded bus and address values.
  *
@@ -89,4 +93,43 @@
     return std::make_unique<Rule>(id, std::move(actions));
 }
 
+/**
+ * Modify the specified file so that fs::remove() fails with an exception.
+ *
+ * The file will be renamed and can be restored by calling makeFileRemovable().
+ *
+ * @param path path to the file
+ */
+inline void makeFileUnRemovable(const fs::path& path)
+{
+    // Rename the file to save its contents
+    fs::path savePath{path.native() + ".save"};
+    fs::rename(path, savePath);
+
+    // Create a directory at the original file path
+    fs::create_directory(path);
+
+    // Create a file within the directory.  fs::remove() will throw an exception
+    // if the path is a non-empty directory.
+    std::ofstream childFile{path / "childFile"};
+}
+
+/**
+ * Modify the specified file so that fs::remove() can successfully delete it.
+ *
+ * Undo the modifications from an earlier call to makeFileUnRemovable().
+ *
+ * @param path path to the file
+ */
+inline void makeFileRemovable(const fs::path& path)
+{
+    // makeFileUnRemovable() creates a directory at the file path.  Remove the
+    // directory and all of its contents.
+    fs::remove_all(path);
+
+    // Rename the file back to the original path to restore its contents
+    fs::path savePath{path.native() + ".save"};
+    fs::rename(savePath, path);
+}
+
 } // namespace phosphor::power::regulators::test_utils