Initial commit
This commit is contained in:
commit
9795660e1f
43 changed files with 2757 additions and 0 deletions
BIN
utils/__pycache__/logger.cpython-313.pyc
Normal file
BIN
utils/__pycache__/logger.cpython-313.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/waiters.cpython-313.pyc
Normal file
BIN
utils/__pycache__/waiters.cpython-313.pyc
Normal file
Binary file not shown.
0
utils/allure_helpers.py
Normal file
0
utils/allure_helpers.py
Normal file
398
utils/logger.py
Normal file
398
utils/logger.py
Normal file
|
|
@ -0,0 +1,398 @@
|
|||
import logging
|
||||
import sys
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any, Union
|
||||
from datetime import datetime
|
||||
import json
|
||||
import traceback
|
||||
import inspect
|
||||
|
||||
# Цвета для консольного вывода (только если поддерживается)
|
||||
from colorama import init, Fore, Back, Style
|
||||
init(autoreset=True)
|
||||
|
||||
class TestLogger:
|
||||
"""Кастомный логгер для тестов с поддержкой Allure и консольного вывода"""
|
||||
|
||||
# Уровни логирования с цветами
|
||||
LEVEL_COLORS = {
|
||||
'DEBUG': Fore.CYAN,
|
||||
'INFO': Fore.GREEN,
|
||||
'WARNING': Fore.YELLOW,
|
||||
'ERROR': Fore.RED,
|
||||
'CRITICAL': Fore.RED + Back.WHITE,
|
||||
}
|
||||
|
||||
# Уровни логирования для Allure
|
||||
ALLURE_LEVELS = {
|
||||
'DEBUG': 'debug',
|
||||
'INFO': 'info',
|
||||
'WARNING': 'warning',
|
||||
'ERROR': 'error',
|
||||
'CRITICAL': 'critical',
|
||||
}
|
||||
|
||||
_instances = {} # Кэш инстансов логгеров
|
||||
|
||||
def __init__(self, name: str, level: str = 'INFO',
|
||||
log_to_file: bool = True,
|
||||
log_to_console: bool = True,
|
||||
log_to_allure: bool = True):
|
||||
"""
|
||||
Инициализация логгера
|
||||
|
||||
Args:
|
||||
name: Имя логгера (обычно __name__)
|
||||
level: Уровень логирования (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||||
log_to_file: Логировать в файл
|
||||
log_to_console: Логировать в консоль
|
||||
log_to_allure: Логировать в Allure отчет
|
||||
"""
|
||||
self.name = name
|
||||
self.log_to_file = log_to_file
|
||||
self.log_to_console = log_to_console
|
||||
self.log_to_allure = log_to_allure
|
||||
|
||||
# Создаем стандартный логгер
|
||||
self.logger = logging.getLogger(name)
|
||||
self.logger.setLevel(getattr(logging, level.upper()))
|
||||
|
||||
# Удаляем существующие обработчики
|
||||
self.logger.handlers.clear()
|
||||
|
||||
# Форматтеры
|
||||
self._setup_formatters()
|
||||
|
||||
# Обработчики
|
||||
self._setup_handlers()
|
||||
|
||||
# Для хранения контекста теста
|
||||
self.test_context = {}
|
||||
|
||||
def _setup_formatters(self):
|
||||
"""Настройка форматтеров"""
|
||||
# Подробный формат для файла
|
||||
self.file_formatter = logging.Formatter(
|
||||
fmt='%(asctime)s.%(msecs)03d | %(levelname)-8s | %(name)s | %(filename)s:%(lineno)d | %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
|
||||
# Простой формат для консоли
|
||||
self.console_formatter = logging.Formatter(
|
||||
fmt='%(asctime)s | %(levelname)-8s | %(name)-20s | %(message)s',
|
||||
datefmt='%H:%M:%S'
|
||||
)
|
||||
|
||||
# JSON формат для машинной обработки
|
||||
self.json_formatter = JSONFormatter()
|
||||
|
||||
def _setup_handlers(self):
|
||||
"""Настройка обработчиков"""
|
||||
# Консольный обработчик
|
||||
if self.log_to_console:
|
||||
console_handler = logging.StreamHandler(sys.stdout)
|
||||
console_handler.setLevel(self.logger.level)
|
||||
console_handler.setFormatter(self.console_formatter)
|
||||
|
||||
console_handler.setFormatter(ColorFormatter())
|
||||
|
||||
self.logger.addHandler(console_handler)
|
||||
|
||||
# Файловый обработчик
|
||||
if self.log_to_file:
|
||||
self._setup_file_handler()
|
||||
|
||||
def _setup_file_handler(self):
|
||||
"""Настройка файлового обработчика"""
|
||||
from config.settings import settings
|
||||
|
||||
log_file = Path(settings.LOG_FILE)
|
||||
log_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
file_handler = logging.FileHandler(log_file, encoding='utf-8')
|
||||
file_handler.setLevel(self.logger.level)
|
||||
file_handler.setFormatter(self.file_formatter)
|
||||
self.logger.addHandler(file_handler)
|
||||
|
||||
# JSON лог для машинной обработки
|
||||
json_log_file = log_file.parent / f"{log_file.stem}_json{log_file.suffix}"
|
||||
json_handler = logging.FileHandler(json_log_file, encoding='utf-8')
|
||||
json_handler.setLevel(self.logger.level)
|
||||
json_handler.setFormatter(self.json_formatter)
|
||||
self.logger.addHandler(json_handler)
|
||||
|
||||
def set_test_context(self, **kwargs):
|
||||
"""Установка контекста теста (имя теста, тестовые данные и т.д.)"""
|
||||
self.test_context.update(kwargs)
|
||||
|
||||
def clear_test_context(self):
|
||||
"""Очистка контекста теста"""
|
||||
self.test_context.clear()
|
||||
|
||||
def debug(self, message: str, **kwargs):
|
||||
"""Логирование на уровне DEBUG"""
|
||||
self._log('DEBUG', message, **kwargs)
|
||||
|
||||
def info(self, message: str, **kwargs):
|
||||
"""Логирование на уровне INFO"""
|
||||
self._log('INFO', message, **kwargs)
|
||||
|
||||
def warning(self, message: str, **kwargs):
|
||||
"""Логирование на уровне WARNING"""
|
||||
self._log('WARNING', message, **kwargs)
|
||||
|
||||
def error(self, message: str, exception: Optional[Exception] = None, **kwargs):
|
||||
"""Логирование на уровне ERROR"""
|
||||
if exception:
|
||||
message = f"{message} | Exception: {exception} | Traceback: {traceback.format_exc()}"
|
||||
self._log('ERROR', message, **kwargs)
|
||||
|
||||
def critical(self, message: str, **kwargs):
|
||||
"""Логирование на уровне CRITICAL"""
|
||||
self._log('CRITICAL', message, **kwargs)
|
||||
|
||||
def _log(self, level: str, message: str, **kwargs):
|
||||
"""Внутренний метод логирования"""
|
||||
# Добавляем контекст к сообщению
|
||||
if self.test_context:
|
||||
context_str = ' | '.join(f"{k}: {v}" for k, v in self.test_context.items())
|
||||
message = f"{message} | Context: {context_str}"
|
||||
|
||||
# Добавляем дополнительные поля
|
||||
if kwargs:
|
||||
extra_fields = ' | '.join(f"{k}: {v}" for k, v in kwargs.items())
|
||||
message = f"{message} | {extra_fields}"
|
||||
|
||||
# Получаем информацию о вызывающем коде
|
||||
curr_frame = inspect.currentframe()
|
||||
lineno = None
|
||||
func_name = None
|
||||
filename = None
|
||||
|
||||
if curr_frame and curr_frame.f_back:
|
||||
frame = curr_frame.f_back.f_back
|
||||
if frame:
|
||||
filename = frame.f_code.co_filename
|
||||
lineno = frame.f_lineno
|
||||
func_name = frame.f_code.co_name
|
||||
|
||||
extra = {
|
||||
'filename': Path(filename).name if filename else '-',
|
||||
'lineno': lineno,
|
||||
'func_name': func_name,
|
||||
'test_context': self.test_context.copy(),
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
# Логируем через стандартный логгер
|
||||
log_method = getattr(self.logger, level.lower())
|
||||
log_method(message, extra=extra)
|
||||
|
||||
# Логируем в Allure
|
||||
if self.log_to_allure:
|
||||
self._log_to_allure(level, message)
|
||||
|
||||
def _log_to_allure(self, level: str, message: str):
|
||||
"""Логирование в Allure отчет"""
|
||||
try:
|
||||
import allure
|
||||
|
||||
allure_level = self.ALLURE_LEVELS.get(level, 'info')
|
||||
getattr(allure.step, allure_level)(message)
|
||||
except ImportError:
|
||||
pass # Allure не установлен
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Failed to log to Allure: {e}")
|
||||
|
||||
def log_api_request(self, method: str, url: str,
|
||||
headers: Optional[dict[str, Any]] = None,
|
||||
body: Any = None,
|
||||
response: Any = None):
|
||||
"""Логирование API запроса"""
|
||||
log_data = {
|
||||
'type': 'API_REQUEST',
|
||||
'method': method,
|
||||
'url': url,
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
if headers:
|
||||
log_data['headers'] = {k: v for k, v in headers.items() if k.lower() != 'authorization'}
|
||||
|
||||
if body:
|
||||
log_data['body'] = str(body)[:500] + ('...' if len(str(body)) > 500 else '')
|
||||
|
||||
if response:
|
||||
log_data['response_status'] = getattr(response, 'status_code', None)
|
||||
log_data['response_body'] = str(getattr(response, 'text', ''))[:500] + ('...' if len(str(getattr(response, 'text', ''))) > 500 else '')
|
||||
|
||||
self.debug(f"API Request: {method} {url}", **log_data)
|
||||
|
||||
def log_ui_action(self, action: str, element: Optional[str] = None,
|
||||
value: Optional[str] = None, success: bool = True):
|
||||
"""Логирование UI действий"""
|
||||
log_data = {
|
||||
'type': 'UI_ACTION',
|
||||
'action': action,
|
||||
'element': element,
|
||||
'value': value,
|
||||
'success': success,
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
level = 'INFO' if success else 'ERROR'
|
||||
message = f"UI Action: {action}"
|
||||
if element:
|
||||
message += f" on {element}"
|
||||
if value:
|
||||
message += f" with value: {value}"
|
||||
|
||||
self._log(level, message, **log_data)
|
||||
|
||||
def log_test_start(self, test_name: str, test_params: Optional[dict] = None):
|
||||
"""Логирование начала теста"""
|
||||
self.set_test_context(test_name=test_name)
|
||||
|
||||
log_data = {
|
||||
'type': 'TEST_START',
|
||||
'test_name': test_name,
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
if test_params:
|
||||
log_data['parameters'] = test_params
|
||||
|
||||
self.info(f"🚀 Starting test: {test_name}", **log_data)
|
||||
|
||||
def log_test_end(self, test_name: str, status: str,
|
||||
duration: float = None, error: str = None):
|
||||
"""Логирование окончания теста"""
|
||||
log_data = {
|
||||
'type': 'TEST_END',
|
||||
'test_name': test_name,
|
||||
'status': status,
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
if duration is not None:
|
||||
log_data['duration_seconds'] = round(duration, 2)
|
||||
|
||||
if error:
|
||||
log_data['error'] = error
|
||||
|
||||
emoji = '✅' if status == 'PASSED' else '❌' if status == 'FAILED' else '⚠️'
|
||||
self.info(f"{emoji} Test {status}: {test_name} "
|
||||
f"(Duration: {duration:.2f}s)" if duration else "", **log_data)
|
||||
|
||||
self.clear_test_context()
|
||||
|
||||
def log_screenshot(self, screenshot_path: str, description: str = "Screenshot"):
|
||||
"""Логирование скриншота"""
|
||||
log_data = {
|
||||
'type': 'SCREENSHOT',
|
||||
'path': screenshot_path,
|
||||
'description': description,
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
self.info(f"📸 Screenshot saved: {description} -> {screenshot_path}", **log_data)
|
||||
|
||||
# Прикрепляем к Allure
|
||||
if self.log_to_allure:
|
||||
try:
|
||||
import allure
|
||||
if Path(screenshot_path).exists():
|
||||
with open(screenshot_path, 'rb') as f:
|
||||
allure.attach(
|
||||
f.read(),
|
||||
name=description,
|
||||
attachment_type=allure.attachment_type.PNG
|
||||
)
|
||||
except Exception as e:
|
||||
self.warning(f"Failed to attach screenshot to Allure: {e}")
|
||||
|
||||
@classmethod
|
||||
def get_logger(cls, name: Optional[str] = None, **kwargs) -> 'TestLogger':
|
||||
"""Фабричный метод для получения логгера (singleton pattern)"""
|
||||
if name is None:
|
||||
# Получаем имя вызывающего модуля
|
||||
curr_frame = inspect.currentframe()
|
||||
if curr_frame:
|
||||
frame = curr_frame.f_back
|
||||
module = inspect.getmodule(frame)
|
||||
name = module.__name__ if module else '__main__'
|
||||
|
||||
if name not in cls._instances and name:
|
||||
cls._instances[name] = cls(name, **kwargs)
|
||||
|
||||
return cls._instances[name]
|
||||
|
||||
|
||||
class ColorFormatter(logging.Formatter):
|
||||
"""Форматтер с цветами для консоли"""
|
||||
|
||||
FORMATS = {
|
||||
logging.DEBUG: Fore.CYAN + '%(asctime)s | %(levelname)-8s | %(name)-20s | %(message)s' + Style.RESET_ALL,
|
||||
logging.INFO: Fore.GREEN + '%(asctime)s | %(levelname)-8s | %(name)-20s | %(message)s' + Style.RESET_ALL,
|
||||
logging.WARNING: Fore.YELLOW + '%(asctime)s | %(levelname)-8s | %(name)-20s | %(message)s' + Style.RESET_ALL,
|
||||
logging.ERROR: Fore.RED + '%(asctime)s | %(levelname)-8s | %(name)-20s | %(message)s' + Style.RESET_ALL,
|
||||
logging.CRITICAL: Fore.RED + Back.WHITE + '%(asctime)s | %(levelname)-8s | %(name)-20s | %(message)s' + Style.RESET_ALL,
|
||||
}
|
||||
|
||||
def format(self, record):
|
||||
log_fmt = self.FORMATS.get(record.levelno, self.FORMATS[logging.INFO])
|
||||
formatter = logging.Formatter(log_fmt, datefmt='%H:%M:%S')
|
||||
return formatter.format(record)
|
||||
|
||||
|
||||
class JSONFormatter(logging.Formatter):
|
||||
"""Форматтер для JSON логов (удобно для машинной обработки)"""
|
||||
|
||||
def format(self, record):
|
||||
log_record = {
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'level': record.levelname,
|
||||
'logger': record.name,
|
||||
'message': record.getMessage(),
|
||||
'module': record.module,
|
||||
'function': record.funcName,
|
||||
'line': record.lineno,
|
||||
'thread': record.threadName,
|
||||
'process': record.processName,
|
||||
}
|
||||
|
||||
if not record:
|
||||
return json.dumps(log_record, ensure_ascii=False)
|
||||
|
||||
# Добавляем дополнительные поля из extra
|
||||
if hasattr(record, 'test_context'):
|
||||
log_record['test_context'] = record.test_context
|
||||
|
||||
if hasattr(record, 'filename'):
|
||||
log_record['filename'] = record.filename
|
||||
|
||||
# Добавляем exception info если есть
|
||||
if record.exc_info:
|
||||
log_record['exception'] = {
|
||||
'type': record.exc_info[0].__name__ if record.exc_info[0] else 'Unknown exception type',
|
||||
'message': str(record.exc_info[1]),
|
||||
'traceback': self.formatException(record.exc_info),
|
||||
}
|
||||
|
||||
return json.dumps(log_record, ensure_ascii=False)
|
||||
|
||||
|
||||
# Глобальный логгер по умолчанию
|
||||
def get_logger(name: Optional[str] = None, **kwargs) -> TestLogger:
|
||||
"""
|
||||
Утилита для получения логгера
|
||||
|
||||
Args:
|
||||
name: Имя логгера (обычно __name__)
|
||||
**kwargs: Дополнительные параметры для TestLogger
|
||||
|
||||
Returns:
|
||||
Экземпляр TestLogger
|
||||
"""
|
||||
return TestLogger.get_logger(name, **kwargs)
|
||||
4
utils/waiters.py
Normal file
4
utils/waiters.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
|
||||
|
||||
waiter: WebDriverWait
|
||||
Loading…
Add table
Add a link
Reference in a new issue