food-market/scripts/generate-badges.sh
nns 1989db32bb test(s16): regression suite 35 flows + visual 60 snapshots + nightly + CI badges
Sprint 16 — постоянный regression-контур: flows + visual + nightly +
CI workflow + README badges.

Ключевые цифры:
- 35 flow-тестов: 35/35 ✓ за ~30 секунд (workers=2 локально).
- 60 visual snapshot'ов (15 страниц × 2 темы × 2 viewport'a):
  60/60 ✓ за ~4 минуты с retries=1.
- Полный регресс прогон: ~5 минут (цель была < 15).

Что сделано:
1. tests/regression/ — Playwright + factories + 8 spec-файлов.
   OrgFactory builder создаёт org через API за O(N) HTTP вызовов
   (signup → token → refs → products → counterparties → posted supplies).
   Каждый flow независим, использует свой fresh-org.
2. tests/regression/visual/ — 15 страниц × 2 темы × 2 viewport'a.
   Маски на динамический контент (артикулы с Date.now, KPI'ы,
   delta-стрелки) чтобы 0.2% threshold не флакал. snapshotPathTemplate
   c {projectName} — desktop+mobile не затирают друг друга.
3. tests/regression/factories/OrgFactory.ts — builder с .withProducts
   .withCounterparties .withSupplies. Retry signup'a на 429.
4. .forgejo/workflows/regression.yml — on workflow_run после
   Docker API/Web; cache на pnpm-store + Playwright-browsers;
   артефакты при failure; Telegram-уведомление в обоих случаях.
5. ~/nightly-verify.sh + cron `0 4 * * *`: health → redeploy если
   нужно → smoke flows; в воскресенье полный flows+visual. Логи с
   ротацией 14 дней. Telegram на провал (~/.fm-watchdog/telegram-*).
6. scripts/generate-badges.sh — coverage из cobertura.xml в SVG через
   shields.io (offline fallback). 4 CI-status badge + coverage badge в
   README; CI step «Update coverage badge» авто-коммитит обновлённый
   SVG на push в main.

Локальное число flake'ов: 1/60 visual на retry=1 (product-new light) —
случайная гонка маски, retry'ит и проходит.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-07 16:14:11 +05:00

96 lines
3.8 KiB
Bash
Executable file

#!/usr/bin/env bash
# Sprint 16: генерация бейджей покрытия / статуса в badges/*.svg.
#
# Запускается вручную или из CI после `dotnet test --collect:"XPlat Code Coverage"`.
# Вход: путь к cobertura.xml (или авто-поиск в TestResults/).
# Выход: badges/coverage.svg + ссылка для добавления в README.
#
# Без зависимостей кроме curl, python3, sed.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
BADGES_DIR="$ROOT/badges"
mkdir -p "$BADGES_DIR"
# 1. Coverage badge
CXML="${1:-}"
if [[ -z "$CXML" ]]; then
CXML="$(find "$ROOT" -name 'coverage.cobertura.xml' -path '*/TestResults/*' 2>/dev/null | head -1)"
fi
if [[ -z "$CXML" || ! -f "$CXML" ]]; then
echo "Usage: $0 [path/to/coverage.cobertura.xml]" >&2
echo "Run 'dotnet test --collect:XPlat Code Coverage' first." >&2
exit 1
fi
PCT=$(python3 -c "
import xml.etree.ElementTree as ET
r = ET.parse('$CXML').getroot()
# Считаем суммарное покрытие по Application + Domain (главные пакеты).
covered, valid = 0, 0
for pkg in r.iter('package'):
if pkg.get('name') in ('foodmarket.Application', 'foodmarket.Domain'):
for line in pkg.iter('line'):
valid += 1
if int(line.get('hits', '0')) > 0:
covered += 1
print(f'{100*covered/valid:.0f}' if valid else '0')
")
# Цвет по порогам — shields.io стандарт.
COLOR="brightgreen"
if (( PCT < 50 )); then COLOR="red"
elif (( PCT < 70 )); then COLOR="yellow"
elif (( PCT < 85 )); then COLOR="green"
fi
# Скачиваем static SVG от shields.io (legacy endpoint без рантайма).
URL="https://img.shields.io/badge/coverage-${PCT}%25-${COLOR}?style=flat-square&label=coverage%20(app%2Bdomain)"
echo "[badges] coverage = ${PCT}% → ${COLOR}"
echo "[badges] fetching $URL"
if curl -fsS "$URL" -o "$BADGES_DIR/coverage.svg"; then
echo "[badges] wrote $BADGES_DIR/coverage.svg ($(wc -c < "$BADGES_DIR/coverage.svg") bytes)"
else
# Offline-fallback: SVG inline.
cat > "$BADGES_DIR/coverage.svg" <<SVG
<svg xmlns="http://www.w3.org/2000/svg" width="170" height="20" role="img" aria-label="coverage: ${PCT}%">
<linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient>
<rect rx="3" width="170" height="20" fill="#555"/>
<rect rx="3" x="130" width="40" height="20" fill="#4c1"/>
<text x="65" y="14" fill="#fff" font-family="Verdana,sans-serif" font-size="11" text-anchor="middle">coverage (app+domain)</text>
<text x="150" y="14" fill="#fff" font-family="Verdana,sans-serif" font-size="11" text-anchor="middle">${PCT}%</text>
</svg>
SVG
echo "[badges] offline fallback SVG written"
fi
# 2. Build/CI status — static shields. Реальный badge берёт SVG c
# Forgejo `actions/workflows/ci.yml/badge.svg` (auto-обновляется).
# Тут просто проверяем, что mirror.svg есть в repo для случая offline-read.
cat > "$BADGES_DIR/ci-status-link.md" <<MD
# CI status badges
Forgejo (primary, обновляется автоматически на каждый workflow run):
\`\`\`markdown
![CI](http://127.0.0.1:3000/nns/food-market/actions/workflows/ci.yml/badge.svg)
![Docker API](http://127.0.0.1:3000/nns/food-market/actions/workflows/docker-api.yml/badge.svg)
![Regression](http://127.0.0.1:3000/nns/food-market/actions/workflows/regression.yml/badge.svg)
\`\`\`
GitHub mirror (для external reader'ов):
\`\`\`markdown
![CI](https://github.com/nurdotnet/food-market/actions/workflows/ci.yml/badge.svg)
\`\`\`
Coverage (regenerated by \`scripts/generate-badges.sh\`):
\`\`\`markdown
![coverage](./badges/coverage.svg)
\`\`\`
MD
echo "[badges] done"