From 9dfa3d22d490f4d9099d99aafd3022eb1ed412e3 Mon Sep 17 00:00:00 2001 From: Gunnar Beutner Date: Wed, 26 Nov 2014 20:43:42 +0100 Subject: [PATCH] Implement support for arrays in command arguments fixes #6709 --- itl/command-plugins.conf | 5 +++ lib/icinga/host.cpp | 2 +- lib/icinga/host.hpp | 2 +- lib/icinga/icinga-type.conf | 2 + lib/icinga/icingaapplication.cpp | 2 +- lib/icinga/icingaapplication.hpp | 2 +- lib/icinga/macroprocessor.cpp | 51 ++++++++++++++-------- lib/icinga/macroprocessor.hpp | 6 +-- lib/icinga/macroresolver.hpp | 2 +- lib/icinga/pluginutility.cpp | 74 ++++++++++++++++++++++++++++---- lib/icinga/pluginutility.hpp | 5 ++- lib/icinga/service.cpp | 2 +- lib/icinga/service.hpp | 2 +- lib/perfdata/graphitewriter.cpp | 20 ++++++++- lib/perfdata/graphitewriter.hpp | 1 + lib/perfdata/perfdatawriter.cpp | 12 +++++- lib/perfdata/perfdatawriter.hpp | 1 + test/CMakeLists.txt | 4 +- test/icinga-macros.cpp | 55 ++++++++++++++++++++++++ 19 files changed, 209 insertions(+), 41 deletions(-) create mode 100644 test/icinga-macros.cpp diff --git a/itl/command-plugins.conf b/itl/command-plugins.conf index 7da0f64a4..af8112c9b 100644 --- a/itl/command-plugins.conf +++ b/itl/command-plugins.conf @@ -736,6 +736,11 @@ object CheckCommand "nrpe" { description = "Make socket timeouts return an UNKNOWN state instead of CRITICAL" } "-t" = "$nrpe_timeout$" + "-a" = { + value = "$nrpe_arguments$" + repeat_key = false + order = 1 + } } vars.nrpe_address = "$address$" diff --git a/lib/icinga/host.cpp b/lib/icinga/host.cpp index 72c42575d..1a7fe818d 100644 --- a/lib/icinga/host.cpp +++ b/lib/icinga/host.cpp @@ -212,7 +212,7 @@ String Host::StateTypeToString(StateType type) return "HARD"; } -bool Host::ResolveMacro(const String& macro, const CheckResult::Ptr&, String *result) const +bool Host::ResolveMacro(const String& macro, const CheckResult::Ptr&, Value *result) const { if (macro == "state") { *result = StateToString(GetState()); diff --git a/lib/icinga/host.hpp b/lib/icinga/host.hpp index 3d8f1a1c6..91e4c6ea2 100644 --- a/lib/icinga/host.hpp +++ b/lib/icinga/host.hpp @@ -63,7 +63,7 @@ public: static StateType StateTypeFromString(const String& state); static String StateTypeToString(StateType state); - virtual bool ResolveMacro(const String& macro, const CheckResult::Ptr& cr, String *result) const; + virtual bool ResolveMacro(const String& macro, const CheckResult::Ptr& cr, Value *result) const; protected: virtual void Stop(void); diff --git a/lib/icinga/icinga-type.conf b/lib/icinga/icinga-type.conf index 4c8d06772..12e5945f2 100644 --- a/lib/icinga/icinga-type.conf +++ b/lib/icinga/icinga-type.conf @@ -202,10 +202,12 @@ %attribute %dictionary "arguments" { %attribute %string "*", %attribute %dictionary "*" { + %attribute %string "key" %attribute %string "value" %attribute %string "description" %attribute %number "required" %attribute %number "skip_key" + %attribute %number "repeat_key" %attribute %string "set_if" %attribute %number "order" } diff --git a/lib/icinga/icingaapplication.cpp b/lib/icinga/icingaapplication.cpp index b52404d92..c957dff1e 100644 --- a/lib/icinga/icingaapplication.cpp +++ b/lib/icinga/icingaapplication.cpp @@ -147,7 +147,7 @@ String IcingaApplication::GetNodeName(void) const return ScriptVariable::Get("NodeName"); } -bool IcingaApplication::ResolveMacro(const String& macro, const CheckResult::Ptr&, String *result) const +bool IcingaApplication::ResolveMacro(const String& macro, const CheckResult::Ptr&, Value *result) const { double now = Utility::GetTime(); diff --git a/lib/icinga/icingaapplication.hpp b/lib/icinga/icingaapplication.hpp index a644ce871..ef0077191 100644 --- a/lib/icinga/icingaapplication.hpp +++ b/lib/icinga/icingaapplication.hpp @@ -50,7 +50,7 @@ public: Dictionary::Ptr GetVars(void) const; String GetNodeName(void) const; - virtual bool ResolveMacro(const String& macro, const CheckResult::Ptr& cr, String *result) const; + virtual bool ResolveMacro(const String& macro, const CheckResult::Ptr& cr, Value *result) const; bool GetEnableNotifications(void) const; void SetEnableNotifications(bool enabled); diff --git a/lib/icinga/macroprocessor.cpp b/lib/icinga/macroprocessor.cpp index d47cec010..8071dbe3b 100644 --- a/lib/icinga/macroprocessor.cpp +++ b/lib/icinga/macroprocessor.cpp @@ -66,7 +66,7 @@ Value MacroProcessor::ResolveMacros(const Value& str, const ResolverList& resolv } bool MacroProcessor::ResolveMacro(const String& macro, const ResolverList& resolvers, - const CheckResult::Ptr& cr, String *result, bool *recursive_macro) + const CheckResult::Ptr& cr, Value *result, bool *recursive_macro) { CONTEXT("Resolving macro '" + macro + "'"); @@ -92,12 +92,7 @@ bool MacroProcessor::ResolveMacro(const String& macro, const ResolverList& resol Dictionary::Ptr vars = dobj->GetVars(); if (vars && vars->Contains(macro)) { - Value value = vars->Get(macro); - - if (value.IsObjectType()) - value = Utility::Join(value, ';'); - - *result = value; + *result = vars->Get(macro); *recursive_macro = true; return true; } @@ -150,9 +145,6 @@ bool MacroProcessor::ResolveMacro(const String& macro, const ResolverList& resol tokens[0] == "notes") *recursive_macro = true; - if (ref.IsObjectType()) - ref = Utility::Join(ref, ';'); - *result = ref; return true; } @@ -161,7 +153,7 @@ bool MacroProcessor::ResolveMacro(const String& macro, const ResolverList& resol return false; } -String MacroProcessor::InternalResolveMacros(const String& str, const ResolverList& resolvers, +Value MacroProcessor::InternalResolveMacros(const String& str, const ResolverList& resolvers, const CheckResult::Ptr& cr, String *missingMacro, const MacroProcessor::EscapeCallback& escapeFn, const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros, int recursionLevel) @@ -183,7 +175,7 @@ String MacroProcessor::InternalResolveMacros(const String& str, const ResolverLi String name = result.SubStr(pos_first + 1, pos_second - pos_first - 1); - String resolved_macro; + Value resolved_macro; bool recursive_macro; bool found; @@ -211,10 +203,24 @@ String MacroProcessor::InternalResolveMacros(const String& str, const ResolverLi } /* recursively resolve macros in the macro if it was a user macro */ - if (recursive_macro) - resolved_macro = InternalResolveMacros(resolved_macro, - resolvers, cr, missingMacro, EscapeCallback(), Dictionary::Ptr(), - false, recursionLevel + 1); + if (recursive_macro) { + if (resolved_macro.IsObjectType()) { + Array::Ptr arr = resolved_macro; + Array::Ptr result = new Array(); + + ObjectLock olock(arr); + BOOST_FOREACH(Value& value, arr) { + result->Add(InternalResolveMacros(value, + resolvers, cr, missingMacro, EscapeCallback(), Dictionary::Ptr(), + false, recursionLevel + 1)); + } + + resolved_macro = result; + } else + resolved_macro = InternalResolveMacros(resolved_macro, + resolvers, cr, missingMacro, EscapeCallback(), Dictionary::Ptr(), + false, recursionLevel + 1); + } if (!useResolvedMacros && found && resolvedMacros) resolvedMacros->Set(name, resolved_macro); @@ -222,8 +228,19 @@ String MacroProcessor::InternalResolveMacros(const String& str, const ResolverLi if (escapeFn) resolved_macro = escapeFn(resolved_macro); + /* we're done if the value is an array */ + if (resolved_macro.IsObjectType()) { + /* don't allow mixing strings and arrays in macro strings */ + if (pos_first != 0 || pos_second != str.GetLength() - 1) + BOOST_THROW_EXCEPTION(std::invalid_argument("Mixing both strings and non-strings in macros is not allowed.")); + + return resolved_macro; + } + + String resolved_macro_str = resolved_macro; + result.Replace(pos_first, pos_second - pos_first + 1, resolved_macro); - offset = pos_first + resolved_macro.GetLength() + 1; + offset = pos_first + resolved_macro_str.GetLength() + 1; } return result; diff --git a/lib/icinga/macroprocessor.hpp b/lib/icinga/macroprocessor.hpp index a7e0b0f9c..00afbda0f 100644 --- a/lib/icinga/macroprocessor.hpp +++ b/lib/icinga/macroprocessor.hpp @@ -37,7 +37,7 @@ namespace icinga class I2_ICINGA_API MacroProcessor { public: - typedef boost::function EscapeCallback; + typedef boost::function EscapeCallback; typedef std::pair ResolverSpec; typedef std::vector ResolverList; @@ -51,8 +51,8 @@ private: MacroProcessor(void); static bool ResolveMacro(const String& macro, const ResolverList& resolvers, - const CheckResult::Ptr& cr, String *result, bool *recursive_macro); - static String InternalResolveMacros(const String& str, + const CheckResult::Ptr& cr, Value *result, bool *recursive_macro); + static Value InternalResolveMacros(const String& str, const ResolverList& resolvers, const CheckResult::Ptr& cr, String *missingMacro, const EscapeCallback& escapeFn, const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros, diff --git a/lib/icinga/macroresolver.hpp b/lib/icinga/macroresolver.hpp index 300145f2b..c2702d6af 100644 --- a/lib/icinga/macroresolver.hpp +++ b/lib/icinga/macroresolver.hpp @@ -38,7 +38,7 @@ class I2_ICINGA_API MacroResolver public: DECLARE_PTR_TYPEDEFS(MacroResolver); - virtual bool ResolveMacro(const String& macro, const CheckResult::Ptr& cr, String *result) const = 0; + virtual bool ResolveMacro(const String& macro, const CheckResult::Ptr& cr, Value *result) const = 0; }; } diff --git a/lib/icinga/pluginutility.cpp b/lib/icinga/pluginutility.cpp index 736d7f465..783431b9a 100644 --- a/lib/icinga/pluginutility.cpp +++ b/lib/icinga/pluginutility.cpp @@ -36,12 +36,13 @@ struct CommandArgument { int Order; bool SkipKey; + bool RepeatKey; bool SkipValue; String Key; - String Value; + Value AValue; CommandArgument(void) - : Order(0), SkipKey(false), SkipValue(false) + : Order(0), SkipKey(false), RepeatKey(true), SkipValue(false) { } bool operator<(const CommandArgument& rhs) const @@ -50,6 +51,35 @@ struct CommandArgument } }; +void PluginUtility::AddArgumentHelper(const Array::Ptr& args, const String& key, const String& value, bool add_key, bool add_value) +{ + if (add_key) + args->Add(key); + + if (add_value) + args->Add(value); +} + +Value PluginUtility::EscapeMacroShellArg(const Value& value) +{ + String result; + + if (value.IsObjectType()) { + Array::Ptr arr = value; + + ObjectLock olock(arr); + BOOST_FOREACH(const Value& arg, arr) { + if (result.GetLength() > 0) + result += " "; + + result += Utility::EscapeShellArg(arg); + } + } else + result = Utility::EscapeShellArg(value); + + return result; +} + void PluginUtility::ExecuteCommand(const Command::Ptr& commandObj, const Checkable::Ptr& checkable, const CheckResult::Ptr& cr, const MacroProcessor::ResolverList& macroResolvers, const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros, @@ -61,7 +91,7 @@ void PluginUtility::ExecuteCommand(const Command::Ptr& commandObj, const Checkab Value command; if (!raw_arguments || raw_command.IsObjectType()) command = MacroProcessor::ResolveMacros(raw_command, macroResolvers, cr, NULL, - Utility::EscapeShellArg, resolvedMacros, useResolvedMacros); + PluginUtility::EscapeMacroShellArg, resolvedMacros, useResolvedMacros); else { Array::Ptr arr = new Array(); arr->Add(raw_command); @@ -83,10 +113,14 @@ void PluginUtility::ExecuteCommand(const Command::Ptr& commandObj, const Checkab if (arginfo.IsObjectType()) { Dictionary::Ptr argdict = arginfo; + if (argdict->Contains("key")) + arg.Key = argdict->Get("key"); argval = argdict->Get("value"); if (argdict->Contains("required")) required = argdict->Get("required"); arg.SkipKey = argdict->Get("skip_key"); + if (argdict->Contains("repeat_key")) + arg.RepeatKey = argdict->Get("repeat_key"); arg.Order = argdict->Get("order"); String set_if = argdict->Get("set_if"); @@ -118,7 +152,7 @@ void PluginUtility::ExecuteCommand(const Command::Ptr& commandObj, const Checkab arg.SkipValue = true; String missingMacro; - arg.Value = MacroProcessor::ResolveMacros(argval, macroResolvers, + arg.AValue = MacroProcessor::ResolveMacros(argval, macroResolvers, cr, &missingMacro, MacroProcessor::EscapeCallback(), resolvedMacros, useResolvedMacros); @@ -152,11 +186,32 @@ void PluginUtility::ExecuteCommand(const Command::Ptr& commandObj, const Checkab Array::Ptr command_arr = command; BOOST_FOREACH(const CommandArgument& arg, args) { - if (!arg.SkipKey) - command_arr->Add(arg.Key); + Array::Ptr arr; + + if (arg.AValue.IsString()) + AddArgumentHelper(command_arr, arg.Key, arg.AValue, !arg.SkipKey, !arg.SkipValue); + else if (arg.AValue.IsObjectType()) + arr = static_cast(arg.AValue); + else + continue; + + if (arr) { + bool first = true; - if (!arg.SkipValue) - command_arr->Add(arg.Value); + ObjectLock olock(arr); + BOOST_FOREACH(const Value& value, arr) { + bool add_key; + + if (first) { + first = false; + add_key = !arg.SkipKey; + } else + add_key = !arg.SkipKey && arg.RepeatKey; + + + AddArgumentHelper(command_arr, arg.Key, value, add_key, !arg.SkipValue); + } + } } } @@ -173,6 +228,9 @@ void PluginUtility::ExecuteCommand(const Command::Ptr& commandObj, const Checkab NULL, MacroProcessor::EscapeCallback(), resolvedMacros, useResolvedMacros); + if (value.IsObjectType()) + value = Utility::Join(value, ';'); + envMacros->Set(kv.first, value); } } diff --git a/lib/icinga/pluginutility.hpp b/lib/icinga/pluginutility.hpp index 2039f1237..d9c6c0dfc 100644 --- a/lib/icinga/pluginutility.hpp +++ b/lib/icinga/pluginutility.hpp @@ -41,7 +41,7 @@ class I2_ICINGA_API PluginUtility public: static void ExecuteCommand(const Command::Ptr& commandObj, const Checkable::Ptr& checkable, const CheckResult::Ptr& cr, const MacroProcessor::ResolverList& macroResolvers, - const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros, + const Dictionary::Ptr& resolvedMacros, bool useResolvedMacros, const boost::function& callback = boost::function()); static ServiceState ExitStatusToState(int exitStatus); @@ -52,6 +52,9 @@ public: private: PluginUtility(void); + + static void AddArgumentHelper(const Array::Ptr& args, const String& key, const String& value, bool add_key, bool add_value); + static Value EscapeMacroShellArg(const Value& value); }; } diff --git a/lib/icinga/service.cpp b/lib/icinga/service.cpp index e0b914c12..5168a5f2f 100644 --- a/lib/icinga/service.cpp +++ b/lib/icinga/service.cpp @@ -134,7 +134,7 @@ String Service::StateTypeToString(StateType type) return "HARD"; } -bool Service::ResolveMacro(const String& macro, const CheckResult::Ptr& cr, String *result) const +bool Service::ResolveMacro(const String& macro, const CheckResult::Ptr& cr, Value *result) const { if (macro == "state") { *result = StateToString(GetState()); diff --git a/lib/icinga/service.hpp b/lib/icinga/service.hpp index 1fe1a532e..2c0ee86a6 100644 --- a/lib/icinga/service.hpp +++ b/lib/icinga/service.hpp @@ -43,7 +43,7 @@ public: Host::Ptr GetHost(void) const; - virtual bool ResolveMacro(const String& macro, const CheckResult::Ptr& cr, String *result) const; + virtual bool ResolveMacro(const String& macro, const CheckResult::Ptr& cr, Value *result) const; static ServiceState StateFromString(const String& state); static String StateToString(ServiceState state); diff --git a/lib/perfdata/graphitewriter.cpp b/lib/perfdata/graphitewriter.cpp index 7498f3f6c..036ac90b5 100644 --- a/lib/perfdata/graphitewriter.cpp +++ b/lib/perfdata/graphitewriter.cpp @@ -117,11 +117,11 @@ void GraphiteWriter::CheckResultHandler(const Checkable::Ptr& checkable, const C String prefix; if (service) { - prefix = MacroProcessor::ResolveMacros(GetServiceNameTemplate(), resolvers, cr, NULL, &GraphiteWriter::EscapeMetric); + prefix = MacroProcessor::ResolveMacros(GetServiceNameTemplate(), resolvers, cr, NULL, &GraphiteWriter::EscapeMacroMetric); SendMetric(prefix, "state", service->GetState()); } else { - prefix = MacroProcessor::ResolveMacros(GetHostNameTemplate(), resolvers, cr, NULL, &GraphiteWriter::EscapeMetric); + prefix = MacroProcessor::ResolveMacros(GetHostNameTemplate(), resolvers, cr, NULL, &GraphiteWriter::EscapeMacroMetric); SendMetric(prefix, "state", host->GetState()); } @@ -214,3 +214,19 @@ String GraphiteWriter::EscapeMetric(const String& str) return result; } + +Value GraphiteWriter::EscapeMacroMetric(const Value& value) +{ + if (value.IsObjectType()) { + Array::Ptr arr = value; + Array::Ptr result = new Array(); + + ObjectLock olock(arr); + BOOST_FOREACH(const Value& arg, arr) { + result->Add(EscapeMetric(arg)); + } + + return Utility::Join(result, '.'); + } else + return EscapeMetric(value); +} diff --git a/lib/perfdata/graphitewriter.hpp b/lib/perfdata/graphitewriter.hpp index f0128266a..fa34fbecb 100644 --- a/lib/perfdata/graphitewriter.hpp +++ b/lib/perfdata/graphitewriter.hpp @@ -55,6 +55,7 @@ private: void SendMetric(const String& prefix, const String& name, double value); void SendPerfdata(const String& prefix, const CheckResult::Ptr& cr); static String EscapeMetric(const String& str); + static Value EscapeMacroMetric(const Value& value); void ReconnectTimerHandler(void); }; diff --git a/lib/perfdata/perfdatawriter.cpp b/lib/perfdata/perfdatawriter.cpp index 40575eac0..288f70cc5 100644 --- a/lib/perfdata/perfdatawriter.cpp +++ b/lib/perfdata/perfdatawriter.cpp @@ -64,6 +64,14 @@ void PerfdataWriter::Start(void) RotateFile(m_HostOutputFile, GetHostTempPath(), GetHostPerfdataPath()); } +Value PerfdataWriter::EscapeMacroMetric(const Value& value) +{ + if (value.IsObjectType()) + return Utility::Join(value, ';'); + else + return value; +} + void PerfdataWriter::CheckResultHandler(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr) { CONTEXT("Writing performance data for object '" + checkable->GetName() + "'"); @@ -86,7 +94,7 @@ void PerfdataWriter::CheckResultHandler(const Checkable::Ptr& checkable, const C resolvers.push_back(std::make_pair("icinga", IcingaApplication::GetInstance())); if (service) { - String line = MacroProcessor::ResolveMacros(GetServiceFormatTemplate(), resolvers, cr); + String line = MacroProcessor::ResolveMacros(GetServiceFormatTemplate(), resolvers, cr, NULL, &PerfdataWriter::EscapeMacroMetric); { ObjectLock olock(this); @@ -96,7 +104,7 @@ void PerfdataWriter::CheckResultHandler(const Checkable::Ptr& checkable, const C m_ServiceOutputFile << line << "\n"; } } else { - String line = MacroProcessor::ResolveMacros(GetHostFormatTemplate(), resolvers, cr); + String line = MacroProcessor::ResolveMacros(GetHostFormatTemplate(), resolvers, cr, NULL, &PerfdataWriter::EscapeMacroMetric); { ObjectLock olock(this); diff --git a/lib/perfdata/perfdatawriter.hpp b/lib/perfdata/perfdatawriter.hpp index 0e3caf0ab..f297f706e 100644 --- a/lib/perfdata/perfdatawriter.hpp +++ b/lib/perfdata/perfdatawriter.hpp @@ -47,6 +47,7 @@ protected: private: void CheckResultHandler(const Checkable::Ptr& checkable, const CheckResult::Ptr& cr); + static Value EscapeMacroMetric(const Value& value); Timer::Ptr m_RotationTimer; void RotationTimerHandler(void); diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index fb0aa3b6c..1a884688f 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -22,7 +22,8 @@ set(base_test_SOURCES base-json.cpp base-match.cpp base-netstring.cpp base-object.cpp base-serialize.cpp base-shellescape.cpp base-stacktrace.cpp base-stream.cpp base-string.cpp base-timer.cpp base-type.cpp - base-value.cpp config-ops.cpp icinga-perfdata.cpp test.cpp + base-value.cpp config-ops.cpp icinga-macros.cpp icinga-perfdata.cpp + test.cpp ) set_property(SOURCE test.cpp PROPERTY EXCLUDE_UNITY_BUILD TRUE) @@ -90,6 +91,7 @@ add_boost_test(base base_value/format config_ops/simple config_ops/advanced + icinga_macros/simple icinga_perfdata/empty icinga_perfdata/simple icinga_perfdata/quotes diff --git a/test/icinga-macros.cpp b/test/icinga-macros.cpp new file mode 100644 index 000000000..e4b61600a --- /dev/null +++ b/test/icinga-macros.cpp @@ -0,0 +1,55 @@ +/****************************************************************************** + * Icinga 2 * + * Copyright (C) 2012-2014 Icinga Development Team (http://www.icinga.org) * + * * + * This program is free software; you can redistribute it and/or * + * modify it under the terms of the GNU General Public License * + * as published by the Free Software Foundation; either version 2 * + * of the License, or (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the Free Software Foundation * + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. * + ******************************************************************************/ + +#include "icinga/macroprocessor.hpp" +#include + +using namespace icinga; + +BOOST_AUTO_TEST_SUITE(icinga_macros) + +BOOST_AUTO_TEST_CASE(simple) +{ + Dictionary::Ptr macrosA = new Dictionary(); + macrosA->Set("testA", 7); + macrosA->Set("testB", "hello"); + + Dictionary::Ptr macrosB = new Dictionary(); + macrosB->Set("testA", 3); + macrosB->Set("testC", "world"); + + Array::Ptr testD = new Array(); + testD->Add(3); + testD->Add("test"); + + macrosB->Set("testD", testD); + + MacroProcessor::ResolverList resolvers; + resolvers.push_back(std::make_pair("macrosA", macrosA)); + resolvers.push_back(std::make_pair("macrosB", macrosB)); + + BOOST_CHECK(MacroProcessor::ResolveMacros("$macrosA.testB$ $macrosB.testC$", resolvers) == "hello world"); + BOOST_CHECK(MacroProcessor::ResolveMacros("$testA$", resolvers) == "7"); + + Array::Ptr result = MacroProcessor::ResolveMacros("$testD$", resolvers); + BOOST_CHECK(result->GetLength() == 2); + +} + +BOOST_AUTO_TEST_SUITE_END() -- 2.40.0