Skip to content

bpo-42722: Add --debug command line option to unittest to enable post-mortem debugging #23900

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Doc/library/unittest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,11 @@ Command-line options

See `Signal Handling`_ for the functions that provide this functionality.

.. cmdoption:: --debug

Run test cases in :meth:`~TestCase.debug` mode.
This is useful for post-mortem debugging via :mod:`pdb`.

.. cmdoption:: -f, --failfast

Stop the test run on the first error or failure.
Expand Down Expand Up @@ -248,6 +253,9 @@ Command-line options
.. versionadded:: 3.7
The command-line option ``-k``.

.. versionadded:: 3.10
The command-line option ``--debug``.

The command line can also be used for test discovery, for running all of the
tests in a project or just a subset.

Expand Down
13 changes: 10 additions & 3 deletions Lib/unittest/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@ class TestProgram(object):
def __init__(self, module='__main__', defaultTest=None, argv=None,
testRunner=None, testLoader=loader.defaultTestLoader,
exit=True, verbosity=1, failfast=None, catchbreak=None,
buffer=None, warnings=None, *, tb_locals=False):
buffer=None, warnings=None, *, tb_locals=False,
debug=False):
if isinstance(module, str):
self.module = __import__(module)
for part in module.split('.')[1:]:
Expand All @@ -81,6 +82,7 @@ def __init__(self, module='__main__', defaultTest=None, argv=None,
self.verbosity = verbosity
self.buffer = buffer
self.tb_locals = tb_locals
self.debug = debug
if warnings is None and not sys.warnoptions:
# even if DeprecationWarnings are ignored by default
# print them anyway unless other warnings settings are
Expand Down Expand Up @@ -175,6 +177,8 @@ def _getParentArgParser(self):
parser.add_argument('--locals', dest='tb_locals',
action='store_true',
help='Show local variables in tracebacks')
parser.add_argument('--debug', action='store_true',
help='Run the given tests in debug mode')
if self.failfast is None:
parser.add_argument('-f', '--failfast', dest='failfast',
action='store_true',
Expand Down Expand Up @@ -268,8 +272,11 @@ def runTests(self):
else:
# it is assumed to be a TestRunner instance
testRunner = self.testRunner
self.result = testRunner.run(self.test)
if self.debug:
testRunner.debug(self.test)
else:
self.result = testRunner.run(self.test)
if self.exit:
sys.exit(not self.result.wasSuccessful())
sys.exit(not (self.debug or self.result.wasSuccessful()))

main = TestProgram
8 changes: 7 additions & 1 deletion Lib/unittest/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@
__unittest = True


class TestRunner:
def debug(self, test):
"""Run the given test case or test suite in debug mode."""
test.debug()


class _WritelnDecorator(object):
"""Used to decorate file-like objects with a handy 'writeln' method"""
def __init__(self,stream):
Expand Down Expand Up @@ -117,7 +123,7 @@ def printErrorList(self, flavour, errors):
self.stream.writeln("%s" % err)


class TextTestRunner(object):
class TextTestRunner(TestRunner):
"""A test runner class that displays results in textual form.

It prints out the names of tests as they are run, errors as they
Expand Down
1 change: 1 addition & 0 deletions Lib/unittest/test/test_break.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ def __init__(self, catchbreak):
self.failfast = failfast
self.catchbreak = catchbreak
self.tb_locals = False
self.debug = False
self.testRunner = FakeRunner
self.test = test
self.result = None
Expand Down
38 changes: 38 additions & 0 deletions Lib/unittest/test/test_program.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,45 @@ def test_ExitAsDefault(self):
testRunner=unittest.TextTestRunner(stream=io.StringIO()),
testLoader=self.FooBarLoader())

class TestRaise(unittest.TestCase):
class Error(Exception):
pass
def test_raise(self):
raise self.Error

class TestRaiseLoader(unittest.TestLoader):
def loadTestsFromModule(self, module):
return self.suiteClass(
[self.loadTestsFromTestCase(Test_TestProgram.TestRaise)])

def loadTestsFromNames(self, names, module):
return self.suiteClass(
[self.loadTestsFromTestCase(Test_TestProgram.TestRaise)])

def test_debug(self):
self.assertRaises(
self.TestRaise.Error,
unittest.main,
argv=["TestRaise", "--debug"],
testRunner=unittest.TextTestRunner(stream=io.StringIO()),
testLoader=self.TestRaiseLoader())

def test_no_debug(self):
self.assertRaises(
SystemExit,
unittest.main,
argv=["TestRaise"],
testRunner=unittest.TextTestRunner(stream=io.StringIO()),
testLoader=self.TestRaiseLoader())


class InitialisableProgram(unittest.TestProgram):
exit = False
result = None
verbosity = 1
defaultTest = None
tb_locals = False
debug = False
testRunner = None
testLoader = unittest.defaultTestLoader
module = '__main__'
Expand Down Expand Up @@ -289,6 +321,12 @@ def test_locals(self):
'verbosity': 1,
'warnings': None})

def test_debug(self):
program = self.program
program.testRunner = FakeRunner
program.parseArgs([None, '--debug'])
self.assertTrue(program.debug)
Comment on lines +324 to +328
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe more tests are needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean more tests for the argument parsing part? I've also added a general test that ensures that any exception is leaked for --debug rather than being consumed by unittest.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ZackerySpytz What kind of tests do you think should be added? I'm happy to add more, but at the moment it's not clear to me what exactly should be tested in addition to what's already there.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Never mind.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ZackerySpytz I'm not familiar with the review process, what do I need to do to further advance this PR?


def testRunTestsOldRunnerClass(self):
program = self.program

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added `--debug` command line option to :mod:`unittest`.