Systematic rename of all semantic anchors (#region, [DEF], @RELATION) across 1400+ files — backend Python, frontend Svelte/TS, specs, docs: - Flat anchors become Namespace.Module.Entity - @RELATION references updated to match new anchor paths - Zero business logic changes
188 lines
7.6 KiB
Python
188 lines
7.6 KiB
Python
# #region Test.BlockingExecutors [C:3] [TYPE Module] [SEMANTICS test,async,executors,blocking,threadpool]
|
|
# @BRIEF Tests for core/utils/executors.py — init, shutdown, run_blocking, run_cpu_blocking.
|
|
# @RELATION BINDS_TO -> [Core.Executors.BlockingExecutorsModule]
|
|
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
|
|
|
|
import unittest
|
|
from unittest.mock import MagicMock, patch, AsyncMock
|
|
import asyncio
|
|
import pytest
|
|
|
|
|
|
class TestInitShutdown:
|
|
"""init_executors and shutdown_executors."""
|
|
|
|
def test_init_creates_executors(self):
|
|
from src.core.utils.executors import init_executors
|
|
with patch("src.core.utils.executors.ThreadPoolExecutor") as mock_tpe:
|
|
init_executors(db_workers=5, file_workers=3, git_workers=2)
|
|
assert mock_tpe.call_count == 3
|
|
|
|
def test_shutdown_without_init(self):
|
|
# shutdown should not raise when executors are None
|
|
from src.core.utils.executors import shutdown_executors
|
|
shutdown_executors()
|
|
|
|
def test_shutdown_calls_shutdown(self):
|
|
from src.core.utils.executors import init_executors, shutdown_executors
|
|
import src.core.utils.executors as exec_mod
|
|
with patch.object(exec_mod, "_db_executor", None), \
|
|
patch.object(exec_mod, "_file_executor", None), \
|
|
patch.object(exec_mod, "_git_executor", None):
|
|
with patch("src.core.utils.executors.ThreadPoolExecutor") as mock_tpe:
|
|
mock_executor = MagicMock()
|
|
mock_tpe.return_value = mock_executor
|
|
init_executors()
|
|
shutdown_executors()
|
|
assert mock_executor.shutdown.call_count >= 1
|
|
for call in mock_executor.shutdown.call_args_list:
|
|
assert call == unittest.mock.call(wait=True, cancel_futures=True)
|
|
|
|
def test_shutdown_no_wait(self):
|
|
from src.core.utils.executors import init_executors, shutdown_executors
|
|
import src.core.utils.executors as exec_mod
|
|
with patch.object(exec_mod, "_db_executor", None), \
|
|
patch.object(exec_mod, "_file_executor", None), \
|
|
patch.object(exec_mod, "_git_executor", None):
|
|
with patch("src.core.utils.executors.ThreadPoolExecutor") as mock_tpe:
|
|
mock_executor = MagicMock()
|
|
mock_tpe.return_value = mock_executor
|
|
init_executors()
|
|
shutdown_executors(wait=False, cancel_futures=False)
|
|
assert mock_executor.shutdown.call_count >= 1
|
|
for call in mock_executor.shutdown.call_args_list:
|
|
assert call == unittest.mock.call(wait=False, cancel_futures=False)
|
|
|
|
|
|
class TestGetExecutor:
|
|
"""_get_executor — returns executor and semaphore by kind."""
|
|
|
|
def test_db_kind(self):
|
|
from src.core.utils.executors import _get_executor, init_executors, shutdown_executors
|
|
with patch("src.core.utils.executors.ThreadPoolExecutor"):
|
|
init_executors()
|
|
executor, sem = _get_executor("db")
|
|
assert executor is not None
|
|
assert sem is not None
|
|
shutdown_executors()
|
|
|
|
def test_file_kind(self):
|
|
from src.core.utils.executors import _get_executor, init_executors, shutdown_executors
|
|
with patch("src.core.utils.executors.ThreadPoolExecutor"):
|
|
init_executors()
|
|
executor, sem = _get_executor("file")
|
|
assert executor is not None
|
|
assert sem is not None
|
|
shutdown_executors()
|
|
|
|
def test_git_kind(self):
|
|
from src.core.utils.executors import _get_executor, init_executors, shutdown_executors
|
|
with patch("src.core.utils.executors.ThreadPoolExecutor"):
|
|
init_executors()
|
|
executor, sem = _get_executor("git")
|
|
assert executor is not None
|
|
assert sem is not None
|
|
shutdown_executors()
|
|
|
|
def test_unknown_kind_raises(self):
|
|
from src.core.utils.executors import _get_executor, init_executors, shutdown_executors
|
|
with patch("src.core.utils.executors.ThreadPoolExecutor"):
|
|
init_executors()
|
|
with pytest.raises(ValueError, match="Unknown executor kind"):
|
|
_get_executor("unknown")
|
|
shutdown_executors()
|
|
|
|
|
|
class TestRunBlocking:
|
|
"""run_blocking — execute blocking function in named executor."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_runs_function(self):
|
|
from src.core.utils.executors import run_blocking, init_executors, shutdown_executors
|
|
|
|
with patch("src.core.utils.executors.ThreadPoolExecutor") as mock_tpe:
|
|
mock_executor = MagicMock()
|
|
mock_tpe.return_value = mock_executor
|
|
init_executors()
|
|
|
|
# Make run_in_executor work
|
|
mock_loop = AsyncMock()
|
|
mock_loop.run_in_executor.return_value = "result"
|
|
get_running_loop = asyncio.get_running_loop
|
|
|
|
with patch.object(asyncio, "get_running_loop", return_value=mock_loop):
|
|
result = await run_blocking("db", lambda: "hello")
|
|
assert result == "result"
|
|
|
|
shutdown_executors()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_semaphore_acquisition_failure(self):
|
|
from src.core.utils.executors import run_blocking, init_executors, shutdown_executors
|
|
|
|
with patch("src.core.utils.executors.ThreadPoolExecutor"):
|
|
init_executors(queue_timeout=0.1)
|
|
|
|
with (
|
|
patch("src.core.utils.executors._db_semaphore") as mock_sem,
|
|
pytest.raises(TimeoutError, match="queue timeout"),
|
|
):
|
|
mock_sem.acquire.side_effect = TimeoutError()
|
|
await run_blocking("db", lambda: "hello", timeout=0.1)
|
|
|
|
shutdown_executors()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_semaphore_release_on_success(self):
|
|
from src.core.utils.executors import run_blocking, init_executors, shutdown_executors
|
|
|
|
with patch("src.core.utils.executors.ThreadPoolExecutor"):
|
|
init_executors()
|
|
mock_loop = AsyncMock()
|
|
mock_loop.run_in_executor.return_value = "done"
|
|
|
|
# Track semaphore acquire/release
|
|
with patch.object(asyncio, "get_running_loop", return_value=mock_loop):
|
|
result = await run_blocking("file", lambda: "work")
|
|
assert result == "done"
|
|
|
|
shutdown_executors()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_cancelled_error_propagates(self):
|
|
from src.core.utils.executors import run_blocking, init_executors, shutdown_executors
|
|
|
|
with patch("src.core.utils.executors.ThreadPoolExecutor"):
|
|
init_executors()
|
|
mock_loop = AsyncMock()
|
|
mock_loop.run_in_executor.side_effect = asyncio.CancelledError()
|
|
|
|
with (
|
|
patch.object(asyncio, "get_running_loop", return_value=mock_loop),
|
|
pytest.raises(asyncio.CancelledError),
|
|
):
|
|
await run_blocking("git", lambda: "fail")
|
|
|
|
shutdown_executors()
|
|
|
|
|
|
class TestRunCpuBlocking:
|
|
"""run_cpu_blocking — CPU-bound work in default executor."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_runs_in_default_executor(self):
|
|
from src.core.utils.executors import run_cpu_blocking
|
|
|
|
mock_loop = AsyncMock()
|
|
mock_loop.run_in_executor.return_value = 42
|
|
|
|
with patch.object(asyncio, "get_running_loop", return_value=mock_loop):
|
|
result = await run_cpu_blocking(lambda: 42)
|
|
assert result == 42
|
|
mock_loop.run_in_executor.assert_called_once_with(None, mock_loop.run_in_executor.call_args[0][1])
|
|
# #endregion Test.BlockingExecutors
|