Files
techadmin 3e24aa2ecd feat: Wiederaufnahme aus working/, --check-config, feldtauglicher Updater (v0.6.0)
Datenverlust behoben:
- Nach hartem Stopp blieb das Original in working/ liegen und wurde nie
  wieder angefasst (_scan_existing sah nur incoming/). Es wird jetzt beim
  Start an Ort und Stelle wieder aufgegriffen, mit Kollisionsschutz gegen
  gleichnamige neue Scans; angefangene __ocr_-Fragmente werden geloescht.
- TimeoutStopSec 30 -> 300, damit laufendes OCR zu Ende laufen darf.

Config-Drift sichtbar gemacht:
- Neues --check-config (Exit 0 sauber / 1 Warnungen / 2 Fehler), das
  update.sh vor dem Neustart ueber alle Instanz-Configs laufen laesst.
- Warnungen fuer [ocr].timeout >= 900 (seit 0.4.0 pro SEITE) und gesetztes
  pdfa_level, beim Dienststart wie im Check.
- Unbekannte Config-Keys werden nicht mehr still verworfen, sondern genannt.

Updater feldtauglich:
- venv-Health-Check erkennt toten Symlink UND Versions-Drift gegen das
  System-Python; --rebuild-venv als ausdruecklicher Weg nach einem Debian-
  Major-Upgrade. Neubau ist ganz-oder-gar-nicht mit Rollback.
- apt-Pakete werden auch beim Update synchronisiert (Quelle: install.sh).
- Instanz-Erfassung inkl. activating/failed, Verifikation prueft is-failed
  und NRestarts statt sleep 1 + is-active.
- Backup enthaelt Configs, Unit, Drop-ins und pip-freeze.txt, liegt auf
  0600 und rotiert auf 5; schlaegt es fehl, bricht das Update vorher ab.
- ERR-Trap faehrt die vorher laufenden Instanzen wieder hoch.
- lxc-compat.conf wird beim Update nachgezogen.
- requirements.txt gepinnt (ocrmypdf 16.13.0, geprueft fuer Python 3.11+3.13).

Doku in Installation / Update / OS-Upgrade aufgeteilt (docs/).
135 Tests gruen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-22 22:04:04 +02:00

166 lines
6.3 KiB
Python

