""" Сохранение результатов тестов в БД (таблица 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) # ВСЕГДА возвращаем соединение в пул