pseq: Add variable support to config file parser

Add template variable support to the functions that parse the
phosphor-power-sequencer configuration file.

Tested:
* Ran automated tests.

Change-Id: I649d66de520e9138fc3a5dc34de61cc4cb03fe53
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 3163aff..0812744 100644
--- a/phosphor-power-sequencer/src/config_file_parser.cpp
+++ b/phosphor-power-sequencer/src/config_file_parser.cpp
@@ -93,14 +93,15 @@
 namespace internal
 {
 
-GPIO parseGPIO(const json& element)
+GPIO parseGPIO(const json& element,
+               const std::map<std::string, std::string>& variables)
 {
     verifyIsObject(element);
     unsigned int propertyCount{0};
 
     // Required line property
     const json& lineElement = getRequiredProperty(element, "line");
-    unsigned int line = parseUnsignedInteger(lineElement);
+    unsigned int line = parseUnsignedInteger(lineElement, variables);
     ++propertyCount;
 
     // Optional active_low property
@@ -108,7 +109,7 @@
     auto activeLowIt = element.find("active_low");
     if (activeLowIt != element.end())
     {
-        activeLow = parseBoolean(*activeLowIt);
+        activeLow = parseBoolean(*activeLowIt, variables);
         ++propertyCount;
     }
 
@@ -141,14 +142,15 @@
     return {bus, address};
 }
 
-std::unique_ptr<Rail> parseRail(const json& element)
+std::unique_ptr<Rail> parseRail(
+    const json& element, const std::map<std::string, std::string>& variables)
 {
     verifyIsObject(element);
     unsigned int propertyCount{0};
 
     // Required name property
     const json& nameElement = getRequiredProperty(element, "name");
-    std::string name = parseString(nameElement);
+    std::string name = parseString(nameElement, false, variables);
     ++propertyCount;
 
     // Optional presence property
@@ -156,7 +158,7 @@
     auto presenceIt = element.find("presence");
     if (presenceIt != element.end())
     {
-        presence = parseString(*presenceIt);
+        presence = parseString(*presenceIt, false, variables);
         ++propertyCount;
     }
 
@@ -165,7 +167,7 @@
     auto pageIt = element.find("page");
     if (pageIt != element.end())
     {
-        page = parseUint8(*pageIt);
+        page = parseUint8(*pageIt, variables);
         ++propertyCount;
     }
 
@@ -174,7 +176,7 @@
     auto isPowerSupplyRailIt = element.find("is_power_supply_rail");
     if (isPowerSupplyRailIt != element.end())
     {
-        isPowerSupplyRail = parseBoolean(*isPowerSupplyRailIt);
+        isPowerSupplyRail = parseBoolean(*isPowerSupplyRailIt, variables);
         ++propertyCount;
     }
 
@@ -183,7 +185,7 @@
     auto checkStatusVoutIt = element.find("check_status_vout");
     if (checkStatusVoutIt != element.end())
     {
-        checkStatusVout = parseBoolean(*checkStatusVoutIt);
+        checkStatusVout = parseBoolean(*checkStatusVoutIt, variables);
         ++propertyCount;
     }
 
@@ -192,7 +194,8 @@
     auto compareVoltageToLimitIt = element.find("compare_voltage_to_limit");
     if (compareVoltageToLimitIt != element.end())
     {
-        compareVoltageToLimit = parseBoolean(*compareVoltageToLimitIt);
+        compareVoltageToLimit =
+            parseBoolean(*compareVoltageToLimitIt, variables);
         ++propertyCount;
     }
 
@@ -201,7 +204,7 @@
     auto gpioIt = element.find("gpio");
     if (gpioIt != element.end())
     {
-        gpio = parseGPIO(*gpioIt);
+        gpio = parseGPIO(*gpioIt, variables);
         ++propertyCount;
     }
 
@@ -219,25 +222,29 @@
                                   checkStatusVout, compareVoltageToLimit, gpio);
 }
 
