From 568eed9e79470b9ddf3e1b3bbad9123ada45eae8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Freitag?= Date: Thu, 7 Mar 2019 21:58:30 +0100 Subject: [PATCH] Fixed #30245 -- Added -k option to DiscoverRunner. --- django/test/runner.py | 23 +++++++++++-- docs/ref/django-admin.txt | 14 +++++++- docs/releases/3.0.txt | 6 ++++ docs/topics/testing/advanced.txt | 5 ++- tests/runtests.py | 16 +++++++-- tests/test_runner/test_discover_runner.py | 40 +++++++++++++++++++++-- tests/test_runner_apps/simple/tests.py | 3 ++ 7 files changed, 99 insertions(+), 8 deletions(-) diff --git a/django/test/runner.py b/django/test/runner.py index 4f755c8fc55..85a7cd12652 100644 --- a/django/test/runner.py +++ b/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: diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index a269f6f8c66..4c7396f9299 100644 --- a/docs/ref/django-admin.txt +++ b/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 `. 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`. Can be specified multiple times. + +.. admonition:: Python 3.7 and later + + This feature is only available for Python 3.7 and later. + + ``testserver`` -------------- diff --git a/docs/releases/3.0.txt b/docs/releases/3.0.txt index 66dfde0b5de..b78d4bd6abd 100644 --- a/docs/releases/3.0.txt +++ b/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 ``'`` instead of the previous equivalent decimal code ``'``. +* The ``django-admin test -k`` option now works as the :option:`unittest + -k` option rather than as a shortcut for ``--keepdb``. + .. _deprecated-features-3.0: Features deprecated in 3.0 diff --git a/docs/topics/testing/advanced.txt b/docs/topics/testing/advanced.txt index 0a228fc4efa..2e4171d376a 100644 --- a/docs/topics/testing/advanced.txt +++ b/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 diff --git a/tests/runtests.py b/tests/runtests.py index a0b429b6df3..96ec30d51a3 100755 --- a/tests/runtests.py +++ b/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) diff --git a/tests/test_runner/test_discover_runner.py b/tests/test_runner/test_discover_runner.py index caa48a852dd..160f174b765 100644 --- a/tests/test_runner/test_discover_runner.py +++ b/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): diff --git a/tests/test_runner_apps/simple/tests.py b/tests/test_runner_apps/simple/tests.py index ac3e9daba01..edd822737cf 100644 --- a/tests/test_runner_apps/simple/tests.py +++ b/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