3e24aa2ecd
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>
199 lines
5.9 KiB
Python
199 lines
5.9 KiB
Python
"""Tests für Legacy-Warnungen und unbekannte Config-Keys.
|
|
|
|
Zwei stille Fallen:
|
|
- [ocr].timeout bedeutet seit 0.4.0 Sekunden pro SEITE (vorher Gesamtlauf)
|
|
- unbekannte Keys (Tippfehler!) wurden beim Laden stumm verworfen
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import sys
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
from pdf_ocr_hotfolder.config import (
|
|
config_warnings,
|
|
legacy_warnings,
|
|
load_config,
|
|
unknown_key_warnings,
|
|
)
|
|
|
|
_PATHS = """
|
|
[paths]
|
|
incoming = "/tmp/in"
|
|
outgoing = "/tmp/out"
|
|
working = "/tmp/work"
|
|
error = "/tmp/err"
|
|
"""
|
|
|
|
|
|
def _write(tmp_path: Path, extra: str = "") -> Path:
|
|
cfg = tmp_path / "config.toml"
|
|
cfg.write_text(_PATHS + extra)
|
|
return cfg
|
|
|
|
|
|
# ---------------- Legacy: [ocr].timeout ----------------
|
|
|
|
def test_legacy_timeout_warns(tmp_path: Path) -> None:
|
|
"""Ein Altwert (Gesamt-Timeout 1800) muss deutlich benannt werden."""
|
|
cfg = load_config(_write(tmp_path, "\n[ocr]\ntimeout = 1800\n"))
|
|
warnings = legacy_warnings(cfg)
|
|
assert len(warnings) == 1
|
|
assert "timeout" in warnings[0]
|
|
assert "PRO SEITE" in warnings[0]
|
|
assert "300" in warnings[0]
|
|
|
|
|
|
def test_timeout_at_threshold_warns(tmp_path: Path) -> None:
|
|
cfg = load_config(_write(tmp_path, "\n[ocr]\ntimeout = 900\n"))
|
|
assert legacy_warnings(cfg)
|
|
|
|
|
|
def test_sane_timeout_does_not_warn(tmp_path: Path) -> None:
|
|
cfg = load_config(_write(tmp_path, "\n[ocr]\ntimeout = 300\n"))
|
|
assert legacy_warnings(cfg) == []
|
|
|
|
|
|
def test_default_config_has_no_warnings(tmp_path: Path) -> None:
|
|
cfg = load_config(_write(tmp_path))
|
|
assert config_warnings(cfg) == []
|
|
|
|
|
|
# ---------------- Legacy: [ocr].pdfa_level ----------------
|
|
|
|
def test_pdfa_level_warns_about_ghostscript(tmp_path: Path) -> None:
|
|
cfg = load_config(_write(tmp_path, '\n[ocr]\npdfa_level = "2"\n'))
|
|
warnings = legacy_warnings(cfg)
|
|
assert len(warnings) == 1
|
|
assert "pdfa_level" in warnings[0]
|
|
assert "Ghostscript" in warnings[0]
|
|
assert "10.02.0" in warnings[0]
|
|
|
|
|
|
def test_empty_pdfa_level_does_not_warn(tmp_path: Path) -> None:
|
|
cfg = load_config(_write(tmp_path, '\n[ocr]\npdfa_level = ""\n'))
|
|
assert legacy_warnings(cfg) == []
|
|
|
|
|
|
def test_both_legacy_warnings_together(tmp_path: Path) -> None:
|
|
cfg = load_config(_write(tmp_path, '\n[ocr]\ntimeout = 1800\npdfa_level = "1"\n'))
|
|
assert len(legacy_warnings(cfg)) == 2
|
|
|
|
|
|
# ---------------- Unbekannte Keys ----------------
|
|
|
|
def test_typo_key_is_collected(tmp_path: Path) -> None:
|
|
"""`langauges` statt `languages` darf nicht mehr stumm verschwinden."""
|
|
cfg = load_config(_write(tmp_path, '\n[ocr]\nlangauges = "deu"\n'))
|
|
assert cfg.unknown_keys == ["[ocr].langauges"]
|
|
assert "[ocr].langauges" in unknown_key_warnings(cfg)[0]
|
|
# Der Rest wird weiterhin normal geladen
|
|
assert cfg.ocr.languages == "deu+eng"
|
|
|
|
|
|
def test_known_keys_are_not_reported(tmp_path: Path) -> None:
|
|
cfg = load_config(_write(tmp_path, '\n[ocr]\nlanguages = "deu"\njobs = 2\n'))
|
|
assert cfg.unknown_keys == []
|
|
assert cfg.ocr.languages == "deu"
|
|
|
|
|
|
def test_unknown_keys_in_all_sections(tmp_path: Path) -> None:
|
|
cfg = load_config(_write(tmp_path, """
|
|
[ocr]
|
|
foo = 1
|
|
|
|
[output]
|
|
bar = "x"
|
|
|
|
[verapdf]
|
|
baz = true
|
|
|
|
[upload.folder]
|
|
qux = ""
|
|
|
|
[upload.nextcloud]
|
|
quux = ""
|
|
|
|
[upload.sftp]
|
|
corge = 0
|
|
|
|
[notify.email]
|
|
grault = ""
|
|
|
|
[logging]
|
|
level = "INFO"
|
|
garply = 1
|
|
"""))
|
|
assert cfg.unknown_keys == [
|
|
"[ocr].foo", "[output].bar", "[verapdf].baz",
|
|
"[upload.folder].qux", "[upload.nextcloud].quux", "[upload.sftp].corge",
|
|
"[notify.email].grault", "[logging].garply",
|
|
]
|
|
|
|
|
|
def test_unknown_top_level_key_is_reported(tmp_path: Path) -> None:
|
|
"""Ein Key ausserhalb jeder Sektion (z.B. vergessene Sektionszeile)."""
|
|
cfg_file = tmp_path / "config.toml"
|
|
cfg_file.write_text('log_level = "DEBUG"\n' + _PATHS)
|
|
cfg = load_config(cfg_file)
|
|
assert cfg.unknown_keys == ["log_level"]
|
|
|
|
|
|
def test_unknown_section_is_reported(tmp_path: Path) -> None:
|
|
cfg = load_config(_write(tmp_path, '\n[ocrr]\nlanguages = "deu"\n\n[upload.ftp]\nhost = "x"\n'))
|
|
assert "[ocrr]" in cfg.unknown_keys
|
|
assert "[upload.ftp]" in cfg.unknown_keys
|
|
|
|
|
|
def test_unknown_key_inside_paths(tmp_path: Path) -> None:
|
|
"""Ein zusätzlicher Key direkt in [paths] wird ebenfalls gemeldet."""
|
|
cfg_file = tmp_path / "config.toml"
|
|
cfg_file.write_text(_PATHS + 'archive = "/tmp/a"\n')
|
|
cfg = load_config(cfg_file)
|
|
assert cfg.unknown_keys == ["[paths].archive"]
|
|
|
|
|
|
def test_load_config_works_without_logging(tmp_path: Path) -> None:
|
|
"""load_config() darf nichts loggen müssen — Tests rufen sie direkt auf."""
|
|
cfg_file = _write(tmp_path, '\n[ocr]\nlangauges = "deu"\n')
|
|
with patch("logging.Logger.warning") as warn:
|
|
cfg = load_config(cfg_file)
|
|
warn.assert_not_called()
|
|
assert cfg.unknown_keys
|
|
|
|
|
|
# ---------------- Warnungen beim Dienststart ----------------
|
|
|
|
def _argv(monkeypatch, cfg_file: Path, *extra: str) -> None:
|
|
monkeypatch.setattr(sys, "argv",
|
|
["pdf-ocr-hotfolder", "--config", str(cfg_file), *extra])
|
|
|
|
|
|
def test_warnings_are_logged_on_service_start(tmp_path: Path, tmp_config,
|
|
monkeypatch, caplog) -> None:
|
|
"""Beim normalen Start landen die Warnungen im Log (nicht nur im Check)."""
|
|
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}"
|
|
|
|
[ocr]
|
|
timeout = 1800
|
|
langauges = "deu"
|
|
""")
|
|
_argv(monkeypatch, cfg_file, "--once")
|
|
|
|
from pdf_ocr_hotfolder.__main__ import main
|
|
|
|
with caplog.at_level(logging.WARNING, logger="pdf_ocr_hotfolder.__main__"), \
|
|
patch("pdf_ocr_hotfolder.service.shutil.which", return_value="/usr/bin/fake"):
|
|
assert main() == 0
|
|
|
|
text = caplog.text
|
|
assert "PRO SEITE" in text
|
|
assert "[ocr].langauges" in text
|