Files
MoviePilot/tests/test_runtime_execution.py
T

91 lines
2.8 KiB
Python

"""运行时同步 worker 的取消与容量合同回归。"""
import asyncio
import threading
import pytest
from anyio.to_thread import current_default_thread_limiter
from app.runtime.execution import run_in_threadpool_to_completion
@pytest.mark.asyncio
async def test_threadpool_capacity_is_held_until_cancelled_call_finishes() -> None:
"""调用方取消后,执行令牌必须由真实同步调用持有到终态。"""
limiter = current_default_thread_limiter()
original_capacity = limiter.total_tokens
release = threading.Event()
first_started = threading.Event()
second_started = threading.Event()
def blocking_call(started: threading.Event) -> None:
started.set()
release.wait()
limiter.total_tokens = 1
first = asyncio.create_task(
run_in_threadpool_to_completion(blocking_call, first_started)
)
second = None
try:
while not first_started.is_set():
await asyncio.sleep(0)
first.cancel()
await asyncio.sleep(0)
first.cancel()
await asyncio.sleep(0)
assert first.done() is False
assert limiter.borrowed_tokens == 1
second = asyncio.create_task(
run_in_threadpool_to_completion(blocking_call, second_started)
)
await asyncio.sleep(0.01)
assert second_started.is_set() is False
release.set()
with pytest.raises(asyncio.CancelledError):
await first
await second
finally:
release.set()
if not first.done():
await asyncio.gather(first, return_exceptions=True)
if second is not None and not second.done():
await asyncio.gather(second, return_exceptions=True)
limiter.total_tokens = original_capacity
@pytest.mark.asyncio
async def test_cancelled_threadpool_call_preserves_worker_failure_as_cause() -> None:
"""调用方取消优先返回,线程终态异常仍保留为诊断原因。"""
loop = asyncio.get_running_loop()
previous_handler = loop.get_exception_handler()
loop_errors: list[dict] = []
release = threading.Event()
started = threading.Event()
def failing_call() -> None:
started.set()
release.wait()
raise ValueError("worker failed")
task = asyncio.create_task(run_in_threadpool_to_completion(failing_call))
while not started.is_set():
await asyncio.sleep(0)
task.cancel()
release.set()
loop.set_exception_handler(lambda _loop, context: loop_errors.append(context))
try:
with pytest.raises(asyncio.CancelledError) as error_info:
await task
await asyncio.sleep(0)
finally:
loop.set_exception_handler(previous_handler)
assert isinstance(error_info.value.__cause__, ValueError)
assert loop_errors == []