Skip to content

Commit f0f29f3

Browse files
bpo-25894: Always report skipped and failed subtests separately (GH-28082)
* In default mode output separate characters for skipped and failed subtests. * In verbose mode output separate lines (including description) for skipped and failed subtests. * In verbose mode output test description for errors in test cleanup.
1 parent ab327f2 commit f0f29f3

File tree

3 files changed

+189
-49
lines changed

3 files changed

+189
-49
lines changed

Lib/unittest/runner.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import warnings
66

77
from . import result
8+
from .case import _SubTest
89
from .signals import registerResult
910

1011
__unittest = True
@@ -40,6 +41,7 @@ def __init__(self, stream, descriptions, verbosity):
4041
self.showAll = verbosity > 1
4142
self.dots = verbosity == 1
4243
self.descriptions = descriptions
44+
self._newline = True
4345

4446
def getDescription(self, test):
4547
doc_first_line = test.shortDescription()
@@ -54,35 +56,63 @@ def startTest(self, test):
5456
self.stream.write(self.getDescription(test))
5557
self.stream.write(" ... ")
5658
self.stream.flush()
59+
self._newline = False
60+
61+
def _write_status(self, test, status):
62+
is_subtest = isinstance(test, _SubTest)
63+
if is_subtest or self._newline:
64+
if not self._newline:
65+
self.stream.writeln()
66+
if is_subtest:
67+
self.stream.write(" ")
68+
self.stream.write(self.getDescription(test))
69+
self.stream.write(" ... ")
70+
self.stream.writeln(status)
71+
self._newline = True
72+
73+
def addSubTest(self, test, subtest, err):
74+
if err is not None:
75+
if self.showAll:
76+
if issubclass(err[0], subtest.failureException):
77+
self._write_status(subtest, "FAIL")
78+
else:
79+
self._write_status(subtest, "ERROR")
80+
elif self.dots:
81+
if issubclass(err[0], subtest.failureException):
82+
self.stream.write('F')
83+
else:
84+
self.stream.write('E')
85+
self.stream.flush()
86+
super(TextTestResult, self).addSubTest(test, subtest, err)
5787

5888
def addSuccess(self, test):
5989
super(TextTestResult, self).addSuccess(test)
6090
if self.showAll:
61-
self.stream.writeln("ok")
91+
self._write_status(test, "ok")
6292
elif self.dots:
6393
self.stream.write('.')
6494
self.stream.flush()
6595

6696
def addError(self, test, err):
6797
super(TextTestResult, self).addError(test, err)
6898
if self.showAll:
69-
self.stream.writeln("ERROR")
99+
self._write_status(test, "ERROR")
70100
elif self.dots:
71101
self.stream.write('E')
72102
self.stream.flush()
73103

74104
def addFailure(self, test, err):
75105
super(TextTestResult, self).addFailure(test, err)
76106
if self.showAll:
77-
self.stream.writeln("FAIL")
107+
self._write_status(test, "FAIL")
78108
elif self.dots:
79109
self.stream.write('F')
80110
self.stream.flush()
81111

82112
def addSkip(self, test, reason):
83113
super(TextTestResult, self).addSkip(test, reason)
84114
if self.showAll:
85-
self.stream.writeln("skipped {0!r}".format(reason))
115+
self._write_status(test, "skipped {0!r}".format(reason))
86116
elif self.dots:
87117
self.stream.write("s")
88118
self.stream.flush()

Lib/unittest/test/test_result.py

Lines changed: 151 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -305,37 +305,76 @@ def test_1(self):
305305
self.assertIs(test_case, subtest)
306306
self.assertIn("some recognizable failure", formatted_exc)
307307

