Skip to content

Commit 6b34d7b

Browse files
kitchoiRémi Lapeyre
and
Rémi Lapeyre
authored
bpo-39385: Add an assertNoLogs context manager to unittest.TestCase (GH-18067)
Co-authored-by: Rémi Lapeyre <[email protected]>
1 parent 5d5c84e commit 6b34d7b

File tree

5 files changed

+131
-8
lines changed

5 files changed

+131
-8
lines changed

Doc/library/unittest.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -950,6 +950,9 @@ Test cases
950950
| :meth:`assertLogs(logger, level) | The ``with`` block logs on *logger* | 3.4 |
951951
| <TestCase.assertLogs>` | with minimum *level* | |
952952
+---------------------------------------------------------+--------------------------------------+------------+
953+
| :meth:`assertNoLogs(logger, level) | The ``with`` block does not log on | 3.10 |
954+
| <TestCase.assertNoLogs>` | *logger* with minimum *level* | |
955+
+---------------------------------------------------------+--------------------------------------+------------+
953956

954957
.. method:: assertRaises(exception, callable, *args, **kwds)
955958
assertRaises(exception, *, msg=None)
@@ -1121,6 +1124,24 @@ Test cases
11211124

11221125
.. versionadded:: 3.4
11231126

1127+
.. method:: assertNoLogs(logger=None, level=None)
1128+
1129+
A context manager to test that no messages are logged on
1130+
the *logger* or one of its children, with at least the given
1131+
*level*.
1132+
1133+
If given, *logger* should be a :class:`logging.Logger` object or a
1134+
:class:`str` giving the name of a logger. The default is the root
1135+
logger, which will catch all messages.
1136+
1137+
If given, *level* should be either a numeric logging level or
1138+
its string equivalent (for example either ``"ERROR"`` or
1139+
:attr:`logging.ERROR`). The default is :attr:`logging.INFO`.
1140+
1141+
Unlike :meth:`assertLogs`, nothing will be returned by the context
1142+
manager.
1143+
1144+
.. versionadded:: 3.10
11241145

11251146
There are also other methods used to perform more specific checks, such as:
11261147

Lib/unittest/_log.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,19 @@ def emit(self, record):
2626

2727

2828
class _AssertLogsContext(_BaseTestCaseContext):
29-
"""A context manager used to implement TestCase.assertLogs()."""
29+
"""A context manager for assertLogs() and assertNoLogs() """
3030

3131
LOGGING_FORMAT = "%(levelname)s:%(name)s:%(message)s"
3232

33-
def __init__(self, test_case, logger_name, level):
33+
def __init__(self, test_case, logger_name, level, no_logs):
3434
_BaseTestCaseContext.__init__(self, test_case)
3535
self.logger_name = logger_name
3636
if level:
3737
self.level = logging._nameToLevel.get(level, level)
3838
else:
3939
self.level = logging.INFO
4040
self.msg = None
41+
self.no_logs = no_logs
4142

4243
def __enter__(self):
4344
if isinstance(self.logger_name, logging.Logger):
@@ -54,16 +55,31 @@ def __enter__(self):
5455
logger.handlers = [handler]
5556
logger.setLevel(self.level)
5657
logger.propagate = False
58+
if self.no_logs:
59+
return
5760
return handler.watcher
5861

5962
def __exit__(self, exc_type, exc_value, tb):
6063
self.logger.handlers = self.old_handlers
6164
self.logger.propagate = self.old_propagate
6265
self.logger.setLevel(self.old_level)
66+
6367
if exc_type is not None:
6468
# let unexpected exceptions pass through
6569
return False
66-
if len(self.watcher.records) == 0:
67-
self._raiseFailure(
68-
"no logs of level {} or higher triggered on {}"
69-
.format(logging.getLevelName(self.level), self.logger.name))
70+
71+
if self.no_logs:
72+
# assertNoLogs
73+
if len(self.watcher.records) > 0:
74+
self._raiseFailure(
75+
"Unexpected logs found: {!r}".format(
76+
self.watcher.output
77+
)
78+
)
79+
80+
else:
81+
# assertLogs
82+
if len(self.watcher.records) == 0:
83+
self._raiseFailure(
84+
"no logs of level {} or higher triggered on {}"
85+
.format(logging.getLevelName(self.level), self.logger.name))

Lib/unittest/case.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,6 @@ def __exit__(self, exc_type, exc_value, tb):
295295
self._raiseFailure("{} not triggered".format(exc_name))
296296

297297

