fix: Fehlerzaehlung, Upload-Fehler und ocr.timeout scharf (v0.4.0)
- [ocr].timeout wird als tesseract_timeout (pro Seite) an ocrmypdf durchgereicht; Default 1800 -> 300, 0 = ocrmypdf-Default - Exceptions nach dem OCR zaehlen als Fehler, Datei wird nach error/ gerettet - Fehlgeschlagene Uploads zaehlen als Fehler und loesen Fehler-Mail aus - name_mode wird im Preflight geprueft, nicht erst pro Datei - Fehlende [paths]-Sektion -> ConfigError mit klarer Meldung statt KeyError - Stabilitaets-Timeout zaehlt als Fehler (--once liefert Exit 1) - upload_folder nutzt shutil.copyfile statt read_bytes/write_bytes - OcrConfig.pdfa_level Default "2" -> "" (Ghostscript-Bug, Issue #3) - 35 neue Tests (92 gesamt), pytest.ini - AI_AGENT_BRIEFING.md auf Stand 0.4.0 gebracht Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
"""Tests für verständliche Fehlermeldungen beim Laden der Config."""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from pdf_ocr_hotfolder.config import ConfigError, load_config
|
||||
|
||||
_FULL_PATHS = """
|
||||
[paths]
|
||||
incoming = "/tmp/in"
|
||||
outgoing = "/tmp/out"
|
||||
working = "/tmp/work"
|
||||
error = "/tmp/err"
|
||||
"""
|
||||
|
||||
|
||||
def _write(tmp_path: Path, content: str) -> Path:
|
||||
cfg = tmp_path / "config.toml"
|
||||
cfg.write_text(content)
|
||||
return cfg
|
||||
|
||||
|
||||
def test_missing_paths_section(tmp_path: Path) -> None:
|
||||
"""Fehlt [paths] komplett → ConfigError statt KeyError."""
|
||||
cfg = _write(tmp_path, '[ocr]\nlanguages = "deu"\n')
|
||||
with pytest.raises(ConfigError) as exc:
|
||||
load_config(cfg)
|
||||
msg = str(exc.value)
|
||||
assert "[paths]" in msg
|
||||
assert str(cfg) in msg
|
||||
|
||||
|
||||
def test_empty_config_file(tmp_path: Path) -> None:
|
||||
cfg = _write(tmp_path, "")
|
||||
with pytest.raises(ConfigError, match=r"\[paths\]"):
|
||||
load_config(cfg)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("missing", ["incoming", "outgoing", "working", "error"])
|
||||
def test_missing_single_path_key(tmp_path: Path, missing: str) -> None:
|
||||
"""Fehlt ein einzelner Key, wird genau dieser genannt."""
|
||||
lines = [line for line in _FULL_PATHS.strip().splitlines()
|
||||
if not line.startswith(missing)]
|
||||
cfg = _write(tmp_path, "\n".join(lines) + "\n")
|
||||
with pytest.raises(ConfigError) as exc:
|
||||
load_config(cfg)
|
||||
msg = str(exc.value)
|
||||
assert missing in msg
|
||||
assert str(cfg) in msg
|
||||
|
||||
|
||||
def test_empty_path_value_is_rejected(tmp_path: Path) -> None:
|
||||
"""Ein leerer Pfad ist genauso falsch wie ein fehlender."""
|
||||
cfg = _write(tmp_path, _FULL_PATHS.replace('working = "/tmp/work"',
|
||||
'working = ""'))
|
||||
with pytest.raises(ConfigError, match="working"):
|
||||
load_config(cfg)
|
||||
|
||||
|
||||
def test_complete_paths_section_loads(tmp_path: Path) -> None:
|
||||
cfg = _write(tmp_path, _FULL_PATHS)
|
||||
loaded = load_config(cfg)
|
||||
assert loaded.paths.incoming == Path("/tmp/in")
|
||||
assert loaded.paths.error == Path("/tmp/err")
|
||||
|
||||
|
||||
def test_main_returns_2_on_broken_config(tmp_path: Path, monkeypatch, capsys) -> None:
|
||||
"""CLI bricht sauber mit Exit-Code 2 ab — ohne Traceback."""
|
||||
cfg = _write(tmp_path, '[ocr]\nlanguages = "deu"\n')
|
||||
monkeypatch.setattr(sys, "argv",
|
||||
["pdf-ocr-hotfolder", "--config", str(cfg), "--once"])
|
||||
from pdf_ocr_hotfolder.__main__ import main
|
||||
assert main() == 2
|
||||
err = capsys.readouterr().err
|
||||
assert "FEHLER" in err
|
||||
assert "[paths]" in err
|
||||
@@ -0,0 +1,257 @@
|
||||
"""Tests für die Fehlerzählung im Service.
|
||||
|
||||
Deckt drei bisher stumme Fehlerpfade ab:
|
||||
- Exception aus `process_pdf()` (z.B. fehlgeschlagener Move nach outgoing/)
|
||||
- fehlgeschlagene Uploads
|
||||
- Timeout im Stabilitäts-Check
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from pdf_ocr_hotfolder.processor import ProcessResult
|
||||
from pdf_ocr_hotfolder.service import HotfolderService
|
||||
|
||||
|
||||
def _run_once(tmp_config, **patches):
|
||||
"""Führt run_once() mit gemocktem Preflight aus und gibt den Service zurück."""
|
||||
stack = [
|
||||
patch("pdf_ocr_hotfolder.service.check_preflight", return_value=None),
|
||||
patch("pdf_ocr_hotfolder.service._wait_until_stable",
|
||||
return_value=patches.pop("stable", True)),
|
||||
]
|
||||
for target, kwargs in patches.items():
|
||||
stack.append(patch(f"pdf_ocr_hotfolder.service.{target}", **kwargs))
|
||||
|
||||
service = HotfolderService(tmp_config)
|
||||
try:
|
||||
for p in stack:
|
||||
p.start()
|
||||
service.run_once()
|
||||
finally:
|
||||
for p in reversed(stack):
|
||||
p.stop()
|
||||
service._executor.shutdown(wait=False)
|
||||
return service
|
||||
|
||||
|
||||
# ---------------- Exception aus process_pdf ----------------
|
||||
|
||||
def test_exception_from_process_pdf_counts_as_error(tmp_config) -> None:
|
||||
"""Eine Exception aus process_pdf() darf die Zählung nicht umgehen."""
|
||||
(tmp_config.paths.incoming / "boom.pdf").write_bytes(b"%PDF-1.4\n")
|
||||
|
||||
def explode(src, **kwargs):
|
||||
raise OSError("move to outgoing failed")
|
||||
|
||||
service = _run_once(tmp_config, process_pdf={"side_effect": explode})
|
||||
|
||||
assert service.error_count == 1
|
||||
assert service.success_count == 0
|
||||
|
||||
|
||||
def test_exception_moves_file_to_error_dir(tmp_config) -> None:
|
||||
"""Die Datei landet nach einer Exception im error-Verzeichnis."""
|
||||
(tmp_config.paths.incoming / "boom.pdf").write_bytes(b"%PDF-1.4\n")
|
||||
|
||||
def explode(src, **kwargs):
|
||||
raise RuntimeError("kaputt")
|
||||
|
||||
_run_once(tmp_config, process_pdf={"side_effect": explode})
|
||||
|
||||
assert (tmp_config.paths.error / "boom.pdf").exists()
|
||||
assert not (tmp_config.paths.incoming / "boom.pdf").exists()
|
||||
|
||||
|
||||
def test_exception_after_move_to_working_rescues_from_working(tmp_config) -> None:
|
||||
"""Realistischer Fall: process_pdf hat schon nach working/ verschoben."""
|
||||
src = tmp_config.paths.incoming / "boom.pdf"
|
||||
src.write_bytes(b"%PDF-1.4\n")
|
||||
|
||||
def explode(src: Path, working_dir: Path, **kwargs):
|
||||
# process_pdf verschiebt zuerst nach working/, dann knallt der Move
|
||||
# nach outgoing/
|
||||
src.rename(working_dir / src.name)
|
||||
raise OSError("move to outgoing failed")
|
||||
|
||||
service = _run_once(tmp_config, process_pdf={"side_effect": explode})
|
||||
|
||||
assert service.error_count == 1
|
||||
assert (tmp_config.paths.error / "boom.pdf").exists()
|
||||
assert not (tmp_config.paths.working / "boom.pdf").exists()
|
||||
|
||||
|
||||
def test_exception_with_vanished_file_does_not_raise(tmp_config) -> None:
|
||||
"""Ist die Datei nicht mehr auffindbar, wird nur geloggt — kein Crash."""
|
||||
(tmp_config.paths.incoming / "boom.pdf").write_bytes(b"%PDF-1.4\n")
|
||||
|
||||
def explode(src: Path, **kwargs):
|
||||
src.unlink(missing_ok=True)
|
||||
raise RuntimeError("kaputt")
|
||||
|
||||
service = _run_once(tmp_config, process_pdf={"side_effect": explode})
|
||||
|
||||
assert service.error_count == 1
|
||||
assert not (tmp_config.paths.error / "boom.pdf").exists()
|
||||
|
||||
|
||||
def test_exception_triggers_error_notification(tmp_config) -> None:
|
||||
"""Auch bei einer Exception geht eine Fehler-Mail raus (success=False)."""
|
||||
(tmp_config.paths.incoming / "boom.pdf").write_bytes(b"%PDF-1.4\n")
|
||||
|
||||
def explode(src, **kwargs):
|
||||
raise RuntimeError("kaputt")
|
||||
|
||||
with patch("pdf_ocr_hotfolder.service.notify_email") as mail:
|
||||
_run_once(tmp_config, process_pdf={"side_effect": explode})
|
||||
|
||||
assert mail.call_count == 1
|
||||
args = mail.call_args[0]
|
||||
assert "FEHLER" in args[1]
|
||||
assert args[3] is False # success-Flag
|
||||
|
||||
|
||||
# ---------------- Upload-Fehler ----------------
|
||||
|
||||
def _fake_success(src: Path, working_dir, outgoing_dir, error_dir, **kwargs):
|
||||
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 test_failed_upload_counts_as_error(tmp_config) -> None:
|
||||
"""Ein fehlgeschlagener Upload zählt als Fehler, nicht als Erfolg."""
|
||||
(tmp_config.paths.incoming / "a.pdf").write_bytes(b"%PDF-1.4\n")
|
||||
|
||||
service = _run_once(
|
||||
tmp_config,
|
||||
process_pdf={"side_effect": _fake_success},
|
||||
upload_nextcloud={"return_value": False},
|
||||
)
|
||||
|
||||
assert service.error_count == 1
|
||||
assert service.success_count == 0
|
||||
|
||||
|
||||
def test_failed_upload_sends_error_mail_naming_targets(tmp_config) -> None:
|
||||
"""Die Fehler-Mail nennt die fehlgeschlagenen Ziele."""
|
||||
(tmp_config.paths.incoming / "a.pdf").write_bytes(b"%PDF-1.4\n")
|
||||
|
||||
with patch("pdf_ocr_hotfolder.service.notify_email") as mail:
|
||||
_run_once(
|
||||
tmp_config,
|
||||
process_pdf={"side_effect": _fake_success},
|
||||
upload_nextcloud={"return_value": False},
|
||||
upload_sftp={"return_value": False},
|
||||
)
|
||||
|
||||
assert mail.call_count == 1
|
||||
_cfg, subject, body, success = mail.call_args[0]
|
||||
assert "FEHLER" in subject
|
||||
assert success is False
|
||||
assert "nextcloud" in body
|
||||
assert "sftp" in body
|
||||
|
||||
|
||||
def test_failed_upload_keeps_pdf_in_outgoing(tmp_config) -> None:
|
||||
"""Das OCR war erfolgreich — die Datei bleibt in outgoing/, nicht error/."""
|
||||
(tmp_config.paths.incoming / "a.pdf").write_bytes(b"%PDF-1.4\n")
|
||||
|
||||
_run_once(
|
||||
tmp_config,
|
||||
process_pdf={"side_effect": _fake_success},
|
||||
upload_folder={"return_value": False},
|
||||
)
|
||||
|
||||
assert (tmp_config.paths.outgoing / "OCR_a.pdf").exists()
|
||||
assert not (tmp_config.paths.error / "OCR_a.pdf").exists()
|
||||
|
||||
|
||||
def test_successful_uploads_count_as_success(tmp_config) -> None:
|
||||
"""Gegenprobe: wenn alle Uploads durchgehen, zählt es als Erfolg."""
|
||||
(tmp_config.paths.incoming / "a.pdf").write_bytes(b"%PDF-1.4\n")
|
||||
|
||||
service = _run_once(tmp_config, process_pdf={"side_effect": _fake_success})
|
||||
|
||||
assert service.success_count == 1
|
||||
assert service.error_count == 0
|
||||
|
||||
|
||||
def test_dispatch_uploads_reports_failed_targets(tmp_config) -> None:
|
||||
"""_dispatch_uploads() liefert die Namen der fehlgeschlagenen Ziele."""
|
||||
service = HotfolderService(tmp_config)
|
||||
try:
|
||||
pdf = tmp_config.paths.outgoing / "x.pdf"
|
||||
pdf.write_bytes(b"%PDF-1.4\n")
|
||||
with patch("pdf_ocr_hotfolder.service.upload_folder", return_value=True), \
|
||||
patch("pdf_ocr_hotfolder.service.upload_nextcloud", return_value=False), \
|
||||
patch("pdf_ocr_hotfolder.service.upload_sftp", return_value=True):
|
||||
assert service._dispatch_uploads(pdf) == ["nextcloud"]
|
||||
|
||||
with patch("pdf_ocr_hotfolder.service.upload_folder", return_value=True), \
|
||||
patch("pdf_ocr_hotfolder.service.upload_nextcloud", return_value=True), \
|
||||
patch("pdf_ocr_hotfolder.service.upload_sftp", return_value=True):
|
||||
assert service._dispatch_uploads(pdf) == []
|
||||
finally:
|
||||
service._executor.shutdown(wait=False)
|
||||
|
||||
|
||||
# ---------------- Stabilitäts-Timeout ----------------
|
||||
|
||||
def test_unstable_file_counts_as_error(tmp_config) -> None:
|
||||
"""Stabilisiert sich eine Datei nicht, ist das ein Fehler (Exit 1)."""
|
||||
(tmp_config.paths.incoming / "slow.pdf").write_bytes(b"%PDF-1.4\n")
|
||||
|
||||
with patch("pdf_ocr_hotfolder.service.check_preflight", return_value=None), \
|
||||
patch("pdf_ocr_hotfolder.service._wait_until_stable", return_value=False), \
|
||||
patch("pdf_ocr_hotfolder.service.process_pdf") as proc:
|
||||
service = HotfolderService(tmp_config)
|
||||
try:
|
||||
errors = service.run_once()
|
||||
finally:
|
||||
service._executor.shutdown(wait=False)
|
||||
|
||||
assert errors == 1
|
||||
assert service.error_count == 1
|
||||
proc.assert_not_called()
|
||||
|
||||
|
||||
def test_unstable_file_stays_in_incoming(tmp_config) -> None:
|
||||
"""Die instabile Datei bleibt bewusst in incoming/ liegen."""
|
||||
(tmp_config.paths.incoming / "slow.pdf").write_bytes(b"%PDF-1.4\n")
|
||||
|
||||
with patch("pdf_ocr_hotfolder.service.check_preflight", return_value=None), \
|
||||
patch("pdf_ocr_hotfolder.service._wait_until_stable", return_value=False):
|
||||
service = HotfolderService(tmp_config)
|
||||
try:
|
||||
service.run_once()
|
||||
finally:
|
||||
service._executor.shutdown(wait=False)
|
||||
|
||||
assert (tmp_config.paths.incoming / "slow.pdf").exists()
|
||||
assert not (tmp_config.paths.error / "slow.pdf").exists()
|
||||
|
||||
|
||||
def test_vanished_file_is_not_an_error(tmp_config) -> None:
|
||||
"""Verschwundene Datei ist kein Fehler — _wait_until_stable liefert dafür
|
||||
ebenfalls False."""
|
||||
pdf = tmp_config.paths.incoming / "weg.pdf"
|
||||
pdf.write_bytes(b"%PDF-1.4\n")
|
||||
|
||||
def vanish(path: Path, **kwargs) -> bool:
|
||||
path.unlink(missing_ok=True)
|
||||
return False
|
||||
|
||||
with patch("pdf_ocr_hotfolder.service.check_preflight", return_value=None), \
|
||||
patch("pdf_ocr_hotfolder.service._wait_until_stable", side_effect=vanish):
|
||||
service = HotfolderService(tmp_config)
|
||||
try:
|
||||
errors = service.run_once()
|
||||
finally:
|
||||
service._executor.shutdown(wait=False)
|
||||
|
||||
assert errors == 0
|
||||
assert service.error_count == 0
|
||||
@@ -0,0 +1,81 @@
|
||||
"""Tests für [ocr].timeout → ocrmypdf `tesseract_timeout`.
|
||||
|
||||
ocrmypdf wird hier komplett gemockt (per sys.modules), es läuft also nie
|
||||
wirklich — die Tests laufen auch ohne installiertes ocrmypdf.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import tomllib
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
|
||||
import pytest
|
||||
|
||||
from pdf_ocr_hotfolder.config import OcrConfig
|
||||
from pdf_ocr_hotfolder.processor import run_ocr
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fake_ocrmypdf(monkeypatch) -> ModuleType:
|
||||
"""Schiebt ein Dummy-ocrmypdf in sys.modules und merkt sich die kwargs."""
|
||||
mod = ModuleType("ocrmypdf")
|
||||
mod.calls = [] # type: ignore[attr-defined]
|
||||
|
||||
def ocr(src, dst, **kwargs):
|
||||
mod.calls.append({"src": src, "dst": dst, "kwargs": kwargs}) # type: ignore[attr-defined]
|
||||
Path(dst).write_bytes(b"%PDF-1.4 ocr\n")
|
||||
|
||||
mod.ocr = ocr # type: ignore[attr-defined]
|
||||
monkeypatch.setitem(sys.modules, "ocrmypdf", mod)
|
||||
return mod
|
||||
|
||||
|
||||
def _run(fake, tmp_path: Path, cfg: OcrConfig) -> dict:
|
||||
src = tmp_path / "in.pdf"
|
||||
src.write_bytes(b"%PDF-1.4\n")
|
||||
run_ocr(src, tmp_path / "out.pdf", cfg)
|
||||
assert len(fake.calls) == 1
|
||||
return fake.calls[0]["kwargs"]
|
||||
|
||||
|
||||
def test_timeout_is_passed_as_tesseract_timeout(fake_ocrmypdf, tmp_path: Path) -> None:
|
||||
kwargs = _run(fake_ocrmypdf, tmp_path, OcrConfig(timeout=120))
|
||||
assert kwargs["tesseract_timeout"] == 120.0
|
||||
|
||||
|
||||
def test_timeout_zero_means_no_limit(fake_ocrmypdf, tmp_path: Path) -> None:
|
||||
"""0 = kein Limit → der Key darf NICHT durchgereicht werden.
|
||||
|
||||
ocrmypdf würde tesseract_timeout=0 als 'OCR überspringen' auslegen.
|
||||
"""
|
||||
kwargs = _run(fake_ocrmypdf, tmp_path, OcrConfig(timeout=0))
|
||||
assert "tesseract_timeout" not in kwargs
|
||||
|
||||
|
||||
def test_negative_timeout_is_ignored(fake_ocrmypdf, tmp_path: Path) -> None:
|
||||
kwargs = _run(fake_ocrmypdf, tmp_path, OcrConfig(timeout=-5))
|
||||
assert "tesseract_timeout" not in kwargs
|
||||
|
||||
|
||||
def test_default_timeout_is_passed(fake_ocrmypdf, tmp_path: Path) -> None:
|
||||
kwargs = _run(fake_ocrmypdf, tmp_path, OcrConfig())
|
||||
assert kwargs["tesseract_timeout"] == 300.0
|
||||
|
||||
|
||||
def test_other_kwargs_still_present(fake_ocrmypdf, tmp_path: Path) -> None:
|
||||
"""Der neue Key ersetzt nichts Bestehendes."""
|
||||
kwargs = _run(fake_ocrmypdf, tmp_path,
|
||||
OcrConfig(languages="deu", jobs=2, pdfa_level=""))
|
||||
assert kwargs["language"] == "deu"
|
||||
assert kwargs["jobs"] == 2
|
||||
assert kwargs["output_type"] == "pdf"
|
||||
assert kwargs["skip_text"] is True
|
||||
|
||||
|
||||
def test_config_default_matches_example(tmp_path: Path) -> None:
|
||||
"""Dataclass-Default und config.example.toml dürfen nicht auseinanderlaufen."""
|
||||
cfg_path = Path(__file__).parent.parent / "config.example.toml"
|
||||
with cfg_path.open("rb") as f:
|
||||
data = tomllib.load(f)
|
||||
assert data["ocr"]["timeout"] == OcrConfig().timeout == 300
|
||||
@@ -60,6 +60,62 @@ def test_check_output_config_invalid_mode() -> None:
|
||||
check_output_config("trash", "")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name_mode", ["prefix", "suffix", "none"])
|
||||
def test_check_output_config_accepts_valid_name_modes(name_mode) -> None:
|
||||
check_output_config("delete", "", name_mode) # ok
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name_mode", ["prefixx", "Prefix", "", "postfix"])
|
||||
def test_check_output_config_invalid_name_mode(name_mode) -> None:
|
||||
"""Tippfehler in name_mode muss schon im Preflight auffallen."""
|
||||
with pytest.raises(PreflightError, match="name_mode"):
|
||||
check_output_config("delete", "", name_mode)
|
||||
|
||||
|
||||
def test_run_once_aborts_on_invalid_name_mode(tmp_config) -> None:
|
||||
"""Der Dienst bricht beim Start ab, bevor eine Datei angefasst wird."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from pdf_ocr_hotfolder.service import HotfolderService
|
||||
|
||||
tmp_config.output.name_mode = "bogus"
|
||||
(tmp_config.paths.incoming / "a.pdf").write_bytes(b"%PDF-1.4\n")
|
||||
|
||||
service = HotfolderService(tmp_config)
|
||||
try:
|
||||
with patch("pdf_ocr_hotfolder.service.check_preflight", return_value=None):
|
||||
with pytest.raises(PreflightError, match="name_mode"):
|
||||
service.run_once()
|
||||
finally:
|
||||
service._executor.shutdown(wait=False)
|
||||
|
||||
# Datei wurde nicht angefasst
|
||||
assert (tmp_config.paths.incoming / "a.pdf").exists()
|
||||
|
||||
|
||||
def test_main_returns_2_on_invalid_name_mode(tmp_path: Path, monkeypatch) -> None:
|
||||
"""CLI liefert Exit-Code 2 — gleicher Mechanismus wie die übrigen Preflights."""
|
||||
import sys
|
||||
from unittest.mock import patch as _patch
|
||||
|
||||
cfg_file = tmp_path / "cfg.toml"
|
||||
cfg_file.write_text(f"""
|
||||
[paths]
|
||||
incoming = "{tmp_path / 'in'}"
|
||||
outgoing = "{tmp_path / 'out'}"
|
||||
working = "{tmp_path / 'work'}"
|
||||
error = "{tmp_path / 'err'}"
|
||||
|
||||
[output]
|
||||
name_mode = "bogus"
|
||||
""")
|
||||
monkeypatch.setattr(sys, "argv",
|
||||
["pdf-ocr-hotfolder", "--config", str(cfg_file), "--once"])
|
||||
with _patch("pdf_ocr_hotfolder.service.check_preflight", return_value=None):
|
||||
from pdf_ocr_hotfolder.__main__ import main
|
||||
assert main() == 2
|
||||
|
||||
|
||||
# ---------------- process_pdf mit Original-Behandlung ----------------
|
||||
|
||||
def _fake_ocr(src: Path, dst: Path, cfg: OcrConfig) -> None:
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
"""Tests für upload_folder() — Kopie per shutil.copyfile statt read_bytes()."""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from pdf_ocr_hotfolder.config import FolderUpload
|
||||
from pdf_ocr_hotfolder.uploaders import upload_folder
|
||||
|
||||
|
||||
def test_upload_folder_copies_file(tmp_path: Path) -> None:
|
||||
src = tmp_path / "out" / "OCR_scan.pdf"
|
||||
src.parent.mkdir()
|
||||
src.write_bytes(b"%PDF-1.4 inhalt\n")
|
||||
target = tmp_path / "ziel"
|
||||
|
||||
assert upload_folder(src, FolderUpload(enabled=True, target=str(target)),
|
||||
tmp_path / "out") is True
|
||||
assert (target / "OCR_scan.pdf").read_bytes() == b"%PDF-1.4 inhalt\n"
|
||||
# Quelle bleibt liegen (Kopie, kein Move)
|
||||
assert src.exists()
|
||||
|
||||
|
||||
def test_upload_folder_uses_copyfile_not_read_bytes(tmp_path: Path) -> None:
|
||||
"""Große PDFs dürfen nicht komplett in den Speicher gelesen werden."""
|
||||
src = tmp_path / "out" / "OCR_scan.pdf"
|
||||
src.parent.mkdir()
|
||||
src.write_bytes(b"%PDF-1.4\n")
|
||||
target = tmp_path / "ziel"
|
||||
|
||||
with patch("pdf_ocr_hotfolder.uploaders.shutil.copyfile") as copyfile:
|
||||
upload_folder(src, FolderUpload(enabled=True, target=str(target)),
|
||||
tmp_path / "out")
|
||||
copyfile.assert_called_once()
|
||||
|
||||
|
||||
def test_upload_folder_skips_self_target(tmp_path: Path) -> None:
|
||||
"""Ist das Ziel = outgoing, wird nicht auf sich selbst kopiert."""
|
||||
out = tmp_path / "out"
|
||||
out.mkdir()
|
||||
src = out / "OCR_scan.pdf"
|
||||
src.write_bytes(b"%PDF-1.4\n")
|
||||
|
||||
with patch("pdf_ocr_hotfolder.uploaders.shutil.copyfile") as copyfile:
|
||||
assert upload_folder(src, FolderUpload(enabled=True, target=""), out) is True
|
||||
copyfile.assert_not_called()
|
||||
assert src.read_bytes() == b"%PDF-1.4\n"
|
||||
|
||||
|
||||
def test_upload_folder_disabled_returns_true(tmp_path: Path) -> None:
|
||||
src = tmp_path / "OCR_scan.pdf"
|
||||
src.write_bytes(b"%PDF-1.4\n")
|
||||
assert upload_folder(src, FolderUpload(enabled=False), tmp_path) is True
|
||||
|
||||
|
||||
def test_upload_folder_reports_failure(tmp_path: Path) -> None:
|
||||
"""OSError beim Kopieren → False (wird vom Service als Fehler gezählt)."""
|
||||
src = tmp_path / "out" / "OCR_scan.pdf"
|
||||
src.parent.mkdir()
|
||||
src.write_bytes(b"%PDF-1.4\n")
|
||||
|
||||
with patch("pdf_ocr_hotfolder.uploaders.shutil.copyfile",
|
||||
side_effect=OSError("disk full")):
|
||||
assert upload_folder(src, FolderUpload(enabled=True,
|
||||
target=str(tmp_path / "ziel")),
|
||||
tmp_path / "out") is False
|
||||
Reference in New Issue
Block a user