Update docker-compose and README for Telegram bot integration; add environment file reference and clarify port usage in documentation.
This commit is contained in:
215
Makefile
Normal file
215
Makefile
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
.PHONY: help build up down logs clean restart status deploy backup restore update clean-monitoring monitoring
|
||||||
|
|
||||||
|
help: ## Показать справку
|
||||||
|
@echo "🏗️ Production Infrastructure - Доступные команды:"
|
||||||
|
@echo ""
|
||||||
|
@echo "🔧 Основные команды:"
|
||||||
|
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
||||||
|
@echo ""
|
||||||
|
@echo "📊 Мониторинг:"
|
||||||
|
@echo " Prometheus: http://localhost:9090"
|
||||||
|
@echo " Grafana: http://localhost:3000 (admin/admin)"
|
||||||
|
@echo " Server Monitor: http://localhost:9091/health"
|
||||||
|
@echo " Bot Health: http://localhost:8080/health"
|
||||||
|
|
||||||
|
build: ## Собрать все контейнеры
|
||||||
|
docker-compose build
|
||||||
|
|
||||||
|
up: ## Запустить все сервисы
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
down: ## Остановить все сервисы
|
||||||
|
docker-compose down
|
||||||
|
|
||||||
|
logs: ## Показать логи всех сервисов
|
||||||
|
docker-compose logs -f
|
||||||
|
|
||||||
|
logs-monitor: ## Показать логи мониторинга
|
||||||
|
docker-compose logs -f server_monitor
|
||||||
|
|
||||||
|
logs-prometheus: ## Показать логи Prometheus
|
||||||
|
docker-compose logs -f prometheus
|
||||||
|
|
||||||
|
logs-grafana: ## Показать логи Grafana
|
||||||
|
docker-compose logs -f grafana
|
||||||
|
|
||||||
|
logs-bot: ## Показать логи Telegram бота
|
||||||
|
docker-compose logs -f telegram-bot
|
||||||
|
|
||||||
|
restart: ## Перезапустить все сервисы
|
||||||
|
docker-compose down
|
||||||
|
docker-compose build --no-cache
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
restart-monitor: ## Перезапустить только мониторинг
|
||||||
|
docker-compose restart server_monitor
|
||||||
|
|
||||||
|
restart-prometheus: ## Перезапустить только Prometheus
|
||||||
|
docker-compose restart prometheus
|
||||||
|
|
||||||
|
restart-grafana: ## Перезапустить только Grafana
|
||||||
|
docker-compose restart grafana
|
||||||
|
|
||||||
|
restart-bot: ## Перезапустить только Telegram бота
|
||||||
|
docker-compose restart telegram-bot
|
||||||
|
|
||||||
|
status: ## Показать статус контейнеров
|
||||||
|
docker-compose ps
|
||||||
|
|
||||||
|
health: ## Проверить здоровье сервисов
|
||||||
|
@echo "🏥 Checking service health..."
|
||||||
|
@curl -f http://localhost:8080/health || echo "❌ Bot health check failed"
|
||||||
|
@curl -f http://localhost:9090/-/healthy || echo "❌ Prometheus health check failed"
|
||||||
|
@curl -f http://localhost:3000/api/health || echo "❌ Grafana health check failed"
|
||||||
|
@curl -f http://localhost:9091/health || echo "❌ Server monitor health check failed"
|
||||||
|
|
||||||
|
deploy: ## Полный деплой на продакшен
|
||||||
|
@echo "🚀 Starting production deployment..."
|
||||||
|
@chmod +x scripts/deploy.sh
|
||||||
|
@./scripts/deploy.sh
|
||||||
|
|
||||||
|
backup: ## Создать backup данных
|
||||||
|
@echo "💾 Creating backup..."
|
||||||
|
@mkdir -p backups
|
||||||
|
@tar -czf "backups/backup-$(date +%Y%m%d-%H%M%S).tar.gz" \
|
||||||
|
infra/grafana/provisioning/ \
|
||||||
|
infra/prometheus/ \
|
||||||
|
infra/monitoring/ \
|
||||||
|
.env \
|
||||||
|
docker-compose.yml
|
||||||
|
@echo "✅ Backup created in backups/"
|
||||||
|
|
||||||
|
restore: ## Восстановить из backup (указать файл: make restore FILE=backup.tar.gz)
|
||||||
|
@echo "🔄 Restoring from backup..."
|
||||||
|
@if [ -z "$(FILE)" ]; then echo "❌ Please specify backup file: make restore FILE=backup.tar.gz"; exit 1; fi
|
||||||
|
@tar -xzf "backups/$(FILE)" -C .
|
||||||
|
@echo "✅ Backup restored"
|
||||||
|
|
||||||
|
update: ## Обновить инфраструктуру (pull latest code and redeploy)
|
||||||
|
@echo "📥 Pulling latest changes..."
|
||||||
|
@git pull origin main
|
||||||
|
@echo "🔨 Rebuilding and restarting..."
|
||||||
|
@make restart
|
||||||
|
|
||||||
|
clean: ## Очистить все контейнеры и образы
|
||||||
|
docker-compose down -v --rmi all
|
||||||
|
docker system prune -f
|
||||||
|
|
||||||
|
clean-monitoring: ## Очистить только данные мониторинга
|
||||||
|
docker-compose down -v
|
||||||
|
docker volume rm prod_prometheus_data prod_grafana_data 2>/dev/null || true
|
||||||
|
|
||||||
|
security-scan: ## Сканировать образы на уязвимости
|
||||||
|
@echo "🔍 Scanning Docker images for vulnerabilities..."
|
||||||
|
@docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
|
||||||
|
-v $(PWD):/workspace \
|
||||||
|
--workdir /workspace \
|
||||||
|
anchore/grype:latest \
|
||||||
|
bots_telegram_bot:latest || echo "⚠️ Grype not available, skipping scan"
|
||||||
|
|
||||||
|
monitoring: ## Открыть мониторинг в браузере
|
||||||
|
@echo "📊 Opening monitoring dashboards..."
|
||||||
|
@open http://localhost:3000 || xdg-open http://localhost:3000 || echo "Please open manually: http://localhost:3000"
|
||||||
|
|
||||||
|
prometheus: ## Открыть Prometheus в браузере
|
||||||
|
@echo "📈 Opening Prometheus..."
|
||||||
|
@open http://localhost:9090 || xdg-open http://localhost:9090 || echo "Please open manually: http://localhost:9090"
|
||||||
|
|
||||||
|
start: build up ## Собрать и запустить все сервисы
|
||||||
|
@echo "🏗️ Production Infrastructure запущена!"
|
||||||
|
@echo "📊 Prometheus: http://localhost:9090"
|
||||||
|
@echo "📈 Grafana: http://localhost:3000 (admin/admin)"
|
||||||
|
@echo "🤖 Bot Health: http://localhost:8080/health"
|
||||||
|
@echo "📡 Server Monitor: http://localhost:9091/health"
|
||||||
|
@echo "📝 Логи: make logs"
|
||||||
|
|
||||||
|
stop: down ## Остановить все сервисы
|
||||||
|
@echo "🛑 Все сервисы остановлены"
|
||||||
|
|
||||||
|
test: ## Запустить все тесты в проекте
|
||||||
|
@echo "🧪 Запускаю все тесты в проекте..."
|
||||||
|
@echo "📊 Тесты инфраструктуры..."
|
||||||
|
@python3 -m pytest tests/infra/ -q --tb=no
|
||||||
|
@echo "🤖 Тесты Telegram бота..."
|
||||||
|
@cd bots/telegram-helper-bot && python3 -m pytest tests/ -q --tb=no
|
||||||
|
@echo "✅ Все тесты завершены!"
|
||||||
|
@echo "📈 Общая статистика:"
|
||||||
|
@echo " - Инфраструктура: $(shell python3 count_tests.py | head -1) тестов"
|
||||||
|
@echo " - Telegram бот: $(shell python3 count_tests.py | head -2 | tail -1) тестов"
|
||||||
|
@echo " - Всего: $(shell python3 count_tests.py | tail -1) тестов"
|
||||||
|
|
||||||
|
test-all: ## Запустить все тесты в одном процессе (только для разработчиков)
|
||||||
|
@echo "🧪 Запускаю все тесты в одном процессе..."
|
||||||
|
@echo "⚠️ Примечание: Эта команда может не работать из-за конфликтов импортов"
|
||||||
|
@echo "📊 Рекомендуется использовать 'make test' для обычного запуска"
|
||||||
|
@PYTHONPATH=$(PWD)/bots/telegram-helper-bot:$(PWD) python3 -m pytest tests/infra/ bots/telegram-helper-bot/tests/ -v
|
||||||
|
|
||||||
|
test-infra: ## Запустить тесты инфраструктуры
|
||||||
|
@echo "🏗️ Запускаю тесты инфраструктуры..."
|
||||||
|
@python3 -m pytest tests/infra/ -v
|
||||||
|
|
||||||
|
test-bot: ## Запустить тесты Telegram бота
|
||||||
|
@echo "🤖 Запускаю тесты Telegram бота..."
|
||||||
|
@cd bots/telegram-helper-bot && python3 -m pytest tests/ -v
|
||||||
|
|
||||||
|
test-coverage: ## Запустить все тесты с отчетом о покрытии
|
||||||
|
@echo "📊 Запускаю все тесты с отчетом о покрытии..."
|
||||||
|
@echo "📈 Покрытие для инфраструктуры..."
|
||||||
|
@python3 -m pytest tests/infra/ --cov=infra --cov-report=term-missing --cov-report=html:htmlcov/infra
|
||||||
|
@echo "🤖 Покрытие для Telegram бота..."
|
||||||
|
@cd bots/telegram-helper-bot && python3 -m pytest tests/ --cov=helper_bot --cov-report=term-missing --cov-report=html:htmlcov/bot
|
||||||
|
@echo "📊 Отчеты о покрытии сохранены в htmlcov/"
|
||||||
|
@echo "📈 Общая статистика:"
|
||||||
|
@echo " - Инфраструктура: $(shell python3 count_tests.py | head -1) тестов"
|
||||||
|
@echo " - Telegram бот: $(shell python3 count_tests.py | head -2 | tail -1) тестов"
|
||||||
|
@echo " - Всего: $(shell python3 count_tests.py | tail -1) тестов"
|
||||||
|
|
||||||
|
test-clean: ## Очистить все файлы тестирования и отчеты
|
||||||
|
@echo "🧹 Очищаю файлы тестирования..."
|
||||||
|
@rm -rf htmlcov/
|
||||||
|
@rm -rf .coverage
|
||||||
|
@rm -rf .pytest_cache/
|
||||||
|
@rm -rf tests/.pytest_cache/
|
||||||
|
@rm -rf bots/telegram-helper-bot/.pytest_cache/
|
||||||
|
@rm -rf bots/telegram-helper-bot/htmlcov/
|
||||||
|
@rm -rf bots/telegram-helper-bot/.coverage
|
||||||
|
@find . -name "*.pyc" -delete 2>/dev/null || true
|
||||||
|
@find . -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true
|
||||||
|
@echo "✅ Файлы тестирования очищены"
|
||||||
|
|
||||||
|
check-ports: ## Проверить занятые порты
|
||||||
|
@echo "🔍 Checking occupied ports..."
|
||||||
|
@echo "Port 3000 (Grafana):"
|
||||||
|
@lsof -i :3000 2>/dev/null || echo " Free"
|
||||||
|
@echo "Port 9090 (Prometheus):"
|
||||||
|
@lsof -i :9090 2>/dev/null || echo " Free"
|
||||||
|
@echo "Port 9091 (Server Monitor):"
|
||||||
|
@lsof -i :9091 2>/dev/null || echo " Free"
|
||||||
|
@echo "Port 8080 (Telegram Bot):"
|
||||||
|
@lsof -i :8080 2>/dev/null || echo " Free"
|
||||||
|
|
||||||
|
check-grafana: ## Проверить состояние Grafana
|
||||||
|
@echo "📊 Checking Grafana status..."
|
||||||
|
@cd infra/monitoring && python3 check_grafana.py
|
||||||
|
|
||||||
|
logs-tail: ## Показать последние логи всех сервисов
|
||||||
|
@echo "📝 Recent logs from all services:"
|
||||||
|
@docker-compose logs --tail=50
|
||||||
|
|
||||||
|
logs-errors: ## Показать только ошибки из логов
|
||||||
|
@echo "❌ Error logs from all services:"
|
||||||
|
@docker-compose logs | grep -i error
|
||||||
|
|
||||||
|
metrics: ## Показать текущие метрики
|
||||||
|
@echo "📊 Current metrics:"
|
||||||
|
@curl -s http://localhost:9091/metrics | head -20
|
||||||
|
@echo "..."
|
||||||
|
@echo "Full metrics: http://localhost:9091/metrics"
|
||||||
|
|
||||||
|
reload-prometheus: ## Перезагрузить конфигурацию Prometheus
|
||||||
|
@echo "🔄 Reloading Prometheus configuration..."
|
||||||
|
@curl -X POST http://localhost:9090/-/reload
|
||||||
|
|
||||||
|
reload-grafana: ## Перезагрузить конфигурацию Grafana
|
||||||
|
@echo "🔄 Reloading Grafana configuration..."
|
||||||
|
@docker-compose restart grafana
|
||||||
187
README.md
187
README.md
@@ -19,6 +19,10 @@ prod/
|
|||||||
|
|
||||||
## 🚀 Быстрый запуск
|
## 🚀 Быстрый запуск
|
||||||
|
|
||||||
|
### ⚠️ Важное замечание
|
||||||
|
**Убедитесь, что вы удалили файл `docker-compose.yml` из папки `bots/telegram-helper-bot/`**
|
||||||
|
для избежания конфликтов портов. Используйте только корневой `docker-compose.yml`.
|
||||||
|
|
||||||
### 1. Настройка переменных окружения
|
### 1. Настройка переменных окружения
|
||||||
|
|
||||||
Скопируйте шаблон и настройте переменные:
|
Скопируйте шаблон и настройте переменные:
|
||||||
@@ -65,14 +69,65 @@ docker-compose ps
|
|||||||
- **Grafana** (порт 3000) - дашборды
|
- **Grafana** (порт 3000) - дашборды
|
||||||
- **Server Monitor** - мониторинг системы + Telegram уведомления
|
- **Server Monitor** - мониторинг системы + Telegram уведомления
|
||||||
|
|
||||||
## 🌐 Доступные адреса
|
## 🌐 Доступные адреса и порты
|
||||||
|
|
||||||
| Сервис | Адрес | Описание |
|
### 📊 Основные сервисы мониторинга
|
||||||
|--------|-------|----------|
|
|
||||||
| **Grafana** | http://localhost:3000 | Дашборды мониторинга (admin/admin) |
|
| Сервис | Порт | Адрес | Описание |
|
||||||
| **Prometheus** | http://localhost:9090 | API метрик и веб-интерфейс |
|
|--------|------|-------|----------|
|
||||||
| **Метрики сервера** | http://localhost:9091/metrics | Endpoint для Prometheus |
|
| **Grafana** | 3000 | http://localhost:3000 | Дашборды мониторинга (admin/admin) |
|
||||||
| **Health check** | http://localhost:9091/health | Проверка состояния мониторинга |
|
| **Prometheus** | 9090 | http://localhost:9090 | API метрик и веб-интерфейс |
|
||||||
|
| **Server Monitor** | 9091 | http://localhost:9091/health | Проверка состояния мониторинга |
|
||||||
|
| **Server Monitor Metrics** | 9091 | http://localhost:9091/metrics | Endpoint для Prometheus |
|
||||||
|
|
||||||
|
### 🤖 Telegram Bot сервисы
|
||||||
|
|
||||||
|
| Сервис | Порт | Адрес | Описание |
|
||||||
|
|--------|------|-------|----------|
|
||||||
|
| **Telegram Bot Health** | 8080 | http://localhost:8080/health | Health check бота |
|
||||||
|
| **Telegram Bot Metrics** | 8080 | http://localhost:8080/metrics | Метрики бота (если включены) |
|
||||||
|
|
||||||
|
### 🔍 Детальная информация о портах
|
||||||
|
|
||||||
|
#### **Порт 3000 - Grafana**
|
||||||
|
- **Контейнер**: `bots_grafana`
|
||||||
|
- **Назначение**: Веб-интерфейс для просмотра дашбордов мониторинга
|
||||||
|
- **Доступ**: Публичный (проброс из контейнера)
|
||||||
|
- **Аутентификация**: admin/admin (по умолчанию)
|
||||||
|
|
||||||
|
#### **Порт 9090 - Prometheus**
|
||||||
|
- **Контейнер**: `bots_prometheus`
|
||||||
|
- **Назначение**: Сбор и хранение метрик, API для запросов
|
||||||
|
- **Доступ**: Публичный (проброс из контейнера)
|
||||||
|
- **Функции**:
|
||||||
|
- Сбор метрик с server_monitor (порт 9091)
|
||||||
|
- Сбор метрик с telegram-bot (порт 8080)
|
||||||
|
- Хранение исторических данных
|
||||||
|
|
||||||
|
#### **Порт 9091 - Server Monitor**
|
||||||
|
- **Контейнер**: `bots_server_monitor`
|
||||||
|
- **Назначение**: Мониторинг системных ресурсов сервера
|
||||||
|
- **Доступ**: Внутренний (только внутри Docker сети)
|
||||||
|
- **Функции**:
|
||||||
|
- Сбор CPU, RAM, Disk метрик
|
||||||
|
- Отправка алертов в Telegram
|
||||||
|
- Предоставление метрик для Prometheus
|
||||||
|
|
||||||
|
#### **Порт 8080 - Telegram Bot**
|
||||||
|
- **Контейнер**: `bots_telegram_bot`
|
||||||
|
- **Назначение**: Основной функционал Telegram бота
|
||||||
|
- **Доступ**: Публичный (проброс из контейнера)
|
||||||
|
- **Функции**:
|
||||||
|
- Health check endpoint
|
||||||
|
- Метрики производительности (если включены)
|
||||||
|
- API для управления ботом
|
||||||
|
|
||||||
|
### 🌐 Сетевые настройки
|
||||||
|
|
||||||
|
- **Основная сеть**: `bots_network` (192.168.100.0/24)
|
||||||
|
- **Все сервисы**: Работают в одной Docker сети для взаимодействия
|
||||||
|
- **Внешний доступ**: Только порты 3000, 8080, 9090
|
||||||
|
- **Внутренние порты**: 9091 доступен только внутри Docker сети
|
||||||
|
|
||||||
## 🔧 Модуль мониторинга
|
## 🔧 Модуль мониторинга
|
||||||
|
|
||||||
@@ -131,3 +186,121 @@ curl http://localhost:3000/api/health
|
|||||||
```bash
|
```bash
|
||||||
docker-compose down
|
docker-compose down
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 🛠️ Управление через Makefile
|
||||||
|
|
||||||
|
В корневой директории доступен Makefile с удобными командами для управления инфраструктурой:
|
||||||
|
|
||||||
|
### 🚀 Основные команды
|
||||||
|
```bash
|
||||||
|
make help # Показать справку по всем командам
|
||||||
|
make start # Собрать и запустить все сервисы
|
||||||
|
make stop # Остановить все сервисы
|
||||||
|
make restart # Перезапустить все сервисы
|
||||||
|
make status # Показать статус контейнеров
|
||||||
|
make health # Проверить здоровье всех сервисов
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📊 Мониторинг и логи
|
||||||
|
```bash
|
||||||
|
make logs # Логи всех сервисов
|
||||||
|
make logs-monitor # Логи только мониторинга
|
||||||
|
make logs-bot # Логи Telegram бота
|
||||||
|
make logs-errors # Только ошибки из логов
|
||||||
|
make monitoring # Открыть Grafana в браузере
|
||||||
|
make prometheus # Открыть Prometheus в браузере
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔧 Управление отдельными сервисами
|
||||||
|
```bash
|
||||||
|
make restart-monitor # Перезапустить только мониторинг
|
||||||
|
make restart-grafana # Перезапустить только Grafana
|
||||||
|
make restart-prometheus # Перезапустить только Prometheus
|
||||||
|
make restart-bot # Перезапустить только Telegram бота
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🧹 Обслуживание
|
||||||
|
```bash
|
||||||
|
make backup # Создать backup конфигурации
|
||||||
|
make restore FILE=... # Восстановить из backup
|
||||||
|
make clean # Очистить все контейнеры и образы
|
||||||
|
make clean-monitoring # Очистить только данные мониторинга
|
||||||
|
make check-ports # Проверить занятые порты
|
||||||
|
```
|
||||||
|
|
||||||
|
### 🔍 Диагностика
|
||||||
|
```bash
|
||||||
|
make metrics # Показать текущие метрики
|
||||||
|
make check-grafana # Проверить состояние Grafana
|
||||||
|
make test # Запустить все тесты в проекте (инфраструктура + бот)
|
||||||
|
make test-infra # Запустить тесты инфраструктуры (мониторинг)
|
||||||
|
make test-bot # Запустить тесты Telegram бота (201 тест)
|
||||||
|
make test-coverage # Запустить все тесты с отчетом о покрытии
|
||||||
|
make test-clean # Очистить все файлы тестирования и отчеты
|
||||||
|
make reload-prometheus # Перезагрузить конфигурацию Prometheus
|
||||||
|
make reload-grafana # Перезагрузить конфигурацию Grafana
|
||||||
|
|
||||||
|
## 🧪 Тестирование
|
||||||
|
|
||||||
|
Проект включает в себя комплексную систему тестирования для всех компонентов:
|
||||||
|
|
||||||
|
### 📁 Структура тестов
|
||||||
|
|
||||||
|
``` text
|
||||||
|
tests/
|
||||||
|
├── __init__.py # Инициализация корневой директории тестов
|
||||||
|
├── pytest.ini # Конфигурация pytest для всего проекта
|
||||||
|
├── test_pytest_config.py # Тест конфигурации pytest
|
||||||
|
└── infra/ # Тесты инфраструктуры
|
||||||
|
├── __init__.py
|
||||||
|
└── test_infra.py # Тесты модулей мониторинга
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📊 Тесты инфраструктуры (`make test-infra`)
|
||||||
|
- **Расположение**: `tests/infra/test_infra.py`
|
||||||
|
- **Количество тестов**: 7 тестов
|
||||||
|
- **Покрытие**: Основные модули мониторинга
|
||||||
|
- **Проверяет**:
|
||||||
|
- Импорт всех модулей
|
||||||
|
- Создание экземпляров классов
|
||||||
|
- Структуру системной информации
|
||||||
|
- Структуру метрик
|
||||||
|
|
||||||
|
### 🤖 Тесты Telegram бота (`make test-bot`)
|
||||||
|
- **Расположение**: `bots/telegram-helper-bot/tests/` (отдельная директория)
|
||||||
|
- **Количество тестов**: 201 тест
|
||||||
|
- **Покрытие**: Все основные компоненты бота
|
||||||
|
- **Проверяет**:
|
||||||
|
- База данных и операции с ней
|
||||||
|
- Обработчики сообщений (admin, group, private)
|
||||||
|
- Клавиатуры и фильтры
|
||||||
|
- Утилиты и вспомогательные функции
|
||||||
|
- Автоматическое разбанивание пользователей
|
||||||
|
- Интеграционные тесты
|
||||||
|
|
||||||
|
### 🚀 Запуск всех тестов (`make test`)
|
||||||
|
Команда `make test` последовательно запускает:
|
||||||
|
1. Тесты инфраструктуры
|
||||||
|
2. Тесты Telegram бота
|
||||||
|
3. Выводит общую статистику
|
||||||
|
|
||||||
|
### 📊 Анализ покрытия тестами (`make test-coverage`)
|
||||||
|
Команда `make test-coverage` запускает все тесты с детальным анализом покрытия:
|
||||||
|
1. **Тесты инфраструктуры** с покрытием модулей мониторинга
|
||||||
|
2. **Тесты Telegram бота** с покрытием всех компонентов
|
||||||
|
3. **HTML отчеты** сохраняются в `htmlcov/` для детального анализа
|
||||||
|
|
||||||
|
### 🧹 Очистка тестовых файлов (`make test-clean`)
|
||||||
|
Команда `make test-clean` удаляет все файлы, созданные в процессе тестирования:
|
||||||
|
- Кэш pytest (`.pytest_cache/`)
|
||||||
|
- Отчеты о покрытии (`htmlcov/`, `.coverage`)
|
||||||
|
- Скомпилированные Python файлы (`*.pyc`, `__pycache__`)
|
||||||
|
- Временные файлы тестирования
|
||||||
|
|
||||||
|
### ⚙️ Конфигурация pytest
|
||||||
|
|
||||||
|
Проект использует централизованную конфигурацию pytest (`pytest.ini`):
|
||||||
|
- **Автоматическое обнаружение**: тесты в директории `tests/`
|
||||||
|
- **Настройки asyncio**: автоматический режим для асинхронных тестов
|
||||||
|
- **Маркеры**: `slow`, `integration`, `unit` для категоризации тестов
|
||||||
|
- **Форматирование**: краткий вывод ошибок, отключение предупреждений
|
||||||
|
|||||||
1
bots/telegram-helper-bot
Submodule
1
bots/telegram-helper-bot
Submodule
Submodule bots/telegram-helper-bot added at 5c2f9e501d
77
count_tests.py
Normal file
77
count_tests.py
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Скрипт для подсчета количества тестов в проекте
|
||||||
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
def count_tests_in_directory(directory):
|
||||||
|
"""Подсчитывает количество тестов в указанной директории"""
|
||||||
|
try:
|
||||||
|
# Запускаем pytest --collect-only для подсчета тестов
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, '-m', 'pytest', directory, '--collect-only', '-q'],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
cwd=os.getcwd()
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
# Ищем строку с количеством собранных тестов
|
||||||
|
for line in result.stdout.split('\n'):
|
||||||
|
if 'collected' in line:
|
||||||
|
# Извлекаем число из строки вида "78 collected"
|
||||||
|
parts = line.strip().split()
|
||||||
|
for part in parts:
|
||||||
|
if part.isdigit():
|
||||||
|
return int(part)
|
||||||
|
return 0
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка при подсчете тестов в {directory}: {e}", file=sys.stderr)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def count_bot_tests():
|
||||||
|
"""Подсчитывает количество тестов бота"""
|
||||||
|
try:
|
||||||
|
# Переходим в директорию бота и запускаем pytest
|
||||||
|
bot_dir = os.path.join(os.getcwd(), 'bots', 'telegram-helper-bot')
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, '-m', 'pytest', 'tests/', '--collect-only', '-q'],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
cwd=bot_dir
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
# Ищем строку с количеством собранных тестов
|
||||||
|
for line in result.stdout.split('\n'):
|
||||||
|
if 'collected' in line:
|
||||||
|
# Извлекаем число из строки вида "201 collected"
|
||||||
|
parts = line.strip().split()
|
||||||
|
for part in parts:
|
||||||
|
if part.isdigit():
|
||||||
|
return int(part)
|
||||||
|
return 0
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка при подсчете тестов бота: {e}", file=sys.stderr)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Основная функция"""
|
||||||
|
# Подсчитываем тесты инфраструктуры
|
||||||
|
infra_tests = count_tests_in_directory('tests/infra/')
|
||||||
|
|
||||||
|
# Подсчитываем тесты бота
|
||||||
|
bot_tests = count_bot_tests()
|
||||||
|
|
||||||
|
total_tests = infra_tests + bot_tests
|
||||||
|
|
||||||
|
# Выводим результат в формате для Makefile
|
||||||
|
print(f"{infra_tests}")
|
||||||
|
print(f"{bot_tests}")
|
||||||
|
print(f"{total_tests}")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -82,6 +82,8 @@ services:
|
|||||||
dockerfile: Dockerfile.bot
|
dockerfile: Dockerfile.bot
|
||||||
container_name: bots_telegram_bot
|
container_name: bots_telegram_bot
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- ./bots/telegram-helper-bot/.env
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -22,15 +22,16 @@ scrape_configs:
|
|||||||
|
|
||||||
- job_name: 'telegram-helper-bot'
|
- job_name: 'telegram-helper-bot'
|
||||||
static_configs:
|
static_configs:
|
||||||
- targets: ['telegram-helper-bot:8080'] # Или IP адрес сервера с ботом
|
- targets: ['bots_telegram_bot:8080'] # Имя контейнера из docker-compose
|
||||||
metrics_path: '/metrics'
|
|
||||||
scrape_interval: 15s
|
|
||||||
scrape_timeout: 10s
|
|
||||||
honor_labels: true
|
|
||||||
labels:
|
labels:
|
||||||
bot_name: 'telegram-helper-bot'
|
bot_name: 'telegram-helper-bot'
|
||||||
environment: 'production'
|
environment: 'production'
|
||||||
service: 'telegram-bot'
|
service: 'telegram-bot'
|
||||||
|
metrics_path: '/metrics'
|
||||||
|
scrape_interval: 15s
|
||||||
|
scrape_timeout: 10s
|
||||||
|
honor_labels: true
|
||||||
|
|
||||||
alerting:
|
alerting:
|
||||||
alertmanagers:
|
alertmanagers:
|
||||||
- static_configs:
|
- static_configs:
|
||||||
|
|||||||
16
pytest.ini
Normal file
16
pytest.ini
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[tool:pytest]
|
||||||
|
testpaths = tests
|
||||||
|
python_files = test_*.py
|
||||||
|
python_classes = Test*
|
||||||
|
python_functions = test_*
|
||||||
|
addopts =
|
||||||
|
-v
|
||||||
|
--tb=short
|
||||||
|
--strict-markers
|
||||||
|
--disable-warnings
|
||||||
|
markers =
|
||||||
|
slow: marks tests as slow (deselect with '-m "not slow"')
|
||||||
|
integration: marks tests as integration tests
|
||||||
|
unit: marks tests as unit tests
|
||||||
|
asyncio_mode = auto
|
||||||
|
asyncio_default_fixture_loop_scope = function
|
||||||
3
tests/__init__.py
Normal file
3
tests/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Корневая директория для всех тестов проекта
|
||||||
|
"""
|
||||||
3
tests/infra/__init__.py
Normal file
3
tests/infra/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Тесты для инфраструктуры проекта
|
||||||
|
"""
|
||||||
317
tests/infra/conftest.py
Normal file
317
tests/infra/conftest.py
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Общие фикстуры для тестов инфраструктуры
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from unittest.mock import Mock, AsyncMock, patch
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Добавляем путь к модулям мониторинга
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../infra/monitoring'))
|
||||||
|
|
||||||
|
# Настройка pytest-asyncio
|
||||||
|
pytest_plugins = ('pytest_asyncio',)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def event_loop():
|
||||||
|
"""Создает event loop для асинхронных тестов"""
|
||||||
|
loop = asyncio.get_event_loop_policy().new_event_loop()
|
||||||
|
yield loop
|
||||||
|
loop.close()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_metrics_data():
|
||||||
|
"""Создает мок данных метрик для тестов"""
|
||||||
|
return {
|
||||||
|
'cpu_usage_percent': 25.5,
|
||||||
|
'ram_usage_percent': 60.2,
|
||||||
|
'disk_usage_percent': 45.8,
|
||||||
|
'load_average_1m': 1.2,
|
||||||
|
'load_average_5m': 1.1,
|
||||||
|
'load_average_15m': 1.0,
|
||||||
|
'swap_usage_percent': 10.5,
|
||||||
|
'disk_io_percent': 15.3,
|
||||||
|
'system_uptime_seconds': 86400.0,
|
||||||
|
'monitor_uptime_seconds': 3600.0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_system_info():
|
||||||
|
"""Создает мок системной информации для тестов"""
|
||||||
|
return {
|
||||||
|
'cpu_percent': 25.5,
|
||||||
|
'load_avg_1m': 1.2,
|
||||||
|
'load_avg_5m': 1.1,
|
||||||
|
'load_avg_15m': 1.0,
|
||||||
|
'cpu_count': 8,
|
||||||
|
'ram_used': 8.0,
|
||||||
|
'ram_total': 16.0,
|
||||||
|
'ram_percent': 50.0,
|
||||||
|
'swap_used': 1.0,
|
||||||
|
'swap_total': 2.0,
|
||||||
|
'swap_percent': 50.0,
|
||||||
|
'disk_used': 100.0,
|
||||||
|
'disk_total': 500.0,
|
||||||
|
'disk_percent': 20.0,
|
||||||
|
'disk_free': 400.0,
|
||||||
|
'disk_read_speed': '1.0 MB/s',
|
||||||
|
'disk_write_speed': '512.0 KB/s',
|
||||||
|
'disk_io_percent': 15,
|
||||||
|
'system_uptime': '1д 0ч 0м',
|
||||||
|
'monitor_uptime': '1ч 0м',
|
||||||
|
'server_hostname': 'test-host',
|
||||||
|
'current_time': '2025-01-01 12:00:00'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_psutil():
|
||||||
|
"""Создает мок для psutil"""
|
||||||
|
mock_psutil = Mock()
|
||||||
|
|
||||||
|
# Мокаем CPU
|
||||||
|
mock_psutil.cpu_percent.return_value = 25.5
|
||||||
|
mock_psutil.getloadavg.return_value = (1.2, 1.1, 1.0)
|
||||||
|
mock_psutil.cpu_count.return_value = 8
|
||||||
|
|
||||||
|
# Мокаем память
|
||||||
|
mock_memory = Mock()
|
||||||
|
mock_memory.used = 8 * (1024**3) # 8 GB
|
||||||
|
mock_memory.total = 16 * (1024**3) # 16 GB
|
||||||
|
mock_psutil.virtual_memory.return_value = mock_memory
|
||||||
|
|
||||||
|
mock_swap = Mock()
|
||||||
|
mock_swap.used = 1 * (1024**3) # 1 GB
|
||||||
|
mock_swap.total = 2 * (1024**3) # 2 GB
|
||||||
|
mock_swap.percent = 50.0
|
||||||
|
mock_psutil.swap_memory.return_value = mock_swap
|
||||||
|
|
||||||
|
# Мокаем диск
|
||||||
|
mock_disk = Mock()
|
||||||
|
mock_disk.used = 100 * (1024**3) # 100 GB
|
||||||
|
mock_disk.total = 500 * (1024**3) # 500 GB
|
||||||
|
mock_disk.free = 400 * (1024**3) # 400 GB
|
||||||
|
mock_psutil.disk_usage.return_value = mock_disk
|
||||||
|
|
||||||
|
# Мокаем disk I/O
|
||||||
|
mock_disk_io = Mock()
|
||||||
|
mock_disk_io.read_count = 1000
|
||||||
|
mock_disk_io.write_count = 500
|
||||||
|
mock_disk_io.read_bytes = 1024 * (1024**2) # 1 GB
|
||||||
|
mock_disk_io.write_bytes = 512 * (1024**2) # 512 MB
|
||||||
|
mock_psutil.disk_io_counters.return_value = mock_disk_io
|
||||||
|
|
||||||
|
# Мокаем boot time
|
||||||
|
import time
|
||||||
|
mock_psutil.boot_time.return_value = time.time() - 86400 # 1 день назад
|
||||||
|
|
||||||
|
return mock_psutil
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_platform():
|
||||||
|
"""Создает мок для platform"""
|
||||||
|
mock_platform = Mock()
|
||||||
|
mock_platform.system.return_value = 'Linux'
|
||||||
|
return mock_platform
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_subprocess():
|
||||||
|
"""Создает мок для subprocess"""
|
||||||
|
mock_subprocess = Mock()
|
||||||
|
|
||||||
|
# Мокаем успешный результат diskutil
|
||||||
|
mock_result = Mock()
|
||||||
|
mock_result.returncode = 0
|
||||||
|
mock_result.stdout = """
|
||||||
|
Container Total Space: 500.0 GB
|
||||||
|
Container Free Space: 400.0 GB
|
||||||
|
"""
|
||||||
|
mock_subprocess.run.return_value = mock_result
|
||||||
|
|
||||||
|
return mock_subprocess
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_os():
|
||||||
|
"""Создает мок для os"""
|
||||||
|
mock_os = Mock()
|
||||||
|
mock_os.getenv.side_effect = lambda key, default=None: {
|
||||||
|
'THRESHOLD': '80.0',
|
||||||
|
'RECOVERY_THRESHOLD': '75.0'
|
||||||
|
}.get(key, default)
|
||||||
|
|
||||||
|
# Мокаем uname
|
||||||
|
mock_uname = Mock()
|
||||||
|
mock_uname.nodename = "test-host"
|
||||||
|
mock_os.uname.return_value = mock_uname
|
||||||
|
|
||||||
|
return mock_os
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def prometheus_config_sample():
|
||||||
|
"""Создает пример конфигурации Prometheus для тестов"""
|
||||||
|
return {
|
||||||
|
'global': {
|
||||||
|
'scrape_interval': '15s',
|
||||||
|
'evaluation_interval': '15s'
|
||||||
|
},
|
||||||
|
'rule_files': [
|
||||||
|
'# - "first_rules.yml"',
|
||||||
|
'# - "second_rules.yml"'
|
||||||
|
],
|
||||||
|
'scrape_configs': [
|
||||||
|
{
|
||||||
|
'job_name': 'prometheus',
|
||||||
|
'static_configs': [
|
||||||
|
{
|
||||||
|
'targets': ['localhost:9090']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'job_name': 'infrastructure',
|
||||||
|
'static_configs': [
|
||||||
|
{
|
||||||
|
'targets': ['host.docker.internal:9091']
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'metrics_path': '/metrics',
|
||||||
|
'scrape_interval': '30s',
|
||||||
|
'scrape_timeout': '10s',
|
||||||
|
'honor_labels': True
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'job_name': 'telegram-helper-bot',
|
||||||
|
'static_configs': [
|
||||||
|
{
|
||||||
|
'targets': ['bots_telegram_bot:8080'],
|
||||||
|
'labels': {
|
||||||
|
'bot_name': 'telegram-helper-bot',
|
||||||
|
'environment': 'production',
|
||||||
|
'service': 'telegram-bot'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'metrics_path': '/metrics',
|
||||||
|
'scrape_interval': '15s',
|
||||||
|
'scrape_timeout': '10s',
|
||||||
|
'honor_labels': True
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'alerting': {
|
||||||
|
'alertmanagers': [
|
||||||
|
{
|
||||||
|
'static_configs': [
|
||||||
|
{
|
||||||
|
'targets': [
|
||||||
|
'# - alertmanager:9093'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_aiohttp():
|
||||||
|
"""Создает мок для aiohttp"""
|
||||||
|
mock_aiohttp = Mock()
|
||||||
|
|
||||||
|
# Мокаем web.Application
|
||||||
|
mock_app = Mock()
|
||||||
|
mock_aiohttp.web.Application.return_value = mock_app
|
||||||
|
|
||||||
|
# Мокаем web.Response
|
||||||
|
mock_response = Mock()
|
||||||
|
mock_response.status = 200
|
||||||
|
mock_response.content_type = 'text/plain'
|
||||||
|
mock_response.text = 'Test response'
|
||||||
|
mock_aiohttp.web.Response.return_value = mock_response
|
||||||
|
|
||||||
|
return mock_aiohttp
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_request():
|
||||||
|
"""Создает мок для HTTP запроса"""
|
||||||
|
request = Mock()
|
||||||
|
request.method = 'GET'
|
||||||
|
request.path = '/metrics'
|
||||||
|
request.headers = {}
|
||||||
|
return request
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def test_environment():
|
||||||
|
"""Создает тестовое окружение"""
|
||||||
|
return {
|
||||||
|
'os_type': 'ubuntu',
|
||||||
|
'threshold': 80.0,
|
||||||
|
'recovery_threshold': 75.0,
|
||||||
|
'host': '127.0.0.1',
|
||||||
|
'port': 9091
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Маркеры для категоризации тестов
|
||||||
|
def pytest_configure(config):
|
||||||
|
"""Настройка маркеров pytest"""
|
||||||
|
config.addinivalue_line(
|
||||||
|
"markers", "asyncio: mark test as async"
|
||||||
|
)
|
||||||
|
config.addinivalue_line(
|
||||||
|
"markers", "slow: mark test as slow"
|
||||||
|
)
|
||||||
|
config.addinivalue_line(
|
||||||
|
"markers", "integration: mark test as integration test"
|
||||||
|
)
|
||||||
|
config.addinivalue_line(
|
||||||
|
"markers", "unit: mark test as unit test"
|
||||||
|
)
|
||||||
|
config.addinivalue_line(
|
||||||
|
"markers", "prometheus: mark test as prometheus related"
|
||||||
|
)
|
||||||
|
config.addinivalue_line(
|
||||||
|
"markers", "metrics: mark test as metrics related"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Автоматическая маркировка тестов
|
||||||
|
def pytest_collection_modifyitems(config, items):
|
||||||
|
"""Автоматически маркирует тесты по их расположению"""
|
||||||
|
for item in items:
|
||||||
|
# Маркируем асинхронные тесты
|
||||||
|
if "async" in item.name or "Async" in item.name:
|
||||||
|
item.add_marker(pytest.mark.asyncio)
|
||||||
|
|
||||||
|
# Маркируем интеграционные тесты
|
||||||
|
if "integration" in item.name.lower() or "Integration" in str(item.cls):
|
||||||
|
item.add_marker(pytest.mark.integration)
|
||||||
|
|
||||||
|
# Маркируем unit тесты
|
||||||
|
if "unit" in item.name.lower() or "Unit" in str(item.cls):
|
||||||
|
item.add_marker(pytest.mark.unit)
|
||||||
|
|
||||||
|
# Маркируем медленные тесты
|
||||||
|
if "slow" in item.name.lower() or "Slow" in str(item.cls):
|
||||||
|
item.add_marker(pytest.mark.slow)
|
||||||
|
|
||||||
|
# Маркируем тесты Prometheus
|
||||||
|
if "prometheus" in item.name.lower() or "Prometheus" in str(item.cls):
|
||||||
|
item.add_marker(pytest.mark.prometheus)
|
||||||
|
|
||||||
|
# Маркируем тесты метрик
|
||||||
|
if "metrics" in item.name.lower() or "Metrics" in str(item.cls):
|
||||||
|
item.add_marker(pytest.mark.metrics)
|
||||||
7
tests/infra/requirements-test.txt
Normal file
7
tests/infra/requirements-test.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
pytest>=7.0.0
|
||||||
|
pytest-asyncio>=0.21.0
|
||||||
|
pytest-mock>=3.10.0
|
||||||
|
pytest-cov>=4.0.0
|
||||||
|
PyYAML>=6.0
|
||||||
|
aiohttp>=3.8.0
|
||||||
|
psutil>=5.9.0
|
||||||
102
tests/infra/test_infra.py
Normal file
102
tests/infra/test_infra.py
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Тесты для инфраструктуры мониторинга
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# Добавляем путь к модулям мониторинга
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../infra/monitoring'))
|
||||||
|
|
||||||
|
def test_imports():
|
||||||
|
"""Тест импорта основных модулей"""
|
||||||
|
try:
|
||||||
|
from metrics_collector import MetricsCollector
|
||||||
|
from message_sender import MessageSender
|
||||||
|
from prometheus_server import PrometheusServer
|
||||||
|
from server_monitor import ServerMonitor
|
||||||
|
assert True
|
||||||
|
except ImportError as e:
|
||||||
|
pytest.fail(f"Failed to import modules: {e}")
|
||||||
|
|
||||||
|
def test_metrics_collector_creation():
|
||||||
|
"""Тест создания MetricsCollector"""
|
||||||
|
try:
|
||||||
|
from metrics_collector import MetricsCollector
|
||||||
|
collector = MetricsCollector()
|
||||||
|
assert collector is not None
|
||||||
|
assert hasattr(collector, 'get_system_info')
|
||||||
|
assert hasattr(collector, 'get_metrics_data')
|
||||||
|
except Exception as e:
|
||||||
|
pytest.fail(f"Failed to create MetricsCollector: {e}")
|
||||||
|
|
||||||
|
def test_message_sender_creation():
|
||||||
|
"""Тест создания MessageSender"""
|
||||||
|
try:
|
||||||
|
from message_sender import MessageSender
|
||||||
|
sender = MessageSender()
|
||||||
|
assert sender is not None
|
||||||
|
except Exception as e:
|
||||||
|
pytest.fail(f"Failed to create MessageSender: {e}")
|
||||||
|
|
||||||
|
def test_prometheus_server_creation():
|
||||||
|
"""Тест создания PrometheusServer"""
|
||||||
|
try:
|
||||||
|
from prometheus_server import PrometheusServer
|
||||||
|
server = PrometheusServer()
|
||||||
|
assert server is not None
|
||||||
|
assert hasattr(server, 'host')
|
||||||
|
assert hasattr(server, 'port')
|
||||||
|
except Exception as e:
|
||||||
|
pytest.fail(f"Failed to create PrometheusServer: {e}")
|
||||||
|
|
||||||
|
def test_server_monitor_creation():
|
||||||
|
"""Тест создания ServerMonitor"""
|
||||||
|
try:
|
||||||
|
from server_monitor import ServerMonitor
|
||||||
|
monitor = ServerMonitor()
|
||||||
|
assert monitor is not None
|
||||||
|
assert hasattr(monitor, 'metrics_collector')
|
||||||
|
assert hasattr(monitor, 'message_sender')
|
||||||
|
assert hasattr(monitor, 'prometheus_server')
|
||||||
|
except Exception as e:
|
||||||
|
pytest.fail(f"Failed to create ServerMonitor: {e}")
|
||||||
|
|
||||||
|
def test_system_info_structure():
|
||||||
|
"""Тест структуры системной информации"""
|
||||||
|
try:
|
||||||
|
from metrics_collector import MetricsCollector
|
||||||
|
collector = MetricsCollector()
|
||||||
|
system_info = collector.get_system_info()
|
||||||
|
|
||||||
|
# Проверяем, что system_info это словарь
|
||||||
|
assert isinstance(system_info, dict)
|
||||||
|
|
||||||
|
# Проверяем наличие основных ключей
|
||||||
|
expected_keys = ['cpu_percent', 'ram_percent', 'disk_percent', 'server_hostname']
|
||||||
|
for key in expected_keys:
|
||||||
|
assert key in system_info, f"Missing key: {key}"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
pytest.fail(f"Failed to get system info: {e}")
|
||||||
|
|
||||||
|
def test_metrics_data_structure():
|
||||||
|
"""Тест структуры метрик"""
|
||||||
|
try:
|
||||||
|
from metrics_collector import MetricsCollector
|
||||||
|
collector = MetricsCollector()
|
||||||
|
metrics = collector.get_metrics_data()
|
||||||
|
|
||||||
|
# Проверяем, что metrics это словарь
|
||||||
|
assert isinstance(metrics, dict)
|
||||||
|
|
||||||
|
# Проверяем, что есть хотя бы одна метрика
|
||||||
|
assert len(metrics) > 0, "Metrics should not be empty"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
pytest.fail(f"Failed to get metrics data: {e}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pytest.main([__file__, "-v"])
|
||||||
441
tests/infra/test_metrics_collector.py
Normal file
441
tests/infra/test_metrics_collector.py
Normal file
@@ -0,0 +1,441 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Тесты для MetricsCollector
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import platform
|
||||||
|
from unittest.mock import Mock, patch, MagicMock
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Добавляем путь к модулям мониторинга
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../infra/monitoring'))
|
||||||
|
|
||||||
|
from metrics_collector import MetricsCollector
|
||||||
|
|
||||||
|
|
||||||
|
class TestMetricsCollector:
|
||||||
|
"""Тесты для класса MetricsCollector"""
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def metrics_collector(self):
|
||||||
|
"""Создает экземпляр MetricsCollector для тестов"""
|
||||||
|
return MetricsCollector()
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_psutil(self):
|
||||||
|
"""Мок для psutil"""
|
||||||
|
mock_psutil = Mock()
|
||||||
|
|
||||||
|
# Мокаем CPU
|
||||||
|
mock_psutil.cpu_percent.return_value = 25.5
|
||||||
|
mock_psutil.getloadavg.return_value = (1.2, 1.1, 1.0)
|
||||||
|
mock_psutil.cpu_count.return_value = 8
|
||||||
|
|
||||||
|
# Мокаем память
|
||||||
|
mock_memory = Mock()
|
||||||
|
mock_memory.used = 8 * (1024**3) # 8 GB
|
||||||
|
mock_memory.total = 16 * (1024**3) # 16 GB
|
||||||
|
mock_psutil.virtual_memory.return_value = mock_memory
|
||||||
|
|
||||||
|
mock_swap = Mock()
|
||||||
|
mock_swap.used = 1 * (1024**3) # 1 GB
|
||||||
|
mock_swap.total = 2 * (1024**3) # 2 GB
|
||||||
|
mock_swap.percent = 50.0
|
||||||
|
mock_psutil.swap_memory.return_value = mock_swap
|
||||||
|
|
||||||
|
# Мокаем диск
|
||||||
|
mock_disk = Mock()
|
||||||
|
mock_disk.used = 100 * (1024**3) # 100 GB
|
||||||
|
mock_disk.total = 500 * (1024**3) # 500 GB
|
||||||
|
mock_disk.free = 400 * (1024**3) # 400 GB
|
||||||
|
mock_psutil.disk_usage.return_value = mock_disk
|
||||||
|
|
||||||
|
# Мокаем disk I/O
|
||||||
|
mock_disk_io = Mock()
|
||||||
|
mock_disk_io.read_count = 1000
|
||||||
|
mock_disk_io.write_count = 500
|
||||||
|
mock_disk_io.read_bytes = 1024 * (1024**2) # 1 GB
|
||||||
|
mock_disk_io.write_bytes = 512 * (1024**2) # 512 MB
|
||||||
|
mock_psutil.disk_io_counters.return_value = mock_disk_io
|
||||||
|
|
||||||
|
# Мокаем boot time
|
||||||
|
mock_psutil.boot_time.return_value = time.time() - 86400 # 1 день назад
|
||||||
|
|
||||||
|
return mock_psutil
|
||||||
|
|
||||||
|
def test_init(self, metrics_collector):
|
||||||
|
"""Тест инициализации MetricsCollector"""
|
||||||
|
assert metrics_collector.threshold == 80.0
|
||||||
|
assert metrics_collector.recovery_threshold == 75.0
|
||||||
|
assert isinstance(metrics_collector.alert_states, dict)
|
||||||
|
assert 'cpu' in metrics_collector.alert_states
|
||||||
|
assert 'ram' in metrics_collector.alert_states
|
||||||
|
assert 'disk' in metrics_collector.alert_states
|
||||||
|
assert metrics_collector.monitor_start_time > 0
|
||||||
|
|
||||||
|
def test_detect_os_macos(self):
|
||||||
|
"""Тест определения macOS"""
|
||||||
|
with patch('platform.system', return_value='Darwin'):
|
||||||
|
collector = MetricsCollector()
|
||||||
|
assert collector.os_type == "macos"
|
||||||
|
|
||||||
|
def test_detect_os_linux(self):
|
||||||
|
"""Тест определения Linux"""
|
||||||
|
with patch('platform.system', return_value='Linux'):
|
||||||
|
collector = MetricsCollector()
|
||||||
|
assert collector.os_type == "ubuntu"
|
||||||
|
|
||||||
|
def test_detect_os_unknown(self):
|
||||||
|
"""Тест определения неизвестной ОС"""
|
||||||
|
with patch('platform.system', return_value='Windows'):
|
||||||
|
collector = MetricsCollector()
|
||||||
|
assert collector.os_type == "unknown"
|
||||||
|
|
||||||
|
def test_get_disk_path(self, metrics_collector):
|
||||||
|
"""Тест получения пути к диску"""
|
||||||
|
# Для всех ОС должен возвращаться "/"
|
||||||
|
assert metrics_collector._get_disk_path() == "/"
|
||||||
|
|
||||||
|
@patch('subprocess.run')
|
||||||
|
def test_get_macos_disk_usage_success(self, mock_subprocess, metrics_collector):
|
||||||
|
"""Тест получения информации о диске macOS через diskutil"""
|
||||||
|
# Настраиваем мок для macOS
|
||||||
|
metrics_collector.os_type = "macos"
|
||||||
|
|
||||||
|
# Мокаем успешный вывод diskutil
|
||||||
|
mock_result = Mock()
|
||||||
|
mock_result.returncode = 0
|
||||||
|
mock_result.stdout = """
|
||||||
|
Container Total Space: 500.0 GB
|
||||||
|
Container Free Space: 400.0 GB
|
||||||
|
"""
|
||||||
|
mock_subprocess.return_value = mock_result
|
||||||
|
|
||||||
|
disk_info = metrics_collector._get_macos_disk_usage()
|
||||||
|
|
||||||
|
assert disk_info is not None
|
||||||
|
assert disk_info.total == 500.0 * (1024**3) # В байтах
|
||||||
|
assert disk_info.free == 400.0 * (1024**3)
|
||||||
|
assert disk_info.used == 100.0 * (1024**3)
|
||||||
|
|
||||||
|
@patch('subprocess.run')
|
||||||
|
def test_get_macos_disk_usage_fallback(self, mock_subprocess, metrics_collector):
|
||||||
|
"""Тест fallback к psutil при ошибке diskutil"""
|
||||||
|
metrics_collector.os_type = "macos"
|
||||||
|
|
||||||
|
# Мокаем неуспешный вывод diskutil
|
||||||
|
mock_result = Mock()
|
||||||
|
mock_result.returncode = 1
|
||||||
|
mock_subprocess.return_value = mock_result
|
||||||
|
|
||||||
|
with patch('metrics_collector.psutil.disk_usage') as mock_psutil_disk:
|
||||||
|
mock_disk = Mock()
|
||||||
|
mock_disk.used = 100 * (1024**3)
|
||||||
|
mock_disk.total = 500 * (1024**3)
|
||||||
|
mock_disk.free = 400 * (1024**3)
|
||||||
|
mock_psutil_disk.return_value = mock_disk
|
||||||
|
|
||||||
|
disk_info = metrics_collector._get_macos_disk_usage()
|
||||||
|
assert disk_info == mock_disk
|
||||||
|
|
||||||
|
def test_get_system_uptime(self, metrics_collector):
|
||||||
|
"""Тест получения uptime системы"""
|
||||||
|
with patch('metrics_collector.psutil.boot_time') as mock_boot_time:
|
||||||
|
mock_boot_time.return_value = time.time() - 3600 # 1 час назад
|
||||||
|
|
||||||
|
uptime = metrics_collector._get_system_uptime()
|
||||||
|
assert uptime > 0
|
||||||
|
assert uptime <= 3600.1 # Не больше часа (с небольшим допуском)
|
||||||
|
|
||||||
|
def test_get_monitor_uptime(self, metrics_collector):
|
||||||
|
"""Тест получения uptime мониторинга"""
|
||||||
|
# Ждем немного, чтобы uptime изменился
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
uptime = metrics_collector.get_monitor_uptime()
|
||||||
|
assert isinstance(uptime, str)
|
||||||
|
assert 'м' in uptime or 'ч' in uptime or 'д' in uptime
|
||||||
|
|
||||||
|
@patch('metrics_collector.psutil')
|
||||||
|
def test_get_system_info_success(self, mock_psutil, metrics_collector):
|
||||||
|
"""Тест получения системной информации"""
|
||||||
|
# Настраиваем моки
|
||||||
|
mock_psutil.cpu_percent.return_value = 25.5
|
||||||
|
mock_psutil.getloadavg.return_value = (1.2, 1.1, 1.0)
|
||||||
|
mock_psutil.cpu_count.return_value = 8
|
||||||
|
|
||||||
|
mock_memory = Mock()
|
||||||
|
mock_memory.used = 8 * (1024**3)
|
||||||
|
mock_memory.total = 16 * (1024**3)
|
||||||
|
mock_psutil.virtual_memory.return_value = mock_memory
|
||||||
|
|
||||||
|
mock_swap = Mock()
|
||||||
|
mock_swap.used = 1 * (1024**3)
|
||||||
|
mock_swap.total = 2 * (1024**3)
|
||||||
|
mock_swap.percent = 50.0
|
||||||
|
mock_psutil.swap_memory.return_value = mock_swap
|
||||||
|
|
||||||
|
mock_disk = Mock()
|
||||||
|
mock_disk.used = 100 * (1024**3)
|
||||||
|
mock_disk.total = 500 * (1024**3)
|
||||||
|
mock_disk.free = 400 * (1024**3)
|
||||||
|
mock_psutil.disk_usage.return_value = mock_disk
|
||||||
|
|
||||||
|
# Мокаем _get_disk_usage чтобы возвращал наш мок
|
||||||
|
with patch.object(metrics_collector, '_get_disk_usage', return_value=mock_disk):
|
||||||
|
mock_disk_io = Mock()
|
||||||
|
mock_disk_io.read_count = 1000
|
||||||
|
mock_disk_io.write_count = 500
|
||||||
|
mock_disk_io.read_bytes = 1024 * (1024**2)
|
||||||
|
mock_disk_io.write_bytes = 512 * (1024**2)
|
||||||
|
mock_psutil.disk_io_counters.return_value = mock_disk_io
|
||||||
|
|
||||||
|
mock_psutil.boot_time.return_value = time.time() - 86400
|
||||||
|
|
||||||
|
with patch('os.uname') as mock_uname:
|
||||||
|
mock_uname.return_value.nodename = "test-host"
|
||||||
|
|
||||||
|
system_info = metrics_collector.get_system_info()
|
||||||
|
|
||||||
|
assert isinstance(system_info, dict)
|
||||||
|
assert 'cpu_percent' in system_info
|
||||||
|
assert 'ram_percent' in system_info
|
||||||
|
assert 'disk_percent' in system_info
|
||||||
|
assert 'server_hostname' in system_info
|
||||||
|
|
||||||
|
# Проверяем расчеты
|
||||||
|
assert system_info['cpu_percent'] == 25.5
|
||||||
|
assert system_info['ram_percent'] == 50.0 # 8/16 * 100
|
||||||
|
assert system_info['disk_percent'] == 20.0 # 100/500 * 100
|
||||||
|
assert system_info['server_hostname'] == "test-host"
|
||||||
|
|
||||||
|
def test_get_system_info_error(self, metrics_collector):
|
||||||
|
"""Тест получения системной информации при ошибке"""
|
||||||
|
with patch('metrics_collector.psutil.cpu_percent', side_effect=Exception("Test error")):
|
||||||
|
system_info = metrics_collector.get_system_info()
|
||||||
|
assert system_info == {}
|
||||||
|
|
||||||
|
def test_format_bytes(self, metrics_collector):
|
||||||
|
"""Тест форматирования байтов"""
|
||||||
|
assert metrics_collector._format_bytes(0) == "0 B"
|
||||||
|
assert metrics_collector._format_bytes(1024) == "1.0 KB"
|
||||||
|
assert metrics_collector._format_bytes(1024**2) == "1.0 MB"
|
||||||
|
assert metrics_collector._format_bytes(1024**3) == "1.0 GB"
|
||||||
|
assert metrics_collector._format_bytes(1024**4) == "1.0 TB"
|
||||||
|
|
||||||
|
def test_format_uptime(self, metrics_collector):
|
||||||
|
"""Тест форматирования uptime"""
|
||||||
|
assert metrics_collector._format_uptime(60) == "1м"
|
||||||
|
assert metrics_collector._format_uptime(3600) == "1ч 0м"
|
||||||
|
assert metrics_collector._format_uptime(86400) == "1д 0ч 0м"
|
||||||
|
assert metrics_collector._format_uptime(90000) == "1д 1ч 0м"
|
||||||
|
|
||||||
|
def test_check_process_status_pid_file(self, metrics_collector, tmp_path):
|
||||||
|
"""Тест проверки статуса процесса по PID файлу"""
|
||||||
|
# Создаем временный PID файл
|
||||||
|
pid_file = tmp_path / "helper_bot.pid"
|
||||||
|
pid_file.write_text("12345")
|
||||||
|
|
||||||
|
# Временно заменяем путь к PID файлу
|
||||||
|
original_pid_files = metrics_collector.pid_files.copy()
|
||||||
|
metrics_collector.pid_files['helper_bot'] = str(pid_file)
|
||||||
|
|
||||||
|
with patch('metrics_collector.psutil.pid_exists', return_value=True), \
|
||||||
|
patch('metrics_collector.psutil.Process') as mock_process:
|
||||||
|
|
||||||
|
mock_proc = Mock()
|
||||||
|
mock_proc.create_time.return_value = time.time() - 3600
|
||||||
|
mock_process.return_value = mock_proc
|
||||||
|
|
||||||
|
status, uptime = metrics_collector.check_process_status('helper_bot')
|
||||||
|
|
||||||
|
assert status == "✅"
|
||||||
|
assert "Uptime" in uptime
|
||||||
|
|
||||||
|
# Восстанавливаем оригинальные PID файлы
|
||||||
|
metrics_collector.pid_files = original_pid_files
|
||||||
|
|
||||||
|
def test_check_process_status_not_running(self, metrics_collector):
|
||||||
|
"""Тест проверки статуса неработающего процесса"""
|
||||||
|
with patch('metrics_collector.psutil.process_iter', return_value=[]):
|
||||||
|
status, message = metrics_collector.check_process_status('nonexistent_bot')
|
||||||
|
assert status == "❌"
|
||||||
|
assert message == "Выключен"
|
||||||
|
|
||||||
|
def test_calculate_disk_speed(self, metrics_collector):
|
||||||
|
"""Тест расчета скорости диска"""
|
||||||
|
# Инициализируем базовые значения
|
||||||
|
metrics_collector._initialize_disk_io()
|
||||||
|
|
||||||
|
# Создаем текущую статистику диска
|
||||||
|
current_disk_io = Mock()
|
||||||
|
current_disk_io.read_bytes = 2048 * (1024**2) # 2 GB
|
||||||
|
current_disk_io.write_bytes = 1024 * (1024**2) # 1 GB
|
||||||
|
|
||||||
|
# Ждем немного для расчета скорости
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
read_speed, write_speed = metrics_collector._calculate_disk_speed(current_disk_io)
|
||||||
|
|
||||||
|
assert isinstance(read_speed, str)
|
||||||
|
assert isinstance(write_speed, str)
|
||||||
|
assert "/s" in read_speed
|
||||||
|
assert "/s" in write_speed
|
||||||
|
|
||||||
|
def test_calculate_disk_io_percent(self, metrics_collector):
|
||||||
|
"""Тест расчета процента загрузки диска"""
|
||||||
|
# Инициализируем базовые значения
|
||||||
|
metrics_collector._initialize_disk_io()
|
||||||
|
|
||||||
|
# Создаем текущую статистику диска
|
||||||
|
current_disk_io = Mock()
|
||||||
|
current_disk_io.read_count = 2000
|
||||||
|
current_disk_io.write_count = 1000
|
||||||
|
current_disk_io.read_bytes = 2048 * (1024**2)
|
||||||
|
current_disk_io.write_bytes = 1024 * (1024**2)
|
||||||
|
|
||||||
|
# Ждем немного для расчета
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
io_percent = metrics_collector._calculate_disk_io_percent()
|
||||||
|
|
||||||
|
assert isinstance(io_percent, int)
|
||||||
|
assert 0 <= io_percent <= 100
|
||||||
|
|
||||||
|
def test_get_metrics_data(self, metrics_collector):
|
||||||
|
"""Тест получения данных для метрик Prometheus"""
|
||||||
|
with patch.object(metrics_collector, 'get_system_info') as mock_get_system_info:
|
||||||
|
mock_get_system_info.return_value = {
|
||||||
|
'cpu_percent': 25.5,
|
||||||
|
'ram_percent': 60.2,
|
||||||
|
'disk_percent': 45.8,
|
||||||
|
'load_avg_1m': 1.2,
|
||||||
|
'load_avg_5m': 1.1,
|
||||||
|
'load_avg_15m': 1.0,
|
||||||
|
'swap_percent': 10.5
|
||||||
|
}
|
||||||
|
|
||||||
|
with patch.object(metrics_collector, '_get_system_uptime', return_value=86400.0):
|
||||||
|
metrics_data = metrics_collector.get_metrics_data()
|
||||||
|
|
||||||
|
assert isinstance(metrics_data, dict)
|
||||||
|
assert 'cpu_usage_percent' in metrics_data
|
||||||
|
assert 'ram_usage_percent' in metrics_data
|
||||||
|
assert 'disk_usage_percent' in metrics_data
|
||||||
|
assert 'load_average_1m' in metrics_data
|
||||||
|
assert 'system_uptime_seconds' in metrics_data
|
||||||
|
assert 'monitor_uptime_seconds' in metrics_data
|
||||||
|
|
||||||
|
def test_check_alerts(self, metrics_collector):
|
||||||
|
"""Тест проверки алертов"""
|
||||||
|
# Тестируем превышение порога CPU
|
||||||
|
system_info = {
|
||||||
|
'cpu_percent': 85.0, # Выше порога 80.0
|
||||||
|
'ram_percent': 60.0, # Ниже порога
|
||||||
|
'disk_percent': 70.0, # Ниже порога
|
||||||
|
'load_avg_1m': 2.5,
|
||||||
|
'ram_used': 8.0,
|
||||||
|
'ram_total': 16.0,
|
||||||
|
'disk_free': 300.0
|
||||||
|
}
|
||||||
|
|
||||||
|
alerts, recoveries = metrics_collector.check_alerts(system_info)
|
||||||
|
|
||||||
|
assert len(alerts) == 1
|
||||||
|
assert alerts[0][0] == 'cpu' # Тип алерта
|
||||||
|
assert alerts[0][1] == 85.0 # Значение
|
||||||
|
assert len(recoveries) == 0
|
||||||
|
|
||||||
|
# Проверяем, что состояние алерта изменилось
|
||||||
|
assert metrics_collector.alert_states['cpu'] is True
|
||||||
|
|
||||||
|
# Тестируем восстановление
|
||||||
|
system_info['cpu_percent'] = 70.0 # Ниже recovery threshold 75.0
|
||||||
|
|
||||||
|
alerts, recoveries = metrics_collector.check_alerts(system_info)
|
||||||
|
|
||||||
|
assert len(alerts) == 0
|
||||||
|
assert len(recoveries) == 1
|
||||||
|
assert recoveries[0][0] == 'cpu'
|
||||||
|
assert metrics_collector.alert_states['cpu'] is False
|
||||||
|
|
||||||
|
def test_environment_variables(self):
|
||||||
|
"""Тест работы с переменными окружения"""
|
||||||
|
with patch.dict(os.environ, {'THRESHOLD': '90.0', 'RECOVERY_THRESHOLD': '85.0'}):
|
||||||
|
collector = MetricsCollector()
|
||||||
|
assert collector.threshold == 90.0
|
||||||
|
assert collector.recovery_threshold == 85.0
|
||||||
|
|
||||||
|
def test_metrics_collector_integration(self, metrics_collector):
|
||||||
|
"""Интеграционный тест MetricsCollector"""
|
||||||
|
# Проверяем, что можем получить системную информацию
|
||||||
|
system_info = metrics_collector.get_system_info()
|
||||||
|
|
||||||
|
# Даже если некоторые метрики недоступны, должны получить словарь
|
||||||
|
assert isinstance(system_info, dict)
|
||||||
|
|
||||||
|
# Проверяем, что можем получить метрики для Prometheus
|
||||||
|
metrics_data = metrics_collector.get_metrics_data()
|
||||||
|
assert isinstance(metrics_data, dict)
|
||||||
|
|
||||||
|
# Проверяем, что можем проверить алерты
|
||||||
|
alerts, recoveries = metrics_collector.check_alerts(system_info)
|
||||||
|
assert isinstance(alerts, list)
|
||||||
|
assert isinstance(recoveries, list)
|
||||||
|
|
||||||
|
|
||||||
|
class TestMetricsCollectorEdgeCases:
|
||||||
|
"""Тесты граничных случаев для MetricsCollector"""
|
||||||
|
|
||||||
|
def test_empty_system_info(self):
|
||||||
|
"""Тест работы с пустой системной информацией"""
|
||||||
|
with patch('metrics_collector.psutil.cpu_percent', side_effect=Exception("Test error")):
|
||||||
|
collector = MetricsCollector()
|
||||||
|
system_info = collector.get_system_info()
|
||||||
|
assert system_info == {}
|
||||||
|
|
||||||
|
def test_missing_disk_info(self):
|
||||||
|
"""Тест работы при отсутствии информации о диске"""
|
||||||
|
collector = MetricsCollector()
|
||||||
|
|
||||||
|
with patch.object(collector, '_get_disk_usage', return_value=None):
|
||||||
|
system_info = collector.get_system_info()
|
||||||
|
assert system_info == {}
|
||||||
|
|
||||||
|
def test_disk_io_calculation_without_previous_data(self):
|
||||||
|
"""Тест расчета I/O диска без предыдущих данных"""
|
||||||
|
collector = MetricsCollector()
|
||||||
|
|
||||||
|
# Сбрасываем предыдущие данные
|
||||||
|
collector.last_disk_io = None
|
||||||
|
collector.last_disk_io_time = None
|
||||||
|
|
||||||
|
current_disk_io = Mock()
|
||||||
|
current_disk_io.read_bytes = 1024
|
||||||
|
current_disk_io.write_bytes = 512
|
||||||
|
|
||||||
|
read_speed, write_speed = collector._calculate_disk_speed(current_disk_io)
|
||||||
|
|
||||||
|
assert read_speed == "0 B/s"
|
||||||
|
assert write_speed == "0 B/s"
|
||||||
|
|
||||||
|
def test_uptime_calculation_edge_cases(self):
|
||||||
|
"""Тест расчета uptime для граничных случаев"""
|
||||||
|
collector = MetricsCollector()
|
||||||
|
|
||||||
|
# Тест для очень малого времени
|
||||||
|
assert collector._format_uptime(0) == "0м"
|
||||||
|
assert collector._format_uptime(30) == "0м"
|
||||||
|
|
||||||
|
# Тест для очень большого времени
|
||||||
|
large_uptime = 365 * 24 * 3600 # 1 год
|
||||||
|
uptime_str = collector._format_uptime(large_uptime)
|
||||||
|
assert "д" in uptime_str
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pytest.main([__file__, "-v"])
|
||||||
343
tests/infra/test_prometheus_config.py
Normal file
343
tests/infra/test_prometheus_config.py
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Тесты для конфигурации Prometheus
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import yaml
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Добавляем путь к модулям мониторинга
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../infra/monitoring'))
|
||||||
|
|
||||||
|
|
||||||
|
class TestPrometheusConfig:
|
||||||
|
"""Тесты для конфигурации Prometheus"""
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def prometheus_config_path(self):
|
||||||
|
"""Путь к файлу конфигурации Prometheus"""
|
||||||
|
return Path(__file__).parent.parent.parent / 'infra' / 'prometheus' / 'prometheus.yml'
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def prometheus_config(self, prometheus_config_path):
|
||||||
|
"""Загруженная конфигурация Prometheus"""
|
||||||
|
if not prometheus_config_path.exists():
|
||||||
|
pytest.skip(f"Prometheus config file not found: {prometheus_config_path}")
|
||||||
|
|
||||||
|
with open(prometheus_config_path, 'r', encoding='utf-8') as f:
|
||||||
|
return yaml.safe_load(f)
|
||||||
|
|
||||||
|
def test_config_file_exists(self, prometheus_config_path):
|
||||||
|
"""Тест существования файла конфигурации"""
|
||||||
|
assert prometheus_config_path.exists(), f"Prometheus config file not found: {prometheus_config_path}"
|
||||||
|
|
||||||
|
def test_config_is_valid_yaml(self, prometheus_config):
|
||||||
|
"""Тест валидности YAML конфигурации"""
|
||||||
|
assert isinstance(prometheus_config, dict), "Config should be a valid YAML dictionary"
|
||||||
|
|
||||||
|
def test_global_section(self, prometheus_config):
|
||||||
|
"""Тест глобальной секции конфигурации"""
|
||||||
|
assert 'global' in prometheus_config, "Config should have global section"
|
||||||
|
|
||||||
|
global_config = prometheus_config['global']
|
||||||
|
assert 'scrape_interval' in global_config, "Global section should have scrape_interval"
|
||||||
|
assert 'evaluation_interval' in global_config, "Global section should have evaluation_interval"
|
||||||
|
|
||||||
|
# Проверяем значения интервалов
|
||||||
|
assert global_config['scrape_interval'] == '15s', "Default scrape_interval should be 15s"
|
||||||
|
assert global_config['evaluation_interval'] == '15s', "Default evaluation_interval should be 15s"
|
||||||
|
|
||||||
|
def test_scrape_configs_section(self, prometheus_config):
|
||||||
|
"""Тест секции scrape_configs"""
|
||||||
|
assert 'scrape_configs' in prometheus_config, "Config should have scrape_configs section"
|
||||||
|
|
||||||
|
scrape_configs = prometheus_config['scrape_configs']
|
||||||
|
assert isinstance(scrape_configs, list), "scrape_configs should be a list"
|
||||||
|
assert len(scrape_configs) >= 1, "Should have at least one scrape config"
|
||||||
|
|
||||||
|
def test_prometheus_job(self, prometheus_config):
|
||||||
|
"""Тест job для самого Prometheus"""
|
||||||
|
scrape_configs = prometheus_config['scrape_configs']
|
||||||
|
|
||||||
|
# Ищем job для prometheus
|
||||||
|
prometheus_job = None
|
||||||
|
for job in scrape_configs:
|
||||||
|
if job.get('job_name') == 'prometheus':
|
||||||
|
prometheus_job = job
|
||||||
|
break
|
||||||
|
|
||||||
|
assert prometheus_job is not None, "Should have prometheus job"
|
||||||
|
assert 'static_configs' in prometheus_job, "Prometheus job should have static_configs"
|
||||||
|
|
||||||
|
static_configs = prometheus_job['static_configs']
|
||||||
|
assert isinstance(static_configs, list), "static_configs should be a list"
|
||||||
|
assert len(static_configs) > 0, "Should have at least one static config"
|
||||||
|
|
||||||
|
# Проверяем targets
|
||||||
|
targets = static_configs[0].get('targets', [])
|
||||||
|
assert 'localhost:9090' in targets, "Prometheus should scrape localhost:9090"
|
||||||
|
|
||||||
|
def test_infrastructure_job(self, prometheus_config):
|
||||||
|
"""Тест job для инфраструктуры"""
|
||||||
|
scrape_configs = prometheus_config['scrape_configs']
|
||||||
|
|
||||||
|
# Ищем job для infrastructure
|
||||||
|
infra_job = None
|
||||||
|
for job in scrape_configs:
|
||||||
|
if job.get('job_name') == 'infrastructure':
|
||||||
|
infra_job = job
|
||||||
|
break
|
||||||
|
|
||||||
|
assert infra_job is not None, "Should have infrastructure job"
|
||||||
|
|
||||||
|
# Проверяем основные параметры
|
||||||
|
assert 'static_configs' in infra_job, "Infrastructure job should have static_configs"
|
||||||
|
assert 'metrics_path' in infra_job, "Infrastructure job should have metrics_path"
|
||||||
|
assert 'scrape_interval' in infra_job, "Infrastructure job should have scrape_interval"
|
||||||
|
assert 'scrape_timeout' in infra_job, "Infrastructure job should have scrape_timeout"
|
||||||
|
assert 'honor_labels' in infra_job, "Infrastructure job should have honor_labels"
|
||||||
|
|
||||||
|
# Проверяем значения
|
||||||
|
assert infra_job['metrics_path'] == '/metrics', "Metrics path should be /metrics"
|
||||||
|
assert infra_job['scrape_interval'] == '30s', "Scrape interval should be 30s"
|
||||||
|
assert infra_job['scrape_timeout'] == '10s', "Scrape timeout should be 10s"
|
||||||
|
assert infra_job['honor_labels'] is True, "honor_labels should be True"
|
||||||
|
|
||||||
|
# Проверяем targets
|
||||||
|
static_configs = infra_job['static_configs']
|
||||||
|
assert len(static_configs) > 0, "Should have at least one static config"
|
||||||
|
|
||||||
|
targets = static_configs[0].get('targets', [])
|
||||||
|
assert 'host.docker.internal:9091' in targets, "Should scrape host.docker.internal:9091"
|
||||||
|
|
||||||
|
def test_telegram_bot_job(self, prometheus_config):
|
||||||
|
"""Тест job для telegram-helper-bot"""
|
||||||
|
scrape_configs = prometheus_config['scrape_configs']
|
||||||
|
|
||||||
|
# Ищем job для telegram-helper-bot
|
||||||
|
bot_job = None
|
||||||
|
for job in scrape_configs:
|
||||||
|
if job.get('job_name') == 'telegram-helper-bot':
|
||||||
|
bot_job = job
|
||||||
|
break
|
||||||
|
|
||||||
|
assert bot_job is not None, "Should have telegram-helper-bot job"
|
||||||
|
|
||||||
|
# Проверяем основные параметры
|
||||||
|
assert 'static_configs' in bot_job, "Bot job should have static_configs"
|
||||||
|
assert 'metrics_path' in bot_job, "Bot job should have metrics_path"
|
||||||
|
assert 'scrape_interval' in bot_job, "Bot job should have scrape_interval"
|
||||||
|
assert 'scrape_timeout' in bot_job, "Bot job should have scrape_timeout"
|
||||||
|
assert 'honor_labels' in bot_job, "Bot job should have honor_labels"
|
||||||
|
|
||||||
|
# Проверяем значения
|
||||||
|
assert bot_job['metrics_path'] == '/metrics', "Metrics path should be /metrics"
|
||||||
|
assert bot_job['scrape_interval'] == '15s', "Scrape interval should be 15s"
|
||||||
|
assert bot_job['scrape_timeout'] == '10s', "Scrape timeout should be 10s"
|
||||||
|
assert bot_job['honor_labels'] is True, "honor_labels should be True"
|
||||||
|
|
||||||
|
# Проверяем static_configs
|
||||||
|
static_configs = bot_job['static_configs']
|
||||||
|
assert len(static_configs) > 0, "Should have at least one static config"
|
||||||
|
|
||||||
|
# Проверяем targets
|
||||||
|
targets = static_configs[0].get('targets', [])
|
||||||
|
assert 'bots_telegram_bot:8080' in targets, "Should scrape bots_telegram_bot:8080"
|
||||||
|
|
||||||
|
# Проверяем labels
|
||||||
|
labels = static_configs[0].get('labels', {})
|
||||||
|
expected_labels = {
|
||||||
|
'bot_name': 'telegram-helper-bot',
|
||||||
|
'environment': 'production',
|
||||||
|
'service': 'telegram-bot'
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value in expected_labels.items():
|
||||||
|
assert key in labels, f"Should have label {key}"
|
||||||
|
assert labels[key] == value, f"Label {key} should be {value}"
|
||||||
|
|
||||||
|
def test_alerting_section(self, prometheus_config):
|
||||||
|
"""Тест секции alerting"""
|
||||||
|
assert 'alerting' in prometheus_config, "Config should have alerting section"
|
||||||
|
|
||||||
|
alerting_config = prometheus_config['alerting']
|
||||||
|
assert 'alertmanagers' in alerting_config, "Alerting section should have alertmanagers"
|
||||||
|
|
||||||
|
alertmanagers = alerting_config['alertmanagers']
|
||||||
|
assert isinstance(alertmanagers, list), "alertmanagers should be a list"
|
||||||
|
|
||||||
|
# Проверяем, что alertmanager закомментирован (не активен)
|
||||||
|
# Это нормально для тестовой среды
|
||||||
|
if len(alertmanagers) > 0:
|
||||||
|
for am in alertmanagers:
|
||||||
|
if 'static_configs' in am:
|
||||||
|
static_configs = am['static_configs']
|
||||||
|
for sc in static_configs:
|
||||||
|
if 'targets' in sc:
|
||||||
|
targets = sc['targets']
|
||||||
|
# targets может быть None если все строки закомментированы
|
||||||
|
if targets is not None:
|
||||||
|
# Проверяем, что все targets закомментированы
|
||||||
|
for target in targets:
|
||||||
|
assert target.startswith('#'), f"Alertmanager target should be commented: {target}"
|
||||||
|
|
||||||
|
def test_rule_files_section(self, prometheus_config):
|
||||||
|
"""Тест секции rule_files"""
|
||||||
|
assert 'rule_files' in prometheus_config, "Config should have rule_files section"
|
||||||
|
|
||||||
|
rule_files = prometheus_config['rule_files']
|
||||||
|
# rule_files может быть None если все строки закомментированы
|
||||||
|
if rule_files is not None:
|
||||||
|
assert isinstance(rule_files, list), "rule_files should be a list"
|
||||||
|
|
||||||
|
# Проверяем, что все rule files закомментированы
|
||||||
|
for rule_file in rule_files:
|
||||||
|
assert rule_file.startswith('#'), f"Rule file should be commented: {rule_file}"
|
||||||
|
|
||||||
|
def test_config_structure_consistency(self, prometheus_config):
|
||||||
|
"""Тест консистентности структуры конфигурации"""
|
||||||
|
# Проверяем, что все job'ы имеют одинаковую структуру
|
||||||
|
scrape_configs = prometheus_config['scrape_configs']
|
||||||
|
|
||||||
|
required_fields = ['job_name', 'static_configs']
|
||||||
|
optional_fields = ['metrics_path', 'scrape_interval', 'scrape_timeout', 'honor_labels']
|
||||||
|
|
||||||
|
for job in scrape_configs:
|
||||||
|
# Проверяем обязательные поля
|
||||||
|
for field in required_fields:
|
||||||
|
assert field in job, f"Job {job.get('job_name', 'unknown')} missing required field: {field}"
|
||||||
|
|
||||||
|
# Проверяем, что static_configs содержит targets
|
||||||
|
static_configs = job['static_configs']
|
||||||
|
assert isinstance(static_configs, list), f"Job {job.get('job_name', 'unknown')} static_configs should be list"
|
||||||
|
|
||||||
|
for static_config in static_configs:
|
||||||
|
assert 'targets' in static_config, f"Static config should have targets"
|
||||||
|
targets = static_config['targets']
|
||||||
|
assert isinstance(targets, list), "Targets should be a list"
|
||||||
|
assert len(targets) > 0, "Targets should not be empty"
|
||||||
|
|
||||||
|
def test_port_configurations(self, prometheus_config):
|
||||||
|
"""Тест конфигурации портов"""
|
||||||
|
scrape_configs = prometheus_config['scrape_configs']
|
||||||
|
|
||||||
|
# Проверяем, что порты корректно настроены
|
||||||
|
for job in scrape_configs:
|
||||||
|
static_configs = job['static_configs']
|
||||||
|
for static_config in static_configs:
|
||||||
|
targets = static_config['targets']
|
||||||
|
for target in targets:
|
||||||
|
if ':' in target:
|
||||||
|
host, port = target.split(':', 1)
|
||||||
|
# Проверяем, что порт это число
|
||||||
|
try:
|
||||||
|
port_num = int(port)
|
||||||
|
assert 1 <= port_num <= 65535, f"Port {port_num} out of range"
|
||||||
|
except ValueError:
|
||||||
|
# Это может быть Docker service name без порта
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_environment_labels(self, prometheus_config):
|
||||||
|
"""Тест labels окружения"""
|
||||||
|
scrape_configs = prometheus_config['scrape_configs']
|
||||||
|
|
||||||
|
# Проверяем, что production окружение правильно помечено
|
||||||
|
for job in scrape_configs:
|
||||||
|
if job.get('job_name') == 'telegram-helper-bot':
|
||||||
|
static_configs = job['static_configs']
|
||||||
|
for static_config in static_configs:
|
||||||
|
labels = static_config.get('labels', {})
|
||||||
|
if 'environment' in labels:
|
||||||
|
assert labels['environment'] == 'production', "Environment should be production"
|
||||||
|
|
||||||
|
def test_metrics_path_consistency(self, prometheus_config):
|
||||||
|
"""Тест консистентности paths для метрик"""
|
||||||
|
scrape_configs = prometheus_config['scrape_configs']
|
||||||
|
|
||||||
|
# Проверяем, что все job'ы используют /metrics
|
||||||
|
for job in scrape_configs:
|
||||||
|
if 'metrics_path' in job:
|
||||||
|
assert job['metrics_path'] == '/metrics', f"Job {job.get('job_name', 'unknown')} should use /metrics path"
|
||||||
|
|
||||||
|
|
||||||
|
class TestPrometheusConfigValidation:
|
||||||
|
"""Тесты валидации конфигурации Prometheus"""
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_valid_config(self):
|
||||||
|
"""Пример валидной конфигурации"""
|
||||||
|
return {
|
||||||
|
'global': {
|
||||||
|
'scrape_interval': '15s',
|
||||||
|
'evaluation_interval': '15s'
|
||||||
|
},
|
||||||
|
'scrape_configs': [
|
||||||
|
{
|
||||||
|
'job_name': 'test',
|
||||||
|
'static_configs': [
|
||||||
|
{
|
||||||
|
'targets': ['localhost:9090']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_minimal_valid_config(self, sample_valid_config):
|
||||||
|
"""Тест минимальной валидной конфигурации"""
|
||||||
|
# Проверяем, что конфигурация содержит все необходимые поля
|
||||||
|
assert 'global' in sample_valid_config
|
||||||
|
assert 'scrape_configs' in sample_valid_config
|
||||||
|
|
||||||
|
global_config = sample_valid_config['global']
|
||||||
|
assert 'scrape_interval' in global_config
|
||||||
|
assert 'evaluation_interval' in global_config
|
||||||
|
|
||||||
|
scrape_configs = sample_valid_config['scrape_configs']
|
||||||
|
assert len(scrape_configs) > 0
|
||||||
|
|
||||||
|
for job in scrape_configs:
|
||||||
|
assert 'job_name' in job
|
||||||
|
assert 'static_configs' in job
|
||||||
|
|
||||||
|
static_configs = job['static_configs']
|
||||||
|
assert len(static_configs) > 0
|
||||||
|
|
||||||
|
for static_config in static_configs:
|
||||||
|
assert 'targets' in static_config
|
||||||
|
targets = static_config['targets']
|
||||||
|
assert len(targets) > 0
|
||||||
|
|
||||||
|
def test_config_without_required_fields(self):
|
||||||
|
"""Тест конфигурации без обязательных полей"""
|
||||||
|
# Конфигурация без global секции
|
||||||
|
config_without_global = {
|
||||||
|
'scrape_configs': []
|
||||||
|
}
|
||||||
|
|
||||||
|
# Конфигурация без scrape_configs
|
||||||
|
config_without_scrape = {
|
||||||
|
'global': {
|
||||||
|
'scrape_interval': '15s'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Конфигурация с пустыми scrape_configs
|
||||||
|
config_empty_scrape = {
|
||||||
|
'global': {
|
||||||
|
'scrape_interval': '15s'
|
||||||
|
},
|
||||||
|
'scrape_configs': []
|
||||||
|
}
|
||||||
|
|
||||||
|
# Все эти конфигурации должны быть невалидными
|
||||||
|
assert 'global' not in config_without_global
|
||||||
|
assert 'scrape_configs' not in config_without_scrape
|
||||||
|
assert len(config_empty_scrape['scrape_configs']) == 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pytest.main([__file__, "-v"])
|
||||||
429
tests/infra/test_prometheus_integration.py
Normal file
429
tests/infra/test_prometheus_integration.py
Normal file
@@ -0,0 +1,429 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Интеграционные тесты для Prometheus и связанных компонентов
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import pytest_asyncio
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import yaml
|
||||||
|
from unittest.mock import Mock, AsyncMock, patch, MagicMock
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Добавляем путь к модулям мониторинга
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../infra/monitoring'))
|
||||||
|
|
||||||
|
from prometheus_server import PrometheusServer
|
||||||
|
from metrics_collector import MetricsCollector
|
||||||
|
|
||||||
|
|
||||||
|
class TestPrometheusIntegration:
|
||||||
|
"""Интеграционные тесты для Prometheus"""
|
||||||
|
|
||||||
|
@pytest_asyncio.fixture
|
||||||
|
async def prometheus_server(self):
|
||||||
|
"""Создает экземпляр PrometheusServer для интеграционных тестов"""
|
||||||
|
server = PrometheusServer(host='127.0.0.1', port=0)
|
||||||
|
return server
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def metrics_collector(self):
|
||||||
|
"""Создает экземпляр MetricsCollector для интеграционных тестов"""
|
||||||
|
return MetricsCollector()
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_prometheus_config(self):
|
||||||
|
"""Создает пример конфигурации Prometheus для тестов"""
|
||||||
|
return {
|
||||||
|
'global': {
|
||||||
|
'scrape_interval': '15s',
|
||||||
|
'evaluation_interval': '15s'
|
||||||
|
},
|
||||||
|
'scrape_configs': [
|
||||||
|
{
|
||||||
|
'job_name': 'test-infrastructure',
|
||||||
|
'static_configs': [
|
||||||
|
{
|
||||||
|
'targets': ['127.0.0.1:9091'],
|
||||||
|
'labels': {
|
||||||
|
'environment': 'test',
|
||||||
|
'service': 'test-monitoring'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
'metrics_path': '/metrics',
|
||||||
|
'scrape_interval': '30s',
|
||||||
|
'scrape_timeout': '10s',
|
||||||
|
'honor_labels': True
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_prometheus_server_with_real_metrics_collector(self, prometheus_server):
|
||||||
|
"""Тест интеграции PrometheusServer с реальным MetricsCollector"""
|
||||||
|
# Получаем реальные метрики
|
||||||
|
metrics_data = prometheus_server.metrics_collector.get_metrics_data()
|
||||||
|
|
||||||
|
# Проверяем, что можем получить метрики
|
||||||
|
assert isinstance(metrics_data, dict)
|
||||||
|
|
||||||
|
# Форматируем метрики в Prometheus формат
|
||||||
|
prometheus_metrics = prometheus_server._format_prometheus_metrics(metrics_data)
|
||||||
|
|
||||||
|
# Проверяем, что метрики содержат системную информацию
|
||||||
|
assert '# HELP system_info System information' in prometheus_metrics
|
||||||
|
assert '# TYPE system_info gauge' in prometheus_metrics
|
||||||
|
|
||||||
|
# Проверяем, что есть хотя бы одна метрика
|
||||||
|
lines = prometheus_metrics.split('\n')
|
||||||
|
assert len(lines) >= 3 # system_info help, type, value
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
def test_metrics_collector_system_integration(self, metrics_collector):
|
||||||
|
"""Тест интеграции MetricsCollector с системой"""
|
||||||
|
# Получаем системную информацию
|
||||||
|
system_info = metrics_collector.get_system_info()
|
||||||
|
|
||||||
|
# Проверяем, что получили словарь
|
||||||
|
assert isinstance(system_info, dict)
|
||||||
|
|
||||||
|
# Проверяем, что можем получить метрики для Prometheus
|
||||||
|
metrics_data = metrics_collector.get_metrics_data()
|
||||||
|
assert isinstance(metrics_data, dict)
|
||||||
|
|
||||||
|
# Проверяем, что можем проверить алерты
|
||||||
|
alerts, recoveries = metrics_collector.check_alerts(system_info)
|
||||||
|
assert isinstance(alerts, list)
|
||||||
|
assert isinstance(recoveries, list)
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
def test_prometheus_metrics_format_integration(self, prometheus_server, metrics_collector):
|
||||||
|
"""Тест интеграции форматирования метрик Prometheus"""
|
||||||
|
# Получаем реальные метрики
|
||||||
|
metrics_data = metrics_collector.get_metrics_data()
|
||||||
|
|
||||||
|
# Форматируем в Prometheus формат
|
||||||
|
prometheus_metrics = prometheus_server._format_prometheus_metrics(metrics_data)
|
||||||
|
|
||||||
|
# Проверяем структуру метрик
|
||||||
|
lines = prometheus_metrics.split('\n')
|
||||||
|
|
||||||
|
# Должна быть системная информация
|
||||||
|
system_info_lines = [line for line in lines if 'system_info' in line]
|
||||||
|
assert len(system_info_lines) >= 3 # help, type, value
|
||||||
|
|
||||||
|
# Проверяем, что метрики содержат правильные типы
|
||||||
|
type_lines = [line for line in lines if '# TYPE' in line]
|
||||||
|
assert len(type_lines) > 0
|
||||||
|
|
||||||
|
# Проверяем, что все метрики имеют правильный формат
|
||||||
|
metric_lines = [line for line in lines if line and not line.startswith('#')]
|
||||||
|
for line in metric_lines:
|
||||||
|
# Проверяем, что строка метрики содержит имя и значение
|
||||||
|
assert ' ' in line
|
||||||
|
parts = line.split(' ')
|
||||||
|
assert len(parts) >= 2
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
def test_os_detection_integration(self):
|
||||||
|
"""Тест интеграции определения ОС"""
|
||||||
|
# Создаем коллектор с реальным определением ОС
|
||||||
|
collector = MetricsCollector()
|
||||||
|
|
||||||
|
# Проверяем, что ОС определена
|
||||||
|
assert collector.os_type in ["macos", "ubuntu", "unknown"]
|
||||||
|
|
||||||
|
# Проверяем, что можем получить информацию о диске
|
||||||
|
disk_info = collector._get_disk_usage()
|
||||||
|
if disk_info is not None:
|
||||||
|
assert hasattr(disk_info, 'total')
|
||||||
|
assert hasattr(disk_info, 'used')
|
||||||
|
assert hasattr(disk_info, 'free')
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
def test_disk_io_calculation_integration(self, metrics_collector):
|
||||||
|
"""Тест интеграции расчета I/O диска"""
|
||||||
|
# Инициализируем базовые значения
|
||||||
|
metrics_collector._initialize_disk_io()
|
||||||
|
|
||||||
|
# Получаем текущую статистику диска
|
||||||
|
current_disk_io = metrics_collector._get_disk_io_counters()
|
||||||
|
|
||||||
|
if current_disk_io is not None:
|
||||||
|
# Рассчитываем скорость
|
||||||
|
read_speed, write_speed = metrics_collector._calculate_disk_speed(current_disk_io)
|
||||||
|
|
||||||
|
# Проверяем, что получили строки с единицами измерения
|
||||||
|
assert isinstance(read_speed, str)
|
||||||
|
assert isinstance(write_speed, str)
|
||||||
|
assert "/s" in read_speed
|
||||||
|
assert "/s" in write_speed
|
||||||
|
|
||||||
|
# Рассчитываем процент загрузки
|
||||||
|
io_percent = metrics_collector._calculate_disk_io_percent()
|
||||||
|
assert isinstance(io_percent, int)
|
||||||
|
assert 0 <= io_percent <= 100
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
def test_process_monitoring_integration(self, metrics_collector):
|
||||||
|
"""Тест интеграции мониторинга процессов"""
|
||||||
|
# Проверяем статус процессов
|
||||||
|
for process_name in ['voice_bot', 'helper_bot']:
|
||||||
|
status, message = metrics_collector.check_process_status(process_name)
|
||||||
|
|
||||||
|
# Статус должен быть либо ✅, либо ❌
|
||||||
|
assert status in ["✅", "❌"]
|
||||||
|
|
||||||
|
# Сообщение должно быть строкой
|
||||||
|
assert isinstance(message, str)
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
def test_alert_system_integration(self, metrics_collector):
|
||||||
|
"""Тест интеграции системы алертов"""
|
||||||
|
# Создаем тестовые данные
|
||||||
|
test_system_info = {
|
||||||
|
'cpu_percent': 85.0, # Выше порога
|
||||||
|
'ram_percent': 60.0, # Ниже порога
|
||||||
|
'disk_percent': 70.0, # Ниже порога
|
||||||
|
'load_avg_1m': 2.5,
|
||||||
|
'ram_used': 8.0,
|
||||||
|
'ram_total': 16.0,
|
||||||
|
'disk_free': 300.0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Проверяем алерты
|
||||||
|
alerts, recoveries = metrics_collector.check_alerts(test_system_info)
|
||||||
|
|
||||||
|
# Должен быть хотя бы один алерт для CPU
|
||||||
|
assert len(alerts) >= 1
|
||||||
|
assert any(alert[0] == 'cpu' for alert in alerts)
|
||||||
|
|
||||||
|
# Проверяем, что состояние алерта изменилось
|
||||||
|
assert metrics_collector.alert_states['cpu'] is True
|
||||||
|
|
||||||
|
# Тестируем восстановление
|
||||||
|
test_system_info['cpu_percent'] = 70.0 # Ниже recovery threshold
|
||||||
|
|
||||||
|
alerts, recoveries = metrics_collector.check_alerts(test_system_info)
|
||||||
|
|
||||||
|
# Должно быть восстановление
|
||||||
|
assert len(recoveries) >= 1
|
||||||
|
assert any(recovery[0] == 'cpu' for recovery in recoveries)
|
||||||
|
assert metrics_collector.alert_states['cpu'] is False
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
def test_uptime_calculation_integration(self, metrics_collector):
|
||||||
|
"""Тест интеграции расчета uptime"""
|
||||||
|
# Получаем uptime системы
|
||||||
|
system_uptime = metrics_collector._get_system_uptime()
|
||||||
|
assert system_uptime > 0
|
||||||
|
|
||||||
|
# Получаем uptime мониторинга
|
||||||
|
monitor_uptime = metrics_collector.get_monitor_uptime()
|
||||||
|
assert isinstance(monitor_uptime, str)
|
||||||
|
assert len(monitor_uptime) > 0
|
||||||
|
|
||||||
|
# Форматируем uptime
|
||||||
|
formatted_uptime = metrics_collector._format_uptime(system_uptime)
|
||||||
|
assert isinstance(formatted_uptime, str)
|
||||||
|
assert len(formatted_uptime) > 0
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
def test_environment_variables_integration(self):
|
||||||
|
"""Тест интеграции с переменными окружения"""
|
||||||
|
# Тестируем с пользовательскими значениями
|
||||||
|
test_threshold = '90.0'
|
||||||
|
test_recovery_threshold = '85.0'
|
||||||
|
|
||||||
|
with patch.dict(os.environ, {
|
||||||
|
'THRESHOLD': test_threshold,
|
||||||
|
'RECOVERY_THRESHOLD': test_recovery_threshold
|
||||||
|
}):
|
||||||
|
collector = MetricsCollector()
|
||||||
|
|
||||||
|
# Проверяем, что значения установлены
|
||||||
|
assert collector.threshold == float(test_threshold)
|
||||||
|
assert collector.recovery_threshold == float(test_recovery_threshold)
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
def test_prometheus_config_validation_integration(self, sample_prometheus_config):
|
||||||
|
"""Тест интеграции валидации конфигурации Prometheus"""
|
||||||
|
# Проверяем структуру конфигурации
|
||||||
|
assert 'global' in sample_prometheus_config
|
||||||
|
assert 'scrape_configs' in sample_prometheus_config
|
||||||
|
|
||||||
|
global_config = sample_prometheus_config['global']
|
||||||
|
assert 'scrape_interval' in global_config
|
||||||
|
assert 'evaluation_interval' in global_config
|
||||||
|
|
||||||
|
scrape_configs = sample_prometheus_config['scrape_configs']
|
||||||
|
assert len(scrape_configs) > 0
|
||||||
|
|
||||||
|
# Проверяем каждый job
|
||||||
|
for job in scrape_configs:
|
||||||
|
assert 'job_name' in job
|
||||||
|
assert 'static_configs' in job
|
||||||
|
|
||||||
|
static_configs = job['static_configs']
|
||||||
|
assert len(static_configs) > 0
|
||||||
|
|
||||||
|
for static_config in static_configs:
|
||||||
|
assert 'targets' in static_config
|
||||||
|
targets = static_config['targets']
|
||||||
|
assert len(targets) > 0
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
def test_metrics_data_consistency_integration(self, prometheus_server, metrics_collector):
|
||||||
|
"""Тест интеграции консистентности данных метрик"""
|
||||||
|
# Получаем метрики разными способами
|
||||||
|
system_info = metrics_collector.get_system_info()
|
||||||
|
metrics_data = metrics_collector.get_metrics_data()
|
||||||
|
|
||||||
|
# Проверяем консистентность между system_info и metrics_data
|
||||||
|
# Реальные метрики могут значительно отличаться из-за времени между вызовами
|
||||||
|
# и системной нагрузки, поэтому используем более широкие допуски
|
||||||
|
|
||||||
|
if 'cpu_percent' in system_info and 'cpu_usage_percent' in metrics_data:
|
||||||
|
# CPU метрики могут сильно колебаться, используем допуск 25%
|
||||||
|
cpu_diff = abs(system_info['cpu_percent'] - metrics_data['cpu_usage_percent'])
|
||||||
|
assert cpu_diff < 25.0, f"CPU metrics difference too large: {cpu_diff}% (system: {system_info['cpu_percent']}%, metrics: {metrics_data['cpu_usage_percent']}%)"
|
||||||
|
|
||||||
|
if 'ram_percent' in system_info and 'ram_usage_percent' in metrics_data:
|
||||||
|
# RAM метрики более стабильны, но все же используем допуск 10%
|
||||||
|
ram_diff = abs(system_info['ram_percent'] - metrics_data['ram_usage_percent'])
|
||||||
|
assert ram_diff < 10.0, f"RAM metrics difference too large: {ram_diff}% (system: {system_info['ram_percent']}%, metrics: {metrics_data['ram_usage_percent']}%)"
|
||||||
|
|
||||||
|
if 'disk_percent' in system_info and 'disk_usage_percent' in metrics_data:
|
||||||
|
# Disk метрики должны быть очень стабильными, допуск 5%
|
||||||
|
disk_diff = abs(system_info['disk_percent'] - metrics_data['disk_usage_percent'])
|
||||||
|
assert disk_diff < 5.0, f"Disk metrics difference too large: {disk_diff}% (system: {system_info['disk_percent']}%, metrics: {metrics_data['disk_usage_percent']}%)"
|
||||||
|
|
||||||
|
# Проверяем, что все метрики имеют разумные значения
|
||||||
|
for metric_name, value in system_info.items():
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
assert value >= 0, f"Metric {metric_name} should be non-negative: {value}"
|
||||||
|
|
||||||
|
for metric_name, value in metrics_data.items():
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
assert value >= 0, f"Metric {metric_name} should be non-negative: {value}"
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
def test_error_handling_integration(self, prometheus_server, metrics_collector):
|
||||||
|
"""Тест интеграции обработки ошибок"""
|
||||||
|
# Тестируем обработку ошибок в PrometheusServer
|
||||||
|
with patch.object(metrics_collector, 'get_metrics_data', side_effect=Exception("Test error")):
|
||||||
|
prometheus_server.metrics_collector = metrics_collector
|
||||||
|
|
||||||
|
# Создаем мок запрос
|
||||||
|
request = Mock()
|
||||||
|
|
||||||
|
# Обрабатываем запрос метрик
|
||||||
|
response = asyncio.run(prometheus_server.metrics_handler(request))
|
||||||
|
|
||||||
|
# Должен вернуться ответ с ошибкой
|
||||||
|
assert response.status == 500
|
||||||
|
assert 'Error: Test error' in response.text
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
def test_performance_integration(self, prometheus_server, metrics_collector):
|
||||||
|
"""Тест интеграции производительности"""
|
||||||
|
import time
|
||||||
|
|
||||||
|
# Измеряем время получения системной информации
|
||||||
|
start_time = time.time()
|
||||||
|
system_info = metrics_collector.get_system_info()
|
||||||
|
system_info_time = time.time() - start_time
|
||||||
|
|
||||||
|
# Измеряем время получения метрик
|
||||||
|
start_time = time.time()
|
||||||
|
metrics_data = metrics_collector.get_metrics_data()
|
||||||
|
metrics_time = time.time() - start_time
|
||||||
|
|
||||||
|
# Измеряем время форматирования Prometheus метрик
|
||||||
|
start_time = time.time()
|
||||||
|
prometheus_metrics = prometheus_server._format_prometheus_metrics(metrics_data)
|
||||||
|
formatting_time = time.time() - start_time
|
||||||
|
|
||||||
|
# Проверяем, что операции выполняются в разумное время
|
||||||
|
assert system_info_time < 5.0, f"System info collection took too long: {system_info_time}s"
|
||||||
|
assert metrics_time < 2.0, f"Metrics collection took too long: {metrics_time}s"
|
||||||
|
assert formatting_time < 0.1, f"Metrics formatting took too long: {formatting_time}s"
|
||||||
|
|
||||||
|
# Проверяем, что получили данные
|
||||||
|
assert isinstance(system_info, dict)
|
||||||
|
assert isinstance(metrics_data, dict)
|
||||||
|
assert isinstance(prometheus_metrics, str)
|
||||||
|
assert len(prometheus_metrics) > 0
|
||||||
|
|
||||||
|
|
||||||
|
class TestPrometheusEndToEnd:
|
||||||
|
"""End-to-end тесты для Prometheus"""
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
@pytest.mark.slow
|
||||||
|
def test_full_metrics_pipeline(self):
|
||||||
|
"""Тест полного пайплайна метрик"""
|
||||||
|
# Создаем все компоненты
|
||||||
|
metrics_collector = MetricsCollector()
|
||||||
|
prometheus_server = PrometheusServer()
|
||||||
|
|
||||||
|
# 1. Собираем системную информацию
|
||||||
|
system_info = metrics_collector.get_system_info()
|
||||||
|
assert isinstance(system_info, dict)
|
||||||
|
|
||||||
|
# 2. Получаем метрики для Prometheus
|
||||||
|
metrics_data = metrics_collector.get_metrics_data()
|
||||||
|
assert isinstance(metrics_data, dict)
|
||||||
|
|
||||||
|
# 3. Форматируем метрики в Prometheus формат
|
||||||
|
prometheus_metrics = prometheus_server._format_prometheus_metrics(metrics_data)
|
||||||
|
assert isinstance(prometheus_metrics, str)
|
||||||
|
|
||||||
|
# 4. Проверяем, что метрики содержат необходимую информацию
|
||||||
|
lines = prometheus_metrics.split('\n')
|
||||||
|
|
||||||
|
# Должна быть системная информация
|
||||||
|
assert any('system_info' in line for line in lines)
|
||||||
|
|
||||||
|
# Должны быть метрики системы
|
||||||
|
assert any('cpu_usage_percent' in line for line in lines) or any('ram_usage_percent' in line for line in lines)
|
||||||
|
|
||||||
|
# 5. Проверяем алерты
|
||||||
|
alerts, recoveries = metrics_collector.check_alerts(system_info)
|
||||||
|
assert isinstance(alerts, list)
|
||||||
|
assert isinstance(recoveries, list)
|
||||||
|
|
||||||
|
@pytest.mark.integration
|
||||||
|
@pytest.mark.slow
|
||||||
|
def test_metrics_stability(self):
|
||||||
|
"""Тест стабильности метрик"""
|
||||||
|
import time
|
||||||
|
metrics_collector = MetricsCollector()
|
||||||
|
|
||||||
|
# Получаем метрики несколько раз подряд
|
||||||
|
metrics_list = []
|
||||||
|
for _ in range(3):
|
||||||
|
metrics = metrics_collector.get_metrics_data()
|
||||||
|
metrics_list.append(metrics)
|
||||||
|
time.sleep(0.1) # Небольшая пауза
|
||||||
|
|
||||||
|
# Проверяем, что структура метрик не изменилась
|
||||||
|
for metrics in metrics_list:
|
||||||
|
assert isinstance(metrics, dict)
|
||||||
|
assert len(metrics) > 0
|
||||||
|
|
||||||
|
# Проверяем, что ключи метрик не изменились
|
||||||
|
first_keys = set(metrics_list[0].keys())
|
||||||
|
for metrics in metrics_list[1:]:
|
||||||
|
current_keys = set(metrics.keys())
|
||||||
|
# Некоторые метрики могут отсутствовать, но структура должна быть похожей
|
||||||
|
assert len(current_keys.intersection(first_keys)) > 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pytest.main([__file__, "-v", "-m", "integration"])
|
||||||
309
tests/infra/test_prometheus_server.py
Normal file
309
tests/infra/test_prometheus_server.py
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Тесты для PrometheusServer
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from unittest.mock import Mock, AsyncMock, patch, MagicMock
|
||||||
|
from aiohttp import web
|
||||||
|
from aiohttp.test_utils import TestClient
|
||||||
|
|
||||||
|
# Добавляем путь к модулям мониторинга
|
||||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../infra/monitoring'))
|
||||||
|
|
||||||
|
from prometheus_server import PrometheusServer
|
||||||
|
|
||||||
|
|
||||||
|
class TestPrometheusServer:
|
||||||
|
"""Тесты для класса PrometheusServer"""
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def prometheus_server(self):
|
||||||
|
"""Создает экземпляр PrometheusServer для тестов"""
|
||||||
|
return PrometheusServer(host='127.0.0.1', port=9091)
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_metrics_collector(self):
|
||||||
|
"""Создает мок MetricsCollector"""
|
||||||
|
mock_collector = Mock()
|
||||||
|
mock_collector.os_type = "ubuntu"
|
||||||
|
mock_collector.get_metrics_data.return_value = {
|
||||||
|
'cpu_usage_percent': 25.5,
|
||||||
|
'ram_usage_percent': 60.2,
|
||||||
|
'disk_usage_percent': 45.8,
|
||||||
|
'load_average_1m': 1.2,
|
||||||
|
'load_average_5m': 1.1,
|
||||||
|
'load_average_15m': 1.0,
|
||||||
|
'swap_usage_percent': 10.5,
|
||||||
|
'disk_io_percent': 15.3,
|
||||||
|
'system_uptime_seconds': 86400.0,
|
||||||
|
'monitor_uptime_seconds': 3600.0
|
||||||
|
}
|
||||||
|
return mock_collector
|
||||||
|
|
||||||
|
def test_init(self, prometheus_server):
|
||||||
|
"""Тест инициализации PrometheusServer"""
|
||||||
|
assert prometheus_server.host == '127.0.0.1'
|
||||||
|
assert prometheus_server.port == 9091
|
||||||
|
assert prometheus_server.metrics_collector is not None
|
||||||
|
assert isinstance(prometheus_server.app, web.Application)
|
||||||
|
|
||||||
|
def test_setup_routes(self, prometheus_server):
|
||||||
|
"""Тест настройки маршрутов"""
|
||||||
|
routes = list(prometheus_server.app.router.routes())
|
||||||
|
# aiohttp создает по 2 маршрута для каждого эндпоинта (GET и HEAD)
|
||||||
|
assert len(routes) == 6
|
||||||
|
|
||||||
|
# Проверяем наличие всех маршрутов
|
||||||
|
route_paths = [route.resource.canonical for route in routes]
|
||||||
|
assert '/' in route_paths
|
||||||
|
assert '/metrics' in route_paths
|
||||||
|
assert '/health' in route_paths
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_root_handler(self, prometheus_server):
|
||||||
|
"""Тест главного обработчика"""
|
||||||
|
request = Mock()
|
||||||
|
response = await prometheus_server.root_handler(request)
|
||||||
|
|
||||||
|
assert isinstance(response, web.Response)
|
||||||
|
assert response.status == 200
|
||||||
|
assert response.content_type == 'text/plain'
|
||||||
|
assert 'Prometheus Metrics Server' in response.text
|
||||||
|
assert '/metrics' in response.text
|
||||||
|
assert '/health' in response.text
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_health_handler(self, prometheus_server):
|
||||||
|
"""Тест health check обработчика"""
|
||||||
|
request = Mock()
|
||||||
|
response = await prometheus_server.health_handler(request)
|
||||||
|
|
||||||
|
assert isinstance(response, web.Response)
|
||||||
|
assert response.status == 200
|
||||||
|
assert response.content_type == 'text/plain'
|
||||||
|
assert response.text == 'OK'
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_metrics_handler_success(self, prometheus_server, mock_metrics_collector):
|
||||||
|
"""Тест обработчика метрик при успешном получении данных"""
|
||||||
|
# Заменяем metrics_collector на мок
|
||||||
|
prometheus_server.metrics_collector = mock_metrics_collector
|
||||||
|
|
||||||
|
request = Mock()
|
||||||
|
response = await prometheus_server.metrics_handler(request)
|
||||||
|
|
||||||
|
assert isinstance(response, web.Response)
|
||||||
|
assert response.status == 200
|
||||||
|
assert response.content_type == 'text/plain'
|
||||||
|
|
||||||
|
# Проверяем, что метрики содержат ожидаемые данные
|
||||||
|
metrics_text = response.text
|
||||||
|
assert '# HELP system_info System information' in metrics_text
|
||||||
|
assert '# TYPE system_info gauge' in metrics_text
|
||||||
|
assert 'system_info{os="ubuntu"}' in metrics_text
|
||||||
|
assert '# HELP cpu_usage_percent CPU usage percentage' in metrics_text
|
||||||
|
assert 'cpu_usage_percent 25.5' in metrics_text
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_metrics_handler_error(self, prometheus_server, mock_metrics_collector):
|
||||||
|
"""Тест обработчика метрик при ошибке"""
|
||||||
|
# Настраиваем мок для вызова исключения
|
||||||
|
mock_metrics_collector.get_metrics_data.side_effect = Exception("Test error")
|
||||||
|
prometheus_server.metrics_collector = mock_metrics_collector
|
||||||
|
|
||||||
|
request = Mock()
|
||||||
|
response = await prometheus_server.metrics_handler(request)
|
||||||
|
|
||||||
|
assert isinstance(response, web.Response)
|
||||||
|
assert response.status == 500
|
||||||
|
assert response.content_type == 'text/plain'
|
||||||
|
assert 'Error: Test error' in response.text
|
||||||
|
|
||||||
|
def test_format_prometheus_metrics(self, prometheus_server, mock_metrics_collector):
|
||||||
|
"""Тест форматирования метрик в Prometheus формат"""
|
||||||
|
prometheus_server.metrics_collector = mock_metrics_collector
|
||||||
|
|
||||||
|
metrics_data = mock_metrics_collector.get_metrics_data()
|
||||||
|
formatted_metrics = prometheus_server._format_prometheus_metrics(metrics_data)
|
||||||
|
|
||||||
|
# Проверяем структуру метрик
|
||||||
|
lines = formatted_metrics.split('\n')
|
||||||
|
|
||||||
|
# Проверяем наличие системной информации
|
||||||
|
assert any('system_info' in line for line in lines)
|
||||||
|
assert any('os="ubuntu"' in line for line in lines)
|
||||||
|
|
||||||
|
# Проверяем наличие CPU метрик
|
||||||
|
assert any('cpu_usage_percent' in line for line in lines)
|
||||||
|
assert any('25.5' in line for line in lines)
|
||||||
|
|
||||||
|
# Проверяем наличие RAM метрик
|
||||||
|
assert any('ram_usage_percent' in line for line in lines)
|
||||||
|
assert any('60.2' in line for line in lines)
|
||||||
|
|
||||||
|
# Проверяем наличие disk метрик
|
||||||
|
assert any('disk_usage_percent' in line for line in lines)
|
||||||
|
assert any('45.8' in line for line in lines)
|
||||||
|
|
||||||
|
# Проверяем наличие load average метрик
|
||||||
|
assert any('load_average_1m' in line for line in lines)
|
||||||
|
assert any('1.2' in line for line in lines)
|
||||||
|
|
||||||
|
def test_format_prometheus_metrics_empty_data(self, prometheus_server):
|
||||||
|
"""Тест форматирования метрик с пустыми данными"""
|
||||||
|
empty_metrics = {}
|
||||||
|
formatted_metrics = prometheus_server._format_prometheus_metrics(empty_metrics)
|
||||||
|
|
||||||
|
# Должна быть только системная информация
|
||||||
|
lines = formatted_metrics.split('\n')
|
||||||
|
assert len(lines) == 3 # system_info help, type, value
|
||||||
|
assert any('system_info' in line for line in lines)
|
||||||
|
|
||||||
|
def test_format_prometheus_metrics_partial_data(self, prometheus_server, mock_metrics_collector):
|
||||||
|
"""Тест форматирования метрик с частичными данными"""
|
||||||
|
prometheus_server.metrics_collector = mock_metrics_collector
|
||||||
|
|
||||||
|
# Только CPU метрики
|
||||||
|
partial_metrics = {
|
||||||
|
'cpu_usage_percent': 50.0,
|
||||||
|
'load_average_1m': 2.5
|
||||||
|
}
|
||||||
|
|
||||||
|
formatted_metrics = prometheus_server._format_prometheus_metrics(partial_metrics)
|
||||||
|
lines = formatted_metrics.split('\n')
|
||||||
|
|
||||||
|
# Проверяем, что есть системная информация + CPU + load average
|
||||||
|
assert any('system_info' in line for line in lines)
|
||||||
|
assert any('cpu_usage_percent' in line for line in lines)
|
||||||
|
assert any('load_average_1m' in line for line in lines)
|
||||||
|
assert any('50.0' in line for line in lines)
|
||||||
|
assert any('2.5' in line for line in lines)
|
||||||
|
|
||||||
|
# Проверяем, что нет RAM метрик
|
||||||
|
assert not any('ram_usage_percent' in line for line in lines)
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_start_and_stop(self, prometheus_server):
|
||||||
|
"""Тест запуска и остановки сервера"""
|
||||||
|
# Мокаем web.AppRunner и TCPSite
|
||||||
|
with patch('prometheus_server.web.AppRunner') as mock_runner_class, \
|
||||||
|
patch('prometheus_server.web.TCPSite') as mock_site_class:
|
||||||
|
|
||||||
|
mock_runner = Mock()
|
||||||
|
mock_runner.setup = AsyncMock()
|
||||||
|
mock_runner.cleanup = AsyncMock()
|
||||||
|
mock_runner_class.return_value = mock_runner
|
||||||
|
|
||||||
|
mock_site = Mock()
|
||||||
|
mock_site.start = AsyncMock()
|
||||||
|
mock_site_class.return_value = mock_site
|
||||||
|
|
||||||
|
# Запускаем сервер
|
||||||
|
runner = await prometheus_server.start()
|
||||||
|
|
||||||
|
# Проверяем, что методы были вызваны
|
||||||
|
mock_runner.setup.assert_called_once()
|
||||||
|
mock_site.start.assert_called_once()
|
||||||
|
assert runner == mock_runner
|
||||||
|
|
||||||
|
# Останавливаем сервер
|
||||||
|
await prometheus_server.stop(runner)
|
||||||
|
mock_runner.cleanup.assert_called_once()
|
||||||
|
|
||||||
|
def test_different_os_types(self):
|
||||||
|
"""Тест работы с разными типами ОС"""
|
||||||
|
# Тестируем macOS
|
||||||
|
with patch('platform.system', return_value='Darwin'):
|
||||||
|
server_macos = PrometheusServer()
|
||||||
|
assert server_macos.metrics_collector.os_type == "macos"
|
||||||
|
|
||||||
|
# Тестируем Linux
|
||||||
|
with patch('platform.system', return_value='Linux'):
|
||||||
|
server_linux = PrometheusServer()
|
||||||
|
assert server_linux.metrics_collector.os_type == "ubuntu"
|
||||||
|
|
||||||
|
# Тестируем неизвестную ОС
|
||||||
|
with patch('platform.system', return_value='Windows'):
|
||||||
|
server_unknown = PrometheusServer()
|
||||||
|
assert server_unknown.metrics_collector.os_type == "unknown"
|
||||||
|
|
||||||
|
def test_custom_host_port(self):
|
||||||
|
"""Тест создания сервера с пользовательскими параметрами"""
|
||||||
|
server = PrometheusServer(host='192.168.1.100', port=9092)
|
||||||
|
assert server.host == '192.168.1.100'
|
||||||
|
assert server.port == 9092
|
||||||
|
|
||||||
|
def test_metrics_collector_integration(self, prometheus_server):
|
||||||
|
"""Тест интеграции с MetricsCollector"""
|
||||||
|
# Проверяем, что metrics_collector имеет необходимые методы
|
||||||
|
collector = prometheus_server.metrics_collector
|
||||||
|
assert hasattr(collector, 'get_metrics_data')
|
||||||
|
assert hasattr(collector, 'os_type')
|
||||||
|
|
||||||
|
# Проверяем, что можем получить данные
|
||||||
|
metrics_data = collector.get_metrics_data()
|
||||||
|
assert isinstance(metrics_data, dict)
|
||||||
|
|
||||||
|
|
||||||
|
class TestPrometheusServerIntegration:
|
||||||
|
"""Интеграционные тесты для PrometheusServer"""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_server_creation_integration(self):
|
||||||
|
"""Интеграционный тест создания сервера"""
|
||||||
|
server = PrometheusServer(host='127.0.0.1', port=0)
|
||||||
|
|
||||||
|
# Проверяем, что сервер создался
|
||||||
|
assert server is not None
|
||||||
|
assert server.host == '127.0.0.1'
|
||||||
|
assert server.port == 0
|
||||||
|
|
||||||
|
# Проверяем, что приложение создалось
|
||||||
|
assert server.app is not None
|
||||||
|
|
||||||
|
# Проверяем, что маршруты настроены
|
||||||
|
routes = list(server.app.router.routes())
|
||||||
|
assert len(routes) > 0
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_metrics_collector_integration(self):
|
||||||
|
"""Интеграционный тест с MetricsCollector"""
|
||||||
|
server = PrometheusServer(host='127.0.0.1', port=0)
|
||||||
|
|
||||||
|
# Проверяем, что можем получить метрики
|
||||||
|
metrics_data = server.metrics_collector.get_metrics_data()
|
||||||
|
assert isinstance(metrics_data, dict)
|
||||||
|
|
||||||
|
# Проверяем, что можем отформатировать метрики
|
||||||
|
prometheus_metrics = server._format_prometheus_metrics(metrics_data)
|
||||||
|
assert isinstance(prometheus_metrics, str)
|
||||||
|
assert len(prometheus_metrics) > 0
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_endpoint_handlers_integration(self):
|
||||||
|
"""Интеграционный тест обработчиков эндпоинтов"""
|
||||||
|
server = PrometheusServer(host='127.0.0.1', port=0)
|
||||||
|
|
||||||
|
# Тестируем корневой обработчик
|
||||||
|
request = Mock()
|
||||||
|
response = await server.root_handler(request)
|
||||||
|
assert response.status == 200
|
||||||
|
assert 'Prometheus Metrics Server' in response.text
|
||||||
|
|
||||||
|
# Тестируем health обработчик
|
||||||
|
response = await server.health_handler(request)
|
||||||
|
assert response.status == 200
|
||||||
|
assert response.text == 'OK'
|
||||||
|
|
||||||
|
# Тестируем metrics обработчик
|
||||||
|
response = await server.metrics_handler(request)
|
||||||
|
assert response.status == 200
|
||||||
|
assert '# HELP system_info' in response.text
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pytest.main([__file__, "-v"])
|
||||||
48
tests/test_pytest_config.py
Normal file
48
tests/test_pytest_config.py
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Тест конфигурации pytest
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def test_pytest_config_loaded():
|
||||||
|
"""Проверяем, что конфигурация pytest загружена"""
|
||||||
|
# Проверяем, что мы находимся в корневой директории проекта
|
||||||
|
assert os.path.exists('pytest.ini'), "pytest.ini должен существовать в корне проекта"
|
||||||
|
|
||||||
|
# Проверяем, что директория tests существует
|
||||||
|
assert os.path.exists('tests'), "Директория tests должна существовать"
|
||||||
|
assert os.path.exists('tests/infra'), "Директория tests/infra должна существовать"
|
||||||
|
assert os.path.exists('tests/bot'), "Директория tests/bot должна существовать"
|
||||||
|
|
||||||
|
def test_import_paths():
|
||||||
|
"""Проверяем, что пути импорта настроены правильно"""
|
||||||
|
# Проверяем, что можем импортировать модули мониторинга
|
||||||
|
sys.path.insert(0, 'infra/monitoring')
|
||||||
|
try:
|
||||||
|
import metrics_collector
|
||||||
|
import message_sender
|
||||||
|
import prometheus_server
|
||||||
|
import server_monitor
|
||||||
|
assert True
|
||||||
|
except ImportError as e:
|
||||||
|
pytest.fail(f"Failed to import monitoring modules: {e}")
|
||||||
|
finally:
|
||||||
|
# Убираем добавленный путь
|
||||||
|
if 'infra/monitoring' in sys.path:
|
||||||
|
sys.path.remove('infra/monitoring')
|
||||||
|
|
||||||
|
def test_test_structure():
|
||||||
|
"""Проверяем структуру тестов"""
|
||||||
|
# Проверяем наличие __init__.py файлов
|
||||||
|
assert os.path.exists('tests/__init__.py'), "tests/__init__.py должен существовать"
|
||||||
|
assert os.path.exists('tests/infra/__init__.py'), "tests/infra/__init__.py должен существовать"
|
||||||
|
assert os.path.exists('tests/bot/__init__.py'), "tests/bot/__init__.py должен существовать"
|
||||||
|
|
||||||
|
# Проверяем наличие тестов инфраструктуры
|
||||||
|
assert os.path.exists('tests/infra/test_infra.py'), "tests/infra/test_infra.py должен существовать"
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pytest.main([__file__, "-v"])
|
||||||
Reference in New Issue
Block a user