diff --git a/changelog/5311.feature.rst b/changelog/5311.feature.rst new file mode 100644 index 00000000000..eec46508332 --- /dev/null +++ b/changelog/5311.feature.rst @@ -0,0 +1,3 @@ +Captured logs that are output for each failing test are formatted using the +ColoredLevelFromatter. As a consequence caplog.text contains the ANSI +escape sequences used for coloring the level names now. diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 08670d2b2cc..cb59d36daa3 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -17,6 +17,11 @@ DEFAULT_LOG_FORMAT = "%(levelname)-8s %(name)s:%(filename)s:%(lineno)d %(message)s" DEFAULT_LOG_DATE_FORMAT = "%H:%M:%S" +_ANSI_ESCAPE_SEQ = re.compile(r"\x1b\[[\d;]+m") + + +def _remove_ansi_escape_sequences(text): + return _ANSI_ESCAPE_SEQ.sub("", text) class ColoredLevelFormatter(logging.Formatter): @@ -256,8 +261,8 @@ def get_records(self, when): @property def text(self): - """Returns the log text.""" - return self.handler.stream.getvalue() + """Returns the formatted log text.""" + return _remove_ansi_escape_sequences(self.handler.stream.getvalue()) @property def records(self): @@ -393,7 +398,7 @@ def __init__(self, config): config.option.verbose = 1 self.print_logs = get_option_ini(config, "log_print") - self.formatter = logging.Formatter( + self.formatter = self._create_formatter( get_option_ini(config, "log_format"), get_option_ini(config, "log_date_format"), ) @@ -427,6 +432,19 @@ def __init__(self, config): if self._log_cli_enabled(): self._setup_cli_logging() + def _create_formatter(self, log_format, log_date_format): + # color option doesn't exist if terminal plugin is disabled + color = getattr(self._config.option, "color", "no") + if color != "no" and ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search( + log_format + ): + formatter = ColoredLevelFormatter( + create_terminal_writer(self._config), log_format, log_date_format + ) + else: + formatter = logging.Formatter(log_format, log_date_format) + return formatter + def _setup_cli_logging(self): config = self._config terminal_reporter = config.pluginmanager.get_plugin("terminalreporter") @@ -437,23 +455,12 @@ def _setup_cli_logging(self): capture_manager = config.pluginmanager.get_plugin("capturemanager") # if capturemanager plugin is disabled, live logging still works. log_cli_handler = _LiveLoggingStreamHandler(terminal_reporter, capture_manager) - log_cli_format = get_option_ini(config, "log_cli_format", "log_format") - log_cli_date_format = get_option_ini( - config, "log_cli_date_format", "log_date_format" + + log_cli_formatter = self._create_formatter( + get_option_ini(config, "log_cli_format", "log_format"), + get_option_ini(config, "log_cli_date_format", "log_date_format"), ) - if ( - config.option.color != "no" - and ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search(log_cli_format) - ): - log_cli_formatter = ColoredLevelFormatter( - create_terminal_writer(config), - log_cli_format, - datefmt=log_cli_date_format, - ) - else: - log_cli_formatter = logging.Formatter( - log_cli_format, datefmt=log_cli_date_format - ) + log_cli_level = get_actual_log_level(config, "log_cli_level", "log_level") self.log_cli_handler = log_cli_handler self.live_logs_context = lambda: catching_logs( diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index 77cf71b435d..e7a3a80ebf1 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -1084,3 +1084,48 @@ def test_second(): with open(os.path.join(report_dir_base, "test_second"), "r") as rfh: content = rfh.read() assert "message from test 2" in content + + +def test_colored_captured_log(testdir): + """ + Test that the level names of captured log messages of a failing test are + colored. + """ + testdir.makepyfile( + """ + import logging + + logger = logging.getLogger(__name__) + + def test_foo(): + logger.info('text going to logger from call') + assert False + """ + ) + result = testdir.runpytest("--log-level=INFO", "--color=yes") + assert result.ret == 1 + result.stdout.fnmatch_lines( + [ + "*-- Captured log call --*", + "\x1b[32mINFO \x1b[0m*text going to logger from call", + ] + ) + + +def test_colored_ansi_esc_caplogtext(testdir): + """ + Make sure that caplog.text does not contain ANSI escape sequences. + """ + testdir.makepyfile( + """ + import logging + + logger = logging.getLogger(__name__) + + def test_foo(caplog): + logger.info('text going to logger from call') + assert '\x1b' not in caplog.text + """ + ) + result = testdir.runpytest("--log-level=INFO", "--color=yes") + assert result.ret == 0