Skip to content

Commit b924b96

Browse files
[3.15] gh-155411: Fix test.support.subTests() for asynchronous tests (GH-155412) (GH-155544)
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 cb34920 commit b924b96

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
@@ -1077,16 +1077,29 @@ def subTests(arg_names, arg_values, /, *, _do_cleanups=False):
10771077
def decorator(func):
10781078
if isinstance(func, type):
10791079
raise TypeError('subTests() can only decorate methods, not classes')
1080-
@functools.wraps(func)
1081-
def wrapper(self, /, *args, **kwargs):
1080+
1081+
def iter_subtest_kwargs():
10821082
for values in arg_values:
1083-
if single_param:
1084-
values = (values,)
1085-
subtest_kwargs = dict(zip(arg_names, values))
1086-
with self.subTest(**subtest_kwargs):
1087-
func(self, *args, **kwargs, **subtest_kwargs)
1088-
if _do_cleanups:
1089-
self.doCleanups()
1083+
yield dict(zip(arg_names, (values,) if single_param else values))
1084+
1085+
# A synchronous wrapper would discard the coroutine without awaiting
1086+
# it, so an asynchronous test would not run at all.
1087+
if inspect.iscoroutinefunction(func):
1088+
@functools.wraps(func)
1089+
async def wrapper(self, /, *args, **kwargs):
1090+
for subtest_kwargs in iter_subtest_kwargs():
1091+
with self.subTest(**subtest_kwargs):
1092+
await func(self, *args, **kwargs, **subtest_kwargs)
1093+
if _do_cleanups:
1094+
self.doCleanups()
1095+
else:
1096+
@functools.wraps(func)
1097+
def wrapper(self, /, *args, **kwargs):
1098+
for subtest_kwargs in iter_subtest_kwargs():
1099+
with self.subTest(**subtest_kwargs):
1100+
func(self, *args, **kwargs, **subtest_kwargs)
1101+
if _do_cleanups:
1102+
self.doCleanups()
10901103
return wrapper
10911104
return decorator
10921105

‎Lib/test/test_support.py‎

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

12601260

1261+
class TestSubTests(unittest.TestCase):
1262+
1263+
def run_test(self, cls):
1264+
result = unittest.TestResult()
1265+
cls('test_it').run(result)
1266+
return result
1267+
1268+
def test_sync(self):
1269+
ran = []
1270+
1271+
class Sample(unittest.TestCase):
1272+
@support.subTests('a', [1, 2, 3])
1273+
def test_it(self, a):
1274+
ran.append(a)
1275+
self.assertNotEqual(a, 2)
1276+
1277+
result = self.run_test(Sample)
1278+
self.assertEqual(ran, [1, 2, 3])
1279+
self.assertEqual(result.testsRun, 1)
1280+
self.assertEqual(len(result.failures), 1)
1281+
self.assertEndsWith(result.failures[0][0].id(), 'test_it (a=2)')
1282+
1283+
# Running an asyncio event loop needs a working socket.
1284+
@support.requires_working_socket()
1285+
def test_async(self):
1286+
# Running an event loop sets the event loop policy, which regrtest
1287+
# reports as a change of the environment.
1288+
import asyncio.events
1289+
self.enterContext(
1290+
support.swap_attr(asyncio.events, '_event_loop_policy', None))
1291+
1292+
# An asynchronous test must be awaited: a synchronous wrapper would
1293+
# make it silently not run at all.
1294+
ran = []
1295+
1296+
class Sample(unittest.IsolatedAsyncioTestCase):
1297+
@support.subTests('a', [1, 2, 3])
1298+
async def test_it(self, a):
1299+
ran.append(a)
1300+
self.assertNotEqual(a, 2)
1301+
1302+
result = self.run_test(Sample)
1303+
self.assertEqual(ran, [1, 2, 3])
1304+
self.assertEqual(result.testsRun, 1)
1305+
self.assertEqual(len(result.failures), 1)
1306+
self.assertEndsWith(result.failures[0][0].id(), 'test_it (a=2)')
1307+
1308+
def test_multiple_parameters(self):
1309+
ran = []
1310+
1311+
class Sample(unittest.TestCase):
1312+
@support.subTests('a,b', [(1, 'x'), (2, 'y')])
1313+
def test_it(self, a, b):
1314+
ran.append((a, b))
1315+
1316+
result = self.run_test(Sample)
1317+
self.assertTrue(result.wasSuccessful(), result.errors)
1318+
self.assertEqual(ran, [(1, 'x'), (2, 'y')])
1319+
1320+
def test_cannot_decorate_class(self):
1321+
with self.assertRaises(TypeError):
1322+
@support.subTests('a', [1])
1323+
class Sample(unittest.TestCase):
1324+
pass
1325+
1326+
12611327
if __name__ == '__main__':
12621328
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)