diff --git a/django/test/runner.py b/django/test/runner.py index eb5026b212..225bc19b09 100644 --- a/django/test/runner.py +++ b/django/test/runner.py @@ -561,7 +561,7 @@ class DiscoverRunner: reverse=False, debug_mode=False, debug_sql=False, parallel=0, tags=None, exclude_tags=None, test_name_patterns=None, pdb=False, buffer=False, enable_faulthandler=True, - timing=False, shuffle=False, **kwargs): + timing=False, shuffle=False, logger=None, **kwargs): self.pattern = pattern self.top_level = top_level @@ -595,6 +595,7 @@ class DiscoverRunner: } self.shuffle = shuffle self._shuffler = None + self.logger = logger @classmethod def add_arguments(cls, parser): @@ -677,16 +678,23 @@ class DiscoverRunner: def log(self, msg, level=None): """ - Log the given message at the given logging level. + Log the message at the given logging level (the default is INFO). - A verbosity of 1 logs INFO (the default level) or above, and verbosity - 2 or higher logs all levels. + If a logger isn't set, the message is instead printed to the console, + respecting the configured verbosity. A verbosity of 0 prints no output, + a verbosity of 1 prints INFO and above, and a verbosity of 2 or higher + prints all levels. """ - if self.verbosity <= 0 or ( - self.verbosity == 1 and level is not None and level < logging.INFO - ): - return - print(msg) + if level is None: + level = logging.INFO + if self.logger is None: + if self.verbosity <= 0 or ( + self.verbosity == 1 and level < logging.INFO + ): + return + print(msg) + else: + self.logger.log(level, msg) def setup_test_environment(self, **kwargs): setup_test_environment(debug=self.debug_mode) diff --git a/docs/releases/4.0.txt b/docs/releases/4.0.txt index 24ce7285e0..aa45d1e1e9 100644 --- a/docs/releases/4.0.txt +++ b/docs/releases/4.0.txt @@ -356,8 +356,11 @@ 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. +* The new ``logger`` argument to :class:`~django.test.runner.DiscoverRunner` + allows a Python :py:ref:`logger ` to be used for logging. + +* The new :meth:`.DiscoverRunner.log` method provides a way to log messages + that uses the ``DiscoverRunner.logger``, or prints to the console if not set. * Django test runner now supports a :option:`--shuffle ` option to execute tests in a random order. diff --git a/docs/topics/testing/advanced.txt b/docs/topics/testing/advanced.txt index 3265c5c641..c0251d8368 100644 --- a/docs/topics/testing/advanced.txt +++ b/docs/topics/testing/advanced.txt @@ -510,7 +510,7 @@ behavior. This class defines the ``run_tests()`` entry point, plus a selection of other methods that are used by ``run_tests()`` to set up, execute and tear down the test suite. -.. class:: DiscoverRunner(pattern='test*.py', top_level=None, verbosity=1, interactive=True, failfast=False, keepdb=False, reverse=False, debug_mode=False, debug_sql=False, parallel=0, tags=None, exclude_tags=None, test_name_patterns=None, pdb=False, buffer=False, enable_faulthandler=True, timing=True, shuffle=False, **kwargs) +.. class:: DiscoverRunner(pattern='test*.py', top_level=None, verbosity=1, interactive=True, failfast=False, keepdb=False, reverse=False, debug_mode=False, debug_sql=False, parallel=0, tags=None, exclude_tags=None, test_name_patterns=None, pdb=False, buffer=False, enable_faulthandler=True, timing=True, shuffle=False, logger=None, **kwargs) ``DiscoverRunner`` will search for tests in any file matching ``pattern``. @@ -580,10 +580,15 @@ and tear down the test suite. If ``shuffle`` is an integer, test cases will be shuffled in a random order prior to execution, using the integer as a random seed. If ``shuffle`` is ``None``, the seed will be generated randomly. In both cases, the seed will - be logged to the console and set to ``self.shuffle_seed`` prior to running - tests. This option can be used to help detect tests that aren't properly - isolated. :ref:`Grouping by test class ` is preserved when - using this option. + be logged and set to ``self.shuffle_seed`` prior to running tests. This + option can be used to help detect tests that aren't properly isolated. + :ref:`Grouping by test class ` is preserved when using this + option. + + ``logger`` can be used to pass a Python :py:ref:`Logger object `. + If provided, the logger will be used to log messages instead of printing to + the console. The logger object will respect its logging level rather than + the ``verbosity``. Django may, from time to time, extend the capabilities of the test runner by adding new arguments. The ``**kwargs`` declaration allows for this @@ -601,7 +606,7 @@ and tear down the test suite. .. versionadded:: 4.0 - The ``shuffle`` argument was added. + The ``logger`` and ``shuffle`` arguments were added. Attributes ~~~~~~~~~~ @@ -726,11 +731,13 @@ Methods .. 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. + If a ``logger`` is set, logs the message at the given integer + `logging level`_ (e.g. ``logging.DEBUG``, ``logging.INFO``, or + ``logging.WARNING``). Otherwise, the message is printed to the console, + respecting the current ``verbosity``. For example, no message will be + printed if the ``verbosity`` is 0, ``INFO`` and above will be printed if + the ``verbosity`` is at least 1, and ``DEBUG`` will be printed if it is at + least 2. The ``level`` defaults to ``logging.INFO``. .. _`logging level`: https://docs.python.org/3/library/logging.html#levels diff --git a/tests/test_runner/test_discover_runner.py b/tests/test_runner/test_discover_runner.py index 625de94067..0012be5a7e 100644 --- a/tests/test_runner/test_discover_runner.py +++ b/tests/test_runner/test_discover_runner.py @@ -623,6 +623,27 @@ class DiscoverRunnerTests(SimpleTestCase): runner.log(msg, level) self.assertEqual(stdout.getvalue(), f'{msg}\n' if output else '') + def test_log_logger(self): + logger = logging.getLogger('test.logging') + cases = [ + (None, 'INFO:test.logging:log message'), + # Test a low custom logging level. + (5, 'Level 5:test.logging:log message'), + (logging.DEBUG, 'DEBUG:test.logging:log message'), + (logging.INFO, 'INFO:test.logging:log message'), + (logging.WARNING, 'WARNING:test.logging:log message'), + # Test a high custom logging level. + (45, 'Level 45:test.logging:log message'), + ] + for level, expected in cases: + with self.subTest(level=level): + runner = DiscoverRunner(logger=logger) + # Pass a logging level smaller than the smallest level in cases + # in order to capture all messages. + with self.assertLogs('test.logging', level=1) as cm: + runner.log('log message', level) + self.assertEqual(cm.output, [expected]) + class DiscoverRunnerGetDatabasesTests(SimpleTestCase): runner = DiscoverRunner(verbosity=2)