- Add Model Mixins: TimestampMixin, SoftDeleteMixin, AuditMixin, etc. - Add Base Services: BaseService, BulkOperationsMixin, QueryOptimizerMixin - Add Base ViewSets with bulk operations - Add BackgroundJob model for Celery task tracking - Add BaseAppCommand for management commands - Add permissions, pagination, filters, cache, logging - Migrate tests to factory_boy + faker - Add CHANGELOG.md - 297 tests passing
269 lines
8.8 KiB
Python
269 lines
8.8 KiB
Python
#!/usr/bin/env python
|
||
"""
|
||
Простой скрипт для запуска тестов, обходящий проблемы с pytest и pdbpp
|
||
Использует стандартный Django test runner с улучшенными возможностями
|
||
Поддерживает coverage и дополнительные опции
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
from io import StringIO
|
||
import argparse
|
||
|
||
import django
|
||
|
||
|
||
def setup_django():
|
||
"""Настройка Django окружения"""
|
||
# Монкипатчим проблематичные модули
|
||
sys.modules["ipdb"] = type("MockModule", (), {"__getattr__": lambda s, n: None})()
|
||
|
||
# Добавляем src в PYTHONPATH
|
||
src_path = os.path.join(os.path.dirname(__file__), "src")
|
||
if src_path not in sys.path:
|
||
sys.path.insert(0, src_path)
|
||
|
||
# Устанавливаем настройки Django (принудительно для тестов)
|
||
os.environ["DJANGO_SETTINGS_MODULE"] = "config.settings.test"
|
||
|
||
# Инициализируем Django
|
||
django.setup()
|
||
|
||
|
||
def run_tests_with_args(test_args, options):
|
||
"""Запуск тестов с заданными аргументами"""
|
||
from django.conf import settings
|
||
from django.test.utils import get_runner
|
||
|
||
# Получаем test runner
|
||
TestRunner = get_runner(settings)
|
||
|
||
# Настройки для test runner
|
||
runner_kwargs = {
|
||
"verbosity": options.verbose,
|
||
"interactive": False,
|
||
"keepdb": options.keepdb,
|
||
"failfast": options.failfast,
|
||
}
|
||
|
||
# Добавляем parallel если указано
|
||
if options.parallel:
|
||
runner_kwargs["parallel"] = options.parallel
|
||
|
||
test_runner = TestRunner(**runner_kwargs)
|
||
|
||
# Запускаем тесты
|
||
failures = test_runner.run_tests(test_args)
|
||
return failures
|
||
|
||
|
||
def parse_arguments():
|
||
"""Парсинг аргументов командной строки"""
|
||
parser = argparse.ArgumentParser(description="Запуск Django тестов с дополнительными возможностями")
|
||
|
||
parser.add_argument(
|
||
"targets",
|
||
nargs="*",
|
||
help="Цели тестирования (по умолчанию: все тесты)",
|
||
default=["tests"]
|
||
)
|
||
|
||
parser.add_argument(
|
||
"--coverage", "--cov",
|
||
action="store_true",
|
||
help="Запуск тестов с измерением покрытия кода"
|
||
)
|
||
|
||
parser.add_argument(
|
||
"--fast",
|
||
action="store_true",
|
||
help="Запуск только быстрых тестов (исключает медленные)"
|
||
)
|
||
|
||
parser.add_argument(
|
||
"--failfast",
|
||
action="store_true",
|
||
help="Остановка при первой ошибке"
|
||
)
|
||
|
||
parser.add_argument(
|
||
"--verbose", "-v",
|
||
action="count",
|
||
default=2,
|
||
help="Уровень детализации вывода"
|
||
)
|
||
|
||
parser.add_argument(
|
||
"--keepdb",
|
||
action="store_true",
|
||
help="Сохранить тестовую базу данных"
|
||
)
|
||
|
||
parser.add_argument(
|
||
"--parallel",
|
||
type=int,
|
||
metavar="N",
|
||
help="Запуск тестов в N параллельных процессах"
|
||
)
|
||
|
||
args = parser.parse_args()
|
||
|
||
# Преобразуем пути для удобства использования
|
||
test_targets = []
|
||
|
||
for target in args.targets:
|
||
# Преобразование путей файлов в модули Django
|
||
if target.endswith(".py"):
|
||
# Убираем расширение .py
|
||
target = target[:-3]
|
||
|
||
# Заменяем слеши на точки для модульных путей
|
||
if "/" in target:
|
||
target = target.replace("/", ".")
|
||
|
||
# Добавляем префикс tests если его нет
|
||
if not target.startswith("tests"):
|
||
if target == "user":
|
||
# Если просто "user", запускаем все тесты user app
|
||
target = "tests.apps.user"
|
||
elif target in ["models", "views", "serializers", "services"]:
|
||
# Если это простые ключевые слова, добавляем test_ префикс
|
||
target = f"tests.apps.user.test_{target}"
|
||
elif (
|
||
"test_" in target
|
||
or "models" in target
|
||
or "views" in target
|
||
or "serializers" in target
|
||
or "services" in target
|
||
):
|
||
# Если это конкретный файл тестов с префиксом или содержит ключевые слова
|
||
if not target.startswith("test_"):
|
||
target = f"tests.apps.user.test_{target}"
|
||
else:
|
||
target = f"tests.apps.user.{target}"
|
||
else:
|
||
# Общий случай
|
||
target = f"tests.{target}"
|
||
|
||
test_targets.append(target)
|
||
|
||
args.targets = test_targets if test_targets else ["tests"]
|
||
return args
|
||
|
||
|
||
def print_test_info(test_targets, options):
|
||
"""Вывод информации о запуске тестов"""
|
||
print("🧪 Запуск тестов (Django test runner)...")
|
||
|
||
if test_targets == ["tests"]:
|
||
print("📁 Цель: Все тесты в проекте")
|
||
else:
|
||
print(f"📁 Цели: {', '.join(test_targets)}")
|
||
|
||
print(f"⚙️ Настройки Django: {os.environ.get('DJANGO_SETTINGS_MODULE')}")
|
||
print(f"📦 Путь к исходникам: {os.path.join(os.path.dirname(__file__), 'src')}")
|
||
|
||
# Дополнительные опции
|
||
options_info = []
|
||
if options.coverage:
|
||
options_info.append("📊 Измерение покрытия")
|
||
if options.fast:
|
||
options_info.append("🚀 Только быстрые тесты")
|
||
if options.failfast:
|
||
options_info.append("❌ Остановка при первой ошибке")
|
||
if options.keepdb:
|
||
options_info.append("💾 Сохранение тестовой БД")
|
||
if options.parallel:
|
||
options_info.append(f"⚡ Параллельность: {options.parallel}")
|
||
|
||
if options_info:
|
||
print("🔧 Опции:", " | ".join(options_info))
|
||
|
||
print("-" * 60)
|
||
|
||
|
||
def setup_coverage():
|
||
"""Настройка coverage"""
|
||
try:
|
||
import coverage
|
||
cov = coverage.Coverage(config_file="pyproject.toml")
|
||
cov.start()
|
||
return cov
|
||
except ImportError:
|
||
print("⚠️ Модуль coverage не установлен. Измерение покрытия недоступно.")
|
||
return None
|
||
|
||
|
||
def finalize_coverage(cov):
|
||
"""Завершение измерения покрытия"""
|
||
if cov:
|
||
cov.stop()
|
||
cov.save()
|
||
|
||
print("\n📊 Отчет о покрытии кода:")
|
||
print("-" * 40)
|
||
cov.report()
|
||
|
||
# Создание HTML отчета
|
||
try:
|
||
cov.html_report()
|
||
print("\n📄 HTML отчет создан в директории: htmlcov/")
|
||
except Exception as e:
|
||
print(f"⚠️ Не удалось создать HTML отчет: {e}")
|
||
|
||
|
||
def main():
|
||
"""Основная функция"""
|
||
cov = None
|
||
try:
|
||
# Парсинг аргументов
|
||
options = parse_arguments()
|
||
|
||
# Настройка coverage если нужно
|
||
if options.coverage:
|
||
cov = setup_coverage()
|
||
|
||
# Настройка Django
|
||
setup_django()
|
||
|
||
# Настройка фильтрации тестов
|
||
if options.fast:
|
||
os.environ["PYTEST_CURRENT_TEST_FILTER"] = "not slow"
|
||
|
||
# Вывод информации
|
||
print_test_info(options.targets, options)
|
||
|
||
# Запуск тестов
|
||
failures = run_tests_with_args(options.targets, options)
|
||
|
||
# Завершение coverage
|
||
if cov:
|
||
finalize_coverage(cov)
|
||
|
||
# Результат
|
||
if failures:
|
||
print(f"\n❌ Тесты завершились с ошибками: {failures} неудачных тестов")
|
||
sys.exit(1)
|
||
else:
|
||
print(f"\n✅ Все тесты прошли успешно!")
|
||
if cov:
|
||
print("📊 Отчет о покрытии сохранен")
|
||
sys.exit(0)
|
||
|
||
except KeyboardInterrupt:
|
||
print("\n❌ Тесты прерваны пользователем")
|
||
if cov:
|
||
cov.stop()
|
||
sys.exit(1)
|
||
except Exception as e:
|
||
print(f"\n❌ Ошибка при запуске тестов: {e}")
|
||
if cov:
|
||
cov.stop()
|
||
import traceback
|
||
traceback.print_exc()
|
||
sys.exit(1)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|