""" API endpoints для тестирования ролей Автор: Сергей Антропов Сайт: https://devops.org.ru """ from fastapi import APIRouter, Request, WebSocket, WebSocketDisconnect, Depends from fastapi.responses import HTMLResponse from fastapi.templating import Jinja2Templates from pathlib import Path from typing import Dict, Optional import json from sqlalchemy.ext.asyncio import AsyncSession from app.core.config import settings from app.core.molecule_executor import MoleculeExecutor from app.services.preset_service import PresetService from app.db.session import get_async_db router = APIRouter() templates_path = Path(__file__).parent.parent.parent.parent / "templates" templates = Jinja2Templates(directory=str(templates_path)) molecule_executor = MoleculeExecutor() @router.get("/roles/{role_name}/test", response_class=HTMLResponse) async def test_role_page( request: Request, role_name: str, db: AsyncSession = Depends(get_async_db) ): """Страница тестирования роли""" # Проверка существования роли roles_dir = settings.PROJECT_ROOT / "roles" / role_name if not roles_dir.exists(): from fastapi import HTTPException raise HTTPException(status_code=404, detail=f"Роль '{role_name}' не найдена") # Получение списка preset'ов из БД presets = await PresetService.get_all_presets(db) return templates.TemplateResponse( "pages/roles/test.html", { "request": request, "role_name": role_name, "presets": presets } ) @router.post("/api/v1/roles/{role_name}/test") async def start_role_test( role_name: str, preset: str = "default", variables: Optional[str] = None ): """Запуск теста роли""" # TODO: Запуск через Celery для фонового выполнения # Пока просто возвращаем информацию return { "success": True, "role_name": role_name, "preset": preset, "message": "Тест запущен", "test_id": f"{role_name}-{preset}" } @router.get("/tests", response_class=HTMLResponse) async def tests_history_page( request: Request, role_name: Optional[str] = None, page: int = 1, per_page: int = 20, db: AsyncSession = Depends(get_async_db) ): """Страница истории тестов""" try: from app.services.history_service import HistoryService from sqlalchemy import select, func from app.models.database import CommandHistory, TestResult history_service = HistoryService() # Получение тестов из БД query = select(CommandHistory).where( CommandHistory.command_type == "test" ) if role_name: query = query.where(CommandHistory.command.like(f"%{role_name}%")) query = query.order_by(CommandHistory.created_at.desc()) # Подсчет общего количества count_query = select(func.count(CommandHistory.id)).where( CommandHistory.command_type == "test" ) if role_name: count_query = count_query.where(CommandHistory.command.like(f"%{role_name}%")) total = (await db.execute(count_query)).scalar() or 0 total_pages = (total + per_page - 1) // per_page if total > 0 else 1 page = max(1, min(page, total_pages)) # Пагинация offset = (page - 1) * per_page query = query.offset(offset).limit(per_page) result = await db.execute(query) tests = result.scalars().all() # Получение списка ролей для фильтра roles_dir = settings.PROJECT_ROOT / "roles" roles = [] if roles_dir.exists(): for role_dir in roles_dir.iterdir(): if role_dir.is_dir() and role_dir.name != "deploy.yml": roles.append(role_dir.name) return templates.TemplateResponse( "pages/tests/index.html", { "request": request, "tests": tests, "roles": sorted(roles), "role_name": role_name or "", "total": total, "page": page, "per_page": per_page, "total_pages": total_pages } ) except Exception as e: import logging logging.error(f"Error loading tests history: {e}", exc_info=True) # Если база данных не настроена, показываем пустую страницу return templates.TemplateResponse( "pages/tests/index.html", { "request": request, "tests": [], "roles": [], "role_name": role_name or "", "total": 0, "page": 1, "per_page": per_page, "total_pages": 1, "error": "База данных не настроена или недоступна" } ) @router.get("/api/v1/tests/recent") async def get_recent_tests(limit: int = 10): """Получение последних тестов""" try: from app.services.history_service import HistoryService history_service = HistoryService() tests = history_service.get_command_history( limit=limit, command_type="test" ) return tests except Exception: # Если база данных не настроена, возвращаем пустой список return [] @router.websocket("/ws/test/{test_id}") async def test_websocket(websocket: WebSocket, test_id: str): """WebSocket для live логов тестирования""" await websocket.accept() try: # Парсинг test_id (формат: role_name-preset или role_name-preset-category) parts = test_id.rsplit("-", 2) if len(parts) < 2: await websocket.send_json({ "type": "error", "data": "Неверный формат test_id. Ожидается: role_name-preset или role_name-preset-category" }) await websocket.close() return role_name = parts[0] preset_name = parts[1] if len(parts) > 1 else "default" preset_category = parts[2] if len(parts) > 2 else "main" # Проверка существования роли roles_dir = settings.PROJECT_ROOT / "roles" / role_name if not roles_dir.exists(): await websocket.send_json({ "type": "error", "data": f"Роль '{role_name}' не найдена" }) await websocket.close() return # Получаем preset из БД async for db in get_async_db(): preset = await PresetService.get_preset(db, preset_name, preset_category) if not preset: await websocket.send_json({ "type": "error", "data": f"Preset '{preset_name}' не найден" }) await websocket.close() return preset_content = preset.content break # Отправка начального сообщения await websocket.send_json({ "type": "info", "data": f"🚀 Запуск теста роли '{role_name}' с preset '{preset_name}'..." }) # Создаем временный файл preset'а из БД molecule_executor.create_temp_preset_file(preset_name, preset_content, preset_category) # Запуск теста через MoleculeExecutor (без Makefile) async for line in molecule_executor.test_role( role_name=role_name, preset_name=preset_name, preset_content=preset_content, preset_category=preset_category, stream=True ): # Очистка строки от лишних символов line = line.rstrip() if not line: continue # Определение типа лога log_type = molecule_executor.detect_log_level(line) await websocket.send_json({ "type": "log", "level": log_type, "data": line }) # Завершение await websocket.send_json({ "type": "complete", "status": "success", "data": "✅ Тест завершен успешно" }) # Удаляем временный файл if preset_name in molecule_executor._temp_preset_files: try: molecule_executor._temp_preset_files[preset_name].unlink() del molecule_executor._temp_preset_files[preset_name] except: pass except WebSocketDisconnect: pass except Exception as e: import traceback error_msg = f"❌ Ошибка: {str(e)}\n{traceback.format_exc()}" await websocket.send_json({ "type": "error", "data": error_msg }) finally: try: await websocket.close() except: pass