Browse Source

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

François Freitag 6 years ago
parent
commit
568eed9e79

+ 21 - 2
django/test/runner.py

@@ -17,6 +17,7 @@ from django.test.utils import (
     teardown_databases as _teardown_databases, teardown_test_environment,
 )
 from django.utils.datastructures import OrderedSet
+from django.utils.version import PY37
 
 try:
     import tblib.pickling_support
@@ -407,7 +408,7 @@ class DiscoverRunner:
     def __init__(self, pattern=None, top_level=None, verbosity=1,
                  interactive=True, failfast=False, keepdb=False,
                  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.top_level = top_level
@@ -421,6 +422,14 @@ class DiscoverRunner:
         self.parallel = parallel
         self.tags = set(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
     def add_arguments(cls, parser):
@@ -433,7 +442,7 @@ class DiscoverRunner:
             help='The test matching pattern. Defaults to test*.py.',
         )
         parser.add_argument(
-            '-k', '--keepdb', action='store_true',
+            '--keepdb', action='store_true',
             help='Preserves the test DB between runs.'
         )
         parser.add_argument(
@@ -461,6 +470,15 @@ class DiscoverRunner:
             '--exclude-tag', action='append', dest='exclude_tags',
             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):
         setup_test_environment(debug=self.debug_mode)
@@ -470,6 +488,7 @@ class DiscoverRunner:
         suite = self.test_suite()
         test_labels = test_labels or ['.']
         extra_tests = extra_tests or []
+        self.test_loader.testNamePatterns = self.test_name_patterns
 
         discover_kwargs = {}
         if self.pattern is not None:

+ 13 - 1
docs/ref/django-admin.txt

@@ -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:
 :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
 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>`.
 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``
 --------------
 

+ 6 - 0
docs/releases/3.0.txt

@@ -238,6 +238,9 @@ Tests
   attribute :attr:`~django.test.Response.exc_info`, a tuple providing
   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
 ~~~~
 
@@ -360,6 +363,9 @@ Miscellaneous
   This converts ``'`` to ``&#x27;`` instead of the previous equivalent decimal
   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:
 
 Features deprecated in 3.0

+ 4 - 1
docs/topics/testing/advanced.txt

@@ -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,
 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``.
 
@@ -463,6 +463,9 @@ execute and tear down the test suite.
     as the traceback. If ``verbosity`` is ``2``, then queries in all tests are
     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
     by adding new arguments. The ``**kwargs`` declaration allows for this
     expansion. If you subclass ``DiscoverRunner`` or write your own test

+ 14 - 2
tests/runtests.py

@@ -28,6 +28,7 @@ else:
         RemovedInDjango31Warning, RemovedInDjango40Warning,
     )
     from django.utils.log import DEFAULT_LOGGING
+    from django.utils.version import PY37
 
 try:
     import MySQLdb
@@ -271,7 +272,8 @@ class ActionSelenium(argparse.Action):
 
 
 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)
     extra_tests = []
 
@@ -290,6 +292,7 @@ def django_tests(verbosity, interactive, failfast, keepdb, reverse,
         parallel=actual_test_processes(parallel),
         tags=tags,
         exclude_tags=exclude_tags,
+        test_name_patterns=test_name_patterns,
     )
     failures = test_runner.run_tests(
         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.',
     )
     parser.add_argument(
-        '-k', '--keepdb', action='store_true',
+        '--keepdb', action='store_true',
         help='Tells Django to preserve the test database between runs.',
     )
     parser.add_argument(
@@ -469,6 +472,14 @@ if __name__ == "__main__":
         '--exclude-tag', dest='exclude_tags', action='append',
         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()
 
@@ -507,6 +518,7 @@ if __name__ == "__main__":
             options.keepdb, options.reverse, options.modules,
             options.debug_sql, options.parallel, options.tags,
             options.exclude_tags,
+            getattr(options, 'test_name_patterns', None),
         )
         if failures:
             sys.exit(1)

+ 38 - 2
tests/test_runner/test_discover_runner.py

@@ -1,12 +1,13 @@
 import os
 from argparse import ArgumentParser
 from contextlib import contextmanager
-from unittest import TestSuite, TextTestRunner, defaultTestLoader
+from unittest import TestSuite, TextTestRunner, defaultTestLoader, skipUnless
 
 from django.db import connections
 from django.test import SimpleTestCase
 from django.test.runner import DiscoverRunner
 from django.test.utils import captured_stdout
+from django.utils.version import PY37
 
 
 @contextmanager
@@ -23,6 +24,13 @@ def change_cwd(directory):
 
 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):
         runner = DiscoverRunner()
         self.assertFalse(runner.debug_mode)
@@ -71,6 +79,34 @@ class DiscoverRunnerTests(SimpleTestCase):
 
         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):
         with change_cwd(".."):
             count = DiscoverRunner().build_suite(
@@ -170,7 +206,7 @@ class DiscoverRunnerTests(SimpleTestCase):
                       msg="Methods of Django cases should be reversed.")
         self.assertIn('test_2', suite[4].id(),
                       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.")
 
     def test_overridable_get_test_runner_kwargs(self):

+ 3 - 0
tests/test_runner_apps/simple/tests.py

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