"""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"