fix: ocrmypdf-Pin auf 17.4.1, Preflight erkennt den GS-Fall, Rauchtest (v0.6.1)

v0.6.0 hat ocrmypdf auf 16.13.0 gepinnt, um einen ungewollten Major-Sprung
zu verhindern. Auf Bestandsinstallationen war das ein DOWNGRADE (dort lief
via ">=16.0" bereits 17.x) — und 16.13.0 bricht auf Debian 12 mit dem
Bord-Ghostscript 10.0.0 bei JEDER PDF ab, sobald skip_text gesetzt ist.
Im Test auf CT 200 lief das Update mit Exit 0 durch, der Dienst blieb
"active", --check-config meldete "Preflight ok" — und jede Datei landete
in error/. Stiller Totalausfall.

- requirements.txt: ocrmypdf==17.4.1 (real auf Debian 12 + gs 10.0.0
  verifiziert). Ab 17.0.0 steht die GS-Pruefung in ocrmypdf unter einem
  `if options.output_type.startswith('pdfa')`; bis 16.x lief sie ohne
  diesen Guard und schlug auch bei output_type="pdf" zu.
- check_preflight() prueft Ghostscript nicht mehr nur bei gesetztem
  pdfa_level, sondern bildet die reale Bedingung ab:
  betroffene GS-Version UND skip_text UND (PDF/A ODER ocrmypdf < 17).
  Der Dienst bricht damit beim Start ab statt bei der ersten Datei.
- update.sh zeigt Versionsspruenge der gepinnten Pakete; Downgrades als
  WARN, auch in der Abschluss-Zusammenfassung.
- update.sh faehrt nach dem Start einen Rauchtest (eingebettete Mini-PDF
  durch die echte Pipeline) und raeumt restlos auf. Uebersprungen, wenn
  Upload-Ziele oder E-Mail-Notify aktiv sind, damit kein Testmuell zum
  Kunden geht. Abschaltbar mit --no-smoke-test.
- Doku korrigiert: pdfa_level = "" allein ist keine Entwarnung, die haengt
  an der ocrmypdf-Version.

152 Tests gruen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-22 22:55:47 +02:00
parent 3e24aa2ecd
commit aa9918adba
15 changed files with 1044 additions and 86 deletions
+148 -19
View File
@@ -1,6 +1,16 @@
"""Tests für Issue #3: Ghostscript 10.0.0–10.02.0 PDF/A-Bug-Erkennung."""
"""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
@@ -9,9 +19,21 @@ 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),
@@ -36,29 +58,92 @@ def test_is_ghostscript_broken(version, expected) -> None:
assert is_ghostscript_broken(version) is expected
def test_check_preflight_without_pdfa_passes_with_broken_gs() -> None:
"""Ohne pdfa_level darf der betroffene GS verwendet werden."""
with patch("pdf_ocr_hotfolder.service.shutil.which", return_value="/usr/bin/fake"), \
patch("pdf_ocr_hotfolder.service.detect_ghostscript_version",
return_value="10.0.0"):
check_preflight(pdfa_level="") # darf nicht werfen
@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
def test_check_preflight_with_pdfa_fails_on_broken_gs() -> None:
"""Mit pdfa_level + kaputtem GS → PreflightError mit hilfreicher Meldung."""
with patch("pdf_ocr_hotfolder.service.shutil.which", return_value="/usr/bin/fake"), \
patch("pdf_ocr_hotfolder.service.detect_ghostscript_version",
return_value="10.0.0"):
# ---------------- 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")
check_preflight(pdfa_level="2", skip_text=True)
def test_check_preflight_with_pdfa_passes_on_fixed_gs() -> None:
"""Mit pdfa_level + gefixtem GS → ok."""
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"):
check_preflight(pdfa_level="2") # darf nicht werfen
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."""
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 "backports" in msg, "Weg 1: Ghostscript aus bookworm-backports"
assert "skip_text = false" in msg, "Weg 2: skip_text abschalten"
def test_default_config_pdfa_level_is_empty() -> None:
@@ -70,3 +155,47 @@ def test_default_config_pdfa_level_is_empty() -> None:
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