261 lines
10 KiB
Markdown
261 lines
10 KiB
Markdown
# История тестов и просмотр результатов — план реализации
|
||
|
||
> Написано 2026-07-26 для ревью Соннетом перед реализацией.
|
||
|
||
## Контекст
|
||
|
||
Приложение `app-autotest` (Flask, `site/app.py`) — автотесты операций облачных сервисов Nubes.
|
||
Работает с dummy-сервисом (svcId=1, Болванка). Уже реализовано:
|
||
|
||
- UI: инфраструктура, сервисы, инстансы, кнопки операций, стадии в реальном времени
|
||
- Бэкенд: полный поток CREATE/MODIFY/SUSPEND/DELETE/RESUME/REDEPLOY 1:1 с Terraform-провайдером
|
||
- Трекер: `site/operations/tracker.py` — `/tmp/instances.json` (только инстансы созданные приложением)
|
||
- Поллинг: `/api/test` возвращает `opUid` сразу, `/api/test/status/<opUid>` — стадии
|
||
|
||
## Проблема
|
||
|
||
В deck-test (UI облака) для каждого инстанса хранится **полная история операций**:
|
||
список всех modify/suspend/delete/... с датами, статусами, этапами, параметрами. Можно открыть любую операцию и посмотреть как выполнялась.
|
||
|
||
У нас история **не сохраняется**. После завершения операции результат теряется. Нельзя:
|
||
- Посмотреть что и когда запускали
|
||
- Сравнить два запуска
|
||
- Понять какая операция упала и почему
|
||
|
||
## Цель
|
||
|
||
Сделать аналог истории deck-test, **но лучше** — потому что наше приложение не просто UI для сервиса, а **инструмент тестирования**.
|
||
|
||
## Что есть в deck-test (базовый минимум)
|
||
|
||
| Фича | Описание |
|
||
|------|---------|
|
||
| Список операций инстанса | Таблица: UUID, название, дата, код ответа, начало, окончание |
|
||
| Детали операции | Параметры, этапы (stages), статус, длительность, ошибка |
|
||
| Фильтр по типу | Показать только modify / только suspend / ... |
|
||
|
||
## Что МОЖЕМ сделать лучше (тест-ориентированные фичи)
|
||
|
||
| Фича | Зачем |
|
||
|------|-------|
|
||
| **Сводная таблица всех тестов** (кросс-инстанс) | Не прыгать между инстансами — видеть всё в одном месте |
|
||
| **Статистика:** % успешных, среднее время по операции | Быстро понять что ломается чаще всего |
|
||
| **Сравнение двух запусков** | «После modify время выросло на 2 секунды» |
|
||
| **Экспорт в JSON** | Забрать результаты в CI/CD пайплайн |
|
||
| **Повторный запуск** из истории (те же параметры) | Не заполнять форму заново |
|
||
| **Таймлайн** с цветовой кодировкой | Визуально видеть последовательность: create → modify → suspend → resume → delete |
|
||
|
||
## Архитектура хранения
|
||
|
||
Сейчас: `instances.json` (JSON-файл в `/tmp/`). Для истории нужна БД.
|
||
|
||
### Варианты
|
||
|
||
1. **SQLite** в `/tmp/autotest_results.db` — ноль зависимостей, встроен в Python
|
||
2. JSON-файл `/tmp/results.json` — проще, но неудобно для запросов/фильтрации
|
||
|
||
**Выбран SQLite** — фильтрация, сортировка, аггрегация (статистика).
|
||
|
||
### Схема БД
|
||
|
||
```sql
|
||
CREATE TABLE test_runs (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
instance_uid TEXT NOT NULL,
|
||
instance_name TEXT NOT NULL,
|
||
operation TEXT NOT NULL, -- create/modify/suspend/delete/resume/redeploy
|
||
svc_operation_id INTEGER NOT NULL,
|
||
op_uid TEXT NOT NULL, -- UUID операции в облаке
|
||
status TEXT NOT NULL, -- OK/FAIL/TIMEOUT
|
||
error TEXT DEFAULT '',
|
||
duration_ms REAL DEFAULT 0, -- миллисекунды
|
||
started_at TEXT NOT NULL, -- ISO datetime
|
||
finished_at TEXT DEFAULT ''
|
||
);
|
||
|
||
CREATE TABLE test_stages (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
test_run_id INTEGER NOT NULL REFERENCES test_runs(id),
|
||
stage_name TEXT NOT NULL, -- "1. Валидация"
|
||
is_successful INTEGER, -- 0/1/NULL
|
||
duration_sec REAL DEFAULT 0,
|
||
stage_msg TEXT DEFAULT '',
|
||
sort_order INTEGER DEFAULT 0
|
||
);
|
||
|
||
CREATE TABLE test_params (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
test_run_id INTEGER NOT NULL REFERENCES test_runs(id),
|
||
param_name TEXT NOT NULL,
|
||
param_value TEXT NOT NULL
|
||
);
|
||
```
|
||
|
||
### Почему SQLite а не `/tmp/*.json`
|
||
|
||
- `/tmp/` на поде **не затирается** при clone (в отличие от `site/`)
|
||
- SQLite даёт `WHERE`, `ORDER BY`, `GROUP BY` из коробки
|
||
- Файл БД можно скачать для офлайн-анализа
|
||
- Нет дополнительных зависимостей (sqlite3 встроен в Python)
|
||
|
||
## API эндпоинты
|
||
|
||
### `GET /api/results`
|
||
|
||
Параметры:
|
||
- `instance_uid` — фильтр по инстансу
|
||
- `operation` — фильтр по типу
|
||
- `status` — OK/FAIL/TIMEOUT
|
||
- `limit` — сколько (по умолчанию 50)
|
||
- `offset` — смещение
|
||
|
||
Ответ:
|
||
```json
|
||
{
|
||
"total": 42,
|
||
"results": [
|
||
{
|
||
"id": 1,
|
||
"instance_uid": "884356cf-...",
|
||
"instance_name": "autotest-1-first",
|
||
"operation": "modify",
|
||
"status": "OK",
|
||
"duration_ms": 4200,
|
||
"started_at": "2026-07-26T14:12:18",
|
||
"stages_count": 5,
|
||
"stages_failed": 0
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
### `GET /api/results/<id>`
|
||
|
||
Полные детали: все стадии + все параметры.
|
||
|
||
### `DELETE /api/results/<id>`
|
||
|
||
Удалить запись.
|
||
|
||
### `GET /api/results/stats`
|
||
|
||
Аггрегация по операциям:
|
||
```json
|
||
{
|
||
"modify": {"total": 10, "ok": 9, "fail": 1, "avg_duration_ms": 5200},
|
||
"suspend": {"total": 8, "ok": 8, "fail": 0, "avg_duration_ms": 3400}
|
||
}
|
||
```
|
||
|
||
## Интеграция с текущим кодом
|
||
|
||
### `site/operations/results_db.py` (новый)
|
||
|
||
```python
|
||
import sqlite3
|
||
import os
|
||
|
||
DB_PATH = "/tmp/autotest_results.db"
|
||
|
||
def get_db():
|
||
conn = sqlite3.connect(DB_PATH)
|
||
conn.row_factory = sqlite3.Row
|
||
_ensure_tables(conn)
|
||
return conn
|
||
|
||
def save_result(instance_uid, instance_name, operation, svc_op_id, op_uid, status, error, duration_ms, stages, params):
|
||
...
|
||
|
||
def get_results(instance_uid=None, operation=None, status=None, limit=50, offset=0):
|
||
...
|
||
|
||
def get_result(id):
|
||
...
|
||
|
||
def delete_result(id):
|
||
...
|
||
|
||
def get_stats():
|
||
...
|
||
```
|
||
|
||
### Изменения в `site/routes/api_test.py`
|
||
|
||
В `_finish_op()` — после завершения операции:
|
||
```python
|
||
from operations.results_db import save_result
|
||
save_result(
|
||
instance_uid=instance_uid,
|
||
instance_name=display_name,
|
||
operation="modify",
|
||
svc_op_id=svc_op_id,
|
||
op_uid=op_uid,
|
||
status=status,
|
||
error=error,
|
||
duration_ms=duration_ms,
|
||
stages=stages,
|
||
params=params
|
||
)
|
||
```
|
||
|
||
Добавить 4 новых роута для `/api/results*`.
|
||
|
||
## UI
|
||
|
||
### Новая карточка «История тестов»
|
||
|
||
Под карточкой «Инстансы»:
|
||
|
||
```
|
||
┌─────────────────────────────────────────┐
|
||
│ История тестов │
|
||
│ [Все▼] [create] [modify] [suspend] ... │ ← фильтры-кнопки
|
||
│ │
|
||
│ # Инстанс Операция Рез-т │
|
||
│ 1 autotest-1-first modify ✅ 4.2s │ ← кликабельно
|
||
│ 2 curl-test-144834 suspend ✅ 3.1s │
|
||
│ 3 autotest-1-first delete ❌ err │
|
||
│ │
|
||
│ Статистика: modify 9/10 OK (90%) ... │
|
||
└─────────────────────────────────────────┘
|
||
```
|
||
|
||
Клик по строке → раскрываются детали (стадии + параметры).
|
||
|
||
### Цветовая кодировка
|
||
|
||
- ✅ зелёный — OK
|
||
- ❌ красный — FAIL
|
||
- ⏱ жёлтый — TIMEOUT
|
||
|
||
## План реализации (фазы)
|
||
|
||
### Фаза 1: база + сохранение (MVP)
|
||
|
||
1. `site/operations/results_db.py` — модуль SQLite
|
||
2. `_finish_op()` — сохранение результата
|
||
3. `GET /api/results` — базовый список
|
||
4. UI — простая таблица без фильтров
|
||
|
||
### Фаза 2: детали + фильтры
|
||
|
||
1. `GET /api/results/<id>` — детали
|
||
2. `GET /api/results/stats` — статистика
|
||
3. UI — фильтры, раскрытие деталей, статистика
|
||
|
||
### Фаза 3: продвинутые фичи
|
||
|
||
1. Экспорт JSON
|
||
2. Повторный запуск из истории
|
||
3. Таймлайн
|
||
4. Сравнение запусков
|
||
|
||
## Вопросы к Соннету
|
||
|
||
1. SQLite в `/tmp/` на поде — насколько это надёжно для тестовой истории? Может лучше PostgreSQL-сервис Nubes?
|
||
2. Схема БД — нормально или что-то упущено?
|
||
3. Стоит ли хранить `stageMsg` (большие JSON-строки с логами Jenkins)?
|
||
4. API эндпоинты — достаточный набор или добавить что-то?
|
||
5. UI — есть ли смысл делать SPA-подобный роутинг или оставить серверный рендеринг как сейчас?
|
||
6. Экспорт результатов — JSON потоком или файлом?
|