#!/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())