regulators: Implements support for chassis

Implements support for parsing the chassis JSON elements in the
configuration file parser.

Signed-off-by: Bob King <Bob_King@wistron.com>
Change-Id: I26e7a3f118c9fc6e0e302be20bb3e0ea63f9e1fd
diff --git a/phosphor-regulators/src/config_file_parser.cpp b/phosphor-regulators/src/config_file_parser.cpp
index 1d64c96..99a2d15 100644
--- a/phosphor-regulators/src/config_file_parser.cpp
+++ b/phosphor-regulators/src/config_file_parser.cpp
@@ -179,18 +179,64 @@
     return actions;
 }
 
+std::unique_ptr<Chassis> parseChassis(const json& element)
+{
+    verifyIsObject(element);
+    unsigned int propertyCount{0};
+
+    // Optional comments property; value not stored
+    if (element.contains("comments"))
+    {
+        ++propertyCount;
+    }
+
+    // Required number property
+    const json& numberElement = getRequiredProperty(element, "number");
+    unsigned int number = parseUnsignedInteger(numberElement);
+    if (number < 1)
+    {
+        throw std::invalid_argument{"Invalid chassis number: Must be > 0"};
+    }
+    ++propertyCount;
+
+    // Optional devices property
+    std::vector<std::unique_ptr<Device>> devices{};
+    auto devicesIt = element.find("devices");
+    if (devicesIt != element.end())
+    {
+        devices = parseDeviceArray(*devicesIt);
+        ++propertyCount;
+    }
+
+    // Verify no invalid properties exist
+    verifyPropertyCount(element, propertyCount);
+
+    return std::make_unique<Chassis>(number, std::move(devices));
+}
+
 std::vector<std::unique_ptr<Chassis>> parseChassisArray(const json& element)
 {
     verifyIsArray(element);
     std::vector<std::unique_ptr<Chassis>> chassis;
-    // TODO: Not implemented yet
-    // for (auto& chassisElement : element)
-    // {
-    //     chassis.emplace_back(parseChassis(chassisElement));
-    // }
+    for (auto& chassisElement : element)
+    {
+        chassis.emplace_back(parseChassis(chassisElement));
+    }
     return chassis;
 }
 
