diff --git a/README.md b/README.md index 15d2edb..1083653 100644 --- a/README.md +++ b/README.md @@ -208,7 +208,7 @@ docker compose -f docker-compose.prod.yml --profile observability up -d promethe 1. **Линт** — `ruff` (весь Python) + `mypy` (чистая доменная логика). Конфиги: [`ruff.toml`](ruff.toml), [`mypy.ini`](mypy.ini). -2. **Юнит-тесты** — `pytest` по сервисам: 118 тестов на ядро детекции, скоринга, +2. **Юнит-тесты** — `pytest` по сервисам: 122 теста на ядро детекции, скоринга, парсеров, форматирования и OAuth, без внешней инфры (БД/Redis/GPU/Ollama замоканы либо не нужны). @@ -231,6 +231,7 @@ make test-one SVC=worker-gost # тесты одного сервиса | L2 — нечёткие (MinHash LSH) | `worker-indexer/app/algorithms/minhash.py` | 6 | | Разбиение на фрагменты | `worker-indexer/app/fragments.py` | 5 | | Автопополнение корпуса | `worker-indexer/app/staging.py` | 6 | +| Извлечение текста из PDF | `worker-indexer/app/extractors/pdf.py` | 4 | | L3 — семантический индекс (FAISS) | `worker-gpu/app/faiss_manager.py` | 5 | | L3 — векторный бэкенд (Qdrant + выбор) | `worker-gpu/app/qdrant_manager.py`, `vector_store.py` | 9 | | L4 — LLM-парафраз | `worker-gpu/app/ollama_client.py` | 12 | diff --git a/services/worker-indexer/app/extractors/pdf.py b/services/worker-indexer/app/extractors/pdf.py index 2dbbbe3..fa54d58 100644 --- a/services/worker-indexer/app/extractors/pdf.py +++ b/services/worker-indexer/app/extractors/pdf.py @@ -23,6 +23,12 @@ def extract_text_from_pdf(data: bytes) -> str: """ import fitz # PyMuPDF + # Частый случай: файл с расширением .pdf, но внутри — HTML/другое (например, + # сохранённая веб-страница). Даём понятное сообщение, а не сырую ошибку MuPDF + # вида "code=7: no objects found", которая ничего не скажет пользователю. + if not data.startswith(b"%PDF-"): + raise ValueError("Файл повреждён или не является PDF-документом") + try: doc = fitz.open(stream=data, filetype="pdf") except Exception as e: diff --git a/services/worker-indexer/requirements-test.txt b/services/worker-indexer/requirements-test.txt index bdcbe3c..5a21bd3 100644 --- a/services/worker-indexer/requirements-test.txt +++ b/services/worker-indexer/requirements-test.txt @@ -6,3 +6,4 @@ xxhash==3.4.1 datasketch==1.6.5 numpy==1.26.4 pydantic-settings==2.2.1 +PyMuPDF==1.24.0 diff --git a/services/worker-indexer/tests/test_extractors_pdf.py b/services/worker-indexer/tests/test_extractors_pdf.py new file mode 100644 index 0000000..5671f1b --- /dev/null +++ b/services/worker-indexer/tests/test_extractors_pdf.py @@ -0,0 +1,41 @@ +"""Юнит-тесты извлечения текста из PDF (app.extractors.pdf).""" + +import pytest +from app.extractors.pdf import extract_text_from_pdf + + +def _minimal_pdf_bytes(text: str) -> bytes: + """Собрать валидный однострочный PDF с заданным текстом через сам PyMuPDF.""" + import fitz + + doc = fitz.open() + page = doc.new_page() + page.insert_text((72, 72), text) + data = doc.tobytes() + doc.close() + return data + + +def test_extracts_text_from_valid_pdf(): + # ASCII — встроенный шрифт PyMuPDF не рендерит кириллицу без явного указания + # шрифта с поддержкой юникода; для проверки самой логики извлечения не важно. + text = extract_text_from_pdf(_minimal_pdf_bytes("Hello, world")) + assert "Hello, world" in text + + +def test_non_pdf_content_raises_friendly_error(): + # Частый реальный случай: файл с расширением .pdf, а внутри — HTML + html = b"not a pdf" + with pytest.raises(ValueError, match="не является PDF"): + extract_text_from_pdf(html) + + +def test_empty_bytes_raises_friendly_error(): + with pytest.raises(ValueError, match="не является PDF"): + extract_text_from_pdf(b"") + + +def test_truncated_pdf_header_only_raises(): + # Начинается как PDF (проходит magic-byte проверку), но структуры внутри нет + with pytest.raises(ValueError): + extract_text_from_pdf(b"%PDF-1.4\n" + b"garbage" * 20)