Fixed #30245 -- Added -k option to DiscoverRunner.

This commit is contained in:
François Freitag 2019-03-07 21:58:30 +01:00 committed by Mariusz Felisiak
parent 719b746620
commit 568eed9e79
7 changed files with 99 additions and 8 deletions

View File

@ -17,6 +17,7 @@ from django.test.utils import (
teardown_databases as _teardown_databases, teardown_test_environment, teardown_databases as _teardown_databases, teardown_test_environment,
) )
from django.utils.datastructures import OrderedSet from django.utils.datastructures import OrderedSet
from django.utils.version import PY37
try: try:
import tblib.pickling_support import tblib.pickling_support
@ -407,7 +408,7 @@ class DiscoverRunner:
def __init__(self, pattern=None, top_level=None, verbosity=1, def __init__(self, pattern=None, top_level=None, verbosity=1,
interactive=True, failfast=False, keepdb=False, interactive=True, failfast=False, keepdb=False,
reverse=False, debug_mode=False, debug_sql=False, parallel=0, reverse=False, debug_mode=False, debug_sql=False, parallel=0,
tags=None, exclude_tags=None, **kwargs): tags=None, exclude_tags=None, test_name_patterns=None, **kwargs):
self.pattern = pattern self.pattern = pattern
self.top_level = top_level self.top_level = top_level
@ -421,6 +422,14 @@ class DiscoverRunner:
self.parallel = parallel self.parallel = parallel
self.tags = set(tags or []) self.tags = set(tags or [])
self.exclude_tags = set(exclude_tags or []) self.exclude_tags = set(exclude_tags or [])
self.test_name_patterns = None
if test_name_patterns:
# unittest does not export the _convert_select_pattern function
# that converts command-line arguments to patterns.
self.test_name_patterns = {
pattern if '*' in pattern else '*%s*' % pattern
for pattern in test_name_patterns
}
@classmethod @classmethod
def add_arguments(cls, parser): def add_arguments(cls, parser):
@ -433,7 +442,7 @@ class DiscoverRunner:
help='The test matching pattern. Defaults to test*.py.', help='The test matching pattern. Defaults to test*.py.',
) )
parser.add_argument( parser.add_argument(
'-k', '--keepdb', action='store_true', '--keepdb', action='store_true',
help='Preserves the test DB between runs.' help='Preserves the test DB between runs.'
) )
parser.add_argument( parser.add_argument(
@ -461,6 +470,15 @@ class DiscoverRunner:
'--exclude-tag', action='append', dest='exclude_tags', '--exclude-tag', action='append', dest='exclude_tags',
help='Do not run tests with the specified tag. Can be used multiple times.', help='Do not run tests with the specified tag. Can be used multiple times.',
) )
if PY37:
parser.add_argument(
'-k', action='append', dest='test_name_patterns',
help=(
'Only run test methods and classes that match the pattern '
'or substring. Can be used multiple times. Same as '
'unittest -k option.'
),
)
def setup_test_environment(self, **kwargs): def setup_test_environment(self, **kwargs):
setup_test_environment(debug=self.debug_mode) setup_test_environment(debug=self.debug_mode)
@ -470,6 +488,7 @@ class DiscoverRunner:
suite = self.test_suite() suite = self.test_suite()
test_labels = test_labels or ['.'] test_labels = test_labels or ['.']
extra_tests = extra_tests or [] extra_tests = extra_tests or []
self.test_loader.testNamePatterns = self.test_name_patterns
discover_kwargs = {} discover_kwargs = {}
if self.pattern is not None: if self.pattern is not None:

View File

