diff --git a/commonLibs/attachmentSupport/CMakeLists.txt b/commonLibs/attachmentSupport/CMakeLists.txt index 2402243..d6c4462 100644 --- a/commonLibs/attachmentSupport/CMakeLists.txt +++ b/commonLibs/attachmentSupport/CMakeLists.txt @@ -44,3 +44,7 @@ add_custom_command(TARGET attachmentSupport POST_BUILD install(TARGETS attachmentSupport LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} NAMELINK_SKIP ) + +if(ENABLE_TESTS) + add_subdirectory(tests) +endif() diff --git a/commonLibs/attachmentSupport/tests/CMakeLists.txt b/commonLibs/attachmentSupport/tests/CMakeLists.txt new file mode 100644 index 0000000..7f6e35f --- /dev/null +++ b/commonLibs/attachmentSupport/tests/CMakeLists.txt @@ -0,0 +1,21 @@ +add_executable(deviceAttachmentSpecParams_tests + deviceAttachmentSpecParams_tests.cpp +) + +target_include_directories(deviceAttachmentSpecParams_tests PRIVATE + ${CMAKE_SOURCE_DIR}/include + ${CMAKE_BINARY_DIR}/include + ${CMAKE_SOURCE_DIR}/libspinscale/tests +) + +target_link_libraries(deviceAttachmentSpecParams_tests + gtest_main + spinscale_test_support + ${Boost_LIBRARIES} +) + +add_dependencies(deviceAttachmentSpecParams_tests gtest_main) + +add_test( + NAME deviceAttachmentSpecParams_tests + COMMAND deviceAttachmentSpecParams_tests) diff --git a/commonLibs/attachmentSupport/tests/deviceAttachmentSpecParams_tests.cpp b/commonLibs/attachmentSupport/tests/deviceAttachmentSpecParams_tests.cpp new file mode 100644 index 0000000..f4104c7 --- /dev/null +++ b/commonLibs/attachmentSupport/tests/deviceAttachmentSpecParams_tests.cpp @@ -0,0 +1,413 @@ +#include +#include +#include +#include +#include + +namespace smo { +namespace device { +namespace { + +using ParamList = std::vector>; + +ParamList makeParams(std::initializer_list> entries) +{ + ParamList params; + for (const auto& entry : entries) + { + params.emplace_back(entry.first, entry.second); + } + + return params; +} + +TEST(DeviceAttachmentSpecNamesContainTest, MatchesVectorAndArraySynonyms) +{ + const std::vector widthSynonyms = { + "frame-width", + "dim-w", + }; + + EXPECT_TRUE(DeviceAttachmentSpec::namesContain(widthSynonyms, "dim-w")); + EXPECT_FALSE(DeviceAttachmentSpec::namesContain(widthSynonyms, "dim-h")); + + const std::array heightSynonyms = { + "frame-height", + "dim-h", + }; + + EXPECT_TRUE(DeviceAttachmentSpec::namesContain(heightSynonyms, "frame-height")); + EXPECT_FALSE(DeviceAttachmentSpec::namesContain(heightSynonyms, "frame-width")); +} + +TEST(DeviceAttachmentSpecParamsContainTest, DetectsExactParamName) +{ + const ParamList params = makeParams({ + {"display", "0"}, + {"screen", "1"}, + }); + + EXPECT_TRUE(DeviceAttachmentSpec::paramsContain(params, "display")); + EXPECT_TRUE(DeviceAttachmentSpec::paramsContain(params, "screen")); + EXPECT_FALSE(DeviceAttachmentSpec::paramsContain(params, "width")); +} + +TEST(DeviceAttachmentSpecParamNameMatchesAnySynonymGroupTest, MatchesAcrossGroups) +{ + const std::vector widthSynonyms = {"dim-w", "frame-w"}; + const std::vector heightSynonyms = {"dim-h", "frame-h"}; + + EXPECT_TRUE(DeviceAttachmentSpec::paramNameMatchesAnySynonymGroup( + "dim-w", widthSynonyms, heightSynonyms)); + EXPECT_TRUE(DeviceAttachmentSpec::paramNameMatchesAnySynonymGroup( + "frame-h", widthSynonyms, heightSynonyms)); + EXPECT_FALSE(DeviceAttachmentSpec::paramNameMatchesAnySynonymGroup( + "colour-space", widthSynonyms, heightSynonyms)); +} + +TEST(DeviceAttachmentSpecNormalizeParamTokenTest, TrimsAndLowercases) +{ + EXPECT_EQ(DeviceAttachmentSpec::normalizeParamToken(" TRUE "), "true"); + EXPECT_EQ(DeviceAttachmentSpec::normalizeParamToken("YUV"), "yuv"); + EXPECT_EQ(DeviceAttachmentSpec::normalizeParamToken("480p"), "480p"); + EXPECT_EQ(DeviceAttachmentSpec::normalizeParamToken(""), ""); +} + +TEST(DeviceAttachmentSpecFindOptionalParamTest, LattermostExactNameWins) +{ + const ParamList params = makeParams({ + {"display", "0"}, + {"other", "ignored"}, + {"display", "2"}, + }); + + const std::optional> matched = + DeviceAttachmentSpec::findOptionalParam(params, "display"); + + ASSERT_TRUE(matched.has_value()); + EXPECT_EQ(matched->first, "display"); + EXPECT_EQ(matched->second, "2"); +} + +TEST(DeviceAttachmentSpecFindOptionalParamWithSynonymsTest, LattermostSynonymWins) +{ + const std::vector synonyms = { + "cmd-timeout-ms", + "command-timeout-ms", + }; + + const ParamList params = makeParams({ + {"cmd-timeout-ms", "5"}, + {"command-timeout-ms", "25"}, + }); + + const std::optional> matched = + DeviceAttachmentSpec::findOptionalParamWithSynonyms(params, synonyms); + + ASSERT_TRUE(matched.has_value()); + EXPECT_EQ(matched->first, "command-timeout-ms"); + EXPECT_EQ(matched->second, "25"); +} + +TEST(DeviceAttachmentSpecFindOptionalParamWithSynonymsTest, AbsentReturnsNullopt) +{ + const std::vector synonyms = {"data-port"}; + const ParamList params = makeParams({{"cmd-port", "56001"}}); + + EXPECT_FALSE(DeviceAttachmentSpec::findOptionalParamWithSynonyms( + params, synonyms).has_value()); +} + +TEST(DeviceAttachmentSpecParseParamValueAsIntTest, ParsesValidInteger) +{ + EXPECT_EQ( + DeviceAttachmentSpec::parseParamValueAsInt("width", "1280"), + 1280); + EXPECT_EQ( + DeviceAttachmentSpec::parseParamValueAsInt("width", "-42"), + -42); +} + +TEST(DeviceAttachmentSpecParseParamValueAsIntTest, InvalidIntegerThrows) +{ + try { + DeviceAttachmentSpec::parseParamValueAsInt("width", "not-a-number"); + FAIL() << "Expected std::runtime_error"; + } + catch (const std::runtime_error& exception) + { + sscl::tests::expectExceptionMessageContains( + exception, "Failed to parse 'width'"); + } +} + +TEST(DeviceAttachmentSpecParseParamValueAsIntTest, OutOfRangeThrows) +{ + try { + DeviceAttachmentSpec::parseParamValueAsInt( + "width", + "999999999999999999999"); + FAIL() << "Expected std::runtime_error"; + } + catch (const std::runtime_error& exception) + { + sscl::tests::expectExceptionMessageContains( + exception, "out of range"); + } +} + +TEST(DeviceAttachmentSpecParseParamValueAsPositiveIntTest, RejectsNonPositive) +{ + EXPECT_EQ( + DeviceAttachmentSpec::parseParamValueAsPositiveInt("width", "640"), + 640); + + try { + DeviceAttachmentSpec::parseParamValueAsPositiveInt("width", "0"); + FAIL() << "Expected std::runtime_error"; + } + catch (const std::runtime_error& exception) + { + sscl::tests::expectExceptionMessageContains( + exception, "must be positive"); + } + + try { + DeviceAttachmentSpec::parseParamValueAsPositiveInt("width", "-10"); + FAIL() << "Expected std::runtime_error"; + } + catch (const std::runtime_error& exception) + { + sscl::tests::expectExceptionMessageContains( + exception, "must be positive"); + } +} + +TEST(DeviceAttachmentSpecParseParamValueAsBoolTest, ParsesRecognizedValues) +{ + EXPECT_TRUE(DeviceAttachmentSpec::parseParamValueAsBool("")); + EXPECT_TRUE(DeviceAttachmentSpec::parseParamValueAsBool("true")); + EXPECT_TRUE(DeviceAttachmentSpec::parseParamValueAsBool(" YES ")); + EXPECT_TRUE(DeviceAttachmentSpec::parseParamValueAsBool("1")); + EXPECT_FALSE(DeviceAttachmentSpec::parseParamValueAsBool("false")); + EXPECT_FALSE(DeviceAttachmentSpec::parseParamValueAsBool("0")); + EXPECT_FALSE(DeviceAttachmentSpec::parseParamValueAsBool("no")); + EXPECT_FALSE(DeviceAttachmentSpec::parseParamValueAsBool( + "", /*emptyMeansTrue=*/false)); +} + +TEST(DeviceAttachmentSpecParseParamValueAsBoolTest, UnknownValueThrows) +{ + EXPECT_THROW( + DeviceAttachmentSpec::parseParamValueAsBool("maybe"), + std::runtime_error); +} + +TEST(DeviceAttachmentSpecParseRequiredParamAsIntTest, MissingParamThrows) +{ + const ParamList params = makeParams({{"screen", "1"}}); + + try { + DeviceAttachmentSpec::parseRequiredParamAsInt(params, "display"); + FAIL() << "Expected std::runtime_error"; + } + catch (const std::runtime_error& exception) + { + sscl::tests::expectExceptionMessageContains( + exception, "No display specified in params"); + } +} + +TEST(DeviceAttachmentSpecParseRequiredParamAsIntTest, LattermostValueWins) +{ + const ParamList params = makeParams({ + {"display", "0"}, + {"display", "3"}, + }); + + EXPECT_EQ( + DeviceAttachmentSpec::parseRequiredParamAsInt(params, "display"), + 3); +} + +TEST(DeviceAttachmentSpecParseOptionalParamAsIntTest, ReturnsDefaultWhenAbsent) +{ + const ParamList params = makeParams({{"screen", "1"}}); + + EXPECT_EQ( + DeviceAttachmentSpec::parseOptionalParamAsInt(params, "display", 42), + 42); +} + +TEST(DeviceAttachmentSpecParseOptionalParamAsIntWithSynonymsTest, SynonymLattermostWins) +{ + const std::vector synonyms = { + "n-dgrams-per-frame", + "num-dgrams-per-frame", + }; + + const ParamList params = makeParams({ + {"n-dgrams-per-frame", "10"}, + {"num-dgrams-per-frame", "99"}, + }); + + EXPECT_EQ( + DeviceAttachmentSpec::parseOptionalParamAsIntWithSynonyms( + params, synonyms, 84), + 99); +} + +TEST(DeviceAttachmentSpecParseRequiredParamAsIntWithSynonymsTest, ParsesMatchedSynonym) +{ + const std::vector synonyms = { + "cmd-timeout-ms", + "command-timeout-ms", + }; + + const ParamList params = makeParams({ + {"command-timeout-ms", "15"}, + }); + + EXPECT_EQ( + DeviceAttachmentSpec::parseRequiredParamAsIntWithSynonyms( + params, + synonyms, + "missing timeout"), + 15); +} + +TEST(DeviceAttachmentSpecParseRequiredParamValueWithSynonymsTest, RequiresNonEmptyValue) +{ + const std::vector synonyms = {"colour-space"}; + const ParamList missing = makeParams({{"width", "640"}}); + + try { + DeviceAttachmentSpec::parseRequiredParamValueWithSynonyms( + missing, synonyms, "colour-space required"); + FAIL() << "Expected std::runtime_error"; + } + catch (const std::runtime_error& exception) + { + sscl::tests::expectExceptionMessageContains( + exception, "colour-space required"); + } + + const ParamList emptyValue = makeParams({{"colour-space", ""}}); + + try { + DeviceAttachmentSpec::parseRequiredParamValueWithSynonyms( + emptyValue, synonyms, "colour-space required"); + FAIL() << "Expected std::runtime_error"; + } + catch (const std::runtime_error& exception) + { + sscl::tests::expectExceptionMessageContains( + exception, "colour-space required"); + } + + const ParamList present = makeParams({{"colour-space", "yuv"}}); + EXPECT_EQ( + DeviceAttachmentSpec::parseRequiredParamValueWithSynonyms( + present, synonyms, "colour-space required"), + "yuv"); +} + +TEST(DeviceAttachmentSpecParseOptionalParamValueWithSynonymsTest, OptionalStringValue) +{ + const std::vector synonyms = {"colour-space"}; + const ParamList absent = makeParams({{"width", "640"}}); + + EXPECT_FALSE(DeviceAttachmentSpec::parseOptionalParamValueWithSynonyms( + absent, synonyms, "empty colour-space").has_value()); + + const ParamList emptyValue = makeParams({{"colour-space", ""}}); + try { + DeviceAttachmentSpec::parseOptionalParamValueWithSynonyms( + emptyValue, synonyms, "empty colour-space"); + FAIL() << "Expected std::runtime_error"; + } + catch (const std::runtime_error& exception) + { + sscl::tests::expectExceptionMessageContains( + exception, "empty colour-space"); + } + + const ParamList present = makeParams({{"colour-space", "yuv"}}); + const std::optional parsed = + DeviceAttachmentSpec::parseOptionalParamValueWithSynonyms( + present, synonyms, "empty colour-space"); + + ASSERT_TRUE(parsed.has_value()); + EXPECT_EQ(*parsed, "yuv"); +} + +TEST(DeviceAttachmentSpecParseOptionalParamAsBoolWithSynonymsTest, ParsesPresenceAndValues) +{ + const std::vector synonyms = { + "full-planar-is-optional", + "opt-planar", + }; + + const ParamList absent = makeParams({{"width", "640"}}); + EXPECT_FALSE(DeviceAttachmentSpec::parseOptionalParamAsBoolWithSynonyms( + absent, synonyms, false)); + + const ParamList presentEmpty = makeParams({{"opt-planar", ""}}); + EXPECT_TRUE(DeviceAttachmentSpec::parseOptionalParamAsBoolWithSynonyms( + presentEmpty, synonyms, false)); + + const ParamList presentFalse = makeParams({{"opt-planar", "false"}}); + EXPECT_FALSE(DeviceAttachmentSpec::parseOptionalParamAsBoolWithSynonyms( + presentFalse, synonyms, true)); + + const ParamList lattermostWins = makeParams({ + {"opt-planar", "true"}, + {"full-planar-is-optional", "false"}, + }); + EXPECT_FALSE(DeviceAttachmentSpec::parseOptionalParamAsBoolWithSynonyms( + lattermostWins, synonyms, true)); +} + +TEST(DeviceAttachmentSpecRejectUnknownParamsTest, RejectsUnknownNames) +{ + const std::vector knownA = {"width", "dim-w"}; + const std::vector knownB = {"height", "dim-h"}; + const ParamList params = makeParams({ + {"dim-w", "640"}, + {"unknown-param", "1"}, + }); + + try { + DeviceAttachmentSpec::rejectUnknownParams( + params, + "unknown param '", + knownA, + knownB); + FAIL() << "Expected std::runtime_error"; + } + catch (const std::runtime_error& exception) + { + sscl::tests::expectExceptionMessageContains( + exception, "unknown-param"); + } +} + +TEST(DeviceAttachmentSpecRejectUnknownParamsTest, AcceptsAllKnownNames) +{ + const std::vector knownA = {"width", "dim-w"}; + const std::vector knownB = {"height", "dim-h"}; + const ParamList params = makeParams({ + {"dim-w", "640"}, + {"dim-h", "480"}, + }); + + EXPECT_NO_THROW(DeviceAttachmentSpec::rejectUnknownParams( + params, + "unknown param '", + knownA, + knownB)); +} + +} // namespace +} // namespace device +} // namespace smo diff --git a/include/user/deviceAttachmentSpec.h b/include/user/deviceAttachmentSpec.h index b3d1c0b..b0985b2 100644 --- a/include/user/deviceAttachmentSpec.h +++ b/include/user/deviceAttachmentSpec.h @@ -1,6 +1,10 @@ #ifndef SENSORDEVICESPEC_H #define SENSORDEVICESPEC_H +#include +#include +#include +#include #include #include #include @@ -8,6 +12,7 @@ #include #include #include +#include namespace smo { namespace device { @@ -138,33 +143,17 @@ public: * @param paramName The name of the parameter to parse * @return The parsed integer value * @throws std::runtime_error if parameter is not found or cannot be parsed + * @note The lattermost supplied matching param wins if multiple are present */ static int parseRequiredParamAsInt( const std::vector>& params, const std::string& paramName ) { - auto it = std::find_if( - params.begin(), - params.end(), - [¶mName](const auto& param) { - return param.first == paramName; - } - ); - - if (it == params.end()) - { - throw std::runtime_error( - "No " + paramName + " specified in params"); - } - - try { - return std::stoi(it->second); - } catch (const std::exception& e) { - throw std::runtime_error( - "Failed to parse '" + paramName + "' param value '" - + it->second + "' as integer: " + e.what()); - } + return parseRequiredParamAsIntWithSynonyms( + params, + std::array{{paramName}}, + "No " + paramName + " specified in params"); } /** @@ -181,9 +170,197 @@ public: int defaultValue ) { - const std::string paramNames[] = {paramName}; return parseOptionalParamAsIntWithSynonyms( - params, paramNames, defaultValue); + params, std::array{{paramName}}, defaultValue); + } + + /** + * @brief Test whether a parameter name appears in a synonym collection + */ + template + static bool namesContain( + const SynonymCollectionT& synonymNames, + std::string_view paramName) + { + return std::find( + std::begin(synonymNames), + std::end(synonymNames), + paramName) != std::end(synonymNames); + } + + /** + * @brief Test whether a parameter list contains an exact parameter name + */ + static bool paramsContain( + const std::vector>& params, + std::string_view paramName) + { + for (const auto& param : params) + { + if (param.first == paramName) { + return true; + } + } + + return false; + } + + /** + * @brief Test whether a parameter name matches any synonym collection + */ + template + static bool paramNameMatchesAnySynonymGroup( + std::string_view paramName, + const SynonymCollectionTs&... synonymGroups) + { + return (namesContain(synonymGroups, paramName) || ...); + } + + /** + * @brief Trim surrounding whitespace and lowercase a DAP param token + */ + static std::string normalizeParamToken(std::string token) + { + while (!token.empty() + && std::isspace(static_cast(token.back()))) + { + token.pop_back(); + } + + while (!token.empty() + && std::isspace(static_cast(token.front()))) + { + token.erase(token.begin()); + } + + for (char& character : token) + { + character = static_cast( + std::tolower(static_cast(character))); + } + + return token; + } + + /** + * @brief Find the lattermost param matching any synonym (name + value pair) + */ + template + static std::optional> + findOptionalParamWithSynonyms( + const std::vector>& params, + const SynonymCollectionT& synonymNames) + { + for (auto paramIt = params.rbegin(); paramIt != params.rend(); ++paramIt) + { + if (namesContain(synonymNames, paramIt->first)) { + return *paramIt; + } + } + + return std::nullopt; + } + + /** + * @brief Find the lattermost param with an exact name (name + value pair) + */ + static std::optional> findOptionalParam( + const std::vector>& params, + std::string_view paramName) + { + return findOptionalParamWithSynonyms( + params, std::array{{paramName}}); + } + + /** + * @brief Parse a DAP integer param value + * @throws std::runtime_error if parsing fails + */ + static int parseParamValueAsInt( + const std::string& paramName, + const std::string& paramValue) + { + try { + return std::stoi(paramValue); + } + catch (const std::invalid_argument&) + { + throw std::runtime_error( + "Failed to parse '" + paramName + "' param value '" + + paramValue + "' as integer"); + } + catch (const std::out_of_range&) + { + throw std::runtime_error( + "'" + paramName + "' param value '" + paramValue + + "' is out of range"); + } + } + + /** + * @brief Parse a DAP integer param value; result must be strictly positive + * @throws std::runtime_error if parsing fails or value is not positive + */ + static int parseParamValueAsPositiveInt( + const std::string& paramName, + const std::string& paramValue) + { + const int parsedValue = parseParamValueAsInt(paramName, paramValue); + if (parsedValue <= 0) + { + throw std::runtime_error( + "'" + paramName + "' must be positive"); + } + + return parsedValue; + } + + /** + * @brief Parse a DAP boolean param value + * @param value Raw param value; empty string means @p emptyMeansTrue + * @throws std::runtime_error if the value is not recognized + */ + static bool parseParamValueAsBool( + const std::string& value, + bool emptyMeansTrue = true) + { + if (value.empty()) { + return emptyMeansTrue; + } + + const std::string lowered = normalizeParamToken(value); + if (lowered == "true" || lowered == "1" || lowered == "yes") { + return true; + } + + if (lowered == "false" || lowered == "0" || lowered == "no") { + return false; + } + + throw std::runtime_error( + "Boolean param value '" + value + + "' is not recognized (use true/false, 1/0, yes/no, or omit value)"); + } + + /** + * @brief Parse a required integer param using synonyms + * @note The lattermost supplied matching param wins if multiple are present + */ + template + static int parseRequiredParamAsIntWithSynonyms( + const std::vector>& params, + const SynonymCollectionT& synonymNames, + const std::string& missingParamMessage) + { + const std::optional> matchedParam = + findOptionalParamWithSynonyms(params, synonymNames); + + if (!matchedParam) + { + throw std::runtime_error(missingParamMessage); + } + + return parseParamValueAsInt(matchedParam->first, matchedParam->second); } /** @@ -201,26 +378,102 @@ public: int defaultValue ) { - // Loop through params in reverse order; lattermost supplied param wins. - for (auto paramIt = params.rbegin(); paramIt != params.rend(); ++paramIt) - { - const auto& [paramName, paramValue] = *paramIt; - auto synonymIt = std::find( - std::begin(synonymNames), std::end(synonymNames), paramName); + const std::optional> matchedParam = + findOptionalParamWithSynonyms(params, synonymNames); - if (synonymIt == std::end(synonymNames)) - { continue; } - - try { - return std::stoi(paramValue); - } catch (const std::exception& e) { - throw std::runtime_error( - "Failed to parse '" + paramName + "' param value '" - + paramValue + "' as integer: " + e.what()); - } + if (!matchedParam) { + return defaultValue; } - return defaultValue; + return parseParamValueAsInt(matchedParam->first, matchedParam->second); + } + + /** + * @brief Parse a required non-empty param value using synonyms + * @note The lattermost supplied matching param wins if multiple are present + * @throws std::runtime_error if no matching param is found or value is empty + */ + template + static std::string parseRequiredParamValueWithSynonyms( + const std::vector>& params, + const SynonymCollectionT& synonymNames, + const std::string& missingOrEmptyValueMessage) + { + const std::optional> matchedParam = + findOptionalParamWithSynonyms(params, synonymNames); + + if (!matchedParam || matchedParam->second.empty()) + { + throw std::runtime_error(missingOrEmptyValueMessage); + } + + return matchedParam->second; + } + + /** + * @brief Parse an optional non-empty param value using synonyms + * @return Parsed value, or std::nullopt when the param is absent + * @throws std::runtime_error if the param is present but its value is empty + */ + template + static std::optional parseOptionalParamValueWithSynonyms( + const std::vector>& params, + const SynonymCollectionT& synonymNames, + const std::string& emptyValueMessage) + { + const std::optional> matchedParam = + findOptionalParamWithSynonyms(params, synonymNames); + + if (!matchedParam) { + return std::nullopt; + } + + if (matchedParam->second.empty()) { + throw std::runtime_error(emptyValueMessage); + } + + return matchedParam->second; + } + + /** + * @brief Parse an optional boolean param using synonyms + * @note The lattermost supplied matching param wins if multiple are present + */ + template + static bool parseOptionalParamAsBoolWithSynonyms( + const std::vector>& params, + const SynonymCollectionT& synonymNames, + bool defaultValue = false, + bool emptyMeansTrue = true) + { + const std::optional> matchedParam = + findOptionalParamWithSynonyms(params, synonymNames); + + if (!matchedParam) { + return defaultValue; + } + + return parseParamValueAsBool(matchedParam->second, emptyMeansTrue); + } + + /** + * @brief Reject params whose names are not covered by any synonym group + */ + template + static void rejectUnknownParams( + const std::vector>& params, + const std::string& unknownParamMessagePrefix, + const SynonymCollectionTs&... synonymGroups) + { + for (const auto& param : params) + { + if (!paramNameMatchesAnySynonymGroup( + param.first, synonymGroups...)) + { + throw std::runtime_error( + unknownParamMessagePrefix + param.first + "'"); + } + } } };