8000 gh-134565: Use ExceptionGroup to handle multiple errors in unittest.d… · faster-cpython/cpython@393773a · GitHub
[go: up one dir, main page]

Skip to content

Commit 393773a

Browse files
pythongh-134565: Use ExceptionGroup to handle multiple errors in unittest.doModuleCleanups() (pythonGH-134566)
1 parent 77eade3 commit 393773a

File tree

5 files changed

+101
-15
lines changed

5 files changed

+101
-15
lines changed
Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1282,14 +1282,22 @@ def setUpModule():
12821282
suite(result)
12831283
expected_out = '\nStdout:\ndo cleanup2\ndo cleanup1\n'
12841284
self.assertEqual(stdout.getvalue(), expected_out)
1285-
self.assertEqual(len(result.errors), 1)
1285+
self.assertEqual(len(result.errors), 2)
12861286
description = 'tearDownModule (Module)'
12871287
test_case, formatted_exc = result.errors[0]
12881288
self.assertEqual(test_case.description, description)
12891289
self.assertIn('ValueError: bad cleanup2', formatted_exc)
1290+
self.assertNotIn('ExceptionGroup', formatted_exc)
12901291
self.assertNotIn('TypeError', formatted_exc)
12911292
self.assertIn(expected_out, formatted_exc)
12921293

1294+
test_case, formatted_exc = result.errors[1]
1295+
self.assertEqual(test_case.description, description)
1296+
self.assertIn('TypeError: bad cleanup1', formatted_exc)
1297+
self.assertNotIn('ExceptionGroup', formatted_exc)
1298+
self.assertNotIn('ValueError', formatted_exc)
1299+
self.assertIn(expected_out, formatted_exc)
1300+
12931301
def testBufferSetUpModule_DoModuleCleanups(self):
12941302
with captured_stdout() as stdout:
12951303
result = unittest.TestResult()
@@ -1313,22 +1321,34 @@ def setUpModule():
13131321
suite(result)
13141322
expected_out = '\nStdout:\nset up module\ndo cleanup2\ndo cleanup1\n'
13151323
self.assertEqual(stdout.getvalue(), expected_out)
1316-
self.assertEqual(len(result.errors), 2)
1324+
self.assertEqual(len(result.errors), 3)
13171325
description = 'setUpModule (Module)'
13181326
test_case, formatted_exc = result.errors[0]
13191327
self.assertEqual(test_case.description, description)
13201328
self.assertIn('ZeroDivisionError: division by zero', formatted_exc)
1329+
self.assertNotIn('ExceptionGroup', formatted_exc)
13211330
self.assertNotIn('ValueError', formatted_exc)
13221331
self.assertNotIn('TypeError', formatted_exc)
13231332
self.assertIn('\nStdout:\nset up module\n', formatted_exc)
1333+
13241334
test_case, formatted_exc = result.errors[1]
13251335
self.assertIn(expected_out, formatted_exc)
13261336
self.assertEqual(test_case.description, description)
13271337
self.assertIn('ValueError: bad cleanup2', formatted_exc)
1338+
self.assertNotIn('ExceptionGroup', formatted_exc)
13281339
self.assertNotIn('ZeroDivisionError', formatted_exc)
13291340
self.assertNotIn('TypeError', formatted_exc)
13301341
self.assertIn(expected_out, formatted_exc)
13311342

