Files
core/ci-server.py

461 lines
19 KiB
Python
Raw Normal View History

2025-08-17 11:09:29 +03:00
#!/usr/bin/env python3
"""
CI Server Script - Запускает серверы для тестирования в неблокирующем режиме
"""
import os
import signal
import subprocess
2025-08-17 11:37:55 +03:00
import sys
2025-08-17 11:09:29 +03:00
import threading
2025-08-17 11:37:55 +03:00
import time
2025-08-17 11:09:29 +03:00
from pathlib import Path
2025-08-17 16:33:54 +03:00
from typing import Any
2025-08-17 11:09:29 +03:00
# Добавляем корневую папку в путь
sys.path.insert(0, str(Path(__file__).parent.parent))
2025-08-17 16:33:54 +03:00
# Импорты на верхнем уровне
import requests
from sqlalchemy import inspect
from orm.base import Base
2025-08-17 17:56:31 +03:00
from storage.db import engine
from utils.logger import root_logger as logger
2025-08-17 11:09:29 +03:00
class CIServerManager:
"""Менеджер CI серверов"""
2025-08-17 11:37:55 +03:00
def __init__(self) -> None:
2025-08-17 16:33:54 +03:00
self.backend_process: subprocess.Popen | None = None
self.frontend_process: subprocess.Popen | None = None
2025-08-17 11:09:29 +03:00
self.backend_pid_file = Path("backend.pid")
self.frontend_pid_file = Path("frontend.pid")
2025-08-17 11:37:55 +03:00
2025-08-17 11:09:29 +03:00
# Настройки по умолчанию
2025-08-17 16:33:54 +03:00
self.backend_host = os.getenv("BACKEND_HOST", "127.0.0.1")
2025-08-17 11:09:29 +03:00
self.backend_port = int(os.getenv("BACKEND_PORT", "8000"))
self.frontend_port = int(os.getenv("FRONTEND_PORT", "3000"))
2025-08-17 11:37:55 +03:00
2025-08-17 11:09:29 +03:00
# Флаги состояния
self.backend_ready = False
self.frontend_ready = False
2025-08-17 11:37:55 +03:00
2025-08-17 11:09:29 +03:00
# Обработчики сигналов для корректного завершения
signal.signal(signal.SIGINT, self._signal_handler)
signal.signal(signal.SIGTERM, self._signal_handler)
2025-08-17 11:37:55 +03:00
2025-08-17 16:33:54 +03:00
def _signal_handler(self, signum: int, _frame: Any | None = None) -> None:
2025-08-17 11:09:29 +03:00
"""Обработчик сигналов для корректного завершения"""
logger.info(f"Получен сигнал {signum}, завершаем работу...")
self.cleanup()
sys.exit(0)
2025-08-17 11:37:55 +03:00
2025-08-17 11:09:29 +03:00
def start_backend_server(self) -> bool:
"""Запускает backend сервер"""
try:
logger.info(f"🚀 Запускаем backend сервер на {self.backend_host}:{self.backend_port}")
2025-08-17 11:37:55 +03:00
2025-08-17 11:09:29 +03:00
# Запускаем сервер в фоне
self.backend_process = subprocess.Popen(
2025-08-17 11:37:55 +03:00
[sys.executable, "dev.py", "--host", self.backend_host, "--port", str(self.backend_port)],
2025-08-17 11:09:29 +03:00
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
2025-08-17 11:37:55 +03:00
universal_newlines=True,
2025-08-17 11:09:29 +03:00
)
2025-08-17 11:37:55 +03:00
2025-08-17 11:09:29 +03:00
# Сохраняем PID
self.backend_pid_file.write_text(str(self.backend_process.pid))
logger.info(f"✅ Backend сервер запущен с PID: {self.backend_process.pid}")
2025-08-17 11:37:55 +03:00
2025-08-17 11:09:29 +03:00
# Запускаем мониторинг в отдельном потоке
2025-08-17 11:37:55 +03:00
threading.Thread(target=self._monitor_backend, daemon=True).start()
2025-08-17 11:09:29 +03:00
return True
2025-08-17 11:37:55 +03:00
2025-08-17 16:33:54 +03:00
except Exception:
logger.exception("❌ Ошибка запуска backend сервера")
2025-08-17 11:09:29 +03:00
return False
2025-08-17 11:37:55 +03:00
2025-08-17 11:09:29 +03:00
def start_frontend_server(self) -> bool:
"""Запускает frontend сервер"""
try:
logger.info(f"🚀 Запускаем frontend сервер на порту {self.frontend_port}")
2025-08-17 11:37:55 +03:00
2025-08-17 11:09:29 +03:00
# Переходим в папку panel
panel_dir = Path("panel")
if not panel_dir.exists():
logger.error("❌ Папка panel не найдена")
return False
2025-08-17 11:37:55 +03:00
2025-08-17 11:09:29 +03:00
# Запускаем npm run dev в фоне
self.frontend_process = subprocess.Popen(
["npm", "run", "dev"],
cwd=panel_dir,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1,
2025-08-17 11:37:55 +03:00
universal_newlines=True,
2025-08-17 11:09:29 +03:00
)
2025-08-17 11:37:55 +03:00
2025-08-17 11:09:29 +03:00
# Сохраняем PID
self.frontend_pid_file.write_text(str(self.frontend_process.pid))
logger.info(f"✅ Frontend сервер запущен с PID: {self.frontend_process.pid}")
2025-08-17 11:37:55 +03:00
2025-08-17 11:09:29 +03:00
# Запускаем мониторинг в отдельном потоке
2025-08-17 11:37:55 +03:00
threading.Thread(target=self._monitor_frontend, daemon=True).start()
2025-08-17 11:09:29 +03:00
return True
2025-08-17 11:37:55 +03:00
2025-08-17 16:33:54 +03:00
except Exception:
logger.exception("❌ Ошибка запуска frontend сервера")
2025-08-17 11:09:29 +03:00
return False
2025-08-17 11:37:55 +03:00
2025-08-17 11:09:29 +03:00
def _monitor_backend(self) -> None:
"""Мониторит backend сервер"""
try:
while self.backend_process and self.backend_process.poll() is None:
time.sleep(1)
2025-08-17 11:37:55 +03:00
2025-08-17 11:09:29 +03:00
# Проверяем доступность сервера
if not self.backend_ready:
try:
2025-08-17 11:37:55 +03:00
response = requests.get(f"http://{self.backend_host}:{self.backend_port}/", timeout=5)
2025-08-17 11:09:29 +03:00
if response.status_code == 200:
self.backend_ready = True
logger.info("✅ Backend сервер готов к работе!")
else:
logger.debug(f"Backend отвечает с кодом: {response.status_code}")
2025-08-17 16:33:54 +03:00
except Exception:
logger.exception("❌ Ошибка мониторинга backend")
2025-08-17 11:37:55 +03:00
2025-08-17 16:33:54 +03:00
except Exception:
logger.exception("❌ Ошибка мониторинга backend")
2025-08-17 11:37:55 +03:00
2025-08-17 11:09:29 +03:00
def _monitor_frontend(self) -> None:
"""Мониторит frontend сервер"""
try:
while self.frontend_process and self.frontend_process.poll() is None:
time.sleep(1)
2025-08-17 11:37:55 +03:00
2025-08-17 11:09:29 +03:00
# Проверяем доступность сервера
if not self.frontend_ready:
try:
2025-08-17 11:37:55 +03:00
response = requests.get(f"http://localhost:{self.frontend_port}/", timeout=5)
2025-08-17 11:09:29 +03:00
if response.status_code == 200:
self.frontend_ready = True
logger.info("✅ Frontend сервер готов к работе!")
else:
logger.debug(f"Frontend отвечает с кодом: {response.status_code}")
2025-08-17 16:33:54 +03:00
except Exception:
logger.exception("❌ Ошибка мониторинга frontend")
2025-08-17 11:37:55 +03:00
2025-08-17 16:33:54 +03:00
except Exception:
logger.exception("❌ Ошибка мониторинга frontend")
2025-08-17 11:37:55 +03:00
def wait_for_servers(self, timeout: int = 180) -> bool: # Увеличил таймаут
2025-08-17 11:09:29 +03:00
"""Ждет пока серверы будут готовы"""
logger.info(f"⏳ Ждем готовности серверов (таймаут: {timeout}с)...")
start_time = time.time()
2025-08-17 11:37:55 +03:00
2025-08-17 11:09:29 +03:00
while time.time() - start_time < timeout:
logger.debug(f"Backend готов: {self.backend_ready}, Frontend готов: {self.frontend_ready}")
2025-08-17 11:37:55 +03:00
2025-08-17 11:09:29 +03:00
if self.backend_ready and self.frontend_ready:
logger.info("🎉 Все серверы готовы к работе!")
return True
2025-08-17 11:37:55 +03:00
time.sleep(3) # Увеличил интервал проверки
2025-08-17 11:09:29 +03:00
logger.error("⏰ Таймаут ожидания готовности серверов")
logger.error(f"Backend готов: {self.backend_ready}, Frontend готов: {self.frontend_ready}")
return False
2025-08-17 11:37:55 +03:00
2025-08-17 11:09:29 +03:00
def cleanup(self) -> None:
"""Очищает ресурсы и завершает процессы"""
logger.info("🧹 Очищаем ресурсы...")
2025-08-17 11:37:55 +03:00
2025-08-17 11:09:29 +03:00
# Завершаем процессы
if self.backend_process:
try:
self.backend_process.terminate()
self.backend_process.wait(timeout=10)
except subprocess.TimeoutExpired:
self.backend_process.kill()
2025-08-17 16:33:54 +03:00
except Exception:
logger.exception("Ошибка завершения backend")
2025-08-17 11:37:55 +03:00
2025-08-17 11:09:29 +03:00
if self.frontend_process:
try:
self.frontend_process.terminate()
self.frontend_process.wait(timeout=10)
except subprocess.TimeoutExpired:
self.frontend_process.kill()
2025-08-17 16:33:54 +03:00
except Exception:
logger.exception("Ошибка завершения frontend")
2025-08-17 11:37:55 +03:00
2025-08-17 11:09:29 +03:00
# Удаляем PID файлы
for pid_file in [self.backend_pid_file, self.frontend_pid_file]:
if pid_file.exists():
try:
pid_file.unlink()
2025-08-17 16:33:54 +03:00
except Exception:
logger.exception(f"Ошибка удаления {pid_file}")
2025-08-17 11:37:55 +03:00
2025-08-17 11:09:29 +03:00
# Убиваем все связанные процессы
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)
2025-08-17 16:33:54 +03:00
except Exception:
logger.exception("Ошибка принудительного завершения")
2025-08-17 11:37:55 +03:00
2025-08-17 11:09:29 +03:00
logger.info("✅ Очистка завершена")
2025-08-17 11:37:55 +03:00
def run_tests_in_ci():
"""Запускаем тесты в CI режиме"""
logger.info("🧪 Запускаем тесты в CI режиме...")
# Создаем папку для результатов тестов
2025-08-17 16:33:54 +03:00
Path("test-results").mkdir(parents=True, exist_ok=True)
# Сначала запускаем проверки качества кода
logger.info("🔍 Запускаем проверки качества кода...")
# Ruff linting
logger.info("📝 Проверяем код с помощью Ruff...")
try:
ruff_result = subprocess.run(
["uv", "run", "ruff", "check", "."],
2025-08-17 17:56:31 +03:00
check=False,
capture_output=False,
2025-08-17 16:33:54 +03:00
text=True,
2025-08-17 17:56:31 +03:00
timeout=300, # 5 минут на linting
2025-08-17 16:33:54 +03:00
)
if ruff_result.returncode == 0:
logger.info("✅ Ruff проверка прошла успешно")
else:
logger.error("❌ Ruff нашел проблемы в коде")
return False
except Exception:
logger.exception("❌ Ошибка при запуске Ruff")
return False
# Ruff formatting check
logger.info("🎨 Проверяем форматирование с помощью Ruff...")
try:
ruff_format_result = subprocess.run(
["uv", "run", "ruff", "format", "--check", "."],
2025-08-17 17:56:31 +03:00
check=False,
capture_output=False,
2025-08-17 16:33:54 +03:00
text=True,
2025-08-17 17:56:31 +03:00
timeout=300, # 5 минут на проверку форматирования
2025-08-17 16:33:54 +03:00
)
if ruff_format_result.returncode == 0:
logger.info("✅ Форматирование корректно")
else:
logger.error("❌ Код не отформатирован согласно стандартам")
return False
except Exception:
logger.exception("❌ Ошибка при проверке форматирования")
return False
# MyPy type checking
logger.info("🏷️ Проверяем типы с помощью MyPy...")
try:
mypy_result = subprocess.run(
["uv", "run", "mypy", ".", "--ignore-missing-imports"],
2025-08-17 17:56:31 +03:00
check=False,
capture_output=False,
2025-08-17 16:33:54 +03:00
text=True,
2025-08-17 17:56:31 +03:00
timeout=600, # 10 минут на type checking
2025-08-17 16:33:54 +03:00
)
if mypy_result.returncode == 0:
logger.info("✅ MyPy проверка прошла успешно")
else:
logger.error("❌ MyPy нашел проблемы с типами")
return False
except Exception:
logger.exception("❌ Ошибка при запуске MyPy")
return False
2025-08-17 11:37:55 +03:00
2025-08-17 16:33:54 +03:00
# Затем проверяем здоровье серверов
2025-08-17 11:37:55 +03:00
logger.info("🏥 Проверяем здоровье серверов...")
try:
health_result = subprocess.run(
["uv", "run", "pytest", "tests/test_server_health.py", "-v"],
2025-08-17 17:56:31 +03:00
check=False,
capture_output=False,
2025-08-17 11:37:55 +03:00
text=True,
timeout=120, # 2 минуты на проверку здоровья
)
if health_result.returncode != 0:
logger.warning("⚠️ Тест здоровья серверов не прошел, но продолжаем...")
else:
logger.info("✅ Серверы здоровы!")
except Exception as e:
logger.warning(f"⚠️ Ошибка при проверке здоровья серверов: {e}, продолжаем...")
test_commands = [
(["uv", "run", "pytest", "tests/", "-m", "not e2e", "-v", "--tb=short"], "Unit тесты"),
(["uv", "run", "pytest", "tests/", "-m", "integration", "-v", "--tb=short"], "Integration тесты"),
(["uv", "run", "pytest", "tests/", "-m", "e2e", "-v", "--tb=short"], "E2E тесты"),
(["uv", "run", "pytest", "tests/", "-m", "browser", "-v", "--tb=short", "--timeout=60"], "Browser тесты"),
]
for cmd, test_type in test_commands:
logger.info(f"🚀 Запускаем {test_type}...")
max_retries = 3 # Увеличиваем количество попыток
for attempt in range(1, max_retries + 1):
logger.info(f"📝 Попытка {attempt}/{max_retries} для {test_type}")
try:
# Запускаем тесты с выводом в реальном времени
result = subprocess.run(
cmd,
2025-08-17 17:56:31 +03:00
check=False,
capture_output=False, # Потоковый вывод
2025-08-17 11:37:55 +03:00
text=True,
timeout=600, # 10 минут на тесты
)
if result.returncode == 0:
logger.info(f"{test_type} прошли успешно!")
break
2025-08-17 16:33:54 +03:00
if attempt == max_retries:
if test_type == "Browser тесты":
2025-08-17 11:37:55 +03:00
logger.warning(
2025-08-17 16:33:54 +03:00
f"⚠️ {test_type} не прошли после {max_retries} попыток (ожидаемо) - продолжаем..."
2025-08-17 11:37:55 +03:00
)
2025-08-17 16:33:54 +03:00
else:
logger.error(f"{test_type} не прошли после {max_retries} попыток")
return False
else:
logger.warning(
f"⚠️ {test_type} не прошли, повторяем через 10 секунд... (попытка {attempt}/{max_retries})"
)
time.sleep(10)
2025-08-17 11:37:55 +03:00
except subprocess.TimeoutExpired:
2025-08-17 16:33:54 +03:00
logger.exception(f"⏰ Таймаут для {test_type} (10 минут)")
2025-08-17 11:37:55 +03:00
if attempt == max_retries:
return False
2025-08-17 16:33:54 +03:00
logger.warning(f"⚠️ Повторяем {test_type} через 10 секунд... (попытка {attempt}/{max_retries})")
time.sleep(10)
except Exception:
logger.exception(f"❌ Ошибка при запуске {test_type}")
2025-08-17 11:37:55 +03:00
if attempt == max_retries:
return False
2025-08-17 16:33:54 +03:00
logger.warning(f"⚠️ Повторяем {test_type} через 10 секунд... (попытка {attempt}/{max_retries})")
time.sleep(10)
2025-08-17 11:37:55 +03:00
logger.info("🎉 Все тесты завершены!")
return True
def initialize_test_database():
"""Инициализирует тестовую базу данных"""
try:
logger.info("🗄️ Инициализируем тестовую базу данных...")
# Создаем файл базы если его нет
db_file = Path("database.db")
if not db_file.exists():
db_file.touch()
logger.info("✅ Создан файл базы данных")
# Импортируем и создаем таблицы
logger.info("✅ Engine импортирован успешно")
logger.info("Creating all tables...")
Base.metadata.create_all(engine)
inspector = inspect(engine)
tables = inspector.get_table_names()
logger.info(f"✅ Созданы таблицы: {tables}")
# Проверяем критически важные таблицы
critical_tables = ["community_author", "community", "author"]
missing_tables = [table for table in critical_tables if table not in tables]
if missing_tables:
logger.error(f"❌ Отсутствуют критически важные таблицы: {missing_tables}")
return False
2025-08-17 16:33:54 +03:00
logger.info("Все критически важные таблицы созданы")
return True
2025-08-17 11:37:55 +03:00
2025-08-17 16:33:54 +03:00
except Exception:
logger.exception("❌ Ошибка инициализации базы данных")
2025-08-17 11:37:55 +03:00
return False
2025-08-17 11:09:29 +03:00
def main():
"""Основная функция"""
logger.info("🚀 Запуск CI Server Manager")
2025-08-17 11:37:55 +03:00
2025-08-17 11:09:29 +03:00
# Создаем менеджер
manager = CIServerManager()
2025-08-17 11:37:55 +03:00
2025-08-17 11:09:29 +03:00
try:
2025-08-17 11:37:55 +03:00
# Инициализируем базу данных
if not initialize_test_database():
logger.error("Не удалось инициализировать базу данных")
return 1
2025-08-17 11:09:29 +03:00
# Запускаем серверы
if not manager.start_backend_server():
logger.error("Не удалось запустить backend сервер")
return 1
2025-08-17 11:37:55 +03:00
2025-08-17 11:09:29 +03:00
if not manager.start_frontend_server():
logger.error("Не удалось запустить frontend сервер")
return 1
2025-08-17 11:37:55 +03:00
2025-08-17 11:09:29 +03:00
# Ждем готовности
if not manager.wait_for_servers():
logger.error("❌ Серверы не готовы в течение таймаута")
return 1
2025-08-17 11:37:55 +03:00
2025-08-17 11:09:29 +03:00
logger.info("🎯 Серверы запущены и готовы к тестированию")
2025-08-17 11:37:55 +03:00
2025-08-17 11:09:29 +03:00
# В CI режиме запускаем тесты автоматически
ci_mode = os.getenv("CI_MODE", "false").lower()
logger.info(f"🔧 Проверяем CI режим: CI_MODE={ci_mode}")
2025-08-17 11:37:55 +03:00
2025-08-17 11:09:29 +03:00
if ci_mode in ["true", "1", "yes"]:
logger.info("🔧 CI режим: запускаем тесты автоматически...")
return run_tests_in_ci()
2025-08-17 16:33:54 +03:00
logger.info("💡 Локальный режим: для запуска тестов нажмите Ctrl+C")
2025-08-17 11:37:55 +03:00
2025-08-17 16:33:54 +03:00
# Держим скрипт запущенным
try:
while True:
time.sleep(1)
2025-08-17 11:37:55 +03:00
2025-08-17 16:33:54 +03:00
# Проверяем что процессы еще живы
if manager.backend_process and manager.backend_process.poll() is not None:
logger.error("❌ Backend сервер завершился неожиданно")
break
2025-08-17 11:37:55 +03:00
2025-08-17 16:33:54 +03:00
if manager.frontend_process and manager.frontend_process.poll() is not None:
logger.error("❌ Frontend сервер завершился неожиданно")
break
2025-08-17 11:37:55 +03:00
2025-08-17 16:33:54 +03:00
except KeyboardInterrupt:
logger.info("👋 Получен сигнал прерывания")
2025-08-17 11:37:55 +03:00
2025-08-17 11:09:29 +03:00
return 0
2025-08-17 11:37:55 +03:00
2025-08-17 16:33:54 +03:00
except Exception:
logger.exception("❌ Критическая ошибка")
2025-08-17 11:09:29 +03:00
return 1
2025-08-17 11:37:55 +03:00
2025-08-17 11:09:29 +03:00
finally:
manager.cleanup()
if __name__ == "__main__":
sys.exit(main())