Skip to content

Commit dcc7e97

Browse files
authored
Improve coroutine timeout handling (#295)
resolves #294 This PR... 1. adds class-level `timeout_ms` attribute to customize runtime of asyncio coroutines. Example: ```py class MyAsyncTestCase(AsyncTestCase): timeout_ms=10000 ``` 2. uses `timeout_ms` argument from test_... coroutines to cutomize test case runtime. Example: ```py async def test_anything(self, timeout_ms=10000): ... ``` 3. raises TimeoutError within context of currently awaited asynchronous coroutine to ensure valuable tracebacks being printed. Example: ``` test_coroutine (test_asyncio_timeout.MyAsyncTestCaseA.test_coroutine) ... ERROR ====================================================================== ERROR: test_coroutine (test_asyncio_timeout.MyAsyncTestCaseA.test_coroutine) ---------------------------------------------------------------------- Traceback (most recent call last): File "<ST>\Data\Packages\UnitTesting\unittesting\core\py313\case.py", line 120, in run outcome.expecting_failure = False ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "<ST>\Data\Packages\UnitTesting\unittesting\core\py313\case.py", line 71, in _callMaybeCoro File "<ST>\Data\Packages\UnitTesting\tests\test_asyncio_timeout.py", line 23, in test_coroutine await a_coro(self) File "<ST>\Data\Packages\UnitTesting\tests\test_asyncio_timeout.py", line 7, in a_coro await a_coro2(test) File "<ST>\Data\Packages\UnitTesting\tests\test_asyncio_timeout.py", line 11, in a_coro2 await a_coro3(test) File "<ST>\Data\Packages\UnitTesting\tests\test_asyncio_timeout.py", line 15, in a_coro3 await asyncio.sleep(10.0) File "./python3.14/asyncio/tasks.py", line 702, in sleep TimeoutError: Task not completed within 4.50 seconds. ---------------------------------------------------------------------- Ran 1 test in 4.667s FAILED (errors=1) ```
1 parent 0792cee commit dcc7e97

9 files changed

Lines changed: 61 additions & 11 deletions

File tree

‎README.md‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,8 @@ async def async_coroutine(view):
590590

591591

592592
class MyAsyncTestCase(AsyncTestCase):
593+
timeout_ms = 4000
594+
"""Class wide coroutine timeout."""
593595

594596
@classmethod
595597
async def setUpClass(cls):
@@ -613,7 +615,10 @@ class MyAsyncTestCase(AsyncTestCase):
613615
"Initial Content"
614616
)
615617

