From 035aec44acb7c2f1b7a3d173f643f72e3add7a4d Mon Sep 17 00:00:00 2001 From: caoqianming Date: Thu, 23 Jul 2026 13:24:26 +0800 Subject: [PATCH] =?UTF-8?q?test(rendering):=20golden=20=E5=9F=BA=E7=BA=BF?= =?UTF-8?q?=E2=80=94=E2=80=94=E5=9B=9B=20profile=20=E6=B8=B2=E6=9F=93?= =?UTF-8?q?=E5=AF=8C=20fixture,docx=20XML=20=E9=80=90=E5=AD=97=E8=8A=82?= =?UTF-8?q?=E5=AF=B9=E8=B4=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 给 md 解析路径的后续重构上硬闸(§8.6 当年"前后字节一致"验收的固化): - fixture 覆盖信息带/TL;DR 卡/判断 callout/blockquote/表格/代码块(含 mermaid 回退)/缺图占位/四类列表/参考文献 DOI·URL 条目/相邻引文上标/化学式/软换行 并合/refs 段外 [n] 行等全部分支 - 对账面 word/document.xml + footer*.xml(无时间戳,确定性已验证:连续两次 渲染逐字节一致);core.xml 元数据刻意不比 - 5 份 golden(brief 彩/黑白、paper zh、proposal、report+toc);有意变更走 ZCBOT_REGEN_GOLDENS=1 重生成并在 commit 里说明,重构类改动必须零 diff Co-Authored-By: Claude Fable 5 --- tests/golden/rendering/brief_bw.xml | 6 + tests/golden/rendering/brief_color.xml | 6 + tests/golden/rendering/paper_zh.xml | 3 + tests/golden/rendering/proposal.xml | 3 + tests/golden/rendering/report_toc.xml | 3 + tests/test_rendering_golden.py | 159 +++++++++++++++++++++++++ 6 files changed, 180 insertions(+) create mode 100644 tests/golden/rendering/brief_bw.xml create mode 100644 tests/golden/rendering/brief_color.xml create mode 100644 tests/golden/rendering/paper_zh.xml create mode 100644 tests/golden/rendering/proposal.xml create mode 100644 tests/golden/rendering/report_toc.xml create mode 100644 tests/test_rendering_golden.py diff --git a/tests/golden/rendering/brief_bw.xml b/tests/golden/rendering/brief_bw.xml new file mode 100644 index 0000000..6b0facb --- /dev/null +++ b/tests/golden/rendering/brief_bw.xml @@ -0,0 +1,6 @@ +===== word/document.xml ===== + +2026 低碳水泥月度简报方向:低碳胶凝材料 | 时间窗:2026-06 | 深度:精读 | 数据源:院库+Web | 受众:课题组一句话要点- LC3 体系中试放大取得进展,C3S 活性提升 12%1,2- 碳化养护路线成本下降,见 Nature 报道W1背景与判断面向低碳水泥,重点关注 CO2 矿化与 C3S/Na2SO4 激发体系,不误伤 LC3、EN 197-5。 这一行是软换行, 应当与上一行并合成同一段。判断:碳化养护是当前性价比最高的路线,建议排期中试。取舍纪律:只收录有 DOI 的一手文献; 预印本单独标注。数据表体系抗压 MPa备注LC3-5042.5EN 197-5CO2 养护48.0中试代码与图def f(x): return x + 1 # 缩进必须保留graph TD%% caption: 工艺流程A-->B[image missing: figures/missing_process.png]1. 有序列表一(2) 括号序号① 圈号第一条 申报书条款样式(仅 proposal 列表模式命中)9 这行在参考文献段之外,应按普通段落并合语义处理 10 与上一行并成同一段参考文献[1] 10.1016/j.cemconres.2026.107891[2] 低碳水泥进展. 张三, 水泥学报, 2026-05. DOI: 10.1016/j.jclepro.2026.135790.[W1] Nature 报道 www.nature.com/articles/x123 碳化养护[W2] 纯文本条目,无链接可挂 +===== word/footer1.xml ===== + + PAGE \ No newline at end of file diff --git a/tests/golden/rendering/brief_color.xml b/tests/golden/rendering/brief_color.xml new file mode 100644 index 0000000..01f85b6 --- /dev/null +++ b/tests/golden/rendering/brief_color.xml @@ -0,0 +1,6 @@ +===== word/document.xml ===== + +2026 低碳水泥月度简报方向:低碳胶凝材料 | 时间窗:2026-06 | 深度:精读 | 数据源:院库+Web | 受众:课题组一句话要点- LC3 体系中试放大取得进展,C3S 活性提升 12%1,2- 碳化养护路线成本下降,见 Nature 报道W1背景与判断面向低碳水泥,重点关注 CO2 矿化与 C3S/Na2SO4 激发体系,不误伤 LC3、EN 197-5。 这一行是软换行, 应当与上一行并合成同一段。判断:碳化养护是当前性价比最高的路线,建议排期中试。取舍纪律:只收录有 DOI 的一手文献; 预印本单独标注。数据表体系抗压 MPa备注LC3-5042.5EN 197-5CO2 养护48.0中试代码与图def f(x): return x + 1 # 缩进必须保留graph TD%% caption: 工艺流程A-->B[image missing: figures/missing_process.png]1. 有序列表一(2) 括号序号① 圈号第一条 申报书条款样式(仅 proposal 列表模式命中)9 这行在参考文献段之外,应按普通段落并合语义处理 10 与上一行并成同一段参考文献[1] 10.1016/j.cemconres.2026.107891[2] 低碳水泥进展. 张三, 水泥学报, 2026-05. DOI: 10.1016/j.jclepro.2026.135790.[W1] Nature 报道 www.nature.com/articles/x123 碳化养护[W2] 纯文本条目,无链接可挂 +===== word/footer1.xml ===== + + PAGE \ No newline at end of file diff --git a/tests/golden/rendering/paper_zh.xml b/tests/golden/rendering/paper_zh.xml new file mode 100644 index 0000000..d6fceb2 --- /dev/null +++ b/tests/golden/rendering/paper_zh.xml @@ -0,0 +1,3 @@ +===== word/document.xml ===== + +2026 低碳水泥月度简报方向:低碳胶凝材料 | 时间窗:2026-06 | 深度:精读 | 数据源:院库+Web | 受众:课题组一句话要点- LC3 体系中试放大取得进展,C3S 活性提升 12%[1][2]- 碳化养护路线成本下降,见 Nature 报道[W1]背景与判断面向低碳水泥,重点关注 CO2 矿化与 C3S/Na2SO4 激发体系,不误伤 LC3、EN 197-5。 这一行是软换行, 应当与上一行并合成同一段。判断:碳化养护是当前性价比最高的路线,建议排期中试。数据表体系抗压 MPa备注LC3-5042.5EN 197-5CO2 养护48.0中试代码与图def f(x): return x + 1 # 缩进必须保留graph TD%% caption: 工艺流程A-->B[image missing: figures/missing_process.png]1. 有序列表一(2) 括号序号① 圈号第一条 申报书条款样式(仅 proposal 列表模式命中)[9] 这行在参考文献段之外,应按普通段落并合语义处理[10] 与上一行并成同一段参考文献[1] 10.1016/j.cemconres.2026.107891[2] 低碳水泥进展. 张三, 水泥学报, 2026-05. DOI: 10.1016/j.jclepro.2026.135790.[W1] Nature 报道 www.nature.com/articles/x123 碳化养护 [W2] 纯文本条目,无链接可挂 \ No newline at end of file diff --git a/tests/golden/rendering/proposal.xml b/tests/golden/rendering/proposal.xml new file mode 100644 index 0000000..ec7a161 --- /dev/null +++ b/tests/golden/rendering/proposal.xml @@ -0,0 +1,3 @@ +===== word/document.xml ===== + +目 录 TOC \o "1-3" \h \z \u [在 Word 中按 F9 或右键此处选择 “更新域” 即可生成完整目录]2026 低碳水泥月度简报方向:低碳胶凝材料 | 时间窗:2026-06 | 深度:精读 | 数据源:院库+Web | 受众:课题组一句话要点- LC3 体系中试放大取得进展,C3S 活性提升 12%[1][2]- 碳化养护路线成本下降,见 Nature 报道[W1]背景与判断面向低碳水泥,重点关注 CO2 矿化与 C3S/Na2SO4 激发体系,不误伤 LC3、EN 197-5。 这一行是软换行, 应当与上一行并合成同一段。判断:碳化养护是当前性价比最高的路线,建议排期中试。数据表体系抗压 MPa备注LC3-5042.5EN 197-5CO2 养护48.0中试代码与图def f(x): return x + 1 # 缩进必须保留graph TD%% caption: 工艺流程A-->B[image missing: figures/missing_process.png]1. 有序列表一(2) 括号序号① 圈号第一条 申报书条款样式(仅 proposal 列表模式命中)[9] 这行在参考文献段之外,应按普通段落并合语义处理[10] 与上一行并成同一段参考文献[1] 10.1016/j.cemconres.2026.107891[2] 低碳水泥进展. 张三, 水泥学报, 2026-05. DOI: 10.1016/j.jclepro.2026.135790.[W1] Nature 报道 www.nature.com/articles/x123 碳化养护 [W2] 纯文本条目,无链接可挂 \ No newline at end of file diff --git a/tests/golden/rendering/report_toc.xml b/tests/golden/rendering/report_toc.xml new file mode 100644 index 0000000..1777d8c --- /dev/null +++ b/tests/golden/rendering/report_toc.xml @@ -0,0 +1,3 @@ +===== word/document.xml ===== + +目 录 TOC \o "1-3" \h \z \u [在 Word 中按 F9 或右键此处选择 “更新域” 即可生成完整目录]2026 低碳水泥月度简报方向:低碳胶凝材料 | 时间窗:2026-06 | 深度:精读 | 数据源:院库+Web | 受众:课题组一句话要点- LC3 体系中试放大取得进展,C3S 活性提升 12%[1][2]- 碳化养护路线成本下降,见 Nature 报道[W1]背景与判断面向低碳水泥,重点关注 CO2 矿化与 C3S/Na2SO4 激发体系,不误伤 LC3、EN 197-5。 这一行是软换行, 应当与上一行并合成同一段。判断:碳化养护是当前性价比最高的路线,建议排期中试。数据表体系抗压 MPa备注LC3-5042.5EN 197-5CO2 养护48.0中试代码与图def f(x): return x + 1 # 缩进必须保留graph TD%% caption: 工艺流程A-->B[image missing: figures/missing_process.png]1. 有序列表一(2) 括号序号① 圈号第一条 申报书条款样式(仅 proposal 列表模式命中)[9] 这行在参考文献段之外,应按普通段落并合语义处理[10] 与上一行并成同一段参考文献[1] 10.1016/j.cemconres.2026.107891[2] 低碳水泥进展. 张三, 水泥学报, 2026-05. DOI: 10.1016/j.jclepro.2026.135790.[W1] Nature 报道 www.nature.com/articles/x123 碳化养护 [W2] 纯文本条目,无链接可挂 \ No newline at end of file diff --git a/tests/test_rendering_golden.py b/tests/test_rendering_golden.py new file mode 100644 index 0000000..dc376c7 --- /dev/null +++ b/tests/test_rendering_golden.py @@ -0,0 +1,159 @@ +"""rendering/ golden 基线:四 profile 渲染同一份富 fixture,docx 内部 XML 逐字节对账。 + +目的:给 md 解析路径的重构(块收集器下沉 common 等)上一道「前后字节一致」的硬闸—— +上次 rendering 抽取(§8.6)就是靠同款验收零回归的,这里把它固化成测试。 + +对账面:word/document.xml(正文全部段落/run/表格/超链/书签,无时间戳,确定性) ++ word/footer*.xml(brief 页脚页码域)。core.xml 的创建时间等元数据刻意不比。 + +golden 更新:行为**有意**变化时(改样式/改解析语义)设 `ZCBOT_REGEN_GOLDENS=1` +重跑本文件再生成,并在 commit message 里说明为什么变;重构类改动 golden 必须零 diff。 +fixture 覆盖:H1 信息带(时间窗)/TL;DR 卡片/判断 callout/blockquote/表格/代码块 +(含 mermaid 回退)/缺图占位/多种列表模式/参考文献 DOI·URL 条目/引文上标 [1][2] 相邻 +/化学式下标/粗斜体行内代码/HR/软换行并合/refs 段外的 [n] 行(并合语义)。 +""" +from __future__ import annotations + +import difflib +import os +import sys +import tempfile +import unittest +import zipfile +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +from rendering import docx_brief, docx_manuscript # noqa: E402 + +GOLDEN_DIR = Path(__file__).parent / "golden" / "rendering" +_REGEN = os.getenv("ZCBOT_REGEN_GOLDENS", "").strip() in ("1", "true", "yes") + +_FIXTURE = """# 2026 低碳水泥月度简报 + +方向:低碳胶凝材料 | 时间窗:2026-06 | 深度:精读 | 数据源:院库+Web | 受众:课题组 + +## 一句话要点 + +- **LC3 体系**中试放大取得进展,C3S 活性提升 12%[1][2] +- 碳化养护路线成本下降,见 *Nature* 报道[W1] + +## 背景与判断 + +面向**低碳水泥**,重点关注 CO2 矿化与 C3S/Na2SO4 激发体系,不误伤 LC3、EN 197-5。 +这一行是软换行, +应当与上一行并合成同一段。 + +**判断**:碳化养护是当前性价比最高的路线,建议排期中试。 + +> 取舍纪律:只收录有 DOI 的一手文献; +> 预印本单独标注。 + +--- + +### 数据表 + +| 体系 | 抗压 MPa | 备注 | +|---|---|---| +| LC3-50 | 42.5 | `EN 197-5` | +| CO2 养护 | 48.0 | **中试** | + +### 代码与图 + +```python +def f(x): + return x + 1 # 缩进必须保留 +``` + +```mermaid +graph TD +%% caption: 工艺流程 +A-->B +``` + +![工艺示意](figures/missing_process.png) + +1. 有序列表一 +(2) 括号序号 +① 圈号 +第一条 申报书条款样式(仅 proposal 列表模式命中) + +[9] 这行在参考文献段之外,应按普通段落并合语义处理 +[10] 与上一行并成同一段 + +## 参考文献 + +[1] 10.1016/j.cemconres.2026.107891 +[2] 低碳水泥进展. 张三, 水泥学报, 2026-05. DOI: 10.1016/j.jclepro.2026.135790. +[W1] Nature 报道 www.nature.com/articles/x123 碳化养护 +[W2] 纯文本条目,无链接可挂 +""" + + +def _write_sections(d: Path) -> Path: + sec = d / "sections" + sec.mkdir() + (sec / "00_main.md").write_text(_FIXTURE, encoding="utf-8") + return sec + + +def _extract_xml(docx_path: Path) -> str: + """document.xml + footer*.xml 拼成一份对账文本(部件名做分隔头)。""" + parts: list[str] = [] + with zipfile.ZipFile(docx_path) as z: + names = [n for n in sorted(z.namelist()) + if n == "word/document.xml" or (n.startswith("word/footer") and n.endswith(".xml"))] + for n in names: + parts.append(f"===== {n} =====") + parts.append(z.read(n).decode("utf-8")) + return "\n".join(parts) + + +def _check_golden(test: unittest.TestCase, name: str, actual: str) -> None: + golden_path = GOLDEN_DIR / f"{name}.xml" + if _REGEN: + golden_path.parent.mkdir(parents=True, exist_ok=True) + golden_path.write_text(actual, encoding="utf-8", newline="\n") + return + if not golden_path.is_file(): + test.fail(f"golden 缺失:{golden_path}(首次生成:ZCBOT_REGEN_GOLDENS=1 重跑)") + expected = golden_path.read_text(encoding="utf-8") + if actual != expected: + diff = "\n".join(difflib.unified_diff( + expected.splitlines(), actual.splitlines(), + fromfile=f"golden/{name}", tofile="actual", lineterm="", n=2, + )) + test.fail( + f"{name} 渲染输出与 golden 不一致(前 4000 字符 diff):\n{diff[:4000]}\n" + f"—— 若为有意的样式/语义变更,ZCBOT_REGEN_GOLDENS=1 重生成并在 commit 里说明;" + f"重构类改动必须零 diff。" + ) + + +class RenderingGoldenTests(unittest.TestCase): + def _render_all(self, d: Path) -> dict[str, Path]: + sec = _write_sections(d) + outs = { + "brief_color": d / "brief_color.docx", + "brief_bw": d / "brief_bw.docx", + "paper_zh": d / "paper_zh.docx", + "proposal": d / "proposal.docx", + "report_toc": d / "report_toc.docx", + } + docx_brief.render_sections(sec, outs["brief_color"], color=True) + docx_brief.render_sections(sec, outs["brief_bw"], color=False) + docx_manuscript.render_sections("paper", sec, outs["paper_zh"], lang="zh") + docx_manuscript.render_sections("proposal", sec, outs["proposal"], fund_type="key_rd") + docx_manuscript.render_sections("report", sec, outs["report_toc"], toc=True) + return outs + + def test_all_profiles_match_golden(self): + with tempfile.TemporaryDirectory() as td: + outs = self._render_all(Path(td)) + for name, path in outs.items(): + with self.subTest(profile=name): + _check_golden(self, name, _extract_xml(path)) + + +if __name__ == "__main__": + unittest.main()