"""Tests for async_utils.""" from __future__ import annotations import asyncio import time from concurrent.futures import ThreadPoolExecutor from unittest import mock import pytest from app.utils.async_utils import logged_task, run_blocking async def test_run_blocking_executes_callable_in_thread() -> None: """run_blocking should execute the provided callable and return its result.""" def blocking_add(x: int, y: int) -> int: return x + y result = await run_blocking(blocking_add, 3, 4) assert result == 7 async def test_run_blocking_accepts_custom_executor() -> None: """run_blocking should use a provided executor when one is passed.""" def blocking_value() -> str: return "ok" executor = ThreadPoolExecutor(max_workers=1) try: result = await run_blocking(blocking_value, executor=executor) assert result == "ok" finally: executor.shutdown(wait=True) async def test_run_blocking_does_not_block_event_loop() -> None: """A blocking callable should not stall other async tasks.""" def sleep_block() -> str: time.sleep(0.05) return "done" task = asyncio.create_task(run_blocking(sleep_block)) # Yield control to ensure the event loop can run other tasks while the # blocking work executes in the thread pool. await asyncio.sleep(0) result = await task assert result == "done" async def test_logged_task_awaits_coroutine() -> None: """logged_task should await and complete the coroutine.""" async def dummy_coro() -> str: await asyncio.sleep(0) return "result" with mock.patch("app.utils.async_utils.log") as mock_log: await logged_task(dummy_coro(), "test_task") mock_log.exception.assert_not_called() async def test_logged_task_catches_exception_and_logs() -> None: """logged_task should catch exceptions and log them with task_name context.""" class CustomError(Exception): pass async def failing_coro() -> None: raise CustomError("task failed") with mock.patch("app.utils.async_utils.log") as mock_log: await logged_task(failing_coro(), "failing_task") mock_log.exception.assert_called_once_with( "background_task_failed", task_name="failing_task", ) async def test_logged_task_with_asyncio_create_task() -> None: """logged_task should work correctly when wrapped with asyncio.create_task.""" results: list[str] = [] async def background_work() -> None: await asyncio.sleep(0.01) results.append("done") with mock.patch("app.utils.async_utils.log"): task = asyncio.create_task( logged_task(background_work(), "bg_task"), name="bg_task", ) await task assert results == ["done"] async def test_logged_task_preserves_exception_info() -> None: """logged_task should preserve traceback when logging the exception.""" async def failing_coro() -> None: raise ValueError("original error message") with mock.patch("app.utils.async_utils.log") as mock_log: await logged_task(failing_coro(), "test_task") mock_log.exception.assert_called_once() # Verify the exception context is logged (exception captures # the traceback automatically) args, kwargs = mock_log.exception.call_args assert args[0] == "background_task_failed" assert kwargs["task_name"] == "test_task"