EPUB как второй вход в конвейер; листинги кода не переводятся и не ломают приёмку
- epub2html.py: EPUB -> тот же XHTML, что pdf2html.py, только стандартная библиотека. Уровень глав определяется по книге, а не берётся из <h1>: в EPUB там обычно название и части. - pdf2html.py: листинг узнаётся по флагу моноширинного шрифта PyMuPDF, переносы и отступы внутри <pre> сохраняются, де-дефисация к коду не применяется. - translate.py: <pre> исключён из проверок языка (английский код утягивал долю кириллицы ниже порога приёмки), рабочий каталог привязан к книге, устаревшие главы прошлого прогона чистятся, инлайновый <code> переживает переводчика. - bookhtml.py: общий каркас документа и CSS для обоих входов. - Тесты: test_epub2html.py на собранном в памяти EPUB, четыре новых случая в test_translate.py.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Самопроверка моста без сети: python3 test_translate.py"""
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import translate as t
|
||||
@@ -17,6 +18,14 @@ def test_marks_roundtrip():
|
||||
assert "&" in body, "спецсимволы должны экранироваться обратно"
|
||||
|
||||
|
||||
def test_inline_code_survives():
|
||||
"""Имена функций в прозе не должны терять разметку по дороге к модели."""
|
||||
marked = t.to_marks("call <code>fetch()</code> twice")
|
||||
assert marked == "call ⟦code⟧fetch()⟦/code⟧ twice"
|
||||
body, ok = t.from_marks(marked)
|
||||
assert ok and "<code>fetch()</code>" in body
|
||||
|
||||
|
||||
def test_broken_marks_drop_tags():
|
||||
body, ok = t.from_marks("текст ⟦i⟧без закрытия")
|
||||
assert not ok and "⟦" not in body and "<i>" not in body
|
||||
@@ -71,6 +80,58 @@ def test_split_and_rebuild(tmp: Path):
|
||||
assert result.count("<h1>") == 2
|
||||
|
||||
|
||||
def test_code_listings_are_left_alone(tmp: Path):
|
||||
"""Листинги не переводятся и не участвуют в приёмке по языку: иначе книга,
|
||||
где кода много, честно переведётся и завалит проверку."""
|
||||
listing = "\n".join([
|
||||
"<pre>def compute_total(items, discount_rate, shipping_cost):",
|
||||
" subtotal = sum(item.price * item.quantity for item in items)",
|
||||
" return subtotal * (1 - discount_rate) + shipping_cost # not translated",
|
||||
"</pre>",
|
||||
])
|
||||
prose = ("<p>Текст главы про обработку данных, списки значений и способы "
|
||||
"хранения промежуточных результатов между запусками.</p>")
|
||||
src = tmp / "code.html"
|
||||
src.write_text("<h1>Глава</h1>\n" + (listing + "\n" + prose + "\n") * 3,
|
||||
encoding="utf-8")
|
||||
|
||||
blocks = t.parse_blocks(src)
|
||||
assert [b[1] for b in blocks] == ["h1", "p", "p", "p"], \
|
||||
"листинг не должен попасть в перевод"
|
||||
|
||||
# латиницы в файле больше, чем кириллицы, — но вся она в <pre>
|
||||
raw = re.sub("<[^>]+>", "", src.read_text(encoding="utf-8"))
|
||||
assert t.detect_language(raw)[0] == "en", "образец должен быть латинским целиком"
|
||||
assert t.verify_output(src, "ru"), "код не должен заваливать приёмку"
|
||||
|
||||
|
||||
def test_workdir_belongs_to_one_book(tmp: Path):
|
||||
"""Имена chapter_NNN.json у всех книг одинаковы: чужой workdir склеит
|
||||
перевод одной книги с текстом другой."""
|
||||
wd = tmp / "wd"
|
||||
wd.mkdir()
|
||||
a = [(0, "p", "", "First book text.")]
|
||||
b = [(0, "p", "", "Совершенно другая книга.")]
|
||||
t.claim_workdir(wd, Path("a.html"), a)
|
||||
t.claim_workdir(wd, Path("a.html"), a) # повторный запуск той же книги — ок
|
||||
try:
|
||||
t.claim_workdir(wd, Path("b.html"), b)
|
||||
except SystemExit as e:
|
||||
assert "занят другой книгой" in str(e)
|
||||
else:
|
||||
raise AssertionError("чужая книга в занятом workdir должна отвергаться")
|
||||
|
||||
|
||||
def test_stale_chapters_removed(tmp: Path):
|
||||
"""Прошлый прогон был длиннее — его главы иначе уедут в перевод."""
|
||||
extracted = tmp / "extracted"
|
||||
extracted.mkdir()
|
||||
(extracted / "chapter_009.json").write_text("{}", encoding="utf-8")
|
||||
t.write_input([[(0, "h1", "", "Глава")]], extracted)
|
||||
assert not (extracted / "chapter_009.json").exists()
|
||||
assert (extracted / "chapter_000.json").exists()
|
||||
|
||||
|
||||
def test_verify_output_catches_untranslated(tmp: Path):
|
||||
"""Регресс: первый боевой прогон отрапортовал успех на непереведённом файле."""
|
||||
bad = tmp / "bad.html"
|
||||
@@ -90,10 +151,12 @@ if __name__ == "__main__":
|
||||
import tempfile
|
||||
|
||||
test_marks_roundtrip()
|
||||
test_inline_code_survives()
|
||||
test_broken_marks_drop_tags()
|
||||
test_language_detection()
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
test_split_and_rebuild(Path(d))
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
test_verify_output_catches_untranslated(Path(d))
|
||||
for case in (test_split_and_rebuild, test_code_listings_are_left_alone,
|
||||
test_workdir_belongs_to_one_book, test_stale_chapters_removed,
|
||||
test_verify_output_catches_untranslated):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
case(Path(d))
|
||||
print("OK")
|
||||
|
||||
Reference in New Issue
Block a user