1343+
test_case, formatted_exc = result.errors[2]
1344+
self.assertIn(expected_out, formatted_exc)
1345+
self.assertEqual(test_case.description, description)
1346+
self.assertIn('TypeError: bad cleanup1', formatted_exc)
1347+
self.assertNotIn('ExceptionGroup', formatted_exc)
1348+
self.assertNotIn('ZeroDivisionError', formatted_exc)
1349+
self.assertNotIn('ValueError', formatted_exc)
1350+
self.assertIn(expected_out, formatted_exc)
1351+
13321352
def testBufferTearDownModule_DoModuleCleanups(self):
13331353
with captured_stdout() as stdout:
13341354
result = unittest.TestResult()
@@ -1355,21 +1375,32 @@ def tearDownModule():
13551375
suite(result)
13561376
expected_out = '\nStdout:\ntear down module\ndo cleanup2\ndo cleanup1\n'
13571377
self.assertEqual(stdout.getvalue(), expected_out)
1358-
self.assertEqual(len(result.errors), 2)
1378+
self.assertEqual(len(result.errors), 3)
13591379
description = 'tearDownModule (Module)'
13601380
test_case, formatted_exc = result.errors[0]
13611381
self.assertEqual(test_case.description, description)
13621382
self.assertIn('ZeroDivisionError: division by zero', formatted_exc)
1383+
self.assertNotIn('ExceptionGroup', formatted_exc)
13631384
self.assertNotIn('ValueError', formatted_exc)
13641385
self.assertNotIn('TypeError', formatted_exc)
13651386
self.assertIn('\nStdout:\ntear down module\n', formatted_exc)
1387+
13661388
test_case, formatted_exc = result.errors[1]
13671389
self.assertEqual(test_case.description, description)
13681390
self.assertIn('ValueError: bad cleanup2', formatted_exc)
1391+
self.assertNotIn('ExceptionGroup', formatted_exc)
13691392
self.assertNotIn('ZeroDivisionError', formatted_exc)
13701393
self.assertNotIn('TypeError', formatted_exc)
13711394
self.assertIn(expected_out, formatted_exc)
13721395

1396+
test_case, formatted_exc = result.errors[2]
1397+
self.assertEqual(test_case.description, description)
1398+
self.assertIn('TypeError: bad cleanup1', formatted_exc)
1399+
self.assertNotIn('ExceptionGroup', formatted_exc)
1400+
self.assertNotIn('ZeroDivisionError', formatted_exc)
1401+
self.assertNotIn('ValueError', formatted_exc)
1402+
self.assertIn(expected_out, formatted_exc)
1403+
13731404

13741405
if __name__ == '__main__':
13751406
unittest.main()

Lib/test/test_unittest/test_runner.py

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
LoggingResult,
1414
ResultWithNoStartTestRunStopTestRun,
1515
)
16+
from test.support.testcase import ExceptionIsLikeMixin
1617

1718

1819
def resultFactory(*_):
@@ -604,7 +605,7 @@ class EmptyTest(unittest.TestCase):
604605

605606

606607
@support.force_not_colorized_test_class
607-
class TestModuleCleanUp(unittest.TestCase):
608+
class TestModuleCleanUp(ExceptionIsLikeMixin, unittest.TestCase):
608609
def test_add_and_do_ModuleCleanup(self):
609610
module_cleanups = []
610611

@@ -646,11 +647,50 @@ class Module(object):
646647
[(module_cleanup_good, (1, 2, 3),
647648
dict(four='hello', five='goodbye')),
648649
(module_cleanup_bad, (), {})])
649-
with self.assertRaises(CustomError) as e:
650+
with self.assertRaises(Exception) as e:
650651
unittest.case.doModuleCleanups()
651-
self.assertEqual(str(e.exception), 'CleanUpExc')
652+
self.assertExceptionIsLike(e.exception,
653+
ExceptionGroup('module cleanup failed',
654+
[CustomError('CleanUpExc')]))
652655
self.assertEqual(unittest.case._module_cleanups, [])
653656

