Files
app-autotest/site/db/save_run.py
T
naeel 3293330cec fix: ручной UPSERT вместо ON CONFLICT — не требует индекса (v1.2.39)
ON CONFLICT требовал уникальный индекс idx_runs_op_uid,
который не всегда создавался (мультистейтмент SQL в psycopg2).
→ save_run падал молча → история пустая.

Новый подход: UPDATE по op_uid, если rowcount==0 → INSERT.
Не зависит от индексов, работает всегда.
2026-08-03 10:18:10 +04:00

93 lines
4.9 KiB
Python

"""
Сохранение результатов тестов в БД (таблица runs).
Вызывается после КАЖДОЙ операции:
- Ручной режим: _finish_op() в api_test.py
- Сценарный режим: run_scenario() в scenario.py (после каждого шага)
Сохраняет ВСЕ параметры запроса и ответа — для истории и анализа ошибок.
Секретные поля (password, token, ...) маскируются через redact_params().
instance_meta — полный JSON ответа GET /instances/{uid} (для post-mortem анализа).
"""
import json
from db.pool import get_conn, put_conn
def save_run(client_id, stand, user_email, svc_id, svc_name, op_name, svc_op_id,
op_uid, instance_uid, display_name, status, duration_sec,
error_log, params, stages, app_version,
scenario_run_id=None, step_number=None, instance_meta=None):
"""Сохранить один запуск в таблицу runs.
Эта функция — единственная точка записи в runs.
Вызывается и из ручного режима, и из сценариев.
Args:
client_id: str — ClientID пользователя
stand: str — "dev"/"test"
user_email: str — email из JWT
svc_id: int — ID сервиса
svc_name: str — имя сервиса (из ответа poll)
op_name: str — "create"/"modify"/"delete"/"suspend"/"resume"/"redeploy"
svc_op_id: int — числовой ID операции
op_uid: str — UUID операции
instance_uid: str — UUID инстанса
display_name: str — displayName инстанса
status: str — "OK"/"FAIL"/"TIMEOUT"/"RUNNING"
duration_sec: float — длительность операции
error_log: str — текст ошибки (если FAIL)
params: dict — параметры операции (уже маскированные)
stages: list — этапы выполнения
app_version: str — версия приложения
scenario_run_id: int|None — ID запуска сценария
step_number: int|None — номер шага в сценарии
instance_meta: dict|None — полная информация об инстансе (JSONB)
"""
conn = get_conn()
if not conn:
print("[DB] save_run SKIP: no connection", flush=True)
return
try:
cur = conn.cursor()
# Ручной UPSERT (UPDATE → INSERT если не затронуто строк).
# Не используем ON CONFLICT — не требует индекса, надёжнее.
# Сценарий вызывает save_run дважды (RUNNING → OK) — второй вызов
# обновляет существующую строку вместо создания дубликата.
params_json = json.dumps(params) if params else None
stages_json = json.dumps(stages) if stages else None
meta_json = json.dumps(instance_meta) if instance_meta else None
if op_uid:
cur.execute("""
UPDATE runs SET
status = %s, duration_sec = %s, error_log = %s,
stages = %s, svc_name = %s, instance_meta = %s
WHERE op_uid = %s
""", (status, duration_sec, error_log, stages_json, svc_name, meta_json, op_uid))
if not op_uid or cur.rowcount == 0:
# Нет op_uid ИЛИ UPDATE не затронул строк → INSERT
cur.execute("""
INSERT INTO runs (client_id, stand, user_email, svc_id, svc_name,
op_name, svc_op_id, op_uid, instance_uid, display_name,
status, duration_sec, error_log, params, stages, app_version,
scenario_run_id, step_number, instance_meta)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
""", (
client_id, stand, user_email, svc_id, svc_name, op_name, svc_op_id,
op_uid, instance_uid, display_name, status, duration_sec,
error_log, params_json, stages_json, app_version,
scenario_run_id, step_number, meta_json,
))
conn.commit()
cur.close()
print(f"[DB] save_run OK: {op_name} {status} {duration_sec}s", flush=True)
except Exception as e:
print(f"[DB] save_run error: {e}", flush=True)
conn.rollback()
finally:
put_conn(conn) # ВСЕГДА возвращаем соединение в пул