pseq: Parsing support for chassis element

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

Tested:
* Ran automated tests.

Change-Id: I563e3db7515108b4fb301e9208f6da22a4934227
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 6d9b169..18fe32e 100644
--- a/phosphor-power-sequencer/src/config_file_parser.cpp
+++ b/phosphor-power-sequencer/src/config_file_parser.cpp
@@ -94,6 +94,125 @@
 namespace internal
 {
 
+std::unique_ptr<Chassis> parseChassis(
+    const json& element, std::map<std::string, JSONRefWrapper> chassisTemplates,
+    Services& services)
+{
+    verifyIsObject(element);
+
+    // If chassis object is not using a template, parse properties normally
+    if (!element.contains("template_id"))
+    {
+        bool isChassisTemplate{false};
+        return parseChassisProperties(element, isChassisTemplate, NO_VARIABLES,
+                                      services);
+    }
+
+    // Parse chassis object that is using a template
+    unsigned int propertyCount{0};
+
+    // Optional comments property; value not stored
+    if (element.contains("comments"))
+    {
+        ++propertyCount;
+    }
+
+    // Required template_id property
+    const json& templateIDElement = getRequiredProperty(element, "template_id");
+    std::string templateID = parseString(templateIDElement);
+    ++propertyCount;
+
+    // Required template_variable_values property
+    const json& variablesElement =
+        getRequiredProperty(element, "template_variable_values");
+    std::map<std::string, std::string> variables =
+        parseVariables(variablesElement);
+    ++propertyCount;
+
+    // Verify no invalid properties exist
+    verifyPropertyCount(element, propertyCount);
+
+    // Get reference to chassis template JSON
+    auto it = chassisTemplates.find(templateID);
+    if (it == chassisTemplates.end())
+    {
+        throw std::invalid_argument{
+            "Invalid chassis template id: " + templateID};
+    }
+    const json& templateElement = it->second.get();
+
+    // Parse properties in template using variable values for this chassis
+    bool isChassisTemplate{true};
+    return parseChassisProperties(templateElement, isChassisTemplate, variables,
+                                  services);
+}
+
+std::vector<std::unique_ptr<Chassis>> parseChassisArray(
+    const json& element, std::map<std::string, JSONRefWrapper> chassisTemplates,
+    Services& services)
+{
+    verifyIsArray(element);
+    std::vector<std::unique_ptr<Chassis>> chassis;
+    for (auto& chassisElement : element)
+    {
+        chassis.emplace_back(
+            parseChassis(chassisElement, chassisTemplates, services));
+    }
+    return chassis;
+}
+
+std::unique_ptr<Chassis> parseChassisProperties(
+    const json& element, bool isChassisTemplate,
+    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 id property if this is a chassis template
+    // Don't parse again; this was already parsed by parseChassisTemplate()
+    if (isChassisTemplate)
+    {
+        getRequiredProperty(element, "id");
+        ++propertyCount;
+    }
+
+    // Required number property
+    const json& numberElement = getRequiredProperty(element, "number");
+    unsigned int number = parseUnsignedInteger(numberElement, variables);
+    if (number < 1)
+    {
+        throw std::invalid_argument{"Invalid chassis number: Must be > 0"};
+    }
+    ++propertyCount;
+
+    // Required inventory_path property
+    const json& inventoryPathElement =
+        getRequiredProperty(element, "inventory_path");
+    std::string inventoryPath =
+        parseString(inventoryPathElement, false, variables);
+    ++propertyCount;
+
+    // Required power_sequencers property
+    const json& powerSequencersElement =
+        getRequiredProperty(element, "power_sequencers");
+    std::vector<std::unique_ptr<PowerSequencerDevice>> powerSequencers =
+        parsePowerSequencerArray(powerSequencersElement, variables, services);
+    ++propertyCount;
+
+    // Verify no invalid properties exist
+    verifyPropertyCount(element, propertyCount);
+
+    return std::make_unique<Chassis>(number, inventoryPath,
+                                     std::move(powerSequencers));
+}
+
 std::tuple<std::string, JSONRefWrapper> parseChassisTemplate(
     const json& element)
 {
@@ -368,6 +487,21 @@
     return rails;
 }
 
+std::map<std::string, std::string> parseVariables(const json& element)
+{
+    verifyIsObject(element);
+
+    std::map<std::string, std::string> variables;
+    std::string name, value;
+    for (const auto& [nameElement, valueElement] : element.items())
+    {
+        name = parseString(nameElement);
+        value = parseString(valueElement);
+        variables.emplace(name, value);
+    }
+    return variables;
+}
+
 } // namespace internal
 
 } // namespace phosphor::power::sequencer::config_file_parser
