regulators: Create configuration file parser

Create the initial version of the configuration file parser.  This will
parse the JSON configuration file that controls the phosphor-regulators
application.

This commit implements support for parsing the following JSON elements:
* root element in the config file (config_file.md)
* array of rules
* rule (rule.md)
* array of actions
* action (action.md)
* pmbus_write_vout_command (pmbus_write_vout_command.md)
* array of chassis (chassis.md)

Signed-off-by: Shawn McCarney <shawnmm@us.ibm.com>
Change-Id: Id5f239517e1828e475fa81e26c56b85f678920cb
diff --git a/phosphor-regulators/test/config_file_parser_tests.cpp b/phosphor-regulators/test/config_file_parser_tests.cpp
new file mode 100644
index 0000000..490cac7
--- /dev/null
+++ b/phosphor-regulators/test/config_file_parser_tests.cpp
@@ -0,0 +1,1085 @@
+/**
+ * Copyright © 2020 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include "action.hpp"
+#include "chassis.hpp"
+#include "config_file_parser.hpp"
+#include "config_file_parser_error.hpp"
+#include "pmbus_utils.hpp"
+#include "pmbus_write_vout_command_action.hpp"
+#include "rule.hpp"
+
+#include <stdlib.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include <nlohmann/json.hpp>
+
+#include <cstdint>
+#include <cstring>
+#include <exception>
+#include <filesystem>
+#include <fstream>
+#include <memory>
+#include <optional>
+#include <stdexcept>
+#include <string>
+#include <tuple>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+using namespace phosphor::power::regulators;
+using namespace phosphor::power::regulators::config_file_parser;
+using namespace phosphor::power::regulators::config_file_parser::internal;
+using json = nlohmann::json;
+
+/**
+ * @class TmpFile
+ *
+ * Temporary file.
+ *
+ * File is deleted automatically by the destructor when the object goes out of
+ * scope.
+ */
+class TmpFile
+{
+  public:
+    TmpFile()
+    {
+        int fd = mkstemp(fileName);
+        if (fd == -1)
+        {
+            throw std::runtime_error{"Unable to create temporary file"};
+        }
+        close(fd);
+    }
+
+    std::string getName()
+    {
+        return fileName;
+    }
+
+    ~TmpFile()
+    {
+        unlink(fileName);
+    }
+
+  private:
+    char fileName[17] = "/tmp/temp-XXXXXX";
+};
+
+void writeConfigFile(const std::filesystem::path& pathName,
+                     const std::string& contents)
+{
+    std::ofstream file{pathName};
+    file << contents;
+}
+
+void writeConfigFile(const std::filesystem::path& pathName,
+                     const json& contents)
+{
+    std::ofstream file{pathName};
+    file << contents;
+}
+
+TEST(ConfigFileParserTests, Parse)
+{
+    // Test where works
+    {
+        const json configFileContents = R"(
+            {
+              "rules": [
+                {
+                  "id": "set_voltage_rule1",
+                  "actions": [
+                    { "pmbus_write_vout_command": { "volts": 1.03, "format": "linear" } }
+                  ]
+                },
+                {
+                  "id": "set_voltage_rule2",
+                  "actions": [
+                    { "pmbus_write_vout_command": { "volts": 1.33, "format": "linear" } }
+                  ]
+                }
+              ],
+              "chassis": [
+                { "number": 1 },
+                { "number": 2 },
+                { "number": 3 }
+              ]
+            }
+        )"_json;
+
+        TmpFile configFile;
+        std::filesystem::path pathName{configFile.getName()};
+        writeConfigFile(pathName, configFileContents);
+
+        std::vector<std::unique_ptr<Rule>> rules{};
+        std::vector<std::unique_ptr<Chassis>> chassis{};
+        std::tie(rules, chassis) = parse(pathName);
+
+        EXPECT_EQ(rules.size(), 2);
+        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);
+    }
+
+    // Test where fails: File does not exist
+    try
+    {
+        std::filesystem::path pathName{"/tmp/non_existent_file"};
+        parse(pathName);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const ConfigFileParserError& e)
+    {
+        // Expected exception; what() message will vary
+    }
+
+    // Test where fails: File is not readable
+    try
+    {
+        const json configFileContents = R"(
+            {
+              "chassis": [ { "number": 1 } ]
+            }
+        )"_json;
+
+        TmpFile configFile;
+        std::filesystem::path pathName{configFile.getName()};
+        writeConfigFile(pathName, configFileContents);
+
+        chmod(pathName.c_str(), 0222);
+
+        parse(pathName);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const ConfigFileParserError& e)
+    {
+        // Expected exception; what() message will vary
+    }
+
+    // Test where fails: File is not valid JSON
+    try
+    {
+        const std::string configFileContents = "] foo [";
+
+        TmpFile configFile;
+        std::filesystem::path pathName{configFile.getName()};
+        writeConfigFile(pathName, configFileContents);
+
+        parse(pathName);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const ConfigFileParserError& e)
+    {
+        // Expected exception; what() message will vary
+    }
+
+    // Test where fails: Error when parsing JSON elements
+    try
+    {
+        const json configFileContents = R"( { "foo": "bar" } )"_json;
+
+        TmpFile configFile;
+        std::filesystem::path pathName{configFile.getName()};
+        writeConfigFile(pathName, configFileContents);
+
+        parse(pathName);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const ConfigFileParserError& e)
+    {
+        // Expected exception; what() message will vary
+    }
+}
+
+TEST(ConfigFileParserTests, GetRequiredProperty)
+{
+    // Test where property exists
+    {
+        const json element = R"( { "format": "linear" } )"_json;
+        const json& propertyElement = getRequiredProperty(element, "format");
+        EXPECT_EQ(propertyElement.get<std::string>(), "linear");
+    }
+
+    // Test where property does not exist
+    try
+    {
+        const json element = R"( { "volts": 1.03 } )"_json;
+        getRequiredProperty(element, "format");
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Required property missing: format");
+    }
+}
+
+TEST(ConfigFileParserTests, ParseAction)
+{
+    // Test where works: comments property specified
+    {
+        const json element = R"(
+            {
+              "comments": [ "Set output voltage." ],
+              "pmbus_write_vout_command": {
+                "format": "linear"
+              }
+            }
+        )"_json;
+        std::unique_ptr<Action> action = parseAction(element);
+        EXPECT_NE(action.get(), nullptr);
+    }
+
+    // Test where works: comments property not specified
+    {
+        const json element = R"(
+            {
+              "pmbus_write_vout_command": {
+                "format": "linear"
+              }
+            }
+        )"_json;
+        std::unique_ptr<Action> action = parseAction(element);
+        EXPECT_NE(action.get(), nullptr);
+    }
+
+    // Test where works: and action type specified
+    // TODO: Not implemented yet
+
+    // Test where works: compare_presence action type specified
+    // TODO: Not implemented yet
+
+    // Test where works: compare_vpd action type specified
+    // TODO: Not implemented yet
+
+    // Test where works: i2c_compare_bit action type specified
+    // TODO: Not implemented yet
+
+    // Test where works: i2c_compare_byte action type specified
+    // TODO: Not implemented yet
+
+    // Test where works: i2c_compare_bytes action type specified
+    // TODO: Not implemented yet
+
+    // Test where works: i2c_write_bit action type specified
+    // TODO: Not implemented yet
+
+    // Test where works: i2c_write_byte action type specified
+    // TODO: Not implemented yet
+
+    // Test where works: i2c_write_bytes action type specified
+    // TODO: Not implemented yet
+
+    // Test where works: if action type specified
+    // TODO: Not implemented yet
+
+    // Test where works: not action type specified
+    // TODO: Not implemented yet
+
+    // Test where works: or action type specified
+    // TODO: Not implemented yet
+
+    // Test where works: pmbus_read_sensor action type specified
+    // TODO: Not implemented yet
+
+    // Test where works: pmbus_write_vout_command action type specified
+    {
+        const json element = R"(
+            {
+              "pmbus_write_vout_command": {
+                "format": "linear"
+              }
+            }
+        )"_json;
+        std::unique_ptr<Action> action = parseAction(element);
+        EXPECT_NE(action.get(), nullptr);
+    }
+
+    // Test where works: run_rule action type specified
+    // TODO: Not implemented yet
+
+    // Test where works: set_device action type specified
+    // TODO: Not implemented yet
+
+    // Test where fails: Element is not an object
+    try
+    {
+        const json element = R"( [ "0xFF", "0x01" ] )"_json;
+        parseAction(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: No action type specified
+    try
+    {
+        const json element = R"(
+            {
+              "comments": [ "Set output voltage." ]
+            }
+        )"_json;
+        parseAction(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Required action type property missing");
+    }
+
+    // Test where fails: Multiple action types specified
+    // TODO: Implement after another action type is supported
+
+    // Test where fails: Invalid property specified
+    try
+    {
+        const json element = R"(
+            {
+              "remarks": [ "Set output voltage." ],
+              "pmbus_write_vout_command": {
+                "format": "linear"
+              }
+            }
+        )"_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(ConfigFileParserTests, ParseActionArray)
+{
+    // Test where works
+    {
+        const json element = R"(
+            [
+              { "pmbus_write_vout_command": { "volts": 1.01, "format": "linear" } },
+              { "pmbus_write_vout_command": { "volts": 1.03, "format": "linear" } }
+            ]
+        )"_json;
+        std::vector<std::unique_ptr<Action>> actions =
+            parseActionArray(element);
+        EXPECT_EQ(actions.size(), 2);
+    }
+
+    // Test where fails: Element is not an array
+    try
+    {
+        const json element = R"(
+            {
+              "foo": "bar"
+            }
+        )"_json;
+        parseActionArray(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, ParseBoolean)
+{
+    // Test where works: true
+    {
+        const json element = R"( true )"_json;
+        bool value = parseBoolean(element);
+        EXPECT_EQ(value, true);
+    }
+
+    // Test where works: false
+    {
+        const json element = R"( false )"_json;
+        bool value = parseBoolean(element);
+        EXPECT_EQ(value, false);
+    }
+
+    // Test where fails: Element is not a boolean
+    try
+    {
+        const json element = R"( 1 )"_json;
+        parseBoolean(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not a boolean");
+    }
+}
+
+TEST(ConfigFileParserTests, ParseChassisArray)
+{
+    // TODO: Not implemented yet
+}
+
+TEST(ConfigFileParserTests, ParseDouble)
+{
+    // Test where works: floating point value
+    {
+        const json element = R"( 1.03 )"_json;
+        double value = parseDouble(element);
+        EXPECT_EQ(value, 1.03);
+    }
+
+    // Test where works: integer value
+    {
+        const json element = R"( 24 )"_json;
+        double value = parseDouble(element);
+        EXPECT_EQ(value, 24.0);
+    }
+
+    // Test where fails: Element is not a number
+    try
+    {
+        const json element = R"( true )"_json;
+        parseDouble(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not a number");
+    }
+}
+
+TEST(ConfigFileParserTests, ParseInt8)
+{
+    // Test where works: INT8_MIN
+    {
+        const json element = R"( -128 )"_json;
+        int8_t value = parseInt8(element);
+        EXPECT_EQ(value, -128);
+    }
+
+    // Test where works: INT8_MAX
+    {
+        const json element = R"( 127 )"_json;
+        int8_t value = parseInt8(element);
+        EXPECT_EQ(value, 127);
+    }
+
+    // Test where fails: Element is not an integer
+    try
+    {
+        const json element = R"( 1.03 )"_json;
+        parseInt8(element);
+        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: Value < INT8_MIN
+    try
+    {
+        const json element = R"( -129 )"_json;
+        parseInt8(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not an 8-bit signed integer");
+    }
+
+    // Test where fails: Value > INT8_MAX
+    try
+    {
+        const json element = R"( 128 )"_json;
+        parseInt8(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not an 8-bit signed integer");
+    }
+}
+
+TEST(ConfigFileParserTests, ParsePMBusWriteVoutCommand)
+{
+    // Test where works: Only required properties specified
+    {
+        const json element = R"(
+            {
+              "format": "linear"
+            }
+        )"_json;
+        std::unique_ptr<PMBusWriteVoutCommandAction> action =
+            parsePMBusWriteVoutCommand(element);
+        EXPECT_EQ(action->getVolts().has_value(), false);
+        EXPECT_EQ(action->getFormat(), pmbus_utils::VoutDataFormat::linear);
+        EXPECT_EQ(action->getExponent().has_value(), false);
+        EXPECT_EQ(action->isVerified(), false);
+    }
+
+    // Test where works: All properties specified
+    {
+        const json element = R"(
+            {
+              "volts": 1.03,
+              "format": "linear",
+              "exponent": -8,
+              "is_verified": true
+            }
+        )"_json;
+        std::unique_ptr<PMBusWriteVoutCommandAction> action =
+            parsePMBusWriteVoutCommand(element);
+        EXPECT_EQ(action->getVolts().has_value(), true);
+        EXPECT_EQ(action->getVolts().value(), 1.03);
+        EXPECT_EQ(action->getFormat(), pmbus_utils::VoutDataFormat::linear);
+        EXPECT_EQ(action->getExponent().has_value(), true);
+        EXPECT_EQ(action->getExponent().value(), -8);
+        EXPECT_EQ(action->isVerified(), true);
+    }
+
+    // Test where fails: Element is not an object
+    try
+    {
+        const json element = R"( [ "0xFF", "0x01" ] )"_json;
+        parsePMBusWriteVoutCommand(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: volts value is invalid
+    try
+    {
+        const json element = R"(
+            {
+              "volts": "foo",
+              "format": "linear"
+            }
+        )"_json;
+        parsePMBusWriteVoutCommand(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not a number");
+    }
+
+    // Test where fails: Required format property not specified
+    try
+    {
+        const json element = R"(
+            {
+              "volts": 1.03,
+              "is_verified": true
+            }
+        )"_json;
+        parsePMBusWriteVoutCommand(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Required property missing: format");
+    }
+
+    // Test where fails: format value is invalid
+    try
+    {
+        const json element = R"(
+            {
+              "format": "linear_11"
+            }
+        )"_json;
+        parsePMBusWriteVoutCommand(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Invalid format value: linear_11");
+    }
+
+    // Test where fails: exponent value is invalid
+    try
+    {
+        const json element = R"(
+            {
+              "format": "linear",
+              "exponent": 1.3
+            }
+        )"_json;
+        parsePMBusWriteVoutCommand(element);
+        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: is_verified value is invalid
+    try
+    {
+        const json element = R"(
+            {
+              "format": "linear",
+              "is_verified": "true"
+            }
+        )"_json;
+        parsePMBusWriteVoutCommand(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not a boolean");
+    }
+
+    // Test where fails: Invalid property specified
+    try
+    {
+        const json element = R"(
+            {
+              "format": "linear",
+              "foo": "bar"
+            }
+        )"_json;
+        parsePMBusWriteVoutCommand(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(ConfigFileParserTests, ParseRoot)
+{
+    // Test where works: Only required properties specified
+    {
+        const json element = R"(
+            {
+              "chassis": [
+                { "number": 1 }
+              ]
+            }
+        )"_json;
+        std::vector<std::unique_ptr<Rule>> rules{};
+        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);
+    }
+
+    // Test where works: All properties specified
+    {
+        const json element = R"(
+            {
+              "comments": [ "Config file for a FooBar one-chassis system" ],
+              "rules": [
+                {
+                  "id": "set_voltage_rule",
+                  "actions": [
+                    { "pmbus_write_vout_command": { "format": "linear" } }
+                  ]
+                }
+              ],
+              "chassis": [
+                { "number": 1 },
+                { "number": 3 }
+              ]
+            }
+        )"_json;
+        std::vector<std::unique_ptr<Rule>> rules{};
+        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);
+    }
+
+    // Test where fails: Element is not an object
+    try
+    {
+        const json element = R"( [ "0xFF", "0x01" ] )"_json;
+        parseRoot(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: chassis property not specified
+    try
+    {
+        const json element = R"(
+            {
+              "rules": [
+                {
+                  "id": "set_voltage_rule",
+                  "actions": [
+                    { "pmbus_write_vout_command": { "format": "linear" } }
+                  ]
+                }
+              ]
+            }
+        )"_json;
+        parseRoot(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Required property missing: chassis");
+    }
+
+    // Test where fails: Invalid property specified
+    try
+    {
+        const json element = R"(
+            {
+              "remarks": [ "Config file for a FooBar one-chassis system" ],
+              "chassis": [
+                { "number": 1 }
+              ]
+            }
+        )"_json;
+        parseRoot(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(ConfigFileParserTests, ParseRule)
+{
+    // Test where works: comments property specified
+    {
+        const json element = R"(
+            {
+              "comments": [ "Set voltage rule" ],
+              "id": "set_voltage_rule",
+              "actions": [
+                { "pmbus_write_vout_command": { "volts": 1.01, "format": "linear" } },
+                { "pmbus_write_vout_command": { "volts": 1.03, "format": "linear" } }
+              ]
+            }
+        )"_json;
+        std::unique_ptr<Rule> rule = parseRule(element);
+        EXPECT_EQ(rule->getID(), "set_voltage_rule");
+        EXPECT_EQ(rule->getActions().size(), 2);
+    }
+
+    // Test where works: comments property not specified
+    {
+        const json element = R"(
+            {
+              "id": "set_voltage_rule",
+              "actions": [
+                { "pmbus_write_vout_command": { "volts": 1.01, "format": "linear" } },
+                { "pmbus_write_vout_command": { "volts": 1.03, "format": "linear" } },
+                { "pmbus_write_vout_command": { "volts": 1.05, "format": "linear" } }
+              ]
+            }
+        )"_json;
+        std::unique_ptr<Rule> rule = parseRule(element);
+        EXPECT_EQ(rule->getID(), "set_voltage_rule");
+        EXPECT_EQ(rule->getActions().size(), 3);
+    }
+
+    // Test where fails: Element is not an object
+    try
+    {
+        const json element = R"( [ "0xFF", "0x01" ] )"_json;
+        parseRule(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: id property not specified
+    try
+    {
+        const json element = R"(
+            {
+              "actions": [
+                { "pmbus_write_vout_command": { "volts": 1.01, "format": "linear" } }
+              ]
+            }
+        )"_json;
+        parseRule(element);
+        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: id property is invalid
+    try
+    {
+        const json element = R"(
+            {
+              "id": "",
+              "actions": [
+                { "pmbus_write_vout_command": { "volts": 1.01, "format": "linear" } }
+              ]
+            }
+        )"_json;
+        parseRule(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: actions property not specified
+    try
+    {
+        const json element = R"(
+            {
+              "comments": [ "Set voltage rule" ],
+              "id": "set_voltage_rule"
+            }
+        )"_json;
+        parseRule(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Required property missing: actions");
+    }
+
+    // Test where fails: actions property is invalid
+    try
+    {
+        const json element = R"(
+            {
+              "id": "set_voltage_rule",
+              "actions": true
+            }
+        )"_json;
+        parseRule(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 where fails: Invalid property specified
+    try
+    {
+        const json element = R"(
+            {
+              "remarks": [ "Set voltage rule" ],
+              "id": "set_voltage_rule",
+              "actions": [
+                { "pmbus_write_vout_command": { "volts": 1.01, "format": "linear" } }
+              ]
+            }
+        )"_json;
+        parseRule(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(ConfigFileParserTests, ParseRuleArray)
+{
+    // Test where works
+    {
+        const json element = R"(
+            [
+              {
+                "id": "set_voltage_rule1",
+                "actions": [
+                  { "pmbus_write_vout_command": { "volts": 1.01, "format": "linear" } }
+                ]
+              },
+              {
+                "id": "set_voltage_rule2",
+                "actions": [
+                  { "pmbus_write_vout_command": { "volts": 1.01, "format": "linear" } },
+                  { "pmbus_write_vout_command": { "volts": 1.11, "format": "linear" } }
+                ]
+              }
+            ]
+        )"_json;
+        std::vector<std::unique_ptr<Rule>> rules = parseRuleArray(element);
+        EXPECT_EQ(rules.size(), 2);
+        EXPECT_EQ(rules[0]->getID(), "set_voltage_rule1");
+        EXPECT_EQ(rules[0]->getActions().size(), 1);
+        EXPECT_EQ(rules[1]->getID(), "set_voltage_rule2");
+        EXPECT_EQ(rules[1]->getActions().size(), 2);
+    }
+
+    // Test where fails: Element is not an array
+    try
+    {
+        const json element = R"( { "id": "set_voltage_rule" } )"_json;
+        parseRuleArray(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, ParseString)
+{
+    // Test where works: Empty string
+    {
+        const json element = "";
+        std::string value = parseString(element, true);
+        EXPECT_EQ(value, "");
+    }
+
+    // Test where works: Non-empty string
+    {
+        const json element = "vdd_regulator";
+        std::string value = parseString(element, false);
+        EXPECT_EQ(value, "vdd_regulator");
+    }
+
+    // Test where fails: Element is not a string
+    try
+    {
+        const json element = R"( { "foo": "bar" } )"_json;
+        parseString(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: Empty string
+    try
+    {
+        const json element = "";
+        parseString(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(ConfigFileParserTests, VerifyIsArray)
+{
+    // Test where element is an array
+    try
+    {
+        const json element = R"( [ "foo", "bar" ] )"_json;
+        verifyIsArray(element);
+    }
+    catch (const std::exception& e)
+    {
+        ADD_FAILURE() << "Should not have caught exception.";
+    }
+
+    // Test where element is not an array
+    try
+    {
+        const json element = R"( { "foo": "bar" } )"_json;
+        verifyIsArray(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, VerifyIsObject)
+{
+    // Test where element is an object
+    try
+    {
+        const json element = R"( { "foo": "bar" } )"_json;
+        verifyIsObject(element);
+    }
+    catch (const std::exception& e)
+    {
+        ADD_FAILURE() << "Should not have caught exception.";
+    }
+
+    // Test where element is not an object
+    try
+    {
+        const json element = R"( [ "foo", "bar" ] )"_json;
+        verifyIsObject(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(ConfigFileParserTests, VerifyPropertyCount)
+{
+    // Test where element has expected number of properties
+    try
+    {
+        const json element = R"(
+            {
+              "comments": [ "Set voltage rule" ],
+              "id": "set_voltage_rule"
+            }
+        )"_json;
+        verifyPropertyCount(element, 2);
+    }
+    catch (const std::exception& e)
+    {
+        ADD_FAILURE() << "Should not have caught exception.";
+    }
+
+    // Test where element has unexpected number of properties
+    try
+    {
+        const json element = R"(
+            {
+              "comments": [ "Set voltage rule" ],
+              "id": "set_voltage_rule",
+              "foo": 1.3
+            }
+        )"_json;
+        verifyPropertyCount(element, 2);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element contains an invalid property");
+    }
+}