初始提交
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
from pathlib import Path
|
||||
|
||||
import typer
|
||||
from pytest import MonkeyPatch
|
||||
|
||||
from cli.completion.installers import (
|
||||
CompletionInstallerService,
|
||||
CompletionInstallerShellSupport,
|
||||
CompletionShellRuntimePolicy,
|
||||
CompletionShellRuntimePolicyRegistry,
|
||||
)
|
||||
|
||||
|
||||
def test_build_source_command_uses_shell_runtime_policy() -> None:
|
||||
"""
|
||||
校验安装服务会按 shell 运行时策略构建 source 命令。
|
||||
|
||||
:return: None
|
||||
"""
|
||||
installer = CompletionInstallerService()
|
||||
target_file = Path('/tmp/shierfeng-completion')
|
||||
|
||||
assert installer.build_source_command(target_file, 'bash') == 'source /tmp/shierfeng-completion'
|
||||
assert installer.build_source_command(target_file, 'zsh') == 'source /tmp/shierfeng-completion'
|
||||
assert (
|
||||
installer.build_source_command(target_file, 'fish')
|
||||
== 'status --is-interactive; and source /tmp/shierfeng-completion'
|
||||
)
|
||||
assert installer.build_source_command(target_file, 'powershell') == '. "/tmp/shierfeng-completion"'
|
||||
|
||||
|
||||
def test_make_bash_completion_script_compatible_wraps_legacy_instructions() -> None:
|
||||
"""
|
||||
校验 Bash 兼容转换会包装旧版 shell 不支持的指令。
|
||||
|
||||
:return: None
|
||||
"""
|
||||
script_text = 'compopt -o dirnames\ncompopt -o default\ncomplete -o nosort -F _shierfeng_completion shierfeng'
|
||||
|
||||
compatible_script = CompletionInstallerShellSupport.make_bash_completion_script_compatible(script_text)
|
||||
|
||||
assert 'if command -v compopt >/dev/null 2>&1; then' in compatible_script
|
||||
assert 'complete -F _shierfeng_completion shierfeng' in compatible_script
|
||||
assert 'complete -o nosort -F _shierfeng_completion shierfeng 2>/dev/null' in compatible_script
|
||||
|
||||
|
||||
def test_render_completion_script_uses_runtime_policy_transformer(monkeypatch: MonkeyPatch) -> None:
|
||||
"""
|
||||
校验脚本渲染会通过 shell 运行时策略执行脚本后处理。
|
||||
|
||||
:param monkeypatch: pytest monkeypatch 工具
|
||||
:return: None
|
||||
"""
|
||||
|
||||
class DummyComplete:
|
||||
"""
|
||||
模拟 Click completion 生成器。
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
"""
|
||||
初始化模拟生成器。
|
||||
|
||||
:param args: 位置参数
|
||||
:param kwargs: 关键字参数
|
||||
:return: None
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def source() -> str:
|
||||
"""
|
||||
返回原始脚本文本。
|
||||
|
||||
:return: 原始脚本文本
|
||||
"""
|
||||
return 'raw-script'
|
||||
|
||||
runtime_policy_registry = CompletionShellRuntimePolicyRegistry(
|
||||
policies={
|
||||
'bash': CompletionShellRuntimePolicy(
|
||||
name='bash',
|
||||
click_completion_class=DummyComplete,
|
||||
script_transformer=lambda script_text: f'transformed::{script_text}',
|
||||
source_command_builder=lambda target_file: f'source {target_file}',
|
||||
)
|
||||
}
|
||||
)
|
||||
installer = CompletionInstallerService(shell_runtime_policy_registry=runtime_policy_registry)
|
||||
monkeypatch.setattr(installer, 'build_completion_click_command', lambda root_cli: object())
|
||||
|
||||
rendered_script = installer.render_completion_script(typer.Typer(), 'bash')
|
||||
|
||||
assert rendered_script == 'transformed::raw-script'
|
||||
|
||||
|
||||
def test_render_powershell_completion_script_contains_native_completer() -> None:
|
||||
"""
|
||||
校验 PowerShell completion 脚本会注册原生命令补全器。
|
||||
|
||||
:return: None
|
||||
"""
|
||||
installer = CompletionInstallerService()
|
||||
root_cli = typer.Typer()
|
||||
|
||||
@root_cli.command()
|
||||
def demo() -> None:
|
||||
return None
|
||||
|
||||
rendered_script = installer.render_completion_script(root_cli, 'powershell')
|
||||
|
||||
assert 'Register-ArgumentCompleter -Native -CommandName shierfeng' in rendered_script
|
||||
assert '$env:_SHIERFENG_COMPLETE = "powershell_complete"' in rendered_script
|
||||
@@ -0,0 +1,442 @@
|
||||
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
|
||||
Reference in New Issue
Block a user