Skip to content

Commit 74f20f9

Browse files
authored
Merge pull request #7434 from bluetech/distinct_log_handlers-rebased
logging: use unique handlers for caplog and reports
2 parents e6e300e + 304f291 commit 74f20f9

File tree

3 files changed

+70
-21
lines changed

3 files changed

+70
-21
lines changed

changelog/7159.improvement.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
When the ``caplog`` fixture is used to change the log level for capturing,
2+
using ``caplog.set_level()`` or ``caplog.at_level()``, it no longer affects
3+
the level of logs that are shown in the "Captured log report" report section.

src/_pytest/logging.py

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@
3434
DEFAULT_LOG_FORMAT = "%(levelname)-8s %(name)s:%(filename)s:%(lineno)d %(message)s"
3535
DEFAULT_LOG_DATE_FORMAT = "%H:%M:%S"
3636
_ANSI_ESCAPE_SEQ = re.compile(r"\x1b\[[\d;]+m")
37-
catch_log_handler_key = StoreKey["LogCaptureHandler"]()
38-
catch_log_records_key = StoreKey[Dict[str, List[logging.LogRecord]]]()
37+
caplog_handler_key = StoreKey["LogCaptureHandler"]()
38+
caplog_records_key = StoreKey[Dict[str, List[logging.LogRecord]]]()
3939

4040

4141
def _remove_ansi_escape_sequences(text: str) -> str:
@@ -362,7 +362,7 @@ def handler(self) -> LogCaptureHandler:
362362
"""
363363
:rtype: LogCaptureHandler
364364
"""
365-
return self._item._store[catch_log_handler_key]
365+
return self._item._store[caplog_handler_key]
366366

367367
def get_records(self, when: str) -> List[logging.LogRecord]:
368368
"""
@@ -376,7 +376,7 @@ def get_records(self, when: str) -> List[logging.LogRecord]:
376376
377377
.. versionadded:: 3.4
378378
"""
379-
return self._item._store[catch_log_records_key].get(when, [])
379+
return self._item._store[caplog_records_key].get(when, [])
380380

381381
@property
382382
def text(self) -> str:
@@ -523,8 +523,10 @@ def __init__(self, config: Config) -> None:
523523
get_option_ini(config, "log_auto_indent"),
524524
)
525525
self.log_level = get_log_level_for_setting(config, "log_level")
526-
self.log_handler = LogCaptureHandler()
527-
self.log_handler.setFormatter(self.formatter)
526+
self.caplog_handler = LogCaptureHandler()
527+
self.caplog_handler.setFormatter(self.formatter)
528+
self.report_handler = LogCaptureHandler()
529+
self.report_handler.setFormatter(self.formatter)
528530

529531
# File logging.
530532
self.log_file_level = get_log_level_for_setting(config, "log_file_level")
@@ -665,22 +667,27 @@ def pytest_runtest_logreport(self) -> None:
665667

666668
def _runtest_for(self, item: nodes.Item, when: str) -> Generator[None, None, None]:
667669
"""Implements the internals of pytest_runtest_xxx() hook."""
668-
with catching_logs(self.log_handler, level=self.log_level) as log_handler:
669-
log_handler.reset()
670-
item._store[catch_log_records_key][when] = log_handler.records
671-
item._store[catch_log_handler_key] = log_handler
670+
with catching_logs(
671+
self.caplog_handler, level=self.log_level,
672+
) as caplog_handler, catching_logs(
673+
self.report_handler, level=self.log_level,
674+
) as report_handler:
675+
caplog_handler.reset()
676+
report_handler.reset()
677+
item._store[caplog_records_key][when] = caplog_handler.records
678+
item._store[caplog_handler_key] = caplog_handler
672679

673680
yield
674681

675-
log = log_handler.stream.getvalue().strip()
682+
log = report_handler.stream.getvalue().strip()
676683
item.add_report_section(when, "log", log)
677684

678685
@pytest.hookimpl(hookwrapper=True)
679686
def pytest_runtest_setup(self, item: nodes.Item) -> Generator[None, None, None]:
680687
self.log_cli_handler.set_when("setup")
681688

682689
empty = {} # type: Dict[str, List[logging.LogRecord]]
683-
item._store[catch_log_records_key] = empty
690+
item._store[caplog_records_key] = empty
684691
yield from self._runtest_for(item, "setup")
685692

