diff --git a/Doc/library/unittest.rst b/Doc/library/unittest.rst index 0a0993518efddc..952148c1e22bf8 100644 --- a/Doc/library/unittest.rst +++ b/Doc/library/unittest.rst @@ -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. @@ -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. diff --git a/Lib/unittest/main.py b/Lib/unittest/main.py index e62469aa2a170f..55bf6caa52832c 100644 --- a/Lib/unittest/main.py +++ b/Lib/unittest/main.py @@ -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:]: @@ -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 @@ -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', @@ -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 diff --git a/Lib/unittest/runner.py b/Lib/unittest/runner.py index 45e7e4c0458d4c..1a6869eb324a82 100644 --- a/Lib/unittest/runner.py +++ b/Lib/unittest/runner.py @@ -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): @@ -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 diff --git a/Lib/unittest/test/test_break.py b/Lib/unittest/test/test_break.py index eebd2b610ce11f..deb9dbe43d7424 100644 --- a/Lib/unittest/test/test_break.py +++ b/Lib/unittest/test/test_break.py @@ -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 diff --git a/Lib/unittest/test/test_program.py b/Lib/unittest/test/test_program.py index eef82ff937ab7c..ce20106b0078b4 100644 --- a/Lib/unittest/test/test_program.py +++ b/Lib/unittest/test/test_program.py @@ -129,6 +129,37 @@ 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 @@ -136,6 +167,7 @@ class InitialisableProgram(unittest.TestProgram): verbosity = 1 defaultTest = None tb_locals = False + debug = False testRunner = None testLoader = unittest.defaultTestLoader module = '__main__' @@ -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) + def testRunTestsOldRunnerClass(self): program = self.program diff --git a/Misc/NEWS.d/next/Library/2020-12-24-02-26-25.bpo-42722.ZNAp9W.rst b/Misc/NEWS.d/next/Library/2020-12-24-02-26-25.bpo-42722.ZNAp9W.rst new file mode 100644 index 00000000000000..02b7dd623a8e0e --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-12-24-02-26-25.bpo-42722.ZNAp9W.rst @@ -0,0 +1 @@ +Added `--debug` command line option to :mod:`unittest`.