diff --git a/django/test/runner.py b/django/test/runner.py index 85a7cd1265..194077d81a 100644 --- a/django/test/runner.py +++ b/django/test/runner.py @@ -19,6 +19,11 @@ from django.test.utils import ( from django.utils.datastructures import OrderedSet from django.utils.version import PY37 +try: + import ipdb as pdb +except ImportError: + import pdb + try: import tblib.pickling_support except ImportError: @@ -72,6 +77,26 @@ class DebugSQLTextTestResult(unittest.TextTestResult): self.stream.writeln(sql_debug) +class PDBDebugResult(unittest.TextTestResult): + """ + Custom result class that triggers a PDB session when an error or failure + occurs. + """ + + def addError(self, test, err): + super().addError(test, err) + self.debug(err) + + def addFailure(self, test, err): + super().addFailure(test, err) + self.debug(err) + + def debug(self, error): + exc_type, exc_value, traceback = error + print("\nOpening PDB: %r" % exc_value) + pdb.post_mortem(traceback) + + class RemoteTestResult: """ Record information about which tests have succeeded and which have failed. @@ -408,7 +433,8 @@ 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, test_name_patterns=None, **kwargs): + tags=None, exclude_tags=None, test_name_patterns=None, + pdb=False, **kwargs): self.pattern = pattern self.top_level = top_level @@ -422,6 +448,9 @@ class DiscoverRunner: self.parallel = parallel self.tags = set(tags or []) self.exclude_tags = set(exclude_tags or []) + self.pdb = pdb + if self.pdb and self.parallel > 1: + raise ValueError('You cannot use --pdb with parallel tests; pass --parallel=1 to use it.') self.test_name_patterns = None if test_name_patterns: # unittest does not export the _convert_select_pattern function @@ -470,6 +499,10 @@ class DiscoverRunner: '--exclude-tag', action='append', dest='exclude_tags', help='Do not run tests with the specified tag. Can be used multiple times.', ) + parser.add_argument( + '--pdb', action='store_true', + help='Runs a debugger (pdb, or ipdb if installed) on error or failure.' + ) if PY37: parser.add_argument( '-k', action='append', dest='test_name_patterns', @@ -574,7 +607,10 @@ class DiscoverRunner: ) def get_resultclass(self): - return DebugSQLTextTestResult if self.debug_sql else None + if self.debug_sql: + return DebugSQLTextTestResult + elif self.pdb: + return PDBDebugResult def get_test_runner_kwargs(self): return { diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt index b5572cd5c7..b38740669f 100644 --- a/docs/ref/django-admin.txt +++ b/docs/ref/django-admin.txt @@ -1456,6 +1456,12 @@ Runs test methods and classes matching test name patterns, in the same way as This feature is only available for Python 3.7 and later. +.. django-admin-option:: --pdb + +.. versionadded:: 3.0 + +Spawns a ``pdb`` debugger at each test error or failure. If you have it +installed, ``ipdb`` is used instead. ``testserver`` -------------- diff --git a/docs/releases/3.0.txt b/docs/releases/3.0.txt index 9de85ab770..ad303ede3e 100644 --- a/docs/releases/3.0.txt +++ b/docs/releases/3.0.txt @@ -348,6 +348,9 @@ Tests * Django test runner now supports ``--start-at`` and ``--start-after`` options to run tests starting from a specific top-level module. +* Django test runner now supports a ``--pdb`` option to spawn a debugger at + each error or failure. + URLs ~~~~ diff --git a/tests/runtests.py b/tests/runtests.py index 153cb0df26..840c06321d 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -284,7 +284,7 @@ class ActionSelenium(argparse.Action): def django_tests(verbosity, interactive, failfast, keepdb, reverse, test_labels, debug_sql, parallel, tags, exclude_tags, - test_name_patterns, start_at, start_after): + test_name_patterns, start_at, start_after, pdb): state = setup(verbosity, test_labels, parallel, start_at, start_after) extra_tests = [] @@ -304,6 +304,7 @@ def django_tests(verbosity, interactive, failfast, keepdb, reverse, tags=tags, exclude_tags=exclude_tags, test_name_patterns=test_name_patterns, + pdb=pdb, ) failures = test_runner.run_tests( test_labels or get_installed(), @@ -495,6 +496,10 @@ if __name__ == "__main__": '--start-at', dest='start_at', help='Run tests starting at the specified top-level module.', ) + parser.add_argument( + '--pdb', action='store_true', + help='Runs the PDB debugger on error or failure.' + ) if PY37: parser.add_argument( '-k', dest='test_name_patterns', action='append', @@ -561,7 +566,7 @@ if __name__ == "__main__": options.debug_sql, options.parallel, options.tags, options.exclude_tags, getattr(options, 'test_name_patterns', None), - options.start_at, options.start_after, + options.start_at, options.start_after, options.pdb, ) if failures: sys.exit(1)