Refs #31621 -- Fixed handling --parallel option in test management command and runtests.py.

Regression in ae89daf46f.
Thanks Tim Graham for the report.
This commit is contained in:
Mariusz Felisiak 2021-08-04 10:49:30 +02:00
parent c2a5735d86
commit 36714be874
6 changed files with 72 additions and 47 deletions

View File

@ -3,6 +3,7 @@ import sys
from django.conf import settings from django.conf import settings
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.core.management.utils import get_command_line_option from django.core.management.utils import get_command_line_option
from django.test.runner import get_max_test_processes
from django.test.utils import NullTimeKeeper, TimeKeeper, get_runner from django.test.utils import NullTimeKeeper, TimeKeeper, get_runner
@ -50,6 +51,9 @@ class Command(BaseCommand):
TestRunner = get_runner(settings, options['testrunner']) TestRunner = get_runner(settings, options['testrunner'])
time_keeper = TimeKeeper() if options.get('timing', False) else NullTimeKeeper() time_keeper = TimeKeeper() if options.get('timing', False) else NullTimeKeeper()
parallel = options.get('parallel')
if parallel == 'auto':
options['parallel'] = get_max_test_processes()
test_runner = TestRunner(**options) test_runner = TestRunner(**options)
with time_keeper.timed('Total run'): with time_keeper.timed('Total run'):
failures = test_runner.run_tests(test_labels) failures = test_runner.run_tests(test_labels)

View File

