Files
pdf-ocr-hotfolder/tests/test_ghostscript_version.py
T
techadmin aa9918adba 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>
2026-09-22 22:55:47 +02:00

202 lines
7.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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