pseq: Standard pgood fault detection algorithm

Create a StandardDevice class for the phosphor-power-sequencer
application.  This class implements the standard pgood (power good)
fault detection algorithm.

When adding support for a new power sequencer device type, a sub-class
of StandardDevice should be created if possible.  This will ensure that
pgood fault detection works consistently across device types.

Tested:
* Verified all new and existing gtests ran successfully

Change-Id: I80c0b36429f1d84fa1e317889803429927797fdc
Signed-off-by: Shawn McCarney <shawnmm@us.ibm.com>
diff --git a/phosphor-power-sequencer/src/meson.build b/phosphor-power-sequencer/src/meson.build
index db8112f..2d14caf 100644
--- a/phosphor-power-sequencer/src/meson.build
+++ b/phosphor-power-sequencer/src/meson.build
@@ -8,6 +8,7 @@
     'config_file_parser.cpp',
     'rail.cpp',
     'services.cpp',
+    'standard_device.cpp',
     implicit_include_directories: false,
     dependencies: [
         nlohmann_json_dep,
diff --git a/phosphor-power-sequencer/src/power_sequencer_device.hpp b/phosphor-power-sequencer/src/power_sequencer_device.hpp
index f6ebb48..7a8c1b3 100644
--- a/phosphor-power-sequencer/src/power_sequencer_device.hpp
+++ b/phosphor-power-sequencer/src/power_sequencer_device.hpp
@@ -129,9 +129,13 @@
     virtual double getVoutUVFaultLimit(uint8_t page) = 0;
 
     /**
-     * Returns whether a pgood fault has occurred on one of the rails being
+     * Checks whether a pgood fault has occurred on one of the rails being
      * monitored by this device.
      *
+     * If a pgood fault was found, this method returns a string containing the
+     * error that should be logged.  If no fault was found, an empty string is
+     * returned.
+     *
      * Throws an exception if an error occurs while trying to obtain the status
      * of the rails.
      *
@@ -141,16 +145,14 @@
      *                         supply error occurred.  This error may be the
      *                         root cause if a pgood fault occurred on a power
      *                         supply rail monitored by this device.
-     * @param error Error that should be logged if this method returns true.
      * @param additionalData Additional data to include in the error log if
-     *                       this method returns true.
-     * @return true if a pgood fault was found on a rail monitored by this
-     *         device, false otherwise
+     *                       a pgood fault was found
+     * @return error that should be logged if a pgood fault was found, or an
+     *         empty string if no pgood fault was found
      */
-    virtual bool
-        hasPgoodFault(Services& services, const std::string& powerSupplyError,
-                      std::string& error,
-                      std::map<std::string, std::string>& additionalData) = 0;
+    virtual std::string
+        findPgoodFault(Services& services, const std::string& powerSupplyError,
+                       std::map<std::string, std::string>& additionalData) = 0;
 };
 
 } // namespace phosphor::power::sequencer
