XmLLM / tests /integration /test_dual_export.py
Claude
Code quality: fix all ruff warnings, add CI/CD, improve test coverage
bbbfba8 unverified
Raw
History Blame Contribute Delete
5.49 kB
"""Integration test: PaddleOCR → CanonicalDocument → ALTO + PAGE (dual export).
Validates that the same canonical document can be serialized to both
ALTO XML and PAGE XML, and that both outputs are structurally correct.
"""
from __future__ import annotations
import json
from typing import TYPE_CHECKING
from lxml import etree
from src.app.domain.models import RawProviderPayload
from src.app.domain.models.geometry import GeometryContext
from src.app.normalization.pipeline import normalize
from src.app.policies.document_policy import DocumentPolicy
from src.app.policies.export_policy import check_alto_export, check_page_export
from src.app.serializers.alto_xml import ALTO_NS, serialize_alto
from src.app.serializers.page_xml import PAGE_NS, serialize_page_xml
from src.app.validators.export_eligibility_validator import compute_export_eligibility
if TYPE_CHECKING:
from pathlib import Path
class TestDualExport:
"""Full pipeline: raw → canon → validate → ALTO + PAGE."""
def test_both_exports_from_same_document(self, fixtures_dir: Path) -> None:
# 1. Load and normalize
with open(fixtures_dir / "paddle_ocr_sample.json") as f:
paddle_output = json.load(f)
raw = RawProviderPayload(
provider_id="paddleocr",
adapter_id="adapter.word_box_json.v1",
runtime_type="local",
payload=paddle_output,
image_width=2480, image_height=3508,
)
geo_ctx = GeometryContext(source_width=2480, source_height=3508)
doc = normalize(
raw, family="word_box_json", geometry_context=geo_ctx,
document_id="dual_export_test", source_filename="page.png",
)
# 2. Check export eligibility
policy = DocumentPolicy()
eligibility = compute_export_eligibility(doc, policy)
alto_decision = check_alto_export(eligibility, policy)
page_decision = check_page_export(eligibility, policy)
assert alto_decision.allowed
assert page_decision.allowed
# 3. Serialize both
alto_bytes = serialize_alto(doc)
page_bytes = serialize_page_xml(doc)
assert alto_bytes
assert page_bytes
# 4. Parse both
alto_root = etree.fromstring(alto_bytes)
page_root = etree.fromstring(page_bytes)
# 5. Validate ALTO structure
alto_strings = alto_root.findall(f".//{{{ALTO_NS}}}String")
assert len(alto_strings) == 5
assert alto_strings[0].get("CONTENT") == "Bonjour"
# 6. Validate PAGE structure
page_words = page_root.findall(f".//{{{PAGE_NS}}}Word")
assert len(page_words) == 5
w1_te = page_words[0].find(f".//{{{PAGE_NS}}}Unicode")
assert w1_te.text == "Bonjour"
# 7. Verify same word count in both
assert len(alto_strings) == len(page_words)
# 8. Verify same text content in both
alto_texts = [s.get("CONTENT") for s in alto_strings]
page_texts = [
w.find(f".//{{{PAGE_NS}}}Unicode").text for w in page_words
]
assert alto_texts == page_texts
def test_page_has_coords_alto_has_hpos(self, fixtures_dir: Path) -> None:
"""PAGE uses Coords/points, ALTO uses HPOS/VPOS/WIDTH/HEIGHT."""
with open(fixtures_dir / "paddle_ocr_sample.json") as f:
paddle_output = json.load(f)
raw = RawProviderPayload(
provider_id="paddleocr", adapter_id="v1", runtime_type="local",
payload=paddle_output, image_width=2480, image_height=3508,
)
geo_ctx = GeometryContext(source_width=2480, source_height=3508)
doc = normalize(
raw, family="word_box_json", geometry_context=geo_ctx,
document_id="coord_test",
)
alto_root = etree.fromstring(serialize_alto(doc))
page_root = etree.fromstring(serialize_page_xml(doc))
# ALTO: String has HPOS, VPOS, WIDTH, HEIGHT
alto_s = alto_root.find(f".//{{{ALTO_NS}}}String")
assert alto_s.get("HPOS") is not None
assert alto_s.get("VPOS") is not None
assert alto_s.get("WIDTH") is not None
assert alto_s.get("HEIGHT") is not None
# PAGE: Word has Coords with points
page_w = page_root.find(f".//{{{PAGE_NS}}}Word")
coords = page_w.find(f"{{{PAGE_NS}}}Coords")
assert coords is not None
points = coords.get("points")
assert points is not None
# Points should be polygon (4 vertices from PaddleOCR)
parts = points.split()
assert len(parts) == 4
def test_page_has_reading_order(self, fixtures_dir: Path) -> None:
with open(fixtures_dir / "paddle_ocr_sample.json") as f:
paddle_output = json.load(f)
raw = RawProviderPayload(
provider_id="paddleocr", adapter_id="v1", runtime_type="local",
payload=paddle_output, image_width=2480, image_height=3508,
)
geo_ctx = GeometryContext(source_width=2480, source_height=3508)
doc = normalize(
raw, family="word_box_json", geometry_context=geo_ctx,
document_id="ro_test",
)
page_root = etree.fromstring(serialize_page_xml(doc))
ro = page_root.find(f".//{{{PAGE_NS}}}ReadingOrder")
assert ro is not None
refs = ro.findall(f".//{{{PAGE_NS}}}RegionRefIndexed")
assert len(refs) >= 1
assert refs[0].get("regionRef") == "tb1"