regulators: Create error_logging_utils

Create the error_logging_utils namespace for utility functions that make
it easier to log errors.  Create two initial functions within the
namespace.

The first function logs an error based on an exception.  The exception
may have one or more nested inner exceptions.  The function finds the
highest priority exception and logs a corresponding error.

The second function provides the same basic behavior as the first, but
it adds an ErrorHistory parameter.  An error will only be logged if it
was not previously logged.  The ErrorHistory object is used to
determine whether an error has been previously logged.  This avoids
logging duplicate errors if a regulator operation is occurring
repeatedly, such as reading sensor values.

Signed-off-by: Shawn McCarney <shawnmm@us.ibm.com>
Change-Id: If246cde9a0f60c5bba34ae4a0d68fb511c0024fc
diff --git a/phosphor-regulators/test/error_logging_utils_tests.cpp b/phosphor-regulators/test/error_logging_utils_tests.cpp
new file mode 100644
index 0000000..f7665c0
--- /dev/null
+++ b/phosphor-regulators/test/error_logging_utils_tests.cpp
@@ -0,0 +1,719 @@
+/**
+ * Copyright © 2021 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 "config_file_parser_error.hpp"
+#include "error_logging.hpp"
+#include "error_logging_utils.hpp"
+#include "i2c_interface.hpp"
+#include "journal.hpp"
+#include "mock_error_logging.hpp"
+#include "mock_journal.hpp"
+#include "mock_services.hpp"
+#include "pmbus_error.hpp"
+#include "write_verification_error.hpp"
+
+#include <errno.h>
+
+#include <sdbusplus/exception.hpp>
+
+#include <exception>
+#include <filesystem>
+#include <stdexcept>
+#include <string>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+using namespace phosphor::power::regulators;
+
+namespace fs = std::filesystem;
+
+using ::testing::Ref;
+
+TEST(ErrorLoggingUtilsTests, LogError_3Parameters)
+{
+    // Create exception with two nesting levels; top priority is inner
+    // PMBusError
+    std::exception_ptr eptr;
+    try
+    {
+        try
+        {
+            throw PMBusError{"VOUT_MODE contains unsupported data format",
+                             "reg1",
+                             "/xyz/openbmc_project/inventory/system/chassis/"
+                             "motherboard/reg1"};
+        }
+        catch (...)
+        {
+            std::throw_with_nested(
+                std::runtime_error{"Unable to set output voltage"});
+        }
+    }
+    catch (...)
+    {
+        eptr = std::current_exception();
+    }
+
+    // Create MockServices.  Expect logPMBusError() to be called.
+    MockServices services{};
+    MockErrorLogging& errorLogging = services.getMockErrorLogging();
+    MockJournal& journal = services.getMockJournal();
+    EXPECT_CALL(
+        errorLogging,
+        logPMBusError(
+            Entry::Level::Error, Ref(journal),
+            "/xyz/openbmc_project/inventory/system/chassis/motherboard/reg1"))
+        .Times(1);
+
+    // Log error based on the nested exception
+    error_logging_utils::logError(eptr, Entry::Level::Error, services);
+}
+
+TEST(ErrorLoggingUtilsTests, LogError_4Parameters)
+{
+    // Test where exception pointer is null
+    {
+        std::exception_ptr eptr;
+
+        // Create MockServices.  Don't expect any log*() methods to be called.
+        MockServices services{};
+
+        ErrorHistory history{};
+        error_logging_utils::logError(eptr, Entry::Level::Error, services,
+                                      history);
+    }
+
+    // Test where exception is not nested
+    {
+        std::exception_ptr eptr;
+        try
+        {
+            throw i2c::I2CException{"Unable to open device reg1", "/dev/i2c-8",
+                                    0x30, ENODEV};
+        }
+        catch (...)
+        {
+            eptr = std::current_exception();
+        }
+
+        // Create MockServices.  Expect logI2CError() to be called.
+        MockServices services{};
+        MockErrorLogging& errorLogging = services.getMockErrorLogging();
+        MockJournal& journal = services.getMockJournal();
+        EXPECT_CALL(errorLogging,
+                    logI2CError(Entry::Level::Critical, Ref(journal),
+                                "/dev/i2c-8", 0x30, ENODEV))
+            .Times(1);
+
+        // Log error based on the nested exception
+        ErrorHistory history{};
+        error_logging_utils::logError(eptr, Entry::Level::Critical, services,
+                                      history);
+    }
+
+    // Test where exception is nested
+    {
+        std::exception_ptr eptr;
+        try
+        {
+            try
+            {
+                throw std::invalid_argument{"JSON element is not an array"};
+            }
+            catch (...)
+            {
+                std::throw_with_nested(ConfigFileParserError{
+                    fs::path{"/etc/phosphor-regulators/config.json"},
+                    "Unable to parse JSON configuration file"});
+            }
+        }
+        catch (...)
+        {
+            eptr = std::current_exception();
+        }
+
+        // Create MockServices.  Expect logConfigFileError() to be called.
+        MockServices services{};
+        MockErrorLogging& errorLogging = services.getMockErrorLogging();
+        MockJournal& journal = services.getMockJournal();
+        EXPECT_CALL(errorLogging,
+                    logConfigFileError(Entry::Level::Warning, Ref(journal)))
+            .Times(1);
+
+        // Log error based on the nested exception
+        ErrorHistory history{};
+        error_logging_utils::logError(eptr, Entry::Level::Warning, services,
+                                      history);
+    }
+
+    // Test where exception is a ConfigFileParserError
+    {
+        std::exception_ptr eptr;
+        try
+        {
+            throw ConfigFileParserError{
+                fs::path{"/etc/phosphor-regulators/config.json"},
+                "Unable to parse JSON configuration file"};
+        }
+        catch (...)
+        {
+            eptr = std::current_exception();
+        }
+
+        // Create MockServices.  Expect logConfigFileError() to be called once.
+        MockServices services{};
+        MockErrorLogging& errorLogging = services.getMockErrorLogging();
+        MockJournal& journal = services.getMockJournal();
+        EXPECT_CALL(errorLogging,
+                    logConfigFileError(Entry::Level::Error, Ref(journal)))
+            .Times(1);
+
+        // Log error based on the nested exception
+        ErrorHistory history{};
+        error_logging_utils::logError(eptr, Entry::Level::Error, services,
+                                      history);
+
+        // Try to log error again.  Should not happen due to ErrorHistory.
+        error_logging_utils::logError(eptr, Entry::Level::Error, services,
+                                      history);
+    }
+
+    // Test where exception is a PMBusError
+    {
+        std::exception_ptr eptr;
+        try
+        {
+            throw PMBusError{"VOUT_MODE contains unsupported data format",
+                             "reg1",
+                             "/xyz/openbmc_project/inventory/system/chassis/"
+                             "motherboard/reg1"};
+        }
+        catch (...)
+        {
+            eptr = std::current_exception();
+        }
+
+        // Create MockServices.  Expect logPMBusError() to be called once.
+        MockServices services{};
+        MockErrorLogging& errorLogging = services.getMockErrorLogging();
+        MockJournal& journal = services.getMockJournal();
+        EXPECT_CALL(errorLogging,
+                    logPMBusError(Entry::Level::Error, Ref(journal),
+                                  "/xyz/openbmc_project/inventory/system/"
+                                  "chassis/motherboard/reg1"))
+            .Times(1);
+
+        // Log error based on the nested exception
+        ErrorHistory history{};
+        error_logging_utils::logError(eptr, Entry::Level::Error, services,
+                                      history);
+
+        // Try to log error again.  Should not happen due to ErrorHistory.
+        error_logging_utils::logError(eptr, Entry::Level::Error, services,
+                                      history);
+    }
+
+    // Test where exception is a WriteVerificationError
+    {
+        std::exception_ptr eptr;
+        try
+        {
+            throw WriteVerificationError{
+                "value_written: 0xDEAD, value_read: 0xBEEF", "reg1",
+                "/xyz/openbmc_project/inventory/system/chassis/motherboard/"
+                "reg1"};
+        }
+        catch (...)
+        {
+            eptr = std::current_exception();
+        }
+
+        // Create MockServices.  Expect logWriteVerificationError() to be
+        // called once.
+        MockServices services{};
+        MockErrorLogging& errorLogging = services.getMockErrorLogging();
+        MockJournal& journal = services.getMockJournal();
+        EXPECT_CALL(errorLogging, logWriteVerificationError(
+                                      Entry::Level::Warning, Ref(journal),
+                                      "/xyz/openbmc_project/inventory/system/"
+                                      "chassis/motherboard/reg1"))
+            .Times(1);
+
+        // Log error based on the nested exception
+        ErrorHistory history{};
+        error_logging_utils::logError(eptr, Entry::Level::Warning, services,
+                                      history);
+
+        // Try to log error again.  Should not happen due to ErrorHistory.
+        error_logging_utils::logError(eptr, Entry::Level::Warning, services,
+                                      history);
+    }
+
+    // Test where exception is a I2CException
+    {
+        std::exception_ptr eptr;
+        try
+        {
+            throw i2c::I2CException{"Unable to open device reg1", "/dev/i2c-8",
+                                    0x30, ENODEV};
+        }
+        catch (...)
+        {
+            eptr = std::current_exception();
+        }
+
+        // Create MockServices.  Expect logI2CError() to be called once.
+        MockServices services{};
+        MockErrorLogging& errorLogging = services.getMockErrorLogging();
+        MockJournal& journal = services.getMockJournal();
+        EXPECT_CALL(errorLogging,
+                    logI2CError(Entry::Level::Informational, Ref(journal),
+                                "/dev/i2c-8", 0x30, ENODEV))
+            .Times(1);
+
+        // Log error based on the nested exception
+        ErrorHistory history{};
+        error_logging_utils::logError(eptr, Entry::Level::Informational,
+                                      services, history);
+
+        // Try to log error again.  Should not happen due to ErrorHistory.
+        error_logging_utils::logError(eptr, Entry::Level::Informational,
+                                      services, history);
+    }
+
+    // Test where exception is a sdbusplus::exception_t
+    {
+        std::exception_ptr eptr;
+        try
+        {
+            // Throw InvalidEnumString; exception_t is a pure virtual base class
+            throw sdbusplus::exception::InvalidEnumString{};
+        }
+        catch (...)
+        {
+            eptr = std::current_exception();
+        }
+
+        // Create MockServices.  Expect logDBusError() to be called once.
+        MockServices services{};
+        MockErrorLogging& errorLogging = services.getMockErrorLogging();
+        MockJournal& journal = services.getMockJournal();
+        EXPECT_CALL(errorLogging,
+                    logDBusError(Entry::Level::Debug, Ref(journal)))
+            .Times(1);
+
+        // Log error based on the nested exception
+        ErrorHistory history{};
+        error_logging_utils::logError(eptr, Entry::Level::Debug, services,
+                                      history);
+
+        // Try to log error again.  Should not happen due to ErrorHistory.
+        error_logging_utils::logError(eptr, Entry::Level::Debug, services,
+                                      history);
+    }
+
+    // Test where exception is a std::exception
+    {
+        std::exception_ptr eptr;
+        try
+        {
+            throw std::runtime_error{
+                "Unable to read configuration file: No such file or directory"};
+        }
+        catch (...)
+        {
+            eptr = std::current_exception();
+        }
+
+        // Create MockServices.  Expect logInternalError() to be called once.
+        MockServices services{};
+        MockErrorLogging& errorLogging = services.getMockErrorLogging();
+        MockJournal& journal = services.getMockJournal();
+        EXPECT_CALL(errorLogging,
+                    logInternalError(Entry::Level::Error, Ref(journal)))
+            .Times(1);
+
+        // Log error based on the nested exception
+        ErrorHistory history{};
+        error_logging_utils::logError(eptr, Entry::Level::Error, services,
+                                      history);
+
+        // Try to log error again.  Should not happen due to ErrorHistory.
+        error_logging_utils::logError(eptr, Entry::Level::Error, services,
+                                      history);
+    }
+
+    // Test where exception is unknown type
+    {
+        std::exception_ptr eptr;
+        try
+        {
+            throw 23;
+        }
+        catch (...)
+        {
+            eptr = std::current_exception();
+        }
+
+        // Create MockServices.  Expect logInternalError() to be called once.
+        MockServices services{};
+        MockErrorLogging& errorLogging = services.getMockErrorLogging();
+        MockJournal& journal = services.getMockJournal();
+        EXPECT_CALL(errorLogging,
+                    logInternalError(Entry::Level::Warning, Ref(journal)))
+            .Times(1);
+
+        // Log error based on the nested exception
+        ErrorHistory history{};
+        error_logging_utils::logError(eptr, Entry::Level::Warning, services,
+                                      history);
+
+        // Try to log error again.  Should not happen due to ErrorHistory.
+        error_logging_utils::logError(eptr, Entry::Level::Warning, services,
+                                      history);
+    }
+}
+
+TEST(ErrorLoggingUtilsTests, GetExceptionToLog)
+{
+    // Test where exception is not nested
+    {
+        std::exception_ptr eptr;
+        try
+        {
+            throw i2c::I2CException{"Unable to open device reg1", "/dev/i2c-8",
+                                    0x30, ENODEV};
+        }
+        catch (...)
+        {
+            eptr = std::current_exception();
+        }
+
+        std::exception_ptr exceptionToLog =
+            error_logging_utils::internal::getExceptionToLog(eptr);
+        EXPECT_EQ(eptr, exceptionToLog);
+    }
+
+    // Test where exception is nested: Highest priority is innermost exception
+    {
+        std::exception_ptr inner, outer;
+        try
+        {
+            try
+            {
+                throw PMBusError{
+                    "VOUT_MODE contains unsupported data format", "reg1",
+                    "/xyz/openbmc_project/inventory/system/chassis/"
+                    "motherboard/reg1"};
+            }
+            catch (...)
+            {
+                inner = std::current_exception();
+                std::throw_with_nested(
+                    std::runtime_error{"Unable to set output voltage"});
+            }
+        }
+        catch (...)
+        {
+            outer = std::current_exception();
+        }
+
+        std::exception_ptr exceptionToLog =
+            error_logging_utils::internal::getExceptionToLog(outer);
+        EXPECT_EQ(inner, exceptionToLog);
+    }
+
+    // Test where exception is nested: Highest priority is middle exception
+    {
+        std::exception_ptr inner, middle, outer;
+        try
+        {
+            try
+            {
+                try
+                {
+                    throw std::invalid_argument{"JSON element is not an array"};
+                }
+                catch (...)
+                {
+                    inner = std::current_exception();
+                    std::throw_with_nested(ConfigFileParserError{
+                        fs::path{"/etc/phosphor-regulators/config.json"},
+                        "Unable to parse JSON configuration file"});
+                }
+            }
+            catch (...)
+            {
+                middle = std::current_exception();
+                std::throw_with_nested(
+                    std::runtime_error{"Unable to load config file"});
+            }
+        }
+        catch (...)
+        {
+            outer = std::current_exception();
+        }
+
+        std::exception_ptr exceptionToLog =
+            error_logging_utils::internal::getExceptionToLog(outer);
+        EXPECT_EQ(middle, exceptionToLog);
+    }
+
+    // Test where exception is nested: Highest priority is outermost exception
+    {
+        std::exception_ptr inner, outer;
+        try
+        {
+            try
+            {
+                throw std::invalid_argument{"JSON element is not an array"};
+            }
+            catch (...)
+            {
+                inner = std::current_exception();
+                std::throw_with_nested(ConfigFileParserError{
+                    fs::path{"/etc/phosphor-regulators/config.json"},
+                    "Unable to parse JSON configuration file"});
+            }
+        }
+        catch (...)
+        {
+            outer = std::current_exception();
+        }
+
+        std::exception_ptr exceptionToLog =
+            error_logging_utils::internal::getExceptionToLog(outer);
+        EXPECT_EQ(outer, exceptionToLog);
+    }
+
+    // Test where exception is nested: Two exceptions have same priority.
+    // Should return outermost exception with that priority.
+    {
+        std::exception_ptr inner, outer;
+        try
+        {
+            try
+            {
+                throw std::invalid_argument{"JSON element is not an array"};
+            }
+            catch (...)
+            {
+                inner = std::current_exception();
+                std::throw_with_nested(
+                    std::runtime_error{"Unable to load config file"});
+            }
+        }
+        catch (...)
+        {
+            outer = std::current_exception();
+        }
+
+        std::exception_ptr exceptionToLog =
+            error_logging_utils::internal::getExceptionToLog(outer);
+        EXPECT_EQ(outer, exceptionToLog);
+    }
+
+    // Test where exception is nested: Highest priority is ConfigFileParserError
+    {
+        std::exception_ptr inner, outer;
+        try
+        {
+            try
+            {
+                throw ConfigFileParserError{
+                    fs::path{"/etc/phosphor-regulators/config.json"},
+                    "Unable to parse JSON configuration file"};
+            }
+            catch (...)
+            {
+                inner = std::current_exception();
+                std::throw_with_nested(
+                    std::runtime_error{"Unable to load config file"});
+            }
+        }
+        catch (...)
+        {
+            outer = std::current_exception();
+        }
+
+        std::exception_ptr exceptionToLog =
+            error_logging_utils::internal::getExceptionToLog(outer);
+        EXPECT_EQ(inner, exceptionToLog);
+    }
+
+    // Test where exception is nested: Highest priority is PMBusError
+    {
+        std::exception_ptr inner, outer;
+        try
+        {
+            try
+            {
+                throw std::invalid_argument{"Invalid VOUT_MODE value"};
+            }
+            catch (...)
+            {
+                inner = std::current_exception();
+                std::throw_with_nested(PMBusError{
+                    "VOUT_MODE contains unsupported data format", "reg1",
+                    "/xyz/openbmc_project/inventory/system/chassis/motherboard/"
+                    "reg1"});
+            }
+        }
+        catch (...)
+        {
+            outer = std::current_exception();
+        }
+
+        std::exception_ptr exceptionToLog =
+            error_logging_utils::internal::getExceptionToLog(outer);
+        EXPECT_EQ(outer, exceptionToLog);
+    }
+
+    // Test where exception is nested: Highest priority is
+    // WriteVerificationError
+    {
+        std::exception_ptr inner, outer;
+        try
+        {
+            try
+            {
+                throw WriteVerificationError{
+                    "value_written: 0xDEAD, value_read: 0xBEEF", "reg1",
+                    "/xyz/openbmc_project/inventory/system/chassis/motherboard/"
+                    "reg1"};
+            }
+            catch (...)
+            {
+                inner = std::current_exception();
+                std::throw_with_nested(
+                    std::runtime_error{"Unable set voltage"});
+            }
+        }
+        catch (...)
+        {
+            outer = std::current_exception();
+        }
+
+        std::exception_ptr exceptionToLog =
+            error_logging_utils::internal::getExceptionToLog(outer);
+        EXPECT_EQ(inner, exceptionToLog);
+    }
+
+    // Test where exception is nested: Highest priority is I2CException
+    {
+        std::exception_ptr inner, outer;
+        try
+        {
+            try
+            {
+                throw std::invalid_argument{"No such device"};
+            }
+            catch (...)
+            {
+                inner = std::current_exception();
+                std::throw_with_nested(i2c::I2CException{
+                    "Unable to open device reg1", "/dev/i2c-8", 0x30, ENODEV});
+            }
+        }
+        catch (...)
+        {
+            outer = std::current_exception();
+        }
+
+        std::exception_ptr exceptionToLog =
+            error_logging_utils::internal::getExceptionToLog(outer);
+        EXPECT_EQ(outer, exceptionToLog);
+    }
+
+    // Test where exception is nested: Highest priority is
+    // sdbusplus::exception_t
+    {
+        std::exception_ptr inner, outer;
+        try
+        {
+            try
+            {
+                // Throw InvalidEnumString; exception_t is pure virtual class
+                throw sdbusplus::exception::InvalidEnumString{};
+            }
+            catch (...)
+            {
+                inner = std::current_exception();
+                std::throw_with_nested(
+                    std::runtime_error{"Unable to call D-Bus method"});
+            }
+        }
+        catch (...)
+        {
+            outer = std::current_exception();
+        }
+
+        std::exception_ptr exceptionToLog =
+            error_logging_utils::internal::getExceptionToLog(outer);
+        EXPECT_EQ(inner, exceptionToLog);
+    }
+
+    // Test where exception is nested: Highest priority is std::exception
+    {
+        std::exception_ptr inner, outer;
+        try
+        {
+            try
+            {
+                throw std::invalid_argument{"No such file or directory"};
+            }
+            catch (...)
+            {
+                inner = std::current_exception();
+                std::throw_with_nested(
+                    std::runtime_error{"Unable load config file"});
+            }
+        }
+        catch (...)
+        {
+            outer = std::current_exception();
+        }
+
+        std::exception_ptr exceptionToLog =
+            error_logging_utils::internal::getExceptionToLog(outer);
+        EXPECT_EQ(outer, exceptionToLog);
+    }
+
+    // Test where exception is nested: Highest priority is unknown type
+    {
+        std::exception_ptr inner, outer;
+        try
+        {
+            try
+            {
+                throw 23;
+            }
+            catch (...)
+            {
+                inner = std::current_exception();
+                std::throw_with_nested(std::string{"Unable load config file"});
+            }
+        }
+        catch (...)
+        {
+            outer = std::current_exception();
+        }
+
+        std::exception_ptr exceptionToLog =
+            error_logging_utils::internal::getExceptionToLog(outer);
+        EXPECT_EQ(outer, exceptionToLog);
+    }
+}