pseq: Support "GPIOs only" power sequencer device

Add support for a "GPIOs only" power sequencer device to the
phosphor-power-sequencer application.

If this device type is specified in the JSON configuration file, then
the application will only use the named GPIOs to power the device on/off
and read the power good signal. No attempt will be made to communicate
with the device otherwise. No pgood fault isolation will be performed.

This device type is useful for simple systems that do not require pgood
fault isolation. It is also useful as a temporary solution when
performing early bring-up work on a new system.

Tested:
* Ran automated tests

Change-Id: Ib5ba9a9c136dd5f5e853372f881f9e227f01a6bd
Signed-off-by: Shawn McCarney <shawnmm@us.ibm.com>
diff --git a/phosphor-power-sequencer/test/config_file_parser_tests.cpp b/phosphor-power-sequencer/test/config_file_parser_tests.cpp
index 78899bb..08b6218 100644
--- a/phosphor-power-sequencer/test/config_file_parser_tests.cpp
+++ b/phosphor-power-sequencer/test/config_file_parser_tests.cpp
@@ -1655,6 +1655,27 @@
         EXPECT_EQ(powerSequencer->getRails()[1]->getName(), "cpu2");
     }
 
+    // Test where works: Type is "gpios_only_device"
+    {
+        const json element = R"(
+            {
+              "type": "gpios_only_device",
+              "power_control_gpio_name": "power-chassis-control",
+              "power_good_gpio_name": "power-chassis-good"
+            }
+        )"_json;
+        std::map<std::string, std::string> variables{};
+        MockServices services{};
+        auto powerSequencer = parsePowerSequencer(element, variables, services);
+        EXPECT_EQ(powerSequencer->getName(), "gpios_only_device");
+        EXPECT_EQ(powerSequencer->getBus(), 0);
+        EXPECT_EQ(powerSequencer->getAddress(), 0);
+        EXPECT_EQ(powerSequencer->getPowerControlGPIOName(),
+                  "power-chassis-control");
+        EXPECT_EQ(powerSequencer->getPowerGoodGPIOName(), "power-chassis-good");
+        EXPECT_EQ(powerSequencer->getRails().size(), 0);
+    }
+
     // Test where fails: Element is not an object
     try
     {
diff --git a/phosphor-power-sequencer/test/gpios_only_device_tests.cpp b/phosphor-power-sequencer/test/gpios_only_device_tests.cpp
new file mode 100644
index 0000000..8bb2df1
--- /dev/null
+++ b/phosphor-power-sequencer/test/gpios_only_device_tests.cpp
@@ -0,0 +1,196 @@
+/**
+ * Copyright © 2025 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 "gpios_only_device.hpp"
+#include "mock_services.hpp"
+#include "rail.hpp"
+#include "services.hpp"
+
+#include <cstdint>
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+using namespace phosphor::power::sequencer;
+
+TEST(GPIOsOnlyDeviceTests, Constructor)
+{
+    std::string powerControlGPIOName{"power-chassis-control"};
+    std::string powerGoodGPIOName{"power-chassis-good"};
+    GPIOsOnlyDevice device{powerControlGPIOName, powerGoodGPIOName};
+
+    EXPECT_EQ(device.getPowerControlGPIOName(), powerControlGPIOName);
+    EXPECT_EQ(device.getPowerGoodGPIOName(), powerGoodGPIOName);
+}
+
+TEST(GPIOsOnlyDeviceTests, GetName)
+{
+    std::string powerControlGPIOName{"power-chassis-control"};
+    std::string powerGoodGPIOName{"power-chassis-good"};
+    GPIOsOnlyDevice device{powerControlGPIOName, powerGoodGPIOName};
+
+    EXPECT_EQ(device.getName(), GPIOsOnlyDevice::deviceName);
+}
+
+TEST(GPIOsOnlyDeviceTests, GetBus)
+{
+    std::string powerControlGPIOName{"power-chassis-control"};
+    std::string powerGoodGPIOName{"power-chassis-good"};
+    GPIOsOnlyDevice device{powerControlGPIOName, powerGoodGPIOName};
+
+    EXPECT_EQ(device.getBus(), 0);
+}
+
+TEST(GPIOsOnlyDeviceTests, GetAddress)
+{
+    std::string powerControlGPIOName{"power-chassis-control"};
+    std::string powerGoodGPIOName{"power-chassis-good"};
+    GPIOsOnlyDevice device{powerControlGPIOName, powerGoodGPIOName};
+
+    EXPECT_EQ(device.getAddress(), 0);
+}
+
+TEST(GPIOsOnlyDeviceTests, GetPowerControlGPIOName)
+{
+    std::string powerControlGPIOName{"power-on"};
+    std::string powerGoodGPIOName{"chassis-pgood"};
+    GPIOsOnlyDevice device{powerControlGPIOName, powerGoodGPIOName};
+
+    EXPECT_EQ(device.getPowerControlGPIOName(), powerControlGPIOName);
+}
+
+TEST(GPIOsOnlyDeviceTests, GetPowerGoodGPIOName)
+{
+    std::string powerControlGPIOName{"power-on"};
+    std::string powerGoodGPIOName{"chassis-pgood"};
+    GPIOsOnlyDevice device{powerControlGPIOName, powerGoodGPIOName};
+
+    EXPECT_EQ(device.getPowerGoodGPIOName(), powerGoodGPIOName);
+}
+
+TEST(GPIOsOnlyDeviceTests, GetRails)
+{
+    std::string powerControlGPIOName{"power-on"};
+    std::string powerGoodGPIOName{"chassis-pgood"};
+    GPIOsOnlyDevice device{powerControlGPIOName, powerGoodGPIOName};
+
+    EXPECT_TRUE(device.getRails().empty());
+}
+
+TEST(GPIOsOnlyDeviceTests, GetGPIOValues)
+{
+    try
+    {
+        std::string powerControlGPIOName{"power-on"};
+        std::string powerGoodGPIOName{"chassis-pgood"};
+        GPIOsOnlyDevice device{powerControlGPIOName, powerGoodGPIOName};
+
+        MockServices services{};
+        device.getGPIOValues(services);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::exception& e)
+    {
+        EXPECT_STREQ(e.what(), "getGPIOValues() is not supported");
+    }
+}
+
+TEST(GPIOsOnlyDeviceTests, GetStatusWord)
+{
+    try
+    {
+        std::string powerControlGPIOName{"power-on"};
+        std::string powerGoodGPIOName{"chassis-pgood"};
+        GPIOsOnlyDevice device{powerControlGPIOName, powerGoodGPIOName};
+
+        device.getStatusWord(0);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::exception& e)
+    {
+        EXPECT_STREQ(e.what(), "getStatusWord() is not supported");
+    }
+}
+
+TEST(GPIOsOnlyDeviceTests, GetStatusVout)
+{
+    try
+    {
+        std::string powerControlGPIOName{"power-on"};
+        std::string powerGoodGPIOName{"chassis-pgood"};
+        GPIOsOnlyDevice device{powerControlGPIOName, powerGoodGPIOName};
+
+        device.getStatusVout(0);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::exception& e)
+    {
+        EXPECT_STREQ(e.what(), "getStatusVout() is not supported");
+    }
+}
+
+TEST(GPIOsOnlyDeviceTests, GetReadVout)
+{
+    try
+    {
+        std::string powerControlGPIOName{"power-on"};
+        std::string powerGoodGPIOName{"chassis-pgood"};
+        GPIOsOnlyDevice device{powerControlGPIOName, powerGoodGPIOName};
+
+        device.getReadVout(0);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::exception& e)
+    {
+        EXPECT_STREQ(e.what(), "getReadVout() is not supported");
+    }
+}
+
+TEST(GPIOsOnlyDeviceTests, GetVoutUVFaultLimit)
+{
+    try
+    {
+        std::string powerControlGPIOName{"power-on"};
+        std::string powerGoodGPIOName{"chassis-pgood"};
+        GPIOsOnlyDevice device{powerControlGPIOName, powerGoodGPIOName};
+
+        device.getVoutUVFaultLimit(0);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::exception& e)
+    {
+        EXPECT_STREQ(e.what(), "getVoutUVFaultLimit() is not supported");
+    }
+}
+
+TEST(GPIOsOnlyDeviceTests, FindPgoodFault)
+{
+    std::string powerControlGPIOName{"power-on"};
+    std::string powerGoodGPIOName{"chassis-pgood"};
+    GPIOsOnlyDevice device{powerControlGPIOName, powerGoodGPIOName};
+
+    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);
+}
diff --git a/phosphor-power-sequencer/test/meson.build b/phosphor-power-sequencer/test/meson.build
index 2827c3f..5de4510 100644
--- a/phosphor-power-sequencer/test/meson.build
+++ b/phosphor-power-sequencer/test/meson.build
@@ -4,6 +4,7 @@
         'phosphor-power-sequencer-tests',
         'chassis_tests.cpp',
         'config_file_parser_tests.cpp',
+        'gpios_only_device_tests.cpp',
         'pmbus_driver_device_tests.cpp',
         'rail_tests.cpp',
         'standard_device_tests.cpp',