Files
MemeMeow/tests/test_search_threadpool.py

74 lines
3.3 KiB
Python

from __future__ import annotations
import asyncio
from contextvars import ContextVar
import os
from pathlib import Path
import threading
from uuid import uuid4
from fastapi import FastAPI, HTTPException
import pytest
from sqlalchemy import create_engine, delete, event
from starlette.requests import Request
import api
from backend.config import Settings
from backend.persistence.models import Scope, ScopeContext
from backend.persistence.resources import DatabaseResources
from backend.scope import ScopeServiceFactory
from backend.search_http import SearchRequest
# 使用真实搜索入口和数据库查询验证线程调度及请求上下文传播。
def test_search_database_queries_run_in_worker_thread(tmp_path: Path) -> None:
"""空测试 scope 通过真实缓存查询返回 503,所有查询均在工作线程执行。"""
url = os.environ.get("MEMEMEOW_TEST_DATABASE_URL")
if not url:
pytest.skip("需要显式提供开发测试 PostgreSQL 连接")
engine = create_engine(url, pool_size=1, max_overflow=0, pool_timeout=3)
scope = ScopeContext(f"search-threadpool-validation-{uuid4().hex}")
resources = DatabaseResources(engine, image_root=tmp_path / "images", data_root=tmp_path, require_local_scope=False)
settings = Settings(data_root=tmp_path, image_root=tmp_path / "images")
factory = ScopeServiceFactory(resources, settings)
request_context = ContextVar("search_request_context", default="missing")
observed: list[tuple[int, str]] = []
def record_query(_connection, _cursor, _statement, _parameters, _context, _executemany) -> None:
"""通过 SQLAlchemy 事件观察真实查询的线程和请求上下文。"""
observed.append((threading.get_ident(), request_context.get()))
try:
with resources.factory() as session, session.begin():
session.add(Scope(id=scope.scope_id))
application = FastAPI()
application.state.settings = settings
request = Request({"type": "http", "method": "POST", "path": "/search", "headers": [], "app": application})
request.state.scope = scope
request.state.services = factory.for_scope(scope)
async def search() -> None:
"""通过真实 API handler 验证查询线程、上下文和错误传播。"""
loop_thread = threading.get_ident()
token = request_context.set(scope.scope_id)
event.listen(engine, "before_cursor_execute", record_query)
try:
with pytest.raises(HTTPException) as captured:
await api.search_images(request, SearchRequest(query="线程调度测试"))
assert captured.value.status_code == 503
assert captured.value.detail["error"] == "cache_not_ready"
assert observed
assert all(thread_id != loop_thread for thread_id, _ in observed)
assert all(context == scope.scope_id for _, context in observed)
assert engine.pool.checkedout() == 0
finally:
event.remove(engine, "before_cursor_execute", record_query)
request_context.reset(token)
asyncio.run(search())
finally:
factory.shutdown()
with resources.factory() as session, session.begin():
session.execute(delete(Scope).where(Scope.id == scope.scope_id))
engine.dispose()