| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202 |
- """Step 6 聚合装配的快速回归测试(不调用 LLM)。"""
- import os
- import tempfile
- import unittest
- import zipfile
- import xml.etree.ElementTree as ET
- from docx import Document as DocxDocument
- from models import BidOutline
- from step6_exporting.assembler import (
- _HeadingRecord,
- _build_index_table_from_template,
- _load_table_template,
- assemble_step5_document,
- )
- _W_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
- def _w(tag: str) -> str:
- return f"{{{_W_NS}}}{tag}"
- def _element_text(element) -> str:
- return "".join(node.text or "" for node in element.iter(_w("t")))
- def _make_chapter(path: str, title: str, sub_headings) -> None:
- doc = DocxDocument()
- section = doc.sections[0]
- section.header.paragraphs[0].text = "章节原生页眉"
- section.footer.paragraphs[0].text = "章节原生页脚"
- doc.add_paragraph(title, style="Heading 1")
- for heading in sub_headings:
- doc.add_paragraph(heading, style="Heading 2")
- doc.add_paragraph(f"这是 {heading} 的响应正文,包含数字 123 和英文 English。")
- doc.save(path)
- def _make_template(path: str) -> None:
- doc = DocxDocument()
- section = doc.sections[0]
- section.different_first_page_header_footer = True
- default_header = section.header
- default_header.paragraphs[0].text = "默认页眉"
- first_header = section.first_page_header
- first_header.paragraphs[0].text = "首页页眉"
- footer = section.footer
- footer.paragraphs[0].text = "页脚"
- doc.add_paragraph("模板占位正文")
- doc.save(path)
- def _make_index_table_template(path: str) -> None:
- doc = DocxDocument()
- table = doc.add_table(rows=2, cols=4)
- header = ["序号", "主要内容概述", "章节", "详细内容所在投标文件页次"]
- for col, text in enumerate(header):
- table.cell(0, col).text = text
- for col, text in enumerate(["1", "示例", "第一章", "P1"]):
- table.cell(1, col).text = text
- doc.save(path)
- class Step6AssemblerTests(unittest.TestCase):
- def test_assemble_produces_cover_toc_index_and_chapters(self):
- temp_dir = tempfile.mkdtemp()
- template_path = os.path.join(temp_dir, "template.docx")
- _make_template(template_path)
- ch1 = os.path.join(temp_dir, "1.docx")
- ch2 = os.path.join(temp_dir, "2.docx")
- ch3 = os.path.join(temp_dir, "3.docx")
- _make_chapter(ch1, "第一章 投标人资格、资信证明", ["十七、“★” 要求承诺函"])
- _make_chapter(ch2, "第二章 投标报价", ["五、报价得分"])
- _make_chapter(ch3, "第三章 需求理解", ["一、服务目标定位"])
- outline = BidOutline(project_name="测试项目")
- outline.evaluation_index_entries = [
- {
- "entry_type": "scoring",
- "source_id": "SC-01#1",
- "criterion_id": "SC-01",
- "display_name": "报价得分",
- "requirement": "报价得分=报价分值×(评标基准价/评审价)",
- "score": 10.0,
- "final_heading_id": "2.5",
- "final_heading_title": "五、报价得分",
- "final_heading_path": "投标报价 → 五、报价得分",
- },
- {
- "entry_type": "rejection",
- "source_id": "RI-01",
- "criterion_id": "",
- "display_name": "资格条件",
- "requirement": "投标人不满足《中华人民共和国政府采购法》第二十二条规定。",
- "score": None,
- "final_heading_id": "1.17",
- "final_heading_title": "十七、“★” 要求承诺函",
- "final_heading_path": "投标人资格、资信证明 → 十七、“★” 要求承诺函",
- },
- ]
- output_path = os.path.join(temp_dir, "aggregated.docx")
- records = [
- {"id": "1", "title": "投标人资格、资信证明", "artifact_path": ch1, "status": "complete"},
- {"id": "2", "title": "投标报价", "artifact_path": ch2, "status": "complete"},
- {"id": "3", "title": "需求理解", "artifact_path": ch3, "status": "complete"},
- ]
- report = assemble_step5_document(
- records,
- outline,
- output_path,
- template_path=template_path,
- project_name="测试项目",
- )
- self.assertTrue(os.path.isfile(output_path))
- self.assertEqual(report.index_row_count, 1)
- self.assertTrue(report.toc_inserted)
- self.assertEqual(report.chapter_count, 3)
- with zipfile.ZipFile(output_path, "r") as output_zip:
- names = set(output_zip.namelist())
- self.assertIn("word/document.xml", names)
- document_root = ET.fromstring(output_zip.read("word/document.xml"))
- body = document_root.find(_w("body"))
- self.assertIsNotNone(body)
- body_text = _element_text(body)
- self.assertIn("测试项目", body_text)
- self.assertIn("目 录", body_text)
- self.assertIn("第一章 投标人资格、资信证明", body_text)
- self.assertIn("第二章 投标报价", body_text)
- self.assertIn("第三章 需求理解", body_text)
- self.assertIn("商务部分", body_text)
- self.assertIn("技术部分", body_text)
- self.assertIn("主要内容概述", body_text)
- instr_texts = [node.text or "" for node in body.iter(_w("instrText"))]
- combined = " ".join(instr_texts)
- self.assertIn("TOC", combined)
- self.assertIn("PAGEREF", combined)
- self.assertNotIn("第二十二条", body_text)
- header1 = output_zip.read("word/header1.xml")
- header2 = output_zip.read("word/header2.xml")
- footer1 = output_zip.read("word/footer1.xml")
- self.assertIn("章节原生页眉".encode("utf-8"), header1)
- self.assertIn("章节原生页脚".encode("utf-8"), footer1)
- # 正文 run 应统一为宋体 + Times New Roman。
- for run in body.iter(_w("r")):
- if _element_text(run).startswith("这是"):
- r_pr = run.find(_w("rPr"))
- self.assertIsNotNone(r_pr)
- fonts = r_pr.find(_w("rFonts"))
- self.assertIsNotNone(fonts)
- self.assertEqual(fonts.get(_w("eastAsia")), "宋体")
- self.assertEqual(fonts.get(_w("ascii")), "Times New Roman")
- break
- else:
- self.fail("未找到正文 run 以验证字体")
- def test_index_table_reuses_extracted_template(self):
- temp_dir = tempfile.mkdtemp()
- table_template_path = os.path.join(temp_dir, "index_table.docx")
- _make_index_table_template(table_template_path)
- template_tbl = _load_table_template(table_template_path)
- self.assertIsNotNone(template_tbl)
- records = [
- _HeadingRecord(chapter_title="投标报价", text="第二章 投标报价", level=1, bookmark="CodexIdx1"),
- _HeadingRecord(chapter_title="投标报价", text="五、报价得分", level=2, bookmark="CodexIdx2"),
- ]
- entries = [
- {
- "entry_type": "scoring",
- "display_name": "报价得分",
- "score": 10.0,
- "final_heading_path": "投标报价 → 五、报价得分",
- }
- ]
- table, row_count = _build_index_table_from_template(template_tbl, entries, records)
- self.assertEqual(row_count, 1)
- rows = table.findall(_w("tr"))
- self.assertEqual(len(rows), 2)
- header_cells = [_element_text(tc).strip() for tc in rows[0].findall(_w("tc"))]
- self.assertEqual(header_cells, ["序号", "主要内容概述", "章节", "详细内容所在投标文件页次"])
- data_cells = [_element_text(tc).strip() for tc in rows[1].findall(_w("tc"))]
- self.assertEqual(data_cells[0], "1")
- self.assertIn("报价得分", data_cells[1])
- self.assertEqual(data_cells[2], "第二章 投标报价 → 五、报价得分")
- if __name__ == "__main__":
- unittest.main()
|