Files
ss-tools/backend/tests/core/test_executors.py
root 632b730fff chore: migrate GRACE-Poly anchors to hierarchical dotted naming
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
2026-07-22 11:48:15 +03:00

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