diff options
author | Kit Choi <kitchoi@users.noreply.github.com> | 2020-07-01 21:08:38 (GMT) |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-07-01 21:08:38 (GMT) |
commit | 6b34d7b51e33fcb21b8827d927474ce9ed1f605c (patch) | |
tree | 3c99cf049311bfa2296d47d69fa998b08da26aab | |
parent | 5d5c84ef78b19211671c2bfa68fe073485135eed (diff) | |
download | cpython-6b34d7b51e33fcb21b8827d927474ce9ed1f605c.zip cpython-6b34d7b51e33fcb21b8827d927474ce9ed1f605c.tar.gz cpython-6b34d7b51e33fcb21b8827d927474ce9ed1f605c.tar.bz2 |
bpo-39385: Add an assertNoLogs context manager to unittest.TestCase (GH-18067)
Co-authored-by: RĂ©mi Lapeyre <remi.lapeyre@henki.fr>
-rw-r--r-- | Doc/library/unittest.rst | 21 | ||||
-rw-r--r-- | Lib/unittest/_log.py | 28 | ||||
-rw-r--r-- | Lib/unittest/case.py | 12 | ||||
-rw-r--r-- | Lib/unittest/test/test_case.py | 75 | ||||
-rw-r--r-- | Misc/NEWS.d/next/Library/2020-04-23-18-21-19.bpo-39385.MIAyS7.rst | 3 |
5 files changed, 131 insertions, 8 deletions
diff --git a/Doc/library/unittest.rst b/Doc/library/unittest.rst index b2e16cf..0dddbd2 100644 --- a/Doc/library/unittest.rst +++ b/Doc/library/unittest.rst @@ -950,6 +950,9 @@ Test cases | :meth:`assertLogs(logger, level) | The ``with`` block logs on *logger* | 3.4 | | <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) assertRaises(exception, *, msg=None) @@ -1121,6 +1124,24 @@ Test cases .. 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: diff --git a/Lib/unittest/_log.py b/Lib/unittest/_log.py index 94e7e75..961c448 100644 --- a/Lib/unittest/_log.py +++ b/Lib/unittest/_log.py @@ -26,11 +26,11 @@ class _CapturingHandler(logging.Handler): 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" - def __init__(self, test_case, logger_name, level): + def __init__(self, test_case, logger_name, level, no_logs): _BaseTestCaseContext.__init__(self, test_case) self.logger_name = logger_name if level: @@ -38,6 +38,7 @@ class _AssertLogsContext(_BaseTestCaseContext): else: self.level = logging.INFO self.msg = None + self.no_logs = no_logs def __enter__(self): if isinstance(self.logger_name, logging.Logger): @@ -54,16 +55,31 @@ class _AssertLogsContext(_BaseTestCaseContext): logger.handlers = [handler] logger.setLevel(self.level) logger.propagate = False + if self.no_logs: + return return handler.watcher def __exit__(self, exc_type, exc_value, tb): self.logger.handlers = self.old_handlers self.logger.propagate = self.old_propagate self.logger.setLevel(self.old_level) + if exc_type is not None: # let unexpected exceptions pass through return False - if len(self.watcher.records) == 0: - self._raiseFailure( - "no logs of level {} or higher triggered on {}" - .format(logging.getLevelName(self.level), self.logger.name)) + + if self.no_logs: + # assertNoLogs + 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)) diff --git a/Lib/unittest/case.py b/Lib/unittest/case.py index 52eb7d0..872f121 100644 --- a/Lib/unittest/case.py +++ b/Lib/unittest/case.py @@ -295,7 +295,6 @@ class _AssertWarnsContext(_AssertRaisesBaseContext): self._raiseFailure("{} not triggered".format(exc_name)) - class _OrderedChainMap(collections.ChainMap): def __iter__(self): seen = set() @@ -788,7 +787,16 @@ class TestCase(object): """ # Lazy import to avoid importing logging if it is not needed. 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): """Get a detailed comparison function for the types of the two args. diff --git a/Lib/unittest/test/test_case.py b/Lib/unittest/test/test_case.py index 3dedcbe..0e41696 100644 --- a/Lib/unittest/test/test_case.py +++ b/Lib/unittest/test/test_case.py @@ -1681,6 +1681,81 @@ test case with self.assertLogs('foo'): 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): """ Test that the deprecated methods raise a DeprecationWarning. See #9424. diff --git a/Misc/NEWS.d/next/Library/2020-04-23-18-21-19.bpo-39385.MIAyS7.rst b/Misc/NEWS.d/next/Library/2020-04-23-18-21-19.bpo-39385.MIAyS7.rst new file mode 100644 index 0000000..e6c5c0d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-04-23-18-21-19.bpo-39385.MIAyS7.rst @@ -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.
|