diff --git a/phosphor-power-sequencer/src/config_file_parser.hpp b/phosphor-power-sequencer/src/config_file_parser.hpp
index b2a18ca..665029e 100644
--- a/phosphor-power-sequencer/src/config_file_parser.hpp
+++ b/phosphor-power-sequencer/src/config_file_parser.hpp
@@ -15,6 +15,7 @@
  */
 #pragma once
 
+#include "chassis.hpp"
 #include "power_sequencer_device.hpp"
 #include "rail.hpp"
 #include "services.hpp"
@@ -86,6 +87,57 @@
 using JSONRefWrapper = std::reference_wrapper<const json>;
 
 /**
+ * Parses a JSON element containing a chassis object.
+ *
+ * Returns the corresponding C++ Chassis object.
+ *
+ * Throws an exception if parsing fails.
+ *
+ * @param element JSON element
+ * @param chassisTemplates chassis templates map
+ * @param services system services like hardware presence and the journal
+ * @return Chassis object
+ */
+std::unique_ptr<Chassis> parseChassis(
+    const json& element, std::map<std::string, JSONRefWrapper> chassisTemplates,
+    Services& services);
+
+/**
+ * Parses a JSON element containing an array of chassis objects.
+ *
+ * Returns the corresponding C++ Chassis objects.
+ *
+ * Throws an exception if parsing fails.
+ *
+ * @param element JSON element
+ * @param chassisTemplates chassis templates map
+ * @param services system services like hardware presence and the journal
+ * @return vector of Chassis objects
+ */
+std::vector<std::unique_ptr<Chassis>> parseChassisArray(
+    const json& element, std::map<std::string, JSONRefWrapper> chassisTemplates,
+    Services& services);
+
+/**
+ * Parses a JSON element containing the properties of a chassis.
+ *
+ * The JSON element may be a chassis object or chassis_template object.
+ *
+ * Returns the corresponding C++ Chassis object.
+ *
+ * Throws an exception if parsing fails.
+ *
+ * @param element JSON element
+ * @param isChassisTemplate specifies whether element is a chassis_template
+ * @param variables variables map used to expand variables in element value
+ * @param services system services like hardware presence and the journal
+ * @return Chassis object
+ */
+std::unique_ptr<Chassis> parseChassisProperties(
+    const json& element, bool isChassisTemplate,
+    const std::map<std::string, std::string>& variables, Services& services);
+
+/**
  * Parses a JSON element containing a chassis_template object.
  *
  * Returns the template ID and a C++ reference_wrapper to the JSON element.
@@ -198,6 +250,18 @@
  */
 std::vector<std::unique_ptr<Rail>> parseRoot(const json& element);
 
+/**
+ * Parses a JSON element containing an object with variable names and values.
+ *
+ * Returns the corresponding C++ map of variable names and values.
+ *
+ * Throws an exception if parsing fails.
+ *
+ * @param element JSON element
+ * @return map of variable names and values
+ */
+std::map<std::string, std::string> parseVariables(const json& element);
+
 } // namespace internal
 
 } // namespace phosphor::power::sequencer::config_file_parser
diff --git a/phosphor-power-sequencer/test/config_file_parser_tests.cpp b/phosphor-power-sequencer/test/config_file_parser_tests.cpp
index a4a8040..3f0797c 100644
--- a/phosphor-power-sequencer/test/config_file_parser_tests.cpp
+++ b/phosphor-power-sequencer/test/config_file_parser_tests.cpp
@@ -13,6 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+#include "chassis.hpp"
 #include "config_file_parser.hpp"
 #include "config_file_parser_error.hpp"
 #include "mock_services.hpp"
@@ -326,6 +327,705 @@
     }
 }
 
