- 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>
143 lines
5.6 KiB
Python
143 lines
5.6 KiB
Python
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 "<" not in block and "&" 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
|