diff --git a/phosphor-power-sequencer/src/standard_device.cpp b/phosphor-power-sequencer/src/standard_device.cpp
new file mode 100644
index 0000000..cc999c5
--- /dev/null
+++ b/phosphor-power-sequencer/src/standard_device.cpp
@@ -0,0 +1,105 @@
+/**
+ * Copyright © 2024 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 "standard_device.hpp"
+
+#include "format_utils.hpp"
+
+#include <exception>
+#include <format>
+#include <span>
+#include <stdexcept>
+
+namespace phosphor::power::sequencer
+{
+
+std::string StandardDevice::findPgoodFault(
+    Services& services, const std::string& powerSupplyError,
+    std::map<std::string, std::string>& additionalData)
+{
+    std::string error{};
+    try
+    {
+        prepareForPgoodFaultDetection(services);
+
+        // Get all GPIO values (if possible) from device.  They may be slow to
+        // obtain, so obtain them once and then pass values to each Rail object.
+        std::vector<int> gpioValues = getGPIOValuesIfPossible();
+
+        // Loop through all the rails checking if any detected a pgood fault.
+        // The rails are in power-on-sequence order.
+        for (std::unique_ptr<Rail>& rail : rails)
+        {
+            if (rail->hasPgoodFault(*this, services, gpioValues,
+                                    additionalData))
+            {
+                services.logErrorMsg(std::format(
+                    "Pgood fault found in rail monitored by device {}", name));
+
+                // If this is a PSU rail and a PSU error was previously detected
+                if (rail->isPowerSupplyRail() && !powerSupplyError.empty())
+                {
+                    // Return power supply error as root cause
+                    error = powerSupplyError;
+                }
+                else
+                {
+                    // Return pgood fault as root cause
+                    error =
+                        "xyz.openbmc_project.Power.Error.PowerSequencerVoltageFault";
+                }
+
+                storePgoodFaultDebugData(services, gpioValues, additionalData);
+                break;
+            }
+        }
+    }
+    catch (const std::exception& e)
+    {
+        throw std::runtime_error{std::format(
+            "Unable to determine if a pgood fault occurred in device {}: {}",
+            name, e.what())};
+    }
+    return error;
+}
+
+std::vector<int> StandardDevice::getGPIOValuesIfPossible()
+{
+    std::vector<int> values{};
+    try
+    {
+        values = getGPIOValues();
+    }
+    catch (...)
+    {}
+    return values;
+}
+
+void StandardDevice::storePgoodFaultDebugData(
+    Services& services, const std::vector<int>& gpioValues,
+    std::map<std::string, std::string>& additionalData)
+{
+    additionalData.emplace("DEVICE_NAME", name);
+    if (!gpioValues.empty())
+    {
+        std::string valuesStr = format_utils::toString(std::span(gpioValues));
+        services.logInfoMsg(
+            std::format("Device {} GPIO values: {}", name, valuesStr));
+        additionalData.emplace("GPIO_VALUES", valuesStr);
+    }
+}
+
+} // namespace phosphor::power::sequencer
diff --git a/phosphor-power-sequencer/src/standard_device.hpp b/phosphor-power-sequencer/src/standard_device.hpp
new file mode 100644
index 0000000..3dbba2f
--- /dev/null
+++ b/phosphor-power-sequencer/src/standard_device.hpp
@@ -0,0 +1,143 @@
+/**
+ * Copyright © 2024 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 "power_sequencer_device.hpp"
+#include "rail.hpp"
+#include "services.hpp"
+
+#include <map>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+namespace phosphor::power::sequencer
+{
+
+/**
+ * @class StandardDevice
+ *
+ * PowerSequencerDevice sub-class that implements the standard pgood fault
+ * detection algorithm.
+ *
+ * When adding support for a new power sequencer device type, create a sub-class
+ * of StandardDevice if possible.  This will ensure that pgood fault detection
+ * works consistently across device types.
+ */
+class StandardDevice : public PowerSequencerDevice
+{
+  public:
+    // Specify which compiler-generated methods we want
+    StandardDevice() = delete;
+    StandardDevice(const StandardDevice&) = delete;
+    StandardDevice(StandardDevice&&) = delete;
+    StandardDevice& operator=(const StandardDevice&) = delete;
+    StandardDevice& operator=(StandardDevice&&) = delete;
+    virtual ~StandardDevice() = default;
+
+    /**
+     * Constructor.
+     *
+     * @param name device name
+     * @param rails voltage rails that are enabled and monitored by this device
+     */
+    explicit StandardDevice(const std::string& name,
+                            std::vector<std::unique_ptr<Rail>> rails) :
+        name{name},
+        rails{std::move(rails)}
+    {}
+
+    /** @copydoc PowerSequencerDevice::getName() */
+    virtual const std::string& getName() const override
+    {
+        return name;
+    }
+
+    /** @copydoc PowerSequencerDevice::getRails() */
+    virtual const std::vector<std::unique_ptr<Rail>>& getRails() const override
+    {
+        return rails;
+    }
+
+    /** @copydoc PowerSequencerDevice::findPgoodFault()
+     *
+     * Calls prepareForPgoodFaultDetection() before starting detection.  If a
+     * pgood fault is detected, calls storePgoodFaultDebugData().
+     */
+    virtual std::string findPgoodFault(
+        Services& services, const std::string& powerSupplyError,
+        std::map<std::string, std::string>& additionalData) override;
+
+  protected:
+    /**
+     * Prepare for pgood fault detection.
+     *
+     * Perform any actions that are necessary to prepare for fault detection.
+     * For example, cache information that is slow to obtain and is used
+     * multiple times during detection.
+     *
+     * Default implementation does nothing.  Override in sub-classes if needed.
+     *
+     * @param services System services like hardware presence and the journal
+     */
+    virtual void
+        prepareForPgoodFaultDetection([[maybe_unused]] Services& services)
+    {}
+
+    /**
+     * Returns the GPIO values that can be read from the device, if possible.
+     *
+     * If the device does not support reading GPIO values or an error occurs, an
+     * empty vector is returned.
+     *
+     * @return GPIO values, or empty vector if values could not be read
+     */
+    virtual std::vector<int> getGPIOValuesIfPossible();
+
+    /**
+     * Store pgood fault debug data in the specified additional data map.
+     *
+     * The default implementation stores the device name and all the GPIO
+     * values.  The GPIO values are stored as a simple list of integers.
+     *
+     * Sub-classes should override if needed to store device-specific data
+     * and/or a formatted representation of the GPIO values.
+     *
+     * This method should NOT throw exceptions.  If debug data cannot be
+     * obtained, the error should be caught and ignored so that pgood error
+     * handling can continue.
+     *
+     * @param services System services like hardware presence and the journal
+     * @param gpioValues GPIO values obtained from the device (if any)
+     * @param additionalData Additional data to include in an error log
+     */
+    virtual void storePgoodFaultDebugData(
+        Services& services, const std::vector<int>& gpioValues,
+        std::map<std::string, std::string>& additionalData);
+
+    /**
+     * Device name.
+     */
+    std::string name{};
+
+    /**
+     * Voltage rails that are enabled and monitored by this device.
+     */
+    std::vector<std::unique_ptr<Rail>> rails{};
+};
+
+} // namespace phosphor::power::sequencer
diff --git a/phosphor-power-sequencer/test/meson.build b/phosphor-power-sequencer/test/meson.build
index db90ffd..21d1529 100644
--- a/phosphor-power-sequencer/test/meson.build
+++ b/phosphor-power-sequencer/test/meson.build
@@ -4,6 +4,7 @@
                 'config_file_parser_tests.cpp',
                 'format_utils_tests.cpp',
                 'rail_tests.cpp',
