"""Punkt 2: error/ darf nichts mehr still überschreiben. Scheiterte dieselbe `scan.pdf` zweimal, ersetzte die zweite die erste in error/ — dieselbe Datenverlust-Klasse, die für outgoing/ bereits geschlossen ist. Betrifft `_move_to_error()` und damit auch `_rescue_to_error()`. """ 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 _move_to_error, process_pdf from pdf_ocr_hotfolder.service import HotfolderService ERSTE = b"%PDF-1.4 erste\n" ZWEITE = b"%PDF-1.4 zweite\n" # ---------------- _move_to_error direkt ---------------- def test_move_to_error_keeps_existing_file(tmp_path: Path) -> None: error_dir = tmp_path / "error" error_dir.mkdir() (error_dir / "scan.pdf").write_bytes(ERSTE) zweite = tmp_path / "scan.pdf" zweite.write_bytes(ZWEITE) _move_to_error(zweite, error_dir) assert (error_dir / "scan.pdf").read_bytes() == ERSTE ausweich = list(error_dir.glob("scan_*.pdf")) assert len(ausweich) == 1 assert ausweich[0].read_bytes() == ZWEITE assert not zweite.exists() def test_move_to_error_without_collision_keeps_name(tmp_path: Path) -> None: error_dir = tmp_path / "error" src = tmp_path / "scan.pdf" src.write_bytes(ERSTE) _move_to_error(src, error_dir) assert (error_dir / "scan.pdf").read_bytes() == ERSTE assert list(error_dir.iterdir()) == [error_dir / "scan.pdf"] def test_move_to_error_logs_warning_on_collision(tmp_path: Path, caplog) -> None: error_dir = tmp_path / "error" error_dir.mkdir() (error_dir / "scan.pdf").write_bytes(ERSTE) src = tmp_path / "scan.pdf" src.write_bytes(ZWEITE) with caplog.at_level(logging.WARNING, logger="pdf_ocr_hotfolder.processor"): _move_to_error(src, error_dir) assert "scan.pdf" in caplog.text assert "überschrieben" in caplog.text def test_move_to_error_creates_dir(tmp_path: Path) -> None: src = tmp_path / "scan.pdf" src.write_bytes(ERSTE) error_dir = tmp_path / "tief" / "error" _move_to_error(src, error_dir) assert (error_dir / "scan.pdf").exists() def test_move_to_error_survives_oserror(tmp_path: Path, caplog) -> None: """Ein fehlgeschlagener Move darf weiterhin nur geloggt werden.""" src = tmp_path / "scan.pdf" src.write_bytes(ERSTE) error_dir = tmp_path / "error" with patch("pdf_ocr_hotfolder.processor.shutil.move", side_effect=OSError("read-only")): _move_to_error(src, error_dir) # darf nicht werfen assert src.exists() # ---------------- über process_pdf: zweimal dieselbe Datei kaputt ---------------- def _prepare(tmp_path: Path) -> dict: dirs = {name: tmp_path / name for name in ("incoming", "working", "outgoing", "error")} for d in dirs.values(): d.mkdir(parents=True, exist_ok=True) return dirs def _run_failing_ocr(dirs: dict, src: Path): with patch("pdf_ocr_hotfolder.processor.run_ocr", side_effect=RuntimeError("ocr kaputt")): return process_pdf( src=src, working_dir=dirs["working"], outgoing_dir=dirs["outgoing"], error_dir=dirs["error"], ocr_cfg=OcrConfig(), vera_cfg=VeraPdfConfig(enabled=False), output_cfg=OutputConfig(), ) def test_same_name_failing_twice_keeps_both(tmp_path: Path) -> None: dirs = _prepare(tmp_path) for inhalt in (ERSTE, ZWEITE): src = dirs["incoming"] / "scan.pdf" src.write_bytes(inhalt) result = _run_failing_ocr(dirs, src) assert not result.success dateien = sorted(p.read_bytes() for p in dirs["error"].iterdir()) assert len(dateien) == 2 assert sorted([ERSTE, ZWEITE]) == dateien # ---------------- _rescue_to_error erbt den Schutz ---------------- def test_rescue_to_error_keeps_existing_file(tmp_config) -> None: """Der Rettungspfad nach einer unerwarteten Exception ebenso.""" (tmp_config.paths.error / "boom.pdf").write_bytes(ERSTE) src = tmp_config.paths.incoming / "boom.pdf" src.write_bytes(ZWEITE) service = HotfolderService(tmp_config) try: service._rescue_to_error(src) finally: service._executor.shutdown(wait=False) assert (tmp_config.paths.error / "boom.pdf").read_bytes() == ERSTE ausweich = list(tmp_config.paths.error.glob("boom_*.pdf")) assert len(ausweich) == 1 assert ausweich[0].read_bytes() == ZWEITE