From 8dbf7581a305ebe512bd2eba4dbb607badb5eed0 Mon Sep 17 00:00:00 2001 From: Felix Schlepper Date: Thu, 11 Jun 2026 16:11:51 +0200 Subject: [PATCH 1/3] Common: allow for dynamic types Signed-off-by: Felix Schlepper --- .../include/CommonUtils/ConfigurableParam.h | 249 +++++++++- .../CommonUtils/ConfigurableParamHelper.h | 28 +- .../CommonUtils/ConfigurableParamTest.h | 11 + Common/Utils/src/ConfigurableParam.cxx | 429 +++++++++++++++++- Common/Utils/src/ConfigurableParamHelper.cxx | 87 +++- Common/Utils/test/testConfigurableParam.cxx | 94 +++- 6 files changed, 865 insertions(+), 33 deletions(-) diff --git a/Common/Utils/include/CommonUtils/ConfigurableParam.h b/Common/Utils/include/CommonUtils/ConfigurableParam.h index b9234926b7c40..6aa6a7009dc57 100644 --- a/Common/Utils/include/CommonUtils/ConfigurableParam.h +++ b/Common/Utils/include/CommonUtils/ConfigurableParam.h @@ -9,15 +9,24 @@ // granted to it by virtue of its status as an Intergovernmental Organization // or submit itself to any jurisdiction. -//first version 8/2018, Sandro Wenzel +// first version 8/2018, Sandro Wenzel #ifndef COMMON_SIMCONFIG_INCLUDE_SIMCONFIG_CONFIGURABLEPARAM_H_ #define COMMON_SIMCONFIG_INCLUDE_SIMCONFIG_CONFIGURABLEPARAM_H_ -#include +#include #include +#include +#include +#include +#include #include +#include +#include +#include +#include #include +#include #include #include #include @@ -136,6 +145,233 @@ class EnumRegistry std::unordered_map entries; }; +template +concept Container = !std::is_same_v, std::string> && requires(T t) { + typename T::value_type; + typename T::iterator; + { t.begin() } -> std::same_as; + { t.end() } -> std::same_as; +}; + +template +concept MapLike = Container && requires { + typename T::key_type; + typename T::mapped_type; +}; + +template +concept SequenceContainer = Container && !MapLike; + +template +concept HasPushBack = requires(T a, typename T::value_type v) { + { a.push_back(v) } -> std::same_as; +}; + +template +inline constexpr bool AlwaysFalse = false; + +class ContainerParser +{ + public: + template + static T parse(const std::string& str) + { + if constexpr (MapLike) { + return parseMap(str); + } else if constexpr (SequenceContainer) { + return parseSet(str); + } else if constexpr (Container) { + return parseSequence(str); + } else { + return parseScalar(str); + } + } + + static std::string trim(const std::string& str) + { + auto start = str.find_first_not_of(" \t\n\r\f\v"); + if (start == std::string::npos) { + return ""; + } + auto end = str.find_last_not_of(" \t\n\r\f\v"); + return str.substr(start, end - start + 1); + } + + private: + // Parse vector, list, deque, array + template + static SequenceT parseSequence(const std::string& str) + { + SequenceT result; + using ValueType = typename SequenceT::value_type; + std::string cleaned = str; + if (!cleaned.empty() && cleaned.front() == '[' && cleaned.back() == ']') { // removed brackets [1,2,3] -> 1,2,3 + cleaned = cleaned.substr(1, cleaned.length() - 2); + } + if (cleaned.empty() || cleaned == "{}") { // nothing to do + return result; + } + if constexpr (Container) { + static_assert(AlwaysFalse, "Nested containers are not supported as configurable parameters"); + } + auto tokens = split(cleaned, ','); + for (const auto& token : tokens) { + std::string trimmed = trim(token); + result.insert(result.end(), parseScalar(trimmed)); + } + return result; + } + + // Parse map, unordered_map, multimap + template + static MapT parseMap(const std::string& str) + { + MapT result; + using KeyType = typename MapT::key_type; + using ValueType = typename MapT::mapped_type; + std::string cleaned = str; + if (!cleaned.empty() && cleaned.front() == '{' && cleaned.back() == '}') { // stip braces {a:1,b:2} -> a:1,b:2 + cleaned = cleaned.substr(1, cleaned.length() - 2); + } + if (cleaned.empty()) { // nothing to do + return result; + } + if constexpr (Container || Container) { + static_assert(AlwaysFalse, "Nested containers are not supported as configurable parameters"); + } + auto pairs = split(cleaned, ','); + for (const auto& pair_str : pairs) { + auto kv = split(pair_str, ':'); + if (kv.size() != 2) { + throw std::runtime_error("Invalid map syntax: " + pair_str + ". Expected 'key:value' format, got "); + } + KeyType key = parseScalar(trim(kv[0])); + result[key] = parseScalar(trim(kv[1])); + } + return result; + } + + // Parse set containers + template + static SetT parseSet(const std::string& str) + { + SetT result; + using ValueType = typename SetT::value_type; + auto vec = parseSequence>(str); + for (const auto& val : vec) { + if constexpr (HasPushBack) { + result.push_back(val); + } else { + result.insert(val); + } + } + return result; + } + + // Parse scalar types + template + static T parseScalar(const std::string& str) + { + if constexpr (std::is_same_v) { + return str; + } else if constexpr (std::is_same_v) { + std::string lower = str; + std::transform(lower.begin(), lower.end(), lower.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); + if (lower == "true" || lower == "1") { + return true; + } + if (lower == "false" || lower == "0") { + return false; + } + throw std::runtime_error("Invalid boolean value: " + str); + } else if constexpr (std::is_same_v || std::is_same_v) { + size_t pos = 0; + long long value = std::stoll(str, &pos); + if (pos != str.size()) { + throw std::runtime_error("Failed to parse '" + str + "' as char type"); + } + if (value < std::numeric_limits::min() || value > std::numeric_limits::max()) { + throw std::runtime_error("Value out of range for char type: " + str); + } + return static_cast(value); + } else if constexpr (std::is_same_v) { + size_t pos = 0; + unsigned long long value = std::stoull(str, &pos); + if (pos != str.size()) { + throw std::runtime_error("Failed to parse '" + str + "' as unsigned char type"); + } + if (value > std::numeric_limits::max()) { + throw std::runtime_error("Value out of range for unsigned char type: " + str); + } + return static_cast(value); + } else if constexpr (std::is_integral_v && std::is_unsigned_v) { + if (!str.empty() && str.front() == '-') { + throw std::runtime_error("Value out of range for unsigned integer type: " + str); + } + size_t pos = 0; + unsigned long long value = std::stoull(str, &pos); + if (pos != str.size() || value > std::numeric_limits::max()) { + throw std::runtime_error("Failed to parse '" + str + "' as unsigned integer type"); + } + return static_cast(value); + } else if constexpr (std::is_integral_v) { + size_t pos = 0; + long long value = std::stoll(str, &pos); + if (pos != str.size() || value < std::numeric_limits::min() || value > std::numeric_limits::max()) { + throw std::runtime_error("Failed to parse '" + str + "' as signed integer type"); + } + return static_cast(value); + } else if constexpr (std::is_floating_point_v) { + size_t pos = 0; + long double value = std::stold(str, &pos); + if (pos != str.size()) { + throw std::runtime_error("Failed to parse '" + str + "' as floating point type"); + } + return static_cast(value); + } else { + std::istringstream iss(str); + T value; + iss >> value; + iss >> std::ws; + if (iss.fail() || !iss.eof()) { + throw std::runtime_error("Failed to parse '" + str + "' as " + typeid(T).name()); + } + return value; + } + } + + // Split respecting nested brackets and braces + static std::vector split(const std::string& str, char delimiter) + { + std::vector tokens; + std::string current; + int bracket_depth = 0; + int brace_depth = 0; + for (char c : str) { + if (c == '[') { + bracket_depth++; + } else if (c == ']') { + bracket_depth--; + } else if (c == '{') { + brace_depth++; + } else if (c == '}') { + brace_depth--; + } else if (c == delimiter && bracket_depth == 0 && brace_depth == 0) { + if (!current.empty()) { + tokens.push_back(current); + current.clear(); + } + continue; + } + current += c; + } + if (!current.empty()) { + tokens.push_back(current); + } + return tokens; + } +}; + class ConfigurableParam { public: @@ -247,6 +483,13 @@ class ConfigurableParam static void setValue(std::string const& key, std::string const& valuestring); static void setEnumValue(const std::string&, const std::string&); static void setArrayValue(const std::string&, const std::string&); + static void setContainerValue(const std::string&, const std::string&); + static bool isRegisteredContainerType(const std::string& typeName); + static void registerContainerType(const std::string& key, const std::string& typeName); + static std::string getRegisteredContainerType(const std::string& key); + static bool assignRegisteredContainer(const std::string& typeName, void* target, const void* source); + static bool areRegisteredContainersEqual(const std::string& typeName, const void* lhs, const void* rhs); + static std::string registeredContainerAsString(const std::string& typeName, const void* source); // update the storagemap from a vector of key/value pairs, calling setValue for each pair static void setValues(std::vector> const& keyValues); @@ -254,7 +497,7 @@ class ConfigurableParam // initializes the parameter database static void initialize(); - // create CCDB snapsnot + // create CCDB snapshot static void toCCDB(std::string filename); // load from (CCDB) snapshot static void fromCCDB(std::string filename); diff --git a/Common/Utils/include/CommonUtils/ConfigurableParamHelper.h b/Common/Utils/include/CommonUtils/ConfigurableParamHelper.h index 76631b449670c..dd28366d0a237 100644 --- a/Common/Utils/include/CommonUtils/ConfigurableParamHelper.h +++ b/Common/Utils/include/CommonUtils/ConfigurableParamHelper.h @@ -9,21 +9,21 @@ // granted to it by virtue of its status as an Intergovernmental Organization // or submit itself to any jurisdiction. -//first version 8/2018, Sandro Wenzel +// first version 8/2018, Sandro Wenzel #ifndef COMMON_SIMCONFIG_INCLUDE_SIMCONFIG_CONFIGURABLEPARAMHELPER_H_ #define COMMON_SIMCONFIG_INCLUDE_SIMCONFIG_CONFIGURABLEPARAMHELPER_H_ #include "CommonUtils/ConfigurableParam.h" -#include "TClass.h" + #include +#include +#include +#include #include #include -#include "TFile.h" -namespace o2 -{ -namespace conf +namespace o2::conf { // ---------------------------------------------------------------- @@ -342,7 +342,19 @@ class ConfigurableParamPromoter : public Base, virtual public ConfigurableParam } }; -} // namespace conf -} // namespace o2 +inline bool isContainer(const std::string& typeName) +{ + return ConfigurableParam::isRegisteredContainerType(typeName); +} + +inline bool isContainer(TDataMember const& dm) +{ + if (auto* cl = dm.GetClass(); cl && isContainer(cl->GetName())) { + return true; + } + return isContainer(dm.GetTrueTypeName()) || isContainer(dm.GetFullTypeName()); +} + +} // namespace o2::conf #endif /* COMMON_SIMCONFIG_INCLUDE_SIMCONFIG_CONFIGURABLEPARAMHELPER_H_ */ diff --git a/Common/Utils/include/CommonUtils/ConfigurableParamTest.h b/Common/Utils/include/CommonUtils/ConfigurableParamTest.h index 547bbf9ba8c38..09d7b45ef1608 100644 --- a/Common/Utils/include/CommonUtils/ConfigurableParamTest.h +++ b/Common/Utils/include/CommonUtils/ConfigurableParamTest.h @@ -15,6 +15,12 @@ #include "CommonUtils/ConfigurableParam.h" #include "CommonUtils/ConfigurableParamHelper.h" +#include +#include +#include +#include +#include + namespace o2::conf::test { struct TestParam : public o2::conf::ConfigurableParamHelper { @@ -37,6 +43,11 @@ struct TestParam : public o2::conf::ConfigurableParamHelper { int iValueProvenanceTest{0}; TestEnum eValue = TestEnum::C; int caValue[3] = {0, 1, 2}; + std::vector vec; + std::vector u8vec; + std::map map; + std::map smap; + std::set set; O2ParamDef(TestParam, "TestParam"); }; diff --git a/Common/Utils/src/ConfigurableParam.cxx b/Common/Utils/src/ConfigurableParam.cxx index fd69f51402cd5..35cc2aead1c1d 100644 --- a/Common/Utils/src/ConfigurableParam.cxx +++ b/Common/Utils/src/ConfigurableParam.cxx @@ -12,6 +12,7 @@ // first version 8/2018, Sandro Wenzel #include "CommonUtils/ConfigurableParam.h" +#include #include "CommonUtils/StringUtils.h" #include "CommonUtils/KeyValParam.h" #include "CommonUtils/ConfigurableParamReaders.h" @@ -24,7 +25,12 @@ #include #include #include +#include +#include +#include +#include #include +#include #ifdef NDEBUG #undef NDEBUG #endif @@ -32,6 +38,7 @@ #include #include #include +#include #include #include "TDataMember.h" #include "TDataType.h" @@ -39,6 +46,13 @@ #include "TEnum.h" #include "TEnumConstant.h" #include +#include +#include +#include +#include +#include +#include +#include namespace o2 { @@ -54,6 +68,11 @@ EnumRegistry* ConfigurableParam::sEnumRegistry = nullptr; bool ConfigurableParam::sIsFullyInitialized = false; bool ConfigurableParam::sRegisterMode = true; +namespace +{ +std::map sKeyToContainerTypeMap; +} // namespace + // ------------------------------------------------------------------ std::ostream& operator<<(std::ostream& out, ConfigurableParam const& param) @@ -77,7 +96,7 @@ bool keyInTree(boost::property_tree::ptree* pt, const std::string& key) return reply; } -// Convert a type info to the appropiate literal suffix +// Convert a type info to the appropriate literal suffix std::string getLiteralSuffixFromType(const std::type_info& type) { if (type == typeid(float)) { @@ -101,6 +120,321 @@ std::string getLiteralSuffixFromType(const std::type_info& type) return ""; } +namespace +{ + +struct ContainerHandler { + std::function parseAssign; + std::function serialize; + std::function assign; + std::function equal; +}; + +struct ContainerHandlerRegistry { + std::map byName; + std::map byType; +}; + +template +struct IsUnorderedSet : std::false_type { +}; + +template +struct IsUnorderedSet> : std::true_type { +}; + +template +struct IsUnorderedMap : std::false_type { +}; + +template +struct IsUnorderedMap> : std::true_type { +}; + +std::string normalizeContainerTypeName(std::string typeName) +{ + typeName = ContainerParser::trim(typeName); + for (size_t pos = typeName.find("std::"); pos != std::string::npos; pos = typeName.find("std::", pos)) { + typeName.erase(pos, 5); + } + + std::string out; + bool pendingSpace = false; + for (char c : typeName) { + if (std::isspace(static_cast(c))) { + if (!out.empty() && out.back() != '<' && out.back() != ',') { + pendingSpace = true; + } + continue; + } + if ((c == ',' || c == '>') && !out.empty() && out.back() == ' ') { + out.pop_back(); + } + if (pendingSpace && c != ',' && c != '>' && !out.empty() && out.back() != '<' && out.back() != ',') { + out += ' '; + } + out += c; + pendingSpace = false; + } + return out; +} + +template +struct TypeName; + +#define REGISTER_SCALAR_NAME(TYPE, NAME) \ + template <> \ + struct TypeName { \ + static constexpr const char* value = NAME; \ + } + +REGISTER_SCALAR_NAME(bool, "bool"); +REGISTER_SCALAR_NAME(char, "char"); +REGISTER_SCALAR_NAME(signed char, "signed char"); +REGISTER_SCALAR_NAME(unsigned char, "unsigned char"); +REGISTER_SCALAR_NAME(short, "short"); +REGISTER_SCALAR_NAME(unsigned short, "unsigned short"); +REGISTER_SCALAR_NAME(int, "int"); +REGISTER_SCALAR_NAME(unsigned int, "unsigned int"); +REGISTER_SCALAR_NAME(long, "long"); +REGISTER_SCALAR_NAME(unsigned long, "unsigned long"); +REGISTER_SCALAR_NAME(long long, "long long"); +REGISTER_SCALAR_NAME(unsigned long long, "unsigned long long"); +REGISTER_SCALAR_NAME(float, "float"); +REGISTER_SCALAR_NAME(double, "double"); +REGISTER_SCALAR_NAME(std::string, "string"); + +#undef REGISTER_SCALAR_NAME + +template +std::string scalarAsString(const T& value) +{ + if constexpr (std::is_same_v) { + return value ? "1" : "0"; + } else if constexpr (std::is_same_v || std::is_same_v) { + return std::to_string(static_cast(value)); + } else if constexpr (std::is_same_v) { + return std::to_string(static_cast(value)); + } else if constexpr (std::is_same_v) { + return value; + } else if constexpr (std::is_floating_point_v) { + std::ostringstream out; + out << std::setprecision(std::numeric_limits::max_digits10) << value; + return out.str(); + } else { + return std::to_string(value); + } +} + +template +std::string sequenceAsString(const ContainerT& container) +{ + using ValueType = typename ContainerT::value_type; + std::ostringstream out; + out << '['; + bool first = true; + std::vector unorderedValues; + if constexpr (IsUnorderedSet::value) { + for (const auto& value : container) { + unorderedValues.push_back(scalarAsString(static_cast(value))); + } + std::sort(unorderedValues.begin(), unorderedValues.end()); + } + const auto emitValue = [&out, &first](const std::string& value) { + if (!first) { + out << ','; + } + out << value; + first = false; + }; + if constexpr (IsUnorderedSet::value) { + for (const auto& value : unorderedValues) { + emitValue(value); + } + } else { + for (const auto& value : container) { + emitValue(scalarAsString(static_cast(value))); + } + } + out << ']'; + return out.str(); +} + +template +std::string mapAsString(const MapT& container) +{ + std::ostringstream out; + out << '{'; + bool first = true; + std::vector> unorderedValues; + if constexpr (IsUnorderedMap::value) { + for (const auto& [key, value] : container) { + unorderedValues.emplace_back(scalarAsString(key), scalarAsString(value)); + } + std::sort(unorderedValues.begin(), unorderedValues.end()); + } + const auto emitValue = [&out, &first](const std::string& key, const std::string& value) { + if (!first) { + out << ','; + } + out << key << ':' << value; + first = false; + }; + if constexpr (IsUnorderedMap::value) { + for (const auto& [key, value] : unorderedValues) { + emitValue(key, value); + } + } else { + for (const auto& [key, value] : container) { + emitValue(scalarAsString(key), scalarAsString(value)); + } + } + out << '}'; + return out.str(); +} + +template +ContainerHandler makeSequenceHandler() +{ + return { + [](void* target, const std::string& value) { + *static_cast(target) = ContainerParser::parse(value); + }, + [](const void* source) { + return sequenceAsString(*static_cast(source)); + }, + [](void* target, const void* source) { + *static_cast(target) = *static_cast(source); + }, + [](const void* lhs, const void* rhs) { + return *static_cast(lhs) == *static_cast(rhs); + }}; +} + +template +ContainerHandler makeMapHandler() +{ + return { + [](void* target, const std::string& value) { + *static_cast(target) = ContainerParser::parse(value); + }, + [](const void* source) { + return mapAsString(*static_cast(source)); + }, + [](void* target, const void* source) { + *static_cast(target) = *static_cast(source); + }, + [](const void* lhs, const void* rhs) { + return *static_cast(lhs) == *static_cast(rhs); + }}; +} + +template +void addHandler(ContainerHandlerRegistry& registry, const std::string& name, ContainerHandler handler) +{ + registry.byName.emplace(normalizeContainerTypeName(name), handler); + registry.byType.emplace(std::type_index(typeid(ContainerT)), std::move(handler)); +} + +template +void addSequenceHandlers(ContainerHandlerRegistry& registry) +{ + const std::string tname = TypeName::value; + addHandler>(registry, "vector<" + tname + ">", makeSequenceHandler>()); + addHandler>(registry, "list<" + tname + ">", makeSequenceHandler>()); + addHandler>(registry, "deque<" + tname + ">", makeSequenceHandler>()); + addHandler>(registry, "set<" + tname + ">", makeSequenceHandler>()); + addHandler>(registry, "unordered_set<" + tname + ">", makeSequenceHandler>()); +} + +template +void addMapHandlers(ContainerHandlerRegistry& registry) +{ + const std::string kname = TypeName::value; + const std::string vname = TypeName::value; + addHandler>(registry, "map<" + kname + "," + vname + ">", makeMapHandler>()); + addHandler>(registry, "unordered_map<" + kname + "," + vname + ">", makeMapHandler>()); +} + +template +void addMapHandlersForKey(ContainerHandlerRegistry& registry) +{ + addMapHandlers(registry); + addMapHandlers(registry); + addMapHandlers(registry); + addMapHandlers(registry); + addMapHandlers(registry); + addMapHandlers(registry); + addMapHandlers(registry); + addMapHandlers(registry); + addMapHandlers(registry); + addMapHandlers(registry); + addMapHandlers(registry); + addMapHandlers(registry); + addMapHandlers(registry); + addMapHandlers(registry); + addMapHandlers(registry); +} + +const ContainerHandlerRegistry& containerHandlers() +{ + static const ContainerHandlerRegistry handlers = [] { + ContainerHandlerRegistry result; + + addSequenceHandlers(result); + addSequenceHandlers(result); + addSequenceHandlers(result); + addSequenceHandlers(result); + addSequenceHandlers(result); + addSequenceHandlers(result); + addSequenceHandlers(result); + addSequenceHandlers(result); + addSequenceHandlers(result); + addSequenceHandlers(result); + addSequenceHandlers(result); + addSequenceHandlers(result); + addSequenceHandlers(result); + addSequenceHandlers(result); + addSequenceHandlers(result); + + addMapHandlersForKey(result); + addMapHandlersForKey(result); + addMapHandlersForKey(result); + addMapHandlersForKey(result); + addMapHandlersForKey(result); + addMapHandlersForKey(result); + addMapHandlersForKey(result); + addMapHandlersForKey(result); + addMapHandlersForKey(result); + addMapHandlersForKey(result); + addMapHandlersForKey(result); + addMapHandlersForKey(result); + addMapHandlersForKey(result); + addMapHandlersForKey(result); + addMapHandlersForKey(result); + + return result; + }(); + return handlers; +} + +const ContainerHandler* getContainerHandler(const std::string& typeName) +{ + const auto normalized = normalizeContainerTypeName(typeName); + const auto& handlers = containerHandlers().byName; + auto iter = handlers.find(normalized); + return iter == handlers.end() ? nullptr : &iter->second; +} + +const ContainerHandler* getContainerHandler(const std::type_info& type) +{ + const auto& handlers = containerHandlers().byType; + auto iter = handlers.find(std::type_index(type)); + return iter == handlers.end() ? nullptr : &iter->second; +} + +} // namespace + // ------------------------------------------------------------------ void EnumRegistry::add(const std::string& key, const TDataMember* dm) @@ -192,6 +526,49 @@ int EnumLegalValues::getIntValue(const std::string& value) const // ----------------------------------------------------------------- +bool ConfigurableParam::isRegisteredContainerType(const std::string& typeName) +{ + return getContainerHandler(typeName) != nullptr; +} + +void ConfigurableParam::registerContainerType(const std::string& key, const std::string& typeName) +{ + sKeyToContainerTypeMap[key] = typeName; +} + +std::string ConfigurableParam::getRegisteredContainerType(const std::string& key) +{ + auto iter = sKeyToContainerTypeMap.find(key); + return iter == sKeyToContainerTypeMap.end() ? std::string{} : iter->second; +} + +bool ConfigurableParam::assignRegisteredContainer(const std::string& typeName, void* target, const void* source) +{ + if (const auto* handler = getContainerHandler(typeName)) { + handler->assign(target, source); + return true; + } + return false; +} + +bool ConfigurableParam::areRegisteredContainersEqual(const std::string& typeName, const void* lhs, const void* rhs) +{ + if (const auto* handler = getContainerHandler(typeName)) { + return handler->equal(lhs, rhs); + } + return false; +} + +std::string ConfigurableParam::registeredContainerAsString(const std::string& typeName, const void* source) +{ + if (const auto* handler = getContainerHandler(typeName)) { + return handler->serialize(source); + } + return {}; +} + +// ----------------------------------------------------------------- + void ConfigurableParam::write(std::string const& filename, std::string const& keyOnly) { if (o2::utils::Str::endsWith(filename, ".ini")) { @@ -253,6 +630,13 @@ void ConfigurableParam::setValue(std::string const& key, std::string const& valu }; try { if (sPtree->get_optional(key).is_initialized()) { + auto iter = sKeyToStorageMap->find(key); + if (iter != sKeyToStorageMap->end()) { + if (!getRegisteredContainerType(key).empty() || getContainerHandler(iter->second.first)) { + setContainerValue(key, valuestring); + return; + } + } try { // try first setting value without stripping a literal suffix setValueImpl(valuestring); @@ -266,7 +650,7 @@ void ConfigurableParam::setValue(std::string const& key, std::string const& valu const auto expectedSuffix = getLiteralSuffixFromType(iter->second.first); if (!expectedSuffix.empty()) { auto valuestringLower = valuestring; - std::transform(valuestring.cbegin(), valuestring.cend(), valuestringLower.begin(), tolower); + std::transform(valuestring.cbegin(), valuestring.cend(), valuestringLower.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); if (valuestringLower.ends_with(expectedSuffix)) { std::string strippedValue = valuestringLower.substr(0, valuestringLower.length() - expectedSuffix.length()); setValueImpl(strippedValue); @@ -436,7 +820,7 @@ void ConfigurableParam::printAllRegisteredParamNames() // If nonempty comma-separated paramsList is provided, only those params will // be updated, absence of data for any of requested params will lead to fatal // If unchangedOnly is true, then only those parameters whose provenance is kCODE will be updated -// (to allow prefernce of run-time settings) +// (to allow preference of run-time settings) void ConfigurableParam::updateFromFile(std::string const& configFile, std::string const& paramsList, bool unchangedOnly) { if (!sIsFullyInitialized) { @@ -597,7 +981,15 @@ void ConfigurableParam::setValues(std::vectorfind(key); + if (iter != sKeyToStorageMap->end()) { + if (!getRegisteredContainerType(key).empty() || getContainerHandler(iter->second.first)) { + setContainerValue(key, value); + continue; + } } if (sEnumRegistry->contains(key)) { @@ -614,9 +1006,7 @@ void ConfigurableParam::setValues(std::vectorfind(key); + if (iter == sKeyToStorageMap->end()) { + LOG(error) << "Container parameter " << key << " not found"; + return; + } + void* targetAddress = iter->second.second; + const auto typeName = getRegisteredContainerType(key); + const auto* handler = typeName.empty() ? getContainerHandler(iter->second.first) : getContainerHandler(typeName); + if (!handler) { + LOG(error) << "Unsupported container configuration: " << (typeName.empty() ? iter->second.first.name() : typeName); + return; + } + try { + handler->parseAssign(targetAddress, value); + sPtree->put(key, handler->serialize(targetAddress)); + if (auto prov = sValueProvenanceMap->find(key); prov != sValueProvenanceMap->end()) { + prov->second = kRT; + } + } catch (const std::exception& e) { + LOG(error) << "Failed to parse container " << key << ": " << e.what(); + } +} + void ConfigurableParam::setEnumValue(const std::string& key, const std::string& value) { int val = (*sEnumRegistry)[key]->getIntValue(value); diff --git a/Common/Utils/src/ConfigurableParamHelper.cxx b/Common/Utils/src/ConfigurableParamHelper.cxx index 161735b3a5ce4..e2637ccaca01e 100644 --- a/Common/Utils/src/ConfigurableParamHelper.cxx +++ b/Common/Utils/src/ConfigurableParamHelper.cxx @@ -9,7 +9,7 @@ // granted to it by virtue of its status as an Intergovernmental Organization // or submit itself to any jurisdiction. -//first version 8/2018, Sandro Wenzel +// first version 8/2018, Sandro Wenzel #include "CommonUtils/ConfigurableParamHelper.h" #include "CommonUtils/ConfigurableParam.h" @@ -27,6 +27,7 @@ #include #include #include +#include #ifdef NDEBUG #undef NDEBUG #endif @@ -72,6 +73,17 @@ bool isString(TDataMember const& dm) return strcmp(dm.GetTrueTypeName(), "string") == 0; } +std::string getContainerTypeName(TDataMember const& dm) +{ + if (auto* cl = dm.GetClass(); cl && ConfigurableParam::isRegisteredContainerType(cl->GetName())) { + return cl->GetName(); + } + if (ConfigurableParam::isRegisteredContainerType(dm.GetTrueTypeName())) { + return dm.GetTrueTypeName(); + } + return dm.GetFullTypeName(); +} + // ---------------------------------------------------------------------- // a generic looper of data members of a TClass; calling a callback @@ -96,6 +108,15 @@ void loopOverMembers(TClass* cl, void* obj, LOG(warning) << "Pointer types not supported in ConfigurableParams: " << dm->GetFullTypeName() << " " << dm->GetName(); continue; } + if (isContainer(*dm)) { + TClass* c = dm->GetClass(); + if (!c || !c->HasDictionary()) { + LOG(warning) << "Skipping container parameter '" << dm->GetName() << "' of type " << dm->GetTrueTypeName() << " - ROOT dictionary not found."; + continue; + } + callback(dm, 0, 1); + continue; + } if (!dm->IsBasic() && !isValidComplex()) { LOG(warning) << "Generic complex types not supported in ConfigurableParams: " << dm->GetFullTypeName() << " " << dm->GetName(); continue; @@ -138,7 +159,7 @@ size_t getSizeOfUnderlyingType(const TDataMember& dm) } else { // for now only catch std::string as other supported type auto tname = dm.GetFullTypeName(); - if (strcmp(tname, "string") == 0 || strcmp(tname, "std::string")) { + if (strcmp(tname, "string") == 0 || strcmp(tname, "std::string") == 0) { return sizeof(std::string); } LOG(error) << "ENCOUNTERED AN UNSUPPORTED TYPE " << tname << "IN A CONFIGURABLE PARAMETER"; @@ -189,9 +210,12 @@ std::string asString(TDataMember const& dm, char* pointer) else if (isString(dm)) { return ((std::string*)pointer)->c_str(); } + if (isContainer(dm)) { + return ConfigurableParam::registeredContainerAsString(getContainerTypeName(dm), pointer); + } // potentially other cases to be added here - LOG(error) << "COULD NOT REPRESENT AS STRING"; + LOG(error) << "COULD NOT REPRESENT AS STRING: " << dm.GetFullTypeName(); return std::string(); } @@ -203,7 +227,7 @@ std::vector* _ParamHelper::getDataMembersImpl(std::string const std::vector* members = new std::vector; auto toDataMember = [&members, obj, mainkey, provmap, globaloffset](const TDataMember* dm, int index, int size) { - auto TS = getSizeOfUnderlyingType(*dm); + auto TS = isContainer(*dm) ? 0 : getSizeOfUnderlyingType(*dm); char* pointer = ((char*)obj) + dm->GetOffset() + index * TS + globaloffset; const std::string name = getName(dm, index, size); auto value = asString(*dm, pointer); @@ -279,7 +303,7 @@ std::type_info const& nameToTypeInfo(const char* tname, TDataType const* dt) } } // if we get here none of the above worked - if (strcmp(tname, "string") == 0 || strcmp(tname, "std::string")) { + if (strcmp(tname, "string") == 0 || strcmp(tname, "std::string") == 0) { return typeid(std::string); } LOG(error) << "ENCOUNTERED AN UNSUPPORTED TYPE " << tname << "IN A CONFIGURABLE PARAMETER"; @@ -293,14 +317,37 @@ void _ParamHelper::fillKeyValuesImpl(std::string const& mainkey, TClass* cl, voi EnumRegistry* enumRegistry, size_t globaloffset) { boost::property_tree::ptree localtree; + using mapped_t = std::pair; auto fillMap = [obj, &mainkey, &localtree, &keytostoragemap, &enumRegistry, globaloffset](const TDataMember* dm, int index, int size) { const auto name = getName(dm, index, size); auto dt = dm->GetDataType(); - auto TS = getSizeOfUnderlyingType(*dm); - char* pointer = ((char*)obj) + dm->GetOffset() + index * TS + globaloffset; - localtree.put(name, asString(*dm, pointer)); + auto TS = isContainer(*dm) ? 0 : getSizeOfUnderlyingType(*dm); + char* pointer = ((char*)obj) + dm->GetOffset() + (index * TS) + globaloffset; + const auto key = mainkey + "." + name; + + if (isContainer(*dm)) { + const auto typeName = getContainerTypeName(*dm); + pointer = ((char*)obj) + dm->GetOffset() + globaloffset; + localtree.put(name, ConfigurableParam::registeredContainerAsString(typeName, pointer)); + TClass* containerClass = dm->GetClass(); + if (!containerClass) { + containerClass = TClass::GetClass(dm->GetFullTypeName()); + } + if (!containerClass) { + LOG(error) << "Cannot get TClass for container " << typeName; + return; + } + const std::type_info* tinfo = containerClass->GetTypeInfo(); + ConfigurableParam::registerContainerType(key, typeName); + keytostoragemap->insert(std::pair( + key, mapped_t(tinfo ? *tinfo : typeid(std::string), pointer))); + if (!tinfo) { + LOG(error) << "Cannot get type_info for container " << typeName; + } + return; + } - auto key = mainkey + "." + name; + localtree.put(name, asString(*dm, pointer)); // If it's an enum, we need to store separately all the legal // values so that we can map to them from the command line @@ -308,7 +355,6 @@ void _ParamHelper::fillKeyValuesImpl(std::string const& mainkey, TClass* cl, voi enumRegistry->add(key, dm); } - using mapped_t = std::pair; auto& ti = nameToTypeInfo(dm->GetTrueTypeName(), dt); keytostoragemap->insert(std::pair(key, mapped_t(ti, pointer))); }; @@ -386,8 +432,7 @@ void _ParamHelper::assignmentImpl(std::string const& mainkey, TClass* cl, void* { auto assignifchanged = [to, from, &mainkey, provmap, globaloffset](const TDataMember* dm, int index, int size) { const auto name = getName(dm, index, size); - auto dt = dm->GetDataType(); - auto TS = getSizeOfUnderlyingType(*dm); + auto TS = isContainer(*dm) ? 0 : getSizeOfUnderlyingType(*dm); char* pointerto = ((char*)to) + dm->GetOffset() + index * TS + globaloffset; char* pointerfrom = ((char*)from) + dm->GetOffset() + index * TS + globaloffset; @@ -405,6 +450,15 @@ void _ParamHelper::assignmentImpl(std::string const& mainkey, TClass* cl, void* // TODO: this could dispatch to the same method used in ConfigurableParam::setValue // but will be slower + if (isContainer(*dm)) { + const auto typeName = getContainerTypeName(*dm); + if (!ConfigurableParam::areRegisteredContainersEqual(typeName, pointerto, pointerfrom)) { + updateProv(); + ConfigurableParam::assignRegisteredContainer(typeName, pointerto, pointerfrom); + } + return; + } + // test if a complicated case if (isString(*dm)) { std::string& target = *(std::string*)pointerto; @@ -433,8 +487,7 @@ void _ParamHelper::syncCCDBandRegistry(const std::string& mainkey, TClass* cl, v { auto sync = [to, from, &mainkey, provmap, globaloffset](const TDataMember* dm, int index, int size) { const auto name = getName(dm, index, size); - auto dt = dm->GetDataType(); - auto TS = getSizeOfUnderlyingType(*dm); + auto TS = isContainer(*dm) ? 0 : getSizeOfUnderlyingType(*dm); char* pointerto = ((char*)to) + dm->GetOffset() + index * TS + globaloffset; char* pointerfrom = ((char*)from) + dm->GetOffset() + index * TS + globaloffset; @@ -450,6 +503,12 @@ void _ParamHelper::syncCCDBandRegistry(const std::string& mainkey, TClass* cl, v proviter->second = ConfigurableParam::EParamProvenance::kCCDB; }; + if (isContainer(*dm)) { + updateProv(); + ConfigurableParam::assignRegisteredContainer(getContainerTypeName(*dm), pointerto, pointerfrom); + return; + } + // test if a complicated case if (isString(*dm)) { std::string& target = *(std::string*)pointerto; diff --git a/Common/Utils/test/testConfigurableParam.cxx b/Common/Utils/test/testConfigurableParam.cxx index 3ef177aaca3fe..a185660fc8829 100644 --- a/Common/Utils/test/testConfigurableParam.cxx +++ b/Common/Utils/test/testConfigurableParam.cxx @@ -9,6 +9,8 @@ // granted to it by virtue of its status as an Intergovernmental Organization // or submit itself to any jurisdiction. +#include +#include #define BOOST_TEST_MODULE Test ConfigurableParams #define BOOST_TEST_MAIN #define BOOST_TEST_DYN_LINK @@ -48,7 +50,6 @@ BOOST_AUTO_TEST_CASE(ConfigurableParam_SG_Fundamental) ConfigurableParam::setValue("TestParam.eValue", "0"); auto& param = TestParam::Instance(); - param.printKeyValues(); BOOST_CHECK_EQUAL(param.iValue, 100); BOOST_CHECK_EQUAL(param.dValue, 2.718); BOOST_CHECK_EQUAL(param.bValue, false); @@ -68,6 +69,34 @@ BOOST_AUTO_TEST_CASE(ConfigurableParam_SG_CArray) BOOST_CHECK_EQUAL(ConfigurableParam::getValueAs("TestParam.caValue[1]"), 99); } +BOOST_AUTO_TEST_CASE(ConfigurableParam_SG_STD) +{ + // tests setting and getting for a std type + ConfigurableParam::setValue("TestParam.vec", "[10,20,30,40]"); + auto& param = TestParam::Instance(); + BOOST_CHECK_EQUAL(param.vec.size(), 4); + BOOST_CHECK_EQUAL(param.vec[0], 10); + BOOST_CHECK_EQUAL(param.vec[1], 20); + BOOST_CHECK_EQUAL(param.vec[2], 30); + BOOST_CHECK_EQUAL(param.vec[3], 40); + BOOST_CHECK_EQUAL(ConfigurableParam::getValueAs("TestParam.vec"), "[10,20,30,40]"); + + ConfigurableParam::setValue("TestParam.u8vec", "[1,2,255]"); + BOOST_CHECK_EQUAL(param.u8vec.size(), 3); + BOOST_CHECK_EQUAL(static_cast(param.u8vec[0]), 1); + BOOST_CHECK_EQUAL(static_cast(param.u8vec[1]), 2); + BOOST_CHECK_EQUAL(static_cast(param.u8vec[2]), 255); + BOOST_CHECK_EQUAL(ConfigurableParam::getValueAs("TestParam.u8vec"), "[1,2,255]"); + + ConfigurableParam::setValues({{"TestParam.map", "{0:1,10:42}"}}); + BOOST_CHECK_EQUAL(param.map.size(), 2); + BOOST_CHECK(param.map.contains(0)); + BOOST_CHECK_EQUAL(param.map.at(0), 1); + BOOST_CHECK(param.map.contains(10)); + BOOST_CHECK_EQUAL(param.map.at(10), 42); + BOOST_CHECK_THROW(param.map.at(33), std::out_of_range); +} + BOOST_AUTO_TEST_CASE(ConfigurableParam_Provenance) { // tests correct setting of provenance @@ -82,12 +111,16 @@ BOOST_AUTO_TEST_CASE(ConfigurableParam_FileIO_Ini) const std::string testFileName = "test_config.ini"; auto iValueBefore = TestParam::Instance().iValue; auto sValueBefore = TestParam::Instance().sValue; + ConfigurableParam::setValue("TestParam.vec", "[7,8]"); + const std::vector vecBefore = TestParam::Instance().vec; ConfigurableParam::writeINI(testFileName); ConfigurableParam::setValue("TestParam.iValue", "999"); ConfigurableParam::setValue("TestParam.sValue", testFileName); + ConfigurableParam::setValue("TestParam.vec", "[1]"); ConfigurableParam::updateFromFile(testFileName); BOOST_CHECK_EQUAL(TestParam::Instance().iValue, iValueBefore); BOOST_CHECK_EQUAL(TestParam::Instance().sValue, sValueBefore); + BOOST_CHECK_EQUAL_COLLECTIONS(TestParam::Instance().vec.begin(), TestParam::Instance().vec.end(), vecBefore.begin(), vecBefore.end()); std::remove(testFileName.c_str()); } @@ -97,12 +130,18 @@ BOOST_AUTO_TEST_CASE(ConfigurableParam_FileIO_Json) const std::string testFileName = "test_config.json"; auto iValueBefore = TestParam::Instance().iValue; auto sValueBefore = TestParam::Instance().sValue; + ConfigurableParam::setValues({{"TestParam.map", "{3:4,5:6}"}}); + const std::map mapBefore = TestParam::Instance().map; ConfigurableParam::writeJSON(testFileName); ConfigurableParam::setValue("TestParam.iValue", "999"); ConfigurableParam::setValue("TestParam.sValue", testFileName); + ConfigurableParam::setValues({{"TestParam.map", "{1:2}"}}); ConfigurableParam::updateFromFile(testFileName); BOOST_CHECK_EQUAL(TestParam::Instance().iValue, iValueBefore); BOOST_CHECK_EQUAL(TestParam::Instance().sValue, sValueBefore); + BOOST_CHECK_EQUAL(TestParam::Instance().map.size(), mapBefore.size()); + BOOST_CHECK_EQUAL(TestParam::Instance().map.at(3), mapBefore.at(3)); + BOOST_CHECK_EQUAL(TestParam::Instance().map.at(5), mapBefore.at(5)); std::remove(testFileName.c_str()); } @@ -143,3 +182,56 @@ BOOST_AUTO_TEST_CASE(ConfigurableParam_LiteralSuffix) ConfigurableParam::setValue("TestParam.ullValue", "888u"); BOOST_CHECK_NE(TestParam::Instance().ullValue, 888); } + +BOOST_AUTO_TEST_CASE(ConfigurableParam_ContainerParserVector) +{ + auto v = ContainerParser::parse>("[1,2,3,4,5]"); + BOOST_CHECK_EQUAL(v.size(), 5); + BOOST_CHECK_EQUAL(v[0], 1); + BOOST_CHECK_EQUAL(v[4], 5); +} + +BOOST_AUTO_TEST_CASE(ConfigurableParam_ContainerParserMap) +{ + auto m = ContainerParser::parse>("{alpha:0.5,beta:0.3,gamma:0.2}"); + BOOST_CHECK_EQUAL(m.size(), 3); + BOOST_CHECK_EQUAL(m["alpha"], 0.5); + BOOST_CHECK_EQUAL(m["beta"], 0.3); +} + +BOOST_AUTO_TEST_CASE(ConfigurableParam_Container_FileIO_ROOT) +{ + // test for root file serialization + const std::string testFileName = "test_config.root"; + TFile* testFile = TFile::Open(testFileName.c_str(), "RECREATE"); + ConfigurableParam::setValue("TestParam.vec", "[1,2,3]"); + ConfigurableParam::setValue("TestParam.u8vec", "[4,5,6]"); + ConfigurableParam::setValue("TestParam.map", "{1:16,2:9,3:23456}"); + ConfigurableParam::setValue("TestParam.smap", "{a:16,b:9,c:23456}"); + ConfigurableParam::setValue("TestParam.set", "[2,3,2,3,2,5]"); + TestParam::Instance().serializeTo(testFile); + testFile->Close(); + ConfigurableParam::setValue("TestParam.vec", "[9]"); + ConfigurableParam::fromCCDB(testFileName); + const auto& tp = TestParam::Instance(); + const std::vector v = {1, 2, 3}; + BOOST_CHECK_EQUAL_COLLECTIONS(tp.vec.begin(), tp.vec.end(), v.begin(), v.end()); + const std::vector v8 = {4, 5, 6}; + BOOST_CHECK_EQUAL_COLLECTIONS(tp.u8vec.begin(), tp.u8vec.end(), v8.begin(), v8.end()); + std::map map{{1, 16}, {2, 9}, {3, 23456}}; + auto testMapEqual = [](const auto& m1, const auto& m2) { + for (const auto& [k, v] : m1) { + BOOST_CHECK(m2.find(k) != m2.end()); + BOOST_CHECK_EQUAL(v, m2.at(k)); + } + }; + testMapEqual(map, tp.map); + std::map smap{{"a", 16}, {"b", 9}, {"c", 23456}}; + testMapEqual(smap, tp.smap); + std::set set{2, 3, 5}; + BOOST_CHECK_EQUAL(set.size(), tp.set.size()); + for (const auto& s : set) { + BOOST_CHECK(tp.set.contains(s)); + } + // std::remove(testFileName.c_str()); +} From 95cb6b65db438f20866b6984cbb438f7984a8314 Mon Sep 17 00:00:00 2001 From: Felix Schlepper Date: Thu, 11 Jun 2026 17:47:57 +0200 Subject: [PATCH 2/3] Common: offer suggested on correct spelling Signed-off-by: Felix Schlepper --- .../CommonUtils/ConfigurableParamHelper.h | 38 ++++++++++++ Common/Utils/src/ConfigurableParam.cxx | 59 ++++++++++++++++++- Common/Utils/test/testConfigurableParam.cxx | 22 +++++++ 3 files changed, 117 insertions(+), 2 deletions(-) diff --git a/Common/Utils/include/CommonUtils/ConfigurableParamHelper.h b/Common/Utils/include/CommonUtils/ConfigurableParamHelper.h index dd28366d0a237..d9180dbb5ea0b 100644 --- a/Common/Utils/include/CommonUtils/ConfigurableParamHelper.h +++ b/Common/Utils/include/CommonUtils/ConfigurableParamHelper.h @@ -16,18 +16,56 @@ #include "CommonUtils/ConfigurableParam.h" +#include #include +#include +#include #include #include #include #include #include +#include namespace o2::conf { // ---------------------------------------------------------------- +inline std::size_t damerauLevenshteinDistance(std::string_view a, std::string_view b) +{ + const std::size_t n = a.size(); + const std::size_t m = b.size(); + if (n == 0) { + return m; + } + if (m == 0) { + return n; + } + std::vector prev(m + 1), curr(m + 1), prev2(m + 1); + std::iota(prev.begin(), prev.end(), 0); + for (std::size_t i = 1; i <= n; ++i) { + curr[0] = i; + for (std::size_t j = 1; j <= m; ++j) { + std::size_t cost = (a[i - 1] == b[j - 1]) ? 0 : 1; + curr[j] = std::min({ + prev[j] + 1, + curr[j - 1] + 1, + prev[j - 1] + cost}); + if (i > 1 && j > 1 && a[i - 1] == b[j - 2] && + a[i - 2] == b[j - 1]) { + curr[j] = std::min(curr[j], prev2[j - 2] + 1); + } + } + prev2 = std::move(prev); + prev = std::move(curr); + curr.assign(m + 1, 0); + } + return prev[m]; +} + +// ---------------------------------------------------------------- + // Utility structure for passing around ConfigurableParam data member info // (where value is the string representation) struct ParamDataMember { diff --git a/Common/Utils/src/ConfigurableParam.cxx b/Common/Utils/src/ConfigurableParam.cxx index 35cc2aead1c1d..202433b04e4c4 100644 --- a/Common/Utils/src/ConfigurableParam.cxx +++ b/Common/Utils/src/ConfigurableParam.cxx @@ -13,6 +13,7 @@ #include "CommonUtils/ConfigurableParam.h" #include +#include "CommonUtils/ConfigurableParamHelper.h" #include "CommonUtils/StringUtils.h" #include "CommonUtils/KeyValParam.h" #include "CommonUtils/ConfigurableParamReaders.h" @@ -433,6 +434,60 @@ const ContainerHandler* getContainerHandler(const std::type_info& type) return iter == handlers.end() ? nullptr : &iter->second; } +std::pair splitConfigurableParamKey(std::string_view key) +{ + const auto separator = key.find('.'); + if (separator == std::string_view::npos) { + return {key, {}}; + } + return {key.substr(0, separator), key.substr(separator + 1)}; +} + +std::string findClosestConfigurableParamKey(const std::string& requestedKey, + const std::map>& storageMap) +{ + if (storageMap.empty()) { + return {}; + } + + const auto [requestedMainKey, requestedSubKey] = splitConfigurableParamKey(requestedKey); + bool mainKeyExists = false; + for (const auto& entry : storageMap) { + const auto mainKey = splitConfigurableParamKey(entry.first).first; + if (mainKey == requestedMainKey) { + mainKeyExists = true; + break; + } + } + + std::string closest; + std::size_t closestDistance = std::numeric_limits::max(); + for (const auto& entry : storageMap) { + const auto& key = entry.first; + const auto [mainKey, subKey] = splitConfigurableParamKey(key); + if (mainKeyExists && mainKey != requestedMainKey) { + continue; + } + const auto distance = mainKeyExists ? damerauLevenshteinDistance(requestedSubKey, subKey) : damerauLevenshteinDistance(requestedKey, key); + if (distance < closestDistance || (distance == closestDistance && (closest.empty() || key < closest))) { + closest = key; + closestDistance = distance; + } + } + return closest; +} + +std::string formatUnknownConfigurableParamKeyMessage(const std::string& prefix, const std::string& key, + const std::map>& storageMap) +{ + std::string message = prefix + key; + auto closest = findClosestConfigurableParamKey(key, storageMap); + if (!closest.empty()) { + message += ". Did you mean '" + closest + "'?"; + } + return message; +} + } // namespace // ------------------------------------------------------------------ @@ -978,10 +1033,10 @@ void ConfigurableParam::setValues(std::vectorfind(key); diff --git a/Common/Utils/test/testConfigurableParam.cxx b/Common/Utils/test/testConfigurableParam.cxx index a185660fc8829..ed9ad19f1c35e 100644 --- a/Common/Utils/test/testConfigurableParam.cxx +++ b/Common/Utils/test/testConfigurableParam.cxx @@ -17,6 +17,7 @@ #include #include +#include #include #include "CommonUtils/ConfigurableParamTest.h" @@ -199,6 +200,27 @@ BOOST_AUTO_TEST_CASE(ConfigurableParam_ContainerParserMap) BOOST_CHECK_EQUAL(m["beta"], 0.3); } +BOOST_AUTO_TEST_CASE(ConfigurableParam_DamerauLevenshteinDistance) +{ + BOOST_CHECK_EQUAL(damerauLevenshteinDistance("TestParam.iValue", "TestParam.iValue"), 0); + BOOST_CHECK_EQUAL(damerauLevenshteinDistance("TestParam.iValu", "TestParam.iValue"), 1); + BOOST_CHECK_EQUAL(damerauLevenshteinDistance("TestParam.jValue", "TestParam.iValue"), 1); + BOOST_CHECK_EQUAL(damerauLevenshteinDistance("TestParam.iVaule", "TestParam.iValue"), 1); +} + +BOOST_AUTO_TEST_CASE(ConfigurableParam_UnknownKeySuggestionsNonFatal) +{ + setenv("ALICEO2_CONFIGURABLEPARAM_WRONGKEYISNONFATAL", "1", 1); + ConfigurableParam::setValue("TestParam.iValue", "222"); + ConfigurableParam::setValue("TestParam.caValue[1]", "33"); + ConfigurableParam::setValues({{"TestParam.iValu", "777"}, + {"TstParam.iValue", "888"}, + {"TestParam.caValue[", "999"}}); + BOOST_CHECK_EQUAL(TestParam::Instance().iValue, 222); + BOOST_CHECK_EQUAL(TestParam::Instance().caValue[1], 33); + unsetenv("ALICEO2_CONFIGURABLEPARAM_WRONGKEYISNONFATAL"); +} + BOOST_AUTO_TEST_CASE(ConfigurableParam_Container_FileIO_ROOT) { // test for root file serialization From 98f3fc0a647238077a70c88ba9069ee53d6fdd7e Mon Sep 17 00:00:00 2001 From: ALICE Action Bot Date: Mon, 15 Jun 2026 07:15:02 +0000 Subject: [PATCH 3/3] Please consider the following formatting changes --- Common/Utils/include/CommonUtils/ConfigurableParamHelper.h | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Common/Utils/include/CommonUtils/ConfigurableParamHelper.h b/Common/Utils/include/CommonUtils/ConfigurableParamHelper.h index d9180dbb5ea0b..f52c2079c6005 100644 --- a/Common/Utils/include/CommonUtils/ConfigurableParamHelper.h +++ b/Common/Utils/include/CommonUtils/ConfigurableParamHelper.h @@ -48,10 +48,9 @@ inline std::size_t damerauLevenshteinDistance(std::string_view a, std::string_vi curr[0] = i; for (std::size_t j = 1; j <= m; ++j) { std::size_t cost = (a[i - 1] == b[j - 1]) ? 0 : 1; - curr[j] = std::min({ - prev[j] + 1, - curr[j - 1] + 1, - prev[j - 1] + cost}); + curr[j] = std::min({prev[j] + 1, + curr[j - 1] + 1, + prev[j - 1] + cost}); if (i > 1 && j > 1 && a[i - 1] == b[j - 2] && a[i - 2] == b[j - 1]) { curr[j] = std::min(curr[j], prev2[j - 2] + 1);