308+
def testStackFrameTrimming(self):
309+
class Frame(object):
310+
class tb_frame(object):
311+
f_globals = {}
312+
result = unittest.TestResult()
313+
self.assertFalse(result._is_relevant_tb_level(Frame))
314+
315+
Frame.tb_frame.f_globals['__unittest'] = True
316+
self.assertTrue(result._is_relevant_tb_level(Frame))
317+
318+
def testFailFast(self):
319+
result = unittest.TestResult()
320+
result._exc_info_to_string = lambda *_: ''
321+
result.failfast = True
322+
result.addError(None, None)
323+
self.assertTrue(result.shouldStop)
324+
325+
result = unittest.TestResult()
326+
result._exc_info_to_string = lambda *_: ''
327+
result.failfast = True
328+
result.addFailure(None, None)
329+
self.assertTrue(result.shouldStop)
330+
331+
result = unittest.TestResult()
332+
result._exc_info_to_string = lambda *_: ''
333+
result.failfast = True
334+
result.addUnexpectedSuccess(None)
335+
self.assertTrue(result.shouldStop)
336+
337+
def testFailFastSetByRunner(self):
338+
runner = unittest.TextTestRunner(stream=io.StringIO(), failfast=True)
339+
def test(result):
340+
self.assertTrue(result.failfast)
341+
result = runner.run(test)
342+
343+
344+
class Test_TextTestResult(unittest.TestCase):
345+
maxDiff = None
346+
308347
def testGetDescriptionWithoutDocstring(self):
309348
result = unittest.TextTestResult(None, True, 1)
310349
self.assertEqual(
311350
result.getDescription(self),
312351
'testGetDescriptionWithoutDocstring (' + __name__ +
313-
'.Test_TestResult)')
352+
'.Test_TextTestResult)')
314353

315354
def testGetSubTestDescriptionWithoutDocstring(self):
316355
with self.subTest(foo=1, bar=2):
317356
result = unittest.TextTestResult(None, True, 1)
318357
self.assertEqual(
319358
result.getDescription(self._subtest),
320359
'testGetSubTestDescriptionWithoutDocstring (' + __name__ +
321-
'.Test_TestResult) (foo=1, bar=2)')
360+
'.Test_TextTestResult) (foo=1, bar=2)')
322361
with self.subTest('some message'):
323362
result = unittest.TextTestResult(None, True, 1)
324363
self.assertEqual(
325364
result.getDescription(self._subtest),
326365
'testGetSubTestDescriptionWithoutDocstring (' + __name__ +
327-
'.Test_TestResult) [some message]')
366+
'.Test_TextTestResult) [some message]')
328367

329368
def testGetSubTestDescriptionWithoutDocstringAndParams(self):
330369
with self.subTest():
331370
result = unittest.TextTestResult(None, True, 1)
332371
self.assertEqual(
333372
result.getDescription(self._subtest),
334373
'testGetSubTestDescriptionWithoutDocstringAndParams '
335-
'(' + __name__ + '.Test_TestResult) (<subtest>)')
374+
'(' + __name__ + '.Test_TextTestResult) (<subtest>)')
336375

337376
def testGetSubTestDescriptionForFalsyValues(self):
338-
expected = 'testGetSubTestDescriptionForFalsyValues (%s.Test_TestResult) [%s]'
377+
expected = 'testGetSubTestDescriptionForFalsyValues (%s.Test_TextTestResult) [%s]'
339378
result = unittest.TextTestResult(None, True, 1)
340379
for arg in [0, None, []]:
341380
with self.subTest(arg):
@@ -351,7 +390,7 @@ def testGetNestedSubTestDescriptionWithoutDocstring(self):
351390
self.assertEqual(
352391
result.getDescription(self._subtest),
353392
'testGetNestedSubTestDescriptionWithoutDocstring '
354-
'(' + __name__ + '.Test_TestResult) (baz=2, bar=3, foo=1)')
393+
'(' + __name__ + '.Test_TextTestResult) (baz=2, bar=3, foo=1)')
355394

356395
def testGetDuplicatedNestedSubTestDescriptionWithoutDocstring(self):
357396
with self.subTest(foo=1, bar=2):
@@ -360,7 +399,7 @@ def testGetDuplicatedNestedSubTestDescriptionWithoutDocstring(self):
360399
self.assertEqual(
361400
result.getDescription(self._subtest),
362401
'testGetDuplicatedNestedSubTestDescriptionWithoutDocstring '
363-
'(' + __name__ + '.Test_TestResult) (baz=3, bar=4, foo=1)')
402+
'(' + __name__ + '.Test_TextTestResult) (baz=3, bar=4, foo=1)')
364403

