textmachine/platform/tools/mutate.py

97 lines
5.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Плант мутаций в КОПИЮ дерева зоны, с вердиктом по ТЕКСТУ падения.
Зачем он в репозитории, а не в скретчпаде: он производит числа, которые едут в акт приёмки
(«посадок N, выживших M»), а число без прибора, которым оно снято, пере-снять нечем. Урок 11.09:
записи этого пака одну смену прожили в скретчпаде, и пере-проверить их было не на чем.
⛔ ЧЕМ ОН УЖЕ ОШИБАЛСЯ, И ПОЧЕМУ ЭТО НАПИСАНО ЗДЕСЬ, А НЕ В ОТЧЁТЕ. Первая редакция передавала
`pkg` в `go test` ОДНОЙ строкой: два пакета через пробел превращались в один неразрешимый аргумент,
`go test` выходил ненулевым — и харнесс читал это как «мутация поймана». Три записи получили ложное
RED, и нашла их не батарея, а ручная пере-проверка одной из них. Отсюда два правила ниже, и оба
исполняются кодом, а не дисциплиной:
· пакеты идут СПИСКОМ аргументов;
· ненулевой выход, в котором нет ни одной строки `--- FAIL`, считается НЕ ПОЙМАННЫМ, а
НЕИЗМЕРЕННЫМ: у сломанной команды и у пойманной мутации один и тот же код возврата.
Формат каталога — JSON-массив записей:
{"id", "file", "pkg", "run", "what", "old", "new"}
`file` и `pkg` — относительно корня зоны; `old` обязан встречаться в файле РОВНО один раз.
Вызов: python3 tools/mutate.py <каталог.json> <корень копии>
⚠ Корень копии обязан быть КОПИЕЙ (`cp -a --parents platform docs/architecture/14-api-contract …`):
скрипт правит файлы на месте и восстанавливает их из оригинала после каждой записи.
"""
import json
import os
import shutil
import subprocess
import sys
ORIGIN = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
def restore(root: str, rel: str) -> None:
shutil.copy2(os.path.join(ORIGIN, rel), os.path.join(root, rel))
def plant(root: str, rel: str, old: str, new: str) -> str:
path = os.path.join(root, rel)
body = open(path, encoding="utf-8").read()
seen = body.count(old)
if seen != 1:
return f"якорь найден {seen} раз(а), нужен ровно один"
open(path, "w", encoding="utf-8").write(body.replace(old, new, 1))
return ""
def run(root: str, pkg: str, pattern: str):
# ⛔ СПИСКОМ, а не строкой: см. шапку.
cmd = ["go", "test", *pkg.split(), "-count=1", "-run", pattern]
p = subprocess.run(cmd, cwd=root, capture_output=True, text=True, timeout=1800)
return p.returncode, p.stdout + p.stderr
def main() -> int:
catalogue, root = sys.argv[1], os.path.abspath(sys.argv[2])
if not os.path.isfile(os.path.join(root, "go.mod")):
print(f"{root} не похож на корень зоны (нет go.mod) — отказ", file=sys.stderr)
return 2
if os.path.realpath(root) == os.path.realpath(ORIGIN):
print("корень копии совпадает с рабочим деревом — отказ: мутации сажаются только в КОПИЮ", file=sys.stderr)
return 2
survived = unmeasured = caught = 0
for m in json.load(open(catalogue, encoding="utf-8")):
restore(root, m["file"])
why = plant(root, m["file"], m["old"], m["new"])
if why:
print(f"=== {m['id']}: НЕ ПОСАЖЕНА — {why}")
restore(root, m["file"])
unmeasured += 1
continue
code, out = run(root, m["pkg"], m["run"])
fails = [l for l in out.splitlines() if l.startswith("--- FAIL")]
if code == 0:
print(f"=== {m['id']}: ⛔ ВЫЖИЛА (exit 0) — {m['what']}")
survived += 1
elif not fails:
# Ненулевой выход без единого упавшего теста — это не поимка: так выглядят и битая
# команда, и несобирающаяся мутация.
print(f"=== {m['id']}: ⚠ НЕ ИЗМЕРЕНА (exit {code}, ни одного `--- FAIL`) — {m['what']}")
for line in [l for l in out.splitlines() if ".go:" in l][:3]:
print(" " + line.strip()[:200])
unmeasured += 1
else:
print(f"=== {m['id']}: RED — {m['what']}")
for line in fails[:2] + [l for l in out.splitlines() if "_test.go:" in l][:3]:
print(" " + line.strip()[:220])
caught += 1
restore(root, m["file"])
print(f"\nитог: посажено {caught + survived + unmeasured} · поймано {caught} · "
f"ВЫЖИЛО {survived} · НЕ ИЗМЕРЕНО {unmeasured}")
return 0
if __name__ == "__main__":
sys.exit(main())