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

96 lines
4.8 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.

"""UI-смоук фронта arm-core/web против стенда регрессии (regress.sh, этап ui; после сценария stand).
Обходит основные экраны под ka_go (токен — в sessionStorage, как после входа), на каждом ждёт затишья сети и
собирает: исключения страницы, ошибки консоли, ответы /api со статусом >= 400. Красный экран — исключение страницы,
ответ /api >= 500 или 4xx (кроме перечисленных в ALLOWED_4XX), пустая страница. Скриншоты — <out>/<экран>.png,
итог — <out>/report.json. Идентификаторы версий и документов берёт из report.json сценария стенда.
uv run --with playwright==1.55.0 python ui_smoke.py --web http://127.0.0.1:13000 --api http://127.0.0.1:18000 \\
--stand-report DIR/stand/report.json --out DIR/ui
"""
import argparse
import json
import sys
import urllib.request
from pathlib import Path
from playwright.sync_api import sync_playwright
# 4xx, которые фронт получает штатно (пусто — любой 4xx красный; дополнять с причиной)
ALLOWED_4XX: set[tuple[str, int]] = set()
def token(api: str, email: str) -> str:
req = urllib.request.Request(
api.rstrip("/") + "/api/v1/dev/token",
data=json.dumps({"email": email}).encode(),
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req, timeout=30) as r:
return json.loads(r.read())["access_token"]
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--web", required=True)
ap.add_argument("--api", required=True)
ap.add_argument("--stand-report", required=True, type=Path)
ap.add_argument("--out", required=True, type=Path)
ap.add_argument("--user", default="ka_go@armka.local")
a = ap.parse_args()
a.out.mkdir(parents=True, exist_ok=True)
ids = json.loads(a.stand_report.read_text()).get("ids", {})
g = ids.get("group_id")
pages = [("groups", "/groups"), ("catalogue-groups", "/catalogue/groups"), ("catalogue-le", "/catalogue/jur_l")]
if g:
pages.append(("group", f"/groups/{g}"))
for sid, vid in (ids.get("versions") or {}).items():
pages += [(f"{sid}-osv", f"/groups/{g}/versions/{vid}/osv"), (f"{sid}-chess", f"/groups/{g}/versions/{vid}/chess")]
for name, doc in (ids.get("documents") or {}).items():
pages.append((f"doc-{name}", f"/documents/{doc}"))
tok = token(a.api, a.user)
results = []
with sync_playwright() as p:
browser = p.chromium.launch()
ctx = browser.new_context(viewport={"width": 1600, "height": 1000}, locale="ru-RU")
ctx.add_init_script(f"try {{ sessionStorage.setItem('armka.token', {json.dumps(tok)}); }} catch (e) {{}}")
for name, path in pages:
page = ctx.new_page()
errors, console, bad_api = [], [], []
page.on("pageerror", lambda e, errors=errors: errors.append(str(e)[:300]))
page.on("console", lambda m, console=console: console.append(m.text[:300]) if m.type == "error" else None)
def on_response(r, bad_api=bad_api):
if "/api/" in r.url and r.status >= 400:
bad_api.append(f"{r.status} {r.request.method} {r.url.split('/api/', 1)[1][:150]}")
page.on("response", on_response)
try:
page.goto(a.web.rstrip("/") + path, wait_until="networkidle", timeout=60000)
page.wait_for_timeout(1500)
text = page.inner_text("body")
except Exception as e: # noqa: BLE001 — экран красный, обход продолжается
errors.append(f"{type(e).__name__}: {e}"[:300])
text = ""
page.screenshot(path=str(a.out / f"{name}.png"), full_page=False)
red_api = [
x for x in bad_api
if int(x.split()[0]) >= 500 or (x.split()[1], int(x.split()[0])) not in ALLOWED_4XX
]
ok = not errors and not red_api and len(text.strip()) > 20
results.append({"name": name, "path": path, "ok": ok, "page_errors": errors, "api_errors": bad_api, "console_errors": console[:10]})
print(("ok " if ok else "FAIL ") + f"{name} {path}" + ("" if ok else f": {errors or red_api or 'пустая страница'}"), flush=True)
page.close()
browser.close()
(a.out / "report.json").write_text(json.dumps(results, ensure_ascii=False, indent=1))
red = [r for r in results if not r["ok"]]
print(f"экранов {len(results)}, красных {len(red)}")
return 1 if red else 0
if __name__ == "__main__":
sys.exit(main())