616-
async def test_coroutine(self):
618+
async def test_coroutine(self, timeout_ms=10000):
619+
"""
620+
A long running coroutine with custom timeout.
621+
"""
617622
await async_coroutine(self.view)
618623
self.assertEqual(
619624
self.view.substr(sublime.Region(0, self.view.size())),

‎tests/_Asyncio/unittesting.json‎

Lines changed: 0 additions & 3 deletions
This file was deleted.
File renamed without changes.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.8
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import asyncio
2+
3+
from unittesting import AsyncTestCase
4+
5+
6+
async def a_coro(test):
7+
await asyncio.sleep(1.0)
8+
9+
10+
class MyAsyncTestCaseA(AsyncTestCase):
11+
timeout_ms = 100
12+
13+
async def test_coroutine_class_timeout(self):
14+
await a_coro(self)
15+
16+
17+
class MyAsyncTestCaseB(AsyncTestCase):
18+
async def test_coroutine_local_timeout(self, timeout_ms=100):
19+
await a_coro(self)

‎tests/test_3141596.py‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,8 @@ def assertOk(self, txt, msg=None):
122122

123123
class TestUnitTesting(UnitTestingTestCase):
124124
fixtures = (
125-
"_Success", "_Failure", "_Empty", "_Output", "_Deferred", "_Async", "_Asyncio"
125+
"_Success", "_Failure", "_Empty", "_Output", "_Deferred", "_Async",
126+
"_Asyncio_Success", "_Asyncio_Timeout"
126127
)
127128

128129
@with_package("_Success")
@@ -154,10 +155,15 @@ def test_async(self, txt):
154155
self.assertOk(txt)
155156

156157
@skipIf(PY33, "not applicable in Python 3.3")
157-
@with_package("_Asyncio")
158-
def test_asyncio(self, txt):
158+
@with_package("_Asyncio_Success")
159+
def test_asyncio_success(self, txt):
159160
self.assertOk(txt)
160161

162+
@skipIf(PY33, "not applicable in Python 3.3")
163+
@with_package("_Asyncio_Timeout")
164+
def test_asyncio_timeout(self, txt):
165+
self.assertRegexContains(txt, r'^ERROR')
166+
161167

162168
class TestSyntax(UnitTestingTestCase):
163169
fixtures = (

‎unittesting/core/py313/case.py‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from unittest.case import _Outcome
1111
from unittest.case import expectedFailure
1212

13+
from .runner import DEFAULT_CONDITION_TIMEOUT
1314
from .runner import defer
1415

1516
__all__ = [
@@ -22,6 +23,7 @@
2223

2324

2425
class DeferrableTestCase(TestCase):
26+
timeout_ms: int = DEFAULT_CONDITION_TIMEOUT
2527

2628
def _callSetUp(self):
2729
return self._callMaybeCoro(self.setUp)
@@ -48,15 +50,24 @@ def _callMaybeCoro(cls, func, /, *args, **kwargs):
4850
elif inspect.iscoroutine(coro):
4951
fut = cls.run_coroutine(coro)
5052

51-
def wait_until_complete():
53+
def await_future():
5254
if not fut.done() and not fut.cancelled():
5355
return False
5456
exception = fut.exception()
5557
if exception is not None:
5658
raise exception from None
5759
return True
5860

59-
yield wait_until_complete
61+
if frame := coro.cr_frame:
62+
# prefer optional timeout from test_... coroutine's arguments
63+
timeout_ms = frame.f_locals.get("timeout_ms", cls.timeout_ms)
64+
else:
65+
timeout_ms = cls.timeout_ms
66+
try:
67+
yield {"condition": await_future, "timeout": timeout_ms}
68+
except TimeoutError:
69+
msg = f"Task not completed within {timeout_ms / 1000:.2f} seconds."
70+
coro.throw(TimeoutError, msg)
6071

6172
@staticmethod
6273
def run_coroutine(coro):

‎unittesting/core/py38/case.py‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from unittest.case import _Outcome
1010
from unittest.case import expectedFailure
1111

12+
from .runner import DEFAULT_CONDITION_TIMEOUT
1213
from .runner import defer
1314

1415
__all__ = [
@@ -21,6 +22,7 @@
2122

2223

2324
class DeferrableTestCase(TestCase):
25+
timeout_ms: int = DEFAULT_CONDITION_TIMEOUT
2426

2527
def _callSetUp(self):
2628
return self._callMaybeCoro(self.setUp)
@@ -47,15 +49,24 @@ def _callMaybeCoro(cls, func, /, *args, **kwargs):
4749
elif inspect.iscoroutine(coro):
4850
fut = cls.run_coroutine(coro)
4951

50-
def wait_until_complete():
52+
def await_future():
5153
if not fut.done() and not fut.cancelled():
5254
return False
5355
exception = fut.exception()
5456
if exception is not None:
5557
raise exception from None
5658
return True
5759

58-
yield wait_until_complete
60+
if frame := coro.cr_frame:
61+
# prefer optional timeout from test_... coroutine's arguments
62+
timeout_ms = frame.f_locals.get("timeout_ms", cls.timeout_ms)
63+
else:
64+
timeout_ms = cls.timeout_ms
65+
try:
66+
yield {"condition": await_future, "timeout": timeout_ms}
67+
except TimeoutError:
68+
msg = f"Task not completed within {timeout_ms / 1000:.2f} seconds."
69+
coro.throw(TimeoutError, msg)
5970

6071
@staticmethod
6172
def run_coroutine(coro):

0 commit comments

Comments
 (0)