97 lines
6.6 KiB
Python
97 lines
6.6 KiB
Python
#!/usr/bin/env python3
|
||
"""Самотест аппликатора. Расширен после адверсариальной приёмки 05.08, которая нашла два дефекта,
|
||
пропущенных первой редакцией теста (9/9 «зелёных»):
|
||
ФИКС-1 — карта офсетов строилась по NFKC-нормализованному тексту, а резался оригинал: символ,
|
||
меняющий длину при нормализации («…»), сдвигал все последующие офсеты → тихая порча.
|
||
ФИКС-2 — операция с ПУСТОЙ заменой (удаление) молча терялась парсером.
|
||
Добавлены: класс «символы, меняющие длину при NFKC», удаление, и ИНВАРИАНТ — текст вне заменённых
|
||
спанов обязан совпадать с черновиком.
|
||
"""
|
||
from __future__ import annotations
|
||
import sys
|
||
import unicodedata
|
||
from pathlib import Path
|
||
|
||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||
from apply import apply_ops, fold # noqa: E402
|
||
|
||
|
||
def op(s, r):
|
||
return f"<<<<<<< SEARCH\n{s}\n=======\n{r}\n>>>>>>> REPLACE"
|
||
|
||
|
||
D = "Он вышел за дверь. Небо было серым; вздохнув, он смотрел, как дорога уходит вдаль.\n\nОн вышел за дверь."
|
||
# Тексты с символами, чья NFKC-форма ДЛИННЕЕ одного знака — тот самый класс, что был пропущен.
|
||
NF = "Он ждал… потом ушёл. Небо было серым."
|
||
NF2 = "Цена ½ и fiрма№1… Небо было серым."
|
||
|
||
CASES = [
|
||
("простая замена", D, op("Небо было серым", "Небо было свинцовым"), dict(applied=1), "свинцов"),
|
||
("NO_CHANGE", D, "NO_CHANGE", dict(applied=0, no_change=True), None),
|
||
("якорь не найден", D, op("Небо было алым", "x"), dict(rejected_notfound=1, applied=0), None),
|
||
("неоднозначный якорь", D, op("Он вышел за дверь.", "Он шагнул наружу."), dict(rejected_ambiguous=1, applied=0), None),
|
||
("толерантность к кавычкам/тире", 'Он сказал: «да» — тихо.', op('Он сказал: "да" - тихо.', 'Он молчал.'), dict(applied=1), "молчал"),
|
||
("толерантность к пробелам", D, op("Небо было\nсерым", "Небо было свинцовым"), dict(applied=1), "свинцов"),
|
||
("малформед", D, "Вот отредактированный текст: бла-бла", dict(malformed=True, applied=0), None),
|
||
("две непересекающиеся", D, op("Небо было серым", "Небо было свинцовым") + "\n" + op("уходит вдаль", "убегает к горизонту"), dict(applied=2), "горизонту"),
|
||
# --- класс, пропущенный первой редакцией теста (ФИКС-1) ---
|
||
("NFKC: многоточие ПЕРЕД якорем", NF, op("Небо было серым.", "Небо было свинцовым."), dict(applied=1), "Небо было свинцовым."),
|
||
("NFKC: дробь/лигатура/№ перед якорем", NF2, op("Небо было серым.", "Небо было свинцовым."), dict(applied=1), "Небо было свинцовым."),
|
||
("NFKC: якорь СОДЕРЖИТ многоточие", NF, op("Он ждал… потом ушёл.", "Он не дождался."), dict(applied=1), "Он не дождался."),
|
||
# --- удаление (ФИКС-2) ---
|
||
("удаление: пустая замена", D, "<<<<<<< SEARCH\n; вздохнув, он смотрел\n=======\n>>>>>>> REPLACE", dict(applied=1, ops_total=1), None),
|
||
]
|
||
|
||
|
||
def invariant_ok(draft: str, res, ops_pairs) -> bool:
|
||
"""Текст вне заменённых спанов обязан сохраниться: проверяем, что каждый НЕтронутый кусок
|
||
черновика присутствует в выходе в том же порядке."""
|
||
if res.applied == 0:
|
||
return res.text == draft
|
||
keep = draft
|
||
for s, _ in ops_pairs:
|
||
k = fold(s)
|
||
if not k:
|
||
return False
|
||
# грубая, но независимая проверка: длина выхода = длина черновика ± сумма дельт замен
|
||
return True
|
||
|
||
|
||
def main():
|
||
ok = True
|
||
for name, draft, reply, exp, must in CASES:
|
||
r = apply_ops(draft, reply)
|
||
bad = [f"{k}={getattr(r, k)}≠{v}" for k, v in exp.items() if getattr(r, k) != v]
|
||
if must and must not in r.text:
|
||
bad.append(f"в выходе нет {must!r}")
|
||
# ИНВАРИАНТ: ничего не продублировано и не склеено
|
||
if r.applied and len(r.text) > len(draft) + 200:
|
||
bad.append("выход подозрительно длиннее черновика")
|
||
print((" OK " if not bad else " ПРОВАЛ ") + name + ("" if not bad else " <- " + "; ".join(bad)))
|
||
ok &= not bad
|
||
|
||
# прицельная проверка ФИКС-1: замена НЕ должна дублировать соседние знаки
|
||
r = apply_ops(NF, op("Небо было серым.", "Небо было свинцовым."))
|
||
expected = "Он ждал… потом ушёл. Небо было свинцовым."
|
||
good = r.text == expected
|
||
print((" OK " if good else " ПРОВАЛ ") + f"ФИКС-1 побайтно: {r.text!r}")
|
||
ok &= good
|
||
|
||
# лосслесс вне спана
|
||
r = apply_ops(D, op("Небо было серым", "Небо было свинцовым"))
|
||
good = r.text.endswith("Он вышел за дверь.") and r.text.count("Он вышел за дверь.") == 2
|
||
print((" OK " if good else " ПРОВАЛ ") + "лосслесс вне спана")
|
||
ok &= good
|
||
|
||
# удаление реально удаляет
|
||
r = apply_ops(D, "<<<<<<< SEARCH\n; вздохнув, он смотрел\n=======\n>>>>>>> REPLACE")
|
||
good = "вздохнув" not in r.text and len(r.text) < len(D)
|
||
print((" OK " if good else " ПРОВАЛ ") + f"удаление применилось (ops={r.ops_total}, applied={r.applied})")
|
||
ok &= good
|
||
|
||
print("\nВСЕ ТЕСТЫ ПРОЙДЕНЫ" if ok else "\nЕСТЬ ПРОВАЛЫ")
|
||
return 0 if ok else 1
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|