101 lines
3.7 KiB
Python
101 lines
3.7 KiB
Python
"""Completed executor tasks must return or propagate their own exceptions."""
|
|
|
|
import subprocess
|
|
import sys
|
|
import textwrap
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.mark.parametrize("mode", ["sync", "async"])
|
|
@pytest.mark.parametrize(
|
|
"outcome",
|
|
["success", "value_error", "builtin_timeout", "asyncio_timeout",
|
|
"concurrent_timeout", "cancelled"],
|
|
)
|
|
def test_guarded_future_completion(mode, outcome):
|
|
# A regression can loop forever on an already-failed Future. Isolate each
|
|
# case so the parent can stop it without leaving a busy thread in pytest.
|
|
script = textwrap.dedent(
|
|
"""
|
|
import asyncio
|
|
import concurrent.futures
|
|
import sys
|
|
import time
|
|
|
|
from hy_memory.utils.async_executor import (
|
|
await_concurrent_future_guarded,
|
|
concurrent_result_guarded,
|
|
)
|
|
|
|
mode, outcome = sys.argv[1:]
|
|
errors = {
|
|
"value_error": ValueError,
|
|
"builtin_timeout": TimeoutError,
|
|
"asyncio_timeout": asyncio.TimeoutError,
|
|
"concurrent_timeout": concurrent.futures.TimeoutError,
|
|
}
|
|
|
|
def work():
|
|
# Exceed several guard intervals: a wait timeout must not cancel
|
|
# or resubmit the underlying task.
|
|
time.sleep(0.08)
|
|
if outcome in errors:
|
|
raise errors[outcome]("task failed")
|
|
return "completed"
|
|
|
|
async def check():
|
|
loop = asyncio.get_running_loop()
|
|
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
|
|
if outcome == "cancelled":
|
|
future = concurrent.futures.Future()
|
|
assert future.cancel()
|
|
else:
|
|
future = pool.submit(work)
|
|
try:
|
|
if mode == "sync":
|
|
result = concurrent_result_guarded(
|
|
future, loop, guard_seconds=0.01,
|
|
)
|
|
else:
|
|
result = await await_concurrent_future_guarded(
|
|
future, loop, guard_seconds=0.01,
|
|
)
|
|
except BaseException as exc:
|
|
if outcome == "cancelled":
|
|
expected = (concurrent.futures.CancelledError
|
|
if mode == "sync" else asyncio.CancelledError)
|
|
assert isinstance(exc, expected), repr(exc)
|
|
else:
|
|
assert outcome in errors, repr(exc)
|
|
# wrap_future translates concurrent.futures.TimeoutError
|
|
# to asyncio.TimeoutError (distinct before Python 3.11).
|
|
expected = (asyncio.TimeoutError
|
|
if mode == "async" and outcome == "concurrent_timeout"
|
|
else errors[outcome])
|
|
assert isinstance(exc, expected), repr(exc)
|
|
assert str(exc) == "task failed"
|
|
else:
|
|
assert outcome == "success"
|
|
assert result == "completed"
|
|
|
|
asyncio.run(check())
|
|
"""
|
|
)
|
|
process = subprocess.Popen(
|
|
[sys.executable, "-c", script, mode, outcome],
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
text=True,
|
|
)
|
|
try:
|
|
stdout, stderr = process.communicate(timeout=10)
|
|
except subprocess.TimeoutExpired:
|
|
process.kill()
|
|
stdout, stderr = process.communicate()
|
|
pytest.fail(
|
|
f"{mode}/{outcome}: completed task did not reach caller; "
|
|
f"stdout={stdout!r}, stderr={stderr!r}"
|
|
)
|
|
assert process.returncode == 0, stderr
|