cd803a3dfe
Vor dem Rollout durchgesehen und die verbliebenen Stellen geschlossen, an denen etwas schiefgehen konnte, ohne dass es irgendwo sichtbar wurde. Datenverlust: - veraPDF: das in [verapdf].binary konfigurierte Programm wird im Preflight geprueft. Bisher galt bei falschem Pfad JEDE Datei als "nicht konform" — Ergebnis nach error/, Original geloescht (Default delete). run_verapdf() trennt jetzt ausserdem ein echtes FAIL-Urteil von einer Stoerung (VeraPdfUnavailable: nicht startbar, abgestuerzt, kein PASS/FAIL in der Ausgabe). Bei Stoerung wandern Original UND Ergebnis nach error/, das Original wird nicht entsorgt. - Gleichnamige Dateien wurden in outgoing/, error/ und beim Ordner-Upload mit abweichendem target kommentarlos ueberschrieben. Jetzt Zeitstempel daneben, mit Warnung; ProcessResult.output traegt den echten Pfad. Robustheit: - Kaputtes oder nicht lesbares TOML beim Start: Exit 2 statt Traceback. - RestartPreventExitStatus=2 in der Unit — Exit 2 (Config/Preflight) laeuft nicht mehr endlos neu, die Instanz bleibt sichtbar failed stehen. - Toter watchdog-Observer wird erkannt: Exit 3, systemd setzt den Watch neu auf. Vorher blieb die Unit "active" und verarbeitete nichts mehr. - Relative Pfade in [paths]/archive_dir/target sind ein Config-Fehler statt still unter /opt zu landen. - Fehler beim Archivieren entwertet den Durchlauf nicht mehr: Upload und Mail laufen, Sichtbarkeit ueber log.error + "OK mit Warnung"-Mail. - Nicht-PDFs in incoming/ werden beim Start-Scan gesammelt gemeldet. - Logging explizit nach stdout (die Doku versprach das schon). Struktur: - Neue lib/common.sh, von install.sh und update.sh gesourct. Die doppelte venv_is_healthy() gibt es nur noch einmal, in der gruendlichen Fassung — die schlanke in install.sh haette eine nach einem Distro-Sprung kaputte venv als gesund durchgewunken (nachgewiesen). - install.sh warnt in Containern, wenn systemd-journald nicht laeuft. Doku: Dateisystem-Festlegung (ext4/xfs/zfs, kein CIFS/NFS wegen inotify), Debian 13 in LXC auf Proxmox scheitert an journald (243/CREDENTIALS, AppArmor blockiert sd-mkdcreds) inkl. Abhilfe, echte Speicher-Messwerte, Exit-Code-Tabelle. 254 Tests gruen (vorher 152). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
141 lines
4.5 KiB
Python
141 lines
4.5 KiB
Python
"""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
|