Add Unit Test for TemplateCharReplace

We keep finding issues in this function, add some tests.

Tested: The tests pass, this is mostly just moving the
code into a header. One test didn't work, but it didn't
work before either. It will be fixed later

Change-Id: I3eb60960104e861b0a0313580dc90bfb12051829
Signed-off-by: James Feist <james.feist@linux.intel.com>
diff --git a/CMakeLists.txt b/CMakeLists.txt
index c57956c..d05b6d8 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -4,6 +4,12 @@
 set (CMAKE_CXX_STANDARD 17)
 set (CMAKE_CXX_STANDARD_REQUIRED ON)
 set (CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake ${CMAKE_MODULE_PATH})
+include ("cmake/HunterGate.cmake")
+huntergate (URL "https://github.com/ruslo/hunter/archive/v0.18.64.tar.gz" SHA1
+            "baf9c8cc4f65306f0e442b5419967b4c4c04589a")
+
+project (entity-manager CXX)
+
 set (
     CMAKE_CXX_FLAGS
     "${CMAKE_CXX_FLAGS} -lstdc++fs \
@@ -92,12 +98,23 @@
 
     include_directories (SYSTEM ${CMAKE_BINARY_DIR}/boost-src)
     set (CMAKE_PREFIX_PATH ${CMAKE_BINARY_DIR}/boost-src ${CMAKE_PREFIX_PATH})
-    option (ENABLE_TEST "Enable Google Test" OFF)
-    if (ENABLE_TEST)
-        hunter_add_package (GTest)
-        find_package (GTest CONFIG REQUIRED)
-        enable_testing ()
-    endif ()
+
+    option (HUNTER_ENABLED "Enable hunter package pulling" ON)
+    hunter_add_package (GTest)
+
+    find_package (GTest CONFIG REQUIRED)
+
+    enable_testing ()
+
+    add_executable (entityManagerTests test/test_entity-manager.cpp
+                    src/Utils.cpp)
+    add_test (NAME test_entitymanager COMMAND entityManagerTests)
+    target_link_libraries (entityManagerTests GTest::main GTest::gtest)
+    target_link_libraries (entityManagerTests -lsystemd)
+    target_link_libraries (entityManagerTests stdc++fs)
+    target_link_libraries (entityManagerTests ${Boost_LIBRARIES})
+    target_link_libraries (entityManagerTests sdbusplus)
+
 endif ()
 
 add_definitions (-DBOOST_ERROR_CODE_HEADER_ONLY)
@@ -133,6 +150,9 @@
     add_dependencies (entity-manager nlohmann-json)
     add_dependencies (entity-manager sdbusplus-project)
     add_dependencies (entity-manager valijson)
+    add_dependencies (entityManagerTests nlohmann-json)
+    add_dependencies (entityManagerTests sdbusplus-project)
+    add_dependencies (entityManagerTests valijson)
     add_dependencies (fru-device nlohmann-json)
     add_dependencies (fru-device valijson)
     add_dependencies (fru-device sdbusplus-project)
diff --git a/include/EntityManager.hpp b/include/EntityManager.hpp
index 548cc5f..4d5de34 100644
--- a/include/EntityManager.hpp
+++ b/include/EntityManager.hpp
@@ -105,13 +105,4 @@
                     "REDFISH_MESSAGE_ID=%s", "OpenBMC.0.1.InventoryRemoved",
                     "REDFISH_MESSAGE_ARGS=%s,%s,%s", model.c_str(),
                     type.c_str(), sn.c_str(), NULL);
-}
-
-enum class TemplateOperation
-{
-    addition,
-    division,
-    multiplication,
-    subtraction,
-    modulo,
-};
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/include/Utils.hpp b/include/Utils.hpp
index e57d146..7f8076c 100644
--- a/include/Utils.hpp
+++ b/include/Utils.hpp
@@ -15,6 +15,7 @@
 */
 
 #pragma once
+
 #include <boost/container/flat_map.hpp>
 #include <filesystem>
 #include <fstream>
@@ -27,6 +28,19 @@
 constexpr const char* versionHashFile = "/var/configuration/version";
 constexpr const char* versionFile = "/etc/os-release";
 
