Fixed #31621 -- Added support for '--parallel auto' to test management command.

This commit is contained in:
Adam Johnson 2020-05-23 19:32:22 +01:00 committed by Mariusz Felisiak
parent 7e38a8d66f
commit ae89daf46f
5 changed files with 67 additions and 22 deletions

View File

@ -1,3 +1,4 @@
import argparse
import ctypes import ctypes
import faulthandler import faulthandler
import hashlib import hashlib
@ -335,16 +336,20 @@ class RemoteTestRunner:
return result return result
def default_test_processes(): def parallel_type(value):
"""Default number of test processes when using the --parallel option.""" """Parse value passed to the --parallel option."""
# The current implementation of the parallel test runner requires # The current implementation of the parallel test runner requires
# multiprocessing to start subprocesses with fork(). # multiprocessing to start subprocesses with fork().
if multiprocessing.get_start_method() != 'fork': if multiprocessing.get_start_method() != 'fork':
return 1 return 1
try: if value == 'auto':
return int(os.environ['DJANGO_TEST_PROCESSES'])
except KeyError:
return multiprocessing.cpu_count() return multiprocessing.cpu_count()
try:
return int(value)
except ValueError:
raise argparse.ArgumentTypeError(
f"{value!r} is not an integer or the string 'auto'"
)
_worker_id = 0 _worker_id = 0
@ -611,10 +616,17 @@ class DiscoverRunner:
'-d', '--debug-sql', action='store_true', '-d', '--debug-sql', action='store_true',
help='Prints logged SQL queries on failure.', help='Prints logged SQL queries on failure.',
) )
try:
default_parallel = int(os.environ['DJANGO_TEST_PROCESSES'])
except KeyError:
default_parallel = 0
parser.add_argument( parser.add_argument(
'--parallel', nargs='?', default=1, type=int, '--parallel', nargs='?', const='auto', default=default_parallel,
const=default_test_processes(), metavar='N', type=parallel_type, metavar='N',
help='Run tests using up to N parallel processes.', help=(
'Run tests using up to N parallel processes. Use the value '
'"auto" to run one test process for each processor core.'
),
) )
parser.add_argument( parser.add_argument(
'--tag', action='append', dest='tags', '--tag', action='append', dest='tags',

View File

@ -1467,10 +1467,12 @@ Enables :ref:`SQL logging <django-db-logger>` for failing tests. If
Runs tests in separate parallel processes. Since modern processors have Runs tests in separate parallel processes. Since modern processors have
multiple cores, this allows running tests significantly faster. multiple cores, this allows running tests significantly faster.
By default ``--parallel`` runs one process per core according to Using ``--parallel`` without a value, or with the value ``auto``, runs one test
:func:`multiprocessing.cpu_count()`. You can adjust the number of processes process per core according to :func:`multiprocessing.cpu_count()`. You can
either by providing it as the option's value, e.g. ``--parallel=4``, or by override this by passing the desired number of processes, e.g.
setting the :envvar:`DJANGO_TEST_PROCESSES` environment variable. ``--parallel 4``. You can also enable ``--parallel`` without passing the flag
by setting the :envvar:`DJANGO_TEST_PROCESSES` environment variable to the
desired number of processes.
Django distributes test cases — :class:`unittest.TestCase` subclasses — to Django distributes test cases — :class:`unittest.TestCase` subclasses — to
subprocesses. If there are fewer test cases than configured processes, Django subprocesses. If there are fewer test cases than configured processes, Django
@ -1511,6 +1513,10 @@ don't.
in order to exchange them between processes. See in order to exchange them between processes. See
:ref:`python:pickle-picklable` for details. :ref:`python:pickle-picklable` for details.
.. versionchanged:: 4.0
Support for the value ``auto`` was added.
.. option:: --tag TAGS .. option:: --tag TAGS
Runs only tests :ref:`marked with the specified tags <topics-tagging-tests>`. Runs only tests :ref:`marked with the specified tags <topics-tagging-tests>`.

View File

@ -350,6 +350,9 @@ Tests
* Django test runner now supports a :option:`--shuffle <test --shuffle>` option * Django test runner now supports a :option:`--shuffle <test --shuffle>` option
to execute tests in a random order. to execute tests in a random order.
* The :option:`test --parallel` option now supports the value ``auto`` to run
one test process for each processor core.
URLs URLs
~~~~ ~~~~

View File

@ -23,7 +23,7 @@ else:
from django.conf import settings from django.conf import settings
from django.db import connection, connections from django.db import connection, connections
from django.test import TestCase, TransactionTestCase from django.test import TestCase, TransactionTestCase
from django.test.runner import default_test_processes from django.test.runner import parallel_type
from django.test.selenium import SeleniumTestCaseBase from django.test.selenium import SeleniumTestCaseBase
from django.test.utils import NullTimeKeeper, TimeKeeper, get_runner from django.test.utils import NullTimeKeeper, TimeKeeper, get_runner
from django.utils.deprecation import ( from django.utils.deprecation import (
@ -329,7 +329,7 @@ def actual_test_processes(parallel):
if parallel == 0: if parallel == 0:
# This doesn't work before django.setup() on some databases. # This doesn't work before django.setup() on some databases.
if all(conn.features.can_clone_databases for conn in connections.all()): if all(conn.features.can_clone_databases for conn in connections.all()):
return default_test_processes() return parallel_type('auto')
else: else:
return 1 return 1
else: else:
@ -354,11 +354,12 @@ def django_tests(verbosity, interactive, failfast, keepdb, reverse,
test_labels, debug_sql, parallel, tags, exclude_tags, 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, shuffle): timing, shuffle):
actual_parallel = actual_test_processes(parallel)
if verbosity >= 1: if verbosity >= 1:
msg = "Testing against Django installed in '%s'" % os.path.dirname(django.__file__) msg = "Testing against Django installed in '%s'" % os.path.dirname(django.__file__)
max_parallel = default_test_processes() if parallel == 0 else parallel if actual_parallel > 1:
if max_parallel > 1: msg += " with up to %d processes" % actual_parallel
msg += " with up to %d processes" % max_parallel
print(msg) print(msg)
test_labels, state = setup_run_tests(verbosity, start_at, start_after, test_labels) test_labels, state = setup_run_tests(verbosity, start_at, start_after, test_labels)
@ -373,7 +374,7 @@ def django_tests(verbosity, interactive, failfast, keepdb, reverse,
keepdb=keepdb, keepdb=keepdb,
reverse=reverse, reverse=reverse,
debug_sql=debug_sql, debug_sql=debug_sql,
parallel=actual_test_processes(parallel), parallel=actual_parallel,
tags=tags, tags=tags,
exclude_tags=exclude_tags, exclude_tags=exclude_tags,
test_name_patterns=test_name_patterns, test_name_patterns=test_name_patterns,
@ -562,10 +563,18 @@ if __name__ == "__main__":
'--debug-sql', action='store_true', '--debug-sql', action='store_true',
help='Turn on the SQL query logger within tests.', help='Turn on the SQL query logger within tests.',
) )
try:
default_parallel = int(os.environ['DJANGO_TEST_PROCESSES'])
except KeyError:
# actual_test_processes() converts this to "auto" later on.
default_parallel = 0
parser.add_argument( parser.add_argument(
'--parallel', nargs='?', default=0, type=int, '--parallel', nargs='?', const='auto', default=default_parallel,
const=default_test_processes(), metavar='N', type=parallel_type, metavar='N',
help='Run tests using up to N parallel processes.', help=(
'Run tests using up to N parallel processes. Use the value "auto" '
'to run one test process for each processor core.'
),
) )
parser.add_argument( parser.add_argument(
'--tag', dest='tags', action='append', '--tag', dest='tags', action='append',

View File

@ -50,16 +50,31 @@ class DiscoverRunnerParallelArgumentTests(SimpleTestCase):
def test_parallel_default(self, *mocked_objects): def test_parallel_default(self, *mocked_objects):
result = self.get_parser().parse_args([]) result = self.get_parser().parse_args([])
self.assertEqual(result.parallel, 1) self.assertEqual(result.parallel, 0)
def test_parallel_flag(self, *mocked_objects): def test_parallel_flag(self, *mocked_objects):
result = self.get_parser().parse_args(['--parallel']) result = self.get_parser().parse_args(['--parallel'])
self.assertEqual(result.parallel, 12) self.assertEqual(result.parallel, 12)
def test_parallel_auto(self, *mocked_objects):
result = self.get_parser().parse_args(['--parallel', 'auto'])
self.assertEqual(result.parallel, 12)
def test_parallel_count(self, *mocked_objects): def test_parallel_count(self, *mocked_objects):
result = self.get_parser().parse_args(['--parallel', '17']) result = self.get_parser().parse_args(['--parallel', '17'])
self.assertEqual(result.parallel, 17) self.assertEqual(result.parallel, 17)
def test_parallel_invalid(self, *mocked_objects):
with self.assertRaises(SystemExit), captured_stderr() as stderr:
self.get_parser().parse_args(['--parallel', 'unaccepted'])
msg = "argument --parallel: 'unaccepted' is not an integer or the string 'auto'"
self.assertIn(msg, stderr.getvalue())
@mock.patch.dict(os.environ, {'DJANGO_TEST_PROCESSES': '7'})
def test_parallel_env_var(self, *mocked_objects):
result = self.get_parser().parse_args([])
self.assertEqual(result.parallel, 7)
@mock.patch.dict(os.environ, {'DJANGO_TEST_PROCESSES': 'typo'}) @mock.patch.dict(os.environ, {'DJANGO_TEST_PROCESSES': 'typo'})
def test_parallel_env_var_non_int(self, *mocked_objects): def test_parallel_env_var_non_int(self, *mocked_objects):
with self.assertRaises(ValueError): with self.assertRaises(ValueError):