diff --git a/README.md b/README.md index b57e2e9c..5fa2d9a2 100644 --- a/README.md +++ b/README.md @@ -590,6 +590,8 @@ async def async_coroutine(view): class MyAsyncTestCase(AsyncTestCase): + timeout_ms = 4000 + """Class wide coroutine timeout.""" @classmethod async def setUpClass(cls): @@ -613,7 +615,10 @@ class MyAsyncTestCase(AsyncTestCase): "Initial Content" ) - async def test_coroutine(self): + async def test_coroutine(self, timeout_ms=10000): + """ + A long running coroutine with custom timeout. + """ await async_coroutine(self.view) self.assertEqual( self.view.substr(sublime.Region(0, self.view.size())), diff --git a/tests/_Asyncio/unittesting.json b/tests/_Asyncio/unittesting.json deleted file mode 100644 index 5fcf8eb9..00000000 --- a/tests/_Asyncio/unittesting.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "deferred": true -} diff --git a/tests/_Asyncio/.python-version b/tests/_Asyncio_Success/.python-version similarity index 100% rename from tests/_Asyncio/.python-version rename to tests/_Asyncio_Success/.python-version diff --git a/tests/_Asyncio/tests/test_coroutine.py b/tests/_Asyncio_Success/tests/test_coroutine.py similarity index 100% rename from tests/_Asyncio/tests/test_coroutine.py rename to tests/_Asyncio_Success/tests/test_coroutine.py diff --git a/tests/_Asyncio_Timeout/.python-version b/tests/_Asyncio_Timeout/.python-version new file mode 100644 index 00000000..98fccd6d --- /dev/null +++ b/tests/_Asyncio_Timeout/.python-version @@ -0,0 +1 @@ +3.8 \ No newline at end of file diff --git a/tests/_Asyncio_Timeout/tests/test_coroutine.py b/tests/_Asyncio_Timeout/tests/test_coroutine.py new file mode 100644 index 00000000..6b0218c7 --- /dev/null +++ b/tests/_Asyncio_Timeout/tests/test_coroutine.py @@ -0,0 +1,19 @@ +import asyncio + +from unittesting import AsyncTestCase + + +async def a_coro(test): + await asyncio.sleep(1.0) + + +class MyAsyncTestCaseA(AsyncTestCase): + timeout_ms = 100 + + async def test_coroutine_class_timeout(self): + await a_coro(self) + + +class MyAsyncTestCaseB(AsyncTestCase): + async def test_coroutine_local_timeout(self, timeout_ms=100): + await a_coro(self) diff --git a/tests/test_3141596.py b/tests/test_3141596.py index 8238faac..d7b253c9 100644 --- a/tests/test_3141596.py +++ b/tests/test_3141596.py @@ -122,7 +122,8 @@ def assertOk(self, txt, msg=None): class TestUnitTesting(UnitTestingTestCase): fixtures = ( - "_Success", "_Failure", "_Empty", "_Output", "_Deferred", "_Async", "_Asyncio" + "_Success", "_Failure", "_Empty", "_Output", "_Deferred", "_Async", + "_Asyncio_Success", "_Asyncio_Timeout" ) @with_package("_Success") @@ -154,10 +155,15 @@ def test_async(self, txt): self.assertOk(txt) @skipIf(PY33, "not applicable in Python 3.3") - @with_package("_Asyncio") - def test_asyncio(self, txt): + @with_package("_Asyncio_Success") + def test_asyncio_success(self, txt): self.assertOk(txt) + @skipIf(PY33, "not applicable in Python 3.3") + @with_package("_Asyncio_Timeout") + def test_asyncio_timeout(self, txt): + self.assertRegexContains(txt, r'^ERROR') + class TestSyntax(UnitTestingTestCase): fixtures = ( diff --git a/unittesting/core/py313/case.py b/unittesting/core/py313/case.py index cee605a3..9083be20 100644 --- a/unittesting/core/py313/case.py +++ b/unittesting/core/py313/case.py @@ -10,6 +10,7 @@ from unittest.case import _Outcome from unittest.case import expectedFailure +from .runner import DEFAULT_CONDITION_TIMEOUT from .runner import defer __all__ = [ @@ -22,6 +23,7 @@ class DeferrableTestCase(TestCase): + timeout_ms: int = DEFAULT_CONDITION_TIMEOUT def _callSetUp(self): return self._callMaybeCoro(self.setUp) @@ -48,7 +50,7 @@ def _callMaybeCoro(cls, func, /, *args, **kwargs): elif inspect.iscoroutine(coro): fut = cls.run_coroutine(coro) - def wait_until_complete(): + def await_future(): if not fut.done() and not fut.cancelled(): return False exception = fut.exception() @@ -56,7 +58,16 @@ def wait_until_complete(): raise exception from None return True - yield wait_until_complete + if frame := coro.cr_frame: + # prefer optional timeout from test_... coroutine's arguments + timeout_ms = frame.f_locals.get("timeout_ms", cls.timeout_ms) + else: + timeout_ms = cls.timeout_ms + try: + yield {"condition": await_future, "timeout": timeout_ms} + except TimeoutError: + msg = f"Task not completed within {timeout_ms / 1000:.2f} seconds." + coro.throw(TimeoutError, msg) @staticmethod def run_coroutine(coro): diff --git a/unittesting/core/py38/case.py b/unittesting/core/py38/case.py index e004f943..6273683b 100644 --- a/unittesting/core/py38/case.py +++ b/unittesting/core/py38/case.py @@ -9,6 +9,7 @@ from unittest.case import _Outcome from unittest.case import expectedFailure +from .runner import DEFAULT_CONDITION_TIMEOUT from .runner import defer __all__ = [ @@ -21,6 +22,7 @@ class DeferrableTestCase(TestCase): + timeout_ms: int = DEFAULT_CONDITION_TIMEOUT def _callSetUp(self): return self._callMaybeCoro(self.setUp) @@ -47,7 +49,7 @@ def _callMaybeCoro(cls, func, /, *args, **kwargs): elif inspect.iscoroutine(coro): fut = cls.run_coroutine(coro) - def wait_until_complete(): + def await_future(): if not fut.done() and not fut.cancelled(): return False exception = fut.exception() @@ -55,7 +57,16 @@ def wait_until_complete(): raise exception from None return True - yield wait_until_complete + if frame := coro.cr_frame: + # prefer optional timeout from test_... coroutine's arguments + timeout_ms = frame.f_locals.get("timeout_ms", cls.timeout_ms) + else: + timeout_ms = cls.timeout_ms + try: + yield {"condition": await_future, "timeout": timeout_ms} + except TimeoutError: + msg = f"Task not completed within {timeout_ms / 1000:.2f} seconds." + coro.throw(TimeoutError, msg) @staticmethod def run_coroutine(coro):