d9abbc1c99
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
208 lines
8.6 KiB
Python
208 lines
8.6 KiB
Python
"""Tests for the head-to-head text comparison logic in process_with_ocr.
|
|
|
|
Covers lines 154-232 of app/tasks/process_with_ocr.py:
|
|
- original_text provided with both texts non-empty (comparison preferred=original)
|
|
- original_text provided with both texts non-empty (comparison preferred=ocr)
|
|
- comparison raises an exception → keep OCR text
|
|
- original_text provided but OCR returns empty text → fallback to original
|
|
- original_text provided but is empty → skip comparison, use OCR output
|
|
"""
|
|
|
|
from unittest.mock import Mock, patch
|
|
|
|
import pytest
|
|
|
|
from app.utils.text_quality import TextComparisonResult
|
|
|
|
|
|
@pytest.mark.unit
|
|
class TestProcessWithOCRTextComparison:
|
|
"""Tests for the head-to-head quality comparison in process_with_ocr."""
|
|
|
|
_MINIMAL_PDF = (
|
|
b"%PDF-1.4\n"
|
|
b"1 0 obj\n<</Type /Catalog /Pages 2 0 R>>\nendobj\n"
|
|
b"2 0 obj\n<</Type /Pages /Kids [3 0 R] /Count 1>>\nendobj\n"
|
|
b"3 0 obj\n<</Type /Page /Parent 2 0 R /MediaBox [0 0 612 792]>>\nendobj\n"
|
|
b"xref\n0 4\n"
|
|
b"0000000000 65535 f \n"
|
|
b"0000000009 00000 n \n"
|
|
b"0000000058 00000 n \n"
|
|
b"0000000115 00000 n \n"
|
|
b"trailer\n<</Size 4 /Root 1 0 R>>\n"
|
|
b"startxref\n190\n%%EOF\n"
|
|
)
|
|
|
|
def _setup_ocr_task(self, tmp_path, extracted_text, searchable_pdf_path=None):
|
|
"""Helper to set up common mocks for OCR task tests."""
|
|
from app.utils.ocr_provider import OCRResult
|
|
|
|
tmp_dir = tmp_path / "tmp"
|
|
tmp_dir.mkdir(exist_ok=True)
|
|
pdf_file = tmp_dir / "test.pdf"
|
|
pdf_file.write_bytes(self._MINIMAL_PDF)
|
|
|
|
mock_result = OCRResult(
|
|
provider="azure",
|
|
text=extracted_text,
|
|
searchable_pdf_path=searchable_pdf_path or str(pdf_file),
|
|
)
|
|
|
|
provider_mock = Mock()
|
|
provider_mock.name = "azure"
|
|
provider_mock.__class__.__name__ = "AzureOCRProvider"
|
|
provider_mock.process.return_value = mock_result
|
|
|
|
return pdf_file, provider_mock
|
|
|
|
@patch("app.tasks.process_with_ocr.log_task_progress")
|
|
@patch("app.tasks.process_with_ocr.rotate_pdf_pages")
|
|
@patch("app.tasks.process_with_ocr.compare_text_quality")
|
|
def test_comparison_prefers_original_text(self, mock_compare, mock_rotate, mock_log, tmp_path):
|
|
"""When comparison prefers original, final_text should be original_text."""
|
|
from app.tasks.process_with_ocr import process_with_ocr
|
|
|
|
pdf_file, provider_mock = self._setup_ocr_task(tmp_path, "OCR extracted text")
|
|
mock_rotate.delay = Mock()
|
|
|
|
mock_compare.return_value = TextComparisonResult(
|
|
preferred="original",
|
|
original_score=90,
|
|
ocr_score=70,
|
|
explanation="Original text is cleaner",
|
|
)
|
|
|
|
with (
|
|
patch("app.tasks.process_with_ocr.settings") as mock_settings,
|
|
patch("app.tasks.process_with_ocr.get_ocr_providers") as mock_providers,
|
|
patch(
|
|
"app.tasks.process_with_ocr.merge_ocr_results",
|
|
return_value=("OCR extracted text", str(pdf_file), {}),
|
|
),
|
|
):
|
|
mock_settings.workdir = str(tmp_path)
|
|
mock_providers.return_value = [provider_mock]
|
|
|
|
result = process_with_ocr.run("test.pdf", file_id=1, original_text="Original embedded text")
|
|
|
|
assert result["cleaned_text"] == "Original embedded text"
|
|
mock_compare.assert_called_once_with("Original embedded text", "OCR extracted text")
|
|
|
|
@patch("app.tasks.process_with_ocr.log_task_progress")
|
|
@patch("app.tasks.process_with_ocr.rotate_pdf_pages")
|
|
@patch("app.tasks.process_with_ocr.compare_text_quality")
|
|
def test_comparison_prefers_ocr_text(self, mock_compare, mock_rotate, mock_log, tmp_path):
|
|
"""When comparison prefers OCR, final_text should be the OCR text."""
|
|
from app.tasks.process_with_ocr import process_with_ocr
|
|
|
|
pdf_file, provider_mock = self._setup_ocr_task(tmp_path, "High quality OCR text")
|
|
mock_rotate.delay = Mock()
|
|
|
|
mock_compare.return_value = TextComparisonResult(
|
|
preferred="ocr",
|
|
original_score=50,
|
|
ocr_score=95,
|
|
explanation="OCR text is more complete",
|
|
)
|
|
|
|
with (
|
|
patch("app.tasks.process_with_ocr.settings") as mock_settings,
|
|
patch("app.tasks.process_with_ocr.get_ocr_providers") as mock_providers,
|
|
patch(
|
|
"app.tasks.process_with_ocr.merge_ocr_results",
|
|
return_value=("High quality OCR text", str(pdf_file), {}),
|
|
),
|
|
):
|
|
mock_settings.workdir = str(tmp_path)
|
|
mock_providers.return_value = [provider_mock]
|
|
|
|
result = process_with_ocr.run("test.pdf", file_id=1, original_text="Low quality original")
|
|
|
|
assert result["cleaned_text"] == "High quality OCR text"
|
|
mock_compare.assert_called_once()
|
|
|
|
@patch("app.tasks.process_with_ocr.log_task_progress")
|
|
@patch("app.tasks.process_with_ocr.rotate_pdf_pages")
|
|
@patch("app.tasks.process_with_ocr.compare_text_quality")
|
|
def test_comparison_exception_keeps_ocr_text(self, mock_compare, mock_rotate, mock_log, tmp_path):
|
|
"""When comparison raises an exception, the OCR text should be kept."""
|
|
from app.tasks.process_with_ocr import process_with_ocr
|
|
|
|
pdf_file, provider_mock = self._setup_ocr_task(tmp_path, "OCR text after error")
|
|
mock_rotate.delay = Mock()
|
|
|
|
mock_compare.side_effect = RuntimeError("AI comparison failed")
|
|
|
|
with (
|
|
patch("app.tasks.process_with_ocr.settings") as mock_settings,
|
|
patch("app.tasks.process_with_ocr.get_ocr_providers") as mock_providers,
|
|
patch(
|
|
"app.tasks.process_with_ocr.merge_ocr_results",
|
|
return_value=("OCR text after error", str(pdf_file), {}),
|
|
),
|
|
):
|
|
mock_settings.workdir = str(tmp_path)
|
|
mock_providers.return_value = [provider_mock]
|
|
|
|
result = process_with_ocr.run("test.pdf", file_id=1, original_text="Some original text")
|
|
|
|
assert result["cleaned_text"] == "OCR text after error"
|
|
|
|
@patch("app.tasks.process_with_ocr.log_task_progress")
|
|
@patch("app.tasks.process_with_ocr.rotate_pdf_pages")
|
|
def test_ocr_empty_falls_back_to_original(self, mock_rotate, mock_log, tmp_path):
|
|
"""When OCR returns empty text but original is non-empty, fallback to original."""
|
|
from app.tasks.process_with_ocr import process_with_ocr
|
|
|
|
pdf_file, provider_mock = self._setup_ocr_task(tmp_path, "")
|
|
mock_rotate.delay = Mock()
|
|
|
|
with (
|
|
patch("app.tasks.process_with_ocr.settings") as mock_settings,
|
|
patch("app.tasks.process_with_ocr.get_ocr_providers") as mock_providers,
|
|
patch(
|
|
"app.tasks.process_with_ocr.merge_ocr_results",
|
|
return_value=("", str(pdf_file), {}),
|
|
),
|
|
):
|
|
mock_settings.workdir = str(tmp_path)
|
|
mock_providers.return_value = [provider_mock]
|
|
|
|
result = process_with_ocr.run("test.pdf", file_id=1, original_text="Fallback original text")
|
|
|
|
assert result["cleaned_text"] == "Fallback original text"
|
|
|
|
@patch("app.tasks.process_with_ocr.log_task_progress")
|
|
@patch("app.tasks.process_with_ocr.rotate_pdf_pages")
|
|
def test_original_text_empty_skips_comparison(self, mock_rotate, mock_log, tmp_path):
|
|
"""When original_text is empty string, skip comparison and use OCR output."""
|
|
from app.tasks.process_with_ocr import process_with_ocr
|
|
|
|
pdf_file, provider_mock = self._setup_ocr_task(tmp_path, "Good OCR text")
|
|
mock_rotate.delay = Mock()
|
|
|
|
with (
|
|
patch("app.tasks.process_with_ocr.settings") as mock_settings,
|
|
patch("app.tasks.process_with_ocr.get_ocr_providers") as mock_providers,
|
|
patch(
|
|
"app.tasks.process_with_ocr.merge_ocr_results",
|
|
return_value=("Good OCR text", str(pdf_file), {}),
|
|
),
|
|
patch("app.tasks.process_with_ocr.compare_text_quality") as mock_compare,
|
|
):
|
|
mock_settings.workdir = str(tmp_path)
|
|
mock_providers.return_value = [provider_mock]
|
|
|
|
result = process_with_ocr.run("test.pdf", file_id=1, original_text="")
|
|
|
|
assert result["cleaned_text"] == "Good OCR text"
|
|
mock_compare.assert_not_called()
|
|
|
|
# Verify "skipped" was logged for compare_ocr_quality
|
|
skip_calls = [
|
|
c
|
|
for c in mock_log.call_args_list
|
|
if len(c[0]) >= 3 and c[0][1] == "compare_ocr_quality" and c[0][2] == "skipped"
|
|
]
|
|
assert len(skip_calls) == 1
|