Skip to content

Commit 5a2a13a

Browse files
[3.14] gh-155411: Fix test.support.subTests() for asynchronous tests (GH-155412) (GH-155545)
An asynchronous test was wrapped in a synchronous function, which discarded the coroutine without awaiting it, so the test did not run at all and was reported as successful. (cherry picked from commit 198a835) Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
1 parent 986c5b0 commit 5a2a13a

3 files changed

Lines changed: 91 additions & 9 deletions

File tree

Lib/test/support/__init__.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -988,16 +988,29 @@ def subTests(arg_names, arg_values, /, *, _do_cleanups=False):
988988
def decorator(func):
989989
if isinstance(func, type):
990990
raise TypeError('subTests() can only decorate methods, not classes')
991-
@functools.wraps(func)
992-
def wrapper(self, /, *args, **kwargs):
991+
992+
def iter_subtest_kwargs():
993993
for values in arg_values:
994-
if single_param:
995-
values = (values,)
996-
subtest_kwargs = dict(zip(arg_names, values))
997-
with self.subTest(**subtest_kwargs):
998-
func(self, *args, **kwargs, **subtest_kwargs)
999-
if _do_cleanups:
1000-
self.doCleanups()
994+
yield dict(zip(arg_names, (values,) if single_param else values))
995+
996+
# A synchronous wrapper would discard the coroutine without awaiting
997+
# it, so an asynchronous test would not run at all.
998+
if inspect.iscoroutinefunction(func):
999+
@functools.wraps(func)
1000+
async def wrapper(self, /, *args, **kwargs):
1001+
for subtest_kwargs in iter_subtest_kwargs():
1002+
with self.subTest(**subtest_kwargs):
1003+
await func(self, *args, **kwargs, **subtest_kwargs)
1004+
if _do_cleanups:
1005+
self.doCleanups()
1006+
else:
1007+
@functools.wraps(func)
1008+
def wrapper(self, /, *args, **kwargs):
1009+
for subtest_kwargs in iter_subtest_kwargs():
1010+
with self.subTest(**subtest_kwargs):
1011+
func(self, *args, **kwargs, **subtest_kwargs)
1012+
if _do_cleanups:
1013+
self.doCleanups()
10011014
return wrapper
10021015
return decorator
10031016

Lib/test/test_support.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1004,5 +1004,71 @@ def test_skipped_without_subprocess_support(self):
10041004
self.assertEqual(calls, [])
10051005

10061006

1007+
class TestSubTests(unittest.TestCase):
1008+
1009+
def run_test(self, cls):
1010+
result = unittest.TestResult()
1011+
cls('test_it').run(result)
1012+
return result
1013+
1014+
def test_sync(self):
1015+
ran = []
1016+
1017+
class Sample(unittest.TestCase):
1018+
@support.subTests('a', [1, 2, 3])
1019+
def test_it(self, a):
1020+
ran.append(a)
1021+
self.assertNotEqual(a, 2)
1022+
1023+
result = self.run_test(Sample)
1024+
self.assertEqual(ran, [1, 2, 3])
1025+
self.assertEqual(result.testsRun, 1)
1026+
self.assertEqual(len(result.failures), 1)
1027+
self.assertEndsWith(result.failures[0][0].id(), 'test_it (a=2)')
1028+
1029+
# Running an asyncio event loop needs a working socket.
1030+
@support.requires_working_socket()
1031+
def test_async(self):
1032+
# Running an event loop sets the event loop policy, which regrtest
1033+
# reports as a change of the environment.
1034+
import asyncio.events
1035+
self.enterContext(
1036+
support.swap_attr(asyncio.events, '_event_loop_policy', None))
1037+
1038+
# An asynchronous test must be awaited: a synchronous wrapper would
1039+
# make it silently not run at all.
1040+
ran = []
1041+
1042+
class Sample(unittest.IsolatedAsyncioTestCase):
1043+
@support.subTests('a', [1, 2, 3])
1044+
async def test_it(self, a):
1045+
ran.append(a)
1046+
self.assertNotEqual(a, 2)
1047+
1048+
result = self.run_test(Sample)
1049+
self.assertEqual(ran, [1, 2, 3])
1050+
self.assertEqual(result.testsRun, 1)
1051+
self.assertEqual(len(result.failures), 1)
1052+
self.assertEndsWith(result.failures[0][0].id(), 'test_it (a=2)')
1053+
1054+
def test_multiple_parameters(self):
1055+
ran = []
1056+
1057+
class Sample(unittest.TestCase):
1058+
@support.subTests('a,b', [(1, 'x'), (2, 'y')])
1059+
def test_it(self, a, b):
1060+
ran.append((a, b))
1061+
1062+
result = self.run_test(Sample)
1063+
self.assertTrue(result.wasSuccessful(), result.errors)
1064+
self.assertEqual(ran, [(1, 'x'), (2, 'y')])
1065+
1066+
def test_cannot_decorate_class(self):
1067+
with self.assertRaises(TypeError):
1068+
@support.subTests('a', [1])
1069+
class Sample(unittest.TestCase):
1070+
pass
1071+
1072+
10071073
if __name__ == '__main__':
10081074
unittest.main()
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix :func:`!test.support.subTests` for asynchronous test methods. They were
2+
wrapped in a synchronous function, which discarded the coroutine without
3+
awaiting it, so the test silently did not run at all.

0 commit comments

Comments
 (0)