mirror of
https://github.com/insistence/RuoYi-Vue3-FastAPI.git
synced 2026-10-16 22:51:11 +00:00
refactor: 统一多前端工程架构、插件配置与 CI (#131)
* refactor: 重构项目架构 * refactor: 重构ci和测试 * fix: 修复测试 * feat: 新增更多E2E测试 * fix: 修复E2E测试
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
import importlib.util
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def load_script(name: str) -> ModuleType:
|
||||
"""
|
||||
按文件路径加载 CI 脚本,避免依赖仓库外的模块搜索路径。
|
||||
|
||||
:param name: 不含扩展名的脚本名称
|
||||
:return: 已执行模块顶层定义的脚本模块
|
||||
"""
|
||||
spec = importlib.util.spec_from_file_location(name, ROOT / '.github/scripts' / f'{name}.py')
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
compose = load_script('check_compose')
|
||||
reports = load_script('check_test_report')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def configuration() -> dict[str, Any]:
|
||||
"""提供前端、nginx 和后端框架一致的 Compose 配置。"""
|
||||
frontend = ROOT / 'ruoyi-fastapi-frontend/vue2/web'
|
||||
return {
|
||||
'services': {
|
||||
'ruoyi-frontend': {
|
||||
'build': {'context': str(frontend), 'dockerfile': 'Dockerfile'},
|
||||
'volumes': [
|
||||
{'source': str(frontend / 'bin/nginx.dockermy.conf'), 'target': '/etc/nginx/conf.d/default.conf'}
|
||||
],
|
||||
},
|
||||
'ruoyi-backend-my': {'environment': {'RUOYI_PLUGIN_FRONTEND_FRAMEWORK': 'vue2'}},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def test_compose_accepts_consistent_framework(configuration: dict[str, Any]) -> None:
|
||||
"""框架一致的 Compose 配置可以通过校验。"""
|
||||
compose.validate_configuration(configuration, 'vue2', 'my', ROOT)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('component', ['build', 'nginx', 'backend'])
|
||||
def test_compose_rejects_mixed_framework_deployment(configuration: dict[str, Any], component: str) -> None:
|
||||
"""前端、nginx 或后端混用框架时拒绝部署配置。"""
|
||||
invalid = deepcopy(configuration)
|
||||
if component == 'build':
|
||||
invalid['services']['ruoyi-frontend']['build']['context'] = str(ROOT / 'ruoyi-fastapi-frontend/vue3/web')
|
||||
elif component == 'nginx':
|
||||
invalid['services']['ruoyi-frontend']['volumes'][0]['source'] = str(
|
||||
ROOT / 'ruoyi-fastapi-frontend/vue3/web/bin/nginx.dockermy.conf'
|
||||
)
|
||||
else:
|
||||
invalid['services']['ruoyi-backend-my']['environment']['RUOYI_PLUGIN_FRONTEND_FRAMEWORK'] = 'vue3'
|
||||
with pytest.raises(ValueError):
|
||||
compose.validate_configuration(invalid, 'vue2', 'my', ROOT)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'cases',
|
||||
['', '<testcase><skipped /></testcase>', '<testcase><failure /></testcase>', '<testcase><error /></testcase>'],
|
||||
)
|
||||
def test_report_rejects_empty_skipped_or_failed_verification(tmp_path: Path, cases: str) -> None:
|
||||
"""空报告、全部跳过和失败用例都不能视为验收成功。"""
|
||||
report = tmp_path / 'results.xml'
|
||||
report.write_text(f'<testsuites><testsuite>{cases}</testsuite></testsuites>', encoding='utf-8')
|
||||
with pytest.raises(ValueError):
|
||||
reports.check_report(report)
|
||||
|
||||
|
||||
def test_expected_platform_skip_still_requires_executed_tests(tmp_path: Path) -> None:
|
||||
"""允许预期平台跳过时仍要求实际执行测试。"""
|
||||
report = tmp_path / 'results.xml'
|
||||
report.write_text(
|
||||
'<testsuites><testsuite><testcase/><testcase><skipped/></testcase></testsuite></testsuites>', encoding='utf-8'
|
||||
)
|
||||
assert reports.check_report(report, allow_skips=True) == (1, 1)
|
||||
|
||||
|
||||
def test_missing_report_is_not_success(tmp_path: Path) -> None:
|
||||
"""报告缺失时不能视为验收成功。"""
|
||||
with pytest.raises(FileNotFoundError):
|
||||
reports.check_report(tmp_path / 'missing.xml')
|
||||
@@ -0,0 +1,169 @@
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
SCRIPT = Path(__file__).resolve().parents[1] / 'scripts' / 'ci_plan.py'
|
||||
SPEC = importlib.util.spec_from_file_location('ci_plan', SCRIPT)
|
||||
ci = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(ci)
|
||||
FRONTEND_PROJECT_COUNT = 4
|
||||
STANDARD_E2E_COMBINATION_COUNT = 4
|
||||
FULL_E2E_COMBINATION_COUNT = 16
|
||||
|
||||
|
||||
def test_readme_change_only_runs_quality() -> None:
|
||||
"""仅文档变更时只执行质量检查。"""
|
||||
assert ci.build_plan(['README.md'])['required'] == {
|
||||
'quality': True,
|
||||
'backend': False,
|
||||
'frontend': False,
|
||||
'integration': False,
|
||||
'e2e': False,
|
||||
'native': False,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'path',
|
||||
[
|
||||
'ruoyi-fastapi-backend/cli/groups/plugin/options.py',
|
||||
'ruoyi-fastapi-backend/cli/groups/plugin/controller.py',
|
||||
'ruoyi-fastapi-backend/cli/runtime/plugin/service.py',
|
||||
'ruoyi-fastapi-backend/tests/module_plugin/controller/test_plugin_release_controller.py',
|
||||
'ruoyi-fastapi-backend/tests/sql/test_plugin_release_schema.py',
|
||||
],
|
||||
)
|
||||
def test_shared_plugin_changes_retain_windows_and_release_coverage(path: str) -> None:
|
||||
"""共享插件变更保留原生模块及发布验收覆盖。"""
|
||||
plan = ci.build_plan([path])
|
||||
assert plan['required']['backend']
|
||||
assert plan['required']['native']
|
||||
assert plan['release']
|
||||
|
||||
|
||||
def test_native_builder_change_runs_native_verification() -> None:
|
||||
"""原生构建器变更必须触发原生验收。"""
|
||||
plan = ci.build_plan(['ruoyi-fastapi-backend/scripts/build_native_plugin.py'])
|
||||
assert plan['required']['native']
|
||||
|
||||
|
||||
def test_bundle_example_change_runs_both_web_consumers() -> None:
|
||||
"""示例开发代理由两版 Web 测试直接引用,变更时均应执行。"""
|
||||
plan = ci.build_plan(['ruoyi-fastapi-backend/plugins/examples/python/bundle_demo/web/dev/mockRequest.js'])
|
||||
assert plan['frontend_matrix'] == {
|
||||
'include': [
|
||||
{'framework': 'vue2', 'target': 'web'},
|
||||
{'framework': 'vue3', 'target': 'web'},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'path',
|
||||
[
|
||||
'ruoyi-fastapi-backend/module_admin/service/login_service.py',
|
||||
'ruoyi-fastapi-backend/tests/scheduler_helpers.py',
|
||||
'ruoyi-fastapi-backend/tests/plugins/core/runtime/test_browser_session.py',
|
||||
],
|
||||
)
|
||||
def test_shared_auth_and_test_helpers_keep_both_network_suites(path: str) -> None:
|
||||
"""共享鉴权和测试夹具变更触发两版网络验收。"""
|
||||
plan = ci.build_plan([path])
|
||||
assert plan['required']['backend']
|
||||
assert plan['required']['integration']
|
||||
assert plan['network_frameworks'] == ['vue2', 'vue3']
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'path', ['.github/workflows/ci.yml', '.github/scripts/ci_plan.py', 'ruoyi-fastapi-backend/requirements-test.txt']
|
||||
)
|
||||
def test_ci_and_dependency_changes_select_every_category(path: str) -> None:
|
||||
"""CI 及共享依赖变更覆盖全部任务分类。"""
|
||||
plan = ci.build_plan([path])
|
||||
assert all(plan['required'].values())
|
||||
assert len(plan['frontend_matrix']['include']) == FRONTEND_PROJECT_COUNT
|
||||
assert len(plan['e2e_matrix']['include']) == STANDARD_E2E_COMBINATION_COUNT
|
||||
|
||||
|
||||
def test_mobile_change_does_not_build_other_projects_or_web_e2e() -> None:
|
||||
"""单个移动端变更只选中对应工程。"""
|
||||
plan = ci.build_plan(['ruoyi-fastapi-frontend/vue2/mobile/src/utils/request.js'])
|
||||
assert plan['frontend_matrix'] == {'include': [{'framework': 'vue2', 'target': 'mobile'}]}
|
||||
assert not plan['required']['e2e']
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'path',
|
||||
[
|
||||
'ruoyi-fastapi-test/frontend/mobile-harness.mjs',
|
||||
'ruoyi-fastapi-test/frontend/mobile-behavior.mjs',
|
||||
],
|
||||
)
|
||||
def test_shared_mobile_harness_runs_both_mobile_projects(path: str) -> None:
|
||||
"""共享移动端测试工具变更同时验证两版工程。"""
|
||||
plan = ci.build_plan([path])
|
||||
assert plan['required']['frontend']
|
||||
assert plan['frontend_matrix'] == {
|
||||
'include': [
|
||||
{'framework': 'vue2', 'target': 'mobile'},
|
||||
{'framework': 'vue3', 'target': 'mobile'},
|
||||
]
|
||||
}
|
||||
assert not plan['required']['e2e']
|
||||
|
||||
|
||||
def test_web_change_selects_matching_framework_and_both_databases() -> None:
|
||||
"""Web 变更使用对应框架并覆盖两类数据库。"""
|
||||
plan = ci.build_plan(['ruoyi-fastapi-frontend/vue2/web/src/views/login.vue'])
|
||||
assert plan['network_frameworks'] == ['vue2']
|
||||
assert {(row['framework'], row['database'], row['python']) for row in plan['e2e_matrix']['include']} == {
|
||||
('vue2', 'mysql', '3.12'),
|
||||
('vue2', 'postgresql', '3.12'),
|
||||
}
|
||||
|
||||
|
||||
def test_shared_time_contract_runs_all_frontends_and_database_checks() -> None:
|
||||
"""共享时间契约触发全部前端及数据库检查。"""
|
||||
plan = ci.build_plan(['ruoyi-fastapi-test/time-contract/fixtures.json'])
|
||||
assert len(plan['frontend_matrix']['include']) == FRONTEND_PROJECT_COUNT
|
||||
assert plan['database']
|
||||
|
||||
|
||||
def test_framework_move_keeps_both_sides_in_plan() -> None:
|
||||
"""跨框架移动文件保留来源和目标两侧验证。"""
|
||||
plan = ci.build_plan(
|
||||
['ruoyi-fastapi-frontend/vue2/web/src/utils/demo.js', 'ruoyi-fastapi-frontend/vue3/web/src/utils/demo.js']
|
||||
)
|
||||
assert plan['network_frameworks'] == ['vue2', 'vue3']
|
||||
|
||||
|
||||
def test_full_run_preserves_python_database_and_framework_combinations() -> None:
|
||||
"""完整运行保留 Python、数据库和框架的全部组合。"""
|
||||
plan = ci.build_plan([], full=True)
|
||||
assert all(plan['required'].values())
|
||||
assert len(plan['e2e_matrix']['include']) == FULL_E2E_COMBINATION_COUNT
|
||||
assert {row['python'] for row in plan['e2e_matrix']['include']} == {'3.10', '3.11', '3.12', '3.13'}
|
||||
|
||||
|
||||
@pytest.mark.parametrize('result', ['failure', 'cancelled', 'skipped', None])
|
||||
def test_gate_rejects_unsuccessful_required_job(result: str | None) -> None:
|
||||
"""必跑任务失败、取消、跳过或缺失均不能通过门禁。"""
|
||||
plan = ci.build_plan(['README.md'])
|
||||
needs = {'plan': {'result': 'success'}, **{name: {'result': 'skipped'} for name in ci.CATEGORIES}}
|
||||
needs['quality']['result'] = result
|
||||
assert ci.check_results(plan, needs)
|
||||
|
||||
|
||||
def test_gate_allows_only_unselected_jobs_to_skip() -> None:
|
||||
"""只有未选中的任务允许跳过。"""
|
||||
plan = ci.build_plan(['README.md'])
|
||||
needs = {'plan': {'result': 'success'}, **{name: {'result': 'skipped'} for name in ci.CATEGORIES}}
|
||||
needs['quality']['result'] = 'success'
|
||||
assert ci.check_results(plan, needs) == []
|
||||
|
||||
|
||||
def test_gate_fails_closed_when_plan_or_job_is_missing() -> None:
|
||||
"""缺少执行计划或任务结果时门禁必须拒绝放行。"""
|
||||
assert ci.check_results({}, {})
|
||||
assert ci.check_results(ci.build_plan(['README.md']), {'plan': {'result': 'success'}})
|
||||
Reference in New Issue
Block a user