-std::vector<std::unique_ptr<Rail>> parseRailArray(const json& element)
+std::vector<std::unique_ptr<Rail>> parseRailArray(
+    const json& element, const std::map<std::string, std::string>& variables)
 {
     verifyIsArray(element);
     std::vector<std::unique_ptr<Rail>> rails;
     for (auto& railElement : element)
     {
-        rails.emplace_back(parseRail(railElement));
+        rails.emplace_back(parseRail(railElement, variables));
     }
     return rails;
 }
 
 std::vector<std::unique_ptr<Rail>> parseRoot(const json& element)
 {
+    std::map<std::string, std::string> variables{};
+
     verifyIsObject(element);
     unsigned int propertyCount{0};
 
     // Required rails property
     const json& railsElement = getRequiredProperty(element, "rails");
-    std::vector<std::unique_ptr<Rail>> rails = parseRailArray(railsElement);
+    std::vector<std::unique_ptr<Rail>> rails =
+        parseRailArray(railsElement, variables);
     ++propertyCount;
 
     // Verify no invalid properties exist
diff --git a/phosphor-power-sequencer/src/config_file_parser.hpp b/phosphor-power-sequencer/src/config_file_parser.hpp
index a57083e..0f5fe3c 100644
--- a/phosphor-power-sequencer/src/config_file_parser.hpp
+++ b/phosphor-power-sequencer/src/config_file_parser.hpp
@@ -86,9 +86,11 @@
  * Throws an exception if parsing fails.
  *
  * @param element JSON element
+ * @param variables variables map used to expand variables in element value
  * @return GPIO object
  */
-GPIO parseGPIO(const nlohmann::json& element);
+GPIO parseGPIO(const nlohmann::json& element,
+               const std::map<std::string, std::string>& variables);
 
 /**
  * Parses a JSON element containing an i2c_interface object.
@@ -113,9 +115,12 @@
  * Throws an exception if parsing fails.
  *
  * @param element JSON element
+ * @param variables variables map used to expand variables in element value
  * @return Rail object
  */
-std::unique_ptr<Rail> parseRail(const nlohmann::json& element);
+std::unique_ptr<Rail> parseRail(
+    const nlohmann::json& element,
+    const std::map<std::string, std::string>& variables);
 
 /**
  * Parses a JSON element containing an array of rails.
@@ -125,10 +130,12 @@
  * Throws an exception if parsing fails.
  *
  * @param element JSON element
+ * @param variables variables map used to expand variables in element value
  * @return vector of Rail objects
  */
 std::vector<std::unique_ptr<Rail>> parseRailArray(
-    const nlohmann::json& element);
+    const nlohmann::json& element,
+    const std::map<std::string, std::string>& variables);
 
 /**
  * Parses the JSON root element of the entire configuration file.
diff --git a/phosphor-power-sequencer/test/config_file_parser_tests.cpp b/phosphor-power-sequencer/test/config_file_parser_tests.cpp
index 0fbf5fe..c6101d3 100644
--- a/phosphor-power-sequencer/test/config_file_parser_tests.cpp
+++ b/phosphor-power-sequencer/test/config_file_parser_tests.cpp
@@ -268,7 +268,7 @@
         fs::path pathName{configFile.getPath()};
         writeConfigFile(pathName, configFileContents);
 
-        std::vector<std::unique_ptr<Rail>> rails = parse(pathName);
+        auto rails = parse(pathName);
 
         EXPECT_EQ(rails.size(), 2);
         EXPECT_EQ(rails[0]->getName(), "VDD_CPU0");
@@ -333,7 +333,8 @@
                 "line": 60
             }
         )"_json;
-        GPIO gpio = parseGPIO(element);
+        std::map<std::string, std::string> variables{};
+        auto gpio = parseGPIO(element, variables);
         EXPECT_EQ(gpio.line, 60);
         EXPECT_FALSE(gpio.activeLow);
     }
@@ -346,16 +347,33 @@
                 "active_low": true
             }
         )"_json;
-        GPIO gpio = parseGPIO(element);
+        std::map<std::string, std::string> variables{};
+        auto gpio = parseGPIO(element, variables);
         EXPECT_EQ(gpio.line, 131);
         EXPECT_TRUE(gpio.activeLow);
     }
 
+    // Test where works: Variables specified
+    {
+        const json element = R"(
+            {
+                "line": "${line}",
+                "active_low": "${active_low}"
+            }
+        )"_json;
+        std::map<std::string, std::string> variables{{"line", "54"},
+                                                     {"active_low", "false"}};
+        auto gpio = parseGPIO(element, variables);
+        EXPECT_EQ(gpio.line, 54);
+        EXPECT_FALSE(gpio.activeLow);
+    }
+
     // Test where fails: Element is not an object
     try
     {
         const json element = R"( [ "vdda", "vddb" ] )"_json;
-        parseGPIO(element);
+        std::map<std::string, std::string> variables{};
+        parseGPIO(element, variables);
         ADD_FAILURE() << "Should not have reached this line.";
     }
     catch (const std::invalid_argument& e)
@@ -371,7 +389,8 @@
                 "active_low": true
             }
         )"_json;
-        parseGPIO(element);
+        std::map<std::string, std::string> variables{};
+        parseGPIO(element, variables);
         ADD_FAILURE() << "Should not have reached this line.";
     }
     catch (const std::invalid_argument& e)
@@ -388,7 +407,8 @@
                 "active_low": true
             }
         )"_json;
-        parseGPIO(element);
+        std::map<std::string, std::string> variables{};
+        parseGPIO(element, variables);
         ADD_FAILURE() << "Should not have reached this line.";
     }
     catch (const std::invalid_argument& e)
@@ -405,7 +425,8 @@
                 "active_low": "true"
             }
         )"_json;
-        parseGPIO(element);
+        std::map<std::string, std::string> variables{};
+        parseGPIO(element, variables);
         ADD_FAILURE() << "Should not have reached this line.";
     }
     catch (const std::invalid_argument& e)
@@ -422,13 +443,33 @@
                 "foo": "bar"
             }
         )"_json;
-        parseGPIO(element);
+        std::map<std::string, std::string> variables{};
+        parseGPIO(element, variables);
         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"(
+            {
+                "line": "${line}",
+                "active_low": "${active_low}"
+            }
+        )"_json;
+        std::map<std::string, std::string> variables{{"line", "-1"},
+                                                     {"active_low", "false"}};
+        parseGPIO(element, variables);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not an unsigned integer");
+    }
 }
 
 TEST(ConfigFileParserTests, ParseI2CInterface)
@@ -593,7 +634,8 @@
                 "name": "VDD_CPU0"
             }
         )"_json;
-        std::unique_ptr<Rail> rail = parseRail(element);
+        std::map<std::string, std::string> variables{};
+        auto rail = parseRail(element, variables);
         EXPECT_EQ(rail->getName(), "VDD_CPU0");
         EXPECT_FALSE(rail->getPresence().has_value());
         EXPECT_FALSE(rail->getPage().has_value());
@@ -616,7 +658,8 @@
                 "gpio": { "line": 60, "active_low": true }
             }
         )"_json;
-        std::unique_ptr<Rail> rail = parseRail(element);
+        std::map<std::string, std::string> variables{};
+        auto rail = parseRail(element, variables);
         EXPECT_EQ(rail->getName(), "12.0VB");
         EXPECT_TRUE(rail->getPresence().has_value());
         EXPECT_EQ(rail->getPresence().value(),
@@ -631,11 +674,49 @@
         EXPECT_TRUE(rail->getGPIO().value().activeLow);
     }
 
+    // Test where works: Variables specified
+    {
+        const json element = R"(
+            {
+                "name": "${name}",
+                "presence": "${presence}",
+                "page": "${page}",
+                "is_power_supply_rail": "${is_power_supply_rail}",
+                "check_status_vout": "${check_status_vout}",
+                "compare_voltage_to_limit": "${compare_voltage_to_limit}",
+                "gpio": { "line": "${line}", "active_low": true }
+            }
+        )"_json;
+        std::map<std::string, std::string> variables{
+            {"name", "vdd"},
+            {"presence",
+             "/xyz/openbmc_project/inventory/system/chassis/powersupply2"},
+            {"page", "9"},
+            {"is_power_supply_rail", "true"},
+            {"check_status_vout", "false"},
+            {"compare_voltage_to_limit", "true"},
+            {"line", "72"}};
+        auto rail = parseRail(element, variables);
+        EXPECT_EQ(rail->getName(), "vdd");
+        EXPECT_TRUE(rail->getPresence().has_value());
+        EXPECT_EQ(rail->getPresence().value(),
+                  "/xyz/openbmc_project/inventory/system/chassis/powersupply2");
+        EXPECT_TRUE(rail->getPage().has_value());
+        EXPECT_EQ(rail->getPage().value(), 9);
+        EXPECT_TRUE(rail->isPowerSupplyRail());
+        EXPECT_FALSE(rail->getCheckStatusVout());
+        EXPECT_TRUE(rail->getCompareVoltageToLimit());
+        EXPECT_TRUE(rail->getGPIO().has_value());
+        EXPECT_EQ(rail->getGPIO().value().line, 72);
+        EXPECT_TRUE(rail->getGPIO().value().activeLow);
+    }
+
     // Test where fails: Element is not an object
     try
     {
         const json element = R"( [ "vdda", "vddb" ] )"_json;
-        parseRail(element);
+        std::map<std::string, std::string> variables{};
+        parseRail(element, variables);
         ADD_FAILURE() << "Should not have reached this line.";
     }
     catch (const std::invalid_argument& e)
@@ -651,7 +732,8 @@
                 "page": 11
             }
         )"_json;
-        parseRail(element);
+        std::map<std::string, std::string> variables{};
+        parseRail(element, variables);
         ADD_FAILURE() << "Should not have reached this line.";
     }
     catch (const std::invalid_argument& e)
@@ -668,7 +750,8 @@
                 "page": 11
             }
         )"_json;
-        parseRail(element);
+        std::map<std::string, std::string> variables{};
+        parseRail(element, variables);
         ADD_FAILURE() << "Should not have reached this line.";
     }
     catch (const std::invalid_argument& e)
@@ -685,7 +768,8 @@
                 "presence": false
             }
         )"_json;
-        parseRail(element);
+        std::map<std::string, std::string> variables{};
+        parseRail(element, variables);
         ADD_FAILURE() << "Should not have reached this line.";
     }
     catch (const std::invalid_argument& e)
@@ -702,7 +786,8 @@
                 "page": 256
             }
         )"_json;
-        parseRail(element);
+        std::map<std::string, std::string> variables{};
+        parseRail(element, variables);
         ADD_FAILURE() << "Should not have reached this line.";
     }
     catch (const std::invalid_argument& e)
@@ -719,7 +804,8 @@
                 "is_power_supply_rail": "true"
             }
         )"_json;
-        parseRail(element);
+        std::map<std::string, std::string> variables{};
+        parseRail(element, variables);
         ADD_FAILURE() << "Should not have reached this line.";
     }
     catch (const std::invalid_argument& e)
@@ -736,7 +822,8 @@
                 "check_status_vout": "false"
             }
         )"_json;
-        parseRail(element);
+        std::map<std::string, std::string> variables{};
+        parseRail(element, variables);
         ADD_FAILURE() << "Should not have reached this line.";
     }
     catch (const std::invalid_argument& e)
@@ -753,7 +840,8 @@
                 "compare_voltage_to_limit": 23
             }
         )"_json;
-        parseRail(element);
+        std::map<std::string, std::string> variables{};
+        parseRail(element, variables);
         ADD_FAILURE() << "Should not have reached this line.";
     }
     catch (const std::invalid_argument& e)
@@ -770,7 +858,8 @@
                 "gpio": 131
             }
         )"_json;
-        parseRail(element);
+        std::map<std::string, std::string> variables{};
+        parseRail(element, variables);
         ADD_FAILURE() << "Should not have reached this line.";
     }
     catch (const std::invalid_argument& e)
@@ -787,7 +876,8 @@
                 "check_status_vout": true
             }
         )"_json;
-        parseRail(element);
+        std::map<std::string, std::string> variables{};
+        parseRail(element, variables);
         ADD_FAILURE() << "Should not have reached this line.";
     }
     catch (const std::invalid_argument& e)
@@ -805,7 +895,8 @@
                 "compare_voltage_to_limit": true
             }
         )"_json;
-        parseRail(element);
+        std::map<std::string, std::string> variables{};
+        parseRail(element, variables);
         ADD_FAILURE() << "Should not have reached this line.";
     }
     catch (const std::invalid_argument& e)
@@ -822,13 +913,32 @@
                 "foo": "bar"
             }
         )"_json;
-        parseRail(element);
+        std::map<std::string, std::string> variables{};
+        parseRail(element, variables);
         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: Undefined variable specified
+    try
+    {
+        const json element = R"(
+            {
+                "name": "12.0VB",
+                "presence": "/xyz/openbmc_project/inventory/system/chassis/powersupply${chassis}"
+            }
+        )"_json;
+        std::map<std::string, std::string> variables{{"foo", "bar"}};
+        parseRail(element, variables);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Undefined variable: chassis");
+    }
 }
 
 TEST(ConfigFileParserTests, ParseRailArray)
@@ -839,7 +949,8 @@
             [
             ]
         )"_json;
-        std::vector<std::unique_ptr<Rail>> rails = parseRailArray(element);
+        std::map<std::string, std::string> variables{};
+        auto rails = parseRailArray(element, variables);
         EXPECT_EQ(rails.size(), 0);
     }
 
@@ -851,12 +962,31 @@
                 { "name": "VCS_CPU1" }
             ]
         )"_json;
-        std::vector<std::unique_ptr<Rail>> rails = parseRailArray(element);
+        std::map<std::string, std::string> variables{};
+        auto rails = parseRailArray(element, variables);
         EXPECT_EQ(rails.size(), 2);
         EXPECT_EQ(rails[0]->getName(), "VDD_CPU0");
         EXPECT_EQ(rails[1]->getName(), "VCS_CPU1");
     }
 
+    // Test where works: Variables specified
+    {
+        const json element = R"(
+            [
+                { "name": "${rail1}" },
+                { "name": "${rail2}" },
+                { "name": "${rail3}" }
+            ]
+        )"_json;
+        std::map<std::string, std::string> variables{
+            {"rail1", "foo"}, {"rail2", "bar"}, {"rail3", "baz"}};
+        auto rails = parseRailArray(element, variables);
+        EXPECT_EQ(rails.size(), 3);
+        EXPECT_EQ(rails[0]->getName(), "foo");
+        EXPECT_EQ(rails[1]->getName(), "bar");
+        EXPECT_EQ(rails[2]->getName(), "baz");
+    }
+
     // Test where fails: Element is not an array
     try
     {
@@ -865,7 +995,8 @@
                 "foo": "bar"
             }
         )"_json;
-        parseRailArray(element);
+        std::map<std::string, std::string> variables{};
+        parseRailArray(element, variables);
         ADD_FAILURE() << "Should not have reached this line.";
     }
     catch (const std::invalid_argument& e)
@@ -882,13 +1013,33 @@
                 23
             ]
         )"_json;
-        parseRailArray(element);
+        std::map<std::string, std::string> variables{};
+        parseRailArray(element, variables);
         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"(
+            [
+                { "name": "VDD_CPU0", "page": "${page1}" },
+                { "name": "VCS_CPU1", "page": "${page2}" }
+            ]
+        )"_json;
+        std::map<std::string, std::string> variables{{"page1", "11"},
+                                                     {"page2", "-1"}};
+        parseRailArray(element, variables);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not an 8-bit unsigned integer");
+    }
 }
 
 TEST(ConfigFileParserTests, ParseRoot)
@@ -911,7 +1062,7 @@
                 ]
             }
         )"_json;
-        std::vector<std::unique_ptr<Rail>> rails = parseRoot(element);
+        auto rails = parseRoot(element);
         EXPECT_EQ(rails.size(), 2);
         EXPECT_EQ(rails[0]->getName(), "VDD_CPU0");
         EXPECT_EQ(rails[1]->getName(), "VCS_CPU1");