Skip to content

Commit f6dc38b

Browse files
[3.13] gh-155411: Fix test.support.subTests() for asynchronous tests (GH-155412) (GH-155546)
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 7fd9801 commit f6dc38b

3 files changed

Lines changed: 92 additions & 9 deletions

File tree

Lib/test/support/__init__.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import contextlib
77
import dataclasses
88
import functools
9+
import inspect
910
import logging
1011
import _opcode
1112
import os
@@ -976,16 +977,29 @@ def subTests(arg_names, arg_values, /, *, _do_cleanups=False):
976977
def decorator(func):
977978
if isinstance(func, type):
978979
raise TypeError('subTests() can only decorate methods, not classes')
979-
@functools.wraps(func)
980-
def wrapper(self, /, *args, **kwargs):
980+
981+
def iter_subtest_kwargs():
981982
for values in arg_values:
982-
if single_param:
983-
values = (values,)
984-
subtest_kwargs = dict(zip(arg_names, values))
985-
with self.subTest(**subtest_kwargs):
986-
func(self, *args, **kwargs, **subtest_kwargs)
987-
if _do_cleanups:
988-
self.doCleanups()
983+
yield dict(zip(arg_names, (values,) if single_param else values))
984+
985+
# A synchronous wrapper would discard the coroutine without awaiting
986+
# it, so an asynchronous test would not run at all.
987+
if inspect.iscoroutinefunction(func):
988+
@functools.wraps(func)
989+
async def wrapper(self, /, *args, **kwargs):
990+
for subtest_kwargs in iter_subtest_kwargs():
991+
with self.subTest(**subtest_kwargs):
992+
await func(self, *args, **kwargs, **subtest_kwargs)
993+
if _do_cleanups:
994+
self.doCleanups()
995+
else:
996+
@functools.wraps(func)
997+
def wrapper(self, /, *args, **kwargs):
998+
for subtest_kwargs in iter_subtest_kwargs():
999+
with self.subTest(**subtest_kwargs):
1000+
func(self, *args, **kwargs, **subtest_kwargs)
1001+
if _do_cleanups:
1002+
self.doCleanups()
9891003
return wrapper
9901004
return decorator
9911005

Lib/test/test_support.py

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

970970

971+
class TestSubTests(ExtraAssertions, unittest.TestCase):
972+
973+
def run_test(self, cls):
974+
result = unittest.TestResult()
975+
cls('test_it').run(result)
976+
return result
977+
978+
def test_sync(self):
979+
ran = []
980+
981+
class Sample(unittest.TestCase):
982+
@support.subTests('a', [1, 2, 3])
983+
def test_it(self, a):
984+
ran.append(a)
985+
self.assertNotEqual(a, 2)
986+
987+
result = self.run_test(Sample)
988+
self.assertEqual(ran, [1, 2, 3])
989+
self.assertEqual(result.testsRun, 1)
990+
self.assertEqual(len(result.failures), 1)
991+
self.assertEndsWith(result.failures[0][0].id(), 'test_it (a=2)')
992+
993+
# Running an asyncio event loop needs a working socket.
994+
@support.requires_working_socket()
995+
def test_async(self):
996+
# Running an event loop sets the event loop policy, which regrtest
997+
# reports as a change of the environment.
998+
import asyncio.events
999+
self.enterContext(
1000+
support.swap_attr(asyncio.events, '_event_loop_policy', None))
1001+
1002+
# An asynchronous test must be awaited: a synchronous wrapper would
1003+
# make it silently not run at all.
1004+
ran = []
1005+
1006+
class Sample(unittest.IsolatedAsyncioTestCase):
1007+
@support.subTests('a', [1, 2, 3])
1008+
async def test_it(self, a):
1009+
ran.append(a)
1010+
self.assertNotEqual(a, 2)
1011+
1012+
result = self.run_test(Sample)
1013+
self.assertEqual(ran, [1, 2, 3])
1014+
self.assertEqual(result.testsRun, 1)
1015+
self.assertEqual(len(result.failures), 1)
1016+
self.assertEndsWith(result.failures[0][0].id(), 'test_it (a=2)')
1017+
1018+
def test_multiple_parameters(self):
1019+
ran = []
1020+
1021+
class Sample(unittest.TestCase):
1022+
@support.subTests('a,b', [(1, 'x'), (2, 'y')])
1023+
def test_it(self, a, b):
1024+
ran.append((a, b))
1025+
1026+
result = self.run_test(Sample)
1027+
self.assertTrue(result.wasSuccessful(), result.errors)
1028+
self.assertEqual(ran, [(1, 'x'), (2, 'y')])
1029+
1030+
def test_cannot_decorate_class(self):
1031+
with self.assertRaises(TypeError):
1032+
@support.subTests('a', [1])
1033+
class Sample(unittest.TestCase):
1034+
pass
1035+
1036+
9711037
if __name__ == '__main__':
9721038
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)