pseq: Parsing support for power_sequencer element

Add JSON parsing support for the new power_sequencer element in the
phosphor-power-sequencer configuration file.

Tested:
* Ran automated tests.

Change-Id: I4c9f27af06c9c349ecc4f60ba753f1a6e636f786
Signed-off-by: Shawn McCarney <shawnmm@us.ibm.com>
diff --git a/phosphor-power-sequencer/src/config_file_parser.cpp b/phosphor-power-sequencer/src/config_file_parser.cpp
index 0812744..4539eb7 100644
--- a/phosphor-power-sequencer/src/config_file_parser.cpp
+++ b/phosphor-power-sequencer/src/config_file_parser.cpp
@@ -18,6 +18,8 @@
 
 #include "config_file_parser_error.hpp"
 #include "json_parser_utils.hpp"
+#include "ucd90160_device.hpp"
+#include "ucd90320_device.hpp"
 
 #include <cstdint>
 #include <exception>
@@ -142,6 +144,82 @@
     return {bus, address};
 }
 
+std::unique_ptr<PowerSequencerDevice> parsePowerSequencer(
+    const nlohmann::json& element,
+    const std::map<std::string, std::string>& variables, Services& services)
+{
+    verifyIsObject(element);
+    unsigned int propertyCount{0};
+
+    // Optional comments property; value not stored
+    if (element.contains("comments"))
+    {
+        ++propertyCount;
+    }
+
+    // Required type property
+    const json& typeElement = getRequiredProperty(element, "type");
+    std::string type = parseString(typeElement, false, variables);
+    ++propertyCount;
+
+    // Required i2c_interface property
+    const json& i2cInterfaceElement =
+        getRequiredProperty(element, "i2c_interface");
+    auto [bus, address] = parseI2CInterface(i2cInterfaceElement, variables);
+    ++propertyCount;
+
+    // Required power_control_gpio_name property
+    const json& powerControlGPIONameElement =
+        getRequiredProperty(element, "power_control_gpio_name");
+    std::string powerControlGPIOName =
+        parseString(powerControlGPIONameElement, false, variables);
+    ++propertyCount;
+
+    // Required power_good_gpio_name property
+    const json& powerGoodGPIONameElement =
+        getRequiredProperty(element, "power_good_gpio_name");
+    std::string powerGoodGPIOName =
+        parseString(powerGoodGPIONameElement, false, variables);
+    ++propertyCount;
+
+    // Required rails property
+    const json& railsElement = getRequiredProperty(element, "rails");
+    std::vector<std::unique_ptr<Rail>> rails =
+        parseRailArray(railsElement, variables);
+    ++propertyCount;
+
+    // Verify no invalid properties exist
+    verifyPropertyCount(element, propertyCount);
+
+    if (type == UCD90160Device::deviceName)
+    {
+        return std::make_unique<UCD90160Device>(
+            bus, address, powerControlGPIOName, powerGoodGPIOName,
+            std::move(rails), services);
+    }
+    else if (type == UCD90320Device::deviceName)
+    {
+        return std::make_unique<UCD90320Device>(
+            bus, address, powerControlGPIOName, powerGoodGPIOName,
+            std::move(rails), services);
+    }
+    throw std::invalid_argument{"Invalid power sequencer type: " + type};
+}
+
+std::vector<std::unique_ptr<PowerSequencerDevice>> parsePowerSequencerArray(
+    const nlohmann::json& element,
+    const std::map<std::string, std::string>& variables, Services& services)
+{
+    verifyIsArray(element);
+    std::vector<std::unique_ptr<PowerSequencerDevice>> powerSequencers;
+    for (auto& powerSequencerElement : element)
+    {
+        powerSequencers.emplace_back(
+            parsePowerSequencer(powerSequencerElement, variables, services));
+    }
+    return powerSequencers;
+}
+
 std::unique_ptr<Rail> parseRail(
     const json& element, const std::map<std::string, std::string>& variables)
 {
diff --git a/phosphor-power-sequencer/src/config_file_parser.hpp b/phosphor-power-sequencer/src/config_file_parser.hpp
index 0f5fe3c..fcae99a 100644
--- a/phosphor-power-sequencer/src/config_file_parser.hpp
+++ b/phosphor-power-sequencer/src/config_file_parser.hpp
@@ -15,7 +15,9 @@
  */
 #pragma once
 
+#include "power_sequencer_device.hpp"
 #include "rail.hpp"
+#include "services.hpp"
 
 #include <nlohmann/json.hpp>
 
@@ -108,6 +110,38 @@
     const std::map<std::string, std::string>& variables);
 
 /**
+ * Parses a JSON element containing a power_sequencer object.
+ *
+ * Returns the corresponding C++ PowerSequencerDevice object.
+ *
+ * Throws an exception if parsing fails.
+ *
+ * @param element JSON element
+ * @param variables variables map used to expand variables in element value
+ * @param services System services like hardware presence and the journal
+ * @return PowerSequencerDevice object
+ */
+std::unique_ptr<PowerSequencerDevice> parsePowerSequencer(
+    const nlohmann::json& element,
+    const std::map<std::string, std::string>& variables, Services& services);
+
+/**
+ * Parses a JSON element containing an array of power_sequencer objects.
+ *
+ * Returns the corresponding C++ PowerSequencerDevice objects.
+ *
+ * Throws an exception if parsing fails.
+ *
+ * @param element JSON element
+ * @param variables variables map used to expand variables in element value
+ * @param services System services like hardware presence and the journal
+ * @return vector of PowerSequencerDevice objects
+ */
+std::vector<std::unique_ptr<PowerSequencerDevice>> parsePowerSequencerArray(
+    const nlohmann::json& element,
+    const std::map<std::string, std::string>& variables, Services& services);
+
+/**
  * Parses a JSON element containing a rail.
  *
  * Returns the corresponding C++ Rail object.
diff --git a/phosphor-power-sequencer/test/config_file_parser_tests.cpp b/phosphor-power-sequencer/test/config_file_parser_tests.cpp
index c6101d3..c1e8426 100644
--- a/phosphor-power-sequencer/test/config_file_parser_tests.cpp
+++ b/phosphor-power-sequencer/test/config_file_parser_tests.cpp
@@ -15,6 +15,8 @@
  */
 #include "config_file_parser.hpp"
 #include "config_file_parser_error.hpp"
