#!/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 this is bad & says “no”' marked = t.to_marks(src) assert "" not in marked and "⟦i⟧" in marked assert "&" not in marked, "сущности должны раскрываться для переводчика" body, ok = t.from_marks(marked) assert ok assert "this is bad" in body assert "&" in body, "спецсимволы должны экранироваться обратно" def test_inline_code_survives(): """Имена функций в прозе не должны терять разметку по дороге к модели.""" marked = t.to_marks("call fetch() twice") assert marked == "call ⟦code⟧fetch()⟦/code⟧ twice" body, ok = t.from_marks(marked) assert ok and "fetch()" in body def test_broken_marks_drop_tags(): body, ok = t.from_marks("текст ⟦i⟧без закрытия") assert not ok and "⟦" not in body and "" 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([ "", "

CHAPTER 1

", "

He said hello loudly.

", '

', "

CHAPTER 2

", "

Second chapter text.

", "", ]), 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 "

Он сказал привет loudly.

" in result assert '' in result, "картинка должна уцелеть" assert result.count("

") == 2 def test_code_listings_are_left_alone(tmp: Path): """Листинги не переводятся и не участвуют в приёмке по языку: иначе книга, где кода много, честно переведётся и завалит проверку.""" listing = "\n".join([ "
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",
        "
", ]) prose = ("

Текст главы про обработку данных, списки значений и способы " "хранения промежуточных результатов между запусками.

") src = tmp / "code.html" src.write_text("

Глава

\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"], \ "листинг не должен попасть в перевод" # латиницы в файле больше, чем кириллицы, — но вся она в
    raw = re.sub("<[^>]+>", "", src.read_text(encoding="utf-8"))
    assert t.detect_language(raw)[0] == "en", "образец должен быть латинским целиком"
    assert t.verify_output(src, "ru"), "код не должен заваливать приёмку"


def test_junk_translation_falls_back_to_original(tmp: Path):
    """Модель иногда возвращает на длинный абзац огрызок «, v», а число абзацев
    при этом сходится — прежние ворота такое пропускали."""
    src = tmp / "book.html"
    long_en = ("A vector is simply a sequence of elements that you can access by "
               "an index, and it is the workhorse of the standard library. ") * 2
    src.write_text("\n".join([
        "

Chapter

", "

%s

" % long_en, "

Second paragraph of the very same chapter, also reasonably long.

", ]), encoding="utf-8") chapters = t.split_chapters(t.parse_blocks(src)) trans = tmp / "translations" trans.mkdir() (trans / "chapter_000_translated.json").write_text(json.dumps( {"number": 0, "paragraphs": ["Глава", ", v", "Второй абзац той же самой главы, тоже достаточно длинный."]}, ensure_ascii=False), encoding="utf-8") out = tmp / "out.html" t.rebuild(src, chapters, tmp, out) result = out.read_text(encoding="utf-8") assert ", v" not in result, "огрызок не должен попадать в книгу" assert "A vector is simply a sequence" in result, "вместо огрызка нужен оригинал" assert "Второй абзац" in result, "нормальный перевод должен остаться" 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("

[UNTRANSLATED] The build is broken.

\n" * 30, encoding="utf-8") assert not t.verify_output(bad, "ru"), "заглушки должны заваливать проверку" english = tmp / "en.html" english.write_text("

The build is broken again today.

\n" * 40, encoding="utf-8") assert not t.verify_output(english, "ru"), "английский результат должен заваливать проверку" good = tmp / "ru.html" good.write_text("

Сборка снова сломалась сегодня утром.

\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_junk_translation_falls_back_to_original, 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")