+                'standard_device_tests.cpp',
                 dependencies: [
                     gmock,
                     gtest,
diff --git a/phosphor-power-sequencer/test/mock_device.hpp b/phosphor-power-sequencer/test/mock_device.hpp
index 01b60ff..431e379 100644
--- a/phosphor-power-sequencer/test/mock_device.hpp
+++ b/phosphor-power-sequencer/test/mock_device.hpp
@@ -46,9 +46,8 @@
     MOCK_METHOD(uint8_t, getStatusVout, (uint8_t page), (override));
     MOCK_METHOD(double, getReadVout, (uint8_t page), (override));
     MOCK_METHOD(double, getVoutUVFaultLimit, (uint8_t page), (override));
-    MOCK_METHOD(bool, hasPgoodFault,
+    MOCK_METHOD(std::string, findPgoodFault,
                 (Services & services, const std::string& powerSupplyError,
-                 std::string& error,
                  (std::map<std::string, std::string> & additionalData)),
                 (override));
 };
diff --git a/phosphor-power-sequencer/test/standard_device_tests.cpp b/phosphor-power-sequencer/test/standard_device_tests.cpp
new file mode 100644
index 0000000..977ad1a
--- /dev/null
+++ b/phosphor-power-sequencer/test/standard_device_tests.cpp
@@ -0,0 +1,475 @@
+/**
+ * Copyright © 2024 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 "mock_services.hpp"
+#include "rail.hpp"
+#include "standard_device.hpp"
+
+#include <cstdint>
+#include <map>
+#include <memory>
+#include <optional>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+using namespace phosphor::power::sequencer;
+
+using ::testing::Return;
+using ::testing::Throw;
+
+/**
+ * @class StandardDeviceImpl
+ *
+ * Concrete subclass of the StandardDevice abstract class.
+ *
+ * This subclass is required for two reasons:
+ * - StandardDevice has some pure virtual methods so it cannot be instantiated.
+ * - The pure virtual methods provide the PMBus and GPIO information.  Mocking
+ *   these makes it possible to test the pgood fault detection algorithm.
+ *
+ * This class is not intended to be used outside of this file.  It is
+ * implementation detail for testing the the StandardDevice class.
+ */
+class StandardDeviceImpl : public StandardDevice
+{
+  public:
+    // Specify which compiler-generated methods we want
+    StandardDeviceImpl() = delete;
+    StandardDeviceImpl(const StandardDeviceImpl&) = delete;
+    StandardDeviceImpl(StandardDeviceImpl&&) = delete;
+    StandardDeviceImpl& operator=(const StandardDeviceImpl&) = delete;
+    StandardDeviceImpl& operator=(StandardDeviceImpl&&) = delete;
+    virtual ~StandardDeviceImpl() = default;
+
+    // Constructor just calls StandardDevice constructor
+    explicit StandardDeviceImpl(const std::string& name,
+                                std::vector<std::unique_ptr<Rail>> rails) :
+        StandardDevice(name, std::move(rails))
+    {}
+
+    // Mock pure virtual methods
+    MOCK_METHOD(std::vector<int>, getGPIOValues, (), (override));
+    MOCK_METHOD(uint16_t, getStatusWord, (uint8_t page), (override));
+    MOCK_METHOD(uint8_t, getStatusVout, (uint8_t page), (override));
+    MOCK_METHOD(double, getReadVout, (uint8_t page), (override));
+    MOCK_METHOD(double, getVoutUVFaultLimit, (uint8_t page), (override));
+};
+
+/**
+ * Creates a Rail object that checks for a pgood fault using STATUS_VOUT.
+ *
+ * @param name Unique name for the rail
+ * @param isPowerSupplyRail Specifies whether the rail is produced by a
+                            power supply
+ * @param pageNum PMBus PAGE number of the rail
+ * @return Rail object
+ */
+std::unique_ptr<Rail> createRailStatusVout(const std::string& name,
+                                           bool isPowerSupplyRail,
+                                           uint8_t pageNum)
+{
+    std::optional<std::string> presence{};
+    std::optional<uint8_t> page{pageNum};
+    bool checkStatusVout{true};
+    bool compareVoltageToLimit{false};
+    std::optional<GPIO> gpio{};
+    return std::make_unique<Rail>(name, presence, page, isPowerSupplyRail,
+                                  checkStatusVout, compareVoltageToLimit, gpio);
+}
+
+/**
+ * Creates a Rail object that checks for a pgood fault using a GPIO.
+ *
+ * @param name Unique name for the rail
+ * @param isPowerSupplyRail Specifies whether the rail is produced by a
+                            power supply
+ * @param gpio GPIO line to read to determine the pgood status of the rail
+ * @return Rail object
+ */
+std::unique_ptr<Rail> createRailGPIO(const std::string& name,
+                                     bool isPowerSupplyRail,
+                                     unsigned int gpioLine)
+{
+    std::optional<std::string> presence{};
+    std::optional<uint8_t> page{};
+    bool checkStatusVout{false};
+    bool compareVoltageToLimit{false};
+    bool activeLow{false};
+    std::optional<GPIO> gpio{GPIO{gpioLine, activeLow}};
+    return std::make_unique<Rail>(name, presence, page, isPowerSupplyRail,
+                                  checkStatusVout, compareVoltageToLimit, gpio);
+}
+
+TEST(StandardDeviceTests, Constructor)
+{
+    // Empty vector of rails
+    {
+        std::vector<std::unique_ptr<Rail>> rails{};
+        StandardDeviceImpl device{"xyz_pseq", std::move(rails)};
+
+        EXPECT_EQ(device.getName(), "xyz_pseq");
+        EXPECT_TRUE(device.getRails().empty());
+    }
+
+    // Non-empty vector of rails
+    {
+        std::vector<std::unique_ptr<Rail>> rails{};
+        rails.emplace_back(createRailGPIO("PSU", true, 3));
+        rails.emplace_back(createRailStatusVout("VDD", false, 5));
+        rails.emplace_back(createRailStatusVout("VIO", false, 7));
+        StandardDeviceImpl device{"abc_pseq", std::move(rails)};
+
+        EXPECT_EQ(device.getName(), "abc_pseq");
+        EXPECT_EQ(device.getRails().size(), 3);
+        EXPECT_EQ(device.getRails()[0]->getName(), "PSU");
+        EXPECT_EQ(device.getRails()[1]->getName(), "VDD");
+        EXPECT_EQ(device.getRails()[2]->getName(), "VIO");
+    }
+}
+
+TEST(StandardDeviceTests, GetName)
+{
+    std::vector<std::unique_ptr<Rail>> rails{};
+    StandardDeviceImpl device{"xyz_pseq", std::move(rails)};
+
+    EXPECT_EQ(device.getName(), "xyz_pseq");
+}
+
+TEST(StandardDeviceTests, GetRails)
+{
+    // Empty vector of rails
+    {
+        std::vector<std::unique_ptr<Rail>> rails{};
+        StandardDeviceImpl device{"xyz_pseq", std::move(rails)};
+
+        EXPECT_TRUE(device.getRails().empty());
+    }
+
+    // Non-empty vector of rails
+    {
+        std::vector<std::unique_ptr<Rail>> rails{};
+        rails.emplace_back(createRailGPIO("PSU", true, 3));
+        rails.emplace_back(createRailStatusVout("VDD", false, 5));
+        rails.emplace_back(createRailStatusVout("VIO", false, 7));
+        StandardDeviceImpl device{"abc_pseq", std::move(rails)};
+
+        EXPECT_EQ(device.getRails().size(), 3);
+        EXPECT_EQ(device.getRails()[0]->getName(), "PSU");
+        EXPECT_EQ(device.getRails()[1]->getName(), "VDD");
+        EXPECT_EQ(device.getRails()[2]->getName(), "VIO");
+    }
+}
+
+TEST(StandardDeviceTests, FindPgoodFault)
+{
+    // No rail has a pgood fault
+    {
+        std::vector<std::unique_ptr<Rail>> rails{};
+        rails.emplace_back(createRailGPIO("PSU", true, 2));
+        rails.emplace_back(createRailStatusVout("VDD", false, 5));
+        rails.emplace_back(createRailStatusVout("VIO", false, 7));
+        StandardDeviceImpl device{"abc_pseq", std::move(rails)};
+
+        std::vector<int> gpioValues{1, 1, 1};
+        EXPECT_CALL(device, getGPIOValues)
+            .Times(1)
+            .WillOnce(Return(gpioValues));
+        EXPECT_CALL(device, getStatusVout(5)).Times(1).WillOnce(Return(0x00));
+        EXPECT_CALL(device, getStatusVout(7)).Times(1).WillOnce(Return(0x00));
+
+        MockServices services{};
+
+        std::string powerSupplyError{};
+        std::map<std::string, std::string> additionalData{};
+        std::string error = device.findPgoodFault(services, powerSupplyError,
+                                                  additionalData);
+        EXPECT_TRUE(error.empty());
+        EXPECT_EQ(additionalData.size(), 0);
+    }
+
+    // First rail has a pgood fault
+    // Is a PSU rail: No PSU error specified
+    {
+        std::vector<std::unique_ptr<Rail>> rails{};
+        rails.emplace_back(createRailGPIO("PSU", true, 2));
+        rails.emplace_back(createRailStatusVout("VDD", false, 5));
+        rails.emplace_back(createRailStatusVout("VIO", false, 7));
+        StandardDeviceImpl device{"abc_pseq", std::move(rails)};
+
+        std::vector<int> gpioValues{1, 1, 0};
+        EXPECT_CALL(device, getGPIOValues)
+            .Times(1)
+            .WillOnce(Return(gpioValues));
+        EXPECT_CALL(device, getStatusVout).Times(0);
+
+        MockServices services{};
+        EXPECT_CALL(services,
+                    logInfoMsg("Device abc_pseq GPIO values: [1, 1, 0]"))
+            .Times(1);
+        EXPECT_CALL(
+            services,
+            logErrorMsg(
+                "Pgood fault found in rail monitored by device abc_pseq"))
+            .Times(1);
+        EXPECT_CALL(services, logErrorMsg("Pgood fault detected in rail PSU"))
+            .Times(1);
+        EXPECT_CALL(
+            services,
+            logErrorMsg(
+                "Rail PSU pgood GPIO line offset 2 has inactive value 0"))
+            .Times(1);
+
+        std::string powerSupplyError{};
+        std::map<std::string, std::string> additionalData{};
+        std::string error = device.findPgoodFault(services, powerSupplyError,
+                                                  additionalData);
+        EXPECT_EQ(error,
+                  "xyz.openbmc_project.Power.Error.PowerSequencerVoltageFault");
+        EXPECT_EQ(additionalData.size(), 5);
+        EXPECT_EQ(additionalData["DEVICE_NAME"], "abc_pseq");
+        EXPECT_EQ(additionalData["GPIO_VALUES"], "[1, 1, 0]");
+        EXPECT_EQ(additionalData["RAIL_NAME"], "PSU");
+        EXPECT_EQ(additionalData["GPIO_LINE"], "2");
+        EXPECT_EQ(additionalData["GPIO_VALUE"], "0");
+    }
+
+    // First rail has a pgood fault
+    // Is a PSU rail: PSU error specified
+    {
+        std::vector<std::unique_ptr<Rail>> rails{};
+        rails.emplace_back(createRailGPIO("PSU", true, 2));
+        rails.emplace_back(createRailStatusVout("VDD", false, 5));
+        rails.emplace_back(createRailStatusVout("VIO", false, 7));
+        StandardDeviceImpl device{"abc_pseq", std::move(rails)};
+
+        std::vector<int> gpioValues{1, 1, 0};
+        EXPECT_CALL(device, getGPIOValues)
+            .Times(1)
+            .WillOnce(Return(gpioValues));
+        EXPECT_CALL(device, getStatusVout).Times(0);
+
+        MockServices services{};
+        EXPECT_CALL(services,
+                    logInfoMsg("Device abc_pseq GPIO values: [1, 1, 0]"))
+            .Times(1);
+        EXPECT_CALL(
+            services,
+            logErrorMsg(
+                "Pgood fault found in rail monitored by device abc_pseq"))
+            .Times(1);
+        EXPECT_CALL(services, logErrorMsg("Pgood fault detected in rail PSU"))
+            .Times(1);
+        EXPECT_CALL(
+            services,
+            logErrorMsg(
+                "Rail PSU pgood GPIO line offset 2 has inactive value 0"))
+            .Times(1);
+
+        std::string powerSupplyError{"Undervoltage fault: PSU1"};
+        std::map<std::string, std::string> additionalData{};
+        std::string error = device.findPgoodFault(services, powerSupplyError,
+                                                  additionalData);
+        EXPECT_EQ(error, powerSupplyError);
+        EXPECT_EQ(additionalData.size(), 5);
+        EXPECT_EQ(additionalData["DEVICE_NAME"], "abc_pseq");
+        EXPECT_EQ(additionalData["GPIO_VALUES"], "[1, 1, 0]");
+        EXPECT_EQ(additionalData["RAIL_NAME"], "PSU");
+        EXPECT_EQ(additionalData["GPIO_LINE"], "2");
+        EXPECT_EQ(additionalData["GPIO_VALUE"], "0");
+    }
+
+    // Middle rail has a pgood fault
+    // Not a PSU rail: PSU error specified
+    {
+        std::vector<std::unique_ptr<Rail>> rails{};
+        rails.emplace_back(createRailGPIO("PSU", true, 2));
+        rails.emplace_back(createRailStatusVout("VDD", false, 5));
+        rails.emplace_back(createRailStatusVout("VIO", false, 7));
+        StandardDeviceImpl device{"abc_pseq", std::move(rails)};
+
+        std::vector<int> gpioValues{1, 1, 1};
+        EXPECT_CALL(device, getGPIOValues)
+            .Times(1)
+            .WillOnce(Return(gpioValues));
+        EXPECT_CALL(device, getStatusVout(5)).Times(1).WillOnce(Return(0x10));
+        EXPECT_CALL(device, getStatusVout(7)).Times(0);
+        EXPECT_CALL(device, getStatusWord(5)).Times(1).WillOnce(Return(0xbeef));
+
+        MockServices services{};
+        EXPECT_CALL(services,
+                    logInfoMsg("Device abc_pseq GPIO values: [1, 1, 1]"))
+            .Times(1);
+        EXPECT_CALL(
+            services,
+            logErrorMsg(
+                "Pgood fault found in rail monitored by device abc_pseq"))
+            .Times(1);
+        EXPECT_CALL(services, logInfoMsg("Rail VDD STATUS_WORD: 0xbeef"))
+            .Times(1);
+        EXPECT_CALL(services, logErrorMsg("Pgood fault detected in rail VDD"))
+            .Times(1);
+        EXPECT_CALL(
+            services,
+            logErrorMsg("Rail VDD has fault bits set in STATUS_VOUT: 0x10"))
+            .Times(1);
+
+        std::string powerSupplyError{"Undervoltage fault: PSU1"};
+        std::map<std::string, std::string> additionalData{};
+        std::string error = device.findPgoodFault(services, powerSupplyError,
+                                                  additionalData);
+        EXPECT_EQ(error,
+                  "xyz.openbmc_project.Power.Error.PowerSequencerVoltageFault");
+        EXPECT_EQ(additionalData.size(), 5);
+        EXPECT_EQ(additionalData["DEVICE_NAME"], "abc_pseq");
+        EXPECT_EQ(additionalData["GPIO_VALUES"], "[1, 1, 1]");
+        EXPECT_EQ(additionalData["RAIL_NAME"], "VDD");
+        EXPECT_EQ(additionalData["STATUS_VOUT"], "0x10");
+        EXPECT_EQ(additionalData["STATUS_WORD"], "0xbeef");
+    }
+
+    // Last rail has a pgood fault
+    // Device returns 0 GPIO values
+    // Does not halt pgood fault detection because GPIO values not used by rails
+    {
+        std::vector<std::unique_ptr<Rail>> rails{};
+        rails.emplace_back(createRailStatusVout("PSU", true, 3));
+        rails.emplace_back(createRailStatusVout("VDD", false, 5));
+        rails.emplace_back(createRailStatusVout("VIO", false, 7));
+        StandardDeviceImpl device{"abc_pseq", std::move(rails)};
+
+        std::vector<int> gpioValues{};
+        EXPECT_CALL(device, getGPIOValues)
+            .Times(1)
+            .WillOnce(Return(gpioValues));
+        EXPECT_CALL(device, getStatusVout(3)).Times(1).WillOnce(Return(0x00));
+        EXPECT_CALL(device, getStatusVout(5)).Times(1).WillOnce(Return(0x00));
+        EXPECT_CALL(device, getStatusVout(7)).Times(1).WillOnce(Return(0x11));
+        EXPECT_CALL(device, getStatusWord(7)).Times(1).WillOnce(Return(0xbeef));
+
+        MockServices services{};
+        EXPECT_CALL(
+            services,
+            logErrorMsg(
+                "Pgood fault found in rail monitored by device abc_pseq"))
+            .Times(1);
+        EXPECT_CALL(services, logInfoMsg("Rail VIO STATUS_WORD: 0xbeef"))
+            .Times(1);
+        EXPECT_CALL(services, logErrorMsg("Pgood fault detected in rail VIO"))
+            .Times(1);
+        EXPECT_CALL(
+            services,
+            logErrorMsg("Rail VIO has fault bits set in STATUS_VOUT: 0x11"))
+            .Times(1);
+
+        std::string powerSupplyError{};
+        std::map<std::string, std::string> additionalData{};
+        std::string error = device.findPgoodFault(services, powerSupplyError,
+                                                  additionalData);
+        EXPECT_EQ(error,
+                  "xyz.openbmc_project.Power.Error.PowerSequencerVoltageFault");
+        EXPECT_EQ(additionalData.size(), 4);
+        EXPECT_EQ(additionalData["DEVICE_NAME"], "abc_pseq");
+        EXPECT_EQ(additionalData["RAIL_NAME"], "VIO");
+        EXPECT_EQ(additionalData["STATUS_VOUT"], "0x11");
+        EXPECT_EQ(additionalData["STATUS_WORD"], "0xbeef");
+    }
+
+    // Last rail has a pgood fault
+    // Exception occurs trying to obtain GPIO values from device
+    // Does not halt pgood fault detection because GPIO values not used by rails
+    {
+        std::vector<std::unique_ptr<Rail>> rails{};
+        rails.emplace_back(createRailStatusVout("PSU", true, 3));
+        rails.emplace_back(createRailStatusVout("VDD", false, 5));
+        rails.emplace_back(createRailStatusVout("VIO", false, 7));
+        StandardDeviceImpl device{"abc_pseq", std::move(rails)};
+
+        EXPECT_CALL(device, getGPIOValues)
+            .Times(1)
+            .WillOnce(Throw(std::runtime_error{"Unable to acquire GPIO line"}));
+        EXPECT_CALL(device, getStatusVout(3)).Times(1).WillOnce(Return(0x00));
+        EXPECT_CALL(device, getStatusVout(5)).Times(1).WillOnce(Return(0x00));
+        EXPECT_CALL(device, getStatusVout(7)).Times(1).WillOnce(Return(0x11));
+        EXPECT_CALL(device, getStatusWord(7)).Times(1).WillOnce(Return(0xbeef));
+
+        MockServices services{};
+        EXPECT_CALL(
+            services,
+            logErrorMsg(
+                "Pgood fault found in rail monitored by device abc_pseq"))
+            .Times(1);
+        EXPECT_CALL(services, logInfoMsg("Rail VIO STATUS_WORD: 0xbeef"))
+            .Times(1);
+        EXPECT_CALL(services, logErrorMsg("Pgood fault detected in rail VIO"))
+            .Times(1);
+        EXPECT_CALL(
+            services,
+            logErrorMsg("Rail VIO has fault bits set in STATUS_VOUT: 0x11"))
+            .Times(1);
+
+        std::string powerSupplyError{};
+        std::map<std::string, std::string> additionalData{};
+        std::string error = device.findPgoodFault(services, powerSupplyError,
+                                                  additionalData);
+        EXPECT_EQ(error,
+                  "xyz.openbmc_project.Power.Error.PowerSequencerVoltageFault");
+        EXPECT_EQ(additionalData.size(), 4);
+        EXPECT_EQ(additionalData["DEVICE_NAME"], "abc_pseq");
+        EXPECT_EQ(additionalData["RAIL_NAME"], "VIO");
+        EXPECT_EQ(additionalData["STATUS_VOUT"], "0x11");
+        EXPECT_EQ(additionalData["STATUS_WORD"], "0xbeef");
+    }
+
+    // Exception is thrown during pgood fault detection
+    {
+        std::vector<std::unique_ptr<Rail>> rails{};
+        rails.emplace_back(createRailGPIO("PSU", true, 2));
+        rails.emplace_back(createRailStatusVout("VDD", false, 5));
+        rails.emplace_back(createRailStatusVout("VIO", false, 7));
+        StandardDeviceImpl device{"abc_pseq", std::move(rails)};
+
+        std::vector<int> gpioValues{1, 1, 1};
+        EXPECT_CALL(device, getGPIOValues)
+            .Times(1)
+            .WillOnce(Return(gpioValues));
+        EXPECT_CALL(device, getStatusVout(5))
+            .Times(1)
+            .WillOnce(Throw(std::runtime_error{"File does not exist"}));
+        EXPECT_CALL(device, getStatusVout(7)).Times(0);
+
+        MockServices services{};
+
+        std::string powerSupplyError{};
+        std::map<std::string, std::string> additionalData{};
+        try
+        {
+            device.findPgoodFault(services, powerSupplyError, additionalData);
+            ADD_FAILURE() << "Should not have reached this line.";
+        }
+        catch (const std::exception& e)
+        {
+            EXPECT_STREQ(
+                e.what(),
+                "Unable to determine if a pgood fault occurred in device abc_pseq: "
+                "Unable to read STATUS_VOUT value for rail VDD: "
+                "File does not exist");
+        }
+    }
+}