657+
def test_doModuleCleanup_with_multiple_errors_in_addModuleCleanup(self):
658+
def module_cleanup_bad1():
659+
raise TypeError('CleanUpExc1')
660+
661+
def module_cleanup_bad2():
662+
raise ValueError('CleanUpExc2')
663+
664+
class Module:
665+
unittest.addModuleCleanup(module_cleanup_bad1)
666+
unittest.addModuleCleanup(module_cleanup_bad2)
667+
with self.assertRaises(ExceptionGroup) as e:
668+
unittest.case.doModuleCleanups()
669+
self.assertExceptionIsLike(e.exception,
670+
ExceptionGroup('module cleanup failed', [
671+
ValueError('CleanUpExc2'),
672+
TypeError('CleanUpExc1'),
673+
]))
674+
675+
def test_doModuleCleanup_with_exception_group_in_addModuleCleanup(self):
676+
def module_cleanup_bad():
677+
raise ExceptionGroup('CleanUpExc', [
678+
ValueError('CleanUpExc2'),
679+
TypeError('CleanUpExc1'),
680+
])
681+
682+
class Module:
683+
unittest.addModuleCleanup(module_cleanup_bad)
684+
with self.assertRaises(ExceptionGroup) as e:
685+
unittest.case.doModuleCleanups()
686+
self.assertExceptionIsLike(e.exception,
687+
ExceptionGroup('module cleanup failed', [
688+
ExceptionGroup('CleanUpExc', [
689+
ValueError('CleanUpExc2'),
690+
TypeError('CleanUpExc1'),
691+
]),
692+
]))
693+
654694
def test_addModuleCleanup_arg_errors(self):
655695
cleanups = []
656696
def cleanup(*args, **kwargs):
@@ -871,9 +911,11 @@ def tearDownClass(cls):
871911
ordering = []
872912
blowUp = True
873913
suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestableTest)
874-
with self.assertRaises(CustomError) as cm:
914+
with self.assertRaises(Exception) as cm:
875915
suite.debug()
876-
self.assertEqual(str(cm.exception), 'CleanUpExc')
916+
self.assertExceptionIsLike(cm.exception,
917+
ExceptionGroup('module cleanup failed',
918+
[CustomError('CleanUpExc')]))
877919
self.assertEqual(ordering, ['setUpModule', 'setUpClass', 'test',
878920
'tearDownClass', 'tearDownModule', 'cleanup_exc'])
879921
self.assertEqual(unittest.case._module_cleanups, [])

Lib/unittest/case.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,9 +149,7 @@ def doModuleCleanups():
149149
except Exception as exc:
150150
exceptions.append(exc)
151151
if exceptions:
152-
# Swallows all but first exception. If a multi-exception handler
153-
# gets written we should use that here instead.
154-
raise exceptions[0]
152+
raise ExceptionGroup('module cleanup failed', exceptions)
155153

156154

157155
def skip(reason):

Lib/unittest/suite.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,11 @@ def _handleModuleFixture(self, test, result):
223223
if result._moduleSetUpFailed:
224224
try:
225225
case.doModuleCleanups()
226+
except ExceptionGroup as eg:
227+
for e in eg.exceptions:
228+
self._createClassOrModuleLevelException(result, e,
229+
'setUpModule',
230+
currentModule)
226231
except Exception as e:
227232
self._createClassOrModuleLevelException(result, e,
228233
'setUpModule',
@@ -235,15 +240,15 @@ def _createClassOrModuleLevelException(self, result, exc, method_name,
235240
errorName = f'{method_name} ({parent})'
236241
self._addClassOrModuleLevelException(result, exc, errorName, info)
237242

238-
def _addClassOrModuleLevelException(self, result, exception, errorName,
243+
def _addClassOrModuleLevelException(self, result, exc, errorName,
239244
info=None):
240245
error = _ErrorHolder(errorName)
241246
addSkip = getattr(result, 'addSkip', None)
242-
if addSkip is not None and isinstance(exception, case.SkipTest):
243-
addSkip(error, str(exception))
247+
if addSkip is not None and isinstance(exc, case.SkipTest):
248+
addSkip(error, str(exc))
244249
else:
245250
if not info:
246-
result.addError(error, sys.exc_info())
251+
result.addError(error, (type(exc), exc, exc.__traceback__))
247252
else:
248253
result.addError(error, info)
249254

@@ -273,6 +278,13 @@ def _handleModuleTearDown(self, result):
273278
previousModule)
274279
try:
275280
case.doModuleCleanups()
281+
except ExceptionGroup as eg:
282+
if isinstance(result, _DebugResult):
283+
raise
284+
for e in eg.exceptions:
285+
self._createClassOrModuleLevelException(result, e,
286+
'tearDownModule',
287+
previousModule)
276288
except Exception as e:
277289
if isinstance(result, _DebugResult):
278290
raise
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:func:`unittest.doModuleCleanups` no longer swallows all but first exception
2+
raised in the cleanup code, but raises a :exc:`ExceptionGroup` if multiple
3+
errors occurred.

0 commit comments

Comments
 (0)
0