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:
parent
c2a5735d86
commit
36714be874
|
@ -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)
|
||||||
|
|
|
@ -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 '
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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" '
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
Loading…
Reference in New Issue