1
0
mirror of https://github.com/django/django.git synced 2025-04-01 03:56:42 +00:00

Fixed #34210 -- Added unittest's durations option to the test runner.

This commit is contained in:
David Smith 2023-06-14 19:22:48 +01:00 committed by Mariusz Felisiak
parent 27b399d235
commit 74b5074174
8 changed files with 110 additions and 5 deletions

View File

@ -29,6 +29,7 @@ from django.test.utils import setup_test_environment
from django.test.utils import teardown_databases as _teardown_databases from django.test.utils import teardown_databases as _teardown_databases
from django.test.utils import teardown_test_environment from django.test.utils import teardown_test_environment
from django.utils.datastructures import OrderedSet from django.utils.datastructures import OrderedSet
from django.utils.version import PY312
try: try:
import ipdb as pdb import ipdb as pdb
@ -285,6 +286,10 @@ failure and get a correct traceback.
super().stopTest(test) super().stopTest(test)
self.events.append(("stopTest", self.test_index)) self.events.append(("stopTest", self.test_index))
def addDuration(self, test, elapsed):
super().addDuration(test, elapsed)
self.events.append(("addDuration", self.test_index, elapsed))
def addError(self, test, err): def addError(self, test, err):
self.check_picklable(test, err) self.check_picklable(test, err)
self.events.append(("addError", self.test_index, err)) self.events.append(("addError", self.test_index, err))
@ -655,6 +660,7 @@ class DiscoverRunner:
timing=False, timing=False,
shuffle=False, shuffle=False,
logger=None, logger=None,
durations=None,
**kwargs, **kwargs,
): ):
self.pattern = pattern self.pattern = pattern
@ -692,6 +698,7 @@ class DiscoverRunner:
self.shuffle = shuffle self.shuffle = shuffle
self._shuffler = None self._shuffler = None
self.logger = logger self.logger = logger
self.durations = durations
@classmethod @classmethod
def add_arguments(cls, parser): def add_arguments(cls, parser):
@ -791,6 +798,15 @@ class DiscoverRunner:
"unittest -k option." "unittest -k option."
), ),
) )
if PY312:
parser.add_argument(
"--durations",
dest="durations",
type=int,
default=None,
metavar="N",
help="Show the N slowest test cases (N=0 for all).",
)
@property @property
def shuffle_seed(self): def shuffle_seed(self):
@ -953,12 +969,15 @@ class DiscoverRunner:
return PDBDebugResult return PDBDebugResult
def get_test_runner_kwargs(self): def get_test_runner_kwargs(self):
return { kwargs = {
"failfast": self.failfast, "failfast": self.failfast,
"resultclass": self.get_resultclass(), "resultclass": self.get_resultclass(),
"verbosity": self.verbosity, "verbosity": self.verbosity,
"buffer": self.buffer, "buffer": self.buffer,
} }
if PY312:
kwargs["durations"] = self.durations
return kwargs
def run_checks(self, databases): def run_checks(self, databases):
# Checks are run after database creation since some checks require # Checks are run after database creation since some checks require

View File

@ -1559,6 +1559,16 @@ tests, which allows it to print a traceback if the interpreter crashes. Pass
Outputs timings, including database setup and total run time. Outputs timings, including database setup and total run time.
.. django-admin-option:: --durations N
.. versionadded:: 5.0
Shows the N slowest test cases (N=0 for all).
.. admonition:: Python 3.12 and later
This feature is only available for Python 3.12 and later.
``testserver`` ``testserver``
-------------- --------------

View File

@ -476,6 +476,9 @@ Tests
* :class:`~django.test.AsyncClient` now supports the ``follow`` parameter. * :class:`~django.test.AsyncClient` now supports the ``follow`` parameter.
* The new :option:`test --durations` option allows showing the duration of the
slowest tests on Python 3.12+.
URLs URLs
~~~~ ~~~~

View File

