bpo-39385: Add an assertNoLogs context manager to unittest.TestCase (GH-18067)
Co-authored-by: Rémi Lapeyre <remi.lapeyre@henki.fr>
This commit is contained in:
parent
5d5c84ef78
commit
6b34d7b51e
|
@ -950,6 +950,9 @@ Test cases
|
||||||
| :meth:`assertLogs(logger, level) | The ``with`` block logs on *logger* | 3.4 |
|
| :meth:`assertLogs(logger, level) | The ``with`` block logs on *logger* | 3.4 |
|
||||||
| <TestCase.assertLogs>` | with minimum *level* | |
|
| <TestCase.assertLogs>` | with minimum *level* | |
|
||||||
+---------------------------------------------------------+--------------------------------------+------------+
|
+---------------------------------------------------------+--------------------------------------+------------+
|
||||||
|
| :meth:`assertNoLogs(logger, level) | The ``with`` block does not log on | 3.10 |
|
||||||
|
| <TestCase.assertNoLogs>` | *logger* with minimum *level* | |
|
||||||
|
+---------------------------------------------------------+--------------------------------------+------------+
|
||||||
|
|
||||||
.. method:: assertRaises(exception, callable, *args, **kwds)
|
.. method:: assertRaises(exception, callable, *args, **kwds)
|
||||||
assertRaises(exception, *, msg=None)
|
assertRaises(exception, *, msg=None)
|
||||||
|
@ -1121,6 +1124,24 @@ Test cases
|
||||||
|
|
||||||
.. versionadded:: 3.4
|
.. versionadded:: 3.4
|
||||||
|
|
||||||
|
.. method:: assertNoLogs(logger=None, level=None)
|
||||||
|
|
||||||
|
A context manager to test that no messages are logged on
|
||||||
|
the *logger* or one of its children, with at least the given
|
||||||
|
*level*.
|
||||||
|
|
||||||
|
If given, *logger* should be a :class:`logging.Logger` object or a
|
||||||
|
:class:`str` giving the name of a logger. The default is the root
|
||||||
|
logger, which will catch all messages.
|
||||||
|
|
||||||
|
If given, *level* should be either a numeric logging level or
|
||||||
|
its string equivalent (for example either ``"ERROR"`` or
|
||||||
|
:attr:`logging.ERROR`). The default is :attr:`logging.INFO`.
|
||||||
|
|
||||||
|
Unlike :meth:`assertLogs`, nothing will be returned by the context
|
||||||
|
manager.
|
||||||
|
|
||||||
|
.. versionadded:: 3.10
|
||||||
|
|
||||||
There are also other methods used to perform more specific checks, such as:
|
There are also other methods used to perform more specific checks, such as:
|
||||||
|
|
||||||
|
|
|
@ -26,11 +26,11 @@ class _CapturingHandler(logging.Handler):
|
||||||
|
|
||||||
|
|
||||||
class _AssertLogsContext(_BaseTestCaseContext):
|
class _AssertLogsContext(_BaseTestCaseContext):
|
||||||
"""A context manager used to implement TestCase.assertLogs()."""
|
"""A context manager for assertLogs() and assertNoLogs() """
|
||||||
|
|
||||||
LOGGING_FORMAT = "%(levelname)s:%(name)s:%(message)s"
|
LOGGING_FORMAT = "%(levelname)s:%(name)s:%(message)s"
|
||||||
|
|
||||||
def __init__(self, test_case, logger_name, level):
|
def __init__(self, test_case, logger_name, level, no_logs):
|
||||||
_BaseTestCaseContext.__init__(self, test_case)
|
_BaseTestCaseContext.__init__(self, test_case)
|
||||||
self.logger_name = logger_name
|
self.logger_name = logger_name
|
||||||
if level:
|
if level:
|
||||||
|
@ -38,6 +38,7 @@ class _AssertLogsContext(_BaseTestCaseContext):
|
||||||
else:
|
else:
|
||||||
self.level = logging.INFO
|
self.level = logging.INFO
|
||||||
self.msg = None
|
self.msg = None
|
||||||
|
self.no_logs = no_logs
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
if isinstance(self.logger_name, logging.Logger):
|
if isinstance(self.logger_name, logging.Logger):
|
||||||
|
@ -54,16 +55,31 @@ class _AssertLogsContext(_BaseTestCaseContext):
|
||||||
logger.handlers = [handler]
|
logger.handlers = [handler]
|
||||||
logger.setLevel(self.level)
|
logger.setLevel(self.level)
|
||||||
logger.propagate = False
|
logger.propagate = False
|
||||||
|
if self.no_logs:
|
||||||
|
return
|
||||||
return handler.watcher
|
return handler.watcher
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_value, tb):
|
def __exit__(self, exc_type, exc_value, tb):
|
||||||
self.logger.handlers = self.old_handlers
|
self.logger.handlers = self.old_handlers
|
||||||
self.logger.propagate = self.old_propagate
|
self.logger.propagate = self.old_propagate
|
||||||
self.logger.setLevel(self.old_level)
|
self.logger.setLevel(self.old_level)
|
||||||
|
|
||||||
if exc_type is not None:
|
if exc_type is not None:
|
||||||
# let unexpected exceptions pass through
|
# let unexpected exceptions pass through
|
||||||
return False
|
return False
|
||||||
if len(self.watcher.records) == 0:
|
|
||||||
self._raiseFailure(
|
if self.no_logs:
|
||||||
"no logs of level {} or higher triggered on {}"
|
# assertNoLogs
|
||||||
.format(logging.getLevelName(self.level), self.logger.name))
|
if len(self.watcher.records) > 0:
|
||||||
|
self._raiseFailure(
|
||||||
|
"Unexpected logs found: {!r}".format(
|
||||||
|
self.watcher.output
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
# assertLogs
|
||||||
|
if len(self.watcher.records) == 0:
|
||||||
|
self._raiseFailure(
|
||||||
|
"no logs of level {} or higher triggered on {}"
|
||||||
|
.format(logging.getLevelName(self.level), self.logger.name))
|
||||||
|
|
|
@ -295,7 +295,6 @@ class _AssertWarnsContext(_AssertRaisesBaseContext):
|
||||||
self._raiseFailure("{} not triggered".format(exc_name))
|
self._raiseFailure("{} not triggered".format(exc_name))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class _OrderedChainMap(collections.ChainMap):
|
class _OrderedChainMap(collections.ChainMap):
|
||||||
def __iter__(self):
|
def __iter__(self):
|
||||||
seen = set()
|
seen = set()
|
||||||
|
@ -788,7 +787,16 @@ class TestCase(object):
|
||||||
"""
|
"""
|
||||||
# Lazy import to avoid importing logging if it is not needed.
|
# Lazy import to avoid importing logging if it is not needed.
|
||||||
from ._log import _AssertLogsContext
|
from ._log import _AssertLogsContext
|
||||||
return _AssertLogsContext(self, logger, level)
|
return _AssertLogsContext(self, logger, level, no_logs=False)
|
||||||
|
|
||||||
|
def assertNoLogs(self, logger=None, level=None):
|
||||||
|
""" Fail unless no log messages of level *level* or higher are emitted
|
||||||
|
on *logger_name* or its children.
|
||||||
|
|
||||||
|
This method must be used as a context manager.
|
||||||
|
"""
|
||||||
|
from ._log import _AssertLogsContext
|
||||||
|
return _AssertLogsContext(self, logger, level, no_logs=True)
|
||||||
|
|
||||||
def _getAssertEqualityFunc(self, first, second):
|
def _getAssertEqualityFunc(self, first, second):
|
||||||
"""Get a detailed comparison function for the types of the two args.
|
"""Get a detailed comparison function for the types of the two args.
|
||||||
|
|
|
@ -1681,6 +1681,81 @@ test case
|
||||||
with self.assertLogs('foo'):
|
with self.assertLogs('foo'):
|
||||||
log_quux.error("1")
|
log_quux.error("1")
|
||||||
|
|
||||||
|
def testAssertLogsUnexpectedException(self):
|
||||||
|
# Check unexpected exception will go through.
|
||||||
|
with self.assertRaises(ZeroDivisionError):
|
||||||
|
with self.assertLogs():
|
||||||
|
raise ZeroDivisionError("Unexpected")
|
||||||
|
|
||||||
|
def testAssertNoLogsDefault(self):
|
||||||
|
with self.assertRaises(self.failureException) as cm:
|
||||||
|
with self.assertNoLogs():
|
||||||
|
log_foo.info("1")
|
||||||
|
log_foobar.debug("2")
|
||||||
|
self.assertEqual(
|
||||||
|
str(cm.exception),
|
||||||
|
"Unexpected logs found: ['INFO:foo:1']",
|
||||||
|
)
|
||||||
|
|
||||||
|
def testAssertNoLogsFailureFoundLogs(self):
|
||||||
|
with self.assertRaises(self.failureException) as cm:
|
||||||
|
with self.assertNoLogs():
|
||||||
|
log_quux.error("1")
|
||||||
|
log_foo.error("foo")
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
str(cm.exception),
|
||||||
|
"Unexpected logs found: ['ERROR:quux:1', 'ERROR:foo:foo']",
|
||||||
|
)
|
||||||
|
|
||||||
|
def testAssertNoLogsPerLogger(self):
|
||||||
|
with self.assertNoStderr():
|
||||||
|
with self.assertLogs(log_quux):
|
||||||
|
with self.assertNoLogs(logger=log_foo):
|
||||||
|
log_quux.error("1")
|
||||||
|
|
||||||
|
def testAssertNoLogsFailurePerLogger(self):
|
||||||
|
# Failure due to unexpected logs for the given logger or its
|
||||||
|
# children.
|
||||||
|
with self.assertRaises(self.failureException) as cm:
|
||||||
|
with self.assertLogs(log_quux):
|
||||||
|
with self.assertNoLogs(logger=log_foo):
|
||||||
|
log_quux.error("1")
|
||||||
|
log_foobar.info("2")
|
||||||
|
self.assertEqual(
|
||||||
|
str(cm.exception),
|
||||||
|
"Unexpected logs found: ['INFO:foo.bar:2']",
|
||||||
|
)
|
||||||
|
|
||||||
|
def testAssertNoLogsPerLevel(self):
|
||||||
|
# Check per-level filtering
|
||||||
|
with self.assertNoStderr():
|
||||||
|
with self.assertNoLogs(level="ERROR"):
|
||||||
|
log_foo.info("foo")
|
||||||
|
log_quux.debug("1")
|
||||||
|
|
||||||
|
def testAssertNoLogsFailurePerLevel(self):
|
||||||
|
# Failure due to unexpected logs at the specified level.
|
||||||
|
with self.assertRaises(self.failureException) as cm:
|
||||||
|
with self.assertNoLogs(level="DEBUG"):
|
||||||
|
log_foo.debug("foo")
|
||||||
|
log_quux.debug("1")
|
||||||
|
self.assertEqual(
|
||||||
|
str(cm.exception),
|
||||||
|
"Unexpected logs found: ['DEBUG:foo:foo', 'DEBUG:quux:1']",
|
||||||
|
)
|
||||||
|
|
||||||
|
def testAssertNoLogsUnexpectedException(self):
|
||||||
|
# Check unexpected exception will go through.
|
||||||
|
with self.assertRaises(ZeroDivisionError):
|
||||||
|
with self.assertNoLogs():
|
||||||
|
raise ZeroDivisionError("Unexpected")
|
||||||
|
|
||||||
|
def testAssertNoLogsYieldsNone(self):
|
||||||
|
with self.assertNoLogs() as value:
|
||||||
|
pass
|
||||||
|
self.assertIsNone(value)
|
||||||
|
|
||||||
def testDeprecatedMethodNames(self):
|
def testDeprecatedMethodNames(self):
|
||||||
"""
|
"""
|
||||||
Test that the deprecated methods raise a DeprecationWarning. See #9424.
|
Test that the deprecated methods raise a DeprecationWarning. See #9424.
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
A new test assertion context-manager, :func:`unittest.assertNoLogs` will
|
||||||
|
ensure a given block of code emits no log messages using the logging module.
|
||||||
|
Contributed by Kit Yan Choi.
|
Loading…
Reference in New Issue