mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-29 03:56:43 +08:00
152 lines
6.2 KiB
Python
152 lines
6.2 KiB
Python
"""后端架构治理工作流的静态契约测试。"""
|
|
|
|
import configparser
|
|
from pathlib import Path
|
|
|
|
from ruamel.yaml import YAML
|
|
|
|
PROJECT_ROOT = Path(__file__).parents[1]
|
|
WORKFLOW_ROOT = PROJECT_ROOT / ".github" / "workflows"
|
|
|
|
|
|
def _load_workflow(name: str) -> dict:
|
|
"""以 YAML 1.2 解析 GitHub Actions 工作流。"""
|
|
yaml = YAML(typ="safe")
|
|
return yaml.load((WORKFLOW_ROOT / name).read_text(encoding="utf-8"))
|
|
|
|
|
|
def _step_commands(workflow: dict, job_name: str) -> str:
|
|
"""拼接指定 job 的命令,便于断言关键门禁没有被移除。"""
|
|
return "\n".join(
|
|
step.get("run", "")
|
|
for step in workflow["jobs"][job_name]["steps"]
|
|
)
|
|
|
|
|
|
def test_unit_test_workflow_has_independent_host_architecture_gate():
|
|
"""主仓 PR 与推送必须在全量分片外快速执行宿主架构门禁。"""
|
|
workflow = _load_workflow("test.yml")
|
|
commands = _step_commands(workflow, "architecture")
|
|
|
|
assert "pull_request" in workflow["on"]
|
|
assert "push" in workflow["on"]
|
|
assert "tests/test_architecture_dependencies.py" in commands
|
|
assert "tests/test_architecture_contract_baseline.py" in commands
|
|
assert "scripts/architecture/baseline.py --check-host" in commands
|
|
assert "scripts/architecture/ruff_ratchet.py" in commands
|
|
assert "scripts/architecture/mypy_ratchet.py" in commands
|
|
assert "scripts/architecture/ruff_ratchet.py --write" not in commands
|
|
assert "scripts/architecture/mypy_ratchet.py --write" not in commands
|
|
assert "scripts/architecture/service_locator.py" in commands
|
|
assert "scripts/startup/performance.py --check --repeat 3" in commands
|
|
|
|
|
|
def test_official_plugin_observation_is_scheduled_and_never_writes_fixture():
|
|
"""跨仓观察应定时或手工运行,只上传语义报告而不刷新 fixture。"""
|
|
workflow = _load_workflow("architecture-observe.yml")
|
|
commands = _step_commands(workflow, "observe")
|
|
steps = workflow["jobs"]["observe"]["steps"]
|
|
|
|
assert "schedule" in workflow["on"]
|
|
assert "workflow_dispatch" in workflow["on"]
|
|
assert "--check-plugins" in commands
|
|
assert "--report official-plugin-architecture-report.json" in commands
|
|
assert "--write" not in commands
|
|
assert any(
|
|
step.get("uses", "").startswith("actions/upload-artifact@")
|
|
for step in steps
|
|
)
|
|
|
|
|
|
def test_coverage_job_runs_full_suite_and_read_only_ratchet() -> None:
|
|
"""PR 与推送的 Coverage job 必须串行采集全量报告并只读检查低水位。"""
|
|
workflow = _load_workflow("test.yml")
|
|
coverage_job = workflow["jobs"]["coverage"]
|
|
steps = coverage_job["steps"]
|
|
assert workflow["on"]["pull_request"]["branches"] == ["v3"]
|
|
assert workflow["on"]["push"]["branches"] == ["v3"]
|
|
assert "workflow_dispatch" in workflow["on"]
|
|
assert coverage_job["runs-on"] == "ubuntu-latest"
|
|
assert "if" not in coverage_job
|
|
|
|
setup_step = next(step for step in steps if step.get("name") == "Set up uv")
|
|
install_step = next(
|
|
step for step in steps if step.get("name") == "Install dependencies"
|
|
)
|
|
generate_step = next(
|
|
step for step in steps if step.get("name") == "Generate coverage reports"
|
|
)
|
|
upload_step = next(
|
|
step for step in steps if step.get("name") == "Upload coverage report"
|
|
)
|
|
ratchet_step = next(
|
|
step for step in steps if step.get("name") == "Check coverage ratchet"
|
|
)
|
|
assert setup_step["with"]["python-version"] == "3.14"
|
|
assert install_step["run"] == "uv sync --locked"
|
|
commands = generate_step["run"]
|
|
expected_commands = [
|
|
"python -m coverage erase",
|
|
"python -m coverage run tests/run.py --serial",
|
|
"python -m coverage report",
|
|
"python -m coverage json",
|
|
"python -m coverage xml",
|
|
]
|
|
positions = [commands.index(command) for command in expected_commands]
|
|
assert positions == sorted(positions)
|
|
assert ratchet_step["run"].endswith("scripts/architecture/coverage_ratchet.py")
|
|
assert steps.index(generate_step) < steps.index(upload_step) < steps.index(ratchet_step)
|
|
|
|
all_commands = _step_commands(workflow, "coverage")
|
|
assert "--write" not in all_commands
|
|
assert "|| true" not in all_commands
|
|
assert all(step.get("continue-on-error") is not True for step in steps)
|
|
assert all("always()" not in str(step.get("if", "")) for step in steps)
|
|
|
|
coverage_config = configparser.ConfigParser()
|
|
coverage_config.read(PROJECT_ROOT / ".coveragerc", encoding="utf-8")
|
|
assert coverage_config["run"]["source"].strip() == "app"
|
|
assert "app/plugins/*/*" in coverage_config["run"]["omit"].splitlines()
|
|
|
|
|
|
def test_pylint_workflow_runs_for_v3_pull_requests_and_pushes():
|
|
"""改动文件应硬门禁,而全仓存量问题只能生成建议性报告。"""
|
|
workflow = _load_workflow("pylint.yml")
|
|
commands = _step_commands(workflow, "pylint")
|
|
|
|
assert workflow["on"]["pull_request"]["branches"] == ["v3"]
|
|
assert workflow["on"]["push"]["branches"] == ["v3"]
|
|
assert "changed-python-files.txt" in commands
|
|
assert "xargs uv run --locked --no-sync pylint" in commands
|
|
assert "pylint app/" in commands
|
|
assert "--output-format=json > pylint-report.json || true" in commands
|
|
|
|
full_report_step = next(
|
|
step
|
|
for step in workflow["jobs"]["pylint"]["steps"]
|
|
if step.get("name") == "Generate full advisory report"
|
|
)
|
|
assert "|| true" in full_report_step["run"]
|
|
|
|
|
|
def test_upload_artifact_actions_share_node24_major():
|
|
"""所有工件上传入口必须共用 Node 24 的 v7 主版本,避免 CI 告警与版本分叉。"""
|
|
actions_by_workflow: dict[str, list[str]] = {}
|
|
for workflow_path in WORKFLOW_ROOT.glob("*.yml"):
|
|
workflow = _load_workflow(workflow_path.name)
|
|
upload_actions = [
|
|
step["uses"]
|
|
for job in workflow.get("jobs", {}).values()
|
|
for step in job.get("steps", [])
|
|
if step.get("uses", "").startswith("actions/upload-artifact@")
|
|
]
|
|
if upload_actions:
|
|
actions_by_workflow[workflow_path.name] = upload_actions
|
|
|
|
assert actions_by_workflow == {
|
|
"architecture-observe.yml": ["actions/upload-artifact@v7"],
|
|
"pylint.yml": ["actions/upload-artifact@v7"],
|
|
"site-adapter-collector.yml": ["actions/upload-artifact@v7"],
|
|
"test.yml": ["actions/upload-artifact@v7"],
|
|
}
|