@ -533,7 +533,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 selection of other methods that are used by ``run_tests()`` to set up, execute
and tear down the test suite. 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, logger=None, **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, durations=None, **kwargs)
``DiscoverRunner`` will search for tests in any file matching ``pattern``. ``DiscoverRunner`` will search for tests in any file matching ``pattern``.
@ -613,6 +613,10 @@ and tear down the test suite.
the console. The logger object will respect its logging level rather than the console. The logger object will respect its logging level rather than
the ``verbosity``. the ``verbosity``.
``durations`` will show a list of the N slowest test cases. Setting this
option to ``0`` will result in the duration for all tests being shown.
Requires Python 3.12+.
Django may, from time to time, extend the capabilities of the test runner Django may, from time to time, extend the capabilities of the test runner
by adding new arguments. The ``**kwargs`` declaration allows for this by adding new arguments. The ``**kwargs`` declaration allows for this
expansion. If you subclass ``DiscoverRunner`` or write your own test expansion. If you subclass ``DiscoverRunner`` or write your own test
@ -623,6 +627,10 @@ and tear down the test suite.
custom arguments by calling ``parser.add_argument()`` inside the method, so custom arguments by calling ``parser.add_argument()`` inside the method, so
that the :djadmin:`test` command will be able to use those arguments. that the :djadmin:`test` command will be able to use those arguments.
.. versionadded:: 5.0
The ``durations`` argument was added.
Attributes Attributes
~~~~~~~~~~ ~~~~~~~~~~

View File

@ -33,6 +33,7 @@ else:
RemovedInDjango60Warning, RemovedInDjango60Warning,
) )
from django.utils.log import DEFAULT_LOGGING from django.utils.log import DEFAULT_LOGGING
from django.utils.version import PY312
try: try:
import MySQLdb import MySQLdb
@ -380,6 +381,7 @@ def django_tests(
buffer, buffer,
timing, timing,
shuffle, shuffle,
durations=None,
): ):
if parallel in {0, "auto"}: if parallel in {0, "auto"}:
max_parallel = get_max_test_processes() max_parallel = get_max_test_processes()
@ -425,6 +427,7 @@ def django_tests(
buffer=buffer, buffer=buffer,
timing=timing, timing=timing,
shuffle=shuffle, shuffle=shuffle,
durations=durations,
) )
failures = test_runner.run_tests(test_labels) failures = test_runner.run_tests(test_labels)
teardown_run_tests(state) teardown_run_tests(state)
@ -688,6 +691,15 @@ if __name__ == "__main__":
"Same as unittest -k option. Can be used multiple times." "Same as unittest -k option. Can be used multiple times."
), ),
) )
if PY312:
parser.add_argument(
"--durations",
dest="durations",
type=int,
default=None,
metavar="N",
help="Show the N slowest test cases (N=0 for all).",
)
options = parser.parse_args() options = parser.parse_args()
@ -785,6 +797,7 @@ if __name__ == "__main__":
options.buffer, options.buffer,
options.timing, options.timing,
options.shuffle, options.shuffle,
getattr(options, "durations", None),
) )
time_keeper.print_results() time_keeper.print_results()
if failures: if failures:

View File

@ -16,6 +16,7 @@ from django.test.utils import (
captured_stderr, captured_stderr,
captured_stdout, captured_stdout,
) )
from django.utils.version import PY312
@contextmanager @contextmanager
@ -765,6 +766,22 @@ class DiscoverRunnerTests(SimpleTestCase):
failures = runner.suite_result(suite, result) failures = runner.suite_result(suite, result)
self.assertEqual(failures, expected_failures) self.assertEqual(failures, expected_failures)
@unittest.skipUnless(PY312, "unittest --durations option requires Python 3.12")
def test_durations(self):
with captured_stderr() as stderr, captured_stdout():
runner = DiscoverRunner(durations=10)
suite = runner.build_suite(["test_runner_apps.simple.tests.SimpleCase1"])
runner.run_suite(suite)
self.assertIn("Slowest test durations", stderr.getvalue())
@unittest.skipUnless(PY312, "unittest --durations option requires Python 3.12")
def test_durations_debug_sql(self):
with captured_stderr() as stderr, captured_stdout():
runner = DiscoverRunner(durations=10, debug_sql=True)
suite = runner.build_suite(["test_runner_apps.simple.SimpleCase1"])
runner.run_suite(suite)
self.assertIn("Slowest test durations", stderr.getvalue())
class DiscoverRunnerGetDatabasesTests(SimpleTestCase): class DiscoverRunnerGetDatabasesTests(SimpleTestCase):
runner = DiscoverRunner(verbosity=2) runner = DiscoverRunner(verbosity=2)

