pseq: Create standalone config file parser

Create a standalone JSON configuration file parser for the
phosphor-power-sequencer application.  Support the new config file
properties in the parser.

Separating the config file parser from the rest of the code makes it
easier to implement and test.

Create automated test cases to verify the parser functions.

Change-Id: Ia753ffadf395359899964a612e479b00377ad495
Signed-off-by: Shawn McCarney <shawnmm@us.ibm.com>
diff --git a/phosphor-power-sequencer/docs/config_file/rail.md b/phosphor-power-sequencer/docs/config_file/rail.md
index d369405..982c6a6 100644
--- a/phosphor-power-sequencer/docs/config_file/rail.md
+++ b/phosphor-power-sequencer/docs/config_file/rail.md
@@ -22,7 +22,7 @@
 ### Notes
 
 - The "page" property is required if the "check_status_vout" or
-  "compare_voltage_to_limits" property is specified.
+  "compare_voltage_to_limits" property is true.
 
 ## Examples
 
diff --git a/phosphor-power-sequencer/src/config_file_parser.cpp b/phosphor-power-sequencer/src/config_file_parser.cpp
new file mode 100644
index 0000000..db055d1
--- /dev/null
+++ b/phosphor-power-sequencer/src/config_file_parser.cpp
@@ -0,0 +1,173 @@
+/**
+ * Copyright © 2024 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 <exception>
+#include <fstream>
+#include <optional>
+
+using json = nlohmann::json;
+
+namespace phosphor::power::sequencer::config_file_parser
+{
+
+std::vector<std::unique_ptr<Rail>> 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
+{
+
+GPIO parseGPIO(const json& element)
+{
+    verifyIsObject(element);
+    unsigned int propertyCount{0};
+
+    // Required line property
+    const json& lineElement = getRequiredProperty(element, "line");
+    unsigned int line = parseUnsignedInteger(lineElement);
+    ++propertyCount;
+
+    // Optional active_low property
+    bool activeLow{false};
+    auto activeLowIt = element.find("active_low");
+    if (activeLowIt != element.end())
+    {
+        activeLow = parseBoolean(*activeLowIt);
+        ++propertyCount;
+    }
+
+    // Verify no invalid properties exist
+    verifyPropertyCount(element, propertyCount);
+
+    return GPIO(line, activeLow);
+}
+
+std::unique_ptr<Rail> parseRail(const json& element)
+{
+    verifyIsObject(element);
+    unsigned int propertyCount{0};
+
+    // Required name property
+    const json& nameElement = getRequiredProperty(element, "name");
+    std::string name = parseString(nameElement);
+    ++propertyCount;
+
+    // Optional presence property
+    std::optional<std::string> presence{};
+    auto presenceIt = element.find("presence");
+    if (presenceIt != element.end())
+    {
+        presence = parseString(*presenceIt);
+        ++propertyCount;
+    }
+
+    // Optional page property
+    std::optional<uint8_t> page{};
+    auto pageIt = element.find("page");
+    if (pageIt != element.end())
+    {
+        page = parseUint8(*pageIt);
+        ++propertyCount;
+    }
+
+    // Optional check_status_vout property
+    bool checkStatusVout{false};
+    auto checkStatusVoutIt = element.find("check_status_vout");
+    if (checkStatusVoutIt != element.end())
+    {
+        checkStatusVout = parseBoolean(*checkStatusVoutIt);
+        ++propertyCount;
+    }
+
+    // Optional compare_voltage_to_limits property
+    bool compareVoltageToLimits{false};
+    auto compareVoltageToLimitsIt = element.find("compare_voltage_to_limits");
+    if (compareVoltageToLimitsIt != element.end())
+    {
+        compareVoltageToLimits = parseBoolean(*compareVoltageToLimitsIt);
+        ++propertyCount;
+    }
+
+    // Optional gpio property
+    std::optional<GPIO> gpio{};
+    auto gpioIt = element.find("gpio");
+    if (gpioIt != element.end())
+    {
+        gpio = parseGPIO(*gpioIt);
+        ++propertyCount;
+    }
+
+    // If check_status_vout or compare_voltage_to_limits property is true,
+    // the page property is required; verify page was specified
+    if ((checkStatusVout || compareVoltageToLimits) && !page.has_value())
+    {
+        throw std::invalid_argument{"Required property missing: page"};
+    }
+
+    // Verify no invalid properties exist
+    verifyPropertyCount(element, propertyCount);
+
+    return std::make_unique<Rail>(name, presence, page, checkStatusVout,
+                                  compareVoltageToLimits, gpio);
+}
+
+std::vector<std::unique_ptr<Rail>> parseRailArray(const json& element)
+{
+    verifyIsArray(element);
+    std::vector<std::unique_ptr<Rail>> rails;
+    for (auto& railElement : element)
+    {
+        rails.emplace_back(parseRail(railElement));
+    }
+    return rails;
+}
+
+std::vector<std::unique_ptr<Rail>> parseRoot(const json& element)
+{
+    verifyIsObject(element);
+    unsigned int propertyCount{0};
+
+    // Required rails property
+    const json& railsElement = getRequiredProperty(element, "rails");
+    std::vector<std::unique_ptr<Rail>> rails = parseRailArray(railsElement);
+    ++propertyCount;
+
+    // Verify no invalid properties exist
+    verifyPropertyCount(element, propertyCount);
+
+    return rails;
+}
+
+} // namespace internal
+
+} // namespace phosphor::power::sequencer::config_file_parser
diff --git a/phosphor-power-sequencer/src/config_file_parser.hpp b/phosphor-power-sequencer/src/config_file_parser.hpp
new file mode 100644
index 0000000..3764b7c
--- /dev/null
+++ b/phosphor-power-sequencer/src/config_file_parser.hpp
@@ -0,0 +1,265 @@
+/**
+ * Copyright © 2024 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 "rail.hpp"
+
+#include <nlohmann/json.hpp>
+
+#include <cstdint>
+#include <filesystem>
+#include <stdexcept>
+#include <string>
+#include <vector>
+
+namespace phosphor::power::sequencer::config_file_parser
+{
+
+/**
+ * Parses the specified JSON configuration file.
+ *
+ * Returns the corresponding C++ Rail objects.
+ *
+ * Throws a ConfigFileParserError if an error occurs.
+ *
+ * @param pathName configuration file path name
+ * @return vector of Rail objects
+ */
+std::vector<std::unique_ptr<Rail>> 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
+ */
+#pragma GCC diagnostic push
+#if __GNUC__ == 13
+#pragma GCC diagnostic ignored "-Wdangling-reference"
+#endif
+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;
+}
+#pragma GCC diagnostic pop
+
+/**
+ * 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 a GPIO.
+ *
+ * Returns the corresponding C++ GPIO object.
+ *
+ * Throws an exception if parsing fails.
+ *
+ * @param element JSON element
+ * @return GPIO object
+ */
+GPIO parseGPIO(const nlohmann::json& element);
+
+/**
+ * Parses a JSON element containing a rail.
+ *
+ * Returns the corresponding C++ Rail object.
+ *
+ * Throws an exception if parsing fails.
+ *
+ * @param element JSON element
+ * @return Rail object
+ */
+std::unique_ptr<Rail> parseRail(const nlohmann::json& element);
+
+/**
+ * Parses a JSON element containing an array of rails.
+ *
+ * Returns the corresponding C++ Rail objects.
+ *
+ * Throws an exception if parsing fails.
+ *
+ * @param element JSON element
+ * @return vector of Rail objects
+ */
+std::vector<std::unique_ptr<Rail>>
+    parseRailArray(const nlohmann::json& element);
+
+/**
+ * Parses the JSON root element of the entire configuration file.
+ *
+ * Returns the corresponding C++ Rail objects.
+ *
+ * Throws an exception if parsing fails.
+ *
+ * @param element JSON element
+ * @return vector of Rail objects
+ */
+std::vector<std::unique_ptr<Rail>> parseRoot(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.get<std::string>();
+    if (value.empty() && !isEmptyValid)
+    {
+        throw std::invalid_argument{"Element contains an empty string"};
+    }
+    return value;
+}
+
+/**
+ * Parses a JSON element containing an 8-bit unsigned integer.
+ *
+ * Returns the corresponding C++ uint8_t value.
+ *
+ * Throws an exception if parsing fails.
+ *
+ * @param element JSON element
+ * @return uint8_t value
+ */
+inline uint8_t parseUint8(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.get<int>();
+    if ((value < 0) || (value > UINT8_MAX))
+    {
+        throw std::invalid_argument{"Element is not an 8-bit unsigned integer"};
+    }
+    return static_cast<uint8_t>(value);
+}
+
+/**
+ * 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.
+ *
+ * @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::sequencer::config_file_parser
diff --git a/phosphor-power-sequencer/src/config_file_parser_error.hpp b/phosphor-power-sequencer/src/config_file_parser_error.hpp
new file mode 100644
index 0000000..4c77952
--- /dev/null
+++ b/phosphor-power-sequencer/src/config_file_parser_error.hpp
@@ -0,0 +1,85 @@
+/**
+ * Copyright © 2024 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 <exception>
+#include <filesystem>
+#include <string>
+
+namespace phosphor::power::sequencer
+{
+
+/**
+ * @class ConfigFileParserError
+ *
+ * An error that occurred while parsing a JSON configuration file.
+ */
+class ConfigFileParserError : public std::exception
+{
+  public:
+    // Specify which compiler-generated methods we want
+    ConfigFileParserError() = delete;
+    ConfigFileParserError(const ConfigFileParserError&) = default;
+    ConfigFileParserError(ConfigFileParserError&&) = default;
+    ConfigFileParserError& operator=(const ConfigFileParserError&) = default;
+    ConfigFileParserError& operator=(ConfigFileParserError&&) = default;
+    virtual ~ConfigFileParserError() = default;
+
+    /**
+     * Constructor.
+     *
+     * @param pathName Configuration file path name
+     * @param error Error message
+     */
+    explicit ConfigFileParserError(const std::filesystem::path& pathName,
+                                   const std::string& error) :
+        pathName{pathName},
+        error{"ConfigFileParserError: " + pathName.string() + ": " + error}
+    {}
+
+    /**
+     * Returns the configuration file path name.
+     *
+     * @return path name
+     */
+    const std::filesystem::path& getPathName()
+    {
+        return pathName;
+    }
+
+    /**
+     * Returns the description of this error.
+     *
+     * @return error description
+     */
+    const char* what() const noexcept override
+    {
+        return error.c_str();
+    }
+
+  private:
+    /**
+     * Configuration file path name.
+     */
+    const std::filesystem::path pathName;
+
+    /**
+     * Error message.
+     */
+    const std::string error{};
+};
+
+} // namespace phosphor::power::sequencer
diff --git a/phosphor-power-sequencer/src/meson.build b/phosphor-power-sequencer/src/meson.build
index de691c9..faab116 100644
--- a/phosphor-power-sequencer/src/meson.build
+++ b/phosphor-power-sequencer/src/meson.build
@@ -3,6 +3,18 @@
     '../..'
 )
 