365404
@unittest.skipIf(sys.flags.optimize >= 2,
366405
"Docstrings are omitted with -O2 and above")
@@ -370,7 +409,7 @@ def testGetDescriptionWithOneLineDocstring(self):
370409
self.assertEqual(
371410
result.getDescription(self),
372411
('testGetDescriptionWithOneLineDocstring '
373-
'(' + __name__ + '.Test_TestResult)\n'
412+
'(' + __name__ + '.Test_TextTestResult)\n'
374413
'Tests getDescription() for a method with a docstring.'))
375414

376415
@unittest.skipIf(sys.flags.optimize >= 2,
@@ -382,7 +421,7 @@ def testGetSubTestDescriptionWithOneLineDocstring(self):
382421
self.assertEqual(
383422
result.getDescription(self._subtest),
384423
('testGetSubTestDescriptionWithOneLineDocstring '
385-
'(' + __name__ + '.Test_TestResult) (foo=1, bar=2)\n'
424+
'(' + __name__ + '.Test_TextTestResult) (foo=1, bar=2)\n'
386425
'Tests getDescription() for a method with a docstring.'))
387426

388427
@unittest.skipIf(sys.flags.optimize >= 2,
@@ -395,7 +434,7 @@ def testGetDescriptionWithMultiLineDocstring(self):
395434
self.assertEqual(
396435
result.getDescription(self),
397436
('testGetDescriptionWithMultiLineDocstring '
398-
'(' + __name__ + '.Test_TestResult)\n'
437+
'(' + __name__ + '.Test_TextTestResult)\n'
399438
'Tests getDescription() for a method with a longer '
400439
'docstring.'))
401440

@@ -410,44 +449,111 @@ def testGetSubTestDescriptionWithMultiLineDocstring(self):
410449
self.assertEqual(
411450
result.getDescription(self._subtest),
412451
('testGetSubTestDescriptionWithMultiLineDocstring '
413-
'(' + __name__ + '.Test_TestResult) (foo=1, bar=2)\n'
452+
'(' + __name__ + '.Test_TextTestResult) (foo=1, bar=2)\n'
414453
'Tests getDescription() for a method with a longer '
415454
'docstring.'))
416455

417-
def testStackFrameTrimming(self):
418-
class Frame(object):
419-
class tb_frame(object):
420-
f_globals = {}
421-
result = unittest.TestResult()
422-
self.assertFalse(result._is_relevant_tb_level(Frame))
423-
424-
Frame.tb_frame.f_globals['__unittest'] = True
425-
self.assertTrue(result._is_relevant_tb_level(Frame))
426-
427-
def testFailFast(self):
428-
result = unittest.TestResult()
429-
result._exc_info_to_string = lambda *_: ''
430-
result.failfast = True
431-
result.addError(None, None)
432-
self.assertTrue(result.shouldStop)
433-
434-
result = unittest.TestResult()
435-
result._exc_info_to_string = lambda *_: ''
436-
result.failfast = True
437-
result.addFailure(None, None)
438-
self.assertTrue(result.shouldStop)
439-
440-
result = unittest.TestResult()
441-
result._exc_info_to_string = lambda *_: ''
442-
result.failfast = True
443-
result.addUnexpectedSuccess(None)
444-
self.assertTrue(result.shouldStop)
445-
446-
def testFailFastSetByRunner(self):
447-
runner = unittest.TextTestRunner(stream=io.StringIO(), failfast=True)
448-
def test(result):
449-
self.assertTrue(result.failfast)
450-
result = runner.run(test)
456+
class Test(unittest.TestCase):
457+
def testSuccess(self):
458+
pass
459+
def testSkip(self):
460+
self.skipTest('skip')
461+
def testFail(self):
462+
self.fail('fail')
463+
def testError(self):
464+
raise Exception('error')
465+
def testSubTestSuccess(self):
466+
with self.subTest('one', a=1):
467+
pass
468+
with self.subTest('two', b=2):
469+
pass
470+
def testSubTestMixed(self):
471+
with self.subTest('success', a=1):
472+
pass
473+
with self.subTest('skip', b=2):
474+
self.skipTest('skip')
475+
with self.subTest('fail', c=3):
476+
self.fail('fail')
477+
with self.subTest('error', d=4):
478+
raise Exception('error')
479+
480+
tearDownError = None
481+
def tearDown(self):
482+
if self.tearDownError is not None:
483+
raise self.tearDownError
484+
485+
def _run_test(self, test_name, verbosity, tearDownError=None):
486+
stream = io.StringIO()
487+
stream = unittest.runner._WritelnDecorator(stream)
488+
result = unittest.TextTestResult(stream, True, verbosity)
489+
test = self.Test(test_name)
490+
test.tearDownError = tearDownError
491+
test.run(result)
492+
return stream.getvalue()
493+
494+
def testDotsOutput(self):
495+
self.assertEqual(self._run_test('testSuccess', 1), '.')
496+
self.assertEqual(self._run_test('testSkip', 1), 's')
497+
self.assertEqual(self._run_test('testFail', 1), 'F')
498+
self.assertEqual(self._run_test('testError', 1), 'E')
499+
500+
def testLongOutput(self):
501+
classname = f'{__name__}.{self.Test.__qualname__}'
502+
self.assertEqual(self._run_test('testSuccess', 2),
503+
f'testSuccess ({classname}) ... ok\n')
504+
self.assertEqual(self._run_test('testSkip', 2),
505+
f"testSkip ({classname}) ... skipped 'skip'\n")
506+
self.assertEqual(self._run_test('testFail', 2),
507+
f'testFail ({classname}) ... FAIL\n')
508+
self.assertEqual(self._run_test('testError', 2),
509+
f'testError ({classname}) ... ERROR\n')
510+
511+
def testDotsOutputSubTestSuccess(self):
512+
self.assertEqual(self._run_test('testSubTestSuccess', 1), '.')
513+
514+
def testLongOutputSubTestSuccess(self):
515+
classname = f'{__name__}.{self.Test.__qualname__}'
516+
self.assertEqual(self._run_test('testSubTestSuccess', 2),
517+
f'testSubTestSuccess ({classname}) ... ok\n')
518+
519+
def testDotsOutputSubTestMixed(self):
520+
self.assertEqual(self._run_test('testSubTestMixed', 1), 'sFE')
521+
522+
def testLongOutputSubTestMixed(self):
523+
classname = f'{__name__}.{self.Test.__qualname__}'
524+
self.assertEqual(self._run_test('testSubTestMixed', 2),
525+
f'testSubTestMixed ({classname}) ... \n'
526+
f" testSubTestMixed ({classname}) [skip] (b=2) ... skipped 'skip'\n"
527+
f' testSubTestMixed ({classname}) [fail] (c=3) ... FAIL\n'
528+
f' testSubTestMixed ({classname}) [error] (d=4) ... ERROR\n')
529+
530+
def testDotsOutputTearDownFail(self):
531+
out = self._run_test('testSuccess', 1, AssertionError('fail'))
532+
self.assertEqual(out, 'F')
533+
out = self._run_test('testError', 1, AssertionError('fail'))
534+
self.assertEqual(out, 'EF')
535+
out = self._run_test('testFail', 1, Exception('error'))
536+
self.assertEqual(out, 'FE')
537+
out = self._run_test('testSkip', 1, AssertionError('fail'))
538+
self.assertEqual(out, 'sF')
539+
540+
def testLongOutputTearDownFail(self):
541+
classname = f'{__name__}.{self.Test.__qualname__}'
542+
out = self._run_test('testSuccess', 2, AssertionError('fail'))
543+
self.assertEqual(out,
544+
f'testSuccess ({classname}) ... FAIL\n')
545+
out = self._run_test('testError', 2, AssertionError('fail'))
546+
self.assertEqual(out,
547+
f'testError ({classname}) ... ERROR\n'
548+
f'testError ({classname}) ... FAIL\n')
549+
out = self._run_test('testFail', 2, Exception('error'))
550+
self.assertEqual(out,
551+
f'testFail ({classname}) ... FAIL\n'
552+
f'testFail ({classname}) ... ERROR\n')
553+
out = self._run_test('testSkip', 2, AssertionError('fail'))
554+
self.assertEqual(out,
555+
f"testSkip ({classname}) ... skipped 'skip'\n"
556+
f'testSkip ({classname}) ... FAIL\n')
451557

452558

453559
classDict = dict(unittest.TestResult.__dict__)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
:mod:`unittest` now always reports skipped and failed subtests separately:
2+
separate characters in default mode and separate lines in verbose mode. Also
3+
the test description is now output for errors in test method, class and
4+
module cleanups.

0 commit comments

Comments
 (0)