Andrey Epifancev 9bc2b26c1c scripts/regression: полная регрессия main всех репозиториев перед выкладкой (#179)
regress.sh — один вход: preflight (main = origin/main, чистые, нет чужих потребителей NATS), venv, lint+unit
всех репозиториев и docs make check, фронт в чистой копии (npm ci --dry-run как проверка lock, установка без
lock), интеграционные с циклом миграций, реальные данные парсера, образы, e2e ядра, изолированный стенд с нуля
(compose-проект arm-ka-regress на своих портах) со сценарием по API, UI-смоук Playwright, парсер по doc-bank.
Не останавливается на первой ошибке; итог — summary.md с SHA и статусом шагов.

Контрольные цифры — expected.json (qa-guide §7.1–7.3, прогоны 08.10/14.10, golden шахматки); снимки свода,
шахматки и форм сравниваются с прошлым прогоном. Ссылки из dev/checks.md и чек-листа синхронизации контура.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 12:09:15 +04:00

88 lines
4.6 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.

"""Итог регрессии: summary.md из results.tsv, shas.tsv и логов шагов (вызывает regress.sh).
Аргументы: каталог прогона, список этапов через пробел (порядок в таблице). Для логов pytest — последняя строка-итог
(«N passed, M skipped …») и число пропусков из-за отсутствия docs/fixtures: зелёный шаг с такими пропусками не доказывает
сверку контракта и прохождение на реальных данных (dev/checks.md, «Внешние ресурсы»).
"""
import json
import re
import sys
from pathlib import Path
PYTEST_TAIL = re.compile(r"=+ (.*\b(passed|failed|error|skipped|deselected)\b.*) in [\d.]+s")
# причины пропуска «нет соседнего репозитория» в сервисах (dev/checks.md)
MISSING_RES = re.compile(r"SKIPPED.*(ARM_KA_FIXTURES|ARM_KA_DOCS|fixtures недоступ|docs недоступ|нет \.\./(docs|fixtures))")
def pytest_info(log: Path) -> str:
try:
text = log.read_text(errors="replace")
except OSError:
return ""
tails = [m.group(1) for m in PYTEST_TAIL.finditer(text)]
missing = len(MISSING_RES.findall(text))
parts = []
if tails:
parts.append(tails[-1] if len(tails) == 1 else " | ".join(tails))
if missing:
parts.append(f"⚠ пропусков без docs/fixtures: {missing}")
return "; ".join(parts)
def first_error(log: Path) -> str:
"""Короткая подсказка для красного шага: первая строка FAILED/ERROR/КРАСНЫЙ/Error из лога."""
try:
lines = log.read_text(errors="replace").splitlines()
except OSError:
return ""
for pat in (r"^FAILED |^ERROR ", r"FAIL|КРАСН|✗", r"[Ee]rror|ошибк"):
for ln in lines:
if re.search(pat, ln):
return ln.strip()[:160]
return lines[-1].strip()[:160] if lines else ""
def main() -> None:
out, stages = Path(sys.argv[1]), sys.argv[2].split()
rows = [ln.split("\t") for ln in (out / "results.tsv").read_text().splitlines() if ln.strip()]
shas = [ln.split("\t") for ln in (out / "shas.tsv").read_text().splitlines()] if (out / "shas.tsv").exists() else []
red = [r for r in rows if r[2] != "0"]
md = [f"# Регрессия {out.name}", ""]
md.append(f"**Итог: {'ЗЕЛЁНАЯ' if not red else f'КРАСНАЯ — {len(red)} из {len(rows)} шагов'}**")
if (out / "allow-dirty").exists():
md.append("")
md.append("⚠ Прогон с `--allow-dirty`: preflight не пройден (не main, не origin/main или есть правки) — см. этап preflight.")
md += ["", "## Репозитории", "", "| Репозиторий | Ветка | SHA | Последний коммит |", "|---|---|---|---|"]
md += [f"| {r} | {b} | `{h[:10]}` | {s} |" for r, b, h, s in shas]
md += ["", "## Шаги", "", "| Этап | Шаг | Статус | Время, с | Детали |", "|---|---|---|---|---|"]
order = {s: i for i, s in enumerate(stages)}
for stage, name, rc, sec, log in sorted(rows, key=lambda r: order.get(r[0], 99)):
logp = Path(log)
detail = pytest_info(logp)
if rc != "0":
detail = "; ".join(x for x in (detail, first_error(logp)) if x)
status = "ok" if rc == "0" else f"**КРАСНЫЙ** ({rc})"
md.append(f"| {stage} | {name} | {status} | {sec} | {detail.replace('|', '/')} |")
stand = out / "stand" / "report.json"
if stand.exists():
rep = json.loads(stand.read_text())
md += ["", "## Стенд: сценарий по API", ""]
red = [c for c in rep["checks"] if not c["ok"]]
md.append(f"Проверок {len(rep['checks'])}, красных {len(red)}.")
md.append("")
md += [f"- {'✓' if c['ok'] else '✗'} {c['name']}" + (f": {c['detail']}" if c["detail"] else "") for c in rep["checks"]]
if rep.get("baseline"):
md += ["", f"Сравнение снимков со стендом прошлого прогона `{rep['baseline']}`: расхождений {len(rep['diffs'])}."]
md += [f"- `{d['file']}`: {d['detail']}" for d in rep["diffs"]]
bank = out / "bank" / "summary.md"
if bank.exists():
md += ["", bank.read_text().strip()]
md += ["", f"Логи: `{out / 'logs'}`"]
(out / "summary.md").write_text("\n".join(md) + "\n")
print("\n".join(md))
if __name__ == "__main__":
main()