View File

@ -4,7 +4,7 @@ import unittest
from django.test import SimpleTestCase from django.test import SimpleTestCase
from django.test.runner import RemoteTestResult from django.test.runner import RemoteTestResult
from django.utils.version import PY311 from django.utils.version import PY311, PY312
try: try:
import tblib.pickling_support import tblib.pickling_support
@ -118,6 +118,10 @@ class RemoteTestResultTest(SimpleTestCase):
subtest_test.run(result=result) subtest_test.run(result=result)
events = result.events events = result.events
# addDurations added in Python 3.12.
if PY312:
self.assertEqual(len(events), 5)
else:
self.assertEqual(len(events), 4) self.assertEqual(len(events), 4)
self.assertIs(result.wasSuccessful(), False) self.assertIs(result.wasSuccessful(), False)
@ -133,3 +137,9 @@ class RemoteTestResultTest(SimpleTestCase):
event = events[2] event = events[2]
self.assertEqual(repr(event[3][1]), "AssertionError('2 != 1')") self.assertEqual(repr(event[3][1]), "AssertionError('2 != 1')")
@unittest.skipUnless(PY312, "unittest --durations option requires Python 3.12")
def test_add_duration(self):
result = RemoteTestResult()
result.addDuration(None, 2.3)
self.assertEqual(result.collectedDurations, [("None", 2.3)])

View File

@ -14,7 +14,7 @@ from django import db
from django.conf import settings from django.conf import settings
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.core.management import call_command from django.core.management import call_command
from django.core.management.base import SystemCheckError from django.core.management.base import CommandError, SystemCheckError
from django.test import SimpleTestCase, TransactionTestCase, skipUnlessDBFeature from django.test import SimpleTestCase, TransactionTestCase, skipUnlessDBFeature
from django.test.runner import ( from django.test.runner import (
DiscoverRunner, DiscoverRunner,
@ -31,6 +31,7 @@ from django.test.utils import (
get_unique_databases_and_mirrors, get_unique_databases_and_mirrors,
iter_test_cases, iter_test_cases,
) )
from django.utils.version import PY312
from .models import B, Person, Through from .models import B, Person, Through
@ -451,6 +452,8 @@ class MockTestRunner:
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
if parallel := kwargs.get("parallel"): if parallel := kwargs.get("parallel"):
sys.stderr.write(f"parallel={parallel}") sys.stderr.write(f"parallel={parallel}")
if durations := kwargs.get("durations"):
sys.stderr.write(f"durations={durations}")
MockTestRunner.run_tests = mock.Mock(return_value=[]) MockTestRunner.run_tests = mock.Mock(return_value=[])
@ -475,6 +478,28 @@ class ManageCommandTests(unittest.TestCase):
) )
self.assertIn("Total run took", stderr.getvalue()) self.assertIn("Total run took", stderr.getvalue())
@unittest.skipUnless(PY312, "unittest --durations option requires Python 3.12")
def test_durations(self):
with captured_stderr() as stderr:
call_command(
"test",
"--durations=10",
"sites",
testrunner="test_runner.tests.MockTestRunner",
)
self.assertIn("durations=10", stderr.getvalue())
@unittest.skipIf(PY312, "unittest --durations option requires Python 3.12")
def test_durations_lt_py312(self):
msg = "Error: unrecognized arguments: --durations=10"
with self.assertRaises(CommandError, msg=msg):
call_command(
"test",
"--durations=10",
"sites",
testrunner="test_runner.tests.MockTestRunner",
)
# Isolate from the real environment. # Isolate from the real environment.
@mock.patch.dict(os.environ, {}, clear=True) @mock.patch.dict(os.environ, {}, clear=True)