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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user