fix mobile and names
This commit is contained in:
1 parent
e3a6d1f9d4
commit
7e5c83bea2
12 files changed
+82792
-6
No files matched your search
@@ -0,0 +1,476 @@
|
||||
"""تستهای قابلیت «افزودن پیشوند آقای/خانم به نام مشتریان».
|
||||
|
||||
همهٔ تستها روی دیتابیس تستی Django اجرا میشوند (SQLite در حافظه) و هیچگاه به
|
||||
``data9/kt.sqlite3`` (دیتابیس واقعی) دست نمیزنند. فایل دادهٔ واقعی هم دستنخورده
|
||||
میماند؛ هر تست روی یک فایل JSON ساختگیِ موقت کار میکند.
|
||||
"""
|
||||
import hashlib
|
||||
import io
|
||||
import itertools
|
||||
import json
|
||||
import shutil
|
||||
import tempfile
|
||||
from datetime import timedelta
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
from django.core.management import call_command
|
||||
from django.core.management.base import CommandError
|
||||
from django.db import connections
|
||||
from django.db.models.query import QuerySet
|
||||
from django.db.utils import OperationalError
|
||||
from django.test import SimpleTestCase, TestCase
|
||||
from django.utils import timezone
|
||||
|
||||
from admin_panel.models import Customer, CustomerNameFixJob
|
||||
from admin_panel.name_fix import (ALGORITHM_VERSION, DatasetError,
|
||||
build_name_index, decide_prefix, load_name_index,
|
||||
normalize_name_key)
|
||||
from admin_panel.management.commands.fix_customer_names import (LOCK_TTL,
|
||||
MAX_DB_RETRIES)
|
||||
|
||||
from .factories import make_customer
|
||||
|
||||
DATASET_RECORDS = [
|
||||
{"Names": "محمد", "Gender": "M"},
|
||||
{"Names": "مریم", "Gender": "F"},
|
||||
{"Names": "علی", "Gender": "M"},
|
||||
{"Names": "علي", "Gender": "M"}, # ي عربی؛ همان کلید نرمالشده
|
||||
{"Names": "كاظم", "Gender": "M"}, # ك عربی
|
||||
{"Names": "کاظم", "Gender": "M"}, # ک فارسی (همان کلید)
|
||||
{"Names": "سارا", "Gender": "F"},
|
||||
{"Names": "سارا", "Gender": "F"}, # تکراری همجنسیت
|
||||
{"Names": "آرش", "Gender": "M"},
|
||||
{"Names": "آرش", "Gender": "F"}, # تکراری متناقض ⇒ مبهم
|
||||
{"Names": "نازنین", "Gender": "D"}, # جنسیت نامعتبر
|
||||
{"Names": 123, "Gender": "M"}, # رکورد malformed
|
||||
{"Names": "", "Gender": "M"}, # نام خالی
|
||||
{"Names": "بیجنس"}, # کلید Gender ندارد
|
||||
]
|
||||
|
||||
|
||||
def sha256_of(path):
|
||||
return hashlib.sha256(Path(path).read_bytes()).hexdigest()
|
||||
|
||||
|
||||
class DatasetFixtureMixin:
|
||||
"""ساخت فایل JSON موقت و پاکسازی آن پس از تست."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.tmpdir = tempfile.mkdtemp(prefix="namefix-")
|
||||
self.addCleanup(shutil.rmtree, self.tmpdir, ignore_errors=True)
|
||||
|
||||
def write_dataset(self, records=DATASET_RECORDS, name="iranianNamesDataset.json"):
|
||||
path = Path(self.tmpdir) / name
|
||||
path.write_text(json.dumps({"RECORDS": records}, ensure_ascii=False),
|
||||
encoding="utf-8")
|
||||
return path
|
||||
|
||||
|
||||
class NameIndexTests(DatasetFixtureMixin, SimpleTestCase):
|
||||
"""اعتبارسنجی فایل، نرمالسازی و ساخت index."""
|
||||
|
||||
def test_load_builds_conservative_index_and_report(self):
|
||||
path = self.write_dataset()
|
||||
name_index = load_name_index(path)
|
||||
|
||||
self.assertEqual(name_index.sha256, sha256_of(path))
|
||||
self.assertEqual(name_index.lookup("محمد"), "M")
|
||||
self.assertEqual(name_index.lookup("مریم"), "F")
|
||||
# ي/ی و ك/ک یکسانسازی میشوند.
|
||||
self.assertEqual(name_index.lookup("علی"), "M")
|
||||
self.assertEqual(name_index.lookup("کاظم"), "M")
|
||||
# تکراری متناقض و جنسیت نامعتبر کنار گذاشته میشوند.
|
||||
self.assertIsNone(name_index.lookup("آرش"))
|
||||
self.assertIsNone(name_index.lookup("نازنین"))
|
||||
|
||||
rep = name_index.report
|
||||
self.assertEqual(rep["records_total"], 14)
|
||||
self.assertEqual(rep["rejected_malformed"], 2)
|
||||
self.assertEqual(rep["rejected_empty_name"], 1)
|
||||
self.assertEqual(rep["rejected_unknown_gender"], 1)
|
||||
self.assertEqual(rep["usable_keys"], 5)
|
||||
self.assertEqual(rep["unusable_keys"], 2)
|
||||
self.assertEqual(rep["duplicate_keys"], 4)
|
||||
self.assertEqual(rep["duplicate_same_gender"], 3)
|
||||
self.assertEqual(rep["duplicate_conflict"], 1)
|
||||
|
||||
def test_normalization_unifies_arabic_and_persian_letters(self):
|
||||
self.assertEqual(normalize_name_key(" علي "), "علی")
|
||||
self.assertEqual(normalize_name_key("كاظم"), "کاظم")
|
||||
self.assertEqual(normalize_name_key("محمد"), "محمد")
|
||||
self.assertEqual(normalize_name_key(None), "")
|
||||
|
||||
def test_missing_records_key_is_rejected(self):
|
||||
path = Path(self.tmpdir) / "bad.json"
|
||||
path.write_text(json.dumps({"DATA": []}), encoding="utf-8")
|
||||
with self.assertRaises(DatasetError):
|
||||
load_name_index(path)
|
||||
|
||||
def test_records_must_be_a_list(self):
|
||||
path = Path(self.tmpdir) / "bad.json"
|
||||
path.write_text(json.dumps({"RECORDS": {"Names": "x"}}), encoding="utf-8")
|
||||
with self.assertRaises(DatasetError):
|
||||
load_name_index(path)
|
||||
|
||||
def test_invalid_json_is_rejected(self):
|
||||
path = Path(self.tmpdir) / "bad.json"
|
||||
path.write_text("{not json", encoding="utf-8")
|
||||
with self.assertRaises(DatasetError):
|
||||
load_name_index(path)
|
||||
|
||||
def test_normalization_collision_with_conflicting_gender_is_ambiguous(self):
|
||||
name_index = build_name_index([
|
||||
{"Names": "علي", "Gender": "M"},
|
||||
{"Names": "علی", "Gender": "F"}, # پس از نرمالسازی همان کلید
|
||||
])
|
||||
self.assertIsNone(name_index.lookup("علی"))
|
||||
self.assertIn("علی", name_index.ambiguous)
|
||||
|
||||
|
||||
class DecidePrefixTests(DatasetFixtureMixin, SimpleTestCase):
|
||||
"""الگوریتم تصمیم، مستقل از دیتابیس."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.name_index = load_name_index(self.write_dataset())
|
||||
|
||||
def test_expected_behaviour_table(self):
|
||||
cases = [
|
||||
("محمد یاوری", "آقای محمد یاوری", "male"),
|
||||
("مریم احمدی", "خانم مریم احمدی", "female"),
|
||||
(" محمد یاوری ", "آقای محمد یاوری", "male"),
|
||||
("\tمریم احمدی\n", "خانم مریم احمدی", "female"),
|
||||
("یاوری", None, "single_word"),
|
||||
("محمد", None, "single_word"),
|
||||
("مریم", None, "single_word"),
|
||||
("آقا محمد یاوری", None, "has_prefix"),
|
||||
("آقای محمد یاوری", None, "has_prefix"),
|
||||
("خانم مریم احمدی", None, "has_prefix"),
|
||||
("نام ناشناخته احمدی", None, "unknown_name"),
|
||||
("آرش محمدی", None, "ambiguous_gender"),
|
||||
("نازنین کریمی", None, "ambiguous_gender"),
|
||||
("علي رضایی", "آقای علي رضایی", "male"), # نام اصلی دستنخورده میماند
|
||||
("", None, "empty"),
|
||||
(" ", None, "empty"),
|
||||
]
|
||||
for raw, expected_new, expected_reason in cases:
|
||||
decision = decide_prefix(raw, self.name_index)
|
||||
self.assertEqual(decision.new_name, expected_new, msg=repr(raw))
|
||||
self.assertEqual(decision.reason, expected_reason, msg=repr(raw))
|
||||
|
||||
def test_none_value_is_handled(self):
|
||||
decision = decide_prefix(None, self.name_index)
|
||||
self.assertIsNone(decision.new_name)
|
||||
self.assertEqual(decision.reason, "empty")
|
||||
|
||||
def test_internal_spaces_are_not_rewritten(self):
|
||||
decision = decide_prefix("محمد یاوری", self.name_index)
|
||||
self.assertEqual(decision.new_name, "آقای محمد یاوری")
|
||||
|
||||
|
||||
|
||||
# (نام اولیه، نتیجهٔ مورد انتظار) — ترتیب = ترتیب ساخت ⇒ ترتیب pk
|
||||
CASES = [
|
||||
("محمد یاوری", "آقای محمد یاوری"),
|
||||
("مریم احمدی", "خانم مریم احمدی"),
|
||||
(" محمد یاوری ", "آقای محمد یاوری"),
|
||||
("یاوری", "یاوری"),
|
||||
("محمد", "محمد"),
|
||||
("مریم", "مریم"),
|
||||
("آقا محمد یاوری", "آقا محمد یاوری"),
|
||||
("آقای محمد یاوری", "آقای محمد یاوری"),
|
||||
("خانم مریم احمدی", "خانم مریم احمدی"),
|
||||
("نام ناشناخته احمدی", "نام ناشناخته احمدی"),
|
||||
("آرش محمدی", "آرش محمدی"),
|
||||
("نازنین کریمی", "نازنین کریمی"),
|
||||
("علي رضایی", "آقای علي رضایی"),
|
||||
(" نام ناشناخته احمدی ", " نام ناشناخته احمدی "),
|
||||
]
|
||||
|
||||
EXPECTED_CHANGED = 4 # موارد ۱، ۲، ۳، ۱۳
|
||||
EXPECTED_SINGLE_WORD = 3 # یاوری، محمد، مریم
|
||||
EXPECTED_HAS_PREFIX = 3
|
||||
EXPECTED_UNKNOWN = 2 # «نام ناشناخته …» دو مورد (با/بی فاصله)
|
||||
EXPECTED_AMBIGUOUS = 2 # آرش، نازنین
|
||||
|
||||
|
||||
class CommandTestMixin(DatasetFixtureMixin):
|
||||
"""ساخت مشتریان نمونه + فایل دادهٔ موقت برای تستهای فرمان."""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.json_path = self.write_dataset()
|
||||
self.customers = [make_customer(name) for name, _ in CASES]
|
||||
|
||||
def run_command(self, **options):
|
||||
options.setdefault("json_path", str(self.json_path))
|
||||
options.setdefault("stdout", self._stdout())
|
||||
return call_command("fix_customer_names", **options)
|
||||
|
||||
def _stdout(self):
|
||||
return io.StringIO()
|
||||
|
||||
def names(self):
|
||||
return list(Customer.objects.order_by("pk")
|
||||
.values_list("name_family", flat=True))
|
||||
|
||||
def expected_names(self):
|
||||
return [expected for _, expected in CASES]
|
||||
|
||||
def max_pk(self):
|
||||
return max(c.pk for c in self.customers)
|
||||
|
||||
|
||||
class DryRunTests(CommandTestMixin, TestCase):
|
||||
"""پیشفرض فرمان نباید هیچچیز بنویسد."""
|
||||
|
||||
def test_dry_run_changes_nothing(self):
|
||||
out = self._stdout()
|
||||
self.run_command(stdout=out)
|
||||
self.assertEqual(self.names(), [name for name, _ in CASES])
|
||||
self.assertEqual(CustomerNameFixJob.objects.count(), 0)
|
||||
self.assertIn("dry-run", out.getvalue())
|
||||
|
||||
def test_dry_run_flag_conflicts_with_apply(self):
|
||||
with self.assertRaises(CommandError):
|
||||
self.run_command(dry_run=True, apply=True, confirm_production=True)
|
||||
self.assertEqual(self.names(), [name for name, _ in CASES])
|
||||
|
||||
def test_dry_run_reports_expected_counts(self):
|
||||
out = self._stdout()
|
||||
self.run_command(stdout=out)
|
||||
text = out.getvalue()
|
||||
self.assertIn(f"مشتریان بررسیشده: {len(CASES)}", text)
|
||||
self.assertIn(f"قابل اصلاح / اصلاحشده: {EXPECTED_CHANGED}", text)
|
||||
self.assertIn(f"تککلمهای: {EXPECTED_SINGLE_WORD}", text)
|
||||
self.assertIn(f"ناشناخته در JSON: {EXPECTED_UNKNOWN}", text)
|
||||
self.assertIn(f"جنسیت مبهم/نامعتبر: {EXPECTED_AMBIGUOUS}", text)
|
||||
# نمونهٔ قبل/بعد نمایش داده میشود.
|
||||
self.assertIn("«محمد یاوری» → «آقای محمد یاوری»", text)
|
||||
|
||||
|
||||
|
||||
class ApplyTests(CommandTestMixin, TestCase):
|
||||
"""اجرای واقعی: گارد تأیید، تغییر تنها مشتریان واجد شرایط، اتمیک بودن."""
|
||||
|
||||
APPLY = {"apply": True, "confirm_production": True}
|
||||
|
||||
def test_apply_without_confirm_production_is_refused(self):
|
||||
with self.assertRaises(CommandError):
|
||||
self.run_command(apply=True)
|
||||
self.assertEqual(self.names(), [name for name, _ in CASES])
|
||||
self.assertEqual(CustomerNameFixJob.objects.count(), 0)
|
||||
|
||||
def test_apply_changes_only_eligible_names(self):
|
||||
self.run_command(batch_size=3, **self.APPLY)
|
||||
self.assertEqual(self.names(), self.expected_names())
|
||||
|
||||
job = CustomerNameFixJob.objects.get()
|
||||
self.assertEqual(job.status, CustomerNameFixJob.Status.COMPLETED)
|
||||
self.assertEqual(job.algorithm_version, ALGORITHM_VERSION)
|
||||
self.assertEqual(job.source_hash, sha256_of(self.json_path))
|
||||
self.assertEqual(job.initial_max_pk, self.max_pk())
|
||||
self.assertEqual(job.last_processed_pk, self.max_pk())
|
||||
self.assertEqual(job.stats["scanned"], len(CASES))
|
||||
self.assertEqual(job.stats["changed"], EXPECTED_CHANGED)
|
||||
self.assertEqual(job.stats["single_word"], EXPECTED_SINGLE_WORD)
|
||||
self.assertEqual(job.stats["has_prefix"], EXPECTED_HAS_PREFIX)
|
||||
self.assertEqual(job.stats["unknown_name"], EXPECTED_UNKNOWN)
|
||||
self.assertEqual(job.stats["ambiguous_gender"], EXPECTED_AMBIGUOUS)
|
||||
self.assertEqual(job.stats["batches"], 5) # ۱۴ مشتری، batch=3
|
||||
|
||||
def test_other_customer_fields_and_json_file_remain_untouched(self):
|
||||
target = self.customers[0]
|
||||
Customer.objects.filter(pk=target.pk).update(
|
||||
address="تهران، خیابان تست", phone1="09120000000",
|
||||
notes="یادداشت مهم", is_active=False)
|
||||
before_hash = sha256_of(self.json_path)
|
||||
|
||||
self.run_command(**self.APPLY)
|
||||
|
||||
target.refresh_from_db()
|
||||
self.assertEqual(target.name_family, "آقای محمد یاوری")
|
||||
self.assertEqual(target.address, "تهران، خیابان تست")
|
||||
self.assertEqual(target.phone1, "09120000000")
|
||||
self.assertEqual(target.notes, "یادداشت مهم")
|
||||
self.assertFalse(target.is_active)
|
||||
self.assertEqual(sha256_of(self.json_path), before_hash)
|
||||
|
||||
def test_customer_model_has_no_gender_field(self):
|
||||
# فقط فیلدهای واقعی ستوندار مدل (بدون روابط معکوس).
|
||||
field_names = {f.name for f in Customer._meta.concrete_fields}
|
||||
self.assertEqual(field_names, {
|
||||
"id", "name_family", "address", "notes", "phone1", "phone2",
|
||||
"phone3", "idCode", "installDate", "create_date", "last_bazdid_dt",
|
||||
"laghv_bazdid", "is_active", "sms_primary_phones",
|
||||
})
|
||||
self.assertNotIn("gender", field_names)
|
||||
|
||||
def test_rerun_of_completed_job_requires_new_job(self):
|
||||
self.run_command(**self.APPLY)
|
||||
with self.assertRaises(CommandError):
|
||||
self.run_command(**self.APPLY)
|
||||
self.assertEqual(self.names(), self.expected_names())
|
||||
self.assertEqual(CustomerNameFixJob.objects.count(), 1)
|
||||
|
||||
def test_new_job_rerun_is_idempotent(self):
|
||||
self.run_command(**self.APPLY)
|
||||
self.run_command(new_job=True, **self.APPLY)
|
||||
# پیشوندهای موجود تکرار نشدهاند.
|
||||
self.assertEqual(self.names(), self.expected_names())
|
||||
self.assertEqual(CustomerNameFixJob.objects.count(), 2)
|
||||
self.assertEqual(CustomerNameFixJob.objects.latest("pk").stats["changed"], 0)
|
||||
|
||||
def test_missing_json_path_is_reported(self):
|
||||
with self.assertRaises(CommandError):
|
||||
self.run_command(json_path=str(Path(self.tmpdir) / "nope.json"),
|
||||
apply=True, confirm_production=True)
|
||||
self.assertEqual(self.names(), [name for name, _ in CASES])
|
||||
|
||||
def test_invalid_dataset_aborts_before_any_change(self):
|
||||
bad = Path(self.tmpdir) / "bad.json"
|
||||
bad.write_text("{oops", encoding="utf-8")
|
||||
with self.assertRaises(CommandError):
|
||||
self.run_command(json_path=str(bad), apply=True, confirm_production=True)
|
||||
self.assertEqual(self.names(), [name for name, _ in CASES])
|
||||
self.assertEqual(CustomerNameFixJob.objects.count(), 0)
|
||||
|
||||
def test_missing_job_table_is_reported(self):
|
||||
with mock.patch.object(connections["default"].introspection,
|
||||
"table_names", return_value=[]):
|
||||
with self.assertRaises(CommandError):
|
||||
self.run_command(**self.APPLY)
|
||||
self.assertEqual(self.names(), [name for name, _ in CASES])
|
||||
|
||||
|
||||
|
||||
class ResilienceTests(CommandTestMixin, TestCase):
|
||||
"""قفل، بازیابی پس از crash، hash، اتمیک بودن و retry قفل دیتابیس."""
|
||||
|
||||
APPLY = {"apply": True, "confirm_production": True}
|
||||
|
||||
def _job(self, **overrides):
|
||||
values = {
|
||||
"source_hash": sha256_of(self.json_path),
|
||||
"algorithm_version": ALGORITHM_VERSION,
|
||||
"status": CustomerNameFixJob.Status.PAUSED,
|
||||
"batch_size": 3,
|
||||
"initial_max_pk": self.max_pk(),
|
||||
"last_processed_pk": 0,
|
||||
"stats": {},
|
||||
"heartbeat_at": timezone.now(),
|
||||
}
|
||||
values.update(overrides)
|
||||
return CustomerNameFixJob.objects.create(**values)
|
||||
|
||||
def test_concurrent_run_is_blocked(self):
|
||||
self._job(status=CustomerNameFixJob.Status.RUNNING,
|
||||
heartbeat_at=timezone.now())
|
||||
with self.assertRaises(CommandError):
|
||||
self.run_command(**self.APPLY)
|
||||
self.assertEqual(self.names(), [name for name, _ in CASES])
|
||||
|
||||
def test_stale_running_lock_is_recovered_and_resumed(self):
|
||||
stale = self._job(status=CustomerNameFixJob.Status.RUNNING,
|
||||
heartbeat_at=timezone.now() - LOCK_TTL - timedelta(minutes=1))
|
||||
self.run_command(**self.APPLY)
|
||||
stale.refresh_from_db()
|
||||
self.assertEqual(stale.status, CustomerNameFixJob.Status.COMPLETED)
|
||||
self.assertEqual(self.names(), self.expected_names())
|
||||
|
||||
def test_resume_continues_from_checkpoint(self):
|
||||
first_pk = self.customers[0].pk
|
||||
job = self._job(last_processed_pk=first_pk)
|
||||
self.run_command(resume=job.pk, **self.APPLY)
|
||||
|
||||
names = self.names()
|
||||
# مشتری قبل از checkpoint دستنخورده میماند
|
||||
self.assertEqual(names[0], "محمد یاوری")
|
||||
self.assertEqual(names[1:], self.expected_names()[1:])
|
||||
job.refresh_from_db()
|
||||
self.assertEqual(job.status, CustomerNameFixJob.Status.COMPLETED)
|
||||
self.assertEqual(job.last_processed_pk, self.max_pk())
|
||||
self.assertEqual(job.stats["scanned"], len(CASES) - 1)
|
||||
|
||||
def test_resume_with_different_hash_is_refused(self):
|
||||
job = self._job(source_hash="0" * 64)
|
||||
with self.assertRaises(CommandError):
|
||||
self.run_command(resume=job.pk, **self.APPLY)
|
||||
self.assertEqual(self.names(), [name for name, _ in CASES])
|
||||
job.refresh_from_db()
|
||||
self.assertEqual(job.status, CustomerNameFixJob.Status.PAUSED)
|
||||
|
||||
def test_unknown_resume_id_is_refused(self):
|
||||
with self.assertRaises(CommandError):
|
||||
self.run_command(resume=999999, **self.APPLY)
|
||||
self.assertEqual(self.names(), [name for name, _ in CASES])
|
||||
|
||||
def test_batch_failure_rolls_back_names_and_checkpoint_atomically(self):
|
||||
original = QuerySet.bulk_update
|
||||
calls = itertools.count(1)
|
||||
|
||||
def flaky(self, objs, fields, batch_size=None):
|
||||
if next(calls) == 2:
|
||||
raise RuntimeError("boom")
|
||||
return original(self, objs, fields, batch_size)
|
||||
|
||||
with mock.patch.object(QuerySet, "bulk_update", flaky):
|
||||
with self.assertRaises(CommandError):
|
||||
self.run_command(batch_size=1, **self.APPLY)
|
||||
|
||||
names = self.names()
|
||||
# batch اول (مشتری ۱) commit شده، batch دوم (مشتری ۲) rollback شده است.
|
||||
self.assertEqual(names[0], "آقای محمد یاوری")
|
||||
self.assertEqual(names[1], "مریم احمدی")
|
||||
|
||||
job = CustomerNameFixJob.objects.get()
|
||||
self.assertEqual(job.status, CustomerNameFixJob.Status.FAILED)
|
||||
# checkpoint دقیقاً روی پایان batch اول است ⇒ ادامه بدون تکرار/پرت شدن داده.
|
||||
self.assertEqual(job.last_processed_pk, self.customers[0].pk)
|
||||
|
||||
def test_locked_database_is_retried_then_succeeds(self):
|
||||
original = QuerySet.bulk_update
|
||||
calls = itertools.count(1)
|
||||
|
||||
def flaky(self, objs, fields, batch_size=None):
|
||||
if next(calls) == 1:
|
||||
raise OperationalError("database is locked")
|
||||
return original(self, objs, fields, batch_size)
|
||||
|
||||
with mock.patch.object(QuerySet, "bulk_update", flaky), \
|
||||
mock.patch("admin_panel.management.commands.fix_customer_names.time.sleep"):
|
||||
self.run_command(**self.APPLY)
|
||||
|
||||
self.assertEqual(self.names(), self.expected_names())
|
||||
self.assertEqual(CustomerNameFixJob.objects.get().status,
|
||||
CustomerNameFixJob.Status.COMPLETED)
|
||||
|
||||
def test_retry_is_bounded_and_surfaces_error(self):
|
||||
calls = itertools.count(1)
|
||||
attempts = []
|
||||
|
||||
def always_locked(self, objs, fields, batch_size=None):
|
||||
attempts.append(next(calls))
|
||||
raise OperationalError("database is locked")
|
||||
|
||||
with mock.patch.object(QuerySet, "bulk_update", always_locked), \
|
||||
mock.patch("admin_panel.management.commands.fix_customer_names.time.sleep"):
|
||||
with self.assertRaises(CommandError):
|
||||
self.run_command(**self.APPLY)
|
||||
|
||||
self.assertEqual(len(attempts), MAX_DB_RETRIES) # retry نامحدود نیست
|
||||
self.assertEqual(self.names(), [name for name, _ in CASES])
|
||||
self.assertEqual(CustomerNameFixJob.objects.get().status,
|
||||
CustomerNameFixJob.Status.FAILED)
|
||||
|
||||
def test_batches_cover_all_customers_across_sizes(self):
|
||||
for batch_size in (1, 2, 5, 200):
|
||||
with self.subTest(batch_size=batch_size):
|
||||
CustomerNameFixJob.objects.all().delete()
|
||||
for customer, (name, _) in zip(self.customers, CASES):
|
||||
Customer.objects.filter(pk=customer.pk).update(name_family=name)
|
||||
self.run_command(batch_size=batch_size, new_job=True, **self.APPLY)
|
||||
self.assertEqual(self.names(), self.expected_names())
|
||||
|
||||
Reference in new issue
Block a user