+TEST(ConfigFileParserTests, ParseChassis)
+{
+    // Constants used by multiple tests
+    const json templateElement = R"(
+        {
+          "id": "foo_chassis",
+          "number": "${chassis_number}",
+          "inventory_path": "/xyz/openbmc_project/inventory/system/chassis${chassis_number}",
+          "power_sequencers": [
+            {
+              "type": "UCD90320",
+              "i2c_interface": { "bus": "${bus}", "address": "${address}" },
+              "power_control_gpio_name": "power-chassis${chassis_number}-control",
+              "power_good_gpio_name": "power-chassis${chassis_number}-good",
+              "rails": []
+            }
+          ]
+        }
+    )"_json;
+    const std::map<std::string, JSONRefWrapper> chassisTemplates{
+        {"foo_chassis", templateElement}};
+
+    // Test where works: Does not use a template
+    {
+        const json element = R"(
+            {
+              "number": 1,
+              "inventory_path": "/xyz/openbmc_project/inventory/system/chassis",
+              "power_sequencers": [
+                {
+                  "type": "UCD90320",
+                  "i2c_interface": { "bus": 3, "address": "0x11" },
+                  "power_control_gpio_name": "power-chassis-control",
+                  "power_good_gpio_name": "power-chassis-good",
+                  "rails": []
+                }
+              ]
+            }
+        )"_json;
+        MockServices services{};
+        auto chassis = parseChassis(element, chassisTemplates, services);
+        EXPECT_EQ(chassis->getNumber(), 1);
+        EXPECT_EQ(chassis->getInventoryPath(),
+                  "/xyz/openbmc_project/inventory/system/chassis");
+        EXPECT_EQ(chassis->getPowerSequencers().size(), 1);
+        EXPECT_EQ(chassis->getPowerSequencers()[0]->getName(), "UCD90320");
+        EXPECT_EQ(chassis->getPowerSequencers()[0]->getBus(), 3);
+        EXPECT_EQ(chassis->getPowerSequencers()[0]->getAddress(), 0x11);
+    }
+
+    // Test where works: Uses template: No comments specified
+    {
+        const json element = R"(
+            {
+              "template_id": "foo_chassis",
+              "template_variable_values": {
+                "chassis_number": "2",
+                "bus": "13",
+                "address": "0x70"
+              }
+            }
+        )"_json;
+        MockServices services{};
+        auto chassis = parseChassis(element, chassisTemplates, services);
+        EXPECT_EQ(chassis->getNumber(), 2);
+        EXPECT_EQ(chassis->getInventoryPath(),
+                  "/xyz/openbmc_project/inventory/system/chassis2");
+        EXPECT_EQ(chassis->getPowerSequencers().size(), 1);
+        EXPECT_EQ(chassis->getPowerSequencers()[0]->getName(), "UCD90320");
+        EXPECT_EQ(chassis->getPowerSequencers()[0]->getBus(), 13);
+        EXPECT_EQ(chassis->getPowerSequencers()[0]->getAddress(), 0x70);
+    }
+
+    // Test where works: Uses template: Comments specified
+    {
+        const json element = R"(
+            {
+              "comments": ["Chassis 3: Standard hardware layout"],
+              "template_id": "foo_chassis",
+              "template_variable_values": {
+                "chassis_number": "3",
+                "bus": "23",
+                "address": "0x54"
+              }
+            }
+        )"_json;
+        MockServices services{};
+        auto chassis = parseChassis(element, chassisTemplates, services);
+        EXPECT_EQ(chassis->getNumber(), 3);
+        EXPECT_EQ(chassis->getInventoryPath(),
+                  "/xyz/openbmc_project/inventory/system/chassis3");
+        EXPECT_EQ(chassis->getPowerSequencers().size(), 1);
+        EXPECT_EQ(chassis->getPowerSequencers()[0]->getName(), "UCD90320");
+        EXPECT_EQ(chassis->getPowerSequencers()[0]->getBus(), 23);
+        EXPECT_EQ(chassis->getPowerSequencers()[0]->getAddress(), 0x54);
+    }
+
+    // Test where fails: Element is not an object
+    try
+    {
+        const json element = R"( [ "vdda", "vddb" ] )"_json;
+        MockServices services{};
+        parseChassis(element, chassisTemplates, 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: Does not use a template: Cannot parse properties
+    try
+    {
+        const json element = R"(
+            {
+              "number": "one",
+              "inventory_path": "/xyz/openbmc_project/inventory/system/chassis",
+              "power_sequencers": []
+            }
+        )"_json;
+        MockServices services{};
+        parseChassis(element, chassisTemplates, 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 where fails: Uses template: Required template_variable_values
+    // property not specified
+    try
+    {
+        const json element = R"(
+            {
+              "template_id": "foo_chassis"
+            }
+        )"_json;
+        MockServices services{};
+        parseChassis(element, chassisTemplates, services);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(),
+                     "Required property missing: template_variable_values");
+    }
+
+    // Test where fails: Uses template: template_id value is invalid: Not a
+    // string
+    try
+    {
+        const json element = R"(
+            {
+              "template_id": 23,
+              "template_variable_values": { "chassis_number": "2" }
+            }
+        )"_json;
+        MockServices services{};
+        parseChassis(element, chassisTemplates, 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: Uses template: template_id value is invalid: No
+    // matching template
+    try
+    {
+        const json element = R"(
+            {
+              "template_id": "does_not_exist",
+              "template_variable_values": { "chassis_number": "2" }
+            }
+        )"_json;
+        MockServices services{};
+        parseChassis(element, chassisTemplates, services);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Invalid chassis template id: does_not_exist");
+    }
+
+    // Test where fails: Uses template: template_variable_values value is
+    // invalid
+    try
+    {
+        const json element = R"(
+            {
+              "template_id": "foo_chassis",
+              "template_variable_values": { "chassis_number": 2 }
+            }
+        )"_json;
+        MockServices services{};
+        parseChassis(element, chassisTemplates, 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: Uses template: Invalid property specified
+    try
+    {
+        const json element = R"(
+            {
+              "template_id": "foo_chassis",
+              "template_variable_values": { "chassis_number": "2" },
+              "foo": "bar"
+            }
+        )"_json;
+        MockServices services{};
+        parseChassis(element, chassisTemplates, 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: Uses template: Cannot parse properties in template
+    try
+    {
+        const json element = R"(
+            {
+              "template_id": "foo_chassis",
+              "template_variable_values": { "chassis_number": "0" }
+            }
+        )"_json;
+        MockServices services{};
+        parseChassis(element, chassisTemplates, services);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Invalid chassis number: Must be > 0");
+    }
+}
+
+TEST(ConfigFileParserTests, ParseChassisArray)
+{
+    // Constants used by multiple tests
+    const json fooTemplateElement = R"(
+        {
+          "id": "foo_chassis",
+          "number": "${chassis_number}",
+          "inventory_path": "/xyz/openbmc_project/inventory/system/chassis${chassis_number}",
+          "power_sequencers": []
+        }
+    )"_json;
+    const json barTemplateElement = R"(
+        {
+          "id": "bar_chassis",
+          "number": "${chassis_number}",
+          "inventory_path": "/xyz/openbmc_project/inventory/system/bar_chassis${chassis_number}",
+          "power_sequencers": []
+        }
+    )"_json;
+    const std::map<std::string, JSONRefWrapper> chassisTemplates{
+        {"foo_chassis", fooTemplateElement},
+        {"bar_chassis", barTemplateElement}};
+
+    // Test where works: Array is empty
+    {
+        const json element = R"(
+            [
+            ]
+        )"_json;
+        MockServices services{};
+        auto chassis = parseChassisArray(element, chassisTemplates, services);
+        EXPECT_EQ(chassis.size(), 0);
+    }
+
+    // Test where works: Template not used
+    {
+        const json element = R"(
+            [
+              {
+                "number": 1,
+                "inventory_path": "/xyz/openbmc_project/inventory/system/chassis1",
+                "power_sequencers": []
+              },
+              {
+                "number": 2,
+                "inventory_path": "/xyz/openbmc_project/inventory/system/chassis2",
+                "power_sequencers": []
+              }
+            ]
+        )"_json;
+        MockServices services{};
+        auto chassis = parseChassisArray(element, chassisTemplates, services);
+        EXPECT_EQ(chassis.size(), 2);
+        EXPECT_EQ(chassis[0]->getNumber(), 1);
+        EXPECT_EQ(chassis[0]->getInventoryPath(),
+                  "/xyz/openbmc_project/inventory/system/chassis1");
+        EXPECT_EQ(chassis[1]->getNumber(), 2);
+        EXPECT_EQ(chassis[1]->getInventoryPath(),
+                  "/xyz/openbmc_project/inventory/system/chassis2");
+    }
+
+    // Test where works: Template used
+    {
+        const json element = R"(
+            [
+              {
+                "template_id": "foo_chassis",
+                "template_variable_values": { "chassis_number": "2" }
+              },
+              {
+                "template_id": "bar_chassis",
+                "template_variable_values": { "chassis_number": "3" }
+              }
+            ]
+        )"_json;
+        MockServices services{};
+        auto chassis = parseChassisArray(element, chassisTemplates, services);
+        EXPECT_EQ(chassis.size(), 2);
+        EXPECT_EQ(chassis[0]->getNumber(), 2);
+        EXPECT_EQ(chassis[0]->getInventoryPath(),
+                  "/xyz/openbmc_project/inventory/system/chassis2");
+        EXPECT_EQ(chassis[1]->getNumber(), 3);
+        EXPECT_EQ(chassis[1]->getInventoryPath(),
+                  "/xyz/openbmc_project/inventory/system/bar_chassis3");
+    }
+
+    // Test where fails: Element is not an array
+    try
+    {
+        const json element = R"(
+            {
+                "foo": "bar"
+            }
+        )"_json;
+        MockServices services{};
+        parseChassisArray(element, chassisTemplates, 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"(
+            [
+              {
+                "number": true,
+                "inventory_path": "/xyz/openbmc_project/inventory/system/chassis",
+                "power_sequencers": []
+              }
+            ]
+        )"_json;
+        MockServices services{};
+        parseChassisArray(element, chassisTemplates, 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 where fails: Invalid variable value specified
+    try
+    {
+        const json element = R"(
+            [
+                {
+                  "template_id": "foo_chassis",
+                  "template_variable_values": { "chassis_number": "two" }
+                }
+            ]
+        )"_json;
+        MockServices services{};
+        parseChassisArray(element, chassisTemplates, 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, ParseChassisProperties)
+{
+    // Test where works: Parse chassis object without template/variables: Has
+    // comments property
+    {
+        const json element = R"(
+            {
+              "comments": [ "Chassis 1: Has all CPUs, fans, and PSUs" ],
+              "number": 1,
+              "inventory_path": "/xyz/openbmc_project/inventory/system/chassis",
+              "power_sequencers": [
+                {
+                  "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;
+        bool isChassisTemplate{false};
+        std::map<std::string, std::string> variables{};
+        MockServices services{};
+        auto chassis = parseChassisProperties(element, isChassisTemplate,
+                                              variables, services);
+        EXPECT_EQ(chassis->getNumber(), 1);
+        EXPECT_EQ(chassis->getInventoryPath(),
+                  "/xyz/openbmc_project/inventory/system/chassis");
+        EXPECT_EQ(chassis->getPowerSequencers().size(), 1);
+        EXPECT_EQ(chassis->getPowerSequencers()[0]->getName(), "UCD90160");
+        EXPECT_EQ(chassis->getPowerSequencers()[0]->getBus(), 3);
+        EXPECT_EQ(chassis->getPowerSequencers()[0]->getAddress(), 0x11);
+        EXPECT_EQ(chassis->getPowerSequencers()[0]->getPowerControlGPIOName(),
+                  "power-chassis-control");
+        EXPECT_EQ(chassis->getPowerSequencers()[0]->getPowerGoodGPIOName(),
+                  "power-chassis-good");
+        EXPECT_EQ(chassis->getPowerSequencers()[0]->getRails().size(), 2);
+        EXPECT_EQ(chassis->getPowerSequencers()[0]->getRails()[0]->getName(),
+                  "VDD_CPU0");
+        EXPECT_EQ(chassis->getPowerSequencers()[0]->getRails()[1]->getName(),
+                  "VCS_CPU1");
+    }
+
+    // Test where works: Parse chassis_template object with variables: No
+    // comments property
+    {
+        const json element = R"(
+            {
+              "id": "foo_chassis",
+              "number": "${chassis_number}",
+              "inventory_path": "/xyz/openbmc_project/inventory/system/chassis${chassis_number}",
+              "power_sequencers": [
+                {
+                  "type": "UCD90320",
+                  "i2c_interface": { "bus": "${bus}", "address": "${address}" },
+                  "power_control_gpio_name": "power-chassis${chassis_number}-control",
+                  "power_good_gpio_name": "power-chassis${chassis_number}-good",
+                  "rails": [ { "name": "vio${chassis_number}" } ]
+                }
+              ]
+            }
+        )"_json;
+        bool isChassisTemplate{true};
+        std::map<std::string, std::string> variables{
+            {"chassis_number", "2"}, {"bus", "12"}, {"address", "0x71"}};
+        MockServices services{};
+        auto chassis = parseChassisProperties(element, isChassisTemplate,
+                                              variables, services);
+        EXPECT_EQ(chassis->getNumber(), 2);
+        EXPECT_EQ(chassis->getInventoryPath(),
+                  "/xyz/openbmc_project/inventory/system/chassis2");
+        EXPECT_EQ(chassis->getPowerSequencers().size(), 1);
+        EXPECT_EQ(chassis->getPowerSequencers()[0]->getName(), "UCD90320");
+        EXPECT_EQ(chassis->getPowerSequencers()[0]->getBus(), 12);
+        EXPECT_EQ(chassis->getPowerSequencers()[0]->getAddress(), 0x71);
+        EXPECT_EQ(chassis->getPowerSequencers()[0]->getPowerControlGPIOName(),
+                  "power-chassis2-control");
+        EXPECT_EQ(chassis->getPowerSequencers()[0]->getPowerGoodGPIOName(),
+                  "power-chassis2-good");
+        EXPECT_EQ(chassis->getPowerSequencers()[0]->getRails().size(), 1);
+        EXPECT_EQ(chassis->getPowerSequencers()[0]->getRails()[0]->getName(),
+                  "vio2");
+    }
+
+    // Test where fails: Element is not an object
+    try
+    {
+        const json element = R"( true )"_json;
+        bool isChassisTemplate{false};
+        std::map<std::string, std::string> variables{};
+        MockServices services{};
+        parseChassisProperties(element, isChassisTemplate, 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 id property not specified in chassis template
+    try
+    {
+        const json element = R"(
+            {
+              "number": "${chassis_number}",
+              "inventory_path": "/xyz/openbmc_project/inventory/system/chassis${chassis_number}",
+              "power_sequencers": []
+            }
+        )"_json;
+        bool isChassisTemplate{true};
+        std::map<std::string, std::string> variables{{"chassis_number", "2"}};
+        MockServices services{};
+        parseChassisProperties(element, isChassisTemplate, variables, services);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Required property missing: id");
+    }
+
+    // Test where fails: Required number property not specified
+    try
+    {
+        const json element = R"(
+            {
+              "inventory_path": "/xyz/openbmc_project/inventory/system/chassis",
+              "power_sequencers": []
+            }
+        )"_json;
+        bool isChassisTemplate{false};
+        std::map<std::string, std::string> variables{};
+        MockServices services{};
+        parseChassisProperties(element, isChassisTemplate, variables, services);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Required property missing: number");
+    }
+
+    // Test where fails: Required inventory_path property not specified
+    try
+    {
+        const json element = R"(
+            {
+              "id": "foo_chassis",
+              "number": "${chassis_number}",
+              "power_sequencers": []
+            }
+        )"_json;
+        bool isChassisTemplate{true};
+        std::map<std::string, std::string> variables{{"chassis_number", "2"}};
+        MockServices services{};
+        parseChassisProperties(element, isChassisTemplate, variables, services);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Required property missing: inventory_path");
+    }
+
+    // Test where fails: Required power_sequencers property not specified
+    try
+    {
+        const json element = R"(
+            {
+              "number": 1,
+              "inventory_path": "/xyz/openbmc_project/inventory/system/chassis"
+            }
+        )"_json;
+        bool isChassisTemplate{false};
+        std::map<std::string, std::string> variables{};
+        MockServices services{};
+        parseChassisProperties(element, isChassisTemplate, variables, services);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Required property missing: power_sequencers");
+    }
+
+    // Test where fails: number value is invalid: Not an integer
+    try
+    {
+        const json element = R"(
+            {
+              "number": "two",
+              "inventory_path": "/xyz/openbmc_project/inventory/system/chassis",
+              "power_sequencers": []
+            }
+        )"_json;
+        bool isChassisTemplate{false};
+        std::map<std::string, std::string> variables{};
+        MockServices services{};
+        parseChassisProperties(element, isChassisTemplate, 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 where fails: number value is invalid: Equal to 0
+    try
+    {
+        const json element = R"(
+            {
+              "number": 0,
+              "inventory_path": "/xyz/openbmc_project/inventory/system/chassis",
+              "power_sequencers": []
+            }
+        )"_json;
+        bool isChassisTemplate{false};
+        std::map<std::string, std::string> variables{};
+        MockServices services{};
+        parseChassisProperties(element, isChassisTemplate, variables, services);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Invalid chassis number: Must be > 0");
+    }
+
+    // Test where fails: inventory_path value is invalid
+    try
+    {
+        const json element = R"(
+            {
+              "number": 1,
+              "inventory_path": "",
+              "power_sequencers": []
+            }
+        )"_json;
+        bool isChassisTemplate{false};
+        std::map<std::string, std::string> variables{};
+        MockServices services{};
+        parseChassisProperties(element, isChassisTemplate, variables, services);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element contains an empty string");
+    }
+
+    // Test where fails: power_sequencers value is invalid
+    try
+    {
+        const json element = R"(
+            {
+              "number": 1,
+              "inventory_path": "/xyz/openbmc_project/inventory/system/chassis",
+              "power_sequencers": { "name": "foo" }
+            }
+        )"_json;
+        bool isChassisTemplate{false};
+        std::map<std::string, std::string> variables{};
+        MockServices services{};
+        parseChassisProperties(element, isChassisTemplate, 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: Invalid property specified
+    try
+    {
+        const json element = R"(
+            {
+              "foo": "bar",
+              "number": 1,
+              "inventory_path": "/xyz/openbmc_project/inventory/system/chassis",
+              "power_sequencers": []
+            }
+        )"_json;
+        bool isChassisTemplate{false};
+        std::map<std::string, std::string> variables{};
+        MockServices services{};
+        parseChassisProperties(element, isChassisTemplate, 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"(
+            {
+              "id": "foo_chassis",
+              "number": "${chassis_number}",
+              "inventory_path": "/xyz/openbmc_project/inventory/system/chassis${chassis_number}",
+              "power_sequencers": []
+            }
+        )"_json;
+        bool isChassisTemplate{true};
+        std::map<std::string, std::string> variables{{"chassis_number", "two"}};
+        MockServices services{};
+        parseChassisProperties(element, isChassisTemplate, 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, ParseChassisTemplate)
 {
     // Test where works: comments specified
@@ -1822,3 +2522,113 @@
         EXPECT_STREQ(e.what(), "Element contains an invalid property");
     }
 }
+
+TEST(ConfigFileParserTests, ParseVariables)
+{
+    // Test where works: No variables specified
+    {
+        const json element = R"(
+            {
+            }
+        )"_json;
+        auto variables = parseVariables(element);
+        EXPECT_EQ(variables.size(), 0);
+    }
+
+    // Test where works: Variables specified
+    {
+        const json element = R"(
+            {
+              "chassis_number": "2",
+              "bus_number": "13"
+            }
+        )"_json;
+        auto variables = parseVariables(element);
+        EXPECT_EQ(variables.size(), 2);
+        EXPECT_EQ(variables["chassis_number"], "2");
+        EXPECT_EQ(variables["bus_number"], "13");
+    }
+
+    // Test where fails: Element is not an object
+    try
+    {
+        const json element = R"(
+            [
+              "chassis_number", "2",
+              "bus_number", "13"
+            ]
+        )"_json;
+        parseVariables(element);
+        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: Key is not a string
+    try
+    {
+        const json element = R"(
+            {
+              chassis_number: "2",
+              "bus_number": "13"
+            }
+        )"_json;
+        parseVariables(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const json::parse_error& e)
+    {}
+
+    // Test where fails: Value is not a string
+    try
+    {
+        const json element = R"(
+            {
+              "chassis_number": "2",
+              "bus_number": 13
+            }
+        )"_json;
+        parseVariables(element);
+        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: Key is an empty string
+    try
+    {
+        const json element = R"(
+            {
+              "chassis_number": "2",
+              "": "13"
+            }
+        )"_json;
+        parseVariables(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element contains an empty string");
+    }
+
+    // Test where fails: Value is an empty string
+    try
+    {
+        const json element = R"(
+            {
+              "chassis_number": "",
+              "bus_number": "13"
+            }
+        )"_json;
+        parseVariables(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element contains an empty string");
+    }
+}