"""PDF→Text-Konvertierung: pymupdf4llm primär, pdftotext-Fallback, mtime-Cache.""" import os import time import fitz # PyMuPDF import pytest import blocks as blx def _mini_pdf(path, text="Approximationsalgorithmen sind wichtig."): doc = fitz.open() page = doc.new_page() page.insert_text((72, 72), text, fontsize=12) doc.save(str(path)) doc.close() def test_convert_writes_markdown_txt(tmp_path): _mini_pdf(tmp_path / "skript.pdf") blx._convert_pdfs(tmp_path) out = (tmp_path / "skript.txt").read_text(encoding="utf-8") assert "Approximationsalgorithmen" in out def test_cache_skips_fresh_txt(tmp_path): _mini_pdf(tmp_path / "a.pdf") (tmp_path / ".pdf-txt-norm").write_text(str(blx._PDF_NORM_VERSION), encoding="utf-8") marker = tmp_path / "a.txt" marker.write_text("MARKER", encoding="utf-8") now = time.time() + 60 os.utime(marker, (now, now)) blx._convert_pdfs(tmp_path) assert marker.read_text(encoding="utf-8") == "MARKER" # nicht neu konvertiert def test_norm_version_erzwingt_rekonvertierung(tmp_path, monkeypatch): """Fehlender/alter .pdf-txt-norm-Marker ignoriert den mtime-Cache einmalig.""" _mini_pdf(tmp_path / "a.pdf") stale = tmp_path / "a.txt" stale.write_text("ALT", encoding="utf-8") now = time.time() + 60 os.utime(stale, (now, now)) monkeypatch.setattr(blx, "_pdf_markdown", lambda p: None) monkeypatch.setattr(blx, "_pdf_plaintext", lambda p: "neu konvertiert") blx._convert_pdfs(tmp_path) assert stale.read_text(encoding="utf-8") == "neu konvertiert" assert (tmp_path / ".pdf-txt-norm").read_text(encoding="utf-8") == str(blx._PDF_NORM_VERSION) stale.write_text("BLEIBT", encoding="utf-8") os.utime(stale, (now, now)) blx._convert_pdfs(tmp_path) # Marker aktuell → Cache greift wieder assert stale.read_text(encoding="utf-8") == "BLEIBT" def test_entzerre_pdf_woerter(): """Small-Caps-Splits mergen; Variablen-Schutzfall und unbelegte Paare bleiben.""" # Regel A inkl. Kette assert blx._entzerre_pdf_woerter("Das H ITTING S ET Problem") == "Das HITTING SET Problem" assert blx._entzerre_pdf_woerter("V ERTEX C OVER ist schwer") == "VERTEX COVER ist schwer" # Schutzfall: Großlauf klein fortgesetzt → L ist Variable assert blx._entzerre_pdf_woerter("die Sprache L NP-vollständig ist") == "die Sprache L NP-vollständig ist" # Regel B: Paar nur mit Frequenz-Beleg (≥3× ungespalten im Dokument) belegt = "N P ist zentral. " + "NP NP NP." assert blx._entzerre_pdf_woerter(belegt).startswith("NP ist zentral.") assert blx._entzerre_pdf_woerter("C Y bleibt getrennt") == "C Y bleibt getrennt" # kein Match mitten im Wort assert blx._entzerre_pdf_woerter("HALTTM IST hier") == "HALTTM IST hier" def test_fallback_to_pdftotext(tmp_path, monkeypatch): _mini_pdf(tmp_path / "b.pdf") monkeypatch.setattr(blx, "_pdf_markdown", lambda p: None) monkeypatch.setattr(blx, "_pdf_plaintext", lambda p: "fallback") blx._convert_pdfs(tmp_path) assert (tmp_path / "b.txt").read_text(encoding="utf-8") == "fallback" def test_ocr_languages_from_tessdata(tmp_path, monkeypatch): import pymupdf monkeypatch.setattr(pymupdf, "get_tessdata", lambda: str(tmp_path)) assert blx._ocr_languages() is None # keine Sprachdaten → OCR aus (tmp_path / "eng.traineddata").touch() assert blx._ocr_languages() == "eng" (tmp_path / "deu.traineddata").touch() assert blx._ocr_languages() == "deu+eng" monkeypatch.setattr(pymupdf, "get_tessdata", lambda: (_ for _ in ()).throw(RuntimeError())) assert blx._ocr_languages() is None def test_fidelity_guard_prefers_faithful_plaintext(): plain = "Definition. P = {L ⊆ Σ | A ∈ L} und ≤ sowie häufig über. " * 20 # Markdown verlor die Formeln (Symbole weg) → plain gewinnt md_lossy = "Definition. und sowie h¨aufig ¨uber. " * 20 text, tool = blx._pick_conversion(md_lossy, plain) assert tool == "pdftotext" # Markdown treu (Symbole + Länge da) → md gewinnt md_ok = "# Def\n" + plain text, tool = blx._pick_conversion(md_ok, plain) assert tool == "pymupdf4llm" # nur eine Quelle verfügbar assert blx._pick_conversion(None, plain)[1] == "pdftotext" assert blx._pick_conversion(md_ok, None)[1] == "pymupdf4llm" assert blx._pick_conversion(None, None) is None def test_entzerre_hyphen_space(): """Regel C: GROSSLAUF + Leerzeichen vor Bindestrich-Kompositum („NP -vollständig", Makro-Spacing) wird gemerged; echter Gedankenstrich („X - y") bleibt.""" assert blx._entzerre_pdf_woerter("die NP -vollständigkeit gilt") == "die NP-vollständigkeit gilt" assert blx._entzerre_pdf_woerter("Begriffe der NP -schwere und NP -vollständigkeit") == \ "Begriffe der NP-schwere und NP-vollständigkeit" assert blx._entzerre_pdf_woerter("Term X - y bleibt") == "Term X - y bleibt" assert blx._entzerre_pdf_woerter("Liste:\n-punkt eins") == "Liste:\n-punkt eins"