@ -1360,7 +1360,7 @@ The ``test`` command receives options on behalf of the specified
:option:`--testrunner`. These are the options of the default test runner: :option:`--testrunner`. These are the options of the default test runner:
:class:`~django.test.runner.DiscoverRunner`. :class:`~django.test.runner.DiscoverRunner`.
.. django-admin-option:: --keepdb, -k .. django-admin-option:: --keepdb
Preserves the test database between test runs. This has the advantage of Preserves the test database between test runs. This has the advantage of
skipping both the create and destroy actions which can greatly decrease the skipping both the create and destroy actions which can greatly decrease the
@ -1438,6 +1438,18 @@ May be specified multiple times and combined with :option:`test --exclude-tag`.
Excludes tests :ref:`marked with the specified tags <topics-tagging-tests>`. Excludes tests :ref:`marked with the specified tags <topics-tagging-tests>`.
May be specified multiple times and combined with :option:`test --tag`. May be specified multiple times and combined with :option:`test --tag`.
.. django-admin-option:: -k TEST_NAME_PATTERNS
.. versionadded:: 3.0
Runs test methods and classes matching test name patterns, in the same way as
:option:`unittest's -k option<unittest.-k>`. Can be specified multiple times.
.. admonition:: Python 3.7 and later
This feature is only available for Python 3.7 and later.
``testserver`` ``testserver``
-------------- --------------

View File

@ -238,6 +238,9 @@ Tests
attribute :attr:`~django.test.Response.exc_info`, a tuple providing attribute :attr:`~django.test.Response.exc_info`, a tuple providing
information of the exception that occurred. information of the exception that occurred.
* Tests and test cases to run can be selected by test name pattern using the
new :option:`test -k` option.
URLs URLs
~~~~ ~~~~
@ -360,6 +363,9 @@ Miscellaneous
This converts ``'`` to ``&#x27;`` instead of the previous equivalent decimal This converts ``'`` to ``&#x27;`` instead of the previous equivalent decimal
code ``&#39;``. code ``&#39;``.
* The ``django-admin test -k`` option now works as the :option:`unittest
-k<unittest.-k>` option rather than as a shortcut for ``--keepdb``.
.. _deprecated-features-3.0: .. _deprecated-features-3.0:
Features deprecated in 3.0 Features deprecated in 3.0

View File

@ -424,7 +424,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, selection of other methods that are used to by ``run_tests()`` to set up,
execute and tear down the test suite. 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, **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, **kwargs)
``DiscoverRunner`` will search for tests in any file matching ``pattern``. ``DiscoverRunner`` will search for tests in any file matching ``pattern``.
@ -463,6 +463,9 @@ execute and tear down the test suite.
as the traceback. If ``verbosity`` is ``2``, then queries in all tests are as the traceback. If ``verbosity`` is ``2``, then queries in all tests are
output. output.
``test_name_patterns`` can be used to specify a set of patterns for
filtering test methods and classes by their names.
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

View File

@ -28,6 +28,7 @@ else:
RemovedInDjango31Warning, RemovedInDjango40Warning, RemovedInDjango31Warning, RemovedInDjango40Warning,
) )
from django.utils.log import DEFAULT_LOGGING from django.utils.log import DEFAULT_LOGGING
from django.utils.version import PY37
try: try:
import MySQLdb import MySQLdb
@ -271,7 +272,8 @@ class ActionSelenium(argparse.Action):
def django_tests(verbosity, interactive, failfast, keepdb, reverse, 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):
state = setup(verbosity, test_labels, parallel) state = setup(verbosity, test_labels, parallel)
extra_tests = [] extra_tests = []
@ -290,6 +292,7 @@ def django_tests(verbosity, interactive, failfast, keepdb, reverse,
parallel=actual_test_processes(parallel), parallel=actual_test_processes(parallel),
tags=tags, tags=tags,
exclude_tags=exclude_tags, exclude_tags=exclude_tags,
test_name_patterns=test_name_patterns,
) )
failures = test_runner.run_tests( failures = test_runner.run_tests(
test_labels or get_installed(), test_labels or get_installed(),
@ -416,7 +419,7 @@ if __name__ == "__main__":
help='Tells Django to stop running the test suite after first failed test.', help='Tells Django to stop running the test suite after first failed test.',
) )
parser.add_argument( parser.add_argument(
'-k', '--keepdb', action='store_true', '--keepdb', action='store_true',
help='Tells Django to preserve the test database between runs.', help='Tells Django to preserve the test database between runs.',
) )
parser.add_argument( parser.add_argument(
@ -469,6 +472,14 @@ if __name__ == "__main__":
'--exclude-tag', dest='exclude_tags', action='append', '--exclude-tag', dest='exclude_tags', action='append',
help='Do not run tests with the specified tag. Can be used multiple times.', help='Do not run tests with the specified tag. Can be used multiple times.',
) )
if PY37:
parser.add_argument(
'-k', dest='test_name_patterns', action='append',
help=(
'Only run test methods and classes matching test name pattern. '
'Same as unittest -k option. Can be used multiple times.'
),
)
options = parser.parse_args() options = parser.parse_args()
@ -507,6 +518,7 @@ if __name__ == "__main__":
options.keepdb, options.reverse, options.modules, options.keepdb, options.reverse, options.modules,
options.debug_sql, options.parallel, options.tags, options.debug_sql, options.parallel, options.tags,
options.exclude_tags, options.exclude_tags,
getattr(options, 'test_name_patterns', None),
) )
if failures: if failures:
sys.exit(1) sys.exit(1)

