Issue #22936: Allow showing local variables in unittest errors.

This commit is contained in:
Robert Collins 2015-03-06 13:46:35 +13:00
parent e37a1946c7
commit f0c819acd0
9 changed files with 118 additions and 32 deletions

View File

@ -223,9 +223,16 @@ Command-line options
Stop the test run on the first error or failure. Stop the test run on the first error or failure.
.. cmdoption:: --locals
Show local variables in tracebacks.
.. versionadded:: 3.2 .. versionadded:: 3.2
The command-line options ``-b``, ``-c`` and ``-f`` were added. The command-line options ``-b``, ``-c`` and ``-f`` were added.
.. versionadded:: 3.5
The command-line option ``--locals``.
The command line can also be used for test discovery, for running all of the The command line can also be used for test discovery, for running all of the
tests in a project or just a subset. tests in a project or just a subset.
@ -1782,12 +1789,10 @@ Loading and running tests
Set to ``True`` when the execution of tests should stop by :meth:`stop`. Set to ``True`` when the execution of tests should stop by :meth:`stop`.
.. attribute:: testsRun .. attribute:: testsRun
The total number of tests run so far. The total number of tests run so far.
.. attribute:: buffer .. attribute:: buffer
If set to true, ``sys.stdout`` and ``sys.stderr`` will be buffered in between If set to true, ``sys.stdout`` and ``sys.stderr`` will be buffered in between
@ -1797,7 +1802,6 @@ Loading and running tests
.. versionadded:: 3.2 .. versionadded:: 3.2
.. attribute:: failfast .. attribute:: failfast
If set to true :meth:`stop` will be called on the first failure or error, If set to true :meth:`stop` will be called on the first failure or error,
@ -1805,6 +1809,11 @@ Loading and running tests
.. versionadded:: 3.2 .. versionadded:: 3.2
.. attribute:: tb_locals
If set to true then local variables will be shown in tracebacks.
.. versionadded:: 3.5
.. method:: wasSuccessful() .. method:: wasSuccessful()
@ -1815,7 +1824,6 @@ Loading and running tests
Returns ``False`` if there were any :attr:`unexpectedSuccesses` Returns ``False`` if there were any :attr:`unexpectedSuccesses`
from tests marked with the :func:`expectedFailure` decorator. from tests marked with the :func:`expectedFailure` decorator.
.. method:: stop() .. method:: stop()
This method can be called to signal that the set of tests being run should This method can be called to signal that the set of tests being run should
@ -1947,12 +1955,14 @@ Loading and running tests
.. class:: TextTestRunner(stream=None, descriptions=True, verbosity=1, failfast=False, \ .. class:: TextTestRunner(stream=None, descriptions=True, verbosity=1, failfast=False, \
buffer=False, resultclass=None, warnings=None) buffer=False, resultclass=None, warnings=None, *, tb_locals=False)
A basic test runner implementation that outputs results to a stream. If *stream* A basic test runner implementation that outputs results to a stream. If *stream*
is ``None``, the default, :data:`sys.stderr` is used as the output stream. This class is ``None``, the default, :data:`sys.stderr` is used as the output stream. This class
has a few configurable parameters, but is essentially very simple. Graphical has a few configurable parameters, but is essentially very simple. Graphical
applications which run test suites should provide alternate implementations. applications which run test suites should provide alternate implementations. Such
implementations should accept ``**kwargs`` as the interface to construct runners
changes when features are added to unittest.
By default this runner shows :exc:`DeprecationWarning`, By default this runner shows :exc:`DeprecationWarning`,
:exc:`PendingDeprecationWarning`, :exc:`ResourceWarning` and :exc:`PendingDeprecationWarning`, :exc:`ResourceWarning` and
@ -1971,6 +1981,9 @@ Loading and running tests
The default stream is set to :data:`sys.stderr` at instantiation time rather The default stream is set to :data:`sys.stderr` at instantiation time rather
than import time. than import time.
.. versionchanged:: 3.5
Added the tb_locals parameter.
.. method:: _makeResult() .. method:: _makeResult()
This method returns the instance of ``TestResult`` used by :meth:`run`. This method returns the instance of ``TestResult`` used by :meth:`run`.

