diff --git a/django/test/testcases.py b/django/test/testcases.py index 98af1d9da1..690fb83d1c 100644 --- a/django/test/testcases.py +++ b/django/test/testcases.py @@ -1,6 +1,7 @@ import asyncio import difflib import json +import logging import posixpath import sys import threading @@ -43,6 +44,7 @@ from django.test.utils import ( ) from django.utils.deprecation import RemovedInDjango41Warning from django.utils.functional import classproperty +from django.utils.version import PY310 from django.views.static import serve __all__ = ('TestCase', 'TransactionTestCase', @@ -730,6 +732,29 @@ class SimpleTestCase(unittest.TestCase): *args, **kwargs ) + # A similar method is available in Python 3.10+. + if not PY310: + @contextmanager + def assertNoLogs(self, logger, level=None): + """ + Assert no messages are logged on the logger, with at least the + given level. + """ + if isinstance(level, int): + level = logging.getLevelName(level) + elif level is None: + level = 'INFO' + try: + with self.assertLogs(logger, level) as cm: + yield + except AssertionError as e: + msg = e.args[0] + expected_msg = f'no logs of level {level} or higher triggered on {logger}' + if msg != expected_msg: + raise e + else: + self.fail(f'Unexpected logs found: {cm.output!r}') + def assertFieldOutput(self, fieldclass, valid, invalid, field_args=None, field_kwargs=None, empty_value=''): """ diff --git a/django/utils/version.py b/django/utils/version.py index 4b26586b36..54b10e89d7 100644 --- a/django/utils/version.py +++ b/django/utils/version.py @@ -13,6 +13,7 @@ PY36 = sys.version_info >= (3, 6) PY37 = sys.version_info >= (3, 7) PY38 = sys.version_info >= (3, 8) PY39 = sys.version_info >= (3, 9) +PY310 = sys.version_info >= (3, 10) def get_version(version=None): diff --git a/tests/admin_views/test_nav_sidebar.py b/tests/admin_views/test_nav_sidebar.py index be1cfb5209..fc1d70baee 100644 --- a/tests/admin_views/test_nav_sidebar.py +++ b/tests/admin_views/test_nav_sidebar.py @@ -82,9 +82,8 @@ class AdminSidebarTests(TestCase): def test_included_app_list_template_context_fully_set(self): # All context variables should be set when rendering the sidebar. url = reverse('test_with_sidebar:auth_user_changelist') - with self.assertRaisesMessage(AssertionError, 'no logs'): - with self.assertLogs('django.template', 'DEBUG'): - self.client.get(url) + with self.assertNoLogs('django.template', 'DEBUG'): + self.client.get(url) @override_settings(ROOT_URLCONF='admin_views.test_nav_sidebar') diff --git a/tests/csrf_tests/tests.py b/tests/csrf_tests/tests.py index 63a2d478b4..b1b37c8601 100644 --- a/tests/csrf_tests/tests.py +++ b/tests/csrf_tests/tests.py @@ -454,10 +454,9 @@ class CsrfViewMiddlewareTestMixin: """ ensure_csrf_cookie() doesn't log warnings (#19436). """ - with self.assertRaisesMessage(AssertionError, 'no logs'): - with self.assertLogs('django.request', 'WARNING'): - req = self._get_GET_no_csrf_cookie_request() - ensure_csrf_cookie_view(req) + with self.assertNoLogs('django.request', 'WARNING'): + req = self._get_GET_no_csrf_cookie_request() + ensure_csrf_cookie_view(req) def test_post_data_read_failure(self): """ diff --git a/tests/gis_tests/geoadmin/tests.py b/tests/gis_tests/geoadmin/tests.py index c66014454f..67cc8101ef 100644 --- a/tests/gis_tests/geoadmin/tests.py +++ b/tests/gis_tests/geoadmin/tests.py @@ -72,9 +72,8 @@ class GeoAdminTest(SimpleTestCase): def test_olwidget_empty_string(self): geoadmin = site._registry[City] form = geoadmin.get_changelist_form(None)({'point': ''}) - with self.assertRaisesMessage(AssertionError, 'no logs'): - with self.assertLogs('django.contrib.gis', 'ERROR'): - output = str(form['point']) + with self.assertNoLogs('django.contrib.gis', 'ERROR'): + output = str(form['point']) self.assertInHTML( '', diff --git a/tests/gis_tests/geoapp/tests.py b/tests/gis_tests/geoapp/tests.py index 3456aad54f..afd26eeb65 100644 --- a/tests/gis_tests/geoapp/tests.py +++ b/tests/gis_tests/geoapp/tests.py @@ -427,9 +427,8 @@ class GeoLookupTest(TestCase): def test_wkt_string_in_lookup(self): # Valid WKT strings don't emit error logs. - with self.assertRaisesMessage(AssertionError, 'no logs'): - with self.assertLogs('django.contrib.gis', 'ERROR'): - State.objects.filter(poly__intersects='LINESTRING(0 0, 1 1, 5 5)') + with self.assertNoLogs('django.contrib.gis', 'ERROR'): + State.objects.filter(poly__intersects='LINESTRING(0 0, 1 1, 5 5)') @skipUnlessDBFeature("supports_relate_lookup") def test_relate_lookup(self): diff --git a/tests/middleware_exceptions/tests.py b/tests/middleware_exceptions/tests.py index 2a389ce125..a68775d173 100644 --- a/tests/middleware_exceptions/tests.py +++ b/tests/middleware_exceptions/tests.py @@ -177,9 +177,8 @@ class MiddlewareNotUsedTests(SimpleTestCase): MIDDLEWARE=['middleware_exceptions.tests.MyMiddleware'], ) def test_do_not_log_when_debug_is_false(self): - with self.assertRaisesMessage(AssertionError, 'no logs'): - with self.assertLogs('django.request', 'DEBUG'): - self.client.get('/middleware_exceptions/view/') + with self.assertNoLogs('django.request', 'DEBUG'): + self.client.get('/middleware_exceptions/view/') @override_settings(MIDDLEWARE=[ 'middleware_exceptions.middleware.SyncAndAsyncMiddleware', diff --git a/tests/template_tests/test_logging.py b/tests/template_tests/test_logging.py index 81f2661cfc..a5bdebaf16 100644 --- a/tests/template_tests/test_logging.py +++ b/tests/template_tests/test_logging.py @@ -62,6 +62,5 @@ class VariableResolveLoggingTests(SimpleTestCase): ) def test_no_log_when_variable_exists(self): - with self.assertRaisesMessage(AssertionError, 'no logs'): - with self.assertLogs('django.template', self.loglevel): - Variable('article.section').resolve({'article': {'section': 'News'}}) + with self.assertNoLogs('django.template', self.loglevel): + Variable('article.section').resolve({'article': {'section': 'News'}}) diff --git a/tests/test_utils/tests.py b/tests/test_utils/tests.py index 7ceb720207..e998573722 100644 --- a/tests/test_utils/tests.py +++ b/tests/test_utils/tests.py @@ -1,3 +1,4 @@ +import logging import os import unittest import warnings @@ -26,6 +27,7 @@ from django.test.utils import ( ) from django.urls import NoReverseMatch, path, reverse, reverse_lazy from django.utils.deprecation import RemovedInDjango41Warning +from django.utils.log import DEFAULT_LOGGING from .models import Car, Person, PossessedCar from .views import empty_response @@ -1105,6 +1107,47 @@ class AssertWarnsMessageTests(SimpleTestCase): func1() +# TODO: Remove when dropping support for PY39. +class AssertNoLogsTest(SimpleTestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + logging.config.dictConfig(DEFAULT_LOGGING) + cls.addClassCleanup(logging.config.dictConfig, settings.LOGGING) + + def setUp(self): + self.logger = logging.getLogger('django') + + @override_settings(DEBUG=True) + def test_fails_when_log_emitted(self): + msg = "Unexpected logs found: ['INFO:django:FAIL!']" + with self.assertRaisesMessage(AssertionError, msg): + with self.assertNoLogs('django', 'INFO'): + self.logger.info('FAIL!') + + @override_settings(DEBUG=True) + def test_text_level(self): + with self.assertNoLogs('django', 'INFO'): + self.logger.debug('DEBUG logs are ignored.') + + @override_settings(DEBUG=True) + def test_int_level(self): + with self.assertNoLogs('django', logging.INFO): + self.logger.debug('DEBUG logs are ignored.') + + @override_settings(DEBUG=True) + def test_default_level(self): + with self.assertNoLogs('django'): + self.logger.debug('DEBUG logs are ignored.') + + @override_settings(DEBUG=True) + def test_does_not_hide_other_failures(self): + msg = '1 != 2' + with self.assertRaisesMessage(AssertionError, msg): + with self.assertNoLogs('django'): + self.assertEqual(1, 2) + + class AssertFieldOutputTests(SimpleTestCase): def test_assert_field_output(self):