+#include "mock_services.hpp"
+#include "power_sequencer_device.hpp"
 #include "rail.hpp"
 #include "temporary_file.hpp"
 #include "temporary_subdirectory.hpp"
@@ -625,6 +627,520 @@
     }
 }
 
+TEST(ConfigFileParserTests, ParsePowerSequencer)
+{
+    // Test where works: Has comments property: Type is "UCD90160"
+    {
+        const json element = R"(
+            {
+              "comments": [ "Power sequencer in chassis 1",
+                            "Controls VDD rails" ],
+              "type": "UCD90160",
+              "i2c_interface": { "bus": 3, "address": "0x11" },
+              "power_control_gpio_name": "power-chassis-control",
+              "power_good_gpio_name": "power-chassis-good",
+              "rails": [ { "name": "VDD_CPU0" }, { "name": "VCS_CPU1" } ]
+            }
+        )"_json;
+        std::map<std::string, std::string> variables{};
+        MockServices services{};
+        auto powerSequencer = parsePowerSequencer(element, variables, services);
+        EXPECT_EQ(powerSequencer->getName(), "UCD90160");
+        EXPECT_EQ(powerSequencer->getBus(), 3);
+        EXPECT_EQ(powerSequencer->getAddress(), 0x11);
+        EXPECT_EQ(powerSequencer->getPowerControlGPIOName(),
+                  "power-chassis-control");
+        EXPECT_EQ(powerSequencer->getPowerGoodGPIOName(), "power-chassis-good");
+        EXPECT_EQ(powerSequencer->getRails().size(), 2);
+        EXPECT_EQ(powerSequencer->getRails()[0]->getName(), "VDD_CPU0");
+        EXPECT_EQ(powerSequencer->getRails()[1]->getName(), "VCS_CPU1");
+    }
+
+    // Test where works: No comments property: Variables specified: Type is
+    // "UCD90320"
+    {
+        const json element = R"(
+            {
+              "type": "${type}",
+              "i2c_interface": { "bus": "${bus}", "address": "${address}" },
+              "power_control_gpio_name": "${power_control_gpio_name}",
+              "power_good_gpio_name": "${power_good_gpio_name}",
+              "rails": [ { "name": "${rail1}" }, { "name": "${rail2}" } ]
+            }
+        )"_json;
+        std::map<std::string, std::string> variables{
+            {"type", "UCD90320"},
+            {"bus", "4"},
+            {"address", "0x24"},
+            {"power_control_gpio_name", "power_on"},
+            {"power_good_gpio_name", "pgood"},
+            {"rail1", "cpu1"},
+            {"rail2", "cpu2"}};
+        MockServices services{};
+        auto powerSequencer = parsePowerSequencer(element, variables, services);
+        EXPECT_EQ(powerSequencer->getName(), "UCD90320");
+        EXPECT_EQ(powerSequencer->getBus(), 4);
+        EXPECT_EQ(powerSequencer->getAddress(), 0x24);
+        EXPECT_EQ(powerSequencer->getPowerControlGPIOName(), "power_on");
+        EXPECT_EQ(powerSequencer->getPowerGoodGPIOName(), "pgood");
+        EXPECT_EQ(powerSequencer->getRails().size(), 2);
+        EXPECT_EQ(powerSequencer->getRails()[0]->getName(), "cpu1");
+        EXPECT_EQ(powerSequencer->getRails()[1]->getName(), "cpu2");
+    }
+
+    // Test where fails: Element is not an object
+    try
+    {
+        const json element = R"( [ "vdda", "vddb" ] )"_json;
+        std::map<std::string, std::string> variables{};
+        MockServices services{};
+        parsePowerSequencer(element, variables, services);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not an object");
+    }
+
+    // Test where fails: Required type property not specified
+    try
+    {
+        const json element = R"(
+            {
+              "i2c_interface": { "bus": 3, "address": "0x11" },
+              "power_control_gpio_name": "power-chassis-control",
+              "power_good_gpio_name": "power-chassis-good",
+              "rails": []
+            }
+        )"_json;
+        std::map<std::string, std::string> variables{};
+        MockServices services{};
+        parsePowerSequencer(element, variables, services);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Required property missing: type");
+    }
+
+    // Test where fails: Required i2c_interface property not specified
+    try
+    {
+        const json element = R"(
+            {
+              "type": "UCD90320",
+              "power_control_gpio_name": "power-chassis-control",
+              "power_good_gpio_name": "power-chassis-good",
+              "rails": []
+            }
+        )"_json;
+        std::map<std::string, std::string> variables{};
+        MockServices services{};
+        parsePowerSequencer(element, variables, services);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Required property missing: i2c_interface");
+    }
+
+    // Test where fails: Required power_control_gpio_name property not specified
+    try
+    {
+        const json element = R"(
+            {
+              "type": "UCD90320",
+              "i2c_interface": { "bus": 3, "address": "0x11" },
+              "power_good_gpio_name": "power-chassis-good",
+              "rails": []
+            }
+        )"_json;
+        std::map<std::string, std::string> variables{};
+        MockServices services{};
+        parsePowerSequencer(element, variables, services);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(),
+                     "Required property missing: power_control_gpio_name");
+    }
+
+    // Test where fails: Required power_good_gpio_name property not specified
+    try
+    {
+        const json element = R"(
+            {
+              "type": "UCD90320",
+              "i2c_interface": { "bus": 3, "address": "0x11" },
+              "power_control_gpio_name": "power-chassis-control",
+              "rails": []
+            }
+        )"_json;
+        std::map<std::string, std::string> variables{};
+        MockServices services{};
+        parsePowerSequencer(element, variables, services);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(),
+                     "Required property missing: power_good_gpio_name");
+    }
+
+    // Test where fails: Required rails property not specified
+    try
+    {
+        const json element = R"(
+            {
+              "type": "UCD90320",
+              "i2c_interface": { "bus": 3, "address": "0x11" },
+              "power_control_gpio_name": "power-chassis-control",
+              "power_good_gpio_name": "power-chassis-good"
+            }
+        )"_json;
+        std::map<std::string, std::string> variables{};
+        MockServices services{};
+        parsePowerSequencer(element, variables, services);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Required property missing: rails");
+    }
+
+    // Test where fails: type value is invalid: Not a string
+    try
+    {
+        const json element = R"(
+            {
+              "type": true,
+              "i2c_interface": { "bus": 3, "address": "0x11" },
+              "power_control_gpio_name": "power-chassis-control",
+              "power_good_gpio_name": "power-chassis-good",
+              "rails": []
+            }
+        )"_json;
+        std::map<std::string, std::string> variables{};
+        MockServices services{};
+        parsePowerSequencer(element, variables, services);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not a string");
+    }
+
+    // Test where fails: type value is invalid: Not a supported type
+    try
+    {
+        const json element = R"(
+            {
+              "type": "foo_bar",
+              "i2c_interface": { "bus": 3, "address": "0x11" },
+              "power_control_gpio_name": "power-chassis-control",
+              "power_good_gpio_name": "power-chassis-good",
+              "rails": []
+            }
+        )"_json;
+        std::map<std::string, std::string> variables{};
+        MockServices services{};
+        parsePowerSequencer(element, variables, services);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Invalid power sequencer type: foo_bar");
+    }
+
+    // Test where fails: i2c_interface value is invalid
+    try
+    {
+        const json element = R"(
+            {
+              "type": "UCD90320",
+              "i2c_interface": 3,
+              "power_control_gpio_name": "power-chassis-control",
+              "power_good_gpio_name": "power-chassis-good",
+              "rails": []
+            }
+        )"_json;
+        std::map<std::string, std::string> variables{};
+        MockServices services{};
+        parsePowerSequencer(element, variables, services);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not an object");
+    }
+
+    // Test where fails: power_control_gpio_name value is invalid
+    try
+    {
+        const json element = R"(
+            {
+              "type": "UCD90320",
+              "i2c_interface": { "bus": 3, "address": "0x11" },
+              "power_control_gpio_name": [],
+              "power_good_gpio_name": "power-chassis-good",
+              "rails": []
+            }
+        )"_json;
+        std::map<std::string, std::string> variables{};
+        MockServices services{};
+        parsePowerSequencer(element, variables, services);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not a string");
+    }
+
+    // Test where fails: power_good_gpio_name value is invalid
+    try
+    {
+        const json element = R"(
+            {
+              "type": "UCD90320",
+              "i2c_interface": { "bus": 3, "address": "0x11" },
+              "power_control_gpio_name": "power-chassis-control",
+              "power_good_gpio_name": 12,
+              "rails": []
+            }
+        )"_json;
+        std::map<std::string, std::string> variables{};
+        MockServices services{};
+        parsePowerSequencer(element, variables, services);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not a string");
+    }
+
+    // Test where fails: rails value is invalid
+    try
+    {
+        const json element = R"(
+            {
+              "type": "UCD90320",
+              "i2c_interface": { "bus": 3, "address": "0x11" },
+              "power_control_gpio_name": "power-chassis-control",
+              "power_good_gpio_name": "power-chassis-good",
+              "rails": [ { "name": 33 } ]
+            }
+        )"_json;
+        std::map<std::string, std::string> variables{};
+        MockServices services{};
+        parsePowerSequencer(element, variables, services);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not a string");
+    }
+
+    // Test where fails: Invalid property specified
+    try
+    {
+        const json element = R"(
+            {
+              "type": "UCD90320",
+              "i2c_interface": { "bus": 3, "address": "0x11" },
+              "power_control_gpio_name": "power-chassis-control",
+              "power_good_gpio_name": "power-chassis-good",
+              "driver_name": "foo",
+              "rails": []
+            }
+        )"_json;
+        std::map<std::string, std::string> variables{};
+        MockServices services{};
+        parsePowerSequencer(element, variables, services);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element contains an invalid property");
+    }
+
+    // Test where fails: Invalid variable value specified
+    try
+    {
+        const json element = R"(
+            {
+              "type": "UCD90320",
+              "i2c_interface": { "bus": "${bus}", "address": "0x11" },
+              "power_control_gpio_name": "power-chassis-control",
+              "power_good_gpio_name": "power-chassis-good",
+              "rails": []
+            }
+        )"_json;
+        std::map<std::string, std::string> variables{{"bus", "two"}};
+        MockServices services{};
+        parsePowerSequencer(element, variables, services);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not an integer");
+    }
+}
+
+TEST(ConfigFileParserTests, ParsePowerSequencerArray)
+{
+    // Test where works: Array is empty
+    {
+        const json element = R"(
+            [
+            ]
+        )"_json;
+        std::map<std::string, std::string> variables{};
+        MockServices services{};
+        auto powerSequencers =
+            parsePowerSequencerArray(element, variables, services);
+        EXPECT_EQ(powerSequencers.size(), 0);
+    }
+
+    // Test where works: Array is not empty
+    {
+        const json element = R"(
+            [
+              {
+                "type": "UCD90160",
+                "i2c_interface": { "bus": 3, "address": "0x11" },
+                "power_control_gpio_name": "power-chassis-control1",
+                "power_good_gpio_name": "power-chassis-good1",
+                "rails": []
+              },
+              {
+                "type": "UCD90320",
+                "i2c_interface": { "bus": 4, "address": "0x70" },
+                "power_control_gpio_name": "power-chassis-control2",
+                "power_good_gpio_name": "power-chassis-good2",
+                "rails": []
+              }
+            ]
+        )"_json;
+        std::map<std::string, std::string> variables{};
+        MockServices services{};
+        auto powerSequencers =
+            parsePowerSequencerArray(element, variables, services);
+        EXPECT_EQ(powerSequencers.size(), 2);
+        EXPECT_EQ(powerSequencers[0]->getName(), "UCD90160");
+        EXPECT_EQ(powerSequencers[0]->getBus(), 3);
+        EXPECT_EQ(powerSequencers[0]->getAddress(), 0x11);
+        EXPECT_EQ(powerSequencers[1]->getName(), "UCD90320");
+        EXPECT_EQ(powerSequencers[1]->getBus(), 4);
+        EXPECT_EQ(powerSequencers[1]->getAddress(), 0x70);
+    }
+
+    // Test where works: Variables specified
+    {
+        const json element = R"(
+            [
+              {
+                "type": "UCD90160",
+                "i2c_interface": { "bus": "${bus1}", "address": "${address1}" },
+                "power_control_gpio_name": "power-chassis-control1",
+                "power_good_gpio_name": "power-chassis-good1",
+                "rails": []
+              },
+              {
+                "type": "UCD90320",
+                "i2c_interface": { "bus": "${bus2}", "address": "${address2}" },
+                "power_control_gpio_name": "power-chassis-control2",
+                "power_good_gpio_name": "power-chassis-good2",
+                "rails": []
+              }
+            ]
+        )"_json;
+        std::map<std::string, std::string> variables{
+            {"bus1", "5"},
+            {"address1", "0x22"},
+            {"bus2", "7"},
+            {"address2", "0x49"}};
+        MockServices services{};
+        auto powerSequencers =
+            parsePowerSequencerArray(element, variables, services);
+        EXPECT_EQ(powerSequencers.size(), 2);
+        EXPECT_EQ(powerSequencers[0]->getName(), "UCD90160");
+        EXPECT_EQ(powerSequencers[0]->getBus(), 5);
+        EXPECT_EQ(powerSequencers[0]->getAddress(), 0x22);
+        EXPECT_EQ(powerSequencers[1]->getName(), "UCD90320");
+        EXPECT_EQ(powerSequencers[1]->getBus(), 7);
+        EXPECT_EQ(powerSequencers[1]->getAddress(), 0x49);
+    }
+
+    // Test where fails: Element is not an array
+    try
+    {
+        const json element = R"(
+            {
+                "foo": "bar"
+            }
+        )"_json;
+        std::map<std::string, std::string> variables{};
+        MockServices services{};
+        parsePowerSequencerArray(element, variables, services);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not an array");
+    }
+
+    // Test where fails: Element within array is invalid
+    try
+    {
+        const json element = R"(
+            [
+              {
+                "type": "UCD90160",
+                "i2c_interface": { "bus": 3, "address": "0x11" },
+                "power_control_gpio_name": "power-chassis-control1",
+                "power_good_gpio_name": "power-chassis-good1",
+                "rails": []
+              },
+              true
+            ]
+        )"_json;
+        std::map<std::string, std::string> variables{};
+        MockServices services{};
+        parsePowerSequencerArray(element, variables, services);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not an object");
+    }
+
+    // Test where fails: Invalid variable value specified
+    try
+    {
+        const json element = R"(
+            [
+              {
+                "type": "UCD90320",
+                "i2c_interface": { "bus": "${bus}", "address": "${address}" },
+                "power_control_gpio_name": "power-chassis-control",
+                "power_good_gpio_name": "power-chassis-good",
+                "rails": []
+              }
+            ]
+        )"_json;
+        std::map<std::string, std::string> variables{{"bus", "7"},
+                                                     {"address", "70"}};
+        MockServices services{};
+        parsePowerSequencerArray(element, variables, services);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not hexadecimal string");
+    }
+}
+
 TEST(ConfigFileParserTests, ParseRail)
 {
     // Test where works: Only required properties specified