Spaces:
Paused
Paused
| """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" | |
| ) | |
| 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()] | |
| 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"] | |
| 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 | |
| # --------------------------------------------------------------------------- | |
| 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 | |
| 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") | |
| 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 | |
| # --------------------------------------------------------------------------- | |
| 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" | |
| 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" | |