View File

@ -58,7 +58,7 @@ class TestProgram(object):
def __init__(self, module='__main__', defaultTest=None, argv=None, def __init__(self, module='__main__', defaultTest=None, argv=None,
testRunner=None, testLoader=loader.defaultTestLoader, testRunner=None, testLoader=loader.defaultTestLoader,
exit=True, verbosity=1, failfast=None, catchbreak=None, exit=True, verbosity=1, failfast=None, catchbreak=None,
buffer=None, warnings=None): buffer=None, warnings=None, *, tb_locals=False):
if isinstance(module, str): if isinstance(module, str):
self.module = __import__(module) self.module = __import__(module)
for part in module.split('.')[1:]: for part in module.split('.')[1:]:
@ -73,6 +73,7 @@ class TestProgram(object):
self.catchbreak = catchbreak self.catchbreak = catchbreak
self.verbosity = verbosity self.verbosity = verbosity
self.buffer = buffer self.buffer = buffer
self.tb_locals = tb_locals
if warnings is None and not sys.warnoptions: if warnings is None and not sys.warnoptions:
# even if DeprecationWarnings are ignored by default # even if DeprecationWarnings are ignored by default
# print them anyway unless other warnings settings are # print them anyway unless other warnings settings are
@ -159,7 +160,9 @@ class TestProgram(object):
parser.add_argument('-q', '--quiet', dest='verbosity', parser.add_argument('-q', '--quiet', dest='verbosity',
action='store_const', const=0, action='store_const', const=0,
help='Quiet output') help='Quiet output')
parser.add_argument('--locals', dest='tb_locals',
action='store_true',
help='Show local variables in tracebacks')
if self.failfast is None: if self.failfast is None:
parser.add_argument('-f', '--failfast', dest='failfast', parser.add_argument('-f', '--failfast', dest='failfast',
action='store_true', action='store_true',
@ -231,10 +234,18 @@ class TestProgram(object):
self.testRunner = runner.TextTestRunner self.testRunner = runner.TextTestRunner
if isinstance(self.testRunner, type): if isinstance(self.testRunner, type):
try: try:
testRunner = self.testRunner(verbosity=self.verbosity, try:
failfast=self.failfast, testRunner = self.testRunner(verbosity=self.verbosity,
buffer=self.buffer, failfast=self.failfast,
warnings=self.warnings) buffer=self.buffer,
warnings=self.warnings,
tb_locals=self.tb_locals)
except TypeError:
# didn't accept the tb_locals argument
testRunner = self.testRunner(verbosity=self.verbosity,
failfast=self.failfast,
buffer=self.buffer,
warnings=self.warnings)
except TypeError: except TypeError:
# didn't accept the verbosity, buffer or failfast arguments # didn't accept the verbosity, buffer or failfast arguments
testRunner = self.testRunner() testRunner = self.testRunner()

View File

@ -45,6 +45,7 @@ class TestResult(object):
self.unexpectedSuccesses = [] self.unexpectedSuccesses = []
self.shouldStop = False self.shouldStop = False
self.buffer = False self.buffer = False
self.tb_locals = False
self._stdout_buffer = None self._stdout_buffer = None
self._stderr_buffer = None self._stderr_buffer = None
self._original_stdout = sys.stdout self._original_stdout = sys.stdout
@ -179,9 +180,11 @@ class TestResult(object):
if exctype is test.failureException: if exctype is test.failureException:
# Skip assert*() traceback levels # Skip assert*() traceback levels
length = self._count_relevant_tb_levels(tb) length = self._count_relevant_tb_levels(tb)
msgLines = traceback.format_exception(exctype, value, tb, length)
else: else:
msgLines = traceback.format_exception(exctype, value, tb) length = None
tb_e = traceback.TracebackException(
exctype, value, tb, limit=length, capture_locals=self.tb_locals)
msgLines = list(tb_e.format())
if self.buffer: if self.buffer:
output = sys.stdout.getvalue() output = sys.stdout.getvalue()

View File

@ -126,7 +126,13 @@ class TextTestRunner(object):
resultclass = TextTestResult resultclass = TextTestResult
def __init__(self, stream=None, descriptions=True, verbosity=1, def __init__(self, stream=None, descriptions=True, verbosity=1,
failfast=False, buffer=False, resultclass=None, warnings=None): failfast=False, buffer=False, resultclass=None, warnings=None,
*, tb_locals=False):
"""Construct a TextTestRunner.
Subclasses should accept **kwargs to ensure compatibility as the
interface changes.
"""
if stream is None: if stream is None:
stream = sys.stderr stream = sys.stderr
self.stream = _WritelnDecorator(stream) self.stream = _WritelnDecorator(stream)
@ -134,6 +140,7 @@ class TextTestRunner(object):
self.verbosity = verbosity self.verbosity = verbosity
self.failfast = failfast self.failfast = failfast
self.buffer = buffer self.buffer = buffer
self.tb_locals = tb_locals
self.warnings = warnings self.warnings = warnings
if resultclass is not None: if resultclass is not None:
self.resultclass = resultclass self.resultclass = resultclass
@ -147,6 +154,7 @@ class TextTestRunner(object):
registerResult(result) registerResult(result)
result.failfast = self.failfast result.failfast = self.failfast
result.buffer = self.buffer result.buffer = self.buffer
result.tb_locals = self.tb_locals
with warnings.catch_warnings(): with warnings.catch_warnings():
if self.warnings: if self.warnings:
# if self.warnings is set, use it to filter all the warnings # if self.warnings is set, use it to filter all the warnings

View File

@ -211,6 +211,7 @@ class TestBreak(unittest.TestCase):
self.verbosity = verbosity self.verbosity = verbosity
self.failfast = failfast self.failfast = failfast
self.catchbreak = catchbreak self.catchbreak = catchbreak
self.tb_locals = False
self.testRunner = FakeRunner self.testRunner = FakeRunner
self.test = test self.test = test
self.result = None self.result = None
@ -221,6 +222,7 @@ class TestBreak(unittest.TestCase):
self.assertEqual(FakeRunner.initArgs, [((), {'buffer': None, self.assertEqual(FakeRunner.initArgs, [((), {'buffer': None,
'verbosity': verbosity, 'verbosity': verbosity,
'failfast': failfast, 'failfast': failfast,
'tb_locals': False,
'warnings': None})]) 'warnings': None})])
self.assertEqual(FakeRunner.runArgs, [test]) self.assertEqual(FakeRunner.runArgs, [test])
self.assertEqual(p.result, result) self.assertEqual(p.result, result)
@ -235,6 +237,7 @@ class TestBreak(unittest.TestCase):
self.assertEqual(FakeRunner.initArgs, [((), {'buffer': None, self.assertEqual(FakeRunner.initArgs, [((), {'buffer': None,
'verbosity': verbosity, 'verbosity': verbosity,
'failfast': failfast, 'failfast': failfast,
'tb_locals': False,
'warnings': None})]) 'warnings': None})])
self.assertEqual(FakeRunner.runArgs, [test]) self.assertEqual(FakeRunner.runArgs, [test])
self.assertEqual(p.result, result) self.assertEqual(p.result, result)

View File

@ -134,6 +134,7 @@ class InitialisableProgram(unittest.TestProgram):
result = None result = None
verbosity = 1 verbosity = 1
defaultTest = None defaultTest = None
tb_locals = False
testRunner = None testRunner = None
testLoader = unittest.defaultTestLoader testLoader = unittest.defaultTestLoader
module = '__main__' module = '__main__'
@ -147,18 +148,19 @@ RESULT = object()
class FakeRunner(object): class FakeRunner(object):
initArgs = None initArgs = None
test = None test = None
raiseError = False raiseError = 0
def __init__(self, **kwargs): def __init__(self, **kwargs):
FakeRunner.initArgs = kwargs FakeRunner.initArgs = kwargs
if FakeRunner.raiseError: if FakeRunner.raiseError:
FakeRunner.raiseError = False FakeRunner.raiseError -= 1
raise TypeError raise TypeError
def run(self, test): def run(self, test):
FakeRunner.test = test FakeRunner.test = test
return RESULT return RESULT
class TestCommandLineArgs(unittest.TestCase): class TestCommandLineArgs(unittest.TestCase):
def setUp(self): def setUp(self):
@ -166,7 +168,7 @@ class TestCommandLineArgs(unittest.TestCase):
self.program.createTests = lambda: None self.program.createTests = lambda: None
FakeRunner.initArgs = None FakeRunner.initArgs = None
FakeRunner.test = None FakeRunner.test = None
FakeRunner.raiseError = False FakeRunner.raiseError = 0
def testVerbosity(self): def testVerbosity(self):
program = self.program program = self.program
@ -256,6 +258,7 @@ class TestCommandLineArgs(unittest.TestCase):
self.assertEqual(FakeRunner.initArgs, {'verbosity': 'verbosity', self.assertEqual(FakeRunner.initArgs, {'verbosity': 'verbosity',
'failfast': 'failfast', 'failfast': 'failfast',
'buffer': 'buffer', 'buffer': 'buffer',
'tb_locals': False,
'warnings': 'warnings'}) 'warnings': 'warnings'})
self.assertEqual(FakeRunner.test, 'test') self.assertEqual(FakeRunner.test, 'test')
self.assertIs(program.result, RESULT) self.assertIs(program.result, RESULT)
@ -274,10 +277,25 @@ class TestCommandLineArgs(unittest.TestCase):
self.assertEqual(FakeRunner.test, 'test') self.assertEqual(FakeRunner.test, 'test')
self.assertIs(program.result, RESULT) self.assertIs(program.result, RESULT)
def test_locals(self):
program = self.program
program.testRunner = FakeRunner
program.parseArgs([None, '--locals'])
self.assertEqual(True, program.tb_locals)
program.runTests()
self.assertEqual(FakeRunner.initArgs, {'buffer': False,
'failfast': False,
'tb_locals': True,
'verbosity': 1,
'warnings': None})
def testRunTestsOldRunnerClass(self): def testRunTestsOldRunnerClass(self):
program = self.program program = self.program
FakeRunner.raiseError = True # Two TypeErrors are needed to fall all the way back to old-style
# runners - one to fail tb_locals, one to fail buffer etc.
FakeRunner.raiseError = 2
program.testRunner = FakeRunner program.testRunner = FakeRunner
program.verbosity = 'verbosity' program.verbosity = 'verbosity'
program.failfast = 'failfast' program.failfast = 'failfast'

View File

@ -8,6 +8,20 @@ import traceback
import unittest import unittest
class MockTraceback(object):
class TracebackException:
def __init__(self, *args, **kwargs):
self.capture_locals = kwargs.get('capture_locals', False)
def format(self):
result = ['A traceback']
if self.capture_locals:
result.append('locals')
return result
def restore_traceback():
unittest.result.traceback = traceback
class Test_TestResult(unittest.TestCase): class Test_TestResult(unittest.TestCase):
# Note: there are not separate tests for TestResult.wasSuccessful(), # Note: there are not separate tests for TestResult.wasSuccessful(),
# TestResult.errors, TestResult.failures, TestResult.testsRun or # TestResult.errors, TestResult.failures, TestResult.testsRun or
@ -227,6 +241,25 @@ class Test_TestResult(unittest.TestCase):
self.assertIs(test_case, test) self.assertIs(test_case, test)
self.assertIsInstance(formatted_exc, str) self.assertIsInstance(formatted_exc, str)
def test_addError_locals(self):
class Foo(unittest.TestCase):
def test_1(self):
1/0
test = Foo('test_1')
result = unittest.TestResult()
result.tb_locals = True
unittest.result.traceback = MockTraceback
self.addCleanup(restore_traceback)
result.startTestRun()
test.run(result)
result.stopTestRun()
self.assertEqual(len(result.errors), 1)
test_case, formatted_exc = result.errors[0]
self.assertEqual('A tracebacklocals', formatted_exc)
def test_addSubTest(self): def test_addSubTest(self):
class Foo(unittest.TestCase): class Foo(unittest.TestCase):
def test_1(self): def test_1(self):
@ -398,6 +431,7 @@ def __init__(self, stream=None, descriptions=None, verbosity=None):
self.testsRun = 0 self.testsRun = 0
self.shouldStop = False self.shouldStop = False
self.buffer = False self.buffer = False
self.tb_locals = False
classDict['__init__'] = __init__ classDict['__init__'] = __init__
OldResult = type('OldResult', (object,), classDict) OldResult = type('OldResult', (object,), classDict)
@ -454,15 +488,6 @@ class Test_OldTestResult(unittest.TestCase):
runner.run(Test('testFoo')) runner.run(Test('testFoo'))
class MockTraceback(object):
@staticmethod
def format_exception(*_):
return ['A traceback']
def restore_traceback():
unittest.result.traceback = traceback
class TestOutputBuffering(unittest.TestCase): class TestOutputBuffering(unittest.TestCase):
def setUp(self): def setUp(self):

View File

@ -158,7 +158,7 @@ class Test_TextTestRunner(unittest.TestCase):
self.assertEqual(runner.warnings, None) self.assertEqual(runner.warnings, None)
self.assertTrue(runner.descriptions) self.assertTrue(runner.descriptions)
self.assertEqual(runner.resultclass, unittest.TextTestResult) self.assertEqual(runner.resultclass, unittest.TextTestResult)
self.assertFalse(runner.tb_locals)
def test_multiple_inheritance(self): def test_multiple_inheritance(self):
class AResult(unittest.TestResult): class AResult(unittest.TestResult):
@ -172,14 +172,13 @@ class Test_TextTestRunner(unittest.TestCase):
# on arguments in its __init__ super call # on arguments in its __init__ super call
ATextResult(None, None, 1) ATextResult(None, None, 1)
def testBufferAndFailfast(self): def testBufferAndFailfast(self):
class Test(unittest.TestCase): class Test(unittest.TestCase):
def testFoo(self): def testFoo(self):
pass pass
result = unittest.TestResult() result = unittest.TestResult()
runner = unittest.TextTestRunner(stream=io.StringIO(), failfast=True, runner = unittest.TextTestRunner(stream=io.StringIO(), failfast=True,
buffer=True) buffer=True)
# Use our result object # Use our result object
runner._makeResult = lambda: result runner._makeResult = lambda: result
runner.run(Test('testFoo')) runner.run(Test('testFoo'))
@ -187,6 +186,11 @@ class Test_TextTestRunner(unittest.TestCase):
self.assertTrue(result.failfast) self.assertTrue(result.failfast)
self.assertTrue(result.buffer) self.assertTrue(result.buffer)
def test_locals(self):
runner = unittest.TextTestRunner(stream=io.StringIO(), tb_locals=True)
result = runner.run(unittest.TestSuite())
self.assertEqual(True, result.tb_locals)
def testRunnerRegistersResult(self): def testRunnerRegistersResult(self):
class Test(unittest.TestCase): class Test(unittest.TestCase):
def testFoo(self): def testFoo(self):

View File

@ -39,7 +39,8 @@ Library
- Issue #21619: Popen objects no longer leave a zombie after exit in the with - Issue #21619: Popen objects no longer leave a zombie after exit in the with
statement if the pipe was broken. Patch by Martin Panter. statement if the pipe was broken. Patch by Martin Panter.
- Issue #22936: Make it possible to show local variables in tracebacks. - Issue #22936: Make it possible to show local variables in tracebacks for
both the traceback module and unittest.
- Issue #15955: Add an option to limit the output size in bz2.decompress(). - Issue #15955: Add an option to limit the output size in bz2.decompress().
Patch by Nikolaus Rath. Patch by Nikolaus Rath.