factory/apps/inm/tests.py

184 lines
6.3 KiB
Python

from concurrent.futures import ThreadPoolExecutor
from decimal import Decimal
from threading import Barrier
from unittest import skipUnless
from django.db import connection, connections, transaction
from django.test import SimpleTestCase, TestCase, TransactionTestCase
from apps.inm.filters import MaterialBatchFilter
from apps.inm.models import MaterialBatch, WareHouse
from apps.inm.serializers import MaterialBatchSerializer
from apps.mtm.models import Material
from apps.qm.models import Defect
class MaterialBatchDefectGradeTests(TestCase):
@classmethod
def setUpTestData(cls):
cls.material = Material.objects.create(name='仓库缺陷等级测试物料')
cls.warehouse = WareHouse.objects.create(
number='GRADE',
name='等级测试仓库',
place='测试地点',
)
cls.defect_b = Defect.objects.create(
name='仓库B类缺陷',
cate=Defect.cate_list[0],
okcate=Defect.DEFECT_OK_B,
)
cls.notok_without_defect = MaterialBatch.objects.create(
material=cls.material,
warehouse=cls.warehouse,
batch='MB-NOTOK-NONE',
count=1,
state=20,
)
cls.normal_with_b_defect = MaterialBatch.objects.create(
material=cls.material,
warehouse=cls.warehouse,
batch='MB-NORMAL-B',
count=1,
state=10,
defect=cls.defect_b,
)
def test_serializer_uses_defect_or_defaults_to_ok_independent_of_state(self):
no_defect_data = MaterialBatchSerializer(
self.notok_without_defect
).data
b_defect_data = MaterialBatchSerializer(
self.normal_with_b_defect
).data
self.assertEqual(no_defect_data['defect_grade'], Defect.DEFECT_OK)
self.assertEqual(no_defect_data['defect_grade_name'], '合格')
self.assertEqual(b_defect_data['defect_grade'], Defect.DEFECT_OK_B)
self.assertEqual(b_defect_data['defect_grade_name'], '合格B类')
def test_effective_grade_filter_is_independent_of_state(self):
ok_items = MaterialBatchFilter(
{'defect_grade': Defect.DEFECT_OK},
queryset=MaterialBatch.objects.all(),
).qs
b_items = MaterialBatchFilter(
{'defect_grade': Defect.DEFECT_OK_B},
queryset=MaterialBatch.objects.all(),
).qs
self.assertQuerySetEqual(
ok_items,
[self.notok_without_defect],
transform=lambda item: item,
)
self.assertQuerySetEqual(
b_items,
[self.normal_with_b_defect],
transform=lambda item: item,
)
class MaterialBatchInventoryKeyTests(SimpleTestCase):
def setUp(self):
self.material = Material(id='100', name='测试物料')
self.warehouse = WareHouse(
id='200',
number='TEST',
name='测试仓库',
place='测试地点',
)
def test_inventory_key_normalizes_omitted_optional_fields(self):
omitted = MaterialBatch._normalize_inventory_lookup(
material=self.material,
batch='BATCH-001',
warehouse=self.warehouse,
)
explicit = MaterialBatch._normalize_inventory_lookup(
material=self.material,
batch='BATCH-001',
warehouse=self.warehouse,
state=10,
defect=None,
)
self.assertEqual(omitted, explicit)
self.assertEqual(
MaterialBatch._inventory_advisory_lock_payload(omitted),
MaterialBatch._inventory_advisory_lock_payload(explicit),
)
def test_inventory_key_requires_material_batch_and_warehouse(self):
required_values = {
'material': self.material,
'batch': 'BATCH-001',
'warehouse': self.warehouse,
}
for field in required_values:
with self.subTest(field=field):
lookup = required_values.copy()
lookup[field] = None
with self.assertRaisesRegex(ValueError, field):
MaterialBatch._normalize_inventory_lookup(**lookup)
def test_inventory_key_rejects_unknown_fields(self):
with self.assertRaisesRegex(TypeError, 'supplier'):
MaterialBatch._normalize_inventory_lookup(
material=self.material,
batch='BATCH-001',
warehouse=self.warehouse,
supplier=None,
)
@skipUnless(
connection.vendor == 'postgresql',
'advisory lock concurrency test requires PostgreSQL',
)
class MaterialBatchConcurrencyTests(TransactionTestCase):
def test_concurrent_first_create_uses_one_inventory_record(self):
material = Material.objects.create(name='仓库并发测试物料')
warehouse = WareHouse.objects.create(
number='CONCURRENT',
name='并发测试仓库',
place='测试地点',
)
barrier = Barrier(2)
def create_inventory():
connections.close_all()
try:
barrier.wait()
with transaction.atomic():
mb, created = (
MaterialBatch.locked_get_or_create_inventory(
material=material,
batch='CONCURRENT-001',
warehouse=warehouse,
defaults={'count': Decimal('0')},
)
)
mb.count += Decimal('1')
mb.save(update_fields=['count'])
return created
finally:
connections.close_all()
with ThreadPoolExecutor(max_workers=2) as executor:
created_results = list(executor.map(
lambda _: create_inventory(),
range(2),
))
queryset = MaterialBatch.objects.filter(
material=material,
batch='CONCURRENT-001',
warehouse=warehouse,
state=10,
defect=None,
)
self.assertEqual(queryset.count(), 1)
self.assertEqual(queryset.get().count, Decimal('2'))
self.assertCountEqual(created_results, [True, False])