Skip to content

Commit 919ac22

Browse files
authored
Merge pull request #7231 from bluetech/logging-error
logging: propagate errors during log message emits
2 parents c98bc4c + b13fcb2 commit 919ac22

File tree

3 files changed

+83
-3
lines changed

3 files changed

+83
-3
lines changed

changelog/6433.feature.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
If an error is encountered while formatting the message in a logging call, for
2+
example ``logging.warning("oh no!: %s: %s", "first")`` (a second argument is
3+
missing), pytest now propagates the error, likely causing the test to fail.
4+
5+
Previously, such a mistake would cause an error to be printed to stderr, which
6+
is not displayed by default for passing tests. This change makes the mistake
7+
visible during testing.
8+
9+
You may supress this behavior temporarily or permanently by setting
10+
``logging.raiseExceptions = False``.

src/_pytest/logging.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,14 @@ def reset(self) -> None:
312312
self.records = []
313313
self.stream = StringIO()
314314

315+
def handleError(self, record: logging.LogRecord) -> None:
316+
if logging.raiseExceptions:
317+
# Fail the test if the log message is bad (emit failed).
318+
# The default behavior of logging is to print "Logging error"
319+
# to stderr with the call stack and some extra details.
320+
# pytest wants to make such mistakes visible during testing.
321+
raise
322+
315323

316324
class LogCaptureFixture:
317325
"""Provides access and control of log capturing."""
@@ -499,9 +507,7 @@ def __init__(self, config: Config) -> None:
499507
# File logging.
500508
self.log_file_level = get_log_level_for_setting(config, "log_file_level")
501509
log_file = get_option_ini(config, "log_file") or os.devnull
502-
self.log_file_handler = logging.FileHandler(
503-
log_file, mode="w", encoding="UTF-8"
504-
)
510+
self.log_file_handler = _FileHandler(log_file, mode="w", encoding="UTF-8")
505511
log_file_format = get_option_ini(config, "log_file_format", "log_format")
506512
log_file_date_format = get_option_ini(
507513
config, "log_file_date_format", "log_date_format"
@@ -687,6 +693,16 @@ def pytest_unconfigure(self):
687693
self.log_file_handler.close()
688694

689695

696+
class _FileHandler(logging.FileHandler):
697+
"""
698+
Custom FileHandler with pytest tweaks.
699+
"""
700+
701+
def handleError(self, record: logging.LogRecord) -> None:
702+
# Handled by LogCaptureHandler.
703+
pass
704+
705+
690706
class _LiveLoggingStreamHandler(logging.StreamHandler):
691707
"""
692708
Custom StreamHandler used by the live logging feature: it will write a newline before the first log message
@@ -737,6 +753,10 @@ def emit(self, record):
737753
self._section_name_shown = True
738754
super().emit(record)
739755

756+
def handleError(self, record: logging.LogRecord) -> None:
757+
# Handled by LogCaptureHandler.
758+
pass
759+
740760

741761
class _LiveLoggingNullHandler(logging.NullHandler):
742762
"""A handler used when live logging is disabled."""
@@ -746,3 +766,7 @@ def reset(self):
746766

747767
def set_when(self, when):
748768
pass
769+
770+
def handleError(self, record: logging.LogRecord) -> None:
771+
# Handled by LogCaptureHandler.
772+
pass

testing/logging/test_reporting.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import re
44

55
import pytest
6+
from _pytest.pytester import Testdir
67

78

89
def test_nothing_logged(testdir):
@@ -1101,3 +1102,48 @@ def test_foo(caplog):
11011102
)
11021103
result = testdir.runpytest("--log-level=INFO", "--color=yes")
11031104
assert result.ret == 0
1105+
1106+
1107+
def test_logging_emit_error(testdir: Testdir) -> None:
1108+
"""
1109+
An exception raised during emit() should fail the test.
1110+
1111+
The default behavior of logging is to print "Logging error"
1112+
to stderr with the call stack and some extra details.
1113+
1114+
pytest overrides this behavior to propagate the exception.
1115+
"""
1116+
testdir.makepyfile(
1117+
"""
1118+
import logging
1119+
1120+
def test_bad_log():
1121+
logging.warning('oops', 'first', 2)
1122+
"""
1123+
)
1124+
result = testdir.runpytest()
1125+
result.assert_outcomes(failed=1)
1126+
result.stdout.fnmatch_lines(
1127+
[
1128+
"====* FAILURES *====",
1129+
"*not all arguments converted during string formatting*",
1130+
]
1131+
)
1132+
1133+
1134+
def test_logging_emit_error_supressed(testdir: Testdir) -> None:
1135+
"""
1136+
If logging is configured to silently ignore errors, pytest
1137+
doesn't propagate errors either.
1138+
"""
1139+
testdir.makepyfile(
1140+
"""
1141+
import logging
1142+
1143+
def test_bad_log(monkeypatch):
1144+
monkeypatch.setattr(logging, 'raiseExceptions', False)
1145+
logging.warning('oops', 'first', 2)
1146+
"""
1147+
)
1148+
result = testdir.runpytest()
1149+
result.assert_outcomes(passed=1)

0 commit comments

Comments
 (0)