#!/usr/bin/env python3 """ CI Server Script - Запускает серверы для тестирования в неблокирующем режиме """ import os import sys import time import signal import subprocess import threading import logging from pathlib import Path from typing import Optional, Dict, Any # Добавляем корневую папку в путь sys.path.insert(0, str(Path(__file__).parent.parent)) # Создаем собственный логгер без дублирования def create_ci_logger(): """Создает логгер для CI без дублирования""" logger = logging.getLogger("ci-server") logger.setLevel(logging.INFO) # Убираем существующие обработчики logger.handlers.clear() # Создаем форматтер formatter = logging.Formatter( "%(asctime)s - %(name)s - %(levelname)s - %(message)s" ) # Создаем обработчик handler = logging.StreamHandler() handler.setFormatter(formatter) logger.addHandler(handler) # Отключаем пропагацию к root logger logger.propagate = False return logger logger = create_ci_logger() class CIServerManager: """Менеджер CI серверов""" def __init__(self): self.backend_process: Optional[subprocess.Popen] = None self.frontend_process: Optional[subprocess.Popen] = None self.backend_pid_file = Path("backend.pid") self.frontend_pid_file = Path("frontend.pid") # Настройки по умолчанию self.backend_host = os.getenv("BACKEND_HOST", "0.0.0.0") self.backend_port = int(os.getenv("BACKEND_PORT", "8000")) self.frontend_port = int(os.getenv("FRONTEND_PORT", "3000")) # Флаги состояния self.backend_ready = False self.frontend_ready = False # Обработчики сигналов для корректного завершения signal.signal(signal.SIGINT, self._signal_handler) signal.signal(signal.SIGTERM, self._signal_handler) def _signal_handler(self, signum: int, frame: Any) -> None: """Обработчик сигналов для корректного завершения""" logger.info(f"Получен сигнал {signum}, завершаем работу...") self.cleanup() sys.exit(0) def start_backend_server(self) -> bool: """Запускает backend сервер""" try: logger.info(f"🚀 Запускаем backend сервер на {self.backend_host}:{self.backend_port}") # Запускаем сервер в фоне self.backend_process = subprocess.Popen( [ sys.executable, "dev.py", "--host", self.backend_host, "--port", str(self.backend_port) ], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1, universal_newlines=True ) # Сохраняем PID self.backend_pid_file.write_text(str(self.backend_process.pid)) logger.info(f"✅ Backend сервер запущен с PID: {self.backend_process.pid}") # Запускаем мониторинг в отдельном потоке threading.Thread( target=self._monitor_backend, daemon=True ).start() return True except Exception as e: logger.error(f"❌ Ошибка запуска backend сервера: {e}") return False def start_frontend_server(self) -> bool: """Запускает frontend сервер""" try: logger.info(f"🚀 Запускаем frontend сервер на порту {self.frontend_port}") # Переходим в папку panel panel_dir = Path("panel") if not panel_dir.exists(): logger.error("❌ Папка panel не найдена") return False # Запускаем npm run dev в фоне self.frontend_process = subprocess.Popen( ["npm", "run", "dev"], cwd=panel_dir, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1, universal_newlines=True ) # Сохраняем PID self.frontend_pid_file.write_text(str(self.frontend_process.pid)) logger.info(f"✅ Frontend сервер запущен с PID: {self.frontend_process.pid}") # Запускаем мониторинг в отдельном потоке threading.Thread( target=self._monitor_frontend, daemon=True ).start() return True except Exception as e: logger.error(f"❌ Ошибка запуска frontend сервера: {e}") return False def _monitor_backend(self) -> None: """Мониторит backend сервер""" try: while self.backend_process and self.backend_process.poll() is None: time.sleep(1) # Проверяем доступность сервера if not self.backend_ready: try: import requests response = requests.get( f"http://{self.backend_host}:{self.backend_port}/", timeout=5 ) if response.status_code == 200: self.backend_ready = True logger.info("✅ Backend сервер готов к работе!") else: logger.debug(f"Backend отвечает с кодом: {response.status_code}") except Exception as e: logger.debug(f"Backend еще не готов: {e}") except Exception as e: logger.error(f"❌ Ошибка мониторинга backend: {e}") def _monitor_frontend(self) -> None: """Мониторит frontend сервер""" try: while self.frontend_process and self.frontend_process.poll() is None: time.sleep(1) # Проверяем доступность сервера if not self.frontend_ready: try: import requests response = requests.get( f"http://localhost:{self.frontend_port}/", timeout=5 ) if response.status_code == 200: self.frontend_ready = True logger.info("✅ Frontend сервер готов к работе!") else: logger.debug(f"Frontend отвечает с кодом: {response.status_code}") except Exception as e: logger.debug(f"Frontend еще не готов: {e}") except Exception as e: logger.error(f"❌ Ошибка мониторинга frontend: {e}") def wait_for_servers(self, timeout: int = 120) -> bool: """Ждет пока серверы будут готовы""" logger.info(f"⏳ Ждем готовности серверов (таймаут: {timeout}с)...") start_time = time.time() while time.time() - start_time < timeout: logger.debug(f"Backend готов: {self.backend_ready}, Frontend готов: {self.frontend_ready}") if self.backend_ready and self.frontend_ready: logger.info("🎉 Все серверы готовы к работе!") return True time.sleep(2) logger.error("⏰ Таймаут ожидания готовности серверов") logger.error(f"Backend готов: {self.backend_ready}, Frontend готов: {self.frontend_ready}") return False def cleanup(self) -> None: """Очищает ресурсы и завершает процессы""" logger.info("🧹 Очищаем ресурсы...") # Завершаем процессы if self.backend_process: try: self.backend_process.terminate() self.backend_process.wait(timeout=10) except subprocess.TimeoutExpired: self.backend_process.kill() except Exception as e: logger.error(f"Ошибка завершения backend: {e}") if self.frontend_process: try: self.frontend_process.terminate() self.frontend_process.wait(timeout=10) except subprocess.TimeoutExpired: self.frontend_process.kill() except Exception as e: logger.error(f"Ошибка завершения frontend: {e}") # Удаляем PID файлы for pid_file in [self.backend_pid_file, self.frontend_pid_file]: if pid_file.exists(): try: pid_file.unlink() except Exception as e: logger.error(f"Ошибка удаления {pid_file}: {e}") # Убиваем все связанные процессы try: subprocess.run(["pkill", "-f", "python dev.py"], check=False) subprocess.run(["pkill", "-f", "npm run dev"], check=False) subprocess.run(["pkill", "-f", "vite"], check=False) except Exception as e: logger.error(f"Ошибка принудительного завершения: {e}") logger.info("✅ Очистка завершена") def main(): """Основная функция""" logger.info("🚀 Запуск CI Server Manager") # Создаем менеджер manager = CIServerManager() try: # Запускаем серверы if not manager.start_backend_server(): logger.error("❌ Не удалось запустить backend сервер") return 1 if not manager.start_frontend_server(): logger.error("❌ Не удалось запустить frontend сервер") return 1 # Ждем готовности if not manager.wait_for_servers(): logger.error("❌ Серверы не готовы в течение таймаута") return 1 logger.info("🎯 Серверы запущены и готовы к тестированию") # В CI режиме запускаем тесты автоматически ci_mode = os.getenv("CI_MODE", "false").lower() logger.info(f"🔧 Проверяем CI режим: CI_MODE={ci_mode}") if ci_mode in ["true", "1", "yes"]: logger.info("🔧 CI режим: запускаем тесты автоматически...") return run_tests_in_ci() else: logger.info("💡 Локальный режим: для запуска тестов нажмите Ctrl+C") # Держим скрипт запущенным try: while True: time.sleep(1) # Проверяем что процессы еще живы if (manager.backend_process and manager.backend_process.poll() is not None): logger.error("❌ Backend сервер завершился неожиданно") break if (manager.frontend_process and manager.frontend_process.poll() is not None): logger.error("❌ Frontend сервер завершился неожиданно") break except KeyboardInterrupt: logger.info("👋 Получен сигнал прерывания") return 0 except Exception as e: logger.error(f"❌ Критическая ошибка: {e}") return 1 finally: manager.cleanup() def run_tests_in_ci() -> int: """Запускает тесты в CI режиме""" try: logger.info("🧪 Запускаем unit тесты...") result = subprocess.run([ "uv", "run", "pytest", "tests/", "-m", "not e2e", "-v", "--tb=short" ], capture_output=False, text=True) # Убираем capture_output=False if result.returncode != 0: logger.error(f"❌ Unit тесты провалились с кодом: {result.returncode}") return result.returncode logger.info("✅ Unit тесты прошли успешно!") logger.info("🧪 Запускаем integration тесты...") result = subprocess.run([ "uv", "run", "pytest", "tests/", "-m", "integration", "-v", "--tb=short" ], capture_output=False, text=True) # Убираем capture_output=False if result.returncode != 0: logger.error(f"❌ Integration тесты провалились с кодом: {result.returncode}") return result.returncode logger.info("✅ Integration тесты прошли успешно!") logger.info("🧪 Запускаем E2E тесты...") result = subprocess.run([ "uv", "run", "pytest", "tests/", "-m", "e2e", "-v", "--tb=short", "--timeout=300" ], capture_output=False, text=True) # Убираем capture_output=False if result.returncode != 0: logger.error(f"❌ E2E тесты провалились с кодом: {result.returncode}") return result.returncode logger.info("✅ E2E тесты прошли успешно!") logger.info("🎉 Все тесты прошли успешно!") return 0 except Exception as e: logger.error(f"❌ Ошибка при запуске тестов: {e}") return 1 if __name__ == "__main__": sys.exit(main())