+using BasicVariantType =
+    std::variant<std::string, int64_t, uint64_t, double, int32_t, uint32_t,
+                 int16_t, uint16_t, uint8_t, bool>;
+
+enum class TemplateOperation
+{
+    addition,
+    division,
+    multiplication,
+    subtraction,
+    modulo,
+};
+
 bool findFiles(const std::filesystem::path& dirPath,
                const std::string& matchString,
                std::vector<std::filesystem::path>& foundPaths);
@@ -94,4 +108,10 @@
     std::ofstream output(versionHashFile);
     output << expectedHash;
     return false;
-}
\ No newline at end of file
+}
+
+void templateCharReplace(
+    nlohmann::json::iterator& keyPair,
+    const boost::container::flat_map<std::string, BasicVariantType>&
+        foundDevice,
+    const size_t foundDeviceIdx);
diff --git a/src/EntityManager.cpp b/src/EntityManager.cpp
index f135c20..25cc86c 100644
--- a/src/EntityManager.cpp
+++ b/src/EntityManager.cpp
@@ -16,6 +16,8 @@
 
 #include "EntityManager.hpp"
 
+#include "VariantVisitors.hpp"
+
 #include <Overlay.hpp>
 #include <Utils.hpp>
 #include <VariantVisitors.hpp>
@@ -26,7 +28,6 @@
 #include <boost/algorithm/string/split.hpp>
 #include <boost/container/flat_map.hpp>
 #include <boost/container/flat_set.hpp>
-#include <boost/lexical_cast.hpp>
 #include <boost/range/iterator_range.hpp>
 #include <filesystem>
 #include <fstream>
@@ -43,7 +44,6 @@
 constexpr const char* lastConfiguration = "/tmp/configuration/last.json";
 constexpr const char* currentConfiguration = "/var/configuration/system.json";
 constexpr const char* globalSchema = "global.json";
-constexpr const char* templateChar = "$";
 constexpr const int32_t MAX_MAPPER_DEPTH = 0;
 
 constexpr const bool DEBUG = false;
@@ -82,10 +82,6 @@
     std::variant<std::vector<std::string>, std::vector<double>, std::string,
                  int64_t, uint64_t, double, int32_t, uint32_t, int16_t,
                  uint16_t, uint8_t, bool>;
-using BasicVariantType =
-    std::variant<std::string, int64_t, uint64_t, double, int32_t, uint32_t,
-                 int16_t, uint16_t, uint8_t, bool>;
-
 using GetSubTreeType = std::vector<
     std::pair<std::string,
               std::vector<std::pair<std::string, std::vector<std::string>>>>>;
@@ -1124,216 +1120,6 @@
     }
 }
 
