diff --git a/django/test/runner.py b/django/test/runner.py index ab5512e628..dea3703d89 100644 --- a/django/test/runner.py +++ b/django/test/runner.py @@ -581,6 +581,19 @@ class DiscoverRunner: ), ) + def log(self, msg, level=None): + """ + Log the given message at the given logging level. + + A verbosity of 1 logs INFO (the default level) or above, and verbosity + 2 or higher logs all levels. + """ + if self.verbosity <= 0 or ( + self.verbosity == 1 and level is not None and level < logging.INFO + ): + return + print(msg) + def setup_test_environment(self, **kwargs): setup_test_environment(debug=self.debug_mode) unittest.installHandler() @@ -639,11 +652,16 @@ class DiscoverRunner: all_tests.extend(iter_test_cases(extra_tests)) if self.tags or self.exclude_tags: - if self.verbosity >= 2: - if self.tags: - print('Including test tag(s): %s.' % ', '.join(sorted(self.tags))) - if self.exclude_tags: - print('Excluding test tag(s): %s.' % ', '.join(sorted(self.exclude_tags))) + if self.tags: + self.log( + 'Including test tag(s): %s.' % ', '.join(sorted(self.tags)), + level=logging.DEBUG, + ) + if self.exclude_tags: + self.log( + 'Excluding test tag(s): %s.' % ', '.join(sorted(self.exclude_tags)), + level=logging.DEBUG, + ) all_tests = filter_tests_by_tags(all_tests, self.tags, self.exclude_tags) # Put the failures detected at load time first for quicker feedback. @@ -651,8 +669,7 @@ class DiscoverRunner: # found or that couldn't be loaded due to syntax errors. test_types = (unittest.loader._FailedTest, *self.reorder_by) all_tests = list(reorder_tests(all_tests, test_types, self.reverse)) - if self.verbosity >= 1: - print('Found %d tests.' % len(all_tests)) + self.log('Found %d tests.' % len(all_tests), level=logging.INFO) suite = self.test_suite(all_tests) if self.parallel > 1: @@ -736,10 +753,12 @@ class DiscoverRunner: def get_databases(self, suite): databases = self._get_databases(suite) - if self.verbosity >= 2: - unused_databases = [alias for alias in connections if alias not in databases] - if unused_databases: - print('Skipping setup of unused database(s): %s.' % ', '.join(sorted(unused_databases))) + unused_databases = [alias for alias in connections if alias not in databases] + if unused_databases: + self.log( + 'Skipping setup of unused database(s): %s.' % ', '.join(sorted(unused_databases)), + level=logging.DEBUG, + ) return databases def run_tests(self, test_labels, extra_tests=None, **kwargs): diff --git a/docs/releases/4.0.txt b/docs/releases/4.0.txt index 7695ce95a2..60c449bc88 100644 --- a/docs/releases/4.0.txt +++ b/docs/releases/4.0.txt @@ -294,6 +294,9 @@ Tests * Django test runner now supports a :option:`--buffer ` option with parallel tests. +* The new :meth:`.DiscoverRunner.log` method allows customizing the way + messages are logged. + URLs ~~~~ diff --git a/docs/topics/testing/advanced.txt b/docs/topics/testing/advanced.txt index d3903757f6..a8a63e7b57 100644 --- a/docs/topics/testing/advanced.txt +++ b/docs/topics/testing/advanced.txt @@ -705,6 +705,17 @@ Methods Computes and returns a return code based on a test suite, and the result from that test suite. +.. method:: DiscoverRunner.log(msg, level=None) + + .. versionadded:: 4.0 + + Prints to the console a message with the given integer `logging level`_ + (e.g. ``logging.DEBUG``, ``logging.INFO``, or ``logging.WARNING``), + respecting the current ``verbosity``. For example, an ``INFO`` message will + be logged if the ``verbosity`` is at least 1, and ``DEBUG`` will be logged + if it is at least 2. + +.. _`logging level`: https://docs.python.org/3/library/logging.html#levels Testing utilities ----------------- diff --git a/tests/test_runner/test_discover_runner.py b/tests/test_runner/test_discover_runner.py index 8f360292f1..d4ec6e97e1 100644 --- a/tests/test_runner/test_discover_runner.py +++ b/tests/test_runner/test_discover_runner.py @@ -1,3 +1,4 @@ +import logging import os import unittest.loader from argparse import ArgumentParser @@ -378,6 +379,43 @@ class DiscoverRunnerTests(SimpleTestCase): self.assertTrue(isinstance(runner.time_keeper, TimeKeeper)) self.assertIn('test', stderr.getvalue()) + def test_log(self): + custom_low_level = 5 + custom_high_level = 45 + msg = 'logging message' + cases = [ + (0, None, False), + (0, custom_low_level, False), + (0, logging.DEBUG, False), + (0, logging.INFO, False), + (0, logging.WARNING, False), + (0, custom_high_level, False), + (1, None, True), + (1, custom_low_level, False), + (1, logging.DEBUG, False), + (1, logging.INFO, True), + (1, logging.WARNING, True), + (1, custom_high_level, True), + (2, None, True), + (2, custom_low_level, True), + (2, logging.DEBUG, True), + (2, logging.INFO, True), + (2, logging.WARNING, True), + (2, custom_high_level, True), + (3, None, True), + (3, custom_low_level, True), + (3, logging.DEBUG, True), + (3, logging.INFO, True), + (3, logging.WARNING, True), + (3, custom_high_level, True), + ] + for verbosity, level, output in cases: + with self.subTest(verbosity=verbosity, level=level): + with captured_stdout() as stdout: + runner = DiscoverRunner(verbosity=verbosity) + runner.log(msg, level) + self.assertEqual(stdout.getvalue(), f'{msg}\n' if output else '') + class DiscoverRunnerGetDatabasesTests(SimpleTestCase): runner = DiscoverRunner(verbosity=2)