127 lines
5.7 KiB
Python
127 lines
5.7 KiB
Python
"""build_db_input 事务调度感知种子测试(JIN05UPD 根因)。
|
||
|
||
复现根因:UPDATE/DELETE 种子对每个 sql_ok 路径都用该路径记录的主键建行,
|
||
不区分记录运行时是否真的执行 UPDATE。JIN05UPD 的 4 条 INSERT 记录主键因此
|
||
全部被预置进 EMPLOYEE → 运行时 INSERT 全部 -803,INSERT 成功分支不可达。
|
||
另外:非 sql_ok INSERT 路径的记录可能不是 INSERT 类(运行时走 WHEN OTHER),
|
||
碰撞行必须回退到真实 INSERT 记录的主键。
|
||
|
||
修复:按 88 级事务类型(INSERT/UPDATE/DELETE 语义标记)沿 MOVE 链追溯调度
|
||
字段,分类每条记录;UPDATE/DELETE 种子仅对 UPDATE/DELETE 记录;INSERT 碰撞
|
||
回退到首条 INSERT 类记录。
|
||
"""
|
||
|
||
import sys
|
||
import os
|
||
|
||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||
|
||
import pytest
|
||
|
||
from cobol_testgen.to_sql import build_db_input
|
||
|
||
|
||
def _fields():
|
||
def f(name, level, pic, ftype, length):
|
||
return {'name': name, 'level': level, 'pic': pic,
|
||
'pic_info': {'type': ftype, 'digits': 0, 'decimal': 0,
|
||
'length': length, 'signed': False},
|
||
'section': 'WS', 'is_filler': False, 'redefines': None,
|
||
'usage': 'DISPLAY', 'occurs': 0, 'occurs_depending': None,
|
||
'value': None, 'values': None}
|
||
base = [
|
||
f('WRK-TRAN-TYPE', 3, 'X(001)', 'alphanumeric', 1),
|
||
f('R01TRAN-TYPE', 3, 'X(001)', 'alphanumeric', 1),
|
||
f('R01TRAN-EMP-ID', 3, 'X(008)', 'alphanumeric', 8),
|
||
f('HV-EMP-ID', 3, 'X(008)', 'alphanumeric', 8),
|
||
]
|
||
for name, parent, vals in [('WRK-TRAN-INSERT', 'WRK-TRAN-TYPE', ['A', 'B', 'C']),
|
||
('WRK-TRAN-UPDATE', 'WRK-TRAN-TYPE', ['D', 'E', 'F', 'G']),
|
||
('WRK-TRAN-DELETE', 'WRK-TRAN-TYPE', ['Z'])]:
|
||
e = f(name, 88, None, 'alphanumeric', 1)
|
||
e['is_88'] = True
|
||
e['parent'] = parent
|
||
e['values'] = vals
|
||
e['value'] = vals[0]
|
||
base.append(e)
|
||
return base
|
||
|
||
|
||
_ASSIGN = {
|
||
'WRK-TRAN-TYPE': [{'type': 'move', 'source_vars': ['R01TRAN-TYPE']}],
|
||
'HV-EMP-ID': [{'type': 'move', 'source_vars': ['R01TRAN-EMP-ID']}],
|
||
}
|
||
|
||
|
||
def _declared():
|
||
return {'EMPLOYEE': [{'name': 'EMP_ID', 'db_type': 'CHAR', 'size': 8}]}
|
||
|
||
|
||
def _sql_meta():
|
||
return [
|
||
{'type': 'exec_sql_insert', 'table': 'EMPLOYEE',
|
||
'columns': ['EMP_ID'], 'host_vars': ['HV-EMP-ID'],
|
||
'sql_text': 'INSERT INTO EMPLOYEE (EMP_ID) VALUES (:HV-EMP-ID)'},
|
||
{'type': 'exec_sql_update', 'table': 'EMPLOYEE',
|
||
'set_clause': 'STATUS = :HV-STATUS',
|
||
'where': 'EMP_ID = :HV-EMP-ID', 'host_vars': ['HV-EMP-ID', 'HV-STATUS'],
|
||
'where_constraints': [{'col': 'EMP_ID', 'type': 'host_var', 'host_var': 'HV-EMP-ID'}],
|
||
'sql_text': 'UPDATE EMPLOYEE SET STATUS = :HV-STATUS WHERE EMP_ID = :HV-EMP-ID'},
|
||
]
|
||
|
||
|
||
def _records():
|
||
return [
|
||
{'R01TRAN-TYPE': 'A', 'R01TRAN-EMP-ID': 'B0000001', 'HV-EMP-ID': 'B0000001'}, # INSERT
|
||
{'R01TRAN-TYPE': 'D', 'R01TRAN-EMP-ID': 'B0000005', 'HV-EMP-ID': 'B0000005'}, # UPDATE
|
||
{'R01TRAN-TYPE': 'H', 'R01TRAN-EMP-ID': 'B0000008', 'HV-EMP-ID': 'B0000008'}, # OTHER
|
||
{'R01TRAN-TYPE': 'I', 'R01TRAN-EMP-ID': 'B0000009', 'HV-EMP-ID': 'B0000009'}, # OTHER (非sql_ok INSERT 路径)
|
||
]
|
||
|
||
|
||
def _paths():
|
||
return [
|
||
([], {}), # path0 sql_ok → rec0 (INSERT)
|
||
([], {}), # path1 sql_ok → rec1 (UPDATE)
|
||
([], {}), # path2 sql_ok → rec2 (OTHER)
|
||
([('SQLCODE', '<>', '0', True)], {}), # path3 INSERT 错误 → rec3 (OTHER)
|
||
]
|
||
|
||
|
||
_INSERT_PK = {'EMPLOYEE': ['EMP_ID']}
|
||
|
||
|
||
def test_update_seed_only_for_update_records():
|
||
"""INSERT/OTHER 记录不应被 UPDATE 种子注入;OTHER 记录主键不得入表。
|
||
|
||
B0000001 允许作为 INSERT 碰撞行出现一次(供 -803 分支),但不得因
|
||
UPDATE 种子而额外注入;B0000008/B0000009(OTHER 记录)绝不能入表。
|
||
"""
|
||
di = build_db_input(_paths(), _fields(), _ASSIGN, _sql_meta(), _declared(),
|
||
records=_records(), insert_pk=_INSERT_PK)
|
||
keys = [r.get('EMP_ID', '').strip() for r in di.get('EMPLOYEE', [])]
|
||
assert keys.count('B0000001') <= 1, 'INSERT 记录主键仅允许作为碰撞行出现一次'
|
||
assert 'B0000005' in keys, 'UPDATE 记录主键应被种子注入'
|
||
assert 'B0000008' not in keys, 'OTHER 记录主键不应被种子注入'
|
||
assert 'B0000009' not in keys, '非 sql_ok OTHER 记录主键不应被 UPDATE 种子注入'
|
||
|
||
|
||
def test_insert_collision_falls_back_to_insert_record():
|
||
"""非 sql_ok INSERT 路径的记录若非 INSERT 类,碰撞行应回退到首条 INSERT 类记录主键。"""
|
||
di = build_db_input(_paths(), _fields(), _ASSIGN, _sql_meta(), _declared(),
|
||
records=_records(), insert_pk=_INSERT_PK)
|
||
keys = [r.get('EMP_ID', '').strip() for r in di.get('EMPLOYEE', [])]
|
||
assert 'B0000001' in keys, f'碰撞行应回退到首条 INSERT 记录主键, 实际 {keys}'
|
||
|
||
|
||
def test_insert_success_path_skips_seed():
|
||
"""sql_ok INSERT 路径不应生成任何种子行(INSERT 成功需主键空闲)。"""
|
||
paths = [
|
||
([('SQLCODE', '=', '0', True)], {}), # INSERT 成功
|
||
]
|
||
recs = [{'R01TRAN-TYPE': 'A', 'R01TRAN-EMP-ID': 'B0000001', 'HV-EMP-ID': 'B0000001'}]
|
||
di = build_db_input(paths, _fields(), _ASSIGN, _sql_meta(), _declared(),
|
||
records=recs, insert_pk=_INSERT_PK)
|
||
keys = [r.get('EMP_ID', '').strip() for r in di.get('EMPLOYEE', [])]
|
||
assert 'B0000001' not in keys, f'INSERT 成功路径不应生成种子, 实际 {keys}'
|