Merge pull request 'feat(registers): add command to generate test data from factories' (#11) from feature/registers-generate-test-data-command into dev
Reviewed-on: #11
This commit was merged in pull request #11.
This commit is contained in:
1
src/apps/registers/management/__init__.py
Normal file
1
src/apps/registers/management/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Management package for registers app."""
|
||||
1
src/apps/registers/management/commands/__init__.py
Normal file
1
src/apps/registers/management/commands/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Management commands for registers app."""
|
||||
199
src/apps/registers/management/commands/generate_test_data.py
Normal file
199
src/apps/registers/management/commands/generate_test_data.py
Normal file
@@ -0,0 +1,199 @@
|
||||
"""Команда генерации тестовых данных по реестрам через test factories."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from importlib import import_module
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from apps.core.management.commands.base import BaseAppCommand
|
||||
from apps.parsers.models import ParserLoadLog
|
||||
from django.conf import settings
|
||||
from django.core.management.base import CommandError
|
||||
from django.db.models import Max
|
||||
|
||||
|
||||
class Command(BaseAppCommand):
|
||||
"""Генерирует тестовые данные для всех реестров на основе фабрик тестов."""
|
||||
|
||||
help = "Генерация тестовых данных для registers/parsers на основе фабрик из tests"
|
||||
use_transaction = True
|
||||
|
||||
def add_arguments(self, parser) -> None:
|
||||
"""Добавление аргументов команды."""
|
||||
super().add_arguments(parser)
|
||||
parser.add_argument(
|
||||
"--count",
|
||||
type=int,
|
||||
default=20,
|
||||
help="Количество записей для каждого парсерного реестра",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--registers-count",
|
||||
type=int,
|
||||
default=5,
|
||||
help="Количество записей в справочнике реестров (apps.registers)",
|
||||
)
|
||||
|
||||
def execute_command(self, *args: Any, **options: Any) -> str:
|
||||
"""Основная логика генерации тестовых данных."""
|
||||
count = options["count"]
|
||||
registers_count = options["registers_count"]
|
||||
|
||||
self._validate_positive("count", count)
|
||||
self._validate_positive("registers_count", registers_count)
|
||||
|
||||
factories = self._load_factories()
|
||||
|
||||
register_factory = factories["register_factory"]
|
||||
organization_factory = factories["organization_factory"]
|
||||
register_upload_factory = factories["register_upload_factory"]
|
||||
registry_membership_period_factory = factories[
|
||||
"registry_membership_period_factory"
|
||||
]
|
||||
parser_load_log_factory = factories["parser_load_log_factory"]
|
||||
proxy_factory = factories["proxy_factory"]
|
||||
industrial_certificate_factory = factories["industrial_certificate_factory"]
|
||||
manufacturer_factory = factories["manufacturer_factory"]
|
||||
industrial_product_factory = factories["industrial_product_factory"]
|
||||
inspection_factory = factories["inspection_factory"]
|
||||
procurement_factory = factories["procurement_factory"]
|
||||
|
||||
run_token = uuid4().hex[:10]
|
||||
run_seed = int(run_token[:6], 16) % 1_000_000
|
||||
|
||||
self.log_info("Создание тестовых реестров organizations/registers...")
|
||||
registers = [
|
||||
register_factory.create(name=f"Тестовый реестр {run_token}-{index + 1}")
|
||||
for index in range(registers_count)
|
||||
]
|
||||
for register_index, register in enumerate(
|
||||
self.progress_iter(
|
||||
registers,
|
||||
desc="Заполнение реестров организаций",
|
||||
total=registers_count,
|
||||
)
|
||||
):
|
||||
for item_index in range(count):
|
||||
org_serial = register_index * count + item_index + 1
|
||||
organization = organization_factory.create(
|
||||
mn_ogrn=80_000_000_000_000 + run_seed * 100_000 + org_serial,
|
||||
mn_inn=1_000_000_000
|
||||
+ (run_seed * 10_000 + org_serial) % 8_000_000_000,
|
||||
)
|
||||
upload = register_upload_factory.create(registry=register)
|
||||
registry_membership_period_factory.create(
|
||||
registry=register,
|
||||
organization=organization,
|
||||
started_by_upload=upload,
|
||||
)
|
||||
|
||||
self.log_info("Создание тестовых данных parser-реестров...")
|
||||
for index in range(1, count + 1):
|
||||
industrial_certificate_factory.create(
|
||||
certificate_number=f"TST-CERT-{run_token}-{index:05d}"
|
||||
)
|
||||
|
||||
for index in range(1, count + 1):
|
||||
manufacturer_factory.create(
|
||||
inn=f"{run_seed:06d}{index:09d}",
|
||||
)
|
||||
|
||||
industrial_product_factory.create_batch(count)
|
||||
|
||||
for index in range(1, count + 1):
|
||||
inspection_factory.create(
|
||||
registration_number=f"TST-INSP-{run_token}-{index:05d}"
|
||||
)
|
||||
|
||||
for index in range(1, count + 1):
|
||||
procurement_factory.create(purchase_number=f"{run_seed:06d}{index:013d}")
|
||||
|
||||
created_by_registry = {
|
||||
"industrial_certificates": count,
|
||||
"manufacturers": count,
|
||||
"industrial_products": count,
|
||||
"inspections": count,
|
||||
"procurements": count,
|
||||
}
|
||||
|
||||
self.log_info("Создание прокси и логов загрузок...")
|
||||
for index in range(1, count + 1):
|
||||
proxy_factory.create(address=f"http://proxy-{run_token}-{index}.local:8080")
|
||||
|
||||
load_log_sources = [source for source, _ in ParserLoadLog.Source.choices]
|
||||
for source in load_log_sources:
|
||||
max_batch = (
|
||||
ParserLoadLog.objects.filter(source=source).aggregate(
|
||||
max_batch=Max("batch_id")
|
||||
)["max_batch"]
|
||||
or 0
|
||||
)
|
||||
for index in range(1, count + 1):
|
||||
parser_load_log_factory.create(
|
||||
source=source,
|
||||
batch_id=max_batch + index,
|
||||
)
|
||||
|
||||
summary_lines = [
|
||||
"Тестовые данные успешно созданы.",
|
||||
f"registers.Register: {registers_count}",
|
||||
f"registers.RegisterUpload: {registers_count * count}",
|
||||
f"registers.RegistryMembershipPeriod: {registers_count * count}",
|
||||
f"parsers.Proxy: {count}",
|
||||
f"parsers.ParserLoadLog: {len(load_log_sources) * count}",
|
||||
]
|
||||
summary_lines.extend(
|
||||
f"parsers.{registry_name}: {created_count}"
|
||||
for registry_name, created_count in created_by_registry.items()
|
||||
)
|
||||
return "\n".join(summary_lines)
|
||||
|
||||
@staticmethod
|
||||
def _validate_positive(option_name: str, value: int) -> None:
|
||||
"""Проверка, что числовой аргумент больше нуля."""
|
||||
if value < 1:
|
||||
raise CommandError(f"--{option_name.replace('_', '-')} должен быть >= 1")
|
||||
|
||||
@staticmethod
|
||||
def _load_factories() -> dict[str, Any]:
|
||||
"""Импорт фабрик из тестов (tests/apps/*/factories.py)."""
|
||||
Command._ensure_project_root_on_path()
|
||||
|
||||
try:
|
||||
registers_factories = import_module("tests.apps.registers.factories")
|
||||
parsers_factories = import_module("tests.apps.parsers.factories")
|
||||
except ModuleNotFoundError as exc:
|
||||
raise CommandError(
|
||||
"Не удалось импортировать тестовые фабрики из tests/apps/*/factories.py. "
|
||||
"Проверьте, что каталог tests доступен в PYTHONPATH."
|
||||
) from exc
|
||||
|
||||
return {
|
||||
"register_factory": registers_factories.RegisterFactory,
|
||||
"organization_factory": registers_factories.OrganizationFactory,
|
||||
"register_upload_factory": registers_factories.RegisterUploadFactory,
|
||||
"registry_membership_period_factory": (
|
||||
registers_factories.RegistryMembershipPeriodFactory
|
||||
),
|
||||
"parser_load_log_factory": parsers_factories.ParserLoadLogFactory,
|
||||
"proxy_factory": parsers_factories.ProxyFactory,
|
||||
"industrial_certificate_factory": (
|
||||
parsers_factories.IndustrialCertificateRecordFactory
|
||||
),
|
||||
"manufacturer_factory": parsers_factories.ManufacturerRecordFactory,
|
||||
"industrial_product_factory": parsers_factories.IndustrialProductRecordFactory,
|
||||
"inspection_factory": parsers_factories.InspectionRecordFactory,
|
||||
"procurement_factory": parsers_factories.ProcurementRecordFactory,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _ensure_project_root_on_path() -> None:
|
||||
"""Добавить корень проекта в PYTHONPATH для импорта tests.*."""
|
||||
base_dir = Path(settings.BASE_DIR)
|
||||
project_root = Path(getattr(settings, "PROJECT_ROOT", base_dir.parent))
|
||||
project_root_str = str(project_root)
|
||||
if project_root_str not in sys.path:
|
||||
sys.path.append(project_root_str)
|
||||
@@ -69,3 +69,10 @@ CACHES = {
|
||||
|
||||
# Email
|
||||
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
||||
|
||||
# DRF: отключаем throttling в dev, чтобы не мешать фронту тестировать API.
|
||||
REST_FRAMEWORK = {
|
||||
**globals().get("REST_FRAMEWORK", {}),
|
||||
"DEFAULT_THROTTLE_CLASSES": [],
|
||||
"DEFAULT_THROTTLE_RATES": {},
|
||||
}
|
||||
|
||||
73
tests/apps/registers/test_generate_test_data_command.py
Normal file
73
tests/apps/registers/test_generate_test_data_command.py
Normal file
@@ -0,0 +1,73 @@
|
||||
"""Тесты команды generate_test_data."""
|
||||
|
||||
from apps.parsers.models import (
|
||||
IndustrialCertificateRecord,
|
||||
IndustrialProductRecord,
|
||||
InspectionRecord,
|
||||
ManufacturerRecord,
|
||||
ParserLoadLog,
|
||||
ProcurementRecord,
|
||||
Proxy,
|
||||
)
|
||||
from apps.registers.models import (
|
||||
Organization,
|
||||
Register,
|
||||
RegisterUpload,
|
||||
RegistryMembershipPeriod,
|
||||
)
|
||||
from django.core.management import call_command
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class GenerateTestDataCommandTest(TestCase):
|
||||
"""Проверки management command generate_test_data."""
|
||||
|
||||
def test_command_creates_data_for_every_registry(self):
|
||||
"""Команда создаёт данные по всем целевым реестрам."""
|
||||
call_command(
|
||||
"generate_test_data",
|
||||
count=2,
|
||||
registers_count=3,
|
||||
silent=True,
|
||||
verbosity=0,
|
||||
)
|
||||
|
||||
self.assertEqual(Register.objects.count(), 3)
|
||||
self.assertEqual(RegisterUpload.objects.count(), 6)
|
||||
self.assertEqual(RegistryMembershipPeriod.objects.count(), 6)
|
||||
self.assertEqual(Organization.objects.count(), 6)
|
||||
|
||||
self.assertEqual(IndustrialCertificateRecord.objects.count(), 2)
|
||||
self.assertEqual(ManufacturerRecord.objects.count(), 2)
|
||||
self.assertEqual(IndustrialProductRecord.objects.count(), 2)
|
||||
self.assertEqual(InspectionRecord.objects.count(), 2)
|
||||
self.assertEqual(ProcurementRecord.objects.count(), 2)
|
||||
|
||||
self.assertEqual(Proxy.objects.count(), 2)
|
||||
self.assertEqual(
|
||||
ParserLoadLog.objects.count(),
|
||||
len(ParserLoadLog.Source.values) * 2,
|
||||
)
|
||||
|
||||
def test_command_dry_run_rolls_back_everything(self):
|
||||
"""В dry-run режиме все изменения откатываются."""
|
||||
call_command(
|
||||
"generate_test_data",
|
||||
count=1,
|
||||
registers_count=1,
|
||||
dry_run=True,
|
||||
silent=True,
|
||||
verbosity=0,
|
||||
)
|
||||
|
||||
self.assertEqual(Register.objects.count(), 0)
|
||||
self.assertEqual(RegisterUpload.objects.count(), 0)
|
||||
self.assertEqual(RegistryMembershipPeriod.objects.count(), 0)
|
||||
self.assertEqual(Organization.objects.count(), 0)
|
||||
self.assertEqual(IndustrialCertificateRecord.objects.count(), 0)
|
||||
self.assertEqual(ManufacturerRecord.objects.count(), 0)
|
||||
self.assertEqual(IndustrialProductRecord.objects.count(), 0)
|
||||
self.assertEqual(InspectionRecord.objects.count(), 0)
|
||||
self.assertEqual(ProcurementRecord.objects.count(), 0)
|
||||
self.assertEqual(Proxy.objects.count(), 0)
|
||||
self.assertEqual(ParserLoadLog.objects.count(), 0)
|
||||
Reference in New Issue
Block a user