-// finds the template character (currently set to $) and replaces the value with
-// the field found in a dbus object i.e. $ADDRESS would get populated with the
-// ADDRESS field from a object on dbus
-void templateCharReplace(
-    nlohmann::json::iterator& keyPair,
-    const boost::container::flat_map<std::string, BasicVariantType>&
-        foundDevice,
-    size_t& foundDeviceIdx)
-{
-    if (keyPair.value().type() == nlohmann::json::value_t::object ||
-        keyPair.value().type() == nlohmann::json::value_t::array)
-    {
-        for (auto nextLayer = keyPair.value().begin();
-             nextLayer != keyPair.value().end(); nextLayer++)
-        {
-            templateCharReplace(nextLayer, foundDevice, foundDeviceIdx);
-        }
-        return;
-    }
-
-    std::string* strPtr = keyPair.value().get_ptr<std::string*>();
-    if (strPtr == nullptr)
-    {
-        return;
-    }
-
-    boost::replace_all(*strPtr, std::string(templateChar) + "index",
-                       std::to_string(foundDeviceIdx));
-
-    for (auto& foundDevicePair : foundDevice)
-    {
-        std::string templateName = templateChar + foundDevicePair.first;
-        boost::iterator_range<std::string::const_iterator> find =
-            boost::ifind_first(*strPtr, templateName);
-        if (find)
-        {
-            size_t start = find.begin() - strPtr->begin();
-            // check for additional operations
-            if (find.end() == strPtr->end())
-            {
-                std::visit([&](auto&& val) { keyPair.value() = val; },
-                           foundDevicePair.second);
-                return;
-            }
-
-            // save the prefix
-            std::string prefix = strPtr->substr(0, start);
-
-            // operate on the rest (+1 for trailing space)
-            std::string end = strPtr->substr(start + templateName.size() + 1);
-
-            std::vector<std::string> split;
-            boost::split(split, end, boost::is_any_of(" "));
-
-            // need at least 1 operation and number
-            if (split.size() < 2)
-            {
-                std::cerr << "Syntax error on template replacement of "
-                          << *strPtr << "\n";
-                for (const std::string& data : split)
-                {
-                    std::cerr << data << " ";
-                }
-                std::cerr << "\n";
-                continue;
-            }
-
-            // we assume that the replacement is a number, because we can
-            // only do math on numbers.. we might concatenate strings in the
-            // future, but thats later
-            int number =
-                std::visit(VariantToIntVisitor(), foundDevicePair.second);
-
-            bool isOperator = true;
-            TemplateOperation next = TemplateOperation::addition;
-
-            auto it = split.begin();
-
-            for (; it != split.end(); it++)
-            {
-                if (isOperator)
-                {
-                    if (*it == "+")
-                    {
-                        next = TemplateOperation::addition;
-                    }
-                    else if (*it == "-")
-                    {
-                        next = TemplateOperation::subtraction;
-                    }
-                    else if (*it == "*")
-                    {
-                        next = TemplateOperation::multiplication;
-                    }
-                    else if (*it == R"(%)")
-                    {
-                        next = TemplateOperation::modulo;
-                    }
-                    else if (*it == R"(/)")
-                    {
-                        next = TemplateOperation::division;
-                    }
-                    else
-                    {
-                        break;
-                    }
-                }
-                else
-                {
-                    int constant = 0;
-                    try
-                    {
-                        constant = std::stoi(*it);
-                    }
-                    catch (std::invalid_argument&)
-                    {
-                        std::cerr << "Parameter not supported for templates "
-                                  << *it << "\n";
-                        continue;
-                    }
-                    switch (next)
-                    {
-                        case TemplateOperation::addition:
-                        {
-                            number += constant;
-                            break;
-                        }
-                        case TemplateOperation::subtraction:
-                        {
-                            number -= constant;
-                            break;
-                        }
-                        case TemplateOperation::multiplication:
-                        {
-                            number *= constant;
-                            break;
-                        }
-                        case TemplateOperation::division:
-                        {
-                            number /= constant;
-                            break;
-                        }
-                        case TemplateOperation::modulo:
-                        {
-                            number = number % constant;
-                            break;
-                        }
-
-                        default:
-                            break;
-                    }
-                }
-                isOperator = !isOperator;
-            }
-            std::string result = prefix + std::to_string(number);
-
-            if (it != split.end())
-            {
-                for (; it != split.end(); it++)
-                {
-                    result += " " + *it;
-                }
-            }
-            keyPair.value() = result;
-
-            // We probably just invalidated the pointer above, so set it to null
-            strPtr = nullptr;
-            break;
-        }
-    }
-
-    strPtr = keyPair.value().get_ptr<std::string*>();
-    if (strPtr == nullptr)
-    {
-        return;
-    }
-
-    // convert hex numbers to ints
-    if (boost::starts_with(*strPtr, "0x"))
-    {
-        try
-        {
-            size_t pos = 0;
-            int64_t temp = std::stoul(*strPtr, &pos, 0);
-            if (pos == strPtr->size())
-            {
-                keyPair.value() = static_cast<uint64_t>(temp);
-            }
-        }
-        catch (std::invalid_argument&)
-        {
-        }
-        catch (std::out_of_range&)
-        {
-        }
-    }
-    // non-hex numbers
-    else
-    {
-        try
-        {
-            uint64_t temp = boost::lexical_cast<uint64_t>(*strPtr);
-            keyPair.value() = temp;
-        }
-        catch (boost::bad_lexical_cast&)
-        {
-        }
-    }
-}
-
 // reads json files out of the filesystem
 bool findJsonFiles(std::list<nlohmann::json>& configurations)
 {
diff --git a/src/Utils.cpp b/src/Utils.cpp
index ac16d36..4662c2a 100644
--- a/src/Utils.cpp
+++ b/src/Utils.cpp
@@ -14,7 +14,17 @@
 // limitations under the License.
 */
 
-#include <Utils.hpp>
+#include "Utils.hpp"
+
+#include "VariantVisitors.hpp"
+
+#include <boost/algorithm/string/classification.hpp>
+#include <boost/algorithm/string/find.hpp>
+#include <boost/algorithm/string/predicate.hpp>
+#include <boost/algorithm/string/replace.hpp>
+#include <boost/algorithm/string/split.hpp>
+#include <boost/container/flat_map.hpp>
+#include <boost/lexical_cast.hpp>
 #include <filesystem>
 #include <fstream>
 #include <regex>
@@ -24,6 +34,8 @@
 #include <valijson/schema_parser.hpp>
 #include <valijson/validator.hpp>
 
+constexpr const char* templateChar = "$";
+
 namespace fs = std::filesystem;
 static bool powerStatusOn = false;
 static std::unique_ptr<sdbusplus::bus::match::match> powerMatch = nullptr;
@@ -124,4 +136,222 @@
         "namespace='/xyz/openbmc_project/Chassis/Control/"
         "Power0',arg0='xyz.openbmc_project.Chassis.Control.Power'",
         eventHandler);
