160 lines
5.5 KiB
Python
160 lines
5.5 KiB
Python
"""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()
|