298-
299298
class _OrderedChainMap(collections.ChainMap):
300299
def __iter__(self):
301300
seen = set()
@@ -788,7 +787,16 @@ def assertLogs(self, logger=None, level=None):
788787
"""
789788
# Lazy import to avoid importing logging if it is not needed.
790789
from ._log import _AssertLogsContext
791-
return _AssertLogsContext(self, logger, level)
790+
return _AssertLogsContext(self, logger, level, no_logs=False)
791+
792+
def assertNoLogs(self, logger=None, level=None):
793+
""" Fail unless no log messages of level *level* or higher are emitted
794+
on *logger_name* or its children.
795+
796+
This method must be used as a context manager.
797+
"""
798+
from ._log import _AssertLogsContext
799+
return _AssertLogsContext(self, logger, level, no_logs=True)
792800

793801
def _getAssertEqualityFunc(self, first, second):
794802
"""Get a detailed comparison function for the types of the two args.

Lib/unittest/test/test_case.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1681,6 +1681,81 @@ def testAssertLogsFailureMismatchingLogger(self):
16811681
with self.assertLogs('foo'):
16821682
log_quux.error("1")
16831683

1684+
def testAssertLogsUnexpectedException(self):
1685+
# Check unexpected exception will go through.
1686+
with self.assertRaises(ZeroDivisionError):
1687+
with self.assertLogs():
1688+
raise ZeroDivisionError("Unexpected")
1689+
1690+
def testAssertNoLogsDefault(self):
1691+
with self.assertRaises(self.failureException) as cm:
1692+
with self.assertNoLogs():
1693+
log_foo.info("1")
1694+
log_foobar.debug("2")
1695+
self.assertEqual(
1696+
str(cm.exception),
1697+
"Unexpected logs found: ['INFO:foo:1']",
1698+
)
1699+
1700+
def testAssertNoLogsFailureFoundLogs(self):
1701+
with self.assertRaises(self.failureException) as cm:
1702+
with self.assertNoLogs():
1703+
log_quux.error("1")
1704+
log_foo.error("foo")
1705+
1706+
self.assertEqual(
1707+
str(cm.exception),
1708+
"Unexpected logs found: ['ERROR:quux:1', 'ERROR:foo:foo']",
1709+
)
1710+
1711+
def testAssertNoLogsPerLogger(self):
1712+
with self.assertNoStderr():
1713+
with self.assertLogs(log_quux):
1714+
with self.assertNoLogs(logger=log_foo):
1715+
log_quux.error("1")
1716+
1717+
def testAssertNoLogsFailurePerLogger(self):
1718+
# Failure due to unexpected logs for the given logger or its
1719+
# children.
1720+
with self.assertRaises(self.failureException) as cm:
1721+
with self.assertLogs(log_quux):
1722+
with self.assertNoLogs(logger=log_foo):
1723+
log_quux.error("1")
1724+
log_foobar.info("2")
1725+
self.assertEqual(
1726+
str(cm.exception),
1727+
"Unexpected logs found: ['INFO:foo.bar:2']",
1728+
)
1729+
1730+
def testAssertNoLogsPerLevel(self):
1731+
# Check per-level filtering
1732+
with self.assertNoStderr():
1733+
with self.assertNoLogs(level="ERROR"):
1734+
log_foo.info("foo")
1735+
log_quux.debug("1")
1736+
1737+
def testAssertNoLogsFailurePerLevel(self):
1738+
# Failure due to unexpected logs at the specified level.
1739+
with self.assertRaises(self.failureException) as cm:
1740+
with self.assertNoLogs(level="DEBUG"):
1741+
log_foo.debug("foo")
1742+
log_quux.debug("1")
1743+
self.assertEqual(
1744+
str(cm.exception),
1745+
"Unexpected logs found: ['DEBUG:foo:foo', 'DEBUG:quux:1']",
1746+
)
1747+
1748+
def testAssertNoLogsUnexpectedException(self):
1749+
# Check unexpected exception will go through.
1750+
with self.assertRaises(ZeroDivisionError):
1751+
with self.assertNoLogs():
1752+
raise ZeroDivisionError("Unexpected")
1753+
1754+
def testAssertNoLogsYieldsNone(self):
1755+
with self.assertNoLogs() as value:
1756+
pass
1757+
self.assertIsNone(value)
1758+
16841759
def testDeprecatedMethodNames(self):
16851760
"""
16861761
Test that the deprecated methods raise a DeprecationWarning. See #9424.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
A new test assertion context-manager, :func:`unittest.assertNoLogs` will
2+
ensure a given block of code emits no log messages using the logging module.
3+
Contributed by Kit Yan Choi.

0 commit comments

Comments
 (0)