"""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}'