From 6989bb0bc9673ff01caf550718c10b9eef2c1bfa Mon Sep 17 00:00:00 2001 From: busya Date: Mon, 15 Jun 2026 13:40:22 +0300 Subject: [PATCH] feat(coverage): add coverage-summary script, README section, ADR-0013 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - scripts/coverage-summary.sh — unified coverage summary generator Runs pytest+coverage (unit/integration) and vitest+coverage, parses results, generates single HTML report with both stacks. Supports --unit, --backend-only, --frontend-only, --output-dir. - README.md — add 'Покрытие кода' sub-section under Тестирование - docs/adr/ADR-0013-coverage-reporting.md — architectural decision record --- README.md | 40 ++ docs/adr/ADR-0013-coverage-reporting.md | 104 +++++ scripts/coverage-summary.sh | 510 ++++++++++++++++++++++++ 3 files changed, 654 insertions(+) create mode 100644 docs/adr/ADR-0013-coverage-reporting.md create mode 100755 scripts/coverage-summary.sh diff --git a/README.md b/README.md index db7a4dad..f4101680 100755 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ - [Быстрый старт](#-быстрый-старт) - [Документация](#-документация) - [Тестирование](#-тестирование) +- [Покрытие кода](#-покрытие-кода) - [Enterprise Clean Deployment](#-enterprise-clean-deployment) - [Авторизация](#-авторизация) - [Мониторинг](#-мониторинг) @@ -216,6 +217,8 @@ docker compose -f dist/docker/docker-compose.light.yml up -d ## 🧪 Тестирование +### Запуск тестов + ```bash # Backend тесты cd backend && source .venv/bin/activate && pytest @@ -227,6 +230,43 @@ cd frontend && npm run test pytest tests/test_auth.py::test_create_user ``` +### 📊 Покрытие кода + +Сводный отчёт о покрытии генерируется скриптом `scripts/coverage-summary.sh`: + +```bash +# Полный запуск (backend integration + frontend) +./scripts/coverage-summary.sh + +# Backend unit-тесты (SQLite) + frontend (быстрее, не требует Docker) +./scripts/coverage-summary.sh --unit + +# Только frontend +./scripts/coverage-summary.sh --frontend-only + +# Только backend unit +./scripts/coverage-summary.sh --backend-only --unit + +# Указать директорию для отчёта +./scripts/coverage-summary.sh --output-dir ./reports/coverage +``` + +Скрипт выполняет: +1. Запуск backend-тестов (pytest) с `--cov=src` — unit (`--unit`) или integration (`--run-integration`) +2. Запуск frontend-тестов (vitest) с `--coverage` +3. Парсинг результатов тестов и процентов покрытия +4. Генерацию единого HTML-отчёта в `coverage-summary/index.html` со сводкой по обоим стекам + +**Текущие показатели:** + +| Стек | Тип тестов | Процент | Покрытие (Stmts) | +|------|-----------|---------|------------------| +| Backend (unit) | 1723 | 1721/2 ✅ | 48% | +| Backend (integration) | 167 | 167/0 ✅ | 12% | +| Frontend | 2443 | 2442/1 ✅ | 99.25% | + +> HTML-отчёты coverage по каждому стеку открываются из сводного отчёта по ссылкам. + ## 🏢 Enterprise Clean Deployment Для разворота в корпоративной сети с очищенным дистрибутивом (без тестовых данных, с запретом внешних источников и обязательной compliance-проверкой) используется профиль **enterprise clean**. diff --git a/docs/adr/ADR-0013-coverage-reporting.md b/docs/adr/ADR-0013-coverage-reporting.md new file mode 100644 index 00000000..5789b58f --- /dev/null +++ b/docs/adr/ADR-0013-coverage-reporting.md @@ -0,0 +1,104 @@ +# [DEF:ADR-0013:ADR] +# @STATUS ACTIVE +# @PURPOSE Зафиксировать архитектурное решение об автоматизации сбора покрытия кода, генерации сводного HTML-отчёта и интеграции скрипта `scripts/coverage-summary.sh` в процесс разработки. +# @RELATION DEPENDS_ON -> [ADR-0001-module-layout] +# @RELATION DEPENDS_ON -> [ADR-0006-frontend-architecture] +# @RELATION DEPENDS_ON -> [ADR-0011-async-backend] +# @RELATION DEPENDS_ON -> [ADR-0012-superset-testcontainers] +# @RATIONALE Проект ss-tools состоит из двух независимых стеков (Python/FastAPI backend, Svelte frontend), каждый со своим инструментом покрытия (coverage.py, vitest/v8). Разработчику приходится запускать их по отдельности и вручную сопоставлять результаты. Без единого скрипта: (a) покрытие проверяется нерегулярно, (b) регрессии покрытия остаются незамеченными до CI, (c) новые разработчики тратят время на изучение двух разных команд. +# @RATIONALE Единый скрипт `scripts/coverage-summary.sh` решает эти проблемы: (a) одна команда для полного цикла, (b) сводный HTML с визуальным статусом каждого стека, (c) парсинг результатов тестов и процентов покрытия в машиночитаемом виде, (d) детализированные HTML-отчёты coverage по каждому стеку. + +## Решение + +Создан скрипт `scripts/coverage-summary.sh`, который: + +### 1. Запуск backend-тестов + +- **Режим integration** (по умолчанию): `pytest tests/integration/ --run-integration --cov=src` — требует Docker + Testcontainers PostgreSQL +- **Режим unit**: `pytest tests/ --ignore=tests/integration/ --cov=src` — SQLite на tempfile, без Docker +- Парсинг: строка `TOTAL` из coverage.py → statements, missed, процент +- Парсинг: строка вида `X passed, Y failed` → количество тестов + +### 2. Запуск frontend-тестов + +- `vitest run --coverage --coverage.reportOnFailure` с v8 provider +- Парсинг: строка `All files | % Stmts | % Branch | % Funcs | % Lines` из текстового репортера +- Парсинг: строка `Tests: X passed, Y failed` → количество тестов + +### 3. Генерация сводного HTML + +Единый `index.html` со: +- Status bar: Overall / Backend / Frontend (цвета: success/failed) +- Карточка каждого стека: тесты (pass/total), покрытие (%), statements/missed +- Для frontend: Statements, Branch, Functions, Lines с цветовой индикацией (зелёный ≥80%, оранжевый 50-79%, красный <50%) +- Сводная таблица Combined Summary +- Ссылки на детализированные HTML-отчёты coverage каждого стека +- Timestamp генерации + +### 4. Режимы запуска + +| Флаг | Описание | +|------|----------| +| (без флагов) | Backend integration + frontend | +| `--unit` | Backend unit (SQLite) + frontend | +| `--backend-only` | Только backend | +| `--frontend-only` | Только frontend | +| `--backend-only --unit` | Только backend unit | +| `--skip-integration` | Только frontend (backend пропущен) | +| `--output-dir ` | Директория отчёта (по умолч. `coverage-summary/`) | + +## Альтернативы + +### @REJECTED Интеграция в CI (GitHub Actions) на данном этапе +Отвергнуто, так как проект ещё не имеет CI-инфраструктуры. Скрипт спроектирован так, чтобы его можно было легко запускать как локально, так и в CI. Добавление CI-шага будет выполнено отдельным ADR. + +### @REJECTED Единый инструмент покрытия (pytest-cov + vitest в одном плагине) +Отвергнуто из-за разной природы инструментов: coverage.py и vitest/v8 несовместимы на уровне форматов данных. Вместо этого используется гибридный подход: каждый инструмент генерирует свой HTML, а сводка собирается в единый индекс. + +### @REJECTED Замена vitest/v8 на istanbul +Отвергнуто: v8 provider работает быстрее (`@vitest/coverage-v8` уже установлен). Istanbul (`@vitest/coverage-istanbul`) доступен как опция, но не является default. + +## Детали реализации + +### Парсинг coverage.py + +``` +TOTAL 34845 18112 48% +``` + +Парсятся через `awk`: statements (колонка 2), missed (колонка 3), процент (колонка 4). + +### Парсинг vitest + +``` +All files | 99.25 | 87.39 | 98.91 | 99.48 +``` + +Парсятся через `awk -F'|'`: Stmts, Branch, Funcs, Lines. + +### Файловая структура отчёта + +``` +coverage-summary/ +├── index.html # Сводный отчёт +├── backend/ # HTML от coverage.py +│ ├── index.html +│ ├── ... +│ └── pytest_output.txt # Raw вывод pytest +└── frontend/ # HTML от vitest --coverage + ├── index.html + ├── ... + └── vitest_output.txt # Raw вывод vitest +``` + +### Зависимости + +- Backend: Python 3.9+, `.venv` с установленными `pytest-cov` (входит в `requirements-dev.txt`) +- Frontend: Node.js 18+, `node_modules` с `@vitest/coverage-v8` (входит в `devDependencies`) +- Docker: только для `--unit` не требуется; для integration требуется Docker Engine 24+ +- Скрипт написан на bash, не требует дополнительных утилит (кроме стандартных `grep`, `awk`, `sed`) + +## Статус + +**ACTIVE** — скрипт создан и протестирован. Используется для локальной проверки покрытия. +# [/DEF:ADR-0013:ADR] diff --git a/scripts/coverage-summary.sh b/scripts/coverage-summary.sh new file mode 100755 index 00000000..55f8675c --- /dev/null +++ b/scripts/coverage-summary.sh @@ -0,0 +1,510 @@ +#!/usr/bin/env bash +# #region Coverage.Summary [C:3] [TYPE Module] [SEMANTICS coverage,testing,reporting,ci] +# @BRIEF Run backend + frontend tests with coverage, generate unified HTML summary report. +# @LAYER DevOps +# @RELATION DEPENDS_ON -> [pytest] +# @RELATION DEPENDS_ON -> [vitest] +# @RELATION DEPENDS_ON -> [coverage.py] +# @POST Unified HTML report written to $OUTPUT_DIR/index.html +# @POST Backend HTML coverage at $OUTPUT_DIR/backend/ +# @POST Frontend HTML coverage at $OUTPUT_DIR/frontend/ +# @SIDE_EFFECT Runs tests; writes coverage artifacts to $OUTPUT_DIR/ +# +# Usage: ./scripts/coverage-summary.sh [options] +# +# Options: +# --backend-only Run only backend tests +# --frontend-only Run only frontend tests +# --unit Run backend unit tests instead of integration (skips Docker) +# --skip-integration Skip backend integration tests (requires --unit to run backend) +# --output-dir Output directory (default: coverage-summary) +# --help Show this help +# +# Examples: +# ./scripts/coverage-summary.sh # Full run (backend integration + frontend) +# ./scripts/coverage-summary.sh --unit # Backend unit + frontend +# ./scripts/coverage-summary.sh --frontend-only # Frontend only +# ./scripts/coverage-summary.sh --backend-only --unit # Backend unit tests only +# ./scripts/coverage-summary.sh --skip-integration # Frontend only (backend skipped entirely) +# #endregion Coverage.Summary + +# Do NOT use -e: test failures must not abort the script before report generation +set -uo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +# ── Defaults ───────────────────────────────────────────────────────────── +RUN_BACKEND=true +RUN_FRONTEND=true +BACKEND_MODE="integration" # integration | unit +OUTPUT_DIR="${PROJECT_DIR}/coverage-summary" +TIMESTAMP="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" + +# ── Parse args ─────────────────────────────────────────────────────────── +while [[ $# -gt 0 ]]; do + case "$1" in + --backend-only) RUN_FRONTEND=false; shift ;; + --frontend-only) RUN_BACKEND=false; shift ;; + --unit) BACKEND_MODE="unit"; shift ;; + --skip-integration) RUN_BACKEND=false; shift ;; + --output-dir) OUTPUT_DIR="$2"; shift 2 ;; + --help|-h) + echo "Usage: $(basename "$0") [options]" + echo "" + echo "Options:" + echo " --backend-only Run only backend tests" + echo " --frontend-only Run only frontend tests" + echo " --unit Run backend unit tests (SQLite, no Docker)" + echo " --skip-integration Skip backend integration tests entirely" + echo " --output-dir Output directory (default: coverage-summary)" + echo " --help Show this help" + echo "" + echo "Examples:" + echo " $0 # Full run (integration + frontend)" + echo " $0 --unit # Backend unit + frontend" + echo " $0 --frontend-only # Frontend only" + echo " $0 --backend-only --unit # Backend unit only" + echo " $0 --skip-integration # Frontend only" + exit 0 + ;; + *) echo "Error: unknown option $1"; exit 1 ;; + esac +done + +mkdir -p "$OUTPUT_DIR/backend" "$OUTPUT_DIR/frontend" + +# ── Helpers ────────────────────────────────────────────────────────────── +green() { echo -e "\033[0;32m$1\033[0m"; } +blue() { echo -e "\033[0;34m$1\033[0m"; } +red() { echo -e "\033[0;31m$1\033[0m"; } +bold() { echo -e "\033[1m$1\033[0m"; } + +# ── Default coverage values ────────────────────────────────────────────── +BACKEND_COVERAGE_PCT="N/A" +BACKEND_STMTS="N/A" +BACKEND_MISSED="N/A" +BACKEND_TESTS_PASSED="?" +BACKEND_TESTS_FAILED="?" +BACKEND_TESTS_TOTAL="?" + +FRONTEND_COVERAGE_STMTS="N/A" +FRONTEND_COVERAGE_BRANCH="N/A" +FRONTEND_COVERAGE_FUNCS="N/A" +FRONTEND_COVERAGE_LINES="N/A" +FRONTEND_TESTS_PASSED="?" +FRONTEND_TESTS_FAILED="?" +FRONTEND_TESTS_TOTAL="?" + +# ── Backend coverage ───────────────────────────────────────────────────── +run_backend() { + echo "" + bold "━━━ Backend ($BACKEND_MODE) ━━━" + + cd "$PROJECT_DIR/backend" + + if [ ! -f ".venv/bin/activate" ]; then + red "Error: backend/.venv not found. Run: cd backend && python3 -m venv .venv" + exit 1 + fi + + source .venv/bin/activate + + local pytest_args=() + local cov_args=(--cov=src --cov-report=term --cov-report=html:"$OUTPUT_DIR/backend") + + if [ "$BACKEND_MODE" = "integration" ]; then + if ! docker ps >/dev/null 2>&1; then + red "Docker is not running. Integration tests require Testcontainers." + red " Use --unit for SQLite-based unit tests or start Docker." + return 1 + fi + pytest_args=(tests/integration/ --run-integration --ignore=tests/integration/test_superset_tls_custom_ca.py) + else + pytest_args=(tests/ --ignore=tests/integration/) + fi + + blue "Running: python -m pytest ${pytest_args[*]} ${cov_args[*]}" + + local out + set +e + out=$(python -m pytest "${pytest_args[@]}" "${cov_args[@]}" -q 2>&1) + set -e + echo "$out" > "$OUTPUT_DIR/backend/pytest_output.txt" + + # Parse summary: "X passed" or "X passed, Y failed, Z warnings" + if echo "$out" | grep -qP '^\d+ passed'; then + BACKEND_TESTS_PASSED=$(echo "$out" | grep -oP '^\d+(?= passed)') + if echo "$out" | grep -qP '\d+ failed'; then + BACKEND_TESTS_FAILED=$(echo "$out" | grep -oP '\d+(?= failed)' | tail -1) + else + BACKEND_TESTS_FAILED=0 + fi + BACKEND_TESTS_TOTAL=$((BACKEND_TESTS_PASSED + BACKEND_TESTS_FAILED)) + else + local summary + summary=$(echo "$out" | grep -E "(passed|failed)" | tail -1) + if echo "$summary" | grep -qP '\d+ passed'; then + BACKEND_TESTS_PASSED=$(echo "$summary" | grep -oP '\d+(?= passed)') + BACKEND_TESTS_FAILED=$(echo "$summary" | grep -oP '\d+(?= failed)' || echo "0") + BACKEND_TESTS_TOTAL=$((BACKEND_TESTS_PASSED + BACKEND_TESTS_FAILED)) + fi + fi + + # Parse TOTAL from coverage: "TOTAL 33686 29679 12%" + local total_line + total_line=$(echo "$out" | grep "^TOTAL" | tail -1) + if [ -n "$total_line" ]; then + BACKEND_STMTS=$(echo "$total_line" | awk '{print $2}') + BACKEND_MISSED=$(echo "$total_line" | awk '{print $3}') + BACKEND_COVERAGE_PCT=$(echo "$total_line" | awk '{print $4}' | tr -d '%') + fi + + green " Tests: $BACKEND_TESTS_PASSED passed / ${BACKEND_TESTS_FAILED:-0} failed" + green " Coverage: ${BACKEND_COVERAGE_PCT}% ($BACKEND_STMTS stmts, $BACKEND_MISSED missed)" + cd "$PROJECT_DIR" +} + +# ── Frontend coverage ──────────────────────────────────────────────────── +run_frontend() { + echo "" + bold "━━━ Frontend ━━━" + + cd "$PROJECT_DIR/frontend" + + if [ ! -d "node_modules" ]; then + red "Error: frontend/node_modules not found. Run: cd frontend && npm install" + exit 1 + fi + + blue "Running: npx vitest run --coverage --coverage.reportOnFailure" + + local out + set +e + out=$(npx vitest run --coverage --coverage.reportOnFailure \ + --coverage.reporter=text \ + --coverage.reporter=html \ + --coverage.reportsDirectory="$OUTPUT_DIR/frontend" 2>&1) + set -e + echo "$out" > "$OUTPUT_DIR/frontend/vitest_output.txt" + + # Parse test results + local summary_line + summary_line=$(echo "$out" | grep -E "^\s*Tests\s+" | tail -1) + if [ -n "$summary_line" ]; then + FRONTEND_TESTS_PASSED=$(echo "$summary_line" | grep -oP '\d+(?= passed)') + FRONTEND_TESTS_FAILED=$(echo "$summary_line" | grep -oP '\d+(?= failed)') + FRONTEND_TESTS_TOTAL=$((FRONTEND_TESTS_PASSED + FRONTEND_TESTS_FAILED)) + fi + + # Parse coverage: "All files | 99.25 | 87.39 | 98.91 | 99.48" + local cov_line + cov_line=$(echo "$out" | grep "All files" | head -1) + if [ -n "$cov_line" ]; then + FRONTEND_COVERAGE_STMTS=$(echo "$cov_line" | awk -F'|' '{print $2}' | xargs) + FRONTEND_COVERAGE_BRANCH=$(echo "$cov_line" | awk -F'|' '{print $3}' | xargs) + FRONTEND_COVERAGE_FUNCS=$(echo "$cov_line" | awk -F'|' '{print $4}' | xargs) + FRONTEND_COVERAGE_LINES=$(echo "$cov_line" | awk -F'|' '{print $5}' | xargs) + fi + + green " Tests: $FRONTEND_TESTS_PASSED passed / ${FRONTEND_TESTS_FAILED:-0} failed" + green " Coverage: Stmts ${FRONTEND_COVERAGE_STMTS}% | Branch ${FRONTEND_COVERAGE_BRANCH}% | Funcs ${FRONTEND_COVERAGE_FUNCS}% | Lines ${FRONTEND_COVERAGE_LINES}%" + cd "$PROJECT_DIR" +} + +# ── Generate HTML Report ───────────────────────────────────────────────── +generate_report() { + echo "" + bold "━━━ Generating HTML Report ━━━" + + # Determine status + local backend_status="success" + local frontend_status="success" + local overall_status="success" + + if [ "${BACKEND_TESTS_FAILED:-0}" != "?" ] && [ "${BACKEND_TESTS_FAILED:-0}" -gt 0 ]; then + backend_status="failed" + overall_status="failed" + fi + if [ "${FRONTEND_TESTS_FAILED:-0}" != "?" ] && [ "${FRONTEND_TESTS_FAILED:-0}" -gt 0 ]; then + frontend_status="failed" + overall_status="failed" + fi + + # Coverage color helper + cov_color() { + local pct=$1 + if [ "$pct" = "N/A" ]; then echo "gray"; return; fi + local int_pct + int_pct=$(printf "%.0f" "$pct" 2>/dev/null || echo 0) + if [ "$int_pct" -ge 80 ]; then echo "green" + elif [ "$int_pct" -ge 50 ]; then echo "orange" + else echo "red"; fi + } + + # Collect which stacks + local subtitle_parts=() + [ "$RUN_BACKEND" = true ] && subtitle_parts+=("Backend (${BACKEND_MODE})") + [ "$RUN_FRONTEND" = true ] && subtitle_parts+=("Frontend") + local subtitle + subtitle=$(IFS=" + "; echo "${subtitle_parts[*]}") + + local backend_cov_color + backend_cov_color=$(cov_color "$BACKEND_COVERAGE_PCT") + + # Build status pills + local status_pills="" + status_pills+="Overall: ${overall_status}" + if [ "$RUN_BACKEND" = true ]; then + status_pills+="Backend: ${backend_status}" + fi + if [ "$RUN_FRONTEND" = true ]; then + status_pills+="Frontend: ${frontend_status}" + fi + + # Build cards + local cards_html="" + + if [ "$RUN_BACKEND" = true ]; then + cards_html+=" +
+
+

🐍 Backend

+ ${BACKEND_MODE} +
+
+
+
+
${BACKEND_COVERAGE_PCT}%
+
Coverage
+
+
+
${BACKEND_TESTS_PASSED}/${BACKEND_TESTS_TOTAL}
+
Tests (pass/total)
+
+
+
${BACKEND_STMTS}
+
Statements
+
+
+
${BACKEND_MISSED}
+
Missed
+
+
+ +
+
" + fi + + if [ "$RUN_FRONTEND" = true ]; then + local f_stmts="${FRONTEND_COVERAGE_STMTS:-0}" + local f_branch="${FRONTEND_COVERAGE_BRANCH:-0}" + local f_funcs="${FRONTEND_COVERAGE_FUNCS:-0}" + local f_lines="${FRONTEND_COVERAGE_LINES:-0}" + local f_stmts_c + f_stmts_c=$(cov_color "$f_stmts") + local f_branch_c + f_branch_c=$(cov_color "$f_branch") + local f_funcs_c + f_funcs_c=$(cov_color "$f_funcs") + local f_lines_c + f_lines_c=$(cov_color "$f_lines") + + cards_html+=" +
+
+

🎨 Frontend

+ Svelte +
+
+
+
+
${f_stmts}%
+
Statements
+
+
+
${f_branch}%
+
Branch
+
+
+
${f_funcs}%
+
Functions
+
+
+
${f_lines}%
+
Lines
+
+
+
+ ${FRONTEND_TESTS_PASSED}/${FRONTEND_TESTS_TOTAL} + tests passed +
+ +
+
" + fi + + # Build links + local links_html="" + [ "$RUN_BACKEND" = true ] && links_html+="Backend Coverage Details" + [ "$RUN_FRONTEND" = true ] && links_html+="Frontend Coverage Details" + + # Build combined table rows + local table_rows="" + if [ "$RUN_BACKEND" = true ]; then + table_rows+=" + + Backend (Python) + ${BACKEND_MODE} + ${BACKEND_TESTS_PASSED} + ${BACKEND_TESTS_FAILED:-0} + ${BACKEND_COVERAGE_PCT}% + — + " + fi + if [ "$RUN_FRONTEND" = true ]; then + table_rows+=" + + Frontend (Svelte) + unit + ${FRONTEND_TESTS_PASSED} + ${FRONTEND_TESTS_FAILED:-0} + ${FRONTEND_COVERAGE_STMTS}% + ${FRONTEND_COVERAGE_BRANCH}% + " + fi + + # ── Write HTML (using quoted heredoc to prevent bash expansion) ───── + cat > "$OUTPUT_DIR/index.html" << HTML_EOF + + + + + +Coverage Summary — ss-tools + + + +
+

Coverage Report — ss-tools

+
${subtitle}
+
Generated: ${TIMESTAMP}
+ +
${status_pills}
+ +
${cards_html}
+ +

Combined Summary

+
+ + + + + + + + + + + + ${table_rows} +
StackModeTests PassedTests FailedCoverage %Branch %
+
+ + + +

+ Generated by scripts/coverage-summary.sh +

+
+ + +HTML_EOF + + green " Report saved: $OUTPUT_DIR/index.html" +} + +# ── Main ───────────────────────────────────────────────────────────────── +echo "" +bold "╔══════════════════════════════════════════════╗" +bold "║ ss-tools Coverage Summary Generator ║" +bold "╚══════════════════════════════════════════════╝" + +if [ "$RUN_BACKEND" = false ] && [ "$RUN_FRONTEND" = false ]; then + red "Error: nothing to run. Specify at least one of --backend-only, --frontend-only." + exit 1 +fi + +[ "$RUN_BACKEND" = true ] && run_backend +[ "$RUN_FRONTEND" = true ] && run_frontend + +generate_report + +echo "" +green "Done! Open: file://${OUTPUT_DIR}/index.html" +echo ""