This commit is contained in:
227
scripts/ci-server.py
Normal file → Executable file
227
scripts/ci-server.py
Normal file → Executable file
@@ -3,7 +3,6 @@
|
||||
CI Server Script - Запускает серверы для тестирования в неблокирующем режиме
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
@@ -11,11 +10,18 @@ import sys
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import Any
|
||||
|
||||
# Добавляем корневую папку в путь
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
# Импорты на верхнем уровне
|
||||
import requests
|
||||
from sqlalchemy import inspect
|
||||
|
||||
from orm.base import Base
|
||||
from services.db import engine
|
||||
|
||||
|
||||
# Создаем собственный логгер без дублирования
|
||||
def create_ci_logger():
|
||||
@@ -47,13 +53,13 @@ class CIServerManager:
|
||||
"""Менеджер CI серверов"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.backend_process: Optional[subprocess.Popen] = None
|
||||
self.frontend_process: Optional[subprocess.Popen] = None
|
||||
self.backend_process: subprocess.Popen | None = None
|
||||
self.frontend_process: subprocess.Popen | None = 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_host = os.getenv("BACKEND_HOST", "127.0.0.1")
|
||||
self.backend_port = int(os.getenv("BACKEND_PORT", "8000"))
|
||||
self.frontend_port = int(os.getenv("FRONTEND_PORT", "3000"))
|
||||
|
||||
@@ -65,7 +71,7 @@ class CIServerManager:
|
||||
signal.signal(signal.SIGINT, self._signal_handler)
|
||||
signal.signal(signal.SIGTERM, self._signal_handler)
|
||||
|
||||
def _signal_handler(self, signum: int, frame: Any) -> None:
|
||||
def _signal_handler(self, signum: int, _frame: Any | None = None) -> None:
|
||||
"""Обработчик сигналов для корректного завершения"""
|
||||
logger.info(f"Получен сигнал {signum}, завершаем работу...")
|
||||
self.cleanup()
|
||||
@@ -95,8 +101,8 @@ class CIServerManager:
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка запуска backend сервера: {e}")
|
||||
except Exception:
|
||||
logger.exception("❌ Ошибка запуска backend сервера")
|
||||
return False
|
||||
|
||||
def start_frontend_server(self) -> bool:
|
||||
@@ -130,8 +136,8 @@ class CIServerManager:
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка запуска frontend сервера: {e}")
|
||||
except Exception:
|
||||
logger.exception("❌ Ошибка запуска frontend сервера")
|
||||
return False
|
||||
|
||||
def _monitor_backend(self) -> None:
|
||||
@@ -143,19 +149,17 @@ class CIServerManager:
|
||||
# Проверяем доступность сервера
|
||||
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:
|
||||
logger.exception("❌ Ошибка мониторинга backend")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка мониторинга backend: {e}")
|
||||
except Exception:
|
||||
logger.exception("❌ Ошибка мониторинга backend")
|
||||
|
||||
def _monitor_frontend(self) -> None:
|
||||
"""Мониторит frontend сервер"""
|
||||
@@ -166,19 +170,17 @@ class CIServerManager:
|
||||
# Проверяем доступность сервера
|
||||
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:
|
||||
logger.exception("❌ Ошибка мониторинга frontend")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка мониторинга frontend: {e}")
|
||||
except Exception:
|
||||
logger.exception("❌ Ошибка мониторинга frontend")
|
||||
|
||||
def wait_for_servers(self, timeout: int = 180) -> bool: # Увеличил таймаут
|
||||
"""Ждет пока серверы будут готовы"""
|
||||
@@ -209,8 +211,8 @@ class CIServerManager:
|
||||
self.backend_process.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.backend_process.kill()
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка завершения backend: {e}")
|
||||
except Exception:
|
||||
logger.exception("Ошибка завершения backend")
|
||||
|
||||
if self.frontend_process:
|
||||
try:
|
||||
@@ -218,24 +220,24 @@ class CIServerManager:
|
||||
self.frontend_process.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:
|
||||
self.frontend_process.kill()
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка завершения frontend: {e}")
|
||||
except Exception:
|
||||
logger.exception("Ошибка завершения frontend")
|
||||
|
||||
# Удаляем 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}")
|
||||
except Exception:
|
||||
logger.exception(f"Ошибка удаления {pid_file}")
|
||||
|
||||
# Убиваем все связанные процессы
|
||||
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}")
|
||||
except Exception:
|
||||
logger.exception("Ошибка принудительного завершения")
|
||||
|
||||
logger.info("✅ Очистка завершена")
|
||||
|
||||
@@ -245,14 +247,71 @@ def run_tests_in_ci():
|
||||
logger.info("🧪 Запускаем тесты в CI режиме...")
|
||||
|
||||
# Создаем папку для результатов тестов
|
||||
os.makedirs("test-results", exist_ok=True)
|
||||
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", "."],
|
||||
check=False, capture_output=False,
|
||||
text=True,
|
||||
timeout=300 # 5 минут на linting
|
||||
)
|
||||
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", "."],
|
||||
check=False, capture_output=False,
|
||||
text=True,
|
||||
timeout=300 # 5 минут на проверку форматирования
|
||||
)
|
||||
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"],
|
||||
check=False, capture_output=False,
|
||||
text=True,
|
||||
timeout=600 # 10 минут на type checking
|
||||
)
|
||||
if mypy_result.returncode == 0:
|
||||
logger.info("✅ MyPy проверка прошла успешно")
|
||||
else:
|
||||
logger.error("❌ MyPy нашел проблемы с типами")
|
||||
return False
|
||||
except Exception:
|
||||
logger.exception("❌ Ошибка при запуске MyPy")
|
||||
return False
|
||||
|
||||
# Затем проверяем здоровье серверов
|
||||
logger.info("🏥 Проверяем здоровье серверов...")
|
||||
try:
|
||||
health_result = subprocess.run(
|
||||
["uv", "run", "pytest", "tests/test_server_health.py", "-v"],
|
||||
capture_output=False,
|
||||
check=False, capture_output=False,
|
||||
text=True,
|
||||
timeout=120, # 2 минуты на проверку здоровья
|
||||
)
|
||||
@@ -280,7 +339,7 @@ def run_tests_in_ci():
|
||||
# Запускаем тесты с выводом в реальном времени
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=False, # Потоковый вывод
|
||||
check=False, capture_output=False, # Потоковый вывод
|
||||
text=True,
|
||||
timeout=600, # 10 минут на тесты
|
||||
)
|
||||
@@ -288,35 +347,32 @@ def run_tests_in_ci():
|
||||
if result.returncode == 0:
|
||||
logger.info(f"✅ {test_type} прошли успешно!")
|
||||
break
|
||||
else:
|
||||
if attempt == max_retries:
|
||||
if test_type == "Browser тесты":
|
||||
logger.warning(
|
||||
f"⚠️ {test_type} не прошли после {max_retries} попыток (ожидаемо) - продолжаем..."
|
||||
)
|
||||
else:
|
||||
logger.error(f"❌ {test_type} не прошли после {max_retries} попыток")
|
||||
return False
|
||||
else:
|
||||
if attempt == max_retries:
|
||||
if test_type == "Browser тесты":
|
||||
logger.warning(
|
||||
f"⚠️ {test_type} не прошли, повторяем через 10 секунд... (попытка {attempt}/{max_retries})"
|
||||
f"⚠️ {test_type} не прошли после {max_retries} попыток (ожидаемо) - продолжаем..."
|
||||
)
|
||||
time.sleep(10)
|
||||
else:
|
||||
logger.error(f"❌ {test_type} не прошли после {max_retries} попыток")
|
||||
return False
|
||||
else:
|
||||
logger.warning(
|
||||
f"⚠️ {test_type} не прошли, повторяем через 10 секунд... (попытка {attempt}/{max_retries})"
|
||||
)
|
||||
time.sleep(10)
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error(f"⏰ Таймаут для {test_type} (10 минут)")
|
||||
logger.exception(f"⏰ Таймаут для {test_type} (10 минут)")
|
||||
if attempt == max_retries:
|
||||
return False
|
||||
else:
|
||||
logger.warning(f"⚠️ Повторяем {test_type} через 10 секунд... (попытка {attempt}/{max_retries})")
|
||||
time.sleep(10)
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка при запуске {test_type}: {e}")
|
||||
logger.warning(f"⚠️ Повторяем {test_type} через 10 секунд... (попытка {attempt}/{max_retries})")
|
||||
time.sleep(10)
|
||||
except Exception:
|
||||
logger.exception(f"❌ Ошибка при запуске {test_type}")
|
||||
if attempt == max_retries:
|
||||
return False
|
||||
else:
|
||||
logger.warning(f"⚠️ Повторяем {test_type} через 10 секунд... (попытка {attempt}/{max_retries})")
|
||||
time.sleep(10)
|
||||
logger.warning(f"⚠️ Повторяем {test_type} через 10 секунд... (попытка {attempt}/{max_retries})")
|
||||
time.sleep(10)
|
||||
|
||||
logger.info("🎉 Все тесты завершены!")
|
||||
return True
|
||||
@@ -334,25 +390,9 @@ def initialize_test_database():
|
||||
logger.info("✅ Создан файл базы данных")
|
||||
|
||||
# Импортируем и создаем таблицы
|
||||
from sqlalchemy import inspect
|
||||
|
||||
from auth.orm import Author, AuthorBookmark, AuthorFollower, AuthorRating
|
||||
from orm.base import Base
|
||||
from orm.community import Community, CommunityAuthor, CommunityFollower
|
||||
from orm.draft import Draft
|
||||
from orm.invite import Invite
|
||||
from orm.notification import Notification
|
||||
from orm.reaction import Reaction
|
||||
from orm.shout import Shout
|
||||
from orm.topic import Topic
|
||||
from services.db import engine
|
||||
|
||||
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}")
|
||||
@@ -364,15 +404,11 @@ def initialize_test_database():
|
||||
if missing_tables:
|
||||
logger.error(f"❌ Отсутствуют критически важные таблицы: {missing_tables}")
|
||||
return False
|
||||
else:
|
||||
logger.info("✅ Все критически важные таблицы созданы")
|
||||
return True
|
||||
logger.info("✅ Все критически важные таблицы созданы")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Ошибка инициализации базы данных: {e}")
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
except Exception:
|
||||
logger.exception("❌ Ошибка инициализации базы данных")
|
||||
return False
|
||||
|
||||
|
||||
@@ -412,30 +448,29 @@ def main():
|
||||
if ci_mode in ["true", "1", "yes"]:
|
||||
logger.info("🔧 CI режим: запускаем тесты автоматически...")
|
||||
return run_tests_in_ci()
|
||||
else:
|
||||
logger.info("💡 Локальный режим: для запуска тестов нажмите Ctrl+C")
|
||||
logger.info("💡 Локальный режим: для запуска тестов нажмите Ctrl+C")
|
||||
|
||||
# Держим скрипт запущенным
|
||||
try:
|
||||
while True:
|
||||
time.sleep(1)
|
||||
# Держим скрипт запущенным
|
||||
try:
|
||||
while True:
|
||||
time.sleep(1)
|
||||
|
||||
# Проверяем что процессы еще живы
|
||||
if manager.backend_process and manager.backend_process.poll() is not None:
|
||||
logger.error("❌ Backend сервер завершился неожиданно")
|
||||
break
|
||||
# Проверяем что процессы еще живы
|
||||
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
|
||||
if manager.frontend_process and manager.frontend_process.poll() is not None:
|
||||
logger.error("❌ Frontend сервер завершился неожиданно")
|
||||
break
|
||||
|
||||
except KeyboardInterrupt:
|
||||
logger.info("👋 Получен сигнал прерывания")
|
||||
except KeyboardInterrupt:
|
||||
logger.info("👋 Получен сигнал прерывания")
|
||||
|
||||
return 0
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ Критическая ошибка: {e}")
|
||||
except Exception:
|
||||
logger.exception("❌ Критическая ошибка")
|
||||
return 1
|
||||
|
||||
finally:
|
||||
|
||||
Reference in New Issue
Block a user