epub2md/tests/test_integration.py
dschlueter 3b07fcf01a fix: Befunde aus der QA-Prüfung (Prompt_Kontrolle.md)
- F1: Nicht beschreibbares Ausgabeziel liefert eine verständliche
  Meldung und Exit 1 statt "Interner Fehler" (Exit 4).
- F2: Anker von <dt id> stehen vor dem fett gesetzten Glossarbegriff
  statt innerhalb der **-Marker.
- F3: Formeln als <img src="*.svg"> erscheinen im Bericht mit
  origin "svg" statt "image".

Regressionstests für alle drei Befunde ergänzt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 19:47:45 +02:00

143 lines
5.6 KiB
Python
Raw Permalink 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.

import json
import re
from pathlib import Path
import pytest
import yaml
from conftest import ROOT, Book, Doc, build_epub
BEISPIELE = sorted((ROOT / "Beispiele").glob("*.epub"))
def simple_book() -> Book:
return Book(docs=[Doc("a.xhtml", "<h1>A</h1><p>Text</p>"), Doc("b.xhtml", "<h1>B</h1><p>Mehr</p>")])
def test_output_structure_and_reports(convert):
run = convert(simple_book(), "--copy-original")
assert run.rc == 0
for rel in ("README.md", "metadata.yaml", "manifest.json", "conversion-report.json",
"diagnostics/warnings.md", "diagnostics/unsupported-elements.md"):
assert (run.out / rel).is_file(), rel
assert len(list((run.out / "source").glob("*.epub"))) == 1
manifest = json.loads((run.out / "manifest.json").read_text(encoding="utf-8"))
assert [c["file"] for c in manifest["chapters"]] == ["chapters/001-a.md", "chapters/002-b.md"]
for c in manifest["chapters"]:
assert (run.out / c["file"]).is_file()
assert c["word_count"] > 0
assert run.report["status"] == "success"
assert run.report["input"]["sha256"]
fm = yaml.safe_load(run.chapter("001").split("---\n")[1])
assert fm["chapter_number"] == 1 and fm["source_href"] == "a.xhtml" and fm["book_title"] == "Testbuch"
def test_no_frontmatter_no_toc(convert):
run = convert(simple_book(), "--no-frontmatter", "--no-toc")
assert not run.chapter("001").startswith("---")
assert not (run.out / "README.md").exists()
def test_dry_run_writes_nothing(convert, tmp_path):
report = tmp_path / "r.json"
run = convert(simple_book(), "--dry-run", "--report", str(report))
assert run.rc == 0
assert not run.out.exists()
assert not report.exists()
assert "chapters/001-a.md" in run.stdout
def test_existing_output_requires_force(convert, tmp_path):
out = tmp_path / "ziel"
out.mkdir()
(out / "wichtig.txt").write_text("nicht löschen")
run = convert(simple_book(), out=out)
assert run.rc == 1
assert (out / "wichtig.txt").exists()
# --force refuses directories that are not epub2md output
assert convert(simple_book(), "--force", out=out).rc == 1
assert (out / "wichtig.txt").exists()
def test_force_replaces_previous_output(convert, tmp_path):
out = tmp_path / "ziel"
assert convert(simple_book(), out=out).rc == 0
(out / "alt.txt").write_text("alt")
run = convert(simple_book(), "--force", out=out)
assert run.rc == 0
assert not (out / "alt.txt").exists()
assert (out / "README.md").exists()
assert not list(tmp_path.glob(".epub2md-*")) # temp dir cleaned up
def test_report_option_and_verbose(convert, tmp_path):
report = tmp_path / "rep" / "bericht.json"
run = convert(simple_book(), "--report", str(report), "--verbose")
assert json.loads(report.read_text(encoding="utf-8"))["tool"] == "epub2md"
assert "Rendere" in run.stderr
def test_invalid_arguments_exit_1(convert, tmp_path):
assert convert(simple_book(), "--math", "falsch").rc == 1
assert convert(simple_book(), "--chapter-level", "9").rc == 1
assert convert(tmp_path / "gibt-es-nicht.epub").rc == 1
def test_deterministic_output(convert, tmp_path):
epub = build_epub(tmp_path / "same.epub", simple_book())
a = convert(epub, out=tmp_path / "x1")
b = convert(epub, out=tmp_path / "x2")
for p in a.chapters():
assert p.read_text() == (b.out / "chapters" / p.name).read_text()
def test_encoding_edge_cases(convert):
text_in = "Ärger über Öl – „Zitat“ — café naïve, Ωμέγα αβγ, ∀x∈ℝ: x² ≥ 0 … ‚einfach‘"
book = Book(docs=[Doc("Kapitel-é.xhtml", f'<h1 id="über">Grüße</h1><p>{text_in}</p>')])
run = convert(book)
text = run.chapter("001")
assert text_in in text
assert "<EFBFBD>" not in text
assert run.chapters()[0].name == "001-gruesse.md"
@pytest.mark.skipif(not BEISPIELE, reason="keine Beispiel-EPUBs vorhanden")
@pytest.mark.parametrize("epub", BEISPIELE, ids=[p.stem[:20] for p in BEISPIELE])
def test_example_books_structural_invariants(convert, epub: Path):
"""Checks structure only – no copyrighted text is asserted."""
run = convert(epub)
assert run.rc == 0, run.stderr
report = run.report
chapters = run.chapters()
assert len(chapters) >= 3
assert report["output_link_check"]["broken_count"] == 0, report["output_link_check"]["broken"][:5]
assert not report["errors"]
for path in chapters:
text = path.read_text(encoding="utf-8")
body = text.split("---\n", 2)[2] if text.startswith("---\n") else text
yaml.safe_load(text.split("---\n")[1])
assert body.strip(), f"leere Kapiteldatei {path.name}"
fences = re.findall(r"^(`{3,})", body, re.M)
assert len(fences) % 2 == 0, f"unbalancierte Code-Fences in {path.name}"
for block in re.findall(r"^```[\w+-]*\n(.*?)\n```$", body, re.M | re.S):
assert "&lt;" not in block and "&amp;" not in block
for m in report["media"]["files"]:
assert (run.out / m["file"]).is_file()
assert sum(c["stats"]["word_count"] for c in report["chapters"]) > 1000
def test_unwritable_output_parent_gives_clear_error(convert, tmp_path):
"""QA-F1: a permission problem at the target must not be reported as internal error (exit 4)."""
import os
import stat
parent = tmp_path / "readonly"
parent.mkdir()
os.chmod(parent, stat.S_IRUSR | stat.S_IXUSR)
try:
run = convert(simple_book(), out=parent / "ziel")
finally:
os.chmod(parent, stat.S_IRWXU)
assert run.rc == 1
assert "Interner Fehler" not in run.stderr
assert "nicht beschreibbar" in run.stderr or "Schreibfehler" in run.stderr