"""Tests für die Wiederaufnahme abgebrochener Läufe aus working/. Hintergrund: `process_pdf()` verschiebt das Original vor dem OCR nach working/. Wird der Dienst dort hart gestoppt (SIGKILL nach TimeoutStopSec), blieb die Datei bisher für immer liegen — weder outgoing/, noch error/, noch eine Mail. ocrmypdf läuft in diesen Tests nie wirklich. """ from __future__ import annotations import logging from pathlib import Path from unittest.mock import patch from pdf_ocr_hotfolder.config import OcrConfig, OutputConfig, VeraPdfConfig from pdf_ocr_hotfolder.processor import OCR_TEMP_PREFIX, ProcessResult, process_pdf from pdf_ocr_hotfolder.service import HotfolderService def _fake_success(src: Path, working_dir, outgoing_dir, error_dir, **kwargs): """Simuliert einen erfolgreichen Durchlauf inkl. Entsorgung des Originals.""" out = outgoing_dir / f"OCR_{src.name}" out.parent.mkdir(parents=True, exist_ok=True) out.write_bytes(b"%PDF-1.4 ocr\n") src.unlink(missing_ok=True) return ProcessResult(src, out, True) def _run_once(tmp_config, fake_process=_fake_success): """run_once() mit gemocktem Preflight und gemocktem process_pdf.""" seen: list[Path] = [] def spy(src, *args, **kwargs): seen.append(src) return fake_process(src, *args, **kwargs) with patch("pdf_ocr_hotfolder.service.check_preflight", return_value=None), \ patch("pdf_ocr_hotfolder.service.process_pdf", side_effect=spy), \ patch("pdf_ocr_hotfolder.service._wait_until_stable", return_value=True): service = HotfolderService(tmp_config) try: service.run_once() finally: service._executor.shutdown(wait=False) return service, seen # ---------------- Aufgreifen aus working/ ---------------- def test_leftover_in_working_is_picked_up(tmp_config) -> None: """Eine in working/ liegen gebliebene PDF wird wieder verarbeitet.""" leftover = tmp_config.paths.working / "abgebrochen.pdf" leftover.write_bytes(b"%PDF-1.4\n") service, seen = _run_once(tmp_config) assert [p.name for p in seen] == ["abgebrochen.pdf"] assert seen[0].parent == tmp_config.paths.working assert service.success_count == 1 assert not leftover.exists() assert (tmp_config.paths.outgoing / "OCR_abgebrochen.pdf").exists() def test_resume_logs_warning(tmp_config, caplog) -> None: """Die Wiederaufnahme muss deutlich im Log stehen.""" (tmp_config.paths.working / "abgebrochen.pdf").write_bytes(b"%PDF-1.4\n") with caplog.at_level(logging.WARNING, logger="pdf_ocr_hotfolder.service"): _run_once(tmp_config) text = caplog.text assert "Abgebrochener Lauf wird fortgesetzt" in text assert "abgebrochen.pdf" in text def test_ocr_fragment_is_removed_and_not_processed(tmp_config, caplog) -> None: """__ocr_-Fragmente sind unbrauchbar: löschen, nicht als Eingabe nehmen.""" leftover = tmp_config.paths.working / "scan.pdf" leftover.write_bytes(b"%PDF-1.4\n") fragment = tmp_config.paths.working / f"{OCR_TEMP_PREFIX}OCR_scan.pdf" fragment.write_bytes(b"%PDF-1.4 halbfertig\n") with caplog.at_level(logging.WARNING, logger="pdf_ocr_hotfolder.service"): service, seen = _run_once(tmp_config) assert [p.name for p in seen] == ["scan.pdf"] assert not fragment.exists() assert service.error_count == 0 assert "Fragment" in caplog.text def test_non_pdf_in_working_is_ignored(tmp_config) -> None: """Fremddateien in working/ werden nicht angefasst.""" junk = tmp_config.paths.working / "notizen.txt" junk.write_text("kein PDF") _service, seen = _run_once(tmp_config) assert seen == [] assert junk.exists() def test_incoming_and_working_both_scanned(tmp_config) -> None: """incoming/ wird weiterhin gescannt — zusätzlich zu working/.""" (tmp_config.paths.incoming / "neu.pdf").write_bytes(b"%PDF-1.4\n") (tmp_config.paths.working / "alt.pdf").write_bytes(b"%PDF-1.4\n") service, seen = _run_once(tmp_config) assert sorted(p.name for p in seen) == ["alt.pdf", "neu.pdf"] assert service.success_count == 2 def test_name_collision_between_working_and_incoming(tmp_config, caplog) -> None: """Gleicher Name in beiden Ordnern: die working-Datei wird umbenannt. Sonst würden sich beide dieselbe working- und dieselbe outgoing-Datei teilen und eine der beiden ginge verloren. """ (tmp_config.paths.incoming / "scan.pdf").write_bytes(b"%PDF-1.4 neu\n") (tmp_config.paths.working / "scan.pdf").write_bytes(b"%PDF-1.4 alt\n") with caplog.at_level(logging.WARNING, logger="pdf_ocr_hotfolder.service"): service, seen = _run_once(tmp_config) names = sorted(p.name for p in seen) assert len(names) == 2 assert "scan.pdf" in names # Die wiederaufgenommene Datei hat einen Zeitstempel bekommen renamed = [n for n in names if n != "scan.pdf"][0] assert renamed.startswith("scan_") and renamed.endswith(".pdf") assert service.success_count == 2 assert "umbenannt" in caplog.text # ---------------- process_pdf: kein zweiter Move ---------------- def _ocr_ok(src: Path, dst: Path, cfg) -> None: dst.write_bytes(b"%PDF-1.4 ocr\n") def test_process_pdf_resumes_without_second_move(tmp_config) -> None: """Eine Datei aus working/ darf nicht erneut nach working/ verschoben werden.""" src = tmp_config.paths.working / "scan.pdf" src.write_bytes(b"%PDF-1.4\n") with patch("pdf_ocr_hotfolder.processor.run_ocr", side_effect=_ocr_ok): result = process_pdf( src=src, working_dir=tmp_config.paths.working, outgoing_dir=tmp_config.paths.outgoing, error_dir=tmp_config.paths.error, ocr_cfg=OcrConfig(), vera_cfg=VeraPdfConfig(enabled=False), output_cfg=OutputConfig(), ) assert result.success assert (tmp_config.paths.outgoing / "OCR_scan.pdf").exists() # Original entsorgt, keine Reste in working/ assert list(tmp_config.paths.working.iterdir()) == [] def test_process_pdf_resume_logs_warning(tmp_config, caplog) -> None: src = tmp_config.paths.working / "scan.pdf" src.write_bytes(b"%PDF-1.4\n") with caplog.at_level(logging.WARNING, logger="pdf_ocr_hotfolder.processor"), \ patch("pdf_ocr_hotfolder.processor.run_ocr", side_effect=_ocr_ok): process_pdf( src=src, working_dir=tmp_config.paths.working, outgoing_dir=tmp_config.paths.outgoing, error_dir=tmp_config.paths.error, ocr_cfg=OcrConfig(), vera_cfg=VeraPdfConfig(enabled=False), output_cfg=OutputConfig(), ) assert "Wiederaufnahme" in caplog.text def test_process_pdf_refuses_to_overwrite_working_file(tmp_config) -> None: """Belegter Name in working/: lieber Fehler als stilles Überschreiben.""" busy = tmp_config.paths.working / "scan.pdf" busy.write_bytes(b"%PDF-1.4 laeuft gerade\n") src = tmp_config.paths.incoming / "scan.pdf" src.write_bytes(b"%PDF-1.4 neu\n") with patch("pdf_ocr_hotfolder.processor.run_ocr", side_effect=_ocr_ok): result = process_pdf( src=src, working_dir=tmp_config.paths.working, outgoing_dir=tmp_config.paths.outgoing, error_dir=tmp_config.paths.error, ocr_cfg=OcrConfig(), vera_cfg=VeraPdfConfig(enabled=False), output_cfg=OutputConfig(), ) assert not result.success assert "scan.pdf" in result.error # Beide Dateien unangetastet assert busy.read_bytes() == b"%PDF-1.4 laeuft gerade\n" assert src.read_bytes() == b"%PDF-1.4 neu\n"