"""D13:锁文件生成契约(部署侧硬性要求) 锁文件 deploy/requirements-{base,moldinsight}.lock.txt 的存在性 + 体积下限 是部署侧硬性要求: - 锁文件必须在 moldinsight conda 环境构建成功后落盘(见 deploy/generate_lockfiles.sh/.bat) - 锁文件必须以 git 跟踪方式提交,CI / 离线构建 / 生产复现部署才能直接 `pip install -r` - 若 lock.txt 缺失或异常空(仅镜像元数据 < 5 行),说明构建流程未走 D13 流程 CI 门禁建议: - 仓库侧默认 pytest(`pytest tests/ -q`)**不**强制这些断言——锁文件属"部署侧产物", 首次构建未完成时不应阻塞日常单测 - 部署侧 / CI 镜像构建 job 用 `--run-lockfile-check` 显式开启本套件(见 conftest.py) """ from pathlib import Path import pytest REPO_ROOT = Path(__file__).resolve().parents[1] DEPLOY_DIR = REPO_ROOT / "deploy" LOCK_FILES = [ DEPLOY_DIR / "requirements-base.lock.txt", DEPLOY_DIR / "requirements-moldinsight.lock.txt", ] GENERATOR_SCRIPTS = [ DEPLOY_DIR / "generate_lockfiles.sh", DEPLOY_DIR / "generate_lockfiles.bat", ] def pytest_collection_modifyitems(config, items): """仅在显式传入 --run-lockfile-check 时启用 D13 部署侧契约。""" if not config.getoption("--run-lockfile-check", default=False): skip_marker = pytest.mark.skip( reason="D13 部署侧契约:默认 skip;CI 镜像构建 job 需传入 --run-lockfile-check 启用" ) for item in items: if "test_lockfile_generation" in item.nodeid: item.add_marker(skip_marker) def pytest_addoption(parser): parser.addoption( "--run-lockfile-check", action="store_true", default=False, help="启用 D13 锁文件部署侧契约测试(CI 镜像构建 job 使用)", ) def pytest_collection_modifyitems(config, items): """仅在显式传入 --run-lockfile-check 时启用 D13 部署侧契约。 说明:本钩子保留作为冗余保护(conftest.py 已注册同名钩子), 即便测试单独跑 pytest tests/test_lockfile_generation.py 也能正确跳过。 """ if not config.getoption("--run-lockfile-check", default=False): skip_marker = pytest.mark.skip( reason="D13 部署侧契约:默认 skip;CI 镜像构建 job 需传入 --run-lockfile-check 启用" ) for item in items: item.add_marker(skip_marker) def pytest_addoption(parser): parser.addoption( "--run-lockfile-check", action="store_true", default=False, help="启用 D13 锁文件部署侧契约测试(CI 镜像构建 job 使用)", ) @pytest.mark.parametrize("lock_path", LOCK_FILES) def test_lockfile_exists_and_is_substantive(lock_path): """锁文件必须存在且非空(≥5 行 pip freeze 产物),否则部署侧契约缺失。""" assert lock_path.exists(), ( f"缺少锁文件 {lock_path.relative_to(REPO_ROOT)};" f"请在 moldinsight conda 环境执行 deploy/generate_lockfiles.sh/.bat 后提交" ) line_count = sum(1 for _ in lock_path.open(encoding="utf-8") if _.strip()) assert line_count >= 5, ( f"锁文件 {lock_path.relative_to(REPO_ROOT)} 体积异常(仅 {line_count} 行非空行)," "可能是构建流程未走通,请重新生成" ) @pytest.mark.parametrize("script_path", GENERATOR_SCRIPTS) def test_lockfile_generator_script_present(script_path): """锁文件生成脚本必须随仓库分发,否则新机器无法落锁。""" assert script_path.exists(), ( f"缺少生成脚本 {script_path.relative_to(REPO_ROOT)};" "D13 流程入口文件缺失" ) def test_lockfile_dockerfile_comment_points_to_generator(): """Dockerfile.moldinsight 必须明确指向锁文件生成脚本。""" dockerfile = (DEPLOY_DIR / "Dockerfile.moldinsight").read_text(encoding="utf-8") assert "generate_lockfiles" in dockerfile, ( "Dockerfile.moldinsight 应在注释中指向 deploy/generate_lockfiles.sh 以引导锁文件生成流程" )