"""测试用假交付包:形状照 40 例交付包写(表头、工作表名、目录结构),内容全部编造。 这里的表头和文件名是按任务书 01 第二部分的表格独立写的,不取自 delivery 的常量, 这样 delivery 的常量写错时测试能发现。各条测试在 `FakeDelivery` 上改出要测的情况,再 `write`。 """ from collections.abc import Sequence from dataclasses import dataclass, field from datetime import datetime from pathlib import Path from typing import Any, cast import openpyxl import yaml from PIL import Image, PngImagePlugin from cadx.build import BuildSummary, run_build REPO_ROOT = Path(__file__).resolve().parents[1] DIAGNOSIS_COLUMN = "病理类型(1 IPMN,2 SPN,3 MCN,4 SCN)" CLINICAL_HEADER = [ "Case_ID", "Sex", "Age", "Diagnosis", "Symptom_Pain", "Symptom_Jaundice", "Symptom_WeightLoss", "Symptom_Fever", "Symptom_Asymptomatic", "Hx_Pancreatitis", "Hx_Alcohol", "Hx_Gallstones", "Hx_Malignancy", "Hx_Family_PC", "CA199", "CA199_Level", "CEA", "CEA_Level", "Amylase", "Amylase_Level", "Lipase", "Lipase_Level", "TBil", "TBil_Level", ] ULTRASOUND_HEADER = [ "患者ID", "CEUS视频ID", "性别", "年龄", DIAGNOSIS_COLUMN, "推注开始时间s", "超声描述", ] MR_HEADER = ["患者ID", "性别", "年龄", DIAGNOSIS_COLUMN, "MR描述"] CLINICAL_PATH = "Clinical_agent/临床工作簿_40例.xlsx" ULTRASOUND_PATH = "CEUS_agent/超声报告文本_40例.xlsx" MR_PATH = "MR_agent/MR报告文本_40例.xlsx" # 编造的报告文字:不含亚型名、结论段标题、性别字、「数字岁」和复核措辞。 ULTRASOUND_TEXT = "胰体部见一无回声区,边界清晰,壁薄。造影后囊壁呈等增强,内部未见增强。" # 假交付包里每段 CEUS 视频文件的内容(不是真视频)。 VIDEO_BYTES = b"not a real video" MR_TEXT = "胰体部见一类圆形长T2信号灶,边界清楚,内见细分隔;增强后分隔轻度强化。" # 测试用的样例映射表:含义是编的,只用来验证解码路径。 SAMPLE_CODE_MAPS: dict[str, dict[int, str]] = { "Sex": {1: "男", 2: "女"}, "Diagnosis": {1: "IPMN", 2: "SPN", 3: "MCN", 4: "SCN"}, **{ column: {0: "否", 1: "是"} for column in ( "Symptom_Pain", "Symptom_Jaundice", "Symptom_WeightLoss", "Symptom_Fever", "Symptom_Asymptomatic", "Hx_Pancreatitis", "Hx_Alcohol", "Hx_Gallstones", "Hx_Malignancy", "Hx_Family_PC", ) }, **{ column: {0: "正常", 1: "异常"} for column in ("CA199_Level", "CEA_Level", "Amylase_Level", "Lipase_Level", "TBil_Level") }, } @dataclass class FakeCase: """一例:三张表各一行,病例目录里的文件名和单例报告表的行。""" number: int clinical: dict[str, object] ultrasound: dict[str, object] mr: dict[str, object] dir_files: list[str] # None 表示单例报告表照抄超声汇总表那一行。 case_report_rows: list[list[object]] | None = None case_report_sheet: str = "Sheet1" @property def no(self) -> str: return f"{self.number:03d}" def make_case(number: int) -> FakeCase: """一例全部检查都能通过的病例。诊断按病例号轮转 1–4。""" no = f"{number:03d}" sex = 1 + number % 2 age = 30 + number diagnosis = 1 + (number - 1) % 4 clinical: dict[str, object] = dict.fromkeys(CLINICAL_HEADER, 0) clinical.update( { "Case_ID": f"P{no}", "Sex": sex, "Age": age, "Diagnosis": diagnosis, "Symptom_Pain": 1, "CA199": 12.5, "CA199_Level": 0, "CEA": 3, "CEA_Level": 1, "Amylase": None, "Amylase_Level": None, "Lipase": None, "Lipase_Level": None, "TBil": 10.2, "TBil_Level": 0, } ) sex_text = {1: "男", 2: "女"}[sex] ultrasound: dict[str, object] = { "患者ID": str(number), "CEUS视频ID": f"U{no}", "性别": sex_text, "年龄": f"{age}岁", DIAGNOSIS_COLUMN: diagnosis, "推注开始时间s": 5, "超声描述": ULTRASOUND_TEXT, } mr: dict[str, object] = { "患者ID": str(number), "性别": sex_text, "年龄": f"{age}岁", DIAGNOSIS_COLUMN: diagnosis, "MR描述": MR_TEXT, } files = [f"U{no}_key.png", f"U{no}_roi2.mp4", f"U{no}_roi2_report.xlsx"] return FakeCase(number, clinical, ultrasound, mr, files) @dataclass class FakeDelivery: """一个假交付包。`extra_rows` 是附加在汇总表末尾的整行(测试编号认不出的行)。""" cases: list[FakeCase] sheet_names: dict[str, str] = field( default_factory=lambda: { CLINICAL_PATH: "Sheet", ULTRASOUND_PATH: "超声报告", MR_PATH: "MR报告", } ) headers: dict[str, list[str]] = field( default_factory=lambda: { CLINICAL_PATH: list(CLINICAL_HEADER), ULTRASOUND_PATH: list(ULTRASOUND_HEADER), MR_PATH: list(MR_HEADER), } ) extra_rows: dict[str, list[list[object]]] = field( default_factory=lambda: {CLINICAL_PATH: [], ULTRASOUND_PATH: [], MR_PATH: []} ) def case(self, number: int) -> FakeCase: return next(c for c in self.cases if c.number == number) def write(self, root: Path) -> Path: tables = { CLINICAL_PATH: [c.clinical for c in self.cases], ULTRASOUND_PATH: [c.ultrasound for c in self.cases], MR_PATH: [c.mr for c in self.cases], } for path, rows in tables.items(): header = self.headers[path] values = [[row.get(h) for h in header] for row in rows] _write_workbook( root / path, self.sheet_names[path], [header, *values, *self.extra_rows[path]] ) for case in self.cases: folder = root / "CEUS_agent" / case.no folder.mkdir(parents=True, exist_ok=True) for name in case.dir_files: if name.endswith(".png"): _write_png(folder / name, case.number) elif name.endswith(".xlsx"): row = [case.ultrasound.get(h) for h in ULTRASOUND_HEADER] rows = case.case_report_rows or [list(ULTRASOUND_HEADER), row] _write_workbook(folder / name, case.case_report_sheet, rows) else: (folder / name).write_bytes(VIDEO_BYTES) (root / "交付说明_40例_20260908.md").write_text("说明不读", encoding="utf-8") return root def make_delivery(count: int) -> FakeDelivery: """病例号 1..count、全部能通过检查的假交付包。""" return FakeDelivery(cases=[make_case(n) for n in range(1, count + 1)]) def _write_workbook(path: Path, sheet: str, rows: Sequence[Sequence[object]]) -> None: path.parent.mkdir(parents=True, exist_ok=True) workbook = openpyxl.Workbook() worksheet = workbook.active assert worksheet is not None worksheet.title = sheet for row in rows: # 假数据里的值都是 openpyxl 认得的单元格类型(字符串、数字、None)。 worksheet.append(cast("list[Any]", list(row))) workbook.save(path) def _write_png(path: Path, number: int) -> None: pixels = bytes(v for i in range(24) for v in (number * 10 % 256, i * 10, 255 - i * 10)) image = Image.frombytes("RGB", (6, 4), pixels) info = PngImagePlugin.PngInfo() info.add_text("PatientName", "should-not-survive") image.save(path, format="PNG", pnginfo=info) def write_config( tmp_path: Path, delivery_dir: Path, seed: int, code_maps: dict[str, dict[int, str]] | None, ceus_run: Path | None, ) -> Path: """在 tmp_path 写一份 build.yaml(路径都写绝对路径);给了映射表就同时写 code_maps.yaml。""" maps_path = None if code_maps is not None: maps_path = tmp_path / "code_maps.yaml" maps_path.write_text(yaml.safe_dump(code_maps, allow_unicode=True), encoding="utf-8") config = { "delivery_dir": str(delivery_dir), "library_root": str(tmp_path / "library"), "answers_root": str(tmp_path / "answers"), "alias_seed": seed, "decode_codes": code_maps is not None, "code_maps": None if maps_path is None else str(maps_path), "ceus_run": None if ceus_run is None else str(ceus_run), } path = tmp_path / "build.yaml" path.write_text(yaml.safe_dump(config, allow_unicode=True), encoding="utf-8") return path def build( tmp_path: Path, delivery: FakeDelivery, seed: int, code_maps: dict[str, dict[int, str]] | None, now: datetime, ) -> BuildSummary: """写出假交付包和配置,跑一次不装 CEUS 结果的构建。""" delivery_dir = delivery.write(tmp_path / "delivery") return run_build(write_config(tmp_path, delivery_dir, seed, code_maps, None), REPO_ROOT, now)