test(rendering): golden 基线——四 profile 渲染富 fixture,docx XML 逐字节对账
给 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 <noreply@anthropic.com>
This commit is contained in:
parent
5a5ebfbe24
commit
035aec44ac
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -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
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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()
|
||||||
Loading…
Reference in New Issue