v1.1.50: scenario_definitions DB+CRUD, seed, runner from DB, 409 lock, UI definitions, drop config.yaml scenarios
This commit is contained in:
+141
-30
@@ -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})
|
||||
|
||||
Reference in New Issue
Block a user