ShiErFeng/shierfeng-fastapi-backend/tests/cli/completion/test_providers.py
2026-07-24 09:43:10 +08:00

443 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import importlib
import os
import sys
from pathlib import Path
from pytest import MonkeyPatch
BACKEND_DIR = Path(__file__).resolve().parents[3]
sys.path.insert(0, str(BACKEND_DIR))
sys.modules.pop('cli.completion.providers', None)
sys.modules.pop('cli', None)
completion_support = importlib.import_module('cli.completion.providers')
completion_gateway = completion_support.COMPLETION_PROVIDER_GATEWAY
completion_registry = completion_gateway.provider_registry
def test_complete_alembic_revisions_returns_default_choices_and_local_revisions(
tmp_path: Path,
monkeypatch: MonkeyPatch,
) -> None:
"""
校验 Alembic 版本补全会返回默认候选和本地迁移版本。
:param tmp_path: pytest 临时目录
:param monkeypatch: pytest monkeypatch 工具
:return: None
"""
versions_dir = tmp_path / 'alembic' / 'versions'
versions_dir.mkdir(parents=True)
(versions_dir / '2026_04_29_1000-abc123_add_demo_table.py').write_text('# test', encoding='utf-8')
monkeypatch.chdir(tmp_path)
candidates = completion_gateway.complete_alembic_revisions(None, None, 'a')
default_candidates = completion_gateway.complete_alembic_revisions(None, None, '')
assert 'abc123' in candidates
assert 'head' in default_candidates
assert 'base' in default_candidates
assert 'current' in default_candidates
assert '-1' in default_candidates
def test_complete_sql_files_returns_project_relative_sql_paths(
tmp_path: Path,
monkeypatch: MonkeyPatch,
) -> None:
"""
校验 SQL 文件补全会返回项目相对路径。
:param tmp_path: pytest 临时目录
:param monkeypatch: pytest monkeypatch 工具
:return: None
"""
sql_dir = tmp_path / 'sql'
sql_dir.mkdir()
(sql_dir / 'demo.sql').write_text('select 1;', encoding='utf-8')
monkeypatch.chdir(tmp_path)
candidates = completion_gateway.complete_sql_files(None, None, 'sql/d')
assert candidates == ['sql/demo.sql']
def test_complete_cache_names_returns_static_cache_name_choices(monkeypatch: MonkeyPatch) -> None:
"""
校验缓存名称补全会返回系统内置缓存名称。
:param monkeypatch: pytest monkeypatch 工具
:return: None
"""
class _FakeCacheKeyConfig:
"""
模拟缓存键配置对象。
"""
def __init__(self, key: str) -> None:
"""
初始化模拟缓存键配置对象。
:param key: 缓存名称
:return: None
"""
self.key = key
class _FakeEnumsModule:
"""
模拟枚举模块。
"""
RedisInitKeyConfig = [_FakeCacheKeyConfig('sys_config'), _FakeCacheKeyConfig('login_tokens')]
monkeypatch.setattr(
completion_registry.dynamic_service, 'load_runtime_module', lambda module_name: _FakeEnumsModule
)
candidates = completion_gateway.complete_cache_names(None, None, 'sys')
assert candidates == ['sys_config']
def test_complete_output_paths_returns_directories_and_zip_files(
tmp_path: Path,
monkeypatch: MonkeyPatch,
) -> None:
"""
校验导出路径补全会返回目录和 zip 文件。
:param tmp_path: pytest 临时目录
:param monkeypatch: pytest monkeypatch 工具
:return: None
"""
build_dir = tmp_path / 'build'
build_dir.mkdir()
(build_dir / 'gen.zip').write_text('zip', encoding='utf-8')
(build_dir / 'notes.txt').write_text('ignore', encoding='utf-8')
monkeypatch.chdir(tmp_path)
candidates = completion_gateway.complete_output_paths(None, None, 'build/')
assert 'build/gen.zip' in candidates
assert 'build/notes.txt' not in candidates
def test_complete_config_keys_returns_dynamic_readonly_choices(monkeypatch: MonkeyPatch) -> None:
"""
校验参数键名补全会返回动态只读查询结果。
:param monkeypatch: pytest monkeypatch 工具
:return: None
"""
class _FakeConfigRuntime:
"""
模拟配置运行时模块。
"""
class ConfigRuntime:
"""
模拟配置运行时服务对象。
"""
@staticmethod
def list_configs(**kwargs: str) -> dict:
"""
模拟配置列表查询入口。
:param kwargs: 查询参数
:return: 伪造的结果字典
"""
return kwargs
CONFIG_RUNTIME = ConfigRuntime()
monkeypatch.setattr(
completion_registry.dynamic_service, 'load_runtime_module', lambda module_name: _FakeConfigRuntime
)
monkeypatch.setattr(
completion_registry.dynamic_service,
'run_completion_coroutine',
lambda coroutine, *, env: {
'ok': True,
'items': [
{'configKey': 'sys.user.initPassword'},
{'configKey': 'sys.user.maxRetryCount'},
],
},
)
candidates = completion_gateway.complete_config_keys(None, None, 'sys.user.i')
assert candidates == ['sys.user.initPassword']
def test_complete_gen_table_names_returns_dynamic_readonly_choices(monkeypatch: MonkeyPatch) -> None:
"""
校验代码生成业务表补全会返回动态只读查询结果。
:param monkeypatch: pytest monkeypatch 工具
:return: None
"""
class _FakeGenRuntime:
"""
模拟代码生成运行时模块。
"""
class GenRuntime:
"""
模拟代码生成运行时服务对象。
"""
@staticmethod
def list_gen_tables(**kwargs: str) -> dict:
"""
模拟业务表列表查询入口。
:param kwargs: 查询参数
:return: 伪造的结果字典
"""
return kwargs
@staticmethod
def list_gen_db_tables(**kwargs: str) -> dict:
"""
模拟数据库物理表列表查询入口。
:param kwargs: 查询参数
:return: 伪造的结果字典
"""
return kwargs
GEN_RUNTIME = GenRuntime()
monkeypatch.setattr(completion_registry.dynamic_service, 'load_runtime_module', lambda module_name: _FakeGenRuntime)
monkeypatch.setattr(
completion_registry.dynamic_service,
'run_completion_coroutine',
lambda coroutine, *, env: {
'ok': True,
'items': [
{'tableName': 'sys_user'},
{'tableName': 'sys_role'},
],
},
)
candidates = completion_gateway.complete_gen_table_names(None, None, 'sys_u')
assert candidates == ['sys_user']
def test_dynamic_completion_returns_empty_list_when_runtime_fails(monkeypatch: MonkeyPatch) -> None:
"""
校验动态补全在运行时失败时会优雅降级为空列表。
:param monkeypatch: pytest monkeypatch 工具
:return: None
"""
monkeypatch.setattr(
completion_registry.dynamic_service,
'load_runtime_module',
lambda module_name: (_ for _ in ()).throw(RuntimeError('boom')),
)
candidates = completion_gateway.complete_gen_db_table_names(None, None, 'sys')
assert candidates == []
def test_complete_cache_keys_returns_dynamic_readonly_choices(monkeypatch: MonkeyPatch) -> None:
"""
校验缓存键名补全会基于缓存名称返回动态只读查询结果。
:param monkeypatch: pytest monkeypatch 工具
:return: None
"""
class _FakeCacheRuntime:
"""
模拟缓存运行时模块。
"""
class CacheRuntime:
"""
模拟缓存运行时服务对象。
"""
@staticmethod
def list_cache_keys(cache_name: str) -> dict[str, str]:
"""
模拟缓存键列表查询入口。
:param cache_name: 缓存名称
:return: 伪造的参数字典
"""
return {'cache_name': cache_name}
CACHE_RUNTIME = CacheRuntime()
class _FakeContext:
"""
模拟 Click 上下文。
"""
params = {'cache_name': 'sys_config'}
monkeypatch.setattr(
completion_registry.dynamic_service, 'load_runtime_module', lambda module_name: _FakeCacheRuntime
)
monkeypatch.setattr(
completion_registry.dynamic_service,
'run_completion_coroutine',
lambda coroutine, *, env: {
'ok': True,
'keys': ['site.name', 'site.logo'],
},
)
candidates = completion_gateway.complete_cache_keys(_FakeContext(), None, 'site.n')
assert candidates == ['site.name']
def test_complete_job_names_returns_dynamic_readonly_choices(monkeypatch: MonkeyPatch) -> None:
"""
校验任务名称补全会返回动态只读查询结果。
:param monkeypatch: pytest monkeypatch 工具
:return: None
"""
class _FakeJobRuntime:
"""
模拟任务运行时模块。
"""
class JobRuntime:
"""
模拟任务运行时服务对象。
"""
@staticmethod
def list_jobs(**kwargs: str) -> dict:
"""
模拟任务列表查询入口。
:param kwargs: 查询参数
:return: 伪造的参数字典
"""
return kwargs
JOB_RUNTIME = JobRuntime()
monkeypatch.setattr(completion_registry.dynamic_service, 'load_runtime_module', lambda module_name: _FakeJobRuntime)
monkeypatch.setattr(
completion_registry.dynamic_service,
'run_completion_coroutine',
lambda coroutine, *, env: {
'ok': True,
'items': [
{'jobName': '同步任务'},
{'jobName': '缓存预热'},
],
},
)
candidates = completion_gateway.complete_job_names(None, None, '')
assert candidates == ['同步任务']
def test_complete_job_ids_returns_dynamic_readonly_choices(monkeypatch: MonkeyPatch) -> None:
"""
校验任务 ID 补全会返回动态只读查询结果。
:param monkeypatch: pytest monkeypatch 工具
:return: None
"""
class _FakeJobRuntime:
"""
模拟任务运行时模块。
"""
class JobRuntime:
"""
模拟任务运行时服务对象。
"""
@staticmethod
def list_jobs(**kwargs: str) -> dict:
"""
模拟任务列表查询入口。
:param kwargs: 查询参数
:return: 伪造的参数字典
"""
return kwargs
JOB_RUNTIME = JobRuntime()
monkeypatch.setattr(completion_registry.dynamic_service, 'load_runtime_module', lambda module_name: _FakeJobRuntime)
monkeypatch.setattr(
completion_registry.dynamic_service,
'run_completion_coroutine',
lambda coroutine, *, env: {
'ok': True,
'items': [
{'jobId': 101},
{'jobId': 202},
],
},
)
candidates = completion_gateway.complete_job_ids(None, None, '1')
assert candidates == ['101']
def test_run_completion_coroutine_scopes_app_env(monkeypatch: MonkeyPatch) -> None:
"""
校验动态补全执行时会临时注入 APP_ENV并在结束后恢复原值。
:param monkeypatch: pytest monkeypatch 工具
:return: None
"""
monkeypatch.setenv('APP_ENV', 'prod')
captured: dict[str, str | None] = {}
async def _read_env() -> str | None:
captured['during'] = os.environ.get('APP_ENV')
return captured['during']
result = completion_registry.dynamic_service.run_completion_coroutine(_read_env(), env='dev')
assert result == 'dev'
assert captured['during'] == 'dev'
assert os.environ.get('APP_ENV') == 'prod'
def test_run_completion_coroutine_clears_scoped_app_env_when_original_missing(monkeypatch: MonkeyPatch) -> None:
"""
校验动态补全执行结束后会移除临时注入的 APP_ENV。
:param monkeypatch: pytest monkeypatch 工具
:return: None
"""
monkeypatch.delenv('APP_ENV', raising=False)
async def _read_env() -> str | None:
return os.environ.get('APP_ENV')
result = completion_registry.dynamic_service.run_completion_coroutine(_read_env(), env='dockerpg')
assert result == 'dockerpg'
assert 'APP_ENV' not in os.environ