6eac0b2cf2
Worker Base Image Build / Build Worker Base Images (worker-base-builder-cache, infra/docker/worker-base-builder.Dockerfile, worker-base-builder, builder) (push) Failing after 1m54s
Worker Base Image Build / Build Worker Base Images (worker-base-runtime-cache, infra/docker/worker-base-runtime.Dockerfile, worker-base-runtime, runtime) (push) Failing after 1m36s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Failing after 1m29s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m4s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m2s
CI/CD Pipeline / Unit Tests (push) Successful in 3m38s
CI/CD Pipeline / Integration Tests (push) Successful in 2m0s
CI/CD Pipeline / Frontend Lint (push) Successful in 28s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 44s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Failing after 2m16s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 8m14s
CI/CD Pipeline / Build Staging Worker Image (push) Failing after 2m0s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Has been skipped
同步内容: 1. CI流水线配置(ci-pipeline.yml)与develop对齐 2. PR构建脚本docker_build_only.sh增加buildx→docker build回退 3. pre-build步骤worker基础镜像构建增加buildx回退 4. 单元测试脚本全量覆盖率改为仅报告不阻塞 5. diff-cover依赖加入requirements-dev.txt 6. worker base builder/runtime Dockerfile同步 7. test_config_oss.py clear=False→clear=True修复OSS污染 8. Frontend Lint增加prettier依赖
227 lines
7.4 KiB
Python
227 lines
7.4 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
根据PR改动文件选择需要运行的单元测试文件。
|
|
|
|
映射规则:
|
|
1. 改了tests/unit下的测试文件 -> 直接跑这些测试
|
|
2. 改了apps/api/app/api/routes/xxx.py -> 匹配 test_*xxx*.py
|
|
3. 改了apps/api/app/services/xxx.py -> 匹配 test_*xxx*.py
|
|
4. 改了apps/worker/.../xxx.py -> 匹配 test_*xxx*.py
|
|
5. 改了apps/worker/video_processing/xxx_engine.py -> 匹配 test_*xxx*.py
|
|
6. 改了packages/.../xxx.py -> 匹配 test_*xxx*.py
|
|
7. 改了公共核心模块(core/middleware/schemas/config/db/auth/dependencies) -> 全量
|
|
8. 改了依赖文件(requirements*.txt, pyproject.toml) -> 全量
|
|
9. 改了alembic/migrations -> 全量
|
|
10. 匹配不到测试的改动 -> 全量兜底
|
|
"""
|
|
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
TESTS_DIR = ROOT / "tests" / "unit"
|
|
|
|
# 触发全量的文件模式(公共核心/基础设施)
|
|
FULL_RUN_PATTERNS = [
|
|
"apps/api/app/core/",
|
|
"apps/api/app/middleware/",
|
|
"apps/api/app/schemas/",
|
|
"apps/api/app/config.py",
|
|
"apps/api/app/db.py",
|
|
"apps/api/app/auth.py",
|
|
"apps/api/app/dependencies.py",
|
|
"packages/shared/",
|
|
"alembic/",
|
|
"migrations/",
|
|
"requirements-base.txt",
|
|
"requirements.txt",
|
|
"requirements-dev.txt",
|
|
"pyproject.toml",
|
|
"setup.cfg",
|
|
".gitea/workflows/",
|
|
"scripts/ci/",
|
|
"tests/conftest.py",
|
|
]
|
|
|
|
# 目录到测试文件关键词的映射(模糊匹配)
|
|
DIR_KEYWORD_MAP = {
|
|
"apps/api/app/api/routes/": "", # 用文件名匹配
|
|
"apps/api/app/services/": "", # 用文件名匹配
|
|
"apps/worker/worker_app/tasks/": "",
|
|
"apps/worker/video_processing/": "",
|
|
"apps/worker/services/": "",
|
|
"packages/application/": "",
|
|
"packages/adapters/": "",
|
|
}
|
|
|
|
|
|
def get_changed_files():
|
|
"""获取改动文件列表(从环境变量或git diff)。"""
|
|
# 优先从环境变量读取(CI中传入)
|
|
changed_env = os.environ.get("CHANGED_FILES", "")
|
|
if changed_env:
|
|
return [f.strip() for f in changed_env.split("\n") if f.strip()]
|
|
|
|
# 回退到git diff(本地调试用)
|
|
try:
|
|
result = subprocess.run(
|
|
["git", "diff", "--name-only", "origin/develop...HEAD"],
|
|
capture_output=True,
|
|
text=True,
|
|
cwd=ROOT,
|
|
)
|
|
if result.returncode == 0:
|
|
return [f.strip() for f in result.stdout.split("\n") if f.strip()]
|
|
except Exception:
|
|
pass
|
|
|
|
return []
|
|
|
|
|
|
def should_full_run(files):
|
|
"""检查是否需要全量运行。"""
|
|
for f in files:
|
|
for pattern in FULL_RUN_PATTERNS:
|
|
if f.startswith(pattern) or f == pattern:
|
|
print(f"[full-run] 触发全量: {f} 匹配 {pattern}")
|
|
return True
|
|
return False
|
|
|
|
|
|
def extract_module_name(filepath):
|
|
"""从文件路径提取模块名(用于匹配测试文件)。"""
|
|
# 去掉扩展名
|
|
name = Path(filepath).stem
|
|
|
|
# 特殊映射
|
|
special_mappings = {
|
|
# 路由文件
|
|
"edit_plans_adjustments": "edit_plan_adjustments",
|
|
"edit_plans_ai": "edit_plan",
|
|
"edit_plans_clips_batch": "edit_plan",
|
|
"edit_plans_cover": "edit_plan_cover",
|
|
"edit_plans_export": "edit_plan_export",
|
|
"edit_plans_filter": "edit_plan_filter",
|
|
"edit_plans_generation": "edit_plan_generation",
|
|
"edit_plans_transitions": "edit_plan_transitions",
|
|
"asset_libraries": "asset_library",
|
|
"classification_jobs": "classification",
|
|
"chunked_upload": "chunked_upload",
|
|
"form_upload": "form_upload",
|
|
# 服务文件
|
|
"edit_template_service": "edit_template_service",
|
|
"edit_plan_service": "edit_plan_service",
|
|
"unified_render_service": "unified_render",
|
|
"job_service": "job_service",
|
|
"auto_clip_service": "auto_clip",
|
|
"cosyvoice_service": "cosyvoice",
|
|
"video_compose_service": "video_compose",
|
|
"email_service": "email_service",
|
|
}
|
|
|
|
return special_mappings.get(name, name)
|
|
|
|
|
|
def find_matching_tests(keyword, all_test_files):
|
|
"""模糊匹配测试文件。"""
|
|
keyword_lower = keyword.lower().replace("_", "")
|
|
matches = []
|
|
for tf in all_test_files:
|
|
tf_name = Path(tf).stem.lower().replace("_", "")
|
|
if keyword_lower in tf_name or tf_name in keyword_lower:
|
|
matches.append(tf)
|
|
return matches
|
|
|
|
|
|
def get_all_test_files():
|
|
"""获取所有单元测试文件。"""
|
|
if not TESTS_DIR.exists():
|
|
return []
|
|
return sorted(str(f.relative_to(ROOT)) for f in TESTS_DIR.glob("test_*.py"))
|
|
|
|
|
|
def select_tests(changed_files):
|
|
"""主函数:选择要运行的测试文件。"""
|
|
all_tests = get_all_test_files()
|
|
|
|
if not changed_files:
|
|
print("[info] 未找到改动文件,全量运行")
|
|
return all_tests, "full (no changes detected)"
|
|
|
|
if should_full_run(changed_files):
|
|
return all_tests, "full (core/common files changed)"
|
|
|
|
selected = set()
|
|
test_file_changes = []
|
|
source_file_changes = []
|
|
|
|
for f in changed_files:
|
|
# 测试文件本身改动(仅保留仍存在的文件,删除的测试文件不加入运行列表)
|
|
if f.startswith("tests/unit/test_") and f.endswith(".py"):
|
|
if (ROOT / f).exists():
|
|
test_file_changes.append(f)
|
|
selected.add(f)
|
|
else:
|
|
print(f"[skip-deleted] 测试文件已删除,跳过: {f}")
|
|
# 源码文件改动
|
|
elif f.endswith(".py"):
|
|
source_file_changes.append(f)
|
|
module_name = extract_module_name(f)
|
|
matches = find_matching_tests(module_name, all_tests)
|
|
if matches:
|
|
for m in matches:
|
|
selected.add(m)
|
|
print(f"[map] {f} -> {len(matches)} 个测试: {[Path(m).name for m in matches]}")
|
|
else:
|
|
print(f"[nomatch] {f} (module: {module_name}) 未找到匹配的测试文件")
|
|
|
|
if not selected:
|
|
print("[info] 未匹配到任何测试文件,全量运行兜底")
|
|
return all_tests, "full (no matching tests)"
|
|
|
|
return sorted(selected), f"incremental ({len(selected)} test files)"
|
|
|
|
|
|
def main():
|
|
changed_files = get_changed_files()
|
|
print(f"=== 改动文件 ({len(changed_files)} 个) ===")
|
|
for f in changed_files[:20]:
|
|
print(f" {f}")
|
|
if len(changed_files) > 20:
|
|
print(f" ... 还有 {len(changed_files) - 20} 个")
|
|
print()
|
|
|
|
selected, mode = select_tests(changed_files)
|
|
|
|
print()
|
|
print(f"=== 运行模式: {mode} ===")
|
|
print(f"=== 选中测试文件: {len(selected)} 个 ===")
|
|
for t in selected[:20]:
|
|
print(f" {t}")
|
|
if len(selected) > 20:
|
|
print(f" ... 还有 {len(selected) - 20} 个")
|
|
|
|
# 输出结果文件(供CI后续步骤使用)
|
|
output_file = os.environ.get("SELECTED_TESTS_OUTPUT", "")
|
|
if output_file:
|
|
with open(output_file, "w") as f:
|
|
for t in selected:
|
|
f.write(t + "\n")
|
|
print(f"\n已写入到: {output_file}")
|
|
|
|
# 设置环境变量标记
|
|
gh_output = os.environ.get("GITHUB_OUTPUT", "")
|
|
if gh_output:
|
|
with open(gh_output, "a") as f:
|
|
f.write(f"test_count={len(selected)}\n")
|
|
f.write("mode=" + ("incremental" if "incremental" in mode else "full") + "\n")
|
|
|
|
# 退出码:0=增量, 1=全量(供CI判断)
|
|
sys.exit(0 if "incremental" in mode else 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|