diff --git a/modules/core/include/opencv2/core/utils/logger.defines.hpp b/modules/core/include/opencv2/core/utils/logger.defines.hpp index b2dfc41704..7d73f02b66 100644 --- a/modules/core/include/opencv2/core/utils/logger.defines.hpp +++ b/modules/core/include/opencv2/core/utils/logger.defines.hpp @@ -17,6 +17,26 @@ #define CV_LOG_LEVEL_DEBUG 5 //!< Debug message. Disabled in the "Release" build. #define CV_LOG_LEVEL_VERBOSE 6 //!< Verbose (trace) messages. Requires verbosity level. Disabled in the "Release" build. +namespace cv { +namespace utils { +namespace logging { + +//! Supported logging levels and their semantic +enum LogLevel { + LOG_LEVEL_SILENT = 0, //!< for using in setLogVevel() call + LOG_LEVEL_FATAL = 1, //!< Fatal (critical) error (unrecoverable internal error) + LOG_LEVEL_ERROR = 2, //!< Error message + LOG_LEVEL_WARNING = 3, //!< Warning message + LOG_LEVEL_INFO = 4, //!< Info message + LOG_LEVEL_DEBUG = 5, //!< Debug message. Disabled in the "Release" build. + LOG_LEVEL_VERBOSE = 6, //!< Verbose (trace) messages. Requires verbosity level. Disabled in the "Release" build. +#ifndef CV_DOXYGEN + ENUM_LOG_LEVEL_FORCE_INT = INT_MAX +#endif +}; + +}}} // namespace + //! @} #endif // OPENCV_LOGGER_DEFINES_HPP diff --git a/modules/core/include/opencv2/core/utils/logger.hpp b/modules/core/include/opencv2/core/utils/logger.hpp index 47094f994d..b00a552bfb 100644 --- a/modules/core/include/opencv2/core/utils/logger.hpp +++ b/modules/core/include/opencv2/core/utils/logger.hpp @@ -10,6 +10,7 @@ #include // INT_MAX #include "logger.defines.hpp" +#include "logtag.hpp" //! @addtogroup core_logging // This section describes OpenCV logging utilities. @@ -20,20 +21,6 @@ namespace cv { namespace utils { namespace logging { -//! Supported logging levels and their semantic -enum LogLevel { - LOG_LEVEL_SILENT = 0, //!< for using in setLogVevel() call - LOG_LEVEL_FATAL = 1, //!< Fatal (critical) error (unrecoverable internal error) - LOG_LEVEL_ERROR = 2, //!< Error message - LOG_LEVEL_WARNING = 3, //!< Warning message - LOG_LEVEL_INFO = 4, //!< Info message - LOG_LEVEL_DEBUG = 5, //!< Debug message. Disabled in the "Release" build. - LOG_LEVEL_VERBOSE = 6, //!< Verbose (trace) messages. Requires verbosity level. Disabled in the "Release" build. -#ifndef CV_DOXYGEN - ENUM_LOG_LEVEL_FORCE_INT = INT_MAX -#endif -}; - /** Set global logging level @return previous logging level */ @@ -41,15 +28,39 @@ CV_EXPORTS LogLevel setLogLevel(LogLevel logLevel); /** Get global logging level */ CV_EXPORTS LogLevel getLogLevel(); +CV_EXPORTS void registerLogTag(cv::utils::logging::LogTag* plogtag); + +CV_EXPORTS void setLogTagLevel(const char* tag, cv::utils::logging::LogLevel level); + +CV_EXPORTS cv::utils::logging::LogLevel getLogTagLevel(const char* tag); + namespace internal { + +/** Get global log tag */ +CV_EXPORTS cv::utils::logging::LogTag* getGlobalLogTag(); + /** Write log message */ CV_EXPORTS void writeLogMessage(LogLevel logLevel, const char* message); + +/** Write log message */ +CV_EXPORTS void writeLogMessageEx(LogLevel logLevel, const char* tag, const char* file, int line, const char* func, const char* message); + } // namespace +struct LogTagAuto + : public LogTag +{ + inline LogTagAuto(const char* _name, LogLevel _level) + : LogTag(_name, _level) + { + registerLogTag(this); + } +}; + /** * \def CV_LOG_STRIP_LEVEL * - * Define CV_LOG_STRIP_LEVEL=CV_LOG_LEVEL_[DEBUG|INFO|WARN|ERROR|FATAL|DISABLED] to compile out anything at that and before that logging level + * Define CV_LOG_STRIP_LEVEL=CV_LOG_LEVEL_[DEBUG|INFO|WARN|ERROR|FATAL|SILENT] to compile out anything at that and before that logging level */ #ifndef CV_LOG_STRIP_LEVEL # if defined NDEBUG @@ -59,26 +70,83 @@ CV_EXPORTS void writeLogMessage(LogLevel logLevel, const char* message); # endif #endif +#define CV_LOGTAG_PTR_CAST(expr) static_cast(expr) + +// CV_LOGTAG_EXPAND_NAME is intended to be re-defined (undef and then define again) +// to allows logging users to use a shorter name argument when calling +// CV_LOG_WITH_TAG or its related macros such as CV_LOG_INFO. +// +// This macro is intended to modify the tag argument as a string (token), via +// preprocessor token pasting or metaprogramming techniques. A typical usage +// is to apply a prefix, such as +// ...... #define CV_LOGTAG_EXPAND_NAME(tag) cv_logtag_##tag +// +// It is permitted to re-define to a hard-coded expression, ignoring the tag. +// This would work identically like the CV_LOGTAG_FALLBACK macro. +// +// Important: When the logging macro is called with tag being NULL, a user-defined +// CV_LOGTAG_EXPAND_NAME may expand it into cv_logtag_0, cv_logtag_NULL, or +// cv_logtag_nullptr. Use with care. Also be mindful of C++ symbol redefinitions. +// +// If there is significant amount of logging code with tag being NULL, it is +// recommended to use (re-define) CV_LOGTAG_FALLBACK to inject locally a default +// tag at the beginning of a compilation unit, to minimize lines of code changes. +// +#define CV_LOGTAG_EXPAND_NAME(tag) tag + +// CV_LOGTAG_FALLBACK is intended to be re-defined (undef and then define again) +// by any other compilation units to provide a log tag when the logging statement +// does not specify one. The macro needs to expand into a C++ expression that can +// be static_cast into (cv::utils::logging::LogTag*). Null (nullptr) is permitted. +#define CV_LOGTAG_FALLBACK nullptr + +// CV_LOGTAG_GLOBAL is the tag used when a log tag is not specified in the logging +// statement nor the compilation unit. The macro needs to expand into a C++ +// expression that can be static_cast into (cv::utils::logging::LogTag*). Must be +// non-null. Do not re-define. +#define CV_LOGTAG_GLOBAL cv::utils::logging::internal::getGlobalLogTag() + +#define CV_LOG_WITH_TAG(tag, msgLevel, ...) \ + for(;;) { \ + const auto cv_temp_msglevel = (cv::utils::logging::LogLevel)(msgLevel); \ + if (cv_temp_msglevel >= (CV_LOG_STRIP_LEVEL)) break; \ + auto cv_temp_logtagptr = CV_LOGTAG_PTR_CAST(CV_LOGTAG_EXPAND_NAME(tag)); \ + if (!cv_temp_logtagptr) cv_temp_logtagptr = CV_LOGTAG_PTR_CAST(CV_LOGTAG_FALLBACK); \ + if (!cv_temp_logtagptr) cv_temp_logtagptr = CV_LOGTAG_PTR_CAST(CV_LOGTAG_GLOBAL); \ + if (cv_temp_logtagptr && (cv_temp_msglevel > cv_temp_logtagptr->level)) break; \ + std::stringstream cv_temp_logstream; \ + cv_temp_logstream << __VA_ARGS__; \ + cv::utils::logging::internal::writeLogMessageEx( \ + cv_temp_msglevel, \ + (cv_temp_logtagptr ? cv_temp_logtagptr->name : nullptr), \ + __FILE__, \ + __LINE__, \ + CV_Func, \ + cv_temp_logstream.str().c_str()); \ + break; \ + } + +#define CV_LOG_FATAL(tag, ...) CV_LOG_WITH_TAG(tag, cv::utils::logging::LOG_LEVEL_FATAL, __VA_ARGS__) +#define CV_LOG_ERROR(tag, ...) CV_LOG_WITH_TAG(tag, cv::utils::logging::LOG_LEVEL_ERROR, __VA_ARGS__) +#define CV_LOG_WARNING(tag, ...) CV_LOG_WITH_TAG(tag, cv::utils::logging::LOG_LEVEL_WARNING, __VA_ARGS__) +#define CV_LOG_INFO(tag, ...) CV_LOG_WITH_TAG(tag, cv::utils::logging::LOG_LEVEL_INFO, __VA_ARGS__) +#define CV_LOG_DEBUG(tag, ...) CV_LOG_WITH_TAG(tag, cv::utils::logging::LOG_LEVEL_DEBUG, __VA_ARGS__) +#define CV_LOG_VERBOSE(tag, v, ...) CV_LOG_WITH_TAG(tag, (cv::utils::logging::LOG_LEVEL_VERBOSE + (int)(v)), __VA_ARGS__) -#define CV_LOG_FATAL(tag, ...) for(;;) { if (cv::utils::logging::getLogLevel() < cv::utils::logging::LOG_LEVEL_FATAL) break; std::stringstream ss; ss << __VA_ARGS__; cv::utils::logging::internal::writeLogMessage(cv::utils::logging::LOG_LEVEL_FATAL, ss.str().c_str()); break; } -#define CV_LOG_ERROR(tag, ...) for(;;) { if (cv::utils::logging::getLogLevel() < cv::utils::logging::LOG_LEVEL_ERROR) break; std::stringstream ss; ss << __VA_ARGS__; cv::utils::logging::internal::writeLogMessage(cv::utils::logging::LOG_LEVEL_ERROR, ss.str().c_str()); break; } -#define CV_LOG_WARNING(tag, ...) for(;;) { if (cv::utils::logging::getLogLevel() < cv::utils::logging::LOG_LEVEL_WARNING) break; std::stringstream ss; ss << __VA_ARGS__; cv::utils::logging::internal::writeLogMessage(cv::utils::logging::LOG_LEVEL_WARNING, ss.str().c_str()); break; } #if CV_LOG_STRIP_LEVEL <= CV_LOG_LEVEL_INFO -#define CV_LOG_INFO(tag, ...) -#else -#define CV_LOG_INFO(tag, ...) for(;;) { if (cv::utils::logging::getLogLevel() < cv::utils::logging::LOG_LEVEL_INFO) break; std::stringstream ss; ss << __VA_ARGS__; cv::utils::logging::internal::writeLogMessage(cv::utils::logging::LOG_LEVEL_INFO, ss.str().c_str()); break; } -#endif -#if CV_LOG_STRIP_LEVEL <= CV_LOG_LEVEL_DEBUG -#define CV_LOG_DEBUG(tag, ...) -#else -#define CV_LOG_DEBUG(tag, ...) for(;;) { if (cv::utils::logging::getLogLevel() < cv::utils::logging::LOG_LEVEL_DEBUG) break; std::stringstream ss; ss << __VA_ARGS__; cv::utils::logging::internal::writeLogMessage(cv::utils::logging::LOG_LEVEL_DEBUG, ss.str().c_str()); break; } -#endif -#if CV_LOG_STRIP_LEVEL <= CV_LOG_LEVEL_VERBOSE -#define CV_LOG_VERBOSE(tag, v, ...) -#else -#define CV_LOG_VERBOSE(tag, v, ...) for(;;) { if (cv::utils::logging::getLogLevel() < cv::utils::logging::LOG_LEVEL_VERBOSE) break; std::stringstream ss; ss << "[VERB" << v << ":" << cv::utils::getThreadID() << "] " << __VA_ARGS__; cv::utils::logging::internal::writeLogMessage(cv::utils::logging::LOG_LEVEL_VERBOSE, ss.str().c_str()); break; } +# undef CV_LOG_INFO +# define CV_LOG_INFO(tag, ...) #endif +#if CV_LOG_STRIP_LEVEL <= CV_LOG_LEVEL_DEBUG +# undef CV_LOG_DEBUG +# define CV_LOG_DEBUG(tag, ...) +#endif + +#if CV_LOG_STRIP_LEVEL <= CV_LOG_LEVEL_VERBOSE +# undef CV_LOG_VERBOSE +# define CV_LOG_VERBOSE(tag, v, ...) +#endif }}} // namespace diff --git a/modules/core/include/opencv2/core/utils/logtag.hpp b/modules/core/include/opencv2/core/utils/logtag.hpp new file mode 100644 index 0000000000..4f95266256 --- /dev/null +++ b/modules/core/include/opencv2/core/utils/logtag.hpp @@ -0,0 +1,28 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. + +#ifndef OPENCV_CORE_LOGTAG_HPP +#define OPENCV_CORE_LOGTAG_HPP + +#include "opencv2/core/cvstd.hpp" +#include "logger.defines.hpp" + +namespace cv { +namespace utils { +namespace logging { + +struct LogTag +{ + const char* name; + LogLevel level; + + inline constexpr LogTag(const char* _name, LogLevel _level) + : name(_name) + , level(_level) + {} +}; + +}}} + +#endif diff --git a/modules/core/src/logger.cpp b/modules/core/src/logger.cpp index e7c6669242..fe85e1bb7a 100644 --- a/modules/core/src/logger.cpp +++ b/modules/core/src/logger.cpp @@ -6,6 +6,8 @@ #include #include +#include "utils/logtagmanager.hpp" +#include "utils/logtagconfigparser.hpp" #include #include @@ -19,52 +21,156 @@ namespace cv { namespace utils { namespace logging { -static LogLevel parseLogLevelConfiguration() +namespace internal { - static cv::String param_log_level = utils::getConfigurationParameterString("OPENCV_LOG_LEVEL", + +// Combining several things that require static dynamic initialization in a +// well-defined order into a struct. +// +struct GlobalLoggingInitStruct +{ +public: #if defined NDEBUG - "WARNING" + static constexpr bool m_isDebugBuild = false; #else - "INFO" + static constexpr bool m_isDebugBuild = true; #endif - ); - if (param_log_level == "DISABLED" || param_log_level == "disabled" || - param_log_level == "0" || param_log_level == "OFF" || param_log_level == "off") - return LOG_LEVEL_SILENT; - if (param_log_level == "FATAL" || param_log_level == "fatal") - return LOG_LEVEL_FATAL; - if (param_log_level == "ERROR" || param_log_level == "error") - return LOG_LEVEL_ERROR; - if (param_log_level == "WARNING" || param_log_level == "warning" || - param_log_level == "WARNINGS" || param_log_level == "warnings" || - param_log_level == "WARN" || param_log_level == "warn") - return LOG_LEVEL_WARNING; - if (param_log_level == "INFO" || param_log_level == "info") - return LOG_LEVEL_INFO; - if (param_log_level == "DEBUG" || param_log_level == "debug") - return LOG_LEVEL_DEBUG; - if (param_log_level == "VERBOSE" || param_log_level == "verbose") - return LOG_LEVEL_VERBOSE; - std::cerr << "ERROR: Unexpected logging level value: " << param_log_level << std::endl; - return LOG_LEVEL_INFO; + +public: + static constexpr LogLevel m_defaultUnconfiguredGlobalLevel = + m_isDebugBuild ? LOG_LEVEL_DEBUG : LOG_LEVEL_WARNING; + +public: + LogTagManager logTagManager; + + GlobalLoggingInitStruct() + : logTagManager(m_defaultUnconfiguredGlobalLevel) + { + applyConfigString(); + handleMalformed(); + } + +private: + void applyConfigString() + { + logTagManager.setConfigString(utils::getConfigurationParameterString("OPENCV_LOG_LEVEL", "")); + } + + void handleMalformed() + { + // need to print warning for malformed log tag config strings? + if (m_isDebugBuild) + { + const auto& parser = logTagManager.getConfigParser(); + if (parser.hasMalformed()) + { + const auto& malformedList = parser.getMalformed(); + for (const auto& malformed : malformedList) + { + std::cout << "Malformed log level config: \"" << malformed << "\"\n"; + } + std::cout.flush(); + } + } + } +}; + +// Static dynamic initialization guard function for the combined struct +// just defined above +// +// An initialization guard function guarantees that outside code cannot +// accidentally see not-yet-dynamically-initialized data, by routing +// all outside access request to this function, so that this function +// has a chance to run the initialization code if necessary. +// +// An initialization guard function only guarantees initialization upon +// the first call to this function. +// +static GlobalLoggingInitStruct& getGlobalLoggingInitStruct() +{ + static GlobalLoggingInitStruct globalLoggingInitInstance; + return globalLoggingInitInstance; +} + +// To ensure that the combined struct defined above is initialized even +// if the initialization guard function wasn't called, a dummy static +// instance of a struct is defined below, which will call the +// initialization guard function. +// +struct GlobalLoggingInitCall +{ + GlobalLoggingInitCall() + { + getGlobalLoggingInitStruct(); + } +}; + +static GlobalLoggingInitCall globalLoggingInitCall; + +static LogTagManager& getLogTagManager() +{ + static LogTagManager& logTagManagerInstance = getGlobalLoggingInitStruct().logTagManager; + return logTagManagerInstance; } static LogLevel& getLogLevelVariable() { - static LogLevel g_logLevel = parseLogLevelConfiguration(); - return g_logLevel; + static LogLevel& refGlobalLogLevel = getGlobalLogTag()->level; + return refGlobalLogLevel; +} + +LogTag* getGlobalLogTag() +{ + static LogTag* globalLogTagPtr = getGlobalLoggingInitStruct().logTagManager.get("global"); + return globalLogTagPtr; +} + +} // namespace + +void registerLogTag(LogTag* plogtag) +{ + if (!plogtag || !plogtag->name) + { + return; + } + internal::getLogTagManager().assign(plogtag->name, plogtag); +} + +void setLogTagLevel(const char* tag, LogLevel level) +{ + if (!tag) + { + return; + } + internal::getLogTagManager().setLevelByFullName(std::string(tag), level); +} + +LogLevel getLogTagLevel(const char* tag) +{ + if (!tag) + { + return getLogLevel(); + } + const LogTag* ptr = internal::getLogTagManager().get(std::string(tag)); + if (!ptr) + { + return getLogLevel(); + } + return ptr->level; } LogLevel setLogLevel(LogLevel logLevel) { - LogLevel old = getLogLevelVariable(); - getLogLevelVariable() = logLevel; + // note: not thread safe, use sparingly and do not critically depend on outcome + LogLevel& refGlobalLevel = internal::getLogLevelVariable(); + const LogLevel old = refGlobalLevel; + refGlobalLevel = logLevel; return old; } LogLevel getLogLevel() { - return getLogLevelVariable(); + return internal::getLogLevelVariable(); } namespace internal { @@ -105,6 +211,29 @@ void writeLogMessage(LogLevel logLevel, const char* message) (*out) << std::flush; } +void writeLogMessageEx(LogLevel logLevel, const char* tag, const char* file, int line, const char* func, const char* message) +{ + std::ostringstream strm; + if (tag) + { + strm << tag << " "; + } + if (file) + { + strm << file << " "; + } + if (line > 0) + { + strm << "(" << line << ") "; + } + if (func) + { + strm << func << " "; + } + strm << message; + writeLogMessage(logLevel, strm.str().c_str()); +} + } // namespace }}} // namespace diff --git a/modules/core/src/utils/logtagconfig.hpp b/modules/core/src/utils/logtagconfig.hpp new file mode 100644 index 0000000000..9635d1bff5 --- /dev/null +++ b/modules/core/src/utils/logtagconfig.hpp @@ -0,0 +1,51 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. + +#ifndef OPENCV_CORE_LOGTAGCONFIG_HPP +#define OPENCV_CORE_LOGTAGCONFIG_HPP + +#if 1 // if not already in precompiled headers +#include +#include +#endif + +namespace cv { +namespace utils { +namespace logging { + +struct LogTagConfig +{ + std::string namePart; + LogLevel level; + bool isGlobal; + bool hasPrefixWildcard; + bool hasSuffixWildcard; + + LogTagConfig() + : namePart() + , level() + , isGlobal() + , hasPrefixWildcard() + , hasSuffixWildcard() + { + } + + LogTagConfig(const std::string& _namePart, LogLevel _level, bool _isGlobal = false, + bool _hasPrefixWildcard = false, bool _hasSuffixWildcard = false) + : namePart(_namePart) + , level(_level) + , isGlobal(_isGlobal) + , hasPrefixWildcard(_hasPrefixWildcard) + , hasSuffixWildcard(_hasSuffixWildcard) + { + } + + LogTagConfig(const LogTagConfig&) = default; + LogTagConfig(LogTagConfig&&) = default; + ~LogTagConfig() = default; +}; + +}}} + +#endif diff --git a/modules/core/src/utils/logtagconfigparser.cpp b/modules/core/src/utils/logtagconfigparser.cpp new file mode 100644 index 0000000000..2d19661db9 --- /dev/null +++ b/modules/core/src/utils/logtagconfigparser.cpp @@ -0,0 +1,305 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. + +#include "../precomp.hpp" +#include "logtagconfigparser.hpp" + +namespace cv { +namespace utils { +namespace logging { + +LogTagConfigParser::LogTagConfigParser() +{ + m_parsedGlobal.namePart = "global"; + m_parsedGlobal.isGlobal = true; + m_parsedGlobal.hasPrefixWildcard = false; + m_parsedGlobal.hasSuffixWildcard = false; + m_parsedGlobal.level = LOG_LEVEL_VERBOSE; +} + +LogTagConfigParser::LogTagConfigParser(const std::string& input) +{ + parse(input); +} + +LogTagConfigParser::~LogTagConfigParser() +{ +} + +bool LogTagConfigParser::parse(const std::string& input) +{ + m_input = input; + segmentTokens(); + return (m_malformed.empty()); +} + +bool LogTagConfigParser::hasMalformed() const +{ + return !m_malformed.empty(); +} + +const LogTagConfig& LogTagConfigParser::getGlobalConfig() const +{ + return m_parsedGlobal; +} + +const std::vector& LogTagConfigParser::getFullNameConfigs() const +{ + return m_parsedFullName; +} + +const std::vector& LogTagConfigParser::getFirstPartConfigs() const +{ + return m_parsedFirstPart; +} + +const std::vector& LogTagConfigParser::getAnyPartConfigs() const +{ + return m_parsedAnyPart; +} + +const std::vector& LogTagConfigParser::getMalformed() const +{ + return m_malformed; +} + +void LogTagConfigParser::segmentTokens() +{ + const size_t len = m_input.length(); + std::vector> startStops; + bool wasSeparator = true; + for (size_t pos = 0u; pos < len; ++pos) + { + char c = m_input[pos]; + bool isSeparator = (c == ' ' || c == '\t' || c == ';'); + if (!isSeparator) + { + if (wasSeparator) + { + startStops.emplace_back(pos, pos + 1u); + } + else + { + startStops.back().second = pos + 1u; + } + } + wasSeparator = isSeparator; + } + for (const auto& startStop : startStops) + { + const auto s = m_input.substr(startStop.first, startStop.second - startStop.first); + parseNameAndLevel(s); + } +} + +void LogTagConfigParser::parseNameAndLevel(const std::string& s) +{ + const size_t npos = std::string::npos; + const size_t len = s.length(); + size_t colonIdx = s.find_first_of(":="); + if (colonIdx == npos) + { + // See if the whole string is a log level + auto parsedLevel = parseLogLevel(s); + if (parsedLevel.second) + { + // If it is, assume the log level is for global + parseWildcard("", parsedLevel.first); + return; + } + else + { + // not sure what to do. + m_malformed.push_back(s); + return; + } + } + if (colonIdx == 0u || colonIdx + 1u == len) + { + // malformed (colon or equal sign at beginning or end), cannot do anything + m_malformed.push_back(s); + return; + } + size_t colonIdx2 = s.find_first_of(":=", colonIdx + 1u); + if (colonIdx2 != npos) + { + // malformed (more than one colon or equal sign), cannot do anything + m_malformed.push_back(s); + return; + } + auto parsedLevel = parseLogLevel(s.substr(colonIdx + 1u)); + if (parsedLevel.second) + { + parseWildcard(s.substr(0u, colonIdx), parsedLevel.first); + return; + } + else + { + // Cannot recognize the right side of the colon or equal sign. + // Not sure what to do. + m_malformed.push_back(s); + return; + } +} + +void LogTagConfigParser::parseWildcard(const std::string& name, LogLevel level) +{ + constexpr size_t npos = std::string::npos; + const size_t len = name.length(); + if (len == 0u) + { + m_parsedGlobal.level = level; + return; + } + const bool hasPrefixWildcard = (name[0u] == '*'); + if (hasPrefixWildcard && len == 1u) + { + m_parsedGlobal.level = level; + return; + } + const size_t firstNonWildcard = name.find_first_not_of("*."); + if (hasPrefixWildcard && firstNonWildcard == npos) + { + m_parsedGlobal.level = level; + return; + } + const bool hasSuffixWildcard = (name[len - 1u] == '*'); + const size_t lastNonWildcard = name.find_last_not_of("*."); + std::string trimmedNamePart = name.substr(firstNonWildcard, lastNonWildcard - firstNonWildcard + 1u); + // The case of a single asterisk has been handled above; + // here we only handle the explicit use of "global" in the log config string. + const bool isGlobal = (trimmedNamePart == "global"); + if (isGlobal) + { + m_parsedGlobal.level = level; + return; + } + LogTagConfig result(trimmedNamePart, level, false, hasPrefixWildcard, hasSuffixWildcard); + if (hasPrefixWildcard) + { + m_parsedAnyPart.emplace_back(std::move(result)); + } + else if (hasSuffixWildcard) + { + m_parsedFirstPart.emplace_back(std::move(result)); + } + else + { + m_parsedFullName.emplace_back(std::move(result)); + } +} + +std::pair LogTagConfigParser::parseLogLevel(const std::string& s) +{ + const auto falseDontCare = std::make_pair(LOG_LEVEL_VERBOSE, false); + const auto make_parsed_result = [](LogLevel lev) -> std::pair + { + return std::make_pair(lev, true); + }; + const size_t len = s.length(); + if (len >= 1u) + { + const char c = (char)std::toupper(s[0]); + switch (c) + { + case '0': + if (len == 1u) + { + return make_parsed_result(LOG_LEVEL_SILENT); + } + break; + case 'D': + if (len == 1u || + (len == 5u && cv::toUpperCase(s) == "DEBUG")) + { + return make_parsed_result(LOG_LEVEL_DEBUG); + } + if ((len == 7u && cv::toUpperCase(s) == "DISABLE") || + (len == 8u && cv::toUpperCase(s) == "DISABLED")) + { + return make_parsed_result(LOG_LEVEL_SILENT); + } + break; + case 'E': + if (len == 1u || + (len == 5u && cv::toUpperCase(s) == "ERROR")) + { + return make_parsed_result(LOG_LEVEL_ERROR); + } + break; + case 'F': + if (len == 1u || + (len == 5u && cv::toUpperCase(s) == "FATAL")) + { + return make_parsed_result(LOG_LEVEL_FATAL); + } + break; + case 'I': + if (len == 1u || + (len == 4u && cv::toUpperCase(s) == "INFO")) + { + return make_parsed_result(LOG_LEVEL_INFO); + } + break; + case 'O': + if (len == 3u && cv::toUpperCase(s) == "OFF") + { + return make_parsed_result(LOG_LEVEL_SILENT); + } + break; + case 'S': + if (len == 1u || + (len == 6u && cv::toUpperCase(s) == "SILENT")) + { + return make_parsed_result(LOG_LEVEL_SILENT); + } + break; + case 'V': + if (len == 1u || + (len == 7u && cv::toUpperCase(s) == "VERBOSE")) + { + return make_parsed_result(LOG_LEVEL_VERBOSE); + } + break; + case 'W': + if (len == 1u || + (len == 4u && cv::toUpperCase(s) == "WARN") || + (len == 7u && cv::toUpperCase(s) == "WARNING") || + (len == 8u && cv::toUpperCase(s) == "WARNINGS")) + { + return make_parsed_result(LOG_LEVEL_WARNING); + } + break; + default: + break; + } + // fall through + } + return falseDontCare; +} + +std::string LogTagConfigParser::toString(LogLevel level) +{ + switch (level) + { + case LOG_LEVEL_SILENT: + return "SILENT"; + case LOG_LEVEL_FATAL: + return "FATAL"; + case LOG_LEVEL_ERROR: + return "ERROR"; + case LOG_LEVEL_WARNING: + return "WARNING"; + case LOG_LEVEL_INFO: + return "INFO"; + case LOG_LEVEL_DEBUG: + return "DEBUG"; + case LOG_LEVEL_VERBOSE: + return "VERBOSE"; + default: + return std::to_string((int)level); + } +} + +}}} //namespace diff --git a/modules/core/src/utils/logtagconfigparser.hpp b/modules/core/src/utils/logtagconfigparser.hpp new file mode 100644 index 0000000000..3db7fb668c --- /dev/null +++ b/modules/core/src/utils/logtagconfigparser.hpp @@ -0,0 +1,55 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. + +#ifndef OPENCV_CORE_LOGTAGCONFIGPARSER_HPP +#define OPENCV_CORE_LOGTAGCONFIGPARSER_HPP + +#if 1 // if not already in precompiled headers +#include +#include +#include +#endif + +#include +#include "logtagconfig.hpp" + +namespace cv { +namespace utils { +namespace logging { + +class LogTagConfigParser +{ +public: + LogTagConfigParser(); + explicit LogTagConfigParser(const std::string& input); + ~LogTagConfigParser(); + +public: + bool parse(const std::string& input); + bool hasMalformed() const; + const LogTagConfig& getGlobalConfig() const; + const std::vector& getFullNameConfigs() const; + const std::vector& getFirstPartConfigs() const; + const std::vector& getAnyPartConfigs() const; + const std::vector& getMalformed() const; + +private: + void segmentTokens(); + void parseNameAndLevel(const std::string& s); + void parseWildcard(const std::string& name, LogLevel level); + static std::pair parseLogLevel(const std::string& s); + static std::string toString(LogLevel level); + +private: + std::string m_input; + LogTagConfig m_parsedGlobal; + std::vector m_parsedFullName; + std::vector m_parsedFirstPart; + std::vector m_parsedAnyPart; + std::vector m_malformed; +}; + +}}} //namespace + +#endif diff --git a/modules/core/src/utils/logtagmanager.cpp b/modules/core/src/utils/logtagmanager.cpp new file mode 100644 index 0000000000..ca7afbfa69 --- /dev/null +++ b/modules/core/src/utils/logtagmanager.cpp @@ -0,0 +1,425 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. + +#include "../precomp.hpp" +#include "logtagmanager.hpp" +#include "logtagconfigparser.hpp" + +namespace cv { +namespace utils { +namespace logging { + +LogTagManager::LogTagManager(LogLevel defaultUnconfiguredGlobalLevel) + : m_mutex() + , m_globalLogTag(new LogTag(m_globalName, defaultUnconfiguredGlobalLevel)) + , m_config(std::make_shared()) +{ + assign(m_globalName, m_globalLogTag.get()); +} + +LogTagManager::~LogTagManager() +{ +} + +void LogTagManager::setConfigString(const std::string& configString, bool apply /*true*/) +{ + m_config->parse(configString); + if (m_config->hasMalformed()) + { + return; + } + if (!apply) + { + return; + } + // The following code is arranged with "priority by overwriting", + // where when the same log tag has multiple matches, the last code + // block has highest priority by literally overwriting the effects + // from the earlier code blocks. + // + // Matching by full name has highest priority. + // Matching by any name part has moderate priority. + // Matching by first name part (prefix) has lowest priority. + // + const auto& globalConfig = m_config->getGlobalConfig(); + m_globalLogTag->level = globalConfig.level; + for (const auto& config : m_config->getFirstPartConfigs()) + { + setLevelByFirstPart(config.namePart, config.level); + } + for (const auto& config : m_config->getAnyPartConfigs()) + { + setLevelByAnyPart(config.namePart, config.level); + } + for (const auto& config : m_config->getFullNameConfigs()) + { + setLevelByFullName(config.namePart, config.level); + } +} + +LogTagConfigParser& LogTagManager::getConfigParser() const +{ + return *m_config; +} + +void LogTagManager::assign(const std::string& fullName, LogTag* ptr) +{ + CV_TRACE_FUNCTION(); + LockType lock(m_mutex); + FullNameLookupResult result(fullName); + result.m_findCrossReferences = true; + m_nameTable.addOrLookupFullName(result); + FullNameInfo& fullNameInfo = *result.m_fullNameInfoPtr; + const bool isPtrChanged = (fullNameInfo.logTagPtr != ptr); + if (!isPtrChanged) + { + return; + } + fullNameInfo.logTagPtr = ptr; + if (!ptr) + { + return; + } + const bool hasAppliedFullNameConfig = internal_applyFullNameConfigToTag(fullNameInfo); + if (hasAppliedFullNameConfig) + { + return; + } + internal_applyNamePartConfigToSpecificTag(result); +} + +void LogTagManager::unassign(const std::string& fullName) +{ + // Lock is inside assign() method. + assign(fullName, nullptr); +} + +LogTag* LogTagManager::get(const std::string& fullName) +{ + CV_TRACE_FUNCTION(); + LockType lock(m_mutex); + FullNameInfo* fullNameInfoPtr = m_nameTable.getFullNameInfo(fullName); + if (fullNameInfoPtr && fullNameInfoPtr->logTagPtr) + { + return fullNameInfoPtr->logTagPtr; + } + return nullptr; +} + +void LogTagManager::setLevelByFullName(const std::string& fullName, LogLevel level) +{ + CV_TRACE_FUNCTION(); + LockType lock(m_mutex); + FullNameLookupResult result(fullName); + result.m_findCrossReferences = false; + m_nameTable.addOrLookupFullName(result); + FullNameInfo& fullNameInfo = *result.m_fullNameInfoPtr; + if (fullNameInfo.parsedLevel.scope == MatchingScope::Full && + fullNameInfo.parsedLevel.level == level) + { + // skip additional processing if nothing changes. + return; + } + // update the cached configured value. + fullNameInfo.parsedLevel.scope = MatchingScope::Full; + fullNameInfo.parsedLevel.level = level; + // update the actual tag, if already registered. + LogTag* logTagPtr = fullNameInfo.logTagPtr; + if (logTagPtr) + { + logTagPtr->level = level; + } +} + +void LogTagManager::setLevelByFirstPart(const std::string& firstPart, LogLevel level) +{ + // Lock is inside setLevelByNamePart() method. + setLevelByNamePart(firstPart, level, MatchingScope::FirstNamePart); +} + +void LogTagManager::setLevelByAnyPart(const std::string& anyPart, LogLevel level) +{ + // Lock is inside setLevelByNamePart() method. + setLevelByNamePart(anyPart, level, MatchingScope::AnyNamePart); +} + +void LogTagManager::setLevelByNamePart(const std::string& namePart, LogLevel level, MatchingScope scope) +{ + CV_TRACE_FUNCTION(); + LockType lock(m_mutex); + NamePartLookupResult result(namePart); + result.m_findCrossReferences = true; + m_nameTable.addOrLookupNamePart(result); + NamePartInfo& namePartInfo = *result.m_namePartInfoPtr; + if (namePartInfo.parsedLevel.scope == scope && + namePartInfo.parsedLevel.level == level) + { + // skip additional processing if nothing changes. + return; + } + namePartInfo.parsedLevel.scope = scope; + namePartInfo.parsedLevel.level = level; + internal_applyNamePartConfigToMatchingTags(result); +} + +std::vector LogTagManager::splitNameParts(const std::string& fullName) +{ + const size_t npos = std::string::npos; + const size_t len = fullName.length(); + std::vector nameParts; + size_t start = 0u; + while (start < len) + { + size_t nextPeriod = fullName.find('.', start); + if (nextPeriod == npos) + { + nextPeriod = len; + } + if (nextPeriod >= start + 1u) + { + nameParts.emplace_back(fullName.substr(start, nextPeriod - start)); + } + start = nextPeriod + 1u; + } + return nameParts; +} + +bool LogTagManager::internal_isNamePartMatch(MatchingScope scope, size_t matchingPos) +{ + switch (scope) + { + case MatchingScope::FirstNamePart: + return (matchingPos == 0u); + case MatchingScope::AnyNamePart: + return true; + case MatchingScope::None: + case MatchingScope::Full: + default: + return false; + } +} + +bool LogTagManager::internal_applyFullNameConfigToTag(FullNameInfo& fullNameInfo) +{ + if (!fullNameInfo.logTagPtr) + { + return false; + } + if (fullNameInfo.parsedLevel.scope == MatchingScope::Full) + { + fullNameInfo.logTagPtr->level = fullNameInfo.parsedLevel.level; + return true; + } + return false; +} + +bool LogTagManager::internal_applyNamePartConfigToSpecificTag(FullNameLookupResult& fullNameResult) +{ + const FullNameInfo& fullNameInfo = *fullNameResult.m_fullNameInfoPtr; + LogTag* const logTag = fullNameInfo.logTagPtr; + if (!logTag) + { + return false; + } + CV_Assert(fullNameResult.m_findCrossReferences); + const auto& crossReferences = fullNameResult.m_crossReferences; + const size_t matchingNamePartCount = crossReferences.size(); + for (size_t k = 0u; k < matchingNamePartCount; ++k) + { + const auto& match = crossReferences.at(k); + const auto& namePartInfo = *match.m_namePartInfo; + const auto& parsedLevel = namePartInfo.parsedLevel; + const auto scope = parsedLevel.scope; + const LogLevel level = parsedLevel.level; + const size_t matchingPos = match.m_matchingPos; + const bool isMatch = internal_isNamePartMatch(scope, matchingPos); + if (isMatch) + { + logTag->level = level; + return true; + } + } + return false; +} + +void LogTagManager::internal_applyNamePartConfigToMatchingTags(NamePartLookupResult& namePartResult) +{ + CV_Assert(namePartResult.m_findCrossReferences); + const auto& crossReferences = namePartResult.m_crossReferences; + const size_t matchingFullNameCount = crossReferences.size(); + NamePartInfo& namePartInfo = *namePartResult.m_namePartInfoPtr; + const MatchingScope scope = namePartInfo.parsedLevel.scope; + CV_Assert(scope != MatchingScope::Full); + if (scope == MatchingScope::None) + { + return; + } + const LogLevel level = namePartInfo.parsedLevel.level; + for (size_t k = 0u; k < matchingFullNameCount; ++k) + { + const auto& match = crossReferences.at(k); + FullNameInfo& fullNameInfo = *match.m_fullNameInfo; + LogTag* logTagPtr = fullNameInfo.logTagPtr; + if (!logTagPtr) + { + continue; + } + if (fullNameInfo.parsedLevel.scope == MatchingScope::Full) + { + // If the full name already has valid config, that full name config + // has precedence over name part config. + continue; + } + const size_t matchingPos = match.m_matchingPos; + const bool isMatch = internal_isNamePartMatch(scope, matchingPos); + if (!isMatch) + { + continue; + } + logTagPtr->level = level; + } +} + +void LogTagManager::NameTable::addOrLookupFullName(FullNameLookupResult& result) +{ + const auto fullNameIdAndFlag = internal_addOrLookupFullName(result.m_fullName); + result.m_fullNameId = fullNameIdAndFlag.first; + result.m_nameParts = LogTagManager::splitNameParts(result.m_fullName); + internal_addOrLookupNameParts(result.m_nameParts, result.m_namePartIds); + const bool isNew = fullNameIdAndFlag.second; + if (isNew) + { + internal_addCrossReference(result.m_fullNameId, result.m_namePartIds); + } + // ====== IMPORTANT ====== Critical order-of-operation ====== + // The gathering of the pointers of FullNameInfo and NamePartInfo are performed + // as the last step of the operation, so that these pointer are not invalidated + // by the vector append operations earlier in this function. + // ====== + result.m_fullNameInfoPtr = internal_getFullNameInfo(result.m_fullNameId); + if (result.m_findCrossReferences) + { + internal_findMatchingNamePartsForFullName(result); + } +} + +void LogTagManager::NameTable::addOrLookupNamePart(NamePartLookupResult& result) +{ + result.m_namePartId = internal_addOrLookupNamePart(result.m_namePart); + result.m_namePartInfoPtr = internal_getNamePartInfo(result.m_namePartId); + if (result.m_findCrossReferences) + { + internal_findMatchingFullNamesForNamePart(result); + } +} + +std::pair LogTagManager::NameTable::internal_addOrLookupFullName(const std::string& fullName) +{ + const auto fullNameIdIter = m_fullNameIds.find(fullName); + if (fullNameIdIter != m_fullNameIds.end()) + { + return std::make_pair(fullNameIdIter->second, false); + } + const size_t fullNameId = m_fullNameInfos.size(); + m_fullNameInfos.emplace_back(FullNameInfo{}); + m_fullNameIds.emplace(fullName, fullNameId); + return std::make_pair(fullNameId, true); +} + +void LogTagManager::NameTable::internal_addOrLookupNameParts(const std::vector& nameParts, + std::vector& namePartIds) +{ + const size_t namePartCount = nameParts.size(); + namePartIds.resize(namePartCount, ~(size_t)0u); + for (size_t namePartIndex = 0u; namePartIndex < namePartCount; ++namePartIndex) + { + const std::string& namePart = nameParts.at(namePartIndex); + const size_t namePartId = internal_addOrLookupNamePart(namePart); + namePartIds.at(namePartIndex) = namePartId; + } +} + +size_t LogTagManager::NameTable::internal_addOrLookupNamePart(const std::string& namePart) +{ + const auto namePartIter = m_namePartIds.find(namePart); + if (namePartIter != m_namePartIds.end()) + { + return namePartIter->second; + } + const size_t namePartId = m_namePartInfos.size(); + m_namePartInfos.emplace_back(NamePartInfo{}); + m_namePartIds.emplace(namePart, namePartId); + return namePartId; +} + +void LogTagManager::NameTable::internal_addCrossReference(size_t fullNameId, const std::vector& namePartIds) +{ + const size_t namePartCount = namePartIds.size(); + for (size_t namePartPos = 0u; namePartPos < namePartCount; ++namePartPos) + { + const size_t namePartId = namePartIds.at(namePartPos); + m_fullNameToNamePartIds.emplace(fullNameId, std::make_pair(namePartId, namePartPos)); + m_namePartToFullNameIds.emplace(namePartId, std::make_pair(fullNameId, namePartPos)); + } +} + +LogTagManager::FullNameInfo* LogTagManager::NameTable::getFullNameInfo(const std::string& fullName) +{ + const auto fullNameIdIter = m_fullNameIds.find(fullName); + if (fullNameIdIter == m_fullNameIds.end()) + { + return nullptr; + } + const size_t fullNameId = fullNameIdIter->second; + return internal_getFullNameInfo(fullNameId); +} + +LogTagManager::FullNameInfo* LogTagManager::NameTable::internal_getFullNameInfo(size_t fullNameId) +{ + return std::addressof(m_fullNameInfos.at(fullNameId)); +} + +LogTagManager::NamePartInfo* LogTagManager::NameTable::internal_getNamePartInfo(size_t namePartId) +{ + return std::addressof(m_namePartInfos.at(namePartId)); +} + +void LogTagManager::NameTable::internal_findMatchingNamePartsForFullName(FullNameLookupResult& fullNameResult) +{ + const size_t fullNameId = fullNameResult.m_fullNameId; + FullNameInfo* fullNameInfo = fullNameResult.m_fullNameInfoPtr; + const auto& namePartIds = fullNameResult.m_namePartIds; + const size_t namePartCount = namePartIds.size(); + auto& crossReferences = fullNameResult.m_crossReferences; + crossReferences.clear(); + crossReferences.reserve(namePartCount); + for (size_t matchingPos = 0u; matchingPos < namePartCount; ++matchingPos) + { + const size_t namePartId = namePartIds.at(matchingPos); + NamePartInfo* namePartInfo = internal_getNamePartInfo(namePartId); + crossReferences.emplace_back(CrossReference(fullNameId, namePartId, matchingPos, fullNameInfo, namePartInfo)); + } +} + +void LogTagManager::NameTable::internal_findMatchingFullNamesForNamePart(NamePartLookupResult& result) +{ + const size_t namePartId = result.m_namePartId; + NamePartInfo* namePartInfo = result.m_namePartInfoPtr; + const size_t matchingFullNameCount = m_namePartToFullNameIds.count(namePartId); + std::vector& crossReferences = result.m_crossReferences; + crossReferences.clear(); + crossReferences.reserve(matchingFullNameCount); + const auto namePartToFullNameIterPair = m_namePartToFullNameIds.equal_range(result.m_namePartId); + const auto iterBegin = namePartToFullNameIterPair.first; + const auto iterEnd = namePartToFullNameIterPair.second; + for (auto iter = iterBegin; iter != iterEnd; ++iter) + { + const size_t fullNameId = iter->second.first; + const size_t matchingPos = iter->second.second; + FullNameInfo* fullNameInfo = internal_getFullNameInfo(fullNameId); + crossReferences.emplace_back(CrossReference(fullNameId, namePartId, matchingPos, fullNameInfo, namePartInfo)); + } +} + +}}} //namespace diff --git a/modules/core/src/utils/logtagmanager.hpp b/modules/core/src/utils/logtagmanager.hpp new file mode 100644 index 0000000000..1bee287980 --- /dev/null +++ b/modules/core/src/utils/logtagmanager.hpp @@ -0,0 +1,284 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. + +#ifndef OPENCV_CORE_LOGTAGMANAGER_HPP +#define OPENCV_CORE_LOGTAGMANAGER_HPP + +#if 1 // if not already in precompiled headers +#include +#include +#include +#include +#include +#include +#endif + +#include +#include "logtagconfig.hpp" + +namespace cv { +namespace utils { +namespace logging { + +// forward declaration +class LogTagConfigParser; + +// A lookup table of LogTags using full name, first part of name, and any part of name. +// The name parts of a LogTag is delimited by period. +// +// This class does not handle wildcard characters. The name-matching method can only be +// selected by calling the appropriate function. +// +class LogTagManager +{ +private: + // Current implementation does not seem to require recursive mutex; + // also, extensible functions (accepting user-provided callback) are not allowed + // to call LogTagManger (to prevent iterator invalidation), which needs enforced + // with a non-recursive mutex. + using MutexType = std::mutex; + using LockType = std::lock_guard; + + enum class MatchingScope + { + None, + Full, + FirstNamePart, + AnyNamePart + }; + + struct ParsedLevel + { + LogLevel level; + MatchingScope scope; + + ParsedLevel() + : level() + , scope(MatchingScope::None) + { + } + }; + + struct FullNameInfo + { + LogTag* logTagPtr; + ParsedLevel parsedLevel; + }; + + struct NamePartInfo + { + ParsedLevel parsedLevel; + }; + + struct CrossReference + { + size_t m_fullNameId; + size_t m_namePartId; + size_t m_matchingPos; + FullNameInfo* m_fullNameInfo; + NamePartInfo* m_namePartInfo; + + explicit CrossReference(size_t fullNameId, size_t namePartId, size_t matchingPos, + FullNameInfo* fullNameInfo, NamePartInfo* namePartInfo) + : m_fullNameId(fullNameId) + , m_namePartId(namePartId) + , m_matchingPos(matchingPos) + , m_fullNameInfo(fullNameInfo) + , m_namePartInfo(namePartInfo) + { + } + }; + + struct FullNameLookupResult + { + // The full name being looked up + std::string m_fullName; + + // The full name being broken down into name parts + std::vector m_nameParts; + + // The full name ID that is added or looked up from the table + size_t m_fullNameId; + + // The name part IDs that are added to or looked up from the table + // listed in the same order as m_nameParts + std::vector m_namePartIds; + + // The information struct for the full name + FullNameInfo* m_fullNameInfoPtr; + + // Specifies whether cross references (full names that match the name part) + // should be computed. + bool m_findCrossReferences; + + // List of all full names that match the given name part. + // This field is computed only if m_findCrossReferences is true. + std::vector m_crossReferences; + + explicit FullNameLookupResult(const std::string& fullName) + : m_fullName(fullName) + , m_nameParts() + , m_fullNameId() + , m_namePartIds() + , m_fullNameInfoPtr() + , m_findCrossReferences() + , m_crossReferences() + { + } + }; + + struct NamePartLookupResult + { + // The name part being looked up + std::string m_namePart; + + // The name part ID that is added or looked up from the table + size_t m_namePartId; + + // Information struct ptr for the name part + NamePartInfo* m_namePartInfoPtr; + + // Specifies whether cross references (full names that match the name part) should be computed. + bool m_findCrossReferences; + + // List of all full names that match the given name part. + // This field is computed only if m_findCrossReferences is true. + std::vector m_crossReferences; + + explicit NamePartLookupResult(const std::string& namePart) + : m_namePart(namePart) + , m_namePartId() + , m_namePartInfoPtr() + , m_findCrossReferences() + , m_crossReferences() + { + } + }; + + struct NameTable + { + // All data structures in this class are append-only. The item count + // is being used as an incrementing integer key. + + public: + // Full name information struct. + std::vector m_fullNameInfos; + + // Name part information struct. + std::vector m_namePartInfos; + + // key: full name (string) + // value: full name ID + // .... (index into the vector of m_fullNameInfos) + // .... (key into m_fullNameToNamePartIds) + // .... (value.second in m_namePartToFullNameIds) + std::unordered_map m_fullNameIds; + + // key: name part (string) + // value: name part ID + // .... (index into the vector of m_namePartInfos) + // .... (key into m_namePartToFullNameIds) + // .... (value.second in m_fullNameToNamePartIds) + std::unordered_map m_namePartIds; + + // key: full name ID + // value.first: name part ID + // value.second: occurrence position of name part in the full name + std::unordered_multimap> m_fullNameToNamePartIds; + + // key: name part ID + // value.first: full name ID + // value.second: occurrence position of name part in the full name + std::unordered_multimap> m_namePartToFullNameIds; + + public: + void addOrLookupFullName(FullNameLookupResult& result); + void addOrLookupNamePart(NamePartLookupResult& result); + FullNameInfo* getFullNameInfo(const std::string& fullName); + + private: + // Add or get full name. Does not compute name parts or access them in the table. + // Returns the full name ID, and a bool indicating if the full name is new. + std::pair internal_addOrLookupFullName(const std::string& fullName); + + // Add or get multiple name parts. Saves name part IDs into a vector. + void internal_addOrLookupNameParts(const std::vector& nameParts, std::vector& namePartIds); + + // Add or get name part. Returns namePartId. + size_t internal_addOrLookupNamePart(const std::string& namePart); + + // For each name part ID, insert the tuples (full name, name part ID, name part position) + // into the cross reference table + void internal_addCrossReference(size_t fullNameId, const std::vector& namePartIds); + + // Gather pointer for full name info struct. + // Note: The pointer is interior to the table vector. The pointers are invalidated + // if the table is modified. + FullNameInfo* internal_getFullNameInfo(size_t fullNameId); + + // Gather pointers for name part info struct. + // Note: The pointers are interior to the table vector. The pointers are invalidated + // if the table is modified. + NamePartInfo* internal_getNamePartInfo(size_t namePartId); + + void internal_findMatchingNamePartsForFullName(FullNameLookupResult& fullNameResult); + void internal_findMatchingFullNamesForNamePart(NamePartLookupResult& result); + }; + +public: + LogTagManager(LogLevel defaultUnconfiguredGlobalLevel); + ~LogTagManager(); + +public: + // Parse and apply the config string. + void setConfigString(const std::string& configString, bool apply = true); + + // Gets the config parser. This is necessary to retrieve the list of malformed strings. + LogTagConfigParser& getConfigParser() const; + + // Add (register) the log tag. + // Note, passing in nullptr as value is equivalent to unassigning. + void assign(const std::string& fullName, LogTag* ptr); + + // Unassign the log tag. This is equivalent to calling assign with nullptr value. + void unassign(const std::string& fullName); + + // Retrieve the log tag by exact name. + LogTag* get(const std::string& fullName); + + // Changes the log level of the tag having the exact full name. + void setLevelByFullName(const std::string& fullName, LogLevel level); + + // Changes the log level of the tags matching the first part of the name. + void setLevelByFirstPart(const std::string& firstPart, LogLevel level); + + // Changes the log level of the tags matching any part of the name. + void setLevelByAnyPart(const std::string& anyPart, LogLevel level); + + // Changes the log level of the tags with matching name part according + // to the specified scope. + void setLevelByNamePart(const std::string& namePart, LogLevel level, MatchingScope scope); + +private: + bool internal_applyFullNameConfigToTag(FullNameInfo& fullNameInfo); + bool internal_applyNamePartConfigToSpecificTag(FullNameLookupResult& fullNameResult); + void internal_applyNamePartConfigToMatchingTags(NamePartLookupResult& namePartResult); + +private: + static std::vector splitNameParts(const std::string& fullName); + static bool internal_isNamePartMatch(MatchingScope scope, size_t matchingPos); + +private: + static constexpr const char* m_globalName = "global"; + +private: + mutable MutexType m_mutex; + std::unique_ptr m_globalLogTag; + NameTable m_nameTable; + std::shared_ptr m_config; +}; + +}}} //namespace + +#endif //OPENCV_CORE_LOGTAGMANAGER_HPP diff --git a/modules/core/test/test_logtagconfigparser.cpp b/modules/core/test/test_logtagconfigparser.cpp new file mode 100644 index 0000000000..2af30e85dd --- /dev/null +++ b/modules/core/test/test_logtagconfigparser.cpp @@ -0,0 +1,281 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. + +#include "test_precomp.hpp" +#include +#include "../src/utils/logtagmanager.hpp" +#include "../src/utils/logtagconfigparser.hpp" + +// Because "LogTagConfigParser" isn't exported from "opencv_core", the only way +// to perform test is to compile the source code into "opencv_test_core". +// This workaround may cause step debugger breakpoints to work unreliably. +#if 1 +#include "../src/utils/logtagconfigparser.cpp" +#endif + +using cv::utils::logging::LogTagConfigParser; + +namespace opencv_test { +namespace { + +typedef testing::TestWithParam> GlobalShouldSucceedTests; + +TEST_P(GlobalShouldSucceedTests, globalCases) +{ + const std::string input = get<0>(GetParam()); + const cv::utils::logging::LogLevel expectedLevel = get<1>(GetParam()); + LogTagConfigParser parser; + parser.parse(input); + EXPECT_FALSE(parser.hasMalformed()); + EXPECT_EQ(parser.getMalformed().size(), 0u) << "Malformed list should be empty"; + EXPECT_TRUE(parser.getGlobalConfig().isGlobal); + EXPECT_EQ(parser.getGlobalConfig().level, expectedLevel); + EXPECT_EQ(parser.getFullNameConfigs().size(), 0u) << "Specifying global log level should not emit full names"; + EXPECT_EQ(parser.getFirstPartConfigs().size(), 0u) << "Specifying global log level should not emit first name part result"; + EXPECT_EQ(parser.getAnyPartConfigs().size(), 0u) << "Specifying global log level should not emit any name part result"; +} + +INSTANTIATE_TEST_CASE_P(Core_LogTagConfigParser, GlobalShouldSucceedTests, + testing::Values( + // Following test cases omit the name part + std::make_tuple("S", cv::utils::logging::LOG_LEVEL_SILENT), + std::make_tuple("SILENT", cv::utils::logging::LOG_LEVEL_SILENT), + std::make_tuple("F", cv::utils::logging::LOG_LEVEL_FATAL), + std::make_tuple("FATAL", cv::utils::logging::LOG_LEVEL_FATAL), + std::make_tuple("E", cv::utils::logging::LOG_LEVEL_ERROR), + std::make_tuple("ERROR", cv::utils::logging::LOG_LEVEL_ERROR), + std::make_tuple("W", cv::utils::logging::LOG_LEVEL_WARNING), + std::make_tuple("WARN", cv::utils::logging::LOG_LEVEL_WARNING), + std::make_tuple("WARNING", cv::utils::logging::LOG_LEVEL_WARNING), + std::make_tuple("I", cv::utils::logging::LOG_LEVEL_INFO), + std::make_tuple("INFO", cv::utils::logging::LOG_LEVEL_INFO), + std::make_tuple("D", cv::utils::logging::LOG_LEVEL_DEBUG), + std::make_tuple("DEBUG", cv::utils::logging::LOG_LEVEL_DEBUG), + std::make_tuple("V", cv::utils::logging::LOG_LEVEL_VERBOSE), + std::make_tuple("VERBOSE", cv::utils::logging::LOG_LEVEL_VERBOSE), + // Following test cases uses a single asterisk as name + std::make_tuple("*:S", cv::utils::logging::LOG_LEVEL_SILENT), + std::make_tuple("*:SILENT", cv::utils::logging::LOG_LEVEL_SILENT), + std::make_tuple("*:V", cv::utils::logging::LOG_LEVEL_VERBOSE), + std::make_tuple("*:VERBOSE", cv::utils::logging::LOG_LEVEL_VERBOSE) + ) +); + +// GlobalShouldSucceedPairedTests, globalNameHandling +// +// The following tests use a strategy of performing two tests as a pair, and require the pair +// to succeed, in order to avoid false negatives due to default settings. +// The first input string is supposed to set global to SILENT, the second input string VERBOSE. + +typedef testing::TestWithParam> GlobalShouldSucceedPairedTests; + +TEST_P(GlobalShouldSucceedPairedTests, globalNameHandling) +{ + const auto firstExpected = cv::utils::logging::LOG_LEVEL_SILENT; + const auto secondExpected = cv::utils::logging::LOG_LEVEL_VERBOSE; + // + const std::string firstInput = get<0>(GetParam()); + LogTagConfigParser firstParser; + firstParser.parse(firstInput); + ASSERT_FALSE(firstParser.hasMalformed()); + ASSERT_EQ(firstParser.getMalformed().size(), 0u) << "Malformed list should be empty"; + ASSERT_TRUE(firstParser.getGlobalConfig().isGlobal); + ASSERT_EQ(firstParser.getFullNameConfigs().size(), 0u) << "Specifying global log level should not emit full names"; + ASSERT_EQ(firstParser.getFirstPartConfigs().size(), 0u) << "Specifying global log level should not emit first name part result"; + ASSERT_EQ(firstParser.getAnyPartConfigs().size(), 0u) << "Specifying global log level should not emit any name part result"; + const cv::utils::logging::LogLevel firstActual = firstParser.getGlobalConfig().level; + // + const std::string secondInput = get<1>(GetParam()); + LogTagConfigParser secondParser; + secondParser.parse(secondInput); + ASSERT_FALSE(secondParser.hasMalformed()); + ASSERT_EQ(secondParser.getMalformed().size(), 0u) << "Malformed list should be empty"; + ASSERT_TRUE(secondParser.getGlobalConfig().isGlobal); + ASSERT_EQ(secondParser.getFullNameConfigs().size(), 0u) << "Specifying global log level should not emit full names"; + ASSERT_EQ(secondParser.getFirstPartConfigs().size(), 0u) << "Specifying global log level should not emit first name part result"; + ASSERT_EQ(secondParser.getAnyPartConfigs().size(), 0u) << "Specifying global log level should not emit any name part result"; + const cv::utils::logging::LogLevel secondActual = secondParser.getGlobalConfig().level; + // + EXPECT_EQ(firstActual, firstExpected); + EXPECT_EQ(secondActual, secondExpected); +} + +// Following test cases uses lowercase "global" as name +INSTANTIATE_TEST_CASE_P(Core_LogTagConfigParser, GlobalShouldSucceedPairedTests, + testing::Values( + std::make_tuple("global:S", "global:V"), + std::make_tuple("global:SILENT", "global:VERBOSE") + ) +); + +// In the next few smoke tests, the use of EXPECT versus ASSERT is as follows. +// Each test will try to read the first element from one of the vector results. +// Prior to that, the vector need to be ASSERT'ed to have at least one element. +// All remaining assertions in the test body would use EXPECT instead. + +TEST(Core_LogTagConfigParser, FullNameSmokeTest) +{ + LogTagConfigParser parser; + parser.parse("something:S"); + EXPECT_FALSE(parser.hasMalformed()); + EXPECT_EQ(parser.getMalformed().size(), 0u); + ASSERT_EQ(parser.getFullNameConfigs().size(), 1u); + EXPECT_EQ(parser.getFirstPartConfigs().size(), 0u); + EXPECT_EQ(parser.getAnyPartConfigs().size(), 0u); + EXPECT_STREQ(parser.getFullNameConfigs().at(0u).namePart.c_str(), "something"); +} + +TEST(Core_LogTagConfigParser, FirstPartSmokeTest_NoPeriod) +{ + LogTagConfigParser parser; + parser.parse("something*:S"); + EXPECT_FALSE(parser.hasMalformed()); + EXPECT_EQ(parser.getMalformed().size(), 0u); + EXPECT_EQ(parser.getFullNameConfigs().size(), 0u); + ASSERT_EQ(parser.getFirstPartConfigs().size(), 1u); + EXPECT_EQ(parser.getAnyPartConfigs().size(), 0u); + EXPECT_STREQ(parser.getFirstPartConfigs().at(0u).namePart.c_str(), "something"); +} + +TEST(Core_LogTagConfigParser, FirstPartSmokeTest_WithPeriod) +{ + LogTagConfigParser parser; + parser.parse("something.*:S"); + EXPECT_FALSE(parser.hasMalformed()); + EXPECT_EQ(parser.getMalformed().size(), 0u); + EXPECT_EQ(parser.getFullNameConfigs().size(), 0u); + ASSERT_EQ(parser.getFirstPartConfigs().size(), 1u); + EXPECT_EQ(parser.getAnyPartConfigs().size(), 0u); + EXPECT_STREQ(parser.getFirstPartConfigs().at(0u).namePart.c_str(), "something"); +} + +TEST(Core_LogTagConfigParser, AnyPartSmokeTest_PrecedeAsterisk_NoPeriod) +{ + LogTagConfigParser parser; + parser.parse("*something:S"); + EXPECT_FALSE(parser.hasMalformed()); + EXPECT_EQ(parser.getMalformed().size(), 0u); + EXPECT_EQ(parser.getFullNameConfigs().size(), 0u); + EXPECT_EQ(parser.getFirstPartConfigs().size(), 0u); + ASSERT_EQ(parser.getAnyPartConfigs().size(), 1u); + EXPECT_STREQ(parser.getAnyPartConfigs().at(0u).namePart.c_str(), "something"); +} + +TEST(Core_LogTagConfigParser, AnyPartSmokeTest_PrecedeAsterisk_WithPeriod) +{ + LogTagConfigParser parser; + parser.parse("*.something:S"); + EXPECT_FALSE(parser.hasMalformed()); + EXPECT_EQ(parser.getMalformed().size(), 0u); + EXPECT_EQ(parser.getFullNameConfigs().size(), 0u); + EXPECT_EQ(parser.getFirstPartConfigs().size(), 0u); + ASSERT_EQ(parser.getAnyPartConfigs().size(), 1u); + EXPECT_STREQ(parser.getAnyPartConfigs().at(0u).namePart.c_str(), "something"); +} + +TEST(Core_LogTagConfigParser, AnyPartSmokeTest_PrecedeFollowAsterisks_NoPeriod) +{ + LogTagConfigParser parser; + parser.parse("*something*:S"); + EXPECT_FALSE(parser.hasMalformed()); + EXPECT_EQ(parser.getMalformed().size(), 0u); + EXPECT_EQ(parser.getFullNameConfigs().size(), 0u); + EXPECT_EQ(parser.getFirstPartConfigs().size(), 0u); + ASSERT_EQ(parser.getAnyPartConfigs().size(), 1u); + EXPECT_STREQ(parser.getAnyPartConfigs().at(0u).namePart.c_str(), "something"); +} + +TEST(Core_LogTagConfigParser, AnyPartSmokeTest_PrecedeFollowAsterisks_WithPeriod) +{ + LogTagConfigParser parser; + parser.parse("*.something.*:S"); + EXPECT_FALSE(parser.hasMalformed()); + EXPECT_EQ(parser.getMalformed().size(), 0u); + EXPECT_EQ(parser.getFullNameConfigs().size(), 0u); + EXPECT_EQ(parser.getFirstPartConfigs().size(), 0u); + ASSERT_EQ(parser.getAnyPartConfigs().size(), 1u); + EXPECT_STREQ(parser.getAnyPartConfigs().at(0u).namePart.c_str(), "something"); +} + +TEST(Core_LogTagConfigParser, FullName_EqualSign_ShouldSucceed) +{ + LogTagConfigParser parser; + parser.parse("something=S"); + EXPECT_FALSE(parser.hasMalformed()); + EXPECT_EQ(parser.getMalformed().size(), 0u); + ASSERT_EQ(parser.getFullNameConfigs().size(), 1u); + EXPECT_EQ(parser.getFirstPartConfigs().size(), 0u); + EXPECT_EQ(parser.getAnyPartConfigs().size(), 0u); + EXPECT_STREQ(parser.getFullNameConfigs().at(0u).namePart.c_str(), "something"); +} + +TEST(Core_LogTagConfigParser, FirstPart_EqualSign_ShouldSucceed) +{ + LogTagConfigParser parser; + parser.parse("something*=S"); + EXPECT_FALSE(parser.hasMalformed()); + EXPECT_EQ(parser.getMalformed().size(), 0u); + EXPECT_EQ(parser.getFullNameConfigs().size(), 0u); + ASSERT_EQ(parser.getFirstPartConfigs().size(), 1u); + EXPECT_EQ(parser.getAnyPartConfigs().size(), 0u); + EXPECT_STREQ(parser.getFirstPartConfigs().at(0u).namePart.c_str(), "something"); +} + +TEST(Core_LogTagConfigParser, AnyPart_EqualSign_ShouldSucceed) +{ + LogTagConfigParser parser; + parser.parse("*something*=S"); + EXPECT_FALSE(parser.hasMalformed()); + EXPECT_EQ(parser.getMalformed().size(), 0u); + EXPECT_EQ(parser.getFullNameConfigs().size(), 0u); + EXPECT_EQ(parser.getFirstPartConfigs().size(), 0u); + ASSERT_EQ(parser.getAnyPartConfigs().size(), 1u); + EXPECT_STREQ(parser.getAnyPartConfigs().at(0u).namePart.c_str(), "something"); +} + +TEST(Core_LogTagConfigParser, DuplicateColon_ShouldFail) +{ + LogTagConfigParser parser; + parser.parse("something::S"); + EXPECT_TRUE(parser.hasMalformed()); + EXPECT_EQ(parser.getMalformed().size(), 1u); + EXPECT_EQ(parser.getFullNameConfigs().size(), 0u); + EXPECT_EQ(parser.getFirstPartConfigs().size(), 0u); + EXPECT_EQ(parser.getAnyPartConfigs().size(), 0u); +} + +TEST(Core_LogTagConfigParser, DuplicateEqual_ShouldFail) +{ + LogTagConfigParser parser; + parser.parse("something==S"); + EXPECT_TRUE(parser.hasMalformed()); + EXPECT_EQ(parser.getMalformed().size(), 1u); + EXPECT_EQ(parser.getFullNameConfigs().size(), 0u); + EXPECT_EQ(parser.getFirstPartConfigs().size(), 0u); + EXPECT_EQ(parser.getAnyPartConfigs().size(), 0u); +} + +TEST(Core_LogTagConfigParser, DuplicateColonAndEqual_ShouldFail) +{ + LogTagConfigParser parser; + parser.parse("something:=S"); + EXPECT_TRUE(parser.hasMalformed()); + EXPECT_EQ(parser.getMalformed().size(), 1u); + EXPECT_EQ(parser.getFullNameConfigs().size(), 0u); + EXPECT_EQ(parser.getFirstPartConfigs().size(), 0u); + EXPECT_EQ(parser.getAnyPartConfigs().size(), 0u); +} + +TEST(Core_LogTagConfigParser, DuplicateEqualAndColon_ShouldFail) +{ + LogTagConfigParser parser; + parser.parse("something=:S"); + EXPECT_TRUE(parser.hasMalformed()); + EXPECT_EQ(parser.getMalformed().size(), 1u); + EXPECT_EQ(parser.getFullNameConfigs().size(), 0u); + EXPECT_EQ(parser.getFirstPartConfigs().size(), 0u); + EXPECT_EQ(parser.getAnyPartConfigs().size(), 0u); +} + +}} // namespace diff --git a/modules/core/test/test_logtagmanager.cpp b/modules/core/test/test_logtagmanager.cpp new file mode 100644 index 0000000000..b7b2a57d9b --- /dev/null +++ b/modules/core/test/test_logtagmanager.cpp @@ -0,0 +1,645 @@ +// This file is part of OpenCV project. +// It is subject to the license terms in the LICENSE file found in the top-level directory +// of this distribution and at http://opencv.org/license.html. + +#include "test_precomp.hpp" +#include + +#include "../src/utils/logtagmanager.hpp" +#include "../src/utils/logtagconfigparser.hpp" + +// Because "LogTagManager" isn't exported from "opencv_core", the only way +// to perform test is to compile the source code into "opencv_test_core". +// This workaround may cause step debugger breakpoints to work unreliably. +#if 1 +#include "../src/utils/logtagmanager.cpp" +#endif + +namespace opencv_test { +namespace { + +using LogLevel = cv::utils::logging::LogLevel; +using LogTag = cv::utils::logging::LogTag; +using LogTagManager = cv::utils::logging::LogTagManager; + +// Value to initialize log tag constructors +static constexpr const LogLevel constTestLevelBegin = cv::utils::logging::LOG_LEVEL_SILENT; + +// Value to be set as part of test (to simulate runtime changes) +static constexpr const LogLevel constTestLevelChanged = cv::utils::logging::LOG_LEVEL_VERBOSE; + +// An alternate value to initialize log tag constructors, +// for test cases where two distinct initialization values are needed. +static constexpr const LogLevel constTestLevelAltBegin = cv::utils::logging::LOG_LEVEL_FATAL; + +// An alternate value to be set as part of test (to simulate runtime changes), +// for test cases where two distinct runtime set values are needed. +static constexpr const LogLevel constTestLevelAltChanged = cv::utils::logging::LOG_LEVEL_DEBUG; + +// Enums for specifying which LogTagManager method to call. +// Used in parameterized tests. +enum class ByWhat +{ + ByFullName = 0, + ByFirstPart = 1, + ByAnyPart = 2 +}; + +std::ostream& operator<< (std::ostream& strm, ByWhat byWhat) +{ + switch (byWhat) + { + case ByWhat::ByFullName: + strm << "ByFullName"; + break; + case ByWhat::ByFirstPart: + strm << "ByFirstPart"; + break; + case ByWhat::ByAnyPart: + strm << "ByAnyPart"; + break; + default: + strm << "(invalid ByWhat enum" << (int)byWhat << ")"; + break; + } + return strm; +} + +// Enums for describing relative timing. +// Used in parameterized tests. +enum class Timing +{ + Never = 0, + Before = 1, + After = 2 +}; + +std::ostream& operator<< (std::ostream& strm, Timing timing) +{ + switch (timing) + { + case Timing::Never: + strm << "Never"; + break; + case Timing::Before: + strm << "Before"; + break; + case Timing::After: + strm << "After"; + break; + default: + strm << "(invalid Timing enum" << (int)timing << ")"; + break; + } + return strm; +} + +// Enums for selecting the substrings used in substring confusion tests. +enum class SubstringType +{ + Prefix = 0, + Midstring = 1, + Suffix = 2, + Straddle = 3 +}; + +std::ostream& operator<< (std::ostream& strm, SubstringType substringType) +{ + switch (substringType) + { + case SubstringType::Prefix: + strm << "Prefix"; + break; + case SubstringType::Midstring: + strm << "Midstring"; + break; + case SubstringType::Suffix: + strm << "Suffix"; + break; + case SubstringType::Straddle: + strm << "Straddle"; + break; + default: + strm << "(invalid SubstringType enum: " << (int)substringType << ")"; + break; + } + return strm; +} + +// A base fixture consisting of the LogTagManager. +// Note that an instance of LogTagManager contains its own instance of "global" log tag. +class LogTagManagerTestFixture + : public ::testing::Test +{ +protected: + LogTagManager m_logTagManager; + +public: + LogTagManagerTestFixture(LogLevel initGlobalLogLevel) + : m_logTagManager(initGlobalLogLevel) + { + } + + ~LogTagManagerTestFixture() + { + } +}; + +// LogTagManagerGlobalSmokeTest verifies that the "global" log tag works as intended. +class LogTagManagerGlobalSmokeTest + : public LogTagManagerTestFixture +{ +protected: + LogTag* m_actualGlobalLogTag; + +protected: + static constexpr const char* m_globalTagName = "global"; + +public: + LogTagManagerGlobalSmokeTest() + : LogTagManagerTestFixture(constTestLevelBegin) + , m_actualGlobalLogTag(nullptr) + { + } +}; + +TEST_F(LogTagManagerGlobalSmokeTest, AfterCtorCanGetGlobalWithoutAssign) +{ + EXPECT_NE(m_logTagManager.get(m_globalTagName), nullptr); +} + +TEST_F(LogTagManagerGlobalSmokeTest, AfterCtorGetGlobalHasDefaultLevel) +{ + auto globalLogTag = m_logTagManager.get(m_globalTagName); + ASSERT_NE(globalLogTag, nullptr); + EXPECT_EQ(globalLogTag->level, constTestLevelBegin); +} + +TEST_F(LogTagManagerGlobalSmokeTest, AfterCtorCanSetGlobalByFullName) +{ + m_logTagManager.setLevelByFullName(m_globalTagName, constTestLevelChanged); + auto globalLogTag = m_logTagManager.get(m_globalTagName); + ASSERT_NE(globalLogTag, nullptr); + EXPECT_EQ(globalLogTag->level, constTestLevelChanged); +} + +#if 0 +// "global" level is not supposed to be settable by name-parts. +// Therefore this test code is supposed to fail. +TEST_F(LogTagManagerGlobalSmokeTest, DISABLED_AfterCtorCanSetGlobalByFirstPart) +{ + m_logTagManager.setLevelByFirstPart(m_globalTagName, constTestLevelChanged); + auto globalLogTag = m_logTagManager.get(m_globalTagName); + ASSERT_NE(globalLogTag, nullptr); + EXPECT_EQ(globalLogTag->level, constTestLevelChanged); +} +#endif + +#if 0 +// "global" level is not supposed to be settable by name-parts. +// Therefore this test code is supposed to fail. +TEST_F(LogTagManagerGlobalSmokeTest, DISABLED_AfterCtorCanSetGlobalByAnyPart) +{ + m_logTagManager.setLevelByAnyPart(m_globalTagName, constTestLevelChanged); + auto globalLogTag = m_logTagManager.get(m_globalTagName); + ASSERT_NE(globalLogTag, nullptr); + EXPECT_EQ(globalLogTag->level, constTestLevelChanged); +} +#endif + +// LogTagManagerNonGlobalSmokeTest performs basic smoke tests to verify that +// a log tag (that is not the "global" log tag) can be assigned, and its +// log level can be configured. +class LogTagManagerNonGlobalSmokeTest + : public LogTagManagerTestFixture +{ +protected: + LogTag m_something; + +protected: + static constexpr const char* m_somethingTagName = "something"; + +public: + LogTagManagerNonGlobalSmokeTest() + : LogTagManagerTestFixture(constTestLevelAltBegin) + , m_something(m_somethingTagName, constTestLevelBegin) + { + } +}; + +TEST_F(LogTagManagerNonGlobalSmokeTest, CanAssignTagAndThenGet) +{ + m_logTagManager.assign(m_something.name, &m_something); + ASSERT_EQ(m_logTagManager.get(m_somethingTagName), &m_something); +} + +TEST_F(LogTagManagerNonGlobalSmokeTest, CanAssignTagAndThenGetAndShouldHaveDefaultLevel) +{ + m_logTagManager.assign(m_something.name, &m_something); + ASSERT_EQ(m_logTagManager.get(m_somethingTagName), &m_something); + EXPECT_EQ(m_logTagManager.get(m_somethingTagName)->level, constTestLevelBegin); +} + +TEST_F(LogTagManagerNonGlobalSmokeTest, CanSetLevelByFullNameBeforeAssignTag) +{ + m_logTagManager.setLevelByFullName(m_somethingTagName, constTestLevelChanged); + m_logTagManager.assign(m_something.name, &m_something); + ASSERT_EQ(m_logTagManager.get(m_somethingTagName), &m_something); + EXPECT_EQ(m_logTagManager.get(m_somethingTagName)->level, constTestLevelChanged); + EXPECT_NE(m_logTagManager.get(m_somethingTagName)->level, constTestLevelBegin) << "Should not be left unchanged (default value)"; +} + +TEST_F(LogTagManagerNonGlobalSmokeTest, CanSetLevelByFirstPartBeforeAssignTag) +{ + m_logTagManager.setLevelByFirstPart(m_somethingTagName, constTestLevelChanged); + m_logTagManager.assign(m_something.name, &m_something); + ASSERT_EQ(m_logTagManager.get(m_somethingTagName), &m_something); + EXPECT_EQ(m_logTagManager.get(m_somethingTagName)->level, constTestLevelChanged); + EXPECT_NE(m_logTagManager.get(m_somethingTagName)->level, constTestLevelBegin) << "Should not be left unchanged (default value)"; +} + +TEST_F(LogTagManagerNonGlobalSmokeTest, CanSetLevelByAnyPartBeforeAssignTag) +{ + m_logTagManager.setLevelByAnyPart(m_somethingTagName, constTestLevelChanged); + m_logTagManager.assign(m_something.name, &m_something); + ASSERT_EQ(m_logTagManager.get(m_somethingTagName), &m_something); + EXPECT_EQ(m_logTagManager.get(m_somethingTagName)->level, constTestLevelChanged); + EXPECT_NE(m_logTagManager.get(m_somethingTagName)->level, constTestLevelBegin) << "Should not be left unchanged (default value)"; +} + +TEST_F(LogTagManagerNonGlobalSmokeTest, CanSetLevelByFullNameAfterAssignTag) +{ + m_logTagManager.assign(m_something.name, &m_something); + m_logTagManager.setLevelByFullName(m_somethingTagName, constTestLevelChanged); + ASSERT_EQ(m_logTagManager.get(m_somethingTagName), &m_something); + EXPECT_EQ(m_logTagManager.get(m_somethingTagName)->level, constTestLevelChanged); + EXPECT_NE(m_logTagManager.get(m_somethingTagName)->level, constTestLevelBegin) << "Should not be left unchanged (default value)"; +} + +TEST_F(LogTagManagerNonGlobalSmokeTest, CanSetLevelByFirstPartAfterAssignTag) +{ + m_logTagManager.assign(m_something.name, &m_something); + m_logTagManager.setLevelByFirstPart(m_somethingTagName, constTestLevelChanged); + ASSERT_EQ(m_logTagManager.get(m_somethingTagName), &m_something); + EXPECT_EQ(m_logTagManager.get(m_somethingTagName)->level, constTestLevelChanged); + EXPECT_NE(m_logTagManager.get(m_somethingTagName)->level, constTestLevelBegin) << "Should not be left unchanged (default value)"; +} + +TEST_F(LogTagManagerNonGlobalSmokeTest, CanSetLevelByAnyPartAfterAssignTag) +{ + m_logTagManager.assign(m_something.name, &m_something); + m_logTagManager.setLevelByAnyPart(m_somethingTagName, constTestLevelChanged); + ASSERT_EQ(m_logTagManager.get(m_somethingTagName), &m_something); + EXPECT_EQ(m_logTagManager.get(m_somethingTagName)->level, constTestLevelChanged); + EXPECT_NE(m_logTagManager.get(m_somethingTagName)->level, constTestLevelBegin) << "Should not be left unchanged (default value)"; +} + +// Non-confusion tests use two or more (non-global) log tags with chosen names to verify +// that LogTagManager does not accidentally apply log levels to log tags other than the +// ones intended. + + +// LogTagManagerSubstrNonConfusionSmokeTest are non-confusion tests that focus on +// substrings. Keep in mind string matching in LogTagManager must be aligned to +// the period delimiter; substrings are not considered for matching. +class LogTagManagerSubstrNonConfusionFixture + : public LogTagManagerTestFixture + , public ::testing::WithParamInterface> +{ +public: + struct MyTestParam + { + const SubstringType detractorNameSelector; + const bool initTargetTagWithCall; + const Timing whenToAssignDetractorTag; + const ByWhat howToSetDetractorLevel; + MyTestParam(const std::tuple& args) + : detractorNameSelector(std::get<0u>(args)) + , initTargetTagWithCall(std::get<1u>(args)) + , whenToAssignDetractorTag(std::get<2u>(args)) + , howToSetDetractorLevel(std::get<3u>(args)) + { + } + }; + + // The following substrings are all derived from "soursop". + // In particular, "ours" is a substring of "soursop". +protected: + LogTag tagSour; + LogTag tagSop; + LogTag tagSoursop; + LogTag tagSourDotSop; + LogTag tagOurs; + +protected: + static constexpr const char* strSour = "sour"; + static constexpr const char* strSop = "sop"; + static constexpr const char* strSoursop = "soursop"; + static constexpr const char* strSourDotSop = "sour.sop"; + static constexpr const char* strOurs = "ours"; + +public: + LogTagManagerSubstrNonConfusionFixture() + : LogTagManagerTestFixture(constTestLevelAltBegin) + , tagSour(strSour, constTestLevelBegin) + , tagSop(strSop, constTestLevelBegin) + , tagSoursop(strSoursop, constTestLevelBegin) + , tagSourDotSop(strSourDotSop, constTestLevelBegin) + , tagOurs(strOurs, constTestLevelBegin) + { + } + + const char* selectDetractorName(const MyTestParam& myTestParam) + { + switch (myTestParam.detractorNameSelector) + { + case SubstringType::Prefix: + return strSour; + case SubstringType::Midstring: + return strOurs; + case SubstringType::Suffix: + return strSop; + case SubstringType::Straddle: + return strSourDotSop; + default: + throw std::logic_error("LogTagManagerSubstrNonConfusionTest::selectDetractorName"); + } + } + + LogTag& selectDetractorTag(const MyTestParam& myTestParam) + { + switch (myTestParam.detractorNameSelector) + { + case SubstringType::Prefix: + return tagSour; + case SubstringType::Midstring: + return tagOurs; + case SubstringType::Suffix: + return tagSop; + case SubstringType::Straddle: + return tagSourDotSop; + default: + throw std::logic_error("LogTagManagerSubstrNonConfusionTest::selectDetractorName"); + } + } +}; + +INSTANTIATE_TEST_CASE_P( + LogTagManagerSubstrNonConfusionTest, + LogTagManagerSubstrNonConfusionFixture, + ::testing::Combine( + ::testing::Values(SubstringType::Prefix, SubstringType::Midstring, SubstringType::Suffix, SubstringType::Straddle), + ::testing::Values(false, true), + ::testing::Values(Timing::Never, Timing::Before, Timing::After), + ::testing::Values(ByWhat::ByFullName, ByWhat::ByFirstPart, ByWhat::ByAnyPart) + ) +); + +TEST_P(LogTagManagerSubstrNonConfusionFixture, ParameterizedTestFunc) +{ + const auto myTestParam = MyTestParam(GetParam()); + const char* detractorName = selectDetractorName(myTestParam); + LogTag& detractorTag = selectDetractorTag(myTestParam); + // Target tag is assigned + m_logTagManager.assign(tagSoursop.name, &tagSoursop); + // If detractor tag is to be assigned "before" + if (myTestParam.whenToAssignDetractorTag == Timing::Before) + { + m_logTagManager.assign(detractorName, &detractorTag); + } + // Initialize target tag to constTestLevelChanged + if (myTestParam.initTargetTagWithCall) + { + m_logTagManager.setLevelByFullName(strSoursop, constTestLevelChanged); + } + else + { + tagSoursop.level = constTestLevelChanged; + } + // If detractor tag is to be assigned "after" + if (myTestParam.whenToAssignDetractorTag == Timing::After) + { + m_logTagManager.assign(detractorName, &detractorTag); + } + // Set the log level using the detractor name + switch (myTestParam.howToSetDetractorLevel) + { + case ByWhat::ByFullName: + m_logTagManager.setLevelByFullName(detractorName, constTestLevelAltChanged); + break; + case ByWhat::ByFirstPart: + m_logTagManager.setLevelByFirstPart(detractorName, constTestLevelAltChanged); + break; + case ByWhat::ByAnyPart: + m_logTagManager.setLevelByAnyPart(detractorName, constTestLevelAltChanged); + break; + default: + FAIL() << "Invalid parameterized test value, check test case."; + } + // Verifies that the target tag is not disturbed by changes made using detractor name + ASSERT_EQ(m_logTagManager.get(strSoursop), &tagSoursop); + EXPECT_EQ(tagSoursop.level, constTestLevelChanged); + EXPECT_NE(tagSoursop.level, constTestLevelAltChanged) << "Should not be changed unless confusion bug exists"; +} + +// LogTagManagerNamePartNonConfusionFixture are non-confusion tests that assumes +// no substring confusions are happening, and proceed to test matching by name parts. +// In particular, setLevelByFirstPart() and setLevelByAnyPart() are the focus of these tests. +class LogTagManagerNamePartNonConfusionFixture + : public LogTagManagerTestFixture + , public ::testing::WithParamInterface> +{ +public: + struct MyTestParam + { + // Config tag name can only specify either full name or exactly one name part. + // When specifying exactly one name part, there is a choice of matching the + // first name part of a tag, or matching any name part that appears in a tag + // name regardless of its position. + const int configTagIndex; + const ByWhat configMatchBy; + const int targetTagIndex; + const Timing whenToAssignTargetTag; + const Timing whenToAssignConfigTag; + MyTestParam(const std::tuple& args) + : configTagIndex(std::get<0u>(args)) + , configMatchBy(std::get<1u>(args)) + , targetTagIndex(std::get<2u>(args)) + , whenToAssignTargetTag(std::get<3u>(args)) + , whenToAssignConfigTag(std::get<4u>(args)) + { + } + }; + +protected: + LogTag m_apple; + LogTag m_banana; + LogTag m_coconut; + LogTag m_orange; + LogTag m_pineapple; + LogTag m_bananaDotOrange; + LogTag m_bananaDotPineapple; + LogTag m_coconutDotPineapple; + +protected: + static constexpr const char* strApple = "apple"; + static constexpr const char* strBanana = "banana"; + static constexpr const char* strCoconut = "coconut"; + static constexpr const char* strOrange = "orange"; + static constexpr const char* strPineapple = "pineapple"; + static constexpr const char* strBananaDotOrange = "banana.orange"; + static constexpr const char* strBananaDotPineapple = "banana.pineapple"; + static constexpr const char* strCoconutDotPineapple = "coconut.pineapple"; + +public: + LogTagManagerNamePartNonConfusionFixture() + : LogTagManagerTestFixture(constTestLevelAltBegin) + , m_apple(strApple, constTestLevelBegin) + , m_banana(strBanana, constTestLevelBegin) + , m_coconut(strCoconut, constTestLevelBegin) + , m_orange(strOrange, constTestLevelBegin) + , m_pineapple(strPineapple, constTestLevelBegin) + , m_bananaDotOrange(strBananaDotOrange, constTestLevelBegin) + , m_bananaDotPineapple(strBananaDotPineapple, constTestLevelBegin) + , m_coconutDotPineapple(strCoconutDotPineapple, constTestLevelBegin) + { + } + +protected: + LogTag* getLogTagByIndex(int index) + { + switch (index) + { + case 0: + return &m_apple; + case 1: + return &m_banana; + case 2: + return &m_coconut; + case 3: + return &m_orange; + case 4: + return &m_pineapple; + case 5: + return &m_bananaDotOrange; + case 6: + return &m_bananaDotPineapple; + case 7: + return &m_coconutDotPineapple; + default: + ADD_FAILURE() << "Invalid parameterized test value, check test case. " + << "Function LogTagManagerNamePartNonConfusionFixture::getLogTagByIndex."; + return nullptr; + } + } + + // findTabulatedExpectedResult returns the hard-coded expected results for parameterized + // test cases. The tables need updated if the index, name, or ordering of test tags are + // changed. + bool findTabulatedExpectedResult(const MyTestParam& myTestParam) const + { + // expectedResultUsingFirstPart: + // Each row ("config") specifies the tag name specifier used to call setLevelByFirstPart(). + // Each column ("target") specifies whether an actual tag with the "target" + // name would have its log level changed because of the call to setLevelByFirstPart(). + static constexpr const bool expectedResultUsingFirstPart[5][8] = + { + /*byFirstPart(apple)*/ { true, false, false, false, false, false, false, false }, + /*byFirstPart(banana)*/ { false, true, false, false, false, true, true, false }, + /*byFirstPart(coconut)*/ { false, false, true, false, false, false, false, true }, + /*byFirstPart(orange)*/ { false, false, false, true, false, false, false, false }, + /*byFirstPart(pineapple)*/ { false, false, false, false, true, false, false, false }, + }; + + // expectedResultUsingAnyPart: + // Each row ("config") specifies the tag name specifier used to call setLevelByAnyPart(). + // Each column ("target") specifies whether an actual tag with the "target" + // name would have its log level changed because of the call to setLevelByAnyPart(). + static constexpr const bool expectedResultUsingAnyPart[5][8] = + { + /*byAnyPart(apple)*/ { true, false, false, false, false, false, false, false }, + /*byAnyPart(banana)*/ { false, true, false, false, false, true, true, false }, + /*byAnyPart(coconut)*/ { false, false, true, false, false, false, false, true }, + /*byAnyPart(orange)*/ { false, false, false, true, false, true, false, false }, + /*byAnyPart(pineapple)*/ { false, false, false, false, true, false, true, true }, + }; + + switch (myTestParam.configMatchBy) + { + case ByWhat::ByFirstPart: + return expectedResultUsingFirstPart[myTestParam.configTagIndex][myTestParam.targetTagIndex]; + case ByWhat::ByAnyPart: + return expectedResultUsingAnyPart[myTestParam.configTagIndex][myTestParam.targetTagIndex]; + default: + ADD_FAILURE() << "Invalid parameterized test value, check test case. " + << "Function LogTagManagerNamePartNonConfusionFixture::getLogTagByIndex."; + return false; + } + } +}; + +INSTANTIATE_TEST_CASE_P( + LogTagManagerNamePartNonConfusionTest, + LogTagManagerNamePartNonConfusionFixture, + ::testing::Combine( + ::testing::Values(0, 1, 2, 3, 4), + ::testing::Values(ByWhat::ByFirstPart, ByWhat::ByAnyPart), + ::testing::Values(0, 1, 2, 3, 4, 5, 6, 7), + ::testing::Values(Timing::Before, Timing::After), + ::testing::Values(Timing::Before, Timing::After, Timing::Never) + ) +); + +TEST_P(LogTagManagerNamePartNonConfusionFixture, NamePartTestFunc) +{ + const auto myTestParam = MyTestParam(GetParam()); + LogTag* configTag = getLogTagByIndex(myTestParam.configTagIndex); + LogTag* targetTag = getLogTagByIndex(myTestParam.targetTagIndex); + ASSERT_NE(configTag, nullptr) << "Invalid parameterized test value, check value of myTestParam.configTagIndex."; + ASSERT_NE(targetTag, nullptr) << "Invalid parameterized test value, check value of myTestParam.targetTagIndex."; + if (myTestParam.whenToAssignConfigTag == Timing::Before) + { + m_logTagManager.assign(configTag->name, configTag); + } + if (myTestParam.whenToAssignTargetTag == Timing::Before) + { + m_logTagManager.assign(targetTag->name, targetTag); + } + switch (myTestParam.configMatchBy) + { + case ByWhat::ByFirstPart: + m_logTagManager.setLevelByFirstPart(configTag->name, constTestLevelChanged); + break; + case ByWhat::ByAnyPart: + m_logTagManager.setLevelByAnyPart(configTag->name, constTestLevelChanged); + break; + default: + FAIL() << "Invalid parameterized test value, check test case. " + << "Fixture LogTagManagerNamePartNonConfusionFixture, Case NamePartTestFunc."; + } + if (myTestParam.whenToAssignConfigTag == Timing::After) + { + m_logTagManager.assign(configTag->name, configTag); + } + if (myTestParam.whenToAssignTargetTag == Timing::After) + { + m_logTagManager.assign(targetTag->name, targetTag); + } + // Verifies the registration of the log tag pointer. If fail, cannot proceed + // because it is not certain whether the returned pointer is valid to dereference + ASSERT_EQ(m_logTagManager.get(targetTag->name), targetTag); + // Verifies the log level of target tag + const bool isChangeExpected = findTabulatedExpectedResult(myTestParam); + if (isChangeExpected) + { + EXPECT_EQ(targetTag->level, constTestLevelChanged); + EXPECT_NE(targetTag->level, constTestLevelBegin); + } + else + { + EXPECT_EQ(targetTag->level, constTestLevelBegin); + EXPECT_NE(targetTag->level, constTestLevelChanged); + } +} + +}} // namespace