465ff8873f
Befunde aus dem ersten echten Erstinstallations-Test auf frischen Debian-12- und Debian-13-Containern. Der Weg selbst hat getragen (Basis-Install, Instanz-Anlage, zweite Instanz, Update mit Rauchtest, Rollback) — diese Stellen haben gelogen oder gefehlt: - Das Ghostscript-Backports-Angebot auf Debian 12 war ein garantierter Leerlauf, der "aktualisiert ✓" meldete: bookworm-backports enthaelt gar kein ghostscript (am Paketindex verifiziert). Die Routine sucht jetzt den echten Kandidaten, vergleicht vorher/nachher und raeumt eine nur zur Probe angelegte Quelle wieder weg. - Derselbe untaugliche Rat stand in der Preflight-Meldung, der pdfa_level-Warnung, config.example.toml und vier Doku-Dateien — ueberall ersetzt durch die echten Optionen. - Mehrzeilige Log-Hinweise waren durch "echo -e" zerrissen und nicht kopierbar; log_* nutzt jetzt printf mit %s. - pip-freeze.txt landete beim Rollback als /pip-freeze.txt im Wurzelverzeichnis, liegt jetzt unter opt/pdf-ocr-hotfolder/. - git und sudo fehlen auf dem Proxmox-Debian-Template; "sudo ./install.sh" scheitert dort. Beide Wege dokumentiert, git als Voraussetzung ergaenzt, HTTPS-Clone als Normalfall. - Rollback: systemctl start kann kein Glob. journald-Reparatur: Instanzen danach neu starten, sonst bleibt das Journal leer. 254 Tests gruen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
228 lines
8.4 KiB
Python
228 lines
8.4 KiB
Python
"""Tests für Issue #3: Ghostscript 10.0.0–10.02.0 Bug-Erkennung.
|
||
|
||
Seit v0.6.1 bildet der Preflight die reale ocrmypdf-Bedingung ab:
|
||
|
||
betroffene GS-Version UND skip_text UND (pdfa_level ODER ocrmypdf < 17)
|
||
|
||
Der letzte Teil ist der Fall, der in v0.6.0 durchrutschte: mit ocrmypdf 16.x
|
||
greift die Ghostscript-Prüfung auch ohne PDF/A, und der Dienst meldete
|
||
trotzdem "Preflight ok", während jede einzelne PDF in error/ landete.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from contextlib import contextmanager
|
||
from unittest.mock import patch
|
||
|
||
import pytest
|
||
|
||
from pdf_ocr_hotfolder.service import (
|
||
PreflightError,
|
||
check_preflight,
|
||
is_ghostscript_broken,
|
||
ocrmypdf_checks_gs_always,
|
||
)
|
||
|
||
|
||
@contextmanager
|
||
def _env(gs_version: str, ocrmypdf_version: str):
|
||
"""Binaries vorhanden, Ghostscript- und ocrmypdf-Version vorgegeben."""
|
||
with patch("pdf_ocr_hotfolder.service.shutil.which", return_value="/usr/bin/fake"), \
|
||
patch("pdf_ocr_hotfolder.service.detect_ghostscript_version",
|
||
return_value=gs_version), \
|
||
patch("pdf_ocr_hotfolder.service.detect_ocrmypdf_version",
|
||
return_value=ocrmypdf_version):
|
||
yield
|
||
|
||
|
||
@pytest.mark.parametrize("version,expected", [
|
||
# Betroffene Versionen
|
||
("10.0.0", True),
|
||
("10.00.0", True),
|
||
("10.01.0", True),
|
||
("10.01.1", True),
|
||
("10.01.2", True),
|
||
("10.02.0", True),
|
||
# Sichere Versionen
|
||
("10.02.1", False),
|
||
("10.03.0", False),
|
||
("10.04.0", False),
|
||
("11.0.0", False),
|
||
("9.56.1", False), # Debian 11 / Ubuntu 22.04
|
||
("9.55.0", False),
|
||
# Edge cases
|
||
("", False),
|
||
(None, False),
|
||
("garbage", False),
|
||
])
|
||
def test_is_ghostscript_broken(version, expected) -> None:
|
||
assert is_ghostscript_broken(version) is expected
|
||
|
||
|
||
@pytest.mark.parametrize("version,expected", [
|
||
("16.13.0", True), # der Pin aus v0.6.0, der den Ausfall ausgeloest hat
|
||
("16.0.0", True),
|
||
("15.4.4", True),
|
||
("17.0.0", False), # ab hier steckt die Pruefung hinter output_type
|
||
("17.4.1", False), # unser Pin
|
||
("17.12.1", False),
|
||
("18.0.0", False),
|
||
(None, False), # unbekannt -> kein Fehlalarm
|
||
("", False),
|
||
("garbage", False),
|
||
])
|
||
def test_ocrmypdf_checks_gs_always(version, expected) -> None:
|
||
assert ocrmypdf_checks_gs_always(version) is expected
|
||
|
||
|
||
# ---------------- Preflight: ocrmypdf 17.x (unser Pin) ----------------
|
||
|
||
def test_broken_gs_skip_text_without_pdfa_passes_on_ocrmypdf_17() -> None:
|
||
"""Der Debian-12-Standardfall: ohne PDF/A fasst ocrmypdf 17 gs nicht an.
|
||
|
||
Das ist die Default-Config (skip_text=true, pdfa_level="") auf Debian 12 —
|
||
sie muss laufen, sonst startet keine einzige Bestandsinstanz mehr.
|
||
"""
|
||
with _env("10.0.0", "17.4.1"):
|
||
check_preflight(pdfa_level="", skip_text=True) # darf nicht werfen
|
||
|
||
|
||
def test_broken_gs_with_pdfa_and_skip_text_fails() -> None:
|
||
"""Mit pdfa_level + skip_text + kaputtem GS → PreflightError."""
|
||
with _env("10.0.0", "17.4.1"):
|
||
with pytest.raises(PreflightError, match="Ghostscript 10.0.0"):
|
||
check_preflight(pdfa_level="2", skip_text=True)
|
||
|
||
|
||
def test_broken_gs_with_pdfa_without_skip_text_passes() -> None:
|
||
"""Ohne skip_text greift die ocrmypdf-Bedingung nicht — kein Abbruch."""
|
||
with _env("10.0.0", "17.4.1"):
|
||
check_preflight(pdfa_level="2", skip_text=False) # darf nicht werfen
|
||
|
||
|
||
def test_healthy_gs_with_pdfa_and_skip_text_passes() -> None:
|
||
"""Nicht betroffene GS-Version → nie ein Abbruch."""
|
||
with _env("10.02.1", "17.4.1"):
|
||
check_preflight(pdfa_level="2", skip_text=True) # darf nicht werfen
|
||
|
||
|
||
# ---------------- Preflight: ocrmypdf 16.x (der Ausfall aus v0.6.0) ----------------
|
||
|
||
def test_broken_gs_skip_text_without_pdfa_fails_on_ocrmypdf_16() -> None:
|
||
"""DER Fall, der in v0.6.0 durchrutschte.
|
||
|
||
ocrmypdf 16.13.0 + Ghostscript 10.0.0 + skip_text=true + pdfa_level="":
|
||
"Preflight ok", Dienst active — und jede PDF landete in error/.
|
||
Jetzt muss der Dienst beim START abbrechen.
|
||
"""
|
||
with _env("10.0.0", "16.13.0"):
|
||
with pytest.raises(PreflightError) as exc_info:
|
||
check_preflight(pdfa_level="", skip_text=True)
|
||
msg = str(exc_info.value)
|
||
assert "10.0.0" in msg
|
||
assert "16.13.0" in msg
|
||
|
||
|
||
def test_broken_gs_without_skip_text_passes_on_ocrmypdf_16() -> None:
|
||
"""skip_text=false → auch 16.x prüft Ghostscript nicht."""
|
||
with _env("10.0.0", "16.13.0"):
|
||
check_preflight(pdfa_level="", skip_text=False) # darf nicht werfen
|
||
|
||
|
||
def test_healthy_gs_passes_on_ocrmypdf_16() -> None:
|
||
"""Nicht betroffene GS-Version → auch mit 16.x kein Abbruch."""
|
||
with _env("10.02.1", "16.13.0"):
|
||
check_preflight(pdfa_level="", skip_text=True) # darf nicht werfen
|
||
|
||
|
||
# ---------------- Meldungstext ----------------
|
||
|
||
def test_error_message_names_both_remedies() -> None:
|
||
"""Der Admin muss aus der Meldung heraus handeln können.
|
||
|
||
Die Wege sind die **echten**: `pdfa_level = ""` (Default), `skip_text =
|
||
false` oder eine Distribution mit neuerem Ghostscript (Debian 13: 10.05.1).
|
||
Bis v0.7.0 stand hier „Ghostscript aus bookworm-backports" als Weg 1 — das
|
||
konnte nie funktionieren, denn bookworm-backports führt gar kein
|
||
Ghostscript-Paket (am echten Paketindex verifiziert: 2606 Pakete,
|
||
`ghostscript` nicht darunter). Der Rat darf nicht zurückkommen.
|
||
"""
|
||
# Fall A: ocrmypdf 16.x ohne PDF/A — Weg 1 ist hier, ocrmypdf anzuheben.
|
||
with _env("10.0.0", "16.13.0"):
|
||
with pytest.raises(PreflightError) as exc_info:
|
||
check_preflight(pdfa_level="", skip_text=True)
|
||
msg_no_pdfa = str(exc_info.value)
|
||
assert "ocrmypdf >= 17" in msg_no_pdfa, "Weg 1: ocrmypdf anheben"
|
||
assert "skip_text = false" in msg_no_pdfa, "Weg 2: skip_text abschalten"
|
||
assert "Debian 13" in msg_no_pdfa, "Weg 3: neuere Distribution"
|
||
|
||
# Fall B: PDF/A gewünscht — Weg 1 ist hier, PDF/A aufzugeben.
|
||
with _env("10.0.0", "17.4.1"):
|
||
with pytest.raises(PreflightError) as exc_info:
|
||
check_preflight(pdfa_level="2", skip_text=True)
|
||
msg_pdfa = str(exc_info.value)
|
||
assert 'pdfa_level = ""' in msg_pdfa, "Weg 1: PDF/A abschalten"
|
||
assert "skip_text = false" in msg_pdfa, "Weg 2: skip_text abschalten"
|
||
assert "Debian 13" in msg_pdfa, "Weg 3: neuere Distribution"
|
||
|
||
# Und in keiner der beiden Meldungen der widerlegte Backports-Rat.
|
||
for msg in (msg_no_pdfa, msg_pdfa):
|
||
assert "apt install -t bookworm-backports" not in msg
|
||
assert "sources.list.d/bookworm-backports.list" not in msg
|
||
# Erwähnt werden darf es — aber nur als ausdrückliche Absage.
|
||
assert "NICHT" in msg and "kein Ghostscript-Paket" in msg
|
||
|
||
|
||
def test_default_config_pdfa_level_is_empty() -> None:
|
||
"""Default-Config der Beispiel-Datei soll pdfa_level='' enthalten (Issue #3)."""
|
||
from pathlib import Path
|
||
import tomllib
|
||
cfg_path = Path(__file__).parent.parent / "config.example.toml"
|
||
with cfg_path.open("rb") as f:
|
||
data = tomllib.load(f)
|
||
assert data["ocr"]["pdfa_level"] == "", \
|
||
"config.example.toml muss pdfa_level='' als sicheren Default haben"
|
||
|
||
|
||
# ---------------- Der Dienst muss beim START abbrechen ----------------
|
||
|
||
def test_run_once_aborts_on_ocrmypdf_16_with_broken_gs(tmp_config) -> None:
|
||
"""Abbruch beim Start statt Totalausfall bei der ersten Datei."""
|
||
from pdf_ocr_hotfolder.service import HotfolderService
|
||
|
||
assert tmp_config.ocr.skip_text is True
|
||
assert tmp_config.ocr.pdfa_level == ""
|
||
service = HotfolderService(tmp_config)
|
||
try:
|
||
with _env("10.0.0", "16.13.0"):
|
||
with pytest.raises(PreflightError):
|
||
service.run_once()
|
||
finally:
|
||
service._executor.shutdown(wait=False)
|
||
|
||
|
||
def test_check_config_returns_2_on_ocrmypdf_16_with_broken_gs(
|
||
tmp_path, tmp_config, monkeypatch, capsys) -> None:
|
||
"""--check-config meldet den Zustand als Fehler (Exit 2) — auch mitten im Update."""
|
||
import sys
|
||
|
||
from pdf_ocr_hotfolder.__main__ import CHECK_ERROR, main
|
||
|
||
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]
|
||
skip_text = true
|
||
pdfa_level = ""
|
||
""")
|
||
monkeypatch.setattr(sys, "argv",
|
||
["pdf-ocr-hotfolder", "--config", str(cfg_file),
|
||
"--check-config"])
|
||
with _env("10.0.0", "16.13.0"):
|
||
assert main() == CHECK_ERROR
|
||
assert "Ghostscript" in capsys.readouterr().err
|