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/src/config_file_parser.cpp b/phosphor-regulators/src/config_file_parser.cpp
new file mode 100644
index 0000000..9ac6060
--- /dev/null
+++ b/phosphor-regulators/src/config_file_parser.cpp
@@ -0,0 +1,322 @@
+/**
+ * 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 "config_file_parser.hpp"
+
+#include "config_file_parser_error.hpp"
+#include "pmbus_utils.hpp"
+
+#include <exception>
+#include <fstream>
+#include <optional>
+#include <utility>
+
+using json = nlohmann::json;
+
+namespace phosphor::power::regulators::config_file_parser
+{
+
+std::tuple<std::vector<std::unique_ptr<Rule>>,
+           std::vector<std::unique_ptr<Chassis>>>
+    parse(const std::filesystem::path& pathName)
+{
+    try
+    {
+        // Use standard JSON parser to create tree of JSON elements
+        std::ifstream file{pathName};
+        json rootElement = json::parse(file);
+
+        // Parse tree of JSON elements and return corresponding C++ objects
+        return internal::parseRoot(rootElement);
+    }
+    catch (const std::exception& e)
+    {
+        throw ConfigFileParserError{pathName, e.what()};
+    }
+}
+
+namespace internal
+{
+
+std::unique_ptr<Action> parseAction(const json& element)
+{
+    verifyIsObject(element);
+    unsigned int propertyCount{0};
+
+    // Optional comments property; value not stored
+    if (element.contains("comments"))
+    {
+        ++propertyCount;
+    }
+
+    // Required action type property; there must be exactly one specified
+    std::unique_ptr<Action> action{};
+    if (element.contains("and"))
+    {
+        // TODO: Not implemented yet
+        // action = parseAnd(element["and"]);
+        // ++propertyCount;
+    }
+    else if (element.contains("compare_presence"))
+    {
+        // TODO: Not implemented yet
+        // action = parseComparePresence(element["compare_presence"]);
+        // ++propertyCount;
+    }
+    else if (element.contains("compare_vpd"))
+    {
+        // TODO: Not implemented yet
+        // action = parseCompareVPD(element["compare_vpd"]);
+        // ++propertyCount;
+    }
+    else if (element.contains("i2c_compare_bit"))
+    {
+        // TODO: Not implemented yet
+        // action = parseI2CCompareBit(element["i2c_compare_bit"]);
+        // ++propertyCount;
+    }
+    else if (element.contains("i2c_compare_byte"))
+    {
+        // TODO: Not implemented yet
+        // action = parseI2CCompareByte(element["i2c_compare_byte"]);
+        // ++propertyCount;
+    }
+    else if (element.contains("i2c_compare_bytes"))
+    {
+        // TODO: Not implemented yet
+        // action = parseI2CCompareBytes(element["i2c_compare_bytes"]);
+        // ++propertyCount;
+    }
+    else if (element.contains("i2c_write_bit"))
+    {
+        // TODO: Not implemented yet
+        // action = parseI2CWriteBit(element["i2c_write_bit"]);
+        // ++propertyCount;
+    }
+    else if (element.contains("i2c_write_byte"))
+    {
+        // TODO: Not implemented yet
+        // action = parseI2CWriteByte(element["i2c_write_byte"]);
+        // ++propertyCount;
+    }
+    else if (element.contains("i2c_write_bytes"))
+    {
+        // TODO: Not implemented yet
+        // action = parseI2CWriteBytes(element["i2c_write_bytes"]);
+        // ++propertyCount;
+    }
+    else if (element.contains("if"))
+    {
+        // TODO: Not implemented yet
+        // action = parseIf(element["if"]);
+        // ++propertyCount;
+    }
+    else if (element.contains("not"))
+    {
+        // TODO: Not implemented yet
+        // action = parseNot(element["not"]);
+        // ++propertyCount;
+    }
+    else if (element.contains("or"))
+    {
+        // TODO: Not implemented yet
+        // action = parseOr(element["or"]);
+        // ++propertyCount;
+    }
+    else if (element.contains("pmbus_read_sensor"))
+    {
+        // TODO: Not implemented yet
+        // action = parsePMBusReadSensor(element["pmbus_read_sensor"]);
+        // ++propertyCount;
+    }
+    else if (element.contains("pmbus_write_vout_command"))
+    {
+        action =
+            parsePMBusWriteVoutCommand(element["pmbus_write_vout_command"]);
+        ++propertyCount;
+    }
+    else if (element.contains("run_rule"))
+    {
+        // TODO: Not implemented yet
+        // action = parseRunRule(element["run_rule"]);
+        // ++propertyCount;
+    }
+    else if (element.contains("set_device"))
+    {
+        // TODO: Not implemented yet
+        // action = parseSetDevice(element["set_device"]);
+        // ++propertyCount;
+    }
+    else
+    {
+        throw std::invalid_argument{"Required action type property missing"};
+    }
+
+    // Verify no invalid properties exist
+    verifyPropertyCount(element, propertyCount);
+
+    return action;
+}
+
+std::vector<std::unique_ptr<Action>> parseActionArray(const json& element)
+{
+    verifyIsArray(element);
+    std::vector<std::unique_ptr<Action>> actions;
+    for (auto& actionElement : element)
+    {
+        actions.emplace_back(parseAction(actionElement));
+    }
+    return actions;
+}
+
+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));
+    // }
+    return chassis;
+}
+
+std::unique_ptr<PMBusWriteVoutCommandAction>
+    parsePMBusWriteVoutCommand(const json& element)
+{
+    verifyIsObject(element);
+    unsigned int propertyCount{0};
+
+    // Optional volts property
+    std::optional<double> volts{};
+    auto voltsIt = element.find("volts");
+    if (voltsIt != element.end())
+    {
+        volts = parseDouble(*voltsIt);
+        ++propertyCount;
+    }
+
+    // Required format property
+    const json& formatElement = getRequiredProperty(element, "format");
+    std::string formatString = parseString(formatElement);
+    if (formatString != "linear")
+    {
+        throw std::invalid_argument{"Invalid format value: " + formatString};
+    }
+    pmbus_utils::VoutDataFormat format = pmbus_utils::VoutDataFormat::linear;
+    ++propertyCount;
+
+    // Optional exponent property
+    std::optional<int8_t> exponent{};
+    auto exponentIt = element.find("exponent");
+    if (exponentIt != element.end())
+    {
+        exponent = parseInt8(*exponentIt);
+        ++propertyCount;
+    }
+
+    // Optional is_verified property
+    bool isVerified = false;
+    auto isVerifiedIt = element.find("is_verified");
+    if (isVerifiedIt != element.end())
+    {
+        isVerified = parseBoolean(*isVerifiedIt);
+        ++propertyCount;
+    }
+
+    // Verify no invalid properties exist
+    verifyPropertyCount(element, propertyCount);
+
+    return std::make_unique<PMBusWriteVoutCommandAction>(volts, format,
+                                                         exponent, isVerified);
+}
+
+std::tuple<std::vector<std::unique_ptr<Rule>>,
+           std::vector<std::unique_ptr<Chassis>>>
+    parseRoot(const json& element)
+{
+    verifyIsObject(element);
+    unsigned int propertyCount{0};
+
+    // Optional comments property; value not stored
+    if (element.contains("comments"))
+    {
+        ++propertyCount;
+    }
+
+    // Optional rules property
+    std::vector<std::unique_ptr<Rule>> rules{};
+    auto rulesIt = element.find("rules");
+    if (rulesIt != element.end())
+    {
+        rules = parseRuleArray(*rulesIt);
+        ++propertyCount;
+    }
+
+    // Required chassis property
+    const json& chassisElement = getRequiredProperty(element, "chassis");
+    std::vector<std::unique_ptr<Chassis>> chassis =
+        parseChassisArray(chassisElement);
+    ++propertyCount;
+
+    // Verify no invalid properties exist
+    verifyPropertyCount(element, propertyCount);
+
+    return std::make_tuple(std::move(rules), std::move(chassis));
+}
+
+std::unique_ptr<Rule> parseRule(const json& element)
+{
+    verifyIsObject(element);
+    unsigned int propertyCount{0};
+
+    // Optional comments property; value not stored
+    if (element.contains("comments"))
+    {
+        ++propertyCount;
+    }
+
+    // Required id property
+    const json& idElement = getRequiredProperty(element, "id");
+    std::string id = parseString(idElement);
+    ++propertyCount;
+
+    // Required actions property
+    const json& actionsElement = getRequiredProperty(element, "actions");
+    std::vector<std::unique_ptr<Action>> actions =
+        parseActionArray(actionsElement);
+    ++propertyCount;
+
+    // Verify no invalid properties exist
+    verifyPropertyCount(element, propertyCount);
+
+    return std::make_unique<Rule>(id, std::move(actions));
+}
+
+std::vector<std::unique_ptr<Rule>> parseRuleArray(const json& element)
+{
+    verifyIsArray(element);
+    std::vector<std::unique_ptr<Rule>> rules;
+    for (auto& ruleElement : element)
+    {
+        rules.emplace_back(parseRule(ruleElement));
+    }
+    return rules;
+}
+
+} // namespace internal
+
+} // namespace phosphor::power::regulators::config_file_parser
diff --git a/phosphor-regulators/src/config_file_parser.hpp b/phosphor-regulators/src/config_file_parser.hpp
new file mode 100644
index 0000000..2b5ca0d
--- /dev/null
+++ b/phosphor-regulators/src/config_file_parser.hpp
@@ -0,0 +1,308 @@
+/**
+ * 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.
+ */
+#pragma once
+
+#include "action.hpp"
+#include "chassis.hpp"
+#include "pmbus_write_vout_command_action.hpp"
+#include "rule.hpp"
+
+#include <nlohmann/json.hpp>
+
+#include <cstdint>
+#include <filesystem>
+#include <memory>
+#include <stdexcept>
+#include <string>
+#include <tuple>
+#include <vector>
+
+namespace phosphor::power::regulators::config_file_parser
+{
+
+/**
+ * Parses the specified JSON configuration file.
+ *
+ * Returns the corresponding C++ Rule and Chassis objects.
+ *
+ * Throws a ConfigFileParserError if an error occurs.
+ *
+ * @param pathName configuration file path name
+ * @return tuple containing vectors of Rule and Chassis objects
+ */
+std::tuple<std::vector<std::unique_ptr<Rule>>,
+           std::vector<std::unique_ptr<Chassis>>>
+    parse(const std::filesystem::path& pathName);
+
+/*
+ * Internal implementation details for parse()
+ */
+namespace internal
+{
+
+/**
+ * Returns the specified property of the specified JSON element.
+ *
+ * Throws an invalid_argument exception if the property does not exist.
+ *
+ * @param element JSON element
+ * @param property property name
+ */
+inline const nlohmann::json& getRequiredProperty(const nlohmann::json& element,
+                                                 const std::string& property)
+{
+    auto it = element.find(property);
+    if (it == element.end())
+    {
+        throw std::invalid_argument{"Required property missing: " + property};
+    }
+    return *it;
+}
+
+/**
+ * Parses a JSON element containing an action.
+ *
+ * Returns the corresponding C++ Action object.
+ *
+ * Throws an exception if parsing fails.
+ *
+ * @param element JSON element
+ * @return Action object
+ */
+std::unique_ptr<Action> parseAction(const nlohmann::json& element);
+
+/**
+ * Parses a JSON element containing an array of actions.
+ *
+ * Returns the corresponding C++ Action objects.
+ *
+ * Throws an exception if parsing fails.
+ *
+ * @param element JSON element
+ * @return vector of Action objects
+ */
+std::vector<std::unique_ptr<Action>>
+    parseActionArray(const nlohmann::json& element);
+
+/**
+ * Parses a JSON element containing a boolean.
+ *
+ * Returns the corresponding C++ boolean value.
+ *
+ * Throws an exception if parsing fails.
+ *
+ * @param element JSON element
+ * @return boolean value
+ */
+inline bool parseBoolean(const nlohmann::json& element)
+{
+    // Verify element contains a boolean
+    if (!element.is_boolean())
+    {
+        throw std::invalid_argument{"Element is not a boolean"};
+    }
+    return element.get<bool>();
+}
+
+/**
+ * Parses a JSON element containing an array of chassis.
+ *
+ * Returns the corresponding C++ Chassis objects.
+ *
+ * Throws an exception if parsing fails.
+ *
+ * @param element JSON element
+ * @return vector of Chassis objects
+ */
+std::vector<std::unique_ptr<Chassis>>
+    parseChassisArray(const nlohmann::json& element);
+
+/**
+ * Parses a JSON element containing a double (floating point number).
+ *
+ * Returns the corresponding C++ double value.
+ *
+ * Throws an exception if parsing fails.
+ *
+ * @param element JSON element
+ * @return double value
+ */
+inline double parseDouble(const nlohmann::json& element)
+{
+    // Verify element contains a number (integer or floating point)
+    if (!element.is_number())
+    {
+        throw std::invalid_argument{"Element is not a number"};
+    }
+    return element.get<double>();
+}
+
+/**
+ * Parses a JSON element containing an 8-bit signed integer.
+ *
+ * Returns the corresponding C++ int8_t value.
+ *
+ * Throws an exception if parsing fails.
+ *
+ * @param element JSON element
+ * @return int8_t value
+ */
+inline int8_t parseInt8(const nlohmann::json& element)
+{
+    // Verify element contains an integer
+    if (!element.is_number_integer())
+    {
+        throw std::invalid_argument{"Element is not an integer"};
+    }
+    int value = element;
+    if ((value < INT8_MIN) || (value > INT8_MAX))
+    {
+        throw std::invalid_argument{"Element is not an 8-bit signed integer"};
+    }
+    return static_cast<int8_t>(value);
+}
+
+/**
+ * Parses a JSON element containing a pmbus_write_vout_command action.
+ *
+ * Returns the corresponding C++ PMBusWriteVoutCommandAction object.
+ *
+ * Throws an exception if parsing fails.
+ *
+ * @param element JSON element
+ * @return PMBusWriteVoutCommandAction object
+ */
+std::unique_ptr<PMBusWriteVoutCommandAction>
+    parsePMBusWriteVoutCommand(const nlohmann::json& element);
+
+/**
+ * Parses the JSON root element of the entire configuration file.
+ *
+ * Returns the corresponding C++ Rule and Chassis objects.
+ *
+ * Throws an exception if parsing fails.
+ *
+ * @param element JSON element
+ * @return tuple containing vectors of Rule and Chassis objects
+ */
+std::tuple<std::vector<std::unique_ptr<Rule>>,
+           std::vector<std::unique_ptr<Chassis>>>
+    parseRoot(const nlohmann::json& element);
+
+/**
+ * Parses a JSON element containing a rule.
+ *
+ * Returns the corresponding C++ Rule object.
+ *
+ * Throws an exception if parsing fails.
+ *
+ * @param element JSON element
+ * @return Rule object
+ */
+std::unique_ptr<Rule> parseRule(const nlohmann::json& element);
+
+/**
+ * Parses a JSON element containing an array of rules.
+ *
+ * Returns the corresponding C++ Rule objects.
+ *
+ * Throws an exception if parsing fails.
+ *
+ * @param element JSON element
+ * @return vector of Rule objects
+ */
+std::vector<std::unique_ptr<Rule>>
+    parseRuleArray(const nlohmann::json& element);
+
+/**
+ * Parses a JSON element containing a string.
+ *
+ * Returns the corresponding C++ string.
+ *
+ * Throws an exception if parsing fails.
+ *
+ * @param element JSON element
+ * @param isEmptyValid indicates whether an empty string value is valid
+ * @return string value
+ */
+inline std::string parseString(const nlohmann::json& element,
+                               bool isEmptyValid = false)
+{
+    if (!element.is_string())
+    {
+        throw std::invalid_argument{"Element is not a string"};
+    }
+    std::string value = element;
+    if (value.empty() && !isEmptyValid)
+    {
+        throw std::invalid_argument{"Element contains an empty string"};
+    }
+    return value;
+}
+
+/**
+ * Verifies that the specified JSON element is a JSON array.
+ *
+ * Throws an invalid_argument exception if the element is not an array.
+ *
+ * @param element JSON element
+ */
+inline void verifyIsArray(const nlohmann::json& element)
+{
+    if (!element.is_array())
+    {
+        throw std::invalid_argument{"Element is not an array"};
+    }
+}
+
+/**
+ * Verifies that the specified JSON element is a JSON object.
+ *
+ * Throws an invalid_argument exception if the element is not an object.
+ *
+ * @param element JSON element
+ */
+inline void verifyIsObject(const nlohmann::json& element)
+{
+    if (!element.is_object())
+    {
+        throw std::invalid_argument{"Element is not an object"};
+    }
+}
+
+/**
+ * Verifies that the specified JSON element contains the expected number of
+ * properties.
+ *
+ * Throws an invalid_argument exception if the element contains a different
+ * number of properties.  This indicates the element contains an invalid
+ * property.
+ *
+ * @param element JSON element
+ * @param expectedCount expected number of properties in element
+ */
+inline void verifyPropertyCount(const nlohmann::json& element,
+                                unsigned int expectedCount)
+{
+    if (element.size() != expectedCount)
+    {
+        throw std::invalid_argument{"Element contains an invalid property"};
+    }
+}
+
+} // namespace internal
+
+} // namespace phosphor::power::regulators::config_file_parser
diff --git a/phosphor-regulators/src/meson.build b/phosphor-regulators/src/meson.build
index b652f08..ef40cb9 100644
--- a/phosphor-regulators/src/meson.build
+++ b/phosphor-regulators/src/meson.build
@@ -4,6 +4,7 @@
 )
 
 phosphor_regulators_library_source_files = [
+    'config_file_parser.cpp',
     'id_map.cpp',
     'pmbus_utils.cpp',
 
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");
+    }
+}
diff --git a/phosphor-regulators/test/meson.build b/phosphor-regulators/test/meson.build
index 53a90c2..e75176d 100644
--- a/phosphor-regulators/test/meson.build
+++ b/phosphor-regulators/test/meson.build
@@ -6,6 +6,7 @@
 phosphor_regulators_tests_source_files = [
     'chassis_tests.cpp',
     'config_file_parser_error_tests.cpp',
+    'config_file_parser_tests.cpp',
     'configuration_tests.cpp',
     'device_tests.cpp',
     'id_map_tests.cpp',