"""Tests für `--check-config`.
Die Exit-Codes werden vom Updater ausgewertet und müssen verlässlich sein:
0 = sauber, 1 = nur Warnungen, 2 = Fehler.
"""
from __future__ import annotations
import sys
from pathlib import Path
from unittest.mock import patch
import pytest
from pdf_ocr_hotfolder.__main__ import CHECK_ERROR, CHECK_OK, CHECK_WARN, main
def _cfg_file(tmp_path: Path, tmp_config, extra: str = "") -> Path:
cfg_file = tmp_path / "cfg.toml"
cfg_file.write_text(f"""
[paths]
incoming = "{tmp_config.paths.incoming}"
outgoing = "{tmp_config.paths.outgoing}"
working = "{tmp_config.paths.working}"
error = "{tmp_config.paths.error}"
""" + extra)
return cfg_file
def _check(monkeypatch, cfg_file: Path, binaries_present: bool = True) -> int:
monkeypatch.setattr(sys, "argv",
["pdf-ocr-hotfolder", "--config", str(cfg_file),
"--check-config"])
which = "/usr/bin/fake" if binaries_present else None
with patch("pdf_ocr_hotfolder.service.shutil.which", return_value=which):
return main()
# ---------------- Exit 0 ----------------
def test_clean_config_returns_0(tmp_path, tmp_config, monkeypatch, capsys) -> None:
rc = _check(monkeypatch, _cfg_file(tmp_path, tmp_config))
assert rc == CHECK_OK
out = capsys.readouterr().out
assert "Config sauber" in out
def test_check_does_not_process_files(tmp_path, tmp_config, monkeypatch) -> None:
"""Der Check darf nichts verarbeiten und nichts verschieben."""
pdf = tmp_config.paths.incoming / "scan.pdf"
pdf.write_bytes(b"%PDF-1.4\n")
assert _check(monkeypatch, _cfg_file(tmp_path, tmp_config)) == CHECK_OK
assert pdf.exists()
assert list(tmp_config.paths.outgoing.iterdir()) == []
# ---------------- Exit 1 (nur Warnungen) ----------------
def test_legacy_timeout_returns_1(tmp_path, tmp_config, monkeypatch, capsys) -> None:
rc = _check(monkeypatch, _cfg_file(tmp_path, tmp_config,
"\n[ocr]\ntimeout = 1800\n"))
assert rc == CHECK_WARN
out = capsys.readouterr().out
assert "WARNUNG" in out
assert "PRO SEITE" in out
def test_unknown_key_returns_1(tmp_path, tmp_config, monkeypatch, capsys) -> None:
rc = _check(monkeypatch, _cfg_file(tmp_path, tmp_config,
'\n[ocr]\nlangauges = "deu"\n'))
assert rc == CHECK_WARN
assert "[ocr].langauges" in capsys.readouterr().out
def test_pdfa_level_with_healthy_ghostscript_returns_1(
tmp_path, tmp_config, monkeypatch, capsys) -> None:
"""Gesundes Ghostscript: nur Hinweis (Exit 1), kein Abbruch."""
monkeypatch.setattr(sys, "argv",
["pdf-ocr-hotfolder", "--config",
str(_cfg_file(tmp_path, tmp_config,
'\n[ocr]\npdfa_level = "2"\n')),
"--check-config"])
with patch("pdf_ocr_hotfolder.service.shutil.which", return_value="/usr/bin/fake"), \
patch("pdf_ocr_hotfolder.service.detect_ghostscript_version",
return_value="10.02.1"):
rc = main()
assert rc == CHECK_WARN
assert "Ghostscript" in capsys.readouterr().out
# ---------------- Exit 2 (Fehler) ----------------
def test_missing_config_file_returns_2(tmp_path, monkeypatch, capsys) -> None:
monkeypatch.setattr(sys, "argv",
["pdf-ocr-hotfolder", "--config",
str(tmp_path / "gibtsnicht.toml"), "--check-config"])
assert main() == CHECK_ERROR
assert "nicht gefunden" in capsys.readouterr().err
def test_broken_paths_section_returns_2(tmp_path, monkeypatch, capsys) -> None:
cfg_file = tmp_path / "cfg.toml"
cfg_file.write_text('[ocr]\nlanguages = "deu"\n')
assert _check(monkeypatch, cfg_file) == CHECK_ERROR
assert "[paths]" in capsys.readouterr().err
def test_invalid_toml_returns_2(tmp_path, monkeypatch, capsys) -> None:
"""Kaputtes TOML: saubere Meldung statt Traceback."""
cfg_file = tmp_path / "cfg.toml"
cfg_file.write_text("[paths\nincoming = ")
assert _check(monkeypatch, cfg_file) == CHECK_ERROR
assert "TOML" in capsys.readouterr().err
def test_missing_binaries_return_2(tmp_path, tmp_config, monkeypatch, capsys) -> None:
rc = _check(monkeypatch, _cfg_file(tmp_path, tmp_config),
binaries_present=False)
assert rc == CHECK_ERROR
err = capsys.readouterr().err
assert "tesseract" in err
def test_invalid_name_mode_returns_2(tmp_path, tmp_config, monkeypatch, capsys) -> None:
rc = _check(monkeypatch, _cfg_file(tmp_path, tmp_config,
'\n[output]\nname_mode = "praefix"\n'))
assert rc == CHECK_ERROR
assert "name_mode" in capsys.readouterr().err
def test_archive_without_dir_returns_2(tmp_path, tmp_config, monkeypatch, capsys) -> None:
rc = _check(monkeypatch, _cfg_file(tmp_path, tmp_config,
'\n[output]\noriginal_on_success = "archive"\n'))
assert rc == CHECK_ERROR
assert "archive_dir" in capsys.readouterr().err
def test_error_beats_warning(tmp_path, tmp_config, monkeypatch) -> None:
"""Fehler + Warnung → Exit 2, nicht 1."""
rc = _check(monkeypatch,
_cfg_file(tmp_path, tmp_config,
'\n[ocr]\ntimeout = 1800\n\n[output]\nname_mode = "x"\n'))
assert rc == CHECK_ERROR
# ---------------- Zusammenspiel mit anderen Optionen ----------------
def test_check_config_wins_over_once(tmp_path, tmp_config, monkeypatch) -> None:
"""--check-config hat Vorrang: es wird nichts verarbeitet."""
pdf = tmp_config.paths.incoming / "scan.pdf"
pdf.write_bytes(b"%PDF-1.4\n")
monkeypatch.setattr(sys, "argv",
["pdf-ocr-hotfolder", "--config",
str(_cfg_file(tmp_path, tmp_config)),
"--once", "--check-config"])
with patch("pdf_ocr_hotfolder.service.shutil.which", return_value="/usr/bin/fake"):
assert main() == CHECK_OK
assert pdf.exists()
@pytest.mark.parametrize("code,expected", [(CHECK_OK, 0), (CHECK_WARN, 1),
(CHECK_ERROR, 2)])
def test_exit_code_constants(code: int, expected: int) -> None:
"""Die Konstanten sind Teil der Schnittstelle zum Updater."""
assert code == expected