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/src/ffdc_file.cpp b/phosphor-regulators/src/ffdc_file.cpp
new file mode 100644
index 0000000..7f528d6
--- /dev/null
+++ b/phosphor-regulators/src/ffdc_file.cpp
@@ -0,0 +1,61 @@
+/**
+ * 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 <errno.h>     // for errno
+#include <fcntl.h>     // for open()
+#include <string.h>    // for strerror()
+#include <sys/stat.h>  // for open()
+#include <sys/types.h> // for open()
+
+#include <stdexcept>
+#include <string>
+
+namespace phosphor::power::regulators
+{
+
+FFDCFile::FFDCFile(FFDCFormat format, uint8_t subType, uint8_t version) :
+    format{format}, subType{subType}, version{version}
+{
+    // Open the temporary file for both reading and writing
+    int fd = open(tempFile.getPath().c_str(), O_RDWR);
+    if (fd == -1)
+    {
+        throw std::runtime_error{std::string{"Unable to open FFDC file: "} +
+                                 strerror(errno)};
+    }
+
+    // Store file descriptor in FileDescriptor object
+    descriptor.set(fd);
+}
+
+void FFDCFile::remove()
+{
+    // Close file descriptor.  Does nothing if descriptor was already closed.
+    // Returns -1 if close failed.
+    if (descriptor.close() == -1)
+    {
+        throw std::runtime_error{std::string{"Unable to close FFDC file: "} +
+                                 strerror(errno)};
+    }
+
+    // Delete temporary file.  Does nothing if file was already deleted.
+    // Throws an exception if the deletion failed.
+    tempFile.remove();
+}
+
+} // namespace phosphor::power::regulators
diff --git a/phosphor-regulators/src/ffdc_file.hpp b/phosphor-regulators/src/ffdc_file.hpp
new file mode 100644
index 0000000..923f09e
--- /dev/null
+++ b/phosphor-regulators/src/ffdc_file.hpp
@@ -0,0 +1,172 @@
+/**
+ * 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.
+ */
+#pragma once
+
+#include "file_descriptor.hpp"
+#include "temporary_file.hpp"
+#include "xyz/openbmc_project/Logging/Create/server.hpp"
+
+#include <cstdint>
+#include <filesystem>
+
+namespace phosphor::power::regulators
+{
+
+namespace fs = std::filesystem;
+using FFDCFormat =
+    sdbusplus::xyz::openbmc_project::Logging::server::Create::FFDCFormat;
+using FileDescriptor = phosphor::power::util::FileDescriptor;
+
+/**
+ * @class FFDCFile
+ *
+ * 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.
+ *
+ * Use getFileDescriptor() to obtain the file descriptor needed to read or write
+ * data to the file.
+ *
+ * Use remove() to delete the file.  Otherwise the file will be deleted by the
+ * destructor.
+ *
+ * FFDCFile objects cannot be copied, but they can be moved.  This enables them
+ * to be stored in containers like std::vector.
+ */
+class FFDCFile
+{
+  public:
+    // Specify which compiler-generated methods we want
+    FFDCFile() = delete;
+    FFDCFile(const FFDCFile&) = delete;
+    FFDCFile(FFDCFile&&) = default;
+    FFDCFile& operator=(const FFDCFile&) = delete;
+    FFDCFile& operator=(FFDCFile&&) = default;
+    ~FFDCFile() = default;
+
+    /**
+     * Constructor.
+     *
+     * Creates the file and opens it for both reading and writing.
+     *
+     * Throws an exception if an error occurs.
+     *
+     * @param format format type of the contained data
+     * @param subType format subtype; used for the 'Custom' type
+     * @param version version of the data format; used for the 'Custom' type
+     */
+    explicit FFDCFile(FFDCFormat format, uint8_t subType = 0,
+                      uint8_t version = 0);
+
+    /**
+     * Returns the file descriptor for the file.
+     *
+     * The file is open for both reading and writing.
+     *
+     * @return file descriptor
+     */
+    int getFileDescriptor()
+    {
+        // Return the integer file descriptor within the FileDescriptor object
+        return descriptor();
+    }
+
+    /**
+     * Returns the format type of the contained data.
+     *
+     * @return format type
+     */
+    FFDCFormat getFormat() const
+    {
+        return format;
+    }
+
+    /**
+     * Returns the absolute path to the file.
+     *
+     * @return absolute path
+     */
+    const fs::path& getPath() const
+    {
+        return tempFile.getPath();
+    }
+
+    /**
+     * Returns the format subtype.
+     *
+     * @return subtype
+     */
+    uint8_t getSubType() const
+    {
+        return subType;
+    }
+
+    /**
+     * Returns the version of the data format.
+     *
+     * @return version
+     */
+    uint8_t getVersion() const
+    {
+        return version;
+    }
+
+    /**
+     * Closes and deletes the file.
+     *
+     * Does nothing if the file has already been removed.
+     *
+     * Throws an exception if an error occurs.
+     */
+    void remove();
+
+  private:
+    /**
+     * Format type of the contained data.
+     */
+    FFDCFormat format{FFDCFormat::Text};
+
+    /**
+     * Format subtype; used for the 'Custom' type.
+     */
+    uint8_t subType{0};
+
+    /**
+     * Version of the data format; used for the 'Custom' type.
+     */
+    uint8_t version{0};
+
+    /**
+     * Temporary file where FFDC data is stored.
+     *
+     * The TemporaryFile destructor will automatically delete the file if it was
+     * not explicitly deleted using remove().
+     */
+    TemporaryFile tempFile{};
+
+    /**
+     * File descriptor for reading from/writing to the file.
+     *
+     * The FileDescriptor destructor will automatically close the file if it was
+     * not explicitly closed using remove().
+     */
+    FileDescriptor descriptor{};
+};
+
+} // namespace phosphor::power::regulators
diff --git a/phosphor-regulators/src/meson.build b/phosphor-regulators/src/meson.build
index 2012855..8303769 100644
--- a/phosphor-regulators/src/meson.build
+++ b/phosphor-regulators/src/meson.build
@@ -10,6 +10,7 @@
     'configuration.cpp',
     'device.cpp',
     'exception_utils.cpp',
+    'ffdc_file.cpp',
     'id_map.cpp',
     'pmbus_utils.cpp',
     'rail.cpp',
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