Refs #31811 -- Added optional timing outputs to the test runner.
This commit is contained in:
parent
21768a99f4
commit
61a0ba43cf
|
@ -3,7 +3,7 @@ import sys
|
|||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.core.management.utils import get_command_line_option
|
||||
from django.test.utils import get_runner
|
||||
from django.test.utils import NullTimeKeeper, TimeKeeper, get_runner
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
@ -49,8 +49,10 @@ class Command(BaseCommand):
|
|||
def handle(self, *test_labels, **options):
|
||||
TestRunner = get_runner(settings, options['testrunner'])
|
||||
|
||||
time_keeper = TimeKeeper() if options.get('timing', False) else NullTimeKeeper()
|
||||
test_runner = TestRunner(**options)
|
||||
with time_keeper.timed('Total run'):
|
||||
failures = test_runner.run_tests(test_labels)
|
||||
|
||||
time_keeper.print_results()
|
||||
if failures:
|
||||
sys.exit(1)
|
||||
|
|
|
@ -16,8 +16,9 @@ from django.core.management import call_command
|
|||
from django.db import connections
|
||||
from django.test import SimpleTestCase, TestCase
|
||||
from django.test.utils import (
|
||||
setup_databases as _setup_databases, setup_test_environment,
|
||||
teardown_databases as _teardown_databases, teardown_test_environment,
|
||||
NullTimeKeeper, TimeKeeper, setup_databases as _setup_databases,
|
||||
setup_test_environment, teardown_databases as _teardown_databases,
|
||||
teardown_test_environment,
|
||||
)
|
||||
from django.utils.datastructures import OrderedSet
|
||||
from django.utils.version import PY37
|
||||
|
@ -437,7 +438,8 @@ class DiscoverRunner:
|
|||
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, **kwargs):
|
||||
pdb=False, buffer=False, enable_faulthandler=True,
|
||||
timing=False, **kwargs):
|
||||
|
||||
self.pattern = pattern
|
||||
self.top_level = top_level
|
||||
|
@ -466,6 +468,7 @@ class DiscoverRunner:
|
|||
'--parallel=1 to use it.'
|
||||
)
|
||||
self.test_name_patterns = None
|
||||
self.time_keeper = TimeKeeper() if timing else NullTimeKeeper()
|
||||
if test_name_patterns:
|
||||
# unittest does not export the _convert_select_pattern function
|
||||
# that converts command-line arguments to patterns.
|
||||
|
@ -525,6 +528,12 @@ class DiscoverRunner:
|
|||
'--no-faulthandler', action='store_false', dest='enable_faulthandler',
|
||||
help='Disables the Python faulthandler module during tests.',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--timing', action='store_true',
|
||||
help=(
|
||||
'Output timings, including database set up and total run time.'
|
||||
),
|
||||
)
|
||||
if PY37:
|
||||
parser.add_argument(
|
||||
'-k', action='append', dest='test_name_patterns',
|
||||
|
@ -624,8 +633,8 @@ class DiscoverRunner:
|
|||
|
||||
def setup_databases(self, **kwargs):
|
||||
return _setup_databases(
|
||||
self.verbosity, self.interactive, self.keepdb, self.debug_sql,
|
||||
self.parallel, **kwargs
|
||||
self.verbosity, self.interactive, time_keeper=self.time_keeper, keepdb=self.keepdb,
|
||||
debug_sql=self.debug_sql, parallel=self.parallel, **kwargs
|
||||
)
|
||||
|
||||
def get_resultclass(self):
|
||||
|
@ -704,6 +713,7 @@ class DiscoverRunner:
|
|||
self.setup_test_environment()
|
||||
suite = self.build_suite(test_labels, extra_tests)
|
||||
databases = self.get_databases(suite)
|
||||
with self.time_keeper.timed('Total database setup'):
|
||||
old_config = self.setup_databases(aliases=databases)
|
||||
run_failed = False
|
||||
try:
|
||||
|
@ -714,6 +724,7 @@ class DiscoverRunner:
|
|||
raise
|
||||
finally:
|
||||
try:
|
||||
with self.time_keeper.timed('Total database teardown'):
|
||||
self.teardown_databases(old_config)
|
||||
self.teardown_test_environment()
|
||||
except Exception:
|
||||
|
@ -721,6 +732,7 @@ class DiscoverRunner:
|
|||
# runs to avoid shadowing it.
|
||||
if not run_failed:
|
||||
raise
|
||||
self.time_keeper.print_results()
|
||||
return self.suite_result(suite, result)
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import asyncio
|
||||
import collections
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
|
@ -152,7 +154,8 @@ def teardown_test_environment():
|
|||
del mail.outbox
|
||||
|
||||
|
||||
def setup_databases(verbosity, interactive, keepdb=False, debug_sql=False, parallel=0, aliases=None, **kwargs):
|
||||
def setup_databases(verbosity, interactive, *, time_keeper, keepdb=False, debug_sql=False, parallel=0,
|
||||
aliases=None):
|
||||
"""Create the test databases."""
|
||||
test_databases, mirrored_aliases = get_unique_databases_and_mirrors(aliases)
|
||||
|
||||
|
@ -167,6 +170,7 @@ def setup_databases(verbosity, interactive, keepdb=False, debug_sql=False, paral
|
|||
# Actually create the database for the first connection
|
||||
if first_alias is None:
|
||||
first_alias = alias
|
||||
with time_keeper.timed(" Creating '%s'" % alias):
|
||||
connection.creation.create_test_db(
|
||||
verbosity=verbosity,
|
||||
autoclobber=not interactive,
|
||||
|
@ -175,6 +179,7 @@ def setup_databases(verbosity, interactive, keepdb=False, debug_sql=False, paral
|
|||
)
|
||||
if parallel > 1:
|
||||
for index in range(parallel):
|
||||
with time_keeper.timed(" Cloning '%s'" % alias):
|
||||
connection.creation.clone_test_db(
|
||||
suffix=str(index + 1),
|
||||
verbosity=verbosity,
|
||||
|
@ -841,6 +846,36 @@ class isolate_apps(TestContextDecorator):
|
|||
setattr(Options, 'default_apps', self.old_apps)
|
||||
|
||||
|
||||
class TimeKeeper:
|
||||
def __init__(self):
|
||||
self.records = collections.defaultdict(list)
|
||||
|
||||
@contextmanager
|
||||
def timed(self, name):
|
||||
self.records[name]
|
||||
start_time = time.perf_counter()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
end_time = time.perf_counter() - start_time
|
||||
self.records[name].append(end_time)
|
||||
|
||||
def print_results(self):
|
||||
for name, end_times in self.records.items():
|
||||
for record_time in end_times:
|
||||
record = '%s took %.3fs' % (name, record_time)
|
||||
sys.stderr.write(record + os.linesep)
|
||||
|
||||
|
||||
class NullTimeKeeper:
|
||||
@contextmanager
|
||||
def timed(self, name):
|
||||
yield
|
||||
|
||||
def print_results(self):
|
||||
pass
|
||||
|
||||
|
||||
def tag(*tags):
|
||||
"""Decorator to add tags to a test class or method."""
|
||||
def decorator(obj):
|
||||
|
|
|
@ -1523,6 +1523,12 @@ Django automatically calls :func:`faulthandler.enable()` when starting the
|
|||
tests, which allows it to print a traceback if the interpreter crashes. Pass
|
||||
``--no-faulthandler`` to disable this behavior.
|
||||
|
||||
.. django-admin-option:: --timing
|
||||
|
||||
.. versionadded:: 3.2
|
||||
|
||||
Outputs timings, including including database setup and total run time.
|
||||
|
||||
``testserver``
|
||||
--------------
|
||||
|
||||
|
|
|
@ -330,6 +330,11 @@ Tests
|
|||
:py:mod:`faulthandler` by default. This can be disabled by using the
|
||||
:option:`test --no-faulthandler` option.
|
||||
|
||||
* :class:`~django.test.runner.DiscoverRunner` and the
|
||||
:djadmin:`test` management command can now track timings, including database
|
||||
setup and total run time. This can be enabled by using the :option:`test
|
||||
--timing` option.
|
||||
|
||||
* :class:`~django.test.Client` now preserves the request query string when
|
||||
following 307 and 308 redirects.
|
||||
|
||||
|
@ -454,6 +459,9 @@ Miscellaneous
|
|||
|
||||
* Instantiating an abstract model now raises ``TypeError``.
|
||||
|
||||
* Keyword arguments to :func:`~django.test.utils.setup_databases` are now
|
||||
keyword-only.
|
||||
|
||||
.. _deprecated-features-3.2:
|
||||
|
||||
Features deprecated in 3.2
|
||||
|
|
|
@ -510,7 +510,7 @@ behavior. This class defines the ``run_tests()`` entry point, plus a
|
|||
selection of other methods that are used to 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, test_name_patterns=None, pdb=False, buffer=False, enable_faulthandler=True, **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, test_name_patterns=None, pdb=False, buffer=False, enable_faulthandler=True, timing=True, **kwargs)
|
||||
|
||||
``DiscoverRunner`` will search for tests in any file matching ``pattern``.
|
||||
|
||||
|
@ -560,6 +560,9 @@ execute and tear down the test suite.
|
|||
If ``enable_faulthandler`` is ``True``, :py:mod:`faulthandler` will be
|
||||
enabled.
|
||||
|
||||
If ``timing`` is ``True``, test timings, including database setup and total
|
||||
run time, will be shown.
|
||||
|
||||
Django may, from time to time, extend the capabilities of the test runner
|
||||
by adding new arguments. The ``**kwargs`` declaration allows for this
|
||||
expansion. If you subclass ``DiscoverRunner`` or write your own test
|
||||
|
@ -576,7 +579,7 @@ execute and tear down the test suite.
|
|||
|
||||
.. versionadded:: 3.2
|
||||
|
||||
The ``enable_faulthandler`` argument was added.
|
||||
The ``enable_faulthandler`` and ``timing`` arguments were added.
|
||||
|
||||
Attributes
|
||||
~~~~~~~~~~
|
||||
|
@ -659,7 +662,7 @@ Methods
|
|||
|
||||
Returns a ``TestSuite`` instance ready to be run.
|
||||
|
||||
.. method:: DiscoverRunner.setup_databases(**kwargs)
|
||||
.. method:: DiscoverRunner.setup_databases(verbosity, interactive, **kwargs)
|
||||
|
||||
Creates the test databases by calling
|
||||
:func:`~django.test.utils.setup_databases`.
|
||||
|
@ -723,7 +726,7 @@ utility methods in the ``django.test.utils`` module.
|
|||
Performs global post-test teardown, such as removing instrumentation from
|
||||
the template system and restoring normal email services.
|
||||
|
||||
.. function:: setup_databases(verbosity, interactive, keepdb=False, debug_sql=False, parallel=0, aliases=None, **kwargs)
|
||||
.. function:: setup_databases(verbosity, interactive, *, time_keeper, keepdb=False, debug_sql=False, parallel=0, aliases=None, **kwargs)
|
||||
|
||||
Creates the test databases.
|
||||
|
||||
|
@ -735,6 +738,11 @@ utility methods in the ``django.test.utils`` module.
|
|||
databases should be setup for. If it's not provided, it defaults to all of
|
||||
:setting:`DATABASES` aliases.
|
||||
|
||||
.. versionchanged:: 3.2
|
||||
|
||||
The ``time_keeper`` kwarg was added, and all kwargs were made
|
||||
keyword-only.
|
||||
|
||||
.. function:: teardown_databases(old_config, parallel=0, keepdb=False)
|
||||
|
||||
Destroys the test databases, restoring pre-test conditions.
|
||||
|
|
|
@ -23,7 +23,7 @@ else:
|
|||
from django.test import TestCase, TransactionTestCase
|
||||
from django.test.runner import default_test_processes
|
||||
from django.test.selenium import SeleniumTestCaseBase
|
||||
from django.test.utils import get_runner
|
||||
from django.test.utils import NullTimeKeeper, TimeKeeper, get_runner
|
||||
from django.utils.deprecation import (
|
||||
RemovedInDjango40Warning, RemovedInDjango41Warning,
|
||||
)
|
||||
|
@ -287,7 +287,8 @@ class ActionSelenium(argparse.Action):
|
|||
|
||||
def django_tests(verbosity, interactive, failfast, keepdb, reverse,
|
||||
test_labels, debug_sql, parallel, tags, exclude_tags,
|
||||
test_name_patterns, start_at, start_after, pdb, buffer):
|
||||
test_name_patterns, start_at, start_after, pdb, buffer,
|
||||
timing):
|
||||
state = setup(verbosity, test_labels, parallel, start_at, start_after)
|
||||
extra_tests = []
|
||||
|
||||
|
@ -309,6 +310,7 @@ def django_tests(verbosity, interactive, failfast, keepdb, reverse,
|
|||
test_name_patterns=test_name_patterns,
|
||||
pdb=pdb,
|
||||
buffer=buffer,
|
||||
timing=timing,
|
||||
)
|
||||
failures = test_runner.run_tests(
|
||||
test_labels or get_installed(),
|
||||
|
@ -508,6 +510,10 @@ if __name__ == "__main__":
|
|||
'-b', '--buffer', action='store_true',
|
||||
help='Discard output of passing tests.',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--timing', action='store_true',
|
||||
help='Output timings, including database set up and total run time.',
|
||||
)
|
||||
if PY37:
|
||||
parser.add_argument(
|
||||
'-k', dest='test_name_patterns', action='append',
|
||||
|
@ -568,6 +574,8 @@ if __name__ == "__main__":
|
|||
options.start_at, options.start_after,
|
||||
)
|
||||
else:
|
||||
time_keeper = TimeKeeper() if options.timing else NullTimeKeeper()
|
||||
with time_keeper.timed('Total run'):
|
||||
failures = django_tests(
|
||||
options.verbosity, options.interactive, options.failfast,
|
||||
options.keepdb, options.reverse, options.modules,
|
||||
|
@ -575,6 +583,8 @@ if __name__ == "__main__":
|
|||
options.exclude_tags,
|
||||
getattr(options, 'test_name_patterns', None),
|
||||
options.start_at, options.start_after, options.pdb, options.buffer,
|
||||
options.timing,
|
||||
)
|
||||
time_keeper.print_results()
|
||||
if failures:
|
||||
sys.exit(1)
|
||||
|
|
|
@ -8,7 +8,9 @@ from unittest import (
|
|||
from django.db import connections
|
||||
from django.test import SimpleTestCase
|
||||
from django.test.runner import DiscoverRunner
|
||||
from django.test.utils import captured_stderr, captured_stdout
|
||||
from django.test.utils import (
|
||||
NullTimeKeeper, TimeKeeper, captured_stderr, captured_stdout,
|
||||
)
|
||||
from django.utils.version import PY37
|
||||
|
||||
|
||||
|
@ -324,6 +326,24 @@ class DiscoverRunnerTests(SimpleTestCase):
|
|||
DiscoverRunner(enable_faulthandler=False)
|
||||
mocked_enable.assert_not_called()
|
||||
|
||||
def test_timings_not_captured(self):
|
||||
runner = DiscoverRunner(timing=False)
|
||||
with captured_stderr() as stderr:
|
||||
with runner.time_keeper.timed('test'):
|
||||
pass
|
||||
runner.time_keeper.print_results()
|
||||
self.assertTrue(isinstance(runner.time_keeper, NullTimeKeeper))
|
||||
self.assertNotIn('test', stderr.getvalue())
|
||||
|
||||
def test_timings_captured(self):
|
||||
runner = DiscoverRunner(timing=True)
|
||||
with captured_stderr() as stderr:
|
||||
with runner.time_keeper.timed('test'):
|
||||
pass
|
||||
runner.time_keeper.print_results()
|
||||
self.assertTrue(isinstance(runner.time_keeper, TimeKeeper))
|
||||
self.assertIn('test', stderr.getvalue())
|
||||
|
||||
|
||||
class DiscoverRunnerGetDatabasesTests(SimpleTestCase):
|
||||
runner = DiscoverRunner(verbosity=2)
|
||||
|
|
|
@ -14,7 +14,7 @@ from django.core.management.base import SystemCheckError
|
|||
from django.test import TransactionTestCase, skipUnlessDBFeature, testcases
|
||||
from django.test.runner import DiscoverRunner
|
||||
from django.test.testcases import connections_support_transactions
|
||||
from django.test.utils import dependency_ordered
|
||||
from django.test.utils import captured_stderr, dependency_ordered
|
||||
|
||||
from .models import B, Person, Through
|
||||
|
||||
|
@ -148,6 +148,11 @@ class ManageCommandTests(unittest.TestCase):
|
|||
with self.assertRaises(AttributeError):
|
||||
call_command('test', 'sites', testrunner='test_runner.NonexistentRunner')
|
||||
|
||||
def test_time_recorded(self):
|
||||
with captured_stderr() as stderr:
|
||||
call_command('test', '--timing', 'sites', testrunner='test_runner.tests.MockTestRunner')
|
||||
self.assertIn('Total run took', stderr.getvalue())
|
||||
|
||||
|
||||
class CustomTestRunnerOptionsSettingsTests(AdminScriptTestCase):
|
||||
"""
|
||||
|
|
Loading…
Reference in New Issue