Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 6b34d7b

Browse filesBrowse 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 <remi.lapeyre@henki.fr>
1 parent 5d5c84e commit 6b34d7b
Copy full SHA for 6b34d7b

File tree

5 files changed

+131
-8
lines changed
Filter options

5 files changed

+131
-8
lines changed

‎Doc/library/unittest.rst

Copy file name to clipboardExpand all lines: Doc/library/unittest.rst
+21Lines 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

Copy file name to clipboardExpand all lines: Lib/unittest/_log.py
+22-6Lines 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

Copy file name to clipboardExpand all lines: Lib/unittest/case.py
+10-2Lines 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

Copy file name to clipboardExpand all lines: Lib/unittest/test/test_case.py
+75Lines 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.
+3Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.