测试:封闭菜单补丁原子性回归
This commit is contained in:
@@ -924,6 +924,30 @@ def _assert_business_permission_inventory(test_case: unittest.TestCase, sql: str
|
|||||||
test_case.assertEqual(Counter(EXPECTED_PERMISSIONS), actual_permissions)
|
test_case.assertEqual(Counter(EXPECTED_PERMISSIONS), actual_permissions)
|
||||||
|
|
||||||
|
|
||||||
|
MENU_PROMOTION_LOCK_STATEMENT = (
|
||||||
|
'select menu_id from sys_menu where menu_id in (1, 2, 3, 121, 122, 123) order by menu_id for update'
|
||||||
|
)
|
||||||
|
MENU_PROMOTION_EXPECTED_UPDATE = _normalized_sql_statement(
|
||||||
|
"""
|
||||||
|
update sys_menu
|
||||||
|
set parent_id = case
|
||||||
|
when menu_id in (121, 122, 123) then 0
|
||||||
|
else parent_id
|
||||||
|
end,
|
||||||
|
order_num = case menu_id
|
||||||
|
when 121 then 1
|
||||||
|
when 122 then 2
|
||||||
|
when 123 then 3
|
||||||
|
when 1 then 4
|
||||||
|
when 2 then 5
|
||||||
|
when 3 then 6
|
||||||
|
else order_num
|
||||||
|
end
|
||||||
|
where menu_id in (1, 2, 3, 121, 122, 123)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _assert_menu_promotion_guard_structure(test_case: unittest.TestCase, sql: str) -> tuple[str, str]:
|
def _assert_menu_promotion_guard_structure(test_case: unittest.TestCase, sql: str) -> tuple[str, str]:
|
||||||
normalized = [_normalized_sql_statement(statement) for statement in _sql_statements(sql)]
|
normalized = [_normalized_sql_statement(statement) for statement in _sql_statements(sql)]
|
||||||
guards = [
|
guards = [
|
||||||
@@ -931,6 +955,29 @@ def _assert_menu_promotion_guard_structure(test_case: unittest.TestCase, sql: st
|
|||||||
]
|
]
|
||||||
test_case.assertEqual(2, len(guards))
|
test_case.assertEqual(2, len(guards))
|
||||||
pre_guard, post_guard = guards
|
pre_guard, post_guard = guards
|
||||||
|
identity_fields = (
|
||||||
|
('menu_name', True),
|
||||||
|
('path', True),
|
||||||
|
('component', True),
|
||||||
|
('query', True),
|
||||||
|
('route_name', True),
|
||||||
|
('is_frame', False),
|
||||||
|
('is_cache', False),
|
||||||
|
('menu_type', True),
|
||||||
|
('visible', True),
|
||||||
|
('status', True),
|
||||||
|
('perms', True),
|
||||||
|
('icon', True),
|
||||||
|
)
|
||||||
|
identity_comparisons = [
|
||||||
|
(
|
||||||
|
rf'\(binary\s+actual\.{field}\s*<=>\s*binary\s+expected\.{field}\)'
|
||||||
|
if binary
|
||||||
|
else rf'\(actual\.{field}\s*<=>\s*expected\.{field}\)'
|
||||||
|
)
|
||||||
|
for field, binary in identity_fields
|
||||||
|
]
|
||||||
|
identity_aggregate = r'sum\s*\(\s*' + r'\s+and\s+'.join(identity_comparisons) + r'\s*\)\s*=\s*6'
|
||||||
for guard, phase in ((pre_guard, 'precondition'), (post_guard, 'postcondition')):
|
for guard, phase in ((pre_guard, 'precondition'), (post_guard, 'postcondition')):
|
||||||
conflict_token = f"'promote-business-menus-{phase}-conflict'"
|
conflict_token = f"'promote-business-menus-{phase}-conflict'"
|
||||||
test_case.assertEqual(2, guard.count(conflict_token))
|
test_case.assertEqual(2, guard.count(conflict_token))
|
||||||
@@ -945,21 +992,7 @@ def _assert_menu_promotion_guard_structure(test_case: unittest.TestCase, sql: st
|
|||||||
expected_join = 'left join sys_menu as actual on actual.menu_id = expected.menu_id'
|
expected_join = 'left join sys_menu as actual on actual.menu_id = expected.menu_id'
|
||||||
test_case.assertEqual(1, guard.count(expected_join))
|
test_case.assertEqual(1, guard.count(expected_join))
|
||||||
test_case.assertIn('count(actual.menu_id) = 6', guard)
|
test_case.assertIn('count(actual.menu_id) = 6', guard)
|
||||||
for field in (
|
test_case.assertRegex(guard, identity_aggregate)
|
||||||
'menu_name',
|
|
||||||
'path',
|
|
||||||
'component',
|
|
||||||
'query',
|
|
||||||
'route_name',
|
|
||||||
'menu_type',
|
|
||||||
'visible',
|
|
||||||
'status',
|
|
||||||
'perms',
|
|
||||||
'icon',
|
|
||||||
):
|
|
||||||
test_case.assertIn(f'binary actual.{field} <=> binary expected.{field}', guard)
|
|
||||||
for field in ('is_frame', 'is_cache'):
|
|
||||||
test_case.assertIn(f'actual.{field} <=> expected.{field}', guard)
|
|
||||||
|
|
||||||
old_state = (
|
old_state = (
|
||||||
r'sum\(\s*\(actual\.parent_id <=> expected\.old_parent_id\)\s*'
|
r'sum\(\s*\(actual\.parent_id <=> expected\.old_parent_id\)\s*'
|
||||||
@@ -986,6 +1019,31 @@ def _assert_menu_promotion_guard_structure(test_case: unittest.TestCase, sql: st
|
|||||||
return pre_guard, post_guard
|
return pre_guard, post_guard
|
||||||
|
|
||||||
|
|
||||||
|
def _assert_menu_promotion_transaction_body(test_case: unittest.TestCase, sql: str) -> None:
|
||||||
|
normalized = [_normalized_sql_statement(statement) for statement in _sql_statements(sql)]
|
||||||
|
pre_guard, post_guard = _assert_menu_promotion_guard_structure(test_case, sql)
|
||||||
|
|
||||||
|
def unique_index(statement: str) -> int:
|
||||||
|
matches = [index for index, actual in enumerate(normalized) if actual == statement]
|
||||||
|
test_case.assertEqual(1, len(matches), statement)
|
||||||
|
return matches[0]
|
||||||
|
|
||||||
|
start_index = unique_index('start transaction')
|
||||||
|
lock_index = unique_index(MENU_PROMOTION_LOCK_STATEMENT)
|
||||||
|
pre_guard_index = unique_index(pre_guard)
|
||||||
|
update_index = unique_index(MENU_PROMOTION_EXPECTED_UPDATE)
|
||||||
|
post_guard_index = unique_index(post_guard)
|
||||||
|
commit_index = unique_index('commit')
|
||||||
|
test_case.assertEqual(
|
||||||
|
sorted((start_index, lock_index, pre_guard_index, update_index, post_guard_index, commit_index)),
|
||||||
|
[start_index, lock_index, pre_guard_index, update_index, post_guard_index, commit_index],
|
||||||
|
)
|
||||||
|
test_case.assertEqual(
|
||||||
|
[MENU_PROMOTION_LOCK_STATEMENT, pre_guard, MENU_PROMOTION_EXPECTED_UPDATE, post_guard],
|
||||||
|
normalized[start_index + 1 : commit_index],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SqlContractTest(unittest.TestCase):
|
class SqlContractTest(unittest.TestCase):
|
||||||
def test_initializers_define_exact_business_table_set(self) -> None:
|
def test_initializers_define_exact_business_table_set(self) -> None:
|
||||||
for filename in INITIALIZERS:
|
for filename in INITIALIZERS:
|
||||||
@@ -1360,10 +1418,10 @@ class SqlContractTest(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
lock_statement = (
|
lock_index = unique_index(
|
||||||
'select menu_id from sys_menu where menu_id in (1, 2, 3, 121, 122, 123) order by menu_id for update'
|
lambda statement: statement == MENU_PROMOTION_LOCK_STATEMENT,
|
||||||
|
'six-row locking read',
|
||||||
)
|
)
|
||||||
lock_index = unique_index(lambda statement: statement == lock_statement, 'six-row locking read')
|
|
||||||
|
|
||||||
pre_guard, post_guard = _assert_menu_promotion_guard_structure(self, raw)
|
pre_guard, post_guard = _assert_menu_promotion_guard_structure(self, raw)
|
||||||
guard_indexes = [
|
guard_indexes = [
|
||||||
@@ -1375,26 +1433,12 @@ class SqlContractTest(unittest.TestCase):
|
|||||||
pre_guard_index, post_guard_index = guard_indexes
|
pre_guard_index, post_guard_index = guard_indexes
|
||||||
self.assertEqual((pre_guard, post_guard), tuple(normalized[index] for index in guard_indexes))
|
self.assertEqual((pre_guard, post_guard), tuple(normalized[index] for index in guard_indexes))
|
||||||
|
|
||||||
expected_update = _normalized_sql_statement(
|
expected_update = MENU_PROMOTION_EXPECTED_UPDATE
|
||||||
"""
|
_assert_menu_promotion_transaction_body(self, raw)
|
||||||
update sys_menu
|
update_index = unique_index(
|
||||||
set parent_id = case
|
lambda statement: statement == expected_update,
|
||||||
when menu_id in (121, 122, 123) then 0
|
'exact permanent update',
|
||||||
else parent_id
|
|
||||||
end,
|
|
||||||
order_num = case menu_id
|
|
||||||
when 121 then 1
|
|
||||||
when 122 then 2
|
|
||||||
when 123 then 3
|
|
||||||
when 1 then 4
|
|
||||||
when 2 then 5
|
|
||||||
when 3 then 6
|
|
||||||
else order_num
|
|
||||||
end
|
|
||||||
where menu_id in (1, 2, 3, 121, 122, 123)
|
|
||||||
"""
|
|
||||||
)
|
)
|
||||||
update_index = unique_index(lambda statement: statement == expected_update, 'exact permanent update')
|
|
||||||
permanent_dml = []
|
permanent_dml = []
|
||||||
for statement in normalized:
|
for statement in normalized:
|
||||||
if not re.match(r'^(?:insert|update|delete|replace|truncate)\b', statement):
|
if not re.match(r'^(?:insert|update|delete|replace|truncate)\b', statement):
|
||||||
@@ -1456,6 +1500,32 @@ class SqlContractTest(unittest.TestCase):
|
|||||||
with self.assertRaises(AssertionError):
|
with self.assertRaises(AssertionError):
|
||||||
_assert_menu_promotion_guard_structure(self, mutated)
|
_assert_menu_promotion_guard_structure(self, mutated)
|
||||||
|
|
||||||
|
def test_menu_promotion_contract_rejects_identity_threshold_and_transaction_body_mutations(self) -> None:
|
||||||
|
raw = MENU_PROMOTION_PATCH_PATH.read_text(encoding='utf-8')
|
||||||
|
identity_threshold_mutation, replacements = re.subn(
|
||||||
|
r'(\(binary\s+actual\.icon\s+<=>\s+binary\s+expected\.icon\)\s*\)\s*)=\s*6',
|
||||||
|
r'\1>= 0',
|
||||||
|
raw,
|
||||||
|
count=1,
|
||||||
|
flags=re.I,
|
||||||
|
)
|
||||||
|
with self.subTest(mutation='identity_threshold'):
|
||||||
|
self.assertEqual(1, replacements)
|
||||||
|
with self.assertRaises(AssertionError):
|
||||||
|
_assert_menu_promotion_guard_structure(self, identity_threshold_mutation)
|
||||||
|
|
||||||
|
transaction_ddl_mutation, replacements = re.subn(
|
||||||
|
r'(start\s+transaction\s*;)',
|
||||||
|
r'\1\nalter table sys_menu engine=innodb;',
|
||||||
|
raw,
|
||||||
|
count=1,
|
||||||
|
flags=re.I,
|
||||||
|
)
|
||||||
|
with self.subTest(mutation='transaction_ddl'):
|
||||||
|
self.assertEqual(1, replacements)
|
||||||
|
with self.assertRaises(AssertionError):
|
||||||
|
_assert_menu_promotion_transaction_body(self, transaction_ddl_mutation)
|
||||||
|
|
||||||
def test_patch_exists_and_preserves_ai_business_tables(self) -> None:
|
def test_patch_exists_and_preserves_ai_business_tables(self) -> None:
|
||||||
self.assertTrue(PATCH_PATH.is_file(), f'missing patch: {PATCH_PATH}')
|
self.assertTrue(PATCH_PATH.is_file(), f'missing patch: {PATCH_PATH}')
|
||||||
raw = PATCH_PATH.read_text(encoding='utf-8').lower()
|
raw = PATCH_PATH.read_text(encoding='utf-8').lower()
|
||||||
|
|||||||
Reference in New Issue
Block a user