test: cover between/hostvar/gcov-merge, class conditions, schema drop-tables, gixsql fixes

This commit is contained in:
hangshuo652
2026-08-09 17:43:16 +08:00
parent 273a3f8211
commit ff51bda962
22 changed files with 2559 additions and 0 deletions
@@ -0,0 +1,279 @@
"""SELECT 的 WHERE 宿主变量经 MOVE 链解析到输入记录键的测试。
根因:`MOVE R02EMP-ID TO DBV-EMP-ID` 在程序里先执行,运行时 SQL 查询键是
输入记录键(R02EMP-ID)。但 build_db_input 直接读记录中的 DBV-EMP-ID
(生成器的独立合成值,如 J 前缀),与输入键不一致,导致 DB 预置行查不到、
正常路径(SQLCODE=0)永远无法覆盖。修复:WHERE 宿主变量若有 MOVE 链,
应优先使用输入记录键。
"""
import sys, os
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")))
from cobol_testgen.to_sql import build_db_input
def _sql_meta_select(table="EMP-MASTER"):
return [{
"type": "exec_sql_select",
"table": table,
"select_list": "EMP_NAME",
"into_vars": ["DBV-EMPLOYEE-NAME"],
"where": "EMP_ID = :DBV-EMP-ID",
"where_constraints": [
{"col": "EMP-ID", "type": "host_var", "host_var": "DBV-EMP-ID", "op": "="}
],
}]
def _declared_columns():
return {
"EMP-MASTER": [
{"name": "EMP-ID", "db_type": "CHAR", "size": 8},
{"name": "EMP_NAME", "db_type": "CHAR", "size": 40},
],
}
def test_select_where_hostvar_moved_from_input_uses_input_key():
"""WHERE 宿主变量 DBV-EMP-ID 由 MOVE R02EMP-ID 赋值时,
DB 预置行主键必须用输入记录键(R02EMP-ID),而非 DBV-EMP-ID 的合成值。"""
branch_paths = [
([('SQLCODE', '=', '0', True)], {}), # SQL 成功路径
]
records = [{'R02EMP-ID': 'A0000001', 'DBV-EMP-ID': 'J0000001'}]
assignments = {
'DBV-EMP-ID': [{'type': 'move', 'source_vars': ['R02EMP-ID']}],
}
db_input = build_db_input(
branch_paths, [], assignments, _sql_meta_select(),
_declared_columns(),
records=records,
)
rows = db_input.get('EMP-MASTER', [])
assert len(rows) == 1
assert rows[0]['EMP-ID'].strip() == 'A0000001'
def test_select_where_hostvar_no_move_keeps_record_value():
"""WHERE 宿主变量没有 MOVE 链时,维持原行为(用记录中宿主变量的值)。"""
branch_paths = [
([('SQLCODE', '=', '0', True)], {}),
]
records = [{'R02EMP-ID': 'A0000001', 'DBV-EMP-ID': 'J0000001'}]
db_input = build_db_input(
branch_paths, [], {}, _sql_meta_select(),
_declared_columns(),
records=records,
)
rows = db_input.get('EMP-MASTER', [])
assert len(rows) == 1
assert rows[0]['EMP-ID'].strip() == 'J0000001'
def test_select_where_hostvar_chain_through_two_moves():
"""MOVE 链可跨多跳解析(A <- B <- C),最终用输入记录键。"""
branch_paths = [
([('SQLCODE', '=', '0', True)], {}),
]
records = [{'R02EMP-ID': 'A0000001', 'WK-EMP-ID': 'M0000001', 'DBV-EMP-ID': 'J0000001'}]
assignments = {
'WK-EMP-ID': [{'type': 'move', 'source_vars': ['R02EMP-ID']}],
'DBV-EMP-ID': [{'type': 'move', 'source_vars': ['WK-EMP-ID']}],
}
db_input = build_db_input(
branch_paths, [], assignments, _sql_meta_select(),
_declared_columns(),
records=records,
)
rows = db_input.get('EMP-MASTER', [])
assert len(rows) == 1
assert rows[0]['EMP-ID'].strip() == 'A0000001'
def test_select_where_hostvar_skip_sql_error_path():
"""SQL 失败路径(SQLCODE<>0)仍不生成预置行。"""
branch_paths = [
([('SQLCODE', '<>', '0', True)], {}),
]
records = [{'R02EMP-ID': 'A0000001', 'DBV-EMP-ID': 'J0000001'}]
assignments = {
'DBV-EMP-ID': [{'type': 'move', 'source_vars': ['R02EMP-ID']}],
}
db_input = build_db_input(
branch_paths, [], assignments, _sql_meta_select(),
_declared_columns(),
records=records,
)
rows = db_input.get('EMP-MASTER', [])
assert not rows
def test_select_uses_declared_columns_with_underscore_key():
"""declared_columns 以下划线键(EMP_MASTER)声明时,SELECT 行生成
也要命中,使用声明的 CHAR(8) 列宽,避免回退到 10 字符推断导致
存储值带尾随空格、与运行时 8 字符键失配。"""
branch_paths = [
([('SQLCODE', '=', '0', True)], {}),
]
records = [{'R02EMP-ID': 'A0000001', 'DBV-EMP-ID': 'A0000001'}]
assignments = {
'DBV-EMP-ID': [{'type': 'move', 'source_vars': ['R02EMP-ID']}],
}
declared = {
"EMP_MASTER": [
{"name": "EMP_ID", "db_type": "CHAR", "size": 8},
{"name": "EMP_NAME", "db_type": "CHAR", "size": 40},
],
}
db_input = build_db_input(
branch_paths, [], assignments, _sql_meta_select(),
declared,
records=records,
)
rows = db_input.get('EMP-MASTER', [])
assert len(rows) == 1
assert rows[0]['EMP_ID'] == 'A0000001'
assert len(rows[0]['EMP_ID']) == 8
def test_update_where_hostvar_moved_from_input_uses_input_key():
"""UPDATE/DELETE 的 WHERE 宿主变量同样经 MOVE 链解析到输入记录键。"""
sql_meta = [{
"type": "exec_sql_update",
"table": "EMP-MASTER",
"columns": ["STATUS"],
"host_vars": ["DBV-STATUS"],
"where": "EMP_ID = :DBV-EMP-ID",
"where_constraints": [
{"col": "EMP-ID", "type": "host_var", "host_var": "DBV-EMP-ID", "op": "="}
],
}]
branch_paths = [
([('SQLCODE', '=', '0', True)], {}),
]
records = [{'R02EMP-ID': 'A0000001', 'DBV-EMP-ID': 'J0000001', 'DBV-STATUS': 'M'}]
assignments = {
'DBV-EMP-ID': [{'type': 'move', 'source_vars': ['R02EMP-ID']}],
}
db_input = build_db_input(
branch_paths, [], assignments, sql_meta,
_declared_columns(),
records=records,
)
rows = db_input.get('EMP-MASTER', [])
assert len(rows) == 1
assert rows[0]['EMP-ID'].strip() == 'A0000001'
def _sql_meta_two_selects():
"""两个 SELECTEMP-MASTER 在前(运行时先查),OVT-MONTHLY 在后。"""
return [
{
"type": "exec_sql_select", "table": "EMP-MASTER",
"select_list": "EMP_NAME", "into_vars": ["DBV-EMPLOYEE-NAME"],
"where": "EMP_ID = :DBV-EMP-ID",
"where_constraints": [
{"col": "EMP-ID", "type": "host_var", "host_var": "DBV-EMP-ID", "op": "="}
],
"pos": 100,
},
{
"type": "exec_sql_select", "table": "OVT-MONTHLY",
"select_list": "SUM(OVT_HOURS)", "into_vars": ["DBV-OVT-HOURS"],
"where": "EMP_ID = :DBV-EMP-ID AND YEAR_MONTH = :WRK-YEAR-MONTH",
"where_constraints": [
{"col": "EMP-ID", "type": "host_var", "host_var": "DBV-EMP-ID", "op": "="},
{"col": "YEAR-MONTH", "type": "host_var", "host_var": "WRK-YEAR-MONTH", "op": "="},
],
"pos": 200,
},
]
def _declared_two_tables():
return {
"EMP-MASTER": [
{"name": "EMP-ID", "db_type": "CHAR", "size": 8},
{"name": "EMP_NAME", "db_type": "CHAR", "size": 40},
],
"OVT-MONTHLY": [
{"name": "EMP-ID", "db_type": "CHAR", "size": 8},
{"name": "YEAR-MONTH", "db_type": "CHAR", "size": 6},
],
}
def test_multiple_selects_drop_last_table_for_last_ok_path():
"""两个 SELECT 时,为最后一条 SQL 成功路径丢弃最后一张表(OVT)的行,
使该记录到达下游查询时无匹配行 → 覆盖下游 SELECT 的 no-data 分支
IF SQLCODE = 0 的 ELSE 分支)。"""
branch_paths = [
([('SQLCODE', '=', '0', True)], {}), # path0 sql ok
([('SQLCODE', '=', '0', True)], {}), # path1 sql ok (last)
]
records = [
{'R02EMP-ID': 'A0000001', 'DBV-EMP-ID': 'A0000001',
'WRK-YEAR-MONTH': 'B00001', 'DBV-OVT-HOURS': '00101'},
{'R02EMP-ID': 'A0000002', 'DBV-EMP-ID': 'A0000002',
'WRK-YEAR-MONTH': 'B00002', 'DBV-OVT-HOURS': '00102'},
]
assignments = {
'DBV-EMP-ID': [{'type': 'move', 'source_vars': ['R02EMP-ID']}],
}
db_input = build_db_input(
branch_paths, [], assignments, _sql_meta_two_selects(),
_declared_two_tables(),
records=records,
)
emp = db_input.get('EMP-MASTER', [])
ovt = db_input.get('OVT-MONTHLY', [])
assert len(emp) == 2, emp # 两条路径都种 EMP(前置查询必须命中)
assert len(ovt) == 1, ovt # 最后一条路径的 OVT 行被丢弃
assert emp[0]['EMP-ID'].strip() == 'A0000001'
assert ovt[0]['EMP-ID'].strip() == 'A0000001'
def test_single_select_keeps_all_rows():
"""只有一个 SELECT 时不触发丢弃(保持原行为)。"""
branch_paths = [
([('SQLCODE', '=', '0', True)], {}),
([('SQLCODE', '=', '0', True)], {}),
]
records = [
{'R02EMP-ID': 'A0000001', 'DBV-EMP-ID': 'A0000001'},
{'R02EMP-ID': 'A0000002', 'DBV-EMP-ID': 'A0000002'},
]
assignments = {
'DBV-EMP-ID': [{'type': 'move', 'source_vars': ['R02EMP-ID']}],
}
db_input = build_db_input(
branch_paths, [], assignments, _sql_meta_select(),
_declared_columns(),
records=records,
)
rows = db_input.get('EMP-MASTER', [])
assert len(rows) == 2, rows
def test_drop_requires_two_sql_ok_paths():
"""只有一条 SQL 成功路径时不丢弃最后一张表(避免该表行被清空)。"""
branch_paths = [
([('SQLCODE', '=', '0', True)], {}), # 唯一的 sql ok 路径
([('SQLCODE', '<>', '0', True)], {}), # 失败路径
]
records = [
{'R02EMP-ID': 'A0000001', 'DBV-EMP-ID': 'A0000001',
'WRK-YEAR-MONTH': 'B00001', 'DBV-OVT-HOURS': '00101'},
{'R02EMP-ID': 'A0000002', 'DBV-EMP-ID': 'A0000002',
'WRK-YEAR-MONTH': 'B00002', 'DBV-OVT-HOURS': '00102'},
]
assignments = {
'DBV-EMP-ID': [{'type': 'move', 'source_vars': ['R02EMP-ID']}],
}
db_input = build_db_input(
branch_paths, [], assignments, _sql_meta_two_selects(),
_declared_two_tables(),
records=records,
)
assert len(db_input.get('EMP-MASTER', [])) == 1
assert len(db_input.get('OVT-MONTHLY', [])) == 1, db_input.get('OVT-MONTHLY')