+}
+
+// finds the template character (currently set to $) and replaces the value with
+// the field found in a dbus object i.e. $ADDRESS would get populated with the
+// ADDRESS field from a object on dbus
+void templateCharReplace(
+    nlohmann::json::iterator& keyPair,
+    const boost::container::flat_map<std::string, BasicVariantType>&
+        foundDevice,
+    const size_t foundDeviceIdx)
+{
+    if (keyPair.value().type() == nlohmann::json::value_t::object ||
+        keyPair.value().type() == nlohmann::json::value_t::array)
+    {
+        for (auto nextLayer = keyPair.value().begin();
+             nextLayer != keyPair.value().end(); nextLayer++)
+        {
+            templateCharReplace(nextLayer, foundDevice, foundDeviceIdx);
+        }
+        return;
+    }
+
+    std::string* strPtr = keyPair.value().get_ptr<std::string*>();
+    if (strPtr == nullptr)
+    {
+        return;
+    }
+
+    boost::replace_all(*strPtr, std::string(templateChar) + "index",
+                       std::to_string(foundDeviceIdx));
+
+    for (auto& foundDevicePair : foundDevice)
+    {
+        std::string templateName = templateChar + foundDevicePair.first;
+        boost::iterator_range<std::string::const_iterator> find =
+            boost::ifind_first(*strPtr, templateName);
+        if (find)
+        {
+            size_t start = find.begin() - strPtr->begin();
+            // check for additional operations
+            if (!start && find.end() == strPtr->end())
+            {
+                std::visit([&](auto&& val) { keyPair.value() = val; },
+                           foundDevicePair.second);
+                return;
+            }
+            else if (find.end() == strPtr->end())
+            {
+                std::string val = std::visit(VariantToStringVisitor(),
+                                             foundDevicePair.second);
+                boost::replace_all(*strPtr,
+                                   templateChar + foundDevicePair.first, val);
+                return;
+            }
+
+            // save the prefix
+            std::string prefix = strPtr->substr(0, start);
+
+            // operate on the rest (+1 for trailing space)
+            std::string end = strPtr->substr(start + templateName.size() + 1);
+
+            std::vector<std::string> split;
+            boost::split(split, end, boost::is_any_of(" "));
+
+            // need at least 1 operation and number
+            if (split.size() < 2)
+            {
+                std::cerr << "Syntax error on template replacement of "
+                          << *strPtr << "\n";
+                for (const std::string& data : split)
+                {
+                    std::cerr << data << " ";
+                }
+                std::cerr << "\n";
+                continue;
+            }
+
+            // we assume that the replacement is a number, because we can
+            // only do math on numbers.. we might concatenate strings in the
+            // future, but thats later
+            int number =
+                std::visit(VariantToIntVisitor(), foundDevicePair.second);
+
+            bool isOperator = true;
+            TemplateOperation next = TemplateOperation::addition;
+
+            auto it = split.begin();
+
+            for (; it != split.end(); it++)
+            {
+                if (isOperator)
+                {
+                    if (*it == "+")
+                    {
+                        next = TemplateOperation::addition;
+                    }
+                    else if (*it == "-")
+                    {
+                        next = TemplateOperation::subtraction;
+                    }
+                    else if (*it == "*")
+                    {
+                        next = TemplateOperation::multiplication;
+                    }
+                    else if (*it == R"(%)")
+                    {
+                        next = TemplateOperation::modulo;
+                    }
+                    else if (*it == R"(/)")
+                    {
+                        next = TemplateOperation::division;
+                    }
+                    else
+                    {
+                        break;
+                    }
+                }
+                else
+                {
+                    int constant = 0;
+                    try
+                    {
+                        constant = std::stoi(*it);
+                    }
+                    catch (std::invalid_argument&)
+                    {
+                        std::cerr << "Parameter not supported for templates "
+                                  << *it << "\n";
+                        continue;
+                    }
+                    switch (next)
+                    {
+                        case TemplateOperation::addition:
+                        {
+                            number += constant;
+                            break;
+                        }
+                        case TemplateOperation::subtraction:
+                        {
+                            number -= constant;
+                            break;
+                        }
+                        case TemplateOperation::multiplication:
+                        {
+                            number *= constant;
+                            break;
+                        }
+                        case TemplateOperation::division:
+                        {
+                            number /= constant;
+                            break;
+                        }
+                        case TemplateOperation::modulo:
+                        {
+                            number = number % constant;
+                            break;
+                        }
+
+                        default:
+                            break;
+                    }
+                }
+                isOperator = !isOperator;
+            }
+            std::string result = prefix + std::to_string(number);
+
+            if (it != split.end())
+            {
+                for (; it != split.end(); it++)
+                {
+                    result += " " + *it;
+                }
+            }
+            keyPair.value() = result;
+
+            // We probably just invalidated the pointer above, so set it to null
+            strPtr = nullptr;
+            break;
+        }
+    }
+
+    strPtr = keyPair.value().get_ptr<std::string*>();
+    if (strPtr == nullptr)
+    {
+        return;
+    }
+
+    // convert hex numbers to ints
+    if (boost::starts_with(*strPtr, "0x"))
+    {
+        try
+        {
+            size_t pos = 0;
+            int64_t temp = std::stoul(*strPtr, &pos, 0);
+            if (pos == strPtr->size())
+            {
+                keyPair.value() = static_cast<uint64_t>(temp);
+            }
+        }
+        catch (std::invalid_argument&)
+        {
+        }
+        catch (std::out_of_range&)
+        {
+        }
+    }
+    // non-hex numbers
+    else
+    {
+        try
+        {
+            uint64_t temp = boost::lexical_cast<uint64_t>(*strPtr);
+            keyPair.value() = temp;
+        }
+        catch (boost::bad_lexical_cast&)
+        {
+        }
+    }
 }
