Skip to content

Commit 9b6315a

Browse files
committed
unittest: Add exception capturing for subTest.
1 parent 9f6f211 commit 9b6315a

File tree

2 files changed

+67
-16
lines changed

2 files changed

+67
-16
lines changed

python-stdlib/unittest/test_unittest.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,14 @@ def test_NotChangedByOtherTest(self):
148148
assert global_context is None
149149
global_context = True
150150

151+
def test_subtest_even(self):
152+
"""
153+
Test that numbers between 0 and 5 are all even.
154+
"""
155+
for i in range(0, 10, 2):
156+
with self.subTest("Should only pass for even numbers", i=i):
157+
self.assertEqual(i % 2, 0)
158+
151159

152160
if __name__ == "__main__":
153161
unittest.main()

python-stdlib/unittest/unittest.py

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,35 @@ def __exit__(self, exc_type, exc_value, tb):
3737
return False
3838

3939

40+
# These are used to provide required context to things like subTest
41+
__current_test__ = None
42+
__test_result__ = None
43+
44+
45+
class SubtestContext:
46+
def __enter__(self):
47+
pass
48+
49+
def __exit__(self, *exc_info):
50+
if exc_info[0] is not None:
51+
# Exception raised
52+
global __test_result__, __current_test__
53+
handle_test_exception(
54+
__current_test__,
55+
__test_result__,
56+
exc_info
57+
)
58+
# Suppress the exception as we've captured it above
59+
return True
60+
61+
62+
63+
4064
class NullContext:
4165
def __enter__(self):
4266
pass
4367

44-
def __exit__(self, a, b, c):
68+
def __exit__(self, exc_type, exc_value, traceback):
4569
pass
4670

4771

@@ -61,7 +85,7 @@ def doCleanups(self):
6185
func(*args, **kwargs)
6286

6387
def subTest(self, msg=None, **params):
64-
return NullContext()
88+
return SubtestContext(msg=msg, params=params)
6589

6690
def skipTest(self, reason):
6791
raise SkipTest(reason)
@@ -298,15 +322,29 @@ def __add__(self, other):
298322
return self
299323

300324

301-
def capture_exc(e):
325+
def capture_exc(exc, traceback):
302326
buf = io.StringIO()
303327
if hasattr(sys, "print_exception"):
304-
sys.print_exception(e, buf)
328+
sys.print_exception(exc, buf)
305329
elif traceback is not None:
306-
traceback.print_exception(None, e, sys.exc_info()[2], file=buf)
330+
traceback.print_exception(None, exc, traceback, file=buf)
307331
return buf.getvalue()
308332

309333

334+
def handle_test_exception(current_test: tuple, test_result: TestResult, exc_info: tuple):
335+
exc = exc_info[1]
336+
traceback = exc_info[2]
337+
ex_str = capture_exc(exc, traceback)
338+
if isinstance(exc, AssertionError):
339+
test_result.failuresNum += 1
340+
test_result.failures.append((current_test, ex_str))
341+
print(" FAIL")
342+
else:
343+
test_result.errorsNum += 1
344+
test_result.errors.append((current_test, ex_str))
345+
print(" ERROR")
346+
347+
310348
def run_suite(c, test_result, suite_name=""):
311349
if isinstance(c, TestSuite):
312350
c.run(test_result)
@@ -324,29 +362,34 @@ def run_suite(c, test_result, suite_name=""):
324362
except AttributeError:
325363
pass
326364

327-
def run_one(m):
365+
def run_one(test_function):
366+
global __test_result__, __current_test__
328367
print("%s (%s) ..." % (name, suite_name), end="")
329368
set_up()
369+
__test_result__ = test_result
370+
test_container = f"({suite_name})"
371+
__current_test__ = (name, test_container)
330372
try:
331373
test_result.testsRun += 1
332-
m()
374+
test_globals = dict(**globals())
375+
test_globals["test_function"] = test_function
376+
exec("test_function()", test_globals, test_globals)
377+
# No exception occurred, test passed
333378
print(" ok")
334379
except SkipTest as e:
335380
print(" skipped:", e.args[0])
336381
test_result.skippedNum += 1
337382
except Exception as ex:
338-
ex_str = capture_exc(ex)
339-
if isinstance(ex, AssertionError):
340-
test_result.failuresNum += 1
341-
test_result.failures.append(((name, c), ex_str))
342-
print(" FAIL")
343-
else:
344-
test_result.errorsNum += 1
345-
test_result.errors.append(((name, c), ex_str))
346-
print(" ERROR")
383+
handle_test_exception(
384+
current_test=(name, c),
385+
test_result=test_result,
386+
exc_info=sys.exc_info()
387+
)
347388
# Uncomment to investigate failure in detail
348389
# raise
349390
finally:
391+
__test_result__ = None
392+
__current_test__ = None
350393
tear_down()
351394
try:
352395
o.doCleanups()

0 commit comments

Comments
 (0)