@ -336,14 +336,24 @@ class RemoteTestRunner:
return result return result
def parallel_type(value): def get_max_test_processes():
"""Parse value passed to the --parallel option.""" """
The maximum number of test processes when using 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
if value == 'auto': try:
return int(os.environ['DJANGO_TEST_PROCESSES'])
except KeyError:
return multiprocessing.cpu_count() return multiprocessing.cpu_count()
def parallel_type(value):
"""Parse value passed to the --parallel option."""
if value == 'auto':
return value
try: try:
return int(value) return int(value)
except ValueError: except ValueError:
@ -616,12 +626,8 @@ 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='?', const='auto', default=default_parallel, '--parallel', nargs='?', const='auto', default=0,
type=parallel_type, metavar='N', type=parallel_type, metavar='N',
help=( help=(
'Run tests using up to N parallel processes. Use the value ' 'Run tests using up to N parallel processes. Use the value '

View File

@ -1470,9 +1470,8 @@ multiple cores, this allows running tests significantly faster.
Using ``--parallel`` without a value, or with the value ``auto``, runs one test Using ``--parallel`` without a value, or with the value ``auto``, runs one test
process per core according to :func:`multiprocessing.cpu_count()`. You can process per core according to :func:`multiprocessing.cpu_count()`. You can
override this by passing the desired number of processes, e.g. override this by passing the desired number of processes, e.g.
``--parallel 4``. You can also enable ``--parallel`` without passing the flag ``--parallel 4``, or by setting the :envvar:`DJANGO_TEST_PROCESSES` environment
by setting the :envvar:`DJANGO_TEST_PROCESSES` environment variable to the variable.
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

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 parallel_type from django.test.runner import get_max_test_processes, 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 (
@ -325,17 +325,6 @@ def teardown_run_tests(state):
del os.environ['RUNNING_DJANGOS_TEST_SUITE'] del os.environ['RUNNING_DJANGOS_TEST_SUITE']
def actual_test_processes(parallel):
if parallel == 0:
# This doesn't work before django.setup() on some databases.
if all(conn.features.can_clone_databases for conn in connections.all()):
return parallel_type('auto')
else:
return 1
else:
return parallel
class ActionSelenium(argparse.Action): class ActionSelenium(argparse.Action):
""" """
Validate the comma-separated list of requested browsers. Validate the comma-separated list of requested browsers.
@ -354,18 +343,29 @@ 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 parallel in {0, 'auto'}:
max_parallel = get_max_test_processes()
else:
max_parallel = 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__)
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)
# Run the test suite, including the extra validation tests. # Run the test suite, including the extra validation tests.
if not hasattr(settings, 'TEST_RUNNER'): if not hasattr(settings, 'TEST_RUNNER'):
settings.TEST_RUNNER = 'django.test.runner.DiscoverRunner' settings.TEST_RUNNER = 'django.test.runner.DiscoverRunner'
if parallel in {0, 'auto'}:
# This doesn't work before django.setup() on some databases.
if all(conn.features.can_clone_databases for conn in connections.all()):
parallel = max_parallel
else:
parallel = 1
TestRunner = get_runner(settings) TestRunner = get_runner(settings)
test_runner = TestRunner( test_runner = TestRunner(
verbosity=verbosity, verbosity=verbosity,
@ -374,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_parallel, parallel=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,
@ -563,13 +563,11 @@ 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: # 0 is converted to "auto" or 1 later on, depending on a method used by
default_parallel = int(os.environ['DJANGO_TEST_PROCESSES']) # multiprocessing to start subprocesses and on the backend support for
except KeyError: # cloning databases.
# actual_test_processes() converts this to "auto" later on.
default_parallel = 0
parser.add_argument( parser.add_argument(
'--parallel', nargs='?', const='auto', default=default_parallel, '--parallel', nargs='?', const='auto', default=0,
type=parallel_type, metavar='N', type=parallel_type, metavar='N',
help=( help=(
'Run tests using up to N parallel processes. Use the value "auto" ' 'Run tests using up to N parallel processes. Use the value "auto" '

View File

@ -9,7 +9,7 @@ from unittest import TestSuite, TextTestRunner, defaultTestLoader, mock
from django.db import connections from django.db import connections
from django.test import SimpleTestCase from django.test import SimpleTestCase
from django.test.runner import DiscoverRunner from django.test.runner import DiscoverRunner, get_max_test_processes
from django.test.utils import ( from django.test.utils import (
NullTimeKeeper, TimeKeeper, captured_stderr, captured_stdout, NullTimeKeeper, TimeKeeper, captured_stderr, captured_stdout,
) )
@ -54,11 +54,11 @@ class DiscoverRunnerParallelArgumentTests(SimpleTestCase):
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, 'auto')
def test_parallel_auto(self, *mocked_objects): def test_parallel_auto(self, *mocked_objects):
result = self.get_parser().parse_args(['--parallel', 'auto']) result = self.get_parser().parse_args(['--parallel', 'auto'])
self.assertEqual(result.parallel, 12) self.assertEqual(result.parallel, 'auto')
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'])
@ -70,20 +70,20 @@ class DiscoverRunnerParallelArgumentTests(SimpleTestCase):
msg = "argument --parallel: 'unaccepted' is not an integer or the string 'auto'" msg = "argument --parallel: 'unaccepted' is not an integer or the string 'auto'"
self.assertIn(msg, stderr.getvalue()) self.assertIn(msg, stderr.getvalue())
def test_get_max_test_processes(self, *mocked_objects):
self.assertEqual(get_max_test_processes(), 12)
@mock.patch.dict(os.environ, {'DJANGO_TEST_PROCESSES': '7'}) @mock.patch.dict(os.environ, {'DJANGO_TEST_PROCESSES': '7'})
def test_parallel_env_var(self, *mocked_objects): def test_get_max_test_processes_env_var(self, *mocked_objects):
result = self.get_parser().parse_args([]) self.assertEqual(get_max_test_processes(), 7)
self.assertEqual(result.parallel, 7)
@mock.patch.dict(os.environ, {'DJANGO_TEST_PROCESSES': 'typo'}) def test_get_max_test_processes_spawn(
def test_parallel_env_var_non_int(self, *mocked_objects): self, mocked_get_start_method, mocked_cpu_count,
with self.assertRaises(ValueError): ):
self.get_parser().parse_args([])
def test_parallel_spawn(self, mocked_get_start_method, mocked_cpu_count):
mocked_get_start_method.return_value = 'spawn' mocked_get_start_method.return_value = 'spawn'
result = self.get_parser().parse_args(['--parallel']) self.assertEqual(get_max_test_processes(), 1)
self.assertEqual(result.parallel, 1) with mock.patch.dict(os.environ, {'DJANGO_TEST_PROCESSES': '7'}):
self.assertEqual(get_max_test_processes(), 1)
class DiscoverRunnerTests(SimpleTestCase): class DiscoverRunnerTests(SimpleTestCase):

View File

@ -434,6 +434,12 @@ class ManageCommandParallelTests(SimpleTestCase):
) )
self.assertEqual(stderr.getvalue(), '') self.assertEqual(stderr.getvalue(), '')
@mock.patch.dict(os.environ, {'DJANGO_TEST_PROCESSES': '7'})
def test_no_parallel_django_test_processes_env(self, *mocked_objects):
with captured_stderr() as stderr:
call_command('test', testrunner='test_runner.tests.MockTestRunner')
self.assertEqual(stderr.getvalue(), '')
@mock.patch.dict(os.environ, {'DJANGO_TEST_PROCESSES': 'invalid'}) @mock.patch.dict(os.environ, {'DJANGO_TEST_PROCESSES': 'invalid'})
def test_django_test_processes_env_non_int(self, *mocked_objects): def test_django_test_processes_env_non_int(self, *mocked_objects):
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
@ -443,6 +449,18 @@ class ManageCommandParallelTests(SimpleTestCase):
testrunner='test_runner.tests.MockTestRunner', testrunner='test_runner.tests.MockTestRunner',
) )
@mock.patch.dict(os.environ, {'DJANGO_TEST_PROCESSES': '7'})
def test_django_test_processes_parallel_default(self, *mocked_objects):
for parallel in ['--parallel', '--parallel=auto']:
with self.subTest(parallel=parallel):
with captured_stderr() as stderr:
call_command(
'test',
parallel,
testrunner='test_runner.tests.MockTestRunner',
)
self.assertIn('parallel=7', stderr.getvalue())
class CustomTestRunnerOptionsSettingsTests(AdminScriptTestCase): class CustomTestRunnerOptionsSettingsTests(AdminScriptTestCase):
""" """