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", "
Text
"), Doc("b.xhtml", "Mehr
")]) 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'{text_in}
')]) run = convert(book) text = run.chapter("001") assert text_in in text assert "�" 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