686693
@pytest.hookimpl(hookwrapper=True)
@@ -694,8 +701,8 @@ def pytest_runtest_teardown(self, item: nodes.Item) -> Generator[None, None, Non
694701
self.log_cli_handler.set_when("teardown")
695702

696703
yield from self._runtest_for(item, "teardown")
697-
del item._store[catch_log_records_key]
698-
del item._store[catch_log_handler_key]
704+
del item._store[caplog_records_key]
705+
del item._store[caplog_handler_key]
699706

700707
@pytest.hookimpl
701708
def pytest_runtest_logfinish(self) -> None:

testing/logging/test_fixture.py

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import logging
22

33
import pytest
4-
from _pytest.logging import catch_log_records_key
4+
from _pytest.logging import caplog_records_key
55

66
logger = logging.getLogger(__name__)
77
sublogger = logging.getLogger(__name__ + ".baz")
@@ -137,7 +137,7 @@ def test_caplog_captures_for_all_stages(caplog, logging_during_setup_and_teardow
137137
assert [x.message for x in caplog.get_records("setup")] == ["a_setup_log"]
138138

139139
# This reaches into private API, don't use this type of thing in real tests!
140-
assert set(caplog._item._store[catch_log_records_key]) == {"setup", "call"}
140+
assert set(caplog._item._store[caplog_records_key]) == {"setup", "call"}
141141

142142

143143
def test_ini_controls_global_log_level(testdir):
@@ -216,12 +216,10 @@ def test_log_level_override(request, caplog):
216216
plugin = request.config.pluginmanager.getplugin('logging-plugin')
217217
assert plugin.log_level == logging.WARNING
218218
219-
logger.info("INFO message won't be shown")
220-
221-
caplog.set_level(logging.INFO, logger.name)
219+
logger.error("ERROR message " + "will be shown")
222220
223221
with caplog.at_level(logging.DEBUG, logger.name):
224-
logger.debug("DEBUG message will be shown")
222+
logger.debug("DEBUG message " + "won't be shown")
225223
raise Exception()
226224
"""
227225
)
@@ -233,5 +231,46 @@ def test_log_level_override(request, caplog):
233231
)
234232

235233
result = testdir.runpytest()
236-
result.stdout.fnmatch_lines(["*DEBUG message will be shown*"])
234+
result.stdout.fnmatch_lines(["*ERROR message will be shown*"])
235+
result.stdout.no_fnmatch_line("*DEBUG message won't be shown*")
236+
assert result.ret == 1
237+
238+
239+
def test_log_report_captures_according_to_config_option_upon_failure(testdir):
240+
""" Test that upon failure:
241+
(1) `caplog` succeeded to capture the DEBUG message and assert on it => No `Exception` is raised
242+
(2) The `DEBUG` message does NOT appear in the `Captured log call` report
243+
(3) The stdout, `INFO`, and `WARNING` messages DO appear in the test reports due to `--log-level=INFO`
244+
"""
245+
testdir.makepyfile(
246+
"""
247+
import pytest
248+
import logging
249+
250+
def function_that_logs():
251+
logging.debug('DEBUG log ' + 'message')
252+
logging.info('INFO log ' + 'message')
253+
logging.warning('WARNING log ' + 'message')
254+
print('Print ' + 'message')
255+
256+
def test_that_fails(request, caplog):
257+
plugin = request.config.pluginmanager.getplugin('logging-plugin')
258+
assert plugin.log_level == logging.INFO
259+
260+
with caplog.at_level(logging.DEBUG):
261+
function_that_logs()
262+
263+
if 'DEBUG log ' + 'message' not in caplog.text:
264+
raise Exception('caplog failed to ' + 'capture DEBUG')
265+
266+
assert False
267+
"""
268+
)
269+
270+
result = testdir.runpytest("--log-level=INFO")
271+
result.stdout.no_fnmatch_line("*Exception: caplog failed to capture DEBUG*")
272+
result.stdout.no_fnmatch_line("*DEBUG log message*")
273+
result.stdout.fnmatch_lines(
274+
["*Print message*", "*INFO log message*", "*WARNING log message*"]
275+
)
237276
assert result.ret == 1

0 commit comments

Comments
 (0)