View File

@ -1,12 +1,13 @@
import os import os
from argparse import ArgumentParser from argparse import ArgumentParser
from contextlib import contextmanager from contextlib import contextmanager
from unittest import TestSuite, TextTestRunner, defaultTestLoader from unittest import TestSuite, TextTestRunner, defaultTestLoader, skipUnless
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
from django.test.utils import captured_stdout from django.test.utils import captured_stdout
from django.utils.version import PY37
@contextmanager @contextmanager
@ -23,6 +24,13 @@ def change_cwd(directory):
class DiscoverRunnerTests(SimpleTestCase): class DiscoverRunnerTests(SimpleTestCase):
@staticmethod
def get_test_methods_names(suite):
return [
t.__class__.__name__ + '.' + t._testMethodName
for t in suite._tests
]
def test_init_debug_mode(self): def test_init_debug_mode(self):
runner = DiscoverRunner() runner = DiscoverRunner()
self.assertFalse(runner.debug_mode) self.assertFalse(runner.debug_mode)
@ -71,6 +79,34 @@ class DiscoverRunnerTests(SimpleTestCase):
self.assertEqual(count, 1) self.assertEqual(count, 1)
@skipUnless(PY37, 'unittest -k option requires Python 3.7 and later')
def test_name_patterns(self):
all_test_1 = [
'DjangoCase1.test_1', 'DjangoCase2.test_1',
'SimpleCase1.test_1', 'SimpleCase2.test_1',
'UnittestCase1.test_1', 'UnittestCase2.test_1',
]
all_test_2 = [
'DjangoCase1.test_2', 'DjangoCase2.test_2',
'SimpleCase1.test_2', 'SimpleCase2.test_2',
'UnittestCase1.test_2', 'UnittestCase2.test_2',
]
all_tests = sorted([*all_test_1, *all_test_2, 'UnittestCase2.test_3_test'])
for pattern, expected in [
[['test_1'], all_test_1],
[['UnittestCase1'], ['UnittestCase1.test_1', 'UnittestCase1.test_2']],
[['*test'], ['UnittestCase2.test_3_test']],
[['test*'], all_tests],
[['test'], all_tests],
[['test_1', 'test_2'], sorted([*all_test_1, *all_test_2])],
[['test*1'], all_test_1],
]:
with self.subTest(pattern):
suite = DiscoverRunner(
test_name_patterns=pattern
).build_suite(['test_runner_apps.simple'])
self.assertEqual(expected, self.get_test_methods_names(suite))
def test_file_path(self): def test_file_path(self):
with change_cwd(".."): with change_cwd(".."):
count = DiscoverRunner().build_suite( count = DiscoverRunner().build_suite(
@ -170,7 +206,7 @@ class DiscoverRunnerTests(SimpleTestCase):
msg="Methods of Django cases should be reversed.") msg="Methods of Django cases should be reversed.")
self.assertIn('test_2', suite[4].id(), self.assertIn('test_2', suite[4].id(),
msg="Methods of simple cases should be reversed.") msg="Methods of simple cases should be reversed.")
self.assertIn('test_2', suite[8].id(), self.assertIn('test_2', suite[9].id(),
msg="Methods of unittest cases should be reversed.") msg="Methods of unittest cases should be reversed.")
def test_overridable_get_test_runner_kwargs(self): def test_overridable_get_test_runner_kwargs(self):

View File

@ -55,3 +55,6 @@ class UnittestCase2(TestCase):
def test_2(self): def test_2(self):
pass pass
def test_3_test(self):
pass