mirror of
https://github.com/insistence/RuoYi-Vue3-FastAPI.git
synced 2026-10-12 12:20:25 +00:00
223 lines
8.0 KiB
Python
223 lines
8.0 KiB
Python
import asyncio
|
|
import importlib
|
|
import sys
|
|
from collections.abc import Callable
|
|
from pathlib import Path
|
|
from types import ModuleType
|
|
|
|
import pytest
|
|
|
|
BACKEND_DIR = Path(__file__).resolve().parents[4]
|
|
|
|
sys.path.insert(0, str(BACKEND_DIR))
|
|
|
|
|
|
class RuntimeQueryStub:
|
|
"""把现有负载工厂映射为新的异步进程内查询接口。"""
|
|
|
|
def __init__(self, runner: Callable[..., object]) -> None:
|
|
self.runner = runner
|
|
|
|
def _payload(self, *arguments: str) -> object:
|
|
return self.runner(*arguments, parse_json=True).payload
|
|
|
|
async def get_app_env(self, env: str) -> object:
|
|
return self._payload('app', 'env', f'--env={env}', '--output=json')
|
|
|
|
async def get_app_config(self, env: str) -> object:
|
|
return self._payload('app', 'config', f'--env={env}', '--output=json')
|
|
|
|
async def get_app_routes(self, env: str) -> object:
|
|
return self._payload('app', 'routes', f'--env={env}', '--output=json')
|
|
|
|
async def get_doctor(self, env: str) -> object:
|
|
return self._payload('app', 'doctor', f'--env={env}', '--output=json')
|
|
|
|
async def get_completion_doctor(self) -> object:
|
|
return self._payload('completion', 'doctor', '--output=json')
|
|
|
|
async def get_completion_preview(self, shell: str) -> object:
|
|
return self.runner('completion', 'show', f'--shell={shell}', parse_json=False)
|
|
|
|
async def get_database_ping(self) -> object:
|
|
return self._payload('ops', 'ping-db', '--output=json')
|
|
|
|
async def get_database_current(self) -> object:
|
|
return self._payload('db', 'current', '--output=json')
|
|
|
|
async def get_database_heads(self) -> object:
|
|
return self._payload('db', 'heads', '--output=json')
|
|
|
|
async def get_database_history(self, *, limit: int = 8) -> object:
|
|
return self._payload('db', 'history', f'--limit={limit}', '--output=json')
|
|
|
|
async def get_redis_ping(self) -> object:
|
|
return self._payload('ops', 'ping-redis', '--output=json')
|
|
|
|
async def get_dependencies(self) -> object:
|
|
return self._payload('ops', 'deps', '--output=json')
|
|
|
|
async def get_server_info(self) -> object:
|
|
return self._payload('ops', 'server-info', '--output=json')
|
|
|
|
async def get_crypto_validation(self) -> object:
|
|
return self._payload('crypto', 'validate', '--output=json')
|
|
|
|
async def get_crypto_public_key(self) -> object:
|
|
return self._payload('crypto', 'export-public', '--output=json')
|
|
|
|
async def get_cache_stats(self) -> object:
|
|
return self._payload('cache', 'stats', '--output=json')
|
|
|
|
async def get_cache_keys(self, cache_name: str) -> object:
|
|
return self._payload('cache', 'keys', cache_name, '--output=json')
|
|
|
|
async def get_cache_value(self, cache_name: str, cache_key: str) -> object:
|
|
return self._payload('cache', 'get', cache_name, cache_key, '--output=json')
|
|
|
|
async def get_cache_ttl(self, cache_name: str, cache_key: str) -> object:
|
|
return self._payload('cache', 'ttl', cache_name, cache_key, '--output=json')
|
|
|
|
async def get_jobs(self) -> object:
|
|
return self._payload('job', 'list', '--paged', '--page-size=8', '--output=json')
|
|
|
|
async def get_job_logs(self, *, job_name: str = '', status: str | None = None, page_size: int = 20) -> object:
|
|
arguments = ['job', 'logs', '--paged', f'--page-size={page_size}', '--output=json']
|
|
if job_name:
|
|
arguments.append(f'--job-name={job_name}')
|
|
if status is not None:
|
|
arguments.append(f'--status={status}')
|
|
return self._payload(*arguments)
|
|
|
|
async def get_job_detail(self, job_id: int) -> object:
|
|
return self._payload('job', 'detail', str(job_id), '--output=json')
|
|
|
|
async def get_gen_tables(self) -> object:
|
|
return self._payload('gen', 'list', '--paged', '--page-size=8', '--output=json')
|
|
|
|
async def get_gen_db_tables(self, *, table_name: str = '', page_size: int = 8) -> object:
|
|
arguments = ['gen', 'db-list', '--paged', f'--page-size={page_size}', '--output=json']
|
|
if table_name:
|
|
arguments.append(f'--table-name={table_name}')
|
|
return self._payload(*arguments)
|
|
|
|
async def get_gen_detail(self, table_id: int) -> object:
|
|
return self._payload('gen', 'detail', str(table_id), '--output=json')
|
|
|
|
async def get_gen_preview(self, table_id: int) -> object:
|
|
return self._payload('gen', 'preview', str(table_id), '--output=json')
|
|
|
|
async def get_gen_export_dry_run(self, table_name: str) -> object:
|
|
return self._payload('gen', 'export', table_name, '--dry-run', '--output=json')
|
|
|
|
async def get_configs(self) -> object:
|
|
return self._payload('config', 'list', '--paged', '--page-size=8', '--output=json')
|
|
|
|
async def get_config_diagnostics(self) -> object:
|
|
return self._payload('config', 'doctor', '--sample-limit=5', '--output=json')
|
|
|
|
async def get_config_detail(self, config_key: str) -> object:
|
|
return self._payload('config', 'get', config_key, '--source=both', '--output=json')
|
|
|
|
|
|
@pytest.fixture
|
|
def install_query_service() -> Callable[[ModuleType, Callable[..., object]], RuntimeQueryStub]:
|
|
"""向适配器及其采集/分区协作对象注入异步查询桩。"""
|
|
|
|
def install(module: ModuleType, runner: Callable[..., object]) -> RuntimeQueryStub:
|
|
query_service = RuntimeQueryStub(runner)
|
|
visited: set[int] = set()
|
|
|
|
def inject(target: object) -> None:
|
|
if id(target) in visited:
|
|
return
|
|
visited.add(id(target))
|
|
if hasattr(target, 'query_service'):
|
|
target.query_service = query_service
|
|
for attribute_name in ('snapshot_collector', 'section_builder'):
|
|
child = getattr(target, attribute_name, None)
|
|
if child is not None:
|
|
inject(child)
|
|
|
|
for attribute_name in dir(module):
|
|
if attribute_name.endswith(('_ADAPTER', '_COLLECTOR')):
|
|
inject(getattr(module, attribute_name))
|
|
return query_service
|
|
|
|
return install
|
|
|
|
|
|
def _load_adapter_module(module_name: str) -> ModuleType:
|
|
sys.modules.pop(module_name, None)
|
|
sys.modules.pop('cli', None)
|
|
return importlib.import_module(module_name)
|
|
|
|
|
|
@pytest.fixture
|
|
def app_adapter() -> ModuleType:
|
|
return _load_adapter_module('cli.tui.adapters.app')
|
|
|
|
|
|
@pytest.fixture
|
|
def cache_adapter() -> ModuleType:
|
|
return _load_adapter_module('cli.tui.adapters.cache')
|
|
|
|
|
|
@pytest.fixture
|
|
def crypto_adapter() -> ModuleType:
|
|
return _load_adapter_module('cli.tui.adapters.crypto')
|
|
|
|
|
|
@pytest.fixture
|
|
def database_adapter() -> ModuleType:
|
|
return _load_adapter_module('cli.tui.adapters.database')
|
|
|
|
|
|
@pytest.fixture
|
|
def jobs_adapter() -> ModuleType:
|
|
return _load_adapter_module('cli.tui.adapters.jobs')
|
|
|
|
|
|
@pytest.fixture
|
|
def gen_adapter() -> ModuleType:
|
|
return _load_adapter_module('cli.tui.adapters.gen')
|
|
|
|
|
|
@pytest.fixture
|
|
def configs_adapter() -> ModuleType:
|
|
return _load_adapter_module('cli.tui.adapters.configs')
|
|
|
|
|
|
@pytest.fixture
|
|
def ops_adapter() -> ModuleType:
|
|
return _load_adapter_module('cli.tui.adapters.ops')
|
|
|
|
|
|
@pytest.fixture
|
|
def health_adapter() -> ModuleType:
|
|
return _load_adapter_module('cli.tui.adapters.health')
|
|
|
|
|
|
@pytest.fixture
|
|
def load_adapter_module() -> Callable[[str], ModuleType]:
|
|
return _load_adapter_module
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def dispose_async_db_engine_after_test() -> None:
|
|
"""
|
|
在每个 TUI adapter 测试结束后尝试释放全局异步数据库连接池。
|
|
|
|
这些适配器测试会按需导入运行时模块;若其中某些路径触发真实数据库访问,
|
|
模块级 `async_engine` 可能在测试进程结束前仍持有连接,从而在 GC 阶段产生
|
|
SQLAlchemy 未归还连接告警。这里统一在测试后主动 `dispose()`,将清理职责收口
|
|
到测试夹具而非业务代码。
|
|
|
|
:return: None
|
|
"""
|
|
yield
|
|
database_module = sys.modules.get('config.database')
|
|
async_engine = getattr(database_module, 'async_engine', None) if database_module is not None else None
|
|
if async_engine is not None:
|
|
asyncio.run(async_engine.dispose())
|