Files
CADx-4/tests/test_build_stops.py
T
iomgaa bceb01e8fa feat: 装配两个库(不含 CEUS 结果)
按任务书 01 实现 delivery、library、answers、runs(快照部分)、build 五个模块,
scripts/build.py、scripts/scan_library.py 与 config/build.yaml(decode_codes: false)。
交付包整表检查 T1–T4、映射表检查 T5、逐例检查 C01–C13,一例遇到第一项不过即整例跳过。

与任务书原文的出入:
- C04 报告表名随视频名配对(负责人 2026-10-16 定)。
- 按两轮独立审阅加严 C11(更多换行符、比例号冒号)、C12(同义词与词根)、
  C13(空格、周岁、中文数字、繁体);泄露扫描做 NFKC;关键图只接受能保真重编码的 PNG;
  YAML 配置拒绝重复键和空路径。
- import-linter 第 1 条拆成互不依赖与不引用其他 cadx 模块两条。

文档:结构.md 第五、六、八部分按实际更新;新增 guides/装配两个库.md;WORK_LOG 更新。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 03:00:20 -04:00

188 lines
6.8 KiB
Python

"""整张表的问题 T1–T5 与配置错误:整次构建停下,两个库下不留完成的构建目录。"""
from datetime import datetime
from pathlib import Path
import pytest
from pydantic import ValidationError
from cadx.build import load_build_config, load_code_maps, run_build
from cadx.delivery import DeliveryError
from fake_delivery import (
CLINICAL_PATH,
MR_PATH,
REPO_ROOT,
SAMPLE_CODE_MAPS,
ULTRASOUND_PATH,
make_delivery,
write_config,
)
NOW = datetime(2026, 10, 16, 9, 0, 0).astimezone()
def _assert_no_finished_build(tmp_path: Path) -> None:
for root in (tmp_path / "library", tmp_path / "answers"):
if root.exists():
assert all(p.name.endswith(".partial") for p in root.iterdir())
def _run(tmp_path: Path, root: Path, code_maps: dict[str, dict[int, str]] | None) -> None:
run_build(write_config(tmp_path, root, 7, code_maps), REPO_ROOT, NOW)
def test_t1_missing_summary_table(tmp_path: Path) -> None:
root = make_delivery(3).write(tmp_path / "delivery")
(root / MR_PATH).unlink()
with pytest.raises(DeliveryError, match=r"^T1"):
_run(tmp_path, root, None)
_assert_no_finished_build(tmp_path)
def test_t2_wrong_sheet_name(tmp_path: Path) -> None:
delivery = make_delivery(3)
delivery.sheet_names[ULTRASOUND_PATH] = "Sheet1"
root = delivery.write(tmp_path / "delivery")
with pytest.raises(DeliveryError, match=r"^T2"):
_run(tmp_path, root, None)
_assert_no_finished_build(tmp_path)
def test_t3_header_differs(tmp_path: Path) -> None:
delivery = make_delivery(3)
delivery.headers[MR_PATH][3] = "病理类型" # 诊断列名里写的编码含义丢了
root = delivery.write(tmp_path / "delivery")
with pytest.raises(DeliveryError, match=r"^T3"):
_run(tmp_path, root, None)
_assert_no_finished_build(tmp_path)
def test_t4_extra_file_in_clinical_folder(tmp_path: Path) -> None:
root = make_delivery(3).write(tmp_path / "delivery")
(root / CLINICAL_PATH).with_name("备注.txt").write_text("x", encoding="utf-8")
with pytest.raises(DeliveryError, match=r"^T4"):
_run(tmp_path, root, None)
_assert_no_finished_build(tmp_path)
def test_t4_unexpected_entry_in_ceus_folder(tmp_path: Path) -> None:
root = make_delivery(3).write(tmp_path / "delivery")
(root / "CEUS_agent" / "41").mkdir() # 不是三位数字
with pytest.raises(DeliveryError, match=r"^T4"):
_run(tmp_path, root, None)
_assert_no_finished_build(tmp_path)
def test_t5_code_maps_missing_a_column(tmp_path: Path) -> None:
root = make_delivery(3).write(tmp_path / "delivery")
maps = {k: v for k, v in SAMPLE_CODE_MAPS.items() if k != "TBil_Level"}
with pytest.raises(ValueError, match=r"^T5"):
_run(tmp_path, root, maps)
_assert_no_finished_build(tmp_path)
def test_t5_code_maps_extra_column(tmp_path: Path) -> None:
root = make_delivery(3).write(tmp_path / "delivery")
maps = {**SAMPLE_CODE_MAPS, "Age": {1: "一"}}
with pytest.raises(ValueError, match=r"^T5"):
_run(tmp_path, root, maps)
_assert_no_finished_build(tmp_path)
def test_t5_sex_must_map_to_chinese_characters(tmp_path: Path) -> None:
root = make_delivery(3).write(tmp_path / "delivery")
maps = {**SAMPLE_CODE_MAPS, "Sex": {1: "M", 2: "F"}}
with pytest.raises(ValueError, match=r"^T5"):
_run(tmp_path, root, maps)
_assert_no_finished_build(tmp_path)
def test_t5_diagnosis_must_map_to_four_subtypes(tmp_path: Path) -> None:
root = make_delivery(3).write(tmp_path / "delivery")
maps = {**SAMPLE_CODE_MAPS, "Diagnosis": {1: "IPMN", 2: "SPN", 3: "MCN", 4: "MCN"}}
with pytest.raises(ValueError, match=r"^T5"):
_run(tmp_path, root, maps)
_assert_no_finished_build(tmp_path)
def _write_yaml(tmp_path: Path, body: str) -> Path:
path = tmp_path / "build.yaml"
path.write_text(body, encoding="utf-8")
return path
_BASE = "delivery_dir: d\nlibrary_root: l\nanswers_root: a\nalias_seed: 1\n"
def test_config_decode_true_needs_code_maps(tmp_path: Path) -> None:
path = _write_yaml(tmp_path, _BASE + "decode_codes: true\ncode_maps: null\n")
with pytest.raises(ValidationError):
load_build_config(path, tmp_path)
def test_config_decode_false_needs_null_code_maps(tmp_path: Path) -> None:
path = _write_yaml(tmp_path, _BASE + "decode_codes: false\ncode_maps: maps.yaml\n")
with pytest.raises(ValidationError):
load_build_config(path, tmp_path)
def test_config_field_missing(tmp_path: Path) -> None:
path = _write_yaml(tmp_path, _BASE + "decode_codes: false\n")
with pytest.raises(ValidationError):
load_build_config(path, tmp_path)
def test_config_extra_field(tmp_path: Path) -> None:
path = _write_yaml(tmp_path, _BASE + "decode_codes: false\ncode_maps: null\nseed: 2\n")
with pytest.raises(ValidationError):
load_build_config(path, tmp_path)
def test_config_seed_must_be_int_not_string(tmp_path: Path) -> None:
body = _BASE.replace("alias_seed: 1", "alias_seed: '1'")
path = _write_yaml(tmp_path, body + "decode_codes: false\ncode_maps: null\n")
with pytest.raises(ValidationError):
load_build_config(path, tmp_path)
def test_config_relative_paths_resolve_against_repo_root(tmp_path: Path) -> None:
path = _write_yaml(tmp_path, _BASE + "decode_codes: false\ncode_maps: null\n")
config = load_build_config(path, Path("/repo"))
assert config.delivery_dir == Path("/repo/d")
assert config.library_root == Path("/repo/l")
assert config.answers_root == Path("/repo/a")
assert config.code_maps is None
def test_committed_config_is_valid() -> None:
config = load_build_config(REPO_ROOT / "config" / "build.yaml", REPO_ROOT)
assert config.decode_codes is False
assert config.code_maps is None
def test_config_duplicate_key(tmp_path: Path) -> None:
body = _BASE + "decode_codes: false\ncode_maps: null\nalias_seed: 2\n"
path = _write_yaml(tmp_path, body)
with pytest.raises(ValueError, match="重复的键"):
load_build_config(path, tmp_path)
def test_config_empty_path(tmp_path: Path) -> None:
body = _BASE.replace("delivery_dir: d", "delivery_dir: ''")
path = _write_yaml(tmp_path, body + "decode_codes: false\ncode_maps: null\n")
with pytest.raises(ValidationError, match="空串"):
load_build_config(path, tmp_path)
def test_config_empty_code_maps_path(tmp_path: Path) -> None:
path = _write_yaml(tmp_path, _BASE + "decode_codes: true\ncode_maps: ''\n")
with pytest.raises(ValidationError, match="空串"):
load_build_config(path, tmp_path)
def test_code_maps_duplicate_key(tmp_path: Path) -> None:
path = tmp_path / "code_maps.yaml"
path.write_text("Sex: {1: 男, 1: 女}\n", encoding="utf-8")
with pytest.raises(ValueError, match="重复的键"):
load_code_maps(path)