97 lines
5.4 KiB
Python
97 lines
5.4 KiB
Python
#!/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())
|