"""JRXML 窗口化模块单元测试。 测试 decompose → split → reassemble 往返链路, 以及元素计数和校验逻辑。 """ from __future__ import annotations import pytest from agent.jrxml_windower import ( decompose_jrxml, reassemble_jrxml, split_band_into_windows, reassemble_band_windows, count_elements, validate_element_count, BandInfo, ) # ── 最小 JRXML 测试夹具 ────────────────────────────────────────────── MINIMAL_JRXML = """ <band height="50"> <staticText> <reportElement x="0" y="0" width="100" height="20"/> <text><![CDATA[Title]]></text> </staticText> <textField> <reportElement x="200" y="0" width="80" height="20"/> <textFieldExpression><![CDATA[$F{name}]]></textFieldExpression> </textField> </band> """ # ── Decompose 测试 ──────────────────────────────────────────────────── class TestDecompose: def test_parses_minimal_jrxml(self): parts = decompose_jrxml(MINIMAL_JRXML) assert parts is not None assert parts.band_count == 4 # title, columnHeader, detail, pageFooter assert parts.total_elements == 6 # 2 + 1 + 2 + 1 def test_declaration_preserved(self): parts = decompose_jrxml(MINIMAL_JRXML) assert '') def test_returns_none_for_non_jrxml(self): parts = decompose_jrxml("") assert parts is None def test_returns_none_for_malformed_xml(self): parts = decompose_jrxml("not xml at all <<<") assert parts is None # ── Roundtrip 测试 ──────────────────────────────────────────────────── class TestRoundtrip: def test_decompose_reassemble_element_count_unchanged(self): parts = decompose_jrxml(MINIMAL_JRXML) band_map = {b.label: b.band_xml for b in parts.bands} result = reassemble_jrxml(parts, band_map) orig = count_elements(MINIMAL_JRXML) reassembled = count_elements(result) assert orig == reassembled, f"Elements: {orig} -> {reassembled}" def test_roundtrip_preserves_text_content(self): parts = decompose_jrxml(MINIMAL_JRXML) band_map = {b.label: b.band_xml for b in parts.bands} result = reassemble_jrxml(parts, band_map) assert 'Title' in result assert 'Header' in result assert '$F{name}' in result assert '$F{amount}' in result def test_empty_bands_preserved(self): """空 band(无元素)在 roundtrip 中不丢失。""" jrxml = """ <band height="50"> <staticText> <reportElement x="0" y="0" width="100" height="20"/> <text><![CDATA[T]]></text> </staticText> </band> """ parts = decompose_jrxml(jrxml) assert parts.band_count == 2 band_map = {b.label: b.band_xml for b in parts.bands} result = reassemble_jrxml(parts, band_map) assert count_elements(jrxml) == count_elements(result) # ── Window Split 测试 ───────────────────────────────────────────────── class TestWindowSplit: def test_small_band_not_split(self): """小 band 不会被切分。""" band = BandInfo( section_name="title", band_index=0, band_xml='', element_count=1, char_length=150, ) windows = split_band_into_windows(band, max_chars=4000) assert len(windows) == 1 def test_large_band_split_at_element_boundaries(self): """超过字符阈值的 band 在元素边界切分。""" inner = "\n" * 80 band_xml = f'{inner}' band = BandInfo( section_name="detail", band_index=0, band_xml=band_xml, element_count=80, char_length=len(band_xml), ) windows = split_band_into_windows(band, max_chars=4000) assert len(windows) > 1, f"Expected multiple windows, got {len(windows)}" def test_split_preserves_element_count(self): """切分后重组元素数不变。""" inner = "\n" * 80 band_xml = f'{inner}' band = BandInfo( section_name="detail", band_index=0, band_xml=band_xml, element_count=80, char_length=len(band_xml), ) windows = split_band_into_windows(band, max_chars=4000) reassembled = reassemble_band_windows(windows) assert count_elements(band_xml) == count_elements(reassembled) def test_no_empty_windows(self): """所有窗口非空。""" inner = "\n" * 80 band_xml = f'{inner}' band = BandInfo( section_name="detail", band_index=0, band_xml=band_xml, element_count=80, char_length=len(band_xml), ) windows = split_band_into_windows(band, max_chars=4000) for i, w in enumerate(windows): assert len(w.strip()) > 0, f"Window {i} is empty" assert '" def test_namespaced_band_split(self): """命名空间前缀的 band 也能正确切分。""" inner = "\n" * 80 band_xml = f'{inner}' band = BandInfo( section_name="detail", band_index=0, band_xml=band_xml, element_count=80, char_length=len(band_xml), ) windows = split_band_into_windows(band, max_chars=4000) assert len(windows) > 1, f"Expected multiple, got {len(windows)}" for w in windows: assert '' in w or w.startswith('', '') r = validate_element_count(MINIMAL_JRXML, xml2, "test") # 0% change since comments don't count as elements assert r["ok"] is True def test_large_change_not_ok(self): """> 10% 变化返回 ok=False。""" short = MINIMAL_JRXML[:500] # 大幅截断 r = validate_element_count(MINIMAL_JRXML, short, "test") if r["original"] > 0 and r["change_pct"] > 0.10: assert r["ok"] is False def test_zero_original_always_ok(self): r = validate_element_count("", MINIMAL_JRXML, "test") assert r["ok"] is True # ── 多 section 多 band 测试 ────────────────────────────────────────── MULTI_BAND_JRXML = """ """ class TestMultiBand: def test_multiple_bands_same_section(self): """同一 section 内的多个 band 分别处理。""" parts = decompose_jrxml(MULTI_BAND_JRXML) assert parts.band_count == 3 # detail_band0, detail_band1, summary labels = [b.label for b in parts.bands] assert labels == ["detail", "detail_band1", "summary"] def test_multi_band_roundtrip(self): parts = decompose_jrxml(MULTI_BAND_JRXML) band_map = {b.label: b.band_xml for b in parts.bands} result = reassemble_jrxml(parts, band_map) assert count_elements(MULTI_BAND_JRXML) == count_elements(result) def test_reassemble_opens_closes_sections(self): parts = decompose_jrxml(MULTI_BAND_JRXML) band_map = {b.label: b.band_xml for b in parts.bands} result = reassemble_jrxml(parts, band_map) assert result.count('') == 1 assert result.count('') == 1 assert result.count('') == 1 assert result.count('') == 1