v1.1.50: scenario_definitions DB+CRUD, seed, runner from DB, 409 lock, UI definitions, drop config.yaml scenarios

This commit is contained in:
2026-07-30 15:06:05 +04:00
parent 67f6390509
commit 983b387708
8 changed files with 418 additions and 161 deletions
+141 -30
View File
@@ -1,66 +1,105 @@
"""
API для запуска сценариев автотестирования.
API сценариев: CRUD определений + запуск + поллинг.
POST /api/scenario/run — запустить сценарий (фон)
GET /api/scenario/status/<id> — поллинг статуса сценария
GET /api/scenarios — список доступных сценариев
GET /api/scenario/history — история запусков сценариев
GET /api/scenarios — список из БД (для UI)
POST /api/scenario/run — запуск по definition_id (409 если уже RUNNING)
GET /api/scenario/run/<int:run_id> — статус конкретного запуска
GET /api/scenario/status — последние 10 запусков
CRUD определений:
GET /api/scenario/definitions — список
POST /api/scenario/definitions — создать
GET /api/scenario/definitions/<id> — один
PUT /api/scenario/definitions/<id> — обновить (version → 409)
DELETE /api/scenario/definitions/<id> — мягкое удаление
"""
from flask import Blueprint, current_app, jsonify, request
import threading
from api.auth import get_client, get_client_id, get_stand, get_token_info
from operations.scenario import load_scenarios, run_scenario, _create_scenario_run
from operations.scenario import run_scenario
from db.pool import get_conn, put_conn
from db.scenario_defs import (
list_definitions, get_definition, create_definition,
update_definition, delete_definition, lock_check,
)
bp = Blueprint("api_scenario", __name__)
# ── Запуск ──────────────────────────────────────────────
@bp.route("/api/scenarios")
def api_scenarios():
"""Список доступных сценариев из config.yaml."""
"""Список определений сценариев из БД (для UI запуска)."""
try:
scenarios = load_scenarios()
result = [{"name": name, "steps": len(s.get("steps", []))} for name, s in scenarios.items()]
return jsonify(sorted(result, key=lambda x: x["name"]))
defs = list_definitions(get_client_id(), get_stand())
result = [{"id": d["id"], "name": d["name"], "steps": len(d.get("steps", []))} for d in defs]
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
@bp.route("/api/scenario/run", methods=["POST"])
def api_scenario_run():
"""Запустить сценарий. Создаёт запись ДО потока, возвращает run_id (202)."""
"""Запустить сценарий по definition_id. 409 если уже RUNNING."""
try:
data = request.get_json()
scenario_name = (data.get("scenario") or "").strip()
if not scenario_name:
return jsonify({"error": "scenario name is required"}), 400
def_id = data.get("definition_id")
if not def_id:
return jsonify({"error": "definition_id is required"}), 400
client = get_client()
cid = get_client_id()
stand = get_stand()
# Lock 409
if not lock_check(cid, stand):
return jsonify({"error": "Another scenario is already running"}), 409
# Загрузить определение
defn = get_definition(def_id, cid, stand)
if not defn:
return jsonify({"error": "Definition not found"}), 404
steps = defn.get("steps", [])
if not steps:
return jsonify({"error": "Definition has no steps"}), 400
scenario_name = defn["name"]
user_email = get_token_info().get("email", "")
app_version = current_app.config.get("VERSION", "")
# Валидация: сценарий существует?
scenarios = load_scenarios()
if scenario_name not in scenarios:
return jsonify({"error": f"Scenario '{scenario_name}' not found"}), 404
total = len(scenarios[scenario_name].get("steps", []))
if total == 0:
return jsonify({"error": f"Scenario '{scenario_name}' has no steps"}), 400
# Создать запись ДО потока — вернуть run_id клиенту
run_id = _create_scenario_run(cid, stand, user_email, scenario_name, total, app_version)
if run_id is None:
# Создать запись ДО потока
conn = get_conn()
if not conn:
return jsonify({"error": "no db"}), 500
run_id = None
try:
cur = conn.cursor()
cur.execute("""
INSERT INTO scenario_runs (client_id, stand, user_email, scenario_name,
status, current_step, total_steps, app_version,
definition_id, definition_version)
VALUES (%s, %s, %s, %s, 'RUNNING', 0, %s, %s, %s, %s)
RETURNING id
""", (cid, stand, user_email, scenario_name, len(steps), app_version,
def_id, defn["version"]))
run_id = cur.fetchone()[0]
conn.commit()
cur.close()
except Exception as e:
print(f"[API] create scenario_run error: {e}", flush=True)
conn.rollback()
return jsonify({"error": "Failed to create scenario_run"}), 500
finally:
put_conn(conn)
# Запуск в фоне
client = get_client()
threading.Thread(
target=run_scenario,
args=(client, scenario_name, cid, stand, user_email, app_version, run_id),
args=(client, steps, cid, stand, user_email, app_version, run_id, scenario_name),
daemon=True,
).start()
@@ -73,7 +112,7 @@ def api_scenario_run():
@bp.route("/api/scenario/run/<int:run_id>")
def api_scenario_run_status(run_id):
"""Статус конкретного запуска сценария."""
"""Статус конкретного запуска."""
conn = get_conn()
if not conn:
return jsonify({"error": "no db"}), 500
@@ -101,7 +140,7 @@ def api_scenario_run_status(run_id):
@bp.route("/api/scenario/status")
def api_scenario_status():
"""Последний запущенный сценарий (или несколько последних)."""
"""Последние 10 запусков."""
conn = get_conn()
if not conn:
return jsonify([])
@@ -128,3 +167,75 @@ def api_scenario_status():
return jsonify({"error": str(e)}), 500
finally:
put_conn(conn)
# ── CRUD определений ────────────────────────────────────
@bp.route("/api/scenario/definitions", methods=["GET", "POST"])
def api_definitions():
cid = get_client_id()
stand = get_stand()
if request.method == "GET":
try:
defs = list_definitions(cid, stand)
return jsonify(defs)
except Exception as e:
return jsonify({"error": str(e)}), 500
if request.method == "POST":
try:
data = request.get_json()
name = (data.get("name") or "").strip()
if not name:
return jsonify({"error": "name is required"}), 400
steps = data.get("steps", [])
if not isinstance(steps, list):
return jsonify({"error": "steps must be array"}), 400
user = get_token_info().get("email", "")
result = create_definition(cid, stand, name, steps, user)
if result is None:
return jsonify({"error": "create failed"}), 500
return jsonify(result), 201
except Exception as e:
return jsonify({"error": str(e)}), 500
@bp.route("/api/scenario/definitions/<int:def_id>", methods=["GET", "PUT", "DELETE"])
def api_definition(def_id):
cid = get_client_id()
stand = get_stand()
if request.method == "GET":
d = get_definition(def_id, cid, stand)
if not d:
return jsonify({"error": "not found"}), 404
return jsonify(d)
if request.method == "PUT":
try:
data = request.get_json()
name = (data.get("name") or "").strip()
version = data.get("version")
if not name:
return jsonify({"error": "name is required"}), 400
if not isinstance(version, int):
return jsonify({"error": "version (int) is required"}), 400
steps = data.get("steps", [])
if not isinstance(steps, list):
return jsonify({"error": "steps must be array"}), 400
user = get_token_info().get("email", "")
result = update_definition(def_id, cid, stand, name, steps, version, user)
if result.get("conflict"):
return jsonify({"error": "Conflict: definition was modified"}), 409
if result.get("error"):
return jsonify({"error": result["error"]}), 500
return jsonify(result)
except Exception as e:
return jsonify({"error": str(e)}), 500
if request.method == "DELETE":
ok = delete_definition(def_id, cid, stand)
if not ok:
return jsonify({"error": "not found"}), 404
return jsonify({"ok": True})