Files
core/scripts/ci-server.py
Untone 4b88a8c449
Some checks failed
Deploy on push / deploy (push) Failing after 1m11s
ci-testing
2025-08-17 11:09:29 +03:00

361 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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())