\ No newline at end of file
diff --git a/test/test_entity-manager.cpp b/test/test_entity-manager.cpp
new file mode 100644
index 0000000..653a982
--- /dev/null
+++ b/test/test_entity-manager.cpp
@@ -0,0 +1,141 @@
+#include "Utils.hpp"
+
+#include <boost/container/flat_map.hpp>
+#include <nlohmann/json.hpp>
+#include <variant>
+
+#include "gtest/gtest.h"
+
+TEST(TemplateCharReplace, replaceOneInt)
+{
+    nlohmann::json j = {{"foo", "$bus"}};
+    auto it = j.begin();
+    boost::container::flat_map<std::string, BasicVariantType> data;
+    data["BUS"] = 23;
+
+    templateCharReplace(it, data, 0);
+
+    nlohmann::json expected = 23;
+    EXPECT_EQ(expected, j["foo"]);
+}
+
+TEST(TemplateCharReplace, replaceOneStr)
+{
+    nlohmann::json j = {{"foo", "$TEST"}};
+    auto it = j.begin();
+    boost::container::flat_map<std::string, BasicVariantType> data;
+    data["TEST"] = std::string("Test");
+
+    templateCharReplace(it, data, 0);
+
+    nlohmann::json expected = "Test";
+    EXPECT_EQ(expected, j["foo"]);
+}
+
+TEST(TemplateCharReplace, replaceSecondStr)
+{
+    nlohmann::json j = {{"foo", "the $TEST"}};
+    auto it = j.begin();
+    boost::container::flat_map<std::string, BasicVariantType> data;
+    data["TEST"] = std::string("Test");
+
+    templateCharReplace(it, data, 0);
+
+    nlohmann::json expected = "the Test";
+    EXPECT_EQ(expected, j["foo"]);
+}
+
+/*
+TODO This should work
+
+TEST(TemplateCharReplace, replaceMiddleStr)
+{
+    nlohmann::json j = {{"foo", "the $TEST worked"}};
+    auto it = j.begin();
+    boost::container::flat_map<std::string, BasicVariantType> data;
+    data["TEST"] = std::string("Test");
+
+    templateCharReplace(it, data, 0);
+
+    nlohmann::json expected = "the Test worked";
+    EXPECT_EQ(expected, j["foo"]);
+}
+*/
+
+TEST(TemplateCharReplace, replaceLastStr)
+{
+    nlohmann::json j = {{"foo", "the Test $TEST"}};
+    auto it = j.begin();
+    boost::container::flat_map<std::string, BasicVariantType> data;
+    data["TEST"] = 23;
+
+    templateCharReplace(it, data, 0);
+
+    nlohmann::json expected = "the Test 23";
+    EXPECT_EQ(expected, j["foo"]);
+}
+
+TEST(TemplateCharReplace, increment)
+{
+    nlohmann::json j = {{"foo", "3 plus 1 equals $TEST + 1"}};
+    auto it = j.begin();
+    boost::container::flat_map<std::string, BasicVariantType> data;
+    data["TEST"] = 3;
+
+    templateCharReplace(it, data, 0);
+
+    nlohmann::json expected = "3 plus 1 equals 4";
+    EXPECT_EQ(expected, j["foo"]);
+}
+
+TEST(TemplateCharReplace, decrement)
+{
+    nlohmann::json j = {{"foo", "3 minus 1 equals $TEST - 1 !"}};
+    auto it = j.begin();
+    boost::container::flat_map<std::string, BasicVariantType> data;
+    data["TEST"] = 3;
+
+    templateCharReplace(it, data, 0);
+
+    nlohmann::json expected = "3 minus 1 equals 2 !";
+    EXPECT_EQ(expected, j["foo"]);
+}
+
+TEST(TemplateCharReplace, modulus)
+{
+    nlohmann::json j = {{"foo", "3 mod 2 equals $TEST % 2"}};
+    auto it = j.begin();
+    boost::container::flat_map<std::string, BasicVariantType> data;
+    data["TEST"] = 3;
+
+    templateCharReplace(it, data, 0);
+
+    nlohmann::json expected = "3 mod 2 equals 1";
+    EXPECT_EQ(expected, j["foo"]);
+}
+
+TEST(TemplateCharReplace, multiply)
+{
+    nlohmann::json j = {{"foo", "3 * 2 equals $TEST * 2"}};
+    auto it = j.begin();
+    boost::container::flat_map<std::string, BasicVariantType> data;
+    data["TEST"] = 3;
+
+    templateCharReplace(it, data, 0);
+
+    nlohmann::json expected = "3 * 2 equals 6";
+    EXPECT_EQ(expected, j["foo"]);
+}
+
+TEST(TemplateCharReplace, divide)
+{
+    nlohmann::json j = {{"foo", "4 / 2 equals $TEST / 2"}};
+    auto it = j.begin();
+    boost::container::flat_map<std::string, BasicVariantType> data;
+    data["TEST"] = 4;
+
+    templateCharReplace(it, data, 0);
+
+    nlohmann::json expected = "4 / 2 equals 2";
+    EXPECT_EQ(expected, j["foo"]);
+}
\ No newline at end of file