feat(core): add core module with mixins, services, and background jobs

- 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
This commit is contained in:
2026-01-21 11:47:26 +01:00
parent 06b30fca02
commit f121445313
72 changed files with 9258 additions and 594 deletions

277
run_tests.py Executable file → Normal file
View File

@@ -1,31 +1,268 @@
#!/usr/bin/env python
"""Скрипт для запуска тестов с обходом проблемы ipdb"""
"""
Простой скрипт для запуска тестов, обходящий проблемы с pytest и pdbpp
Использует стандартный Django test runner с улучшенными возможностями
Поддерживает coverage и дополнительные опции
"""
import os
import sys
from io import StringIO
import argparse
import django
# Монкипатчим ipdb до импорта Django
sys.modules["ipdb"] = type("MockModule", (), {"__getattr__": lambda s, n: None})()
# Настройка Django
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.development")
django.setup()
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)
# Теперь можем безопасно импортировать и запускать тесты
from django.core.management import execute_from_command_line
if __name__ == "__main__":
# Добавляем аргументы командной строки
args = sys.argv[1:] # Убираем имя скрипта
if not args:
# По умолчанию запускаем все тесты user app
args = ["test", "apps.user"]
# Подготовка аргументов для Django
django_args = ["manage.py"] + args
sys.argv = django_args
execute_from_command_line(sys.argv)
main()