feat(registers): add command to generate test data from factories #11
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
|
||||||
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
|
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