test_template_text_reading.py 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
  1. import tempfile
  2. import unittest
  3. from concurrent.futures import ThreadPoolExecutor
  4. from pathlib import Path
  5. from types import SimpleNamespace
  6. from unittest.mock import patch
  7. from docx import Document
  8. from docx.enum.text import WD_BREAK
  9. from docx.oxml import OxmlElement
  10. from models import BidOutline, Chapter, CompanyInfo, TemplateSection, TemplateStructure, TenderAnalysis
  11. from step3_outlining.template_parser import (
  12. _read_template_body_texts, get_template_text_for_chapter, get_template_text_for_section,
  13. )
  14. from step4_writing import _write_single_chapter
  15. from step4_writing.content_injector import _ContentInjector
  16. class TemplateTextReadingTests(unittest.TestCase):
  17. def setUp(self):
  18. self.directory = tempfile.TemporaryDirectory()
  19. self.addCleanup(self.directory.cleanup)
  20. self.path = Path(self.directory.name) / 'template.docx'
  21. doc = Document()
  22. doc.add_paragraph('第四章 服务方案')
  23. doc.add_paragraph('')
  24. doc.add_table(rows=1, cols=1).cell(0, 0).text = '表内文本不计入正文索引'
  25. p = doc.add_paragraph('正文\t制表\n换行')
  26. p.add_run().add_break(WD_BREAK.PAGE)
  27. link = OxmlElement('w:hyperlink')
  28. run = OxmlElement('w:r')
  29. text = OxmlElement('w:t')
  30. text.text = '链接'
  31. run.append(text)
  32. run.append(OxmlElement('w:noBreakHyphen'))
  33. link.append(run)
  34. p._p.append(link)
  35. doc.add_paragraph('第五章 其他方案')
  36. self.expected = [p.text for p in doc.paragraphs]
  37. doc.save(self.path)
  38. self.structure = TemplateStructure(file_path=str(self.path), sections=[
  39. TemplateSection(name='第四章 服务方案', start_para=0, end_para=2),
  40. TemplateSection(name='第五章 其他方案', start_para=3, end_para=99),
  41. ])
  42. def test_top_level_indices_and_visible_text_match_python_docx(self):
  43. self.assertEqual(_read_template_body_texts(str(self.path)), self.expected)
  44. def test_parallel_section_and_title_reads_do_not_use_native_parser(self):
  45. expected = '\n'.join(t.strip() for t in self.expected[:3] if t.strip())
  46. def read(index):
  47. if index % 2:
  48. return get_template_text_for_section(self.structure, '第四章 服务方案')
  49. return get_template_text_for_chapter(self.structure, '第四章 服务方案', use_llm=False)
  50. with patch('step3_outlining.template_parser.DocxDocument', side_effect=AssertionError(
  51. '并发纯文本取文不得进入python-docx原生解析器'
  52. )), ThreadPoolExecutor(max_workers=5) as pool:
  53. self.assertEqual(list(pool.map(read, range(25))), [expected] * 25)
  54. self.assertEqual(get_template_text_for_section(self.structure, '第五章 其他方案'), self.expected[3])
  55. self.assertEqual(get_template_text_for_section(self.structure, '不存在'), '')
  56. def test_step4_chapter_generation_uses_streamed_template_text(self):
  57. chapter = Chapter(id='4', title='服务方案', template_section='第四章 服务方案')
  58. outline = BidOutline(project_name='测试项目', chapters=[chapter])
  59. with patch('step3_outlining.template_parser.DocxDocument', side_effect=TypeError(
  60. "'CT_P' object is not callable"
  61. )):
  62. result = _write_single_chapter(
  63. chapter=chapter, writer=SimpleNamespace(), injector=_ContentInjector({}, []),
  64. analysis=TenderAnalysis(project_name='测试项目'), company_info=CompanyInfo(),
  65. template_structure=self.structure, cfg=SimpleNamespace(enforce_word_limit=False),
  66. outline=outline, placeholder_map={},
  67. )
  68. self.assertEqual(result.generated_content, '\n'.join(t for t in self.expected[:3] if t))
  69. self.assertTrue(result.preserve_template_layout)