aa9918adba
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>
202 lines
7.0 KiB
Python
202 lines
7.0 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."""
|
||
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:
|
||
"""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
|