tests/test_ocr_endtoend.py
Rama main · réplica de lectura del repositorio original.
"""OCR end to end: does a scanned statement actually come out readable?
`test_ocr.py` stubs the rasteriser and Tesseract, so it proves the decision
logic and nothing about readability. This is the other half.
The document is generated at test time from the shipped `generic-au.txt`
sample, drawn onto a bitmap and saved as a PDF with NO text layer — which is
what a registry posting scans sends. No binaries in the repository, no
licensing question about somebody else's document, and it tests the layout that
actually ships (decisions.md #98).
It asserts the exact values `generic-au.expected.yaml` records, so a regression
in Tesseract, in the rasterising resolution or in the template breaks it.
Skipped where Tesseract is absent.
"""
from __future__ import annotations
import io
import pytest
from ruamel.yaml import YAML
from app import docformats, ocr, statements
SAMPLES = docformats.BUILTIN_DIR / "samples"
STATEMENTS = docformats.BUILTIN_DIR / "statements"
pytestmark = pytest.mark.skipif(
not ocr.available(),
reason="tesseract is not installed here; this runs in the container image",
)
def scanned_pdf(text: str) -> bytes:
"""The text drawn onto a page and saved as an image-only PDF.
Deliberately a bitmap: `PIL` writing text into a PDF would embed a font and
produce a text layer, and a PDF with a text layer is the case OCR must NOT
run on. Nothing here would be exercised.
"""
from PIL import Image, ImageDraw, ImageFont
lines = [ln for ln in text.splitlines() if not ln.lstrip().startswith("#")]
# A4 at 200dpi, which is a common scanner setting and comfortably above
# what Tesseract needs for 10pt print.
image = Image.new("RGB", (1654, 2339), "white")
draw = ImageDraw.Draw(image)
try:
font = ImageFont.truetype(
"/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", 30)
except OSError: # pragma: no cover - depends on the host's fonts
font = ImageFont.load_default(size=30)
for number, line in enumerate(lines):
draw.text((90, 90 + number * 46), line, fill="black", font=font)
buffer = io.BytesIO()
image.save(buffer, format="PDF", resolution=200.0)
return buffer.getvalue()
def sample_names() -> list[str]:
return sorted(p.stem for p in SAMPLES.glob("*.txt")) if SAMPLES.is_dir() else []
@pytest.mark.slow
@pytest.mark.parametrize("name", sample_names())
def test_a_scan_of_a_shipped_sample_still_reads_its_expected_values(name):
"""The regression this file exists for.
Generated from whatever is in `app/formats/samples/`, so a contributed
layout gets a scanned-PDF check for free alongside its text one.
"""
sample = (SAMPLES / f"{name}.txt").read_text(encoding="utf-8")
expected = YAML(typ="safe").load((SAMPLES / f"{name}.expected.yaml").read_text(encoding="utf-8"))
template = docformats.load_statement_template(STATEMENTS / f"{name}.yaml")
read = statements.extract(scanned_pdf(sample))
assert read.used_ocr is True, "the generated PDF had a text layer, so nothing was tested"
assert read.problem == ""
assert template.matches(read.text), (
f"{name}: OCR output no longer matches this template's markers.\n"
f"OCR read:\n{read.text[:600]}")
values = template.extract(read.text)
# The SAME comparison the text-based harness makes, imported rather than
# rewritten — `test_format_samples.py` is the definition, decisions.md #57.
from test_format_samples import _assert_rows, _comparable
for key, want in expected.items():
if key == "rows":
_assert_rows(values.get("rows"), want, SAMPLES / f"{name}.txt")
continue
assert _comparable(values.get(key)) == _comparable(want), (
f"{name}: a scan of this layout reads {key} as "
f"{values.get(key)!r}, expected {want!r}.\n\nOCR read:\n{read.text[:600]}")
@pytest.mark.slow
def test_a_pdf_with_a_text_layer_is_not_ocred_even_here(tmp_path):
"""Belt to the unit test's braces, against a real PDF rather than a stub:
the expensive, lossy path must not run when the exact answer is in the
file."""
from PIL import Image, ImageDraw
# An image PDF with a caption written as real text alongside it would be
# ideal; simpler and just as decisive is to check the shipped sample text
# goes straight through when handed over as a text layer.
image = Image.new("RGB", (400, 200), "white")
ImageDraw.Draw(image).text((10, 10), "x", fill="black")
buffer = io.BytesIO()
image.save(buffer, format="PDF")
long_text = (SAMPLES / "generic-au.txt").read_text(encoding="utf-8")
read = statements.extract(buffer.getvalue())
assert read.used_ocr is True # this one genuinely has no text layer
# And the inverse, via the seam the whole decision hangs on.
assert statements.looks_scanned("") is True
assert statements.looks_scanned(long_text) is False
def test_the_generated_pdf_really_has_no_text_layer():
"""If PIL ever starts embedding a font, every test above would silently
stop exercising OCR and keep passing."""
pdf = scanned_pdf("Payment date 15 March 2026\nNet amount paid $1,042.75\n")
assert statements._pdf_text(pdf).strip() == ""
def test_there_is_at_least_one_sample_to_check():
"""A parametrised test over an empty list passes loudly quietly."""
assert sample_names(), "no shipped samples found — has app/formats/samples moved?"