+phosphor_power_sequencer_library = static_library(
+    'phosphor-power-sequencer',
+    'config_file_parser.cpp',
+    implicit_include_directories: false,
+    dependencies: [
+        nlohmann_json_dep
+    ],
+    include_directories: [
+        phosphor_power_sequencer_include_directories
+    ]
+)
+
 phosphor_power_sequencer = executable(
     'phosphor-power-control',
     'power_control_main.cpp',
diff --git a/phosphor-power-sequencer/test/config_file_parser_error_tests.cpp b/phosphor-power-sequencer/test/config_file_parser_error_tests.cpp
new file mode 100644
index 0000000..9af389a
--- /dev/null
+++ b/phosphor-power-sequencer/test/config_file_parser_error_tests.cpp
@@ -0,0 +1,53 @@
+/**
+ * Copyright © 2024 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_error.hpp"
+
+#include <filesystem>
+
+#include <gtest/gtest.h>
+
+using namespace phosphor::power::sequencer;
+
+TEST(ConfigFileParserErrorTests, Constructor)
+{
+    std::filesystem::path pathName{
+        "/usr/share/phosphor-power-sequencer/foo.json"};
+    ConfigFileParserError error{pathName, "Required property missing"};
+    EXPECT_EQ(error.getPathName(), pathName);
+    EXPECT_STREQ(
+        error.what(),
+        "ConfigFileParserError: /usr/share/phosphor-power-sequencer/foo.json: "
+        "Required property missing");
+}
+
+TEST(ConfigFileParserErrorTests, GetPathName)
+{
+    std::filesystem::path pathName{
+        "/usr/share/phosphor-power-sequencer/bar.json"};
+    ConfigFileParserError error{pathName, "Required property missing"};
+    EXPECT_EQ(error.getPathName(), pathName);
+}
+
+TEST(ConfigFileParserErrorTests, What)
+{
+    std::filesystem::path pathName{
+        "/usr/share/phosphor-power-sequencer/bar.json"};
+    ConfigFileParserError error{pathName, "Required property missing"};
+    EXPECT_STREQ(
+        error.what(),
+        "ConfigFileParserError: /usr/share/phosphor-power-sequencer/bar.json: "
+        "Required property missing");
+}
diff --git a/phosphor-power-sequencer/test/config_file_parser_tests.cpp b/phosphor-power-sequencer/test/config_file_parser_tests.cpp
new file mode 100644
index 0000000..bfc9680
--- /dev/null
+++ b/phosphor-power-sequencer/test/config_file_parser_tests.cpp
@@ -0,0 +1,877 @@
+/**
+ * Copyright © 2024 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 "rail.hpp"
+#include "temporary_file.hpp"
+
+#include <sys/stat.h> // for chmod()
+
+#include <nlohmann/json.hpp>
+
+#include <cstdint>
+#include <exception>
+#include <filesystem>
+#include <fstream>
+#include <memory>
+#include <optional>
+#include <stdexcept>
+#include <string>
+#include <vector>
+
+#include <gtest/gtest.h>
+
+using namespace phosphor::power::sequencer;
+using namespace phosphor::power::sequencer::config_file_parser;
+using namespace phosphor::power::sequencer::config_file_parser::internal;
+using json = nlohmann::json;
+using TemporaryFile = phosphor::power::util::TemporaryFile;
+
+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"(
+            {
+                "rails": [
+                    {
+                        "name": "VDD_CPU0",
+                        "page": 11,
+                        "check_status_vout": true
+                    },
+                    {
+                        "name": "VCS_CPU1",
+                        "presence": "/xyz/openbmc_project/inventory/system/chassis/motherboard/cpu1",
+                        "gpio": { "line": 60 }
+                    }
+                ]
+            }
+        )"_json;
+
+        TemporaryFile configFile;
+        std::filesystem::path pathName{configFile.getPath()};
+        writeConfigFile(pathName, configFileContents);
+
+        std::vector<std::unique_ptr<Rail>> rails = parse(pathName);
+
+        EXPECT_EQ(rails.size(), 2);
+        EXPECT_EQ(rails[0]->getName(), "VDD_CPU0");
+        EXPECT_EQ(rails[1]->getName(), "VCS_CPU1");
+    }
+
+    // Test where fails: File does not exist
+    {
+        std::filesystem::path pathName{"/tmp/non_existent_file"};
+        EXPECT_THROW(parse(pathName), ConfigFileParserError);
+    }
+
+    // Test where fails: File is not readable
+    {
+        const json configFileContents = R"(
+            {
+                "rails": [
+                    {
+                        "name": "VDD_CPU0"
+                    }
+                ]
+            }
+        )"_json;
+
+        TemporaryFile configFile;
+        std::filesystem::path pathName{configFile.getPath()};
+        writeConfigFile(pathName, configFileContents);
+
+        chmod(pathName.c_str(), 0222);
+        EXPECT_THROW(parse(pathName), ConfigFileParserError);
+    }
+
+    // Test where fails: File is not valid JSON
+    {
+        const std::string configFileContents = "] foo [";
+
+        TemporaryFile configFile;
+        std::filesystem::path pathName{configFile.getPath()};
+        writeConfigFile(pathName, configFileContents);
+
+        EXPECT_THROW(parse(pathName), ConfigFileParserError);
+    }
+
+    // Test where fails: JSON does not conform to config file format
+    {
+        const json configFileContents = R"( [ "foo", "bar" ] )"_json;
+
+        TemporaryFile configFile;
+        std::filesystem::path pathName{configFile.getPath()};
+        writeConfigFile(pathName, configFileContents);
+
+        EXPECT_THROW(parse(pathName), ConfigFileParserError);
+    }
+}
+
+TEST(ConfigFileParserTests, GetRequiredProperty)
+{
+    // Test where property exists
+    {
+        const json element = R"( { "name": "VDD_CPU0" } )"_json;
+        const json& propertyElement = getRequiredProperty(element, "name");
+        EXPECT_EQ(propertyElement.get<std::string>(), "VDD_CPU0");
+    }
+
+    // Test where property does not exist
+    try
+    {
+        const json element = R"( { "foo": 23 } )"_json;
+        getRequiredProperty(element, "name");
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Required property missing: name");
+    }
+}
+
+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, ParseGPIO)
+{
+    // Test where works: Only required properties specified
+    {
+        const json element = R"(
+            {
+                "line": 60
+            }
+        )"_json;
+        GPIO gpio = parseGPIO(element);
+        EXPECT_EQ(gpio.line, 60);
+        EXPECT_FALSE(gpio.activeLow);
+    }
+
+    // Test where works: All properties specified
+    {
+        const json element = R"(
+            {
+                "line": 131,
+                "active_low": true
+            }
+        )"_json;
+        GPIO gpio = parseGPIO(element);
+        EXPECT_EQ(gpio.line, 131);
+        EXPECT_TRUE(gpio.activeLow);
+    }
+
+    // Test where fails: Element is not an object
+    try
+    {
+        const json element = R"( [ "vdda", "vddb" ] )"_json;
+        parseGPIO(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: Required line property not specified
+    try
+    {
+        const json element = R"(
+            {
+                "active_low": true
+            }
+        )"_json;
+        parseGPIO(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Required property missing: line");
+    }
+
+    // Test where fails: line value is invalid
+    try
+    {
+        const json element = R"(
+            {
+                "line": -131,
+                "active_low": true
+            }
+        )"_json;
+        parseGPIO(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: active_low value is invalid
+    try
+    {
+        const json element = R"(
+            {
+                "line": 131,
+                "active_low": "true"
+            }
+        )"_json;
+        parseGPIO(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"(
+            {
+                "line": 131,
+                "foo": "bar"
+            }
+        )"_json;
+        parseGPIO(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, ParseRail)
+{
+    // Test where works: Only required properties specified
+    {
+        const json element = R"(
+            {
+                "name": "VDD_CPU0"
+            }
+        )"_json;
+        std::unique_ptr<Rail> rail = parseRail(element);
+        EXPECT_EQ(rail->getName(), "VDD_CPU0");
+        EXPECT_FALSE(rail->getPresence().has_value());
+        EXPECT_FALSE(rail->getPage().has_value());
+        EXPECT_FALSE(rail->getCheckStatusVout());
+        EXPECT_FALSE(rail->getCompareVoltageToLimits());
+        EXPECT_FALSE(rail->getGPIO().has_value());
+    }
+
+    // Test where works: All properties specified
+    {
+        const json element = R"(
+            {
+                "name": "VCS_CPU1",
+                "presence": "/xyz/openbmc_project/inventory/system/chassis/motherboard/cpu1",
+                "page": 11,
+                "check_status_vout": true,
+                "compare_voltage_to_limits": true,
+                "gpio": { "line": 60, "active_low": true }
+            }
+        )"_json;
+        std::unique_ptr<Rail> rail = parseRail(element);
+        EXPECT_EQ(rail->getName(), "VCS_CPU1");
+        EXPECT_TRUE(rail->getPresence().has_value());
+        EXPECT_EQ(
+            rail->getPresence().value(),
+            "/xyz/openbmc_project/inventory/system/chassis/motherboard/cpu1");
+        EXPECT_TRUE(rail->getPage().has_value());
+        EXPECT_EQ(rail->getPage().value(), 11);
+        EXPECT_TRUE(rail->getCheckStatusVout());
+        EXPECT_TRUE(rail->getCompareVoltageToLimits());
+        EXPECT_TRUE(rail->getGPIO().has_value());
+        EXPECT_EQ(rail->getGPIO().value().line, 60);
+        EXPECT_TRUE(rail->getGPIO().value().activeLow);
+    }
+
+    // Test where fails: Element is not an object
+    try
+    {
+        const json element = R"( [ "vdda", "vddb" ] )"_json;
+        parseRail(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: Required name property not specified
+    try
+    {
+        const json element = R"(
+            {
+                "page": 11
+            }
+        )"_json;
+        parseRail(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Required property missing: name");
+    }
+
+    // Test where fails: name value is invalid
+    try
+    {
+        const json element = R"(
+            {
+                "name": 31,
+                "page": 11
+            }
+        )"_json;
+        parseRail(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: presence value is invalid
+    try
+    {
+        const json element = R"(
+            {
+                "name": "VCS_CPU1",
+                "presence": false
+            }
+        )"_json;
+        parseRail(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: page value is invalid
+    try
+    {
+        const json element = R"(
+            {
+                "name": "VCS_CPU1",
+                "page": 256
+            }
+        )"_json;
+        parseRail(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 unsigned integer");
+    }
+
+    // Test where fails: check_status_vout value is invalid
+    try
+    {
+        const json element = R"(
+            {
+                "name": "VCS_CPU1",
+                "check_status_vout": "false"
+            }
+        )"_json;
+        parseRail(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: compare_voltage_to_limits value is invalid
+    try
+    {
+        const json element = R"(
+            {
+                "name": "VCS_CPU1",
+                "compare_voltage_to_limits": 23
+            }
+        )"_json;
+        parseRail(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: gpio value is invalid
+    try
+    {
+        const json element = R"(
+            {
+                "name": "VCS_CPU1",
+                "gpio": 131
+            }
+        )"_json;
+        parseRail(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: check_status_vout is true and page not specified
+    try
+    {
+        const json element = R"(
+            {
+                "name": "VCS_CPU1",
+                "check_status_vout": true
+            }
+        )"_json;
+        parseRail(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Required property missing: page");
+    }
+
+    // Test where fails: compare_voltage_to_limits is true and page not
+    // specified
+    try
+    {
+        const json element = R"(
+            {
+                "name": "VCS_CPU1",
+                "compare_voltage_to_limits": true
+            }
+        )"_json;
+        parseRail(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Required property missing: page");
+    }
+
+    // Test where fails: Invalid property specified
+    try
+    {
+        const json element = R"(
+            {
+                "name": "VCS_CPU1",
+                "foo": "bar"
+            }
+        )"_json;
+        parseRail(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, ParseRailArray)
+{
+    // Test where works: Array is empty
+    {
+        const json element = R"(
+            [
+            ]
+        )"_json;
+        std::vector<std::unique_ptr<Rail>> rails = parseRailArray(element);
+        EXPECT_EQ(rails.size(), 0);
+    }
+
+    // Test where works: Array is not empty
+    {
+        const json element = R"(
+            [
+                { "name": "VDD_CPU0" },
+                { "name": "VCS_CPU1" }
+            ]
+        )"_json;
+        std::vector<std::unique_ptr<Rail>> rails = parseRailArray(element);
+        EXPECT_EQ(rails.size(), 2);
+        EXPECT_EQ(rails[0]->getName(), "VDD_CPU0");
+        EXPECT_EQ(rails[1]->getName(), "VCS_CPU1");
+    }
+
+    // Test where fails: Element is not an array
+    try
+    {
+        const json element = R"(
+            {
+                "foo": "bar"
+            }
+        )"_json;
+        parseRailArray(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: Element within array is invalid
+    try
+    {
+        const json element = R"(
+            [
+                { "name": "VDD_CPU0" },
+                23
+            ]
+        )"_json;
+        parseRailArray(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, ParseRoot)
+{
+    // Test where works
+    {
+        const json element = R"(
+            {
+                "rails": [
+                    {
+                        "name": "VDD_CPU0",
+                        "page": 11,
+                        "check_status_vout": true
+                    },
+                    {
+                        "name": "VCS_CPU1",
+                        "presence": "/xyz/openbmc_project/inventory/system/chassis/motherboard/cpu1",
+                        "gpio": { "line": 60 }
+                    }
+                ]
+            }
+        )"_json;
+        std::vector<std::unique_ptr<Rail>> rails = parseRoot(element);
+        EXPECT_EQ(rails.size(), 2);
+        EXPECT_EQ(rails[0]->getName(), "VDD_CPU0");
+        EXPECT_EQ(rails[1]->getName(), "VCS_CPU1");
+    }
+
+    // Test where fails: Element is not an object
+    try
+    {
+        const json element = R"( [ "VDD_CPU0", "VCS_CPU1" ] )"_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: Required rails property not specified
+    try
+    {
+        const json element = R"(
+            {
+            }
+        )"_json;
+        parseRoot(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Required property missing: rails");
+    }
+
+    // Test where fails: rails value is invalid
+    try
+    {
+        const json element = R"(
+            {
+                "rails": 31
+            }
+        )"_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 array");
+    }
+
+    // Test where fails: Invalid property specified
+    try
+    {
+        const json element = R"(
+            {
+                "rails": [
+                    {
+                        "name": "VDD_CPU0",
+                        "page": 11,
+                        "check_status_vout": true
+                    }
+                ],
+                "foo": true
+            }
+        )"_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, 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_cpu1";
+        std::string value = parseString(element, false);
+        EXPECT_EQ(value, "vdd_cpu1");
+    }
+
+    // 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, ParseUint8)
+{
+    // Test where works: 0
+    {
+        const json element = R"( 0 )"_json;
+        uint8_t value = parseUint8(element);
+        EXPECT_EQ(value, 0);
+    }
+
+    // Test where works: UINT8_MAX
+    {
+        const json element = R"( 255 )"_json;
+        uint8_t value = parseUint8(element);
+        EXPECT_EQ(value, 255);
+    }
+
+    // Test where fails: Element is not an integer
+    try
+    {
+        const json element = R"( 1.03 )"_json;
+        parseUint8(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 < 0
+    try
+    {
+        const json element = R"( -1 )"_json;
+        parseUint8(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 unsigned integer");
+    }
+
+    // Test where fails: Value > UINT8_MAX
+    try
+    {
+        const json element = R"( 256 )"_json;
+        parseUint8(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 unsigned integer");
+    }
+}
+
+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
+    {
+        const json element = R"( [ "foo", "bar" ] )"_json;
+        verifyIsArray(element);
+    }
+
+    // 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
+    {
+        const json element = R"( { "foo": "bar" } )"_json;
+        verifyIsObject(element);
+    }
+
+    // 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
+    {
+        const json element = R"(
+            {
+                "line": 131,
+                "active_low": true
+            }
+        )"_json;
+        verifyPropertyCount(element, 2);
+    }
+
+    // Test where element has unexpected number of properties
+    try
+    {
+        const json element = R"(
+            {
+                "line": 131,
+                "active_low": true,
+                "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-power-sequencer/test/meson.build b/phosphor-power-sequencer/test/meson.build
index ab03041..4f6b167 100644
--- a/phosphor-power-sequencer/test/meson.build
+++ b/phosphor-power-sequencer/test/meson.build
@@ -1,5 +1,7 @@
 test('phosphor-power-sequencer-tests',
      executable('phosphor-power-sequencer-tests',
+                'config_file_parser_error_tests.cpp',
+                'config_file_parser_tests.cpp',
                 'rail_tests.cpp',
                 dependencies: [
                     gtest,
@@ -7,10 +9,15 @@
                 ],
                 link_args: dynamic_linker,
                 build_rpath: get_option('oe-sdk').allowed() ? rpath : '',
+                link_with: [
+                    phosphor_power_sequencer_library,
+                    libpower
+                ],
                 implicit_include_directories: false,
                 include_directories: [
                     '.',
-                    '../src'
+                    '../src',
+                    '../..'
                 ]
      )
 )