ClassLens-dev / chatkit /backend /tests /test_native_parsers.py
Yu Chen
simplify the answer parsing tool selection
73c0cf0
Raw
History Blame Contribute Delete
4.06 kB
"""Integration tests for native PDF table extraction.
Covers the public `native_pdf_tables` facade (registered) and exercises the
underlying engine classes directly to keep regression coverage on each.
"""
from __future__ import annotations
from pathlib import Path
import pytest
from app.parsers import get_parser, pick_auto
from app.parsers.base import ParserFile
from app.parsers.pdfplumber_tables import PdfPlumberTablesParser
from app.parsers.pymupdf_tables import PyMuPDFTablesParser
SAMPLE_PDF = Path(
"/Users/yu.c/Desktop/npo-ca-classlens-data copy/example-2/9A作答一覽表.pdf"
)
@pytest.fixture
def sample_file() -> ParserFile:
if not SAMPLE_PDF.exists():
pytest.skip(f"Sample PDF not available: {SAMPLE_PDF}")
return ParserFile(filename=SAMPLE_PDF.name, content=SAMPLE_PDF.read_bytes())
# ---------------------------------------------------------------------------
# Engine-level coverage (PyMuPDF + pdfplumber)
# ---------------------------------------------------------------------------
def _engines():
return [PyMuPDFTablesParser(), PdfPlumberTablesParser()]
@pytest.mark.integration
@pytest.mark.parametrize("engine", _engines(), ids=lambda e: e.name)
async def test_engine_parses_student_answers(sample_file: ParserFile, engine):
result = await engine.parse([sample_file], "student_answers")
students = result.data["students"]
assert len(students) == 7, f"expected 7 students, got {len(students)}"
for s in students:
assert 40 <= len(s["answers"]) <= 50
assert s["name"]
assert s["id"]
by_name = {s["name"]: s for s in students}
assert "陳筱琳" in by_name
assert by_name["陳筱琳"]["id"] == "902-13"
first_10 = [a["answer"] for a in by_name["陳筱琳"]["answers"][:10]]
# From the PDF: "==CBBBA==C"
assert first_10 == ["=", "=", "C", "B", "B", "B", "A", "=", "=", "C"]
@pytest.mark.integration
@pytest.mark.parametrize("engine", _engines(), ids=lambda e: e.name)
async def test_engine_parses_teacher_answers(sample_file: ParserFile, engine):
result = await engine.parse([sample_file], "teacher_answers")
answers = result.data["answers"]
assert 40 <= len(answers) <= 50
first_10 = "".join(a["correct_answer"] for a in answers[:10])
assert first_10 == "BAACCACACB"
# ---------------------------------------------------------------------------
# Facade behavior
# ---------------------------------------------------------------------------
@pytest.mark.integration
async def test_native_facade_succeeds_via_first_engine(sample_file: ParserFile):
parser = get_parser("native_pdf_tables")
result = await parser.parse([sample_file], "student_answers")
assert result.parser_name == "native_pdf_tables"
assert any("native PDF engine" in n for n in result.notes)
assert len(result.data["students"]) == 7
@pytest.mark.unit
async def test_native_facade_raises_csv_hint_when_all_engines_fail():
parser = get_parser("native_pdf_tables")
fake = ParserFile(filename="not-a.pdf", content=b"%PDF-1.4 garbage")
with pytest.raises(ValueError, match=r"(?i)please convert to csv"):
await parser.parse([fake], "student_answers")
@pytest.mark.unit
def test_native_facade_refuses_image_file():
parser = get_parser("native_pdf_tables")
png_bytes = b"\x89PNG\r\n\x1a\n"
assert parser.can_handle(png_bytes, "scan.png", "student_answers") is False
# ---------------------------------------------------------------------------
# Auto-router
# ---------------------------------------------------------------------------
@pytest.mark.integration
async def test_auto_picks_native_facade_for_text_pdf(sample_file: ParserFile):
chosen = pick_auto(sample_file.content, sample_file.filename, "student_answers")
assert chosen.name == "native_pdf_tables"
@pytest.mark.integration
async def test_auto_falls_back_to_llm_for_questions(sample_file: ParserFile):
chosen = pick_auto(sample_file.content, sample_file.filename, "questions")
assert chosen.name == "llm_vision"