+std::vector<std::unique_ptr<Device>> parseDeviceArray(const json& element)
+{
+    verifyIsArray(element);
+    std::vector<std::unique_ptr<Device>> devices;
+    // TODO: Not implemented yet
+    // for (auto& deviceElement : element)
+    // {
+    //     devices.emplace_back(parseDevice(deviceElement));
+    // }
+    return devices;
+}
+
 std::vector<uint8_t> parseHexByteArray(const json& element)
 {
     verifyIsArray(element);
diff --git a/phosphor-regulators/src/config_file_parser.hpp b/phosphor-regulators/src/config_file_parser.hpp
index 0a931d8..3ebab4b 100644
--- a/phosphor-regulators/src/config_file_parser.hpp
+++ b/phosphor-regulators/src/config_file_parser.hpp
@@ -17,6 +17,7 @@
 
 #include "action.hpp"
 #include "chassis.hpp"
+#include "device.hpp"
 #include "i2c_write_bit_action.hpp"
 #include "i2c_write_byte_action.hpp"
 #include "i2c_write_bytes_action.hpp"
@@ -172,6 +173,18 @@
 }
 
 /**
+ * Parses a JSON element containing a chassis.
+ *
+ * Returns the corresponding C++ Chassis object.
+ *
+ * Throws an exception if parsing fails.
+ *
+ * @param element JSON element
+ * @return Chassis object
+ */
+std::unique_ptr<Chassis> parseChassis(const nlohmann::json& element);
+
+/**
  * Parses a JSON element containing an array of chassis.
  *
  * Returns the corresponding C++ Chassis objects.
@@ -185,6 +198,19 @@
     parseChassisArray(const nlohmann::json& element);
 
 /**
+ * Parses a JSON element containing an array of devices.
+ *
+ * Returns the corresponding C++ Device objects.
+ *
+ * Throws an exception if parsing fails.
+ *
+ * @param element JSON element
+ * @return vector of Device objects
+ */
+std::vector<std::unique_ptr<Device>>
+    parseDeviceArray(const nlohmann::json& element);
+
+/**
  * Parses a JSON element containing a double (floating point number).
  *
  * Returns the corresponding C++ double value.
@@ -431,6 +457,26 @@
 }
 
 /**
+ * Parses a JSON element containing an unsigned integer.
+ *
+ * Returns the corresponding C++ unsigned int value.
+ *
+ * Throws an exception if parsing fails.
+ *
+ * @param element JSON element
+ * @return unsigned int value
+ */
+inline unsigned int parseUnsignedInteger(const nlohmann::json& element)
+{
+    // Verify element contains an unsigned integer
+    if (!element.is_number_unsigned())
+    {
+        throw std::invalid_argument{"Element is not an unsigned integer"};
+    }
+    return element.get<unsigned int>();
+}
+
+/**
  * Verifies that the specified JSON element is a JSON array.
  *
  * Throws an invalid_argument exception if the element is not an array.
diff --git a/phosphor-regulators/test/config_file_parser_tests.cpp b/phosphor-regulators/test/config_file_parser_tests.cpp
index 9942c04..92706a3 100644
--- a/phosphor-regulators/test/config_file_parser_tests.cpp
+++ b/phosphor-regulators/test/config_file_parser_tests.cpp
@@ -17,6 +17,7 @@
 #include "chassis.hpp"
 #include "config_file_parser.hpp"
 #include "config_file_parser_error.hpp"
+#include "device.hpp"
 #include "i2c_interface.hpp"
 #include "i2c_write_bit_action.hpp"
 #include "i2c_write_byte_action.hpp"
@@ -104,11 +105,10 @@
         EXPECT_EQ(rules[0]->getID(), "set_voltage_rule1");
         EXPECT_EQ(rules[1]->getID(), "set_voltage_rule2");
 
-        // TODO: Not implemented yet
-        // EXPECT_EQ(chassis.size(), 3);
-        // EXPECT_EQ(chassis[0]->getNumber(), 1);
-        // EXPECT_EQ(chassis[1]->getNumber(), 2);
-        // EXPECT_EQ(chassis[2]->getNumber(), 3);
+        EXPECT_EQ(chassis.size(), 3);
+        EXPECT_EQ(chassis[0]->getNumber(), 1);
+        EXPECT_EQ(chassis[1]->getNumber(), 2);
+        EXPECT_EQ(chassis[2]->getNumber(), 3);
     }
 
     // Test where fails: File does not exist
@@ -361,7 +361,21 @@
     }
 
     // Test where fails: Multiple action types specified
-    // TODO: Implement after another action type is supported
+    try
+    {
+        const json element = R"(
+            {
+              "pmbus_write_vout_command": { "format": "linear" },
+              "run_rule": "set_voltage_rule"
+            }
+        )"_json;
+        parseAction(element);
+        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 property specified
     try
@@ -550,9 +564,186 @@
     }
 }
 
+TEST(ConfigFileParserTests, ParseChassis)
+{
+    // Test where works: Only required properties specified
+    {
+        const json element = R"(
+            {
+              "number": 1
+            }
+        )"_json;
+        std::unique_ptr<Chassis> chassis = parseChassis(element);
+        EXPECT_EQ(chassis->getNumber(), 1);
+        // TODO: Not implemented yet
+        // EXPECT_EQ(chassis->getDevices().size(), 0);
+    }
+
+    // Test where works: All properties specified
+    {
+        const json element = R"(
+            {
+              "comments": [ "comments property" ],
+              "number": 2,
+              "devices": [
+                {
+                  "id": "vdd_regulator",
+                  "is_regulator": true,
+                  "fru": "/system/chassis/motherboard/regulator2",
+                  "i2c_interface":
+                  {
+                      "bus": 1,
+                      "address": "0x70"
+                  }
+                }
+              ]
+            }
+        )"_json;
+        std::unique_ptr<Chassis> chassis = parseChassis(element);
+        EXPECT_EQ(chassis->getNumber(), 2);
+        // TODO: Not implemented yet
+        // EXPECT_EQ(chassis->getDevices().size(), 1);
+        // EXPECT_EQ(chassis->getDevices()[0]->getID(), "vdd_regulator")
+    }
+
+    // Test where fails: number value is invalid
+    try
+    {
+        const json element = R"(
+            {
+              "number": 0.5
+            }
+        )"_json;
+        parseChassis(element);
+        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 where fails: Invalid property specified
+    try
+    {
+        const json element = R"(
+            {
+              "number": 1,
+              "foo": 2
+            }
+        )"_json;
+        parseChassis(element);
+        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: Required number property not specified
+    try
+    {
+        const json element = R"(
+            {
+              "devices": [
+                {
+                  "id": "vdd_regulator",
+                  "is_regulator": true,
+                  "fru": "/system/chassis/motherboard/regulator2",
+                  "i2c_interface":
+                  {
+                      "bus": 1,
+                      "address": "0x70"
+                  }
+                }
+              ]
+            }
+        )"_json;
+        parseChassis(element);
+        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: Element is not an object
+    try
+    {
+        const json element = R"( [ "0xFF", "0x01" ] )"_json;
+        parseChassis(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: number value is < 1
+    try
+    {
+        const json element = R"(
+            {
+              "number": 0
+            }
+        )"_json;
+        parseChassis(element);
+        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: devices value is invalid
+    try
+    {
+        const json element = R"(
+            {
+              "number": 1,
+              "devices": 2
+            }
+        )"_json;
+        parseChassis(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not an array");
+    }
+}
+
 TEST(ConfigFileParserTests, ParseChassisArray)
 {
-    // TODO: Not implemented yet
+    // Test where works
+    {
+        const json element = R"(
+            [
+              { "number": 1 },
+              { "number": 2 }
+            ]
+        )"_json;
+        std::vector<std::unique_ptr<Chassis>> chassis =
+            parseChassisArray(element);
+        EXPECT_EQ(chassis.size(), 2);
+        EXPECT_EQ(chassis[0]->getNumber(), 1);
+        EXPECT_EQ(chassis[1]->getNumber(), 2);
+    }
+
+    // Test where fails: Element is not an array
+    try
+    {
+        const json element = R"(
+            {
+              "foo": "bar"
+            }
+        )"_json;
+        parseChassisArray(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not an array");
+    }
 }
 
 TEST(ConfigFileParserTests, ParseDouble)
@@ -1414,8 +1605,7 @@
         std::vector<std::unique_ptr<Chassis>> chassis{};
         std::tie(rules, chassis) = parseRoot(element);
         EXPECT_EQ(rules.size(), 0);
-        // TODO: Not implemented yet
-        // EXPECT_EQ(chassis.size(), 1);
+        EXPECT_EQ(chassis.size(), 1);
     }
 
     // Test where works: All properties specified
@@ -1441,8 +1631,7 @@
         std::vector<std::unique_ptr<Chassis>> chassis{};
         std::tie(rules, chassis) = parseRoot(element);
         EXPECT_EQ(rules.size(), 1);
-        // TODO: Not implemented yet
-        // EXPECT_EQ(chassis.size(), 2);
+        EXPECT_EQ(chassis.size(), 2);
     }
 
     // Test where fails: Element is not an object
@@ -1810,6 +1999,40 @@
     }
 }
 
+TEST(ConfigFileParserTests, ParseUnsignedInteger)
+{
+    // Test where works: 1
+    {
+        const json element = R"( 1 )"_json;
+        unsigned int value = parseUnsignedInteger(element);
+        EXPECT_EQ(value, 1);
+    }
+
+    // Test where fails: Element is not an integer
+    try
+    {
+        const json element = R"( 1.5 )"_json;
+        parseUnsignedInteger(element);
+        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 where fails: Value < 0
+    try
+    {
+        const json element = R"( -1 )"_json;
+        parseUnsignedInteger(element);
+        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, VerifyIsArray)
 {
     // Test where element is an array