db9fe5e0fa
- 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.
163 lines
7.4 KiB
Python
163 lines
7.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Самопроверка моста без сети: python3 test_translate.py"""
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
|
|
import translate as t
|
|
|
|
|
|
def test_marks_roundtrip():
|
|
src = 'She thinks <i>this is bad</i> & says “no”'
|
|
marked = t.to_marks(src)
|
|
assert "<i>" not in marked and "⟦i⟧" in marked
|
|
assert "&" not in marked, "сущности должны раскрываться для переводчика"
|
|
body, ok = t.from_marks(marked)
|
|
assert ok
|
|
assert "<i>this is bad</i>" in body
|
|
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
|
|
|
|
|
|
def test_language_detection():
|
|
assert t.detect_language("the quick brown fox " * 40)[0] == "en"
|
|
assert t.detect_language("быстрая бурая лиса " * 40)[0] == "ru"
|
|
assert t.detect_language("short")[0] == "unknown"
|
|
|
|
|
|
def test_split_and_rebuild(tmp: Path):
|
|
src = tmp / "book.html"
|
|
src.write_text("\n".join([
|
|
"<html><body>",
|
|
"<h1>CHAPTER 1</h1>",
|
|
"<p>He said <i>hello</i> loudly.</p>",
|
|
'<p class="figure"><img src="images/img01.png"/></p>',
|
|
"<h1>CHAPTER 2</h1>",
|
|
"<p>Second chapter text.</p>",
|
|
"</body></html>",
|
|
]), encoding="utf-8")
|
|
|
|
blocks = t.parse_blocks(src)
|
|
assert len(blocks) == 4, "картинка не должна попадать в перевод"
|
|
chapters = t.split_chapters(blocks)
|
|
assert [len(c) for c in chapters] == [2, 2]
|
|
|
|
extracted = tmp / "extracted"
|
|
t.write_input(chapters, extracted)
|
|
ch0 = json.loads((extracted / "chapter_000.json").read_text(encoding="utf-8"))
|
|
assert ch0["title"] == "CHAPTER 1"
|
|
assert ch0["paragraphs"][1] == "He said ⟦i⟧hello⟦/i⟧ loudly."
|
|
|
|
# подделываем ответ book_translator: тот же формат, тот же порядок
|
|
trans = tmp / "translations"
|
|
trans.mkdir()
|
|
for n, ch in enumerate(chapters):
|
|
paragraphs = [json.loads((extracted / ("chapter_%03d.json" % n))
|
|
.read_text(encoding="utf-8"))["paragraphs"][i]
|
|
.replace("He said", "Он сказал").replace("hello", "привет")
|
|
for i in range(len(ch))]
|
|
(trans / ("chapter_%03d_translated.json" % n)).write_text(
|
|
json.dumps({"number": n, "paragraphs": paragraphs}, ensure_ascii=False),
|
|
encoding="utf-8")
|
|
|
|
out = tmp / "book.ru.html"
|
|
assert t.rebuild(src, chapters, tmp, out)
|
|
result = out.read_text(encoding="utf-8")
|
|
assert "<p>Он сказал <i>привет</i> loudly.</p>" in result
|
|
assert '<img src="images/img01.png"/>' in result, "картинка должна уцелеть"
|
|
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"
|
|
bad.write_text("<p>[UNTRANSLATED] The build is broken.</p>\n" * 30, encoding="utf-8")
|
|
assert not t.verify_output(bad, "ru"), "заглушки должны заваливать проверку"
|
|
|
|
english = tmp / "en.html"
|
|
english.write_text("<p>The build is broken again today.</p>\n" * 40, encoding="utf-8")
|
|
assert not t.verify_output(english, "ru"), "английский результат должен заваливать проверку"
|
|
|
|
good = tmp / "ru.html"
|
|
good.write_text("<p>Сборка снова сломалась сегодня утром.</p>\n" * 40, encoding="utf-8")
|
|
assert t.verify_output(good, "ru")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import tempfile
|
|
|
|
test_marks_roundtrip()
|
|
test_inline_code_survives()
|
|
test_broken_marks_drop_tags()
|
|
test_language_detection()
|
|
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")
|