"""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 = """
"""
# ── 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 = """
"""
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