XmLLM / tests /unit /test_viewer_builder.py
Claude
Code quality: fix all ruff warnings, add CI/CD, improve test coverage
bbbfba8 unverified
Raw
History Blame Contribute Delete
9.35 kB
"""Tests for the ViewerProjection builder and overlay generation."""
from __future__ import annotations
import json
from typing import TYPE_CHECKING
import pytest
from src.app.domain.models import (
AltoReadiness,
CanonicalDocument,
EvidenceType,
ExportEligibility,
Geometry,
GeometryStatus,
NonTextRegion,
OverlayLevel,
Page,
PageXmlReadiness,
Provenance,
RawProviderPayload,
ReadinessLevel,
Source,
TextLine,
TextRegion,
Word,
)
from src.app.domain.models.geometry import GeometryContext
from src.app.domain.models.status import BlockRole, InputType, NonTextKind
from src.app.normalization.pipeline import normalize
from src.app.viewer.overlays import (
line_to_overlay,
region_to_overlay,
word_to_inspection,
word_to_overlay,
)
from src.app.viewer.projection_builder import build_all_projections, build_projection
if TYPE_CHECKING:
from pathlib import Path
def _prov() -> Provenance:
return Provenance(
provider="test", adapter="v1", source_ref="$",
evidence_type=EvidenceType.PROVIDER_NATIVE,
)
def _geo(x: float = 10, y: float = 10, w: float = 100, h: float = 30) -> Geometry:
return Geometry(bbox=(x, y, w, h), status=GeometryStatus.EXACT)
def _simple_doc() -> CanonicalDocument:
return CanonicalDocument(
document_id="viewer_test",
source=Source(input_type=InputType.IMAGE, filename="test.png"),
pages=[Page(
id="p1", page_index=0, width=2480, height=3508,
alto_readiness=AltoReadiness(level=ReadinessLevel.FULL),
page_readiness=PageXmlReadiness(level=ReadinessLevel.FULL),
reading_order=["tb1"],
text_regions=[TextRegion(
id="tb1", role=BlockRole.BODY,
geometry=_geo(100, 200, 1200, 900), provenance=_prov(), lang="fra",
lines=[
TextLine(id="tl1", geometry=_geo(110, 220, 1100, 42),
provenance=_prov(), lang="fra",
words=[
Word(id="w1", text="Bonjour", geometry=_geo(110, 220, 90, 40),
provenance=_prov(), confidence=0.96, lang="fra"),
Word(id="w2", text="monde", geometry=_geo(220, 220, 80, 40),
provenance=_prov(), confidence=0.94, lang="fra"),
]),
TextLine(id="tl2", geometry=_geo(110, 280, 1100, 42),
provenance=_prov(),
words=[
Word(id="w3", text="Test", geometry=_geo(110, 280, 60, 40),
provenance=_prov(), confidence=0.90),
]),
],
)],
non_text_regions=[NonTextRegion(
id="ntr1", kind=NonTextKind.ILLUSTRATION,
geometry=_geo(1500, 200, 400, 300), provenance=_prov(),
)],
)],
)
# -- Overlay unit tests -------------------------------------------------------
class TestOverlayGeneration:
def test_word_overlay(self) -> None:
w = Word(id="w1", text="Hello", geometry=_geo(10, 20, 50, 30),
provenance=_prov(), confidence=0.95, lang="eng")
ov = word_to_overlay(w)
assert ov.id == "w1"
assert ov.level == OverlayLevel.WORD
assert ov.text == "Hello"
assert ov.bbox == (10, 20, 50, 30)
assert ov.confidence == 0.95
assert ov.provenance_type == EvidenceType.PROVIDER_NATIVE
assert ov.geometry_status == GeometryStatus.EXACT
def test_line_overlay(self) -> None:
line = TextLine(id="tl1", geometry=_geo(10, 20, 200, 30),
provenance=_prov(),
words=[Word(id="w1", text="Hello", geometry=_geo(),
provenance=_prov())])
ov = line_to_overlay(line)
assert ov.id == "tl1"
assert ov.level == OverlayLevel.LINE
assert ov.text == "Hello"
def test_region_overlay(self) -> None:
r = TextRegion(id="tb1", role=BlockRole.HEADING,
geometry=_geo(0, 0, 500, 100), provenance=_prov(),
lines=[TextLine(id="tl1", geometry=_geo(),
provenance=_prov(),
words=[Word(id="w1", text="Title",
geometry=_geo(),
provenance=_prov())])])
ov = region_to_overlay(r)
assert ov.id == "tb1"
assert ov.level == OverlayLevel.BLOCK
assert ov.label == "heading"
def test_word_inspection(self) -> None:
w = Word(id="w1", text="Hello", geometry=_geo(10, 20, 50, 30),
provenance=_prov(), confidence=0.95, lang="eng")
insp = word_to_inspection(w)
assert insp.id == "w1"
assert insp.text == "Hello"
assert insp.lang == "eng"
assert insp.provenance_provider == "test"
# -- Projection builder -------------------------------------------------------
class TestProjectionBuilder:
def test_build_projection(self) -> None:
doc = _simple_doc()
vp = build_projection(doc)
assert vp.image_ref == "test.png"
assert vp.image_width == 2480
assert vp.image_height == 3508
def test_block_overlays(self) -> None:
doc = _simple_doc()
vp = build_projection(doc)
assert len(vp.block_overlays) == 1
assert vp.block_overlays[0].id == "tb1"
def test_line_overlays(self) -> None:
doc = _simple_doc()
vp = build_projection(doc)
assert len(vp.line_overlays) == 2
def test_word_overlays(self) -> None:
doc = _simple_doc()
vp = build_projection(doc)
assert len(vp.word_overlays) == 3
word_ids = {o.id for o in vp.word_overlays}
assert word_ids == {"w1", "w2", "w3"}
def test_non_text_overlays(self) -> None:
doc = _simple_doc()
vp = build_projection(doc)
assert len(vp.non_text_overlays) == 1
assert vp.non_text_overlays[0].id == "ntr1"
def test_inspection_index(self) -> None:
doc = _simple_doc()
vp = build_projection(doc)
# Should contain all blocks, lines, and words
assert "tb1" in vp.inspection_index
assert "tl1" in vp.inspection_index
assert "tl2" in vp.inspection_index
assert "w1" in vp.inspection_index
assert "w2" in vp.inspection_index
assert "w3" in vp.inspection_index
def test_inspection_data_content(self) -> None:
doc = _simple_doc()
vp = build_projection(doc)
w1 = vp.inspection_index["w1"]
assert w1.text == "Bonjour"
assert w1.confidence == 0.96
assert w1.lang == "fra"
assert w1.provenance_provider == "test"
def test_export_status_default(self) -> None:
doc = _simple_doc()
vp = build_projection(doc)
assert vp.export_status.alto_export == ReadinessLevel.NONE
def test_export_status_custom(self) -> None:
doc = _simple_doc()
elig = ExportEligibility(
alto_export=ReadinessLevel.FULL,
page_export=ReadinessLevel.FULL,
viewer_render=ReadinessLevel.FULL,
)
vp = build_projection(doc, export_status=elig)
assert vp.export_status.alto_export == ReadinessLevel.FULL
def test_page_index_out_of_range(self) -> None:
doc = _simple_doc()
with pytest.raises(ValueError, match="out of range"):
build_projection(doc, page_index=5)
def test_build_all_projections(self) -> None:
doc = _simple_doc()
projections = build_all_projections(doc)
assert len(projections) == 1
assert projections[0].image_width == 2480
def test_json_roundtrip(self) -> None:
doc = _simple_doc()
vp = build_projection(doc)
data = vp.model_dump(mode="json")
# Verify it's fully serializable
import json
json_str = json.dumps(data)
restored = json.loads(json_str)
assert restored["image_width"] == 2480
assert len(restored["word_overlays"]) == 3
assert "w1" in restored["inspection_index"]
class TestProjectionFromPipeline:
"""Integration: PaddleOCR raw → normalize → build_projection."""
def test_from_paddle(self, fixtures_dir: Path) -> None:
with open(fixtures_dir / "paddle_ocr_sample.json") as f:
payload = json.load(f)
raw = RawProviderPayload(
provider_id="paddleocr", adapter_id="v1", runtime_type="local",
payload=payload, 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="vp_test", source_filename="page.png")
vp = build_projection(doc)
assert vp.image_ref == "page.png"
assert len(vp.word_overlays) == 5
assert vp.word_overlays[0].text == "Bonjour"
assert len(vp.inspection_index) > 0